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

1
import { zodResolver } from "@hookform/resolvers/zod"
2
import {
3
  createFileRoute,
4
  Link as RouterLink,
5
  redirect,
6
} from "@tanstack/react-router"
7
import { useForm } from "react-hook-form"
8
import { z } from "zod"
9
import { AuthLayout } from "@/components/Common/AuthLayout"
10
import {
11
  Form,
12
  FormControl,
13
  FormField,
14
  FormItem,
15
  FormLabel,
16
  FormMessage,
17
} from "@/components/ui/form"
18
import { Input } from "@/components/ui/input"
19
import { LoadingButton } from "@/components/ui/loading-button"
20
import { PasswordInput } from "@/components/ui/password-input"
21
import useAuth, { isLoggedIn } from "@/hooks/useAuth"
22
23
const formSchema = z
24
  .object({
25
    email: z.email(),
26
    full_name: z.string().min(1, { message: "Full Name is required" }),
27
    password: z
28
      .string()
29
      .min(1, { message: "Password is required" })
30
      .min(8, { message: "Password must be at least 8 characters" }),
31
    confirm_password: z
32
      .string()
33
      .min(1, { message: "Password confirmation is required" }),
34
  })
35
  .refine((data) => data.password === data.confirm_password, {
36
    message: "The passwords don't match",
37
    path: ["confirm_password"],
38
  })
39
40
type FormData = z.infer<typeof formSchema>
41
42
export const Route = createFileRoute("/signup")({
43
  component: SignUp,
44
  beforeLoad: async () => {
45
    if (isLoggedIn()) {
46
      throw redirect({
47
        to: "/",
48
      })
49
    }
50
  },
51
  head: () => ({
52
    meta: [
53
      {
54
        title: "Sign Up - FastAPI Template",
55
      },
56
    ],
57
  }),
58
})
59
60
function SignUp() {
61
  const { signUpMutation } = useAuth()
62
  const form = useForm<FormData>({
63
    resolver: zodResolver(formSchema),
64
    mode: "onBlur",
65
    criteriaMode: "all",
66
    defaultValues: {
67
      email: "",
68
      full_name: "",
69
      password: "",
70
      confirm_password: "",
71
    },
72
  })
73
74
  const onSubmit = (data: FormData) => {
75
    if (signUpMutation.isPending) return
76
77
    // exclude confirm_password from submission data
78
    const { confirm_password: _confirm_password, ...submitData } = data
79
    signUpMutation.mutate(submitData)
80
  }
81
82
  return (
83
    <AuthLayout>
84
      <Form {...form}>
85
        <form
86
          onSubmit={form.handleSubmit(onSubmit)}
87
          className="flex flex-col gap-6"
88
        >
89
          <div className="flex flex-col items-center gap-2 text-center">
90
            <h1 className="text-2xl font-bold">Create an account</h1>
91
          </div>
92
93
          <div className="grid gap-4">
94
            <FormField
95
              control={form.control}
96
              name="full_name"
97
              render={({ field }) => (
98
                <FormItem>
99
                  <FormLabel>Full Name</FormLabel>
100
                  <FormControl>
101
                    <Input
102
                      data-testid="full-name-input"
103
                      placeholder="User"
104
                      type="text"
105
                      {...field}
106
                    />
107
                  </FormControl>
108
                  <FormMessage />
109
                </FormItem>
110
              )}
111
            />
112
113
            <FormField
114
              control={form.control}
115
              name="email"
116
              render={({ field }) => (
117
                <FormItem>
118
                  <FormLabel>Email</FormLabel>
119
                  <FormControl>
120
                    <Input
121
                      data-testid="email-input"
122
                      placeholder="user@example.com"
123
                      type="email"
124
                      {...field}
125
                    />
126
                  </FormControl>
127
                  <FormMessage />
128
                </FormItem>
129
              )}
130
            />
131
132
            <FormField
133
              control={form.control}
134
              name="password"
135
              render={({ field }) => (
136
                <FormItem>
137
                  <FormLabel>Password</FormLabel>
138
                  <FormControl>
139
                    <PasswordInput
140
                      data-testid="password-input"
141
                      placeholder="Password"
142
                      {...field}
143
                    />
144
                  </FormControl>
145
                  <FormMessage />
146
                </FormItem>
147
              )}
148
            />
149
150
            <FormField
151
              control={form.control}
152
              name="confirm_password"
153
              render={({ field }) => (
154
                <FormItem>
155
                  <FormLabel>Confirm Password</FormLabel>
156
                  <FormControl>
157
                    <PasswordInput
158
                      data-testid="confirm-password-input"
159
                      placeholder="Confirm Password"
160
                      {...field}
161
                    />
162
                  </FormControl>
163
                  <FormMessage />
164
                </FormItem>
165
              )}
166
            />
167
168
            <LoadingButton
169
              type="submit"
170
              className="w-full"
171
              loading={signUpMutation.isPending}
172
            >
173
              Sign Up
174
            </LoadingButton>
175
          </div>
176
177
          <div className="text-center text-sm">
178
            Already have an account?{" "}
179
            <RouterLink to="/login" className="underline underline-offset-4">
180
              Log in
181
            </RouterLink>
182
          </div>
183
        </form>
184
      </Form>
185
    </AuthLayout>
186
  )
187
}
188
189
export default SignUp
190