From b7484e4e4e905772e9dcb1b496faa46490bca572 Mon Sep 17 00:00:00 2001 From: Akshat Neeraj Date: Sun, 17 May 2026 22:26:14 +0530 Subject: [PATCH 1/3] Add language breakdown badges to TopRepos --- src/app/api/metrics/repos/route.ts | 80 ++++++++++++++++++++++++++---- src/components/TopRepos.tsx | 80 ++++++++++++++++++++++++++++++ 2 files changed, 150 insertions(+), 10 deletions(-) diff --git a/src/app/api/metrics/repos/route.ts b/src/app/api/metrics/repos/route.ts index 23f6c06a..ff202aa9 100644 --- a/src/app/api/metrics/repos/route.ts +++ b/src/app/api/metrics/repos/route.ts @@ -8,30 +8,41 @@ import { } from "@/lib/github-accounts"; import { GITHUB_API } from "@/lib/github"; import { supabaseAdmin } from "@/lib/supabase"; +import { url } from "inspector"; export const dynamic = "force-dynamic"; +interface RepoLanguage { + name: string; + percentage: number; +} + interface RepoSummary { name: string; commits: number; + url: string; + languages?: RepoLanguage[]; } interface RepoResponse { repos: RepoSummary[]; days: number; } - function mergeRepoCommits( a: Array<{ name: string; commits: number }>, b: Array<{ name: string; commits: number }> -): Array<{ name: string; commits: number }> { +) { const map = new Map(); + for (const repo of [...a, ...b]) { map.set(repo.name, (map.get(repo.name) ?? 0) + repo.commits); } - return Array.from(map.entries()) - .map(([name, commits]) => ({ name, commits })) - .sort((x, y) => y.commits - x.commits); + + return Array.from(map.entries()).map(([name, commits]) => ({ + name, + commits, + url: "", + })); } async function fetchReposForAccount( @@ -70,13 +81,62 @@ async function fetchReposForAccount( const name = item.repository.full_name; repoMap[name] = (repoMap[name] ?? 0) + 1; } +const repos = await Promise.all( + Object.entries(repoMap).map(async ([name, commits]) => { + const repoRes = await fetch( + `https://api.github.com/repos/${name}`, + { + headers: { + Authorization: `Bearer ${token}`, + }, + } + ); + + const repoData = await repoRes.json(); + + const languageRes = await fetch( + `https://api.github.com/repos/${name}/languages`, + { + headers: { + Authorization: `Bearer ${token}`, + }, + } + ); + + let languages: RepoLanguage[] = []; + + if (languageRes.ok) { + const languageData = await languageRes.json(); + + const totalBytes = Object.values(languageData).reduce( + (sum: number, bytes: any) => sum + Number(bytes), + 0 + ); + + languages = Object.entries(languageData) + .map(([lang, bytes]) => ({ + name: lang, + percentage: Math.round((Number(bytes) / totalBytes) * 100), + })) + .sort((a, b) => b.percentage - a.percentage) + .slice(0, 3); + } + + return { + name, + commits, + url: repoData.html_url, + languages, + }; + }) +); + +repos.sort((a, b) => b.commits - a.commits); + +const topRepos = repos.slice(0, 6); - const repos = Object.entries(repoMap) - .map(([name, commits]) => ({ name, commits })) - .sort((a, b) => b.commits - a.commits) - .slice(0, 6); +return { repos: topRepos, days }; - return { repos, days }; } export async function GET(req: NextRequest) { diff --git a/src/components/TopRepos.tsx b/src/components/TopRepos.tsx index fdd492a4..9f8ada08 100644 --- a/src/components/TopRepos.tsx +++ b/src/components/TopRepos.tsx @@ -20,6 +20,9 @@ export default function TopRepos() { const [minutesAgo, setMinutesAgo] = useState(0); const [healthScores, setHealthScores] = useState>({}); const [healthLoading, setHealthLoading] = useState(true); + const [repoLanguages, setRepoLanguages] = useState< + Record +>({}); const fetchRepos = useCallback(() => { setLoading(true); @@ -37,6 +40,52 @@ export default function TopRepos() { setMinutesAgo(0); }); }, [days, selectedAccount]); + const fetchRepoLanguages = useCallback(async () => { + if (!selectedAccount || repos.length === 0) return; + + try { + const languageResults = await Promise.all( + repos.map(async (repo) => { + try { + const response = await fetch( + `https://api.github.com/repos/${repo.name}/languages` + ); + + if (!response.ok) { + return [repo.name, []]; + } + + const data = await response.json(); + + const total = Object.values(data).reduce( + (sum: number, value: any) => sum + value, + 0 + ); + + if (total === 0) { + return [repo.name, []]; + } + + const topLanguages = Object.entries(data) + .map(([name, bytes]) => ({ + name, + percentage: Math.round((Number(bytes) / total) * 100), + })) + .sort((a, b) => b.percentage - a.percentage) + .slice(0, 3); + + return [repo.name, topLanguages]; + } catch { + return [repo.name, []]; + } + }) + ); + + setRepoLanguages(Object.fromEntries(languageResults)); + } catch (err) { + console.error("Failed to fetch repo languages", err); + } + }, [repos, selectedAccount]); const fetchHealthScores = useCallback(() => { setHealthLoading(true); @@ -65,6 +114,9 @@ export default function TopRepos() { return () => clearInterval(interval); }, [lastUpdated]); + useEffect(() => { + fetchRepoLanguages(); +}, [fetchRepoLanguages]); useEffect(() => { fetchRepos(); @@ -72,6 +124,15 @@ export default function TopRepos() { }, [fetchRepos, fetchHealthScores, selectedAccount]); const maxCommits = repos[0]?.commits ?? 1; + const languageColors: Record = { + TypeScript: "#3178c6", + JavaScript: "#f1e05a", + HTML: "#e34c26", + CSS: "#563d7c", + Python: "#3572A5", + Java: "#b07219", + Shell: "#89e051", +}; return (
@@ -164,6 +225,25 @@ export default function TopRepos() { style={{ width: `${barWidth}%` }} />
+ {repoLanguages[repo.name]?.length > 0 && ( +
+ {repoLanguages[repo.name].map((lang) => ( + + + {lang.name} {lang.percentage}% + + ))} +
+)} ); })} From e5550f2c5978c67c53e0757e2f0e1bfd11d56d9a Mon Sep 17 00:00:00 2001 From: Akshat Neeraj <166710055+Akshat-Neeraj@users.noreply.github.com> Date: Tue, 19 May 2026 11:00:57 +0530 Subject: [PATCH 2/3] Remove accidental inspector import --- src/app/api/metrics/repos/route.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/api/metrics/repos/route.ts b/src/app/api/metrics/repos/route.ts index ff202aa9..af62eca0 100644 --- a/src/app/api/metrics/repos/route.ts +++ b/src/app/api/metrics/repos/route.ts @@ -8,7 +8,6 @@ import { } from "@/lib/github-accounts"; import { GITHUB_API } from "@/lib/github"; import { supabaseAdmin } from "@/lib/supabase"; -import { url } from "inspector"; export const dynamic = "force-dynamic"; From 4e6fdfddd61a359a104b930d65d51cef8401d3da Mon Sep 17 00:00:00 2001 From: Akshat Neeraj Date: Thu, 21 May 2026 18:58:20 +0530 Subject: [PATCH 3/3] fix review comments --- src/app/api/metrics/repos/route.ts | 330 +++++++++++++++-------------- src/components/TopRepos.tsx | 70 +----- 2 files changed, 176 insertions(+), 224 deletions(-) diff --git a/src/app/api/metrics/repos/route.ts b/src/app/api/metrics/repos/route.ts index af62eca0..a29782a8 100644 --- a/src/app/api/metrics/repos/route.ts +++ b/src/app/api/metrics/repos/route.ts @@ -37,176 +37,207 @@ function mergeRepoCommits( map.set(repo.name, (map.get(repo.name) ?? 0) + repo.commits); } - return Array.from(map.entries()).map(([name, commits]) => ({ - name, - commits, - url: "", - })); -} - -async function fetchReposForAccount( - token: string, - githubLogin: string, - days: number -): Promise { - const since = new Date(); - since.setDate(since.getDate() - days); - const sinceStr = since.toISOString().slice(0, 10); - - const searchRes = await fetch( - `${GITHUB_API}/search/commits?q=author:${githubLogin}+author-date:>=${sinceStr}&per_page=100&sort=author-date&order=desc`, - { - headers: { - Authorization: `Bearer ${token}`, - Accept: "application/vnd.github+json", - }, - cache: "no-store", - } - ); - - if (!searchRes.ok) { - throw new Error("GitHub API error"); - } - - const data = (await searchRes.json()) as { - items: Array<{ - repository: { full_name: string; html_url: string }; - commit: { author: { date: string } }; - }>; - }; - - const repoMap: Record = {}; - for (const item of data.items) { - const name = item.repository.full_name; - repoMap[name] = (repoMap[name] ?? 0) + 1; - } -const repos = await Promise.all( - Object.entries(repoMap).map(async ([name, commits]) => { - const repoRes = await fetch( - `https://api.github.com/repos/${name}`, + return Array.from(map.entries()) + .map(([name, commits]) => ({ + name, + commits, + url: "", + })) + .sort((a, b) => b.commits - a.commits); + + + async function fetchReposForAccount( + token: string, + githubLogin: string, + days: number + ): Promise { + const since = new Date(); + since.setDate(since.getDate() - days); + const sinceStr = since.toISOString().slice(0, 10); + + const searchRes = await fetch( + `${GITHUB_API}/search/commits?q=author:${githubLogin}+author-date:>=${sinceStr}&per_page=100&sort=author-date&order=desc`, { headers: { Authorization: `Bearer ${token}`, + Accept: "application/vnd.github+json", }, + cache: "no-store", } ); - const repoData = await repoRes.json(); + if (!searchRes.ok) { + throw new Error("GitHub API error"); + } - const languageRes = await fetch( - `https://api.github.com/repos/${name}/languages`, - { - headers: { - Authorization: `Bearer ${token}`, - }, - } - ); + const data = (await searchRes.json()) as { + items: Array<{ + repository: { full_name: string; html_url: string }; + commit: { author: { date: string } }; + }>; + }; - let languages: RepoLanguage[] = []; + const repoMap: Record = {}; + for (const item of data.items) { + const name = item.repository.full_name; + repoMap[name] = (repoMap[name] ?? 0) + 1; + } + + const repos = await Promise.all( + Object.entries(repoMap).map(async ([name, commits]) => { + const repoRes = await fetch( + `https://api.github.com/repos/${name}`, + { + headers: { + Authorization: `Bearer ${token}`, + }, + } + ); - if (languageRes.ok) { - const languageData = await languageRes.json(); + const repoData = await repoRes.json(); - const totalBytes = Object.values(languageData).reduce( - (sum: number, bytes: any) => sum + Number(bytes), - 0 + const languageRes = await fetch( + `https://api.github.com/repos/${name}/languages`, + { + headers: { + Authorization: `Bearer ${token}`, + }, + } ); - languages = Object.entries(languageData) - .map(([lang, bytes]) => ({ - name: lang, - percentage: Math.round((Number(bytes) / totalBytes) * 100), - })) - .sort((a, b) => b.percentage - a.percentage) - .slice(0, 3); - } + let languages: RepoLanguage[] = []; - return { - name, - commits, - url: repoData.html_url, - languages, - }; - }) -); + if (languageRes.ok) { + const languageData = await languageRes.json(); + + const totalBytes = Object.values(languageData).reduce( + (sum: number, bytes: any) => sum + Number(bytes), + 0 + ); + + languages = Object.entries(languageData) + .map(([lang, bytes]) => ({ + name: lang, + percentage: Math.round((Number(bytes) / totalBytes) * 100), + })) + .sort((a, b) => b.percentage - a.percentage) + .slice(0, 3); + } -repos.sort((a, b) => b.commits - a.commits); + return { + name, + commits, + url: repoData.html_url, + languages, + }; + }) + ); -const topRepos = repos.slice(0, 6); + repos.sort((a, b) => b.commits - a.commits); -return { repos: topRepos, days }; + const topRepos = repos.slice(0, 6); + return { repos: topRepos, days }; +} } + export async function GET(req: NextRequest) { + const session = await getServerSession(authOptions); + if (!session?.accessToken || !session.githubLogin) { + return Response.json({ error: "Unauthorized" }, { status: 401 }); + } -export async function GET(req: NextRequest) { - const session = await getServerSession(authOptions); - if (!session?.accessToken || !session.githubLogin) { - return Response.json({ error: "Unauthorized" }, { status: 401 }); - } + const days = Number(req.nextUrl.searchParams.get("days")) || 30; + const accountId = req.nextUrl.searchParams.get("accountId"); + + if (!accountId) { + try { + const result = await fetchReposForAccount( + session.accessToken, + session.githubLogin, + days + ); + return Response.json(result); + } catch { + return Response.json({ error: "GitHub API error" }, { status: 502 }); + } + } - const days = Number(req.nextUrl.searchParams.get("days")) || 30; - const accountId = req.nextUrl.searchParams.get("accountId"); + if (!session.githubId) { + return Response.json({ error: "Unauthorized" }, { status: 401 }); + } - if (!accountId) { - try { - const result = await fetchReposForAccount( - session.accessToken, - session.githubLogin, - days - ); - return Response.json(result); - } catch { - return Response.json({ error: "GitHub API error" }, { status: 502 }); + const { data: userRow } = await supabaseAdmin + .from("users") + .select("id") + .eq("github_id", session.githubId) + .single(); + + if (!userRow) { + return Response.json({ error: "Unauthorized" }, { status: 401 }); } - } - if (!session.githubId) { - return Response.json({ error: "Unauthorized" }, { status: 401 }); - } + if (accountId === "combined") { + const accounts = await getAllAccounts( + { + token: session.accessToken, + githubId: session.githubId, + githubLogin: session.githubLogin, + }, + userRow.id + ); - const { data: userRow } = await supabaseAdmin - .from("users") - .select("id") - .eq("github_id", session.githubId) - .single(); + const results = await Promise.allSettled( + accounts.map((account) => + fetchReposForAccount(account.token, account.githubLogin, days) + ) + ); - if (!userRow) { - return Response.json({ error: "Unauthorized" }, { status: 401 }); - } + const merged = mergeMetrics(results, (a, b) => ({ + days: a.days, + repos: mergeRepoCommits(a.repos, b.repos), + })); - if (accountId === "combined") { - const accounts = await getAllAccounts( - { - token: session.accessToken, - githubId: session.githubId, - githubLogin: session.githubLogin, - }, - userRow.id - ); + if (!merged) { + return Response.json({ error: "GitHub API error" }, { status: 502 }); + } - const results = await Promise.allSettled( - accounts.map((account) => - fetchReposForAccount(account.token, account.githubLogin, days) - ) - ); + return Response.json(merged); + } - const merged = mergeMetrics(results, (a, b) => ({ - days: a.days, - repos: mergeRepoCommits(a.repos, b.repos), - })); + if (accountId === session.githubId) { + try { + const result = await fetchReposForAccount( + session.accessToken, + session.githubLogin, + days + ); + return Response.json(result); + } catch { + return Response.json({ error: "GitHub API error" }, { status: 502 }); + } + } - if (!merged) { - return Response.json({ error: "GitHub API error" }, { status: 502 }); + const accountToken = await getAccountToken(userRow.id, accountId); + + if (!accountToken) { + return Response.json({ error: "Account not found" }, { status: 404 }); } - return Response.json(merged); - } + const { data: accountRow } = await supabaseAdmin + .from("user_github_accounts") + .select("github_login") + .eq("user_id", userRow.id) + .eq("github_id", accountId) + .single(); + + if (!accountRow?.github_login) { + return Response.json({ error: "Account not found" }, { status: 404 }); + } - if (accountId === session.githubId) { try { const result = await fetchReposForAccount( - session.accessToken, - session.githubLogin, + accountToken, + accountRow.github_login, days ); return Response.json(result); @@ -214,32 +245,3 @@ export async function GET(req: NextRequest) { return Response.json({ error: "GitHub API error" }, { status: 502 }); } } - - const accountToken = await getAccountToken(userRow.id, accountId); - - if (!accountToken) { - return Response.json({ error: "Account not found" }, { status: 404 }); - } - - const { data: accountRow } = await supabaseAdmin - .from("user_github_accounts") - .select("github_login") - .eq("user_id", userRow.id) - .eq("github_id", accountId) - .single(); - - if (!accountRow?.github_login) { - return Response.json({ error: "Account not found" }, { status: 404 }); - } - - try { - const result = await fetchReposForAccount( - accountToken, - accountRow.github_login, - days - ); - return Response.json(result); - } catch { - return Response.json({ error: "GitHub API error" }, { status: 502 }); - } -} diff --git a/src/components/TopRepos.tsx b/src/components/TopRepos.tsx index 9f8ada08..b6ac88ff 100644 --- a/src/components/TopRepos.tsx +++ b/src/components/TopRepos.tsx @@ -5,9 +5,13 @@ import { useAccount } from "@/components/AccountContext"; import type { RepoHealthScore } from "@/types/repo-health"; interface Repo { - name: string; - commits: number; - url: string; + name: string; + commits: number; + url: string; + languages?: { + name: string; + percentage: number; + }[]; } export default function TopRepos() { @@ -20,9 +24,6 @@ export default function TopRepos() { const [minutesAgo, setMinutesAgo] = useState(0); const [healthScores, setHealthScores] = useState>({}); const [healthLoading, setHealthLoading] = useState(true); - const [repoLanguages, setRepoLanguages] = useState< - Record ->({}); const fetchRepos = useCallback(() => { setLoading(true); @@ -39,54 +40,7 @@ export default function TopRepos() { setLastUpdated(new Date()); setMinutesAgo(0); }); - }, [days, selectedAccount]); - const fetchRepoLanguages = useCallback(async () => { - if (!selectedAccount || repos.length === 0) return; - - try { - const languageResults = await Promise.all( - repos.map(async (repo) => { - try { - const response = await fetch( - `https://api.github.com/repos/${repo.name}/languages` - ); - - if (!response.ok) { - return [repo.name, []]; - } - - const data = await response.json(); - - const total = Object.values(data).reduce( - (sum: number, value: any) => sum + value, - 0 - ); - - if (total === 0) { - return [repo.name, []]; - } - - const topLanguages = Object.entries(data) - .map(([name, bytes]) => ({ - name, - percentage: Math.round((Number(bytes) / total) * 100), - })) - .sort((a, b) => b.percentage - a.percentage) - .slice(0, 3); - - return [repo.name, topLanguages]; - } catch { - return [repo.name, []]; - } - }) - ); - - setRepoLanguages(Object.fromEntries(languageResults)); - } catch (err) { - console.error("Failed to fetch repo languages", err); - } - }, [repos, selectedAccount]); - + }, [days, selectedAccount]); const fetchHealthScores = useCallback(() => { setHealthLoading(true); const accountParam = selectedAccount !== null @@ -114,10 +68,6 @@ export default function TopRepos() { return () => clearInterval(interval); }, [lastUpdated]); - useEffect(() => { - fetchRepoLanguages(); -}, [fetchRepoLanguages]); - useEffect(() => { fetchRepos(); fetchHealthScores(); @@ -225,9 +175,9 @@ export default function TopRepos() { style={{ width: `${barWidth}%` }} /> - {repoLanguages[repo.name]?.length > 0 && ( + {repo.languages && repo.languages.length > 0 && (
- {repoLanguages[repo.name].map((lang) => ( + {repo.languages.map((lang) => (