diff --git a/docs/app/dashboard/page.tsx b/docs/app/dashboard/page.tsx index 949b51e..71e3b19 100644 --- a/docs/app/dashboard/page.tsx +++ b/docs/app/dashboard/page.tsx @@ -1,6 +1,7 @@ "use client"; import { useState, useCallback, useEffect } from "react"; +import { motion, AnimatePresence } from "framer-motion"; import { useGitHubStatsCache } from "../hooks/useGitHubStatsCache"; import CloudBackground from "../components/CloudBackground"; import RainOverlay from "../components/RainOverlay"; @@ -49,16 +50,14 @@ function formatCountdown(targetMs: number): string { * * Data is cached in localStorage for 5 minutes. Page reloads within that window * serve cached data without hitting the GitHub API. A background timer - * auto-refreshes every 5 minutes. A manual force-refresh button is available - * with a 60-second cooldown. Rate limit is displayed in the footer. + * auto-refreshes every 5 minutes. An animated refresh icon appears during loading. */ export default function DashboardPage() { const { - forceRefresh, lastFetchedAt, nextRefreshAt, - canForceRefresh, isFromCache, + isRefreshing, ...stats } = useGitHubStatsCache(); @@ -91,16 +90,6 @@ export default function DashboardPage() { const toggleStargazers = useCallback(() => setShowStargazers((prev) => !prev), []); const closeStargazers = useCallback(() => setShowStargazers(false), []); - // Compute cooldown remaining for the button tooltip - const cooldownRemaining = lastFetchedAt !== null - ? Math.max(0, Math.ceil((60_000 - (Date.now() - lastFetchedAt)) / 1000)) - : 0; - - const refreshButtonDisabled = stats.loading || !canForceRefresh; - const refreshButtonTitle = !canForceRefresh && cooldownRemaining > 0 - ? `Available in ${cooldownRemaining}s` - : "Refresh data"; - return (
@@ -119,7 +108,7 @@ export default function DashboardPage() {
- {/* Header with refresh */} + {/* Header with loading indicator */}

Project Dashboard

@@ -127,27 +116,30 @@ export default function DashboardPage() { Live metrics ยท phranck/TUIkit

- + {/* Animated refresh icon โ€” fades in while refreshing, spins, then fades out */} + + {isRefreshing && ( + + + + + + )} +
{/* Error state */} {stats.error && (
Error: {stats.error} - + Will retry automatically
)} diff --git a/docs/app/hooks/useGitHubStatsCache.ts b/docs/app/hooks/useGitHubStatsCache.ts index b529ae6..8257c41 100644 --- a/docs/app/hooks/useGitHubStatsCache.ts +++ b/docs/app/hooks/useGitHubStatsCache.ts @@ -30,6 +30,8 @@ export interface UseGitHubStatsCacheReturn extends GitHubStats { canForceRefresh: boolean; /** Whether the currently displayed data was served from localStorage cache. */ isFromCache: boolean; + /** Whether a background refresh is in progress (for showing a subtle indicator). */ + isRefreshing: boolean; } // --------------------------------------------------------------------------- @@ -88,29 +90,35 @@ export function useGitHubStatsCache(): UseGitHubStatsCacheReturn { const [lastFetchedAt, setLastFetchedAt] = useState(null); const [nextRefreshAt, setNextRefreshAt] = useState(null); const [isFromCache, setIsFromCache] = useState(false); + const [isRefreshing, setIsRefreshing] = useState(false); const intervalRef = useRef | null>(null); const initializedRef = useRef(false); // The stats to expose โ€” override (cached) data takes priority while it's set + // Force loading: false when we have data, so components don't show skeletons during background refresh const activeStats = overrideStats ?? rawStats; + const hasData = lastFetchedAt !== null; // ------------------------------------------------------------------------- // Core fetch + cache-write logic // ------------------------------------------------------------------------- const doFetchAndCache = useCallback(async () => { + setIsRefreshing(true); try { const freshData = await fetchData(); const timestamp = writeCache(freshData); setLastFetchedAt(timestamp); setNextRefreshAt(timestamp + REFRESH_INTERVAL_MS); setIsFromCache(false); - // Clear override so the live rawStats (now updated by useGitHubStats) show through - setOverrideStats(null); + // Store the fresh data as override with loading: false to prevent skeleton flash + setOverrideStats({ ...freshData, loading: false }); } catch { - // Errors are already reflected in rawStats.error via useGitHubStats - setOverrideStats(null); + // On error, keep showing previous data (overrideStats stays as-is) + // Errors are also reflected in rawStats.error via useGitHubStats if needed + } finally { + setIsRefreshing(false); } }, [fetchData]); @@ -169,12 +177,18 @@ export function useGitHubStatsCache(): UseGitHubStatsCacheReturn { }, REFRESH_INTERVAL_MS); }, [lastFetchedAt, doFetchAndCache]); + // Override loading to false if we already have data โ€” prevents skeleton flash during background refresh + const statsWithLoadingOverride = hasData + ? { ...activeStats, loading: false } + : activeStats; + return { - ...activeStats, + ...statsWithLoadingOverride, forceRefresh, lastFetchedAt, nextRefreshAt, canForceRefresh, isFromCache, + isRefreshing, }; }