feat: Updated marketing site
This commit is contained in:
@@ -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
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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'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'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>
|
||||
|
||||
Reference in New Issue
Block a user