From abee4e65ebdc013e0898fd2236c8dae64930ff28 Mon Sep 17 00:00:00 2001 From: PRASHANT S BISHT Date: Tue, 19 May 2026 16:15:52 +0530 Subject: [PATCH 1/4] Add repository contribution distribution chart --- ".env.local\303\270" | 10 + src/app/dashboard/page.tsx | 2 + .../RepoContributionDistribution.tsx | 239 ++++++++++++++++++ tsconfig.json | 2 +- 4 files changed, 252 insertions(+), 1 deletion(-) create mode 100644 ".env.local\303\270" create mode 100644 src/components/RepoContributionDistribution.tsx diff --git "a/.env.local\303\270" "b/.env.local\303\270" new file mode 100644 index 00000000..cc8b793d --- /dev/null +++ "b/.env.local\303\270" @@ -0,0 +1,10 @@ +NEXT_PUBLIC_SUPABASE_URL=https://dummy.supabase.co +NEXT_PUBLIC_SUPABASE_ANON_KEY=dummy-anon-key +SUPABASE_SERVICE_ROLE_KEY=dummy-service-role-key + +NEXTAUTH_URL=http://localhost:3000 +NEXTAUTH_SECRET=local-dev-secret + +GITHUB_ID=dummy-github-id +GITHUB_SECRET=dummy-github-secret +GITHUB_WEBHOOK_SECRET=dummy-webhook-secret diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index 23f9881f..844a117f 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"; @@ -56,6 +57,7 @@ export default async function DashboardPage() {
+
diff --git a/src/components/RepoContributionDistribution.tsx b/src/components/RepoContributionDistribution.tsx new file mode 100644 index 00000000..9b7b5a55 --- /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) => ( + + ))} + + + )} + +
+ + )} +
+ ); +} diff --git a/tsconfig.json b/tsconfig.json index d0fd9315..9b708bb6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,7 +14,7 @@ "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, - "jsx": "react-jsx", + "jsx": "preserve", "incremental": true, "plugins": [ { From 375efd94191f361f1e1a2218264f09ee92e7cf66 Mon Sep 17 00:00:00 2001 From: PRASHANT S BISHT Date: Tue, 19 May 2026 16:20:44 +0530 Subject: [PATCH 2/4] 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 9b7b5a55..4948e349 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 4dab49503a5ce8249db795aee66d0bb53231fc38 Mon Sep 17 00:00:00 2001 From: PRASHANT S BISHT Date: Tue, 19 May 2026 17:40:34 +0530 Subject: [PATCH 3/4] Fix repository chart theme styles and env tracking --- ".env.local\303\270" | 10 -- .gitignore | 1 + .../RepoContributionDistribution.tsx | 100 ++++++++++++------ 3 files changed, 69 insertions(+), 42 deletions(-) delete mode 100644 ".env.local\303\270" diff --git "a/.env.local\303\270" "b/.env.local\303\270" deleted file mode 100644 index cc8b793d..00000000 --- "a/.env.local\303\270" +++ /dev/null @@ -1,10 +0,0 @@ -NEXT_PUBLIC_SUPABASE_URL=https://dummy.supabase.co -NEXT_PUBLIC_SUPABASE_ANON_KEY=dummy-anon-key -SUPABASE_SERVICE_ROLE_KEY=dummy-service-role-key - -NEXTAUTH_URL=http://localhost:3000 -NEXTAUTH_SECRET=local-dev-secret - -GITHUB_ID=dummy-github-id -GITHUB_SECRET=dummy-github-secret -GITHUB_WEBHOOK_SECRET=dummy-webhook-secret diff --git a/.gitignore b/.gitignore index 236cc8a3..8d750e30 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,4 @@ Thumbs.db *.swp desktop.ini +.env.local* diff --git a/src/components/RepoContributionDistribution.tsx b/src/components/RepoContributionDistribution.tsx index 4948e349..49884b6a 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 3a216078c8dd98e578cbb1f5738d0524ac599da7 Mon Sep 17 00:00:00 2001 From: PRASHANT S BISHT Date: Wed, 20 May 2026 13:23:51 +0530 Subject: [PATCH 4/4] 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 49884b6a..82a362ca 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 ? (