@mohasinac/appkit 2.7.30 → 2.7.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/dist/_internal/client/features/filters/filter-load-options.d.ts +29 -0
  2. package/dist/_internal/client/features/filters/filter-load-options.js +98 -0
  3. package/dist/_internal/server/features/classified/adapters.js +1 -1
  4. package/dist/_internal/server/features/digital-code/adapters.js +1 -1
  5. package/dist/_internal/server/features/live/adapters.js +1 -1
  6. package/dist/_internal/shared/actions/action-registry.js +194 -1
  7. package/dist/client.d.ts +2 -0
  8. package/dist/client.js +1 -0
  9. package/dist/constants/api-endpoints.d.ts +6 -0
  10. package/dist/constants/api-endpoints.js +2 -0
  11. package/dist/constants/field-names.d.ts +1 -0
  12. package/dist/constants/field-names.js +1 -0
  13. package/dist/features/about/components/PublicProfileView.js +2 -2
  14. package/dist/features/admin/components/AdminAddressEditorView.d.ts +7 -0
  15. package/dist/features/admin/components/AdminAddressEditorView.js +117 -0
  16. package/dist/features/admin/components/AdminCategoryEditorView.js +12 -9
  17. package/dist/features/admin/components/AdminFeatureFlagsView.js +58 -13
  18. package/dist/features/admin/components/AdminProductEditorView.d.ts +3 -1
  19. package/dist/features/admin/components/AdminProductEditorView.js +12 -9
  20. package/dist/features/admin/components/AdminReturnRequestsView.js +5 -4
  21. package/dist/features/admin/components/AdminReviewsView.js +5 -4
  22. package/dist/features/admin/components/index.d.ts +2 -0
  23. package/dist/features/admin/components/index.js +1 -0
  24. package/dist/features/auctions/components/AuctionBidsTable.d.ts +13 -0
  25. package/dist/features/auctions/components/AuctionBidsTable.js +62 -0
  26. package/dist/features/auctions/components/AuctionDetailPageView.js +2 -2
  27. package/dist/features/auctions/components/AuctionFilters.d.ts +7 -1
  28. package/dist/features/auctions/components/AuctionFilters.js +3 -2
  29. package/dist/features/auctions/components/index.d.ts +2 -0
  30. package/dist/features/auctions/components/index.js +1 -0
  31. package/dist/features/cart/components/CartView.js +2 -1
  32. package/dist/features/cart/components/CheckoutView.js +2 -1
  33. package/dist/features/categories/components/BrandDetailTabs.d.ts +5 -1
  34. package/dist/features/categories/components/BrandDetailTabs.js +29 -15
  35. package/dist/features/categories/components/CategoryDetailTabs.d.ts +5 -1
  36. package/dist/features/categories/components/CategoryDetailTabs.js +30 -15
  37. package/dist/features/classified/components/ClassifiedFilters.d.ts +5 -1
  38. package/dist/features/classified/components/ClassifiedFilters.js +3 -2
  39. package/dist/features/digital-codes/components/DigitalCodeFilters.d.ts +5 -1
  40. package/dist/features/digital-codes/components/DigitalCodeFilters.js +3 -2
  41. package/dist/features/events/repository/event-entry.repository.d.ts +1 -0
  42. package/dist/features/events/repository/event-entry.repository.js +5 -0
  43. package/dist/features/filters/index.d.ts +2 -0
  44. package/dist/features/filters/index.js +1 -0
  45. package/dist/features/layout/AppLayoutShell.d.ts +1 -1
  46. package/dist/features/layout/AppLayoutShell.js +1 -1
  47. package/dist/features/live/components/LiveItemFilters.d.ts +5 -1
  48. package/dist/features/live/components/LiveItemFilters.js +3 -2
  49. package/dist/features/pre-orders/components/PreOrderDetailPageView.js +2 -2
  50. package/dist/features/pre-orders/components/PreOrderFilters.d.ts +7 -1
  51. package/dist/features/pre-orders/components/PreOrderFilters.js +3 -2
  52. package/dist/features/products/columns/productTableColumns.js +1 -1
  53. package/dist/features/products/components/CompareOverlay.d.ts +2 -0
  54. package/dist/features/products/components/CompareOverlay.js +1 -1
  55. package/dist/features/products/components/ProductFilters.d.ts +7 -1
  56. package/dist/features/products/components/ProductFilters.js +3 -2
  57. package/dist/features/products/components/ProductForm.js +6 -6
  58. package/dist/features/products/components/ProductGrid.js +1 -1
  59. package/dist/features/products/repository/products.repository.d.ts +4 -0
  60. package/dist/features/products/repository/products.repository.js +22 -3
  61. package/dist/features/products/schemas/firestore.d.ts +7 -1
  62. package/dist/features/products/types/index.d.ts +4 -0
  63. package/dist/features/stores/components/StoreDetailLayoutView.js +16 -2
  64. package/dist/index.d.ts +6 -0
  65. package/dist/index.js +6 -0
  66. package/dist/next/routing/route-map.d.ts +14 -0
  67. package/dist/next/routing/route-map.js +7 -0
  68. package/dist/seed/products-auctions-seed-data.js +40 -40
  69. package/dist/seed/products-preorders-seed-data.js +16 -16
  70. package/dist/seed/products-prize-draws-seed-data.js +4 -4
  71. package/dist/seed/products-standard-seed-data.js +202 -202
  72. package/dist/tailwind-utilities.css +1 -1
  73. package/package.json +1 -1
@@ -1,8 +1,8 @@
1
1
  "use client";
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import React from "react";
4
4
  import { useMutation, useQuery } from "@tanstack/react-query";
5
- import { Button, Form, InlineCreateSelect, Input, StackedViewShell, Text, Toggle, useToast } from "../../../ui";
5
+ import { Button, Card, CardBody, ConfirmDeleteModal, Form, InlineCreateSelect, Input, StackedViewShell, Text, Toggle, useToast } from "../../../ui";
6
6
  import { apiClient } from "../../../http";
7
7
  import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
8
8
  import { CategoryQuickCreateForm } from "./CategoryQuickCreateForm";
@@ -28,6 +28,7 @@ function toCategorySlug(str) {
28
28
  }
