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",
|
"react-dom": "^19.1.1",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"tailwindcss": "^4.1.11",
|
"tailwindcss": "^4.1.11",
|
||||||
"vite-tsconfig-paths": "^5.1.4"
|
"vite-tsconfig-paths": "^5.1.4",
|
||||||
|
"zod": "^4.0.17"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "2.1.3",
|
"@biomejs/biome": "2.1.3",
|
||||||
@ -3663,6 +3664,15 @@
|
|||||||
"node": ">=10"
|
"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": {
|
"node_modules/@nodelib/fs.scandir": {
|
||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||||
@ -6632,6 +6642,15 @@
|
|||||||
"vite": ">=6.0.0"
|
"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": {
|
"node_modules/@tanstack/react-start-server": {
|
||||||
"version": "1.130.12",
|
"version": "1.130.12",
|
||||||
"resolved": "https://registry.npmjs.org/@tanstack/react-start-server/-/react-start-server-1.130.12.tgz",
|
"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"
|
"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": {
|
"node_modules/@tanstack/router-plugin": {
|
||||||
"version": "1.130.15",
|
"version": "1.130.15",
|
||||||
"resolved": "https://registry.npmjs.org/@tanstack/router-plugin/-/router-plugin-1.130.15.tgz",
|
"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": {
|
"node_modules/@tanstack/router-utils": {
|
||||||
"version": "1.130.12",
|
"version": "1.130.12",
|
||||||
"resolved": "https://registry.npmjs.org/@tanstack/router-utils/-/router-utils-1.130.12.tgz",
|
"resolved": "https://registry.npmjs.org/@tanstack/router-utils/-/router-utils-1.130.12.tgz",
|
||||||
@ -6938,6 +6975,15 @@
|
|||||||
"node": ">=6.9.0"
|
"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": {
|
"node_modules/@tanstack/start-server-core": {
|
||||||
"version": "1.130.12",
|
"version": "1.130.12",
|
||||||
"resolved": "https://registry.npmjs.org/@tanstack/start-server-core/-/start-server-core-1.130.12.tgz",
|
"resolved": "https://registry.npmjs.org/@tanstack/start-server-core/-/start-server-core-1.130.12.tgz",
|
||||||
@ -14740,9 +14786,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/zod": {
|
"node_modules/zod": {
|
||||||
"version": "3.25.76",
|
"version": "4.0.17",
|
||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
"resolved": "https://registry.npmjs.org/zod/-/zod-4.0.17.tgz",
|
||||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
"integrity": "sha512-1PHjlYRevNxxdy2JZ8JcNAw7rX8V9P1AKkP+x/xZfxB0K5FYfuV+Ug6P/6NVSR2jHQ+FzDDoDHS04nYUsOIyLQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
|
|||||||
@ -28,7 +28,8 @@
|
|||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"tailwindcss": "^4.1.11",
|
"tailwindcss": "^4.1.11",
|
||||||
"vite-tsconfig-paths": "^5.1.4"
|
"vite-tsconfig-paths": "^5.1.4",
|
||||||
|
"zod": "^4.0.17"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "2.1.3",
|
"@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 { createServerFn, useServerFn } from "@tanstack/react-start"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import { getSupabaseServerClient } from "@/integrations/supabase/supabase"
|
import { getSupabaseServerClient } from "@/integrations/supabase/supabase"
|
||||||
|
import { loginSchema } from "../schemas/login"
|
||||||
|
|
||||||
const apiRouter = getRouteApi("/login")
|
const apiRouter = getRouteApi("/login")
|
||||||
|
|
||||||
export const loginFn = createServerFn({ method: "POST" })
|
export const loginFn = createServerFn({ method: "POST" })
|
||||||
.validator((d: { email: string; password: string }) => d)
|
.validator(loginSchema)
|
||||||
.handler(async ({ data }) => {
|
.handler(async ({ data }) => {
|
||||||
const supabase = getSupabaseServerClient()
|
const supabase = getSupabaseServerClient()
|
||||||
const response = await supabase.auth.signInWithPassword({
|
const response = await supabase.auth.signInWithPassword({
|
||||||
@ -36,12 +37,9 @@ export const mutationLogin = () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
onSuccess: (data, ctx) => {
|
onSuccess: (data) => {
|
||||||
console.log("Login successful", data)
|
|
||||||
console.log("ctx", ctx)
|
|
||||||
|
|
||||||
if (data?.error) {
|
if (data?.error) {
|
||||||
toast.error(data.message)
|
toast.error(data.message, { id: "login" })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
toast.success("Login successful! Redirecting to posts...", {
|
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 { createServerFn, useServerFn } from "@tanstack/react-start"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import { getSupabaseServerClient } from "@/integrations/supabase/supabase"
|
import { getSupabaseServerClient } from "@/integrations/supabase/supabase"
|
||||||
|
import { signupSchema } from "../schemas/signup"
|
||||||
|
|
||||||
const apiRouter = getRouteApi("/signup")
|
const apiRouter = getRouteApi("/signup")
|
||||||
export const signupFn = createServerFn({ method: "POST" })
|
export const signupFn = createServerFn({ method: "POST" })
|
||||||
.validator(
|
.validator(signupSchema)
|
||||||
(d: { email: string; password: string; redirectUrl?: string }) => d
|
|
||||||
)
|
|
||||||
.handler(async ({ data }) => {
|
.handler(async ({ data }) => {
|
||||||
const supabase = getSupabaseServerClient()
|
const supabase = getSupabaseServerClient()
|
||||||
const { error } = await supabase.auth.signUp({
|
const { error } = await supabase.auth.signUp({
|
||||||
@ -47,7 +46,9 @@ export const mutationSignup = () => {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success("Signup successful! Redirecting to login...", { id: "signup" })
|
toast.success("Signup successful! Redirecting to login...", {
|
||||||
|
id: "signup"
|
||||||
|
})
|
||||||
navigate({
|
navigate({
|
||||||
to: "/login"
|
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 { Button, Form, Input } from "@heroui/react"
|
||||||
import { createFileRoute } from "@tanstack/react-router"
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
|
import { useValidation } from "@/lib/hooks/useValidation"
|
||||||
import { mutationLogin } from "@/lib/mutations/mutationLogin"
|
import { mutationLogin } from "@/lib/mutations/mutationLogin"
|
||||||
|
import { loginSchema } from "@/lib/schemas/login"
|
||||||
|
|
||||||
export const Route = createFileRoute("/login")({
|
export const Route = createFileRoute("/login")({
|
||||||
component: LoginComp
|
component: LoginComp
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function LoginComp() {
|
function LoginComp() {
|
||||||
const loginMutation = mutationLogin();
|
const loginMutation = mutationLogin()
|
||||||
|
const { errors, validate } = useValidation({
|
||||||
|
defaultSchema: loginSchema
|
||||||
|
})
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-center items-center flex-col h-screen">
|
<div className="flex justify-center items-center flex-col h-screen">
|
||||||
<p className="font-semibold mb-3">Login</p>
|
<p className="font-semibold mb-3">Login</p>
|
||||||
<Form
|
<Form
|
||||||
|
validationErrors={errors}
|
||||||
className="grid gap-2 max-w-sm w-full"
|
className="grid gap-2 max-w-sm w-full"
|
||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
const formData = new FormData(e.currentTarget)
|
const formData = new FormData(e.currentTarget)
|
||||||
const email = formData.get("email") as string
|
const email = formData.get("email") as string
|
||||||
const password = formData.get("password") 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="email" label="Email" />
|
||||||
<Input name="password" type="password" placeholder="Password" />
|
<Input name="password" type="password" label="Password" />
|
||||||
<Button type="submit">Enviar</Button>
|
<Button type="submit" isLoading={loginMutation.isPending}>
|
||||||
|
Entrar
|
||||||
|
</Button>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import { Button, Form, Input } from "@heroui/react"
|
import { Button, Form, Input } from "@heroui/react"
|
||||||
import { createFileRoute } from "@tanstack/react-router"
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
|
import { useValidation } from "@/lib/hooks/useValidation"
|
||||||
import { mutationSignup } from "@/lib/mutations/mutationSignup"
|
import { mutationSignup } from "@/lib/mutations/mutationSignup"
|
||||||
|
import { signupSchema } from "@/lib/schemas/signup"
|
||||||
|
|
||||||
export const Route = createFileRoute("/signup")({
|
export const Route = createFileRoute("/signup")({
|
||||||
component: SignupComp
|
component: SignupComp
|
||||||
@ -8,24 +10,33 @@ export const Route = createFileRoute("/signup")({
|
|||||||
|
|
||||||
function SignupComp() {
|
function SignupComp() {
|
||||||
const signupMutation = mutationSignup()
|
const signupMutation = mutationSignup()
|
||||||
|
const { errors, validate } = useValidation({
|
||||||
|
defaultSchema: signupSchema
|
||||||
|
})
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-center items-center flex-col h-screen">
|
<div className="flex justify-center items-center flex-col h-screen">
|
||||||
<p className="font-semibold mb-3">Signup</p>
|
<p className="font-semibold mb-3">Signup</p>
|
||||||
<Form
|
<Form
|
||||||
className="grid gap-2 max-w-5xl w-full"
|
validationErrors={errors}
|
||||||
|
className="grid gap-2 max-w-sm w-full"
|
||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
const formData = new FormData(e.currentTarget)
|
const formData = new FormData(e.currentTarget)
|
||||||
const email = formData.get("email") as string
|
const email = formData.get("email") as string
|
||||||
const password = formData.get("password") as string
|
const password = formData.get("password") as string
|
||||||
signupMutation.mutate({
|
if (
|
||||||
email,
|
validate({
|
||||||
password
|
formData: { email, password }
|
||||||
})
|
})
|
||||||
|
)
|
||||||
|
signupMutation.mutate({
|
||||||
|
email,
|
||||||
|
password
|
||||||
|
})
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Input name="email" type="email" placeholder="Email" />
|
<Input name="email" type="email" label="Email" />
|
||||||
<Input name="password" type="password" placeholder="Password" />
|
<Input name="password" type="password" label="Password" />
|
||||||
<Button type="submit" isLoading={signupMutation.isPending}>
|
<Button type="submit" isLoading={signupMutation.isPending}>
|
||||||
Enviar
|
Enviar
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user