All files / src/components/admin AdminMetric.tsx

100% Statements 7/7
100% Branches 6/6
100% Functions 2/2
100% Lines 7/7

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129                                                                                                          7x                       377x                                         377x   377x 32x                           345x                           94x                      
import { Link } from "@tanstack/react-router";
import type { ReactNode } from "react";
 
import { Skeleton } from "../ui/skeleton";
import { cn } from "../../lib/utils";
 
/**
 * `.metric` — the Job Book's figure tile. **Figures and rows lead.**
 *
 * The spec is `.metric` / `.metrics` in `.context/mockups/_base.css` §7.3:
 *
 *     padding: 0.5rem 0 0.4375rem;
 *     border-bottom: 2px solid var(--border-strong);
 *     > .label { display:block; color: var(--foreground-muted) }
 *     > .num   { display:block; margin-top: 0.1875rem }
 *
 * A figure is **a label, a rule and a number** — not a card. The admin
 * dashboard, the search console and the platform analytics screen all drew
 * their figures as `Card > CardHeader > CardTitle + CardContent`: four boxes
 * with shadows, a 14px title and the number buried in the third element. Boxes
 * are the one thing the Job Book forbids, so the box is gone and the 2px rule
 * under each figure is what groups them.
 *
 * `value` is a `ReactNode`, not a number: an admin figure is routinely `"-"`
 * (not computed yet) or a rupee string, and `.num` is about alignment and
 * tabular figures rather than about the type of the thing printed.
 *
 * **State is a printed code, never a colour.** The dashboard's "pros pending
 * publish" tile used to go warning-coloured when it was non-zero, which is
 * colour travelling alone. Pass `code` and the state is a word.
 *
 * NOTE ON DUPLICATION: `TrafficFigure` in
 * `components/analytics/MarketplaceTraffic.tsx` is this same `.metric` spec
 * with a narrower prop set (`value: number`, a fixed window string, no link,
 * no code). That file belongs to another group this phase; folding it into
 * this component is a one-line swap and is reported rather than made here.
 */
export interface AdminMetricProps {
	/** `.label` — caps, 620 weight, muted. */
	label: ReactNode;
	/** The figure. `.num-xl`, tabular, right-aligned. */
	value: ReactNode;
	/** Printed under the figure: the window, the breakdown, the next action. */
	note?: ReactNode;
	/** A printed status code beside the label — how state is carried. */
	code?: ReactNode;
	/** Makes the whole tile the link target. */
	href?: string;
	/** Swaps the figure for a skeleton of the right height. */
	isLoading?: boolean;
	className?: string;
}
 
const LABEL = "text-xs font-semibold uppercase tracking-wider";
 
export function AdminMetric({
	label,
	value,
	note,
	code,
	href,
	isLoading,
	className,
}: AdminMetricProps) {
	const body = (
		<>
			<span className="flex items-center gap-1.5">
				<span className={cn(LABEL, "text-foreground-muted")}>{label}</span>
				{code}
			</span>
			{isLoading ? (
				<Skeleton className="mt-1 h-7 w-16" />
			) : (
				// `.num-xl`: 1.5rem, tabular, right-aligned.
				<span className="mt-0.5 block text-right text-2xl font-bold tabular-nums text-foreground-default">
					{value}
				</span>
			)}
			{note && (
				<span className="block text-xs text-foreground-subtle">{note}</span>
			)}
		</>
	);
 
	// The 2px strong rule under the figure is what groups a row of them. No
	// box, no shadow — and no hover lift, which means "pick-up-able".
	const tile = "block border-b-2 border-border-strong pt-2 pb-1.5";
 
	if (href) {
		return (
			<Link
				to={href}
				className={cn(
					tile,
					"transition-colors hover:bg-foreground-default/7 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500",
					className,
				)}
			>
				{body}
			</Link>
		);
	}
 
	return <div className={cn(tile, className)}>{body}</div>;
}
 
/**
 * `.metrics` — the auto-fitting grid a row of figures sits in
 * (`repeat(auto-fit, minmax(6.5rem, 1fr))`, `gap: 0 1rem`).
 */
export function AdminMetrics({
	children,
	className,
}: {
	children: ReactNode;
	className?: string;
}) {
	return (
		<div
			className={cn(
				"grid grid-cols-2 gap-x-4 sm:grid-cols-3 lg:grid-cols-4",
				className,
			)}
		>
			{children}
		</div>
	);
}