Files
saladeaula.digital/apps/saladeaula.digital/app/routes/settings/password.tsx
2025-12-08 19:58:17 -03:00

158 lines
4.6 KiB
TypeScript

import type { Route } from './+types/password'
import { useToggle } from 'ahooks'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
import { toast } from 'sonner'
import { useFetcher } from 'react-router'
import { useEffect } from 'react'
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage
} from '@repo/ui/components/ui/form'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle
} from '@repo/ui/components/ui/card'
import { Input } from '@repo/ui/components/ui/input'
import { Button } from '@repo/ui/components/ui/button'
import { Spinner } from '@repo/ui/components/ui/spinner'
import { Checkbox } from '@repo/ui/components/ui/checkbox'
import { Label } from '@repo/ui/components/ui/label'
import { request as req, HttpMethod } from '@repo/util/request'
import { userContext } from '@repo/auth/context'
import type { User } from '@repo/auth/auth'
const formSchema = z
.object({
new_password: z.string().min(6, 'Deve ter no mínimo 6 caracteres'),
confirm_password: z.string().min(6, 'Deve ter no mínimo 6 caracteres')
})
.refine((data) => data.new_password === data.confirm_password, {
message: 'As senhas não coincidem',
path: ['confirm_password']
})
type Schema = z.infer<typeof formSchema>
export async function action({ request, context }: Route.ActionArgs) {
const user = context.get(userContext) as User
const body = await request.json()
const r = await req({
url: `users/${user.sub}/password`,
headers: new Headers({ 'Content-Type': 'application/json' }),
method: HttpMethod.POST,
body: JSON.stringify(body),
request,
context
})
if (!r.ok) {
const error = await r.json().catch(() => ({}))
return { ok: false, error }
}
return { ok: true }
}
export default function Route({}: Route.ComponentProps) {
const [show, { toggle }] = useToggle()
const inputType = show ? 'text' : 'password'
const fetcher = useFetcher()
const form = useForm({
resolver: zodResolver(formSchema)
})
const { control, formState, handleSubmit, reset } = form
const onSubmit = async ({ new_password }: Schema) => {
await fetcher.submit(JSON.stringify({ new_password }), {
method: 'post',
encType: 'application/json'
})
}
useEffect(() => {
if (fetcher.data?.ok) {
toast.success('Sua senha foi alterada.')
return reset()
}
}, [fetcher.data, reset])
return (
<Form {...form}>
<form onSubmit={handleSubmit(onSubmit)}>
<Card>
<CardHeader>
<CardTitle className="text-lg">Alterar senha</CardTitle>
<CardDescription>
Sua senha deve ter no mínimo 6 caracteres. Recomendamos que inclua
uma combinação de números, letras e caracteres especiais (ex.:
!$@%).
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<FormField
control={control}
name="new_password"
defaultValue=""
render={({ field }) => (
<FormItem>
<FormLabel>Nova senha</FormLabel>
<FormControl>
<Input type={inputType} autoComplete="false" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="confirm_password"
defaultValue=""
render={({ field }) => (
<FormItem>
<FormLabel>Confirmar nova senha</FormLabel>
<FormControl>
<Input type={inputType} autoComplete="false" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex items-center gap-3">
<Checkbox id="showPassword" onClick={toggle} tabIndex={-1} />
<Label htmlFor="showPassword" className="cursor-pointer">
Mostrar senha
</Label>
</div>
<Button
type="submit"
className="relative overflow-hidden cursor-pointer"
>
{formState.isSubmitting && (
<div className="absolute inset-0 bg-lime-500 flex items-center justify-center">
<Spinner />
</div>
)}
Alterar senha
</Button>
</CardContent>
</Card>
</form>
</Form>
)
}