diff --git a/.gitignore b/.gitignore
index 236cc8a..8d750e3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -32,3 +32,4 @@ Thumbs.db
*.swp
desktop.ini
+.env.local*
diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx
index 9dcc11e..ec0d99f 100644
--- a/src/app/dashboard/page.tsx
+++ b/src/app/dashboard/page.tsx
@@ -1,5 +1,6 @@
import ContributionGraph from "@/components/ContributionGraph";
import ContributionHeatmap from "@/components/ContributionHeatmap";
+import RepoContributionDistribution from "@/components/RepoContributionDistribution";
import PRMetrics from "@/components/PRMetrics";
import PRBreakdownChart from "@/components/PRBreakdownChart";
import GoalTracker from "@/components/GoalTracker";
@@ -62,6 +63,7 @@ export default async function DashboardPage() {
+
diff --git a/src/components/RepoContributionDistribution.tsx b/src/components/RepoContributionDistribution.tsx
new file mode 100644
index 0000000..82a362c
--- /dev/null
+++ b/src/components/RepoContributionDistribution.tsx
@@ -0,0 +1,316 @@
+"use client";
+
+import { useEffect, useMemo, useState } from "react";
+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;
+};
+
+// First color follows the active theme accent.
+// Remaining colors are chart-specific categorical hues used to distinguish repositories;
+// there are no matching project semantic tokens for every chart slice/bar.
+const COLORS = [
+ "var(--accent)",
+ "#22c55e",
+ "#f97316",
+ "#06b6d4",
+ "#ec4899",
+ "#eab308",
+ "#8b5cf6",
+ "#14b8a6",
+];
+
+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 [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) => (
+ |
+ ))}
+
+
+ )}
+
+
+ >
+ )}
+
+ );
+}