Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Chat controls, public profiles, shadcn selects, and no Docs tab in a project | 1 | /** |
| 2 | * A person's last year on their profile: a square a day, a column a week, | |
| 3 | * shaded in the accent by how much they did (issues and pull requests | |
| 4 | * opened, reviews given) on repositories the viewer can see. The layout is | |
| 5 | * lib/contribution-calendar.ts; the counts come from the work service's | |
| 6 | * `contributions`, which never counts work the viewer could not open. | |
| 7 | * | |
| 8 | * One hint serves every square: it follows the pointer, so a year of days | |
| 9 | * costs one tooltip, not 365. | |
| 10 | */ | |
| 11 | import { type PointerEvent, useEffect, useMemo, useRef, useState } from "react"; | |
| 12 | ||
| 13 | import { type Level, buildCalendar, dayLabel, totalLabel } from "../lib/contribution-calendar"; | |
| 14 | import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"; | |
| 15 | ||
| 16 | /** A square's side and the space between squares, in pixels. */ | |
| 17 | const CELL = 10; | |
| 18 | const GAP = 3; | |
| 19 | const STEP = CELL + GAP; | |
| 20 | ||
| 21 | /** Each level's shade: the raised gray for nothing, then the accent, stronger with more. */ | |
| 22 | const SHADES: Record<Level, string> = { | |
| 23 | 0: "bg-raised", | |
| 24 | 1: "bg-accent/25", | |
| 25 | 2: "bg-accent/45", | |
| 26 | 3: "bg-accent/70", | |
| 27 | 4: "bg-accent", | |
| 28 | }; | |
| 29 | ||
| 30 | const WEEKDAYS = ["", "Mon", "", "Wed", "", "Fri", ""]; | |
| 31 | ||
| 32 | type Hovered = { date: string; count: number; left: number; top: number }; | |
| 33 | ||
| 34 | export function ContributionCalendar({ | |
| 35 | days, | |
| 36 | total, | |
| 37 | today, | |
| 38 | from, | |
| 39 | }: { | |
| 40 | days: readonly { date: string; count: number }[]; | |
| 41 | total: number; | |
| 42 | /** `YYYY-MM-DD`, UTC: the last day shown. */ | |
| 43 | today: string; | |
| 44 | /** The first day counted; a year before `today` when absent. */ | |
| 45 | from?: string; | |
| 46 | }) { | |
| 47 | const calendar = useMemo(() => buildCalendar(days, today, from), [days, today, from]); | |
| 48 | const scroller = useRef<HTMLDivElement>(null); | |
| 49 | const [hovered, setHovered] = useState<Hovered | null>(null); | |
| 50 | ||
| 51 | // On a narrow screen the calendar scrolls inside its card; start at today. | |
| 52 | useEffect(() => { | |
| 53 | const element = scroller.current; | |
| 54 | if (element) element.scrollLeft = element.scrollWidth; | |
| 55 | }, []); | |
| 56 | ||
| 57 | const over = (event: PointerEvent<HTMLDivElement>) => { | |
| 58 | const cell = (event.target as HTMLElement).closest<HTMLElement>("[data-date]"); | |
| 59 | if (!cell) return setHovered(null); | |
| 60 | const date = cell.dataset.date!; | |
| 61 | if (hovered?.date === date) return; | |
| 62 | setHovered({ date, count: Number(cell.dataset.count ?? 0), left: cell.offsetLeft, top: cell.offsetTop }); | |
| 63 | }; | |
| 64 | ||
| 65 | const heading = totalLabel(total); | |
| 66 | return ( | |
| 67 | <section aria-label="Contributions" className="rounded-xl border border-line bg-surface/50 px-4 pt-3.5 pb-3"> | |
| 68 | <h2 className="text-sm font-medium">{heading}</h2> | |
| 69 | <div ref={scroller} className="mt-3 overflow-x-auto overflow-y-hidden pb-1 [scrollbar-width:thin]"> | |
| 70 | <div className="flex w-max text-[0.625rem] leading-none text-faint"> | |
| 71 | {/* The weekdays, under the months' row. */} | |
| 72 | <div aria-hidden="true" className="mr-1.5 flex shrink-0 flex-col" style={{ gap: GAP, paddingTop: 16 }}> | |
| 73 | {WEEKDAYS.map((label, index) => ( | |
| 74 | <span key={index} className="flex items-center" style={{ height: CELL }}> | |
| 75 | {label} | |
| 76 | </span> | |
| 77 | ))} | |
| 78 | </div> | |
| 79 | <div | |
| 80 | role="img" | |
| 81 | aria-label={heading} | |
| 82 | className="relative" | |
| 83 | onPointerOver={over} | |
| 84 | // A tap's pointer leaves as the finger lifts: its hint stays until the next tap elsewhere. | |
| 85 | onPointerLeave={(event) => event.pointerType === "mouse" && setHovered(null)} | |
| 86 | > | |
| 87 | <div aria-hidden="true" className="relative h-4" style={{ width: calendar.weeks.length * STEP - GAP }}> | |
| 88 | {calendar.months.map((month) => ( | |
| 89 | <span key={`${month.week}:${month.label}`} className="absolute top-0" style={{ left: month.week * STEP }}> | |
| 90 | {month.label} | |
| 91 | </span> | |
| 92 | ))} | |
| 93 | </div> | |
| 94 | <div className="flex" style={{ gap: GAP }}> | |
| 95 | {calendar.weeks.map((week, column) => ( | |
| 96 | <div key={column} className="flex flex-col" style={{ gap: GAP }}> | |
| 97 | {week.map((day, row) => | |
| 98 | day ? ( | |
| 99 | <span | |
| 100 | key={row} | |
| 101 | data-date={day.date} | |
| 102 | data-count={day.count} | |
| 103 | className={`block rounded-[2px] ${SHADES[day.level]} ${hovered?.date === day.date ? "ring-1 ring-fg/60" : ""}`} | |
| 104 | style={{ width: CELL, height: CELL }} | |
| 105 | /> | |
| 106 | ) : ( | |
| 107 | <span key={row} className="block" style={{ width: CELL, height: CELL }} /> | |
| 108 | ), | |
| 109 | )} | |
| 110 | </div> | |
| 111 | ))} | |
| 112 | </div> | |
| 113 | {/* The one hint, over whichever square the pointer is on. */} | |
| 114 | <Tooltip open={hovered != null} onOpenChange={(open) => !open && setHovered(null)}> | |
| 115 | <TooltipTrigger asChild> | |
| 116 | <span | |
| 117 | aria-hidden="true" | |
| 118 | className="pointer-events-none absolute" | |
| 119 | style={{ left: hovered?.left ?? 0, top: hovered?.top ?? 0, width: CELL, height: CELL }} | |
| 120 | /> | |
| 121 | </TooltipTrigger> | |
| 122 | <TooltipContent side="top">{hovered ? dayLabel(hovered.count, hovered.date) : null}</TooltipContent> | |
| 123 | </Tooltip> | |
| 124 | </div> | |
| 125 | </div> | |
| 126 | </div> | |
| 127 | <div aria-hidden="true" className="mt-2 flex items-center justify-end gap-1 text-[0.6875rem] text-faint"> | |
| 128 | <span className="mr-1">Less</span> | |
| 129 | {([0, 1, 2, 3, 4] as Level[]).map((level) => ( | |
| 130 | <span key={level} className={`block rounded-[2px] ${SHADES[level]}`} style={{ width: CELL, height: CELL }} /> | |
| 131 | ))} | |
| 132 | <span className="ml-1">More</span> | |
| 133 | </div> | |
| 134 | </section> | |
| 135 | ); | |
| 136 | } |