From ba5b3aaf7c952bbe7fdaa5696b944fe083432475 Mon Sep 17 00:00:00 2001 From: Ronald Gene Date: Mon, 22 Sep 2025 09:53:55 -0600 Subject: [PATCH 1/5] feat: Add Sign In and Sign Up pages with form handling using react-hook-form --- package-lock.json | 17 +++ package.json | 1 + src/actions/auth/signup/action.ts | 4 +- src/app/(auth)/signin/page.tsx | 143 ++++++++++++++++++++++ src/app/(auth)/signup/page.tsx | 191 ++++++++++++++++++++++++++++++ src/app/auth/signin/page.tsx | 5 - src/app/auth/signup/page.tsx | 5 - 7 files changed, 354 insertions(+), 12 deletions(-) create mode 100644 src/app/(auth)/signin/page.tsx create mode 100644 src/app/(auth)/signup/page.tsx delete mode 100644 src/app/auth/signin/page.tsx delete mode 100644 src/app/auth/signup/page.tsx diff --git a/package-lock.json b/package-lock.json index 8411e4f..22fa723 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "next-safe-action": "^8.0.11", "react": "19.1.0", "react-dom": "19.1.0", + "react-hook-form": "^7.63.0", "react-icons": "^5.5.0", "tailwind-merge": "^3.3.1", "viem": "^2.37.5", @@ -8897,6 +8898,22 @@ "react": "^19.1.0" } }, + "node_modules/react-hook-form": { + "version": "7.63.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.63.0.tgz", + "integrity": "sha512-ZwueDMvUeucovM2VjkCf7zIHcs1aAlDimZu2Hvel5C5907gUzMpm4xCrQXtRzCvsBqFjonB4m3x4LzCFI1ZKWA==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, "node_modules/react-icons": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", diff --git a/package.json b/package.json index b78e3e5..7f19f33 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "next-safe-action": "^8.0.11", "react": "19.1.0", "react-dom": "19.1.0", + "react-hook-form": "^7.63.0", "react-icons": "^5.5.0", "tailwind-merge": "^3.3.1", "viem": "^2.37.5", diff --git a/src/actions/auth/signup/action.ts b/src/actions/auth/signup/action.ts index 9edaffc..fb84552 100644 --- a/src/actions/auth/signup/action.ts +++ b/src/actions/auth/signup/action.ts @@ -2,10 +2,10 @@ import { actionClient } from '@/lib/action'; import { Signup} from './logic'; -import { SignupSchema} from './schema'; +import { signupSchema} from './schema'; export const signupAction = actionClient - .inputSchema(SignupSchema) + .inputSchema(signupSchema) .metadata({ actionName: 'signup' }) .action(async ({ parsedInput }) => { try { diff --git a/src/app/(auth)/signin/page.tsx b/src/app/(auth)/signin/page.tsx new file mode 100644 index 0000000..34f6941 --- /dev/null +++ b/src/app/(auth)/signin/page.tsx @@ -0,0 +1,143 @@ +"use client"; + +import Link from "next/link"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { signinAction } from "@/actions/auth/signin/action"; +import { useForm } from "react-hook-form"; +import { useRouter } from "next/navigation"; + +type SignInFormValues = { + email: string; + password: string; + remember?: boolean; +}; + +export default function SignInPage() { + const router = useRouter(); + const { + register, + handleSubmit, + formState: { errors, isSubmitting }, + setError, + clearErrors, + } = useForm(); + + const onSubmit = async (data: SignInFormValues) => { + clearErrors("root"); + try { + await signinAction(data); + router.push("/"); + } catch (err) { + if (err instanceof Error) { + setError("root", { + type: "server", + message: err.message || "Something went wrong", + }); + } else { + setError("root", { type: "server", message: "Something went wrong" }); + } + } + }; + + return ( +
+ + + + Sign in to Sentiopulse + + + Enter your credentials to access your account + + +
+ +
+ + + {errors.email && ( +

{errors.email.message}

+ )} +
+
+ + + {errors.password && ( +

+ {errors.password.message} +

+ )} +
+
+
+ + +
+ + Forgot password? + +
+ {errors.root && ( +
+ {errors.root.message} +
+ )} +
+ + +

+ Don't have an account?{" "} + + Sign up + +

+
+
+
+
+ ); +} diff --git a/src/app/(auth)/signup/page.tsx b/src/app/(auth)/signup/page.tsx new file mode 100644 index 0000000..35d5627 --- /dev/null +++ b/src/app/(auth)/signup/page.tsx @@ -0,0 +1,191 @@ +"use client"; + +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { + Card, + CardContent, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { signupAction } from "@/actions/auth/signup/action"; +import { useForm } from "react-hook-form"; + +type SignUpFormValues = { + name: string; + email: string; + password: string; + confirmPassword: string; + terms: boolean; +}; + +export default function SignUpPage() { + const { + register, + handleSubmit, + formState: { errors, isSubmitting }, + watch, + } = useForm(); + + const router = useRouter(); + const { setError, clearErrors } = useForm(); + + const password = watch("password"); + + const onSubmit = async (data: SignUpFormValues) => { + clearErrors("root"); + try { + await signupAction(data); + router.push("/"); + } catch (err) { + if (err instanceof Error) { + setError("root", { + type: "server", + message: err.message || "Something went wrong", + }); + } else { + setError("root", { type: "server", message: "Something went wrong" }); + } + } + }; + + return ( +
+ + + + Create your account + + + +
+ + {/* Full Name */} +
+ + + {errors.name && ( +

{errors.name.message}

+ )} +
+ + {/* Email */} +
+ + + {errors.email && ( +

{errors.email.message}

+ )} +
+ + {/* Password */} +
+ + + {errors.password && ( +

+ {errors.password.message} +

+ )} +
+ + {/* Confirm Password */} +
+ + + value === password || "Passwords do not match", + })} + /> + {errors.confirmPassword && ( +

+ {errors.confirmPassword.message} +

+ )} +
+ + {/* Terms */} +
+ + +
+ {errors.terms && ( +

{errors.terms.message}

+ )} + + {/* Server Error */} + {errors.root && ( +
+ {errors.root.message} +
+ )} +
+ + + +

+ Already have an account?{" "} + + Sign in + +

+
+
+
+
+ ); +} diff --git a/src/app/auth/signin/page.tsx b/src/app/auth/signin/page.tsx deleted file mode 100644 index 6fee84b..0000000 --- a/src/app/auth/signin/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import LoginForm from "@/components/LoginForm"; - -export default function SignInPage() { - return ; -} diff --git a/src/app/auth/signup/page.tsx b/src/app/auth/signup/page.tsx deleted file mode 100644 index e71e51d..0000000 --- a/src/app/auth/signup/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import RegisterForm from "@/components/RegisterForm"; - -export default function SignUpPage() { - return ; -} From b8f5f0757c6115b0e65d74905bb9041100585ab5 Mon Sep 17 00:00:00 2001 From: Ronald Gene Date: Mon, 22 Sep 2025 10:08:37 -0600 Subject: [PATCH 2/5] fix: Update Sign In and Sign Up links to remove redundant path segments --- src/app/(auth)/signin/page.tsx | 2 +- src/app/(auth)/signup/page.tsx | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app/(auth)/signin/page.tsx b/src/app/(auth)/signin/page.tsx index 34f6941..208fb5b 100644 --- a/src/app/(auth)/signin/page.tsx +++ b/src/app/(auth)/signin/page.tsx @@ -129,7 +129,7 @@ export default function SignInPage() {

Don't have an account?{" "} Sign up diff --git a/src/app/(auth)/signup/page.tsx b/src/app/(auth)/signup/page.tsx index 35d5627..2eb192e 100644 --- a/src/app/(auth)/signup/page.tsx +++ b/src/app/(auth)/signup/page.tsx @@ -28,10 +28,10 @@ export default function SignUpPage() { handleSubmit, formState: { errors, isSubmitting }, watch, + setError, + clearErrors, } = useForm(); - const router = useRouter(); - const { setError, clearErrors } = useForm(); const password = watch("password"); @@ -177,7 +177,7 @@ export default function SignUpPage() {

Already have an account?{" "} Sign in From b7435d41346b025977a091e21a8d1a64d729f698 Mon Sep 17 00:00:00 2001 From: Ronald Gene Date: Tue, 23 Sep 2025 03:28:55 -0600 Subject: [PATCH 3/5] feat: Refactor authentication forms and add validation with Zod; implement reusable form components --- package-lock.json | 55 +++++ package.json | 4 + src/actions/auth/signup/schema.ts | 28 ++- src/app/(auth)/signin/page.tsx | 140 +------------ src/app/(auth)/signup/page.tsx | 196 +----------------- .../pages/signin/SigninPageContainer.tsx | 116 +++++++++++ .../pages/signup/SignupPageContainer.tsx | 143 +++++++++++++ .../shared/Form/FormButton/FormButton.tsx | 27 +++ .../shared/Form/FormCheckbox/FormCheckbox.tsx | 53 +++++ .../shared/Form/FormInput/FormInput.tsx | 46 ++++ src/components/ui/form.tsx | 167 +++++++++++++++ src/components/ui/label.tsx | 24 +++ 12 files changed, 662 insertions(+), 337 deletions(-) create mode 100644 src/components/pages/signin/SigninPageContainer.tsx create mode 100644 src/components/pages/signup/SignupPageContainer.tsx create mode 100644 src/components/shared/Form/FormButton/FormButton.tsx create mode 100644 src/components/shared/Form/FormCheckbox/FormCheckbox.tsx create mode 100644 src/components/shared/Form/FormInput/FormInput.tsx create mode 100644 src/components/ui/form.tsx create mode 100644 src/components/ui/label.tsx diff --git a/package-lock.json b/package-lock.json index 22fa723..5913709 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,11 @@ "version": "0.1.0", "hasInstallScript": true, "dependencies": { + "@hookform/resolvers": "^5.2.2", "@prisma/client": "^6.15.0", + "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-slot": "^1.2.3", "@rainbow-me/rainbowkit": "^2.2.8", "@tanstack/react-query": "^5.87.1", "bcryptjs": "^3.0.2", @@ -25,6 +28,7 @@ "react-dom": "19.1.0", "react-hook-form": "^7.63.0", "react-icons": "^5.5.0", + "sonner": "^2.0.7", "tailwind-merge": "^3.3.1", "viem": "^2.37.5", "wagmi": "^2.16.9", @@ -563,6 +567,18 @@ "viem": ">=2.0.0" } }, + "node_modules/@hookform/resolvers": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.2.2.tgz", + "integrity": "sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==", + "license": "MIT", + "dependencies": { + "@standard-schema/utils": "^0.3.0" + }, + "peerDependencies": { + "react-hook-form": "^7.55.0" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -1684,6 +1700,29 @@ } } }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz", + "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-popper": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", @@ -3122,6 +3161,12 @@ "devOptional": true, "license": "MIT" }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -9544,6 +9589,16 @@ "atomic-sleep": "^1.0.0" } }, + "node_modules/sonner": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", + "integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", diff --git a/package.json b/package.json index 7f19f33..d6f704c 100644 --- a/package.json +++ b/package.json @@ -15,8 +15,11 @@ "prisma:reset": "prisma migrate reset" }, "dependencies": { + "@hookform/resolvers": "^5.2.2", "@prisma/client": "^6.15.0", + "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-slot": "^1.2.3", "@rainbow-me/rainbowkit": "^2.2.8", "@tanstack/react-query": "^5.87.1", "bcryptjs": "^3.0.2", @@ -31,6 +34,7 @@ "react-dom": "19.1.0", "react-hook-form": "^7.63.0", "react-icons": "^5.5.0", + "sonner": "^2.0.7", "tailwind-merge": "^3.3.1", "viem": "^2.37.5", "wagmi": "^2.16.9", diff --git a/src/actions/auth/signup/schema.ts b/src/actions/auth/signup/schema.ts index 208e79b..fbaed25 100644 --- a/src/actions/auth/signup/schema.ts +++ b/src/actions/auth/signup/schema.ts @@ -1,14 +1,22 @@ import { z } from "zod"; -export const signupSchema = z.object({ - name: z.string().min(1, 'Name is required'), - email: z.string().email('Invalid email format'), - password: z.string() - .min(8, 'Password must be at least 8 characters long') - .regex(/(?=.*[a-z])/, 'Password must contain at least one lowercase letter') - .regex(/(?=.*[A-Z])/, 'Password must contain at least one uppercase letter') - .regex(/(?=.*\d)/, 'Password must contain at least one number') - -}) +export const signupSchema = z + .object({ + name: z.string().min(1, 'Name is required'), + email: z.string().email('Invalid email format'), + password: z.string() + .min(8, 'Password must be at least 8 characters long') + .regex(/(?=.*[a-z])/, 'Password must contain at least one lowercase letter') + .regex(/(?=.*[A-Z])/, 'Password must contain at least one uppercase letter') + .regex(/(?=.*\d)/, 'Password must contain at least one number'), + confirmPassword: z.string().min(1, 'Please confirm your password'), + terms: z.literal(true, { + errorMap: () => ({ message: 'You must accept the terms' }) + }), + }) + .refine((data) => data.password === data.confirmPassword, { + message: 'Passwords do not match', + path: ['confirmPassword'], + }); export type signupInput = z.infer \ No newline at end of file diff --git a/src/app/(auth)/signin/page.tsx b/src/app/(auth)/signin/page.tsx index 208fb5b..bf85d52 100644 --- a/src/app/(auth)/signin/page.tsx +++ b/src/app/(auth)/signin/page.tsx @@ -1,143 +1,7 @@ "use client"; -import Link from "next/link"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { signinAction } from "@/actions/auth/signin/action"; -import { useForm } from "react-hook-form"; -import { useRouter } from "next/navigation"; - -type SignInFormValues = { - email: string; - password: string; - remember?: boolean; -}; +import SigninPageContainer from "@/components/pages/signin/SigninPageContainer"; export default function SignInPage() { - const router = useRouter(); - const { - register, - handleSubmit, - formState: { errors, isSubmitting }, - setError, - clearErrors, - } = useForm(); - - const onSubmit = async (data: SignInFormValues) => { - clearErrors("root"); - try { - await signinAction(data); - router.push("/"); - } catch (err) { - if (err instanceof Error) { - setError("root", { - type: "server", - message: err.message || "Something went wrong", - }); - } else { - setError("root", { type: "server", message: "Something went wrong" }); - } - } - }; - - return ( -

- - - - Sign in to Sentiopulse - - - Enter your credentials to access your account - - -
- -
- - - {errors.email && ( -

{errors.email.message}

- )} -
-
- - - {errors.password && ( -

- {errors.password.message} -

- )} -
-
-
- - -
- - Forgot password? - -
- {errors.root && ( -
- {errors.root.message} -
- )} -
- - -

- Don't have an account?{" "} - - Sign up - -

-
-
-
-
- ); + return ; } diff --git a/src/app/(auth)/signup/page.tsx b/src/app/(auth)/signup/page.tsx index 2eb192e..28e1598 100644 --- a/src/app/(auth)/signup/page.tsx +++ b/src/app/(auth)/signup/page.tsx @@ -1,191 +1,9 @@ -"use client"; +import SignupPageContainer from "@/components/pages/signup/SignupPageContainer"; -import Link from "next/link"; -import { useRouter } from "next/navigation"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { - Card, - CardContent, - CardFooter, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { signupAction } from "@/actions/auth/signup/action"; -import { useForm } from "react-hook-form"; - -type SignUpFormValues = { - name: string; - email: string; - password: string; - confirmPassword: string; - terms: boolean; -}; - -export default function SignUpPage() { - const { - register, - handleSubmit, - formState: { errors, isSubmitting }, - watch, - setError, - clearErrors, - } = useForm(); - const router = useRouter(); - - const password = watch("password"); - - const onSubmit = async (data: SignUpFormValues) => { - clearErrors("root"); - try { - await signupAction(data); - router.push("/"); - } catch (err) { - if (err instanceof Error) { - setError("root", { - type: "server", - message: err.message || "Something went wrong", - }); - } else { - setError("root", { type: "server", message: "Something went wrong" }); - } - } - }; - - return ( -
- - - - Create your account - - - -
- - {/* Full Name */} -
- - - {errors.name && ( -

{errors.name.message}

- )} -
- - {/* Email */} -
- - - {errors.email && ( -

{errors.email.message}

- )} -
- - {/* Password */} -
- - - {errors.password && ( -

- {errors.password.message} -

- )} -
- - {/* Confirm Password */} -
- - - value === password || "Passwords do not match", - })} - /> - {errors.confirmPassword && ( -

- {errors.confirmPassword.message} -

- )} -
- - {/* Terms */} -
- - -
- {errors.terms && ( -

{errors.terms.message}

- )} - - {/* Server Error */} - {errors.root && ( -
- {errors.root.message} -
- )} -
- - - -

- Already have an account?{" "} - - Sign in - -

-
-
-
+export default function Signup(){ + return( +
+
- ); -} + ) +} \ No newline at end of file diff --git a/src/components/pages/signin/SigninPageContainer.tsx b/src/components/pages/signin/SigninPageContainer.tsx new file mode 100644 index 0000000..a11e710 --- /dev/null +++ b/src/components/pages/signin/SigninPageContainer.tsx @@ -0,0 +1,116 @@ +"use client"; + +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { signinAction } from "@/actions/auth/signin/action"; +import { useForm } from "react-hook-form"; +import { useAction } from "next-safe-action/hooks"; +import { Form } from "@/components/ui/form"; +import FormInput from "@/components/shared/Form/FormInput/FormInput"; +import FormButton from "@/components/shared/Form/FormButton/FormButton"; +import { signinSchema } from "@/actions/auth/signin/schema"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { toast } from "sonner"; + +type SignInFormValues = z.infer; + +export default function SigninPageContainer() { + const form = useForm({ + mode: "onChange", + resolver: zodResolver(signinSchema), + }); + const router = useRouter(); + + const { execute, isExecuting } = useAction(signinAction, { + onSuccess: () => { + toast.success("Signed in successfully!"); + router.push("/"); + }, + onError: (error) => { + const fieldErrors = error.error.validationErrors?.fieldErrors; + const errorMessage = + error.error.serverError ?? + (fieldErrors + ? Object.entries(fieldErrors) + .map(([key, value]) => `${key}: ${value}`) + .join(", ") + : "An unknown error occurred"); + toast.error(errorMessage); + }, + }); + + return ( +
+ + + + Sign in to Sentiopulse + + + Enter your credentials to access your account + + + +
+ + + + + +
+ + Forgot password? + +
+
+ + + + Sign in + +

+ Don't have an account?{" "} + + Sign up + +

+
+
+ +
+
+ ); +} diff --git a/src/components/pages/signup/SignupPageContainer.tsx b/src/components/pages/signup/SignupPageContainer.tsx new file mode 100644 index 0000000..dc93ef3 --- /dev/null +++ b/src/components/pages/signup/SignupPageContainer.tsx @@ -0,0 +1,143 @@ +"use client"; + +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { + Card, + CardContent, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { signupAction } from "@/actions/auth/signup/action"; +import { useForm } from "react-hook-form"; +import { useAction } from "next-safe-action/hooks"; +import { Form } from "@/components/ui/form"; +import FormInput from "@/components/shared/Form/FormInput/FormInput"; +import FormCheckbox from "@/components/shared/Form/FormCheckbox/FormCheckbox"; +import FormButton from "@/components/shared/Form/FormButton/FormButton"; +import { signupSchema } from "@/actions/auth/signup/schema"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { toast } from "sonner"; + +type SignUpFormValues = z.infer; + +export default function SignupPageContainer() { + const form = useForm({ + mode: "onChange", + resolver: zodResolver(signupSchema), + }); + const router = useRouter(); + + const { execute, isExecuting } = useAction(signupAction, { + onSuccess: () => { + toast.success("Account created successfully!"); + form.reset(); + router.push("/"); + }, + onError: (error) => { + const fieldErrors = error.error.validationErrors?.fieldErrors; + const errorMessage = + error.error.serverError ?? + (fieldErrors + ? Object.entries(fieldErrors) + .map(([key, value]) => `${key}: ${value}`) + .join(", ") + : "An unknown error occurred"); + toast.error(errorMessage); + }, + }); + + return ( +
+ + + + Create your account + + + +
+ + + + + + + + I agree to the{" "} + + Terms of Service + {" "} + and{" "} + + Privacy Policy + + + } + required + /> + + + + + Create account + +

+ Already have an account?{" "} + + Sign in + +

+
+
+ +
+
+ ); +} diff --git a/src/components/shared/Form/FormButton/FormButton.tsx b/src/components/shared/Form/FormButton/FormButton.tsx new file mode 100644 index 0000000..f51e074 --- /dev/null +++ b/src/components/shared/Form/FormButton/FormButton.tsx @@ -0,0 +1,27 @@ +import { Button } from "@/components/ui/button"; + +type FormButtonProps = { + type?: "button" | "submit" | "reset"; + loading?: boolean; + disabled?: boolean; + className?: string; + children: React.ReactNode; +}; + +export default function FormButton({ + type = "submit", + loading = false, + disabled = false, + className = "", + children, +}: FormButtonProps) { + return ( + + ); +} \ No newline at end of file diff --git a/src/components/shared/Form/FormCheckbox/FormCheckbox.tsx b/src/components/shared/Form/FormCheckbox/FormCheckbox.tsx new file mode 100644 index 0000000..c683695 --- /dev/null +++ b/src/components/shared/Form/FormCheckbox/FormCheckbox.tsx @@ -0,0 +1,53 @@ +import { Control, FieldValues, Path } from "react-hook-form"; +import { + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; + +type FormCheckboxProps = { + control: Control; + name: Path; + label: React.ReactNode; + required?: boolean; +}; + +export function FormCheckbox({ + control, + name, + label, + required = false, +}: FormCheckboxProps) { + return ( + ( + +
+ + + + + {label} + {required && *} + +
+ +
+ )} + /> + ); +} + +export default FormCheckbox; diff --git a/src/components/shared/Form/FormInput/FormInput.tsx b/src/components/shared/Form/FormInput/FormInput.tsx new file mode 100644 index 0000000..4386e32 --- /dev/null +++ b/src/components/shared/Form/FormInput/FormInput.tsx @@ -0,0 +1,46 @@ +import { Control, FieldValues, Path } from "react-hook-form"; +import { Input } from "@/components/ui/input"; +import { FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; + +type FormInputProps = { + control: Control; + name: Path; + label: string; + placeholder?: string; + type?: string; + required?: boolean; +}; + +export function FormInput({ + control, + name, + label, + placeholder, + type = "text", + required = false, +}: FormInputProps) { + return ( + ( + + + {label} {required && *} + + + + + + + )} + /> + ); +} + +export default FormInput; \ No newline at end of file diff --git a/src/components/ui/form.tsx b/src/components/ui/form.tsx new file mode 100644 index 0000000..524b986 --- /dev/null +++ b/src/components/ui/form.tsx @@ -0,0 +1,167 @@ +"use client" + +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { Slot } from "@radix-ui/react-slot" +import { + Controller, + FormProvider, + useFormContext, + useFormState, + type ControllerProps, + type FieldPath, + type FieldValues, +} 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 = FieldPath, +> = { + name: TName +} + +const FormFieldContext = React.createContext( + {} as FormFieldContextValue +) + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +>({ + ...props +}: ControllerProps) => { + return ( + + + + ) +} + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext) + const itemContext = React.useContext(FormItemContext) + const { getFieldState } = useFormContext() + const formState = useFormState({ name: fieldContext.name }) + const fieldState = getFieldState(fieldContext.name, formState) + + if (!fieldContext) { + throw new Error("useFormField should be used within ") + } + + 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( + {} as FormItemContextValue +) + +function FormItem({ className, ...props }: React.ComponentProps<"div">) { + const id = React.useId() + + return ( + +
+ + ) +} + +function FormLabel({ + className, + ...props +}: React.ComponentProps) { + const { error, formItemId } = useFormField() + + return ( +