mirror of
https://github.com/prdlk/leetcode.git
synced 2026-11-02 21:22:43 +00:00
67 lines
2.7 KiB
TypeScript
67 lines
2.7 KiB
TypeScript
/**
|
|||
|
|
* What the four progress islands share besides the data: the not-ready body
|
||
|
|
* (a skeleton while /api/stats loads, one muted line if it fails) and the
|
||
|
|
* hover tooltip for the two charts drawn as plain SVG (the heatmap calendar
|
||
|
|
* and the topic bubbles — Recharts has neither layout). The tooltip wears the
|
||
|
|
* exact classes of shadcn's ChartTooltipContent, so all four cards' tooltips
|
||
|
|
* look like one component.
|
||
|
|
*/
|
||
|
|
import { type PointerEvent, type ReactNode, useRef, useState } from "react";
|
||
|
|
import { cn } from "../../lib/utils";
|
||
|
|
import type { Load } from "../../lib/stats";
|
||
|
|
import { Skeleton } from "../ui/skeleton";
|
||
|
|
|
||
|
|
export function StatsPending({ load, className }: { load: Exclude<Load, { phase: "ready" }>; className: string }) {
|
||
|
|
if (load.phase === "loading") return <Skeleton className={className} />;
|
||
|
|
return (
|
||
|
|
<p className="my-0! text-muted-foreground text-sm">
|
||
|
|
Stats unavailable — they are served live from the SRS Worker, and it did not respond.
|
||
|
|
</p>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
interface Tip {
|
||
|
|
x: number;
|
||
|
|
y: number;
|
||
|
|
content: ReactNode;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* A pointer-following tooltip for SVG marks. Put `ref` on a `relative`
|
||
|
|
* wrapper, call `show` from a mark's pointer events, render `tip` inside the
|
||
|
|
* wrapper. Positions are relative to the wrapper, so page scroll never skews it.
|
||
|
|
*/
|
||
|
|
export function useHoverTip<T extends HTMLElement>() {
|
||
|
|
const ref = useRef<T>(null);
|
||
|
|
const [tip, setTip] = useState<Tip | null>(null);
|
||
|
|
const show = (event: PointerEvent, content: ReactNode) => {
|
||
|
|
const box = ref.current?.getBoundingClientRect();
|
||
|
|
if (box) setTip({ x: event.clientX - box.left, y: event.clientY - box.top, content });
|
||
|
|
};
|
||
|
|
const hide = () => setTip(null);
|
||
|
|
const node = tip && (
|
||
|
|
<div
|
||
|
|
className={cn(
|
||
|
|
"pointer-events-none absolute z-10 grid min-w-[8rem] -translate-x-1/2 -translate-y-[calc(100%+0.75rem)] items-start gap-1.5",
|
||
|
|
"rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
|
||
|
|
)}
|
||
|
|
style={{ left: tip.x, top: tip.y }}
|
||
|
|
>
|
||
|
|
{tip.content}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
return { ref, show, hide, node };
|
||
|
|
}
|
||
|
|
|
||
|
|
/** One tooltip row, the shape ChartTooltipContent draws: colour dot, label, mono value. */
|
||
|
|
export function TipRow({ color, label, value }: { color: string; label: ReactNode; value: ReactNode }) {
|
||
|
|
return (
|
||
|
|
<div className="flex w-full items-center gap-2">
|
||
|
|
<div className="size-2.5 shrink-0 rounded-[2px]" style={{ background: color }} />
|
||
|
|
<div className="flex flex-1 items-center justify-between gap-4 leading-none">
|
||
|
|
<span className="text-muted-foreground">{label}</span>
|
||
|
|
<span className="font-medium font-mono text-foreground tabular-nums">{value}</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|