@mohasinac/appkit 4.11.3 → 4.12.0
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 +36 -3
- package/dist/_internal/server/features/checkout/actions.js +122 -44
- package/dist/_internal/server/features/orders/adapters.js +13 -0
- package/dist/_internal/server/features/products/list-public.d.ts +28 -0
- package/dist/_internal/server/features/products/list-public.js +92 -0
- package/dist/_internal/server/jobs/core/auctionSettlement.js +5 -2
- package/dist/_internal/shared/checkout/lanes.d.ts +20 -0
- package/dist/_internal/shared/checkout/lanes.js +28 -0
- package/dist/_internal/shared/features/checkout/config.d.ts +4 -0
- package/dist/_internal/shared/features/checkout/config.js +8 -0
- package/dist/_internal/shared/fees/calculator.d.ts +34 -2
- package/dist/_internal/shared/fees/calculator.js +45 -1
- package/dist/client.d.ts +7 -1
- package/dist/client.js +4 -1
- package/dist/constants/api-endpoints.d.ts +6 -0
- package/dist/constants/api-endpoints.js +2 -0
- package/dist/features/admin/components/AdminAddressesView.js +18 -10
- package/dist/features/admin/components/AdminArtView.d.ts +9 -0
- package/dist/features/admin/components/AdminArtView.js +11 -35
- package/dist/features/admin/components/AdminClassifiedView.d.ts +9 -0
- package/dist/features/admin/components/AdminClassifiedView.js +11 -35
- package/dist/features/admin/components/AdminDigitalCodesView.d.ts +9 -0
- package/dist/features/admin/components/AdminDigitalCodesView.js +11 -42
- package/dist/features/admin/components/AdminLiveView.d.ts +9 -0
- package/dist/features/admin/components/AdminLiveView.js +11 -41
- package/dist/features/admin/components/AdminOrderEditorView.d.ts +8 -1
- package/dist/features/admin/components/AdminOrderEditorView.js +4 -3
- package/dist/features/admin/components/AdminOrdersView.js +14 -1
- package/dist/features/admin/components/AdminPaymentMethodsView.js +15 -10
- package/dist/features/admin/components/AdminSiteConfigGuideView.js +1 -1
- package/dist/features/admin/components/AdminSiteSettingsView.js +4 -2
- package/dist/features/admin/components/AdminStickersView.d.ts +9 -0
- package/dist/features/admin/components/AdminStickersView.js +11 -35
- package/dist/features/admin/hooks/useAdminListing.js +7 -2
- package/dist/features/admin/schemas/firestore.d.ts +5 -0
- package/dist/features/admin/schemas/firestore.js +1 -0
- package/dist/features/auctions/actions/bid-actions.js +2 -2
- package/dist/features/cart/components/CartDrawer.d.ts +9 -1
- package/dist/features/cart/components/CartDrawer.js +4 -3
- package/dist/features/cart/components/CartPriceBreakdown.d.ts +61 -0
- package/dist/features/cart/components/CartPriceBreakdown.js +25 -0
- package/dist/features/cart/components/StoreAddonsPicker.d.ts +47 -0
- package/dist/features/cart/components/StoreAddonsPicker.js +21 -0
- package/dist/features/cart/components/index.d.ts +4 -0
- package/dist/features/cart/components/index.js +2 -0
- package/dist/features/cart/repository/cart.repository.d.ts +20 -2
- package/dist/features/cart/repository/cart.repository.js +49 -0
- package/dist/features/cart/schemas/firestore.d.ts +26 -0
- package/dist/features/layout/BottomActions.js +37 -4
- package/dist/features/layout/BottomActionsContext.d.ts +12 -0
- package/dist/features/layout/BottomActionsContext.js +5 -0
- package/dist/features/layout/hooks/useBottomActions.d.ts +6 -0
- package/dist/features/layout/hooks/useBottomActions.js +11 -1
- package/dist/features/orders/components/OrderAddonBadges.d.ts +36 -0
- package/dist/features/orders/components/OrderAddonBadges.js +28 -0
- package/dist/features/orders/components/index.d.ts +1 -0
- package/dist/features/orders/components/index.js +1 -0
- package/dist/features/orders/repository/orders.repository.d.ts +32 -0
- package/dist/features/orders/repository/orders.repository.js +14 -0
- package/dist/features/orders/types/index.d.ts +22 -0
- package/dist/features/products/components/ArtStickersListView.js +5 -4
- package/dist/features/products/components/PrizeDrawsListingView.js +18 -33
- package/dist/features/products/components/ProductsIndexListing.js +87 -11
- package/dist/features/products/components/ProductsIndexPageView.js +14 -2
- package/dist/features/products/config/listing-type-listing-config.d.ts +31 -0
- package/dist/features/products/config/listing-type-listing-config.js +74 -0
- package/dist/features/products/hooks/useProducts.js +14 -0
- package/dist/features/products/types/index.d.ts +15 -0
- package/dist/features/seller/components/SellerOrdersView.js +2 -1
- package/dist/features/stores/components/StoreArtStickersPageView.d.ts +21 -0
- package/dist/features/stores/components/StoreArtStickersPageView.js +30 -0
- package/dist/features/stores/components/StoreAuctionsPageView.d.ts +4 -1
- package/dist/features/stores/components/StoreAuctionsPageView.js +17 -17
- package/dist/features/stores/components/StoreClassifiedsListing.js +7 -0
- package/dist/features/stores/components/StoreClassifiedsPageView.d.ts +4 -1
- package/dist/features/stores/components/StoreClassifiedsPageView.js +16 -12
- package/dist/features/stores/components/StoreDetailLayoutView.js +4 -3
- package/dist/features/stores/components/StoreDigitalCodesListing.js +5 -0
- package/dist/features/stores/components/StoreDigitalCodesPageView.d.ts +4 -1
- package/dist/features/stores/components/StoreDigitalCodesPageView.js +16 -12
- package/dist/features/stores/components/StoreLiveItemsListing.js +7 -0
- package/dist/features/stores/components/StoreLiveItemsPageView.d.ts +4 -1
- package/dist/features/stores/components/StoreLiveItemsPageView.js +16 -12
- package/dist/features/stores/components/StorePreOrdersPageView.d.ts +4 -1
- package/dist/features/stores/components/StorePreOrdersPageView.js +17 -17
- package/dist/features/stores/components/StorePrizeDrawsPageView.d.ts +4 -8
- package/dist/features/stores/components/StorePrizeDrawsPageView.js +17 -22
- package/dist/features/stores/components/StoreProductsListing.d.ts +7 -1
- package/dist/features/stores/components/StoreProductsListing.js +3 -2
- package/dist/features/stores/components/StoreProductsPageView.d.ts +1 -1
- package/dist/features/stores/components/StoreProductsPageView.js +16 -21
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +602 -0
- package/dist/next/routing/route-map.d.ts +2 -0
- package/dist/next/routing/route-map.js +5 -0
- package/dist/seed/reviews-seed-data.js +52 -0
- package/dist/server-entry.d.ts +1 -0
- package/dist/server-entry.js +1 -0
- package/dist/styles.css +12 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/Checkbox.d.ts +10 -1
- package/dist/ui/components/Checkbox.js +14 -2
- package/dist/ui/components/Checkbox.style.css +11 -0
- package/package.json +1 -1
|
@@ -1,33 +1,13 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { sieveFilter, SIEVE_OP } from "@mohasinac/appkit";
|
|
3
|
-
import { productRepository } from "../../../repositories";
|
|
4
2
|
import { Container, Main, Heading, Section, Text } from "../../../ui";
|
|
5
3
|
import { AdSlot } from "../../homepage/components/AdSlot";
|
|
6
|
-
import {
|
|
4
|
+
import { PRODUCT_FIELDS } from "../../../constants/field-names";
|
|
5
|
+
import { sortBy } from "../../../constants/sort";
|
|
6
|
+
import { listPublicProducts, parsePublicProductParams, defaultTogglesForListingTypes, } from "../../../_internal/server/features/products/list-public";
|
|
7
7
|
import { PrizeDrawsIndexListing } from "./PrizeDrawsIndexListing";
|
|
8
|
-
const
|
|
8
|
+
const LISTING_TYPES = ["prize-draw"];
|
|
9
9
|
const DEFAULT_PAGE_SIZE = 24;
|
|
10
|
-
const DEFAULT_SORT =
|
|
11
|
-
function sp(params, key) {
|
|
12
|
-
const v = params[key];
|
|
13
|
-
return Array.isArray(v) ? v[0] ?? "" : v ?? "";
|
|
14
|
-
}
|
|
15
|
-
function buildPrizeDrawFilters(params) {
|
|
16
|
-
const parts = ["status==published", "listingType==prize-draw"];
|
|
17
|
-
const minPrice = sp(params, "minPrice");
|
|
18
|
-
const maxPrice = sp(params, "maxPrice");
|
|
19
|
-
if (minPrice)
|
|
20
|
-
parts.push(sieveFilter("pricePerEntry", SIEVE_OP.GTE, minPrice));
|
|
21
|
-
if (maxPrice)
|
|
22
|
-
parts.push(sieveFilter("pricePerEntry", SIEVE_OP.LTE, maxPrice));
|
|
23
|
-
const store = sp(params, "storeId");
|
|
24
|
-
if (store)
|
|
25
|
-
parts.push(sieveFilter("storeId", SIEVE_OP.EQ, store));
|
|
26
|
-
const status = sp(params, "prizeRevealStatus");
|
|
27
|
-
if (status)
|
|
28
|
-
parts.push(sieveFilter("prizeRevealStatus", SIEVE_OP.EQ, status));
|
|
29
|
-
return parts.join(",");
|
|
30
|
-
}
|
|
10
|
+
const DEFAULT_SORT = sortBy(PRODUCT_FIELDS.CREATED_AT);
|
|
31
11
|
/**
|
|
32
12
|
* Public listing page (SB4-F). Server-fetches published prize-draw products,
|
|
33
13
|
* hydrates the client `PrizeDrawsIndexListing` which renders the filter
|
|
@@ -38,13 +18,18 @@ function buildPrizeDrawFilters(params) {
|
|
|
38
18
|
* already gone (matches `PrizeDrawCollage`'s `hideWonState` prop).
|
|
39
19
|
*/
|
|
40
20
|
export async function PrizeDrawsListingView({ searchParams = {}, }) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
21
|
+
// Routed through the shared query (2026-08-21). The hand-rolled builder this
|
|
22
|
+
// replaces had its own Root Cause #30 divergence: it applied the price range
|
|
23
|
+
// to `pricePerEntry`, while the client's refetch sends minPrice/maxPrice,
|
|
24
|
+
// which the shared query applies to `price`. Both fields carry the same
|
|
25
|
+
// value today, so the split was invisible — until one of them drifted.
|
|
26
|
+
// `price` wins because it is also what PRIZE_DRAW_SORT_OPTIONS sorts by for
|
|
27
|
+
// "Entry: Low to High".
|
|
28
|
+
const result = await listPublicProducts(parsePublicProductParams(searchParams, {
|
|
29
|
+
listingTypes: LISTING_TYPES,
|
|
30
|
+
pageSize: DEFAULT_PAGE_SIZE,
|
|
31
|
+
sorts: DEFAULT_SORT,
|
|
32
|
+
...defaultTogglesForListingTypes(LISTING_TYPES),
|
|
33
|
+
}));
|
|
49
34
|
return (_jsx(Main, { children: _jsx(Section, { padding: "y-2xl", children: _jsxs(Container, { size: "xl", children: [_jsx(Heading, { level: 1, className: "mb-2", color: "primary", size: "3xl", weight: "semibold", children: "Prize Draws" }), _jsx(Text, { className: "mb-6 text-[var(--appkit-color-text-muted)]", size: "sm", children: "Fair-RNG draws for sealed Pok\u00E9mon, Hot Wheels Super Treasure Hunts, Gundam kits and more. Every winner picked by crypto.randomInt \u2014 proof on GitHub." }), _jsx(AdSlot, { id: "listing-sidebar-top", className: "mb-6" }), _jsx(PrizeDrawsIndexListing, { initialData: result ?? undefined }), _jsx(AdSlot, { id: "listing-sidebar-bottom", className: "mt-8" })] }) }) }));
|
|
50
35
|
}
|
|
@@ -12,7 +12,7 @@ import { ACTION_ID, ACTION_META, COMPARE_MAX_ITEMS } from "../constants/action-d
|
|
|
12
12
|
import { CompareOverlay } from "./CompareOverlay";
|
|
13
13
|
import { ROUTES } from "../../../next";
|
|
14
14
|
import { pluginFor } from "../../../_internal/shared/listing-types/_registry";
|
|
15
|
-
import { ProductGrid, ProductFilters
|
|
15
|
+
import { ProductGrid, ProductFilters } from ".";
|
|
16
16
|
import { useGuestCart } from "../../cart/hooks/useGuestCart";
|
|
17
17
|
import { getGuestCartItems } from "../../cart/utils/guest-cart";
|
|
18
18
|
import { useGuestWishlist } from "../../wishlist/hooks/useGuestWishlist";
|
|
@@ -28,6 +28,9 @@ import { sortBy } from "../../../constants/sort";
|
|
|
28
28
|
import { PRODUCT_FIELDS } from "../../../constants/field-names";
|
|
29
29
|
import { useBottomActions } from "../../layout";
|
|
30
30
|
import { GENERIC_PRODUCT_LISTING_TYPES, PRODUCT_TYPE_FILTER_TABS } from "../constants/listing-tabs";
|
|
31
|
+
import { parseSelectedListingTypes } from "../utils/listing-type";
|
|
32
|
+
import { commonSortOptionsFor, hideDefaultsFor } from "../../../_internal/shared/listing-types/_registry";
|
|
33
|
+
import { TextLink } from "../../../ui";
|
|
31
34
|
const __P = {
|
|
32
35
|
p3: "p-[var(--appkit-space-3)]",
|
|
33
36
|
};
|
|
@@ -46,7 +49,40 @@ export function ProductsIndexListing({ initialData, listingTypes = GENERIC_PRODU
|
|
|
46
49
|
const [compareIds, setCompareIds] = useState([]);
|
|
47
50
|
const showSold = table.get(TABLE_KEYS.SHOW_SOLD) === "true";
|
|
48
51
|
const [view, setView] = useState(table.get(TABLE_KEYS.VIEW) || VIEW_MODE.GRID);
|
|
49
|
-
|
|
52
|
+
// ── Type filter (multi-select) ───────────────────────────────────────────
|
|
53
|
+
// The URL value is a pipe-joined set; empty means "every type this page
|
|
54
|
+
// spans". Unknown tokens are dropped rather than passed through to a
|
|
55
|
+
// Firestore `==` that could never match (Root Cause #33).
|
|
56
|
+
const typeParam = table.get(TABLE_KEYS.LISTING_TYPE);
|
|
57
|
+
const selectedTypes = parseSelectedListingTypes(typeParam).filter((t) => listingTypes.includes(t));
|
|
58
|
+
const effectiveTypes = (selectedTypes.length > 0 ? selectedTypes : listingTypes);
|
|
59
|
+
// Sorts valid across every selected type. Pick Auctions alone and you get
|
|
60
|
+
// "Ending Soon"; pick Auctions + Products and you get only what both
|
|
61
|
+
// support, because a sort on `auctionEndDate` would order the products
|
|
62
|
+
// arbitrarily (and needs an index nobody declares for that shape).
|
|
63
|
+
const sortOptions = commonSortOptionsFor(effectiveTypes, "public");
|
|
64
|
+
const sortValue = table.get(TABLE_KEYS.SORT) || DEFAULT_SORT;
|
|
65
|
+
// Guard against a sort carried over from a previous selection that the new
|
|
66
|
+
// one can't satisfy — that combination throws FAILED_PRECONDITION in
|
|
67
|
+
// Firestore and surfaces as a bare empty page (Root Cause #2).
|
|
68
|
+
const effectiveSort = sortOptions.some((o) => o.value === sortValue)
|
|
69
|
+
? sortValue
|
|
70
|
+
: DEFAULT_SORT;
|
|
71
|
+
// Which "Show X" toggles are meaningful for the current selection. A mixed
|
|
72
|
+
// set can need both (products sell out, auctions end).
|
|
73
|
+
const hideDefaults = hideDefaultsFor(effectiveTypes);
|
|
74
|
+
const showsSoldToggle = hideDefaults.includes("sold");
|
|
75
|
+
const showsEndedToggle = hideDefaults.includes("ended") || hideDefaults.includes("closed");
|
|
76
|
+
const showEnded = table.get(TABLE_KEYS.SHOW_ENDED) === "true";
|
|
77
|
+
// "Full <type> filters →" — a single selected type with a dedicated browse
|
|
78
|
+
// page gets a link to it, since that page carries facets this generic one
|
|
79
|
+
// can't (bid range, delivery-date window, reveal status).
|
|
80
|
+
const dedicatedPage = selectedTypes.length === 1 && pluginFor(selectedTypes[0]).browseRoute
|
|
81
|
+
? {
|
|
82
|
+
href: pluginFor(selectedTypes[0]).browseRoute,
|
|
83
|
+
label: `Full ${pluginFor(selectedTypes[0]).pluralLabel} filters →`,
|
|
84
|
+
}
|
|
85
|
+
: null;
|
|
50
86
|
const { pendingTable, filterActiveCount, onFilterApply, onFilterClear, onResetAll, onFilterReset } = usePendingTable(table, FILTER_KEYS);
|
|
51
87
|
const openFilters = useCallback(() => {
|
|
52
88
|
onFilterReset();
|
|
@@ -57,11 +93,19 @@ export function ProductsIndexListing({ initialData, listingTypes = GENERIC_PRODU
|
|
|
57
93
|
setFilterOpen(false);
|
|
58
94
|
}, [onFilterApply]);
|
|
59
95
|
const resetAll = useCallback(() => {
|
|
60
|
-
onResetAll({
|
|
96
|
+
onResetAll({
|
|
97
|
+
[TABLE_KEYS.QUERY]: "",
|
|
98
|
+
[TABLE_KEYS.SORT]: "",
|
|
99
|
+
[TABLE_KEYS.SHOW_SOLD]: "",
|
|
100
|
+
[TABLE_KEYS.SHOW_ENDED]: "",
|
|
101
|
+
[TABLE_KEYS.LISTING_TYPE]: "",
|
|
102
|
+
});
|
|
61
103
|
setSearchInput("");
|
|
62
104
|
}, [onResetAll]);
|
|
63
105
|
const hasActiveState = !!table.get(TABLE_KEYS.QUERY) ||
|
|
64
106
|
table.get(TABLE_KEYS.SHOW_SOLD) === "true" ||
|
|
107
|
+
table.get(TABLE_KEYS.SHOW_ENDED) === "true" ||
|
|
108
|
+
!!typeParam ||
|
|
65
109
|
table.get(TABLE_KEYS.SORT) !== DEFAULT_SORT ||
|
|
66
110
|
filterActiveCount > 0;
|
|
67
111
|
const localCart = useGuestCart();
|
|
@@ -81,13 +125,27 @@ export function ProductsIndexListing({ initialData, listingTypes = GENERIC_PRODU
|
|
|
81
125
|
brand: table.get(TABLE_KEYS.BRAND) || undefined,
|
|
82
126
|
storeId: table.get(TABLE_KEYS.STORE_ID) || undefined,
|
|
83
127
|
freeShipping: table.get(TABLE_KEYS.FREE_SHIPPING) === "true" ? true : undefined,
|
|
84
|
-
|
|
128
|
+
// These three are in FILTER_KEYS (so they count toward the filter badge)
|
|
129
|
+
// but were never put on the wire — the drawer sections were inert.
|
|
130
|
+
tags: table.get(TABLE_KEYS.TAGS) || undefined,
|
|
131
|
+
sublistingCategory: table.get(TABLE_KEYS.SUBLISTING_CATEGORY) || undefined,
|
|
132
|
+
features: table.get(TABLE_KEYS.FEATURES) || undefined,
|
|
133
|
+
sort: effectiveSort,
|
|
85
134
|
page: table.getNumber(TABLE_KEYS.PAGE, 1),
|
|
86
135
|
perPage: table.getNumber(TABLE_KEYS.PAGE_SIZE, 24),
|
|
87
|
-
|
|
88
|
-
//
|
|
89
|
-
|
|
90
|
-
|
|
136
|
+
// Pipe-joined OR-group — sievejs parses it as a same-field OR and the
|
|
137
|
+
// Firebase adapter upgrades it to a `.where(…, "in", …)` query.
|
|
138
|
+
listingType: effectiveTypes.join("|"),
|
|
139
|
+
// Hide sold-out items by default, but only when a sold-out-able type is in
|
|
140
|
+
// play — an auction has no meaningful stock, so applying it to an
|
|
141
|
+
// auctions-only view would hide every live auction.
|
|
142
|
+
inStock: showsSoldToggle && !showSold ? true : undefined,
|
|
143
|
+
// Same shape for time-boxed types. `dateFrom` only takes effect when
|
|
144
|
+
// exactly one type is selected (there is no single end-date field shared
|
|
145
|
+
// across types), which `dateFieldFor` already enforces server-side.
|
|
146
|
+
dateFrom: showsEndedToggle && !showEnded && selectedTypes.length === 1
|
|
147
|
+
? new Date().toISOString()
|
|
148
|
+
: undefined,
|
|
91
149
|
};
|
|
92
150
|
const { products, totalPages, page, isLoading } = useProducts(params, { initialData });
|
|
93
151
|
const selection = useBulkSelection({ items: products, keyExtractor: (p) => p.id });
|
|
@@ -200,14 +258,32 @@ export function ProductsIndexListing({ initialData, listingTypes = GENERIC_PRODU
|
|
|
200
258
|
},
|
|
201
259
|
},
|
|
202
260
|
] } } : {});
|
|
203
|
-
return (_jsxs(Div, { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: filterActiveCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: searchPlaceholder, onSearchChange: setSearchInput, onSearchCommit: commitSearch, onSearchKeyDown: handleSearchKeyDown, sortValue:
|
|
204
|
-
|
|
261
|
+
return (_jsxs(Div, { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: filterActiveCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: searchPlaceholder, onSearchChange: setSearchInput, onSearchCommit: commitSearch, onSearchKeyDown: handleSearchKeyDown, sortValue: effectiveSort, sortOptions: sortOptions, onSortChange: (v) => { table.set(TABLE_KEYS.SORT, v); }, view: view, onViewChange: handleViewToggle, onResetAll: resetAll, hasActiveState: hasActiveState, bulkMode: selection.isSelecting, bulkSelectedCount: selection.selectedCount, bulkTotalCount: products.length, onBulkSelectAll: selection.toggleAll, onBulkClear: selection.clearSelection, toggles: [
|
|
262
|
+
// Each toggle appears only when it means something for the selected
|
|
263
|
+
// types — "Show ended" against a Products-only view would be noise,
|
|
264
|
+
// and "Show sold" against an Auctions-only view is meaningless.
|
|
265
|
+
...(showsSoldToggle
|
|
266
|
+
? [{ label: "Show sold", active: showSold, onChange: (next) => table.set(TABLE_KEYS.SHOW_SOLD, next ? "true" : "") }]
|
|
267
|
+
: []),
|
|
268
|
+
...(showsEndedToggle
|
|
269
|
+
? [{ label: "Show ended", active: showEnded, onChange: (next) => table.set(TABLE_KEYS.SHOW_ENDED, next ? "true" : "") }]
|
|
270
|
+
: []),
|
|
205
271
|
// Inline quick-filter: the highest-frequency drawer-only facet promoted
|
|
206
272
|
// to the sticky toolbar so it doesn't require open-drawer → check → Apply
|
|
207
273
|
// → close for every toggle. The full facet set (category/price/brand/…)
|
|
208
274
|
// remains in <FilterDrawer> below.
|
|
209
275
|
{ label: "Free shipping", active: table.get(TABLE_KEYS.FREE_SHIPPING) === "true", onChange: (next) => table.set(TABLE_KEYS.FREE_SHIPPING, next ? "true" : "") },
|
|
210
|
-
] }), _jsx(Div, { padding: "y-sm", children:
|
|
276
|
+
] }), _jsx(Div, { padding: "y-sm", children: _jsxs(Row, { justify: "between", align: "center", wrap: true, gap: "sm", children: [_jsx(FilterChipGroup, { multiple: true, label: "Type", tabs: typeTabs, value: selectedTypes.join("|"), onChange: (next) => {
|
|
277
|
+
// Clear `sort` alongside the type change: a sort valid for the
|
|
278
|
+
// old selection (say -auctionEndDate) can be invalid for the new
|
|
279
|
+
// one, and useUrlTable's single router.replace means we must set
|
|
280
|
+
// both in ONE call — a follow-up setPage/set would read stale
|
|
281
|
+
// searchParams and overwrite this update (Root Cause #13).
|
|
282
|
+
table.setMany({
|
|
283
|
+
[TABLE_KEYS.LISTING_TYPE]: next,
|
|
284
|
+
[TABLE_KEYS.SORT]: "",
|
|
285
|
+
});
|
|
286
|
+
} }), dedicatedPage && (_jsx(TextLink, { href: dedicatedPage.href, size: "sm", weight: "medium", children: dedicatedPage.label }))] }) }), _jsx(BulkActionBar, { selectedCount: selection.selectedCount, onClearSelection: selection.clearSelection, actions: [
|
|
211
287
|
{
|
|
212
288
|
id: ACTION_ID.ADD_TO_CART,
|
|
213
289
|
label: ACTION_META[ACTION_ID.ADD_TO_CART].label,
|
|
@@ -4,19 +4,31 @@ import { Container, Div, Heading, Main, Section, Text, TextLink } from "../../..
|
|
|
4
4
|
import { AdSlot } from "../../homepage/components/AdSlot";
|
|
5
5
|
import { ProductsIndexListing } from "./ProductsIndexListing";
|
|
6
6
|
import { PRODUCT_FIELDS } from "../../../constants/field-names";
|
|
7
|
+
import { TABLE_KEYS } from "../../../constants/table-keys";
|
|
7
8
|
import { sortBy } from "../../../constants/sort";
|
|
8
9
|
import { GENERIC_PRODUCT_LISTING_TYPES } from "../constants/listing-tabs";
|
|
9
|
-
import { listPublicProducts, parsePublicProductParams, } from "../../../_internal/server/features/products/list-public";
|
|
10
|
+
import { listPublicProducts, parsePublicProductParams, defaultTogglesForListingTypes, } from "../../../_internal/server/features/products/list-public";
|
|
11
|
+
import { parseSelectedListingTypes } from "../utils/listing-type";
|
|
10
12
|
const DEFAULT_PAGE_SIZE = 24;
|
|
11
13
|
const DEFAULT_SORT = sortBy(PRODUCT_FIELDS.CREATED_AT);
|
|
12
14
|
export async function ProductsIndexPageView({ searchParams = {} }) {
|
|
15
|
+
// The "Show sold" / "Show ended" defaults depend on WHICH types are in play,
|
|
16
|
+
// and this page now spans all nine. Both sides derive them from the same
|
|
17
|
+
// helper — a hardcoded `hideSoldByDefault: true` here would disagree with
|
|
18
|
+
// the client the moment the user ticks Auctions, and `staleTime: Infinity`
|
|
19
|
+
// would freeze that disagreement (Root Cause #30).
|
|
20
|
+
const raw = searchParams[TABLE_KEYS.LISTING_TYPE];
|
|
21
|
+
const selectedTypes = parseSelectedListingTypes(Array.isArray(raw) ? raw[0] : raw);
|
|
22
|
+
const effectiveTypes = selectedTypes.length > 0 ? selectedTypes : GENERIC_PRODUCT_LISTING_TYPES;
|
|
23
|
+
const { hideSoldByDefault, hideEndedByDefault } = defaultTogglesForListingTypes(effectiveTypes);
|
|
13
24
|
// See ArtStickersListView — one shared filter implementation with
|
|
14
25
|
// /api/products, so SSR and the client refetch agree by construction.
|
|
15
26
|
const products = await listPublicProducts(parsePublicProductParams(searchParams, {
|
|
16
27
|
listingTypes: GENERIC_PRODUCT_LISTING_TYPES,
|
|
17
28
|
pageSize: DEFAULT_PAGE_SIZE,
|
|
18
29
|
sorts: DEFAULT_SORT,
|
|
19
|
-
hideSoldByDefault
|
|
30
|
+
hideSoldByDefault,
|
|
31
|
+
hideEndedByDefault,
|
|
20
32
|
}));
|
|
21
33
|
return (_jsxs(Main, { children: [_jsx(Section, { border: "bottom-subtle", paddingY: "b-md", padding: "t-xl", children: _jsxs(Container, { size: "xl", children: [_jsx(Heading, { level: 1, size: "3xl", weight: "bold", color: "primary", children: "Products" }), _jsx(Text, { className: "mt-1", color: "muted", size: "sm", children: "Discover amazing products and deals" }), _jsx(Div, { className: "mt-3", children: _jsx(TextLink, { variant: "bare", href: String(ROUTES.PUBLIC.AUCTIONS), rounded: "full", paddingX: "sm", paddingY: "2xs", size: "xs", weight: "medium", layout: "inline-flex", align: "center", gap: "sm", className: "border border-primary/30 bg-primary/10 text-primary-700 dark:text-primary-400 hover:bg-primary/15 transition-colors", children: "\uD83C\uDFF7\uFE0F Looking for unique deals? Browse Auctions \u2192" }) })] }) }), _jsxs(Container, { size: "xl", padding: "x-md", children: [_jsx(AdSlot, { id: "listing-sidebar-top", className: "mb-4 mt-4" }), _jsx(ProductsIndexListing, { initialData: products }), _jsx(AdSlot, { id: "listing-sidebar-bottom", className: "mt-8" })] })] }));
|
|
22
34
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { JsonArray } from "../../../schemas/types";
|
|
2
|
+
import type { ListingType } from "../types";
|
|
3
|
+
import type { ListingViewConfig } from "../../admin/components/DataListingView";
|
|
4
|
+
export interface ListingTypeListingRow {
|
|
5
|
+
id: string;
|
|
6
|
+
primary: string;
|
|
7
|
+
secondary: string;
|
|
8
|
+
status: string;
|
|
9
|
+
updatedAt: string;
|
|
10
|
+
image?: string;
|
|
11
|
+
}
|
|
12
|
+
interface ListingTypeListingResponse {
|
|
13
|
+
items?: JsonArray;
|
|
14
|
+
total?: number;
|
|
15
|
+
}
|
|
16
|
+
export interface BuildListingTypeListingOptions {
|
|
17
|
+
/** Override the page heading; defaults to the type's registered plural label. */
|
|
18
|
+
title?: string;
|
|
19
|
+
searchPlaceholder?: string;
|
|
20
|
+
emptyLabel?: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Admin browse config for one listing type.
|
|
24
|
+
*
|
|
25
|
+
* `status` is a real filter here (unlike the five hand-written copies, which
|
|
26
|
+
* offered none) because an admin's main reason to open a per-type list is to
|
|
27
|
+
* find the drafts and in-review items — exactly the rows a status filter
|
|
28
|
+
* surfaces.
|
|
29
|
+
*/
|
|
30
|
+
export declare function buildListingTypeListingConfig(type: ListingType, opts?: BuildListingTypeListingOptions): ListingViewConfig<ListingTypeListingResponse, ListingTypeListingRow>;
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* WHY: Five admin views (Art, Stickers, Classified, Digital Codes, Live Items)
|
|
3
|
+
* were five copies of ONE config that differed only by listing type,
|
|
4
|
+
* title and placeholder text. Five copies is well past the Duplication
|
|
5
|
+
* Framework's Rule of Three, and it showed: every one carried
|
|
6
|
+
* `filterKeys: []` (no filter drawer at all despite filterable data) and
|
|
7
|
+
* its own hand-written three-option sort array, so a fix to any of it had
|
|
8
|
+
* to be applied five times and never was.
|
|
9
|
+
*
|
|
10
|
+
* WHAT: `buildListingTypeListingConfig(type, opts)` — one config, driven by
|
|
11
|
+
* the listing-type plugin registry. Title, sort set and per-type facet
|
|
12
|
+
* keys all come from `pluginFor(type)`, so a new listing type gets a
|
|
13
|
+
* working admin browse page with no new file.
|
|
14
|
+
*
|
|
15
|
+
* EXPORTS: ListingTypeListingRow, BuildListingTypeListingOptions,
|
|
16
|
+
* buildListingTypeListingConfig
|
|
17
|
+
*
|
|
18
|
+
* @tag domain:products
|
|
19
|
+
* @tag layer:config
|
|
20
|
+
* @tag pattern:factory
|
|
21
|
+
* @tag access:client
|
|
22
|
+
* @tag consumers:AdminArtView,AdminStickersView,AdminClassifiedView,AdminDigitalCodesView,AdminLiveView
|
|
23
|
+
* @tag sideEffects:none
|
|
24
|
+
*/
|
|
25
|
+
import { SIEVE_OP, sieveAnd, sieveFilter } from "../../../utils/sieve-builder";
|
|
26
|
+
import { sortBy } from "../../../constants/sort";
|
|
27
|
+
import { PRODUCT_FIELDS } from "../../../constants/field-names";
|
|
28
|
+
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
29
|
+
import { ROUTES } from "../../../next/routing/route-map";
|
|
30
|
+
import { pluginFor } from "../../../_internal/shared/listing-types/_registry";
|
|
31
|
+
import { toRecordArray, toRelativeDate, toCurrency, toStringValue, } from "../../admin/hooks/useAdminListingData";
|
|
32
|
+
/**
|
|
33
|
+
* Admin browse config for one listing type.
|
|
34
|
+
*
|
|
35
|
+
* `status` is a real filter here (unlike the five hand-written copies, which
|
|
36
|
+
* offered none) because an admin's main reason to open a per-type list is to
|
|
37
|
+
* find the drafts and in-review items — exactly the rows a status filter
|
|
38
|
+
* surfaces.
|
|
39
|
+
*/
|
|
40
|
+
export function buildListingTypeListingConfig(type, opts = {}) {
|
|
41
|
+
const plugin = pluginFor(type);
|
|
42
|
+
const label = opts.title ?? plugin.pluralLabel;
|
|
43
|
+
return {
|
|
44
|
+
portal: "admin",
|
|
45
|
+
title: label,
|
|
46
|
+
searchPlaceholder: opts.searchPlaceholder ?? `Search ${label.toLowerCase()} by name or seller`,
|
|
47
|
+
emptyLabel: opts.emptyLabel ?? `No ${label.toLowerCase()} listings`,
|
|
48
|
+
filterKeys: ["status"],
|
|
49
|
+
defaultSort: sortBy(PRODUCT_FIELDS.CREATED_AT),
|
|
50
|
+
// Keyed on the canonical type so two types can never share a cache entry.
|
|
51
|
+
queryKey: ["admin", "listing-type", type],
|
|
52
|
+
endpoint: ADMIN_ENDPOINTS.PRODUCTS,
|
|
53
|
+
// The type's own sort set, not a copy-pasted three-option array.
|
|
54
|
+
sortOptions: [...plugin.sortOptions],
|
|
55
|
+
mapRows: (response) => toRecordArray(response.items).map((item, index) => ({
|
|
56
|
+
id: toStringValue(item.id, `${type}-${index}`),
|
|
57
|
+
primary: toStringValue(item.title ?? item.productTitle, "Untitled listing"),
|
|
58
|
+
secondary: [
|
|
59
|
+
toStringValue(item.sellerName, "Unknown seller"),
|
|
60
|
+
toCurrency(item.price),
|
|
61
|
+
].join(" · "),
|
|
62
|
+
status: toStringValue(item.status, "draft"),
|
|
63
|
+
updatedAt: toRelativeDate(item.updatedAt ?? item.createdAt),
|
|
64
|
+
image: toStringValue(item.mainImage, "") || undefined,
|
|
65
|
+
})),
|
|
66
|
+
getTotal: (response, mappedRows) => typeof response.total === "number" ? response.total : mappedRows.length,
|
|
67
|
+
buildFilters: (state) => sieveAnd(sieveFilter(PRODUCT_FIELDS.LISTING_TYPE, SIEVE_OP.EQ, type), state.status && state.status !== "All"
|
|
68
|
+
? sieveFilter(PRODUCT_FIELDS.STATUS, SIEVE_OP.EQ, state.status)
|
|
69
|
+
: "") || undefined,
|
|
70
|
+
// These are products filtered by listingType — reuse the real admin
|
|
71
|
+
// product editor rather than leaving rows non-navigable.
|
|
72
|
+
rowHrefTemplate: String(ROUTES.ADMIN.PRODUCTS_EDIT("{id}")),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -52,6 +52,20 @@ export function useProducts(params = {}, opts) {
|
|
|
52
52
|
sp.set("brand", params.brand);
|
|
53
53
|
if (params.freeShipping !== undefined)
|
|
54
54
|
sp.set("freeShipping", String(params.freeShipping));
|
|
55
|
+
// Pipe-joined multi-select facets. Omitted here until 2026-08-21, so the
|
|
56
|
+
// Tags / Sublisting Type / Features drawer sections were inert.
|
|
57
|
+
if (params.tags)
|
|
58
|
+
sp.set("tags", params.tags);
|
|
59
|
+
if (params.sublistingCategory)
|
|
60
|
+
sp.set("sublistingCategory", params.sublistingCategory);
|
|
61
|
+
if (params.features)
|
|
62
|
+
sp.set("features", params.features);
|
|
63
|
+
// Per-type facets travel under their own TABLE_KEY names, which is what
|
|
64
|
+
// parsePublicProductParams reads them back from.
|
|
65
|
+
for (const [key, value] of Object.entries(params.typeFacets ?? {})) {
|
|
66
|
+
if (value)
|
|
67
|
+
sp.set(key, value);
|
|
68
|
+
}
|
|
55
69
|
const qs = sp.toString();
|
|
56
70
|
const query = useQuery({
|
|
57
71
|
queryKey: ["products", qs],
|
|
@@ -202,4 +202,19 @@ export interface ProductListParams {
|
|
|
202
202
|
brand?: string;
|
|
203
203
|
/** Shipping / free shipping */
|
|
204
204
|
freeShipping?: boolean;
|
|
205
|
+
/**
|
|
206
|
+
* Pipe-joined multi-select facets. All three were rendered by
|
|
207
|
+
* `<ProductFilters>` and counted toward the active-filter badge, but no
|
|
208
|
+
* caller ever put them on the wire — ticking one inflated the badge and
|
|
209
|
+
* changed nothing (fixed 2026-08-21).
|
|
210
|
+
*/
|
|
211
|
+
tags?: string;
|
|
212
|
+
sublistingCategory?: string;
|
|
213
|
+
features?: string;
|
|
214
|
+
/**
|
|
215
|
+
* Per-listing-type facets keyed by TABLE_KEY (classified city/negotiable,
|
|
216
|
+
* digital-code deliveryMethod, live species/sex/jurisdiction). The server
|
|
217
|
+
* resolves each to its nested Firestore path and ignores unknown keys.
|
|
218
|
+
*/
|
|
219
|
+
typeFacets?: Record<string, string>;
|
|
205
220
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { OrderAddonBadges } from "../../orders/components/OrderAddonBadges";
|
|
3
4
|
import { normalizeError } from "../../../errors/normalize";
|
|
4
5
|
import { Row, SIEVE_OP, sieveFilter } from "@mohasinac/appkit/client";
|
|
5
6
|
import { sortBy } from "@mohasinac/appkit/client";
|
|
@@ -167,7 +168,7 @@ export function SellerOrderDetailPanel({ orderId, apiBase, onClose, }) {
|
|
|
167
168
|
};
|
|
168
169
|
const addr = order?.shippingAddress ?? {};
|
|
169
170
|
const addrLine = [addr.addressLine1, addr.city, addr.state, addr.pincode].filter(Boolean).join(", ");
|
|
170
|
-
return (_jsxs(_Fragment, { children: [loading && (_jsx(Row, { align: "center", justify: "center", padding: "y-4xl", children: _jsx(Div, { className: "h-6 w-6 animate-spin border-2 border-[var(--appkit-color-primary)] border-t-transparent", rounded: "full" }) })), fetchError && (_jsx(Div, { textSize: "sm", className: "mx-4 mt-4 border border-error/20", color: "error", surface: "danger-surface", padding: "inline", rounded: "lg", children: fetchError })), order && !loading && (_jsxs(Stack, { gap: "none", children: [_jsxs(Stack, { className: `flex-1 ${__O.yAuto}`, gap: "5", padding: "md", children: [_jsxs(Row, { align: "center", justify: "between", children: [_jsx(Badge, { variant: STATUS_BADGE_VARIANT[order.status?.toUpperCase()] ?? "default", children: order.status ?? "Unknown" }), _jsx(Text, { size: "sm", className: "text-[var(--appkit-color-text-secondary)]", children: toRelativeDate(order.createdAt) })] }), (order.items ?? []).length > 0 && (_jsxs(Div, { children: [_jsx(Text, { size: "sm", className: "text-[var(--appkit-color-text-primary)] mb-2", weight: "semibold", children: "Items" }), _jsx(Div, { className: "divide-y divide-[var(--appkit-color-border)] divide-[var(--appkit-color-border)] border border-[var(--appkit-color-border)]", rounded: "lg", children: (order.items ?? []).map((item, i) => (_jsxs(Row, { paddingY: "y-xs-tall", padding: "x-sm", align: "center", justify: "between", gap: "3", children: [_jsxs(Row, { align: "center", gap: "sm", className: "min-w-0", children: [_jsx(Div, { className: "h-10 w-10 shrink-0", rounded: "md", overflow: "hidden", children: _jsx(MediaImage, { src: item.image, alt: item.title ?? "Order item", size: "thumbnail" }) }), _jsxs(Div, { className: "min-w-0", children: [_jsx(Text, { size: "sm", className: "truncate", weight: "medium", children: item.title ?? item.productId ?? "Item" }), _jsxs(Text, { size: "xs", className: "text-[var(--appkit-color-text-secondary)]", children: ["Qty: ", item.quantity ?? 1] })] })] }), _jsx(Text, { size: "sm", className: "shrink-0", weight: "medium", children: toCurrency(item.price ?? 0) })] }, i))) })] })), order.giftWrapAddon && (_jsxs(Div, { className: "border border-[var(--appkit-color-primary-200)] dark:border-[var(--appkit-color-primary-800)]", surface: "subtle", padding: "inline", rounded: "lg", children: [_jsx(Text, { size: "sm", weight: "semibold", children: "\uD83C\uDF81 Gift wrap requested" }), order.giftWrapMessage && (_jsxs(Text, { size: "sm", className: "mt-1 text-[var(--appkit-color-text-secondary)]", children: ["\u201C", order.giftWrapMessage, "\u201D"] }))] })), _jsxs(Row, { surface: "muted", padding: "inline", align: "center", justify: "between", rounded: "lg", children: [_jsx(Text, { size: "sm", weight: "semibold", children: "Total" }), _jsx(Text, { size: "sm", className: "text-[var(--appkit-color-primary)]", weight: "bold", children: toCurrency(order.totalAmount ?? 0) })] }), addrLine && (_jsxs(Div, { children: [_jsx(Text, { size: "sm", className: "mb-1", weight: "semibold", children: "Shipping address" }), _jsx(Text, { size: "sm", className: "text-[var(--appkit-color-text-secondary)]", children: [String(addr.fullName ?? ""), addrLine].filter(Boolean).join(" · ") })] })), order.paymentMethod && (_jsxs(Div, { children: [_jsxs(Row, { align: "center", justify: "between", className: "mb-1", gap: "3", children: [_jsx(Text, { size: "sm", weight: "semibold", children: "Payment" }), isManualPaymentMethod(order.paymentMethod) && (_jsx(Badge, { variant: sellerPaymentBadge(order).variant, children: sellerPaymentBadge(order).label }))] }), _jsx(Text, { size: "sm", className: "text-[var(--appkit-color-text-secondary)]", transform: "capitalize", children: order.paymentMethod }), isManualPaymentMethod(order.paymentMethod) && order.paymentTransactionId && (_jsxs(Text, { size: "xs", color: "muted", className: "mt-1", children: ["UTR: ", order.paymentTransactionId] }))] })), order.emiEnabled && (_jsxs(Div, { className: "border-t border-[var(--appkit-color-border)]", padding: "t-md", children: [_jsxs(Row, { align: "center", justify: "between", className: "mb-2", children: [_jsxs(Text, { size: "sm", weight: "semibold", children: ["EMI plan ", order.emiTenureMonths ? `· ${order.emiTenureMonths} months` : ""] }), _jsx(Badge, { variant: order.emiComplete ? "success" : "warning", children: order.emiComplete ? "Fully paid" : "In progress" })] }), _jsxs(Row, { align: "center", justify: "between", className: "mb-2", children: [_jsx(Text, { size: "xs", color: "muted", children: "Token collected" }), _jsx(Text, { size: "xs", weight: "medium", children: toCurrency(order.emiTokenAmount ?? 0) })] }), _jsxs(Row, { align: "center", justify: "between", className: "mb-3", children: [_jsx(Text, { size: "xs", color: "muted", children: "Remaining balance" }), _jsx(Text, { size: "xs", weight: "medium", children: toCurrency(order.emiRemainingBalance ?? 0) })] }), _jsx(Div, { className: "divide-y divide-[var(--appkit-color-border)] border border-[var(--appkit-color-border)]", rounded: "lg", children: (order.emiInstallments ?? []).map((inst) => (_jsxs(Row, { paddingY: "y-xs-tall", padding: "x-sm", align: "center", justify: "between", gap: "3", children: [_jsxs(Div, { className: "min-w-0", children: [_jsxs(Text, { size: "sm", weight: "medium", children: ["Installment ", inst.index, " \u00B7 ", toCurrency(inst.amount)] }), _jsx(Text, { size: "xs", color: "muted", children: inst.status === "paid" && inst.paidAt
|
|
171
|
+
return (_jsxs(_Fragment, { children: [loading && (_jsx(Row, { align: "center", justify: "center", padding: "y-4xl", children: _jsx(Div, { className: "h-6 w-6 animate-spin border-2 border-[var(--appkit-color-primary)] border-t-transparent", rounded: "full" }) })), fetchError && (_jsx(Div, { textSize: "sm", className: "mx-4 mt-4 border border-error/20", color: "error", surface: "danger-surface", padding: "inline", rounded: "lg", children: fetchError })), order && !loading && (_jsxs(Stack, { gap: "none", children: [_jsxs(Stack, { className: `flex-1 ${__O.yAuto}`, gap: "5", padding: "md", children: [_jsxs(Row, { align: "center", justify: "between", children: [_jsx(Badge, { variant: STATUS_BADGE_VARIANT[order.status?.toUpperCase()] ?? "default", children: order.status ?? "Unknown" }), _jsx(Text, { size: "sm", className: "text-[var(--appkit-color-text-secondary)]", children: toRelativeDate(order.createdAt) })] }), (order.items ?? []).length > 0 && (_jsxs(Div, { children: [_jsx(Text, { size: "sm", className: "text-[var(--appkit-color-text-primary)] mb-2", weight: "semibold", children: "Items" }), _jsx(Div, { className: "divide-y divide-[var(--appkit-color-border)] divide-[var(--appkit-color-border)] border border-[var(--appkit-color-border)]", rounded: "lg", children: (order.items ?? []).map((item, i) => (_jsxs(Row, { paddingY: "y-xs-tall", padding: "x-sm", align: "center", justify: "between", gap: "3", children: [_jsxs(Row, { align: "center", gap: "sm", className: "min-w-0", children: [_jsx(Div, { className: "h-10 w-10 shrink-0", rounded: "md", overflow: "hidden", children: _jsx(MediaImage, { src: item.image, alt: item.title ?? "Order item", size: "thumbnail" }) }), _jsxs(Div, { className: "min-w-0", children: [_jsx(Text, { size: "sm", className: "truncate", weight: "medium", children: item.title ?? item.productId ?? "Item" }), _jsxs(Text, { size: "xs", className: "text-[var(--appkit-color-text-secondary)]", children: ["Qty: ", item.quantity ?? 1] })] })] }), _jsx(Text, { size: "sm", className: "shrink-0", weight: "medium", children: toCurrency(item.price ?? 0) })] }, i))) })] })), _jsx(OrderAddonBadges, { order: order, variant: "detail" }), order.giftWrapAddon && (_jsxs(Div, { className: "border border-[var(--appkit-color-primary-200)] dark:border-[var(--appkit-color-primary-800)]", surface: "subtle", padding: "inline", rounded: "lg", children: [_jsx(Text, { size: "sm", weight: "semibold", children: "\uD83C\uDF81 Gift wrap requested" }), order.giftWrapMessage && (_jsxs(Text, { size: "sm", className: "mt-1 text-[var(--appkit-color-text-secondary)]", children: ["\u201C", order.giftWrapMessage, "\u201D"] }))] })), _jsxs(Row, { surface: "muted", padding: "inline", align: "center", justify: "between", rounded: "lg", children: [_jsx(Text, { size: "sm", weight: "semibold", children: "Total" }), _jsx(Text, { size: "sm", className: "text-[var(--appkit-color-primary)]", weight: "bold", children: toCurrency(order.totalAmount ?? 0) })] }), addrLine && (_jsxs(Div, { children: [_jsx(Text, { size: "sm", className: "mb-1", weight: "semibold", children: "Shipping address" }), _jsx(Text, { size: "sm", className: "text-[var(--appkit-color-text-secondary)]", children: [String(addr.fullName ?? ""), addrLine].filter(Boolean).join(" · ") })] })), order.paymentMethod && (_jsxs(Div, { children: [_jsxs(Row, { align: "center", justify: "between", className: "mb-1", gap: "3", children: [_jsx(Text, { size: "sm", weight: "semibold", children: "Payment" }), isManualPaymentMethod(order.paymentMethod) && (_jsx(Badge, { variant: sellerPaymentBadge(order).variant, children: sellerPaymentBadge(order).label }))] }), _jsx(Text, { size: "sm", className: "text-[var(--appkit-color-text-secondary)]", transform: "capitalize", children: order.paymentMethod }), isManualPaymentMethod(order.paymentMethod) && order.paymentTransactionId && (_jsxs(Text, { size: "xs", color: "muted", className: "mt-1", children: ["UTR: ", order.paymentTransactionId] }))] })), order.emiEnabled && (_jsxs(Div, { className: "border-t border-[var(--appkit-color-border)]", padding: "t-md", children: [_jsxs(Row, { align: "center", justify: "between", className: "mb-2", children: [_jsxs(Text, { size: "sm", weight: "semibold", children: ["EMI plan ", order.emiTenureMonths ? `· ${order.emiTenureMonths} months` : ""] }), _jsx(Badge, { variant: order.emiComplete ? "success" : "warning", children: order.emiComplete ? "Fully paid" : "In progress" })] }), _jsxs(Row, { align: "center", justify: "between", className: "mb-2", children: [_jsx(Text, { size: "xs", color: "muted", children: "Token collected" }), _jsx(Text, { size: "xs", weight: "medium", children: toCurrency(order.emiTokenAmount ?? 0) })] }), _jsxs(Row, { align: "center", justify: "between", className: "mb-3", children: [_jsx(Text, { size: "xs", color: "muted", children: "Remaining balance" }), _jsx(Text, { size: "xs", weight: "medium", children: toCurrency(order.emiRemainingBalance ?? 0) })] }), _jsx(Div, { className: "divide-y divide-[var(--appkit-color-border)] border border-[var(--appkit-color-border)]", rounded: "lg", children: (order.emiInstallments ?? []).map((inst) => (_jsxs(Row, { paddingY: "y-xs-tall", padding: "x-sm", align: "center", justify: "between", gap: "3", children: [_jsxs(Div, { className: "min-w-0", children: [_jsxs(Text, { size: "sm", weight: "medium", children: ["Installment ", inst.index, " \u00B7 ", toCurrency(inst.amount)] }), _jsx(Text, { size: "xs", color: "muted", children: inst.status === "paid" && inst.paidAt
|
|
171
172
|
? `Paid ${toRelativeDate(inst.paidAt)}`
|
|
172
173
|
: inst.dueDate
|
|
173
174
|
? `Due ${toRelativeDate(inst.dueDate)}`
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
type SearchParams = Record<string, string | string[]>;
|
|
3
|
+
export interface StoreArtStickersPageViewProps {
|
|
4
|
+
storeSlug: string;
|
|
5
|
+
searchParams?: SearchParams;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* A store's combined Art & Stickers tab.
|
|
9
|
+
*
|
|
10
|
+
* This page did not exist until 2026-08-21: `STORE_PAGE_TABS` had an
|
|
11
|
+
* "Art & Stickers" entry with a real count query behind it, but its href
|
|
12
|
+
* pointed at `STORE_PRODUCTS` — a page that filters `listingType == standard`.
|
|
13
|
+
* So the tab advertised a non-zero count and then rendered none of those items,
|
|
14
|
+
* with no error. Same shape as the category/brand tabs that rendered blank
|
|
15
|
+
* panels (see `audit-tab-body-coverage.mjs`), one level up.
|
|
16
|
+
*
|
|
17
|
+
* Mirrors the public `/art` page in spanning both types at once, since neither
|
|
18
|
+
* has enough inventory to justify a tab of its own.
|
|
19
|
+
*/
|
|
20
|
+
export declare function StoreArtStickersPageView({ storeSlug, searchParams, }: StoreArtStickersPageViewProps): Promise<React.JSX.Element | null>;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { PRODUCT_FIELDS } from "../../../constants/field-names";
|
|
3
|
+
import { sortBy } from "../../../constants/sort";
|
|
4
|
+
import { listStoreProducts } from "../../../_internal/server/features/products/list-public";
|
|
5
|
+
import { ART_STICKERS_LISTING_TYPES } from "../../products/constants/listing-tabs";
|
|
6
|
+
import { getStoreBySlug } from "./StoreDetailLayoutView";
|
|
7
|
+
import { StoreProductsListing } from "./StoreProductsListing";
|
|
8
|
+
const DEFAULT_PAGE_SIZE = 24;
|
|
9
|
+
const DEFAULT_SORT = sortBy(PRODUCT_FIELDS.CREATED_AT);
|
|
10
|
+
/**
|
|
11
|
+
* A store's combined Art & Stickers tab.
|
|
12
|
+
*
|
|
13
|
+
* This page did not exist until 2026-08-21: `STORE_PAGE_TABS` had an
|
|
14
|
+
* "Art & Stickers" entry with a real count query behind it, but its href
|
|
15
|
+
* pointed at `STORE_PRODUCTS` — a page that filters `listingType == standard`.
|
|
16
|
+
* So the tab advertised a non-zero count and then rendered none of those items,
|
|
17
|
+
* with no error. Same shape as the category/brand tabs that rendered blank
|
|
18
|
+
* panels (see `audit-tab-body-coverage.mjs`), one level up.
|
|
19
|
+
*
|
|
20
|
+
* Mirrors the public `/art` page in spanning both types at once, since neither
|
|
21
|
+
* has enough inventory to justify a tab of its own.
|
|
22
|
+
*/
|
|
23
|
+
export async function StoreArtStickersPageView({ storeSlug, searchParams, }) {
|
|
24
|
+
const store = await getStoreBySlug(storeSlug);
|
|
25
|
+
const storeId = store?.id;
|
|
26
|
+
if (typeof storeId !== "string" || !storeId)
|
|
27
|
+
return null;
|
|
28
|
+
const result = await listStoreProducts(storeId, ART_STICKERS_LISTING_TYPES, searchParams ?? {}, { pageSize: DEFAULT_PAGE_SIZE, sorts: DEFAULT_SORT });
|
|
29
|
+
return (_jsx(StoreProductsListing, { storeId: storeId, listingTypes: ART_STICKERS_LISTING_TYPES, initialData: result ?? undefined }));
|
|
30
|
+
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
+
type SearchParams = Record<string, string | string[]>;
|
|
2
3
|
export interface StoreAuctionsPageViewProps {
|
|
3
4
|
storeSlug: string;
|
|
5
|
+
searchParams?: SearchParams;
|
|
4
6
|
}
|
|
5
|
-
export declare function StoreAuctionsPageView({ storeSlug }: StoreAuctionsPageViewProps): Promise<React.JSX.Element | null>;
|
|
7
|
+
export declare function StoreAuctionsPageView({ storeSlug, searchParams }: StoreAuctionsPageViewProps): Promise<React.JSX.Element | null>;
|
|
8
|
+
export {};
|
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
3
|
-
import { sortBy } from "
|
|
4
|
-
import {
|
|
2
|
+
import { PRODUCT_FIELDS } from "../../../constants/field-names";
|
|
3
|
+
import { sortBy } from "../../../constants/sort";
|
|
4
|
+
import { listStoreProducts } from "../../../_internal/server/features/products/list-public";
|
|
5
5
|
import { getStoreBySlug } from "./StoreDetailLayoutView";
|
|
6
6
|
import { StoreAuctionsListing } from "./StoreAuctionsListing";
|
|
7
|
-
|
|
7
|
+
const LISTING_TYPES = ["auction"];
|
|
8
|
+
const DEFAULT_PAGE_SIZE = 24;
|
|
9
|
+
const DEFAULT_SORT = sortBy(PRODUCT_FIELDS.AUCTION_END_DATE, "ASC");
|
|
10
|
+
export async function StoreAuctionsPageView({ storeSlug, searchParams }) {
|
|
8
11
|
const store = await getStoreBySlug(storeSlug);
|
|
9
12
|
const storeId = store?.id;
|
|
10
|
-
|
|
11
|
-
? await productRepository
|
|
12
|
-
.list({
|
|
13
|
-
filters: sieveAnd(sieveFilter("storeId", SIEVE_OP.EQ, storeId), sieveFilter("status", SIEVE_OP.EQ, "published"), sieveFilter("listingType", SIEVE_OP.EQ, "auction")),
|
|
14
|
-
sorts: sortBy("auctionEndDate", "ASC"),
|
|
15
|
-
page: 1,
|
|
16
|
-
pageSize: 24,
|
|
17
|
-
})
|
|
18
|
-
.catch(() => null)
|
|
19
|
-
: null;
|
|
20
|
-
if (!storeId) {
|
|
13
|
+
if (typeof storeId !== "string" || !storeId)
|
|
21
14
|
return null;
|
|
22
|
-
|
|
23
|
-
|
|
15
|
+
// Shared query — see listStoreProducts. This view used to call
|
|
16
|
+
// productRepository.list() with an inline filter string and swallow every
|
|
17
|
+
// failure via a bare .catch(() => null), so a missing index rendered as an
|
|
18
|
+
// empty store tab with nothing logged (Root Cause #30's family).
|
|
19
|
+
const result = await listStoreProducts(storeId, LISTING_TYPES, searchParams ?? {}, {
|
|
20
|
+
pageSize: DEFAULT_PAGE_SIZE,
|
|
21
|
+
sorts: DEFAULT_SORT,
|
|
22
|
+
});
|
|
23
|
+
return _jsx(StoreAuctionsListing, { storeId: storeId, initialData: result ?? undefined });
|
|
24
24
|
}
|
|
@@ -37,6 +37,13 @@ export function StoreClassifiedsListing({ storeId, initialData }) {
|
|
|
37
37
|
perPage: table.getNumber("pageSize", 24),
|
|
38
38
|
storeId: storeId || undefined,
|
|
39
39
|
listingType: "classified",
|
|
40
|
+
// Per-type facets — declared in FILTER_KEYS (so they counted toward the
|
|
41
|
+
// filter badge) and rendered in the drawer, but never sent until 2026-08-21.
|
|
42
|
+
typeFacets: {
|
|
43
|
+
[TABLE_KEYS.CITY]: table.get(TABLE_KEYS.CITY),
|
|
44
|
+
[TABLE_KEYS.NEGOTIABLE]: table.get(TABLE_KEYS.NEGOTIABLE),
|
|
45
|
+
[TABLE_KEYS.ACCEPTS_SHIPPING]: table.get(TABLE_KEYS.ACCEPTS_SHIPPING),
|
|
46
|
+
},
|
|
40
47
|
};
|
|
41
48
|
const { products, totalPages, page, isLoading } = useProducts(params, { initialData });
|
|
42
49
|
const gridClass = "grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-[var(--appkit-space-4)]";
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
+
type SearchParams = Record<string, string | string[]>;
|
|
2
3
|
export interface StoreClassifiedsPageViewProps {
|
|
3
4
|
storeSlug: string;
|
|
5
|
+
searchParams?: SearchParams;
|
|
4
6
|
}
|
|
5
|
-
export declare function StoreClassifiedsPageView({ storeSlug }: StoreClassifiedsPageViewProps): Promise<React.JSX.Element | null>;
|
|
7
|
+
export declare function StoreClassifiedsPageView({ storeSlug, searchParams }: StoreClassifiedsPageViewProps): Promise<React.JSX.Element | null>;
|
|
8
|
+
export {};
|