feat: add login - sign up submit function

This commit is contained in:
Sosokker 2025-02-13 22:07:38 +07:00
parent 125a6a7018
commit 98d4765e0d
11 changed files with 611 additions and 47 deletions

View File

@ -0,0 +1,9 @@
import Image from "next/image";
export function GoogleSigninButton() {
return (
<div className="flex w-1/3 justify-center rounded-full border-2 border-border bg-gray-100 hover:bg-gray-300 duration-300 cursor-pointer ">
<Image src="/google-logo.png" alt="Google Logo" width={35} height={35} className="object-contain" />
</div>
);
}

View File

@ -1,3 +1,10 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { signInSchema } from "@/schema/authSchema";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Checkbox } from "@/components/ui/checkbox"; import { Checkbox } from "@/components/ui/checkbox";
@ -6,19 +13,67 @@ import ForgotPasswordModal from "./forgot-password-modal";
import Link from "next/link"; import Link from "next/link";
import Image from "next/image"; import Image from "next/image";
import { GoogleSigninButton } from "./google-oauth";
import { z } from "zod";
import { useState } from "react";
import { useRouter } from "next/navigation";
export default function SigninPage() { export default function SigninPage() {
const [serverError, setServerError] = useState(null);
const router = useRouter();
const {
register,
handleSubmit,
formState: { errors },
} = useForm({
resolver: zodResolver(signInSchema),
defaultValues: {
email: "",
password: "",
},
});
const onSubmit = async (values: z.infer<typeof signInSchema>) => {
setServerError(null); // reset previous errors
try {
const response = await fetch("http://localhost:8000/auth/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
Email: values.email,
Password: values.password,
}),
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.message || "Failed to log in.");
}
localStorage.setItem("token", data.Token);
localStorage.setItem("user", values.email);
router.push("/setup");
} catch (error) {
console.error("Error logging in:", error);
setServerError(error.message);
}
};
return ( return (
<div> <div>
<div className="grid grid-cols-[0.7fr_1.2fr] h-screen overflow-hidden"> <div className="grid grid-cols-[0.7fr_1.2fr] h-screen overflow-hidden">
<div className="flex bg-[url('/plant-background.jpeg')] bg-cover bg-center"></div> <div className="flex bg-[url('/plant-background.jpeg')] bg-cover bg-center"></div>
<div className="flex justify-center items-center"> <div className="flex justify-center items-center">
{/* login box */}
<div className="container px-[25%]"> <div className="container px-[25%]">
<div className="flex flex-col justify-center items-center"> <div className="flex flex-col justify-center items-center">
<span> <span>
<Image src={`/forfarm-logo.png`} alt="Forfarm" width={150} height={150}></Image> <Image src="/forfarm-logo.png" alt="Forfarm" width={150} height={150} />
</span> </span>
<h1 className="text-3xl font-semibold">Welcome back.</h1> <h1 className="text-3xl font-semibold">Welcome back.</h1>
<div className="flex whitespace-nowrap gap-x-2 mt-2"> <div className="flex whitespace-nowrap gap-x-2 mt-2">
@ -31,27 +86,28 @@ export default function SigninPage() {
</div> </div>
</div> </div>
<div className="flex flex-col mt-4"> {/* Sign in form */}
<form onSubmit={handleSubmit(onSubmit)} className="flex flex-col mt-4">
<div> <div>
<Label htmlFor="email">Email</Label> <Label htmlFor="email">Email</Label>
<Input type="email" id="email" placeholder="Email" /> <Input type="email" id="email" placeholder="Email" {...register("email")} />
{errors.email && <p className="text-red-600 text-sm">{errors.email.message}</p>}
</div> </div>
<div className="mt-5"> <div className="mt-5">
<div>
<Label htmlFor="password">Password</Label> <Label htmlFor="password">Password</Label>
<Input type="empasswordail" id="password" placeholder="Password" /> <Input type="password" id="password" placeholder="Password" {...register("password")} />
</div> {errors.password && <p className="text-red-600 text-sm">{errors.password.message}</p>}
</div> </div>
<Button className="mt-5 rounded-full">Log in</Button> <Button type="submit" className="mt-5 rounded-full">
</div> Log in
</Button>
</form>
<div id="signin-footer" className="flex justify-between mt-5"> <div id="signin-footer" className="flex justify-between mt-5">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Checkbox id="terms" /> <Checkbox id="terms" />
<label <label htmlFor="terms" className="text-sm font-medium leading-none">
htmlFor="terms"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
Remember me Remember me
</label> </label>
</div> </div>
@ -60,12 +116,8 @@ export default function SigninPage() {
<div className="my-5"> <div className="my-5">
<p className="text-sm">Or log in with</p> <p className="text-sm">Or log in with</p>
{/* OAUTH */}
<div className="flex flex-col gap-x-5 mt-3"> <div className="flex flex-col gap-x-5 mt-3">
{/* Google */} <GoogleSigninButton />
<div className="flex w-1/3 justify-center rounded-full border-2 border-border bg-gray-100 hover:bg-gray-300 duration-300 cursor-pointer ">
<Image src="/google-logo.png" alt="Google Logo" width={35} height={35} className="object-contain" />
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,62 +1,133 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Checkbox } from "@/components/ui/checkbox"; import { Checkbox } from "@/components/ui/checkbox";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { signUpSchema } from "@/schema/authSchema";
import Link from "next/link"; import Link from "next/link";
import Image from "next/image"; import Image from "next/image";
import { useState } from "react";
import { z } from "zod";
import { useRouter } from "next/navigation";
export default function SignupPage() { export default function SignupPage() {
const [serverError, setServerError] = useState(null);
const [successMessage, setSuccessMessage] = useState(null);
const router = useRouter();
const {
register,
handleSubmit,
formState: { errors },
} = useForm<z.infer<typeof signUpSchema>>({
resolver: zodResolver(signUpSchema),
defaultValues: {
email: "",
password: "",
confirmPassword: "",
},
});
const onSubmit = async (values: z.infer<typeof signUpSchema>) => {
setServerError(null);
setSuccessMessage(null);
try {
const response = await fetch("http://localhost:8000/auth/register", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
Email: values.email,
Password: values.password,
}),
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.message || "Failed to register.");
}
localStorage.setItem("token", data.token);
localStorage.setItem("user", values.email);
// Assume registration returns a token or user data.
console.log("Registration successful:", data);
setSuccessMessage("Registration successful! You can now sign in.");
router.push("/setup");
} catch (error: any) {
console.error("Error during registration:", error);
setServerError(error.message);
}
};
return ( return (
<div> <div>
<div className="grid grid-cols-[0.7fr_1.2fr] h-screen overflow-hidden"> <div className="grid grid-cols-[0.7fr_1.2fr] h-screen overflow-hidden">
<div className="flex bg-[url('/plant-background.jpeg')] bg-cover bg-center"></div> <div className="flex bg-[url('/plant-background.jpeg')] bg-cover bg-center"></div>
<div className="flex justify-center items-center"> <div className="flex justify-center items-center">
{/* login box */}
<div className="container px-[25%]"> <div className="container px-[25%]">
<div className="flex flex-col justify-center items-center"> <div className="flex flex-col justify-center items-center">
<span> <span>
<Image src={`/forfarm-logo.png`} alt="Forfarm" width={150} height={150}></Image> <Image src="/forfarm-logo.png" alt="Forfarm" width={150} height={150} />
</span> </span>
<h1 className="text-3xl font-semibold">Hi! Welcome</h1> <h1 className="text-3xl font-semibold">Hi! Welcome</h1>
<div className="flex whitespace-nowrap gap-x-2 mt-2"> <div className="flex whitespace-nowrap gap-x-2 mt-2">
<span className="text-md">Already have accounts?</span> <span className="text-md">Already have an account?</span>
<span className="text-green-600"> <span className="text-green-600">
<Link href="signin" className="underline"> <Link href="/auth/signin" className="underline">
Sign in Sign in
</Link> </Link>
</span> </span>
</div> </div>
</div> </div>
<div className="flex flex-col mt-4"> {/* Signup form */}
<form onSubmit={handleSubmit(onSubmit)} className="flex flex-col mt-4">
<div> <div>
<Label htmlFor="email">Email</Label> <Label htmlFor="email">Email</Label>
<Input type="email" id="email" placeholder="Email" /> <Input type="email" id="email" placeholder="Email" {...register("email")} />
{errors.email && <p className="text-red-600 text-sm">{errors.email.message}</p>}
</div> </div>
<div className="mt-5"> <div className="mt-5">
<div>
<Label htmlFor="password">Password</Label> <Label htmlFor="password">Password</Label>
<Input type="empasswordail" id="password" placeholder="Password" /> <Input type="password" id="password" placeholder="Password" {...register("password")} />
</div> {errors.password && <p className="text-red-600 text-sm">{errors.password.message}</p>}
</div> </div>
<div className="mt-5"> <div className="mt-5">
<div> <Label htmlFor="confirmPassword">Confirm Password</Label>
<Label htmlFor="password">Confirm Password</Label> <Input
<Input type="empasswordail" id="password" placeholder="Password" /> type="password"
</div> id="confirmPassword"
placeholder="Confirm Password"
{...register("confirmPassword")}
/>
{errors.confirmPassword && <p className="text-red-600 text-sm">{errors.confirmPassword.message}</p>}
</div> </div>
<Button className="mt-5 rounded-full">Sign up</Button> {serverError && <p className="text-red-600 mt-2 text-sm">{serverError}</p>}
</div> {successMessage && <p className="text-green-600 mt-2 text-sm">{successMessage}</p>}
<Button type="submit" className="mt-5 rounded-full">
Sign up
</Button>
</form>
<div className="my-5"> <div className="my-5">
<p className="text-sm">Or log in with</p> <p className="text-sm">Or sign up with</p>
{/* OAUTH */}
<div className="flex flex-col gap-x-5 mt-3"> <div className="flex flex-col gap-x-5 mt-3">
{/* Google */} {/* Google OAuth button or additional providers */}
<div className="flex w-1/3 justify-center rounded-full border-2 border-border bg-gray-100 hover:bg-gray-300 duration-300 cursor-pointer "> <div className="flex w-1/3 justify-center rounded-full border-2 border-border bg-gray-100 hover:bg-gray-300 duration-300 cursor-pointer">
<Image src="/google-logo.png" alt="Google Logo" width={35} height={35} className="object-contain" /> <Image src="/google-logo.png" alt="Google Logo" width={35} height={35} className="object-contain" />
</div> </div>
</div> </div>

View File

@ -3,6 +3,8 @@ import { Open_Sans, Roboto_Mono } from "next/font/google";
import "./globals.css"; import "./globals.css";
import { ThemeProvider } from "@/components/theme-provider"; import { ThemeProvider } from "@/components/theme-provider";
import { SessionProvider } from "@/context/SessionContext";
const openSans = Open_Sans({ const openSans = Open_Sans({
subsets: ["latin"], subsets: ["latin"],
display: "swap", display: "swap",
@ -33,6 +35,7 @@ export default function RootLayout({
return ( return (
<html lang="en" suppressHydrationWarning> <html lang="en" suppressHydrationWarning>
<head /> <head />
<SessionProvider>
<body className={`${openSans.variable} ${robotoMono.variable} font-sans antialiased`}> <body className={`${openSans.variable} ${robotoMono.variable} font-sans antialiased`}>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem> <ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<div className="relative flex min-h-screen flex-col"> <div className="relative flex min-h-screen flex-col">
@ -40,6 +43,7 @@ export default function RootLayout({
</div> </div>
</ThemeProvider> </ThemeProvider>
</body> </body>
</SessionProvider>
</html> </html>
); );
} }

View File

@ -0,0 +1,12 @@
"use client";
import React, { ReactNode } from "react";
import { SessionProvider } from "next-auth/react";
interface Props {
children: ReactNode;
}
export function SessesionProviderClient(props: Props) {
return <SessionProvider>{props.children}</SessionProvider>;
}

View File

@ -0,0 +1,178 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { Slot } from "@radix-ui/react-slot"
import {
Controller,
ControllerProps,
FieldPath,
FieldValues,
FormProvider,
useFormContext,
} from "react-hook-form"
import { cn } from "@/lib/utils"
import { Label } from "@/components/ui/label"
const Form = FormProvider
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
> = {
name: TName
}
const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue
)
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
)
}
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext)
const itemContext = React.useContext(FormItemContext)
const { getFieldState, formState } = useFormContext()
const fieldState = getFieldState(fieldContext.name, formState)
if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>")
}
const { id } = itemContext
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
}
}
type FormItemContextValue = {
id: string
}
const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue
)
const FormItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const id = React.useId()
return (
<FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn("space-y-2", className)} {...props} />
</FormItemContext.Provider>
)
})
FormItem.displayName = "FormItem"
const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField()
return (
<Label
ref={ref}
className={cn(error && "text-destructive", className)}
htmlFor={formItemId}
{...props}
/>
)
})
FormLabel.displayName = "FormLabel"
const FormControl = React.forwardRef<
React.ElementRef<typeof Slot>,
React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
return (
<Slot
ref={ref}
id={formItemId}
aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
/>
)
})
FormControl.displayName = "FormControl"
const FormDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField()
return (
<p
ref={ref}
id={formDescriptionId}
className={cn("text-[0.8rem] text-muted-foreground", className)}
{...props}
/>
)
})
FormDescription.displayName = "FormDescription"
const FormMessage = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField()
const body = error ? String(error?.message) : children
if (!body) {
return null
}
return (
<p
ref={ref}
id={formMessageId}
className={cn("text-[0.8rem] font-medium text-destructive", className)}
{...props}
>
{body}
</p>
)
})
FormMessage.displayName = "FormMessage"
export {
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField,
}

