import { cn } from "@/shared/lib/cn"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip"; const WEEK_COUNT = 26; const DAYS_PER_WEEK = 7; // Intensity ramp shared by the cells and the legend dots. // Tints of the theme's primary token: the most active cells are fully // saturated primary, quieter days fade toward the muted background. const LEVEL_CLASSES = [ "bg-muted/40 dark:bg-muted/30", "bg-primary/25", "bg-primary/50", "bg-primary/75", "bg-primary", ]; // Descriptors matching the thresholds in `levelFor`. const LEVEL_LABELS = [ "No activity", "1–2 events", "3–5 events", "6–9 events", "10+ events", ]; /** Activity intensity legend shared with the contribution graph header. */ export function ProjectsContributionLegend() { return (
{LEVEL_CLASSES.map((levelClass, level) => ( {LEVEL_LABELS[level]} ))}
); } function dayKeyOf(date: Date) { const month = `${date.getMonth() + 1}`.padStart(2, "0"); const day = `${date.getDate()}`.padStart(2, "0"); return `${date.getFullYear()}-${month}-${day}`; } function levelFor(count: number) { if (count <= 0) return 0; if (count <= 2) return 1; if (count <= 5) return 2; if (count <= 9) return 3; return 4; } /** Week columns (each 7 days, Sunday first) ending with the current week. */ function buildWeeks(today: Date, weekCount: number) { const start = new Date( today.getFullYear(), today.getMonth(), today.getDate() - today.getDay() - (weekCount - 1) * DAYS_PER_WEEK, ); return Array.from({ length: weekCount }, (_, weekIndex) => Array.from({ length: DAYS_PER_WEEK }, (_, dayIndex) => { const date = new Date(start); date.setDate(start.getDate() + weekIndex * DAYS_PER_WEEK + dayIndex); return date; }), ); } // Minimum columns between labels so adjacent months never overlap. const MIN_LABEL_GAP = 3; function monthLabels(weeks: Date[][]) { let lastLabeledIndex = -MIN_LABEL_GAP; return weeks.map((week, index) => { const isNewMonth = index === 0 || week[0].getMonth() !== weeks[index - 1][0].getMonth(); if (!isNewMonth || index - lastLabeledIndex < MIN_LABEL_GAP) return ""; lastLabeledIndex = index; return week[0].toLocaleDateString(undefined, { month: "short" }); }); } /** * GitHub-style activity heatmap for the last 26 weeks, fed by per-day * activity counts (see `ProjectActivitySummary.activityByDay`). */ export function ProjectsContributionGraph({ activityByDay, className, compact = false, }: { activityByDay: Record; className?: string; compact?: boolean; }) { const today = new Date(); const weeks = buildWeeks(today, compact ? 18 : WEEK_COUNT); const labels = monthLabels(weeks); const gridTemplateColumns = `repeat(${weeks.length}, minmax(0, 1fr))`; const todayKey = dayKeyOf(today); return (
{labels.map((label, index) => ( {label} ))}
{weeks.map((week) => week.map((day) => { const key = dayKeyOf(day); if (key > todayKey) { return ( ); } const count = activityByDay[key] ?? 0; const dateLabel = day.toLocaleDateString(undefined, { month: "short", day: "numeric", }); return ( {count > 0 ? `${count} ${count === 1 ? "event" : "events"} · ${dateLabel}` : `No activity · ${dateLabel}`} ); }), )}
); }