diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index 617bea420..a70ccbc60 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -1,7 +1,7 @@ import DiscussionsWidget from "@/components/DiscussionsWidget"; import CommunityMetrics from "@/components/CommunityMetrics"; import GoalTracker from "@/components/GoalTracker"; -import DashboardHeader from "@/components/DashboardHeader"; +import DashboardHeader, { DashboardSyncProvider } from "@/components/DashboardHeader"; import StreakTracker from "@/components/StreakTracker"; import TopRepos from "@/components/TopRepos"; import PinnedRepos from "@/components/PinnedRepos"; @@ -106,132 +106,128 @@ export default async function DashboardPage() { return (
- -
- - Year in Code - - - Settings - - -
- - -
- -
-
-
-

Your Year in Code is here! ✨

-

Discover your top languages, longest streaks, and coding habits of the year.

-
-
- View Wrapped + + + +
+ + Year in Code + + + Settings + + +
+ + +
+ +
+
+
+

Your Year in Code is here! ✨

+

Discover your top languages, longest streaks, and coding habits of the year.

+
+
+ View Wrapped +
+
+
-
-
-
- -
+ +
-
- -
+
+ +
-
- -
+
+ +
-
- -
+
+ +
- {/* Row 1: Contribution graph + Streak + Local Coding Time */} -
-
- -
- -
-
- -
-
- + {/* Row 1: Contribution graph + Streak + Local Coding Time */} +
+
+ +
+ +
-
-
- - -
- +
+ + +
+ +
-
- {/* Row 2: PR metrics, community metrics, PR breakdown & Time Chart */} -
- - - - -
- {/* Row 2b: Activity Ring Chart */} -
- -
+ {/* Row 2: PR metrics, community metrics, PR breakdown & Time Chart */} +
+ + + + +
+ {/* Row 2b: Activity Ring Chart */} +
+ +
-
- -
+
+ +
-
- -
+
+ +
- {/* Row 3: Issue metrics + CI analytics */} -
-
- + {/* Row 3: Issue metrics + CI analytics */} +
+
+ +
+ +
+ {/* Row 3b: Discussion activity */} +
+
- -
- {/* Row 3b: Discussion activity */} -
- -
- {/* Row 4: Pinned repositories */} -
- -
+ {/* Row 4: Pinned repositories */} +
+ +
- {/* Row 5: Inactive repository reminder */} -
- -
+ {/* Row 5: Inactive repository reminder */} +
+ +
- {/* Row 6: Top repos + Language breakdown + Goal tracker */} -
- - - -
+ {/* Row 6: Top repos + Language breakdown + Goal tracker */} +
+ + + +
- {/* Row 7: Recent GitHub activity */} -
- + {/* Row 7: Recent GitHub activity */} +
+ +
-
); } diff --git a/src/components/DashboardHeader.tsx b/src/components/DashboardHeader.tsx index 0b53420a6..09f722077 100644 --- a/src/components/DashboardHeader.tsx +++ b/src/components/DashboardHeader.tsx @@ -1,7 +1,15 @@ "use client"; import NotificationBell from "@/components/NotificationBell"; -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"; @@ -9,9 +17,78 @@ import ThemeToggle from "@/components/ThemeToggle"; import UserAvatar from "@/components/UserAvatar"; import KeyboardShortcuts from "@/components/KeyboardShortcuts"; +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 [isPublic, setIsPublic] = useState(null); + const { lastSynced } = useDashboardSync(); + const [now, setNow] = useState(() => Date.now()); useEffect(() => { if (!session) { @@ -38,6 +115,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 (
@@ -53,6 +144,11 @@ export default function DashboardHeader() { > coding activity at a glance

+ {minutesAgo !== null && ( +

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

+ )}
{/* Right Section */} diff --git a/src/lib/supabase.ts b/src/lib/supabase.ts index bc01e8e30..7e2e812ce 100644 --- a/src/lib/supabase.ts +++ b/src/lib/supabase.ts @@ -2,11 +2,14 @@ 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") === true || + serviceRoleKey?.includes("placeholder-service-role-key") === true; // Do not throw here — build-time rendering can touch this module before // runtime environment variables are present. Guard call sites instead. export const supabaseAdmin: any = - supabaseUrl && serviceRoleKey && !supabaseUrl.includes("placeholder") + supabaseUrl && serviceRoleKey && !isPlaceholderSupabaseConfig ? createClient(supabaseUrl, serviceRoleKey) : null; @@ -26,13 +29,15 @@ interface User { export async function getUserByUsername( username: string ): Promise { - if (!supabaseAdmin) return null; + if (!supabaseAdmin || isPlaceholderSupabaseConfig) { + return null; + } try { const { data, error } = await supabaseAdmin .from("users") .select("id,github_id,github_login,is_public,created_at,updated_at") - .ilike("github_login", username) + .eq("github_login", username) .eq("is_public", true) .single(); @@ -58,7 +63,9 @@ export async function updateUserPublicFlag( userId: string, isPublic: boolean ): Promise { - if (!supabaseAdmin) return null; + if (!supabaseAdmin || isPlaceholderSupabaseConfig) { + return null; + } try { const { data, error } = await supabaseAdmin