@@ -159,195 +51,36 @@ export default async function DashboardPage() {
-
+
+
{/* Stats Row */}
}>
-
- {/* Level Progress */}
-
-
- LEVEL PROGRESS
-
-
-
- L{level}
-
-
-
-
- {xp.toLocaleString()} / {(xp + needed).toLocaleString()} XP TO L{nextLevel}
-
-
-
-
-
- {/* Total Merges */}
-
-
- TOTAL MERGES
-
-
-
- {(merges ?? 0).toString().padStart(2, '0')}
-
-
-
-
-
- {/* Mentor Points */}
-
-
- MENTOR POINTS
-
-
-
- {mentorPoints.toLocaleString()}
-
-
-
-
-
- {/* Current Streak */}
-
-
- CURRENT STREAK
-
-
- {(streak ?? 0) > 0 ? (
- <>
-
- {(streak ?? 0).toString().padStart(2, '0')}
-
-
- DAYS 🔥
-
- >
- ) : (
-
- NO STREAK
-
- )}
-
-
-
+
{/* Main Columns */}
{/* Left Column */}
-
-
-
- ACTIVE ISSUES
-
-
- BROWSE MORE
-
-
-
- {recs.length > 0 ? (
-
- ) : (
-
- No recommendations yet. Check back soon.
-
- )}
-
+
}>
+
+
-
-
-
- YOUR MENTEES
-
-
-
- {enrichedMentees && enrichedMentees.length > 0 ? (
- enrichedMentees.map((mentee: any) => (
-
-
-
- {mentee.github_handle.substring(0, 2)}
-
-
-
- {mentee.github_handle}
-
-
Help Request: {mentee.status}
-
-
-
- REVIEW DRAFT
-
-
- ))
- ) : (
-
- No active mentees assigned to you.
-
- )}
-
-
+
}>
+
+
{/* Right Column */}
-
-
-
-
-
- LEADERBOARD SNAPSHOT
-
- GLOBAL
-
-
-
- {leaders && leaders.length > 0 ? (
- leaders.map((leader, index) => {
- const isMe = leader.github_handle === profile?.github_handle;
- return (
-
-
-
- {(index + 1).toString().padStart(2, '0')}
-
- {leader.github_handle} {isMe && '(YOU)'}
-
-
{leader.xp.toLocaleString()} XP
-
- );
- })
- ) : (
-
- BE THE FIRST ON THE BOARD — MERGE A PR TO EARN XP
-
- )}
-
-
+
}>
+
+
+
}>
+
+
@@ -371,14 +104,6 @@ export default async function DashboardPage() {
);
}
-function levelProgressPct(xp: number, level: number): number {
- const floor = xpForLevel(level);
- const ceiling = xpForLevel(level + 1);
- if (ceiling <= floor) return 100;
- const pct = ((xp - floor) / (ceiling - floor)) * 100;
- return Math.max(0, Math.min(100, pct));
-}
-
function NotConfigured() {
return (
@@ -389,54 +114,3 @@ function NotConfigured() {
);
}
-
-function StatsSkeleton() {
- return (
-
- {/* Level Progress Skeleton */}
-
-
- {/* Total Merges Skeleton */}
-
-
- {/* Mentor Points Skeleton */}
-
-
- {/* Current Streak Skeleton */}
-
-
- );
-}
diff --git a/src/app/(app)/dashboard/stats-row.tsx b/src/app/(app)/dashboard/stats-row.tsx
new file mode 100644
index 0000000..e115925
--- /dev/null
+++ b/src/app/(app)/dashboard/stats-row.tsx
@@ -0,0 +1,180 @@
+import { getServiceSupabase } from '@/lib/supabase/service';
+import { xpToNextLevel, xpForLevel } from '@/lib/xp/curve';
+import { cacheGet, cacheSet } from '@/lib/cache';
+import { TrendingUp, Box } from 'lucide-react';
+
+type DashboardCache = {
+ merges: number | null;
+ streak: number | null;
+ syncedAt: string | null;
+};
+
+function levelProgressPct(xp: number, level: number): number {
+ const floor = xpForLevel(level);
+ const ceiling = xpForLevel(level + 1);
+ if (ceiling <= floor) return 100;
+ const pct = ((xp - floor) / (ceiling - floor)) * 100;
+ return Math.max(0, Math.min(100, pct));
+}
+
+type PartialProfile = {
+ github_handle: string | null;
+ xp: number;
+ level: number;
+ github_total_merges: number | null;
+ github_streak: number | null;
+ github_stats_synced_at: string | null;
+} | null;
+
+export default async function StatsRow({
+ userId,
+ profile,
+}: {
+ userId: string;
+ profile: PartialProfile;
+}) {
+ const service = getServiceSupabase();
+ if (!service) return null;
+
+ const xp = profile?.xp ?? 0;
+ const level = profile?.level ?? 0;
+ const { needed, next } = xpToNextLevel(xp);
+ const nextLevel = next ?? level;
+
+ // Read stats from Redis cache, fall back to profile data
+ const cacheKey = `gh:dashboard:${userId}`;
+ let dashCache = await cacheGet
(cacheKey);
+
+ if (!dashCache) {
+ dashCache = {
+ merges: (profile?.github_total_merges as number | null) ?? null,
+ streak: (profile?.github_streak as number | null) ?? null,
+ syncedAt: (profile?.github_stats_synced_at as string | null) ?? null,
+ };
+ await cacheSet(cacheKey, dashCache, 300);
+ }
+
+ // Mentor points
+ const { data: mentorEvents } = await service
+ .from('xp_events')
+ .select('xp_delta')
+ .eq('user_id', userId)
+ .in('source', ['review', 'help_review']);
+ const mentorPoints = mentorEvents?.reduce((acc, e) => acc + (e.xp_delta || 0), 0) || 0;
+
+ const merges = dashCache.merges;
+ const streak = dashCache.streak;
+
+ return (
+
+ {/* Level Progress */}
+
+
+ LEVEL PROGRESS
+
+
+
+ L{level}
+
+
+
+
+ {xp.toLocaleString()} / {(xp + needed).toLocaleString()} XP TO L{nextLevel}
+
+
+
+
+
+ {/* Total Merges */}
+
+
TOTAL MERGES
+
+
+ {(merges ?? 0).toString().padStart(2, '0')}
+
+
+
+
+
+ {/* Mentor Points */}
+
+
+ MENTOR POINTS
+
+
+ {mentorPoints.toLocaleString()}
+
+
+
+
+ {/* Current Streak */}
+
+
+ CURRENT STREAK
+
+
+
+ {(streak ?? 0).toString().padStart(2, '0')}
+
+ DAYS 🔥
+
+
+
+ );
+}
+
+export function StatsSkeleton() {
+ return (
+
+ {/* Level Progress Skeleton */}
+
+
+ {/* Total Merges Skeleton */}
+
+
+ {/* Mentor Points Skeleton */}
+
+
+ {/* Current Streak Skeleton */}
+
+
+ );
+}
diff --git a/src/lib/cache.ts b/src/lib/cache.ts
index 467233f..8cfb3ee 100644
--- a/src/lib/cache.ts
+++ b/src/lib/cache.ts
@@ -153,7 +153,7 @@ function pickDefaultBackend(): CacheBackend {
maxRetriesPerRequest: 1,
retryStrategy: () => null, // Do not keep retrying connection
});
- client.on('error', (err) => {
+ client.on('error', (err: Error) => {
console.warn(`[cache] Local Redis error: ${err.message}. Falling back to memory.`);
backend = new MemoryBackend();
client.disconnect();