From 77516ea133d706db79f9efeb4a0b4abd24f5d9a0 Mon Sep 17 00:00:00 2001 From: PRASHANT S BISHT Date: Tue, 19 May 2026 16:15:52 +0530 Subject: [PATCH 1/6] Add repository contribution distribution chart --- src/app/dashboard/page.tsx | 11 + .../RepoContributionDistribution.tsx | 239 ++++++++++++++++++ 2 files changed, 250 insertions(+) create mode 100644 src/components/RepoContributionDistribution.tsx diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index aac71d790..ee30eaece 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/components/RepoContributionDistribution.tsx b/src/components/RepoContributionDistribution.tsx new file mode 100644 index 000000000..9b7b5a552 --- /dev/null +++ b/src/components/RepoContributionDistribution.tsx @@ -0,0 +1,239 @@ +"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"; + +const COLORS = [ + "#6366f1", + "#22c55e", + "#f97316", + "#06b6d4", + "#ec4899", + "#eab308", + "#8b5cf6", + "#14b8a6", +]; + +function normalizeRepos(payload: any): RepoChartItem[] { + const repos = Array.isArray(payload) ? payload : payload?.repos || payload?.data || []; + + const mapped = repos + .map((repo: any) => { + const name = + repo.name || + repo.repo || + repo.repository || + repo.full_name || + repo.fullName || + "Unknown repository"; + + const commits = + Number( + repo.commits ?? + repo.commitCount ?? + repo.contributions ?? + repo.count ?? + repo.totalCommits ?? + 0 + ) || 0; + + return { name, commits }; + }) + .filter((repo: { commits: number }) => repo.commits > 0) + .sort((a: { commits: number }, b: { commits: number }) => b.commits - a.commits) + .slice(0, 8); + + const total = mapped.reduce((sum: number, repo: { commits: number }) => sum + repo.commits, 0); + + return mapped.map((repo: { name: string; commits: number }) => ({ + ...repo, + percentage: total > 0 ? Number(((repo.commits / total) * 100).toFixed(1)) : 0, + })); +} + +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 res = await fetch(`/api/metrics/repos?days=${days}`); + + if (!res.ok) { + throw new Error("Failed to fetch repository metrics."); + } + + const payload = await res.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); + } + } + } + + 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" ? ( + + `${percentage}%`} + > + {data.map((_, index) => ( + + ))} + + [ + `${value} commits (${props.payload.percentage}%)`, + props.payload.name, + ]} + /> + + ) : ( + + + + + [ + `${value} commits (${props.payload.percentage}%)`, + props.payload.name, + ]} + /> + + {data.map((_, index) => ( + + ))} + + + )} + +
+ + )} +
+ ); +} From 394bb3d94be251fb6f97e067bf8cd88da9ec9f0a Mon Sep 17 00:00:00 2001 From: PRASHANT S BISHT Date: Tue, 19 May 2026 16:20:44 +0530 Subject: [PATCH 2/6] Add repository contribution distribution chart --- .../RepoContributionDistribution.tsx | 120 ++++++++++++------ 1 file changed, 79 insertions(+), 41 deletions(-) diff --git a/src/components/RepoContributionDistribution.tsx b/src/components/RepoContributionDistribution.tsx index 9b7b5a552..4948e349f 100644 --- a/src/components/RepoContributionDistribution.tsx +++ b/src/components/RepoContributionDistribution.tsx @@ -33,43 +33,84 @@ const COLORS = [ "#14b8a6", ]; -function normalizeRepos(payload: any): RepoChartItem[] { - const repos = Array.isArray(payload) ? payload : payload?.repos || payload?.data || []; +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((repo: any) => { - const name = - repo.name || - repo.repo || - repo.repository || - repo.full_name || - repo.fullName || - "Unknown repository"; - - const commits = - Number( - repo.commits ?? - repo.commitCount ?? - repo.contributions ?? - repo.count ?? - repo.totalCommits ?? - 0 - ) || 0; + .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: { commits: number }) => repo.commits > 0) - .sort((a: { commits: number }, b: { commits: number }) => b.commits - a.commits) + .filter((repo) => repo.commits > 0) + .sort((a, b) => b.commits - a.commits) .slice(0, 8); - const total = mapped.reduce((sum: number, repo: { commits: number }) => sum + repo.commits, 0); + const total = mapped.reduce((sum, repo) => sum + repo.commits, 0); - return mapped.map((repo: { name: string; commits: number }) => ({ + return mapped.map((repo) => ({ ...repo, percentage: total > 0 ? Number(((repo.commits / total) * 100).toFixed(1)) : 0, })); } +function renderPieLabel(props: { payload?: RepoChartItem }) { + return props.payload ? `${props.payload.percentage}%` : ""; +} + export default function RepoContributionDistribution({ days = 365 }: { days?: number }) { const [data, setData] = useState([]); const [loading, setLoading] = useState(true); @@ -84,13 +125,13 @@ export default function RepoContributionDistribution({ days = 365 }: { days?: nu setLoading(true); setError(""); - const res = await fetch(`/api/metrics/repos?days=${days}`); + const response = await fetch(`/api/metrics/repos?days=${days}`); - if (!res.ok) { + if (!response.ok) { throw new Error("Failed to fetch repository metrics."); } - const payload = await res.json(); + const payload: unknown = await response.json(); const normalized = normalizeRepos(payload); if (!cancelled) { @@ -108,17 +149,14 @@ export default function RepoContributionDistribution({ days = 365 }: { days?: nu } } - loadRepos(); + void loadRepos(); return () => { cancelled = true; }; }, [days]); - const totalCommits = useMemo( - () => data.reduce((sum, repo) => sum + repo.commits, 0), - [data] - ); + const totalCommits = useMemo(() => data.reduce((sum, repo) => sum + repo.commits, 0), [data]); return (
@@ -192,17 +230,17 @@ export default function RepoContributionDistribution({ days = 365 }: { days?: nu innerRadius={60} outerRadius={105} paddingAngle={2} - label={({ percentage }) => `${percentage}%`} + label={renderPieLabel} > {data.map((_, index) => ( ))} [ - `${value} commits (${props.payload.percentage}%)`, - props.payload.name, - ]} + formatter={(value, _name, item) => { + const payload = item.payload as RepoChartItem; + return [`${value} commits (${payload.percentage}%)`, payload.name]; + }} /> ) : ( @@ -218,10 +256,10 @@ export default function RepoContributionDistribution({ days = 365 }: { days?: nu /> [ - `${value} commits (${props.payload.percentage}%)`, - props.payload.name, - ]} + formatter={(value, _name, item) => { + const payload = item.payload as RepoChartItem; + return [`${value} commits (${payload.percentage}%)`, payload.name]; + }} /> {data.map((_, index) => ( From ec94cb1604f44a220914a6c34f9146d0d8772f62 Mon Sep 17 00:00:00 2001 From: PRASHANT S BISHT Date: Tue, 19 May 2026 17:40:34 +0530 Subject: [PATCH 3/6] Fix repository chart theme styles and env tracking --- .../RepoContributionDistribution.tsx | 100 ++++++++++++------ 1 file changed, 68 insertions(+), 32 deletions(-) diff --git a/src/components/RepoContributionDistribution.tsx b/src/components/RepoContributionDistribution.tsx index 4948e349f..49884b6ad 100644 --- a/src/components/RepoContributionDistribution.tsx +++ b/src/components/RepoContributionDistribution.tsx @@ -22,8 +22,13 @@ type RepoChartItem = { type ChartType = "pie" | "bar"; +type ChartTooltipPayload = { + payload?: RepoChartItem; + value?: number; +}; + const COLORS = [ - "#6366f1", + "var(--accent)", "#22c55e", "#f97316", "#06b6d4", @@ -40,23 +45,28 @@ function asRecord(value: unknown): 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; } @@ -107,8 +117,34 @@ function normalizeRepos(payload: unknown): RepoChartItem[] { })); } -function renderPieLabel(props: { payload?: RepoChartItem }) { - return props.payload ? `${props.payload.percentage}%` : ""; +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 }) { @@ -159,21 +195,25 @@ export default function RepoContributionDistribution({ days = 365 }: { days?: nu const totalCommits = useMemo(() => data.reduce((sum, repo) => sum + repo.commits, 0), [data]); return ( -
+
-

Repository Contribution Distribution

-

+

+ Repository Contribution Distribution +

+

Repo-wise contribution share based on recent commit activity.

-
+
{loading ? ( -
+
Loading repository distribution...
) : error ? ( @@ -199,23 +241,27 @@ export default function RepoContributionDistribution({ days = 365 }: { days?: nu {error}
) : data.length === 0 ? ( -
+
No repository contribution data available yet.
) : ( <>
-
-

Repositories

-

{data.length}

+
+

Repositories

+

{data.length}

-
-

Total commits

-

{totalCommits}

+
+

Total commits

+

+ {totalCommits} +

-
-

Top repo

-

{data[0]?.name}

+
+

Top repo

+

+ {data[0]?.name} +

@@ -236,12 +282,7 @@ export default function RepoContributionDistribution({ days = 365 }: { days?: nu ))} - { - const payload = item.payload as RepoChartItem; - return [`${value} commits (${payload.percentage}%)`, payload.name]; - }} - /> + } /> ) : ( @@ -255,12 +296,7 @@ export default function RepoContributionDistribution({ days = 365 }: { days?: nu height={70} /> - { - const payload = item.payload as RepoChartItem; - return [`${value} commits (${payload.percentage}%)`, payload.name]; - }} - /> + } /> {data.map((_, index) => ( From e1ecc39e7dace16b85c9f2f9831f69fa088c6c62 Mon Sep 17 00:00:00 2001 From: PRASHANT S BISHT Date: Wed, 20 May 2026 13:23:51 +0530 Subject: [PATCH 4/6] Address repository chart review fixes --- src/components/RepoContributionDistribution.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/RepoContributionDistribution.tsx b/src/components/RepoContributionDistribution.tsx index 49884b6ad..82a362ca2 100644 --- a/src/components/RepoContributionDistribution.tsx +++ b/src/components/RepoContributionDistribution.tsx @@ -27,6 +27,9 @@ type ChartTooltipPayload = { 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", @@ -237,7 +240,7 @@ export default function RepoContributionDistribution({ days = 365 }: { days?: nu Loading repository distribution...
) : error ? ( -
+
{error}
) : data.length === 0 ? ( From c88d822eb28df8cb929b1a672de54fa31b7a66cb Mon Sep 17 00:00:00 2001 From: PRASHANT S BISHT Date: Thu, 21 May 2026 23:47:19 +0530 Subject: [PATCH 5/6] Replace raw error colors with theme variables --- src/components/RepoContributionDistribution.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/RepoContributionDistribution.tsx b/src/components/RepoContributionDistribution.tsx index 82a362ca2..84a80f278 100644 --- a/src/components/RepoContributionDistribution.tsx +++ b/src/components/RepoContributionDistribution.tsx @@ -240,7 +240,7 @@ export default function RepoContributionDistribution({ days = 365 }: { days?: nu Loading repository distribution...
) : error ? ( -
+
{error}
) : data.length === 0 ? ( From 3b11be9fbb0ce999da928b8be725416bdddc833f Mon Sep 17 00:00:00 2001 From: PRASHANT S BISHT Date: Fri, 22 May 2026 19:08:06 +0530 Subject: [PATCH 6/6] style(chart): resolve recharts colors dynamically from theme-aware CSS variables --- .gitignore | 1 + src/app/globals.css | 46 +++++++++++++++--- .../RepoContributionDistribution.tsx | 48 +++++++++++++------ 3 files changed, 74 insertions(+), 21 deletions(-) diff --git a/.gitignore b/.gitignore index 1158f0893..418741edb 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/globals.css b/src/app/globals.css index e85566853..198ca5465 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 index 84a80f278..da04d64ac 100644 --- a/src/components/RepoContributionDistribution.tsx +++ b/src/components/RepoContributionDistribution.tsx @@ -1,6 +1,7 @@ "use client"; import { useEffect, useMemo, useState } from "react"; +import { useTheme } from "./ThemeContext"; import { Bar, BarChart, @@ -27,19 +28,7 @@ type ChartTooltipPayload = { 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) : {}; @@ -151,6 +140,35 @@ function ChartTooltip({ } 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(""); @@ -282,7 +300,7 @@ export default function RepoContributionDistribution({ days = 365 }: { days?: nu label={renderPieLabel} > {data.map((_, index) => ( - + ))} } /> @@ -302,7 +320,7 @@ export default function RepoContributionDistribution({ days = 365 }: { days?: nu } /> {data.map((_, index) => ( - + ))}