"use client"; import { useState } from "react"; import { useTranslations } from "next-intl"; import { useRouter } from "next/navigation"; import { Card } from "@/components/ui/card"; type FormState = "idle" | "submitting" | "success" | "error"; type AccountType = "personal" | "company"; /** * Registration entry — Bug 1 redesign. * * Previously a hidden checkbox ("Register as an individual") sat on top * of the company-flavoured form, which buried personal accounts under a * single click that most users miss. The new layout puts a primary * account-type chooser at the top: two large cards, one for Personal, * one for Company. Selection is required before the form below * appears, so the rest of the layout adapts cleanly without a * collapsing-checkbox feel. * * Bug 12: per-field validation runs on submit. The native HTML required * attribute already blocks empty submits at the browser level; the * server-side Zod schema in `/api/register` is the authoritative * second line of defence. * * Behaviour: * - "Personal account": company-name field is hidden; on submit, the * server generates an opaque `personal-{8hex}` org name (Bug 9). * - "Company account": company-name field is required; the server * additionally runs the duplicate-domain check. * - Returning users (those who arrive here by accident) can switch * types after picking — the choice cards stay clickable above the * form. Field state is preserved across switches so they don't * have to re-type their name. */ export default function RegisterPage() { const t = useTranslations("register"); const tCommon = useTranslations("common"); const router = useRouter(); const [accountType, setAccountType] = useState(null); const [form, setForm] = useState({ companyName: "", givenName: "", familyName: "", email: "", }); const [state, setState] = useState("idle"); const [error, setError] = useState(""); const isPersonal = accountType === "personal"; const handleChange = (e: React.ChangeEvent) => { setForm((prev) => ({ ...prev, [e.target.name]: e.target.value })); }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!accountType) return; // Should be impossible — submit button is gated setError(""); setState("submitting"); try { // Build the request body explicitly. For personals we omit // companyName so the server generates an opaque ZITADEL org name // (`personal-{8hex}`); the Zod schema accepts the omission. const body: Record = { givenName: form.givenName, familyName: form.familyName, email: form.email, isPersonal, }; if (!isPersonal) { body.companyName = form.companyName; } const res = await fetch("/api/register", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); if (!res.ok) { const data = await res.json(); if (data.code === "duplicate_domain" && data.domain) { throw new Error(t("duplicateDomain", { domain: data.domain })); } throw new Error(data.error || "Registration failed"); } setState("success"); } catch (err: any) { setError(err.message); setState("error"); } }; if (state === "success") { return (

{t("successTitle")}

{t("successDescription")}

); } return (

{t("title")}

{t("subtitle")}

{/* Account type chooser — required first step */}
setAccountType("personal")} label={t("personalCardTitle")} description={t("personalCardDescription")} icon={ } /> setAccountType("company")} label={t("companyCardTitle")} description={t("companyCardDescription")} icon={ } />
{/* Form — only shown after a choice is made. Animation delay-2 lines up with the cards animating in first, so the form feels like it appears in response to selection. */} {accountType && (
{/* Company name — only for company accounts (Bug 2 mirror) */} {!isPersonal && (
)} {/* Name row */}
{/* Email */}
{error && (
{error}
)}

{t("hasAccount")}{" "} {tCommon("login")}

)}

{t("footer")}

); } /** * Account-type radio card. Visually a card, semantically a radio: arrow * keys move between cards, Space/Enter selects. * * Selected state is rendered with the accent ring + tinted background; * unselected is the standard surface-2 with hover affordance. The icon * and text colours intensify when selected to give a clear "this one * is on" signal beyond just the border colour. */ function AccountTypeCard({ selected, onClick, label, description, icon, }: { selected: boolean; onClick: () => void; label: string; description: string; icon: React.ReactNode; }) { return ( ); }