29
29
  export function AdminCategoryEditorView({ categoryId, onSaved, onDeleted, embedded, ...rest }) {
30
30
  const isEdit = Boolean(categoryId);
31
+ const [deleteOpen, setDeleteOpen] = React.useState(false);
31
32
  const [name, setName] = React.useState("");
32
33
  const [slug, setSlug] = React.useState("");
33
34
  const [slugManual, setSlugManual] = React.useState(false);
@@ -99,19 +100,21 @@ export function AdminCategoryEditorView({ categoryId, onSaved, onDeleted, embedd
99
100
  onError: (err) => showToast(err?.message ?? "Failed to delete category.", "error"),
100
101
  });
101
102
  const isSubmitting = saveMutation.isPending || categoryQuery.isLoading;
102
- const formSection = (_jsxs(Form, { onSubmit: (e) => {
103
+ const actionSidebar = (_jsxs(Card, { variant: "outlined", padding: "md", className: "space-y-3", children: [_jsx(Text, { className: "text-xs font-semibold uppercase tracking-widest text-zinc-500 dark:text-zinc-400", children: "Status" }), _jsx(Text, { className: "text-sm text-[var(--appkit-color-text-muted)]", children: isEdit ? (isActive ? "Active" : "Inactive") : "New" }), _jsx(Button, { type: "submit", form: "category-editor-form", className: "w-full", isLoading: isSubmitting, disabled: !name || isSubmitting, children: isEdit ? "Save changes" : "Create category" }), isEdit && (_jsx(Button, { type: "button", variant: "danger", className: "w-full", isLoading: deleteMutation.isPending, onClick: () => setDeleteOpen(true), children: "Delete category" }))] }));
104
+ const formContent = (_jsxs(Form, { id: "category-editor-form", onSubmit: (e) => {
103
105
  e.preventDefault();
104
106
  saveMutation.mutate();
105
- }, className: "space-y-4", children: [_jsxs("div", { className: "grid sm:grid-cols-2 gap-4", children: [_jsx(Input, { label: "Category name", value: name, onChange: (e) => handleNameChange(e.target.value), required: true, placeholder: "e.g. Toys & Games" }), _jsx(Input, { label: "Slug", value: slug, onChange: (e) => {
106
- setSlug(e.target.value);
107
- setSlugManual(true);
108
- }, placeholder: "toys-and-games", helperText: "Auto-generated from name. Used in URLs." })] }), _jsx(Input, { label: "Description", value: description, onChange: (e) => setDescription(e.target.value), placeholder: "Brief description of the category" }), _jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("label", { className: "text-sm font-medium text-zinc-700 dark:text-zinc-300", children: "Parent category" }), _jsx(InlineCreateSelect, { value: parentId || null, onChange: (v) => setParentId(v ?? ""), loadOptions: loadCategoryOptions, placeholder: "Search categories\u2026 (leave empty for root)", searchPlaceholder: "Type category name\u2026", noResultsText: "No categories found", ariaLabel: "Parent category", createLabel: "Category", renderCreateForm: ({ onCreated, onCancel }) => (_jsx(CategoryQuickCreateForm, { onSaved: (id, name) => { setParentId(id); onCreated({ value: id, label: name }); }, onCancel: onCancel })) }), _jsx(Text, { className: "text-xs text-neutral-500 dark:text-neutral-400", children: "Leave empty to create a root category." })] }), _jsx(Input, { label: "Display order", value: order, onChange: (e) => setOrder(e.target.value), type: "number", min: 0, placeholder: "0" }), _jsx(Toggle, { label: "Active", checked: isActive, onChange: setIsActive }), _jsx(Toggle, { label: "Show in menu", checked: showInMenu, onChange: setShowInMenu }), _jsxs("div", { className: "flex gap-3 pt-2", children: [_jsx(Button, { type: "submit", isLoading: isSubmitting, disabled: !name || isSubmitting, children: isEdit ? "Save changes" : "Create category" }), isEdit && (_jsx(Button, { type: "button", variant: "danger", isLoading: deleteMutation.isPending, onClick: () => {
107
+ }, className: "space-y-6", children: [_jsxs(Card, { variant: "outlined", padding: "lg", children: [_jsx(Text, { className: "text-xs font-semibold uppercase tracking-widest text-zinc-500 dark:text-zinc-400 mb-4", children: "Identity" }), _jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "grid sm:grid-cols-2 gap-4", children: [_jsx(Input, { label: "Category name", value: name, onChange: (e) => handleNameChange(e.target.value), required: true, placeholder: "e.g. Toys & Games" }), _jsx(Input, { label: "Slug", value: slug, onChange: (e) => {
108
+ setSlug(e.target.value);
109
+ setSlugManual(true);
110
+ }, placeholder: "toys-and-games", helperText: "Auto-generated from name. Used in URLs." })] }), _jsx(Input, { label: "Description", value: description, onChange: (e) => setDescription(e.target.value), placeholder: "Brief description of the category" }), _jsxs("div", { className: "flex flex-col gap-1", children: [_jsx(Text, { className: "text-sm font-medium text-zinc-700 dark:text-zinc-300", children: "Parent category" }), _jsx(InlineCreateSelect, { value: parentId || null, onChange: (v) => setParentId(v ?? ""), loadOptions: loadCategoryOptions, placeholder: "Search categories\u2026 (leave empty for root)", searchPlaceholder: "Type category name\u2026", noResultsText: "No categories found", ariaLabel: "Parent category", createLabel: "Category", renderCreateForm: ({ onCreated, onCancel }) => (_jsx(CategoryQuickCreateForm, { onSaved: (id, n) => { setParentId(id); onCreated({ value: id, label: n }); }, onCancel: onCancel })) }), _jsx(Text, { className: "text-xs text-neutral-500 dark:text-neutral-400", children: "Leave empty to create a root category." })] })] })] }), _jsxs(Card, { variant: "outlined", padding: "lg", children: [_jsx(Text, { className: "text-xs font-semibold uppercase tracking-widest text-zinc-500 dark:text-zinc-400 mb-4", children: "Display" }), _jsxs("div", { className: "space-y-4", children: [_jsx(Input, { label: "Display order", value: order, onChange: (e) => setOrder(e.target.value), type: "number", min: 0, placeholder: "0" }), _jsx(Toggle, { label: "Active", checked: isActive, onChange: setIsActive }), _jsx(Toggle, { label: "Show in menu", checked: showInMenu, onChange: setShowInMenu })] })] }), _jsxs("div", { className: "flex gap-3 lg:hidden", children: [_jsx(Button, { type: "submit", isLoading: isSubmitting, disabled: !name || isSubmitting, children: isEdit ? "Save changes" : "Create category" }), isEdit && (_jsx(Button, { type: "button", variant: "danger", isLoading: deleteMutation.isPending, onClick: () => {
109
111
  if (confirm("Delete this category? Products in this category will become uncategorized.")) {
110
112
  deleteMutation.mutate();
111
113
  }
112
114
  }, children: "Delete category" }))] })] }, "cat-form"));
113
115
  if (embedded) {
114
- return _jsx("div", { className: "overflow-y-auto p-4", children: formSection });
116
+ return _jsx("div", { className: "overflow-y-auto p-4", children: formContent });
115
117
  }
116
- return (_jsx(StackedViewShell, { portal: "admin", ...rest, title: isEdit ? "Edit Category" : "Create Category", sections: [formSection] }));
118
+ const twoPanel = (_jsxs("div", { className: "grid gap-6 lg:grid-cols-[1fr_280px] lg:items-start", children: [_jsx(CardBody, { className: "min-w-0 space-y-6 p-0", children: formContent }), _jsx("div", { className: "hidden lg:block lg:sticky lg:top-[var(--header-height,0px)]", children: actionSidebar })] }));
119
+ return (_jsxs(_Fragment, { children: [_jsx(StackedViewShell, { portal: "admin", ...rest, title: isEdit ? "Edit Category" : "Create Category", sections: [twoPanel] }), _jsx(ConfirmDeleteModal, { isOpen: deleteOpen, onClose: () => setDeleteOpen(false), onConfirm: () => deleteMutation.mutate(), isDeleting: deleteMutation.isPending, title: "Delete this category?", message: "Products in this category will become uncategorized. This action cannot be undone.", confirmText: "Delete category", variant: "danger" })] }));
117
120
  }
@@ -5,6 +5,44 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
5
5
  import { Alert, Button, Div, Form, FormActions, Input, StackedViewShell, Text, Toggle, useToast, } from "../../../ui";
6
6
  import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
7
7
  import { apiClient } from "../../../http";
8
+ // Platform feature keys (flat booleans with rollout % support)
9
+ const PLATFORM_FLAGS = [
10
+ { key: "blog", label: "Blog Posts", desc: "Publish articles for admins / sellers" },
11
+ { key: "events", label: "Events", desc: "Raffles, spin wheels, polls" },
12
+ { key: "reviews", label: "Reviews", desc: "Product review system" },
13
+ { key: "wishlists", label: "Wishlists", desc: "Per-user saved lists" },
14
+ { key: "coupons", label: "Coupons", desc: "Discount code system" },
15
+ { key: "notifications", label: "Notifications", desc: "In-app notification feed" },
16
+ { key: "chats", label: "Live Chat", desc: "Buyer-seller messaging" },
17
+ { key: "sellerRegistration", label: "Seller Reg.", desc: "New vendor registration" },
18
+ { key: "smsVerification", label: "SMS Verify", desc: "SMS OTP verification" },
19
+ { key: "translations", label: "Translations", desc: "Multi-language support" },
20
+ { key: "seedPanel", label: "Seed Panel", desc: "Dev seed data panel" },
21
+ ];
22
+ // Listing type keys (nested under featureFlags.listingTypes — no rollout %)
23
+ const LISTING_TYPE_FLAGS = [
24
+ { key: "standard", label: "Standard", desc: "Fixed-price products (Phase 1)" },
25
+ { key: "auction", label: "Auction", desc: "eBay-style bidding (Phase 1)" },
26
+ { key: "pre-order", label: "Pre-order", desc: "Future delivery + deposit (Phase 1)" },
27
+ { key: "prize-draw", label: "Prize Draw", desc: "Raffle reveal window (Phase 1)" },
28
+ { key: "classified", label: "Classified", desc: "Chat-only / no cart (Phase 2)" },
29
+ { key: "digital-code", label: "Digital Code", desc: "Steam-key instant delivery (Phase 2)" },
30
+ { key: "live", label: "Live Items", desc: "Animals / plants + verification (Phase 2)" },
31
+ ];
32
+ // Category type keys (nested under featureFlags.categoryTypes — no rollout %)
33
+ const CATEGORY_TYPE_FLAGS = [
34
+ { key: "category", label: "Categories", desc: "Standard hierarchy tier 1–3" },
35
+ { key: "sublisting", label: "Sublistings", desc: "Grading / card-number sub-tiers" },
36
+ { key: "brand", label: "Brands", desc: "Brand storefront pages" },
37
+ { key: "bundle", label: "Bundles", desc: "Curated product collections" },
38
+ ];
39
+ function AccordionSection({ title, defaultOpen = true, children }) {
40
+ const [open, setOpen] = React.useState(defaultOpen);
41
+ return (_jsxs(Div, { className: "rounded-lg border border-zinc-200 dark:border-zinc-700 overflow-hidden", children: [_jsxs("button", { type: "button", onClick: () => setOpen((v) => !v), className: "w-full flex items-center justify-between px-4 py-3 bg-zinc-50 dark:bg-zinc-800/60 text-left", "aria-expanded": open, children: [_jsx(Text, { className: "text-xs font-semibold uppercase tracking-widest text-zinc-500 dark:text-zinc-400", children: title }), _jsx("svg", { className: `h-4 w-4 text-zinc-400 transition-transform ${open ? "rotate-180" : ""}`, fill: "none", stroke: "currentColor", strokeWidth: 2, viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M19 9l-7 7-7-7" }) })] }), open && (_jsx(Div, { className: "divide-y divide-zinc-100 dark:divide-zinc-800", children: children }))] }));
42
+ }
43
+ function FlagRow({ label, desc, checked, onChange, rollout, onRolloutChange, showRollout }) {
44
+ return (_jsxs(Div, { className: "flex items-center justify-between gap-4 px-4 py-3 bg-white dark:bg-zinc-900", children: [_jsxs(Div, { className: "flex-1 min-w-0", children: [_jsx(Toggle, { checked: checked, onChange: onChange, label: label }), _jsx(Text, { className: "mt-0.5 ml-10 text-xs text-zinc-400 dark:text-zinc-500 truncate", children: desc })] }), showRollout && (_jsxs(Div, { className: "flex flex-col gap-1 w-28 shrink-0", children: [_jsx(Text, { className: "text-xs text-zinc-500 dark:text-zinc-400", children: "Rollout %" }), _jsx(Input, { type: "number", min: 0, max: 100, value: rollout ?? 100, onChange: (e) => onRolloutChange?.(Math.min(100, Math.max(0, Number(e.target.value)))), disabled: !checked, className: "w-full" })] }))] }));
45
+ }
8
46
  export function AdminFeatureFlagsView({ labels = {}, renderFlags, ...rest }) {
9
47
  const queryClient = useQueryClient();
10
48
  const { showToast } = useToast();
@@ -23,6 +61,24 @@ export function AdminFeatureFlagsView({ labels = {}, renderFlags, ...rest }) {
23
61
  setFlags(data.flags ?? {});
24
62
  setRollouts(data.rollouts ?? {});
25
63
  }, [data]);
64
+ const getPlatformFlag = (key) => Boolean(flags[key]);
65
+ const setPlatformFlag = (key, val) => setFlags((prev) => ({ ...prev, [key]: val }));
66
+ const getListingTypeFlag = (key) => {
67
+ const lt = flags.listingTypes;
68
+ return lt?.[key] ?? false;
69
+ };
70
+ const setListingTypeFlag = (key, val) => setFlags((prev) => ({
71
+ ...prev,
72
+ listingTypes: { ...(prev.listingTypes ?? {}), [key]: val },
73
+ }));
74
+ const getCategoryTypeFlag = (key) => {
75
+ const ct = flags.categoryTypes;
76
+ return ct?.[key] ?? false;
77
+ };
78
+ const setCategoryTypeFlag = (key, val) => setFlags((prev) => ({
79
+ ...prev,
80
+ categoryTypes: { ...(prev.categoryTypes ?? {}), [key]: val },
81
+ }));
26
82
  const saveFlags = useMutation({
27
83
  mutationFn: async () => {
28
84
  await apiClient.put(ADMIN_ENDPOINTS.FEATURE_FLAGS, { flags, rollouts });
@@ -35,24 +91,13 @@ export function AdminFeatureFlagsView({ labels = {}, renderFlags, ...rest }) {
35
91
  showToast("Failed to save feature flags.", "error");
36
92
  },
37
93
  });
38
- const featureKeys = React.useMemo(() => Object.keys(flags), [flags]);
39
- const formatLabel = (value) => value
40
- .replace(/([A-Z])/g, " $1")
41
- .replace(/_/g, " ")
42
- .replace(/^./, (char) => char.toUpperCase())
43
- .trim();
44
94
  const defaultFlags = () => (_jsxs(Form, { onSubmit: (event) => {
45
95
  event.preventDefault();
46
96
  saveFlags.mutate();
47
- }, className: "space-y-4", children: [featureKeys.length === 0 && !isLoading && (_jsx(Text, { className: "text-sm text-zinc-500 dark:text-zinc-400", children: "No feature flags configured. Add flags to siteSettings.featureFlags to manage them here." })), featureKeys.map((key) => (_jsxs(Div, { className: "flex items-start justify-between gap-4 rounded-lg border border-zinc-200 dark:border-zinc-700 bg-white dark:bg-zinc-900 p-3", children: [_jsxs(Div, { className: "flex-1", children: [_jsx(Toggle, { checked: Boolean(flags[key]), onChange: (value) => {
48
- setFlags((prev) => ({ ...prev, [key]: value }));
49
- }, label: formatLabel(key) }), _jsx(Text, { className: "mt-0.5 ml-10 text-xs text-zinc-400 dark:text-zinc-500 font-mono", children: key })] }), _jsxs(Div, { className: "flex flex-col gap-1 w-32 shrink-0", children: [_jsx(Text, { className: "text-xs text-zinc-500 dark:text-zinc-400", children: "Rollout %" }), _jsx(Input, { type: "number", min: 0, max: 100, value: rollouts[key] ?? 100, onChange: (e) => setRollouts((prev) => ({
50
- ...prev,
51
- [key]: Math.min(100, Math.max(0, Number(e.target.value))),
52
- })), disabled: !flags[key], className: "w-full" })] })] }, key))), _jsxs(FormActions, { align: "right", children: [_jsx(Button, { type: "button", variant: "secondary", onClick: () => {
97
+ }, className: "space-y-4", children: [_jsx(AccordionSection, { title: "Platform Features", children: PLATFORM_FLAGS.map(({ key, label, desc }) => (_jsx(FlagRow, { label: label, desc: desc, checked: getPlatformFlag(key), onChange: (val) => setPlatformFlag(key, val), rollout: rollouts[key], onRolloutChange: (val) => setRollouts((prev) => ({ ...prev, [key]: val })), showRollout: true }, key))) }), _jsx(AccordionSection, { title: "Listing Types", children: LISTING_TYPE_FLAGS.map(({ key, label, desc }) => (_jsx(FlagRow, { label: label, desc: desc, checked: getListingTypeFlag(key), onChange: (val) => setListingTypeFlag(key, val), showRollout: false }, key))) }), _jsx(AccordionSection, { title: "Category Types", children: CATEGORY_TYPE_FLAGS.map(({ key, label, desc }) => (_jsx(FlagRow, { label: label, desc: desc, checked: getCategoryTypeFlag(key), onChange: (val) => setCategoryTypeFlag(key, val), showRollout: false }, key))) }), _jsxs(FormActions, { align: "right", children: [_jsx(Button, { type: "button", variant: "secondary", onClick: () => {
53
98
  setFlags(data?.flags ?? {});
54
99
  setRollouts(data?.rollouts ?? {});
55
- }, disabled: saveFlags.isPending, children: "Reset" }), _jsx(Button, { type: "submit", isLoading: saveFlags.isPending, disabled: saveFlags.isPending || featureKeys.length === 0, children: "Save feature flags" })] })] }));
100
+ }, disabled: saveFlags.isPending, children: "Reset" }), _jsx(Button, { type: "submit", isLoading: saveFlags.isPending, disabled: saveFlags.isPending, children: "Save All Flags" })] })] }));
56
101
  const loadingAlert = isLoading ? (_jsx(Alert, { variant: "info", title: "Loading feature flags", children: "Fetching feature flags\u2026" })) : null;
57
102
  const errorAlert = error ? (_jsx(Alert, { variant: "error", title: "Could not load feature flags", children: error instanceof Error ? error.message : "Unknown error" })) : null;
58
103
  return (_jsx(StackedViewShell, { portal: "admin", ...rest, title: labels.title ?? "Feature Flags", sections: [loadingAlert, errorAlert, renderFlags?.() ?? defaultFlags()] }));
@@ -4,5 +4,7 @@ export interface AdminProductEditorViewProps extends Omit<StackedViewShellProps,
4
4
  onSaved?: (id: string) => void;
5
5
  onDeleted?: () => void;
6
6
  embedded?: boolean;
7
+ /** Listing types that are enabled. When provided, restricts the mode tabs. */
8
+ enabledListingTypes?: string[];
7
9
  }
8
- export declare function AdminProductEditorView({ productId, onSaved, onDeleted, embedded, ...rest }: AdminProductEditorViewProps): import("react/jsx-runtime").JSX.Element;
10
+ export declare function AdminProductEditorView({ productId, onSaved, onDeleted, embedded, enabledListingTypes, ...rest }: AdminProductEditorViewProps): import("react/jsx-runtime").JSX.Element;
@@ -1,8 +1,8 @@
1
1
  "use client";
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import React from "react";
4
4
  import { useMutation, useQuery } from "@tanstack/react-query";
5
- import { Button, DynamicSelect, InlineCreateSelect, Form, Stack, StackedViewShell, Tabs, TabsContent, TabsList, TabsTrigger, Text, useToast, } from "../../../ui";
5
+ import { Button, Card, CardBody, ConfirmDeleteModal, DynamicSelect, InlineCreateSelect, Form, Stack, StackedViewShell, Tabs, TabsContent, TabsList, TabsTrigger, Text, useToast, } from "../../../ui";
6
6
  import { apiClient } from "../../../http";
7
7
  import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
8
8
  import { ProductForm } from "../../products/components/ProductForm";
@@ -76,10 +76,11 @@ const EMPTY_PRODUCT = {
76
76
  // SB1-G Phase 4 — listingType is the only listing-kind field on the schema.
77
77
  listingType: "standard",
78
78
  };
79
- export function AdminProductEditorView({ productId, onSaved, onDeleted, embedded, ...rest }) {
79
+ export function AdminProductEditorView({ productId, onSaved, onDeleted, embedded, enabledListingTypes, ...rest }) {
80
80
  const isEdit = Boolean(productId);
81
81
  const [product, setProduct] = React.useState(EMPTY_PRODUCT);
82
82
  const [mode, setMode] = React.useState("standard");
83
+ const [deleteOpen, setDeleteOpen] = React.useState(false);
83
84
  const { showToast } = useToast();
84
85
  const productQuery = useQuery({
85
86
  queryKey: ["admin", "product", productId],
@@ -140,18 +141,20 @@ export function AdminProductEditorView({ productId, onSaved, onDeleted, embedded
140
141
  },
141
142
  });
142
143
  const isSubmitting = saveMutation.isPending || productQuery.isLoading;
143
- const formSection = (_jsxs(Form, { onSubmit: (e) => {
144
+ const actionSidebar = (_jsxs(Card, { variant: "outlined", padding: "md", className: "space-y-3", children: [_jsx(Text, { className: "text-xs font-semibold uppercase tracking-widest text-zinc-500 dark:text-zinc-400", children: "Status" }), _jsx(Text, { className: "text-sm text-[var(--appkit-color-text-muted)]", children: isEdit ? product.status ?? "—" : "New" }), _jsx(Button, { type: "submit", form: "product-editor-form", className: "w-full", isLoading: isSubmitting, disabled: !product.title || isSubmitting, children: isEdit ? "Save changes" : "Create product" }), isEdit && (_jsx(Button, { type: "button", variant: "danger", className: "w-full", isLoading: deleteMutation.isPending, onClick: () => setDeleteOpen(true), children: "Delete product" }))] }));
145
+ const formContent = (_jsxs(Form, { id: "product-editor-form", onSubmit: (e) => {
144
146
  e.preventDefault();
145
147
  saveMutation.mutate();
146
- }, className: "space-y-6", children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Text, { className: "text-sm font-medium text-zinc-900 dark:text-zinc-100", children: "Listing type" }), _jsxs(Tabs, { value: mode, onChange: handleModeChange, children: [_jsxs(TabsList, { children: [_jsx(TabsTrigger, { value: "standard", children: "Standard" }), _jsx(TabsTrigger, { value: "auction", children: "Auction" }), _jsx(TabsTrigger, { value: "preorder", children: "Pre-order" })] }), _jsx(TabsContent, { value: "standard" }), _jsx(TabsContent, { value: "auction" }), _jsx(TabsContent, { value: "preorder" })] })] }), _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { className: "text-sm font-medium text-zinc-900 dark:text-zinc-100", children: "Store" }), _jsx(DynamicSelect, { value: product.storeId ?? null, onChange: handleStoreSelect, loadOptions: loadStoreOptions, placeholder: "Search stores\u2026", searchPlaceholder: "Type store name\u2026", noResultsText: "No stores found", ariaLabel: "Store" })] }), _jsx(ProductForm, { product: product, onChange: setProduct, renderGroupSettings: isEdit && productId
147
- ? (p) => (_jsx(GroupSettingsPanel, { productId: productId, productSlug: p.slug ?? productId, groupId: p.groupId, isGroupParent: p.isGroupParent, groupParentSlug: p.groupParentSlug, groupChildSlugs: p.groupChildSlugs, groupTitle: p.groupTitle, isAuction: modeFromProduct(p) === "auction", storeProductsEndpoint: "/api/admin/products", onGroupChanged: () => productQuery.refetch() }))
148
- : undefined, renderCategorySelector: ({ label, value, onChange, disabled }) => (_jsxs(Stack, { gap: "xs", children: [_jsx(Text, { className: "text-sm font-medium text-zinc-900 dark:text-zinc-100", children: label }), _jsx(InlineCreateSelect, { value: value || null, onChange: (v) => onChange(v ?? ""), loadOptions: loadCategoryOptions, placeholder: "Search categories\u2026", searchPlaceholder: "Type category name\u2026", noResultsText: "No categories found", ariaLabel: label, disabled: disabled, createLabel: "Category", renderCreateForm: ({ onCreated, onCancel }) => (_jsx(CategoryQuickCreateForm, { onSaved: (id, name) => onCreated({ value: id, label: name }), onCancel: onCancel })) })] })), renderBrandSelector: (args) => (_jsxs(Stack, { gap: "xs", children: [_jsx(Text, { className: "text-sm font-medium text-zinc-900 dark:text-zinc-100", children: args.label }), _jsx(InlineCreateSelect, { value: args.multi ? null : (args.value || null), onChange: (v) => args.onValueChange(v ?? ""), loadOptions: loadBrandOptions, placeholder: "Search brands\u2026", searchPlaceholder: "Type brand name\u2026", noResultsText: "No brands found", ariaLabel: args.label, disabled: args.disabled, createLabel: "Brand", renderCreateForm: ({ onCreated, onCancel }) => (_jsx(BrandQuickCreateForm, { onSaved: (id, name) => onCreated({ value: id, label: name }), onCancel: onCancel })) })] })) }), _jsxs("div", { className: "flex gap-3 pt-2", children: [_jsx(Button, { type: "submit", isLoading: isSubmitting, disabled: !product.title || isSubmitting, children: isEdit ? "Save changes" : "Create product" }), isEdit && (_jsx(Button, { type: "button", variant: "danger", isLoading: deleteMutation.isPending, onClick: () => {
148
+ }, className: "space-y-6", children: [_jsxs(Card, { variant: "outlined", padding: "lg", children: [_jsx(Text, { className: "text-xs font-semibold uppercase tracking-widest text-zinc-500 dark:text-zinc-400 mb-4", children: "Listing Type" }), _jsxs(Tabs, { value: mode, onChange: handleModeChange, children: [_jsxs(TabsList, { children: [(!enabledListingTypes || enabledListingTypes.includes("standard")) && (_jsx(TabsTrigger, { value: "standard", children: "Standard" })), (!enabledListingTypes || enabledListingTypes.includes("auction")) && (_jsx(TabsTrigger, { value: "auction", children: "Auction" })), (!enabledListingTypes || enabledListingTypes.includes("pre-order")) && (_jsx(TabsTrigger, { value: "preorder", children: "Pre-order" }))] }), _jsx(TabsContent, { value: "standard" }), _jsx(TabsContent, { value: "auction" }), _jsx(TabsContent, { value: "preorder" })] })] }), _jsxs(Card, { variant: "outlined", padding: "lg", children: [_jsx(Text, { className: "text-xs font-semibold uppercase tracking-widest text-zinc-500 dark:text-zinc-400 mb-4", children: "Classification" }), _jsxs("div", { className: "space-y-4", children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Text, { className: "text-sm font-medium text-zinc-900 dark:text-zinc-100", children: "Store" }), _jsx(DynamicSelect, { value: product.storeId ?? null, onChange: handleStoreSelect, loadOptions: loadStoreOptions, placeholder: "Search stores\u2026", searchPlaceholder: "Type store name\u2026", noResultsText: "No stores found", ariaLabel: "Store" })] }), _jsx(ProductForm, { product: product, onChange: setProduct, renderGroupSettings: isEdit && productId
149
+ ? (p) => (_jsx(GroupSettingsPanel, { productId: productId, productSlug: p.slug ?? productId, groupId: p.groupId, isGroupParent: p.isGroupParent, groupParentSlug: p.groupParentSlug, groupChildSlugs: p.groupChildSlugs, groupTitle: p.groupTitle, isAuction: modeFromProduct(p) === "auction", storeProductsEndpoint: "/api/admin/products", onGroupChanged: () => productQuery.refetch() }))
150
+ : undefined, renderCategorySelector: ({ label, value, onChange, disabled }) => (_jsxs(Stack, { gap: "xs", children: [_jsx(Text, { className: "text-sm font-medium text-zinc-900 dark:text-zinc-100", children: label }), _jsx(InlineCreateSelect, { value: value || null, onChange: (v) => onChange(v ?? ""), loadOptions: loadCategoryOptions, placeholder: "Search categories\u2026", searchPlaceholder: "Type category name\u2026", noResultsText: "No categories found", ariaLabel: label, disabled: disabled, createLabel: "Category", renderCreateForm: ({ onCreated, onCancel }) => (_jsx(CategoryQuickCreateForm, { onSaved: (id, name) => onCreated({ value: id, label: name }), onCancel: onCancel })) })] })), renderBrandSelector: (args) => (_jsxs(Stack, { gap: "xs", children: [_jsx(Text, { className: "text-sm font-medium text-zinc-900 dark:text-zinc-100", children: args.label }), _jsx(InlineCreateSelect, { value: args.multi ? null : (args.value || null), onChange: (v) => args.onValueChange(v ?? ""), loadOptions: loadBrandOptions, placeholder: "Search brands\u2026", searchPlaceholder: "Type brand name\u2026", noResultsText: "No brands found", ariaLabel: args.label, disabled: args.disabled, createLabel: "Brand", renderCreateForm: ({ onCreated, onCancel }) => (_jsx(BrandQuickCreateForm, { onSaved: (id, name) => onCreated({ value: id, label: name }), onCancel: onCancel })) })] })) })] })] }), _jsxs("div", { className: "flex gap-3 lg:hidden", children: [_jsx(Button, { type: "submit", isLoading: isSubmitting, disabled: !product.title || isSubmitting, children: isEdit ? "Save changes" : "Create product" }), isEdit && (_jsx(Button, { type: "button", variant: "danger", isLoading: deleteMutation.isPending, onClick: () => {
149
151
  if (confirm("Delete this product? This cannot be undone.")) {
150
152
  deleteMutation.mutate();
151
153
  }
152
154
  }, children: "Delete product" }))] })] }, "product-form"));
153
155
  if (embedded) {
154
- return _jsx("div", { className: "overflow-y-auto p-4", children: formSection });
156
+ return _jsx("div", { className: "overflow-y-auto p-4", children: formContent });
155
157
  }
156
- return (_jsx(StackedViewShell, { portal: "admin", ...rest, title: isEdit ? "Edit Product" : "Create Product", sections: [formSection] }));
158
+ const twoPanel = (_jsxs("div", { className: "grid gap-6 lg:grid-cols-[1fr_280px] lg:items-start", children: [_jsx(CardBody, { className: "min-w-0 space-y-6 p-0", children: formContent }), _jsx("div", { className: "hidden lg:block lg:sticky lg:top-[var(--header-height,0px)]", children: actionSidebar })] }));
159
+ return (_jsxs(_Fragment, { children: [_jsx(StackedViewShell, { portal: "admin", ...rest, title: isEdit ? "Edit Product" : "Create Product", sections: [twoPanel] }), _jsx(ConfirmDeleteModal, { isOpen: deleteOpen, onClose: () => setDeleteOpen(false), onConfirm: () => deleteMutation.mutate(), isDeleting: deleteMutation.isPending, title: "Delete this product?", message: "This product will be permanently removed. This action cannot be undone.", confirmText: "Delete product", variant: "danger" })] }));
157
160
  }
@@ -6,6 +6,7 @@ import { useUrlTable } from "../../../react/hooks/useUrlTable";
6
6
  import { useBulkSelection } from "../../../react/hooks/useBulkSelection";
7
7
  import { ListingToolbar, Pagination, ConfirmDeleteModal, RowActionMenu, useToast } from "../../../ui";
8
8
  import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
9
+ import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
9
10
  import { toRecordArray, toRelativeDate, toRupees, toStringValue, useAdminListingData, } from "../hooks/useAdminListingData";
10
11
  import { DataTable } from "./DataTable";
11
12
  import { apiClient } from "../../../http";
@@ -89,16 +90,16 @@ export function AdminReturnRequestsView({ children: _children }) {
89
90
  const rr = row;
90
91
  return (_jsx(RowActionMenu, { actions: [
91
92
  {
92
- label: "Approve return",
93
+ label: ACTIONS.ADMIN["approve-return"].label,
93
94
  onClick: () => { setSelectedRow(rr); setApproveOpen(true); },
94
95
  },
95
96
  {
96
- label: "Reject return",
97
+ label: ACTIONS.ADMIN["reject-return"].label,
97
98
  destructive: true,
98
99
  onClick: () => { setSelectedRow(rr); setRejectOpen(true); },
99
100
  },
100
101
  ] }));
101
102
  } })] })] }), _jsx(ConfirmDeleteModal, { isOpen: approveOpen, onClose: () => { setApproveOpen(false); setSelectedRow(null); }, onConfirm: () => { if (selectedRow)
102
- approveMutation.mutate(selectedRow.id); }, isDeleting: approveMutation.isPending, title: "Approve return request?", message: "The order status will be updated to Refunded. The buyer will be notified and the refund process will begin.", confirmText: "Approve return", variant: "primary" }), _jsx(ConfirmDeleteModal, { isOpen: rejectOpen, onClose: () => { setRejectOpen(false); setSelectedRow(null); }, onConfirm: () => { if (selectedRow)
103
- rejectMutation.mutate(selectedRow.id); }, isDeleting: rejectMutation.isPending, title: "Reject return request?", message: "The order status will be reverted to Delivered. The buyer's return request will be declined.", confirmText: "Reject return", variant: "danger" })] }));
103
+ approveMutation.mutate(selectedRow.id); }, isDeleting: approveMutation.isPending, title: ACTIONS.ADMIN["approve-return"].confirmation.title, message: "The order status will be updated to Refunded. The buyer will be notified and the refund process will begin.", confirmText: ACTIONS.ADMIN["approve-return"].confirmation.confirmLabel, variant: "primary" }), _jsx(ConfirmDeleteModal, { isOpen: rejectOpen, onClose: () => { setRejectOpen(false); setSelectedRow(null); }, onConfirm: () => { if (selectedRow)
104
+ rejectMutation.mutate(selectedRow.id); }, isDeleting: rejectMutation.isPending, title: ACTIONS.ADMIN["reject-return"].confirmation.title, message: "The order status will be reverted to Delivered. The buyer's return request will be declined.", confirmText: ACTIONS.ADMIN["reject-return"].confirmation.confirmLabel, variant: "danger" })] }));
104
105
  }
