184 lines
4.9 KiB
TypeScript
184 lines
4.9 KiB
TypeScript
"use client";
|
|
|
|
import { useActionState } from "react";
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
import { Checkbox } from "@/components/ui/checkbox";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
|
|
import { registerAction, type RegisterState } from "./actions";
|
|
|
|
const initialRegisterState: RegisterState = { attempt: 0 };
|
|
|
|
export function RegisterForm() {
|
|
const [state, formAction, pending] = useActionState(
|
|
registerAction,
|
|
initialRegisterState,
|
|
);
|
|
|
|
// Bumping the key on each attempt forces uncontrolled inputs to remount with
|
|
// the echoed defaultValue from the server — otherwise React's <form action>
|
|
// semantics would wipe them after a failed submit.
|
|
const attemptKey = state.attempt;
|
|
|
|
return (
|
|
<form action={formAction} className="space-y-4">
|
|
<div className="grid gap-4 sm:grid-cols-2">
|
|
<FormField
|
|
key={`firstName-${attemptKey}`}
|
|
id="firstName"
|
|
name="firstName"
|
|
label="Vorname"
|
|
autoComplete="given-name"
|
|
required
|
|
defaultValue={state.values?.firstName ?? ""}
|
|
error={state.errors?.firstName?.[0]}
|
|
/>
|
|
<FormField
|
|
key={`lastName-${attemptKey}`}
|
|
id="lastName"
|
|
name="lastName"
|
|
label="Nachname"
|
|
autoComplete="family-name"
|
|
required
|
|
defaultValue={state.values?.lastName ?? ""}
|
|
error={state.errors?.lastName?.[0]}
|
|
/>
|
|
</div>
|
|
|
|
<FormField
|
|
key={`email-${attemptKey}`}
|
|
id="email"
|
|
name="email"
|
|
type="email"
|
|
label="E-Mail"
|
|
autoComplete="email"
|
|
required
|
|
defaultValue={state.values?.email ?? ""}
|
|
error={state.errors?.email?.[0]}
|
|
/>
|
|
|
|
<FormField
|
|
key={`password-${attemptKey}`}
|
|
id="password"
|
|
name="password"
|
|
type="password"
|
|
label="Passwort"
|
|
autoComplete="new-password"
|
|
required
|
|
helperText="Mindestens 8 Zeichen."
|
|
error={state.errors?.password?.[0]}
|
|
/>
|
|
|
|
<div className="flex items-start gap-3 pt-2">
|
|
<Checkbox
|
|
key={`acceptPrivacyPolicy-${attemptKey}`}
|
|
id="acceptPrivacyPolicy"
|
|
name="acceptPrivacyPolicy"
|
|
required
|
|
defaultChecked={state.values?.acceptPrivacyPolicy === true}
|
|
aria-invalid={!!state.errors?.acceptPrivacyPolicy?.[0]}
|
|
aria-describedby={
|
|
state.errors?.acceptPrivacyPolicy?.[0]
|
|
? "acceptPrivacyPolicy-error"
|
|
: undefined
|
|
}
|
|
/>
|
|
<div className="grid gap-1 leading-none">
|
|
<Label htmlFor="acceptPrivacyPolicy" className="text-sm font-normal">
|
|
Ich habe die{" "}
|
|
<a
|
|
href="/datenschutz"
|
|
className="underline"
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
>
|
|
Datenschutzerklärung
|
|
</a>{" "}
|
|
gelesen und akzeptiere sie.
|
|
</Label>
|
|
{state.errors?.acceptPrivacyPolicy?.[0] && (
|
|
<p
|
|
id="acceptPrivacyPolicy-error"
|
|
role="alert"
|
|
aria-live="polite"
|
|
className="text-xs text-destructive"
|
|
>
|
|
{state.errors.acceptPrivacyPolicy[0]}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{state.errors?._form?.[0] && (
|
|
<p
|
|
role="alert"
|
|
aria-live="polite"
|
|
className="rounded-md border border-destructive/50 bg-destructive/10 p-3 text-sm text-destructive"
|
|
>
|
|
{state.errors._form[0]}
|
|
</p>
|
|
)}
|
|
|
|
<Button type="submit" className="w-full" disabled={pending}>
|
|
{pending ? "Wird erstellt…" : "Account erstellen"}
|
|
</Button>
|
|
</form>
|
|
);
|
|
}
|
|
|
|
function FormField({
|
|
id,
|
|
name,
|
|
label,
|
|
type = "text",
|
|
required,
|
|
autoComplete,
|
|
helperText,
|
|
defaultValue,
|
|
error,
|
|
}: {
|
|
id: string;
|
|
name: string;
|
|
label: string;
|
|
type?: string;
|
|
required?: boolean;
|
|
autoComplete?: string;
|
|
helperText?: string;
|
|
defaultValue?: string;
|
|
error?: string;
|
|
}) {
|
|
const errorId = error ? `${id}-error` : undefined;
|
|
const helperId = !error && helperText ? `${id}-helper` : undefined;
|
|
return (
|
|
<div className="space-y-1.5">
|
|
<Label htmlFor={id}>{label}</Label>
|
|
<Input
|
|
id={id}
|
|
name={name}
|
|
type={type}
|
|
required={required}
|
|
autoComplete={autoComplete}
|
|
defaultValue={defaultValue}
|
|
aria-invalid={!!error}
|
|
aria-describedby={errorId ?? helperId}
|
|
/>
|
|
{error ? (
|
|
<p
|
|
id={errorId}
|
|
role="alert"
|
|
aria-live="polite"
|
|
className="text-xs text-destructive"
|
|
>
|
|
{error}
|
|
</p>
|
|
) : helperText ? (
|
|
<p id={helperId} className="text-xs text-muted-foreground">
|
|
{helperText}
|
|
</p>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|