diff --git a/apps/cli/src/helpers/auth-setup.ts b/apps/cli/src/helpers/auth-setup.ts index 445a39a..0b8bce6 100644 --- a/apps/cli/src/helpers/auth-setup.ts +++ b/apps/cli/src/helpers/auth-setup.ts @@ -17,9 +17,17 @@ export async function configureAuth( try { if (!enableAuth) { await fs.remove(path.join(clientDir, "src/components/sign-up-form.tsx")); + await fs.remove(path.join(clientDir, "src/components/sign-in-form.tsx")); + await fs.remove(path.join(clientDir, "src/components/auth-forms.tsx")); await fs.remove(path.join(clientDir, "src/components/user-menu.tsx")); await fs.remove(path.join(clientDir, "src/lib/auth-client.ts")); - await fs.remove(path.join(clientDir, "src/lib/schemas.ts")); + + const indexRoutePath = path.join(clientDir, "src/routes/index.tsx"); + const indexRouteContent = await fs.readFile(indexRoutePath, "utf8"); + const updatedIndexRouteContent = indexRouteContent + .replace(/import AuthForms from "@\/components\/auth-forms";\n/, "") + .replace(//, ""); + await fs.writeFile(indexRoutePath, updatedIndexRouteContent, "utf8"); await fs.remove(path.join(serverDir, "src/lib/auth.ts")); diff --git a/apps/cli/template/base/packages/client/package.json b/apps/cli/template/base/packages/client/package.json index 1829250..140876a 100644 --- a/apps/cli/template/base/packages/client/package.json +++ b/apps/cli/template/base/packages/client/package.json @@ -28,6 +28,7 @@ "@radix-ui/react-label": "^2.1.2", "@radix-ui/react-slot": "^1.1.2", "@tailwindcss/vite": "^4.0.5", + "@tanstack/react-form": "^1.0.5", "@tanstack/react-query": "^5.66.0", "@tanstack/react-query-devtools": "^5.66.0", "@tanstack/react-router": "^1.101.0", @@ -42,7 +43,6 @@ "next-themes": "^0.4.4", "react": "^19.0.0", "react-dom": "^19.0.0", - "react-hook-form": "^7.54.2", "sonner": "^1.7.4", "tailwind-merge": "^2.6.0", "tailwindcss-animate": "^1.0.7", diff --git a/apps/cli/template/base/packages/client/src/components/auth-forms.tsx b/apps/cli/template/base/packages/client/src/components/auth-forms.tsx new file mode 100644 index 0000000..3ba200a --- /dev/null +++ b/apps/cli/template/base/packages/client/src/components/auth-forms.tsx @@ -0,0 +1,13 @@ +import { useState } from "react"; +import SignInForm from "./sign-in-form"; +import SignUpForm from "./sign-up-form"; + +export default function AuthForms() { + const [showSignIn, setShowSignIn] = useState(false); + + return showSignIn ? ( + setShowSignIn(false)} /> + ) : ( + setShowSignIn(true)} /> + ); +} diff --git a/apps/cli/template/base/packages/client/src/components/sign-in-form.tsx b/apps/cli/template/base/packages/client/src/components/sign-in-form.tsx new file mode 100644 index 0000000..6bae864 --- /dev/null +++ b/apps/cli/template/base/packages/client/src/components/sign-in-form.tsx @@ -0,0 +1,141 @@ +import { authClient } from "@/lib/auth-client"; +import { useForm } from "@tanstack/react-form"; +import { useNavigate } from "@tanstack/react-router"; +import { toast } from "sonner"; +import { z } from "zod"; +import Loader from "./loader"; +import { Button } from "./ui/button"; +import { Input } from "./ui/input"; +import { Label } from "./ui/label"; + +export default function SignInForm({ + onSwitchToSignUp, +}: { + onSwitchToSignUp: () => void; +}) { + const navigate = useNavigate({ + from: "/", + }); + const { isPending } = authClient.useSession(); + + const form = useForm({ + defaultValues: { + email: "", + password: "", + }, + onSubmit: async ({ value }) => { + await authClient.signIn.email( + { + email: value.email, + password: value.password, + }, + { + onSuccess: () => { + toast.success("Sign in successful"); + navigate({ + to: "/dashboard", + }); + }, + onError: (error) => { + toast.error(error.error.message); + }, + }, + ); + }, + validators: { + onSubmit: z.object({ + email: z.string().email("Invalid email address"), + password: z.string().min(6, "Password must be at least 6 characters"), + }), + }, + }); + + if (isPending) { + return ; + } + + return ( +
+

Welcome Back

+ +
{ + e.preventDefault(); + e.stopPropagation(); + void form.handleSubmit(); + }} + className="space-y-4" + > +
+ ( +
+ + field.handleChange(e.target.value)} + /> + {field.state.meta.errors.map((error) => ( +

+ {error?.message} +

+ ))} +
+ )} + /> +
+ +
+ ( +
+ + field.handleChange(e.target.value)} + /> + {field.state.meta.errors.map((error) => ( +

+ {error?.message} +

+ ))} +
+ )} + /> +
+ + + {(state) => ( + + )} + +
+ +
+ +
+
+ ); +} diff --git a/apps/cli/template/base/packages/client/src/components/sign-up-form.tsx b/apps/cli/template/base/packages/client/src/components/sign-up-form.tsx index bd84cde..08f7b98 100644 --- a/apps/cli/template/base/packages/client/src/components/sign-up-form.tsx +++ b/apps/cli/template/base/packages/client/src/components/sign-up-form.tsx @@ -1,157 +1,164 @@ import { authClient } from "@/lib/auth-client"; -import { signInSchema, signUpSchema } from "@/lib/schemas"; -import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "@tanstack/react-form"; import { useNavigate } from "@tanstack/react-router"; -import { useState } from "react"; -import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; import Loader from "./loader"; import { Button } from "./ui/button"; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "./ui/form"; import { Input } from "./ui/input"; +import { Label } from "./ui/label"; -export default function AuthForm() { - const navigate = useNavigate({ - from: "/", - }); - const [isSignUp, setIsSignUp] = useState(false); - const { isPending } = authClient.useSession(); +export default function SignUpForm({ + onSwitchToSignIn, +}: { + onSwitchToSignIn: () => void; +}) { + const navigate = useNavigate({ + from: "/", + }); + const { isPending } = authClient.useSession(); - const form = useForm>({ - resolver: zodResolver(isSignUp ? signUpSchema : signInSchema), - defaultValues: { - email: "", - password: "", - name: "", - }, - }); + const form = useForm({ + defaultValues: { + email: "", + password: "", + name: "", + }, + onSubmit: async ({ value }) => { + await authClient.signUp.email( + { + email: value.email, + password: value.password, + name: value.name, + }, + { + onSuccess: () => { + toast.success("Sign up successful"); + navigate({ + to: "/dashboard", + }); + }, + onError: (error) => { + toast.error(error.error.message); + }, + }, + ); + }, + validators: { + onSubmit: z.object({ + name: z.string().min(2, "Name must be at least 2 characters"), + email: z.string().email("Invalid email address"), + password: z.string().min(6, "Password must be at least 6 characters"), + }), + }, + }); - const onSubmit = async (values: z.infer) => { - if (isSignUp) { - await authClient.signUp.email( - { - email: values.email, - password: values.password, - name: values.name, - }, - { - onSuccess: () => { - toast.success("Sign up successful"); - navigate({ - to: "/dashboard", - }); - }, - onError: (ctx) => { - form.setError("email", { - type: "manual", - message: ctx.error.message, - }); - }, - }, - ); - } else { - await authClient.signIn.email( - { - email: values.email, - password: values.password, - }, - { - onSuccess: () => { - toast.success("Sign in successful"); - navigate({ - to: "/dashboard", - }); - }, - onError: (ctx) => { - form.setError("email", { - type: "manual", - message: ctx.error.message, - }); - }, - }, - ); - } - }; + if (isPending) { + return ; + } - if (isPending) { - return ; - } + return ( +
+

Create Account

- return ( -
-

- {isSignUp ? "Create Account" : "Welcome Back"} -

-
- - {isSignUp && ( - ( - - Name - - - - - - )} - /> - )} - ( - - Email - - - - - - )} - /> - ( - - Password - - - - - - )} - /> - - - -
- -
-
- ); +
{ + e.preventDefault(); + e.stopPropagation(); + void form.handleSubmit(); + }} + className="space-y-4" + > +
+ + {(field) => ( +
+ + field.handleChange(e.target.value)} + /> + {field.state.meta.errors.map((error) => ( +

+ {error?.message} +

+ ))} +
+ )} +
+
+ +
+ + {(field) => ( +
+ + field.handleChange(e.target.value)} + /> + {field.state.meta.errors.map((error) => ( +

+ {error?.message} +

+ ))} +
+ )} +
+
+ +
+ + {(field) => ( +
+ + field.handleChange(e.target.value)} + /> + {field.state.meta.errors.map((error) => ( +

+ {error?.message} +

+ ))} +
+ )} +
+
+ + + {(state) => ( + + )} + +
+ +
+ +
+
+ ); } diff --git a/apps/cli/template/base/packages/client/src/components/ui/form.tsx b/apps/cli/template/base/packages/client/src/components/ui/form.tsx deleted file mode 100644 index 508e642..0000000 --- a/apps/cli/template/base/packages/client/src/components/ui/form.tsx +++ /dev/null @@ -1,179 +0,0 @@ -"use client"; - -import * as React from "react"; -import * as LabelPrimitive from "@radix-ui/react-label"; -import { Slot } from "@radix-ui/react-slot"; -import { - Controller, - type ControllerProps, - type FieldPath, - type 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 = 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, formState } = useFormContext(); - - 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, -); - -const FormItem = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => { - const id = React.useId(); - - return ( - -
- - ); -}); -FormItem.displayName = "FormItem"; - -const FormLabel = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => { - const { error, formItemId } = useFormField(); - - return ( -
); }