Files
HiveGrid-Wireframes/hivegrid-cloud/apps/web/app/modules/users/components/RegisterScreen.tsx
T

235 lines
10 KiB
TypeScript

import { useMemo, useState } from "react";
import { Form, Link, useNavigation, useSearchParams } from "react-router";
import { useTranslation } from "react-i18next";
import { FeatherMail } from "@subframe/core";
import { useRootData } from "~/utils/data/useRootData";
import { AuthShell, AuthStatus } from "~/components/auth/AuthShell";
import { Button } from "~/ui/components/Button";
import { Checkbox } from "~/ui/components/Checkbox";
import { LinkButton } from "~/ui/components/LinkButton";
import { Progress } from "~/ui/components/Progress";
import { TextInput } from "~/ui/components/TextInput";
import Captcha from "~/components/captcha/Captcha";
import { getPasswordChecks, type PasswordPolicy } from "~/modules/users/components/RegisterForm";
// Signup-Screen nach Davids Subframe-Design ("Signup"): Logo + Card (Konto erstellen) + E-Mail, Passwort
// (+ Staerke-Progress), Firmenname, Vor-/Nachname, Telefon, USt-IdNr, 3 Consent-Checkboxen + "Konto erstellen".
// Voll an die bestehende Register-Action verdrahtet (gleiche Feld-Namen, Password-Policy, Captcha, Lead-Felder).
// verificationEmailSent -> Davids Success-Status. Footer "Schon ein Konto? Anmelden".
//
// 💡 Backend-Luecken (gerendert + gepostet, aber von registerFromRequest noch nicht persistiert):
// - phone: User-Modell hat die Spalte (accounts.prisma) -> Action muss das Feld nur noch lesen.
// - vatNumber (USt-IdNr): keine Spalte im LOCKED-Modell -> braucht Data-Model-Entscheidung.
// - 3 Consents: Backend nutzt impliziten Terms-Consent; hier als Client-Gate (kein Server-Tracking).
type RegisterData = { company?: string; firstName?: string; lastName?: string; email?: string; slug?: string };
export default function RegisterScreen({
requireCaptcha = false,
error,
verificationEmailSent,
data = {},
leadConversion,
passwordPolicy,
}: {
requireCaptcha?: boolean;
error?: string;
verificationEmailSent?: boolean;
data?: RegisterData;
leadConversion?: { leadId: string; token: string; planId?: string | null };
passwordPolicy?: PasswordPolicy;
}) {
const { t } = useTranslation();
const { appConfiguration, csrf } = useRootData();
const navigation = useNavigation();
const [searchParams] = useSearchParams();
const isSubmitting = navigation.state !== "idle";
const redirect = searchParams.get("redirect") ?? "";
const [password, setPassword] = useState("");
const [acceptTerms, setAcceptTerms] = useState(false);
const [acceptPrivacy, setAcceptPrivacy] = useState(false);
const [acceptDpa, setAcceptDpa] = useState(false);
const allConsents = acceptTerms && acceptPrivacy && acceptDpa;
const showPasswordInput = !appConfiguration.auth.requireEmailVerification;
const checks = useMemo(() => getPasswordChecks(password, passwordPolicy), [password, passwordPolicy]);
const strength = checks.length === 0 ? 0 : Math.round((checks.filter((c) => c.valid).length / checks.length) * 100);
if (verificationEmailSent) {
return (
<AuthStatus
icon={<FeatherMail />}
tone="positive"
title={t("account.verify.title")}
message={t("account.verify.emailSent")}
dataUiId="ui.auth.register.verificationSent"
>
<Link to="/login" className="no-underline">
<LinkButton size="small">{t("account.login.backToLogin")}</LinkButton>
</Link>
</AuthStatus>
);
}
return (
<AuthShell
title={t("account.register.title")}
subtitle={t("account.register.subtitle")}
cardClassName="max-w-[480px] gap-5 shadow-lg"
dataUiId="ui.auth.register.page"
footer={
<>
<span className="text-body-small-regular-0 font-body-small-regular-0 text-text-neutral-tertiary">
{t("account.register.haveAccount")}
</span>
<Link to="/login" className="no-underline" data-ui-id="ui.auth.register.login">
<LinkButton size="small" hideIcon>
{t("account.register.signIn")}
</LinkButton>
</Link>
</>
}
>
{appConfiguration.auth.authMethods.emailPassword.enabled ? (
<Form method="post" noValidate className="flex w-full flex-col gap-5" data-ui-id="ui.auth.register.form">
<input type="hidden" name="redirectTo" value={redirect} readOnly />
<input type="hidden" name="csrf" value={csrf} readOnly />
{leadConversion ? (
<>
<input type="hidden" name="leadId" value={leadConversion.leadId} readOnly />
<input type="hidden" name="leadToken" value={leadConversion.token} readOnly />
{leadConversion.planId ? <input type="hidden" name="leadPlanId" value={leadConversion.planId} readOnly /> : null}
</>
) : null}
{/* Davids Felder-Gruppe: alle Inputs in einem gap-4-Wrapper; Form-gap-5 trennt Gruppe/Consent/Button. */}
<div className="flex w-full flex-col items-start gap-4">
<TextInput className="h-auto w-full flex-none" fillContainer showLabel label={t("account.shared.email")} type="email">
<TextInput.Input
name="email"
type="email"
required
autoComplete="email"
placeholder={t("account.shared.emailPlaceholder")}
defaultValue={data.email}
readOnly={Boolean(leadConversion)}
disabled={isSubmitting}
data-ui-id="ui.auth.register.email"
/>
</TextInput>
{showPasswordInput ? (
<>
<div className="flex w-full flex-col items-start gap-2">
<div className="flex w-full items-end gap-2">
<TextInput className="h-auto grow shrink-0 basis-0" fillContainer showLabel label={t("account.shared.password")} type="password">
<TextInput.Input
name="password"
type="password"
required
autoComplete="new-password"
placeholder={t("account.shared.password")}
value={password}
onChange={(event) => setPassword(event.target.value)}
disabled={isSubmitting}
data-ui-id="ui.auth.register.password"
/>
</TextInput>
</div>
{passwordPolicy ? (
<>
<Progress className="h-auto w-full flex-none" hideLabel value={strength} />
<span className="text-body-small-medium-0 font-body-small-medium-0 text-text-neutral-tertiary">
{t("account.register.passwordStrength")}
</span>
</>
) : null}
</div>
{passwordPolicy ? (
<span className="w-full text-body-small-regular-0 font-body-small-regular-0 text-text-neutral-tertiary">
{t("account.register.passwordHint")}
</span>
) : null}
</>
) : null}
<TextInput className="h-auto w-full flex-none" fillContainer showLabel label={t("account.register.companyLabel")}>
<TextInput.Input
name="company"
required={appConfiguration.auth.requireOrganization}
placeholder={t("account.shared.companyPlaceholder")}
defaultValue={data.company}
disabled={isSubmitting}
data-ui-id="ui.auth.register.company"
/>
</TextInput>
<div className="flex w-full items-start gap-4 mobile:flex-col">
<TextInput className="h-auto grow shrink-0 basis-0" showLabel label={t("models.user.firstName")}>
<TextInput.Input
name="first-name"
placeholder={t("account.shared.firstNamePlaceholder")}
defaultValue={data.firstName}
disabled={isSubmitting}
data-ui-id="ui.auth.register.firstName"
/>
</TextInput>
<TextInput className="h-auto grow shrink-0 basis-0" showLabel label={t("models.user.lastName")}>
<TextInput.Input
name="last-name"
placeholder={t("account.shared.lastNamePlaceholder")}
defaultValue={data.lastName}
disabled={isSubmitting}
data-ui-id="ui.auth.register.lastName"
/>
</TextInput>
</div>
<TextInput className="h-auto w-full flex-none" fillContainer showLabel label={t("account.register.phone")} type="tel">
<TextInput.Input
name="phone"
type="tel"
placeholder={t("account.register.phonePlaceholder")}
disabled={isSubmitting}
data-ui-id="ui.auth.register.phone"
/>
</TextInput>
<TextInput className="h-auto w-full flex-none" fillContainer showLabel label={t("account.register.vatId")}>
<TextInput.Input
name="vatNumber"
placeholder={t("account.register.vatIdPlaceholder")}
disabled={isSubmitting}
data-ui-id="ui.auth.register.vat"
/>
</TextInput>
</div>
<div className="flex w-full flex-col items-start gap-3">
<Checkbox label={t("account.register.consentTerms")} checked={acceptTerms} onCheckedChange={setAcceptTerms} />
<Checkbox label={t("account.register.consentPrivacy")} checked={acceptPrivacy} onCheckedChange={setAcceptPrivacy} />
<Checkbox label={t("account.register.consentDpa")} checked={acceptDpa} onCheckedChange={setAcceptDpa} />
</div>
<Captcha
enabled={requireCaptcha && appConfiguration.auth.captcha.enabled}
instanceUrl={appConfiguration.auth.captcha.instanceUrl}
sitekey={appConfiguration.auth.captcha.loginSiteKey}
/>
<Button type="submit" loading={isSubmitting} disabled={isSubmitting || !allConsents} data-ui-id="ui.auth.register.submit">
{t("account.register.title")}
</Button>
{error && navigation.state === "idle" ? (
<p className="text-center text-body-small-regular-0 font-body-small-regular-0 text-text-feedback-negative" role="alert">
{error}
</p>
) : null}
</Form>
) : null}
</AuthShell>
);
}