"use client"; import { useMemo, useRef, useState, type CSSProperties, type KeyboardEvent, type PointerEvent } from "react"; import "./uptime-ribbon.css"; /** * Uptime Ribbon * Each bar is a day, coloured by how it went. Scrubbing lifts the bars around * the pointer (a smooth bump, not a single highlight) and a tooltip follows * with the date, uptime and any incident. The whole ribbon is one focusable * control; ←/→ move day by day. */ export type Day = { date: string; uptime: number; incident?: string }; export type Service = { name: string; days: Day[] }; type Props = { services: Service[]; className?: string }; const level = (u: number) => (u >= 99.95 ? "ok" : u >= 99 ? "minor" : "major"); function Ribbon({ s }: { s: Service }) { const [at, setAt] = useState(null); const ref = useRef(null); const avg = useMemo(() => s.days.reduce((a, d) => a + d.uptime, 0) / s.days.length, [s.days]); const n = s.days.length; const move = (e: PointerEvent) => { const r = ref.current!.getBoundingClientRect(); setAt(Math.max(0, Math.min(n - 1, Math.floor(((e.clientX - r.left) / r.width) * n)))); }; const key = (e: KeyboardEvent) => { if (e.key === "ArrowRight") { e.preventDefault(); setAt((a) => Math.min(n - 1, (a ?? n - 1) + 1)); } if (e.key === "ArrowLeft") { e.preventDefault(); setAt((a) => Math.max(0, (a ?? n) - 1)); } if (e.key === "End") setAt(n - 1); if (e.key === "Home") setAt(0); }; const d = at !== null ? s.days[at] : null; const worst = s.days.some((x) => level(x.uptime) === "major") ? "major" : s.days.some((x) => level(x.uptime) === "minor") ? "minor" : "ok"; return (
{s.name} {avg.toFixed(2)}% uptime
setAt(null)} onKeyDown={key} onBlur={() => setAt(null)} style={{ "--n": n } as CSSProperties} > {s.days.map((day, i) => { const dist = at === null ? 99 : Math.abs(i - at); const lift = Math.max(0, 1 - dist / 4); return ; })} {d && at !== null && (
{d.date} {d.uptime.toFixed(2)}% uptime {d.incident && {d.incident}}
)}
); } export function UptimeRibbon({ services, className = "" }: Props) { return (
{services.map((s) => )}
); }