import { Link } from "@tanstack/react-router"; import { useState } from "react"; import { cn } from "~/utils/cn"; import type { ContributionDay } from "~/server/contributions"; const WEEK = 7; const LEVELS = ["bg-kumo-base", "bg-accent-950", "bg-accent-800", "bg-accent-600", "bg-accent-400"]; const LEVEL_STEPS = [0, 1, 3, 6, 10]; const TOOLTIP_OFFSET_PX = 8; const BREAKDOWN_LABELS = [ { key: "commits", singular: "commit", plural: "commits" }, { key: "pulls", singular: "pull request event", plural: "pull request events" }, { key: "issues", singular: "issue opened", plural: "issues opened" }, { key: "reviews", singular: "comment or review", plural: "comments and reviews" }, { key: "other", singular: "other event", plural: "other events" }, { key: "private", singular: "contribution in a private repository", plural: "contributions in private repositories" }, ] as const; const DATE_FORMAT = new Intl.DateTimeFormat(undefined, { weekday: "short", month: "short", day: "numeric", year: "numeric", timeZone: "UTC", }); type Hover = { day: ContributionDay; left: number; top: number }; function level(count: number): string { let index = 0; for (const [step, threshold] of LEVEL_STEPS.entries()) if (count >= threshold) index = step; return LEVELS[index]; } function formatDay(date: string): string { return DATE_FORMAT.format(new Date(`${date}T00:00:00Z`)); } function DayCard({ hover }: { hover: Hover }) { const { day } = hover; const lines = day.breakdown ? BREAKDOWN_LABELS.flatMap((label) => { const value = day.breakdown?.[label.key] ?? 0; return value > 0 ? [`${value} ${value === 1 ? label.singular : label.plural}`] : []; }) : []; return (
{day.count === 0 ? "No contributions" : `${day.count} contribution${day.count === 1 ? "" : "s"}`}
{formatDay(day.date)}
{lines.length > 0 && ( )} {day.repos.length > 0 && (
{day.repos.map((repo) => (
{repo}
))}
)}
); } type Props = { days: ContributionDay[]; years: number[]; year: number | null; owner: string; /** The day whose details are open below the graph. */ selected: string | null; actions?: React.ReactNode; }; function YearList({ years, year, owner }: Pick) { if (years.length < 2) return null; const options = [{ value: null, label: "Last year" }, ...years.map((value) => ({ value, label: String(value) }))]; return ( ); } export function ContributionGraph({ days, years, year, owner, selected, actions }: Props) { const [hover, setHover] = useState(null); if (days.length === 0) return null; const weeks: ContributionDay[][] = []; for (let index = 0; index < days.length; index += WEEK) weeks.push(days.slice(index, index + WEEK)); const total = days.reduce((sum, day) => sum + day.count, 0); function show(day: ContributionDay, target: HTMLElement) { const container = target.closest("[data-contribution-grid]"); if (!(container instanceof HTMLElement)) return; const box = target.getBoundingClientRect(); const frame = container.getBoundingClientRect(); setHover({ day, left: box.left - frame.left + box.width / 2, top: box.top - frame.top }); } return (

{total} contribution{total === 1 ? "" : "s"} {year === null ? "in the last year" : `in ${year}`}

{actions}
setHover(null)}> {weeks.map((week) => (
{week.map((day) => ( ({ ...previous, day: selected === day.date ? undefined : day.date, repoPage: undefined, activityPage: undefined, })} resetScroll={false} aria-label={`${day.count} contributions on ${formatDay(day.date)}`} aria-current={selected === day.date ? "date" : undefined} onMouseEnter={(event) => show(day, event.currentTarget)} onFocus={(event) => show(day, event.currentTarget)} onBlur={() => setHover(null)} className={cn( "block size-2.5 rounded-xs outline-offset-1 focus-visible:outline-2 focus-visible:outline-accent-500", level(day.count), hover?.day.date === day.date && "ring-1 ring-gray-300", selected === day.date && "ring-2 ring-accent-400", )} /> ))}
))}
{hover && }
); }