diff --git a/src/app/api/metrics/contributions/daily/route.ts b/src/app/api/metrics/contributions/daily/route.ts new file mode 100644 index 00000000..76997e88 Binary files /dev/null and b/src/app/api/metrics/contributions/daily/route.ts differ diff --git a/src/app/api/metrics/contributions/hourly/route.ts b/src/app/api/metrics/contributions/hourly/route.ts new file mode 100644 index 00000000..4b9c4be2 Binary files /dev/null and b/src/app/api/metrics/contributions/hourly/route.ts differ diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index 0a5de926..575712a0 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -1,3 +1,4 @@ +import ActivityRingChart from "@/components/ActivityRingChart"; import ContributionGraph from "@/components/ContributionGraph"; import ContributionHeatmap from "@/components/ContributionHeatmap"; import PRMetrics from "@/components/PRMetrics"; @@ -71,6 +72,10 @@ export default async function DashboardPage() { + {/* Row 2b: Activity Ring Chart */} +
+ +
diff --git a/src/components/ActivityRingChart.tsx b/src/components/ActivityRingChart.tsx new file mode 100644 index 00000000..eab67297 Binary files /dev/null and b/src/components/ActivityRingChart.tsx differ diff --git a/src/components/ContributionHeatmap.tsx b/src/components/ContributionHeatmap.tsx index 8c81db44..6ce0d61d 100644 --- a/src/components/ContributionHeatmap.tsx +++ b/src/components/ContributionHeatmap.tsx @@ -1,7 +1,8 @@ "use client"; -import { useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { useHeatmapTheme } from "@/hooks/useHeatmapTheme"; +import DailyBreakdownSheet from "@/components/DailyBreakdownSheet"; interface ContributionHeatmapProps { days?: number; @@ -77,6 +78,8 @@ export default function ContributionHeatmap({ const [error, setError] = useState(null); const [lastUpdated, setLastUpdated] = useState(null); const [minutesAgo, setMinutesAgo] = useState(0); + const [selectedDate, setSelectedDate] = useState(null); + const handleCloseSheet = useCallback(() => setSelectedDate(null), []); useEffect(() => { let active = true; @@ -286,6 +289,7 @@ export default function ContributionHeatmap({ title={isFuture ? "" : tooltip} aria-label={isFuture ? `${cell.dateKey}: future date` : tooltip} disabled={isFuture} + onClick={() => !isFuture && setSelectedDate(cell.dateKey)} className={`group relative z-0 h-3 w-3 rounded-[3px] border transition-transform hover:z-20 hover:scale-110 focus:z-20 focus:outline-none focus:ring-2 focus:ring-[var(--heatmap-focus-ring)] disabled:cursor-default disabled:opacity-30 ${ cell.inRange ? "" : "opacity-35" }`} @@ -333,6 +337,11 @@ export default function ContributionHeatmap({
)} + ); -} \ No newline at end of file +} diff --git a/src/components/DailyBreakdownSheet.tsx b/src/components/DailyBreakdownSheet.tsx new file mode 100644 index 00000000..f7b7df8f --- /dev/null +++ b/src/components/DailyBreakdownSheet.tsx @@ -0,0 +1,139 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; + +interface DailyBreakdownSheetProps { + date: string | null; + onClose: () => void; + heatmapData?: Record; +} + +interface RepoCommit { + repo: string; + count: number; + url: string; +} + +export default function DailyBreakdownSheet({ + date, + onClose, + heatmapData, +}: DailyBreakdownSheetProps) { + const [commits, setCommits] = useState([]); + const [loading, setLoading] = useState(false); + const isOpen = date !== null; + + useEffect(() => { + if (!date) return; + const totalForDay = heatmapData?.[date] ?? 0; + if (totalForDay === 0) { + setCommits([]); + setLoading(false); + return; + } + setLoading(true); + fetch(`/api/metrics/contributions/daily?date=${date}`) + .then((res) => res.json()) + .then((result) => { + setCommits(result.repos ?? []); + }) + .catch(() => setCommits([])) + .finally(() => setLoading(false)); + }, [date, heatmapData]); + + const onCloseRef = useRef(onClose); + useEffect(() => { + onCloseRef.current = onClose; + }, [onClose]); + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape") onCloseRef.current(); + }; + document.addEventListener("keydown", handleKeyDown); + return () => document.removeEventListener("keydown", handleKeyDown); + }, []); + + if (!isOpen) return null; + + const formattedDate = date + ? new Date(date + "T00:00:00").toLocaleDateString("en-US", { + weekday: "long", + month: "long", + day: "numeric", + year: "numeric", + }) + : ""; + + return ( + <> +