diff --git a/src/app/api/metrics/repos/route.ts b/src/app/api/metrics/repos/route.ts index 399a20b7..07012088 100644 --- a/src/app/api/metrics/repos/route.ts +++ b/src/app/api/metrics/repos/route.ts @@ -18,31 +18,67 @@ import { resolveAppUser } from "@/lib/resolve-user"; export const dynamic = "force-dynamic"; +interface RepoLanguage { + name: string; + percentage: number; +} + interface RepoSummary { name: string; commits: number; description: string | null; + languages?: RepoLanguage[]; } interface RepoResponse { repos: RepoSummary[]; days: number; } - function mergeRepoCommits( - a: Array<{ name: string; commits: number; description: string | null }>, - b: Array<{ name: string; commits: number; description: string | null }> -): Array<{ name: string; commits: number; description: string | null }> { - const map = new Map(); + a: Array<{ + name: string; + commits: number; + description: string | null; + languages?: RepoLanguage[]; + }>, + b: Array<{ + name: string; + commits: number; + description: string | null; + languages?: RepoLanguage[]; + }> +): Array<{ + name: string; + commits: number; + description: string | null; + languages?: RepoLanguage[]; +}> { + const map = new Map< + string, + { + commits: number; + description: string | null; + languages?: RepoLanguage[]; + } + >(); + for (const repo of [...a, ...b]) { const existing = map.get(repo.name); + map.set(repo.name, { commits: (existing?.commits ?? 0) + repo.commits, description: existing?.description ?? repo.description, + languages: existing?.languages ?? repo.languages, }); } + return Array.from(map.entries()) - .map(([name, { commits, description }]) => ({ name, commits, description })) + .map(([name, { commits, description, languages }]) => ({ + name, + commits, + description, + languages, + })) .sort((x, y) => y.commits - x.commits); } @@ -85,137 +121,201 @@ async function fetchReposForAccount( const data = (await searchRes.json()) as { items: Array<{ - repository: { full_name: string; html_url: string; description: string | null }; + repository: { + full_name: string; + html_url: string; + description: string | null; + }; commit: { author: { date: string } }; }>; }; - const repoMap: Record = {}; + const repoMap: Record< + string, + { + commits: number; + description: string | null; + } + > = {}; + for (const item of data.items) { const name = item.repository.full_name; + repoMap[name] = { commits: (repoMap[name]?.commits ?? 0) + 1, description: item.repository.description, }; } - const repos = Object.entries(repoMap) - .map(([name, { commits, description }]) => ({ name, commits, description })) - .sort((a, b) => b.commits - a.commits) - .slice(0, 6); + const repos = await Promise.all( + Object.entries(repoMap).map(async ([name, { commits, description }]) => { + 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(); - return { repos, days }; + 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, + description, + languages, + }; + }) + ); + + repos.sort((a, b) => b.commits - a.commits); + + 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 }); - } + const session = await getServerSession(authOptions); + if (!session?.accessToken || !session.githubLogin) { + return Response.json({ error: "Unauthorized" }, { status: 401 }); + } - const daysParam = req.nextUrl.searchParams.get("days"); - const parsedDays = daysParam ? parseInt(daysParam, 10) : NaN; - const days = isNaN(parsedDays) ? 30 : Math.max(1, Math.min(365, parsedDays)); - const accountId = req.nextUrl.searchParams.get("accountId"); - const bypass = isMetricsCacheBypassed(req); + 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, - { bypass, userId: session.githubId ?? session.githubLogin } - ); - return Response.json(result); - } catch { - return Response.json({ error: "GitHub API error" }, { status: 502 }); - } + if (!accountId) { + try { +const result = await fetchReposForAccount( + session.accessToken, + session.githubLogin, + days, + { + bypass: false, + userId: session.githubId ?? session.githubLogin, } +); + return Response.json(result); + } catch { + return Response.json({ error: "GitHub API error" }, { status: 502 }); + } + } - if (!session.githubId) { - return Response.json({ error: "Unauthorized" }, { status: 401 }); - } + if (!session.githubId) { + return Response.json({ error: "Unauthorized" }, { status: 401 }); + } - const userRow = await resolveAppUser(session.githubId, session.githubLogin); + 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 (!userRow) { + 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 + ); - if (accountId === "combined") { - const accounts = await getAllAccounts( + const results = await Promise.allSettled( + accounts.map((account) => + fetchReposForAccount( + account.token, + account.githubLogin, + days, { - token: session.accessToken, - githubId: session.githubId, - githubLogin: session.githubLogin, - }, - userRow.id - ); - - const results = await Promise.allSettled( - accounts.map((account) => - fetchReposForAccount(account.token, account.githubLogin, days, { - bypass, - userId: account.githubId, - }) - ) - ); + bypass: false, + userId: account.githubId, + } + ) + ) +); - const merged = mergeMetrics(results, (a, b) => ({ - days: a.days, - repos: mergeRepoCommits(a.repos, b.repos), - })); + const merged = mergeMetrics(results, (a, b) => ({ + days: a.days, + repos: mergeRepoCommits(a.repos, b.repos), + })); - if (!merged) { - return Response.json({ error: "GitHub API error" }, { status: 502 }); + if (!merged) { + return Response.json({ error: "GitHub API error" }, { status: 502 }); + } + + return Response.json(merged); } - return Response.json(merged); + if (accountId === session.githubId) { + try { + const result = await fetchReposForAccount( + session.accessToken, + session.githubLogin, + days, + { + bypass: false, + userId: session.githubId ?? session.githubLogin, } +); + return Response.json(result); + } catch { + 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 }); + } - if (accountId === session.githubId) { try { const result = await fetchReposForAccount( - session.accessToken, - session.githubLogin, - days, - { bypass, userId: session.githubId } - ); + accountToken, + accountRow.github_login, + days, + { + bypass: false, + userId: accountId, + } +) return Response.json(result); } catch { 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, - { bypass, userId: accountId } - ); - 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 3dc9a4be..8fd322ac 100644 --- a/src/components/TopRepos.tsx +++ b/src/components/TopRepos.tsx @@ -5,10 +5,13 @@ import { useAccount } from "@/components/AccountContext"; import type { RepoHealthScore } from "@/types/repo-health"; interface Repo { - name: string; - commits: number; - url: string; - description: string | null; + name: string; + commits: number; + url: string; + languages?: { + name: string; + percentage: number; + }[]; } export default function TopRepos() { @@ -39,8 +42,7 @@ export default function TopRepos() { setLastUpdated(new Date()); setMinutesAgo(0); }); - }, [days, selectedAccount]); - + }, [days, selectedAccount]); const fetchHealthScores = useCallback(() => { setHealthLoading(true); const accountParam = selectedAccount !== null @@ -68,7 +70,6 @@ export default function TopRepos() { return () => clearInterval(interval); }, [lastUpdated]); - useEffect(() => { fetchRepos(); fetchHealthScores(); @@ -96,7 +97,16 @@ export default function TopRepos() { : b.commits - a.commits; }); - const maxCommits = sortedRepos[0]?.commits ?? 1; + const maxCommits = repos[0]?.commits ?? 1; + const languageColors: Record = { + TypeScript: "#3178c6", + JavaScript: "#f1e05a", + HTML: "#e34c26", + CSS: "#563d7c", + Python: "#3572A5", + Java: "#b07219", + Shell: "#89e051", +}; return (
@@ -233,6 +243,25 @@ export default function TopRepos() { style={{ width: `${barWidth}%` }} />
+ {repo.languages && repo.languages.length > 0 && ( +
+ {repo.languages.map((lang) => ( + + + {lang.name} {lang.percentage}% + + ))} +
+)} ); })}