From fde17c23082179fc9a8c80c003121bceffca4d4a Mon Sep 17 00:00:00 2001 From: gbanyan Date: Fri, 13 Feb 2026 16:59:56 +0800 Subject: [PATCH] feat: add GitHub projects page Co-authored-by: Cursor --- .env.local.example | 6 +++ app/projects/page.tsx | 77 ++++++++++++++++++++++++++++++++++++++ components/site-header.tsx | 1 + lib/github.ts | 74 ++++++++++++++++++++++++++++++++++++ 4 files changed, 158 insertions(+) create mode 100644 app/projects/page.tsx create mode 100644 lib/github.ts diff --git a/.env.local.example b/.env.local.example index 0c8e741..8a4260b 100644 --- a/.env.local.example +++ b/.env.local.example @@ -33,3 +33,9 @@ NEXT_PUBLIC_TWITTER_CARD_TYPE="summary_large_image" # Analytics (public ID only) NEXT_PUBLIC_ANALYTICS_ID="" + +# Server-side only (NOT exposed to browser) +# Used to fetch GitHub repositories for the /projects page. +# Copy these into your local `.env.local` and fill in real values. +GITHUB_USERNAME="your-github-username" +GITHUB_TOKEN="your-github-token" diff --git a/app/projects/page.tsx b/app/projects/page.tsx new file mode 100644 index 0000000..df05305 --- /dev/null +++ b/app/projects/page.tsx @@ -0,0 +1,77 @@ +import Link from 'next/link'; +import { fetchPublicRepos } from '@/lib/github'; +import { SidebarLayout } from '@/components/sidebar-layout'; + +export const revalidate = 3600; + +export const metadata = { + title: 'GitHub 專案', +}; + +export default async function ProjectsPage() { + const repos = await fetchPublicRepos(); + + return ( +
+ +
+

+ GitHub 專案 +

+

+ 從我的 GitHub 帳號自動抓取公開的程式庫與專案。 +

+
+ + {repos.length === 0 ? ( +

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

+ ) : ( +
    + {repos.map((repo) => ( +
  • +
    + + {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/components/site-header.tsx b/components/site-header.tsx index fb72f59..6995adf 100644 --- a/components/site-header.tsx +++ b/components/site-header.tsx @@ -57,6 +57,7 @@ export function SiteHeader() { const navItems: NavLinkItem[] = [ { key: 'home', href: '/', label: '首頁', iconKey: 'home' }, + { key: 'projects', href: '/projects', label: '作品', iconKey: 'pen' }, { key: 'about', href: aboutChildren[0]?.href, diff --git a/lib/github.ts b/lib/github.ts new file mode 100644 index 0000000..6d09450 --- /dev/null +++ b/lib/github.ts @@ -0,0 +1,74 @@ +export type RepoSummary = { + id: number; + name: string; + fullName: string; + htmlUrl: string; + description: string | null; + language: string | null; + stargazersCount: number; + updatedAt: string; +}; + +const GITHUB_API_BASE = 'https://api.github.com'; + +function getGithubHeaders() { + const headers: Record = { + Accept: 'application/vnd.github+json', + 'User-Agent': 'blog-nextjs-app', + }; + + const token = process.env.GITHUB_TOKEN; + if (token && token.trim() !== '') { + headers.Authorization = `Bearer ${token}`; + } + + return headers; +} + +/** + * 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. + */ +export async function fetchPublicRepos(usernameOverride?: string): Promise { + const username = usernameOverride || process.env.GITHUB_USERNAME; + + if (!username) { + console.error('GITHUB_USERNAME is not set; cannot fetch GitHub repositories.'); + return []; + } + + const url = `${GITHUB_API_BASE}/users/${encodeURIComponent( + username + )}/repos?type=public&sort=updated`; + + try { + const res = await fetch(url, { + headers: getGithubHeaders(), + // Use Next.js App Router caching / ISR + next: { revalidate: 3600 }, + }); + + if (!res.ok) { + console.error('Failed to fetch GitHub repositories:', res.status, res.statusText); + return []; + } + + const data = (await res.json()) as any[]; + + return data.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 []; + } +} +