First concept
This commit is contained in:
16
lib/config.ts
Normal file
16
lib/config.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
// ★ UPDATE THESE ★
|
||||
export const GITEA_URL = process.env.NEXT_PUBLIC_GITEA_URL ?? 'https://loud-cool-pigeon.01b529f7.katapult.cloud';
|
||||
export const GITEA_USERNAME = process.env.NEXT_PUBLIC_GITEA_USERNAME ?? 'm0dus';
|
||||
export const GITEA_TOKEN = process.env.NEXT_PUBLIC_GITEA_TOKEN ?? '';
|
||||
|
||||
export const SITE = {
|
||||
name: 'William March',
|
||||
title: 'Software Engineer',
|
||||
tagline: 'Building precise, purposeful, open-source software.',
|
||||
about1: "I'm a software engineer focused on writing clean, reliable code. I care about open-source tools that solve real problems — the kind that make other engineers' lives easier.",
|
||||
about2: "From small CLI utilities to complex systems, I approach every project with the same level of craft. Good code reads like good writing: clear, intentional, and easy to reason about.",
|
||||
skills: ['Go', 'Python', 'TypeScript', 'Rust', 'Linux', 'Docker', 'PostgreSQL', 'Git', 'Nix'],
|
||||
email: 'william@williammarch.xyz',
|
||||
repoLimit: 6,
|
||||
commitFetchRepos: 4,
|
||||
};
|
||||
76
lib/gitea.ts
Normal file
76
lib/gitea.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { GITEA_URL, GITEA_USERNAME, GITEA_TOKEN } from './config';
|
||||
import type { GiteaRepo, GiteaCommit, HeatmapEntry } from './types';
|
||||
|
||||
function headers() {
|
||||
const h: Record<string,string> = { 'Accept': 'application/json' };
|
||||
if (GITEA_TOKEN) h['Authorization'] = `token ${GITEA_TOKEN}`;
|
||||
return h;
|
||||
}
|
||||
|
||||
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}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function getRepos(limit = 6): Promise<GiteaRepo[]> {
|
||||
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 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 }))
|
||||
)
|
||||
)
|
||||
);
|
||||
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);
|
||||
}
|
||||
export function calcStats(heatmap: HeatmapEntry[]) {
|
||||
const yearAgo = Date.now() / 1000 - 365 * 86400;
|
||||
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 dateSet = new Set<string>();
|
||||
year.forEach(e => {
|
||||
const d = new Date(e.timestamp * 1000);
|
||||
dateSet.add(d.toISOString().split('T')[0]);
|
||||
});
|
||||
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
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])) {
|
||||
currentStreak++;
|
||||
cur.setDate(cur.getDate() - 1);
|
||||
}
|
||||
|
||||
let longestStreak = 0, streak = 0;
|
||||
const sorted = Array.from(dateSet).sort(); // ← only change
|
||||
for (let i = 0; i < sorted.length; i++) {
|
||||
if (i === 0) { streak = 1; continue; }
|
||||
const prev = new Date(sorted[i - 1]);
|
||||
prev.setDate(prev.getDate() + 1);
|
||||
if (prev.toISOString().split('T')[0] === sorted[i]) { streak++; }
|
||||
else { streak = 1; }
|
||||
if (streak > longestStreak) longestStreak = streak;
|
||||
}
|
||||
if (streak > longestStreak) longestStreak = streak;
|
||||
|
||||
return { total, currentStreak, longestStreak, activeDays };
|
||||
}
|
||||
38
lib/types.ts
Normal file
38
lib/types.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
export interface GiteaRepo {
|
||||
id: number;
|
||||
name: string;
|
||||
full_name: string;
|
||||
description: string;
|
||||
html_url: string;
|
||||
language: string | null;
|
||||
stars_count: number;
|
||||
forks_count: number;
|
||||
updated: string;
|
||||
private: boolean;
|
||||
fork: boolean;
|
||||
}
|
||||
|
||||
export interface GiteaCommit {
|
||||
sha: string;
|
||||
html_url: string;
|
||||
created: string;
|
||||
commit: {
|
||||
message: string;
|
||||
author: { name: string; date: string };
|
||||
};
|
||||
author?: { login: string; avatar_url: string };
|
||||
_repo?: string;
|
||||
_repoUrl?: string;
|
||||
}
|
||||
|
||||
export interface HeatmapEntry {
|
||||
timestamp: number;
|
||||
contributions: number;
|
||||
}
|
||||
|
||||
export interface CommitStats {
|
||||
total: number;
|
||||
currentStreak: number;
|
||||
longestStreak: number;
|
||||
activeDays: number;
|
||||
}
|
||||
Reference in New Issue
Block a user