diff --git a/app/projects/page.tsx b/app/projects/page.tsx index df05305..465137e 100644 --- a/app/projects/page.tsx +++ b/app/projects/page.tsx @@ -1,6 +1,7 @@ -import Link from 'next/link'; +import { FaGithub } from 'react-icons/fa'; import { fetchPublicRepos } from '@/lib/github'; import { SidebarLayout } from '@/components/sidebar-layout'; +import { RepoCard } from '@/components/repo-card'; export const revalidate = 3600; @@ -20,53 +21,27 @@ export default async function ProjectsPage() {

從我的 GitHub 帳號自動抓取公開的程式庫與專案。 + {repos.length > 0 && ( + 共 {repos.length} 個專案 + )}

{repos.length === 0 ? ( -

- 目前沒有可顯示的 GitHub 專案,或暫時無法連線到 GitHub。 -

+
+ +

+ 目前沒有可顯示的 GitHub 專案,或暫時無法連線到 GitHub。 +

+
) : ( )} diff --git a/components/repo-card.tsx b/components/repo-card.tsx new file mode 100644 index 0000000..7e57369 --- /dev/null +++ b/components/repo-card.tsx @@ -0,0 +1,64 @@ +import Link from 'next/link'; +import { FiExternalLink } from 'react-icons/fi'; +import type { RepoSummary } from '@/lib/github'; +import { getLanguageColor } from '@/lib/github-lang-colors'; + +interface RepoCardProps { + repo: RepoSummary; + animationDelay?: number; +} + +export function RepoCard({ repo, animationDelay = 0 }: RepoCardProps) { + const langColor = getLanguageColor(repo.language); + + return ( +
  • 0 ? 'repo-card-enter' : ''}`} + style={ + animationDelay > 0 ? { animationDelay: `${animationDelay}ms` } : undefined + } + > +
    +
    + + {repo.name} + + + {repo.stargazersCount > 0 && ( + + ★ {repo.stargazersCount} + + )} +
    + + {repo.description && ( +

    + {repo.description} +

    + )} + +
    + + + {repo.language ?? '其他'} + + + 更新於{' '} + {repo.updatedAt + ? new Date(repo.updatedAt).toLocaleDateString('zh-TW') + : '未知'} + +
    +
  • + ); +} diff --git a/lib/github-lang-colors.ts b/lib/github-lang-colors.ts new file mode 100644 index 0000000..07e9f1a --- /dev/null +++ b/lib/github-lang-colors.ts @@ -0,0 +1,43 @@ +/** + * GitHub-style language colors for repo cards. + * Fallback: #94a3b8 (slate-400) for unknown languages. + */ +const LANG_COLORS: Record = { + TypeScript: '#3178c6', + JavaScript: '#f1e05a', + Python: '#3572A5', + Rust: '#dea584', + Go: '#00ADD8', + Ruby: '#701516', + PHP: '#4F5D95', + Java: '#b07219', + Kotlin: '#A97BFF', + Swift: '#F05138', + C: '#555555', + 'C++': '#f34b7d', + 'C#': '#239120', + Shell: '#89e051', + HTML: '#e34c26', + CSS: '#563d7c', + Vue: '#41b883', + Svelte: '#ff3e00', + Dart: '#00B4AB', + Scala: '#c22d40', + Elixir: '#6e4a7e', + Lua: '#000080', + R: '#198CE7', + Markdown: '#083fa1', + YAML: '#cb171e', + JSON: '#292929', +}; + +const FALLBACK_COLOR = '#94a3b8'; + +/** + * Returns the GitHub-style hex color for a programming language. + * Unknown languages use a neutral slate fallback. + */ +export function getLanguageColor(lang: string | null): string { + if (!lang || !lang.trim()) return FALLBACK_COLOR; + return LANG_COLORS[lang] ?? FALLBACK_COLOR; +} diff --git a/lib/github.ts b/lib/github.ts index 6d09450..79eee76 100644 --- a/lib/github.ts +++ b/lib/github.ts @@ -27,8 +27,8 @@ function getGithubHeaders() { /** * Fetch all public repositories for the configured GitHub user. - * Returns an empty array on error instead of throwing, so the UI - * can render a graceful fallback. + * Excludes forked repositories. Returns an empty array on error instead of + * throwing, so the UI can render a graceful fallback. */ export async function fetchPublicRepos(usernameOverride?: string): Promise { const username = usernameOverride || process.env.GITHUB_USERNAME; @@ -56,16 +56,18 @@ export async function fetchPublicRepos(usernameOverride?: string): Promise ({ - id: repo.id, - name: repo.name, - fullName: repo.full_name, - htmlUrl: repo.html_url, - description: repo.description, - language: repo.language, - stargazersCount: repo.stargazers_count, - updatedAt: repo.updated_at, - })); + return data + .filter((repo) => !repo.fork) + .map((repo) => ({ + id: repo.id, + name: repo.name, + fullName: repo.full_name, + htmlUrl: repo.html_url, + description: repo.description, + language: repo.language, + stargazersCount: repo.stargazers_count, + updatedAt: repo.updated_at, + })); } catch (error) { console.error('Error while fetching GitHub repositories:', error); return []; diff --git a/styles/globals.css b/styles/globals.css index 63e22d1..b03e12f 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -40,6 +40,16 @@ 100% { opacity: 1; transform: translateY(0) scale(1); } } +.repo-card-enter { + animation: fade-in-up 0.6s ease-out both; +} + +@media (prefers-reduced-motion: reduce) { + .repo-card-enter { + animation: none; + } +} + @keyframes float-soft { 0% { transform: translate3d(0,0,0) scale(1); } 50% { transform: translate3d(4px,-6px,0) scale(1.03); }