View File

@ -0,0 +1,56 @@
"use client";
import React, { createContext, useContext, useState, useEffect, ReactNode } from "react";
interface SessionContextType {
token: string | null;
user: any | null;
setToken: (token: string | null) => void;
setUser: (user: any | null) => void;
}
const SessionContext = createContext<SessionContextType | undefined>(undefined);
interface SessionProviderProps {
children: ReactNode;
}
export function SessionProvider({ children }: SessionProviderProps) {
const [token, setTokenState] = useState<string | null>(null);
const [user, setUserState] = useState<any | null>(null);
const setToken = (newToken: string | null) => {
if (newToken) {
localStorage.setItem("token", newToken);
} else {
localStorage.removeItem("token");
}
setTokenState(newToken);
};
const setUser = (newUser: any | null) => {
if (newUser) {
localStorage.setItem("user", JSON.stringify(newUser));
} else {
localStorage.removeItem("user");
}
setUserState(newUser);
};
useEffect(() => {
const storedToken = localStorage.getItem("token");
const storedUser = localStorage.getItem("user");
if (storedToken) {
setTokenState(storedToken);
}
if (storedUser) {
try {
setUserState(JSON.parse(storedUser));
} catch (error) {
console.error("Failed to parse stored user.", error);
}
}
}, []);
return <SessionContext.Provider value={{ token, user, setToken, setUser }}>{children}</SessionContext.Provider>;
}

