← full-stack-fastapi-template  /  frontend/src/routes/recover-password.tsx

1
import { zodResolver } from "@hookform/resolvers/zod"
2
import { useMutation } from "@tanstack/react-query"
3
import {
4
  createFileRoute,
5
  Link as RouterLink,
6
  redirect,
7
} from "@tanstack/react-router"
8
import { useForm } from "react-hook-form"
9
import { z } from "zod"
10
11
import { LoginService } from "@/client"
12
import { AuthLayout } from "@/components/Common/AuthLayout"
13
import {
14
  Form,
15
  FormControl,
16
  FormField,
17
  FormItem,
18
  FormLabel,
19
  FormMessage,
20
} from "@/components/ui/form"
21
import { Input } from "@/components/ui/input"
22
import { LoadingButton } from "@/components/ui/loading-button"
23
import { isLoggedIn } from "@/hooks/useAuth"
24
import useCustomToast from "@/hooks/useCustomToast"
25
import { handleError } from "@/utils"
26
27
const formSchema = z.object({
28
  email: z.email(),
29
})
30
31
type FormData = z.infer<typeof formSchema>
32
33
export const Route = createFileRoute("/recover-password")({
34
  component: RecoverPassword,
35
  beforeLoad: async () => {
36
    if (isLoggedIn()) {
37
      throw redirect({
38
        to: "/",
39
      })
40
    }
41
  },
42
  head: () => ({
43
    meta: [
44
      {
45
        title: "Recover Password - FastAPI Template",
46
      },
47
    ],
48
  }),
49
})
50
51
function RecoverPassword() {
52
  const form = useForm<FormData>({
53
    resolver: zodResolver(formSchema),
54
    defaultValues: {
55
      email: "",
56
    },
57
  })
58
  const { showSuccessToast, showErrorToast } = useCustomToast()
59
60
  const recoverPassword = async (data: FormData) => {
61
    await LoginService.recoverPassword({
62
      email: data.email,
63
    })
64
  }
65
66
  const mutation = useMutation({
67
    mutationFn: recoverPassword,
68
    onSuccess: () => {
69
      showSuccessToast("Password recovery email sent successfully")
70
      form.reset()
71
    },
72
    onError: handleError.bind(showErrorToast),
73
  })
74
75
  const onSubmit = async (data: FormData) => {
76
    if (mutation.isPending) return
77
    mutation.mutate(data)
78
  }
79
80
  return (
81
    <AuthLayout>
82
      <Form {...form}>
83
        <form
84
          onSubmit={form.handleSubmit(onSubmit)}
85
          className="flex flex-col gap-6"
86
        >
87
          <div className="flex flex-col items-center gap-2 text-center">
88
            <h1 className="text-2xl font-bold">Password Recovery</h1>
89
          </div>
90
91
          <div className="grid gap-4">
92
            <FormField
93
              control={form.control}
94
              name="email"
95
              render={({ field }) => (
96
                <FormItem>
97
                  <FormLabel>Email</FormLabel>
98
                  <FormControl>
99
                    <Input
100
                      data-testid="email-input"
101
                      placeholder="user@example.com"
102
                      type="email"
103
                      {...field}
104
                    />
105
                  </FormControl>
106
                  <FormMessage />
107
                </FormItem>
108
              )}
109
            />
110
111
            <LoadingButton
112
              type="submit"
113
              className="w-full"
114
              loading={mutation.isPending}
115
            >
116
              Continue
117
            </LoadingButton>
118
          </div>
119
120
          <div className="text-center text-sm">
121
            Remember your password?{" "}
122
            <RouterLink to="/login" className="underline underline-offset-4">
123
              Log in
124
            </RouterLink>
125
          </div>
126
        </form>
127
      </Form>
128
    </AuthLayout>
129
  )
130
}
131