-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
💩 chore (library): Added new signup component using react-hook-form
- Loading branch information
1 parent
3faf54d
commit 3dc62be
Showing
5 changed files
with
368 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
182 changes: 182 additions & 0 deletions
182
apps/library/src/app/(auth)/signup/signup-form-experimental.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<typeof formSchema>; | ||
|
||
export default function SignupFormUpdated() { | ||
const [errors, setErrors] = useState(""); | ||
|
||
// 1. Define your form. | ||
const form = useForm<FormFields>({ | ||
resolver: zodResolver(formSchema), | ||
defaultValues: { | ||
username: "", | ||
password: "", | ||
}, | ||
}); | ||
|
||
// 2. Define a submit handler. | ||
// function onSubmit(values: z.infer<typeof formSchema>) { | ||
// // 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 ( | ||
<Form {...form}> | ||
<form | ||
onSubmit={form.handleSubmit((data) => handleFormAction(data))} | ||
className="space-y-8" | ||
> | ||
<FormField | ||
control={form.control} | ||
name="username" | ||
render={({ field }) => ( | ||
<FormItem> | ||
<FormLabel>Username</FormLabel> | ||
<FormControl> | ||
<Input placeholder="Enter username" {...field} /> | ||
</FormControl> | ||
{errors && <FormMessage>{errors}</FormMessage>} | ||
</FormItem> | ||
)} | ||
/> | ||
<FormField | ||
control={form.control} | ||
name="password" | ||
render={({ field }) => ( | ||
<FormItem> | ||
<FormLabel>Password</FormLabel> | ||
<FormControl> | ||
<Input placeholder="Enter password" {...field} /> | ||
</FormControl> | ||
<FormMessage /> | ||
</FormItem> | ||
)} | ||
/> | ||
<SubmitButton /> | ||
</form> | ||
</Form> | ||
); | ||
} | ||
|
||
function SubmitButton() { | ||
const { pending } = useFormStatus(); | ||
|
||
return ( | ||
<Button disabled={pending} type="submit"> | ||
{pending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} | ||
Sign up | ||
</Button> | ||
); | ||
} | ||
|
||
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<ActionResult> { | ||
"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"); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<TFieldValues> = FieldPath<TFieldValues> | ||
> = { | ||
name: TName | ||
} | ||
|
||
const FormFieldContext = React.createContext<FormFieldContextValue>( | ||
{} as FormFieldContextValue | ||
) | ||
|
||
const FormField = < | ||
TFieldValues extends FieldValues = FieldValues, | ||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues> | ||
>({ | ||
...props | ||
}: ControllerProps<TFieldValues, TName>) => { | ||
return ( | ||
<FormFieldContext.Provider value={{ name: props.name }}> | ||
<Controller {...props} /> | ||
</FormFieldContext.Provider> | ||
) | ||
} | ||
|
||
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 <FormField>") | ||
} | ||
|
||
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<FormItemContextValue>( | ||
{} as FormItemContextValue | ||
) | ||
|
||
const FormItem = React.forwardRef< | ||
HTMLDivElement, | ||
React.HTMLAttributes<HTMLDivElement> | ||
>(({ className, ...props }, ref) => { | ||
const id = React.useId() | ||
|
||
return ( | ||
<FormItemContext.Provider value={{ id }}> | ||
<div ref={ref} className={cn("space-y-2", className)} {...props} /> | ||
</FormItemContext.Provider> | ||
) | ||
}) | ||
FormItem.displayName = "FormItem" | ||
|
||
const FormLabel = React.forwardRef< | ||
React.ElementRef<typeof LabelPrimitive.Root>, | ||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> | ||
>(({ className, ...props }, ref) => { | ||
const { error, formItemId } = useFormField() | ||
|
||
return ( | ||
<Label | ||
ref={ref} | ||
className={cn(error && "text-destructive", className)} | ||
htmlFor={formItemId} | ||
{...props} | ||
/> | ||
) | ||
}) | ||
FormLabel.displayName = "FormLabel" | ||
|
||
const FormControl = React.forwardRef< | ||
React.ElementRef<typeof Slot>, | ||
React.ComponentPropsWithoutRef<typeof Slot> | ||
>(({ ...props }, ref) => { | ||
const { error, formItemId, formDescriptionId, formMessageId } = useFormField() | ||
|
||
return ( | ||
<Slot | ||
ref={ref} | ||
id={formItemId} | ||
aria-describedby={ | ||
!error | ||
? `${formDescriptionId}` | ||
: `${formDescriptionId} ${formMessageId}` | ||
} | ||
aria-invalid={!!error} | ||
{...props} | ||
/> | ||
) | ||
}) | ||
FormControl.displayName = "FormControl" | ||
|
||
const FormDescription = React.forwardRef< | ||
HTMLParagraphElement, | ||
React.HTMLAttributes<HTMLParagraphElement> | ||
>(({ className, ...props }, ref) => { | ||
const { formDescriptionId } = useFormField() | ||
|
||
return ( | ||
<p | ||
ref={ref} | ||
id={formDescriptionId} | ||
className={cn("text-sm text-muted-foreground", className)} | ||
{...props} | ||
/> | ||
) | ||
}) | ||
FormDescription.displayName = "FormDescription" | ||
|
||
const FormMessage = React.forwardRef< | ||
HTMLParagraphElement, | ||
React.HTMLAttributes<HTMLParagraphElement> | ||
>(({ className, children, ...props }, ref) => { | ||
const { error, formMessageId } = useFormField() | ||
const body = error ? String(error?.message) : children | ||
|
||
if (!body) { | ||
return null | ||
} | ||
|
||
return ( | ||
<p | ||
ref={ref} | ||
id={formMessageId} | ||
className={cn("text-sm font-medium text-destructive", className)} | ||
{...props} | ||
> | ||
{body} | ||
</p> | ||
) | ||
}) | ||
FormMessage.displayName = "FormMessage" | ||
|
||
export { | ||
useFormField, | ||
Form, | ||
FormItem, | ||
FormLabel, | ||
FormControl, | ||
FormDescription, | ||
FormMessage, | ||
FormField, | ||
} |
Oops, something went wrong.