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:
@@ -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) };
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user