@@ -7,6 +7,7 @@ import { useUrlTable } from "../../../react/hooks/useUrlTable";
7
7
  import { useBulkSelection } from "../../../react/hooks/useBulkSelection";
8
8
  import { BulkActionBar, FilterChipGroup, ListingToolbar, Pagination, ListingViewShell, Modal, RowActionMenu, Button, useToast } from "../../../ui";
9
9
  import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
10
+ import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
10
11
  import { ADMIN_REVIEW_STATUS_TABS, ADMIN_REVIEW_RATING_TABS } from "../constants/filter-tabs";
11
12
  import { toRecordArray, toRelativeDate, toStringValue, useAdminListingData, } from "../hooks/useAdminListingData";
12
13
  import { DataTable } from "./DataTable";
@@ -150,13 +151,13 @@ export function AdminReviewsView({ renderDetailView, children, ...props }) {
150
151
  }
151
152
  const selection = useBulkSelection({ items: rows ?? [], keyExtractor: (r) => r.id });
152
153
  return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search reviews, products, or seller names", onSearchChange: setSearchInput, onSearchCommit: commitSearch, sortValue: table.get("sort") || DEFAULT_SORT, sortOptions: SORT_OPTIONS, onSortChange: (v) => { table.set("sort", v); }, showTableView: true, view: view, onViewChange: (v) => setView(v), onResetAll: resetAll, hasActiveState: hasActiveState }), _jsx(BulkActionBar, { selectedCount: selection.selectedCount, onClearSelection: selection.clearSelection, actions: [
