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 (
+
+
+ );
+}
+
+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 (
+
+ )
+})
+FormLabel.displayName = "FormLabel"
+
+const FormControl = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ ...props }, ref) => {
+ const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
+
+ return (
+
+ )
+})
+FormControl.displayName = "FormControl"
+
+const FormDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const { formDescriptionId } = useFormField()
+
+ return (
+
+ )
+})
+FormDescription.displayName = "FormDescription"
+
+const FormMessage = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, children, ...props }, ref) => {
+ const { error, formMessageId } = useFormField()
+ const body = error ? String(error?.message) : children
+
+ if (!body) {
+ return null
+ }
+
+ return (
+
+ {body}
+
+ )
+})
+FormMessage.displayName = "FormMessage"
+
+export {
+ useFormField,
+ Form,
+ FormItem,
+ FormLabel,
+ FormControl,
+ FormDescription,
+ FormMessage,
+ FormField,
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index a477547..175d57b 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -172,6 +172,9 @@ importers:
apps/library:
dependencies:
+ '@hookform/resolvers':
+ specifier: ^3.3.4
+ version: 3.3.4(react-hook-form@7.50.1)
'@lucia-auth/adapter-drizzle':
specifier: ^1.0.2
version: 1.0.2(lucia@3.0.1)
@@ -235,6 +238,9 @@ importers:
react-dom:
specifier: 18.2.0
version: 18.2.0(react@18.2.0)
+ react-hook-form:
+ specifier: ^7.50.1
+ version: 7.50.1(react@18.2.0)
server-only:
specifier: ^0.0.1
version: 0.0.1