View File

@ -0,0 +1,12 @@
"use client";
import { SessionContext } from "next-auth/react";
import { useContext } from "react";
export function useSession() {
const context = useContext(SessionContext);
if (!context) {
throw new Error("useSession must be used within a SessionProvider");
}
return context;
}

View File

@ -9,6 +9,7 @@
"lint": "next lint" "lint": "next lint"
}, },
"dependencies": { "dependencies": {
"@hookform/resolvers": "^4.0.0",
"@radix-ui/react-avatar": "^1.1.3", "@radix-ui/react-avatar": "^1.1.3",
"@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-checkbox": "^1.1.4",
"@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dialog": "^1.1.6",
@ -23,6 +24,7 @@
"clsx": "^2.1.1", "clsx": "^2.1.1",
"lucide-react": "^0.475.0", "lucide-react": "^0.475.0",
"next": "15.1.0", "next": "15.1.0",
"next-auth": "^4.24.11",
"next-themes": "^0.4.4", "next-themes": "^0.4.4",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",

View File

@ -8,6 +8,9 @@ importers:
.: .:
dependencies: dependencies:
'@hookform/resolvers':
specifier: ^4.0.0
version: 4.0.0(react-hook-form@7.54.2(react@19.0.0))
'@radix-ui/react-avatar': '@radix-ui/react-avatar':
specifier: ^1.1.3 specifier: ^1.1.3
version: 1.1.3(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) version: 1.1.3(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
@ -50,6 +53,9 @@ importers:
next: next:
specifier: 15.1.0 specifier: 15.1.0
version: 15.1.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0) version: 15.1.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
next-auth:
specifier: ^4.24.11
version: 4.24.11(next@15.1.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
next-themes: next-themes:
specifier: ^0.4.4 specifier: ^0.4.4
version: 0.4.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0) version: 0.4.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
@ -106,6 +112,10 @@ packages:
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
engines: {node: '>=10'} engines: {node: '>=10'}
'@babel/runtime@7.26.7':
resolution: {integrity: sha512-AOPI3D+a8dXnja+iwsUqGRjr1BbZIe771sXdapOtYI531gSqpi92vXivKcq2asu/DFpdl1ceFAKZyRzK2PCVcQ==}
engines: {node: '>=6.9.0'}
'@emnapi/runtime@1.3.1': '@emnapi/runtime@1.3.1':
resolution: {integrity: sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==} resolution: {integrity: sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==}
@ -162,6 +172,11 @@ packages:
'@floating-ui/utils@0.2.9': '@floating-ui/utils@0.2.9':
resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==} resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==}
'@hookform/resolvers@4.0.0':
resolution: {integrity: sha512-93ZueVlTaeMF0pRbrLbcnzrxeb2mGA/xyO3RgfrsKs5OCtcfjoWcdjBJm+N7096Jfg/JYlGPjuyQCgqVEodSTg==}
peerDependencies:
react-hook-form: ^7.0.0
'@humanfs/core@0.19.1': '@humanfs/core@0.19.1':
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
engines: {node: '>=18.18.0'} engines: {node: '>=18.18.0'}
@ -379,6 +394,9 @@ packages:
resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==}
engines: {node: '>=12.4.0'} engines: {node: '>=12.4.0'}
'@panva/hkdf@1.2.1':
resolution: {integrity: sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==}
'@pkgjs/parseargs@0.11.0': '@pkgjs/parseargs@0.11.0':
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'} engines: {node: '>=14'}
@ -1016,6 +1034,10 @@ packages:
concat-map@0.0.1: concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
cookie@0.7.2:
resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
engines: {node: '>= 0.6'}
cross-spawn@7.0.6: cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
@ -1538,6 +1560,9 @@ packages:
resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==}
hasBin: true hasBin: true
jose@4.15.9:
resolution: {integrity: sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==}
js-tokens@4.0.0: js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
@ -1603,6 +1628,10 @@ packages:
lru-cache@10.4.3: lru-cache@10.4.3:
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
lru-cache@6.0.0:
resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
engines: {node: '>=10'}
lucide-react@0.475.0: lucide-react@0.475.0:
resolution: {integrity: sha512-NJzvVu1HwFVeZ+Gwq2q00KygM1aBhy/ZrhY9FsAgJtpB+E4R7uxRk9M2iKvHa6/vNxZydIB59htha4c2vvwvVg==} resolution: {integrity: sha512-NJzvVu1HwFVeZ+Gwq2q00KygM1aBhy/ZrhY9FsAgJtpB+E4R7uxRk9M2iKvHa6/vNxZydIB59htha4c2vvwvVg==}
peerDependencies: peerDependencies:
@ -1648,6 +1677,20 @@ packages:
natural-compare@1.4.0: natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
next-auth@4.24.11:
resolution: {integrity: sha512-pCFXzIDQX7xmHFs4KVH4luCjaCbuPRtZ9oBUjUhOk84mZ9WVPf94n87TxYI4rSRf9HmfHEF8Yep3JrYDVOo3Cw==}
peerDependencies:
'@auth/core': 0.34.2
next: ^12.2.5 || ^13 || ^14 || ^15
nodemailer: ^6.6.5
react: ^17.0.2 || ^18 || ^19
react-dom: ^17.0.2 || ^18 || ^19
peerDependenciesMeta:
'@auth/core':
optional: true
nodemailer:
optional: true
next-themes@0.4.4: next-themes@0.4.4:
resolution: {integrity: sha512-LDQ2qIOJF0VnuVrrMSMLrWGjRMkq+0mpgl6e0juCLqdJ+oo8Q84JRWT6Wh11VDQKkMMe+dVzDKLWs5n87T+PkQ==} resolution: {integrity: sha512-LDQ2qIOJF0VnuVrrMSMLrWGjRMkq+0mpgl6e0juCLqdJ+oo8Q84JRWT6Wh11VDQKkMMe+dVzDKLWs5n87T+PkQ==}
peerDependencies: peerDependencies:
@ -1679,10 +1722,17 @@ packages:
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
oauth@0.9.15:
resolution: {integrity: sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==}
object-assign@4.1.1: object-assign@4.1.1:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
object-hash@2.2.0:
resolution: {integrity: sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==}
engines: {node: '>= 6'}
object-hash@3.0.0: object-hash@3.0.0:
resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
@ -1715,6 +1765,13 @@ packages:
resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
oidc-token-hash@5.0.3:
resolution: {integrity: sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==}
engines: {node: ^10.13.0 || >=12.0.0}
openid-client@5.7.1:
resolution: {integrity: sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==}
optionator@0.9.4: optionator@0.9.4:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
@ -1821,10 +1878,21 @@ packages:
resolution: {integrity: sha512-MjOadfU3Ys9KYoX0AdkBlFEF1Vx37uCCeN4ZHnmwm9FfpbsGWMZeBLMmmpY+6Ocqod7mkdZ0DT31OlbsFrLlkA==} resolution: {integrity: sha512-MjOadfU3Ys9KYoX0AdkBlFEF1Vx37uCCeN4ZHnmwm9FfpbsGWMZeBLMmmpY+6Ocqod7mkdZ0DT31OlbsFrLlkA==}
engines: {node: ^10 || ^12 || >=14} engines: {node: ^10 || ^12 || >=14}
preact-render-to-string@5.2.6:
resolution: {integrity: sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==}
peerDependencies:
preact: '>=10'
preact@10.25.4:
resolution: {integrity: sha512-jLdZDb+Q+odkHJ+MpW/9U5cODzqnB+fy2EiHSZES7ldV5LK7yjlVzTp7R8Xy6W6y75kfK8iWYtFVH7lvjwrCMA==}
prelude-ls@1.2.1: prelude-ls@1.2.1:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
pretty-format@3.8.0:
resolution: {integrity: sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==}
prop-types@15.8.1: prop-types@15.8.1:
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
@ -1894,6 +1962,9 @@ packages:
resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
regenerator-runtime@0.14.1:
resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
regexp.prototype.flags@1.5.4: regexp.prototype.flags@1.5.4:
resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@ -2177,6 +2248,10 @@ packages:
util-deprecate@1.0.2: util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
uuid@8.3.2:
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
hasBin: true
which-boxed-primitive@1.1.1: which-boxed-primitive@1.1.1:
resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@ -2210,6 +2285,9 @@ packages:
resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
engines: {node: '>=12'} engines: {node: '>=12'}
yallist@4.0.0:
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
yaml@2.7.0: yaml@2.7.0:
resolution: {integrity: sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==} resolution: {integrity: sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==}
engines: {node: '>= 14'} engines: {node: '>= 14'}
@ -2226,6 +2304,10 @@ snapshots:
'@alloc/quick-lru@5.2.0': {} '@alloc/quick-lru@5.2.0': {}
'@babel/runtime@7.26.7':
dependencies:
regenerator-runtime: 0.14.1
'@emnapi/runtime@1.3.1': '@emnapi/runtime@1.3.1':
dependencies: dependencies:
tslib: 2.8.1 tslib: 2.8.1
@ -2294,6 +2376,10 @@ snapshots:
'@floating-ui/utils@0.2.9': {} '@floating-ui/utils@0.2.9': {}
'@hookform/resolvers@4.0.0(react-hook-form@7.54.2(react@19.0.0))':
dependencies:
react-hook-form: 7.54.2(react@19.0.0)
'@humanfs/core@0.19.1': {} '@humanfs/core@0.19.1': {}
'@humanfs/node@0.16.6': '@humanfs/node@0.16.6':
@ -2452,6 +2538,8 @@ snapshots:
'@nolyfill/is-core-module@1.0.39': {} '@nolyfill/is-core-module@1.0.39': {}
'@panva/hkdf@1.2.1': {}
'@pkgjs/parseargs@0.11.0': '@pkgjs/parseargs@0.11.0':
optional: true optional: true
@ -3117,6 +3205,8 @@ snapshots:
concat-map@0.0.1: {} concat-map@0.0.1: {}
cookie@0.7.2: {}
cross-spawn@7.0.6: cross-spawn@7.0.6:
dependencies: dependencies:
path-key: 3.1.1 path-key: 3.1.1
@ -3803,6 +3893,8 @@ snapshots:
jiti@1.21.7: {} jiti@1.21.7: {}
jose@4.15.9: {}
js-tokens@4.0.0: {} js-tokens@4.0.0: {}
js-yaml@4.1.0: js-yaml@4.1.0:
@ -3861,6 +3953,10 @@ snapshots:
lru-cache@10.4.3: {} lru-cache@10.4.3: {}
lru-cache@6.0.0:
dependencies:
yallist: 4.0.0
lucide-react@0.475.0(react@19.0.0): lucide-react@0.475.0(react@19.0.0):
dependencies: dependencies:
react: 19.0.0 react: 19.0.0
@ -3898,6 +3994,21 @@ snapshots:
natural-compare@1.4.0: {} natural-compare@1.4.0: {}
next-auth@4.24.11(next@15.1.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
dependencies:
'@babel/runtime': 7.26.7
'@panva/hkdf': 1.2.1
cookie: 0.7.2
jose: 4.15.9
next: 15.1.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
oauth: 0.9.15
openid-client: 5.7.1
preact: 10.25.4
preact-render-to-string: 5.2.6(preact@10.25.4)
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
uuid: 8.3.2
next-themes@0.4.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0): next-themes@0.4.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
dependencies: dependencies:
react: 19.0.0 react: 19.0.0
@ -3930,8 +4041,12 @@ snapshots:
normalize-path@3.0.0: {} normalize-path@3.0.0: {}
oauth@0.9.15: {}
object-assign@4.1.1: {} object-assign@4.1.1: {}
object-hash@2.2.0: {}
object-hash@3.0.0: {} object-hash@3.0.0: {}
object-inspect@1.13.4: {} object-inspect@1.13.4: {}
@ -3973,6 +4088,15 @@ snapshots:
define-properties: 1.2.1 define-properties: 1.2.1
es-object-atoms: 1.1.1 es-object-atoms: 1.1.1
oidc-token-hash@5.0.3: {}
openid-client@5.7.1:
dependencies:
jose: 4.15.9
lru-cache: 6.0.0
object-hash: 2.2.0
oidc-token-hash: 5.0.3
optionator@0.9.4: optionator@0.9.4:
dependencies: dependencies:
deep-is: 0.1.4 deep-is: 0.1.4
@ -4071,8 +4195,17 @@ snapshots:
picocolors: 1.1.1 picocolors: 1.1.1
source-map-js: 1.2.1 source-map-js: 1.2.1
preact-render-to-string@5.2.6(preact@10.25.4):
dependencies:
preact: 10.25.4
pretty-format: 3.8.0
preact@10.25.4: {}
prelude-ls@1.2.1: {} prelude-ls@1.2.1: {}
pretty-format@3.8.0: {}
prop-types@15.8.1: prop-types@15.8.1:
dependencies: dependencies:
loose-envify: 1.4.0 loose-envify: 1.4.0
@ -4142,6 +4275,8 @@ snapshots:
get-proto: 1.0.1 get-proto: 1.0.1
which-builtin-type: 1.2.1 which-builtin-type: 1.2.1
regenerator-runtime@0.14.1: {}
regexp.prototype.flags@1.5.4: regexp.prototype.flags@1.5.4:
dependencies: dependencies:
call-bind: 1.0.8 call-bind: 1.0.8
@ -4520,6 +4655,8 @@ snapshots:
util-deprecate@1.0.2: {} util-deprecate@1.0.2: {}
uuid@8.3.2: {}
which-boxed-primitive@1.1.1: which-boxed-primitive@1.1.1:
dependencies: dependencies:
is-bigint: 1.1.0 is-bigint: 1.1.0
@ -4578,6 +4715,8 @@ snapshots:
string-width: 5.1.2 string-width: 5.1.2
strip-ansi: 7.1.0 strip-ansi: 7.1.0
yallist@4.0.0: {}
yaml@2.7.0: {} yaml@2.7.0: {}
yocto-queue@0.1.0: {} yocto-queue@0.1.0: {}

View File

@ -0,0 +1,29 @@
import { z } from "zod";
export const signInSchema = z.object({
email: z
.string({ required_error: "Email is required" })
.min(1, { message: "Email is required" })
.email({ message: "Invalid email address" }),
password: z
.string({ required_error: "Password is required" })
.min(6, { message: "Password must be at least 6 characters long" }),
});
export const signUpSchema = z
.object({
email: z
.string({ required_error: "Email is required" })
.min(1, { message: "Email is required" })
.email({ message: "Invalid email address" }),
password: z
.string({ required_error: "Password is required" })
.min(6, { message: "Password must be at least 6 characters" }),
confirmPassword: z
.string({ required_error: "Confirm your password" })
.min(6, { message: "Confirm Password must be at least 6 characters" }),
})
.refine((data) => data.password === data.confirmPassword, {
message: "Passwords do not match",
path: ["confirmPassword"],
});