feat: add validation schemas for login and signup, integrate validation in respective components

This commit is contained in:
juan 2025-08-10 18:30:07 +02:00
parent 51c7b9f86d
commit e45772e2a9
9 changed files with 150 additions and 27 deletions

54
package-lock.json generated
View File

@ -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"

View File

@ -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",

View 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 }
}

View File

@ -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...", {

View File

@ -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
View 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")
})

View 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()
})

View File

@ -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
loginMutation.mutate({ email, password })
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>
)

View File

@ -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
signupMutation.mutate({
email,
password
})
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>