├── .changeset
├── README.md
└── config.json
├── .eslintrc.json
├── .github
├── FUNDING.yml
└── workflows
│ ├── depoly.yml
│ └── release.yml
├── .gitignore
├── .vscode
└── settings.json
├── CHANGELOG.md
├── LICENSE
├── README.md
├── next.config.js
├── package.json
├── pnpm-lock.yaml
├── postcss.config.js
├── public
├── apple-icon.svg
├── favicon.svg
├── icon.png
├── icon.svg
└── images
│ └── intro.png
├── src
├── app
│ ├── api
│ │ ├── dependents
│ │ │ └── route.ts
│ │ └── exist
│ │ │ └── route.ts
│ ├── globals.css
│ ├── layout.tsx
│ └── page.tsx
├── components
│ ├── analytics.tsx
│ ├── icons.tsx
│ ├── intro.tsx
│ ├── mode-toggle.tsx
│ ├── package-select.tsx
│ ├── repo.tsx
│ ├── result.tsx
│ ├── search-input.tsx
│ ├── site-footer.tsx
│ ├── site-header.tsx
│ ├── theme-provider.tsx
│ └── ui
│ │ ├── button.tsx
│ │ ├── card.tsx
│ │ ├── dropdown-menu.tsx
│ │ ├── input.tsx
│ │ ├── label.tsx
│ │ ├── select.tsx
│ │ ├── switch.tsx
│ │ ├── toast.tsx
│ │ ├── toaster.tsx
│ │ └── use-toast.ts
├── config.ts
├── hooks
│ └── useDependents.ts
└── lib
│ ├── parse-page.ts
│ └── utils.ts
├── tailwind.config.js
├── tsconfig.json
└── vercel.json
/.changeset/README.md:
--------------------------------------------------------------------------------
1 | # Changesets
2 |
3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
4 | with multi-package repos, or single-package repos to help you version and publish your code. You can
5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets)
6 |
7 | We have a quick list of common questions to get you started engaging with this project in
8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
9 |
--------------------------------------------------------------------------------
/.changeset/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json",
3 | "changelog": "@changesets/cli/changelog",
4 | "commit": false,
5 | "fixed": [],
6 | "linked": [],
7 | "access": "restricted",
8 | "baseBranch": "main",
9 | "updateInternalDependencies": "patch",
10 | "ignore": []
11 | }
12 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: izayl
2 |
--------------------------------------------------------------------------------
/.github/workflows/depoly.yml:
--------------------------------------------------------------------------------
1 | name: Production Tag Deployment
2 | env:
3 | VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
4 | VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
5 | on:
6 | workflow_dispatch:
7 | push:
8 | branches:
9 | - main
10 | tags:
11 | - '*' # Push events to every tag not containing /
12 | jobs:
13 | Deploy-Production:
14 | runs-on: ubuntu-latest
15 | steps:
16 | - uses: actions/checkout@v3
17 | - uses: pnpm/action-setup@v2
18 | with:
19 | version: 8
20 | - name: Install Vercel CLI
21 | run: npm install --global vercel@latest
22 | - name: Pull Vercel Environment Information
23 | run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}
24 | - name: Build Project Artifacts
25 | run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}
26 | - name: Deploy Project Artifacts to Vercel
27 | run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}
28 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | concurrency: ${{ github.workflow }}-${{ github.ref }}
9 |
10 | jobs:
11 | release:
12 | name: Release
13 | runs-on: ubuntu-latest
14 | permissions:
15 | pull-requests: write
16 | contents: write
17 | steps:
18 | - name: Checkout Repo
19 | uses: actions/checkout@v3
20 |
21 | - name: Set up pnpm
22 | uses: pnpm/action-setup@v2.2.4
23 | with:
24 | version: 8
25 |
26 | - name: Setup node@18
27 | uses: actions/setup-node@v3
28 | with:
29 | node-version: 18
30 | cache: 'pnpm'
31 |
32 | - name: Install Dependencies
33 | run: pnpm i --ignore-scripts
34 |
35 | - name: Create Release Pull Request
36 | uses: changesets/action@v1
37 | with:
38 | version: pnpm changeset:version
39 | publish: pnpm changeset:release
40 | commit: 'chore(changeset): bump version'
41 | title: 'chore(changeset): bump version'
42 | env:
43 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
44 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env*.local
29 |
30 | # vercel
31 | .vercel
32 |
33 | # typescript
34 | *.tsbuildinfo
35 | next-env.d.ts
36 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "typescript.tsdk": "node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib",
3 | "typescript.enablePromptUseWorkspaceTsdk": true
4 | }
5 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # useful-dependents
2 |
3 | ## 0.1.5
4 |
5 | ### Patch Changes
6 |
7 | - f411223: feat: add package selector
8 | - 19f0fc8: feat: add empty result
9 | - 1f62c00: fix: add reach end status to show correctly end status
10 |
11 | ## 0.1.4
12 |
13 | ### Patch Changes
14 |
15 | - 4bcbcb2: reduce vercel image opt cost
16 |
17 | ## 0.1.3
18 |
19 | ### Patch Changes
20 |
21 | - 343df8d: adjust mobile cover position
22 |
23 | ## 0.1.2
24 |
25 | ### Patch Changes
26 |
27 | - 23e6af5: ci: deploy when new tag
28 |
29 | ## 0.1.1
30 |
31 | ### Patch Changes
32 |
33 | - d72c011: Add changeset to keep version and changelog
34 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 izayl
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Useful Dependents
2 |
3 | search useful dependents for GitHub project.
4 |
5 | Every time I look at a new open source project to learn, in addition to reading the documentation, I also want to see how other projects that use this project for development are being used. However, GitHub's dependents always has to flip through many pages to find a suitable project. So I wrote a tool to help me find the corresponding dependents projects. May it help you too.
6 |
7 | Even tiny pull requests are greatly appreciated ❤️.
8 |
9 | ## Try it out ⚡️
10 |
11 | [https://useful-dependents.vercel.app/](https://useful-dependents.vercel.app/)
12 |
13 | ## ✨ Inspiration
14 |
15 | this project is inspired by [useful-forks](https://github.com/useful-forks/useful-forks.github.io).
16 |
17 | ## 🚀 Development
18 |
19 | ```sh
20 | # install dependencies
21 | pnpm install
22 |
23 | # or use [ni](https://github.com/antfu/ni)
24 | ni
25 |
26 | # serve with hot reload at localhost:3000
27 | pnpm dev
28 | ```
29 |
30 | ## Tech Stack
31 |
32 | - [Next 13](https://beta.nextjs.org/docs)
33 | - [shadcn/ui](https://github.com/shadcn/ui)
34 | - [tailwindcss](https://tailwindcss.com/)
35 | - [swr](https://swr.vercel.app/)
36 | - [wretch](https://github.com/elbywan/wretch)
37 |
38 | ---
39 | [MIT License](./LICENSE) © 2023 [izayl](https://github.com/izayl)
40 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | images: {
4 | domains: ['avatars.githubusercontent.com'],
5 | },
6 | experimental: {
7 | appDir: true,
8 | },
9 | }
10 |
11 | module.exports = nextConfig
12 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "useful-dependents",
3 | "version": "0.1.5",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint",
10 | "changeset:version": "changeset status & changeset version",
11 | "changeset:release": "changeset tag"
12 | },
13 | "shadcn-ui": {
14 | "location": ""
15 | },
16 | "dependencies": {
17 | "@radix-ui/react-dropdown-menu": "^2.0.4",
18 | "@radix-ui/react-label": "^2.0.1",
19 | "@radix-ui/react-select": "^1.2.1",
20 | "@radix-ui/react-switch": "^1.0.2",
21 | "@radix-ui/react-toast": "^1.1.3",
22 | "@types/node": "18.16.2",
23 | "@types/react": "18.2.0",
24 | "@types/react-dom": "18.2.1",
25 | "@vercel/analytics": "^1.0.0",
26 | "autoprefixer": "10.4.14",
27 | "cheerio": "1.0.0-rc.12",
28 | "class-variance-authority": "^0.6.0",
29 | "clsx": "^1.2.1",
30 | "eslint": "8.39.0",
31 | "eslint-config-next": "13.3.1",
32 | "lodash.orderby": "^4.6.0",
33 | "lucide-react": "^0.187.0",
34 | "next": "13.3.1",
35 | "next-themes": "^0.2.1",
36 | "postcss": "8.4.23",
37 | "react": "18.2.0",
38 | "react-dom": "18.2.0",
39 | "swr": "^2.1.5",
40 | "tailwind-merge": "^1.12.0",
41 | "tailwindcss": "3.3.2",
42 | "tailwindcss-animate": "^1.0.5",
43 | "typescript": "5.0.4",
44 | "wretch": "^2.5.2"
45 | },
46 | "devDependencies": {
47 | "@changesets/cli": "^2.26.1",
48 | "@types/lodash.orderby": "^4.6.7"
49 | }
50 | }
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/public/apple-icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/favicon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/izayl/useful-dependents/45a86a0cfeecbf52eed8a020d6a8a04322bc558f/public/icon.png
--------------------------------------------------------------------------------
/public/icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/images/intro.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/izayl/useful-dependents/45a86a0cfeecbf52eed8a020d6a8a04322bc558f/public/images/intro.png
--------------------------------------------------------------------------------
/src/app/api/dependents/route.ts:
--------------------------------------------------------------------------------
1 | import { parsePage } from '@/lib/parse-page'
2 | import { NextResponse } from 'next/server'
3 | import fetch from 'wretch'
4 |
5 | export async function POST(request: Request) {
6 | try {
7 | const body = await request.json()
8 | const { nextUrl } = body
9 | const page = await fetch(nextUrl)
10 | .get()
11 | .badRequest(async e => {
12 | throw Error(e.message)
13 | })
14 | .internalError(e => {
15 | throw Error(e.message)
16 | })
17 | .forbidden(err => {
18 | throw Error(err.message)
19 | })
20 | .text()
21 | const json = await parsePage(page)
22 | return NextResponse.json(json)
23 | } catch (error) {
24 | console.trace(error)
25 | const e = error?.toString() ?? 'Error fetching next page'
26 | return new Response(e, { status: 500 })
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/app/api/exist/route.ts:
--------------------------------------------------------------------------------
1 | export async function POST(request: Request) {
2 | try {
3 | const body = await request.json()
4 | const { url } = body
5 | if (!url) {
6 | throw Error('No URL provided')
7 | }
8 |
9 | const res = await fetch(url, { method: 'HEAD' })
10 | if (res.status !== 200) {
11 | throw Error(res.statusText)
12 | }
13 |
14 | return new Response('OK', { status: 200 })
15 | } catch (error) {
16 | console.log(error?.toString())
17 | return new Response(error?.toString() ?? 'repo not found', { status: 500 })
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root {
7 | --background: 0 0% 100%;
8 | --foreground: 222.2 47.4% 11.2%;
9 |
10 | --muted: 210 40% 96.1%;
11 | --muted-foreground: 215.4 16.3% 46.9%;
12 |
13 | --popover: 0 0% 100%;
14 | --popover-foreground: 222.2 47.4% 11.2%;
15 |
16 | --card: 0 0% 100%;
17 | --card-foreground: 222.2 47.4% 11.2%;
18 |
19 | --border: 214.3 31.8% 91.4%;
20 | --input: 214.3 31.8% 91.4%;
21 |
22 | --primary: 222.2 47.4% 11.2%;
23 | --primary-foreground: 210 40% 98%;
24 |
25 | --secondary: 210 40% 96.1%;
26 | --secondary-foreground: 222.2 47.4% 11.2%;
27 |
28 | --accent: 210 40% 96.1%;
29 | --accent-foreground: 222.2 47.4% 11.2%;
30 |
31 | --destructive: 0 100% 50%;
32 | --destructive-foreground: 210 40% 98%;
33 |
34 | --ring: 215 20.2% 65.1%;
35 |
36 | --radius: 0.5rem;
37 | }
38 |
39 | .dark {
40 | --background: 224 71% 4%;
41 | --foreground: 207 30% 90.7%;
42 |
43 | --muted: 223 47% 11%;
44 | --muted-foreground: 215.4 16.3% 56.9%;
45 |
46 | --popover: 224 71% 4%;
47 | --popover-foreground: 207 35% 92.7%;
48 |
49 | --card: 0 0% 100%;
50 | --card-foreground: 222.2 47.4% 11.2%;
51 |
52 | --border: 216 34% 17%;
53 | --input: 216 34% 17%;
54 |
55 | --primary: 210 40% 98%;
56 | --primary-foreground: 222.2 47.4% 1.2%;
57 |
58 | --secondary: 222.2 47.4% 11.2%;
59 | --secondary-foreground: 210 40% 98%;
60 |
61 | --accent: 216 34% 17%;
62 | --accent-foreground: 210 40% 98%;
63 |
64 | --destructive: 0 63% 31%;
65 | --destructive-foreground: 210 40% 98%;
66 |
67 | --ring: 216 34% 17%;
68 |
69 | --radius: 0.5rem;
70 | }
71 | }
72 |
73 | @layer base {
74 | * {
75 | @apply border-border;
76 | }
77 | body {
78 | @apply bg-background text-foreground;
79 | font-feature-settings: 'rlig' 1, 'calt' 1;
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import './globals.css'
2 | import { Inter } from 'next/font/google'
3 | import { ThemeProvider } from '@/components/theme-provider'
4 | import { Toaster } from '@/components/ui/toaster'
5 | import { SiteHeader } from '@/components/site-header'
6 | import { SiteFooter } from '@/components/site-footer'
7 | import { cn } from '@/lib/utils'
8 | import { Analytics } from '@/components/analytics'
9 |
10 | const inter = Inter({ subsets: ['latin'] })
11 |
12 | export default function RootLayout({
13 | children,
14 | }: {
15 | children: React.ReactNode
16 | }) {
17 | return (
18 |
19 |
25 |
26 |
27 |
28 |
{children}
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | )
37 | }
38 |
--------------------------------------------------------------------------------
/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | import { Result } from '@/components/result'
2 | import { SearchInput } from '@/components/search-input'
3 | import { siteConfig } from '@/config'
4 | import { Metadata } from 'next'
5 |
6 | export const metadata: Metadata = {
7 | title: siteConfig.name,
8 | description: siteConfig.description,
9 | keywords: siteConfig.keywords,
10 | icons: '/favicon.svg',
11 | authors: [
12 | {
13 | name: siteConfig.author,
14 | url: siteConfig.authorUrl,
15 | },
16 | ],
17 | openGraph: {
18 | title: siteConfig.name,
19 | description: siteConfig.description,
20 | type: 'website',
21 | images: '/images/intro.png',
22 | },
23 | twitter: {
24 | card: 'summary_large_image',
25 | title: siteConfig.name,
26 | description: siteConfig.description,
27 | siteId: '3049993370',
28 | creator: '@izayl_',
29 | creatorId: '3049993370',
30 | images: ['/images/intro.png'],
31 | },
32 | }
33 | export default function Home() {
34 | return (
35 |
36 |
37 |
38 |
39 |
40 |
41 | )
42 | }
43 |
--------------------------------------------------------------------------------
/src/components/analytics.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { Analytics as VercelAnalytics } from '@vercel/analytics/react'
4 |
5 | export function Analytics() {
6 | return
7 | }
8 |
--------------------------------------------------------------------------------
/src/components/icons.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Laptop,
3 | LucideProps,
4 | Moon,
5 | Settings,
6 | Star,
7 | Sun,
8 | Twitter,
9 | Loader2,
10 | } from 'lucide-react'
11 |
12 | export const Icons = {
13 | logo: (props: LucideProps) => (
14 |
56 | ),
57 | sun: Sun,
58 | moon: Moon,
59 | laptop: Laptop,
60 | github: (props: LucideProps) => (
61 |
67 | ),
68 | twitter: Twitter,
69 | star: Star,
70 | fork: (props: LucideProps) => (
71 |
74 | ),
75 | settings: Settings,
76 | loading: Loader2,
77 | }
78 |
--------------------------------------------------------------------------------
/src/components/intro.tsx:
--------------------------------------------------------------------------------
1 | import Image from 'next/image'
2 | import { Card, CardContent, CardHeader, CardTitle } from './ui/card'
3 | import Link from 'next/link'
4 | import { siteConfig } from '@/config'
5 |
6 | export const Intro: React.FC = () => {
7 | return (
8 |
9 |
10 | Useful Dependents
11 |
12 |
13 |
14 |
21 |
22 |
23 |
24 | Find useful dependents for a GitHub repository. Just type the GitHub
25 | url and click the find button.
26 |
27 |
28 | You can also ignore zero star repositories by toggle the{' '}
29 | ignore zero star option.
30 |
31 |
32 | see example:{' '}
33 |
37 | egoist/tsup
38 | {' '}
39 | or{' '}
40 |
44 | facebook/react
45 |
46 |
47 |
48 | for more information, please checkout the{' '}
49 |
53 | GitHub repository
54 |
55 |
56 |
57 |
58 |
59 | )
60 | }
61 |
--------------------------------------------------------------------------------
/src/components/mode-toggle.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { useTheme } from 'next-themes'
4 | import { Button } from '@/components/ui/button'
5 | import {
6 | DropdownMenu,
7 | DropdownMenuContent,
8 | DropdownMenuItem,
9 | DropdownMenuPortal,
10 | DropdownMenuTrigger,
11 | } from '@/components/ui/dropdown-menu'
12 | import { Icons } from '@/components/icons'
13 |
14 | export function ModeToggle() {
15 | const { setTheme } = useTheme()
16 |
17 | return (
18 |
19 |
20 |
25 |
26 |
27 |
28 | setTheme('light')}>
29 |
30 | Light
31 |
32 | setTheme('dark')}>
33 |
34 | Dark
35 |
36 | setTheme('system')}>
37 |
38 | System
39 |
40 |
41 |
42 |
43 | )
44 | }
45 |
--------------------------------------------------------------------------------
/src/components/package-select.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import {
4 | Select,
5 | SelectContent,
6 | SelectItem,
7 | SelectTrigger,
8 | SelectValue,
9 | } from '@/components/ui/select'
10 | import { Package } from '@/lib/parse-page'
11 | import { usePathname, useRouter, useSearchParams } from 'next/navigation'
12 | import { Label } from './ui/label'
13 |
14 | type PackageSelectProps = {
15 | options: Package[]
16 | value?: string
17 | }
18 |
19 | export const PackageSelect: React.FC = ({
20 | options,
21 | }) => {
22 | const router = useRouter()
23 | const pathname = usePathname()
24 | const searchParams = useSearchParams()
25 | const value = searchParams.get('package_id')
26 | const onSelect = (v: string) => {
27 | if (v === value) return
28 | const params = new URLSearchParams(
29 | searchParams as unknown as URLSearchParams
30 | )
31 | params.set('package_id', v)
32 | router.push(`${pathname}?${params.toString()}`)
33 | }
34 | const currentOption = options.find(o => o.id === value) ?? options[0]
35 |
36 | return (
37 |
38 |
41 |
54 |
55 | )
56 | }
57 |
--------------------------------------------------------------------------------
/src/components/repo.tsx:
--------------------------------------------------------------------------------
1 | import Image from 'next/image'
2 | import { Icons } from './icons'
3 | import Link from 'next/link'
4 | import { toUnit } from '@/lib/utils'
5 |
6 | export const Repo: React.FC<{
7 | avatarUrl: string
8 | name: string
9 | stars: number
10 | forks: number
11 | }> = ({ avatarUrl, name, stars, forks }) => {
12 | const [user, repo] = name.split('/')
13 | return (
14 |
15 |
16 |
24 |
25 |
29 | {user}
30 |
31 | /
32 |
36 | {repo}
37 |
38 |
39 |
40 |
41 |
42 |
43 | {toUnit(stars)}
44 |
45 |
46 |
47 | {toUnit(forks)}
48 |
49 |
50 |
51 | )
52 | }
53 |
--------------------------------------------------------------------------------
/src/components/result.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { useEffect, useMemo, useState } from 'react'
4 | import orderBy from 'lodash.orderby'
5 | import { Repo } from './repo'
6 | import { Intro } from './intro'
7 | import { Switch } from './ui/switch'
8 | import { Label } from './ui/label'
9 | import { useDependents } from '@/hooks/useDependents'
10 | import { useSearchParams } from 'next/navigation'
11 | import { Button } from './ui/button'
12 | import { Icons } from './icons'
13 | import { PackageSelect } from './package-select'
14 |
15 | const EmptySlate: React.FC = ({ children }) => {
16 | return (
17 |
18 |
{children}
19 |
20 | Please try again later.
21 |
22 |
23 | )
24 | }
25 |
26 | export const Result: React.FC = () => {
27 | const searchParams = useSearchParams()
28 | const repo = searchParams.get('repo')
29 | const packageId = searchParams.get('package_id')
30 | const [loadPage, setLoadPage] = useState(5)
31 | const { data, setSize, packages, size, isLoading, isFinished, emptyText } =
32 | useDependents(repo as string, packageId as string)
33 | const [ignoreZeroStar, setIgnoreZeroStar] = useState(true)
34 | const sortedData = useMemo(() => {
35 | const filteredData = ignoreZeroStar ? data.filter(d => d.stars > 0) : data
36 | return orderBy(filteredData ?? [], 'stars', 'desc')
37 | }, [data, ignoreZeroStar])
38 | const isFetching = useMemo(() => {
39 | const last = 30 * (loadPage - 1)
40 | if (isFinished) return false
41 | return size > 0 && data && typeof data[last] === 'undefined'
42 | }, [size, data, loadPage, isFinished])
43 |
44 | useEffect(() => {
45 | if (repo && !isLoading) {
46 | setSize(loadPage)
47 | }
48 | }, [repo, isLoading, setSize, loadPage])
49 |
50 | if (!repo) {
51 | return
52 | }
53 |
54 | return (
55 | <>
56 | {packages.length ? : null}
57 |
58 |
59 |
60 | {isFetching ? (
61 |
62 | ) : null}
63 |
64 | {data.length} Repos
65 | {!isFetching ? (
66 | setLoadPage(p => p + 5)}
69 | >
70 | load more
71 |
72 | ) : null}
73 |
74 |
75 |
76 |
82 |
83 |
84 |
85 |
86 | {sortedData.map((result, id) => (
87 |
94 | ))}
95 | {emptyText ? {emptyText} : null}
96 |
97 | {!isFinished ? (
98 |
99 | {isFetching ? (
100 |
101 | ) : (
102 |
109 | )}
110 |
111 | ) : null}
112 |
113 | >
114 | )
115 | }
116 |
--------------------------------------------------------------------------------
/src/components/search-input.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { usePathname, useRouter, useSearchParams } from 'next/navigation'
4 | import { Button } from './ui/button'
5 | import { Input } from './ui/input'
6 | import { useRef } from 'react'
7 | import { checkRepoExist } from '@/lib/utils'
8 | import { useToast } from './ui/use-toast'
9 |
10 | export const SearchInput = () => {
11 | const router = useRouter()
12 | const pathname = usePathname()
13 | const searchParams = useSearchParams()
14 | const repo = searchParams.get('repo')
15 | const input = useRef(null)
16 | const { toast } = useToast()
17 |
18 | const onSubmit = async () => {
19 | const value = input.current?.value
20 | if (!value) return
21 | const repoError = await checkRepoExist(value.trim()).catch(err => err)
22 | if (repoError) {
23 | toast({
24 | variant: 'destructive',
25 | title: 'Repo not found',
26 | description: 'Please check the repo name and try again.',
27 | })
28 | return
29 | }
30 | const params = new URLSearchParams(
31 | searchParams as unknown as URLSearchParams
32 | )
33 | params.set('repo', value.trim())
34 | router.push(`${pathname}?${params.toString()}`)
35 | }
36 |
37 | return (
38 |
39 |
40 | {/* */}
46 | {
53 | if (e.key === 'Enter') {
54 | onSubmit()
55 | }
56 | }}
57 | />
58 |
61 |
62 |
63 | )
64 | }
65 |
--------------------------------------------------------------------------------
/src/components/site-footer.tsx:
--------------------------------------------------------------------------------
1 | import { Icons } from '@/components/icons'
2 | import { siteConfig } from '@/config'
3 |
4 | export function SiteFooter() {
5 | return (
6 |
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/src/components/site-header.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils'
2 | import Link from 'next/link'
3 | import { buttonVariants } from './ui/button'
4 | import { Icons } from './icons'
5 | import { ModeToggle } from './mode-toggle'
6 | import { siteConfig } from '@/config'
7 |
8 | export function SiteHeader() {
9 | return (
10 |
61 | )
62 | }
63 |
--------------------------------------------------------------------------------
/src/components/theme-provider.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { ThemeProvider as NextThemesProvider } from 'next-themes'
4 | import { ThemeProviderProps } from 'next-themes/dist/types'
5 |
6 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
7 | return {children}
8 | }
9 |
--------------------------------------------------------------------------------
/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { VariantProps, cva } from "class-variance-authority"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const buttonVariants = cva(
7 | "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background",
8 | {
9 | variants: {
10 | variant: {
11 | default: "bg-primary text-primary-foreground hover:bg-primary/90",
12 | destructive:
13 | "bg-destructive text-destructive-foreground hover:bg-destructive/90",
14 | outline:
15 | "border border-input hover:bg-accent hover:text-accent-foreground",
16 | secondary:
17 | "bg-secondary text-secondary-foreground hover:bg-secondary/80",
18 | ghost: "hover:bg-accent hover:text-accent-foreground",
19 | link: "underline-offset-4 hover:underline text-primary",
20 | },
21 | size: {
22 | default: "h-10 py-2 px-4",
23 | sm: "h-9 px-3 rounded-md",
24 | lg: "h-11 px-8 rounded-md",
25 | },
26 | },
27 | defaultVariants: {
28 | variant: "default",
29 | size: "default",
30 | },
31 | }
32 | )
33 |
34 | export interface ButtonProps
35 | extends React.ButtonHTMLAttributes,
36 | VariantProps {}
37 |
38 | const Button = React.forwardRef(
39 | ({ className, variant, size, ...props }, ref) => {
40 | return (
41 |
46 | )
47 | }
48 | )
49 | Button.displayName = "Button"
50 |
51 | export { Button, buttonVariants }
52 |
--------------------------------------------------------------------------------
/src/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Card = React.forwardRef<
6 | HTMLDivElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
17 | ))
18 | Card.displayName = "Card"
19 |
20 | const CardHeader = React.forwardRef<
21 | HTMLDivElement,
22 | React.HTMLAttributes
23 | >(({ className, ...props }, ref) => (
24 |
29 | ))
30 | CardHeader.displayName = "CardHeader"
31 |
32 | const CardTitle = React.forwardRef<
33 | HTMLParagraphElement,
34 | React.HTMLAttributes
35 | >(({ className, ...props }, ref) => (
36 |
44 | ))
45 | CardTitle.displayName = "CardTitle"
46 |
47 | const CardDescription = React.forwardRef<
48 | HTMLParagraphElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ))
57 | CardDescription.displayName = "CardDescription"
58 |
59 | const CardContent = React.forwardRef<
60 | HTMLDivElement,
61 | React.HTMLAttributes
62 | >(({ className, ...props }, ref) => (
63 |
64 | ))
65 | CardContent.displayName = "CardContent"
66 |
67 | const CardFooter = React.forwardRef<
68 | HTMLDivElement,
69 | React.HTMLAttributes
70 | >(({ className, ...props }, ref) => (
71 |
76 | ))
77 | CardFooter.displayName = "CardFooter"
78 |
79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
80 |
--------------------------------------------------------------------------------
/src/components/ui/dropdown-menu.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
5 | import { Check, ChevronRight, Circle } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const DropdownMenu = DropdownMenuPrimitive.Root
10 |
11 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
12 |
13 | const DropdownMenuGroup = DropdownMenuPrimitive.Group
14 |
15 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal
16 |
17 | const DropdownMenuSub = DropdownMenuPrimitive.Sub
18 |
19 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
20 |
21 | const DropdownMenuSubTrigger = React.forwardRef<
22 | React.ElementRef,
23 | React.ComponentPropsWithoutRef & {
24 | inset?: boolean
25 | }
26 | >(({ className, inset, children, ...props }, ref) => (
27 |
36 | {children}
37 |
38 |
39 | ))
40 | DropdownMenuSubTrigger.displayName =
41 | DropdownMenuPrimitive.SubTrigger.displayName
42 |
43 | const DropdownMenuSubContent = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef
46 | >(({ className, ...props }, ref) => (
47 |
55 | ))
56 | DropdownMenuSubContent.displayName =
57 | DropdownMenuPrimitive.SubContent.displayName
58 |
59 | const DropdownMenuContent = React.forwardRef<
60 | React.ElementRef,
61 | React.ComponentPropsWithoutRef
62 | >(({ className, sideOffset = 4, ...props }, ref) => (
63 |
64 |
73 |
74 | ))
75 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
76 |
77 | const DropdownMenuItem = React.forwardRef<
78 | React.ElementRef,
79 | React.ComponentPropsWithoutRef & {
80 | inset?: boolean
81 | }
82 | >(({ className, inset, ...props }, ref) => (
83 |
92 | ))
93 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
94 |
95 | const DropdownMenuCheckboxItem = React.forwardRef<
96 | React.ElementRef,
97 | React.ComponentPropsWithoutRef
98 | >(({ className, children, checked, ...props }, ref) => (
99 |
108 |
109 |
110 |
111 |
112 |
113 | {children}
114 |
115 | ))
116 | DropdownMenuCheckboxItem.displayName =
117 | DropdownMenuPrimitive.CheckboxItem.displayName
118 |
119 | const DropdownMenuRadioItem = React.forwardRef<
120 | React.ElementRef,
121 | React.ComponentPropsWithoutRef
122 | >(({ className, children, ...props }, ref) => (
123 |
131 |
132 |
133 |
134 |
135 |
136 | {children}
137 |
138 | ))
139 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
140 |
141 | const DropdownMenuLabel = React.forwardRef<
142 | React.ElementRef,
143 | React.ComponentPropsWithoutRef & {
144 | inset?: boolean
145 | }
146 | >(({ className, inset, ...props }, ref) => (
147 |
156 | ))
157 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
158 |
159 | const DropdownMenuSeparator = React.forwardRef<
160 | React.ElementRef,
161 | React.ComponentPropsWithoutRef
162 | >(({ className, ...props }, ref) => (
163 |
168 | ))
169 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
170 |
171 | const DropdownMenuShortcut = ({
172 | className,
173 | ...props
174 | }: React.HTMLAttributes) => {
175 | return (
176 |
180 | )
181 | }
182 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
183 |
184 | export {
185 | DropdownMenu,
186 | DropdownMenuTrigger,
187 | DropdownMenuContent,
188 | DropdownMenuItem,
189 | DropdownMenuCheckboxItem,
190 | DropdownMenuRadioItem,
191 | DropdownMenuLabel,
192 | DropdownMenuSeparator,
193 | DropdownMenuShortcut,
194 | DropdownMenuGroup,
195 | DropdownMenuPortal,
196 | DropdownMenuSub,
197 | DropdownMenuSubContent,
198 | DropdownMenuSubTrigger,
199 | DropdownMenuRadioGroup,
200 | }
201 |
--------------------------------------------------------------------------------
/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | )
21 | }
22 | )
23 | Input.displayName = "Input"
24 |
25 | export { Input }
26 |
--------------------------------------------------------------------------------
/src/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 | import { VariantProps, cva } from "class-variance-authority"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const labelVariants = cva(
10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
11 | )
12 |
13 | const Label = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef &
16 | VariantProps
17 | >(({ className, ...props }, ref) => (
18 |
23 | ))
24 | Label.displayName = LabelPrimitive.Root.displayName
25 |
26 | export { Label }
27 |
--------------------------------------------------------------------------------
/src/components/ui/select.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SelectPrimitive from "@radix-ui/react-select"
5 | import { Check, ChevronDown } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Select = SelectPrimitive.Root
10 |
11 | const SelectGroup = SelectPrimitive.Group
12 |
13 | const SelectValue = SelectPrimitive.Value
14 |
15 | const SelectTrigger = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, children, ...props }, ref) => (
19 |
27 | {children}
28 |
29 |
30 |
31 |
32 | ))
33 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
34 |
35 | const SelectContent = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, children, position = "popper", ...props }, ref) => (
39 |
40 |
50 |
57 | {children}
58 |
59 |
60 |
61 | ))
62 | SelectContent.displayName = SelectPrimitive.Content.displayName
63 |
64 | const SelectLabel = React.forwardRef<
65 | React.ElementRef,
66 | React.ComponentPropsWithoutRef
67 | >(({ className, ...props }, ref) => (
68 |
73 | ))
74 | SelectLabel.displayName = SelectPrimitive.Label.displayName
75 |
76 | const SelectItem = React.forwardRef<
77 | React.ElementRef,
78 | React.ComponentPropsWithoutRef
79 | >(({ className, children, ...props }, ref) => (
80 |
88 |
89 |
90 |
91 |
92 |
93 |
94 | {children}
95 |
96 | ))
97 | SelectItem.displayName = SelectPrimitive.Item.displayName
98 |
99 | const SelectSeparator = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName
110 |
111 | export {
112 | Select,
113 | SelectGroup,
114 | SelectValue,
115 | SelectTrigger,
116 | SelectContent,
117 | SelectLabel,
118 | SelectItem,
119 | SelectSeparator,
120 | }
121 |
--------------------------------------------------------------------------------
/src/components/ui/switch.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SwitchPrimitives from "@radix-ui/react-switch"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Switch = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 |
25 |
26 | ))
27 | Switch.displayName = SwitchPrimitives.Root.displayName
28 |
29 | export { Switch }
30 |
--------------------------------------------------------------------------------
/src/components/ui/toast.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as ToastPrimitives from "@radix-ui/react-toast"
3 | import { VariantProps, cva } from "class-variance-authority"
4 | import { X } from "lucide-react"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const ToastProvider = ToastPrimitives.Provider
9 |
10 | const ToastViewport = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, ...props }, ref) => (
14 |
22 | ))
23 | ToastViewport.displayName = ToastPrimitives.Viewport.displayName
24 |
25 | const toastVariants = cva(
26 | "data-[swipe=move]:transition-none group relative pointer-events-auto flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full data-[state=closed]:slide-out-to-right-full",
27 | {
28 | variants: {
29 | variant: {
30 | default: "bg-background border",
31 | destructive:
32 | "group destructive border-destructive bg-destructive text-destructive-foreground",
33 | },
34 | },
35 | defaultVariants: {
36 | variant: "default",
37 | },
38 | }
39 | )
40 |
41 | const Toast = React.forwardRef<
42 | React.ElementRef,
43 | React.ComponentPropsWithoutRef &
44 | VariantProps
45 | >(({ className, variant, ...props }, ref) => {
46 | return (
47 |
52 | )
53 | })
54 | Toast.displayName = ToastPrimitives.Root.displayName
55 |
56 | const ToastAction = React.forwardRef<
57 | React.ElementRef,
58 | React.ComponentPropsWithoutRef
59 | >(({ className, ...props }, ref) => (
60 |
68 | ))
69 | ToastAction.displayName = ToastPrimitives.Action.displayName
70 |
71 | const ToastClose = React.forwardRef<
72 | React.ElementRef,
73 | React.ComponentPropsWithoutRef
74 | >(({ className, ...props }, ref) => (
75 |
84 |
85 |
86 | ))
87 | ToastClose.displayName = ToastPrimitives.Close.displayName
88 |
89 | const ToastTitle = React.forwardRef<
90 | React.ElementRef,
91 | React.ComponentPropsWithoutRef
92 | >(({ className, ...props }, ref) => (
93 |
98 | ))
99 | ToastTitle.displayName = ToastPrimitives.Title.displayName
100 |
101 | const ToastDescription = React.forwardRef<
102 | React.ElementRef,
103 | React.ComponentPropsWithoutRef
104 | >(({ className, ...props }, ref) => (
105 |
110 | ))
111 | ToastDescription.displayName = ToastPrimitives.Description.displayName
112 |
113 | type ToastProps = React.ComponentPropsWithoutRef
114 |
115 | type ToastActionElement = React.ReactElement
116 |
117 | export {
118 | type ToastProps,
119 | type ToastActionElement,
120 | ToastProvider,
121 | ToastViewport,
122 | Toast,
123 | ToastTitle,
124 | ToastDescription,
125 | ToastClose,
126 | ToastAction,
127 | }
128 |
--------------------------------------------------------------------------------
/src/components/ui/toaster.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import {
4 | Toast,
5 | ToastClose,
6 | ToastDescription,
7 | ToastProvider,
8 | ToastTitle,
9 | ToastViewport,
10 | } from "@/components/ui/toast"
11 | import { useToast } from "@/components/ui/use-toast"
12 |
13 | export function Toaster() {
14 | const { toasts } = useToast()
15 |
16 | return (
17 |
18 | {toasts.map(function ({ id, title, description, action, ...props }) {
19 | return (
20 |
21 |
22 | {title && {title}}
23 | {description && (
24 | {description}
25 | )}
26 |
27 | {action}
28 |
29 |
30 | )
31 | })}
32 |
33 |
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/src/components/ui/use-toast.ts:
--------------------------------------------------------------------------------
1 | // Inspired by react-hot-toast library
2 | import * as React from "react"
3 |
4 | import { ToastActionElement, type ToastProps } from "@/components/ui/toast"
5 |
6 | const TOAST_LIMIT = 1
7 | const TOAST_REMOVE_DELAY = 1000000
8 |
9 | type ToasterToast = ToastProps & {
10 | id: string
11 | title?: React.ReactNode
12 | description?: React.ReactNode
13 | action?: ToastActionElement
14 | }
15 |
16 | const actionTypes = {
17 | ADD_TOAST: "ADD_TOAST",
18 | UPDATE_TOAST: "UPDATE_TOAST",
19 | DISMISS_TOAST: "DISMISS_TOAST",
20 | REMOVE_TOAST: "REMOVE_TOAST",
21 | } as const
22 |
23 | let count = 0
24 |
25 | function genId() {
26 | count = (count + 1) % Number.MAX_VALUE
27 | return count.toString()
28 | }
29 |
30 | type ActionType = typeof actionTypes
31 |
32 | type Action =
33 | | {
34 | type: ActionType["ADD_TOAST"]
35 | toast: ToasterToast
36 | }
37 | | {
38 | type: ActionType["UPDATE_TOAST"]
39 | toast: Partial
40 | }
41 | | {
42 | type: ActionType["DISMISS_TOAST"]
43 | toastId?: ToasterToast["id"]
44 | }
45 | | {
46 | type: ActionType["REMOVE_TOAST"]
47 | toastId?: ToasterToast["id"]
48 | }
49 |
50 | interface State {
51 | toasts: ToasterToast[]
52 | }
53 |
54 | const toastTimeouts = new Map>()
55 |
56 | const addToRemoveQueue = (toastId: string) => {
57 | if (toastTimeouts.has(toastId)) {
58 | return
59 | }
60 |
61 | const timeout = setTimeout(() => {
62 | toastTimeouts.delete(toastId)
63 | dispatch({
64 | type: "REMOVE_TOAST",
65 | toastId: toastId,
66 | })
67 | }, TOAST_REMOVE_DELAY)
68 |
69 | toastTimeouts.set(toastId, timeout)
70 | }
71 |
72 | export const reducer = (state: State, action: Action): State => {
73 | switch (action.type) {
74 | case "ADD_TOAST":
75 | return {
76 | ...state,
77 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
78 | }
79 |
80 | case "UPDATE_TOAST":
81 | return {
82 | ...state,
83 | toasts: state.toasts.map((t) =>
84 | t.id === action.toast.id ? { ...t, ...action.toast } : t
85 | ),
86 | }
87 |
88 | case "DISMISS_TOAST": {
89 | const { toastId } = action
90 |
91 | // ! Side effects ! - This could be extracted into a dismissToast() action,
92 | // but I'll keep it here for simplicity
93 | if (toastId) {
94 | addToRemoveQueue(toastId)
95 | } else {
96 | state.toasts.forEach((toast) => {
97 | addToRemoveQueue(toast.id)
98 | })
99 | }
100 |
101 | return {
102 | ...state,
103 | toasts: state.toasts.map((t) =>
104 | t.id === toastId || toastId === undefined
105 | ? {
106 | ...t,
107 | open: false,
108 | }
109 | : t
110 | ),
111 | }
112 | }
113 | case "REMOVE_TOAST":
114 | if (action.toastId === undefined) {
115 | return {
116 | ...state,
117 | toasts: [],
118 | }
119 | }
120 | return {
121 | ...state,
122 | toasts: state.toasts.filter((t) => t.id !== action.toastId),
123 | }
124 | }
125 | }
126 |
127 | const listeners: Array<(state: State) => void> = []
128 |
129 | let memoryState: State = { toasts: [] }
130 |
131 | function dispatch(action: Action) {
132 | memoryState = reducer(memoryState, action)
133 | listeners.forEach((listener) => {
134 | listener(memoryState)
135 | })
136 | }
137 |
138 | interface Toast extends Omit {}
139 |
140 | function toast({ ...props }: Toast) {
141 | const id = genId()
142 |
143 | const update = (props: ToasterToast) =>
144 | dispatch({
145 | type: "UPDATE_TOAST",
146 | toast: { ...props, id },
147 | })
148 | const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
149 |
150 | dispatch({
151 | type: "ADD_TOAST",
152 | toast: {
153 | ...props,
154 | id,
155 | open: true,
156 | onOpenChange: (open) => {
157 | if (!open) dismiss()
158 | },
159 | },
160 | })
161 |
162 | return {
163 | id: id,
164 | dismiss,
165 | update,
166 | }
167 | }
168 |
169 | function useToast() {
170 | const [state, setState] = React.useState(memoryState)
171 |
172 | React.useEffect(() => {
173 | listeners.push(setState)
174 | return () => {
175 | const index = listeners.indexOf(setState)
176 | if (index > -1) {
177 | listeners.splice(index, 1)
178 | }
179 | }
180 | }, [state])
181 |
182 | return {
183 | ...state,
184 | toast,
185 | dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
186 | }
187 | }
188 |
189 | export { useToast, toast }
190 |
--------------------------------------------------------------------------------
/src/config.ts:
--------------------------------------------------------------------------------
1 | export const siteConfig = {
2 | name: 'Useful Dependents',
3 | url: 'https://github.com/izayl/useful-dependents',
4 | description: 'search useful dependents for GitHub project',
5 | author: 'izayl',
6 | authorUrl: 'https://github.com/izayl',
7 | keywords: [
8 | 'useful-dependents',
9 | 'GitHub',
10 | 'dependents',
11 | 'npm',
12 | 'search',
13 | 'package',
14 | ],
15 | links: {
16 | github: 'https://github.com/izayl/useful-dependents',
17 | twitter: 'https://twitter.com/izayl_',
18 | },
19 | }
20 |
--------------------------------------------------------------------------------
/src/hooks/useDependents.ts:
--------------------------------------------------------------------------------
1 | import fetch from 'wretch'
2 | import { ParseResult } from '@/lib/parse-page'
3 | import useSWRInfinite from 'swr/infinite'
4 |
5 | const getKey = (prevPageData: ParseResult | null, params: {
6 | repo: string
7 | packageId?: string
8 | }) => {
9 | const { repo, packageId } = params
10 | if (!prevPageData?.nextURL && !repo) return null
11 |
12 | // support both repo/name and github full link
13 | const baseURL = repo.startsWith('https://github.com')
14 | ? `${repo}/network/dependents`
15 | : `https://github.com/${repo}/network/dependents`
16 | let nextURL = prevPageData?.nextURL ?? baseURL
17 |
18 | if (packageId) {
19 | nextURL = `${nextURL}?package_id=${packageId}`
20 | }
21 |
22 | return nextURL
23 | }
24 |
25 | export const useDependents = (repo: string, packageId?: string) => {
26 | const response = useSWRInfinite(
27 | (pageIndex, prevPageData) => {
28 | const nextURL = getKey(prevPageData, {repo, packageId})
29 | return nextURL
30 | },
31 | (url: string) =>
32 | fetch(`${location.origin}/api/dependents`).post({ nextUrl: url }).json(),
33 | {
34 | revalidateOnFocus: false,
35 | revalidateOnReconnect: false,
36 | refreshWhenOffline: false,
37 | refreshWhenHidden: false,
38 | refreshInterval: 0,
39 | shouldRetryOnError: false,
40 | }
41 | )
42 |
43 | const data = response.data?.flatMap(d => d?.dependents || []) ?? []
44 |
45 | return {
46 | ...response,
47 | isFinished: response.data?.some(d => !d?.nextURL),
48 | emptyText: response.data?.[0]?.empty,
49 | data: data.filter(Boolean),
50 | packages: response.data ? response.data[0]?.packages : [],
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/lib/parse-page.ts:
--------------------------------------------------------------------------------
1 | import { extractNumbers } from './utils'
2 | import cheerio, { CheerioAPI } from 'cheerio'
3 |
4 | export type Dependent = {
5 | avatarUrl: string
6 | repo: string
7 | stars: number
8 | forks: number
9 | }
10 |
11 | export type Package = {
12 | name: string
13 | url: string
14 | id: string
15 | }
16 |
17 | export type ParseResult = {
18 | dependents: Dependent[]
19 | nextURL: string | null
20 | packages: Package[]
21 | empty: string
22 | }
23 |
24 | function parsePackages($: CheerioAPI): Package[] {
25 | const packages: Package[] = []
26 | $('#dependents > .select-menu').find('a.select-menu-item').each((i, e) => {
27 | const name = $(e).find('span').text().trim()
28 | const url = $(e).attr('href') ?? ''
29 | if (!name || !url) return
30 | const id = url.split('package_id=').pop() ?? ''
31 | packages.push({ name, url: `https://github.com/${url}`, id })
32 | })
33 | return packages
34 | }
35 |
36 | export async function parsePage(res?: string): Promise {
37 | if (!res) {
38 | throw Error('No response body')
39 | }
40 | const $ = cheerio.load(res)
41 | const packages = parsePackages($)
42 | const dependents: Dependent[] = []
43 | let empty = ''
44 | let nextURL = null
45 |
46 | if ($('#dependents > .blankslate')) {
47 | empty = $('#dependents .blankslate-heading').text().trim()
48 | }
49 |
50 | if (!empty) {
51 | $('#dependents > div.Box > div').each((i, e) => {
52 | // The first element is the header row
53 | if (i === 0) return
54 |
55 | const avatarUrl = $(e).find('.avatar').attr('src') ?? ''
56 |
57 | const repo =
58 | `${$(e).find('span > a:nth-child(1)').text().trim()}/${$(e).find('span > a:nth-child(2)').text().trim()}`
59 |
60 | const stars = extractNumbers(
61 | $(e).find('div > span:nth-child(1)').last().text().trim()
62 | )
63 | const forks = extractNumbers(
64 | $(e).find('div > span:nth-child(2)').last().text().trim()
65 | )
66 |
67 | dependents.push({
68 | avatarUrl,
69 | repo,
70 | stars,
71 | forks,
72 | })
73 | })
74 |
75 | nextURL =
76 | $('#dependents > div.paginate-container > div > a:contains("Next")')?.prop(
77 | 'href'
78 | ) || null
79 | }
80 | return {
81 | dependents,
82 | nextURL,
83 | packages,
84 | empty,
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { ClassValue, clsx } from 'clsx'
2 | import { twMerge } from 'tailwind-merge'
3 | import fetch from 'wretch'
4 |
5 | export function cn(...inputs: ClassValue[]) {
6 | return twMerge(clsx(inputs))
7 | }
8 |
9 | export function extractNumbers(str: string | undefined): number {
10 | if (!str) return 0
11 |
12 | const numbers = str.match(/\d+/g)
13 | return numbers ? parseInt(numbers.join('')) : 0
14 | }
15 |
16 | export const wait = async (ms: number) => new Promise(r => setTimeout(r, ms))
17 |
18 | export const toUnit = (num: number) => {
19 | if (num < 1000) {
20 | return num
21 | }
22 |
23 | return `${(num / 1000).toFixed(1)}k`
24 | }
25 |
26 | export const checkRepoExist = async (repo: string): Promise => {
27 | let link
28 | try {
29 | link = new URL(repo)
30 | } catch (error) {
31 | console.log('try from url failed', repo, error?.toString())
32 | try {
33 | link = new URL(repo, 'https://github.com/')
34 | } catch (error) {
35 | console.log(
36 | 'try from name failed',
37 | `https://github.com/${repo}`,
38 | error?.toString()
39 | )
40 | throw new Error('invalid repo name')
41 | }
42 | }
43 |
44 | if (link) {
45 | try {
46 | await fetch('/api/exist').post({ url: link.href }).res()
47 | return
48 | } catch (error) {
49 | console.log('fetch failed', error?.toString())
50 | throw new Error('fetch repo failed')
51 | }
52 | }
53 |
54 | throw new Error('invalid repo name')
55 | }
56 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | darkMode: ["class"],
4 | content: [
5 | './src/pages/**/*.{ts,tsx}',
6 | './src/components/**/*.{ts,tsx}',
7 | './src/app/**/*.{ts,tsx}',
8 | ],
9 | theme: {
10 | container: {
11 | center: true,
12 | padding: "2rem",
13 | screens: {
14 | "2xl": "1400px",
15 | },
16 | },
17 | extend: {
18 | colors: {
19 | border: "hsl(var(--border))",
20 | input: "hsl(var(--input))",
21 | ring: "hsl(var(--ring))",
22 | background: "hsl(var(--background))",
23 | foreground: "hsl(var(--foreground))",
24 | primary: {
25 | DEFAULT: "hsl(var(--primary))",
26 | foreground: "hsl(var(--primary-foreground))",
27 | },
28 | secondary: {
29 | DEFAULT: "hsl(var(--secondary))",
30 | foreground: "hsl(var(--secondary-foreground))",
31 | },
32 | destructive: {
33 | DEFAULT: "hsl(var(--destructive))",
34 | foreground: "hsl(var(--destructive-foreground))",
35 | },
36 | muted: {
37 | DEFAULT: "hsl(var(--muted))",
38 | foreground: "hsl(var(--muted-foreground))",
39 | },
40 | accent: {
41 | DEFAULT: "hsl(var(--accent))",
42 | foreground: "hsl(var(--accent-foreground))",
43 | },
44 | popover: {
45 | DEFAULT: "hsl(var(--popover))",
46 | foreground: "hsl(var(--popover-foreground))",
47 | },
48 | card: {
49 | DEFAULT: "hsl(var(--card))",
50 | foreground: "hsl(var(--card-foreground))",
51 | },
52 | },
53 | borderRadius: {
54 | lg: "var(--radius)",
55 | md: "calc(var(--radius) - 2px)",
56 | sm: "calc(var(--radius) - 4px)",
57 | },
58 | keyframes: {
59 | "accordion-down": {
60 | from: { height: 0 },
61 | to: { height: "var(--radix-accordion-content-height)" },
62 | },
63 | "accordion-up": {
64 | from: { height: "var(--radix-accordion-content-height)" },
65 | to: { height: 0 },
66 | },
67 | },
68 | animation: {
69 | "accordion-down": "accordion-down 0.2s ease-out",
70 | "accordion-up": "accordion-up 0.2s ease-out",
71 | },
72 | },
73 | },
74 | plugins: [require("tailwindcss-animate")],
75 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "incremental": true,
17 | "plugins": [
18 | {
19 | "name": "next"
20 | }
21 | ],
22 | "paths": {
23 | "@/*": ["./src/*"]
24 | }
25 | },
26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
27 | "exclude": ["node_modules"]
28 | }
29 |
--------------------------------------------------------------------------------
/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "git": {
3 | "deploymentEnabled": {
4 | "main": false
5 | }
6 | }
7 | }
--------------------------------------------------------------------------------