feat: Updated marketing site

This commit is contained in:
2026-07-28 12:46:28 +01:00
parent febe48a974
commit f10e61cde4
39 changed files with 599 additions and 1050 deletions
+50 -60
View File
@@ -9,7 +9,7 @@ import { Group } from "@/components/settings/Group";
import { inputClass } from "@/components/settings/Field";
/*
* A licence is a document issued, dated, signed, and carrying a reference
* A licence is a document issued, dated, signed, and carrying a reference
* you quote to support. This page is built to read as that document rather
* than as another settings form, which is also how adminsite/ presents the
* same object from the issuing side: state as a pill that carries a label and
@@ -34,7 +34,7 @@ const STATE: Record<LicenseState, { label: string; rule: string; text: string }>
// this app's to choose. Each one says what is wrong and what to do about it.
const REASON: Record<string, string> = {
no_license: "No licence is installed on this instance. Paste one below to activate it.",
bad_signature: "This licence's signature could not be verified. It may have been edited or truncated in transit paste the original again.",
bad_signature: "This licence's signature could not be verified. It may have been edited or truncated in transit paste the original again.",
deployment_mismatch: "This licence was issued for a different kind of deployment, so it cannot be used here.",
instance_mismatch: "This licence was issued for a different instance. Check the instance ID below against the one you bought against.",
expired: "This licence has passed its expiry date. Your servers and monitors keep running, but changes are disabled until it is renewed.",
@@ -60,25 +60,19 @@ const SUPPORT_LABELS: Record<string, string> = {
/*
* Retention is a duration, not a cap, so it gets a fact rather than a meter.
* Allowance's bar needs a numerator and there isn't one how much of a
* Allowance's bar needs a numerator and there isn't one how much of a
* retention window have you "used"?
*/
function Retention({ days }: { days: number }) {
const forever = days === UNLIMITED;
return (
<Card padding={false} className="p-4">
<p className="font-mono text-[0.62rem] uppercase tracking-[0.14em] text-text-tertiary">
Audit history
</p>
<p className="font-mono text-[0.62rem] uppercase tracking-[0.14em] text-text-tertiary">Audit history</p>
<p className="mt-2 text-2xl font-extrabold tracking-[-0.03em] tabular-nums text-text-primary">
{forever ? "Kept" : days}
<span className="ml-1.5 text-sm font-medium tracking-normal text-text-secondary">
{forever ? "indefinitely" : "days"}
</span>
</p>
<p className="mt-3 font-mono text-[0.62rem] uppercase tracking-[0.14em] text-text-secondary">
{forever ? "Never trimmed" : "Older entries removed daily"}
<span className="ml-1.5 text-sm font-medium tracking-normal text-text-secondary">{forever ? "indefinitely" : "days"}</span>
</p>
<p className="mt-3 font-mono text-[0.62rem] uppercase tracking-[0.14em] text-text-secondary">{forever ? "Never trimmed" : "Older entries removed daily"}</p>
</Card>
);
}
@@ -137,9 +131,7 @@ function Feature({ label, included }: { label: string; included: boolean }) {
{included ? "✓" : ""}
</span>
<span className="text-sm text-text-primary">{label}</span>
<span className={`ml-auto font-mono text-[0.62rem] uppercase tracking-[0.14em] ${included ? "text-success" : "text-text-tertiary"}`}>
{included ? "Included" : "Not included"}
</span>
<span className={`ml-auto font-mono text-[0.62rem] uppercase tracking-[0.14em] ${included ? "text-success" : "text-text-tertiary"}`}>{included ? "Included" : "Not included"}</span>
</div>
);
}
@@ -162,9 +154,7 @@ function RecordPanel({ license }: { license: LicenseInfo }) {
already carries the colour. A dot would be the third telling. */}
<h2 className={`text-2xl font-extrabold tracking-[-0.03em] ${s.text}`}>{s.label}</h2>
{license.tier && <p className="text-sm text-text-secondary">{license.tier} tier</p>}
{license.support_level && SUPPORT_LABELS[license.support_level] && (
<p className="text-sm text-text-secondary">{SUPPORT_LABELS[license.support_level]}</p>
)}
{license.support_level && SUPPORT_LABELS[license.support_level] && <p className="text-sm text-text-secondary">{SUPPORT_LABELS[license.support_level]}</p>}
</div>
{license.reason && <p className="mt-3 max-w-prose text-sm text-text-secondary">{REASON[license.reason] ?? license.reason}</p>}
@@ -297,51 +287,51 @@ export default function LicensePage() {
</Card>
</Group>
) : (
<Group label="Add or replace">
<Card>
<label htmlFor="licence-blob" className="mb-1.5 block text-sm font-medium text-text-secondary">
Licence key
</label>
<textarea
id="licence-blob"
className={`${inputClass} h-32 resize-y font-mono text-xs`}
placeholder="Paste your licence key"
value={blob}
onChange={(e) => {
setBlob(e.target.value);
setFileName("");
}}
/>
<div className="mt-3 flex flex-wrap items-center gap-3">
{/* A bare file input cannot be styled, so the button is the
label and the input itself is visually hidden. */}
<label className="inline-flex cursor-pointer items-center gap-2 rounded border border-border bg-surface px-3 py-1.5 text-sm font-semibold text-text-primary transition-colors hover:border-text-tertiary focus-within:ring-2 focus-within:ring-accent">
<input
type="file"
accept=".lic,.txt"
className="sr-only"
onChange={async (e) => {
const f = e.target.files?.[0];
if (!f) return;
setBlob((await f.text()).trim());
setFileName(f.name);
}}
/>
Choose a file
<Group label="Add or replace">
<Card>
<label htmlFor="licence-blob" className="mb-1.5 block text-sm font-medium text-text-secondary">
Licence key
</label>
<span className="text-xs text-text-tertiary">{fileName ? `Loaded ${fileName}` : "or paste the key above"}</span>
</div>
<textarea
id="licence-blob"
className={`${inputClass} h-32 resize-y font-mono text-xs`}
placeholder="Paste your licence key"
value={blob}
onChange={(e) => {
setBlob(e.target.value);
setFileName("");
}}
/>
{error && <p className="mt-4 rounded border border-danger/30 bg-danger/10 px-3 py-2 text-sm text-danger">{error}</p>}
<div className="mt-3 flex flex-wrap items-center gap-3">
{/* A bare file input cannot be styled, so the button is the
label and the input itself is visually hidden. */}
<label className="inline-flex cursor-pointer items-center gap-2 rounded border border-border bg-surface px-3 py-1.5 text-sm font-semibold text-text-primary transition-colors hover:border-text-tertiary focus-within:ring-2 focus-within:ring-accent">
<input
type="file"
accept=".lic,.txt"
className="sr-only"
onChange={async (e) => {
const f = e.target.files?.[0];
if (!f) return;
setBlob((await f.text()).trim());
setFileName(f.name);
}}
/>
Choose a file
</label>
<span className="text-xs text-text-tertiary">{fileName ? `Loaded ${fileName}` : "or paste the key above"}</span>
</div>
<div className="mt-5 border-t border-border-soft pt-5">
<Button type="button" variant="primary" loading={save.isPending} disabled={!blob.trim()} onClick={() => save.mutate()}>
{save.isPending ? "Checking…" : "Save licence"}
</Button>
</div>
</Card>
</Group>
{error && <p className="mt-4 rounded border border-danger/30 bg-danger/10 px-3 py-2 text-sm text-danger">{error}</p>}
<div className="mt-5 border-t border-border-soft pt-5">
<Button type="button" variant="primary" loading={save.isPending} disabled={!blob.trim()} onClick={() => save.mutate()}>
{save.isPending ? "Checking…" : "Save licence"}
</Button>
</div>
</Card>
</Group>
)}
</div>
</div>
+13 -17
View File
@@ -3,23 +3,19 @@ import "./globals.css";
import { Providers } from "@/components/Providers";
export const metadata: Metadata = {
title: "Vantage",
description: "Self-hosted SSH key management",
title: "Vantage",
description: "Self-hosted SSH key management",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
// data-theme is what site/ and adminsite/ switch on. This app is locked to
// dark, so the attribute is fixed rather than toggleable — it is here so
// the three apps declare their ground the same way.
<html lang="en" className="dark" data-theme="dark">
<body className="bg-background font-sans text-text-primary">
<Providers>{children}</Providers>
</body>
</html>
);
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
// data-theme is what site/ and adminsite/ switch on. This app is locked to
// dark, so the attribute is fixed rather than toggleable it is here so
// the three apps declare their ground the same way.
<html lang="en" className="dark" data-theme="dark">
<body className="bg-background font-sans text-text-primary">
<Providers>{children}</Providers>
</body>
</html>
);
}
+4 -8
View File
@@ -89,20 +89,16 @@ export default function SetupPage() {
<Card>
<p className="text-sm text-text-secondary">
{created.slug} has its own address, and sign-in is kept separate per instance. Continue to your instance&apos;s sign-in page and log in with the email and password
you just chose.
{created.slug} has its own address, and sign-in is kept separate per instance. Continue to your instance&apos;s sign-in page and log in with the email and password you just
chose.
</p>
<code className="mt-3 block overflow-x-auto rounded bg-surface-2 px-2 py-1.5 font-mono text-xs text-text-primary">{created.loginUrl}</code>
<div className="mt-4 rounded border border-border p-3">
<p className="text-xs text-text-tertiary">Instance ID needed to activate a licence</p>
<p className="text-xs text-text-tertiary">Instance ID needed to activate a licence</p>
<div className="mt-1 flex items-center gap-2">
<code className="overflow-x-auto font-mono text-xs text-text-primary">{created.instanceId}</code>
<button
type="button"
className="text-xs underline"
onClick={() => navigator.clipboard.writeText(created.instanceId)}
>
<button type="button" className="text-xs underline" onClick={() => navigator.clipboard.writeText(created.instanceId)}>
Copy
</button>
</div>