"use client"; import { useMemo, useState } from "react"; import type { GithubContributionDay } from "@/lib/project-feeds"; const CELL_SIZE = 10; const CELL_GAP = 2; const CELL_STEP = CELL_SIZE + CELL_GAP; const GRID_LEFT = 27; const GRID_TOP = 20; const GRAPH_HEIGHT = 104; const DAY_IN_MS = 24 * 60 * 60 * 1000; const LEVEL_CLASSES: Record = { 0: "fill-zinc-200 dark:fill-zinc-800", 1: "fill-[#ff7373]", 2: "fill-[#ff5959]", 3: "fill-[#dc2626]", 4: "fill-[#b01e1e]", }; type ContributionGraphLabels = { calendar: string; unavailable: string; none: string; singular: string; plural: string; }; type PositionedDay = GithubContributionDay & { dayIndex: number; weekIndex: number; x: number; y: number; }; type ActiveDay = PositionedDay & { label: string; }; function parseDate(date: string) { return new Date(`${date}T00:00:00Z`); } export function GithubContributionGraph({ days, labels, locale, }: { days: GithubContributionDay[]; labels: ContributionGraphLabels; locale: string; }) { const [activeDay, setActiveDay] = useState(null); const graph = useMemo(() => { if (days.length === 0) return null; const firstDate = parseDate(days[0].date); const positionedDays: PositionedDay[] = days.map((day) => { const date = parseDate(day.date); const weekIndex = Math.floor( (date.getTime() - firstDate.getTime()) / (7 * DAY_IN_MS), ); const dayIndex = date.getUTCDay(); return { ...day, dayIndex, weekIndex, x: GRID_LEFT + weekIndex * CELL_STEP, y: GRID_TOP + dayIndex * CELL_STEP, }; }); const weekCount = Math.max(...positionedDays.map((day) => day.weekIndex)) + 1; const width = GRID_LEFT + weekCount * CELL_STEP + CELL_STEP; const monthFormatter = new Intl.DateTimeFormat( locale === "tr" ? "tr-TR" : "en-US", { month: "short", timeZone: "UTC" }, ); const dateFormatter = new Intl.DateTimeFormat( locale === "tr" ? "tr-TR" : "en-US", { day: "numeric", month: "long", year: "numeric", timeZone: "UTC" }, ); const months: Array<{ label: string; x: number }> = []; let previousMonth = firstDate.getUTCMonth(); positionedDays.forEach((day, index) => { const date = parseDate(day.date); const month = date.getUTCMonth(); if (index > 0 && month !== previousMonth) { months.push({ label: monthFormatter.format(date).replace(".", ""), x: day.x, }); } previousMonth = month; }); return { positionedDays, months, width, dateFormatter }; }, [days, locale]); if (!graph) { return (
{labels.unavailable}
); } const weekdayLabels = locale === "tr" ? [ { index: 1, label: "Pzt" }, { index: 3, label: "Çar" }, { index: 5, label: "Cum" }, ] : [ { index: 1, label: "Mon" }, { index: 3, label: "Wed" }, { index: 5, label: "Fri" }, ]; function getTooltipLabel(day: GithubContributionDay) { const date = graph?.dateFormatter.format(parseDate(day.date)) ?? day.date; if (day.count === 0) return `${date}: ${labels.none}`; return `${date}: ${day.count} ${ day.count === 1 ? labels.singular : labels.plural }`; } const tooltipX = activeDay ? Math.max(100, Math.min(graph.width - 100, activeDay.x + CELL_SIZE / 2)) : 0; const tooltipBelow = Boolean(activeDay && activeDay.y < 48); return (
setActiveDay(null)} > {weekdayLabels.map((day) => ( {day.label} ))} {graph.months.map((month) => ( {month.label} ))} {Array.from({ length: 7 }, (_, dayIndex) => ( {graph.positionedDays .filter((day) => day.dayIndex === dayIndex) .map((day) => { const label = getTooltipLabel(day); return ( setActiveDay({ ...day, label }) } onFocus={() => setActiveDay({ ...day, label })} onBlur={() => setActiveDay(null)} /> ); })} ))} {activeDay ? (
{activeDay.label}
) : null}
); }