'use client'; import { useEffect, useState, useMemo } from 'react'; import { getRepos, getHeatmap, getRecentCommits, calcStats } from '@/lib/gitea'; import type { GiteaRepo, GiteaCommit, HeatmapEntry } from '@/lib/types'; // ── Calendar constants ── const CELL = 12, GAP = 3, STEP = CELL + GAP; const DAY_LABEL_W = 26; const WEEKS = 52; const LEVELS = [ 'rgba(15,23,42,0.05)', 'rgba(15,23,42,0.12)', 'rgba(15,23,42,0.22)', 'rgba(15,23,42,0.38)', 'rgba(15,23,42,0.62)', ]; function getLevel(n: number) { if (n === 0) return 0; if (n <= 2) return 1; if (n <= 5) return 2; if (n <= 9) return 3; return 4; } function timeAgo(s: string) { const d = Date.now() - new Date(s).getTime(); const m = Math.floor(d / 60000); if (m < 1) return 'just now'; if (m < 60) return `${m}m`; const h = Math.floor(m / 60); if (h < 24) return `${h}h`; const days = Math.floor(h / 24); if (days < 30) return `${days}d`; return `${Math.floor(days / 30)}mo`; } // ── Stat item ── function Stat({ label, value, sub, }: { label: string; value: string | number; sub: string; }) { return (
{label} {value} {sub}
); } // ── Contribution Calendar ── function ContributionCalendar({ data }: { data: HeatmapEntry[] }) { const [tooltip, setTooltip] = useState<{ x: number; y: number; date: string; count: number; } | null>(null); const dateMap = useMemo(() => { const m = new Map(); data.forEach((e) => { const k = new Date(e.timestamp * 1000).toISOString().split('T')[0]; m.set(k, (m.get(k) || 0) + e.contributions); }); return m; }, [data]); const { weeks, monthLabels } = useMemo(() => { const today = new Date(); today.setHours(0, 0, 0, 0); const startSunday = new Date(today); startSunday.setDate(today.getDate() - today.getDay() - 51 * 7); const weeks: { date: Date; key: string; count: number }[][] = []; const monthLabels: { label: string; x: number }[] = []; let lastMonth = -1; const cur = new Date(startSunday); for (let w = 0; w < WEEKS; w++) { const week: { date: Date; key: string; count: number }[] = []; for (let d = 0; d < 7; d++) { const k = cur.toISOString().split('T')[0]; week.push({ date: new Date(cur), key: k, count: dateMap.get(k) || 0 }); cur.setDate(cur.getDate() + 1); } const mo = week[0].date.getMonth(); if (mo !== lastMonth) { monthLabels.push({ label: week[0].date.toLocaleDateString('en-US', { month: 'short' }), x: w * STEP + DAY_LABEL_W, }); lastMonth = mo; } weeks.push(week); } return { weeks, monthLabels }; }, [dateMap]); const svgW = DAY_LABEL_W + WEEKS * STEP; const svgH = 7 * STEP + 20; return (
Less {LEVELS.map((c, i) => (
{monthLabels.map((m, i) => ( {m.label} ))} {['', 'Mon', '', 'Wed', '', 'Fri', ''].map((d, i) => d ? ( {d} ) : null )} {weeks.map((week, wi) => week.map((day, di) => ( setTooltip({ x: e.clientX, y: e.clientY, date: day.date.toLocaleDateString('en-GB', { weekday: 'short', day: 'numeric', month: 'short', year: 'numeric', }), count: day.count, }) } onMouseLeave={() => setTooltip(null)} className="transition-opacity duration-100 hover:opacity-70 cursor-default" > {`${day.count} contribution${day.count !== 1 ? 's' : ''} on ${day.date.toLocaleDateString()}`} )) )}
{tooltip && (
{tooltip.count} {' contribution'} {tooltip.count !== 1 ? 's' : ''} {tooltip.date}
)}
); } // ── Monthly chart ── function MonthlyChart({ data }: { data: HeatmapEntry[] }) { const months = useMemo(() => { return Array.from({ length: 12 }, (_, i) => { const d = new Date(); d.setMonth(d.getMonth() - 11 + i); const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`; const label = d.toLocaleDateString('en-US', { month: 'short' }); const count = data .filter((e) => { const ed = new Date(e.timestamp * 1000); return `${ed.getFullYear()}-${String(ed.getMonth() + 1).padStart(2, '0')}` === key; }) .reduce((s, e) => s + e.contributions, 0); return { label, count }; }); }, [data]); const maxVal = Math.max(...months.map((m) => m.count), 1); return (
Monthly activity
{months.map((m, i) => { const pct = (m.count / maxVal) * 100; return (
{m.label}
); })}
); } // ── Recent commits ── function RecentCommits({ commits }: { commits: GiteaCommit[] }) { if (!commits.length) { return

No recent commits.

; } return (
Recent commits
    {commits.slice(0, 10).map((c) => { const sha = (c.sha || '').slice(0, 7); const msg = (c.commit?.message || '').split('\n')[0].slice(0, 72); const date = c.created || c.commit?.author?.date; return (
  1. ); })}
); } // ── Main section ── export default function CommitSection() { const [repos, setRepos] = useState([]); const [heatmap, setHeatmap] = useState([]); const [commits, setCommits] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { async function load() { try { const [r, h] = await Promise.all([getRepos(6), getHeatmap()]); setRepos(r); setHeatmap(h); const c = await getRecentCommits(r); setCommits(c); } catch (e: unknown) { const msg = e instanceof Error ? e.message : 'Unknown error'; setError(msg); } finally { setLoading(false); } } load(); }, []); const stats = useMemo(() => calcStats(heatmap), [heatmap]); return (

{/* Left side */}
{loading ? (
) : error ? (

Could not load heatmap

{error}

) : ( )}
{/* Right side */}
{loading ? (
) : ( )}
{loading ? (
{[...Array(5)].map((_, i) => (
))}
) : ( )}
{!!repos.length && (
Projects
{repos.slice(0, 6).map((repo) => ( {repo.name} ))}
)}
); }