├── .github ├── FUNDING.yml └── workflows │ ├── depoly.yml │ └── release.yml ├── .eslintrc.json ├── public ├── icon.png ├── images │ └── intro.png ├── icon.svg ├── apple-icon.svg └── favicon.svg ├── vercel.json ├── postcss.config.js ├── .vscode └── settings.json ├── src ├── components │ ├── analytics.tsx │ ├── theme-provider.tsx │ ├── ui │ │ ├── label.tsx │ │ ├── input.tsx │ │ ├── toaster.tsx │ │ ├── switch.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── select.tsx │ │ ├── use-toast.ts │ │ ├── toast.tsx │ │ └── dropdown-menu.tsx │ ├── site-footer.tsx │ ├── mode-toggle.tsx │ ├── repo.tsx │ ├── package-select.tsx │ ├── intro.tsx │ ├── search-input.tsx │ ├── site-header.tsx │ ├── result.tsx │ └── icons.tsx ├── config.ts ├── app │ ├── api │ │ ├── exist │ │ │ └── route.ts │ │ └── dependents │ │ │ └── route.ts │ ├── page.tsx │ ├── layout.tsx │ └── globals.css ├── lib │ ├── utils.ts │ └── parse-page.ts └── hooks │ └── useDependents.ts ├── next.config.js ├── .changeset ├── config.json └── README.md ├── .gitignore ├── CHANGELOG.md ├── tsconfig.json ├── LICENSE ├── README.md ├── package.json └── tailwind.config.js /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: izayl 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/izayl/useful-dependents/HEAD/public/icon.png -------------------------------------------------------------------------------- /public/images/intro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/izayl/useful-dependents/HEAD/public/images/intro.png -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "git": { 3 | "deploymentEnabled": { 4 | "main": false 5 | } 6 | } 7 | } -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib", 3 | "typescript.enablePromptUseWorkspaceTsdk": true 4 | } 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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/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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /public/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 10 | 11 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | { 53 | if (e.key === 'Enter') { 54 | onSubmit() 55 | } 56 | }} 57 | /> 58 | 61 | 62 | 63 | ) 64 | } 65 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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/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/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/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/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/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 | --------------------------------------------------------------------------------