Repository: ZenkoPc/testLanding
Pull request #4: Create basic dashboard UI
Author: login: sbetav
Created: 2025-04-07T04:21:35Z | Merged: 2025-04-07T04:56:21Z



Diff:
diff --git a/package-lock.json b/package-lock.json
index 4a8061d..21c2fa3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,6 +12,7 @@
         "@hookform/resolvers": "^5.0.1",
         "@prisma/client": "^6.5.0",
         "@radix-ui/react-dialog": "^1.1.6",
+        "@radix-ui/react-dropdown-menu": "^2.1.6",
         "@radix-ui/react-label": "^2.1.2",
         "@radix-ui/react-separator": "^1.1.2",
         "@radix-ui/react-slot": "^1.1.2",
@@ -1264,6 +1265,32 @@
         }
       }
     },
+    "node_modules/@radix-ui/react-collection": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.2.tgz",
+      "integrity": "sha512-9z54IEKRxIa9VityapoEYMuByaG42iSy1ZXlY2KcuLSEtq8x4987/N6m15ppoMffgZX72gER2uHe1D9Y6Unlcw==",
+      "license": "MIT",
+      "dependencies": {
+        "@radix-ui/react-compose-refs": "1.1.1",
+        "@radix-ui/react-context": "1.1.1",
+        "@radix-ui/react-primitive": "2.0.2",
+        "@radix-ui/react-slot": "1.1.2"
+      },
+      "peerDependencies": {
+        "@types/react": "*",
+        "@types/react-dom": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+        "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        },
+        "@types/react-dom": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/@radix-ui/react-compose-refs": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz",
@@ -1330,6 +1357,21 @@
         }
       }
     },
+    "node_modules/@radix-ui/react-direction": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz",
+      "integrity": "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==",
+      "license": "MIT",
+      "peerDependencies": {
+        "@types/react": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/@radix-ui/react-dismissable-layer": {
       "version": "1.1.5",
       "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.5.tgz",
@@ -1357,6 +1399,35 @@
         }
       }
     },
+    "node_modules/@radix-ui/react-dropdown-menu": {
+      "version": "2.1.6",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.6.tgz",
+      "integrity": "sha512-no3X7V5fD487wab/ZYSHXq3H37u4NVeLDKI/Ks724X/eEFSSEFYZxWgsIlr1UBeEyDaM29HM5x9p1Nv8DuTYPA==",
+      "license": "MIT",
+      "dependencies": {
+        "@radix-ui/primitive": "1.1.1",
+        "@radix-ui/react-compose-refs": "1.1.1",
+        "@radix-ui/react-context": "1.1.1",
+        "@radix-ui/react-id": "1.1.0",
+        "@radix-ui/react-menu": "2.1.6",
+        "@radix-ui/react-primitive": "2.0.2",
+        "@radix-ui/react-use-controllable-state": "1.1.0"
+      },
+      "peerDependencies": {
+        "@types/react": "*",
+        "@types/react-dom": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+        "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        },
+        "@types/react-dom": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/@radix-ui/react-focus-guards": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz",
@@ -1438,6 +1509,46 @@
         }
       }
     },
+    "node_modules/@radix-ui/react-menu": {
+      "version": "2.1.6",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.6.tgz",
+      "integrity": "sha512-tBBb5CXDJW3t2mo9WlO7r6GTmWV0F0uzHZVFmlRmYpiSK1CDU5IKojP1pm7oknpBOrFZx/YgBRW9oorPO2S/Lg==",
+      "license": "MIT",
+      "dependencies": {
+        "@radix-ui/primitive": "1.1.1",
+        "@radix-ui/react-collection": "1.1.2",
+        "@radix-ui/react-compose-refs": "1.1.1",
+        "@radix-ui/react-context": "1.1.1",
+        "@radix-ui/react-direction": "1.1.0",
+        "@radix-ui/react-dismissable-layer": "1.1.5",
+        "@radix-ui/react-focus-guards": "1.1.1",
+        "@radix-ui/react-focus-scope": "1.1.2",
+        "@radix-ui/react-id": "1.1.0",
+        "@radix-ui/react-popper": "1.2.2",
+        "@radix-ui/react-portal": "1.1.4",
+        "@radix-ui/react-presence": "1.1.2",
+        "@radix-ui/react-primitive": "2.0.2",
+        "@radix-ui/react-roving-focus": "1.1.2",
+        "@radix-ui/react-slot": "1.1.2",
+        "@radix-ui/react-use-callback-ref": "1.1.0",
+        "aria-hidden": "^1.2.4",
+        "react-remove-scroll": "^2.6.3"
+      },
+      "peerDependencies": {
+        "@types/react": "*",
+        "@types/react-dom": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+        "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        },
+        "@types/react-dom": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/@radix-ui/react-popper": {
       "version": "1.2.2",
       "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.2.tgz",
@@ -1541,6 +1652,37 @@
         }
       }
     },
