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。
+
+
) : (
- {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')
- : '未知'}
-
-
-
+ repo={repo}
+ animationDelay={index * 50}
+ />
))}
)}
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); }