diff --git a/.gitignore b/.gitignore
index 1158f089..418741ed 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,6 +12,7 @@ out/
# Environment files — NEVER commit these
.env
.env.local
+.env.local*
.env.production
.env.*.local
diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx
index aac71d79..ee30eaec 100644
--- a/src/app/dashboard/page.tsx
+++ b/src/app/dashboard/page.tsx
@@ -78,6 +78,14 @@ const ContributionHeatmap = dynamic(
{ ssr: false, loading: () => },
);
+const RepoContributionDistribution = dynamic(
+ () => import("@/components/RepoContributionDistribution"),
+ {
+ ssr: false,
+ loading: () => ,
+ },
+);
+
const PRMetrics = dynamic(() => import("@/components/PRMetrics"), {
ssr: false,
loading: () => ,
@@ -173,6 +181,9 @@ export default async function DashboardPage() {
+ }>
+
+
}>
diff --git a/src/app/globals.css b/src/app/globals.css
index e8556685..198ca546 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -28,6 +28,13 @@
--destructive-foreground: #ffffff;
--shadow-soft: 0 12px 30px -20px rgba(37, 99, 235, 0.35);
--shadow-medium: 0 18px 35px -24px rgba(37, 99, 235, 0.4);
+ --chart-2: #22c55e;
+ --chart-3: #f97316;
+ --chart-4: #06b6d4;
+ --chart-5: #ec4899;
+ --chart-6: #eab308;
+ --chart-7: #8b5cf6;
+ --chart-8: #14b8a6;
}
.dark {
@@ -61,6 +68,13 @@
--destructive-foreground: #ffffff;
--shadow-soft: 0 16px 34px -24px rgba(2, 6, 23, 0.8);
--shadow-medium: 0 24px 45px -28px rgba(2, 6, 23, 0.85);
+ --chart-2: #4ade80;
+ --chart-3: #fb923c;
+ --chart-4: #22d3ee;
+ --chart-5: #f472b6;
+ --chart-6: #facc15;
+ --chart-7: #a78bfa;
+ --chart-8: #2dd4bf;
}
html, body {
@@ -138,12 +152,32 @@ body {
scrollbar-width: thin;
scrollbar-color: var(--muted-foreground) transparent;
}
-.scrollbar-thin::-webkit-scrollbar { width: 6px; height: 6px; }
-.scrollbar-thin::-webkit-scrollbar-track { background: transparent; }
-.scrollbar-thin::-webkit-scrollbar-thumb { background: var(--muted-foreground); border-radius: 9999px; }
-.scrollbar-thin::-webkit-scrollbar-thumb:hover { background: var(--muted-foreground); }
-.dark .scrollbar-thin::-webkit-scrollbar-thumb { background: var(--muted-foreground); }
-.dark .scrollbar-thin::-webkit-scrollbar-thumb:hover { background: var(--muted-foreground); }
+
+.scrollbar-thin::-webkit-scrollbar {
+ width: 6px;
+ height: 6px;
+}
+
+.scrollbar-thin::-webkit-scrollbar-track {
+ background: transparent;
+}
+
+.scrollbar-thin::-webkit-scrollbar-thumb {
+ background: var(--muted-foreground);
+ border-radius: 9999px;
+}
+
+.scrollbar-thin::-webkit-scrollbar-thumb:hover {
+ background: var(--muted-foreground);
+}
+
+.dark .scrollbar-thin::-webkit-scrollbar-thumb {
+ background: var(--muted-foreground);
+}
+
+.dark .scrollbar-thin::-webkit-scrollbar-thumb:hover {
+ background: var(--muted-foreground);
+}
@keyframes fadeUp {
from {
diff --git a/src/components/RepoContributionDistribution.tsx b/src/components/RepoContributionDistribution.tsx
new file mode 100644
index 00000000..da04d64a
--- /dev/null
+++ b/src/components/RepoContributionDistribution.tsx
@@ -0,0 +1,334 @@
+"use client";
+
+import { useEffect, useMemo, useState } from "react";
+import { useTheme } from "./ThemeContext";
+import {
+ Bar,
+ BarChart,
+ CartesianGrid,
+ Cell,
+ Pie,
+ PieChart,
+ ResponsiveContainer,
+ Tooltip,
+ XAxis,
+ YAxis,
+} from "recharts";
+
+type RepoChartItem = {
+ name: string;
+ commits: number;
+ percentage: number;
+};
+
+type ChartType = "pie" | "bar";
+
+type ChartTooltipPayload = {
+ payload?: RepoChartItem;
+ value?: number;
+};
+
+
+
+function asRecord(value: unknown): Record {
+ return typeof value === "object" && value !== null ? (value as Record) : {};
+}
+
+function getStringValue(record: Record, keys: string[], fallback: string) {
+ for (const key of keys) {
+ const value = record[key];
+
+ if (typeof value === "string" && value.trim().length > 0) {
+ return value;
+ }
+ }
+
+ return fallback;
+}
+
+function getNumberValue(record: Record, keys: string[]) {
+ for (const key of keys) {
+ const value = record[key];
+
+ if (typeof value === "number" && Number.isFinite(value)) {
+ return value;
+ }
+
+ if (typeof value === "string" && value.trim() !== "" && Number.isFinite(Number(value))) {
+ return Number(value);
+ }
+ }
+
+ return 0;
+}
+
+function getRepoArray(payload: unknown): unknown[] {
+ if (Array.isArray(payload)) return payload;
+
+ const record = asRecord(payload);
+
+ if (Array.isArray(record.repos)) return record.repos;
+ if (Array.isArray(record.data)) return record.data;
+ if (Array.isArray(record.repositories)) return record.repositories;
+
+ return [];
+}
+
+function normalizeRepos(payload: unknown): RepoChartItem[] {
+ const repos = getRepoArray(payload);
+
+ const mapped = repos
+ .map((item) => {
+ const repo = asRecord(item);
+
+ const name = getStringValue(
+ repo,
+ ["name", "repo", "repository", "full_name", "fullName"],
+ "Unknown repository"
+ );
+
+ const commits = getNumberValue(repo, [
+ "commits",
+ "commitCount",
+ "contributions",
+ "count",
+ "totalCommits",
+ ]);
+
+ return { name, commits };
+ })
+ .filter((repo) => repo.commits > 0)
+ .sort((a, b) => b.commits - a.commits)
+ .slice(0, 8);
+
+ const total = mapped.reduce((sum, repo) => sum + repo.commits, 0);
+
+ return mapped.map((repo) => ({
+ ...repo,
+ percentage: total > 0 ? Number(((repo.commits / total) * 100).toFixed(1)) : 0,
+ }));
+}
+
+function renderPieLabel(props: unknown) {
+ const record = asRecord(props);
+ const payload = asRecord(record.payload);
+ const percentage = payload.percentage;
+
+ return typeof percentage === "number" ? `${percentage}%` : "";
+}
+
+function ChartTooltip({
+ active,
+ payload,
+}: {
+ active?: boolean;
+ payload?: ChartTooltipPayload[];
+}) {
+ if (!active || !payload?.length || !payload[0]?.payload) {
+ return null;
+ }
+
+ const repo = payload[0].payload;
+
+ return (
+
+
{repo.name}
+
{repo.commits} commits
+
{repo.percentage}% contribution
+
+ );
+}
+
+export default function RepoContributionDistribution({ days = 365 }: { days?: number }) {
+ const { theme } = useTheme();
+ const [colors, setColors] = useState([]);
+
+ useEffect(() => {
+ const style = getComputedStyle(document.documentElement);
+ const resolvedColors = [
+ style.getPropertyValue("--accent").trim() || "var(--accent)",
+ style.getPropertyValue("--chart-2").trim() || "var(--chart-2)",
+ style.getPropertyValue("--chart-3").trim() || "var(--chart-3)",
+ style.getPropertyValue("--chart-4").trim() || "var(--chart-4)",
+ style.getPropertyValue("--chart-5").trim() || "var(--chart-5)",
+ style.getPropertyValue("--chart-6").trim() || "var(--chart-6)",
+ style.getPropertyValue("--chart-7").trim() || "var(--chart-7)",
+ style.getPropertyValue("--chart-8").trim() || "var(--chart-8)",
+ ];
+ setColors(resolvedColors);
+ }, [theme]);
+
+ const activeColors = colors.length > 0 ? colors : [
+ "var(--accent)",
+ "var(--chart-2)",
+ "var(--chart-3)",
+ "var(--chart-4)",
+ "var(--chart-5)",
+ "var(--chart-6)",
+ "var(--chart-7)",
+ "var(--chart-8)",
+ ];
+
+ const [data, setData] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState("");
+ const [chartType, setChartType] = useState("pie");
+
+ useEffect(() => {
+ let cancelled = false;
+
+ async function loadRepos() {
+ try {
+ setLoading(true);
+ setError("");
+
+ const response = await fetch(`/api/metrics/repos?days=${days}`);
+
+ if (!response.ok) {
+ throw new Error("Failed to fetch repository metrics.");
+ }
+
+ const payload: unknown = await response.json();
+ const normalized = normalizeRepos(payload);
+
+ if (!cancelled) {
+ setData(normalized);
+ }
+ } catch (err) {
+ if (!cancelled) {
+ setError(err instanceof Error ? err.message : "Failed to load repository chart.");
+ setData([]);
+ }
+ } finally {
+ if (!cancelled) {
+ setLoading(false);
+ }
+ }
+ }
+
+ void loadRepos();
+
+ return () => {
+ cancelled = true;
+ };
+ }, [days]);
+
+ const totalCommits = useMemo(() => data.reduce((sum, repo) => sum + repo.commits, 0), [data]);
+
+ return (
+
+
+
+
+ Repository Contribution Distribution
+
+
+ Repo-wise contribution share based on recent commit activity.
+
+
+
+
+
+
+
+
+
+ {loading ? (
+
+ Loading repository distribution...
+
+ ) : error ? (
+
+ {error}
+
+ ) : data.length === 0 ? (
+
+ No repository contribution data available yet.
+
+ ) : (
+ <>
+
+
+
Repositories
+
{data.length}
+
+
+
Total commits
+
+ {totalCommits}
+
+
+
+
Top repo
+
+ {data[0]?.name}
+
+
+
+
+
+
+ {chartType === "pie" ? (
+
+
+ {data.map((_, index) => (
+ |
+ ))}
+
+ } />
+
+ ) : (
+
+
+
+
+ } />
+
+ {data.map((_, index) => (
+ |
+ ))}
+
+
+ )}
+
+
+ >
+ )}
+
+ );
+}