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,
};
}