From 333429adbaaa592cc96b480a5228f0e3f1de4cc2 Mon Sep 17 00:00:00 2001 From: MohamedBassem Date: Thu, 8 Feb 2024 03:25:43 +0000 Subject: [refactor] Use react form hock for the AddLink --- bun.lockb | Bin 270624 -> 90744 bytes web/app/dashboard/bookmarks/components/AddLink.tsx | 70 +++++--- web/components/ui/form.tsx | 176 +++++++++++++++++++++ web/components/ui/label.tsx | 26 +++ web/package.json | 3 + 5 files changed, 252 insertions(+), 23 deletions(-) create mode 100644 web/components/ui/form.tsx create mode 100644 web/components/ui/label.tsx diff --git a/bun.lockb b/bun.lockb index 1ea151a9..b8f6722b 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/web/app/dashboard/bookmarks/components/AddLink.tsx b/web/app/dashboard/bookmarks/components/AddLink.tsx index fab4db8b..fb77786c 100644 --- a/web/app/dashboard/bookmarks/components/AddLink.tsx +++ b/web/app/dashboard/bookmarks/components/AddLink.tsx @@ -1,43 +1,67 @@ "use client"; import { Button } from "@/components/ui/button"; +import { Form, FormControl, FormField, FormItem } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import APIClient from "@/lib/api"; import { Plus } from "lucide-react"; import { useRouter } from "next/navigation"; -import { useState } from "react"; +import { useForm, SubmitErrorHandler } from "react-hook-form"; +import { z } from "zod"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { toast } from "@/components/ui/use-toast"; + +const formSchema = z.object({ + url: z.string().url({ message: "The link must be a valid URL" }), +}); export default function AddLink() { const router = useRouter(); - const [link, setLink] = useState(""); - const bookmarkLink = async () => { - const [_resp, error] = await APIClient.bookmarkLink(link); + const form = useForm>({ + resolver: zodResolver(formSchema), + }); + + async function onSubmit(value: z.infer) { + const [_resp, error] = await APIClient.bookmarkLink(value.url); if (error) { - // TODO: Proper error handling - alert(error.message); + toast({ description: error.message, variant: "destructive" }); return; } router.refresh(); + } + + const onError: SubmitErrorHandler> = (errors) => { + toast({ + description: Object.values(errors) + .map((v) => v.message) + .join("\n"), + variant: "destructive", + }); }; return ( -
- setLink(val.target.value)} - onKeyUp={async (event) => { - if (event.key == "Enter") { - bookmarkLink(); - setLink(""); - } - }} - /> - -
+
+ +
+ { + return ( + + + + + + ); + }} + /> + +
+
+ ); } diff --git a/web/components/ui/form.tsx b/web/components/ui/form.tsx new file mode 100644 index 00000000..4603f8b3 --- /dev/null +++ b/web/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 ( +