feat(site): /start creates an account, not an instance
The form posts to admin's signup and the slug preview goes: there is no instance at this point, and previewing one promises something the submission does not create. Creating the instance is now a step in the portal, which the page's What happens next panel spells out. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+10
-13
@@ -1,9 +1,9 @@
|
||||
import type { Metadata } from "next";
|
||||
import { InstanceForm } from "@/components/InstanceForm";
|
||||
import { AccountForm } from "@/components/AccountForm";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Vantage Cloud",
|
||||
description: "An instance owns its servers, keys, workflows, monitors and secrets. Free for three servers, hosted or self-hosted.",
|
||||
description: "Your account is where instances, licences and billing live. Free for three servers, hosted or self-hosted.",
|
||||
};
|
||||
|
||||
export default function StartPage() {
|
||||
@@ -12,14 +12,13 @@ export default function StartPage() {
|
||||
<div className="split">
|
||||
<div>
|
||||
<span className="tag">Vantage Cloud</span>
|
||||
<h1 style={{ fontSize: "var(--s-3)", margin: "0.8rem 0 1rem", maxWidth: "15ch" }}>Set up your instance.</h1>
|
||||
<h1 style={{ fontSize: "var(--s-3)", margin: "0.8rem 0 1rem", maxWidth: "15ch" }}>Create your account.</h1>
|
||||
<p className="lede" style={{ fontSize: "var(--s-0)" }}>
|
||||
An instance owns its servers, keys, workflows, monitors and secrets. Nothing inside it is visible to any other instance. Confirm your email and it is created with you
|
||||
as its owner.
|
||||
Your account is where instances, licences and billing live. Confirm your email and you can create a free instance straight away — three servers, hosted by us.
|
||||
</p>
|
||||
|
||||
<div className="card" style={{ marginTop: "1.9rem" }}>
|
||||
<InstanceForm />
|
||||
<AccountForm />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -30,23 +29,21 @@ export default function StartPage() {
|
||||
<span className="spec__k">FIRST</span>
|
||||
<div>
|
||||
<h3>Confirm your email</h3>
|
||||
<p>We send a link that works once. Your instance is created when you open it, not before.</p>
|
||||
<p>We send a link that works once. Your account is created when you open it, not before.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="spec">
|
||||
<span className="spec__k">THEN</span>
|
||||
<div>
|
||||
<h3>Add a key</h3>
|
||||
<p>
|
||||
Paste the contents of <code>~/.ssh/id_ed25519.pub</code>. Vantage fingerprints it and refuses duplicates.
|
||||
</p>
|
||||
<h3>Create your instance</h3>
|
||||
<p>One click in the portal. It gets its own subdomain and a free licence, and you are its owner.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="spec">
|
||||
<span className="spec__k">THEN</span>
|
||||
<div>
|
||||
<h3>Add a server</h3>
|
||||
<p>Run the install command as root. It expires in an hour and works once.</p>
|
||||
<h3>Add a key and a server</h3>
|
||||
<p>Paste your public key, then run the install command as root. It expires in an hour and works once.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="spec">
|
||||
|
||||
@@ -2,20 +2,11 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { Honeypot } from "@/components/Honeypot";
|
||||
import { submitSignup, type SubmitResult } from "@/lib/submit";
|
||||
import { submitAccountSignup, type SubmitResult } from "@/lib/submit";
|
||||
|
||||
const MIN_PASSWORD = 12;
|
||||
|
||||
function slugify(value: string) {
|
||||
return value
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-|-$/g, "");
|
||||
}
|
||||
|
||||
export function InstanceForm() {
|
||||
const [slug, setSlug] = useState("");
|
||||
export function AccountForm() {
|
||||
const [result, setResult] = useState<SubmitResult>({ state: "idle" });
|
||||
const sending = result.state === "sending";
|
||||
|
||||
@@ -24,8 +15,8 @@ export function InstanceForm() {
|
||||
const data = new FormData(event.currentTarget);
|
||||
setResult({ state: "sending" });
|
||||
setResult(
|
||||
await submitSignup({
|
||||
instance_name: String(data.get("instance_name") ?? ""),
|
||||
await submitAccountSignup({
|
||||
name: String(data.get("name") ?? ""),
|
||||
email: String(data.get("email") ?? ""),
|
||||
password: String(data.get("password") ?? ""),
|
||||
website: String(data.get("website") ?? ""),
|
||||
@@ -38,7 +29,7 @@ export function InstanceForm() {
|
||||
<div role="status">
|
||||
<h2 style={{ fontSize: "var(--s-1)" }}>Check your email.</h2>
|
||||
<p style={{ color: "var(--ink-2)", marginTop: "0.5rem" }}>
|
||||
We sent a confirmation link. Open it and <b>{slug || "your instance"}</b> is created with you as its owner. The link works once and expires in 24 hours.
|
||||
We sent a confirmation link. Open it and your Vantage account is ready — then you can create your first instance from the portal. The link works once and expires in 24 hours.
|
||||
</p>
|
||||
<p style={{ color: "var(--ink-3)", marginTop: "0.75rem", fontSize: "0.88rem" }}>
|
||||
Nothing exists until you confirm if the email does not arrive, start again or contact support@hostxtra.co.uk.
|
||||
@@ -54,14 +45,11 @@ export function InstanceForm() {
|
||||
<Honeypot />
|
||||
|
||||
<div className="field">
|
||||
<label htmlFor="o-instance">Instance name</label>
|
||||
<input id="o-instance" name="instance_name" type="text" placeholder="Northgate Systems" required onChange={(e) => setSlug(slugify(e.target.value))} aria-describedby="o-instance-err" />
|
||||
<span className="hostline">
|
||||
<b>{slug || "your-instance"}</b>.vantage.hostxtra.co.uk
|
||||
</span>
|
||||
{fieldError("instance_name") && (
|
||||
<small id="o-instance-err" className="field__err">
|
||||
{fieldError("instance_name")}
|
||||
<label htmlFor="o-name">Your organisation</label>
|
||||
<input id="o-name" name="name" type="text" placeholder="Northgate Systems" required aria-describedby="o-name-err" />
|
||||
{fieldError("name") && (
|
||||
<small id="o-name-err" className="field__err">
|
||||
{fieldError("name")}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
+16
-4
@@ -9,6 +9,7 @@
|
||||
*/
|
||||
|
||||
const SITE_API = (process.env.NEXT_PUBLIC_SITE_API ?? "").replace(/\/$/, "");
|
||||
const ADMIN_API = (process.env.NEXT_PUBLIC_ADMIN_API_URL ?? "").replace(/\/$/, "");
|
||||
const FALLBACK_ADDRESS = process.env.NEXT_PUBLIC_CONTACT_EMAIL ?? "support@hostxtra.co.uk";
|
||||
|
||||
export type SubmitState = "idle" | "sending" | "sent" | "error";
|
||||
@@ -56,12 +57,23 @@ export async function submitContact(fields: { name: string; email: string; serve
|
||||
return post(`${SITE_API}/api/contact`, fields);
|
||||
}
|
||||
|
||||
export async function submitSignup(fields: { instance_name: string; email: string; password: string; website: string }): Promise<SubmitResult> {
|
||||
if (!SITE_API) {
|
||||
/*
|
||||
* Account signup posts to the admin service, not sitesvc. The two form targets
|
||||
* are deliberately separate variables rather than one base URL: contact and
|
||||
* signup are owned by different services, and an implied shared host is how they
|
||||
* silently end up pointing at the wrong one.
|
||||
*/
|
||||
export async function submitAccountSignup(fields: {
|
||||
name: string;
|
||||
email: string;
|
||||
password: string;
|
||||
website: string;
|
||||
}): Promise<SubmitResult> {
|
||||
if (!ADMIN_API) {
|
||||
return {
|
||||
state: "error",
|
||||
message: `Signup is not available from here yet. Email ${FALLBACK_ADDRESS} and we will set you up.`,
|
||||
message: `Signup is not connected yet. Email ${FALLBACK_ADDRESS} and we will set you up.`,
|
||||
};
|
||||
}
|
||||
return post(`${SITE_API}/api/signup`, fields);
|
||||
return post(`${ADMIN_API}/auth/signup`, fields);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user