|
1 | | -import { Container, Heading, Text } from "@chakra-ui/react" |
| 1 | +import { zodResolver } from "@hookform/resolvers/zod" |
2 | 2 | import { useMutation } from "@tanstack/react-query" |
3 | | -import { createFileRoute, redirect, useNavigate } from "@tanstack/react-router" |
4 | | -import { type SubmitHandler, useForm } from "react-hook-form" |
5 | | -import { FiLock } from "react-icons/fi" |
| 3 | +import { createFileRoute, Link as RouterLink, redirect, useNavigate } from "@tanstack/react-router" |
| 4 | +import { useForm } from "react-hook-form" |
| 5 | +import { z } from "zod" |
6 | 6 |
|
7 | | -import { type ApiError, LoginService, type NewPassword } from "@/client" |
8 | | -import { Button } from "@/components/ui/button" |
9 | | -import { PasswordInput } from "@/components/ui/password-input" |
| 7 | +import { LoginService } from "@/client" |
10 | 8 | import { isLoggedIn } from "@/hooks/useAuth" |
11 | 9 | import useCustomToast from "@/hooks/useCustomToast" |
12 | | -import { confirmPasswordRules, handleError, passwordRules } from "@/utils" |
| 10 | +import { handleError } from "@/utils" |
| 11 | +import { AuthLayout } from "@/components/Common/AuthLayout" |
| 12 | +import { LoadingButton } from "@/components/ui/loading-button" |
| 13 | +import { PasswordInput } from "@/components/ui/password-input" |
| 14 | +import { |
| 15 | + Form, |
| 16 | + FormControl, |
| 17 | + FormField, |
| 18 | + FormItem, |
| 19 | + FormLabel, |
| 20 | + FormMessage, |
| 21 | +} from "@/components/ui/form" |
13 | 22 |
|
14 | | -interface NewPasswordForm extends NewPassword { |
15 | | - confirm_password: string |
16 | | -} |
| 23 | +const searchSchema = z.object({ |
| 24 | + token: z.string().catch(""), |
| 25 | +}) |
| 26 | + |
| 27 | +const formSchema = z |
| 28 | + .object({ |
| 29 | + new_password: z |
| 30 | + .string() |
| 31 | + .min(1, { message: "Password is required" }) |
| 32 | + .min(8, { message: "Password must be at least 8 characters" }), |
| 33 | + confirm_password: z.string().min(1, { message: "Password confirmation is required" }), |
| 34 | + }) |
| 35 | + .refine((data) => data.new_password === data.confirm_password, { |
| 36 | + message: "The passwords don't match", |
| 37 | + path: ["confirm_password"], |
| 38 | + }) |
| 39 | + |
| 40 | +type FormData = z.infer<typeof formSchema> |
17 | 41 |
|
18 | 42 | export const Route = createFileRoute("/reset-password")({ |
19 | 43 | component: ResetPassword, |
20 | | - beforeLoad: async () => { |
| 44 | + validateSearch: searchSchema, |
| 45 | + beforeLoad: async ({ search }) => { |
21 | 46 | if (isLoggedIn()) { |
22 | | - throw redirect({ |
23 | | - to: "/", |
24 | | - }) |
| 47 | + throw redirect({ to: "/" }) |
| 48 | + } |
| 49 | + if (!search.token) { |
| 50 | + throw redirect({ to: "/login" }) |
25 | 51 | } |
26 | 52 | }, |
27 | 53 | }) |
28 | 54 |
|
29 | 55 | function ResetPassword() { |
30 | | - const { |
31 | | - register, |
32 | | - handleSubmit, |
33 | | - getValues, |
34 | | - reset, |
35 | | - formState: { errors }, |
36 | | - } = useForm<NewPasswordForm>({ |
| 56 | + const { token } = Route.useSearch() |
| 57 | + const { showSuccessToast, showErrorToast } = useCustomToast() |
| 58 | + const navigate = useNavigate() |
| 59 | + |
| 60 | + const form = useForm<FormData>({ |
| 61 | + resolver: zodResolver(formSchema), |
37 | 62 | mode: "onBlur", |
38 | 63 | criteriaMode: "all", |
39 | 64 | defaultValues: { |
40 | 65 | new_password: "", |
| 66 | + confirm_password: "", |
41 | 67 | }, |
42 | 68 | }) |
43 | | - const { showSuccessToast } = useCustomToast() |
44 | | - const navigate = useNavigate() |
45 | | - |
46 | | - const resetPassword = async (data: NewPassword) => { |
47 | | - const token = new URLSearchParams(window.location.search).get("token") |
48 | | - if (!token) return |
49 | | - await LoginService.resetPassword({ |
50 | | - requestBody: { new_password: data.new_password, token: token }, |
51 | | - }) |
52 | | - } |
53 | 69 |
|
54 | 70 | const mutation = useMutation({ |
55 | | - mutationFn: resetPassword, |
| 71 | + mutationFn: (data: { new_password: string; token: string }) => |
| 72 | + LoginService.resetPassword({ requestBody: data }), |
56 | 73 | onSuccess: () => { |
57 | 74 | showSuccessToast("Password updated successfully.") |
58 | | - reset() |
| 75 | + form.reset() |
59 | 76 | navigate({ to: "/login" }) |
60 | 77 | }, |
61 | | - onError: (err: ApiError) => { |
62 | | - handleError(err) |
63 | | - }, |
| 78 | + onError: handleError.bind(showErrorToast), |
64 | 79 | }) |
65 | 80 |
|
66 | | - const onSubmit: SubmitHandler<NewPasswordForm> = async (data) => { |
67 | | - mutation.mutate(data) |
| 81 | + const onSubmit = (data: FormData) => { |
| 82 | + mutation.mutate({ new_password: data.new_password, token }) |
68 | 83 | } |
69 | 84 |
|
70 | 85 | return ( |
71 | | - <Container |
72 | | - as="form" |
73 | | - onSubmit={handleSubmit(onSubmit)} |
74 | | - h="100vh" |
75 | | - maxW="sm" |
76 | | - alignItems="stretch" |
77 | | - justifyContent="center" |
78 | | - gap={4} |
79 | | - centerContent |
80 | | - > |
81 | | - <Heading size="xl" color="ui.main" textAlign="center" mb={2}> |
82 | | - Reset Password |
83 | | - </Heading> |
84 | | - <Text textAlign="center"> |
85 | | - Please enter your new password and confirm it to reset your password. |
86 | | - </Text> |
87 | | - <PasswordInput |
88 | | - startElement={<FiLock />} |
89 | | - type="new_password" |
90 | | - errors={errors} |
91 | | - {...register("new_password", passwordRules())} |
92 | | - placeholder="New Password" |
93 | | - /> |
94 | | - <PasswordInput |
95 | | - startElement={<FiLock />} |
96 | | - type="confirm_password" |
97 | | - errors={errors} |
98 | | - {...register("confirm_password", confirmPasswordRules(getValues))} |
99 | | - placeholder="Confirm Password" |
100 | | - /> |
101 | | - <Button variant="solid" type="submit"> |
102 | | - Reset Password |
103 | | - </Button> |
104 | | - </Container> |
| 86 | + <AuthLayout> |
| 87 | + <Form {...form}> |
| 88 | + <form |
| 89 | + onSubmit={form.handleSubmit(onSubmit)} |
| 90 | + className="flex flex-col gap-6" |
| 91 | + > |
| 92 | + <div className="flex flex-col items-center gap-2 text-center"> |
| 93 | + <h1 className="text-2xl font-bold">Reset Password</h1> |
| 94 | + </div> |
| 95 | + |
| 96 | + <div className="grid gap-4"> |
| 97 | + <FormField |
| 98 | + control={form.control} |
| 99 | + name="new_password" |
| 100 | + render={({ field }) => ( |
| 101 | + <FormItem> |
| 102 | + <FormLabel>New Password</FormLabel> |
| 103 | + <FormControl> |
| 104 | + <PasswordInput placeholder="New Password" {...field} /> |
| 105 | + </FormControl> |
| 106 | + <FormMessage /> |
| 107 | + </FormItem> |
| 108 | + )} |
| 109 | + /> |
| 110 | + |
| 111 | + <FormField |
| 112 | + control={form.control} |
| 113 | + name="confirm_password" |
| 114 | + render={({ field }) => ( |
| 115 | + <FormItem> |
| 116 | + <FormLabel>Confirm Password</FormLabel> |
| 117 | + <FormControl> |
| 118 | + <PasswordInput placeholder="Confirm Password" {...field} /> |
| 119 | + </FormControl> |
| 120 | + <FormMessage /> |
| 121 | + </FormItem> |
| 122 | + )} |
| 123 | + /> |
| 124 | + |
| 125 | + <LoadingButton |
| 126 | + type="submit" |
| 127 | + className="w-full" |
| 128 | + loading={mutation.isPending} |
| 129 | + > |
| 130 | + Reset Password |
| 131 | + </LoadingButton> |
| 132 | + </div> |
| 133 | + |
| 134 | + <div className="text-center text-sm"> |
| 135 | + Remember your password?{" "} |
| 136 | + <RouterLink to="/login" className="underline underline-offset-4"> |
| 137 | + Log in |
| 138 | + </RouterLink> |
| 139 | + </div> |
| 140 | + </form> |
| 141 | + </Form> |
| 142 | + </AuthLayout> |
105 | 143 | ) |
106 | 144 | } |
0 commit comments