diff --git a/src/app/api/goals/route.ts b/src/app/api/goals/route.ts index 592a64d..628b125 100644 --- a/src/app/api/goals/route.ts +++ b/src/app/api/goals/route.ts @@ -4,6 +4,18 @@ import { supabaseAdmin } from "@/lib/supabase"; export const dynamic = "force-dynamic"; +interface Goal { + id: string; + user_id: string; + title: string; + target: number; + current: number; + unit: string; + recurrence: string; + period_start: string | null; + created_at: string; +} + type Recurrence = "none" | "weekly" | "monthly"; function getPeriodStart(recurrence: Recurrence): string { @@ -50,7 +62,7 @@ export async function GET() { // Reset progress if we're in a new period const processedGoals = await Promise.all( - (goals ?? []).map(async (goal) => { + (goals ?? []).map(async (goal: Goal) => { if (goal.recurrence === "none") return goal; const periodStart = new Date(getPeriodStart(goal.recurrence as Recurrence)); diff --git a/src/app/api/user/github-accounts/route.ts b/src/app/api/user/github-accounts/route.ts index 0077923..f339ebe 100644 --- a/src/app/api/user/github-accounts/route.ts +++ b/src/app/api/user/github-accounts/route.ts @@ -5,6 +5,13 @@ import { supabaseAdmin } from "@/lib/supabase"; export const dynamic = "force-dynamic"; +interface LinkedAccount { + id: string; + github_id: string; + github_login: string; + added_at: string; +} + export async function GET() { const session = await getServerSession(authOptions); @@ -36,7 +43,7 @@ export async function GET() { } return NextResponse.json({ - accounts: (accounts ?? []).map((account) => ({ + accounts: (accounts ?? []).map((account: LinkedAccount) => ({ id: account.id, githubId: account.github_id, githubLogin: account.github_login, diff --git a/src/app/leaderboard/page.tsx b/src/app/leaderboard/page.tsx new file mode 100644 index 0000000..781c29b --- /dev/null +++ b/src/app/leaderboard/page.tsx @@ -0,0 +1,5 @@ +import LeaderboardPage from "@/components/LeaderboardPage"; + +export default function Page() { + return ; +} diff --git a/src/components/LeaderboardPage.tsx b/src/components/LeaderboardPage.tsx new file mode 100644 index 0000000..49e1bf1 --- /dev/null +++ b/src/components/LeaderboardPage.tsx @@ -0,0 +1,222 @@ +"use client"; + +import React, { useState, useEffect } from "react"; + +const Trophy = ({ className }: { className?: string }) => ( + +); + +const Loader2 = ({ className }: { className?: string }) => ( + +); + +const AlertCircle = ({ className }: { className?: string }) => ( + +); + +export interface Contributor { + id: string; + username: string; + avatar_url: string; + streak_count: number; + total_commits: number; + rank: number; +} + +export function useLeaderboard() { + const [contributors, setContributors] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + async function fetchLeaderboard() { + try { + setLoading(true); + setError(null); + + const response = await fetch('/api/leaderboard'); + if (!response.ok) throw new Error('Failed to fetch leaderboard data'); + + const data = await response.json(); + const rankedData: Contributor[] = (data.contributors || []).map((user: any, index: number) => ({ + id: user.id, + username: user.username, + avatar_url: user.avatar_url, + streak_count: user.streak_count, + total_commits: user.total_commits, + rank: user.rank || index + 1, + })); + + setContributors(rankedData); + } catch (err: unknown) { + console.error("Leaderboard fetch error:", err); + setError("Failed to fetch leaderboard data"); + } finally { + setLoading(false); + } + } + + fetchLeaderboard(); + }, []); + + return { contributors, loading, error }; +} + +export default function LeaderboardPage() { + const { contributors, loading, error } = useLeaderboard(); + + // Helper arrays for explicit desktop spatial layout assignment [Rank 2, Rank 1, Rank 3] + const topThree = contributors.slice(0, 3); + const podiumOrder = topThree.length === 3 ? [topThree[1], topThree[0], topThree[2]] : topThree; + + return ( +
+
+ + {/* Header Block */} +
+
+ METRICS-ENGINE +
+

+ System Core Contributors +

+

+ Real-time algorithmic indexing of project repository contributors based on consecutive daily streaks and absolute commit volume. +

+
+ + {/* Sync Status Overlay */} + {loading && ( +
+ + Parsing repository ledger indices... +
+ )} + + {error && ( +
+ + {error} +
+ )} + + {/* Structural Interface Elements */} + {!loading && !error && contributors.length > 0 && ( +
+ + {/* Asymmetric Desktop Podium Grid Layout */} +
+ {podiumOrder.map((user) => { + const isFirst = user.rank === 1; + return ( +
+ {/* Architectural Counter Badge */} +
+ [{user.rank.toString().padStart(2, "0")}] +
+ +
+
+ + {isFirst && ( +
+ Leader +
+ )} +
+ +
+

+ {user.username} +

+
+ + {/* Monospace Metric Matrix Block */} +
+
+ Streak + {user.streak_count}d +
+
+ Commits + {user.total_commits} +
+
+
+
+ ); + })} +
+ + {/* Industrial Ledger List View for Remaining Contributor Entries */} + {contributors.length > 3 && ( +
+
+ + + + + + + + + + + {contributors.slice(3).map((user) => ( + + + + + + + ))} + +
IndexDeveloper EntityStreak MetricsCommit Volume
+ {user.rank.toString().padStart(2, "0")} + +
+ + + {user.username} + +
+
+ {user.streak_count}d + + {user.total_commits} +
+
+
+ )} +
+ )} + + {/* Empty State Vector Block */} + {!loading && !error && contributors.length === 0 && ( +
+ Matrix structural index register is currently empty. +
+ )} +
+
+ ); +}