Files
playground/app/frontend/components/ECGMonitorCard.tsx

163 lines
6.5 KiB
TypeScript

import type { ReactNode } from "react";
import type { PollState } from "@/types/monitor";
type Tone = "mint" | "blue" | "coral";
const toneStyles: Record<Tone, string> = {
mint: "from-white via-white to-emerald-50 ring-emerald-100",
blue: "from-white via-white to-sky-50 ring-sky-100",
coral: "from-white via-white to-rose-50 ring-rose-100",
};
interface ECGMonitorCardProps<T> {
title: string;
endpoint: string;
intervalLabel: string;
tone?: Tone;
now: string;
wavePoints: string;
waveHeight: number;
healthy: boolean;
loading: boolean;
statusLabel: string;
statusDetail: string;
lastUpdatedLabel: string;
state: PollState<T>;
children?: ReactNode;
onManualTrigger: () => void;
}
/**
* Presentational card that renders endpoint status and the shared ECG waveform with
* consistent tone, status badges, and contextual metadata.
*/
export function ECGMonitorCard<T>({
title,
endpoint,
intervalLabel,
tone = "mint",
now,
wavePoints,
waveHeight,
healthy,
loading,
statusLabel,
statusDetail,
lastUpdatedLabel,
state,
children,
onManualTrigger,
}: ECGMonitorCardProps<T>) {
const signalId = `${endpoint.replace(/[^a-z0-9]/gi, "-")}-stroke`;
const badgeColor = loading
? "border-amber-200 bg-amber-50 text-amber-800"
: healthy
? "border-emerald-200 bg-emerald-50 text-emerald-800"
: "border-rose-200 bg-rose-50 text-rose-800";
const dotColor = loading ? "bg-pulse" : healthy ? "bg-success" : "bg-danger";
const strokeColor = loading ? "#f59e0b" : healthy ? "#22c55e" : "#ef4444";
return (
<article
className={`relative overflow-hidden rounded-3xl border border-black/5 bg-gradient-to-br ${toneStyles[tone]} p-5 shadow-card ring-4 ring-transparent transition hover:-translate-y-1 hover:shadow-[0_24px_60px_rgba(15,23,42,0.16)]`}
>
<div className="absolute inset-0 -z-10 bg-[radial-gradient(circle_at_20%_10%,rgba(96,165,250,0.08),transparent_30%),radial-gradient(circle_at_80%_0%,rgba(52,211,153,0.1),transparent_30%)]" />
<div className="flex items-start justify-between gap-3">
<div className="space-y-2 min-w-0">
<div className="inline-flex max-w-full items-center gap-2 rounded-full border border-black/5 bg-black/5 px-3 py-1 text-xs font-semibold uppercase tracking-[0.12em] text-inkMuted">
<span className="break-words">{endpoint}</span>
<span className="h-1.5 w-1.5 shrink-0 rounded-full bg-gradient-to-r from-accent-blue to-accent-mint" />
</div>
<h3 className="text-lg font-semibold text-ink">{title}</h3>
<p className="text-sm text-inkMuted">every {intervalLabel}</p>
</div>
<div className="flex flex-col items-end gap-2 text-right">
<div className="flex items-center gap-2">
<span
className={`inline-flex items-center gap-2 rounded-full border px-3 py-2 text-sm font-semibold shadow-sm ${badgeColor}`}
>
<span className={`h-2.5 w-2.5 rounded-full ${dotColor}`} />
{statusLabel}
</span>
<button
type="button"
onClick={onManualTrigger}
className="inline-flex items-center gap-2 rounded-full bg-ink text-white px-3 py-2 text-xs font-semibold shadow-card transition hover:bg-inkSoft focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ink"
>
<span className="h-2 w-2 rounded-full bg-white" aria-hidden />
Poll now
</button>
</div>
<span className="text-xs font-semibold uppercase tracking-[0.08em] text-inkMuted">
{now}
</span>
</div>
</div>
<div className="mt-4 flex flex-col gap-3">
<div className="rounded-2xl border border-black/5 bg-white/80 px-3 py-2">
<p className="text-xs uppercase tracking-[0.08em] text-inkMuted">
Last updated
</p>
<p className="text-base font-semibold text-ink">{lastUpdatedLabel}</p>
</div>
<div className="relative overflow-hidden rounded-2xl border border-black/5 bg-gradient-to-br from-white via-white to-slate-50 p-4">
<div className="pointer-events-none absolute inset-0 opacity-60 mix-blend-multiply">
<div className="absolute inset-0 bg-[radial-gradient(circle_at_10%_20%,rgba(96,165,250,0.12),transparent_35%),radial-gradient(circle_at_80%_0%,rgba(52,211,153,0.12),transparent_30%)]" />
</div>
<div className="flex items-center justify-between text-xs text-inkMuted">
<span>ECG signal</span>
<span className="inline-flex items-center gap-2 rounded-full bg-black/5 px-2 py-1 font-semibold">
<span className={`h-2 w-2 rounded-full ${dotColor}`} />
{loading ? "Polling" : healthy ? "Healthy" : "Unhealthy"}
</span>
</div>
<svg
viewBox="0 0 360 120"
preserveAspectRatio="none"
className="mt-2 h-28 w-full text-success drop-shadow-sm"
>
<defs>
<linearGradient id={signalId} x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stopColor={strokeColor} stopOpacity="0.9" />
<stop offset="50%" stopColor={strokeColor} stopOpacity="0.75" />
<stop offset="100%" stopColor={strokeColor} stopOpacity="0.95" />
</linearGradient>
</defs>
<polyline
points={wavePoints}
fill="none"
stroke={`url(#${signalId})`}
strokeWidth="3"
strokeLinecap="round"
strokeLinejoin="round"
/>
<circle
cx="360"
cy={waveHeight}
r="6"
className={`stroke-white/40 ${dotColor}`}
/>
</svg>
</div>
<div className="grid grid-cols-2 gap-3 text-sm">
<div className="rounded-2xl border border-black/5 bg-white/80 px-3 py-2">
<p className="text-xs uppercase tracking-[0.08em] text-inkMuted">Status</p>
<p className="text-base font-semibold text-ink">{statusDetail}</p>
</div>
<div className="rounded-2xl border border-black/5 bg-white/80 px-3 py-2">
<p className="text-xs uppercase tracking-[0.08em] text-inkMuted">Endpoint</p>
<p className="text-xs font-mono text-ink">{state.attemptedUrl}</p>
</div>
</div>
{children}
</div>
</article>
);
}