@mohasinac/appkit 4.11.2 → 4.11.3
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/server/features/checkout/actions.d.ts +4 -0
- package/dist/_internal/server/features/checkout/actions.js +111 -4
- package/dist/_internal/server/features/checkout/data.d.ts +10 -0
- package/dist/_internal/server/features/orders/adapters.js +7 -0
- package/dist/_internal/server/features/products/data.d.ts +12 -3
- package/dist/_internal/server/features/products/data.js +55 -4
- package/dist/_internal/server/features/products/index.d.ts +1 -1
- package/dist/_internal/server/features/products/index.js +1 -1
- package/dist/_internal/server/features/products/list-public.d.ts +19 -0
- package/dist/_internal/server/features/products/list-public.js +39 -6
- package/dist/_internal/server/features/reviews/data.d.ts +1 -1
- package/dist/_internal/server/features/reviews/data.js +3 -2
- package/dist/_internal/server/features/tester/visibility.js +3 -3
- package/dist/_internal/server/jobs/core/testerSandboxCleanup.d.ts +1 -1
- package/dist/_internal/server/jobs/core/testerSandboxCleanup.js +4 -1
- package/dist/_internal/server/jobs/core/testerSandboxRefresh.js +2 -0
- package/dist/_internal/server/jobs/handlers/_helpers.js +4 -1
- package/dist/_internal/shared/features/promotions/schema.d.ts +0 -16
- package/dist/_internal/shared/features/promotions/schema.js +0 -2
- package/dist/_internal/shared/features/reviews/config.d.ts +8 -1
- package/dist/_internal/shared/features/reviews/config.js +8 -1
- package/dist/_internal/shared/listing-types/_registry.d.ts +88 -1
- package/dist/_internal/shared/listing-types/_registry.js +63 -1
- package/dist/_internal/shared/listing-types/art/config.d.ts +59 -0
- package/dist/_internal/shared/listing-types/art/config.js +12 -0
- package/dist/_internal/shared/listing-types/auction/config.d.ts +53 -0
- package/dist/_internal/shared/listing-types/auction/config.js +17 -0
- package/dist/_internal/shared/listing-types/classified/config.d.ts +59 -0
- package/dist/_internal/shared/listing-types/classified/config.js +14 -0
- package/dist/_internal/shared/listing-types/digital-code/config.d.ts +59 -0
- package/dist/_internal/shared/listing-types/digital-code/config.js +12 -0
- package/dist/_internal/shared/listing-types/live/config.d.ts +59 -0
- package/dist/_internal/shared/listing-types/live/config.js +14 -0
- package/dist/_internal/shared/listing-types/pre-order/config.d.ts +110 -0
- package/dist/_internal/shared/listing-types/pre-order/config.js +14 -0
- package/dist/_internal/shared/listing-types/prize-draw/config.d.ts +44 -0
- package/dist/_internal/shared/listing-types/prize-draw/config.js +10 -0
- package/dist/_internal/shared/listing-types/standard/config.d.ts +59 -0
- package/dist/_internal/shared/listing-types/standard/config.js +9 -0
- package/dist/_internal/shared/listing-types/stickers/config.d.ts +59 -0
- package/dist/_internal/shared/listing-types/stickers/config.js +12 -0
- package/dist/client.d.ts +5 -1
- package/dist/client.js +4 -1
- package/dist/constants/field-names.d.ts +12 -0
- package/dist/constants/field-names.js +12 -0
- package/dist/constants/table-keys.d.ts +2 -0
- package/dist/constants/table-keys.js +2 -0
- package/dist/features/account/components/UserOrdersView.js +27 -5
- package/dist/features/admin/components/AdminAddressesView.js +3 -1
- package/dist/features/admin/components/AdminCouponEditorView.js +1 -4
- package/dist/features/admin/components/AdminPaymentMethodsView.js +3 -1
- package/dist/features/admin/components/AdminProductsView.js +34 -18
- package/dist/features/admin/constants/filter-tabs.d.ts +26 -9
- package/dist/features/admin/constants/filter-tabs.js +22 -9
- package/dist/features/auctions/components/AuctionBidsTable.js +1 -1
- package/dist/features/auctions/components/AuctionBottomActions.js +1 -0
- package/dist/features/auctions/components/AuctionDetailPageView.js +13 -11
- package/dist/features/cart/schemas/firestore.d.ts +0 -2
- package/dist/features/categories/components/BrandDetailTabs.js +31 -11
- package/dist/features/categories/components/CategoryDetailTabs.js +27 -7
- package/dist/features/categories/components/CategoryProductsListing.d.ts +9 -1
- package/dist/features/categories/components/CategoryProductsListing.js +5 -2
- package/dist/features/classified/components/ClassifiedDetailPageView.js +2 -1
- package/dist/features/contact/email.js +5 -1
- package/dist/features/digital-codes/components/DigitalCodeDetailPageView.js +17 -16
- package/dist/features/homepage/components/FeaturedResultsSection.js +1 -1
- package/dist/features/homepage/components/SectionCarousel.js +1 -1
- package/dist/features/homepage/components/ShopByCategorySection.js +1 -1
- package/dist/features/layout/AppLayoutShell.js +9 -2
- package/dist/features/layout/BottomActions.js +26 -3
- package/dist/features/layout/BottomActionsContext.d.ts +14 -0
- package/dist/features/layout/BottomActionsContext.js +13 -1
- package/dist/features/layout/hooks/useBottomActions.d.ts +6 -1
- package/dist/features/layout/hooks/useBottomActions.js +4 -1
- package/dist/features/layout/index.d.ts +1 -1
- package/dist/features/live/components/LiveItemDetailPageView.js +7 -6
- package/dist/features/orders/repository/orders.repository.d.ts +4 -0
- package/dist/features/orders/repository/orders.repository.js +4 -0
- package/dist/features/orders/types/index.d.ts +16 -0
- package/dist/features/pre-orders/components/PreOrderBottomActions.d.ts +1 -1
- package/dist/features/pre-orders/components/PreOrderBottomActions.js +3 -19
- package/dist/features/products/components/AuctionsIndexListing.js +1 -2
- package/dist/features/products/components/ListingBottomActions.d.ts +28 -0
- package/dist/features/products/components/ListingBottomActions.js +25 -0
- package/dist/features/products/components/PrizeDrawBottomActions.d.ts +1 -1
- package/dist/features/products/components/PrizeDrawBottomActions.js +3 -21
- package/dist/features/products/components/ProductDetailActions.js +4 -0
- package/dist/features/products/components/ProductDetailPageView.js +7 -7
- package/dist/features/products/constants/listing-tabs.d.ts +43 -198
- package/dist/features/products/constants/listing-tabs.js +71 -58
- package/dist/features/products/constants/sieve.d.ts +104 -1
- package/dist/features/products/constants/sieve.js +27 -7
- package/dist/features/products/repository/products.repository.d.ts +45 -1
- package/dist/features/products/repository/products.repository.js +26 -3
- package/dist/features/products/utils/listing-badge-variant.d.ts +20 -1
- package/dist/features/products/utils/listing-badge-variant.js +26 -1
- package/dist/features/products/utils/listing-type.d.ts +24 -0
- package/dist/features/products/utils/listing-type.js +48 -0
- package/dist/features/promotions/actions/coupon-actions.d.ts +7 -0
- package/dist/features/promotions/actions/coupon-actions.js +12 -1
- package/dist/features/promotions/actions/coupon-stacking.d.ts +24 -0
- package/dist/features/promotions/actions/coupon-stacking.js +21 -0
- package/dist/features/promotions/actions/index.d.ts +1 -0
- package/dist/features/promotions/actions/index.js +1 -0
- package/dist/features/promotions/actions/seller-coupon-actions.d.ts +0 -1
- package/dist/features/promotions/actions/seller-coupon-actions.js +0 -1
- package/dist/features/promotions/components/CouponHelpDetails.d.ts +20 -0
- package/dist/features/promotions/components/CouponHelpDetails.js +17 -0
- package/dist/features/promotions/components/CouponsIndexListing.js +2 -1
- package/dist/features/promotions/components/index.d.ts +2 -0
- package/dist/features/promotions/components/index.js +1 -0
- package/dist/features/promotions/constants/coupon-help.d.ts +27 -0
- package/dist/features/promotions/constants/coupon-help.js +42 -0
- package/dist/features/promotions/repository/coupons.repository.d.ts +10 -1
- package/dist/features/promotions/repository/coupons.repository.js +22 -8
- package/dist/features/promotions/schemas/firestore.d.ts +0 -1
- package/dist/features/promotions/schemas/firestore.js +1 -1
- package/dist/features/promotions/schemas/index.d.ts +0 -20
- package/dist/features/promotions/schemas/index.js +0 -1
- package/dist/features/reviews/actions/review-actions.js +5 -1
- package/dist/features/reviews/components/ReviewDetailPageView.js +2 -1
- package/dist/features/reviews/components/ReviewFilters.d.ts +7 -1
- package/dist/features/reviews/components/ReviewFilters.js +4 -3
- package/dist/features/reviews/components/ReviewsIndexListing.d.ts +6 -0
- package/dist/features/reviews/components/ReviewsIndexListing.js +9 -109
- package/dist/features/reviews/components/ReviewsListingPanel.d.ts +41 -0
- package/dist/features/reviews/components/ReviewsListingPanel.js +226 -0
- package/dist/features/reviews/components/index.d.ts +2 -0
- package/dist/features/reviews/components/index.js +1 -0
- package/dist/features/reviews/hooks/useReviews.d.ts +8 -0
- package/dist/features/reviews/hooks/useReviews.js +14 -6
- package/dist/features/reviews/repository/reviews.repository.d.ts +29 -1
- package/dist/features/reviews/repository/reviews.repository.js +77 -41
- package/dist/features/reviews/types/index.d.ts +8 -1
- package/dist/features/seller/components/SellerProductsCards.js +2 -2
- package/dist/features/seller/components/SellerProductsView.js +21 -18
- package/dist/features/stores/actions/store-query-actions.js +1 -1
- package/dist/features/stores/components/StoreAuctionsListing.js +5 -11
- package/dist/features/stores/components/StorePreOrdersListing.js +6 -11
- package/dist/features/stores/components/StoreReviewsListing.d.ts +4 -0
- package/dist/features/stores/components/StoreReviewsListing.js +7 -108
- package/dist/features/stores/schemas/index.d.ts +2 -2
- package/dist/features/tester/seed-data/coupons-tester-seed-data.js +3 -3
- package/dist/features/tester/seed-data/index.d.ts +1 -0
- package/dist/features/tester/seed-data/index.js +1 -0
- package/dist/features/tester/seed-data/offers-tester-seed-data.d.ts +2 -0
- package/dist/features/tester/seed-data/offers-tester-seed-data.js +61 -0
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +337 -1
- package/dist/index.d.ts +9 -3
- package/dist/index.js +11 -2
- package/dist/react/hooks/useListingTypeFlags.d.ts +12 -19
- package/dist/react/hooks/useListingTypeFlags.js +4 -9
- package/dist/schemas/registry.d.ts +0 -12
- package/dist/seed/claimed-coupons-seed-data.js +3 -3
- package/dist/seed/coupons-seed-data.js +29 -10
- package/dist/seed/faq-seed-data.js +2 -2
- package/dist/seed/manifest.js +2 -2
- package/dist/styles.css +44 -39
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/FilterChipGroup.d.ts +21 -3
- package/dist/ui/components/FilterChipGroup.js +33 -4
- package/dist/ui/components/HorizontalScroller.d.ts +1 -2
- package/dist/ui/components/HorizontalScroller.js +9 -5
- package/dist/ui/components/HorizontalScroller.style.css +43 -38
- package/package.json +1 -1
|
@@ -60,7 +60,9 @@ function buildAddressConfig(banStatus, onAction) {
|
|
|
60
60
|
? `${ADMIN_ENDPOINTS.ADDRESSES}?banStatus=${encodeURIComponent(banStatus)}`
|
|
61
61
|
: ADMIN_ENDPOINTS.ADDRESSES,
|
|
62
62
|
sortOptions: [
|
|
63
|
-
|
|
63
|
+
// -bannedAt (newest flagged first) to match defaultSort — the option was
|
|
64
|
+
// "bannedAt" (oldest first), so the dropdown opened with nothing selected.
|
|
65
|
+
{ value: sortBy("bannedAt", "DESC"), label: "Flagged Date" },
|
|
64
66
|
{ value: "city", label: "City" },
|
|
65
67
|
],
|
|
66
68
|
columns: ADDRESS_COLUMNS,
|
|
@@ -81,7 +81,6 @@ export function AdminCouponEditorView({ couponId, onSaved, onDeleted, embedded,
|
|
|
81
81
|
const [isActive, setIsActive] = React.useState(true);
|
|
82
82
|
// restrictions
|
|
83
83
|
const [firstTimeOnly, setFirstTimeOnly] = React.useState(false);
|
|
84
|
-
const [combinable, setCombinable] = React.useState(false);
|
|
85
84
|
const [appliesToAuctions, setAppliesToAuctions] = React.useState(false);
|
|
86
85
|
const [applicableProducts, setApplicableProducts] = React.useState([]);
|
|
87
86
|
const [applicableCategories, setApplicableCategories] = React.useState([]);
|
|
@@ -122,7 +121,6 @@ export function AdminCouponEditorView({ couponId, onSaved, onDeleted, embedded,
|
|
|
122
121
|
setEndDate(toDateInputValue(c.validity?.endDate));
|
|
123
122
|
setIsActive(c.validity?.isActive ?? false);
|
|
124
123
|
setFirstTimeOnly(c.restrictions?.firstTimeUserOnly ?? false);
|
|
125
|
-
setCombinable(c.restrictions?.combineWithSellerCoupons ?? false);
|
|
126
124
|
setApplicableProducts(Array.isArray(c.restrictions?.applicableProducts) ? c.restrictions.applicableProducts : []);
|
|
127
125
|
setApplicableCategories(Array.isArray(c.restrictions?.applicableCategories) ? c.restrictions.applicableCategories : []);
|
|
128
126
|
setAppliesToAuctions(c.applicableToAuctions ?? false);
|
|
@@ -162,7 +160,6 @@ export function AdminCouponEditorView({ couponId, onSaved, onDeleted, embedded,
|
|
|
162
160
|
},
|
|
163
161
|
restrictions: {
|
|
164
162
|
firstTimeUserOnly: firstTimeOnly,
|
|
165
|
-
combineWithSellerCoupons: combinable,
|
|
166
163
|
...(applicableProducts.length > 0 && { applicableProducts }),
|
|
167
164
|
...(applicableCategories.length > 0 && { applicableCategories }),
|
|
168
165
|
},
|
|
@@ -208,7 +205,7 @@ export function AdminCouponEditorView({ couponId, onSaved, onDeleted, embedded,
|
|
|
208
205
|
const formSection = (_jsxs(_Fragment, { children: [_jsx(Form, { schema: couponFormSchema, onSubmit: (e) => e.preventDefault(), spacing: "md", children: ({ setFieldError, clearErrors }) => (_jsxs(_Fragment, { children: [_jsx(Select, { label: "Coupon type", options: TYPE_OPTIONS, value: type, onValueChange: (v) => setType(v), required: true }), _jsx(FieldInput, { name: "name", label: "Campaign name", value: name, onChange: (v) => handleNameChange(v), required: true, placeholder: "e.g. Summer Sale 20%" }), !isEdit && (_jsx(Input, { label: "Coupon code", value: code, onChange: (e) => {
|
|
209
206
|
setCode(toCouponCode(e.target.value));
|
|
210
207
|
setCodeManual(true);
|
|
211
|
-
}, required: true, placeholder: "e.g. SUMMER20", helperText: "Auto-generated from name. Uppercase alphanumeric + hyphens only." })), isEdit && (_jsx(Input, { label: "Coupon code", value: code, disabled: true, helperText: "Code cannot be changed after creation." })), _jsx(Input, { label: "Description (optional)", value: description, onChange: (e) => setDescription(e.target.value), placeholder: "Internal notes about this coupon" }), _jsx(CouponDiscountFields, { type: type, discountValue: discountValue, setDiscountValue: setDiscountValue, maxDiscount: maxDiscount, setMaxDiscount: setMaxDiscount, minPurchase: minPurchase, setMinPurchase: setMinPurchase, buyQty: buyQty, setBuyQty: setBuyQty, getQty: getQty, setGetQty: setGetQty, discountLabel: discountLabel }), _jsx(CouponValidityFields, { startDate: startDate, setStartDate: setStartDate, endDate: endDate, setEndDate: setEndDate, isActive: isActive, setIsActive: setIsActive, totalLimit: totalLimit, setTotalLimit: setTotalLimit, perUserLimit: perUserLimit, setPerUserLimit: setPerUserLimit, isEdit: isEdit, currentUsage: currentUsage }), _jsx(Toggle, { label: "First-time users only", checked: firstTimeOnly, onChange: setFirstTimeOnly }), _jsx(Toggle, { label: "
|
|
208
|
+
}, required: true, placeholder: "e.g. SUMMER20", helperText: "Auto-generated from name. Uppercase alphanumeric + hyphens only." })), isEdit && (_jsx(Input, { label: "Coupon code", value: code, disabled: true, helperText: "Code cannot be changed after creation." })), _jsx(Input, { label: "Description (optional)", value: description, onChange: (e) => setDescription(e.target.value), placeholder: "Internal notes about this coupon" }), _jsx(CouponDiscountFields, { type: type, discountValue: discountValue, setDiscountValue: setDiscountValue, maxDiscount: maxDiscount, setMaxDiscount: setMaxDiscount, minPurchase: minPurchase, setMinPurchase: setMinPurchase, buyQty: buyQty, setBuyQty: setBuyQty, getQty: getQty, setGetQty: setGetQty, discountLabel: discountLabel }), _jsx(CouponValidityFields, { startDate: startDate, setStartDate: setStartDate, endDate: endDate, setEndDate: setEndDate, isActive: isActive, setIsActive: setIsActive, totalLimit: totalLimit, setTotalLimit: setTotalLimit, perUserLimit: perUserLimit, setPerUserLimit: setPerUserLimit, isEdit: isEdit, currentUsage: currentUsage }), _jsx(Toggle, { label: "First-time users only", checked: firstTimeOnly, onChange: setFirstTimeOnly }), _jsx(Toggle, { label: "Applies to auctions", checked: appliesToAuctions, onChange: setAppliesToAuctions }), _jsxs(Stack, { gap: "xs", children: [_jsx(Label, { size: "sm", weight: "medium", color: "primary", children: "Applicable products (optional)" }), _jsx(ProductInlineSelect, { scope: "admin", multiple: true, value: applicableProducts, onChange: setApplicableProducts, placeholder: "Restrict to specific products\u2026" }), _jsx(Text, { size: "xs", color: "muted", children: "Leave empty to apply the coupon to every eligible product." })] }), _jsxs(Stack, { gap: "xs", children: [_jsx(Label, { size: "sm", weight: "medium", color: "primary", children: "Applicable categories (optional)" }), _jsx(CategoryInlineSelect, { value: "", onChange: (id) => {
|
|
212
209
|
if (!id || applicableCategories.includes(id))
|
|
213
210
|
return;
|
|
214
211
|
setApplicableCategories([...applicableCategories, id]);
|
|
@@ -57,7 +57,9 @@ function buildConfig(banStatus, onAction) {
|
|
|
57
57
|
? `${ADMIN_ENDPOINTS.PAYMENT_METHODS}?banStatus=${encodeURIComponent(banStatus)}`
|
|
58
58
|
: ADMIN_ENDPOINTS.PAYMENT_METHODS,
|
|
59
59
|
sortOptions: [
|
|
60
|
-
|
|
60
|
+
// -bannedAt (newest flagged first) to match defaultSort — the option was
|
|
61
|
+
// "bannedAt" (oldest first), so the dropdown opened with nothing selected.
|
|
62
|
+
{ value: sortBy("bannedAt", "DESC"), label: "Flagged Date" },
|
|
61
63
|
{ value: "type", label: "Type" },
|
|
62
64
|
],
|
|
63
65
|
columns: PM_COLUMNS,
|
|
@@ -15,21 +15,17 @@ import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
|
|
|
15
15
|
import { AdminProductEditorView } from "./AdminProductEditorView";
|
|
16
16
|
import { QuickEditMenu } from "./QuickEditMenu";
|
|
17
17
|
import { ADMIN_BULK_ACTIONS, ROW_ACTION_META, ROW_ACTION_ID } from "../../products/constants/action-defs";
|
|
18
|
+
import { parseSelectedListingTypes } from "../../products/utils/listing-type";
|
|
18
19
|
const FLAG_DEFS = [
|
|
19
20
|
{ key: "featured", label: "Featured" },
|
|
20
21
|
{ key: "isPromoted", label: "Promoted" },
|
|
21
22
|
{ key: "isOnSale", label: "On Sale" },
|
|
22
23
|
{ key: "isSold", label: "Sold" },
|
|
23
24
|
];
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
Products: "listingType==standard",
|
|
29
|
-
Classifieds: "listingType==classified",
|
|
30
|
-
"Digital Codes": "listingType==digital-code",
|
|
31
|
-
"Live Items": "listingType==live",
|
|
32
|
-
};
|
|
25
|
+
// TYPE_FILTER_MAP (label -> Sieve clause) was deleted 2026-08-21. Chip ids are
|
|
26
|
+
// now canonical ListingType values, so the clause is built directly and the
|
|
27
|
+
// array is covered by audit-filter-tab-enums. The map had no `art`/`stickers`
|
|
28
|
+
// entries, which is why admin could never filter to either type.
|
|
33
29
|
function buildBaseColumns() {
|
|
34
30
|
return [
|
|
35
31
|
{
|
|
@@ -112,11 +108,23 @@ export function AdminProductsView({ children, ...props }) {
|
|
|
112
108
|
defaultSort: sortBy("createdAt", "DESC"),
|
|
113
109
|
queryKey: ["admin", "products", "listing"],
|
|
114
110
|
endpoint: ADMIN_ENDPOINTS.PRODUCTS,
|
|
111
|
+
// "Featured first" / "Promoted first" existed in STANDARD_SORT_OPTIONS but
|
|
112
|
+
// were offered nowhere, and would not have worked if they had been:
|
|
113
|
+
// `featured`/`isPromoted` were `canSort: false`, so sievejs dropped the
|
|
114
|
+
// sort silently. Both are now sortable (products.repository.ts) with
|
|
115
|
+
// matching composite indexes, and admin is where curating them matters.
|
|
116
|
+
//
|
|
117
|
+
// Deliberately a curated set, not the full 10-option STANDARD_SORT_OPTIONS:
|
|
118
|
+
// every additional sort field multiplies out against this view's
|
|
119
|
+
// isSold/status/listingType filter combinations into more composite
|
|
120
|
+
// indexes, and Rule #6 says spend those only where they earn their keep.
|
|
115
121
|
sortOptions: [
|
|
116
122
|
{ value: sortBy("createdAt", "DESC"), label: "Newest" },
|
|
117
123
|
{ value: sortBy("createdAt", "ASC"), label: "Oldest" },
|
|
118
124
|
{ value: "title", label: "Title A–Z" },
|
|
119
125
|
{ value: sortBy("price", "DESC"), label: "Highest price" },
|
|
126
|
+
{ value: sortBy("featured", "DESC"), label: "Featured first" },
|
|
127
|
+
{ value: sortBy("isPromoted", "DESC"), label: "Promoted first" },
|
|
120
128
|
],
|
|
121
129
|
columns: [...buildBaseColumns(), flagColumn],
|
|
122
130
|
mapRows: (response) => toRecordArray(response.items).map((item, index) => {
|
|
@@ -165,16 +173,24 @@ export function AdminProductsView({ children, ...props }) {
|
|
|
165
173
|
parts.push("isSold==false");
|
|
166
174
|
if (state.status && state.status !== "All")
|
|
167
175
|
parts.push(sieveFilter("status", SIEVE_OP.EQ, state.status));
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
//
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
176
|
+
// `state.type` is a pipe-joined set of canonical ListingType values
|
|
177
|
+
// (multi-select chips). It used to be a single display LABEL resolved
|
|
178
|
+
// through a TYPE_FILTER_MAP lookup — an indirection that silently
|
|
179
|
+
// dropped `art` and `stickers`, which had no map entry.
|
|
180
|
+
const selectedTypes = parseSelectedListingTypes(state.type);
|
|
181
|
+
if (selectedTypes.length > 0) {
|
|
182
|
+
parts.push(sieveFilter("listingType", SIEVE_OP.EQ, selectedTypes.join("|")));
|
|
183
|
+
}
|
|
184
|
+
// Ended/closed are per-type predicates: `auctionEndDate` and
|
|
185
|
+
// `prizeRevealWindowEnd` only exist on auctions and prize-draws. Apply
|
|
186
|
+
// each only when its type is the ONLY one selected — with a mixed
|
|
187
|
+
// selection the inequality would exclude every row of the other types
|
|
188
|
+
// (they have no such field at all), which reads as "no results".
|
|
189
|
+
const onlyType = selectedTypes.length === 1 ? selectedTypes[0] : null;
|
|
190
|
+
if (onlyType === "auction" && state.showEnded !== "true") {
|
|
175
191
|
parts.push(sieveFilter("auctionEndDate", SIEVE_OP.GTE, new Date().toISOString()));
|
|
176
192
|
}
|
|
177
|
-
if (
|
|
193
|
+
if (onlyType === "prize-draw" && state.showClosed !== "true") {
|
|
178
194
|
parts.push(sieveFilter("prizeRevealWindowEnd", SIEVE_OP.GTE, new Date().toISOString()));
|
|
179
195
|
}
|
|
180
196
|
return parts.join(",") || undefined;
|
|
@@ -247,7 +263,7 @@ export function AdminProductsView({ children, ...props }) {
|
|
|
247
263
|
submitLabel: "Save",
|
|
248
264
|
},
|
|
249
265
|
] })),
|
|
250
|
-
renderFilterPanel: ({ pendingFilters, setPendingFilters }) => (_jsxs(_Fragment, { children: [_jsx(FilterChipGroup, { label: "Status", tabs: ADMIN_PRODUCT_STATUS_TABS, value: pendingFilters.status ?? "", onChange: (id) => setPendingFilters((p) => ({ ...p, status: id })) }), _jsx(FilterChipGroup, { label: "Type", tabs: ADMIN_PRODUCT_LISTING_TYPE_TABS, value: pendingFilters.type ?? "", onChange: (
|
|
266
|
+
renderFilterPanel: ({ pendingFilters, setPendingFilters }) => (_jsxs(_Fragment, { children: [_jsx(FilterChipGroup, { label: "Status", tabs: ADMIN_PRODUCT_STATUS_TABS, value: pendingFilters.status ?? "", onChange: (id) => setPendingFilters((p) => ({ ...p, status: id })) }), _jsx(FilterChipGroup, { multiple: true, label: "Type", tabs: ADMIN_PRODUCT_LISTING_TYPE_TABS, value: pendingFilters.type ?? "", onChange: (ids) => setPendingFilters((p) => ({ ...p, type: ids })) }), _jsx(FilterChipGroup, { label: "Sold", tabs: [
|
|
251
267
|
{ id: "", label: "Hide sold" },
|
|
252
268
|
{ id: "true", label: "Show sold" },
|
|
253
269
|
], value: pendingFilters.showSold ?? "", onChange: (id) => setPendingFilters((p) => ({ ...p, showSold: id })), allId: "" }), _jsx(FilterChipGroup, { label: "Ended", tabs: [
|
|
@@ -38,32 +38,49 @@ export declare const ADMIN_PRODUCT_STATUS_TABS: readonly [{
|
|
|
38
38
|
readonly id: "archived";
|
|
39
39
|
readonly label: "Archived";
|
|
40
40
|
}];
|
|
41
|
-
/**
|
|
42
|
-
*
|
|
41
|
+
/**
|
|
42
|
+
* Admin > Products — listing-type filter chip set.
|
|
43
|
+
*
|
|
44
|
+
* IDs are canonical `ListingType` values (2026-08-21). They used to be display
|
|
45
|
+
* LABELS ("Products", "Pre-orders", …) translated to a Sieve clause through a
|
|
46
|
+
* `TYPE_FILTER_MAP` lookup in AdminProductsView — an indirection that hid two
|
|
47
|
+
* whole missing types (`art`, `stickers`) and put the array outside the reach
|
|
48
|
+
* of `audit-filter-tab-enums.mjs`, which compares chip ids against the real
|
|
49
|
+
* backing enum. Now registered there, so a missing or misspelled type blocks.
|
|
50
|
+
*
|
|
51
|
+
* Multi-select: this feeds a `<FilterChipGroup multiple>`, so several types can
|
|
52
|
+
* be active at once and the selection is emitted as a pipe-joined OR-group.
|
|
53
|
+
*/
|
|
43
54
|
export declare const ADMIN_PRODUCT_LISTING_TYPE_TABS: readonly [{
|
|
44
55
|
readonly id: "All";
|
|
45
56
|
readonly label: "All";
|
|
46
57
|
}, {
|
|
47
|
-
readonly id: "
|
|
58
|
+
readonly id: "standard";
|
|
48
59
|
readonly label: "Products";
|
|
49
60
|
}, {
|
|
50
|
-
readonly id: "
|
|
61
|
+
readonly id: "auction";
|
|
51
62
|
readonly label: "Auctions";
|
|
52
63
|
}, {
|
|
53
|
-
readonly id: "
|
|
64
|
+
readonly id: "pre-order";
|
|
54
65
|
readonly label: "Pre-orders";
|
|
55
66
|
}, {
|
|
56
|
-
readonly id: "
|
|
67
|
+
readonly id: "prize-draw";
|
|
57
68
|
readonly label: "Prize Draws";
|
|
58
69
|
}, {
|
|
59
|
-
readonly id: "
|
|
70
|
+
readonly id: "classified";
|
|
60
71
|
readonly label: "Classifieds";
|
|
61
72
|
}, {
|
|
62
|
-
readonly id: "
|
|
73
|
+
readonly id: "digital-code";
|
|
63
74
|
readonly label: "Digital Codes";
|
|
64
75
|
}, {
|
|
65
|
-
readonly id: "
|
|
76
|
+
readonly id: "live";
|
|
66
77
|
readonly label: "Live Items";
|
|
78
|
+
}, {
|
|
79
|
+
readonly id: "art";
|
|
80
|
+
readonly label: "Art";
|
|
81
|
+
}, {
|
|
82
|
+
readonly id: "stickers";
|
|
83
|
+
readonly label: "Stickers";
|
|
67
84
|
}];
|
|
68
85
|
/** Admin > Blog — post status filter chip set. */
|
|
69
86
|
export declare const ADMIN_BLOG_STATUS_TABS: readonly [{
|
|
@@ -20,17 +20,30 @@ export const ADMIN_PRODUCT_STATUS_TABS = [
|
|
|
20
20
|
{ id: "draft", label: "Draft" },
|
|
21
21
|
{ id: "archived", label: "Archived" },
|
|
22
22
|
];
|
|
23
|
-
/**
|
|
24
|
-
*
|
|
23
|
+
/**
|
|
24
|
+
* Admin > Products — listing-type filter chip set.
|
|
25
|
+
*
|
|
26
|
+
* IDs are canonical `ListingType` values (2026-08-21). They used to be display
|
|
27
|
+
* LABELS ("Products", "Pre-orders", …) translated to a Sieve clause through a
|
|
28
|
+
* `TYPE_FILTER_MAP` lookup in AdminProductsView — an indirection that hid two
|
|
29
|
+
* whole missing types (`art`, `stickers`) and put the array outside the reach
|
|
30
|
+
* of `audit-filter-tab-enums.mjs`, which compares chip ids against the real
|
|
31
|
+
* backing enum. Now registered there, so a missing or misspelled type blocks.
|
|
32
|
+
*
|
|
33
|
+
* Multi-select: this feeds a `<FilterChipGroup multiple>`, so several types can
|
|
34
|
+
* be active at once and the selection is emitted as a pipe-joined OR-group.
|
|
35
|
+
*/
|
|
25
36
|
export const ADMIN_PRODUCT_LISTING_TYPE_TABS = [
|
|
26
37
|
ALL_TAB,
|
|
27
|
-
{ id: "
|
|
28
|
-
{ id: "
|
|
29
|
-
{ id: "
|
|
30
|
-
{ id: "
|
|
31
|
-
{ id: "
|
|
32
|
-
{ id: "
|
|
33
|
-
{ id: "
|
|
38
|
+
{ id: "standard", label: "Products" },
|
|
39
|
+
{ id: "auction", label: "Auctions" },
|
|
40
|
+
{ id: "pre-order", label: "Pre-orders" },
|
|
41
|
+
{ id: "prize-draw", label: "Prize Draws" },
|
|
42
|
+
{ id: "classified", label: "Classifieds" },
|
|
43
|
+
{ id: "digital-code", label: "Digital Codes" },
|
|
44
|
+
{ id: "live", label: "Live Items" },
|
|
45
|
+
{ id: "art", label: "Art" },
|
|
46
|
+
{ id: "stickers", label: "Stickers" },
|
|
34
47
|
];
|
|
35
48
|
/** Admin > Blog — post status filter chip set. */
|
|
36
49
|
export const ADMIN_BLOG_STATUS_TABS = [
|
|
@@ -60,7 +60,7 @@ function AuctionRow({ auction, portal, }) {
|
|
|
60
60
|
const sorted = useMemo(() => [...auction.bids].sort((a, b) => +new Date(b.bidDate) - +new Date(a.bidDate)), [auction.bids]);
|
|
61
61
|
const highest = auction.bids.reduce((max, b) => Math.max(max, b.bidAmount), 0);
|
|
62
62
|
const isWinning = auction.bids.some((b) => b.isWinning);
|
|
63
|
-
return (_jsxs(Div, { className: `border border-[var(--appkit-color-border)] ${__O.hidden} bg-[var(--appkit-color-surface)]`, rounded: "xl", shadow: "sm", children: [_jsxs(Button, { type: "button", variant: "ghost", onClick: () => setExpanded((v) => !v), gap: "lg", paddingX: "md", paddingY: "md", className: "w-full 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-[var(--appkit-color-text)] line-clamp-1", size: "sm", weight: "semibold", children: auction.productTitle }) }), _jsxs(Row, { gap: "sm", className: "shrink-0", children: [isWinning && (_jsx(Badge, { variant: "active", children: "Winning" })), _jsxs(Text, { variant: "secondary", size: "xs", children: [auction.bids.length, " bid", auction.bids.length !== 1 ? "s" : ""] }), _jsx(Text, { className: "text-[var(--appkit-color-text)]", size: "sm", weight: "semibold", children: formatBidAmount(highest) })] })] }), expanded && (_jsxs(Div, { className: "border-t border-[var(--appkit-color-border)]", children: [_jsxs(Div, { textSize: "xs", textWeight: "medium", paddingX: "x-md", paddingY: "y-xs", className: `grid text-[var(--appkit-color-text-muted)] uppercase tracking-wide border-b border-[var(--appkit-color-border-subtle)] ${portal === "buyer" ? "[grid-template-columns:1fr_auto_auto]" : "[grid-template-columns: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, { layout: "grid", align: "center", paddingX: "x-md", paddingY: "y-xs-tall", className: `border-b border-[var(--appkit-color-border-subtle)] last:border-0 hover:bg-[var(--appkit-color-border-subtle)] transition-colors ${portal === "buyer" ? "[grid-template-columns:1fr_auto_auto]" : "[grid-template-columns:1fr_1fr_auto_auto]"}`, children: [portal !== "buyer" && (_jsx(Text, { className: "text-[var(--appkit-color-text)] truncate pr-[0.75rem]", size: "sm", children: bid.userName || bid.userId })), _jsx(Text, { className: "text-[var(--appkit-color-text)]", size: "sm", weight: "medium", children: formatBidAmount(bid.bidAmount) }), _jsx(Badge, { variant: STATUS_VARIANT[bid.status] ?? "pending", className: "capitalize", children: bid.status }), _jsxs(Stack, { gap: "xs",
|
|
63
|
+
return (_jsxs(Div, { className: `border border-[var(--appkit-color-border)] ${__O.hidden} bg-[var(--appkit-color-surface)]`, rounded: "xl", shadow: "sm", children: [_jsxs(Button, { type: "button", variant: "ghost", onClick: () => setExpanded((v) => !v), gap: "lg", paddingX: "md", paddingY: "md", className: "w-full 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-[var(--appkit-color-text)] line-clamp-1", size: "sm", weight: "semibold", children: auction.productTitle }) }), _jsxs(Row, { gap: "sm", className: "shrink-0", children: [isWinning && (_jsx(Badge, { variant: "active", children: "Winning" })), _jsxs(Text, { variant: "secondary", size: "xs", children: [auction.bids.length, " bid", auction.bids.length !== 1 ? "s" : ""] }), _jsx(Text, { className: "text-[var(--appkit-color-text)]", size: "sm", weight: "semibold", children: formatBidAmount(highest) })] })] }), expanded && (_jsxs(Div, { className: "border-t border-[var(--appkit-color-border)]", children: [_jsxs(Div, { textSize: "xs", textWeight: "medium", paddingX: "x-md", paddingY: "y-xs", className: `grid text-[var(--appkit-color-text-muted)] uppercase tracking-wide border-b border-[var(--appkit-color-border-subtle)] ${portal === "buyer" ? "[grid-template-columns:1fr_auto_auto]" : "[grid-template-columns: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, { layout: "grid", align: "center", paddingX: "x-md", paddingY: "y-xs-tall", className: `border-b border-[var(--appkit-color-border-subtle)] last:border-0 hover:bg-[var(--appkit-color-border-subtle)] transition-colors ${portal === "buyer" ? "[grid-template-columns:1fr_auto_auto]" : "[grid-template-columns:1fr_1fr_auto_auto]"}`, children: [portal !== "buyer" && (_jsx(Text, { className: "text-[var(--appkit-color-text)] truncate pr-[0.75rem]", size: "sm", children: bid.userName || bid.userId })), _jsx(Text, { className: "text-[var(--appkit-color-text)]", size: "sm", weight: "medium", children: formatBidAmount(bid.bidAmount) }), _jsx(Badge, { variant: STATUS_VARIANT[bid.status] ?? "pending", className: "capitalize", children: bid.status }), _jsxs(Stack, { gap: "xs", align: "end", children: [_jsx(Text, { variant: "secondary", size: "xs", align: "end", children: relDate(bid.bidDate) }), portal === "buyer" && bid.status === "won" && (_jsx(TextLink, { href: bid.orderId
|
|
64
64
|
? String(ROUTES.USER.ORDER_DETAIL(bid.orderId))
|
|
65
65
|
: `${String(ROUTES.USER.CHECKOUT)}?lane=${CART_LANE.AUCTION}`, size: "xs", weight: "semibold", children: bid.orderId ? "View order" : "Pay now →" }))] })] }, bid.id)))] }))] }));
|
|
66
66
|
}
|
|
@@ -31,6 +31,7 @@ export function AuctionBottomActions({ currentBid, currency, bidCount, isEnded,
|
|
|
31
31
|
],
|
|
32
32
|
secondaryLabel: formatCountdownLabel(remaining),
|
|
33
33
|
infoLabel: `${formatCurrency(currentBid, currency)} · ${bidCount} bid${bidCount !== 1 ? "s" : ""}`,
|
|
34
|
+
desktop: "after-scroll",
|
|
34
35
|
});
|
|
35
36
|
return null;
|
|
36
37
|
}
|
|
@@ -9,8 +9,6 @@ const __P = {
|
|
|
9
9
|
};
|
|
10
10
|
const CLS_BREADCRUMB_LINK = "hover:text-primary-600 transition-colors";
|
|
11
11
|
const CLS_LIVE_BADGE = "inline-block rounded-full bg-warning-surface dark:bg-warning-surface px-[var(--appkit-space-2-5)] py-[var(--appkit-space-0-5)] text-warning dark:text-warning";
|
|
12
|
-
const CLS_STAR_ON = "text-warning";
|
|
13
|
-
const CLS_STAR_OFF = "text-zinc-200 dark:text-zinc-700";
|
|
14
12
|
import { ROUTES } from "../../../next";
|
|
15
13
|
import { getDefaultCurrency } from "../../../core/baseline-resolver";
|
|
16
14
|
import { formatCurrency } from "../../../utils/number.formatter";
|
|
@@ -30,7 +28,7 @@ import { FeatureBadgeList } from "../../products/components/FeatureBadge";
|
|
|
30
28
|
import { HistoryTracker } from "../../history/components/HistoryTracker";
|
|
31
29
|
import { ShareButton } from "../../products/components/ShareButton";
|
|
32
30
|
import { MarketplaceAuctionGrid } from "./MarketplaceAuctionGrid";
|
|
33
|
-
import {
|
|
31
|
+
import { ReviewsListingPanel } from "../../reviews/components/ReviewsListingPanel";
|
|
34
32
|
import { PlaceBidModalButton } from "./PlaceBidFormClient";
|
|
35
33
|
import { CollapsibleBidHistory } from "./CollapsibleBidHistory";
|
|
36
34
|
import { LiveBidPrice } from "./LiveBidPrice";
|
|
@@ -50,11 +48,18 @@ function renderAuctionInfoPanel(props) {
|
|
|
50
48
|
const { productId, title, currentBid, currency, bidCount, isEnded, endDate, buyNowPrice, featured, freeShipping, condition, category, categoryName, brand, brandSlug, productFeatures, features, descriptionHtml, safeSeller, storeHref } = props;
|
|
51
49
|
return (_jsxs(Stack, { gap: "md", children: [_jsxs(Div, { children: [_jsxs(Row, { gap: "xs", wrap: true, className: "mb-2", children: [_jsx(Span, { size: "xs", weight: "semibold", className: CLS_LIVE_BADGE, children: "\uD83C\uDFF7\uFE0F Live Auction" }), isEnded ? (_jsx(Span, { color: "error", surface: "danger-surface", size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", children: "Ended" })) : (_jsx(Span, { color: "success", surface: "success-surface", size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", children: "Active" }))] }), _jsx(Heading, { level: 1, className: "leading-snug", smSize: "2xl", color: "primary", size: "xl", weight: "bold", children: title })] }), _jsxs(Div, { children: [_jsx(Text, { className: "mb-0.5", color: "muted", size: "xs", children: "Current bid" }), _jsx(LiveBidPrice, { productId: productId, currentBid: currentBid, bidCount: bidCount, currency: currency, isEnded: isEnded, priceSize: "2xl", badgeSize: "sm" }), endDate && !isEnded && (_jsxs(Text, { className: "mt-1.5", color: "muted", size: "sm", children: ["Ends in ", _jsx(Span, { weight: "medium", color: "muted", children: _jsx(CountdownDisplay, { targetDate: endDate, format: "auto", expiredLabel: "Ended" }) })] })), endDate && _jsxs(Text, { className: "mt-0.5", color: "faint", size: "xs", children: [isEnded ? "Ended" : "Ends", " ", endDate.toLocaleString()] })] }), buyNowPrice !== null && !isEnded && (_jsxs(Row, { align: "center", gap: "sm", className: "border border-[var(--appkit-color-primary-200)] dark:border-[var(--appkit-color-primary-800)] bg-primary-50 dark:bg-primary-900/20", padding: "inlineSm", rounded: "lg", children: [_jsx(Span, { size: "xs", color: "muted", children: "Buy Now:" }), _jsx(Span, { size: "base", weight: "bold", className: "text-primary-700 dark:text-primary-300", children: formatCurrency(buyNowPrice, currency) })] })), _jsx(ProductFeatureBadges, { featured: featured, freeShipping: freeShipping, condition: condition ?? undefined, labels: { featured: "Featured", fasterDelivery: "Faster Delivery", ratedSeller: "Rated Seller", condition: "Condition", conditionNew: "New", conditionUsed: "Used", conditionBroken: "For Parts", conditionRefurbished: "Refurbished", returnable: "Returnable", freeShipping: "Free Shipping", codAvailable: "Cash on Delivery", emiAvailable: "EMI Available", wishlistCount: (n) => `${n} wishlisted`, categoryProductCount: (n, cat) => `${n} in ${cat}` } }), (categoryName || category || brand) && (_jsxs(Row, { gap: "sm", wrap: true, children: [category && _jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category)), className: "inline-flex items-center rounded-full border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] px-[var(--appkit-space-2-5)] py-[var(--appkit-space-1)] text-[length:var(--appkit-text-xs)] font-medium text-[var(--appkit-color-text-muted)] transition-colors hover:border-primary-300 hover:bg-primary-50 hover:text-primary-700 dark:hover:border-primary-700/60 dark:hover:bg-primary-900/20 dark:hover:text-primary-400", children: categoryName || category }), !category && categoryName && _jsx(Span, { layout: "inline-flex", size: "xs", weight: "medium", border: "subtle", rounded: "full", padding: "pill-sm-tall", surface: "muted", color: "muted", children: categoryName }), brand && brandSlug && _jsx(Link, { href: String(ROUTES.PUBLIC.BRAND_DETAIL(brandSlug)), className: "inline-flex items-center rounded-full border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] px-[var(--appkit-space-2-5)] py-[var(--appkit-space-1)] text-[length:var(--appkit-text-xs)] font-medium text-[var(--appkit-color-text-muted)] transition-colors hover:border-primary-300 hover:bg-primary-50 hover:text-primary-700 dark:hover:border-primary-700/60 dark:hover:bg-primary-900/20 dark:hover:text-primary-400", children: brand }), brand && !brandSlug && _jsx(Span, { layout: "inline-flex", size: "xs", weight: "medium", border: "subtle", rounded: "full", padding: "pill-sm-tall", surface: "muted", color: "muted", children: brand })] })), productFeatures && features.length > 0 && _jsx(FeatureBadgeList, { productFeatureIds: features, features: productFeatures }), !productFeatures && features.length > 0 && (_jsxs(Div, { border: "subtle", surface: "muted", padding: "inline", rounded: "xl", children: [_jsx(Text, { className: "mb-2 tracking-wide", color: "muted", size: "xs", weight: "semibold", transform: "uppercase", children: "About this item" }), _jsx(Ul, { spacing: "comfortable", size: "sm", color: "primary", children: features.map((f, i) => _jsxs(Li, { layout: "flex-start", gap: "2", children: [_jsx(Span, { className: "mt-0.5 flex-shrink-0 text-primary-500", children: "\u2022" }), f] }, i)) })] })), descriptionHtml && _jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "text-[length:var(--appkit-text-sm)] leading-relaxed text-[var(--appkit-color-text-muted)] line-clamp-4" }), safeSeller && (_jsx(Div, { className: `${__P.p3}`, border: "subtle", rounded: "xl", surface: "muted", children: _jsxs(Row, { justify: "between", align: "center", children: [_jsxs(Div, { children: [_jsx(Text, { className: "text-[10px] tracking-wide mb-0.5", color: "faint", transform: "uppercase", children: "Listed by" }), _jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: safeSeller })] }), storeHref && _jsx(Link, { href: storeHref, className: "shrink-0 rounded-lg bg-primary/10 dark:bg-primary/20 px-[var(--appkit-space-3)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-xs)] font-semibold text-primary-700 dark:text-primary-300 hover:bg-primary/20 dark:hover:bg-primary/30 transition-colors", children: "Visit Store \u2192" })] }) }))] }));
|
|
52
50
|
}
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
/**
|
|
52
|
+
* Store reviews for the auction's seller.
|
|
53
|
+
*
|
|
54
|
+
* Was a bespoke `slice(0, 10)` block over `listReviewsBySeller()` — which concatenates
|
|
55
|
+
* per-product batches, so the "latest 10" it showed were in product order, not date
|
|
56
|
+
* order, and reviews 11+ were unreachable. `ReviewsListingPanel` pages the real
|
|
57
|
+
* store-reviews endpoint instead.
|
|
58
|
+
*/
|
|
59
|
+
function renderAuctionStoreReviews(storeSlug) {
|
|
60
|
+
if (!storeSlug)
|
|
55
61
|
return null;
|
|
56
|
-
|
|
57
|
-
return (_jsxs(Section, { className: "mt-10", children: [_jsx(Heading, { level: 2, className: "mb-2", color: "primary", size: "xl", weight: "semibold", children: "Store Reviews" }), _jsxs(Row, { className: "mb-4", align: "center", gap: "3", children: [_jsx(Span, { weight: "bold", size: "3xl", color: "primary", children: avg.toFixed(1) }), _jsxs(Div, { children: [_jsx(Row, { gap: "xs", children: [1, 2, 3, 4, 5].map((star) => _jsx(Span, { className: star <= Math.round(avg) ? CLS_STAR_ON : CLS_STAR_OFF, children: "\u2605" }, star)) }), _jsxs(Text, { size: "xs", color: "muted", children: [storeReviews.length, " review", storeReviews.length !== 1 ? "s" : ""] })] })] }), _jsx(Stack, { gap: "sm", children: storeReviews.slice(0, 10).map((review) => (_jsxs(Stack, { gap: "xs", surface: "card", padding: "sm", className: "", children: [_jsx(Row, { justify: "between", align: "center", children: _jsxs(Row, { gap: "xs", align: "center", children: [_jsx(Span, { size: "sm", weight: "medium", color: "primary", children: review.userName }), _jsx(Row, { gap: "xs", children: [1, 2, 3, 4, 5].map((star) => _jsx(Span, { size: "xs", className: star <= review.rating ? CLS_STAR_ON : CLS_STAR_OFF, children: "\u2605" }, star)) })] }) }), review.title && _jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: review.title }), _jsx(Text, { className: "leading-relaxed", color: "muted", size: "sm", children: review.comment }), _jsx(Text, { size: "xs", color: "faint", children: review.productTitle })] }, review.id))) })] }));
|
|
62
|
+
return (_jsxs(Section, { className: "mt-10", children: [_jsx(Heading, { level: 2, className: "mb-2", color: "primary", size: "xl", weight: "semibold", children: "Store Reviews" }), _jsx(ReviewsListingPanel, { source: { kind: "store", storeSlug }, stateMode: "local", context: "store", emptyLabel: "This store has no reviews yet." })] }));
|
|
58
63
|
}
|
|
59
64
|
export async function AuctionDetailPageView({ id, initialAuction, onPlaceBid, onBuyNow, productFeatures }) {
|
|
60
65
|
const product = initialAuction !== undefined
|
|
@@ -64,9 +69,6 @@ export async function AuctionDetailPageView({ id, initialAuction, onPlaceBid, on
|
|
|
64
69
|
? await listBidsByProduct(String(product.id), { pageSize: 5 }).catch(() => null)
|
|
65
70
|
: null;
|
|
66
71
|
const storeId = product?.storeId;
|
|
67
|
-
const storeReviews = storeId
|
|
68
|
-
? await listReviewsBySeller(storeId).catch(() => [])
|
|
69
|
-
: [];
|
|
70
72
|
if (!product) {
|
|
71
73
|
return (_jsx(Main, { children: _jsx(Section, { padding: "y-5xl", children: _jsx(Container, { size: "md", children: _jsxs(Stack, { align: "start", gap: "md", className: "text-left", children: [_jsx(Heading, { level: 1, size: "2xl", weight: "semibold", color: "primary", children: "Auction Not Found" }), _jsx(Text, { color: "muted", children: "This auction may have ended or the link is incorrect." }), _jsx(Link, { href: String(ROUTES.PUBLIC.AUCTIONS), className: "text-[length:var(--appkit-text-sm)] font-medium text-primary-600 hover:underline", children: "Browse Auctions" })] }) }) }) }));
|
|
72
74
|
}
|
|
@@ -191,5 +193,5 @@ export async function AuctionDetailPageView({ id, initialAuction, onPlaceBid, on
|
|
|
191
193
|
slug: typeof r.slug === "string" ? r.slug : undefined,
|
|
192
194
|
}));
|
|
193
195
|
return (_jsxs(Stack, { gap: "xl", children: [related.length > 0 && (_jsx(RelatedProducts, { labels: { title: "Similar Auctions" }, renderGrid: () => (_jsx(MarketplaceAuctionGrid, { auctions: related, gridClassName: "grid grid-cols-2 gap-[var(--appkit-space-4)] sm:grid-cols-3 lg:grid-cols-4" })) })), _jsx(GroupedListingsCarousel, { groups: groups }), _jsx(RelatedItemsSection, { relatedItems: relatedItems, relatedByBrand: relatedByBrand, relatedByTags: relatedByTags, relatedByStore: relatedByStore, categoryLabel: categoryName || category || undefined, brandLabel: brand || undefined, storeLabel: storeName || undefined })] }));
|
|
194
|
-
} }), renderAuctionStoreReviews(
|
|
196
|
+
} }), renderAuctionStoreReviews(storeId), _jsx(AuctionBottomActions, { currentBid: currentBid, currency: currency, bidCount: bidCount, isEnded: isEnded, auctionEndDate: endDate })] })] }));
|
|
195
197
|
}
|
|
@@ -82,8 +82,6 @@ export interface CartAppliedCoupon {
|
|
|
82
82
|
storeId?: string;
|
|
83
83
|
/** Item IDs (CartItemDocument.itemId) this coupon was calculated against */
|
|
84
84
|
applicableItemIds?: string[];
|
|
85
|
-
/** Mirrors CouponDocument.restrictions.combineWithSellerCoupons — used for conflict detection when adding future coupons */
|
|
86
|
-
combineWithSellerCoupons?: boolean;
|
|
87
85
|
}
|
|
88
86
|
export interface CartDocument extends BaseDocument {
|
|
89
87
|
userId: string;
|
|
@@ -7,14 +7,35 @@ import { AuctionsIndexListing } from "../../products/components/AuctionsIndexLis
|
|
|
7
7
|
import { PreOrdersIndexListing } from "../../pre-orders/components/PreOrdersIndexListing";
|
|
8
8
|
import { PrizeDrawsIndexListing } from "../../products/components/PrizeDrawsIndexListing";
|
|
9
9
|
import { CategoryBundlesListing } from "./CategoryBundlesListing";
|
|
10
|
-
import { CATEGORY_PAGE_TABS } from "../../products/constants/listing-tabs";
|
|
10
|
+
import { CATEGORY_PAGE_TABS, ART_STICKERS_LISTING_TYPES, } from "../../products/constants/listing-tabs";
|
|
11
|
+
import { ALL_LISTING_TYPES } from "../../../_internal/shared/listing-types/feature-flags";
|
|
12
|
+
import { pluginFor } from "../../../_internal/shared/listing-types/_registry";
|
|
13
|
+
/**
|
|
14
|
+
* Tab slug → listing/category type, derived (2026-08-21). The hand-written
|
|
15
|
+
* version covered 5 of 10 tabs, and this view's filter did `if (!mapping)
|
|
16
|
+
* return false` — so classifieds, digital codes, live items and art were
|
|
17
|
+
* silently DROPPED from every brand page rather than rendering blank (the
|
|
18
|
+
* failure mode its category-page twin had). Same root gap, opposite symptom.
|
|
19
|
+
*
|
|
20
|
+
* `stores` is intentionally absent: a brand has no store roster, which is the
|
|
21
|
+
* one real difference between this component and CategoryDetailTabs (see
|
|
22
|
+
* CLAUDE.md's Categories & Brands Reference on why the duplication stands).
|
|
23
|
+
*/
|
|
11
24
|
const TAB_TYPE_MAP = {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
25
|
+
...Object.fromEntries(ALL_LISTING_TYPES.map((type) => [
|
|
26
|
+
pluginFor(type).tabSlug,
|
|
27
|
+
{ kind: "listing", type },
|
|
28
|
+
])),
|
|
16
29
|
bundles: { kind: "category", type: "bundle" },
|
|
17
30
|
};
|
|
31
|
+
/** Listing types rendered by the shared products listing rather than a dedicated one. */
|
|
32
|
+
const GENERIC_TAB_LISTING_TYPES = {
|
|
33
|
+
[pluginFor("standard").tabSlug]: ["standard"],
|
|
34
|
+
[pluginFor("classified").tabSlug]: ["classified"],
|
|
35
|
+
[pluginFor("digital-code").tabSlug]: ["digital-code"],
|
|
36
|
+
[pluginFor("live").tabSlug]: ["live"],
|
|
37
|
+
[pluginFor("art").tabSlug]: ART_STICKERS_LISTING_TYPES,
|
|
38
|
+
};
|
|
18
39
|
export function BrandDetailTabs({ brandName, initialProductsData, initialBundles = [], counts, enabledListingTypes, enabledCategoryTypes, }) {
|
|
19
40
|
const countFor = (id) => {
|
|
20
41
|
switch (id) {
|
|
@@ -28,11 +49,10 @@ export function BrandDetailTabs({ brandName, initialProductsData, initialBundles
|
|
|
28
49
|
};
|
|
29
50
|
const visibleTabs = CATEGORY_PAGE_TABS.filter((t) => {
|
|
30
51
|
const mapping = TAB_TYPE_MAP[t.id];
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
// here rather than rely on downstream JSX to silently render nothing.
|
|
52
|
+
// A tab with no mapping has no render branch below and would show a blank
|
|
53
|
+
// panel, so drop it here rather than let downstream JSX render nothing.
|
|
54
|
+
// Today that's only `stores` — a brand has no store roster. Every listing
|
|
55
|
+
// type IS mapped now (it used to also silently drop four real types).
|
|
36
56
|
if (!mapping)
|
|
37
57
|
return false;
|
|
38
58
|
if (mapping.kind === "listing" && enabledListingTypes) {
|
|
@@ -49,5 +69,5 @@ export function BrandDetailTabs({ brandName, initialProductsData, initialBundles
|
|
|
49
69
|
});
|
|
50
70
|
const firstTabId = (visibleTabs[0]?.id ?? "products");
|
|
51
71
|
const [activeTab, setActiveTab] = useState(firstTabId);
|
|
52
|
-
return (_jsxs(_Fragment, { children: [_jsx(Tabs, { value: activeTab, onChange: (v) => setActiveTab(v), className: "mb-6", children: _jsx(TabsList, { children: visibleTabs.map((t) => (_jsx(TabsTrigger, { value: t.id, badge: countFor(t.id), children: t.label }, t.id))) }) }), activeTab
|
|
72
|
+
return (_jsxs(_Fragment, { children: [_jsx(Tabs, { value: activeTab, onChange: (v) => setActiveTab(v), className: "mb-6", children: _jsx(TabsList, { children: visibleTabs.map((t) => (_jsx(TabsTrigger, { value: t.id, badge: countFor(t.id), children: t.label }, t.id))) }) }), GENERIC_TAB_LISTING_TYPES[activeTab] && (_jsx(CategoryProductsListing, { categorySlug: "", brandName: brandName, listingTypes: GENERIC_TAB_LISTING_TYPES[activeTab], initialData: activeTab === pluginFor("standard").tabSlug ? initialProductsData : undefined })), activeTab === pluginFor("auction").tabSlug && (_jsx(AuctionsIndexListing, { brandName: brandName })), activeTab === pluginFor("pre-order").tabSlug && (_jsx(PreOrdersIndexListing, { brandName: brandName })), activeTab === pluginFor("prize-draw").tabSlug && (_jsx(PrizeDrawsIndexListing, { brandName: brandName })), activeTab === "bundles" && (_jsx(CategoryBundlesListing, { initialBundles: initialBundles, brandName: brandName }))] }));
|
|
53
73
|
}
|
|
@@ -8,16 +8,36 @@ import { PreOrdersIndexListing } from "../../pre-orders/components/PreOrdersInde
|
|
|
8
8
|
import { PrizeDrawsIndexListing } from "../../products/components/PrizeDrawsIndexListing";
|
|
9
9
|
import { CategoryBundlesListing } from "./CategoryBundlesListing";
|
|
10
10
|
import { CategoryStoresListing } from "./CategoryStoresListing";
|
|
11
|
-
import { CATEGORY_PAGE_TABS } from "../../products/constants/listing-tabs";
|
|
12
|
-
|
|
11
|
+
import { CATEGORY_PAGE_TABS, ART_STICKERS_LISTING_TYPES, } from "../../products/constants/listing-tabs";
|
|
12
|
+
import { ALL_LISTING_TYPES } from "../../../_internal/shared/listing-types/feature-flags";
|
|
13
|
+
import { pluginFor } from "../../../_internal/shared/listing-types/_registry";
|
|
14
|
+
/**
|
|
15
|
+
* Maps CATEGORY_PAGE_TABS id (a `tabSlug`) → listing type / category type key
|
|
16
|
+
* for feature-flag filtering.
|
|
17
|
+
*
|
|
18
|
+
* The listing-type half is DERIVED (2026-08-21). It used to be hand-written
|
|
19
|
+
* and covered only 6 of the 10 tabs — `classifieds`, `digital-codes`, `live`
|
|
20
|
+
* and `art` had no entry, so they skipped the flag check entirely AND (worse)
|
|
21
|
+
* had no render branch below, meaning all four rendered a tab you could click
|
|
22
|
+
* that showed an empty page.
|
|
23
|
+
*/
|
|
13
24
|
const TAB_TYPE_MAP = {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
25
|
+
...Object.fromEntries(ALL_LISTING_TYPES.map((type) => [
|
|
26
|
+
pluginFor(type).tabSlug,
|
|
27
|
+
{ kind: "listing", type },
|
|
28
|
+
])),
|
|
18
29
|
bundles: { kind: "category", type: "bundle" },
|
|
19
30
|
stores: { kind: "entity", type: "stores" },
|
|
20
31
|
};
|
|
32
|
+
/** Listing types rendered by the shared products listing rather than a dedicated one. */
|
|
33
|
+
const GENERIC_TAB_LISTING_TYPES = {
|
|
34
|
+
[pluginFor("standard").tabSlug]: ["standard"],
|
|
35
|
+
[pluginFor("classified").tabSlug]: ["classified"],
|
|
36
|
+
[pluginFor("digital-code").tabSlug]: ["digital-code"],
|
|
37
|
+
[pluginFor("live").tabSlug]: ["live"],
|
|
38
|
+
// The combined Art & Stickers tab spans both types under the `art` slug.
|
|
39
|
+
[pluginFor("art").tabSlug]: ART_STICKERS_LISTING_TYPES,
|
|
40
|
+
};
|
|
21
41
|
export function CategoryDetailTabs({ categorySlug, categoryId, initialProductsData, initialBundles = [], initialStores = [], counts, enabledListingTypes, enabledCategoryTypes, }) {
|
|
22
42
|
const countFor = (id) => {
|
|
23
43
|
switch (id) {
|
|
@@ -51,5 +71,5 @@ export function CategoryDetailTabs({ categorySlug, categoryId, initialProductsDa
|
|
|
51
71
|
});
|
|
52
72
|
const firstTabId = (visibleTabs[0]?.id ?? "products");
|
|
53
73
|
const [activeTab, setActiveTab] = useState(firstTabId);
|
|
54
|
-
return (_jsxs(_Fragment, { children: [_jsx(Tabs, { value: activeTab, onChange: (v) => setActiveTab(v), className: "mb-6", children: _jsx(TabsList, { children: visibleTabs.map((t) => (_jsx(TabsTrigger, { value: t.id, badge: countFor(t.id), children: t.label }, t.id))) }) }), activeTab
|
|
74
|
+
return (_jsxs(_Fragment, { children: [_jsx(Tabs, { value: activeTab, onChange: (v) => setActiveTab(v), className: "mb-6", children: _jsx(TabsList, { children: visibleTabs.map((t) => (_jsx(TabsTrigger, { value: t.id, badge: countFor(t.id), children: t.label }, t.id))) }) }), GENERIC_TAB_LISTING_TYPES[activeTab] && (_jsx(CategoryProductsListing, { categorySlug: categorySlug, categoryId: categoryId, listingTypes: GENERIC_TAB_LISTING_TYPES[activeTab], initialData: activeTab === pluginFor("standard").tabSlug ? initialProductsData : undefined })), activeTab === pluginFor("auction").tabSlug && (_jsx(AuctionsIndexListing, { categorySlug: categorySlug })), activeTab === pluginFor("pre-order").tabSlug && (_jsx(PreOrdersIndexListing, { categorySlug: categorySlug })), activeTab === pluginFor("prize-draw").tabSlug && (_jsx(PrizeDrawsIndexListing, { categorySlug: categorySlug })), activeTab === "bundles" && (_jsx(CategoryBundlesListing, { initialBundles: initialBundles })), activeTab === "stores" && (_jsx(CategoryStoresListing, { stores: initialStores }))] }));
|
|
55
75
|
}
|
|
@@ -4,6 +4,14 @@ export interface CategoryProductsListingProps {
|
|
|
4
4
|
categoryId?: string;
|
|
5
5
|
/** Filter products by brand name (for brand detail pages) */
|
|
6
6
|
brandName?: string;
|
|
7
|
+
/**
|
|
8
|
+
* Listing types this tab spans. Defaults to `["standard"]` — the original,
|
|
9
|
+
* only behaviour. Category/brand pages pass a single non-standard type (or
|
|
10
|
+
* the `["art","stickers"]` pair) so the classifieds / digital-codes / live /
|
|
11
|
+
* art tabs render real content instead of a blank panel: those four tabs
|
|
12
|
+
* existed in CATEGORY_PAGE_TABS but had no render branch at all.
|
|
13
|
+
*/
|
|
14
|
+
listingTypes?: readonly string[];
|
|
7
15
|
initialData?: any;
|
|
8
16
|
}
|
|
9
|
-
export declare function CategoryProductsListing({ categorySlug, categoryId, brandName, initialData, }: CategoryProductsListingProps): React.JSX.Element;
|
|
17
|
+
export declare function CategoryProductsListing({ categorySlug, categoryId, brandName, listingTypes, initialData, }: CategoryProductsListingProps): React.JSX.Element;
|
|
@@ -27,7 +27,7 @@ function renderCategoryProductGrid(props) {
|
|
|
27
27
|
}
|
|
28
28
|
return (_jsx(ProductGrid, { products: products, getProductHref: getProductHref, view: view, emptyLabel: "No products found in this category.", onWishlistToggle: onWishlistToggle, wishlistedIds: wishlistedIds, onAddToCart: onAddToCart }));
|
|
29
29
|
}
|
|
30
|
-
export function CategoryProductsListing({ categorySlug, categoryId, brandName, initialData, }) {
|
|
30
|
+
export function CategoryProductsListing({ categorySlug, categoryId, brandName, listingTypes = ["standard"], initialData, }) {
|
|
31
31
|
const table = useUrlTable({ defaults: { pageSize: "24", sort: "-createdAt" } });
|
|
32
32
|
const { showToast } = useToast();
|
|
33
33
|
const { requireAuth, modalOpen, modalMessage, closeModal } = useAuthGate();
|
|
@@ -48,7 +48,10 @@ export function CategoryProductsListing({ categorySlug, categoryId, brandName, i
|
|
|
48
48
|
sort: table.get("sort") || "-createdAt",
|
|
49
49
|
page: table.getNumber("page", 1),
|
|
50
50
|
perPage: table.getNumber("pageSize", 24),
|
|
51
|
-
|
|
51
|
+
// Pipe-joined OR-group when the tab spans several types (art + stickers);
|
|
52
|
+
// sievejs parses it as a same-field OR and the Firebase adapter upgrades
|
|
53
|
+
// it to a `.where(…, "in", …)` query.
|
|
54
|
+
listingType: listingTypes.join("|"),
|
|
52
55
|
};
|
|
53
56
|
const { products, totalPages, page, isLoading } = useProducts(params, { initialData });
|
|
54
57
|
const commitSearch = useCallback(() => {
|
|
@@ -21,6 +21,7 @@ import { computeRelatedItems } from "../../../_internal/server/features/products
|
|
|
21
21
|
import { GroupedListingsCarousel } from "../../grouped/components/GroupedListingsCarousel";
|
|
22
22
|
import { getGroupsWithItemsForProduct } from "../../../_internal/server/features/grouped/data";
|
|
23
23
|
import { ClassifiedContactSellerPanel } from "./ClassifiedContactSellerPanel";
|
|
24
|
+
import { ListingBottomActions } from "../../products/components/ListingBottomActions";
|
|
24
25
|
function toDescriptionHtml(raw) {
|
|
25
26
|
if (!raw)
|
|
26
27
|
return "";
|
|
@@ -65,5 +66,5 @@ export async function ClassifiedDetailPageView({ slug, initialProduct }) {
|
|
|
65
66
|
id: s.id,
|
|
66
67
|
label: s.title,
|
|
67
68
|
content: _jsx(CustomSectionTabContent, { section: s }),
|
|
68
|
-
})) })), renderBuyBar: () => (_jsx(Stack, { id: "classified-contact-bar", className: "p-[var(--appkit-space-5)]", border: "subtle", gap: "md", rounded: "xl", surface: "muted", children: _jsx(ClassifiedContactSellerPanel, { productId: String(product.id), onContactSeller: startClassifiedConversationAction }) })), renderRelated: () => (_jsxs(Stack, { gap: "xl", children: [_jsx(GroupedListingsCarousel, { groups: groups }), _jsx(RelatedItemsSection, { relatedItems: relatedItems, relatedByBrand: relatedByBrand, relatedByTags: relatedByTags, relatedByStore: relatedByStore, categoryLabel: categoryName || category || undefined, brandLabel: brand, storeLabel: storeName ?? undefined })] })) })] })] }));
|
|
69
|
+
})) })), renderBuyBar: () => (_jsx(Stack, { id: "classified-contact-bar", className: "p-[var(--appkit-space-5)]", border: "subtle", gap: "md", rounded: "xl", surface: "muted", children: _jsx(ClassifiedContactSellerPanel, { productId: String(product.id), onContactSeller: startClassifiedConversationAction }) })), renderRelated: () => (_jsxs(Stack, { gap: "xl", children: [_jsx(GroupedListingsCarousel, { groups: groups }), _jsx(RelatedItemsSection, { relatedItems: relatedItems, relatedByBrand: relatedByBrand, relatedByTags: relatedByTags, relatedByStore: relatedByStore, categoryLabel: categoryName || category || undefined, brandLabel: brand, storeLabel: storeName ?? undefined })] })) }), _jsx(ListingBottomActions, { listingType: "classified", anchorId: "classified-contact-bar", price: price, currency: currency })] })] }));
|
|
69
70
|
}
|
|
@@ -47,7 +47,11 @@ async function resolveEmailProvider() {
|
|
|
47
47
|
try {
|
|
48
48
|
return getProviders().email;
|
|
49
49
|
}
|
|
50
|
-
catch {
|
|
50
|
+
catch (err) {
|
|
51
|
+
// Not an error condition — "no email provider registered yet" is the
|
|
52
|
+
// expected signal on the Functions runtime, and the fallback below builds
|
|
53
|
+
// one from credentials. Normalized anyway so the catch is never bare.
|
|
54
|
+
void normalizeError(err);
|
|
51
55
|
const [creds, settings] = await Promise.all([
|
|
52
56
|
siteSettingsRepository.getDecryptedCredentials().catch((err) => {
|
|
53
57
|
void normalizeError(err);
|