Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 65 additions & 8 deletions src/app/api/metrics/repos/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ interface RepoSummary {
name: string;
commits: number;
description: string | null;
url: string;
languages?: RepoLanguage[];
}

interface RepoLanguage {
name: string;
bytes: number;
percentage: number;
}

interface RepoResponse {
Expand All @@ -30,22 +38,63 @@ interface RepoResponse {
}

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<string, { commits: number; description: string | null }>();
a: Array<RepoSummary>,
b: Array<RepoSummary>
): Array<RepoSummary> {
const map = new Map<string, { commits: number; description: string | null; url: string; 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,
url: existing?.url ?? repo.url,
languages: existing?.languages ?? repo.languages,
});
}
return Array.from(map.entries())
.map(([name, { commits, description }]) => ({ name, commits, description }))
.map(([name, { commits, description, url, languages }]) => ({
name,
commits,
description,
url,
languages,
}))
.sort((x, y) => y.commits - x.commits);
}

async function fetchRepoLanguages(
token: string,
repoName: string
): Promise<RepoLanguage[]> {
const res = await fetch(`${GITHUB_API}/repos/${repoName}/languages`, {
headers: {
Authorization: `Bearer ${token}`,
Accept: "application/vnd.github+json",
},
cache: "no-store",
});

if (!res.ok) {
return [];
}

const langs = (await res.json()) as Record<string, number>;
const totalBytes = Object.values(langs).reduce((sum, bytes) => sum + bytes, 0);

if (totalBytes <= 0) {
return [];
}

return Object.entries(langs)
.map(([name, bytes]) => ({
name,
bytes,
percentage: Math.round((bytes / totalBytes) * 1000) / 10,
}))
.sort((a, b) => b.percentage - a.percentage)
.slice(0, 6);
}

async function fetchReposForAccount(
token: string,
githubLogin: string,
Expand Down Expand Up @@ -90,21 +139,29 @@ async function fetchReposForAccount(
}>;
};

const repoMap: Record<string, { commits: number; description: string | null }> = {};
const repoMap: Record<string, { commits: number; description: string | null; url: string }> = {};
for (const item of data.items) {
const name = item.repository.full_name;
repoMap[name] = {
commits: (repoMap[name]?.commits ?? 0) + 1,
description: item.repository.description,
url: item.repository.html_url,
};
}

const repos = Object.entries(repoMap)
.map(([name, { commits, description }]) => ({ name, commits, description }))
.map(([name, { commits, description, url }]) => ({ name, commits, description, url }))
.sort((a, b) => b.commits - a.commits)
.slice(0, 6);

return { repos, days };
const reposWithLanguages = await Promise.all(
repos.map(async (repo) => {
const languages = await fetchRepoLanguages(token, repo.name);
return languages.length > 0 ? { ...repo, languages } : repo;
})
);

return { repos: reposWithLanguages, days };
}
);
}
Expand Down
80 changes: 80 additions & 0 deletions src/components/TopRepos.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,70 @@ import { useCallback, useEffect, useState } from "react";
import { useAccount } from "@/components/AccountContext";
import type { RepoHealthScore } from "@/types/repo-health";

interface RepoLanguage {
name: string;
bytes: number;
percentage: number;
}

interface Repo {
name: string;
commits: number;
url: string;
description: string | null;
languages?: RepoLanguage[];
}

const LANGUAGE_COLORS: Record<string, string> = {
TypeScript: "#3178c6",
JavaScript: "#f7df1e",
Python: "#3572A5",
Go: "#00ADD8",
Rust: "#dea584",
Java: "#b07219",
CSS: "#563d7c",
HTML: "#e34c26",
Ruby: "#701516",
Shell: "#89e051",
};

const FALLBACK_LANGUAGE_COLOR = "#6b7280";

function getLanguageColor(name: string): string {
return LANGUAGE_COLORS[name] ?? FALLBACK_LANGUAGE_COLOR;
}

function getVisibleLanguages(languages: RepoLanguage[]): RepoLanguage[] {
const sorted = [...languages].sort((a, b) => b.percentage - a.percentage);

if (sorted.length <= 3) {
const total = sorted.reduce((sum, lang) => sum + lang.percentage, 0);
if (total < 100 && sorted.length > 0) {
return [
...sorted,
{
name: "Other",
bytes: 0,
percentage: Math.round((100 - total) * 10) / 10,
},
];
}
return sorted;
}

const topLanguages = sorted.slice(0, 2);
const otherPercentage = Math.round(
sorted.slice(2).reduce((sum, lang) => sum + lang.percentage, 0) * 10
) / 10;

return [
...topLanguages,
{
name: "Other",
bytes: 0,
percentage: otherPercentage,
},
];
}

export default function TopRepos() {
Expand Down Expand Up @@ -191,6 +250,7 @@ export default function TopRepos() {
: health?.grade === "yellow"
? "bg-yellow-500/15 text-yellow-300 border border-yellow-500/25"
: "bg-red-500/15 text-red-300 border border-red-500/25";
const visibleLanguages = repo.languages ? getVisibleLanguages(repo.languages) : [];
return (
<li key={repo.name}>
<div className="flex items-center justify-between text-sm mb-1">
Expand Down Expand Up @@ -233,6 +293,26 @@ export default function TopRepos() {
style={{ width: `${barWidth}%` }}
/>
</div>
<div className="mt-2 min-h-6">
{visibleLanguages.length > 0 && (
<div className="flex flex-wrap gap-1.5 text-[11px] text-[var(--muted-foreground)]">
{visibleLanguages.map((language) => (
<span
key={language.name}
className="inline-flex items-center gap-1 rounded-full border border-[var(--border)] bg-[var(--control)] px-2 py-0.5"
title={`${language.name}: ${language.percentage}%`}
>
<span
className="h-2 w-2 rounded-full"
style={{ backgroundColor: getLanguageColor(language.name) }}
/>
<span className="text-[var(--card-foreground)]">{language.name}</span>
<span>{language.percentage}%</span>
</span>
))}
</div>
)}
</div>
</li>
);
})}
Expand Down
Loading