+    "node_modules/@radix-ui/react-roving-focus": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.2.tgz",
+      "integrity": "sha512-zgMQWkNO169GtGqRvYrzb0Zf8NhMHS2DuEB/TiEmVnpr5OqPU3i8lfbxaAmC2J/KYuIQxyoQQ6DxepyXp61/xw==",
+      "license": "MIT",
+      "dependencies": {
+        "@radix-ui/primitive": "1.1.1",
+        "@radix-ui/react-collection": "1.1.2",
+        "@radix-ui/react-compose-refs": "1.1.1",
+        "@radix-ui/react-context": "1.1.1",
+        "@radix-ui/react-direction": "1.1.0",
+        "@radix-ui/react-id": "1.1.0",
+        "@radix-ui/react-primitive": "2.0.2",
+        "@radix-ui/react-use-callback-ref": "1.1.0",
+        "@radix-ui/react-use-controllable-state": "1.1.0"
+      },
+      "peerDependencies": {
+        "@types/react": "*",
+        "@types/react-dom": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+        "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        },
+        "@types/react-dom": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/@radix-ui/react-separator": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.2.tgz",
diff --git a/package.json b/package.json
index 121975f..e1bc8b6 100644
--- a/package.json
+++ b/package.json
@@ -13,6 +13,7 @@
     "@hookform/resolvers": "^5.0.1",
     "@prisma/client": "^6.5.0",
     "@radix-ui/react-dialog": "^1.1.6",
+    "@radix-ui/react-dropdown-menu": "^2.1.6",
     "@radix-ui/react-label": "^2.1.2",
     "@radix-ui/react-separator": "^1.1.2",
     "@radix-ui/react-slot": "^1.1.2",
diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx
new file mode 100644
index 0000000..264f7f4
--- /dev/null
+++ b/src/app/dashboard/page.tsx
@@ -0,0 +1,118 @@
+import CreateItemModal from "@/components/dashboard/CreateItemModal";
+import { Button } from "@/components/ui/button";
+import {
+    DropdownMenu,
+    DropdownMenuContent,
+    DropdownMenuItem,
+    DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import { EllipsisVertical, LogOut } from "lucide-react";
+import { FC } from "react";
+
+interface PageProps {}
+
+interface Item {
+  id: string;
+  name: string;
+  description: string;
+  imageUrl: string;
+  price: number;
+}
+
+const Page: FC<PageProps> = ({}) => {
+  const isAdmin = true;
+
+  const items: Item[] = [
+    {
+      id: "1",
+      name: "Camiseta Negra",
+      description: "Una camiseta básica negra, cómoda y elegante.",
+      imageUrl:
+        "https://st2.depositphotos.com/39040180/43269/i/450/depositphotos_432698912-stock-photo-blank-shirt-color-black-template.jpg",
+      price: 19.99,
+    },
+    {
+      id: "2",
+      name: "Jeans Azules",
+      description: "Pantalones de mezclilla clásicos de corte recto.",
+      imageUrl:
+        "https://t3.ftcdn.net/jpg/04/83/25/50/360_F_483255019_m1r1ujM8EOkr8PamCHF85tQ0rHG3Fiqz.jpg",
+      price: 49.99,
+    },
+    {
+      id: "3",
+      name: "Tenis Blancos",
+      description: "Zapatos deportivos blancos de alta calidad.",
+      imageUrl:
+        "https://t4.ftcdn.net/jpg/02/07/04/29/360_F_207042944_wheO6ZShP5o8Nli11LSFWCYGRt2N3mNp.jpg",
+      price: 79.99,
+    },
+    {
+      id: "4",
+      name: "Gorra Negra",
+      description: "Gorra ajustable negra, perfecta para el sol.",
+      imageUrl:
+        "https://media.istockphoto.com/id/1157599346/photo/black-baseball-cap-isolated-on-white-background-with-clipping-path.jpg?s=612x612&w=0&k=20&c=ltNry5F1DwY8hX6Zk1gCXkwfL4LUuPhfTGGmfwDs5xI=",
+      price: 14.99,
+    },
+  ];
+
+  return (
+    <main className="text-white bg-[#050505] w-full min-h-svh flex justify-center">
+      <div className="w-full max-w-6xl p-4 flex flex-col gap-6 py-14">
+        <div className="flex items-center justify-between">
+          <h1 className="text-4xl font-bold">Productos</h1>
+          <div className="flex gap-3">
+            {isAdmin && <CreateItemModal />}
+            <Button className="rounded-xl hover:bg-gray-700 bg-gray-600 cursor-pointer">
+              <LogOut />
+              Salir
+            </Button>
+          </div>
+        </div>
+
+        {/* Grid de productos */}
+        <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
+          {items.map((item) => (
+            <div
+              key={item.id}
+              className="relative bg-[#1a1a1a] rounded-xl overflow-hidden shadow-md hover:scale-[1.02] transition-transform p-4 flex flex-col"
+            >
+              {isAdmin && (
+                <div className="absolute top-2 right-2">
+                  <DropdownMenu>
+                    <DropdownMenuTrigger asChild>
+                      <button className="text-white  rounded-full p-1 bg-black border-white/20 border cursor-pointer outline-none">
+                        <EllipsisVertical size={20} />
+                      </button>
+                    </DropdownMenuTrigger>
+                    <DropdownMenuContent className="bg-[#1a1a1a] text-white border-white/20">
+                      <DropdownMenuItem className="hover:bg-red-500/20 text-red-400 cursor-pointer">
+                        Eliminar
+                      </DropdownMenuItem>
+                    </DropdownMenuContent>
+                  </DropdownMenu>
+                </div>
+              )}
+
+              <img
+                src={item.imageUrl}
+                alt={item.name}
+                className="w-full h-40 object-cover rounded-md mb-4"
+              />
+              <h2 className="text-xl font-semibold mb-2">{item.name}</h2>
+              <p className="text-sm text-gray-400 mb-4 line-clamp-2">
+                {item.description}
+              </p>
+              <span className="text-lg font-bold">
+                ${item.price.toFixed(2)}
+              </span>
+            </div>
+          ))}
+        </div>
+      </div>
+    </main>
+  );
+};
+
+export default Page;
diff --git a/src/components/dashboard/CreateItemModal.tsx b/src/components/dashboard/CreateItemModal.tsx
new file mode 100644
index 0000000..8b71d6c
--- /dev/null
+++ b/src/components/dashboard/CreateItemModal.tsx
@@ -0,0 +1,164 @@
+"use client";
+
+import { Dialog, DialogDescription, DialogTitle } from "@radix-ui/react-dialog";
+import { FC } from "react";
+import {
+    DialogContent,
+    DialogFooter,
+    DialogHeader,
+    DialogTrigger,
+} from "../ui/dialog";
+import { Button } from "../ui/button";
+import { z } from "zod";
+import { useForm } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+import {
+    Form,
+    FormControl,
+    FormField,
+    FormItem,
+    FormLabel,
+    FormMessage,
+} from "../ui/form";
+import { Input } from "../ui/input";
+import { PlusCircle } from "lucide-react";
+
+interface CreateItemModalProps {}
+
+const itemSchema = z.object({
+  name: z.string().min(1, { message: "El nombre es requerido" }),
+  description: z.string().min(1, { message: "La descripción es requerida" }),
+  price: z.coerce
+    .number()
+    .positive({ message: "El precio debe ser mayor a 0" }),
+  imageUrl: z.string().url({ message: "La URL de la imagen es inválida" }),
+});
+
+const CreateItemModal: FC<CreateItemModalProps> = ({}) => {
+  const form = useForm<z.infer<typeof itemSchema>>({
+    resolver: zodResolver(itemSchema),
+    defaultValues: {
+      name: "",
+      description: "",
+      price: 0,
+      imageUrl: "",
+    },
+  });
+
+  function onSubmit(values: z.infer<typeof itemSchema>) {
+    console.log("Nuevo producto creado:", values);
+  }
+
+  return (
+    <Dialog>
+      <DialogTrigger asChild>
+        <Button className="rounded-xl hover:bg-violet-500/75 bg-violet-500 cursor-pointer">
+          <PlusCircle />
+          Nuevo producto
+        </Button>
+      </DialogTrigger>
+      <DialogContent className="sm:max-w-[425px] bg-black text-white border-white/20">
+        <DialogHeader>
+          <DialogTitle>Nuevo producto</DialogTitle>
+          <DialogDescription className="text-sm text-white/60">
+            Agrega un nuevo producto a tu tienda. Asegúrate de que toda la
+            información sea correcta antes de guardar.
+          </DialogDescription>
+        </DialogHeader>
+        <Form {...form}>
+          <form
+            onSubmit={form.handleSubmit(onSubmit)}
+            className="space-y-5 w-full"
+          >
+            <FormField
+              control={form.control}
+              name="name"
+              render={({ field }) => (
+                <FormItem>
+                  <FormLabel>Nombre</FormLabel>
+                  <FormControl>
+                    <Input
+                      placeholder="Camiseta Negra"
+                      className="!border-white/20 placeholder:text-white/50"
+                      {...field}
+                    />
+                  </FormControl>
+                  <FormMessage />
+                </FormItem>
+              )}
+            />
+
+            <FormField
+              control={form.control}
+              name="description"
+              render={({ field }) => (
+                <FormItem>
+                  <FormLabel>Descripción</FormLabel>
+                  <FormControl>
+                    <Input
+                      placeholder="Una camiseta básica negra, cómoda y elegante."
+                      className="!border-white/20 placeholder:text-white/50"
+                      {...field}
+                    />
+                  </FormControl>
+                  <FormMessage />
+                </FormItem>
+              )}
+            />
+
+            <FormField
+              control={form.control}
+              name="price"
+              render={({ field }) => (
+                <FormItem>
+                  <FormLabel>Precio</FormLabel>
+                  <FormControl>
+                    <Input
+                      placeholder="29.99"
+                      className="!border-white/20 placeholder:text-white/50"
+                      inputMode="decimal"
+                      pattern="[0-9]*"
+                      {...field}
+                      value={field.value === 0 ? "" : field.value}
+                      onChange={(e) => field.onChange(e.target.value)}
+                    />
+                  </FormControl>
+                  <FormMessage />
+                </FormItem>
+              )}
+            />
+
+            <FormField
+              control={form.control}
+              name="imageUrl"
+              render={({ field }) => (
+                <FormItem>
+                  <FormLabel>URL de la imagen</FormLabel>
+                  <FormControl>
+                    <Input
+                      placeholder="https://via.placeholder.com/150"
+                      className="!border-white/20 placeholder:text-white/50"
+                      {...field}
+                    />
+                  </FormControl>
+                  <FormMessage />
+                </FormItem>
+              )}
+            />
+
+            <DialogFooter>
+              <Button
+                type="submit"
+                className="rounded-xl hover:bg-violet-500/75 bg-violet-500 cursor-pointer w-full"
+              >
+                Guardar
+              </Button>
+            </DialogFooter>
+          </form>
+        </Form>
+      </DialogContent>
+    </Dialog>
+  );
+};
+
+export default CreateItemModal;
diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx
new file mode 100644
index 0000000..7d7a9d3
--- /dev/null
+++ b/src/components/ui/dialog.tsx
@@ -0,0 +1,135 @@
+"use client"
+
+import * as React from "react"
+import * as DialogPrimitive from "@radix-ui/react-dialog"
+import { XIcon } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+function Dialog({
+  ...props
+}: React.ComponentProps<typeof DialogPrimitive.Root>) {
+  return <DialogPrimitive.Root data-slot="dialog" {...props} />
+}
+
+function DialogTrigger({
+  ...props
+}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
+  return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
+}
+
+function DialogPortal({
+  ...props
+}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
+  return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
+}
+
+function DialogClose({
+  ...props
+}: React.ComponentProps<typeof DialogPrimitive.Close>) {
+  return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
+}
+
+function DialogOverlay({
+  className,
+  ...props
+}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
+  return (
+    <DialogPrimitive.Overlay
+      data-slot="dialog-overlay"
+      className={cn(
+        "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function DialogContent({
+  className,
+  children,
+  ...props
+}: React.ComponentProps<typeof DialogPrimitive.Content>) {
+  return (
+    <DialogPortal data-slot="dialog-portal">
+      <DialogOverlay />
+      <DialogPrimitive.Content
+        data-slot="dialog-content"
+        className={cn(
+          "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
+          className
+        )}
+        {...props}
+      >
+        {children}
+        <DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4">
+          <XIcon />
+          <span className="sr-only">Close</span>
+        </DialogPrimitive.Close>
+      </DialogPrimitive.Content>
+    </DialogPortal>
+  )
+}
+
+function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="dialog-header"
+      className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
+      {...props}
+    />
+  )
+}
+
+function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="dialog-footer"
+      className={cn(
+        "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function DialogTitle({
+  className,
+  ...props
+}: React.ComponentProps<typeof DialogPrimitive.Title>) {
+  return (
+    <DialogPrimitive.Title
+      data-slot="dialog-title"
+      className={cn("text-lg leading-none font-semibold", className)}
+      {...props}
+    />
+  )
+}
+
+function DialogDescription({
+  className,
+  ...props
+}: React.ComponentProps<typeof DialogPrimitive.Description>) {
+  return (
+    <DialogPrimitive.Description
+      data-slot="dialog-description"
+      className={cn("text-muted-foreground text-sm", className)}
+      {...props}
+    />
+  )
+}
+
+export {
+  Dialog,
+  DialogClose,
+  DialogContent,
+  DialogDescription,
+  DialogFooter,
+  DialogHeader,
+  DialogOverlay,
+  DialogPortal,
+  DialogTitle,
+  DialogTrigger,
+}
diff --git a/src/components/ui/dropdown-menu.tsx b/src/components/ui/dropdown-menu.tsx
new file mode 100644
index 0000000..ec51e9c
--- /dev/null
+++ b/src/components/ui/dropdown-menu.tsx
@@ -0,0 +1,257 @@
+"use client"
+
+import * as React from "react"
+import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
+import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+function DropdownMenu({
+  ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
+  return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
+}
+
+function DropdownMenuPortal({
+  ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
+  return (
+    <DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
+  )
+}
+
+function DropdownMenuTrigger({
+  ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
+  return (
+    <DropdownMenuPrimitive.Trigger
+      data-slot="dropdown-menu-trigger"
+      {...props}
+    />
+  )
+}
+
+function DropdownMenuContent({
+  className,
+  sideOffset = 4,
+  ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
+  return (
+    <DropdownMenuPrimitive.Portal>
+      <DropdownMenuPrimitive.Content
+        data-slot="dropdown-menu-content"
+        sideOffset={sideOffset}
+        className={cn(
+          "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
+          className
+        )}
+        {...props}
+      />
+    </DropdownMenuPrimitive.Portal>
+  )
+}
+
+function DropdownMenuGroup({
+  ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
+  return (
+    <DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
+  )
+}
+
+function DropdownMenuItem({
+  className,
+  inset,
+  variant = "default",
+  ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
+  inset?: boolean
+  variant?: "default" | "destructive"
+}) {
+  return (
+    <DropdownMenuPrimitive.Item
+      data-slot="dropdown-menu-item"
+      data-inset={inset}
+      data-variant={variant}
+      className={cn(
+        "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function DropdownMenuCheckboxItem({
+  className,
+  children,
+  checked,
+  ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
+  return (
+    <DropdownMenuPrimitive.CheckboxItem
+      data-slot="dropdown-menu-checkbox-item"
+      className={cn(
+        "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
+        className
+      )}
+      checked={checked}
+      {...props}
+    >
+      <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
+        <DropdownMenuPrimitive.ItemIndicator>
+          <CheckIcon className="size-4" />
+        </DropdownMenuPrimitive.ItemIndicator>
+      </span>
+      {children}
+    </DropdownMenuPrimitive.CheckboxItem>
+  )
+}
+
+function DropdownMenuRadioGroup({
+  ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
+  return (
+    <DropdownMenuPrimitive.RadioGroup
+      data-slot="dropdown-menu-radio-group"
+      {...props}
+    />
+  )
+}
+
+function DropdownMenuRadioItem({
+  className,
+  children,
+  ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
+  return (
+    <DropdownMenuPrimitive.RadioItem
+      data-slot="dropdown-menu-radio-item"
+      className={cn(
+        "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
+        className
+      )}
+      {...props}
+    >
+      <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
+        <DropdownMenuPrimitive.ItemIndicator>
+          <CircleIcon className="size-2 fill-current" />
+        </DropdownMenuPrimitive.ItemIndicator>
+      </span>
+      {children}
+    </DropdownMenuPrimitive.RadioItem>
+  )
+}
+
+function DropdownMenuLabel({
+  className,
+  inset,
+  ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
+  inset?: boolean
+}) {
+  return (
+    <DropdownMenuPrimitive.Label
+      data-slot="dropdown-menu-label"
+      data-inset={inset}
+      className={cn(
+        "px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function DropdownMenuSeparator({
+  className,
+  ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
+  return (
+    <DropdownMenuPrimitive.Separator
+      data-slot="dropdown-menu-separator"
+      className={cn("bg-border -mx-1 my-1 h-px", className)}
+      {...props}
+    />
+  )
+}
+
+function DropdownMenuShortcut({
+  className,
+  ...props
+}: React.ComponentProps<"span">) {
+  return (
+    <span
+      data-slot="dropdown-menu-shortcut"
+      className={cn(
+        "text-muted-foreground ml-auto text-xs tracking-widest",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function DropdownMenuSub({
+  ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
+  return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
+}
+
+function DropdownMenuSubTrigger({
+  className,
+  inset,
+  children,
+  ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
+  inset?: boolean
+}) {
+  return (
+    <DropdownMenuPrimitive.SubTrigger
+      data-slot="dropdown-menu-sub-trigger"
+      data-inset={inset}
+      className={cn(
+        "focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
+        className
+      )}
+      {...props}
+    >
+      {children}
+      <ChevronRightIcon className="ml-auto size-4" />
+    </DropdownMenuPrimitive.SubTrigger>
+  )
+}
+
+function DropdownMenuSubContent({
+  className,
+  ...props
+}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
+  return (
+    <DropdownMenuPrimitive.SubContent
+      data-slot="dropdown-menu-sub-content"
+      className={cn(
+        "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+export {
+  DropdownMenu,
+  DropdownMenuPortal,
+  DropdownMenuTrigger,
+  DropdownMenuContent,
+  DropdownMenuGroup,
+  DropdownMenuLabel,
+  DropdownMenuItem,
+  DropdownMenuCheckboxItem,
+  DropdownMenuRadioGroup,
+  DropdownMenuRadioItem,
+  DropdownMenuSeparator,
+  DropdownMenuShortcut,
+  DropdownMenuSub,
+  DropdownMenuSubTrigger,
+  DropdownMenuSubContent,
+}


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: 

Copilot reviewed 5 out of 6 changed files in this pull request and generated 1 comment.

<details>
<summary>Files not reviewed (1)</summary>

* **package.json**: Language not supported
</details>



state: COMMENTED
author_association: NONE
submitted_at: 2025-04-07T04:55:38Z
user: login: ZenkoPc
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-07T04:56:13Z

Review comments:
diff_hunk: @@ -0,0 +1,164 @@
+"use client";
+
+import { Dialog, DialogDescription, DialogTitle } from "@radix-ui/react-dialog";
+import { FC } from "react";
+import {
+    DialogContent,
+    DialogFooter,
+    DialogHeader,
+    DialogTrigger,
+} from "../ui/dialog";
+import { Button } from "../ui/button";
+import { z } from "zod";
+import { useForm } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+import {
+    Form,
+    FormControl,
+    FormField,
+    FormItem,
+    FormLabel,
+    FormMessage,
+} from "../ui/form";
+import { Input } from "../ui/input";
+import { PlusCircle } from "lucide-react";
+
+interface CreateItemModalProps {}
+
+const itemSchema = z.object({
+  name: z.string().min(1, { message: "El nombre es requerido" }),
+  description: z.string().min(1, { message: "La descripción es requerida" }),
+  price: z.coerce
+    .number()
+    .positive({ message: "El precio debe ser mayor a 0" }),
+  imageUrl: z.string().url({ message: "La URL de la imagen es inválida" }),
+});
+
+const CreateItemModal: FC<CreateItemModalProps> = ({}) => {
+  const form = useForm<z.infer<typeof itemSchema>>({
+    resolver: zodResolver(itemSchema),
+    defaultValues: {
+      name: "",
+      description: "",
+      price: 0,
+      imageUrl: "",
+    },
+  });
+
+  function onSubmit(values: z.infer<typeof itemSchema>) {
+    console.log("Nuevo producto creado:", values);
+  }
+
+  return (
+    <Dialog>
+      <DialogTrigger asChild>
+        <Button className="rounded-xl hover:bg-violet-500/75 bg-violet-500 cursor-pointer">
+          <PlusCircle />
+          Nuevo producto
+        </Button>
+      </DialogTrigger>
+      <DialogContent className="sm:max-w-[425px] bg-black text-white border-white/20">
+        <DialogHeader>
+          <DialogTitle>Nuevo producto</DialogTitle>
+          <DialogDescription className="text-sm text-white/60">
+            Agrega un nuevo producto a tu tienda. Asegúrate de que toda la
+            información sea correcta antes de guardar.
+          </DialogDescription>
+        </DialogHeader>
+        <Form {...form}>
+          <form
+            onSubmit={form.handleSubmit(onSubmit)}
+            className="space-y-5 w-full"
+          >
+            <FormField
+              control={form.control}
+              name="name"
+              render={({ field }) => (
+                <FormItem>
+                  <FormLabel>Nombre</FormLabel>
+                  <FormControl>
+                    <Input
+                      placeholder="Camiseta Negra"
+                      className="!border-white/20 placeholder:text-white/50"
+                      {...field}
+                    />
+                  </FormControl>
+                  <FormMessage />
+                </FormItem>
+              )}
+            />
+
+            <FormField
+              control={form.control}
+              name="description"
+              render={({ field }) => (
+                <FormItem>
+                  <FormLabel>Descripción</FormLabel>
+                  <FormControl>
+                    <Input
+                      placeholder="Una camiseta básica negra, cómoda y elegante."
+                      className="!border-white/20 placeholder:text-white/50"
+                      {...field}
+                    />
+                  </FormControl>
+                  <FormMessage />
+                </FormItem>
+              )}
+            />
+
+            <FormField
+              control={form.control}
+              name="price"
+              render={({ field }) => (
+                <FormItem>
+                  <FormLabel>Precio</FormLabel>
+                  <FormControl>
+                    <Input
+                      placeholder="29.99"
+                      className="!border-white/20 placeholder:text-white/50"
+                      inputMode="decimal"
+                      pattern="[0-9]*"
path: src/components/dashboard/CreateItemModal.tsx
user: login: Copilot
body: The 'pattern' attribute for the price input only accepts integer digits, which might prevent entering valid decimal values. Consider updating the pattern (e.g., to "[0-9]*(\.[0-9]+)?") to properly support decimal input.
```suggestion
                      pattern="[0-9]*(\.[0-9]+)?"
```
created_at: 2025-04-07T04:55:37Z
author_association: NONE
diff_hunk: @@ -0,0 +1,164 @@
+"use client";
+
+import { Dialog, DialogDescription, DialogTitle } from "@radix-ui/react-dialog";
+import { FC } from "react";
+import {
+    DialogContent,
+    DialogFooter,
+    DialogHeader,
+    DialogTrigger,
+} from "../ui/dialog";
+import { Button } from "../ui/button";
+import { z } from "zod";
+import { useForm } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+import {
+    Form,
+    FormControl,
+    FormField,
+    FormItem,
+    FormLabel,
+    FormMessage,
+} from "../ui/form";
+import { Input } from "../ui/input";
+import { PlusCircle } from "lucide-react";
+
+interface CreateItemModalProps {}
+
+const itemSchema = z.object({
+  name: z.string().min(1, { message: "El nombre es requerido" }),
+  description: z.string().min(1, { message: "La descripción es requerida" }),
+  price: z.coerce
+    .number()
+    .positive({ message: "El precio debe ser mayor a 0" }),
+  imageUrl: z.string().url({ message: "La URL de la imagen es inválida" }),
+});
+
+const CreateItemModal: FC<CreateItemModalProps> = ({}) => {
+  const form = useForm<z.infer<typeof itemSchema>>({
+    resolver: zodResolver(itemSchema),
+    defaultValues: {
+      name: "",
+      description: "",
+      price: 0,
+      imageUrl: "",
+    },
+  });
+
+  function onSubmit(values: z.infer<typeof itemSchema>) {
+    console.log("Nuevo producto creado:", values);
+  }
+
+  return (
+    <Dialog>
+      <DialogTrigger asChild>
+        <Button className="rounded-xl hover:bg-violet-500/75 bg-violet-500 cursor-pointer">
+          <PlusCircle />
+          Nuevo producto
+        </Button>
+      </DialogTrigger>
+      <DialogContent className="sm:max-w-[425px] bg-black text-white border-white/20">
+        <DialogHeader>
+          <DialogTitle>Nuevo producto</DialogTitle>
+          <DialogDescription className="text-sm text-white/60">
+            Agrega un nuevo producto a tu tienda. Asegúrate de que toda la
+            información sea correcta antes de guardar.
+          </DialogDescription>
+        </DialogHeader>
+        <Form {...form}>
+          <form
+            onSubmit={form.handleSubmit(onSubmit)}
+            className="space-y-5 w-full"
+          >
+            <FormField
+              control={form.control}
+              name="name"
+              render={({ field }) => (
+                <FormItem>
+                  <FormLabel>Nombre</FormLabel>
+                  <FormControl>
+                    <Input
+                      placeholder="Camiseta Negra"
+                      className="!border-white/20 placeholder:text-white/50"
+                      {...field}
+                    />
+                  </FormControl>
+                  <FormMessage />
+                </FormItem>
+              )}
+            />
+
+            <FormField
+              control={form.control}
+              name="description"
+              render={({ field }) => (
+                <FormItem>
+                  <FormLabel>Descripción</FormLabel>
+                  <FormControl>
+                    <Input
+                      placeholder="Una camiseta básica negra, cómoda y elegante."
+                      className="!border-white/20 placeholder:text-white/50"
+                      {...field}
+                    />
+                  </FormControl>
+                  <FormMessage />
+                </FormItem>
+              )}
+            />
+
+            <FormField
+              control={form.control}
+              name="price"
+              render={({ field }) => (
+                <FormItem>
+                  <FormLabel>Precio</FormLabel>
+                  <FormControl>
+                    <Input
+                      placeholder="29.99"
+                      className="!border-white/20 placeholder:text-white/50"
+                      inputMode="decimal"
+                      pattern="[0-9]*"
path: src/components/dashboard/CreateItemModal.tsx
user: login: ZenkoPc
body: for now, its not relevant as the values are placeholder examples
created_at: 2025-04-07T04:56:13Z
author_association: OWNER

PR comments:
user: login: vercel[bot]
created_at: 2025-04-07T04:21:39Z
body: @sbetav is attempting to deploy a commit to the **zenkopc's projects** Team on [Vercel](https://vercel.com).

A member of the Team first needs to [authorize it](https://vercel.com/git/authorize?team=zenkopc's%20projects&type=github&job=%7B%22headInfo%22%3A%7B%22sha%22%3A%22ff05c8f605646d04b49897dbe1d95722477fb0fc%22%7D%2C%22id%22%3A%22QmdgsweDYKk32iXrn2D4HRGvHfvdtGQMEBkyVUAHNsdqv3%22%2C%22org%22%3A%22ZenkoPc%22%2C%22prId%22%3A4%2C%22repo%22%3A%22testLanding%22%7D).


author_association: NONE
user: login: vercel[bot]
created_at: 2025-04-07T04:26:33Z
body: [vc]: #GxFv8NRJkH0V7znFf+juyT4mRGaSa7/UHFsSWnaIC/g=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJ0ZXN0LWxhbmRpbmciLCJyb290RGlyZWN0b3J5IjpudWxsLCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vemVua29wY3MtcHJvamVjdHMvdGVzdC1sYW5kaW5nLzVta01zejRWRWhXdzJRcThwczNRUmR4OHM5a2UiLCJwcmV2aWV3VXJsIjoidGVzdC1sYW5kaW5nLWdpdC1mb3JrLXNiZXRhdi1mZWF0dXJlLWQtMzllZWIwLXplbmtvcGNzLXByb2plY3RzLnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiREVQTE9ZRUQiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoidGVzdC1sYW5kaW5nLWdpdC1mb3JrLXNiZXRhdi1mZWF0dXJlLWQtMzllZWIwLXplbmtvcGNzLXByb2plY3RzLnZlcmNlbC5hcHAifX1dfQ==
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **test-landing** | ✅ Ready ([Inspect](https://vercel.com/zenkopcs-projects/test-landing/5mkMsz4VEhWw2Qq8ps3QRdx8s9ke)) | [Visit Preview](https://test-landing-git-fork-sbetav-feature-d-39eeb0-zenkopcs-projects.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/test-landing-git-fork-sbetav-feature-d-39eeb0-zenkopcs-projects.vercel.app?via=pr-comment-feedback-link) | Apr 7, 2025 4:27am |


author_association: NONE