From e9220bb6e3eae59ca02f3130da38312837fee68a Mon Sep 17 00:00:00 2001 From: tanushree-adhikari Date: Wed, 27 May 2026 16:47:16 +0530 Subject: [PATCH] feat: add last synced timestamp to dashboard header (#186) --- src/app/dashboard/page.tsx | 85 +++++++++++++------------- src/components/DashboardHeader.tsx | 98 +++++++++++++++++++++++++++++- src/lib/supabase.ts | 11 ++++ 3 files changed, 152 insertions(+), 42 deletions(-) diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index 9e4bdb66..68d81864 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -18,6 +18,7 @@ import PersonalRecords from "@/components/PersonalRecords"; import { authOptions } from "@/lib/auth"; import { getServerSession } from "next-auth"; import { redirect } from "next/navigation"; +import { DashboardSyncProvider } from "@/components/DashboardHeader"; export default async function DashboardPage() { const session = await getServerSession(authOptions); @@ -27,57 +28,59 @@ export default async function DashboardPage() { } return ( -
- -
- -
- + +
+ +
+ +
+ - + -
- -
+
+ +
- {/* Row 1: Contribution graph + Streak + Friend Comparison */} -
-
- -
- + {/* Row 1: Contribution graph + Streak + Friend Comparison */} +
+
+ +
+ +
-
-
- - +
+ + +
-
- {/* Row 2: PR metrics, PR breakdown & Time Chart */} -
- - - -
+ {/* Row 2: PR metrics, PR breakdown & Time Chart */} +
+ + + +
- {/* Row 3: Issue metrics */} -
- -
+ {/* Row 3: Issue metrics */} +
+ +
- {/* Row 4: Pinned repositories */} -
- -
+ {/* Row 4: Pinned repositories */} +
+ +
- {/* Row 5: Top repos + Language breakdown + Goal tracker */} -
- - - + {/* Row 5: Top repos + Language breakdown + Goal tracker */} +
+ + + +
-
+ ); } \ No newline at end of file diff --git a/src/components/DashboardHeader.tsx b/src/components/DashboardHeader.tsx index c7ba314d..7bcaa599 100644 --- a/src/components/DashboardHeader.tsx +++ b/src/components/DashboardHeader.tsx @@ -1,6 +1,14 @@ "use client" -import { useEffect, useState } from "react"; +import { + createContext, + ReactNode, + useContext, + useEffect, + useLayoutEffect, + useMemo, + useState, +} from "react"; import { useSession } from "next-auth/react"; import AccountToggle from "@/components/AccountToggle"; import SignOutButton from "@/components/SignOutButton"; @@ -11,9 +19,78 @@ interface UserSettings { is_public: boolean; } +type DashboardSyncContextValue = { + lastSynced: Date | null; +}; + +const DashboardSyncContext = createContext({ + lastSynced: null, +}); + +function getRequestPath(input: RequestInfo | URL): string { + if (typeof input === "string") { + return input.startsWith("http") ? new URL(input).pathname : input; + } + + if (input instanceof URL) { + return input.pathname; + } + + return new URL(input.url).pathname; +} + +function isDashboardDataRequest(input: RequestInfo | URL): boolean { + const requestPath = getRequestPath(input); + + return ( + requestPath.startsWith("/api/metrics/") || + requestPath === "/api/goals" || + requestPath.startsWith("/api/goals/") || + requestPath.startsWith("/api/streak/") || + requestPath === "/api/user/github-accounts" || + requestPath.startsWith("/api/badge/") + ); +} + +export function DashboardSyncProvider({ children }: { children: ReactNode }) { + const [lastSynced, setLastSynced] = useState(null); + + useLayoutEffect(() => { + const originalFetch = window.fetch; + + window.fetch = async (...args) => { + const response = await originalFetch(...args); + + if (response.ok && isDashboardDataRequest(args[0])) { + setLastSynced(new Date()); + } + + return response; + }; + + return () => { + window.fetch = originalFetch; + }; + }, []); + + const value = useMemo(() => ({ lastSynced }), [lastSynced]); + + return ( + + {children} + + ); +} + +function useDashboardSync() { + return useContext(DashboardSyncContext); +} + export default function DashboardHeader() { const { data: session } = useSession(); const [settings, setSettings] = useState(null); + const { lastSynced } = useDashboardSync(); + const [now, setNow] = useState(() => Date.now()); useEffect(() => { if (!session) return; @@ -33,6 +110,20 @@ export default function DashboardHeader() { loadSettings(); }, [session]); + useEffect(() => { + if (!lastSynced) return; + + const interval = setInterval(() => { + setNow(Date.now()); + }, 60000); + + return () => clearInterval(interval); + }, [lastSynced]); + + const minutesAgo = lastSynced + ? Math.floor((now - lastSynced.getTime()) / 60000) + : null; + return (
@@ -43,6 +134,11 @@ export default function DashboardHeader() {

Your coding activity at a glance

+ {minutesAgo !== null && ( +

+ {minutesAgo <= 0 ? "Synced just now" : `Synced ${minutesAgo} min ago`} +

+ )}
diff --git a/src/lib/supabase.ts b/src/lib/supabase.ts index b7cbfbec..9f93f950 100644 --- a/src/lib/supabase.ts +++ b/src/lib/supabase.ts @@ -2,6 +2,9 @@ import { createClient } from "@supabase/supabase-js"; const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!; const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY!; +const isPlaceholderSupabaseConfig = + supabaseUrl.includes("placeholder.supabase.co") || + serviceRoleKey.includes("placeholder-service-role-key"); // Server-side only — use in API routes, never import in client components. // Service role bypasses RLS; auth is enforced by getServerSession checks. @@ -21,6 +24,10 @@ interface User { * Returns the user row if found and is_public is true, otherwise null. */ export async function getUserByUsername(username: string): Promise { + if (isPlaceholderSupabaseConfig) { + return null; + } + const { data, error } = await supabaseAdmin .from("users") .select("id,github_id,github_login,is_public,created_at,updated_at") @@ -47,6 +54,10 @@ export async function updateUserPublicFlag( userId: string, isPublic: boolean ): Promise { + if (isPlaceholderSupabaseConfig) { + return null; + } + const { data, error } = await supabaseAdmin .from("users") .update({ is_public: isPublic })