153
- { id: "approve", label: "Approve Selected", variant: "primary", onClick: () => { selection.clearSelection(); } },
154
- { id: "reject", label: "Reject Selected", variant: "secondary", onClick: () => { selection.clearSelection(); } },
154
+ { id: "approve", label: `${ACTIONS.ADMIN["approve-review"].label} Selected`, variant: "primary", onClick: () => { selection.clearSelection(); } },
155
+ { id: "reject", label: `${ACTIONS.ADMIN["reject-review"].label} Selected`, variant: "secondary", onClick: () => { selection.clearSelection(); } },
155
156
  ] }), totalPages > 1 && (_jsx("div", { className: "sticky top-[calc(var(--header-height,0px)+44px)] z-10 flex justify-center bg-white/95 dark:bg-slate-900/95 backdrop-blur-sm border-b border-zinc-200 dark:border-slate-700 px-3 py-1.5", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) })), _jsxs("div", { className: "py-4 px-3 sm:px-4", children: [errorMessage && (_jsx("div", { className: "mb-4 rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700 dark:border-red-900/60 dark:bg-red-950/40 dark:text-red-200", children: errorMessage })), _jsx(DataTable, { rows: rows, isLoading: isLoading, emptyLabel: "No reviews found", renderRowActions: (row) => {
156
157
  const rr = row;
157
158
  return (_jsx(RowActionMenu, { actions: [
158
- { label: "Approve", onClick: () => patchMutation.mutate({ id: rr.id, payload: { status: "approved" } }) },
159
- { label: "Reject", onClick: () => patchMutation.mutate({ id: rr.id, payload: { status: "rejected" } }) },
159
+ { label: ACTIONS.ADMIN["approve-review"].label, onClick: () => patchMutation.mutate({ id: rr.id, payload: { status: "approved" } }) },
160
+ { label: ACTIONS.ADMIN["reject-review"].label, destructive: true, onClick: () => patchMutation.mutate({ id: rr.id, payload: { status: "rejected" } }) },
160
161
  {
161
162
  label: rr.isFeatured ? "Unfeature" : "Feature",
162
163
  onClick: () => patchMutation.mutate({ id: rr.id, payload: { featured: !rr.isFeatured } }),
@@ -30,6 +30,8 @@ export { AdminBrandsView } from "./AdminBrandsView";
30
30
  export type { AdminBrandsViewProps } from "./AdminBrandsView";
31
31
  export { AdminBrandEditorView } from "./AdminBrandEditorView";
32
32
  export type { AdminBrandEditorViewProps } from "./AdminBrandEditorView";
33
+ export { AdminAddressEditorView } from "./AdminAddressEditorView";
34
+ export type { AdminAddressEditorViewProps } from "./AdminAddressEditorView";
33
35
  export { AdminPrizeDrawsView } from "./AdminPrizeDrawsView";
34
36
  export type { AdminPrizeDrawsViewProps } from "./AdminPrizeDrawsView";
35
37
  export { AdminBundlesView } from "./AdminBundlesView";
@@ -15,6 +15,7 @@ export { AdminCategoriesView } from "./AdminCategoriesView";
15
15
  export { AdminCategoryEditorView } from "./AdminCategoryEditorView";
16
16
  export { AdminBrandsView } from "./AdminBrandsView";
17
17
  export { AdminBrandEditorView } from "./AdminBrandEditorView";
18
+ export { AdminAddressEditorView } from "./AdminAddressEditorView";
18
19
  // SB4-E 2026-05-14 — admin prize draws listing view.
19
20
  export { AdminPrizeDrawsView } from "./AdminPrizeDrawsView";
20
21
  // S-SBUNI-4 2026-05-13 — admin bundle CRUD views.
@@ -0,0 +1,13 @@
1
+ import React from "react";
2
+ import type { BidDocument } from "../schemas/firestore";
3
+ export interface AuctionWithBids {
4
+ productId: string;
5
+ productTitle: string;
6
+ bids: BidDocument[];
7
+ }
8
+ export interface AuctionBidsTableProps {
9
+ bids: BidDocument[];
10
+ portal?: "buyer" | "store" | "admin";
11
+ emptyLabel?: React.ReactNode;
12
+ }
13
+ export declare function AuctionBidsTable({ bids, portal, emptyLabel, }: AuctionBidsTableProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,62 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useMemo, useState } from "react";
4
+ import { ChevronRight } from "lucide-react";
5
+ import { Div, Row, Text, Badge } from "../../../ui";
6
+ function paise(amount) {
7
+ return new Intl.NumberFormat("en-IN", {
8
+ style: "currency",
9
+ currency: "INR",
10
+ maximumFractionDigits: 0,
11
+ }).format(amount / 100);
12
+ }
13
+ function relDate(d) {
14
+ const ms = new Date(d).getTime();
15
+ if (isNaN(ms))
16
+ return "";
17
+ const diff = Date.now() - ms;
18
+ const m = Math.floor(diff / 60000);
19
+ if (m < 1)
20
+ return "just now";
21
+ if (m < 60)
22
+ return `${m}m ago`;
23
+ const h = Math.floor(m / 60);
24
+ if (h < 24)
25
+ return `${h}h ago`;
26
+ return `${Math.floor(h / 24)}d ago`;
27
+ }
28
+ const STATUS_VARIANT = {
29
+ active: "active",
30
+ won: "active",
31
+ outbid: "pending",
32
+ lost: "info",
33
+ cancelled: "danger",
34
+ };
35
+ function groupByAuction(bids) {
36
+ const map = new Map();
37
+ for (const bid of bids) {
38
+ if (!map.has(bid.productId)) {
39
+ map.set(bid.productId, {
40
+ productId: bid.productId,
41
+ productTitle: bid.productTitle,
42
+ bids: [],
43
+ });
44
+ }
45
+ map.get(bid.productId).bids.push(bid);
46
+ }
47
+ return Array.from(map.values());
48
+ }
49
+ function AuctionRow({ auction, portal, }) {
50
+ const [expanded, setExpanded] = useState(false);
51
+ const sorted = useMemo(() => [...auction.bids].sort((a, b) => b.bidAmount - a.bidAmount), [auction.bids]);
52
+ const highest = sorted[0]?.bidAmount ?? 0;
53
+ const isWinning = auction.bids.some((b) => b.isWinning);
54
+ return (_jsxs(Div, { className: "border border-[var(--appkit-color-border)] rounded-xl overflow-hidden bg-[var(--appkit-color-surface)] shadow-sm", children: [_jsxs("button", { type: "button", onClick: () => setExpanded((v) => !v), className: "w-full flex items-center gap-3 px-4 py-3 hover:bg-[var(--appkit-color-border-subtle)] transition-colors text-left", "aria-expanded": expanded, children: [_jsx(ChevronRight, { className: `shrink-0 text-[var(--appkit-color-text-muted)] transition-transform ${expanded ? "rotate-90" : ""}`, size: 16 }), _jsx(Div, { className: "flex-1 min-w-0", children: _jsx(Text, { className: "text-sm font-semibold text-[var(--appkit-color-text)] line-clamp-1", children: auction.productTitle }) }), _jsxs(Row, { gap: "sm", className: "shrink-0 items-center", children: [isWinning && (_jsx(Badge, { variant: "active", children: "Winning" })), _jsxs(Text, { variant: "secondary", className: "text-xs", children: [auction.bids.length, " bid", auction.bids.length !== 1 ? "s" : ""] }), _jsx(Text, { className: "text-sm font-semibold text-[var(--appkit-color-text)]", children: paise(highest) })] })] }), expanded && (_jsxs(Div, { className: "border-t border-[var(--appkit-color-border)]", children: [_jsxs("div", { className: "grid text-xs font-medium text-[var(--appkit-color-text-muted)] uppercase tracking-wide px-4 py-2 border-b border-[var(--appkit-color-border-subtle)]", style: { gridTemplateColumns: portal === "buyer" ? "1fr auto auto" : "1fr 1fr auto auto" }, children: [_jsx("span", { children: portal === "buyer" ? "Amount" : "Bidder" }), portal !== "buyer" && _jsx("span", { children: "Amount" }), _jsx("span", { children: "Status" }), _jsx("span", { className: "text-right", children: "Time" })] }), sorted.map((bid) => (_jsxs("div", { className: "grid items-center px-4 py-2.5 border-b border-[var(--appkit-color-border-subtle)] last:border-0 hover:bg-[var(--appkit-color-border-subtle)] transition-colors", style: { gridTemplateColumns: portal === "buyer" ? "1fr auto auto" : "1fr 1fr auto auto" }, children: [portal !== "buyer" && (_jsx(Text, { className: "text-sm text-[var(--appkit-color-text)] truncate pr-3", children: bid.userName || bid.userId })), _jsx(Text, { className: "text-sm font-medium text-[var(--appkit-color-text)]", children: paise(bid.bidAmount) }), _jsx(Badge, { variant: STATUS_VARIANT[bid.status] ?? "pending", className: "capitalize", children: bid.status }), _jsx(Text, { variant: "secondary", className: "text-xs text-right", children: relDate(bid.bidDate) })] }, bid.id)))] }))] }));
55
+ }
56
+ export function AuctionBidsTable({ bids, portal = "buyer", emptyLabel = "No bids found.", }) {
57
+ const auctions = useMemo(() => groupByAuction(bids), [bids]);
58
+ if (auctions.length === 0) {
59
+ return (_jsx(Div, { className: "py-24 text-center", children: _jsx(Text, { variant: "secondary", children: emptyLabel }) }));
60
+ }
61
+ return (_jsx(Div, { className: "space-y-3", children: auctions.map((auction) => (_jsx(AuctionRow, { auction: auction, portal: portal }, auction.productId))) }));
62
+ }
@@ -89,8 +89,8 @@ export async function AuctionDetailPageView({ id, initialAuction, onPlaceBid, pr
89
89
  const storeHref = storeSlug
90
90
  ? String(ROUTES.PUBLIC.STORE_DETAIL(storeSlug))
91
91
  : null;
92
- const category = typeof p.category === "string" ? p.category : null;
93
- const categoryName = typeof p.categoryName === "string" ? p.categoryName : null;
92
+ const category = Array.isArray(p.categorySlugs) && p.categorySlugs.length > 0 ? p.categorySlugs[0] : (typeof p.category === "string" ? p.category : null);
93
+ const categoryName = Array.isArray(p.categoryNames) && p.categoryNames.length > 0 ? p.categoryNames[0] : (typeof p.categoryName === "string" ? p.categoryName : null);
94
94
  const brand = typeof p.brand === "string" ? p.brand : null;
95
95
  const brandSlug = typeof p.brandSlug === "string" ? p.brandSlug : null;
96
96
  const tags = Array.isArray(p.tags) ? p.tags : [];
@@ -1,10 +1,16 @@
1
1
  import type { FacetOption } from "../../filters/FilterFacetSection";
2
2
  import type { UrlTable } from "../../filters/FilterPanel";
3
+ import type { AsyncFacetSectionProps } from "../../filters/AsyncFacetSection";
4
+ type LoadOptionsFn = AsyncFacetSectionProps["loadOptions"];
3
5
  export interface AuctionFiltersProps {
4
6
  table: UrlTable;
5
7
  categoryOptions?: FacetOption[];
6
8
  brandOptions?: FacetOption[];
7
9
  storeOptions?: FacetOption[];
10
+ loadCategoryOptions?: LoadOptionsFn;
11
+ loadBrandOptions?: LoadOptionsFn;
12
+ loadStoreOptions?: LoadOptionsFn;
8
13
  currencyPrefix?: string;
9
14
  }
10
- export declare function AuctionFilters({ table, categoryOptions, brandOptions, storeOptions, currencyPrefix, }: AuctionFiltersProps): import("react/jsx-runtime").JSX.Element;
15
+ export declare function AuctionFilters({ table, categoryOptions, brandOptions, storeOptions, loadCategoryOptions, loadBrandOptions, loadStoreOptions, currencyPrefix, }: AuctionFiltersProps): import("react/jsx-runtime").JSX.Element;
16
+ export {};
@@ -2,9 +2,10 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useTranslations } from "next-intl";
4
4
  import { FilterFacetSection } from "../../filters/FilterFacetSection";
5
+ import { AsyncFacetSection } from "../../filters/AsyncFacetSection";
5
6
  import { RangeFilter } from "../../filters/RangeFilter";
6
7
  import { Div } from "../../../ui";
7
- export function AuctionFilters({ table, categoryOptions = [], brandOptions = [], storeOptions = [], currencyPrefix = "", }) {
8
+ export function AuctionFilters({ table, categoryOptions = [], brandOptions = [], storeOptions = [], loadCategoryOptions, loadBrandOptions, loadStoreOptions, currencyPrefix = "", }) {
8
9
  const t = useTranslations("filters");
9
10
  const selectedCategories = table.get("category")
10
11
  ? table.get("category").split("|").filter(Boolean)
@@ -13,5 +14,5 @@ export function AuctionFilters({ table, categoryOptions = [], brandOptions = [],
13
14
  const selectedStores = table.get("storeId")
14
15
  ? table.get("storeId").split("|").filter(Boolean)
15
16
  : [];
16
- return (_jsxs(Div, { children: [categoryOptions.length > 0 && (_jsx(FilterFacetSection, { title: t("category"), options: categoryOptions, selected: selectedCategories, onChange: (vals) => table.set("category", vals.join("|")), searchable: true, defaultCollapsed: categoryOptions.length > 6 })), brandOptions.length > 0 && (_jsx(FilterFacetSection, { title: t("brand"), options: brandOptions, selected: selectedBrands, onChange: (vals) => table.set("brand", vals[0] ?? ""), searchable: brandOptions.length > 4, defaultCollapsed: brandOptions.length > 6 })), _jsx(RangeFilter, { title: t("bidPriceRange"), minValue: table.get("minBid"), maxValue: table.get("maxBid"), onMinChange: (v) => table.set("minBid", v), onMaxChange: (v) => table.set("maxBid", v), prefix: currencyPrefix, showSlider: true, minBound: 0, maxBound: 500000, step: 500, minPlaceholder: t("minBidPrice"), maxPlaceholder: t("maxBidPrice"), defaultCollapsed: false }), storeOptions.length > 0 && (_jsx(FilterFacetSection, { title: t("store"), options: storeOptions, selected: selectedStores, onChange: (vals) => table.set("storeId", vals[0] ?? ""), searchable: storeOptions.length > 4, defaultCollapsed: storeOptions.length > 6 })), _jsx(RangeFilter, { title: t("dateRange"), type: "date", minValue: table.get("dateFrom"), maxValue: table.get("dateTo"), onMinChange: (v) => table.set("dateFrom", v), onMaxChange: (v) => table.set("dateTo", v), minPlaceholder: t("minDate"), maxPlaceholder: t("maxDate"), defaultCollapsed: false })] }));
17
+ return (_jsxs(Div, { children: [loadCategoryOptions ? (_jsx(AsyncFacetSection, { title: t("category"), loadOptions: loadCategoryOptions, selected: selectedCategories, onChange: (vals) => table.set("category", vals.join("|")), defaultCollapsed: false })) : categoryOptions.length > 0 ? (_jsx(FilterFacetSection, { title: t("category"), options: categoryOptions, selected: selectedCategories, onChange: (vals) => table.set("category", vals.join("|")), searchable: true, defaultCollapsed: categoryOptions.length > 6 })) : null, loadBrandOptions ? (_jsx(AsyncFacetSection, { title: t("brand"), loadOptions: loadBrandOptions, selected: selectedBrands, onChange: (vals) => table.set("brand", vals[0] ?? ""), defaultCollapsed: false })) : brandOptions.length > 0 ? (_jsx(FilterFacetSection, { title: t("brand"), options: brandOptions, selected: selectedBrands, onChange: (vals) => table.set("brand", vals[0] ?? ""), searchable: brandOptions.length > 4, defaultCollapsed: brandOptions.length > 6 })) : null, _jsx(RangeFilter, { title: t("bidPriceRange"), minValue: table.get("minBid"), maxValue: table.get("maxBid"), onMinChange: (v) => table.set("minBid", v), onMaxChange: (v) => table.set("maxBid", v), prefix: currencyPrefix, showSlider: true, minBound: 0, maxBound: 500000, step: 500, minPlaceholder: t("minBidPrice"), maxPlaceholder: t("maxBidPrice"), defaultCollapsed: false }), loadStoreOptions ? (_jsx(AsyncFacetSection, { title: t("store"), loadOptions: loadStoreOptions, selected: selectedStores, onChange: (vals) => table.set("storeId", vals[0] ?? ""), defaultCollapsed: false })) : storeOptions.length > 0 ? (_jsx(FilterFacetSection, { title: t("store"), options: storeOptions, selected: selectedStores, onChange: (vals) => table.set("storeId", vals[0] ?? ""), searchable: storeOptions.length > 4, defaultCollapsed: storeOptions.length > 6 })) : null, _jsx(RangeFilter, { title: t("dateRange"), type: "date", minValue: table.get("dateFrom"), maxValue: table.get("dateTo"), onMinChange: (v) => table.set("dateFrom", v), onMaxChange: (v) => table.set("dateTo", v), minPlaceholder: t("minDate"), maxPlaceholder: t("maxDate"), defaultCollapsed: false })] }));
17
18
  }
@@ -3,3 +3,5 @@ export { MarketplaceAuctionGrid, type MarketplaceAuctionGridLabels, type Marketp
3
3
  export { AuctionsListView } from "./AuctionsListView";
4
4
  export { AuctionDetailPageView } from "./AuctionDetailPageView";
5
5
  export type { AuctionDetailPageViewProps } from "./AuctionDetailPageView";
6
+ export { AuctionBidsTable } from "./AuctionBidsTable";
7
+ export type { AuctionBidsTableProps, AuctionWithBids } from "./AuctionBidsTable";
@@ -2,3 +2,4 @@ export { MarketplaceAuctionCard, } from "./MarketplaceAuctionCard";
2
2
  export { MarketplaceAuctionGrid, } from "./MarketplaceAuctionGrid";
3
3
  export { AuctionsListView } from "./AuctionsListView";
4
4
  export { AuctionDetailPageView } from "./AuctionDetailPageView";
5
+ export { AuctionBidsTable } from "./AuctionBidsTable";
@@ -1,7 +1,8 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Div, Heading } from "../../../ui";
3
+ import { AdSlot } from "../../homepage/components/AdSlot";
3
4
  export function CartView({ labels = {}, renderItems, renderGroups, groups = [], renderSummary, renderPromoCode, renderCheckoutButton, renderEmpty, isEmpty = false, isLoading = false, className = "", }) {
4
5
  if (isEmpty && renderEmpty)
5
6
  return _jsx(_Fragment, { children: renderEmpty() });
6
- return (_jsxs(Div, { className: className, children: [labels.title && (_jsx(Heading, { level: 1, className: "text-2xl font-bold mb-6", children: labels.title })), _jsxs(Div, { className: "flex flex-col gap-6 lg:flex-row lg:gap-8", children: [_jsxs(Div, { className: "flex-1", children: [renderGroups ? renderGroups(groups, isLoading) : renderItems(isLoading), !renderGroups && renderPromoCode?.(), renderGroups && renderPromoCode?.()] }), _jsxs(Div, { className: "w-full lg:w-80", children: [renderSummary?.(), renderCheckoutButton?.(() => { }, isLoading)] })] })] }));
7
+ return (_jsxs(Div, { className: className, children: [labels.title && (_jsx(Heading, { level: 1, className: "text-2xl font-bold mb-6", children: labels.title })), _jsxs(Div, { className: "flex flex-col gap-6 lg:flex-row lg:gap-8", children: [_jsxs(Div, { className: "flex-1", children: [renderGroups ? renderGroups(groups, isLoading) : renderItems(isLoading), !renderGroups && renderPromoCode?.(), renderGroups && renderPromoCode?.(), _jsx(AdSlot, { id: "cart-upsell", className: "mt-6" })] }), _jsxs(Div, { className: "w-full lg:w-80", children: [renderSummary?.(), renderCheckoutButton?.(() => { }, isLoading)] })] })] }));
7
8
  }
@@ -2,6 +2,7 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import React from "react";
4
4
  import { Div, Heading } from "../../../ui";
5
+ import { AdSlot } from "../../homepage/components/AdSlot";
5
6
  export function CheckoutView({ labels = {}, renderStep, renderStepIndicator, renderOrderSummary, totalSteps = 3, initialStep = 0, activeStep, onStepChange, className = "", }) {
6
7
  const [internalStep, setInternalStep] = React.useState(initialStep);
7
8
  const step = activeStep ?? internalStep;
@@ -14,5 +15,5 @@ export function CheckoutView({ labels = {}, renderStep, renderStepIndicator, ren
14
15
  }
15
16
  onStepChange?.(nextStep);
16
17
  }, [activeStep, onStepChange, step]);
17
- return (_jsxs(Div, { className: className, children: [labels.title && (_jsx(Heading, { level: 1, className: "text-2xl font-bold mb-6", children: labels.title })), renderStepIndicator?.(step, totalSteps, setStep), _jsxs(Div, { className: "flex flex-col gap-6 lg:flex-row lg:gap-8", children: [_jsx(Div, { className: "flex-1", children: renderStep(step, setStep) }), renderOrderSummary && (_jsx(Div, { className: "w-full lg:w-80", children: renderOrderSummary() }))] })] }));
18
+ return (_jsxs(Div, { className: className, children: [labels.title && (_jsx(Heading, { level: 1, className: "text-2xl font-bold mb-6", children: labels.title })), renderStepIndicator?.(step, totalSteps, setStep), _jsxs(Div, { className: "flex flex-col gap-6 lg:flex-row lg:gap-8", children: [_jsxs(Div, { className: "flex-1", children: [renderStep(step, setStep), _jsx(AdSlot, { id: "checkout-upsell", className: "mt-6" })] }), renderOrderSummary && (_jsx(Div, { className: "w-full lg:w-80", children: renderOrderSummary() }))] })] }));
18
19
  }
@@ -10,5 +10,9 @@ export interface BrandDetailTabsProps {
10
10
  prizeDraws?: number;
11
11
  bundles?: number;
12
12
  };
13
+ /** Enabled listing types (e.g. ["standard","auction"]). When omitted, all tabs shown. */
14
+ enabledListingTypes?: string[];
15
+ /** Enabled category types (e.g. ["category","brand","bundle"]). When omitted, all tabs shown. */
16
+ enabledCategoryTypes?: string[];
13
17
  }
14
- export declare function BrandDetailTabs({ brandName, initialProductsData, initialBundles, counts, }: BrandDetailTabsProps): import("react/jsx-runtime").JSX.Element;
18
+ export declare function BrandDetailTabs({ brandName, initialProductsData, initialBundles, counts, enabledListingTypes, enabledCategoryTypes, }: BrandDetailTabsProps): import("react/jsx-runtime").JSX.Element;
@@ -12,25 +12,39 @@ function tabLabel(label, count) {
12
12
  return label;
13
13
  return `${label} (${count.toLocaleString()})`;
14
14
  }
15
- export function BrandDetailTabs({ brandName, initialProductsData, initialBundles = [], counts, }) {
16
- const [activeTab, setActiveTab] = useState("products");
15
+ const TAB_TYPE_MAP = {
16
+ products: { kind: "listing", type: "standard" },
17
+ auctions: { kind: "listing", type: "auction" },
18
+ "pre-orders": { kind: "listing", type: "pre-order" },
19
+ "prize-draws": { kind: "listing", type: "prize-draw" },
20
+ bundles: { kind: "category", type: "bundle" },
21
+ };
22
+ export function BrandDetailTabs({ brandName, initialProductsData, initialBundles = [], counts, enabledListingTypes, enabledCategoryTypes, }) {
23
+ const visibleTabs = CATEGORY_PAGE_TABS.filter((t) => {
24
+ const mapping = TAB_TYPE_MAP[t.id];
25
+ if (!mapping)
26
+ return true;
27
+ if (mapping.kind === "listing" && enabledListingTypes) {
28
+ return enabledListingTypes.includes(mapping.type);
29
+ }
30
+ if (mapping.kind === "category" && enabledCategoryTypes) {
31
+ return enabledCategoryTypes.includes(mapping.type);
32
+ }
33
+ return true;
34
+ });
35
+ const firstTabId = (visibleTabs[0]?.id ?? "products");
36
+ const [activeTab, setActiveTab] = useState(firstTabId);
17
37
  const countFor = (id) => {
18
38
  switch (id) {
19
- case "products":
20
- return counts?.products;
21
- case "auctions":
22
- return counts?.auctions;
23
- case "pre-orders":
24
- return counts?.preOrders;
25
- case "prize-draws":
26
- return counts?.prizeDraws;
27
- case "bundles":
28
- return counts?.bundles;
29
- default:
30
- return undefined;
39
+ case "products": return counts?.products;
40
+ case "auctions": return counts?.auctions;
41
+ case "pre-orders": return counts?.preOrders;
42
+ case "prize-draws": return counts?.prizeDraws;
43
+ case "bundles": return counts?.bundles;
44
+ default: return undefined;
31
45
  }
32
46
  };
33
- return (_jsxs(_Fragment, { children: [_jsx("div", { className: "flex border-b border-zinc-200 dark:border-slate-700 mb-6 overflow-x-auto", children: CATEGORY_PAGE_TABS.map((t) => (_jsx("button", { type: "button", onClick: () => setActiveTab(t.id), className: `px-5 py-2.5 text-sm font-medium whitespace-nowrap transition-colors -mb-px border-b-2 ${activeTab === t.id
47
+ return (_jsxs(_Fragment, { children: [_jsx("div", { className: "flex border-b border-zinc-200 dark:border-slate-700 mb-6 overflow-x-auto", children: visibleTabs.map((t) => (_jsx("button", { type: "button", onClick: () => setActiveTab(t.id), className: `px-5 py-2.5 text-sm font-medium whitespace-nowrap transition-colors -mb-px border-b-2 ${activeTab === t.id
34
48
  ? "border-primary text-primary"
35
49
  : "border-transparent text-zinc-500 hover:text-zinc-900 dark:hover:text-zinc-100"}`, children: tabLabel(t.label, countFor(t.id)) }, t.id))) }), activeTab === "products" && (_jsx(CategoryProductsListing, { categorySlug: "", brandName: brandName, initialData: initialProductsData })), activeTab === "auctions" && (_jsx(AuctionsIndexListing, { brandName: brandName })), activeTab === "pre-orders" && (_jsx(PreOrdersIndexListing, { brandName: brandName })), activeTab === "prize-draws" && (_jsx(PrizeDrawsIndexListing, { brandName: brandName })), activeTab === "bundles" && (_jsx(CategoryBundlesListing, { initialBundles: initialBundles, brandName: brandName }))] }));
36
50
  }
@@ -11,5 +11,9 @@ export interface CategoryDetailTabsProps {
11
11
  prizeDraws?: number;
12
12
  bundles?: number;
13
13
  };
14
+ /** Enabled listing types (e.g. ["standard","auction","pre-order"]). When omitted, all tabs shown. */
15
+ enabledListingTypes?: string[];
16
+ /** Enabled category types (e.g. ["category","brand","bundle"]). When omitted, all tabs shown. */
17
+ enabledCategoryTypes?: string[];
14
18
  }
15
- export declare function CategoryDetailTabs({ categorySlug, categoryId, initialProductsData, initialBundles, counts, }: CategoryDetailTabsProps): import("react/jsx-runtime").JSX.Element;
19
+ export declare function CategoryDetailTabs({ categorySlug, categoryId, initialProductsData, initialBundles, counts, enabledListingTypes, enabledCategoryTypes, }: CategoryDetailTabsProps): import("react/jsx-runtime").JSX.Element;