From e45772e2a975546fefe2aaca3b54731da56e5ece Mon Sep 17 00:00:00 2001 From: juan Date: Sun, 10 Aug 2025 18:30:07 +0200 Subject: [PATCH] feat: add validation schemas for login and signup, integrate validation in respective components --- package-lock.json | 54 +++++++++++++++++++++++++--- package.json | 3 +- src/lib/hooks/useValidation.tsx | 37 +++++++++++++++++++ src/lib/mutations/mutationLogin.tsx | 10 +++--- src/lib/mutations/mutationSignup.tsx | 9 ++--- src/lib/schemas/login.ts | 6 ++++ src/lib/schemas/signup.ts | 7 ++++ src/routes/login.tsx | 26 +++++++++++--- src/routes/signup.tsx | 25 +++++++++---- 9 files changed, 150 insertions(+), 27 deletions(-) create mode 100644 src/lib/hooks/useValidation.tsx create mode 100644 src/lib/schemas/login.ts create mode 100644 src/lib/schemas/signup.ts diff --git a/package-lock.json b/package-lock.json index 49e70d7..f39ae46 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" diff --git a/package.json b/package.json index 8be49e2..1de3c7e 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/lib/hooks/useValidation.tsx b/src/lib/hooks/useValidation.tsx new file mode 100644 index 0000000..b89253c --- /dev/null +++ b/src/lib/hooks/useValidation.tsx @@ -0,0 +1,37 @@ +import { useState } from "react" +import type { z } from "zod" + +type FormDataValidation = Record + +export const useValidation = ({ + defaultSchema +}: { + defaultSchema?: z.ZodSchema +}) => { + const [errors, setErrors] = useState() + + const validate = ({ + formData, + schema + }: { + formData: FormDataValidation + schema?: z.ZodSchema + }) => { + 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 } +} diff --git a/src/lib/mutations/mutationLogin.tsx b/src/lib/mutations/mutationLogin.tsx index 587ded3..fe9fc1b 100644 --- a/src/lib/mutations/mutationLogin.tsx +++ b/src/lib/mutations/mutationLogin.tsx @@ -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...", { diff --git a/src/lib/mutations/mutationSignup.tsx b/src/lib/mutations/mutationSignup.tsx index 57b5aa4..a4f8596 100644 --- a/src/lib/mutations/mutationSignup.tsx +++ b/src/lib/mutations/mutationSignup.tsx @@ -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" }) diff --git a/src/lib/schemas/login.ts b/src/lib/schemas/login.ts new file mode 100644 index 0000000..e9ba067 --- /dev/null +++ b/src/lib/schemas/login.ts @@ -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") +}) diff --git a/src/lib/schemas/signup.ts b/src/lib/schemas/signup.ts new file mode 100644 index 0000000..93b081b --- /dev/null +++ b/src/lib/schemas/signup.ts @@ -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() +}) diff --git a/src/routes/login.tsx b/src/routes/login.tsx index 01ce9e9..33aa73a 100644 --- a/src/routes/login.tsx +++ b/src/routes/login.tsx @@ -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 (

Login

{ 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 }) }} > - - - + + +
) diff --git a/src/routes/signup.tsx b/src/routes/signup.tsx index 5c36830..7fa08b9 100644 --- a/src/routes/signup.tsx +++ b/src/routes/signup.tsx @@ -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 (

Signup

{ 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 + }) }} > - - + +