fixed version control heatmap and activity

This commit is contained in:
will
2026-04-02 20:38:56 +01:00
parent 1da5da43e1
commit 250a7030bf
1777 changed files with 170575 additions and 83 deletions

View File

@@ -1,5 +1,5 @@
// ★ UPDATE THESE ★
export const GITEA_URL = process.env.NEXT_PUBLIC_GITEA_URL ?? 'https://loud-cool-pigeon.01b529f7.katapult.cloud';
export const GITEA_URL = process.env.NEXT_PUBLIC_GITEA_URL ?? 'https://git.williammarch.xyz';
export const GITEA_USERNAME = process.env.NEXT_PUBLIC_GITEA_USERNAME ?? 'm0dus';
export const GITEA_TOKEN = process.env.NEXT_PUBLIC_GITEA_TOKEN ?? '';

View File

@@ -1,76 +1,121 @@
import { GITEA_URL, GITEA_USERNAME, GITEA_TOKEN } from './config';
import type { GiteaRepo, GiteaCommit, HeatmapEntry } from './types';
import { GITEA_USERNAME } from './config'; // keep only safe, non-secret values
import type {
GiteaRepo,
GiteaCommit,
HeatmapEntry,
CommitStats,
} from './types';
function headers() {
const h: Record<string,string> = { 'Accept': 'application/json' };
if (GITEA_TOKEN) h['Authorization'] = `token ${GITEA_TOKEN}`;
return h;
}
const API_PREFIX = '/api/gitea';
async function get<T>(path: string): Promise<T> {
const res = await fetch(`${GITEA_URL}/api/v1${path}`, { headers: headers() });
if (!res.ok) throw new Error(`Gitea API ${path}: HTTP ${res.status}`);
const res = await fetch(`${API_PREFIX}${path}`, {
headers: { Accept: 'application/json' },
cache: 'no-store',
});
if (!res.ok) throw new Error(`Gitea proxy ${path}: HTTP ${res.status}`);
return res.json();
}
export async function getRepos(limit = 6): Promise<GiteaRepo[]> {
return get<GiteaRepo[]>(`/users/${GITEA_USERNAME}/repos?limit=${limit}&sort=newest`);
return get<GiteaRepo[]>(
`/users/${GITEA_USERNAME}/repos?limit=${limit}&sort=newest`
);
}
export async function getHeatmap(): Promise<HeatmapEntry[]> {
return get<HeatmapEntry[]>(`/users/${GITEA_USERNAME}/heatmap`);
}
export async function getRepoCommits(repo: string, limit = 5): Promise<GiteaCommit[]> {
return get<GiteaCommit[]>(`/repos/${GITEA_USERNAME}/${repo}/commits?limit=${limit}`);
export async function getRepoCommits(
repo: string,
limit = 5
): Promise<GiteaCommit[]> {
return get<GiteaCommit[]>(
`/repos/${GITEA_USERNAME}/${repo}/commits?limit=${limit}`
);
}
export async function getRecentCommits(repos: GiteaRepo[], perRepo = 4): Promise<GiteaCommit[]> {
export async function getRecentCommits(
repos: GiteaRepo[],
perRepo = 4
): Promise<GiteaCommit[]> {
const results = await Promise.allSettled(
repos.slice(0, 4).map(r =>
getRepoCommits(r.name, perRepo).then(commits =>
commits.map(c => ({ ...c, _repo: r.name, _repoUrl: r.html_url }))
repos.slice(0, 4).map((r) =>
getRepoCommits(r.name, perRepo).then((commits) =>
commits.map((c) => ({
...c,
_repo: r.name,
_repoUrl: r.html_url,
}))
)
)
);
const all: GiteaCommit[] = [];
results.forEach(r => { if (r.status === 'fulfilled') all.push(...r.value); });
return all.sort((a, b) => new Date(b.created).getTime() - new Date(a.created).getTime()).slice(0, 15);
for (const r of results) {
if (r.status === 'fulfilled') all.push(...r.value);
}
return all
.sort(
(a, b) =>
new Date(b.created).getTime() - new Date(a.created).getTime()
)
.slice(0, 15);
}
export function calcStats(heatmap: HeatmapEntry[]) {
/** Local date key (avoids UTC shift bugs) */
function dayKey(date: Date): string {
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, '0');
const d = String(date.getDate()).padStart(2, '0');
return `${y}-${m}-${d}`;
}
export function calcStats(heatmap: HeatmapEntry[]): CommitStats {
const yearAgo = Date.now() / 1000 - 365 * 86400;
const year = heatmap.filter(e => e.timestamp > yearAgo);
const year = heatmap.filter((e) => e.timestamp > yearAgo);
const total = year.reduce((s, e) => s + e.contributions, 0);
const activeDays = year.filter(e => e.contributions > 0).length;
const activeDays = year.filter((e) => e.contributions > 0).length;
const dateSet = new Set<string>();
year.forEach(e => {
for (const e of year) {
if (e.contributions <= 0) continue;
const d = new Date(e.timestamp * 1000);
dateSet.add(d.toISOString().split('T')[0]);
});
dateSet.add(dayKey(d));
}
const today = new Date();
today.setHours(0, 0, 0, 0);
// Current streak (backwards from today)
let currentStreak = 0;
const cur = new Date(today);
if (!dateSet.has(cur.toISOString().split('T')[0])) cur.setDate(cur.getDate() - 1);
while (dateSet.has(cur.toISOString().split('T')[0])) {
if (!dateSet.has(dayKey(cur))) {
cur.setDate(cur.getDate() - 1);
}
while (dateSet.has(dayKey(cur))) {
currentStreak++;
cur.setDate(cur.getDate() - 1);
}
let longestStreak = 0, streak = 0;
const sorted = Array.from(dateSet).sort(); // ← only change
// Longest streak
let longestStreak = 0;
let streak = 0;
const sorted = Array.from(dateSet).sort();
for (let i = 0; i < sorted.length; i++) {
if (i === 0) { streak = 1; continue; }
const prev = new Date(sorted[i - 1]);
if (i === 0) {
streak = 1;
longestStreak = 1;
continue;
}
const prev = new Date(sorted[i - 1] + 'T00:00:00');
prev.setDate(prev.getDate() + 1);
if (prev.toISOString().split('T')[0] === sorted[i]) { streak++; }
else { streak = 1; }
if (dayKey(prev) === sorted[i]) streak++;
else streak = 1;
if (streak > longestStreak) longestStreak = streak;
}
if (streak > longestStreak) longestStreak = streak;
return { total, currentStreak, longestStreak, activeDays };
}