feat(projects): add interactive GitHub contribution graph

This commit is contained in:
poyrazavsever
2026-08-31 12:26:04 +03:00
parent 1522ef0355
commit a2259848dc
5 changed files with 316 additions and 13 deletions
+57
View File
@@ -24,6 +24,12 @@ export type GithubActivity = {
createdAt: string;
};
export type GithubContributionDay = {
date: string;
count: number;
level: 0 | 1 | 2 | 3 | 4;
};
type GithubRepoApi = {
id: number;
name: string;
@@ -136,3 +142,54 @@ export async function getGithubActivity(): Promise<GithubActivity[]> {
return [];
}
}
export function parseGithubContributions(
html: string,
): GithubContributionDay[] {
const contributions: GithubContributionDay[] = [];
const cellPattern =
/<td\b([^>]*\bdata-date="[^"]+"[^>]*)><\/td>\s*<tool-tip\b[^>]*>([\s\S]*?)<\/tool-tip>/g;
for (const match of html.matchAll(cellPattern)) {
const attributes = match[1];
const tooltip = match[2].replace(/<[^>]+>/g, "").trim();
const date = attributes.match(/data-date="([^"]+)"/)?.[1];
const levelValue = attributes.match(/data-level="([0-4])"/)?.[1];
const countValue = tooltip.match(/^([\d,]+)\s+contributions?\b/i)?.[1];
if (!date || levelValue === undefined) continue;
contributions.push({
date,
count: countValue ? Number(countValue.replaceAll(",", "")) : 0,
level: Number(levelValue) as GithubContributionDay["level"],
});
}
return contributions.sort((first, second) =>
first.date.localeCompare(second.date),
);
}
export async function getGithubContributions(): Promise<
GithubContributionDay[]
> {
try {
const response = await fetch(
`https://github.com/users/${USERNAME}/contributions`,
{
next: { revalidate: 3600 },
headers: {
Accept: "text/html",
"User-Agent": "portfolio-new",
},
},
);
if (!response.ok) return [];
return parseGithubContributions(await response.text());
} catch {
return [];
}
}