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 (
+ <>
+
+
+
+
+
+ Daily Breakdown
+
+
+ {formattedDate}
+
+
+
+
+
+ {loading ? (
+
+ {[1, 2, 3].map((i) => (
+
+ ))}
+
+ ) : commits.length === 0 ? (
+
+
+ No commit data available for this day.
+
+
+ ) : (
+
+ )}
+
+
+ >
+ );
+}