diff --git a/demo/nextjs/components/sign-in.tsx b/demo/nextjs/components/sign-in.tsx index 393e21b7..9508be3e 100644 --- a/demo/nextjs/components/sign-in.tsx +++ b/demo/nextjs/components/sign-in.tsx @@ -5,28 +5,51 @@ import { Card, CardContent, CardDescription, - CardFooter, CardHeader, CardTitle, } from "@/components/ui/card"; -import { Checkbox } from "@/components/ui/checkbox"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { PasswordInput } from "@/components/ui/password-input"; import { signIn } from "@/lib/auth-client"; import { DiscordLogoIcon, GitHubLogoIcon } from "@radix-ui/react-icons"; -import { Key, Loader2 } from "lucide-react"; +import { Loader2 } from "lucide-react"; import Link from "next/link"; import { useRouter } from "next/navigation"; -import { useState } from "react"; +import { useState, useEffect } from "react"; import { toast } from "sonner"; +import { z } from "zod"; + +const signInSchema = z.object({ + email: z.string().email("Invalid email address"), + password: z.string().min(8, "Password must be at least 8 characters"), +}); export default function SignIn() { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [rememberMe, setRememberMe] = useState(false); + const [errors, setErrors] = useState({ email: "", password: "" }); + const [isValid, setIsValid] = useState(false); + const [touched, setTouched] = useState({ email: false, password: false }); const router = useRouter(); const [loading, setLoading] = useState(false); + + useEffect(() => { + const validationResult = signInSchema.safeParse({ email, password }); + if (!validationResult.success) { + const newErrors: any = {}; + validationResult.error.errors.forEach((err) => { + newErrors[err.path[0]] = err.message; + }); + setErrors(newErrors); + setIsValid(false); + } else { + setErrors({ email: "", password: "" }); + setIsValid(true); + } + }, [email, password]); + return ( @@ -46,9 +69,13 @@ export default function SignIn() { required onChange={(e) => { setEmail(e.target.value); + setTouched((prev) => ({ ...prev, email: true })); }} value={email} /> + {touched.email && errors.email && ( +

{errors.email}

+ )}
@@ -63,24 +90,21 @@ export default function SignIn() { setPassword(e.target.value)} + onChange={(e) => { + setPassword(e.target.value); + setTouched((prev) => ({ ...prev, password: true })); + }} autoComplete="password" placeholder="Password" /> + {touched.password && errors.password && ( +

{errors.password}

+ )}
-
- { - setRememberMe(!rememberMe); - }} - /> - -
- +
-
- -
-

- Secured by better-auth. -

-
-
); } diff --git a/demo/nextjs/components/sign-up.tsx b/demo/nextjs/components/sign-up.tsx index b7dfecaa..6ba5c7b0 100644 --- a/demo/nextjs/components/sign-up.tsx +++ b/demo/nextjs/components/sign-up.tsx @@ -1,48 +1,102 @@ "use client"; - +import Image from "next/image"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, - CardFooter, CardHeader, CardTitle, } from "@/components/ui/card"; +import { toast } from "sonner"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { PasswordInput } from "@/components/ui/password-input"; -import { DiscordLogoIcon, GitHubLogoIcon } from "@radix-ui/react-icons"; -import { useState } from "react"; -import { signIn, signUp } from "@/lib/auth-client"; -import Image from "next/image"; -import { Loader2, X } from "lucide-react"; -import { toast } from "sonner"; +import { signUp, signIn } from "@/lib/auth-client"; import { useRouter } from "next/navigation"; +import { useState, useEffect, ChangeEvent } from "react"; +import { z } from "zod"; +import { DiscordLogoIcon, GitHubLogoIcon } from "@radix-ui/react-icons"; +import { Loader2, X } from "lucide-react"; +const signUpSchema = z + .object({ + firstName: z.string().min(1, "First name is required"), + lastName: z.string().min(1, "Last name is required"), + email: z.string().email("Invalid email address"), + image: z + .instanceof(File) + .optional() + .refine((file) => !file || file.type.startsWith("image/"), { + message: "Invalid file type. Only images are allowed.", + path: ["image"], + }), + password: z.string().min(6, "Password must be at least 6 characters"), + passwordConfirmation: z + .string() + .min(6, "Password must be at least 6 characters"), + }) + + .refine((data) => data.password === data.passwordConfirmation, { + message: "Passwords do not match", + path: ["passwordConfirmation"], + }); + +type FormDataType = z.infer; + +type ErrorsType = Partial>; + +type TouchedType = Partial>; export function SignUp() { - const [firstName, setFirstName] = useState(""); - const [lastName, setLastName] = useState(""); - const [email, setEmail] = useState(""); - const [password, setPassword] = useState(""); - const [passwordConfirmation, setPasswordConfirmation] = useState(""); + const [formData, setFormData] = useState({ + firstName: "", + lastName: "", + email: "", + password: "", + image: undefined, + passwordConfirmation: "", + }); + const [errors, setErrors] = useState({}); + const [isValid, setIsValid] = useState(false); + const [touched, setTouched] = useState({}); + const [loading, setLoading] = useState(false); const [image, setImage] = useState(null); const [imagePreview, setImagePreview] = useState(null); const router = useRouter(); - const handleImageChange = (e: React.ChangeEvent) => { - const file = e.target.files?.[0]; + useEffect(() => { + const validationResult = signUpSchema.safeParse(formData); + if (!validationResult.success) { + const newErrors: ErrorsType = {}; + validationResult.error.errors.forEach((err) => { + if (err.path[0]) + newErrors[err.path[0] as keyof FormDataType] = err.message; + }); + setErrors(newErrors); + setIsValid(false); + } else { + setErrors({}); + setIsValid(true); + } + }, [formData]); + + const handleChange = (e: ChangeEvent) => { + const { id, value } = e.target; + setFormData((prev) => ({ ...prev, [id]: value })); + setTouched((prev) => ({ ...prev, [id]: true })); + }; + const handleImageChange = (e: ChangeEvent) => { + const file = e.target.files?.[0] || null; + setImage(file); + setFormData((prev) => ({ ...prev, image: file || undefined })); if (file) { - setImage(file); const reader = new FileReader(); - reader.onloadend = () => { - setImagePreview(reader.result as string); - }; + reader.onload = () => setImagePreview(reader.result as string); reader.readAsDataURL(file); + } else { + setImagePreview(null); } }; - const [loading, setLoading] = useState(false); - return ( @@ -55,28 +109,28 @@ export function SignUp() {
- + { - setFirstName(e.target.value); - }} - value={firstName} + value={formData.firstName} + onChange={handleChange} /> + {touched.firstName && errors.firstName && ( +

{errors.firstName}

+ )}
- + { - setLastName(e.target.value); - }} - value={lastName} + value={formData.lastName} + onChange={handleChange} /> + {touched.lastName && errors.lastName && ( +

{errors.lastName}

+ )}
@@ -85,32 +139,38 @@ export function SignUp() { id="email" type="email" placeholder="m@example.com" - required - onChange={(e) => { - setEmail(e.target.value); - }} - value={email} + value={formData.email} + onChange={handleChange} /> + {touched.email && errors.email && ( +

{errors.email}

+ )}
setPassword(e.target.value)} - autoComplete="new-password" + value={formData.password} + onChange={handleChange} placeholder="Password" /> + {touched.password && errors.password && ( +

{errors.password}

+ )}
- + setPasswordConfirmation(e.target.value)} - autoComplete="new-password" + id="passwordConfirmation" + value={formData.passwordConfirmation} + onChange={handleChange} placeholder="Confirm Password" /> + {touched.passwordConfirmation && errors.passwordConfirmation && ( +

+ {errors.passwordConfirmation} +

+ )}
@@ -125,42 +185,36 @@ export function SignUp() { />
)} -
- + {imagePreview && ( + { + setImage(null); + setImagePreview(null); + }} /> - {imagePreview && ( - { - setImage(null); - setImagePreview(null); - }} - /> - )} -
+ )}