From 5228f7ab3c13458aba01dfd41c4ba418e6c76cff Mon Sep 17 00:00:00 2001 From: yachikadev Date: Sun, 2 Aug 2026 09:26:21 -0700 Subject: [PATCH 1/7] feat: add localStorage cache utility for SWR pattern Adds saveSnapshot/getSnapshot/clearSnapshot helpers with try-catch guards so localStorage failures (disabled, quota full, SSR context) never throw at runtime. --- src/lib/localCache.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/lib/localCache.ts diff --git a/src/lib/localCache.ts b/src/lib/localCache.ts new file mode 100644 index 000000000..e69de29bb From 39db869eb2110521bd4fbd4f4548e2f9ec6ef2b4 Mon Sep 17 00:00:00 2001 From: yachikadev Date: Sun, 2 Aug 2026 09:27:38 -0700 Subject: [PATCH 2/7] feat: add localStorage cache utility for SWR pattern Adds saveSnapshot/getSnapshot/clearSnapshot helpers with try-catch guards so localStorage failures (disabled, quota full, SSR context) never throw at runtime. --- src/lib/localCache.ts | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/lib/localCache.ts b/src/lib/localCache.ts index e69de29bb..fb5a6a0b7 100644 --- a/src/lib/localCache.ts +++ b/src/lib/localCache.ts @@ -0,0 +1,36 @@ +const CACHE_PREFIX = "devtrack_cache_"; + +interface CachedSnapshot { + data: T; + timestamp: number; +} + +export function saveSnapshot(key: string, data: T): void { + try { + const payload: CachedSnapshot = { data, timestamp: Date.now() }; + localStorage.setItem(CACHE_PREFIX + key, JSON.stringify(payload)); + } catch (err) { + // localStorage full, disabled, or unavailable (SSR) — fail silently + console.warn(`[localCache] Could not save snapshot for "${key}"`, err); + } +} + +export function getSnapshot(key: string): CachedSnapshot | null { + try { + if (typeof window === "undefined") return null; // SSR guard + const raw = localStorage.getItem(CACHE_PREFIX + key); + if (!raw) return null; + return JSON.parse(raw) as CachedSnapshot; + } catch (err) { + console.warn(`[localCache] Could not read snapshot for "${key}"`, err); + return null; + } +} + +export function clearSnapshot(key: string): void { + try { + localStorage.removeItem(CACHE_PREFIX + key); + } catch { + // ignore + } +} \ No newline at end of file From 06a5a342b7ce207bb0dd40bd49cb591bcde47ed3 Mon Sep 17 00:00:00 2001 From: yachikadev Date: Sun, 2 Aug 2026 09:28:48 -0700 Subject: [PATCH 3/7] feat: instant-hydrate StreakTracker from cached snapshot - Load last cached streak/contribution data from localStorage on mount before the network request resolves - Persist fresh data to localStorage on successful fetch - Fall back to cached snapshot instead of showing an error when the live fetch fails (offline / rate-limited) Part of #2801 --- src/components/StreakTracker.tsx | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/components/StreakTracker.tsx b/src/components/StreakTracker.tsx index 51f8ab70d..4d4cefade 100644 --- a/src/components/StreakTracker.tsx +++ b/src/components/StreakTracker.tsx @@ -1,5 +1,6 @@ "use client"; import SectionHeader from "./SectionHeader"; +import { saveSnapshot, getSnapshot } from "@/lib/localCache"; import { useCallback, useEffect, useState, useRef } from "react"; import { useAccount } from "@/components/AccountContext"; import { useDashboardWidgetA11y } from "@/components/dashboard/DashboardWidgetA11yContext"; @@ -40,6 +41,7 @@ interface FreezeData { export function useStreakTracker() { const { selectedAccount } = useAccount(); + const cacheKey = selectedAccount ? `streak-${selectedAccount}` : "streak-default"; const [data, setData] = useState(null); const [contributionData, setContributionData] = useState(null); const [freezeDates, setFreezeDates] = useState([]); @@ -112,9 +114,22 @@ export function useStreakTracker() { setData(streakData); setContributionData(contribData); setFreezeDates(streakData.freezeDates || []); + + // SWR: persist fresh snapshot for offline / instant-hydration use + saveSnapshot(cacheKey, { streak: streakData, contribution: contribData }); } catch (err) { console.error("Failed to fetch streak data:", err); - setError("We couldn't load your streak data right now. Please try again in a moment."); + + // SWR fallback: agar live fetch fail ho, cached snapshot dikhado + const cached = getSnapshot<{ streak: StreakData; contribution: ContributionData }>(cacheKey); + if (cached) { + setData(cached.data.streak); + setContributionData(cached.data.contribution); + setFreezeDates(cached.data.streak.freezeDates || []); + setError(null); + } else { + setError("We couldn't load your streak data right now. Please try again in a moment."); + } } finally { setLoading(false); setLastUpdated(new Date()); @@ -134,6 +149,17 @@ export function useStreakTracker() { .finally(() => setFreezeLoading(false)); }, []); + // SWR Step 1: instant hydration from localStorage before network resolves + useEffect(() => { + const cached = getSnapshot<{ streak: StreakData; contribution: ContributionData }>(cacheKey); + if (cached) { + setData(cached.data.streak); + setContributionData(cached.data.contribution); + setFreezeDates(cached.data.streak.freezeDates || []); + setLoading(false); // show stale data immediately, no spinner + } + }, [cacheKey]); + useEffect(() => { fetchStreak(); fetchFreeze(); From 5e31350cbcc680181d402064f21a2a3cfe34a900 Mon Sep 17 00:00:00 2001 From: yachikadev Date: Sun, 2 Aug 2026 09:32:26 -0700 Subject: [PATCH 4/7] feat: instant-hydrate GoalTracker from cached snapshot - Load last cached goals list from localStorage on mount - Persist fresh goals to localStorage on successful load - Fall back to cached goals instead of an error message when the live fetch fails Closes #2801 --- src/components/GoalTracker.tsx | 24 ++++++++++++++++++++++-- src/components/StreakTracker.tsx | 2 +- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/components/GoalTracker.tsx b/src/components/GoalTracker.tsx index d2eecbe22..af4c1fa93 100644 --- a/src/components/GoalTracker.tsx +++ b/src/components/GoalTracker.tsx @@ -9,6 +9,7 @@ import { buildPublicGoalShareUrl } from "@/lib/goals/share"; import GoalHistory from "@/components/GoalHistory"; import EmptyState from "@/components/EmptyState"; import WidgetSkeleton, { SkeletonBlock } from "./WidgetSkeleton"; +import { saveSnapshot, getSnapshot } from "@/lib/localCache"; type Recurrence = "none" | "weekly" | "monthly"; @@ -90,9 +91,12 @@ export function useGoalTracker() { const data: { goals: Goal[] } = await response.json(); const fetchedGoals = data.goals ?? []; setGoals(fetchedGoals); + + // SWR: persist fresh snapshot for offline / instant-hydration use + saveSnapshot("goals", fetchedGoals); + return fetchedGoals; }, []); - const handleSync = useCallback(async () => { setSyncing(true); setSyncError(null); @@ -131,6 +135,15 @@ export function useGoalTracker() { } }, [loadGoals]); + // SWR Step 1: instant hydration from localStorage before network resolves + useEffect(() => { + const cached = getSnapshot("goals"); + if (cached) { + setGoals(cached.data); + setLoading(false); // show stale goals immediately, no spinner + } + }, []); + useEffect(() => { loadGoals() .then(async (fetchedGoals) => { @@ -145,7 +158,14 @@ export function useGoalTracker() { } }) .catch(() => { - setSyncError("Failed to load goals. Please try again."); + // SWR fallback: agar live fetch fail ho, cached snapshot dikhta rahe + const cached = getSnapshot("goals"); + if (cached) { + setGoals(cached.data); + setSyncError(null); + } else { + setSyncError("Failed to load goals. Please try again."); + } }) .finally(() => { setLoading(false); diff --git a/src/components/StreakTracker.tsx b/src/components/StreakTracker.tsx index 4d4cefade..ae8ef3209 100644 --- a/src/components/StreakTracker.tsx +++ b/src/components/StreakTracker.tsx @@ -135,7 +135,7 @@ export function useStreakTracker() { setLastUpdated(new Date()); setMinutesAgo(0); } - }, [selectedAccount]); + }, [selectedAccount, cacheKey]); const fetchFreeze = useCallback(() => { setFreezeLoading(true); From 3acc4a97e5c9d7c57b655694936c08e8588a4ebc Mon Sep 17 00:00:00 2001 From: Yachika Sharma Date: Thu, 6 Aug 2026 21:58:05 +0530 Subject: [PATCH 5/7] Update comment for clarity in GoalTracker component --- src/components/GoalTracker.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/GoalTracker.tsx b/src/components/GoalTracker.tsx index af4c1fa93..df32c9bb7 100644 --- a/src/components/GoalTracker.tsx +++ b/src/components/GoalTracker.tsx @@ -158,7 +158,7 @@ export function useGoalTracker() { } }) .catch(() => { - // SWR fallback: agar live fetch fail ho, cached snapshot dikhta rahe + // SWR fallback: if the live fetch fails, keep showing the cached snapshot const cached = getSnapshot("goals"); if (cached) { setGoals(cached.data); From 0675fa4ccc79979f0b51d66da134cefa28eabcab Mon Sep 17 00:00:00 2001 From: Yachika Sharma Date: Thu, 6 Aug 2026 21:59:03 +0530 Subject: [PATCH 6/7] Update error message for fetching streak data --- src/components/StreakTracker.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/StreakTracker.tsx b/src/components/StreakTracker.tsx index ae8ef3209..a695a53d6 100644 --- a/src/components/StreakTracker.tsx +++ b/src/components/StreakTracker.tsx @@ -120,7 +120,7 @@ export function useStreakTracker() { } catch (err) { console.error("Failed to fetch streak data:", err); - // SWR fallback: agar live fetch fail ho, cached snapshot dikhado + // SWR fallback: if the live fetch fails, fall back to the cached snapshot const cached = getSnapshot<{ streak: StreakData; contribution: ContributionData }>(cacheKey); if (cached) { setData(cached.data.streak); @@ -1195,4 +1195,4 @@ export function calculateMonthlyTrend(contrib: ContributionData | undefined | nu } return { isValid: true, thisMonth, lastMonth, text, colorClass }; -} \ No newline at end of file +} From 56092c10e72acf299437826cd735d11abfda3714 Mon Sep 17 00:00:00 2001 From: yachikadev Date: Sun, 9 Aug 2026 22:08:21 +0530 Subject: [PATCH 7/7] chore: trigger CI re-run