From 74112997299622ebe791d7e32093e09901cd5150 Mon Sep 17 00:00:00 2001 From: alihamasdev Date: Thu, 3 Jul 2025 17:27:57 +0500 Subject: [PATCH 1/3] feat: add new component 'comp-577' for file upload with shadcn form and zod validation and update the registry to add component --- config/components.ts | 1 + package.json | 8 +- registry.json | 19 +++ registry/default/components/comp-577.tsx | 177 +++++++++++++++++++++++ registry/default/ui/form.tsx | 167 +++++++++++++++++++++ 5 files changed, 369 insertions(+), 3 deletions(-) create mode 100644 registry/default/components/comp-577.tsx create mode 100644 registry/default/ui/form.tsx diff --git a/config/components.ts b/config/components.ts index 9f7798119..9a8066abb 100644 --- a/config/components.ts +++ b/config/components.ts @@ -333,6 +333,7 @@ export const categories: ComponentCategory[] = [ { name: "comp-552" }, { name: "comp-553" }, { name: "comp-554" }, + { name: "comp-577" }, ], }, { diff --git a/package.json b/package.json index 0b5c376b9..4f1eb9b3e 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@dnd-kit/utilities": "^3.2.2", "@headless-tree/core": "^1.0.0", "@headless-tree/react": "^1.0.0", + "@hookform/resolvers": "^5.1.1", "@internationalized/date": "^3.7.0", "@origin-space/image-cropper": "^0.1.8", "@radix-ui/react-accordion": "^1.2.3", @@ -31,14 +32,14 @@ "@radix-ui/react-dropdown-menu": "^2.1.6", "@radix-ui/react-hover-card": "^1.1.6", "@radix-ui/react-icons": "^1.3.2", - "@radix-ui/react-label": "^2.1.2", + "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-popover": "^1.1.6", "@radix-ui/react-progress": "^1.1.3", "@radix-ui/react-radio-group": "^1.2.3", "@radix-ui/react-scroll-area": "^1.2.3", "@radix-ui/react-select": "^2.1.6", "@radix-ui/react-slider": "^1.2.3", - "@radix-ui/react-slot": "^1.1.2", + "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-switch": "^1.1.3", "@radix-ui/react-tabs": "^1.1.3", "@radix-ui/react-toast": "^1.2.6", @@ -64,6 +65,7 @@ "react-aria-components": "^1.7.1", "react-day-picker": "^9.6.4", "react-dom": "^19.1.0", + "react-hook-form": "^7.59.0", "react-intersection-observer": "^9.16.0", "react-payment-inputs": "^1.2.0", "react-phone-number-input": "^3.4.12", @@ -74,7 +76,7 @@ "ts-morph": "^25.0.1", "tsx": "^4.19.3", "use-mask-input": "^3.4.2", - "zod": "^3.24.2" + "zod": "^3.25.71" }, "devDependencies": { "@ianvs/prettier-plugin-sort-imports": "^4.4.1", diff --git a/registry.json b/registry.json index 4bf16f95e..54ef1bfd9 100644 --- a/registry.json +++ b/registry.json @@ -10370,6 +10370,25 @@ "meta": { "tags": ["tree", "menu"] } + }, + { + "name": "comp-577", + "type": "registry:component", + "registryDependencies": ["https://originui.com/r/button.json", "https://originui.com/r/button.json"], + "files": [ + { + "path": "registry/default/components/comp-577.tsx", + "type": "registry:component" + }, + { + "path": "registry/default/hooks/use-file-upload.ts", + "type": "registry:hook" + } + ], + "meta": { + "tags": ["upload", "file", "image", "drag and drop", "shadcn form", "zod file"], + "colSpan": 3 + } } ] } \ No newline at end of file diff --git a/registry/default/components/comp-577.tsx b/registry/default/components/comp-577.tsx new file mode 100644 index 000000000..af5699a2a --- /dev/null +++ b/registry/default/components/comp-577.tsx @@ -0,0 +1,177 @@ +"use client" + +import { useEffect } from "react" +import { zodResolver } from "@hookform/resolvers/zod" +import { ImageIcon, UploadIcon, XIcon } from "lucide-react" +import { useForm } from "react-hook-form" +import { toast } from "sonner" +import { z } from "zod/v4" + +import { useFileUpload } from "@/registry/default/hooks/use-file-upload" +import { Button } from "@/registry/default/ui/button" +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/registry/default/ui/form" + +const maxSizeMB = 2 +const maxSize = maxSizeMB * 1024 * 1024 // 2MB default +const imageTypes = "image/svg+xml,image/png,image/jpeg,image/jpg,image/gif" + +const formSchema = z.object({ + image: z + .file() + .max(maxSize, { error: `Image can only be max size of ${maxSizeMB}MB` }) + .mime(imageTypes.split(",")) + .nullable(), +}) + +export default function Component() { + const [ + { files, isDragging }, + { + handleDragEnter, + handleDragLeave, + handleDragOver, + handleDrop, + openFileDialog, + removeFile, + getInputProps, + }, + ] = useFileUpload({ accept: imageTypes }) + + const previewUrl = files[0]?.preview || null + + useEffect(() => { + const imageFile = files[0] ? files[0].file : null + form.setValue("image", imageFile as File) + }, [files]) + + const form = useForm>({ + resolver: zodResolver(formSchema), + mode: "onChange", + defaultValues: { + image: null, + }, + }) + + function onSubmit({ image }: z.infer) { + image + ? toast(`You submitted the image of name "${image.name}"`) + : toast("You did not sumbit the form with value `null`") + } + + return ( +
+
+ + ( + + Avatar +
+ + + + {previewUrl ? ( +
+ {files[0]?.file?.name +
+ ) : ( +
+ +

+ Drop your image here +

+

+ SVG, PNG, JPG or GIF (max. {maxSizeMB}MB) +

+ +
+ )} + + {previewUrl && ( +
+ +
+ )} +
+
+
+ +
+ +
+
+ )} + /> + + + +

+ Image uploader with shadcn form ∙ (by{" "} + + Ali Hamas + + ) +

+
+ ) +} diff --git a/registry/default/ui/form.tsx b/registry/default/ui/form.tsx new file mode 100644 index 000000000..8eae02a21 --- /dev/null +++ b/registry/default/ui/form.tsx @@ -0,0 +1,167 @@ +"use client" + +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { Slot } from "@radix-ui/react-slot" +import { + Controller, + FormProvider, + useFormContext, + useFormState, + type ControllerProps, + type FieldPath, + type FieldValues, +} from "react-hook-form" + +import { cn } from "@/registry/default/lib/utils" +import { Label } from "@/registry/default/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 } = useFormContext() + const formState = useFormState({ name: fieldContext.name }) + 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 +) + +function FormItem({ className, ...props }: React.ComponentProps<"div">) { + const id = React.useId() + + return ( + +
+ + ) +} + +function FormLabel({ + className, + ...props +}: React.ComponentProps) { + const { error, formItemId } = useFormField() + + return ( +