From 890e4716e22e60d229d19231827fb673f2842244 Mon Sep 17 00:00:00 2001 From: Boddepalli naveen <1234naveenboddepalli@gmail.com> Date: Sun, 7 Jun 2026 20:24:32 +0530 Subject: [PATCH] Real-time dashboard --- src/components/DashboardHeader.tsx | 78 +++++++++----- src/components/StreakTracker.tsx | 51 +++++++-- src/hooks/useRealtimeSync.ts | 162 +++++++++++++++++++++++++++++ 3 files changed, 256 insertions(+), 35 deletions(-) create mode 100644 src/hooks/useRealtimeSync.ts diff --git a/src/components/DashboardHeader.tsx b/src/components/DashboardHeader.tsx index ac6ec6e5..c5f507d9 100644 --- a/src/components/DashboardHeader.tsx +++ b/src/components/DashboardHeader.tsx @@ -18,6 +18,7 @@ import UserAvatar from "@/components/UserAvatar"; import KeyboardShortcuts from "@/components/KeyboardShortcuts"; import { Moon, Sun } from "lucide-react"; import { toast } from "sonner"; +import { useRealtimeSync } from "@/hooks/useRealtimeSync"; type DashboardSyncContextValue = { lastSynced: Date | null; @@ -110,6 +111,42 @@ export default function DashboardHeader() { setGreeting(computeCurrentGreeting()); }, []); + // Extracted to useCallback so useRealtimeSync can call it as a stable reference. + const loadSettings = useCallback(async () => { + if (!session) { + setIsPublic(null); + return; + } + try { + const res = await fetch("/api/user/settings"); + if (res.ok) { + const data = await res.json(); + setIsPublic(data.is_public === true); + } else { + setIsPublic(false); + } + } catch (error) { + console.error("Failed to load settings:", error); + setIsPublic(false); + } + }, [session]); + + useEffect(() => { + loadSettings(); + }, [loadSettings]); + + // ------------------------------------------------------------------------- + // Realtime: re-fetch user settings whenever the `users` row changes + // (e.g. is_public toggled in another tab). Falls back to 60-second polling. + // NOTE: enable Realtime for the `users` table in Supabase and ensure the + // anon role has a SELECT policy, or provide a user-scoped filter once a + // Supabase JWT is available in the session. + // ------------------------------------------------------------------------- + const { isLive: isHeaderLive } = useRealtimeSync( + "users", + ["UPDATE"], + loadSettings, + ); useEffect(() => { if (!session?.githubLogin) return; @@ -159,31 +196,6 @@ export default function DashboardHeader() { const { lastSynced } = useDashboardSync(); const [now, setNow] = useState(() => Date.now()); - useEffect(() => { - if (!session) { - setIsPublic(null); - return; - } - - async function loadSettings() { - try { - const res = await fetch("/api/user/settings"); - - if (res.ok) { - const data = await res.json(); - setIsPublic(data.is_public === true); - } else { - setIsPublic(false); - } - } catch (error) { - console.error("Failed to load settings:", error); - setIsPublic(false); - } - } - - loadSettings(); - }, [session]); - // Extract a fallback username parameter from active session data strings const displayName = session?.user?.name || session?.githubLogin || "Developer"; useEffect(() => { @@ -252,8 +264,20 @@ export default function DashboardHeader() { coding activity at a glance
{minutesAgo !== null && ( -+
{minutesAgo <= 0 ? "Synced just now" : `Synced ${minutesAgo} min ago`} + {isHeaderLive && ( + + + + + + Live + + )}
)} @@ -388,4 +412,4 @@ export default function DashboardHeader() { ); -} +} \ No newline at end of file diff --git a/src/components/StreakTracker.tsx b/src/components/StreakTracker.tsx index 9f681b27..4ec4751a 100644 --- a/src/components/StreakTracker.tsx +++ b/src/components/StreakTracker.tsx @@ -2,6 +2,7 @@ import SectionHeader from "./SectionHeader"; import { useCallback, useEffect, useState, useRef } from "react"; import { useAccount } from "@/components/AccountContext"; +import { useRealtimeSync } from "@/hooks/useRealtimeSync"; import { useCountUp } from "@/hooks/useCountUp"; import StreakMilestoneBanner from "@/components/StreakMilestoneBanner"; import { useHeatmapTheme } from "@/hooks/useHeatmapTheme"; @@ -243,7 +244,7 @@ export function useStreakTracker() { } } - const currentMilestone = + const currentMilestone = [...STREAK_MILESTONES] .reverse() .find( @@ -252,7 +253,7 @@ export function useStreakTracker() { data.current >= m && m > lastCelebratedMilestone ); - const shouldShowBanner = + const shouldShowBanner = currentMilestone && !dismissedMilestones.includes(currentMilestone); @@ -310,8 +311,27 @@ export function useStreakTracker() { } }; + // ------------------------------------------------------------------------- + // Realtime: re-fetch when streak_freezes rows change in Supabase. + // Falls back to 60-second polling if the WebSocket cannot connect. + // NOTE: enable Realtime for the `streak_freezes` table in the Supabase + // dashboard and ensure the anon role has a SELECT policy (or use a + // user-scoped filter once a Supabase JWT is available in the session). + // ------------------------------------------------------------------------- + const handleRealtimeFreeze = useCallback(() => { + fetchFreeze(); + fetchStreak(); + }, [fetchFreeze, fetchStreak]); + + const { isLive: isStreakLive } = useRealtimeSync( + "streak_freezes", + ["INSERT", "DELETE"], + handleRealtimeFreeze, + ); + return { selectedAccount, + isStreakLive, data, setData, contributionData, @@ -361,6 +381,7 @@ export function useStreakTracker() { export default function StreakTracker() { const { selectedAccount, + isStreakLive, data, setData, contributionData, @@ -564,7 +585,21 @@ export default function StreakTracker() { )}