146 lines
6.4 KiB
TypeScript
146 lines
6.4 KiB
TypeScript
import { useState } from "react";
|
|
import { Form, useLocation, useNavigate, useNavigation } from "react-router";
|
|
import { useTranslation } from "react-i18next";
|
|
import { FeatherAlertTriangle, FeatherCheck } from "@subframe/core";
|
|
import { useRootData } from "~/utils/data/useRootData";
|
|
import { AuthShell, AuthStatus } from "~/components/auth/AuthShell";
|
|
import { Button } from "~/ui/components/Button";
|
|
import { LinkButton } from "~/ui/components/LinkButton";
|
|
import { Progress } from "~/ui/components/Progress";
|
|
import { TextInput } from "~/ui/components/TextInput";
|
|
|
|
// Reset-Password-Screen nach Davids Subframe-Design ("Reset Password" + States): Logo + Card
|
|
// (Neues Passwort + Staerke-Progress + Bestaetigung + "Passwort speichern"). Token kommt aus der URL (t),
|
|
// E-Mail aus dem Loader (e) -> beide als hidden Inputs. action validiert (mismatch/weak/token).
|
|
// success -> Davids "Reset Password _ Success"; fehlende/ungueltige Link-Daten -> "Reset Link _ Invalid".
|
|
|
|
function passwordStrength(pw: string): number {
|
|
if (!pw) return 0;
|
|
const checks = [pw.length >= 8, /[A-Z]/.test(pw), /[a-z]/.test(pw), /\d/.test(pw), /[^A-Za-z0-9]/.test(pw)];
|
|
return Math.round((checks.filter(Boolean).length / checks.length) * 100);
|
|
}
|
|
|
|
export default function ResetPasswordScreen({
|
|
data,
|
|
actionData,
|
|
}: {
|
|
data: { email: string };
|
|
actionData: { success?: string; error?: string; fields?: { verifyToken?: string; email?: string } } | null | undefined;
|
|
}) {
|
|
const { t } = useTranslation();
|
|
const { csrf } = useRootData();
|
|
const navigation = useNavigation();
|
|
const navigate = useNavigate();
|
|
const location = useLocation();
|
|
const isSubmitting = navigation.state !== "idle";
|
|
const verifyToken = actionData?.fields?.verifyToken || new URLSearchParams(location.search).get("t") || "";
|
|
const [password, setPassword] = useState("");
|
|
const strength = passwordStrength(password);
|
|
const strengthLabel =
|
|
strength < 40 ? t("account.newPassword.strengthWeak") : strength < 80 ? t("account.newPassword.strengthMedium") : t("account.newPassword.strengthStrong");
|
|
|
|
if (actionData?.success) {
|
|
return (
|
|
<AuthStatus
|
|
icon={<FeatherCheck />}
|
|
tone="positive"
|
|
title={t("account.newPassword.successTitle")}
|
|
message={t("account.newPassword.successBody")}
|
|
dataUiId="ui.auth.reset.success"
|
|
>
|
|
<LinkButton size="small" hideIcon onClick={() => navigate("/login")} data-ui-id="ui.auth.reset.successBackToLogin">
|
|
{t("account.forgot.backToLogin")}
|
|
</LinkButton>
|
|
</AuthStatus>
|
|
);
|
|
}
|
|
|
|
// Kein/ungueltiger Link (keine E-Mail im e-Param) -> Davids "Reset Link _ Invalid".
|
|
if (!data.email) {
|
|
return (
|
|
<AuthStatus
|
|
icon={<FeatherAlertTriangle />}
|
|
tone="negative"
|
|
title={t("account.newPassword.invalidTitle")}
|
|
message={t("account.newPassword.invalidBody")}
|
|
dataUiId="ui.auth.reset.invalid"
|
|
>
|
|
<Button onClick={() => navigate("/forgot-password")} data-ui-id="ui.auth.reset.requestNew">
|
|
{t("account.newPassword.requestNew")}
|
|
</Button>
|
|
<LinkButton size="small" hideIcon onClick={() => navigate("/login")} data-ui-id="ui.auth.reset.invalidBackToLogin">
|
|
{t("account.forgot.backToLogin")}
|
|
</LinkButton>
|
|
</AuthStatus>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<AuthShell title={t("account.newPassword.title")} subtitle={t("account.newPassword.subtitle")} dataUiId="ui.auth.reset.page">
|
|
<Form method="post" noValidate className="flex w-full flex-col gap-4" data-ui-id="ui.auth.reset.form">
|
|
<input type="hidden" name="csrf" value={csrf} readOnly />
|
|
<input type="hidden" name="verify-token" value={verifyToken} readOnly />
|
|
<input type="hidden" name="email" value={data.email} readOnly />
|
|
|
|
<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.newPassword.passwordLabel")} type="password">
|
|
<TextInput.Input
|
|
name="password"
|
|
type="password"
|
|
required
|
|
autoFocus
|
|
autoComplete="new-password"
|
|
placeholder={t("account.shared.password")}
|
|
value={password}
|
|
onChange={(event) => setPassword(event.target.value)}
|
|
disabled={isSubmitting}
|
|
data-ui-id="ui.auth.reset.password"
|
|
/>
|
|
</TextInput>
|
|
</div>
|
|
<Progress className="h-auto w-full flex-none" hideLabel value={strength} state={strength >= 80 ? "success" : undefined} />
|
|
<div className="flex w-full items-center justify-between">
|
|
<span className="text-body-extra-small-regular-0 font-body-extra-small-regular-0 text-text-neutral-tertiary">
|
|
{t("account.register.passwordStrength")}: {strengthLabel}
|
|
</span>
|
|
</div>
|
|
<span className="w-full text-body-extra-small-medium-150 font-body-extra-small-medium-150 text-text-neutral-tertiary">
|
|
{t("account.register.passwordHint")}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="flex w-full items-end gap-2">
|
|
<TextInput className="h-auto grow shrink-0 basis-0" fillContainer showLabel label={t("account.register.confirmPassword")} type="password">
|
|
<TextInput.Input
|
|
name="password-confirm"
|
|
type="password"
|
|
required
|
|
autoComplete="new-password"
|
|
placeholder={t("account.register.confirmPassword")}
|
|
disabled={isSubmitting}
|
|
data-ui-id="ui.auth.reset.passwordConfirm"
|
|
/>
|
|
</TextInput>
|
|
</div>
|
|
|
|
<Button className="h-9 w-full flex-none" type="submit" loading={isSubmitting} disabled={isSubmitting} data-ui-id="ui.auth.reset.submit">
|
|
{t("account.newPassword.button")}
|
|
</Button>
|
|
|
|
{actionData?.error && navigation.state === "idle" ? (
|
|
<p className="text-center text-body-small-regular-0 font-body-small-regular-0 text-text-feedback-negative" role="alert">
|
|
{actionData.error}
|
|
</p>
|
|
) : null}
|
|
</Form>
|
|
|
|
<div className="flex w-full items-center justify-center">
|
|
<LinkButton size="small" hideIcon onClick={() => navigate("/login")} data-ui-id="ui.auth.reset.backToLogin">
|
|
{t("account.forgot.backToLogin")}
|
|
</LinkButton>
|
|
</div>
|
|
</AuthShell>
|
|
);
|
|
}
|