feat(adminsite): instance cards and the customer overview
State reads three ways on every card -- a stripe, a shaped-and-labelled pill, and the copy -- so it survives a colourblind reader and a glance at arm's length. Colour alone would fail on the one screen where getting it wrong costs money. The expired card leads with what still works, because that is the first thing a worried customer wants to know and the backend really does keep servers, monitors and agent keys running. The awaiting-link card is deliberately loud: a customer who has paid and not linked has paid for nothing yet. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
import Link from "next/link";
|
||||
import clsx from "clsx";
|
||||
import type { Instance, License } from "@/lib/api";
|
||||
import { daysRemaining, formatDate, licenceState } from "@/lib/format";
|
||||
import { StatePill } from "./StatePill";
|
||||
|
||||
const STRIPE = {
|
||||
valid: "before:bg-valid",
|
||||
warn: "before:bg-warn",
|
||||
expired: "before:bg-expired",
|
||||
none: "before:bg-accent",
|
||||
} as const;
|
||||
|
||||
export function InstanceCard({ instance, license }: { instance: Instance; license?: License }) {
|
||||
const state = licenceState(license?.expires_at, Boolean(license));
|
||||
const days = license ? daysRemaining(license.expires_at) : 0;
|
||||
const cloud = instance.deployment === "cloud";
|
||||
|
||||
return (
|
||||
<article
|
||||
className={clsx(
|
||||
"relative grid gap-3 rounded border border-rule bg-panel p-4 pl-5",
|
||||
"before:absolute before:inset-y-0 before:left-0 before:w-1 before:content-['']",
|
||||
STRIPE[state],
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<h3 className="text-lg">{instance.name || "Unnamed instance"}</h3>
|
||||
<p className="font-mono text-[0.72rem] uppercase tracking-[0.1em] text-ink-3">
|
||||
{cloud ? "Cloud" : "Self-hosted"}
|
||||
{instance.tier ? ` · ${instance.tier.replace("_", " ")}` : ""}
|
||||
</p>
|
||||
</div>
|
||||
<StatePill state={state} />
|
||||
</div>
|
||||
|
||||
{state === "expired" && (
|
||||
<p className="text-[0.82rem] text-ink-2">
|
||||
Servers and monitors are still running, and your agents keep their keys. Changes
|
||||
are disabled until you renew.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{state === "none" && (
|
||||
<p className="text-[0.82rem] text-ink-2">
|
||||
You have paid for this but it is not attached to an install yet, so no licence
|
||||
has been issued. Linking takes a minute.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{license && state !== "expired" && (
|
||||
<div className="grid gap-1 font-mono text-[0.82rem] tabular-nums text-ink-2">
|
||||
<span>{days} days remaining</span>
|
||||
<div className="h-[3px] overflow-hidden rounded-sm bg-rule-soft">
|
||||
<div
|
||||
className={clsx("h-full", state === "warn" ? "bg-warn" : "bg-valid")}
|
||||
style={{ width: `${Math.max(2, Math.min(100, (days / 365) * 100))}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span>Renews {formatDate(license.expires_at)}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{state === "none" ? (
|
||||
<Link
|
||||
href="/instances/link"
|
||||
className="justify-self-start text-[0.82rem] font-semibold text-accent underline"
|
||||
>
|
||||
Link an install
|
||||
</Link>
|
||||
) : cloud && instance.slug ? (
|
||||
<a
|
||||
href={`https://${instance.slug}.vantage.hostxtra.co.uk`}
|
||||
className="justify-self-start text-[0.82rem] font-semibold text-accent underline"
|
||||
>
|
||||
Open {instance.slug}.vantage.hostxtra.co.uk
|
||||
</a>
|
||||
) : (
|
||||
<Link
|
||||
href={`/instances/${instance.instance_id}`}
|
||||
className="justify-self-start text-[0.82rem] font-semibold text-accent underline"
|
||||
>
|
||||
{state === "expired" ? "Renew and download" : "Licence and download"}
|
||||
</Link>
|
||||
)}
|
||||
</article>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user