{"version":1,"id":"product-list-table","framework":"next","kind":"block","files":[{"path":"src/blocks/gfa/product-list-table.tsx","content":"\"use client\";\n\nimport { useEffect, useMemo, useState, type ReactNode } from \"react\";\nimport Image from \"next/image\";\nimport {\n  FolderIcon,\n  PackageIcon,\n  PencilIcon,\n  PlusIcon,\n  SearchIcon,\n  SendIcon,\n  Trash2Icon,\n} from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\nconst LOW_STOCK_THRESHOLD = 5;\nconst PRIMARY = \"#ff6d1f\";\n\ntype ListingStatus =\n  | \"APPROVED\"\n  | \"PENDING_REVIEW\"\n  | \"DRAFT\"\n  | \"REJECTED\"\n  | \"SUSPENDED\";\n\ntype ProductFilter =\n  | \"all\"\n  | \"listed\"\n  | \"pending\"\n  | \"draft\"\n  | \"rejected\"\n  | \"low\";\n\ntype ListingActionKind = \"unlist\" | \"submit\";\n\nexport type ProductListItem = {\n  id: string;\n  name: string;\n  slug: string;\n  sku: string | null;\n  price: number;\n  discountedPrice: number | null;\n  stock: number;\n  imageUrl: string | null;\n  categoryName: string | null;\n  listingStatus: ListingStatus;\n};\n\n/** Unsplash CDN photo ids — shuffled on each load. */\nconst UNSPLASH_PHOTO_IDS = [\n  \"1505740420928-5e560c06d30e\",\n  \"1523275335684-37898b6baf30\",\n  \"1542291026-7eec264c27ff\",\n  \"1572635196237-14b3f281503f\",\n  \"1560343090-f0409e92791a\",\n  \"1585386959984-a4155224a1ad\",\n  \"1526170375885-4d8ecf77b99f\",\n  \"1491553895911-0055cfa8775c\",\n  \"1511499767150-a48a237f0083\",\n  \"1441986300917-64674bd600d8\",\n  \"1483985988355-763728e1935b\",\n  \"1460353581641-37baddab0fa2\",\n  \"1514228742587-6b1558fcca3d\",\n  \"1556228578-0d85b1a4d571\",\n  \"1600565193348-f74bd3bb3b32\",\n  \"1596755094514-f87e34085b2c\",\n  \"1611930022073-b7a4ba5fec8c\",\n  \"1625772452859-1c800e6c0f4c\",\n] as const;\n\nfunction unsplashUrl(photoId: string) {\n  return `https://images.unsplash.com/photo-${photoId}?auto=format&fit=crop&w=160&h=160&q=80`;\n}\n\nfunction pickRandomUnsplashUrls(count: number): string[] {\n  const pool = [...UNSPLASH_PHOTO_IDS];\n  for (let i = pool.length - 1; i > 0; i--) {\n    const j = Math.floor(Math.random() * (i + 1));\n    [pool[i], pool[j]] = [pool[j]!, pool[i]!];\n  }\n  return pool.slice(0, count).map((id) => unsplashUrl(id));\n}\n\nconst DEMO_PRODUCT_BASE: Omit<ProductListItem, \"imageUrl\">[] = [\n  {\n    id: \"1\",\n    name: \"Nordic Linen Shirt\",\n    slug: \"nordic-linen-shirt\",\n    sku: \"NLS-001\",\n    price: 1299,\n    discountedPrice: 999,\n    stock: 42,\n    categoryName: \"Apparel › Men › Shirts\",\n    listingStatus: \"APPROVED\",\n  },\n  {\n    id: \"2\",\n    name: \"Ceramic Pour-Over Set\",\n    slug: \"ceramic-pour-over\",\n    sku: \"CPS-214\",\n    price: 890,\n    discountedPrice: null,\n    stock: 3,\n    categoryName: \"Home › Kitchen\",\n    listingStatus: \"APPROVED\",\n  },\n  {\n    id: \"3\",\n    name: \"Trail Runner Pro\",\n    slug: \"trail-runner-pro\",\n    sku: \"TRP-088\",\n    price: 2490,\n    discountedPrice: 2190,\n    stock: 18,\n    categoryName: \"Shoes › Sport\",\n    listingStatus: \"PENDING_REVIEW\",\n  },\n  {\n    id: \"4\",\n    name: \"Desk Mat — Slate\",\n    slug: \"desk-mat-slate\",\n    sku: null,\n    price: 450,\n    discountedPrice: null,\n    stock: 0,\n    categoryName: \"Office › Accessories\",\n    listingStatus: \"DRAFT\",\n  },\n  {\n    id: \"5\",\n    name: \"Wireless Charge Pad\",\n    slug: \"wireless-charge-pad\",\n    sku: \"WCP-12\",\n    price: 699,\n    discountedPrice: null,\n    stock: 27,\n    categoryName: \"Electronics › Accessories\",\n    listingStatus: \"REJECTED\",\n  },\n  {\n    id: \"6\",\n    name: \"Oak Serving Board\",\n    slug: \"oak-serving-board\",\n    sku: \"OSB-03\",\n    price: 1120,\n    discountedPrice: null,\n    stock: 9,\n    categoryName: \"Home › Tableware\",\n    listingStatus: \"APPROVED\",\n  },\n];\n\nfunction buildStableDemoProducts(): ProductListItem[] {\n  return DEMO_PRODUCT_BASE.map((product, i) => ({\n    ...product,\n    imageUrl: unsplashUrl(\n      UNSPLASH_PHOTO_IDS[i % UNSPLASH_PHOTO_IDS.length]!,\n    ),\n  }));\n}\n\nfunction buildDemoProducts(): ProductListItem[] {\n  const urls = pickRandomUnsplashUrls(DEMO_PRODUCT_BASE.length);\n  return DEMO_PRODUCT_BASE.map((product, i) => ({\n    ...product,\n    imageUrl: urls[i] ?? null,\n  }));\n}\n\nfunction formatMoney(value: number) {\n  return new Intl.NumberFormat(\"en-US\", {\n    style: \"currency\",\n    currency: \"USD\",\n    maximumFractionDigits: 0,\n  }).format(value);\n}\n\nfunction statusLabel(status: ListingStatus) {\n  switch (status) {\n    case \"APPROVED\":\n      return \"Listed\";\n    case \"PENDING_REVIEW\":\n      return \"In review\";\n    case \"REJECTED\":\n      return \"Rejected\";\n    case \"SUSPENDED\":\n      return \"Suspended\";\n    default:\n      return \"Draft\";\n  }\n}\n\nfunction statusTone(status: ListingStatus) {\n  switch (status) {\n    case \"APPROVED\":\n      return \"bg-[#ff6d1f]/10 text-[#ff6d1f] ring-[#ff6d1f]/20\";\n    case \"PENDING_REVIEW\":\n      return \"bg-amber-500/10 text-amber-800 ring-amber-500/20 dark:text-amber-300\";\n    case \"REJECTED\":\n      return \"bg-red-50 text-red-700 ring-red-200 dark:bg-red-950/40 dark:text-red-300 dark:ring-red-900\";\n    case \"SUSPENDED\":\n      return \"bg-orange-500/10 text-orange-900 ring-orange-500/20\";\n    default:\n      return \"bg-[#f5f5f7] text-zinc-500 ring-black/[0.06] dark:bg-zinc-800 dark:text-zinc-400\";\n  }\n}\n\nfunction ProductPriceCell({ product }: { product: ProductListItem }) {\n  const discounted =\n    product.discountedPrice != null &&\n    product.discountedPrice < product.price;\n  const effective = discounted\n    ? product.discountedPrice!\n    : product.price;\n\n  return (\n    <div>\n      <span\n        className={cn(\n          \"tabular-nums\",\n          discounted\n            ? \"font-medium text-[#ff6d1f]\"\n            : \"text-zinc-950 dark:text-zinc-50\",\n        )}\n      >\n        {formatMoney(effective)}\n      </span>\n      {discounted ? (\n        <span className=\"mt-0.5 block text-[11px] tabular-nums text-zinc-400 line-through\">\n          {formatMoney(product.price)}\n        </span>\n      ) : null}\n    </div>\n  );\n}\n\nfunction IconAction({\n  label,\n  onClick,\n  disabled,\n  tone = \"default\",\n  children,\n}: {\n  label: string;\n  onClick?: () => void;\n  disabled?: boolean;\n  tone?: \"default\" | \"danger\";\n  children: ReactNode;\n}) {\n  return (\n    <button\n      type=\"button\"\n      aria-label={label}\n      title={label}\n      disabled={disabled}\n      onClick={onClick}\n      className={cn(\n        \"inline-flex size-9 items-center justify-center rounded-[0.625rem] bg-white ring-1 ring-black/[0.08] transition hover:bg-[#f5f5f7] disabled:opacity-50\",\n        \"dark:bg-zinc-950 dark:ring-white/[0.1] dark:hover:bg-zinc-800\",\n        tone === \"danger\"\n          ? \"text-red-600 hover:bg-red-50 dark:text-red-400 dark:hover:bg-red-950/40\"\n          : \"text-zinc-950 dark:text-zinc-50\",\n      )}\n    >\n      {children}\n    </button>\n  );\n}\n\nexport type ProductListTableProps = {\n  products?: ProductListItem[];\n  className?: string;\n  onEdit?: (product: ProductListItem) => void;\n  onDelete?: (product: ProductListItem) => void;\n  onSubmit?: (product: ProductListItem) => void;\n};\n\nexport function ProductListTable({\n  products: productsProp,\n  className,\n  onEdit,\n  onDelete,\n  onSubmit,\n}: ProductListTableProps) {\n  const useDemo = productsProp == null;\n  const [products, setProducts] = useState<ProductListItem[]>(\n    () => productsProp ?? buildStableDemoProducts(),\n  );\n  const [query, setQuery] = useState(\"\");\n  const [filter, setFilter] = useState<ProductFilter>(\"all\");\n  const [actingId, setActingId] = useState<string | null>(null);\n\n  useEffect(() => {\n    if (productsProp != null) {\n      setProducts(productsProp);\n      return;\n    }\n    setProducts(buildDemoProducts());\n  }, [productsProp]);\n\n  const filtered = useMemo(() => {\n    const q = query.trim().toLowerCase();\n    return products.filter((product) => {\n      if (filter === \"listed\" && product.listingStatus !== \"APPROVED\") {\n        return false;\n      }\n      if (filter === \"pending\" && product.listingStatus !== \"PENDING_REVIEW\") {\n        return false;\n      }\n      if (filter === \"draft\" && product.listingStatus !== \"DRAFT\") {\n        return false;\n      }\n      if (filter === \"rejected\" && product.listingStatus !== \"REJECTED\") {\n        return false;\n      }\n      if (\n        filter === \"low\" &&\n        (product.listingStatus !== \"APPROVED\" ||\n          product.stock > LOW_STOCK_THRESHOLD)\n      ) {\n        return false;\n      }\n      if (!q) return true;\n      return (\n        product.name.toLowerCase().includes(q) ||\n        product.slug.toLowerCase().includes(q) ||\n        (product.sku?.toLowerCase().includes(q) ?? false) ||\n        (product.categoryName?.toLowerCase().includes(q) ?? false)\n      );\n    });\n  }, [products, query, filter]);\n\n  function listingActionKind(product: ProductListItem): ListingActionKind | null {\n    if (product.listingStatus === \"SUSPENDED\") return null;\n    if (\n      product.listingStatus === \"APPROVED\" ||\n      product.listingStatus === \"PENDING_REVIEW\"\n    ) {\n      return \"unlist\";\n    }\n    if (\n      product.listingStatus === \"DRAFT\" ||\n      product.listingStatus === \"REJECTED\"\n    ) {\n      return \"submit\";\n    }\n    return null;\n  }\n\n  function listingAction(product: ProductListItem) {\n    const kind = listingActionKind(product);\n    if (kind === \"unlist\") onDelete?.(product);\n    if (kind === \"submit\") onSubmit?.(product);\n\n    if (!useDemo && productsProp != null) return;\n\n    setActingId(product.id);\n    window.setTimeout(() => {\n      setProducts((prev) =>\n        prev.map((p) => {\n          if (p.id !== product.id) return p;\n          if (\n            p.listingStatus === \"APPROVED\" ||\n            p.listingStatus === \"PENDING_REVIEW\"\n          ) {\n            return { ...p, listingStatus: \"DRAFT\" };\n          }\n          if (p.listingStatus === \"DRAFT\" || p.listingStatus === \"REJECTED\") {\n            return { ...p, listingStatus: \"PENDING_REVIEW\" };\n          }\n          return p;\n        }),\n      );\n      setActingId(null);\n    }, 280);\n  }\n\n  return (\n    <div\n      className={cn(\"space-y-4\", className)}\n      data-slot=\"product-list-table\"\n    >\n      <div className=\"rounded-[0.625rem] bg-[#fbfbfd] ring-1 ring-black/[0.04] dark:bg-zinc-900 dark:ring-white/[0.06]\">\n        <div className=\"flex flex-col gap-3 border-b border-black/[0.04] p-4 dark:border-white/[0.06] lg:flex-row lg:items-center lg:justify-between\">\n          <div className=\"flex flex-wrap gap-2\">\n            {(\n              [\n                [\"all\", \"All\"],\n                [\"listed\", \"Listed\"],\n                [\"pending\", \"In review\"],\n                [\"draft\", \"Draft\"],\n                [\"rejected\", \"Rejected\"],\n                [\"low\", \"Low stock\"],\n              ] as const\n            ).map(([key, label]) => (\n              <button\n                key={key}\n                type=\"button\"\n                onClick={() => setFilter(key)}\n                className={cn(\n                  \"rounded-full px-3 py-1.5 text-[12px] font-medium transition\",\n                  filter === key\n                    ? \"bg-[#ff6d1f]/10 text-[#ff6d1f]\"\n                    : \"bg-white text-zinc-500 ring-1 ring-black/[0.06] hover:text-zinc-950 dark:bg-zinc-950 dark:text-zinc-400 dark:ring-white/[0.08] dark:hover:text-zinc-100\",\n                )}\n              >\n                {label}\n              </button>\n            ))}\n          </div>\n\n          <div className=\"flex flex-col gap-2 sm:flex-row sm:items-center\">\n            <label className=\"relative block sm:w-56\">\n              <SearchIcon\n                className=\"pointer-events-none absolute left-3 top-1/2 size-3.5 -translate-y-1/2 text-zinc-400\"\n                aria-hidden\n              />\n              <input\n                type=\"search\"\n                value={query}\n                onChange={(e) => setQuery(e.target.value)}\n                placeholder=\"Search products…\"\n                className=\"h-10 w-full rounded-[0.625rem] bg-white pl-9 pr-3 text-[13px] text-zinc-950 ring-1 ring-black/[0.06] focus:outline-none focus:ring-2 focus:ring-[#ff6d1f]/30 dark:bg-zinc-950 dark:text-zinc-50 dark:ring-white/[0.08]\"\n              />\n            </label>\n            <button\n              type=\"button\"\n              className=\"inline-flex h-10 items-center justify-center gap-2 rounded-[0.625rem] bg-white px-4 text-[13px] font-medium text-zinc-950 ring-1 ring-black/[0.06] hover:bg-[#f5f5f7] dark:bg-zinc-950 dark:text-zinc-50 dark:ring-white/[0.08] dark:hover:bg-zinc-800\"\n            >\n              <FolderIcon className=\"size-3.5\" aria-hidden />\n              Import\n            </button>\n            <button\n              type=\"button\"\n              className=\"inline-flex h-10 items-center justify-center gap-2 rounded-[0.625rem] px-4 text-[13px] font-medium text-white hover:brightness-95\"\n              style={{ backgroundColor: PRIMARY }}\n            >\n              <PlusIcon className=\"size-3.5\" aria-hidden />\n              New product\n            </button>\n          </div>\n        </div>\n\n        {filtered.length === 0 ? (\n          <div className=\"px-5 py-16 text-center\">\n            <PackageIcon className=\"mx-auto mb-4 size-7 text-zinc-400\" />\n            <p className=\"text-[15px] text-zinc-500\">\n              {products.length === 0\n                ? \"No products yet.\"\n                : \"No products match this filter.\"}\n            </p>\n          </div>\n        ) : (\n          <div className=\"overflow-x-auto\">\n            <table className=\"w-full min-w-[880px] text-left text-[13px]\">\n              <thead>\n                <tr className=\"border-b border-black/[0.04] bg-[#f5f5f7]/40 dark:border-white/[0.06] dark:bg-zinc-800/40\">\n                  <th className=\"px-4 py-3 font-medium text-zinc-500\">\n                    Product\n                  </th>\n                  <th className=\"px-4 py-3 font-medium text-zinc-500\">\n                    Category\n                  </th>\n                  <th className=\"px-4 py-3 font-medium text-zinc-500\">Price</th>\n                  <th className=\"px-4 py-3 font-medium text-zinc-500\">Stock</th>\n                  <th className=\"px-4 py-3 font-medium text-zinc-500\">Status</th>\n                  <th className=\"px-4 py-3 font-medium text-zinc-500\">\n                    Actions\n                  </th>\n                </tr>\n              </thead>\n              <tbody>\n                {filtered.map((product) => {\n                  const lowStock =\n                    product.listingStatus === \"APPROVED\" &&\n                    product.stock <= LOW_STOCK_THRESHOLD;\n                  const actionKind = listingActionKind(product);\n                  return (\n                    <tr\n                      key={product.id}\n                      className=\"border-b border-black/[0.03] transition hover:bg-[#f5f5f7]/30 dark:border-white/[0.04] dark:hover:bg-zinc-800/40\"\n                    >\n                      <td className=\"px-4 py-3\">\n                        <div className=\"flex items-center gap-3\">\n                          <span className=\"relative size-11 shrink-0 overflow-hidden rounded-[0.625rem] bg-[#f5f5f7] ring-1 ring-black/[0.06] dark:bg-zinc-800 dark:ring-white/[0.08]\">\n                            {product.imageUrl ? (\n                              <Image\n                                src={product.imageUrl}\n                                alt={product.name}\n                                fill\n                                sizes=\"44px\"\n                                className=\"object-cover\"\n                              />\n                            ) : (\n                              <span className=\"flex size-full items-center justify-center\">\n                                <PackageIcon className=\"size-4 text-zinc-400\" />\n                              </span>\n                            )}\n                          </span>\n                          <div className=\"min-w-0\">\n                            <p className=\"truncate font-medium text-zinc-950 dark:text-zinc-50\">\n                              {product.name}\n                            </p>\n                            <p className=\"truncate font-mono text-[11px] text-zinc-400\">\n                              {product.sku ?? product.slug}\n                            </p>\n                          </div>\n                        </div>\n                      </td>\n                      <td className=\"px-4 py-3 text-zinc-950 dark:text-zinc-100\">\n                        {product.categoryName ?? \"—\"}\n                      </td>\n                      <td className=\"px-4 py-3\">\n                        <ProductPriceCell product={product} />\n                      </td>\n                      <td className=\"px-4 py-3\">\n                        <span\n                          className={\n                            lowStock\n                              ? \"font-medium text-[#ff6d1f]\"\n                              : \"text-zinc-950 dark:text-zinc-100\"\n                          }\n                        >\n                          {product.stock}\n                        </span>\n                      </td>\n                      <td className=\"px-4 py-3\">\n                        <span\n                          className={cn(\n                            \"inline-flex rounded-full px-2 py-0.5 text-[11px] font-medium ring-1\",\n                            statusTone(product.listingStatus),\n                          )}\n                        >\n                          {statusLabel(product.listingStatus)}\n                        </span>\n                      </td>\n                      <td className=\"px-4 py-3\">\n                        <div className=\"flex flex-wrap items-center gap-1.5\">\n                          <IconAction\n                            label=\"Edit\"\n                            onClick={() => onEdit?.(product)}\n                          >\n                            <PencilIcon className=\"size-3.5\" aria-hidden />\n                          </IconAction>\n                          {actionKind === \"unlist\" ? (\n                            <IconAction\n                              label=\"Delete\"\n                              tone=\"danger\"\n                              disabled={actingId === product.id}\n                              onClick={() => listingAction(product)}\n                            >\n                              <Trash2Icon className=\"size-3.5\" aria-hidden />\n                            </IconAction>\n                          ) : null}\n                          {actionKind === \"submit\" ? (\n                            <IconAction\n                              label=\"Submit for review\"\n                              disabled={actingId === product.id}\n                              onClick={() => listingAction(product)}\n                            >\n                              <SendIcon className=\"size-3.5\" aria-hidden />\n                            </IconAction>\n                          ) : null}\n                        </div>\n                      </td>\n                    </tr>\n                  );\n                })}\n              </tbody>\n            </table>\n          </div>\n        )}\n      </div>\n    </div>\n  );\n}\n"}],"meta":{"studioName":"GFA Development & Design Studio","siteUrl":"https://ui.gkhn.dev"}}