From 23efeca2d1e18e77ec76dc4c4872b6ad0fc47037 Mon Sep 17 00:00:00 2001 From: AdityaM-IITH Date: Sun, 17 May 2026 23:52:46 +0530 Subject: [PATCH 1/2] feat: implement premium leaderboard component and data layer (Closes #259) --- package-lock.json | 10 + package.json | 1 + src/app/leaderboard/page.tsx | 5 + src/components/LeaderboardPage.tsx | 310 +++++++++++++++++++++++++++++ src/lib/supabase.ts | 12 +- 5 files changed, 337 insertions(+), 1 deletion(-) create mode 100644 src/app/leaderboard/page.tsx create mode 100644 src/components/LeaderboardPage.tsx diff --git a/package-lock.json b/package-lock.json index 467c7810..6453748a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "date-fns": "^3.6.0", "jspdf": "^4.2.1", "jspdf-autotable": "^5.0.7", + "lucide-react": "^1.16.0", "next": "^14.2.35", "next-auth": "^4.24.7", "react": "^18", @@ -4345,6 +4346,15 @@ "node": ">=10" } }, + "node_modules/lucide-react": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.16.0.tgz", + "integrity": "sha512-dYwyPzb4MEKpGUmNYk3WKWPnMrHs3FKM+q94kAnJrcDIqqn1hq2xY8scaS2ovsOCM5D51ey2gaRG3PBb1vgoYQ==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", diff --git a/package.json b/package.json index 807987b6..95e4a7cf 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "date-fns": "^3.6.0", "jspdf": "^4.2.1", "jspdf-autotable": "^5.0.7", + "lucide-react": "^1.16.0", "next": "^14.2.35", "next-auth": "^4.24.7", "react": "^18", diff --git a/src/app/leaderboard/page.tsx b/src/app/leaderboard/page.tsx new file mode 100644 index 00000000..781c29bc --- /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 00000000..968e90c3 --- /dev/null +++ b/src/components/LeaderboardPage.tsx @@ -0,0 +1,310 @@ +"use client"; + +import React, { useState, useEffect } from "react"; +import { supabase } from "@/lib/supabase"; +import { Trophy, Flame, GitCommit, AlertCircle, Loader2, Award } from "lucide-react"; + +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() { + const mockContributors: Contributor[] = [ + { + id: "1", + username: "AdityaM-IITH", + avatar_url: "https://images.unsplash.com/photo-1534528741775-53994a69daeb?auto=format&fit=crop&w=150&q=80", + streak_count: 42, + total_commits: 156, + rank: 1, + }, + { + id: "2", + username: "alex_dev", + avatar_url: "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?auto=format&fit=crop&w=150&q=80", + streak_count: 28, + total_commits: 98, + rank: 2, + }, + { + id: "3", + username: "sofia_codes", + avatar_url: "https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&w=150&q=80", + streak_count: 24, + total_commits: 84, + rank: 3, + }, + { + id: "4", + username: "dev_wizard", + avatar_url: "https://images.unsplash.com/photo-1500648767791-00dcc994a43e?auto=format&fit=crop&w=150&q=80", + streak_count: 19, + total_commits: 73, + rank: 4, + }, + { + id: "5", + username: "git_guru", + avatar_url: "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?auto=format&fit=crop&w=150&q=80", + streak_count: 15, + total_commits: 62, + rank: 5, + }, + { + id: "6", + username: "pixel_pioneer", + avatar_url: "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?auto=format&fit=crop&w=150&q=80", + streak_count: 12, + total_commits: 51, + rank: 6, + }, + { + id: "7", + username: "clara_t", + avatar_url: "https://images.unsplash.com/photo-1517841905240-472988babdf9?auto=format&fit=crop&w=150&q=80", + streak_count: 10, + total_commits: 44, + rank: 7, + }, + { + id: "8", + username: "dan_builds", + avatar_url: "https://images.unsplash.com/photo-1539571696357-5a69c17a67c6?auto=format&fit=crop&w=150&q=80", + streak_count: 9, + total_commits: 39, + rank: 8, + }, + { + id: "9", + username: "elena_k", + avatar_url: "https://images.unsplash.com/photo-1524504388940-b1c1722653e1?auto=format&fit=crop&w=150&q=80", + streak_count: 8, + total_commits: 35, + rank: 9, + }, + { + id: "10", + username: "sam_smith", + avatar_url: "https://images.unsplash.com/photo-1506794778202-cad84cf45f1d?auto=format&fit=crop&w=150&q=80", + streak_count: 7, + total_commits: 31, + rank: 10, + } + ]; + + try { + setLoading(true); + setError(null); + + if (!process.env.NEXT_PUBLIC_SUPABASE_URL || !process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || !supabase) { + setContributors(mockContributors); + return; + } + + const { data, error: fetchError } = await supabase + .from("contributors") + .select("id, username, avatar_url, streak_count, total_commits") + .order("streak_count", { ascending: false }) + .order("total_commits", { ascending: false }) + .limit(50); + + if (fetchError) throw fetchError; + + if (!data || data.length === 0) { + setContributors(mockContributors); + return; + } + + const rankedData: Contributor[] = data.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: index + 1, + })); + + setContributors(rankedData); + } catch (err: unknown) { + console.warn("Supabase fetch failed, falling back to mock leaderboard data:", err); + setContributors(mockContributors); + } 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. +
+ )} +
+
+ ); +} diff --git a/src/lib/supabase.ts b/src/lib/supabase.ts index b7cbfbec..bdcc64e9 100644 --- a/src/lib/supabase.ts +++ b/src/lib/supabase.ts @@ -2,10 +2,18 @@ import { createClient } from "@supabase/supabase-js"; const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!; const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY!; +const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!; // Server-side only — use in API routes, never import in client components. // Service role bypasses RLS; auth is enforced by getServerSession checks. -export const supabaseAdmin = createClient(supabaseUrl, serviceRoleKey); +export const supabaseAdmin = supabaseUrl && serviceRoleKey + ? createClient(supabaseUrl, serviceRoleKey) + : null as any; + +// Client-side supabase instance for browser operations +export const supabase = supabaseUrl && supabaseAnonKey + ? createClient(supabaseUrl, supabaseAnonKey) + : null as any; interface User { id: string; @@ -21,6 +29,7 @@ interface User { * Returns the user row if found and is_public is true, otherwise null. */ export async function getUserByUsername(username: string): Promise { + if (!supabaseAdmin) return null; const { data, error } = await supabaseAdmin .from("users") .select("id,github_id,github_login,is_public,created_at,updated_at") @@ -47,6 +56,7 @@ export async function updateUserPublicFlag( userId: string, isPublic: boolean ): Promise { + if (!supabaseAdmin) return null; const { data, error } = await supabaseAdmin .from("users") .update({ is_public: isPublic }) From 029fe8a08c03477035bf2ce63f58dede537dbb10 Mon Sep 17 00:00:00 2001 From: AdityaM-IITH Date: Tue, 19 May 2026 13:04:34 +0530 Subject: [PATCH 2/2] fix: resolve TS errors, remove lucide-react, and rewire data layer to /api/leaderboard --- package-lock.json | 10 -- package.json | 1 - src/app/api/goals/route.ts | 14 ++- src/app/api/user/github-accounts/route.ts | 9 +- src/components/LeaderboardPage.tsx | 130 ++++------------------ src/lib/supabase.ts | 12 +- 6 files changed, 43 insertions(+), 133 deletions(-) diff --git a/package-lock.json b/package-lock.json index b5296d1a..56dea05c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,6 @@ "html-to-image": "^1.11.13", "jspdf": "^4.2.1", "jspdf-autotable": "^5.0.7", - "lucide-react": "^1.16.0", "next": "^14.2.35", "next-auth": "^4.24.7", "react": "^18", @@ -4346,15 +4345,6 @@ "node": ">=10" } }, - "node_modules/lucide-react": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.16.0.tgz", - "integrity": "sha512-dYwyPzb4MEKpGUmNYk3WKWPnMrHs3FKM+q94kAnJrcDIqqn1hq2xY8scaS2ovsOCM5D51ey2gaRG3PBb1vgoYQ==", - "license": "ISC", - "peerDependencies": { - "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", diff --git a/package.json b/package.json index 7f659e10..af35b0c1 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,6 @@ "html-to-image": "^1.11.13", "jspdf": "^4.2.1", "jspdf-autotable": "^5.0.7", - "lucide-react": "^1.16.0", "next": "^14.2.35", "next-auth": "^4.24.7", "react": "^18", diff --git a/src/app/api/goals/route.ts b/src/app/api/goals/route.ts index 592a64da..628b1253 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 00779237..f339ebee 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/components/LeaderboardPage.tsx b/src/components/LeaderboardPage.tsx index 968e90c3..49e1bf17 100644 --- a/src/components/LeaderboardPage.tsx +++ b/src/components/LeaderboardPage.tsx @@ -1,8 +1,18 @@ "use client"; import React, { useState, useEffect } from "react"; -import { supabase } from "@/lib/supabase"; -import { Trophy, Flame, GitCommit, AlertCircle, Loader2, Award } from "lucide-react"; + +const Trophy = ({ className }: { className?: string }) => ( + +); + +const Loader2 = ({ className }: { className?: string }) => ( + +); + +const AlertCircle = ({ className }: { className?: string }) => ( + +); export interface Contributor { id: string; @@ -20,125 +30,27 @@ export function useLeaderboard() { useEffect(() => { async function fetchLeaderboard() { - const mockContributors: Contributor[] = [ - { - id: "1", - username: "AdityaM-IITH", - avatar_url: "https://images.unsplash.com/photo-1534528741775-53994a69daeb?auto=format&fit=crop&w=150&q=80", - streak_count: 42, - total_commits: 156, - rank: 1, - }, - { - id: "2", - username: "alex_dev", - avatar_url: "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?auto=format&fit=crop&w=150&q=80", - streak_count: 28, - total_commits: 98, - rank: 2, - }, - { - id: "3", - username: "sofia_codes", - avatar_url: "https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&w=150&q=80", - streak_count: 24, - total_commits: 84, - rank: 3, - }, - { - id: "4", - username: "dev_wizard", - avatar_url: "https://images.unsplash.com/photo-1500648767791-00dcc994a43e?auto=format&fit=crop&w=150&q=80", - streak_count: 19, - total_commits: 73, - rank: 4, - }, - { - id: "5", - username: "git_guru", - avatar_url: "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?auto=format&fit=crop&w=150&q=80", - streak_count: 15, - total_commits: 62, - rank: 5, - }, - { - id: "6", - username: "pixel_pioneer", - avatar_url: "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?auto=format&fit=crop&w=150&q=80", - streak_count: 12, - total_commits: 51, - rank: 6, - }, - { - id: "7", - username: "clara_t", - avatar_url: "https://images.unsplash.com/photo-1517841905240-472988babdf9?auto=format&fit=crop&w=150&q=80", - streak_count: 10, - total_commits: 44, - rank: 7, - }, - { - id: "8", - username: "dan_builds", - avatar_url: "https://images.unsplash.com/photo-1539571696357-5a69c17a67c6?auto=format&fit=crop&w=150&q=80", - streak_count: 9, - total_commits: 39, - rank: 8, - }, - { - id: "9", - username: "elena_k", - avatar_url: "https://images.unsplash.com/photo-1524504388940-b1c1722653e1?auto=format&fit=crop&w=150&q=80", - streak_count: 8, - total_commits: 35, - rank: 9, - }, - { - id: "10", - username: "sam_smith", - avatar_url: "https://images.unsplash.com/photo-1506794778202-cad84cf45f1d?auto=format&fit=crop&w=150&q=80", - streak_count: 7, - total_commits: 31, - rank: 10, - } - ]; - try { setLoading(true); setError(null); - - if (!process.env.NEXT_PUBLIC_SUPABASE_URL || !process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || !supabase) { - setContributors(mockContributors); - return; - } - - const { data, error: fetchError } = await supabase - .from("contributors") - .select("id, username, avatar_url, streak_count, total_commits") - .order("streak_count", { ascending: false }) - .order("total_commits", { ascending: false }) - .limit(50); - - if (fetchError) throw fetchError; - - if (!data || data.length === 0) { - setContributors(mockContributors); - return; - } - - const rankedData: Contributor[] = data.map((user: any, index: number) => ({ + + 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: index + 1, + rank: user.rank || index + 1, })); setContributors(rankedData); } catch (err: unknown) { - console.warn("Supabase fetch failed, falling back to mock leaderboard data:", err); - setContributors(mockContributors); + console.error("Leaderboard fetch error:", err); + setError("Failed to fetch leaderboard data"); } finally { setLoading(false); } diff --git a/src/lib/supabase.ts b/src/lib/supabase.ts index bdcc64e9..b7cbfbec 100644 --- a/src/lib/supabase.ts +++ b/src/lib/supabase.ts @@ -2,18 +2,10 @@ import { createClient } from "@supabase/supabase-js"; const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!; const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY!; -const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!; // Server-side only — use in API routes, never import in client components. // Service role bypasses RLS; auth is enforced by getServerSession checks. -export const supabaseAdmin = supabaseUrl && serviceRoleKey - ? createClient(supabaseUrl, serviceRoleKey) - : null as any; - -// Client-side supabase instance for browser operations -export const supabase = supabaseUrl && supabaseAnonKey - ? createClient(supabaseUrl, supabaseAnonKey) - : null as any; +export const supabaseAdmin = createClient(supabaseUrl, serviceRoleKey); interface User { id: string; @@ -29,7 +21,6 @@ interface User { * Returns the user row if found and is_public is true, otherwise null. */ export async function getUserByUsername(username: string): Promise { - if (!supabaseAdmin) return null; const { data, error } = await supabaseAdmin .from("users") .select("id,github_id,github_login,is_public,created_at,updated_at") @@ -56,7 +47,6 @@ export async function updateUserPublicFlag( userId: string, isPublic: boolean ): Promise { - if (!supabaseAdmin) return null; const { data, error } = await supabaseAdmin .from("users") .update({ is_public: isPublic })