@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.
- package/dist/_internal/client/features/filters/filter-load-options.d.ts +29 -0
- package/dist/_internal/client/features/filters/filter-load-options.js +98 -0
- package/dist/_internal/server/features/classified/adapters.js +1 -1
- package/dist/_internal/server/features/digital-code/adapters.js +1 -1
- package/dist/_internal/server/features/live/adapters.js +1 -1
- package/dist/_internal/shared/actions/action-registry.js +194 -1
- package/dist/client.d.ts +2 -0
- package/dist/client.js +1 -0
- package/dist/constants/api-endpoints.d.ts +6 -0
- package/dist/constants/api-endpoints.js +2 -0
- package/dist/constants/field-names.d.ts +1 -0
- package/dist/constants/field-names.js +1 -0
- package/dist/features/about/components/PublicProfileView.js +2 -2
- package/dist/features/admin/components/AdminAddressEditorView.d.ts +7 -0
- package/dist/features/admin/components/AdminAddressEditorView.js +117 -0
- package/dist/features/admin/components/AdminCategoryEditorView.js +12 -9
- package/dist/features/admin/components/AdminFeatureFlagsView.js +58 -13
- package/dist/features/admin/components/AdminProductEditorView.d.ts +3 -1
- package/dist/features/admin/components/AdminProductEditorView.js +12 -9
- package/dist/features/admin/components/AdminReturnRequestsView.js +5 -4
- package/dist/features/admin/components/AdminReviewsView.js +5 -4
- package/dist/features/admin/components/index.d.ts +2 -0
- package/dist/features/admin/components/index.js +1 -0
- package/dist/features/auctions/components/AuctionBidsTable.d.ts +13 -0
- package/dist/features/auctions/components/AuctionBidsTable.js +62 -0
- package/dist/features/auctions/components/AuctionDetailPageView.js +2 -2
- package/dist/features/auctions/components/AuctionFilters.d.ts +7 -1
- package/dist/features/auctions/components/AuctionFilters.js +3 -2
- package/dist/features/auctions/components/index.d.ts +2 -0
- package/dist/features/auctions/components/index.js +1 -0
- package/dist/features/cart/components/CartView.js +2 -1
- package/dist/features/cart/components/CheckoutView.js +2 -1
- package/dist/features/categories/components/BrandDetailTabs.d.ts +5 -1
- package/dist/features/categories/components/BrandDetailTabs.js +29 -15
- package/dist/features/categories/components/CategoryDetailTabs.d.ts +5 -1
- package/dist/features/categories/components/CategoryDetailTabs.js +30 -15
- package/dist/features/classified/components/ClassifiedFilters.d.ts +5 -1
- package/dist/features/classified/components/ClassifiedFilters.js +3 -2
- package/dist/features/digital-codes/components/DigitalCodeFilters.d.ts +5 -1
- package/dist/features/digital-codes/components/DigitalCodeFilters.js +3 -2
- package/dist/features/events/repository/event-entry.repository.d.ts +1 -0
- package/dist/features/events/repository/event-entry.repository.js +5 -0
- package/dist/features/filters/index.d.ts +2 -0
- package/dist/features/filters/index.js +1 -0
- package/dist/features/layout/AppLayoutShell.d.ts +1 -1
- package/dist/features/layout/AppLayoutShell.js +1 -1
- package/dist/features/live/components/LiveItemFilters.d.ts +5 -1
- package/dist/features/live/components/LiveItemFilters.js +3 -2
- package/dist/features/pre-orders/components/PreOrderDetailPageView.js +2 -2
- package/dist/features/pre-orders/components/PreOrderFilters.d.ts +7 -1
- package/dist/features/pre-orders/components/PreOrderFilters.js +3 -2
- package/dist/features/products/columns/productTableColumns.js +1 -1
- package/dist/features/products/components/CompareOverlay.d.ts +2 -0
- package/dist/features/products/components/CompareOverlay.js +1 -1
- package/dist/features/products/components/ProductFilters.d.ts +7 -1
- package/dist/features/products/components/ProductFilters.js +3 -2
- package/dist/features/products/components/ProductForm.js +6 -6
- package/dist/features/products/components/ProductGrid.js +1 -1
- package/dist/features/products/repository/products.repository.d.ts +4 -0
- package/dist/features/products/repository/products.repository.js +22 -3
- package/dist/features/products/schemas/firestore.d.ts +7 -1
- package/dist/features/products/types/index.d.ts +4 -0
- package/dist/features/stores/components/StoreDetailLayoutView.js +16 -2
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/next/routing/route-map.d.ts +14 -0
- package/dist/next/routing/route-map.js +7 -0
- package/dist/seed/products-auctions-seed-data.js +40 -40
- package/dist/seed/products-preorders-seed-data.js +16 -16
- package/dist/seed/products-prize-draws-seed-data.js +4 -4
- package/dist/seed/products-standard-seed-data.js +202 -202
- package/dist/tailwind-utilities.css +1 -1
- 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
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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:
|
|
116
|
+
return _jsx("div", { className: "overflow-y-auto p-4", children: formContent });
|
|
115
117
|
}
|
|
116
|
-
|
|
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: [
|
|
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
|
|
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
|
|
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(
|
|
147
|
-
|
|
148
|
-
|
|
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:
|
|
156
|
+
return _jsx("div", { className: "overflow-y-auto p-4", children: formContent });
|
|
155
157
|
}
|
|
156
|
-
|
|
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: "
|
|
93
|
+
label: ACTIONS.ADMIN["approve-return"].label,
|
|
93
94
|
onClick: () => { setSelectedRow(rr); setApproveOpen(true); },
|
|
94
95
|
},
|
|
95
96
|
{
|
|
96
|
-
label: "
|
|
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: "
|
|
103
|
-
rejectMutation.mutate(selectedRow.id); }, isDeleting: rejectMutation.isPending, title: "
|
|
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: "
|
|
154
|
-
{ id: "reject", label: "
|
|
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: "
|
|
159
|
-
{ label: "
|
|
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
|
|
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: [
|
|
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
|
-
|
|
16
|
-
|
|
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
|
-
|
|
21
|
-
case "
|
|
22
|
-
|
|
23
|
-
case "
|
|
24
|
-
|
|
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:
|
|
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;
|