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

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>
);
}