├── .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 | 2 | 3 | 4 | 7 | 10 | 11 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 10 | 11 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/izayl/useful-dependents/45a86a0cfeecbf52eed8a020d6a8a04322bc558f/public/icon.png -------------------------------------------------------------------------------- /public/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 10 | 11 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /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 | 20 | 29 | 30 | 34 | 38 | 39 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | ), 57 | sun: Sun, 58 | moon: Moon, 59 | laptop: Laptop, 60 | github: (props: LucideProps) => ( 61 | 62 | 66 | 67 | ), 68 | twitter: Twitter, 69 | star: Star, 70 | fork: (props: LucideProps) => ( 71 | 72 | 73 | 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 | intro 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 | {name} 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 |
11 |
12 |
13 | 14 | 15 | {siteConfig.name} 16 | 17 |
18 |
19 | 58 |
59 |
60 |
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 |