├── public ├── robots.txt └── favicon.ico ├── bun.lockb ├── components └── ui │ ├── input │ ├── index.ts │ └── Input.vue │ ├── dialog │ ├── DialogClose.vue │ ├── DialogTrigger.vue │ ├── DialogHeader.vue │ ├── Dialog.vue │ ├── DialogFooter.vue │ ├── index.ts │ ├── DialogDescription.vue │ ├── DialogTitle.vue │ ├── DialogScrollContent.vue │ └── DialogContent.vue │ ├── command │ ├── CommandShortcut.vue │ ├── index.ts │ ├── CommandEmpty.vue │ ├── CommandSeparator.vue │ ├── CommandList.vue │ ├── Command.vue │ ├── CommandItem.vue │ ├── CommandDialog.vue │ ├── CommandGroup.vue │ └── CommandInput.vue │ └── button │ ├── Button.vue │ └── index.ts ├── server ├── tsconfig.json └── api │ └── stars.ts ├── tsconfig.json ├── layouts └── default.vue ├── lib └── utils.ts ├── nuxt.config.ts ├── .gitignore ├── README.md ├── components.json ├── package.json ├── .github └── workflows │ └── fetch-star-projects.yml ├── fetch-star-projects.ts ├── assets └── css │ └── tailwind.css ├── tailwind.config.js ├── pages ├── components │ └── search.vue └── index.vue └── app.vue /public/robots.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoxiunique/ustars/HEAD/bun.lockb -------------------------------------------------------------------------------- /components/ui/input/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Input } from './Input.vue' 2 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.nuxt/tsconfig.server.json" 3 | } 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoxiunique/ustars/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://nuxt.com/docs/guide/concepts/typescript 3 | "extends": "./.nuxt/tsconfig.json" 4 | } 5 | -------------------------------------------------------------------------------- /server/api/stars.ts: -------------------------------------------------------------------------------- 1 | import allStars from './all-stars.json' 2 | 3 | export default defineEventHandler((event) => { 4 | return allStars 5 | }) -------------------------------------------------------------------------------- /layouts/default.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from 'clsx' 2 | import { twMerge } from 'tailwind-merge' 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /nuxt.config.ts: -------------------------------------------------------------------------------- 1 | // https://nuxt.com/docs/api/configuration/nuxt-config 2 | export default defineNuxtConfig({ 3 | compatibilityDate: '2024-11-01', 4 | modules: ['@nuxt/fonts', '@nuxtjs/tailwindcss', 'shadcn-nuxt'] 5 | }) -------------------------------------------------------------------------------- /components/ui/dialog/DialogClose.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Nuxt dev/build outputs 2 | .output 3 | .data 4 | .nuxt 5 | .nitro 6 | .cache 7 | dist 8 | 9 | # Node dependencies 10 | node_modules 11 | 12 | # Logs 13 | logs 14 | *.log 15 | 16 | # Misc 17 | .DS_Store 18 | .fleet 19 | .idea 20 | 21 | # Local env files 22 | .env 23 | .env.* 24 | !.env.example 25 | -------------------------------------------------------------------------------- /components/ui/dialog/DialogTrigger.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /components/ui/command/CommandShortcut.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /components/ui/dialog/DialogHeader.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 17 | -------------------------------------------------------------------------------- /components/ui/dialog/Dialog.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /components/ui/dialog/DialogFooter.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UStars(Github Star Manger) 2 | UStars 可以方便查询和整理你的 Github Star 仓库, 而不必每次使用 Github Star 功能(太难用) 3 | 4 | [预览](https://ustars.dev) 5 | 6 | ## 使用 7 | 8 | - fork 项目 9 | - 配置 Github Token 10 | - 部署项目到 vercel 或其他平台 11 | 12 | ## 技术栈 13 | 14 | - 前端框架:Nuxt.js 15 | - 数据来源:GitHub API 16 | - 更新机制:每日自动同步 `fetch-star-projects` 17 | 18 | ## 开发 19 | 20 | ```bash 21 | # 安装依赖 22 | bun i 23 | 24 | # 启动开发服务器 25 | bun run dev 26 | 27 | # 运行脚本 同步 Github Star 仓库 28 | bun run sync 29 | ``` 30 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://shadcn-vue.com/schema.json", 3 | "style": "default", 4 | "typescript": true, 5 | "tsConfigPath": ".nuxt/tsconfig.json", 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "assets/css/tailwind.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "framework": "nuxt", 14 | "aliases": { 15 | "components": "@/components", 16 | "utils": "@/lib/utils" 17 | } 18 | } -------------------------------------------------------------------------------- /components/ui/command/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Command } from './Command.vue' 2 | export { default as CommandDialog } from './CommandDialog.vue' 3 | export { default as CommandEmpty } from './CommandEmpty.vue' 4 | export { default as CommandGroup } from './CommandGroup.vue' 5 | export { default as CommandInput } from './CommandInput.vue' 6 | export { default as CommandItem } from './CommandItem.vue' 7 | export { default as CommandList } from './CommandList.vue' 8 | export { default as CommandSeparator } from './CommandSeparator.vue' 9 | export { default as CommandShortcut } from './CommandShortcut.vue' 10 | -------------------------------------------------------------------------------- /components/ui/dialog/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Dialog } from './Dialog.vue' 2 | export { default as DialogClose } from './DialogClose.vue' 3 | export { default as DialogContent } from './DialogContent.vue' 4 | export { default as DialogDescription } from './DialogDescription.vue' 5 | export { default as DialogFooter } from './DialogFooter.vue' 6 | export { default as DialogHeader } from './DialogHeader.vue' 7 | export { default as DialogScrollContent } from './DialogScrollContent.vue' 8 | export { default as DialogTitle } from './DialogTitle.vue' 9 | export { default as DialogTrigger } from './DialogTrigger.vue' 10 | -------------------------------------------------------------------------------- /components/ui/command/CommandEmpty.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 21 | -------------------------------------------------------------------------------- /components/ui/command/CommandSeparator.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 24 | -------------------------------------------------------------------------------- /components/ui/button/Button.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 27 | -------------------------------------------------------------------------------- /components/ui/dialog/DialogDescription.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 25 | -------------------------------------------------------------------------------- /components/ui/dialog/DialogTitle.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuxt-app", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "build": "nuxt build", 7 | "dev": "nuxt dev", 8 | "generate": "nuxt generate", 9 | "preview": "nuxt preview", 10 | "postinstall": "nuxt prepare", 11 | "sync": "bun run fetch-star-projects.ts" 12 | }, 13 | "dependencies": { 14 | "@nuxt/fonts": "0.10.3", 15 | "@nuxtjs/color-mode": "^3.5.2", 16 | "@nuxtjs/tailwindcss": "6.12.2", 17 | "@vueuse/core": "^12.3.0", 18 | "class-variance-authority": "^0.7.1", 19 | "clsx": "^2.1.1", 20 | "fuse.js": "^7.0.0", 21 | "lucide-vue-next": "^0.469.0", 22 | "nuxt": "^3.15.1", 23 | "octokit": "^4.0.3", 24 | "radix-vue": "^1.9.12", 25 | "shadcn-nuxt": "0.11.3", 26 | "tailwind-merge": "^2.6.0", 27 | "tailwindcss-animate": "^1.0.7", 28 | "vue": "latest", 29 | "vue-router": "latest" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /components/ui/command/CommandList.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 28 | -------------------------------------------------------------------------------- /components/ui/command/Command.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 31 | -------------------------------------------------------------------------------- /components/ui/input/Input.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 25 | -------------------------------------------------------------------------------- /components/ui/command/CommandItem.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 27 | -------------------------------------------------------------------------------- /components/ui/command/CommandDialog.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 22 | -------------------------------------------------------------------------------- /components/ui/command/CommandGroup.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 30 | -------------------------------------------------------------------------------- /components/ui/command/CommandInput.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 34 | -------------------------------------------------------------------------------- /.github/workflows/fetch-star-projects.yml: -------------------------------------------------------------------------------- 1 | name: Fetch Star Projects 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' # 每天运行 6 | workflow_dispatch: # 允许手动触发 7 | 8 | jobs: 9 | fetch-star-projects: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | with: 14 | token: ${{ secrets.GH_TOKEN }} # 使用你设置的 secret 15 | 16 | - name: Setup Git 17 | run: | 18 | git config --global user.name 'GitHub Actions Bot' 19 | git config --global user.email 'github-actions[bot]@users.noreply.github.com' 20 | 21 | - name: Setup Bun 22 | uses: oven-sh/setup-bun@v1 23 | with: 24 | bun-version: latest 25 | 26 | - name: Install dependencies 27 | run: bun install 28 | 29 | - name: Run fetch script 30 | env: 31 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 32 | run: bun run sync 33 | 34 | - name: Commit and push changes 35 | run: | 36 | git add . 37 | git commit -m "chore: fetch star projects [skip ci]" || exit 0 38 | git push 39 | -------------------------------------------------------------------------------- /fetch-star-projects.ts: -------------------------------------------------------------------------------- 1 | import { Octokit } from 'octokit' 2 | import fs from 'fs' 3 | import path from 'path' 4 | 5 | const GITHUB_TOKEN = process.env.GH_TOKEN; 6 | const octokit = new Octokit({ auth: GITHUB_TOKEN }) 7 | 8 | export async function fetchStarProjects() { 9 | let page = 1 10 | const allStarredProjects = [] 11 | 12 | while (true) { 13 | const response = await octokit.request('GET /user/starred', { 14 | per_page: 100, 15 | page: page, 16 | }) 17 | 18 | console.log('fetching page', page) 19 | if (response.data.length === 0) { 20 | break 21 | } 22 | 23 | allStarredProjects.push(...response.data) 24 | page++ 25 | 26 | await new Promise(resolve => setTimeout(resolve, 1_000)) 27 | } 28 | 29 | return allStarredProjects 30 | } 31 | 32 | const allStars = await fetchStarProjects() 33 | const copiedStars = allStars.map(star => ({ 34 | stargazers_count: star.stargazers_count, 35 | language: star.language, 36 | description: star.description, 37 | full_name: star.full_name, 38 | name: star.name, 39 | topics: star.topics, 40 | user: { 41 | avatar: star.owner.avatar_url, 42 | login: star.owner.login, 43 | }, 44 | html_url: star.html_url, 45 | created_at: star.created_at, 46 | updated_at: star.updated_at, 47 | })) 48 | 49 | fs.writeFileSync(path.join(__dirname, 'server', 'api', 'all-stars.json'), JSON.stringify(copiedStars)) 50 | console.log('done') -------------------------------------------------------------------------------- /components/ui/button/index.ts: -------------------------------------------------------------------------------- 1 | import { cva, type VariantProps } from 'class-variance-authority' 2 | 3 | export { default as Button } from './Button.vue' 4 | 5 | export const buttonVariants = cva( 6 | 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0', 7 | { 8 | variants: { 9 | variant: { 10 | default: 11 | 'bg-primary text-primary-foreground shadow hover:bg-primary/90', 12 | destructive: 13 | 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90', 14 | outline: 15 | 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground', 16 | secondary: 17 | 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80', 18 | ghost: 'hover:bg-accent hover:text-accent-foreground', 19 | link: 'text-primary underline-offset-4 hover:underline', 20 | }, 21 | size: { 22 | default: 'h-9 px-4 py-2', 23 | sm: 'h-8 rounded-md px-3 text-xs', 24 | lg: 'h-10 rounded-md px-8', 25 | icon: 'h-9 w-9', 26 | }, 27 | }, 28 | defaultVariants: { 29 | variant: 'default', 30 | size: 'default', 31 | }, 32 | }, 33 | ) 34 | 35 | export type ButtonVariants = VariantProps 36 | -------------------------------------------------------------------------------- /assets/css/tailwind.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 84% 4.9%; 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 84% 4.9%; 15 | 16 | --card: 0 0% 100%; 17 | --card-foreground: 222.2 84% 4.9%; 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 84.2% 60.2%; 32 | --destructive-foreground: 210 40% 98%; 33 | 34 | --ring: 222.2 84% 4.9%; 35 | 36 | --radius: 0.5rem; 37 | } 38 | 39 | .dark { 40 | --background: 222.2 84% 4.9%; 41 | --foreground: 210 40% 98%; 42 | 43 | --muted: 217.2 32.6% 17.5%; 44 | --muted-foreground: 215 20.2% 65.1%; 45 | 46 | --popover: 222.2 84% 4.9%; 47 | --popover-foreground: 210 40% 98%; 48 | 49 | --card: 222.2 84% 4.9%; 50 | --card-foreground: 210 40% 98%; 51 | 52 | --border: 217.2 32.6% 17.5%; 53 | --input: 217.2 32.6% 17.5%; 54 | 55 | --primary: 210 40% 98%; 56 | --primary-foreground: 222.2 47.4% 11.2%; 57 | 58 | --secondary: 217.2 32.6% 17.5%; 59 | --secondary-foreground: 210 40% 98%; 60 | 61 | --accent: 217.2 32.6% 17.5%; 62 | --accent-foreground: 210 40% 98%; 63 | 64 | --destructive: 0 62.8% 30.6%; 65 | --destructive-foreground: 210 40% 98%; 66 | 67 | --ring: 212.7 26.8% 83.9%; 68 | } 69 | } 70 | 71 | @layer base { 72 | * { 73 | @apply border-border; 74 | } 75 | body { 76 | @apply bg-background text-foreground; 77 | } 78 | } -------------------------------------------------------------------------------- /components/ui/dialog/DialogScrollContent.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 60 | -------------------------------------------------------------------------------- /components/ui/dialog/DialogContent.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 51 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | const animate = require("tailwindcss-animate") 2 | 3 | /** @type {import('tailwindcss').Config} */ 4 | module.exports = { 5 | darkMode: ["class"], 6 | safelist: ["dark"], 7 | prefix: "", 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 | xl: "calc(var(--radius) + 4px)", 55 | lg: "var(--radius)", 56 | md: "calc(var(--radius) - 2px)", 57 | sm: "calc(var(--radius) - 4px)", 58 | }, 59 | keyframes: { 60 | "accordion-down": { 61 | from: { height: 0 }, 62 | to: { height: "var(--radix-accordion-content-height)" }, 63 | }, 64 | "accordion-up": { 65 | from: { height: "var(--radix-accordion-content-height)" }, 66 | to: { height: 0 }, 67 | }, 68 | "collapsible-down": { 69 | from: { height: 0 }, 70 | to: { height: 'var(--radix-collapsible-content-height)' }, 71 | }, 72 | "collapsible-up": { 73 | from: { height: 'var(--radix-collapsible-content-height)' }, 74 | to: { height: 0 }, 75 | }, 76 | }, 77 | animation: { 78 | "accordion-down": "accordion-down 0.2s ease-out", 79 | "accordion-up": "accordion-up 0.2s ease-out", 80 | "collapsible-down": "collapsible-down 0.2s ease-in-out", 81 | "collapsible-up": "collapsible-up 0.2s ease-in-out", 82 | }, 83 | }, 84 | }, 85 | plugins: [animate], 86 | } -------------------------------------------------------------------------------- /pages/components/search.vue: -------------------------------------------------------------------------------- 1 | 76 | 77 | -------------------------------------------------------------------------------- /pages/index.vue: -------------------------------------------------------------------------------- 1 | 71 | 72 | 161 | 162 | 163 | 169 | -------------------------------------------------------------------------------- /app.vue: -------------------------------------------------------------------------------- 1 | 687 | 695 | --------------------------------------------------------------------------------