infoalloggi-monorepo/apps/infoalloggi/src/forms/FormPwSetup.tsx

214 lines
7.2 KiB
TypeScript
Raw Normal View History

import { z } from "zod/v4";
2025-08-04 17:45:44 +02:00
import { useZodForm } from "~/lib/zodForm";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "~/components/custom_ui/form";
import Input from "~/components/custom_ui/input";
import { useTranslation } from "~/providers/I18nProvider";
import { api } from "~/utils/api";
import { useRouter } from "next/router";
import toast from "react-hot-toast";
import { Button } from "~/components/ui/button";
import { LoadingSpinner } from "~/components/spinner";
import { useState } from "react";
import { Checkbox } from "~/components/ui/checkbox";
import type { ServizioServizioId } from "~/schemas/public/Servizio";
import type { Users } from "~/schemas/public/Users";
import { useStrongPassword } from "~/hooks/useStrongPassword";
import { cn } from "~/lib/utils";
import { Eye, EyeOff } from "lucide-react";
type FormPwSetupProps = {
user: Pick<Users, "id" | "nome" | "cognome" | "email" | "telefono">;
servizioId: ServizioServizioId;
};
export const FormPwSetup = ({ user, servizioId }: FormPwSetupProps) => {
const router = useRouter();
const {
isVisible,
getStrengthColor,
passwordRefine,
getStrengthTxt,
strengthScore,
toggleVisibility,
} = useStrongPassword();
const Schema = z
2025-08-04 17:45:44 +02:00
.object({
password: z.string(),
checkbox: z.boolean().refine((val) => val === false), // Honeypot field
})
.superRefine(({ password }, refinementContext) => {
passwordRefine({
password,
refinementContext,
path: "password",
});
});
type LoginFormValues = z.infer<typeof Schema>;
2025-08-04 17:45:44 +02:00
const defaultValues: LoginFormValues = {
password: "",
checkbox: false, // Honeypot field
};
const { locale, t } = useTranslation();
z.config(z.locales[locale]());
2025-08-04 17:45:44 +02:00
const form = useZodForm(Schema, {
2025-08-04 17:45:44 +02:00
defaultValues: defaultValues,
});
const utils = api.useUtils();
const { mutate, isPending } = api.auth.UpdatePasswordAndLogin.useMutation({
onSuccess: async () => {
await utils.auth.getSession.invalidate();
setAlreadyRequested(true);
toast.success(t.auth.login.success);
await router.push(`/servizio/onboard/${servizioId}`);
2025-08-04 17:45:44 +02:00
},
onError: () => {
toast.error(t.auth.login.fail);
},
});
const [altreadyRequested, setAlreadyRequested] = useState(false);
function onSubmit(fields: LoginFormValues) {
mutate({
id: user.id,
password: fields.password,
});
}
return (
<div className="mx-auto w-full max-w-3xl space-y-8 px-4 py-8">
<div className="my-8 space-y-5">
<h3 className="text-3xl font-bold text-gray-800 sm:text-3xl">
Scegli la tua password
</h3>
<p className="text-muted-foreground">
Imposta una password per accedere al tuo account.
</p>
</div>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-8 px-0.5"
>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<div className="flex flex-col gap-x-2">
<FormLabel htmlFor="password" className="text-neutral-600">
{t.auth.signup.password}
</FormLabel>
<FormMessage
className={cn(
"whitespace-pre-wrap",
getStrengthTxt(strengthScore),
)}
/>
</div>
<FormControl>
<div className="relative">
<Input
{...field}
id="password"
className="pe-9"
autoComplete="current-password"
placeholder="Password"
type={isVisible ? "text" : "password"}
onChange={async (e) => {
field.onChange(e);
await form.trigger("password");
}}
aria-invalid={form.getFieldState("password").invalid}
aria-describedby="password-strength"
/>
<button
className="text-muted-foreground/80 hover:text-foreground focus-visible:border-ring focus-visible:text-foreground focus-visible:ring-ring/30 absolute end-0 top-1 flex size-9 items-center justify-center rounded-e-lg transition-shadow focus-visible:border focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50"
type="button"
onClick={toggleVisibility}
aria-label={isVisible ? "Hide password" : "Show password"}
aria-pressed={isVisible}
aria-controls="password"
>
{isVisible ? (
<EyeOff size={16} strokeWidth={2} aria-hidden="true" />
) : (
<Eye size={16} strokeWidth={2} aria-hidden="true" />
)}
</button>
<div
className="bg-border mt-3 mb-4 h-1 w-full overflow-hidden rounded-full"
role="progressbar"
aria-valuenow={strengthScore}
aria-valuemin={0}
aria-valuemax={4}
aria-label="Password strength"
>
<div
className={cn(
"h-full transition-all duration-500 ease-out",
getStrengthColor(strengthScore),
)}
style={{ width: `${(strengthScore / 4) * 100}%` }}
/>
</div>
</div>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="checkbox"
render={({ field }) => (
<FormItem className="hidden">
<div className="flex flex-wrap items-center gap-x-2">
<FormLabel htmlFor="checkbox">Check Me</FormLabel>
<FormMessage />
</div>
<FormControl>
<Checkbox
id="checkbox"
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
<Button
disabled={isPending || altreadyRequested}
type="submit"
className="w-full rounded-lg bg-indigo-600 px-4 py-2 font-medium text-white duration-150 hover:bg-indigo-500 active:bg-indigo-600"
>
{isPending ? (
<div className="flex items-center justify-center">
<LoadingSpinner size={25} />
</div>
) : (
<span>Salva la password e prosegui</span>
)}
</Button>
</form>
</Form>
</div>
);
};