From 6f2cb69e83697914826ede0a673f7253a4b16734 Mon Sep 17 00:00:00 2001 From: tanushree-adhikari Date: Mon, 18 May 2026 20:28:01 +0530 Subject: [PATCH] feat: add refresh button to each dashboard widget --- src/components/ContributionGraph.tsx | 372 +++++++++------------------ src/components/PRMetrics.tsx | 21 +- src/components/StreakTracker.tsx | 365 +++++++++++++------------- src/components/TopRepos.tsx | 53 ++-- 4 files changed, 368 insertions(+), 443 deletions(-) diff --git a/src/components/ContributionGraph.tsx b/src/components/ContributionGraph.tsx index baa2c04..045b2b2 100644 --- a/src/components/ContributionGraph.tsx +++ b/src/components/ContributionGraph.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { useAccount } from "@/components/AccountContext"; import { BarChart, @@ -42,13 +42,10 @@ const charts: { key: ViewMode; label: string }[] = [ { key: "area", label: "Area" }, ]; -function mergeContributionData( - myData: DayData[], - friendData: DayData[] -): GraphPoint[] { +function mergeContributionData(myData: DayData[], friendData: DayData[]): GraphPoint[] { const map = new Map(); - myData.forEach(d => { + myData.forEach((d) => { map.set(d.day, { date: d.day, you: d.commits, @@ -56,7 +53,7 @@ function mergeContributionData( }); }); - friendData.forEach(d => { + friendData.forEach((d) => { if (!map.has(d.day)) { map.set(d.day, { date: d.day, @@ -68,9 +65,7 @@ function mergeContributionData( } }); - return Array.from(map.values()).sort((a, b) => - a.date.localeCompare(b.date) - ); + return Array.from(map.values()).sort((a, b) => a.date.localeCompare(b.date)); } export default function ContributionGraph() { @@ -83,7 +78,7 @@ export default function ContributionGraph() { const [minutesAgo, setMinutesAgo] = useState(0); const [error, setError] = useState(null); const [usesTouchTooltip, setUsesTouchTooltip] = useState(false); - + // Compare mode state const [compareMode, setCompareMode] = useState(false); const [compareUser, setCompareUser] = useState(null); @@ -92,7 +87,7 @@ export default function ContributionGraph() { const [compareLoading, setCompareLoading] = useState(false); const [compareRequestId, setCompareRequestId] = useState(0); - // Fetch my data + // Restore saved range useEffect(() => { if (typeof window !== "undefined") { try { @@ -130,32 +125,29 @@ export default function ContributionGraph() { } }; - useEffect(() => { + const fetchGraph = useCallback(async () => { setLoading(true); setError(null); - const accountParam = - selectedAccount !== null - ? `&accountId=${encodeURIComponent(selectedAccount)}` - : ""; - fetch(`/api/metrics/contributions?days=${days}${accountParam}`) - .then((r) => { - if (!r.ok) throw new Error("API error"); - return r.json(); - }) - .then((res: { data: Record }) => { - const sorted = Object.entries(res.data ?? {}) - .sort(([a], [b]) => a.localeCompare(b)) - .map(([day, commits]) => ({ day, commits })); - setData(sorted); - }) - .catch(() => { - setError("Failed to load contribution data."); - }) - .finally(() => { - setLoading(false); - setLastUpdated(new Date()); - setMinutesAgo(0); - }); + + const accountParam = selectedAccount !== null ? `&accountId=${encodeURIComponent(selectedAccount)}` : ""; + + try { + const response = await fetch(`/api/metrics/contributions?days=${days}${accountParam}`); + if (!response.ok) throw new Error("API error"); + + const res = (await response.json()) as { data: Record }; + const sorted = Object.entries(res.data ?? {}) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([day, commits]) => ({ day, commits })); + + setData(sorted); + setLastUpdated(new Date()); + setMinutesAgo(0); + } catch { + setError("Failed to load contribution data."); + } finally { + setLoading(false); + } }, [days, selectedAccount]); // Fetch friend data when compare mode is on and compareUser changes @@ -168,7 +160,7 @@ export default function ContributionGraph() { setCompareLoading(true); setCompareError(null); - + fetch(`/api/metrics/contributions?days=${days}&username=${encodeURIComponent(compareUser)}`) .then((r) => { if (!r.ok) throw new Error("Failed to fetch friend data"); @@ -224,6 +216,10 @@ export default function ContributionGraph() { return () => window.removeEventListener("toggleChart", handleToggleChart); }, []); + useEffect(() => { + void fetchGraph(); + }, [fetchGraph]); + useEffect(() => { if (!lastUpdated) return; const interval = setInterval(() => { @@ -241,24 +237,18 @@ export default function ContributionGraph() { setCompareError(null); }; - const mergedData = - compareMode && data.length > 0 - ? mergeContributionData(data, friendData) - : []; + const mergedData = compareMode && data.length > 0 ? mergeContributionData(data, friendData) : []; const displayData = compareMode ? mergedData : data; const hasFriendData = compareMode && friendData.length > 0 && !compareError; const tooltipTrigger = usesTouchTooltip ? "click" : "hover"; return ( -
-
+
+

- {compareMode && compareUser ? `You vs ${compareUser}` : "Your Commits"} + {compareMode && compareUser ? `You vs ${compareUser}` : "Commit Activity"}

{compareMode && compareError && (

{compareError}

@@ -269,7 +259,18 @@ export default function ContributionGraph() {
- {/* Range buttons */} + +
{RANGES.map((r) => (
- {/* Chart Toggle Buttons */} {displayData.length > 0 && !error && ( -
+
{charts.map((chart) => (
)} - {/* Clear compare button */} {compareMode && ( - )}
- {loading ? ( -
- ) : error ? ( -
-

- {error} Please try refreshing. -

+ {loading && data.length === 0 ? ( +
+ ) : error && data.length === 0 ? ( +
+

{error} Please try refreshing.

) : displayData.length === 0 ? ( -

- No commits in the last {days} days. -

+

No commits in the last {days} days.

) : (
- - {chartType === "bar" ? ( - - - - - - {hasFriendData && ( - - )} - {compareMode && hasFriendData ? ( - <> - - - + + {chartType === "bar" ? ( + compareMode && hasFriendData ? ( + + + + + + {hasFriendData && } + + + ) : ( - - )} - - ) : chartType === "line" ? ( - - - - - - {hasFriendData && ( - - )} - {compareMode && hasFriendData ? ( - <> - - - + + + + + + + + ) + ) : chartType === "line" ? ( + compareMode && hasFriendData ? ( + + + + + + {hasFriendData && } + + + ) : ( - - )} - - ) : ( - - - - - - {hasFriendData && ( - - )} - {compareMode && hasFriendData ? ( - <> - - - - ) : ( - - )} - - )} - + + + + + + + + ) + ) : compareMode && hasFriendData ? ( + + + + + + {hasFriendData && } + + + + ) : ( + + + + + + + + )} +
)} - - {lastUpdated && !compareMode && ( -

- {minutesAgo === 0 - ? "Updated just now" - : `Updated ${minutesAgo} min ago`} -

- )} - - {compareMode && compareUser && !compareLoading && !compareError && ( -

- Comparing with {compareUser} -

- )} + + {error && data.length > 0 &&

{error} Showing the last successful data.

} + + {lastUpdated && !compareMode &&

{minutesAgo === 0 ? "Updated just now" : `Updated ${minutesAgo} min ago`}

} + + {compareMode && compareUser && !compareLoading && !compareError && (

Comparing with {compareUser}

)}
); -} \ No newline at end of file +} diff --git a/src/components/PRMetrics.tsx b/src/components/PRMetrics.tsx index 2d860b1..8699c30 100644 --- a/src/components/PRMetrics.tsx +++ b/src/components/PRMetrics.tsx @@ -66,10 +66,25 @@ export default function PRMetrics() { ] : []; + const showSkeleton = loading && !metrics; + return ( -
-

PR Analytics

- {loading ? ( +
+
+

PR Analytics

+ +
+ {showSkeleton ? (
{[1, 2, 3, 4].map((i) => (
{ + setFreezeLoading(true); + try { + const response = await fetch("/api/streak/freeze"); + const freezeData = (await response.json()) as FreezeData; + setFreeze(freezeData); + } catch { + setFreeze(null); + } finally { + setFreezeLoading(false); + } + }, []); + const fetchStreak = useCallback(async () => { setLoading(true); setError(null); @@ -77,19 +90,13 @@ export default function StreakTracker() { } }, [selectedAccount]); - const fetchFreeze = () => { - setFreezeLoading(true); - fetch("/api/streak/freeze") - .then((r) => r.json()) - .then((d: FreezeData) => setFreeze(d)) - .catch(() => setFreeze(null)) - .finally(() => setFreezeLoading(false)); - }; + const refreshWidget = useCallback(async () => { + await Promise.all([fetchStreak(), fetchFreeze()]); + }, [fetchFreeze, fetchStreak]); useEffect(() => { - fetchStreak(); - fetchFreeze(); - }, [fetchStreak]); + void refreshWidget(); + }, [refreshWidget]); useEffect(() => { if (!lastUpdated) return; @@ -134,36 +141,8 @@ export default function StreakTracker() { } } - if (loading) { - return ( -
-
-
- {[1, 2, 3, 4].map((i) => ( -
- ))} -
-
- ); - } - - if (error) { - return ( -
-

Commit Streaks

-
-

{error}

- -
-
- ); - } + const isRefreshing = loading || freezeLoading; + const showSkeleton = loading && !data; const MILESTONES = [ { days: 30, label: "30-day streak!", emoji: "🏅" }, @@ -238,164 +217,196 @@ export default function StreakTracker() { }; return ( -
-
+
+

Commit Streaks

- {data && ( +
- )} -
-
- {stats.map((stat) => ( -
-
{stat.icon}
-
- {stat.value} - {stat.unit && ( - - {stat.unit} - + {copied ? ( + Copied! + ) : ( + 📋 )} -
-
{stat.label}
-
- ))} + + )} +
- {monthlyTrend.isValid && ( -
- - This month: {monthlyTrend.thisMonth} active days - - - ({monthlyTrend.text}) - + + {error ? ( +
+

{error}

+
- )} - {badge && ( -
- {badge.emoji} - {badge.label} + ) : showSkeleton ? ( +
+ {[1, 2, 3, 4].map((i) => ( +
+ ))}
- )} - - {activeDayData.isValid && activeDayData.peakDay && ( -
-
-
-
Most Active Day
-
- {activeDayData.peakDay.label}{" "} - - (avg {activeDayData.peakDay.avgCommits.toFixed(1)} commits) - + ) : ( + <> +
+ {stats.map((stat) => ( +
+
+ {stat.icon} +
+
+ {stat.value} + {stat.unit && ( + + {stat.unit} + + )} +
+
{stat.label}
+ ))} +
+ + {badge && ( +
+ {badge.emoji} + {badge.label}
+ )} -
- {activeDayData.insights.map((item) => { - const maxAvg = activeDayData.peakDay?.avgCommits ?? 1; - const heightPercent = maxAvg > 0 ? Math.max(15, Math.round((item.avgCommits / maxAvg) * 100)) : 15; - const isPeak = item.label === activeDayData.peakDay?.label; - - return ( -
-
-
-
- - {item.shortLabel} + {activeDayData.isValid && activeDayData.peakDay && ( +
+
+
+
Most Active Day
+
+ {activeDayData.peakDay.label}{" "} + + (avg {activeDayData.peakDay.avgCommits.toFixed(1)} commits)
- ); - })} +
+ +
+ {activeDayData.insights.map((item) => { + const maxAvg = activeDayData.peakDay?.avgCommits ?? 1; + const heightPercent = maxAvg > 0 ? Math.max(15, Math.round((item.avgCommits / maxAvg) * 100)) : 15; + const isPeak = item.label === activeDayData.peakDay?.label; + + return ( +
+
+
+
+ + {item.shortLabel} + +
+ ); + })} +
+
-
-
- )} - {lastUpdated && ( -

- {minutesAgo === 0 - ? "Updated just now" - : `Updated ${minutesAgo} min ago`} -

- )} + )} - {!freezeLoading && freeze?.hasFreeze && ( -
- ✓ Freeze active today - {confirmCancel ? ( -
- Remove freeze? - - + {lastUpdated && ( +

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

+ )} + + {!freezeLoading && freeze?.hasFreeze && ( +
+ ✓ Freeze active today + {confirmCancel ? ( +
+ Remove freeze? + + +
+ ) : ( + + )}
- ) : ( - )} -
- )} - {/* Streak Calendar Section */} - {contributionData ? ( - - ) : null} + {/* Streak Calendar Section */} + {contributionData ? ( + + ) : null} + + )}
); } diff --git a/src/components/TopRepos.tsx b/src/components/TopRepos.tsx index 2bc9756..2cf3c71 100644 --- a/src/components/TopRepos.tsx +++ b/src/components/TopRepos.tsx @@ -58,6 +58,10 @@ export default function TopRepos() { .finally(() => setHealthLoading(false)); }, [days, selectedAccount]); + const refreshWidget = useCallback(async () => { + await Promise.all([fetchRepos(), fetchHealthScores()]); + }, [fetchHealthScores, fetchRepos]); + useEffect(() => { if (!lastUpdated) return; const interval = setInterval(() => { @@ -69,9 +73,9 @@ export default function TopRepos() { useEffect(() => { - fetchRepos(); - fetchHealthScores(); - }, [fetchRepos, fetchHealthScores, selectedAccount]); + void refreshWidget(); + }, [refreshWidget]); + // toggle sort: same column flips direction, new column resets to desc const handleSort = (column: "commits" | "name") => { if (sortColumn === column) { @@ -81,6 +85,7 @@ export default function TopRepos() { setSortDirection("desc"); } }; + // sort repos based on selected column and direction before rendering const sortedRepos = [...repos].sort((a, b) => { if (sortColumn === "name") { @@ -96,23 +101,39 @@ export default function TopRepos() { }); const maxCommits = sortedRepos[0]?.commits ?? 1; + const isRefreshing = loading || healthLoading; + const showSkeleton = loading && repos.length === 0; + return ( -
-
+
+

Top Repositories

- +
+ + +
- {loading ? ( + {showSkeleton ? (
{[1, 2, 3, 4].map((i) => (