feat: add validation schemas for login and signup, integrate validation in respective components
This commit is contained in:
parent
51c7b9f86d
commit
e45772e2a9
54
package-lock.json
generated
54
package-lock.json
generated
@ -22,7 +22,8 @@
|
||||
"react-dom": "^19.1.1",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"vite-tsconfig-paths": "^5.1.4"
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"zod": "^4.0.17"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.1.3",
|
||||
@ -3663,6 +3664,15 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@netlify/zip-it-and-ship-it/node_modules/zod": {
|
||||
"version": "3.25.76",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
@ -6632,6 +6642,15 @@
|
||||
"vite": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-start-plugin/node_modules/zod": {
|
||||
"version": "3.25.76",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-start-server": {
|
||||
"version": "1.130.12",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-start-server/-/react-start-server-1.130.12.tgz",
|
||||
@ -6767,6 +6786,15 @@
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/router-generator/node_modules/zod": {
|
||||
"version": "3.25.76",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/router-plugin": {
|
||||
"version": "1.130.15",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/router-plugin/-/router-plugin-1.130.15.tgz",
|
||||
@ -6820,6 +6848,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/router-plugin/node_modules/zod": {
|
||||
"version": "3.25.76",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/router-utils": {
|
||||
"version": "1.130.12",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/router-utils/-/router-utils-1.130.12.tgz",
|
||||
@ -6938,6 +6975,15 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/start-plugin-core/node_modules/zod": {
|
||||
"version": "3.25.76",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/start-server-core": {
|
||||
"version": "1.130.12",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/start-server-core/-/start-server-core-1.130.12.tgz",
|
||||
@ -14740,9 +14786,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "3.25.76",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||
"version": "4.0.17",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.0.17.tgz",
|
||||
"integrity": "sha512-1PHjlYRevNxxdy2JZ8JcNAw7rX8V9P1AKkP+x/xZfxB0K5FYfuV+Ug6P/6NVSR2jHQ+FzDDoDHS04nYUsOIyLQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
|
||||
@ -28,7 +28,8 @@
|
||||
"react-dom": "^19.1.1",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"vite-tsconfig-paths": "^5.1.4"
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"zod": "^4.0.17"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.1.3",
|
||||
|
||||
37
src/lib/hooks/useValidation.tsx
Normal file
37
src/lib/hooks/useValidation.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import { useState } from "react"
|
||||
import type { z } from "zod"
|
||||
|
||||
type FormDataValidation = Record<string, FormDataEntryValue>
|
||||
|
||||
export const useValidation = <T,>({
|
||||
defaultSchema
|
||||
}: {
|
||||
defaultSchema?: z.ZodSchema<T>
|
||||
}) => {
|
||||
const [errors, setErrors] = useState<T>()
|
||||
|
||||
const validate = ({
|
||||
formData,
|
||||
schema
|
||||
}: {
|
||||
formData: FormDataValidation
|
||||
schema?: z.ZodSchema<T>
|
||||
}) => {
|
||||
const result =
|
||||
schema?.safeParse(formData) ?? defaultSchema?.safeParse(formData)
|
||||
|
||||
if (!result) {
|
||||
throw new Error("No schema provided")
|
||||
}
|
||||
|
||||
if (!result.success) {
|
||||
setErrors(result.error.flatten().fieldErrors as T)
|
||||
return false
|
||||
}
|
||||
|
||||
setErrors(undefined)
|
||||
return true
|
||||
}
|
||||
|
||||
return { errors, validate }
|
||||
}
|
||||
@ -3,11 +3,12 @@ import { getRouteApi } from "@tanstack/react-router"
|
||||
import { createServerFn, useServerFn } from "@tanstack/react-start"
|
||||
import { toast } from "sonner"
|
||||
import { getSupabaseServerClient } from "@/integrations/supabase/supabase"
|
||||
import { loginSchema } from "../schemas/login"
|
||||
|
||||
const apiRouter = getRouteApi("/login")
|
||||
|
||||
export const loginFn = createServerFn({ method: "POST" })
|
||||
.validator((d: { email: string; password: string }) => d)
|
||||
.validator(loginSchema)
|
||||
.handler(async ({ data }) => {
|
||||
const supabase = getSupabaseServerClient()
|
||||
const response = await supabase.auth.signInWithPassword({
|
||||
@ -36,12 +37,9 @@ export const mutationLogin = () => {
|
||||
}
|
||||
})
|
||||
},
|
||||
onSuccess: (data, ctx) => {
|
||||
console.log("Login successful", data)
|
||||
console.log("ctx", ctx)
|
||||
|
||||
onSuccess: (data) => {
|
||||
if (data?.error) {
|
||||
toast.error(data.message)
|
||||
toast.error(data.message, { id: "login" })
|
||||
return
|
||||
}
|
||||
toast.success("Login successful! Redirecting to posts...", {
|
||||
|
||||
@ -3,12 +3,11 @@ import { getRouteApi, redirect } from "@tanstack/react-router"
|
||||
import { createServerFn, useServerFn } from "@tanstack/react-start"
|
||||
import { toast } from "sonner"
|
||||
import { getSupabaseServerClient } from "@/integrations/supabase/supabase"
|
||||
import { signupSchema } from "../schemas/signup"
|
||||
|
||||
const apiRouter = getRouteApi("/signup")
|
||||
export const signupFn = createServerFn({ method: "POST" })
|
||||
.validator(
|
||||
(d: { email: string; password: string; redirectUrl?: string }) => d
|
||||
)
|
||||
.validator(signupSchema)
|
||||
.handler(async ({ data }) => {
|
||||
const supabase = getSupabaseServerClient()
|
||||
const { error } = await supabase.auth.signUp({
|
||||
@ -47,7 +46,9 @@ export const mutationSignup = () => {
|
||||
})
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success("Signup successful! Redirecting to login...", { id: "signup" })
|
||||
toast.success("Signup successful! Redirecting to login...", {
|
||||
id: "signup"
|
||||
})
|
||||
navigate({
|
||||
to: "/login"
|
||||
})
|
||||
|
||||
6
src/lib/schemas/login.ts
Normal file
6
src/lib/schemas/login.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import z from "zod"
|
||||
|
||||
export const loginSchema = z.object({
|
||||
email: z.email("Invalid email address"),
|
||||
password: z.string().min(6, "Password must be at least 6 characters long")
|
||||
})
|
||||
7
src/lib/schemas/signup.ts
Normal file
7
src/lib/schemas/signup.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import z from "zod"
|
||||
|
||||
export const signupSchema = z.object({
|
||||
email: z.email("Invalid email address"),
|
||||
password: z.string().min(6, "Password must be at least 6 characters long"),
|
||||
redirectUrl: z.string().optional()
|
||||
})
|
||||
@ -1,28 +1,44 @@
|
||||
import { Button, Form, Input } from "@heroui/react"
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import { useValidation } from "@/lib/hooks/useValidation"
|
||||
import { mutationLogin } from "@/lib/mutations/mutationLogin"
|
||||
import { loginSchema } from "@/lib/schemas/login"
|
||||
|
||||
export const Route = createFileRoute("/login")({
|
||||
component: LoginComp
|
||||
})
|
||||
|
||||
|
||||
|
||||
function LoginComp() {
|
||||
const loginMutation = mutationLogin();
|
||||
const loginMutation = mutationLogin()
|
||||
const { errors, validate } = useValidation({
|
||||
defaultSchema: loginSchema
|
||||
})
|
||||
return (
|
||||
<div className="flex justify-center items-center flex-col h-screen">
|
||||
<p className="font-semibold mb-3">Login</p>
|
||||
<Form
|
||||
validationErrors={errors}
|
||||
className="grid gap-2 max-w-sm w-full"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
const formData = new FormData(e.currentTarget)
|
||||
const email = formData.get("email") as string
|
||||
const password = formData.get("password") as string
|
||||
if (
|
||||
validate({
|
||||
formData: { email, password }
|
||||
})
|
||||
)
|
||||
loginMutation.mutate({ email, password })
|
||||
}}
|
||||
>
|
||||
<Input name="email" type="email" placeholder="Email" />
|
||||
<Input name="password" type="password" placeholder="Password" />
|
||||
<Button type="submit">Enviar</Button>
|
||||
<Input name="email" label="Email" />
|
||||
<Input name="password" type="password" label="Password" />
|
||||
<Button type="submit" isLoading={loginMutation.isPending}>
|
||||
Entrar
|
||||
</Button>
|
||||
</Form>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import { Button, Form, Input } from "@heroui/react"
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import { useValidation } from "@/lib/hooks/useValidation"
|
||||
import { mutationSignup } from "@/lib/mutations/mutationSignup"
|
||||
import { signupSchema } from "@/lib/schemas/signup"
|
||||
|
||||
export const Route = createFileRoute("/signup")({
|
||||
component: SignupComp
|
||||
@ -8,24 +10,33 @@ export const Route = createFileRoute("/signup")({
|
||||
|
||||
function SignupComp() {
|
||||
const signupMutation = mutationSignup()
|
||||
const { errors, validate } = useValidation({
|
||||
defaultSchema: signupSchema
|
||||
})
|
||||
return (
|
||||
<div className="flex justify-center items-center flex-col h-screen">
|
||||
<p className="font-semibold mb-3">Signup</p>
|
||||
<Form
|
||||
className="grid gap-2 max-w-5xl w-full"
|
||||
validationErrors={errors}
|
||||
className="grid gap-2 max-w-sm w-full"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
const formData = new FormData(e.currentTarget)
|
||||
const email = formData.get("email") as string
|
||||
const password = formData.get("password") as string
|
||||
if (
|
||||
validate({
|
||||
formData: { email, password }
|
||||
})
|
||||
)
|
||||
signupMutation.mutate({
|
||||
email,
|
||||
password
|
||||
})
|
||||
}}
|
||||
>
|
||||
<Input name="email" type="email" placeholder="Email" />
|
||||
<Input name="password" type="password" placeholder="Password" />
|
||||
<Input name="email" type="email" label="Email" />
|
||||
<Input name="password" type="password" label="Password" />
|
||||
<Button type="submit" isLoading={signupMutation.isPending}>
|
||||
Enviar
|
||||
</Button>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user