From 3dc62beffe27ad1552d905f460baa66ac02afb36 Mon Sep 17 00:00:00 2001 From: Aryan Prince Date: Mon, 4 Mar 2024 05:26:27 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=92=A9=20chore=20(library):=20Added=20new?= =?UTF-8?q?=20signup=20component=20using=20react-hook-form?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/library/package.json | 2 + apps/library/src/app/(auth)/signup/page.tsx | 3 +- .../signup/signup-form-experimental.tsx | 182 ++++++++++++++++++ apps/library/src/components/ui/form.tsx | 176 +++++++++++++++++ pnpm-lock.yaml | 6 + 5 files changed, 368 insertions(+), 1 deletion(-) create mode 100644 apps/library/src/app/(auth)/signup/signup-form-experimental.tsx create mode 100644 apps/library/src/components/ui/form.tsx diff --git a/apps/library/package.json b/apps/library/package.json index e99f9be..296da56 100644 --- a/apps/library/package.json +++ b/apps/library/package.json @@ -22,6 +22,7 @@ "db:reset": "pnpm db:migrate && pnpm db:seed" }, "dependencies": { + "@hookform/resolvers": "^3.3.4", "@lucia-auth/adapter-drizzle": "^1.0.2", "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-slot": "^1.0.2", @@ -43,6 +44,7 @@ "postgres": "^3.4.3", "react": "18.2.0", "react-dom": "18.2.0", + "react-hook-form": "^7.50.1", "server-only": "^0.0.1", "superjson": "^2.2.1", "tailwind-merge": "^2.2.1", diff --git a/apps/library/src/app/(auth)/signup/page.tsx b/apps/library/src/app/(auth)/signup/page.tsx index 2ed6607..029176f 100644 --- a/apps/library/src/app/(auth)/signup/page.tsx +++ b/apps/library/src/app/(auth)/signup/page.tsx @@ -2,7 +2,7 @@ import Image from "next/image"; import SignupForm from "./signup-form"; -export default async function Page() { +export default async function SignupPage() { return ( <>
@@ -21,6 +21,7 @@ export default async function Page() {

+ {/* */} ); diff --git a/apps/library/src/app/(auth)/signup/signup-form-experimental.tsx b/apps/library/src/app/(auth)/signup/signup-form-experimental.tsx new file mode 100644 index 0000000..fd5a62b --- /dev/null +++ b/apps/library/src/app/(auth)/signup/signup-form-experimental.tsx @@ -0,0 +1,182 @@ +"use client"; + +import { useState } from "react"; +import { cookies } from "next/headers"; +import { redirect } from "next/navigation"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { generateId, Scrypt } from "lucia"; +import { Loader2 } from "lucide-react"; +import { useFormStatus } from "react-dom"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; + +import { Button } from "~/components/ui/button"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "~/components/ui/form"; +import { Input } from "~/components/ui/input"; +import { lucia } from "~/server/auth"; +import { db } from "~/server/db/index"; +import { user } from "~/server/db/schema"; + +const formSchema = z.object({ + username: z.string().min(8, "Username too short").max(50), + password: z.string().min(8, "Password too short").max(100), +}); + +export type FormFields = z.infer; + +export default function SignupFormUpdated() { + const [errors, setErrors] = useState(""); + + // 1. Define your form. + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + username: "", + password: "", + }, + }); + + // 2. Define a submit handler. + // function onSubmit(values: z.infer) { + // // Do something with the form values. + // // ✅ This will be type-safe and validated. + // console.log(values); + // } + + // const [formState, formAction] = useFormState(signup, null); + + async function handleFormAction(data: FormFields) { + const { error } = await signup(data); + if (error) { + setErrors(error); + } + console.log(error); + } + + return ( +
+ handleFormAction(data))} + className="space-y-8" + > + ( + + Username + + + + {errors && {errors}} + + )} + /> + ( + + Password + + + + + + )} + /> + + + + ); +} + +function SubmitButton() { + const { pending } = useFormStatus(); + + return ( + + ); +} + +interface ActionResult { + error: string; +} + +/** + * Creates a user, and then sets a new cookie for the user. + * @returns An error message, if any + */ +export async function signup( + formData: FormFields, + _initialState?: unknown, +): Promise { + "use server"; + console.log("Signing up..."); + + const username = formData.username; + // username must be between 4 ~ 31 characters, and only consists of lowercase letters, 0-9, -, and _ + // keep in mind some database (e.g. mysql) are case insensitive + // if ( + // typeof username !== "string" || + // username.length < 3 || + // username.length > 31 || + // !/^[a-z0-9_-]+$/.test(username) + // ) { + // return { + // error: "Invalid username", + // }; + // } + const password = formData.password; + // if ( + // typeof password !== "string" || + // password.length < 6 || + // password.length > 255 + // ) { + // return { + // error: "Invalid password", + // }; + // } + + const hashedPassword = await new Scrypt().hash(password); + const userId = generateId(15); + + // TODO: check if username is already used + + try { + await db.insert(user).values({ + id: userId, + username: username, + hashedPassword: hashedPassword, + }); + } catch (e) { + console.log("Error adding to DB, user already exists...", e); + return { + error: "User already exists", + }; + } + + console.log("Added to DB..."); + + const session = await lucia.createSession(userId, {}); + const sessionCookie = lucia.createSessionCookie(session.id); + cookies().set( + sessionCookie.name, + sessionCookie.value, + sessionCookie.attributes, + ); + + console.log("Finished signing up..."); + + return redirect("/protected"); +} diff --git a/apps/library/src/components/ui/form.tsx b/apps/library/src/components/ui/form.tsx new file mode 100644 index 0000000..fa5ebc1 --- /dev/null +++ b/apps/library/src/components/ui/form.tsx @@ -0,0 +1,176 @@ +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 = 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 ( +