feat(slice-3): Create-storefront screen + API helper (§5.3, PUC-4/5/7)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 22:41:04 -07:00
parent ddc949615e
commit 10d22938f1
2 changed files with 110 additions and 0 deletions
+18
View File
@@ -59,3 +59,21 @@ export async function verifyCode(
export async function logout(): Promise<void> {
await fetch("/api/auth/logout", { method: "POST", credentials: "include" });
}
export interface StorefrontResult {
id: number;
name: string;
}
export async function createStorefront(
name: string,
): Promise<{ ok: true; storefront: StorefrontResult } | { ok: false; error: ApiError }> {
const resp = await fetch("/api/storefronts", {
method: "POST",
headers: { "content-type": "application/json" },
credentials: "include",
body: JSON.stringify(name.trim() ? { name: name.trim() } : {}),
});
if (resp.ok) return { ok: true, storefront: (await resp.json()) as StorefrontResult };
return { ok: false, error: await errorOf(resp) };
}
+92
View File
@@ -0,0 +1,92 @@
// Create storefront (SD-0001 §5.3) — a storefront-less Merchant establishes their one
// storefront (PUC-4/5; PUC-7 defense-in-depth on 409). Visuals per the ui/designs export
// (hf-storefront). The welcome banner carries PUC-3's honest copy from the verify step.
import { useState } from "react";
import { createStorefront, logout, type StorefrontResult } from "../api";
import { AccountChip, AuthCard, Banner, Eyebrow, Field, Footer, PrimaryButton, Screen, TopBar, Wordmark } from "../ui/kit";
interface Props {
email: string;
welcome: "new" | "back" | null;
onCreated: (storefront: StorefrontResult) => void;
onAlreadyOwns: () => void;
onSignedOut: () => void;
}
export default function CreateStorefront({ email, welcome, onCreated, onAlreadyOwns, onSignedOut }: Props) {
const [name, setName] = useState("");
const [busy, setBusy] = useState(false);
const [alreadyOwns, setAlreadyOwns] = useState(false);
const [error, setError] = useState<string | null>(null);
async function signOut() {
await logout();
onSignedOut();
}
async function submit(e: React.FormEvent) {
e.preventDefault();
setBusy(true);
setError(null);
const res = await createStorefront(name);
setBusy(false);
if (res.ok) {
onCreated(res.storefront);
return;
}
if (res.error.code === "already_owns_storefront") setAlreadyOwns(true);
else setError(res.error.message);
}
return (
<Screen horizon>
<TopBar left={<Wordmark size={22} />} right={<AccountChip email={email} onSignOut={signOut} />} />
<main className="screen__main">
<AuthCard>
<form onSubmit={submit} style={{ display: "contents" }}>
<div>
<Eyebrow>One storefront, fully yours</Eyebrow>
<h1 style={{ marginTop: 12 }}>Create your storefront</h1>
<p className="card__sub">
This is the one thing to do right now. It costs nothing and commits you to nothing.
</p>
</div>
{welcome && (
<Banner tone="info" title={welcome === "new" ? "Welcome to ecomm" : "Welcome back"}>
{welcome === "new"
? "A new account was created for this email."
: "Signed in to your existing account."}
</Banner>
)}
{alreadyOwns && (
<Banner title="Your account already has its storefront">
ecomm is one storefront per account today.{" "}
<button type="button" className="lk" onClick={onAlreadyOwns}>
Go to your admin
</button>
</Banner>
)}
{error && <Banner title="That didn't work">{error} Try again.</Banner>}
<Field
label="Storefront name"
optional
helper="You can leave this blank — we'll pick a placeholder name you can change any time."
inputProps={{
type: "text",
value: name,
placeholder: "e.g. Ben's Bets",
autoFocus: true,
onChange: (ev) => setName(ev.target.value),
}}
/>
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
<PrimaryButton busy={busy}>{busy ? "Creating…" : "Create storefront"}</PrimaryButton>
<p className="note">You can rename, configure, or close your storefront whenever you like.</p>
</div>
</form>
</AuthCard>
</main>
<Footer />
</Screen>
);
}