├── bin
└── starProject
├── web
├── public
│ └── icon.webp
├── postcss.config.js
├── src
│ ├── App.jsx
│ ├── main.jsx
│ ├── components
│ │ ├── layout
│ │ │ ├── Layout.jsx
│ │ │ ├── Footer.jsx
│ │ │ └── Header.jsx
│ │ ├── ui
│ │ │ ├── Loading.jsx
│ │ │ ├── Card.jsx
│ │ │ ├── Button.jsx
│ │ │ ├── Input.jsx
│ │ │ └── SearchAndFilter.jsx
│ │ └── project
│ │ │ └── ProjectCard.jsx
│ ├── App.css
│ ├── index.css
│ ├── assets
│ │ └── react.svg
│ └── pages
│ │ └── HomePage.jsx
├── vite.config.js
├── .gitignore
├── index.html
├── README.md
├── package.json
├── eslint.config.js
└── tailwind.config.js
├── .gitignore
├── README_CN.md
├── go.mod
├── README.md
├── model
└── star_model.go
├── go.sum
├── .github
└── workflows
│ └── auto-compile-and-commit.yml
├── .trae
└── documents
│ ├── notion-style-github-showcase-prd.md
│ └── notion-style-technical-architecture.md
└── main.go
/bin/starProject:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mran/githubstartog/HEAD/bin/starProject
--------------------------------------------------------------------------------
/web/public/icon.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mran/githubstartog/HEAD/web/public/icon.webp
--------------------------------------------------------------------------------
/web/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /main_test.go
2 | /.idea/.gitignore
3 | /.idea/deployment.xml
4 | /.idea/githubstartog.iml
5 | *.exe
6 | /.idea/modules.xml
7 | /.idea/vcs.xml
8 |
--------------------------------------------------------------------------------
/web/src/App.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import HomePage from './pages/HomePage';
3 |
4 | const App = () => {
5 | return ;
6 | };
7 |
8 | export default App;
9 |
--------------------------------------------------------------------------------
/web/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import react from '@vitejs/plugin-react'
3 |
4 | // https://vite.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | })
8 |
--------------------------------------------------------------------------------
/web/src/main.jsx:
--------------------------------------------------------------------------------
1 | import { StrictMode } from 'react'
2 | import { createRoot } from 'react-dom/client'
3 | import './index.css'
4 | import App from './App.jsx'
5 |
6 | createRoot(document.getElementById('root')).render(
7 |
8 |
9 | ,
10 | )
11 |
--------------------------------------------------------------------------------
/web/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/web/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | My Github start page
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/README_CN.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | [中文]("./README_CN.md")
6 |
7 | ## 项目描述
8 |
9 | GithubStartog 通过 Github API 获取用户的标星项目,根据项目的标签,并生成一个 Markdown 文件和前端页面。
10 | [DEMO](https://githubstartog.pages.dev/)
11 |
12 | ## 如何使用
13 |
14 | 1. fork 本项目
15 | 2. 在 `settings` > `Actions` > `General` 中,启用 Github Actions,允许读写权限
16 | 3. 手动启动 Github Actions 完成构建
17 | 4. 使用 Cloudflare Pages 部署
--------------------------------------------------------------------------------
/web/README.md:
--------------------------------------------------------------------------------
1 | # React + Vite
2 |
3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4 |
5 | Currently, two official plugins are available:
6 |
7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
9 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module githubstartog
2 |
3 | go 1.21
4 |
5 | toolchain go1.23.4
6 |
7 | require (
8 | github.com/go-resty/resty/v2 v2.16.2
9 | github.com/openai/openai-go v0.1.0-alpha.39
10 | github.com/sashabaranov/go-openai v1.36.0
11 | )
12 |
13 | require (
14 | github.com/tidwall/gjson v1.14.4 // indirect
15 | github.com/tidwall/match v1.1.1 // indirect
16 | github.com/tidwall/pretty v1.2.1 // indirect
17 | github.com/tidwall/sjson v1.2.5 // indirect
18 | golang.org/x/net v0.27.0 // indirect
19 | )
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ## Project Description
6 |
7 | GithubStartog uses the Github API to get the starred projects of a user, generate a Markdown file and a frontend page based on the tags of the projects.
8 | [DEMO](https://githubstartog.pages.dev/)
9 |
10 | ## How to use
11 |
12 | 1. Fork this project
13 | 2. Enable Github Actions in `settings` > `Actions` > `General`>`Read and write permissions`
14 | 3. Wait for Github Actions to finish building
15 | 4. Deploy with Cloudflare Pages
--------------------------------------------------------------------------------
/web/src/components/layout/Layout.jsx:
--------------------------------------------------------------------------------
1 | import { motion } from 'framer-motion';
2 | import Header from './Header';
3 | import Footer from './Footer';
4 |
5 | const Layout = ({ children }) => {
6 | return (
7 |
8 |
9 |
15 | {children}
16 |
17 |
18 |
19 | );
20 | };
21 |
22 | export default Layout;
--------------------------------------------------------------------------------
/web/src/App.css:
--------------------------------------------------------------------------------
1 | #root {
2 | padding: 2rem;
3 | text-align: center;
4 | }
5 |
6 | .logo {
7 | height: 6em;
8 | padding: 1.5em;
9 | will-change: filter;
10 | transition: filter 300ms;
11 | }
12 | .logo:hover {
13 | filter: drop-shadow(0 0 2em #646cffaa);
14 | }
15 | .logo.react:hover {
16 | filter: drop-shadow(0 0 2em #61dafbaa);
17 | }
18 |
19 | @keyframes logo-spin {
20 | from {
21 | transform: rotate(0deg);
22 | }
23 | to {
24 | transform: rotate(360deg);
25 | }
26 | }
27 |
28 | @media (prefers-reduced-motion: no-preference) {
29 | a:nth-of-type(2) .logo {
30 | animation: logo-spin infinite 20s linear;
31 | }
32 | }
33 |
34 | .card {
35 | padding: 2em;
36 | }
37 |
38 | .read-the-docs {
39 | color: #888;
40 | }
41 |
--------------------------------------------------------------------------------
/web/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "web",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "vite build",
9 | "lint": "eslint .",
10 | "preview": "vite preview"
11 | },
12 | "dependencies": {
13 | "framer-motion": "^12.23.12",
14 | "fuse.js": "^7.1.0",
15 | "lucide-react": "^0.542.0",
16 | "react": "^18.3.1",
17 | "react-dom": "^18.3.1",
18 | "react-router-dom": "^7.8.2",
19 | "typescript": "^5.9.2"
20 | },
21 | "devDependencies": {
22 | "@eslint/js": "^9.15.0",
23 | "@types/react": "^18.3.24",
24 | "@types/react-dom": "^18.3.7",
25 | "@vitejs/plugin-react": "^4.3.4",
26 | "autoprefixer": "^10.4.20",
27 | "eslint": "^9.15.0",
28 | "eslint-plugin-react": "^7.37.2",
29 | "eslint-plugin-react-hooks": "^5.0.0",
30 | "eslint-plugin-react-refresh": "^0.4.14",
31 | "globals": "^15.12.0",
32 | "postcss": "^8.4.49",
33 | "tailwindcss": "^3.4.16",
34 | "vite": "^6.0.1"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/web/src/components/ui/Loading.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { motion } from 'framer-motion';
3 |
4 | const Loading = ({ size = 'medium', text = '加载中...' }) => {
5 | const sizeClasses = {
6 | small: 'w-6 h-6',
7 | medium: 'w-12 h-12',
8 | large: 'w-16 h-16'
9 | };
10 |
11 | return (
12 |
13 |
22 | {text && (
23 |
29 | {text}
30 |
31 | )}
32 |
33 | );
34 | };
35 |
36 | export default Loading;
--------------------------------------------------------------------------------
/web/eslint.config.js:
--------------------------------------------------------------------------------
1 | import js from '@eslint/js'
2 | import globals from 'globals'
3 | import react from 'eslint-plugin-react'
4 | import reactHooks from 'eslint-plugin-react-hooks'
5 | import reactRefresh from 'eslint-plugin-react-refresh'
6 |
7 | export default [
8 | { ignores: ['dist'] },
9 | {
10 | files: ['**/*.{js,jsx}'],
11 | languageOptions: {
12 | ecmaVersion: 2020,
13 | globals: globals.browser,
14 | parserOptions: {
15 | ecmaVersion: 'latest',
16 | ecmaFeatures: { jsx: true },
17 | sourceType: 'module',
18 | },
19 | },
20 | settings: { react: { version: '18.3' } },
21 | plugins: {
22 | react,
23 | 'react-hooks': reactHooks,
24 | 'react-refresh': reactRefresh,
25 | },
26 | rules: {
27 | ...js.configs.recommended.rules,
28 | ...react.configs.recommended.rules,
29 | ...react.configs['jsx-runtime'].rules,
30 | ...reactHooks.configs.recommended.rules,
31 | 'react/jsx-no-target-blank': 'off',
32 | 'react-refresh/only-export-components': [
33 | 'warn',
34 | { allowConstantExport: true },
35 | ],
36 | },
37 | },
38 | ]
39 |
--------------------------------------------------------------------------------
/model/star_model.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import "time"
4 |
5 | type StarInfo struct {
6 | Name string `json:"name"`
7 | FullName string `json:"full_name"`
8 | Url string `json:"url"`
9 | HtmlUrl string `json:"html_url"`
10 | Description *string `json:"description"`
11 | Homepage *string `json:"homepage"`
12 | Language *string `json:"language"`
13 | Topics []string `json:"topics"`
14 | StargazersCount int `json:"stargazers_count"`
15 | UpdatedAt time.Time `json:"updated_at"`
16 | AI_tag AI_tag `json:"ai_tag"`
17 | }
18 | type ReadmeData struct {
19 | Name string `json:"name"`
20 | Path string `json:"path"`
21 | Sha string `json:"sha"`
22 | Size int `json:"size"`
23 | Url string `json:"url"`
24 | HtmlUrl string `json:"html_url"`
25 | GitUrl string `json:"git_url"`
26 | DownloadUrl string `json:"download_url"`
27 | Type string `json:"type"`
28 | Content string `json:"content"`
29 | RawContent string
30 |
31 | Encoding string `json:"encoding"`
32 | Links struct {
33 | Self string `json:"self"`
34 | Git string `json:"git"`
35 | Html string `json:"html"`
36 | } `json:"_links"`
37 | }
38 | type AI_tag struct {
39 | Group string `json:"group"`
40 | Tags []string `json:"tags"`
41 | Desc string `json:"desc"`
42 | }
43 |
--------------------------------------------------------------------------------
/web/src/components/ui/Card.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { motion } from 'framer-motion';
3 |
4 | const Card = ({
5 | children,
6 | className = '',
7 | hover = true,
8 | padding = 'medium',
9 | shadow = 'medium',
10 | onClick,
11 | ...props
12 | }) => {
13 | const baseClasses = 'bg-white border border-notion-gray-200 rounded-lg transition-all duration-200';
14 |
15 | const paddingClasses = {
16 | none: '',
17 | small: 'p-4',
18 | medium: 'p-6',
19 | large: 'p-8'
20 | };
21 |
22 | const shadowClasses = {
23 | none: '',
24 | small: 'shadow-sm',
25 | medium: 'shadow-notion-card',
26 | large: 'shadow-lg'
27 | };
28 |
29 | const hoverClasses = hover ? 'hover:shadow-notion-card-hover hover:border-notion-gray-300' : '';
30 | const clickableClasses = onClick ? 'cursor-pointer' : '';
31 |
32 | const cardClasses = `
33 | ${baseClasses}
34 | ${paddingClasses[padding]}
35 | ${shadowClasses[shadow]}
36 | ${hoverClasses}
37 | ${clickableClasses}
38 | ${className}
39 | `.trim();
40 |
41 | const cardVariants = {
42 | initial: { opacity: 0, y: 20 },
43 | animate: { opacity: 1, y: 0 },
44 | hover: hover ? { y: -2, scale: 1.01 } : {},
45 | tap: onClick ? { scale: 0.98 } : {}
46 | };
47 |
48 | return (
49 |
60 | {children}
61 |
62 | );
63 | };
64 |
65 | export default Card;
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/go-resty/resty/v2 v2.16.2 h1:CpRqTjIzq/rweXUt9+GxzzQdlkqMdt8Lm/fuK/CAbAg=
2 | github.com/go-resty/resty/v2 v2.16.2/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU=
3 | github.com/openai/openai-go v0.1.0-alpha.39 h1:FvoNWy7BPhA0TjGOK5huRGU5sAUEx2jeubLXz34K9LE=
4 | github.com/openai/openai-go v0.1.0-alpha.39/go.mod h1:3SdE6BffOX9HPEQv8IL/fi3LYZ5TUpRYaqGQZbyk11A=
5 | github.com/sashabaranov/go-openai v1.36.0 h1:fcSrn8uGuorzPWCBp8L0aCR95Zjb/Dd+ZSML0YZy9EI=
6 | github.com/sashabaranov/go-openai v1.36.0/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
7 | github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
8 | github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
9 | github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
10 | github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
11 | github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
12 | github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
13 | github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
14 | github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
15 | github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
16 | github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
17 | golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
18 | golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
19 | golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
20 | golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
21 |
--------------------------------------------------------------------------------
/web/src/components/ui/Button.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { motion } from 'framer-motion';
3 |
4 | const Button = ({
5 | children,
6 | variant = 'primary',
7 | size = 'medium',
8 | onClick,
9 | disabled = false,
10 | className = '',
11 | ...props
12 | }) => {
13 | const baseClasses = 'inline-flex items-center justify-center font-medium rounded-lg transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2';
14 |
15 | const variants = {
16 | primary: 'bg-notion-blue-600 text-white hover:bg-notion-blue-700 focus:ring-notion-blue-500 shadow-sm',
17 | secondary: 'bg-notion-gray-100 text-notion-gray-700 hover:bg-notion-gray-200 focus:ring-notion-gray-500 border border-notion-gray-300',
18 | ghost: 'text-notion-gray-600 hover:bg-notion-gray-100 hover:text-notion-gray-900 focus:ring-notion-gray-500',
19 | danger: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500 shadow-sm'
20 | };
21 |
22 | const sizes = {
23 | small: 'px-3 py-1.5 text-sm',
24 | medium: 'px-4 py-2 text-sm',
25 | large: 'px-6 py-3 text-base'
26 | };
27 |
28 | const disabledClasses = 'opacity-50 cursor-not-allowed';
29 |
30 | const buttonClasses = `
31 | ${baseClasses}
32 | ${variants[variant]}
33 | ${sizes[size]}
34 | ${disabled ? disabledClasses : ''}
35 | ${className}
36 | `.trim();
37 |
38 | return (
39 |
48 | {children}
49 |
50 | );
51 | };
52 |
53 | export default Button;
--------------------------------------------------------------------------------
/web/src/components/ui/Input.jsx:
--------------------------------------------------------------------------------
1 | import React, { forwardRef } from 'react';
2 | import { motion } from 'framer-motion';
3 |
4 | const Input = forwardRef(({
5 | type = 'text',
6 | placeholder,
7 | value,
8 | onChange,
9 | onFocus,
10 | onBlur,
11 | disabled = false,
12 | error = false,
13 | className = '',
14 | icon: Icon,
15 | ...props
16 | }, ref) => {
17 | const baseClasses = 'w-full px-4 py-2.5 text-notion-gray-900 bg-white border rounded-lg transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-0';
18 |
19 | const stateClasses = error
20 | ? 'border-red-300 focus:border-red-500 focus:ring-red-200'
21 | : 'border-notion-gray-300 focus:border-notion-blue-500 focus:ring-notion-blue-200 hover:border-notion-gray-400';
22 |
23 | const disabledClasses = 'opacity-50 cursor-not-allowed bg-notion-gray-50';
24 |
25 | const inputClasses = `
26 | ${baseClasses}
27 | ${stateClasses}
28 | ${disabled ? disabledClasses : ''}
29 | ${Icon ? 'pl-10' : ''}
30 | ${className}
31 | `.trim();
32 |
33 | return (
34 |
35 | {Icon && (
36 |
37 |
38 |
39 | )}
40 |
54 |
55 | );
56 | });
57 |
58 | Input.displayName = 'Input';
59 |
60 | export default Input;
--------------------------------------------------------------------------------
/web/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | export default {
3 | content: [
4 | "./index.html",
5 | "./src/**/*.{js,ts,jsx,tsx}",
6 | ],
7 | theme: {
8 | extend: {
9 | fontFamily: {
10 | 'sans': ['Inter', 'system-ui', 'sans-serif'],
11 | 'mono': ['JetBrains Mono', 'monospace'],
12 | },
13 | colors: {
14 | notion: {
15 | blue: '#2383E2',
16 | orange: '#FF6B35',
17 | gray: {
18 | 50: '#F7F6F3',
19 | 100: '#E9E9E7',
20 | 200: '#CBCAC8',
21 | 300: '#A0A0A0',
22 | 400: '#6F6F6F',
23 | 500: '#2F2F2F',
24 | }
25 | }
26 | },
27 | animation: {
28 | 'fade-in': 'fadeIn 0.3s ease-out',
29 | 'slide-up': 'slideUp 0.3s ease-out',
30 | 'scale-in': 'scaleIn 0.2s ease-out',
31 | },
32 | keyframes: {
33 | fadeIn: {
34 | '0%': { opacity: '0' },
35 | '100%': { opacity: '1' },
36 | },
37 | slideUp: {
38 | '0%': { transform: 'translateY(10px)', opacity: '0' },
39 | '100%': { transform: 'translateY(0)', opacity: '1' },
40 | },
41 | scaleIn: {
42 | '0%': { transform: 'scale(0.95)', opacity: '0' },
43 | '100%': { transform: 'scale(1)', opacity: '1' },
44 | },
45 | },
46 | boxShadow: {
47 | 'notion': '0 8px 32px rgba(0, 0, 0, 0.12)',
48 | 'notion-hover': '0 12px 48px rgba(0, 0, 0, 0.18)',
49 | },
50 | borderRadius: {
51 | 'notion-sm': '8px',
52 | 'notion-md': '12px',
53 | 'notion-lg': '16px',
54 | },
55 | spacing: {
56 | '18': '4.5rem',
57 | '88': '22rem',
58 | },
59 | transitionDuration: {
60 | '250': '250ms',
61 | },
62 | },
63 | },
64 | plugins: [],
65 | }
66 |
67 |
--------------------------------------------------------------------------------
/.github/workflows/auto-compile-and-commit.yml:
--------------------------------------------------------------------------------
1 | name: Go Build and Compile
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | workflow_dispatch:
8 | # 可以添加可选的输入参数
9 | inputs:
10 | reason:
11 | description: 'Reason for manual run'
12 | required: false
13 | default: 'Manual trigger'
14 | jobs:
15 | build-and-compile:
16 | runs-on: ubuntu-latest
17 | # 设置权限,允许 push
18 | permissions:
19 | contents: write
20 |
21 | steps:
22 | - name: Checkout code
23 | uses: actions/checkout@v4 # 使用最新版本
24 |
25 | - name: Set up Go environment
26 | uses: actions/setup-go@v5 # 使用最新版本
27 | with:
28 | go-version: '1.23'
29 | check-latest: true
30 |
31 | # 使用环境变量
32 | - name: Build and compile
33 | env:
34 | LLM_BASEURL: ${{ secrets.LLM_BASEURL }}
35 | LLM_TOKEN: ${{ secrets.LLM_TOKEN }}
36 | run: |
37 | go build -o ./bin/starProject
38 |
39 | - name: Execute compiled binary
40 | env:
41 | LLM_BASEURL: ${{ secrets.LLM_BASEURL }}
42 | LLM_TOKEN: ${{ secrets.LLM_TOKEN }}
43 | USERNAME: ${{ github.repository_owner }}
44 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
45 | run: |
46 | ./bin/starProject
47 | - name: Copy aiTagProcess.json to web/public
48 | run: |
49 | cp aiTagProcess.json web/public/projects.json
50 |
51 | - name: Commit all files
52 | # 使用 GITHUB_TOKEN 进行 git 操作
53 | env:
54 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
55 | run: |
56 | git config --global user.name "github-actions[bot]"
57 | git config --global user.email "github-actions[bot]@users.noreply.github.com"
58 | git add .
59 | git commit -m "Automated commit after execution" || echo "No changes to commit"
60 | git push
--------------------------------------------------------------------------------
/.trae/documents/notion-style-github-showcase-prd.md:
--------------------------------------------------------------------------------
1 | # GitHub项目展示平台 - Notion风格重设计
2 |
3 | ## 1. 产品概述
4 |
5 | 这不是一个简单的GitHub项目列表,而是一个具有Notion级别设计标准的知识展示平台。我们要打造一个让开发者沉浸其中、优雅浏览项目的数字空间。
6 |
7 | 产品的核心使命是将冰冷的代码仓库转化为有温度的知识资产,通过极简主义的设计语言和精致的交互体验,让每个项目都能得到应有的展示。
8 |
9 | 目标是成为开发者个人品牌展示的新标杆,重新定义技术项目的呈现方式。
10 |
11 | ## 2. 核心功能
12 |
13 | ### 2.1 用户角色
14 |
15 | 本产品采用单一用户模式,专注于为项目所有者提供最佳的展示体验。
16 |
17 | ### 2.2 功能模块
18 |
19 | 我们的重设计包含以下核心页面:
20 |
21 | 1. **主展示页面**:沉浸式项目浏览体验,智能搜索与筛选系统
22 | 2. **项目详情页面**:深度项目信息展示,优雅的代码预览
23 | 3. **关于页面**:个人品牌故事展示
24 |
25 | ### 2.3 页面详情
26 |
27 | | 页面名称 | 模块名称 | 功能描述 |
28 | |---------|---------|----------|
29 | | 主展示页面 | 顶部导航栏 | 极简logo展示,搜索入口,主题切换。采用Notion式的悬浮设计 |
30 | | 主展示页面 | 智能搜索系统 | 实时模糊搜索,智能标签建议,搜索历史。支持快捷键操作 |
31 | | 主展示页面 | 项目卡片网格 | 响应式瀑布流布局,悬停微动效,渐进式加载。每个卡片都是艺术品 |
32 | | 主展示页面 | 高级筛选面板 | 侧边栏筛选器,技术栈分类,时间轴筛选,星标数排序 |
33 | | 主展示页面 | 统计仪表板 | 项目总览数据,技术栈分布图,活跃度热力图 |
34 | | 项目详情页面 | 项目头部信息 | 大标题展示,关键指标可视化,快速操作按钮组 |
35 | | 项目详情页面 | 代码预览模块 | 语法高亮代码展示,文件树导航,在线预览功能 |
36 | | 项目详情页面 | 项目时间线 | 提交历史可视化,里程碑展示,贡献者信息 |
37 | | 关于页面 | 个人简介区域 | 头像展示,技能标签云,联系方式,个人故事 |
38 | | 关于页面 | 技术栈展示 | 交互式技能图表,经验时长可视化,项目关联 |
39 |
40 | ## 3. 核心流程
41 |
42 | **主要用户操作流程:**
43 |
44 | 用户进入主页 → 被优雅的视觉设计吸引 → 通过智能搜索或浏览发现感兴趣的项目 → 点击项目卡片进入详情页 → 深度了解项目信息 → 通过外链访问GitHub仓库或在线演示 → 返回主页继续探索或访问关于页面了解作者
45 |
46 | **页面导航流程图:**
47 |
48 | ```mermaid
49 | graph TD
50 | A[主展示页面] --> B[项目详情页面]
51 | A --> C[关于页面]
52 | B --> A
53 | B --> D[GitHub仓库]
54 | B --> E[在线演示]
55 | C --> A
56 | C --> F[联系方式]
57 | ```
58 |
59 | ## 4. 用户界面设计
60 |
61 | ### 4.1 设计风格
62 |
63 | **色彩系统:**
64 | - 主色调:纯净白色 (#FFFFFF) 和深邃黑色 (#000000)
65 | - 辅助色:温暖灰色系列 (#F7F6F3, #E9E9E7, #CBCAC8)
66 | - 强调色:Notion蓝 (#2383E2) 和警示橙 (#FF6B35)
67 | - 文字色彩:主文字 (#2F2F2F),次要文字 (#6F6F6F),辅助文字 (#A0A0A0)
68 |
69 | **字体系统:**
70 | - 主字体:Inter (现代无衬线,极佳的屏幕显示效果)
71 | - 代码字体:JetBrains Mono (等宽字体,代码展示专用)
72 | - 字号层级:12px/14px/16px/20px/24px/32px/48px
73 |
74 | **设计原则:**
75 | - 按钮风格:圆角8px,悬停状态微妙阴影,点击反馈
76 | - 布局风格:大量留白,内容居中,最大宽度1200px
77 | - 动效风格:缓动函数 ease-out,持续时间200-300ms
78 | - 图标风格:线性图标,2px描边,24px标准尺寸
79 |
80 | ### 4.2 页面设计概览
81 |
82 | | 页面名称 | 模块名称 | UI元素 |
83 | |---------|---------|--------|
84 | | 主展示页面 | 顶部导航栏 | 高度64px,背景模糊效果,固定定位。Logo使用Inter字体,搜索框圆角12px |
85 | | 主展示页面 | 项目卡片 | 白色背景,圆角16px,悬停阴影0 8px 32px rgba(0,0,0,0.12)。标题使用Inter Medium 18px |
86 | | 主展示页面 | 筛选面板 | 侧边栏宽度280px,背景#F7F6F3,分组标题Inter SemiBold 14px |
87 | | 项目详情页面 | 项目头部 | 大标题Inter Bold 32px,指标卡片使用圆角12px,间距24px |
88 | | 项目详情页面 | 代码预览 | 背景#1E1E1E,圆角12px,JetBrains Mono 14px,行高1.5 |
89 | | 关于页面 | 个人简介 | 头像圆形120px,技能标签圆角20px,背景渐变色 |
90 |
91 | ### 4.3 响应式设计
92 |
93 | 产品采用移动优先的响应式设计策略:
94 | - 桌面端:1200px+ 三列网格布局,侧边栏固定
95 | - 平板端:768px-1199px 两列网格,侧边栏可收起
96 | - 移动端:<768px 单列布局,底部导航,手势优化
97 |
98 | 支持触摸交互优化,包括滑动切换、长按菜单、双击缩放等移动端专属交互。
--------------------------------------------------------------------------------
/web/src/index.css:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap');
2 | @tailwind base;
3 | @tailwind components;
4 | @tailwind utilities;
5 |
6 | /* Notion风格的CSS变量 */
7 | :root {
8 | --spacing-unit: 8px;
9 | --border-radius-sm: 8px;
10 | --border-radius-md: 12px;
11 | --border-radius-lg: 16px;
12 | --transition-fast: 0.15s ease-out;
13 | --transition-normal: 0.3s ease-out;
14 | --transition-slow: 0.5s ease-out;
15 |
16 | /* 字体设置 */
17 | font-family: 'Inter', system-ui, -apple-system, sans-serif;
18 | line-height: 1.6;
19 | font-weight: 400;
20 |
21 | /* 渲染优化 */
22 | font-synthesis: none;
23 | text-rendering: optimizeLegibility;
24 | -webkit-font-smoothing: antialiased;
25 | -moz-osx-font-smoothing: grayscale;
26 |
27 | /* 颜色主题 */
28 | color: #2F2F2F;
29 | background-color: #FFFFFF;
30 | }
31 |
32 | /* 全局样式重置 */
33 | * {
34 | box-sizing: border-box;
35 | }
36 |
37 | body {
38 | margin: 0;
39 | padding: 0;
40 | min-height: 100vh;
41 | background-color: #FFFFFF;
42 | color: #2F2F2F;
43 | }
44 |
45 | /* Notion风格的通用组件样式 */
46 | @layer components {
47 | .notion-card {
48 | @apply bg-white rounded-notion-lg shadow-notion hover:shadow-notion-hover;
49 | @apply transition-all duration-300 ease-out;
50 | @apply border border-gray-100;
51 | }
52 |
53 | .notion-button {
54 | @apply px-4 py-2 rounded-notion-md font-medium;
55 | @apply transition-all duration-200 ease-out;
56 | @apply focus:outline-none focus:ring-2 focus:ring-notion-blue focus:ring-opacity-50;
57 | }
58 |
59 | .notion-input {
60 | @apply w-full px-4 py-3 rounded-notion-md border border-gray-200;
61 | @apply focus:border-notion-blue focus:ring-2 focus:ring-notion-blue focus:ring-opacity-20;
62 | @apply transition-all duration-200 ease-out;
63 | @apply font-sans placeholder-notion-gray-300;
64 | }
65 |
66 | .notion-text-primary {
67 | @apply text-notion-gray-500;
68 | }
69 |
70 | .notion-text-secondary {
71 | @apply text-notion-gray-400;
72 | }
73 |
74 | .notion-text-tertiary {
75 | @apply text-notion-gray-300;
76 | }
77 | }
78 |
79 | /* 链接样式 */
80 | a {
81 | color: #2383E2;
82 | text-decoration: none;
83 | transition: color var(--transition-fast);
84 | }
85 |
86 | a:hover {
87 | color: #1a6bc7;
88 | }
89 |
90 | /* 标题样式 */
91 | h1, h2, h3, h4, h5, h6 {
92 | font-family: 'Inter', sans-serif;
93 | font-weight: 600;
94 | line-height: 1.3;
95 | margin: 0;
96 | }
97 |
98 | /* 滚动条样式 */
99 | ::-webkit-scrollbar {
100 | width: 6px;
101 | height: 6px;
102 | }
103 |
104 | ::-webkit-scrollbar-track {
105 | background: #F7F6F3;
106 | }
107 |
108 | ::-webkit-scrollbar-thumb {
109 | background: #CBCAC8;
110 | border-radius: 3px;
111 | }
112 |
113 | ::-webkit-scrollbar-thumb:hover {
114 | background: #A0A0A0;
115 | }
116 |
--------------------------------------------------------------------------------
/web/src/assets/react.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/web/src/components/layout/Footer.jsx:
--------------------------------------------------------------------------------
1 | import { motion } from 'framer-motion';
2 | import { Github, Twitter, Linkedin, Mail } from 'lucide-react';
3 |
4 | const Footer = () => {
5 | const socialLinks = [
6 | { icon: Github, href: '#', label: 'GitHub' },
7 | { icon: Twitter, href: '#', label: 'Twitter' },
8 | { icon: Linkedin, href: '#', label: 'LinkedIn' },
9 | { icon: Mail, href: '#', label: 'Email' },
10 | ];
11 |
12 | return (
13 |
106 | );
107 | };
108 |
109 | export default Footer;
--------------------------------------------------------------------------------
/web/src/components/layout/Header.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { motion, AnimatePresence } from 'framer-motion';
3 | import { Search, Menu, X, Github } from 'lucide-react';
4 | import Button from '../ui/Button';
5 |
6 | const Header = () => {
7 | const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
8 |
9 | return (
10 |
16 |
17 |
18 | {/* Logo */}
19 |
24 |
25 |
26 |
27 |
28 | Projects
29 |
30 |
31 |
32 | {/* Desktop Navigation */}
33 |
34 |
40 | Home
41 |
42 |
48 | Projects
49 |
50 |
56 | About
57 |
58 |
59 |
60 | {/* Search and Mobile Menu */}
61 |
62 |
67 |
68 |
69 |
70 | setIsMobileMenuOpen(!isMobileMenuOpen)}
75 | >
76 | {isMobileMenuOpen ? (
77 |
78 | ) : (
79 |
80 | )}
81 |
82 |
83 |
84 |
85 |
86 |
87 | {/* Mobile Navigation Menu */}
88 |
89 | {isMobileMenuOpen && (
90 |
97 |
98 | setIsMobileMenuOpen(false)}
103 | >
104 | Home
105 |
106 | setIsMobileMenuOpen(false)}
111 | >
112 | Projects
113 |
114 | setIsMobileMenuOpen(false)}
119 | >
120 | About
121 |
122 |
123 |
124 | )}
125 |
126 |
127 | );
128 | };
129 |
130 | export default Header;
--------------------------------------------------------------------------------
/web/src/components/project/ProjectCard.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { motion } from 'framer-motion';
3 | import { Star, GitFork, ExternalLink, Calendar, Github } from 'lucide-react';
4 | import Card from '../ui/Card';
5 | import Button from '../ui/Button';
6 |
7 | const ProjectCard = ({ project, index = 0 }) => {
8 | // Merge tags from different sources
9 | const allTags = [
10 | ...(project.topics || []),
11 | ...(project.ai_tag?.tags || []),
12 | project.language
13 | ].filter(Boolean);
14 |
15 | // Notion-style tag colors
16 | const getTagStyle = (tag) => {
17 | const tagStyles = {
18 | 'JavaScript': 'bg-yellow-50 text-yellow-700 border-yellow-200',
19 | 'TypeScript': 'bg-blue-50 text-blue-700 border-blue-200',
20 | 'React': 'bg-cyan-50 text-cyan-700 border-cyan-200',
21 | 'Vue': 'bg-green-50 text-green-700 border-green-200',
22 | 'Python': 'bg-emerald-50 text-emerald-700 border-emerald-200',
23 | 'Java': 'bg-red-50 text-red-700 border-red-200',
24 | 'CSS': 'bg-indigo-50 text-indigo-700 border-indigo-200',
25 | 'HTML': 'bg-orange-50 text-orange-700 border-orange-200',
26 | 'Node.js': 'bg-lime-50 text-lime-700 border-lime-200',
27 | 'default': 'bg-notion-gray-50 text-notion-gray-600 border-notion-gray-200'
28 | };
29 | return tagStyles[tag] || tagStyles.default;
30 | };
31 |
32 | // Format date
33 | const formatDate = (dateString) => {
34 | return new Date(dateString).toLocaleDateString('en-US', {
35 | year: 'numeric',
36 | month: 'short',
37 | day: 'numeric'
38 | });
39 | };
40 |
41 | return (
42 |
47 | {/* Card Header */}
48 |
49 |
75 |
76 | {/* Description */}
77 |
78 | {project.description || 'No description available'}
79 |
80 |
81 | {/* Tags */}
82 | {allTags.length > 0 && (
83 |
84 | {allTags.slice(0, 4).map((tag) => (
85 |
90 | {tag}
91 |
92 | ))}
93 | {allTags.length > 4 && (
94 |
95 | +{allTags.length - 4}
96 |
97 | )}
98 |
99 | )}
100 |
101 |
102 | {/* Card Footer */}
103 |
104 |
105 | {/* Last Updated */}
106 |
107 |
108 |
109 | {project.updated_at ? formatDate(project.updated_at) : 'Unknown'}
110 |
111 |
112 |
113 | {/* Action Buttons */}
114 |
115 | window.open(project.html_url, '_blank')}
119 | className="flex items-center space-x-2"
120 | >
121 |
122 | GitHub
123 |
124 | {project.homepage && (
125 | window.open(project.homepage, '_blank')}
129 | className="flex items-center space-x-2"
130 | >
131 |
132 | Live Demo
133 |
134 | )}
135 |
136 |
137 |
138 |
139 | );
140 | };
141 |
142 | export default ProjectCard;
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "encoding/base64"
6 | "encoding/json"
7 | "fmt"
8 | "github.com/go-resty/resty/v2"
9 | "github.com/openai/openai-go"
10 | "github.com/openai/openai-go/option"
11 | "githubstartog/model"
12 | "os"
13 | "strconv"
14 | "strings"
15 | "sync"
16 | //"github.com/sashabaranov/go-openai"
17 | )
18 |
19 | func FetchUserStar(user string, page int) *[]model.StarInfo {
20 | client := resty.New()
21 | // 请求https://api.github.com/users/mran/starred?page=10
22 | url := "https://api.github.com/users/" + user + "/starred"
23 | token := os.Getenv("GITHUB_TOKEN")
24 | resp, err := client.R().
25 | SetQueryParam("page", strconv.Itoa(page)).
26 | SetHeader("Authorization", "token "+token).
27 | Get(url)
28 | if err != nil {
29 | panic(err)
30 | }
31 | body := resp.String()
32 | if len(body) == 0 {
33 | return nil
34 | }
35 | //转为json 对象
36 | var infos []model.StarInfo
37 | er := json.Unmarshal([]byte(body), &infos)
38 | if er != nil {
39 | fmt.Println(body)
40 | panic(er)
41 | }
42 | return &infos
43 | }
44 |
45 | // openai 通用请求代码
46 | func OpenaiRequest(prompt string) string {
47 | // 如果 prompt 的长度(以字节计)大于 65536 则截断(注意中文字符)
48 | if len([]rune(prompt)) > 10000 {
49 | prompt = string([]rune(prompt)[:10000])
50 | }
51 | a := []option.RequestOption{
52 | option.WithAPIKey(LLMTOKEN),
53 | option.WithBaseURL(LLMBASEURL),
54 | }
55 | client := openai.NewClient(a...)
56 | chatCompletion, err := client.Chat.Completions.New(
57 | context.TODO(),
58 | openai.ChatCompletionNewParams{
59 | Messages: openai.F([]openai.ChatCompletionMessageParamUnion{
60 | openai.UserMessage(
61 | `我有一个标过star的github库需要进行分类和标记,给出最相关的8个标记。我会给你它的的readme文件,请你分析后给出一个json结构的标签,
62 | 限制:只给出严格的json结果,其他内容比如json标签等不需要。
63 | 输出格式严格如下
64 | {
65 | "tags": []
66 | }`,
67 | ),
68 | openai.UserMessage(prompt),
69 | }),
70 | Model: openai.F("deepseek-chat"),
71 | },
72 | )
73 | if err != nil {
74 | fmt.Println(prompt)
75 | return ""
76 | }
77 | return chatCompletion.Choices[0].Message.Content
78 | }
79 |
80 | // 获取指定的github项目的readme 内容
81 | func GetReadme(redmeUrl string) *model.ReadmeData {
82 | client := resty.New()
83 | //client.SetProxy("http://127.0.0.1:11081")
84 | url := redmeUrl + "/readme"
85 | resp, err := client.R().
86 | SetHeader("Authorization", "token "+gtihubToken).
87 | Get(url)
88 | if err != nil {
89 | return nil
90 |
91 | }
92 | body := resp.String()
93 | var data model.ReadmeData
94 | err = json.Unmarshal([]byte(body), &data)
95 | if err != nil {
96 | return nil
97 | }
98 | decodedContent, _ := base64.StdEncoding.DecodeString(data.Content)
99 | data.RawContent = string(decodedContent)
100 | return &data
101 | }
102 |
103 | // 并行处理
104 | func AIParallelProcess(data []model.StarInfo) []model.StarInfo {
105 |
106 | var wg sync.WaitGroup
107 | results := make(chan model.StarInfo, len(data))
108 | //增加一个限速
109 | limiter := make(chan struct{}, 30)
110 | for _, star := range data {
111 | wg.Add(1)
112 | go func(star model.StarInfo) {
113 | defer wg.Done()
114 | limiter <- struct{}{}
115 | defer func() { <-limiter }()
116 | // 获取readme
117 | readme := GetReadme(star.Url)
118 | if readme == nil {
119 | return
120 | }
121 | fmt.Println(star.FullName)
122 | // 调用openai接口
123 | result := OpenaiRequest(readme.RawContent)
124 | var ai_tag model.AI_tag
125 | err := json.Unmarshal([]byte(result), &ai_tag)
126 | if err != nil {
127 | return
128 | }
129 | star.AI_tag = ai_tag
130 | //还results需要添加对应的github项目标签
131 | results <- star
132 | }(star)
133 | }
134 |
135 | go func() {
136 | wg.Wait()
137 | close(results)
138 | }()
139 | //取出结果
140 | var resultlist []model.StarInfo
141 | for result := range results {
142 | resultlist = append(resultlist, result)
143 | fmt.Println("Result from OpenAI:", result)
144 | }
145 | return resultlist
146 | }
147 |
148 | func SaveMiddleResult(allStar interface{}, fileName string) {
149 | // 结果写入文件
150 | file, err := os.Create(fileName)
151 | if err != nil {
152 | panic(err)
153 | }
154 | defer file.Close()
155 | encoder := json.NewEncoder(file)
156 | encoder.SetIndent("", " ")
157 | err = encoder.Encode(allStar)
158 | if err != nil {
159 | panic(err)
160 | }
161 | }
162 | func readFileCache(filename string) []model.StarInfo {
163 | //读取allStar.json
164 | var allStar []model.StarInfo
165 |
166 | file, err := os.Open(filename)
167 | if err != nil {
168 | return allStar
169 | }
170 | defer file.Close()
171 | decoder := json.NewDecoder(file)
172 | err = decoder.Decode(&allStar)
173 | if err != nil {
174 | panic(err)
175 | }
176 | return allStar
177 | }
178 | func Json2md(aiTagProcess []model.StarInfo) string {
179 | var md string
180 | for _, star := range aiTagProcess {
181 | md += fmt.Sprintf("### [%s](%s)\n", star.FullName, star.HtmlUrl)
182 | if star.Description != nil {
183 | md += fmt.Sprintf("- **Description:** %s\n", *star.Description)
184 | }
185 | if len(star.AI_tag.Tags) > 0 {
186 | md += fmt.Sprintf("- **Tags:** %s\n", strings.Join(star.AI_tag.Tags, ", "))
187 | }
188 | md += "\n"
189 | }
190 | //保存md到文件
191 | file, err := os.Create("starProject.md")
192 | if err != nil {
193 | panic(err)
194 | }
195 | defer file.Close()
196 | _, err = file.WriteString(md)
197 | if err != nil {
198 | panic(err)
199 | }
200 | return md
201 |
202 | }
203 |
204 | var gtihubToken = os.Getenv("GITHUB_TOKEN")
205 | var LLMTOKEN = os.Getenv("LLM_TOKEN")
206 | var LLMBASEURL = os.Getenv("LLM_BASEURL")
207 | var username = os.Getenv("USERNAME")
208 |
209 | // main 加入启动参数
210 | func main() {
211 | if len(username) == 0 {
212 | panic("need username")
213 | }
214 | if len(gtihubToken) == 0 {
215 | panic("need gtihubToken")
216 |
217 | }
218 |
219 | //分页获取用户所有的star,加入数组
220 | var allStar []model.StarInfo
221 | page := 1
222 | for {
223 | stars := FetchUserStar(username, page)
224 | if len(*stars) == 0 {
225 | break
226 | }
227 | for _, star := range *stars {
228 | println(star.FullName)
229 | allStar = append(allStar, star)
230 | }
231 |
232 | page++
233 | }
234 | //保存两份
235 | SaveMiddleResult(allStar, "allStar.json")
236 | SaveMiddleResult(allStar, "aiTagProcess.json")
237 |
238 | var aiTagProcess []model.StarInfo
239 | aiTagProcess = readFileCache("aiTagProcess.json")
240 | //需要AI标签的处理
241 | if len(LLMTOKEN) != 0 && len(LLMBASEURL) != 0 {
242 | if len(allStar) != 0 {
243 | aiTagProcess := AIParallelProcess(allStar)
244 | SaveMiddleResult(aiTagProcess, "aiTagProcess.json")
245 | }
246 | }
247 |
248 | Json2md(aiTagProcess)
249 | }
250 |
--------------------------------------------------------------------------------
/.trae/documents/notion-style-technical-architecture.md:
--------------------------------------------------------------------------------
1 | # GitHub项目展示平台 - 技术架构设计
2 |
3 | ## 1. 架构设计
4 |
5 | ```mermaid
6 | graph TD
7 | A[用户浏览器] --> B[React 18 前端应用]
8 | B --> C[Framer Motion 动画引擎]
9 | B --> D[Tailwind CSS 样式系统]
10 | B --> E[Fuse.js 搜索引擎]
11 | B --> F[静态JSON数据源]
12 |
13 | subgraph "前端层"
14 | B
15 | C
16 | D
17 | E
18 | end
19 |
20 | subgraph "数据层"
21 | F
22 | end
23 |
24 | subgraph "构建工具链"
25 | G[Vite 构建工具]
26 | H[PostCSS 处理器]
27 | end
28 | ```
29 |
30 | ## 2. 技术描述
31 |
32 | * **前端框架**: React\@18 + TypeScript\@5 + Vite\@6
33 |
34 | * **样式系统**: Tailwind CSS\@3 + PostCSS\@8 + Autoprefixer\@10
35 |
36 | * **动画库**: Framer Motion\@11 (实现Notion级别的微交互)
37 |
38 | * **搜索引擎**: Fuse.js\@7 (模糊搜索和智能筛选)
39 |
40 | * **图标系统**: Lucide React\@0.400+ (一致的线性图标)
41 |
42 | * **字体**: Inter字体族 + JetBrains Mono (代码显示)
43 |
44 | * **构建优化**: Vite插件生态 + 代码分割 + 懒加载
45 |
46 | ## 3. 路由定义
47 |
48 | | 路由 | 用途 |
49 | | ------------ | ----------------- |
50 | | / | 主展示页面,项目网格展示和搜索功能 |
51 | | /project/:id | 项目详情页面,深度展示单个项目信息 |
52 | | /about | 关于页面,个人品牌和技能展示 |
53 | | /search | 高级搜索页面,复杂筛选和排序功能 |
54 |
55 | ## 4. 组件架构设计
56 |
57 | ### 4.1 核心组件层次
58 |
59 | ```mermaid
60 | graph TD
61 | A[App 根组件] --> B[Layout 布局组件]
62 | B --> C[Header 顶部导航]
63 | B --> D[Main 主内容区]
64 | B --> E[Footer 底部信息]
65 |
66 | D --> F[ProjectGrid 项目网格]
67 | D --> G[SearchPanel 搜索面板]
68 | D --> H[FilterSidebar 筛选侧栏]
69 |
70 | F --> I[ProjectCard 项目卡片]
71 | I --> J[ProjectMeta 项目元信息]
72 | I --> K[TechStack 技术栈标签]
73 | I --> L[ProjectStats 项目统计]
74 |
75 | subgraph "原子组件"
76 | M[Button 按钮]
77 | N[Input 输入框]
78 | O[Tag 标签]
79 | P[Avatar 头像]
80 | Q[Icon 图标]
81 | end
82 | ```
83 |
84 | ### 4.2 状态管理架构
85 |
86 | ```typescript
87 | // 全局状态接口定义
88 | interface AppState {
89 | projects: Project[];
90 | searchTerm: string;
91 | selectedTags: string[];
92 | sortMethod: 'default' | 'stars' | 'updated' | 'name';
93 | viewMode: 'grid' | 'list' | 'masonry';
94 | theme: 'light' | 'dark' | 'auto';
95 | isLoading: boolean;
96 | error: string | null;
97 | }
98 |
99 | // 项目数据接口
100 | interface Project {
101 | id: string;
102 | full_name: string;
103 | description: string;
104 | html_url: string;
105 | homepage?: string;
106 | stargazers_count: number;
107 | language: string;
108 | topics: string[];
109 | updated_at: string;
110 | created_at: string;
111 | ai_tag?: {
112 | tags: string[];
113 | category: string;
114 | complexity: 'beginner' | 'intermediate' | 'advanced';
115 | };
116 | }
117 | ```
118 |
119 | ## 5. 样式系统架构
120 |
121 | ### 5.1 Tailwind配置扩展
122 |
123 | ```javascript
124 | // tailwind.config.js 核心配置
125 | module.exports = {
126 | content: ['./src/**/*.{js,jsx,ts,tsx}'],
127 | theme: {
128 | extend: {
129 | fontFamily: {
130 | 'sans': ['Inter', 'system-ui', 'sans-serif'],
131 | 'mono': ['JetBrains Mono', 'monospace'],
132 | },
133 | colors: {
134 | notion: {
135 | blue: '#2383E2',
136 | gray: {
137 | 50: '#F7F6F3',
138 | 100: '#E9E9E7',
139 | 200: '#CBCAC8',
140 | 300: '#A0A0A0',
141 | 400: '#6F6F6F',
142 | 500: '#2F2F2F',
143 | }
144 | }
145 | },
146 | animation: {
147 | 'fade-in': 'fadeIn 0.3s ease-out',
148 | 'slide-up': 'slideUp 0.3s ease-out',
149 | 'scale-in': 'scaleIn 0.2s ease-out',
150 | },
151 | boxShadow: {
152 | 'notion': '0 8px 32px rgba(0, 0, 0, 0.12)',
153 | 'notion-hover': '0 12px 48px rgba(0, 0, 0, 0.18)',
154 | }
155 | }
156 | }
157 | }
158 | ```
159 |
160 | ### 5.2 组件样式规范
161 |
162 | ```css
163 | /* 核心样式变量 */
164 | :root {
165 | --spacing-unit: 8px;
166 | --border-radius-sm: 8px;
167 | --border-radius-md: 12px;
168 | --border-radius-lg: 16px;
169 | --transition-fast: 0.15s ease-out;
170 | --transition-normal: 0.3s ease-out;
171 | --transition-slow: 0.5s ease-out;
172 | }
173 |
174 | /* 通用组件基础样式 */
175 | .notion-card {
176 | @apply bg-white rounded-lg shadow-notion hover:shadow-notion-hover;
177 | @apply transition-all duration-300 ease-out;
178 | @apply border border-gray-100;
179 | }
180 |
181 | .notion-button {
182 | @apply px-4 py-2 rounded-lg font-medium;
183 | @apply transition-all duration-200 ease-out;
184 | @apply focus:outline-none focus:ring-2 focus:ring-notion-blue focus:ring-opacity-50;
185 | }
186 |
187 | .notion-input {
188 | @apply w-full px-4 py-3 rounded-lg border border-gray-200;
189 | @apply focus:border-notion-blue focus:ring-2 focus:ring-notion-blue focus:ring-opacity-20;
190 | @apply transition-all duration-200 ease-out;
191 | }
192 | ```
193 |
194 | ## 6. 性能优化策略
195 |
196 | ### 6.1 代码分割和懒加载
197 |
198 | ```typescript
199 | // 路由级别的代码分割
200 | const ProjectDetail = lazy(() => import('./pages/ProjectDetail'));
201 | const About = lazy(() => import('./pages/About'));
202 | const Search = lazy(() => import('./pages/Search'));
203 |
204 | // 组件级别的懒加载
205 | const HeavyChart = lazy(() => import('./components/HeavyChart'));
206 | ```
207 |
208 | ### 6.2 图片和资源优化
209 |
210 | * 使用WebP格式图片,fallback到PNG
211 |
212 | * 实现图片懒加载和渐进式加载
213 |
214 | * SVG图标内联优化
215 |
216 | * 字体子集化,只加载需要的字符
217 |
218 | ### 6.3 搜索性能优化
219 |
220 | ```typescript
221 | // 搜索防抖和缓存策略
222 | const useOptimizedSearch = () => {
223 | const [searchResults, setSearchResults] = useState([]);
224 | const searchCache = useRef(new Map());
225 |
226 | const debouncedSearch = useMemo(
227 | () => debounce((term: string) => {
228 | if (searchCache.current.has(term)) {
229 | setSearchResults(searchCache.current.get(term));
230 | return;
231 | }
232 |
233 | const results = fuse.search(term);
234 | searchCache.current.set(term, results);
235 | setSearchResults(results);
236 | }, 300),
237 | []
238 | );
239 |
240 | return { searchResults, debouncedSearch };
241 | };
242 | ```
243 |
244 | ## 7. 部署和构建配置
245 |
246 | ### 7.1 Vite构建优化
247 |
248 | ```javascript
249 | // vite.config.js
250 | export default defineConfig({
251 | plugins: [react()],
252 | build: {
253 | rollupOptions: {
254 | output: {
255 | manualChunks: {
256 | vendor: ['react', 'react-dom'],
257 | animations: ['framer-motion'],
258 | search: ['fuse.js'],
259 | }
260 | }
261 | },
262 | chunkSizeWarningLimit: 1000,
263 | },
264 | optimizeDeps: {
265 | include: ['react', 'react-dom', 'framer-motion', 'fuse.js']
266 | }
267 | });
268 | ```
269 |
270 | ### 7.2 静态资源处理
271 |
272 | * 项目数据通过静态JSON文件提供
273 |
274 | * 支持增量更新和版本控制
275 |
276 | * CDN部署优化,全球加速访问
277 |
278 | * Gzip压缩和Brotli压缩支持
279 |
280 |
--------------------------------------------------------------------------------
/web/src/components/ui/SearchAndFilter.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useMemo } from 'react';
2 | import { motion, AnimatePresence } from 'framer-motion';
3 | import { Search, Filter, X, ChevronDown, SortAsc, SortDesc } from 'lucide-react';
4 | import Input from './Input';
5 | import Button from './Button';
6 |
7 | const SearchAndFilter = ({
8 | searchTerm,
9 | onSearchChange,
10 | selectedTags,
11 | onTagsChange,
12 | sortMethod,
13 | onSortChange,
14 | tagFrequency,
15 | totalProjects,
16 | filteredCount
17 | }) => {
18 | const [isFilterOpen, setIsFilterOpen] = useState(false);
19 | const [isSortOpen, setIsSortOpen] = useState(false);
20 |
21 | // Get top tags for display
22 | const topTags = useMemo(() => {
23 | return Object.entries(tagFrequency)
24 | .sort((a, b) => b[1] - a[1])
25 | .slice(0, 20)
26 | .map(([tag, count]) => ({ tag, count }));
27 | }, [tagFrequency]);
28 |
29 | const sortOptions = [
30 | { value: 'default', label: 'Default Order', icon: null },
31 | { value: 'stars', label: 'Most Stars', icon: SortDesc },
32 | { value: 'updated-time', label: 'Recently Updated', icon: SortAsc },
33 | ];
34 |
35 | const handleTagToggle = (tag) => {
36 | const newTags = selectedTags.includes(tag)
37 | ? selectedTags.filter(t => t !== tag)
38 | : [...selectedTags, tag];
39 | onTagsChange(newTags);
40 | };
41 |
42 | const clearAllFilters = () => {
43 | onSearchChange('');
44 | onTagsChange([]);
45 | onSortChange('default');
46 | };
47 |
48 | const hasActiveFilters = searchTerm || selectedTags.length > 0 || sortMethod !== 'default';
49 |
50 | return (
51 |
52 | {/* Search Bar */}
53 |
54 |
60 | onSearchChange(e.target.value)}
65 | icon={Search}
66 | />
67 | {searchTerm && (
68 | onSearchChange('')}
73 | className="absolute right-4 top-1/2 transform -translate-y-1/2 p-1 rounded-notion-sm hover:bg-notion-gray-100 text-notion-gray-400 hover:text-notion-gray-600 transition-colors duration-200"
74 | >
75 |
76 |
77 | )}
78 |
79 |
80 |
81 | {/* Filter and Sort Controls */}
82 |
88 |
89 | {/* Filter Button */}
90 |
91 | 0 ? 'primary' : 'secondary'}
93 | onClick={() => setIsFilterOpen(!isFilterOpen)}
94 | className="flex items-center space-x-2"
95 | >
96 |
97 | Filter
98 | {selectedTags.length > 0 && (
99 |
100 | {selectedTags.length}
101 |
102 | )}
103 |
107 |
108 |
109 |
110 |
111 |
112 | {/* Sort Button */}
113 |
114 |
setIsSortOpen(!isSortOpen)}
118 | className={`flex items-center space-x-2 px-4 py-2 rounded-notion-md border transition-all duration-200 ${
119 | isSortOpen || sortMethod !== 'default'
120 | ? 'bg-notion-blue text-white border-notion-blue'
121 | : 'bg-white text-notion-gray-600 border-notion-gray-200 hover:border-notion-gray-300'
122 | }`}
123 | >
124 | {sortOptions.find(opt => opt.value === sortMethod)?.icon && (
125 | React.createElement(sortOptions.find(opt => opt.value === sortMethod).icon, { className: 'w-4 h-4' })
126 | )}
127 |
128 | {sortOptions.find(opt => opt.value === sortMethod)?.label}
129 |
130 |
133 |
134 |
135 | {/* Sort Dropdown */}
136 |
137 | {isSortOpen && (
138 |
145 | {sortOptions.map((option) => (
146 | {
150 | onSortChange(option.value);
151 | setIsSortOpen(false);
152 | }}
153 | className={`w-full flex items-center space-x-3 px-4 py-3 text-left transition-colors duration-150 first:rounded-t-notion-md last:rounded-b-notion-md ${
154 | sortMethod === option.value
155 | ? 'bg-notion-blue text-white'
156 | : 'text-notion-gray-700 hover:bg-notion-gray-50'
157 | }`}
158 | >
159 | {option.icon && }
160 | {option.label}
161 |
162 | ))}
163 |
164 | )}
165 |
166 |
167 |
168 | {/* Clear Filters */}
169 | {hasActiveFilters && (
170 |
179 |
180 | Clear all
181 |
182 | )}
183 |
184 |
185 | {/* Results Count */}
186 |
187 | Showing {filteredCount} of{' '}
188 | {totalProjects} projects
189 |
190 |
191 |
192 | {/* Filter Tags Panel */}
193 |
194 | {isFilterOpen && (
195 |
202 |
203 | Filter by Technology
204 |
205 |
206 | {topTags.map(({ tag, count }) => (
207 | handleTagToggle(tag)}
212 | className="flex items-center space-x-1 sm:space-x-2 justify-start text-xs sm:text-sm"
213 | >
214 | {tag}
215 |
220 | {count}
221 |
222 |
223 | ))}
224 |
225 |
226 | )}
227 |
228 |
229 | {/* Active Filters Display */}
230 | {selectedTags.length > 0 && (
231 |
236 | Active filters:
237 | {selectedTags.map((tag) => (
238 |
245 | {tag}
246 | handleTagToggle(tag)}
248 | className="hover:bg-white/20 rounded-full p-0.5 transition-colors duration-150"
249 | >
250 |
251 |
252 |
253 | ))}
254 |
255 | )}
256 |
257 | );
258 | };
259 |
260 | export default SearchAndFilter;
--------------------------------------------------------------------------------
/web/src/pages/HomePage.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useMemo } from 'react';
2 | import { motion, AnimatePresence } from 'framer-motion';
3 | import Fuse from 'fuse.js';
4 | import Layout from '../components/layout/Layout';
5 | import ProjectCard from '../components/project/ProjectCard';
6 | import SearchAndFilter from '../components/ui/SearchAndFilter';
7 | import Loading from '../components/ui/Loading';
8 | import Card from '../components/ui/Card';
9 | import { Github, Sparkles } from 'lucide-react';
10 |
11 | const HomePage = () => {
12 | const [projects, setProjects] = useState([]);
13 | const [isLoading, setIsLoading] = useState(true);
14 | const [searchTerm, setSearchTerm] = useState('');
15 | const [selectedTags, setSelectedTags] = useState([]);
16 | const [sortMethod, setSortMethod] = useState('default');
17 |
18 | // Load JSON data
19 | useEffect(() => {
20 | const loadProjects = async () => {
21 | try {
22 | const response = await fetch('/projects.json');
23 | const data = await response.json();
24 | setProjects(data);
25 | setIsLoading(false);
26 | } catch (error) {
27 | console.error('Error loading projects:', error);
28 | setIsLoading(false);
29 | }
30 | };
31 |
32 | loadProjects();
33 | }, []);
34 |
35 | // Calculate tag frequencies
36 | const tagFrequency = useMemo(() => {
37 | const frequencies = {};
38 | projects.forEach(project => {
39 | const projectTags = [
40 | ...(project.topics || []),
41 | ...(project.ai_tag?.tags || []),
42 | project.language
43 | ].filter(Boolean);
44 |
45 | projectTags.forEach(tag => {
46 | frequencies[tag] = (frequencies[tag] || 0) + 1;
47 | });
48 | });
49 | return frequencies;
50 | }, [projects]);
51 |
52 | // Fuzzy search setup
53 | const fuse = useMemo(() => {
54 | const options = {
55 | keys: ['full_name', 'name', 'description', 'ai_tag.tags', 'topics', 'language'],
56 | threshold: 0.3,
57 | includeScore: true,
58 | };
59 | return new Fuse(projects, options);
60 | }, [projects]);
61 |
62 | // Filtered and sorted projects
63 | const filteredProjects = useMemo(() => {
64 | let result = projects;
65 |
66 | // Apply fuzzy search if there's a search term
67 | if (searchTerm.trim()) {
68 | result = fuse.search(searchTerm.trim()).map(r => r.item);
69 | }
70 |
71 | // Apply tag filtering
72 | if (selectedTags.length > 0) {
73 | result = result.filter(project => {
74 | const projectTags = [
75 | ...(project.topics || []),
76 | ...(project.ai_tag?.tags || []),
77 | project.language
78 | ].filter(Boolean);
79 |
80 | return selectedTags.every(tag => projectTags.includes(tag));
81 | });
82 | }
83 |
84 | // Apply sorting
85 | if (sortMethod === 'stars') {
86 | result = [...result].sort((a, b) =>
87 | (b.stargazers_count || 0) - (a.stargazers_count || 0)
88 | );
89 | } else if (sortMethod === 'updated-time') {
90 | result = [...result].sort((a, b) =>
91 | new Date(b.updated_at) - new Date(a.updated_at)
92 | );
93 | }
94 |
95 | return result;
96 | }, [projects, searchTerm, selectedTags, sortMethod, fuse]);
97 |
98 | // Loading state
99 | if (isLoading) {
100 | return (
101 |
102 |
103 |
104 |
105 |
106 | );
107 | }
108 |
109 | return (
110 |
111 | {/* GitHub Star Button - Fixed Position */}
112 |
123 |
124 | Star on GitHub
125 |
130 |
131 |
132 |
133 | {/* Hero Section */}
134 |
140 |
146 |
147 | Curated Project Collection
148 |
149 |
150 |
156 | My GitHub
157 |
158 | Project Showcase
159 |
160 |
161 |
167 | A carefully curated collection of my development projects, showcasing innovation,
168 |
169 | craftsmanship, and attention to detail in every line of code.
170 |
171 |
172 |
173 | {/* Search and Filter */}
174 |
180 |
191 |
192 |
193 | {/* Projects Grid */}
194 |
199 | {filteredProjects.length > 0 ? (
200 |
201 | {filteredProjects.map((project, index) => (
202 |
207 | ))}
208 |
209 | ) : (
210 |
216 |
217 |
218 |
219 |
220 | No projects found
221 |
222 |
223 | Try adjusting your search terms or filters to find what you're looking for.
224 |
225 | {
229 | setSearchTerm('');
230 | setSelectedTags([]);
231 | setSortMethod('default');
232 | }}
233 | className="bg-notion-blue text-white px-6 py-3 rounded-notion-md font-medium hover:bg-notion-blue/90 transition-colors duration-200"
234 | >
235 | Clear all filters
236 |
237 |
238 | )}
239 |
240 |
241 | {/* Stats Section */}
242 | {filteredProjects.length > 0 && (
243 |
249 |
250 |
251 |
257 | {projects.length}
258 |
259 |
260 | Total Projects
261 |
262 |
263 |
264 |
270 | {Object.keys(tagFrequency).length}
271 |
272 |
273 | Technologies
274 |
275 |
276 |
277 |
283 | {projects.reduce((sum, p) => sum + (p.stargazers_count || 0), 0)}
284 |
285 |
286 | Total Stars
287 |
288 |
289 |
290 |
291 | )}
292 |
293 |
294 | );
295 | };
296 |
297 | export default HomePage;
--------------------------------------------------------------------------------