@mohasinac/appkit 2.7.9 → 2.7.11
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/shared/config/index.d.ts +1 -1
- package/dist/_internal/shared/config/schema.d.ts +95 -1
- package/dist/client-entry.d.ts +1 -1
- package/dist/client.d.ts +3 -0
- package/dist/client.js +5 -0
- package/dist/configs/next.js +17 -1
- package/dist/features/account/components/UserOffersPanel.js +17 -5
- package/dist/features/auctions/components/AuctionDetailPageView.js +17 -1
- package/dist/features/auctions/components/PlaceBidFormClient.js +10 -5
- package/dist/features/blog/components/BlogIndexListing.js +8 -1
- package/dist/features/categories/components/BundleBuyNowCta.js +13 -6
- package/dist/features/categories/components/CategoriesIndexListing.js +9 -2
- package/dist/features/events/components/EventPollWidget.js +7 -1
- package/dist/features/events/components/EventsIndexListing.js +8 -1
- package/dist/features/events/components/SpinWheelView.js +12 -5
- package/dist/features/layout/TitleBarLayout.js +1 -1
- package/dist/features/pre-orders/components/PreOrderActionsClient.js +10 -3
- package/dist/features/pre-orders/components/PreOrdersIndexListing.js +11 -7
- package/dist/features/products/components/AuctionsIndexListing.js +31 -2
- package/dist/features/products/components/MakeOfferButton.js +16 -8
- package/dist/features/products/components/PrizeDrawsIndexListing.js +5 -2
- package/dist/features/products/components/PrizeRevealModal.js +18 -5
- package/dist/features/products/components/ProductDetailPageView.js +23 -1
- package/dist/features/products/components/ProductsIndexListing.js +12 -8
- package/dist/features/reviews/components/ReviewsIndexListing.js +8 -1
- package/dist/features/seller/components/SellerOffersPanel.js +17 -5
- package/dist/features/stores/components/StoreAuctionsListing.js +2 -1
- package/dist/features/stores/components/StorePreOrdersListing.js +2 -1
- package/dist/features/stores/components/StoresIndexListing.js +25 -11
- package/dist/features/wishlist/components/WishlistToggleButton.js +26 -12
- package/dist/http/api-handler.js +7 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.js +2 -0
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/BaseListingCard.js +8 -6
- package/dist/ui/components/Button.style.css +8 -5
- package/dist/ui/components/ListingLayout.style.css +19 -9
- package/dist/ui/components/ListingToolbar.d.ts +7 -4
- package/dist/ui/components/ListingToolbar.js +4 -3
- package/dist/ui/components/LoginRequiredModal.d.ts +9 -0
- package/dist/ui/components/LoginRequiredModal.js +11 -0
- package/dist/ui/components/SiteLogo.js +1 -1
- package/dist/ui/index.d.ts +2 -0
- package/dist/ui/index.js +1 -0
- package/dist/utils/auth-error.d.ts +8 -0
- package/dist/utils/auth-error.js +20 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/package.json +1 -1
|
@@ -30,7 +30,7 @@ export function TitleBarLayout({ onToggleSidebar, sidebarOpen, onSearchToggle, s
|
|
|
30
30
|
const authButtonsEl = !user && (loginHref || registerHref) ? (_jsxs(Row, { gap: "xs", className: "hidden lg:flex items-center", children: [loginHref && (_jsx(Link, { href: loginHref, className: "px-3 py-1.5 text-sm font-medium text-zinc-700 dark:text-zinc-300 hover:text-primary-700 dark:hover:text-secondary-400 transition-colors rounded-lg hover:bg-primary-50 dark:hover:bg-slate-800", children: "Sign in" })), registerHref && (_jsx(Link, { href: registerHref, className: "px-3 py-1.5 text-sm font-semibold rounded-lg bg-primary text-white hover:bg-primary/90 transition-colors btn-glow shadow-sm", children: "Register" }))] })) : null;
|
|
31
31
|
const hasTb2 = !!(wishlistEl || cartEl || profileEl);
|
|
32
32
|
// ── Render ───────────────────────────────────────────────────────────────────
|
|
33
|
-
return (_jsxs(BlockHeader, { id: id, className: `sticky top-0 z-50 bg-white/95 dark:bg-slate-950/95 backdrop-blur-md border-b border-zinc-100 dark:border-slate-800 shadow-sm ${className}`, children: [promoStripText && (_jsx(Div, { className: "bg-gradient-to-r from-primary-700 to-secondary-600 dark:from-primary-700 dark:to-cobalt-700 text-white text-xs py-1 text-center font-medium", children: promoStripText })), _jsxs(Div, { className: "container mx-auto px-4 sm:px-6 lg:px-8 max-w-[1920px]", children: [_jsxs(Row, { justify: "between", gap: "none", className: "h-14", children: [_jsx(Row, { gap: "3", children: _jsx(Link, { href: logoHref, "aria-label": brandName, className: "flex items-center transition-opacity hover:opacity-80", children: _jsx(SiteLogo, { title: brandName, src: siteLogoUrl, className: "h-7 md:h-9 lg:h-10" }) }) })
|
|
33
|
+
return (_jsxs(BlockHeader, { id: id, className: `sticky top-0 z-50 bg-white/95 dark:bg-slate-950/95 backdrop-blur-md border-b border-zinc-100 dark:border-slate-800 shadow-sm ${className}`, children: [promoStripText && (_jsx(Div, { className: "bg-gradient-to-r from-primary-700 to-secondary-600 dark:from-primary-700 dark:to-cobalt-700 text-white text-xs py-1 text-center font-medium", children: promoStripText })), _jsxs(Div, { className: "container mx-auto px-4 sm:px-6 lg:px-8 max-w-[1920px]", children: [_jsxs(Row, { justify: "between", gap: "none", className: "relative h-14", children: [_jsx(Row, { gap: "3", children: _jsx(Link, { href: logoHref, "aria-label": brandName, className: "flex items-center transition-opacity hover:opacity-80", children: _jsx(SiteLogo, { title: brandName, className: "h-7 md:h-9 lg:h-10" }) }) }), siteLogoUrl ? (_jsx(Div, { className: "absolute inset-y-0 left-1/2 -translate-x-1/2 flex items-center", children: _jsx(Link, { href: logoHref, "aria-label": brandName, className: "flex items-center transition-opacity hover:opacity-80", children: _jsx(SiteLogo, { src: siteLogoUrl, title: brandName, className: "h-7 md:h-9 lg:h-10" }) }) })) : (navSlot && _jsx(Div, { className: "hidden md:flex", children: navSlot })), _jsxs(Row, { gap: "xs", children: [devSlot, compareEl, notificationSlot, wishlistEl && _jsx(Div, { className: "hidden lg:flex", children: wishlistEl }), cartEl && _jsx(Div, { className: "hidden lg:flex", children: cartEl }), user
|
|
34
34
|
? profileEl && _jsx(Div, { className: "hidden lg:flex", children: profileEl })
|
|
35
35
|
: authButtonsEl ?? (profileEl && _jsx(Div, { className: "hidden lg:flex", children: profileEl })), searchBtn, promotionsEl, themeBtn, hamburgerBtn] })] }), hasTb2 && (_jsxs(Row, { as: "nav", "aria-label": "Account actions", justify: "end", gap: "xs", className: "flex lg:hidden h-10 border-t border-zinc-100 dark:border-slate-800 px-1", children: [wishlistEl, cartEl, profileEl] }))] })] }));
|
|
36
36
|
}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useState, useTransition } from "react";
|
|
4
|
-
import { Button, Div, Stack, Text } from "../../../ui";
|
|
4
|
+
import { Button, Div, LoginRequiredModal, Stack, Text } from "../../../ui";
|
|
5
|
+
import { isAuthError } from "../../../utils/auth-error";
|
|
5
6
|
import { formatCurrency } from "../../../utils/number.formatter";
|
|
6
7
|
export function PreOrderActionsClient({ productId, price, currency, depositAmount, depositPercent, isCancellable, tags = [], onReserveNow, }) {
|
|
7
8
|
const [isPending, startTransition] = useTransition();
|
|
8
9
|
const [error, setError] = useState(null);
|
|
9
10
|
const [success, setSuccess] = useState(false);
|
|
11
|
+
const [showLoginModal, setShowLoginModal] = useState(false);
|
|
10
12
|
function handleReserve() {
|
|
11
13
|
setError(null);
|
|
12
14
|
setSuccess(false);
|
|
@@ -16,11 +18,16 @@ export function PreOrderActionsClient({ productId, price, currency, depositAmoun
|
|
|
16
18
|
setSuccess(true);
|
|
17
19
|
}
|
|
18
20
|
catch (err) {
|
|
19
|
-
|
|
21
|
+
if (isAuthError(err)) {
|
|
22
|
+
setShowLoginModal(true);
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
setError(err instanceof Error ? err.message : "Failed to reserve. Please try again.");
|
|
26
|
+
}
|
|
20
27
|
}
|
|
21
28
|
});
|
|
22
29
|
}
|
|
23
|
-
return (_jsxs(Div, { className: "space-y-4", children: [price !== null && (_jsxs(Div, { children: [_jsx(Text, { className: "text-2xl font-bold text-zinc-900 dark:text-zinc-50", children: formatCurrency(price, currency) }), depositAmount !== null && (_jsxs(Text, { className: "mt-0.5 text-xs text-zinc-500", children: ["Reserve with ", formatCurrency(depositAmount, currency), depositPercent !== null ? ` (${depositPercent}% deposit)` : ""] }))] })), error && (_jsx(Text, { className: "text-xs text-red-600 dark:text-red-400", children: error })), success && (_jsx(Text, { className: "text-xs text-emerald-600 dark:text-emerald-400", children: "\u2713 Reserved! Redirecting to checkout\u2026" })), _jsxs(Stack, { gap: "sm", children: [_jsx(Button, { variant: "primary", size: "md", className: "w-full", disabled: isPending, onClick: handleReserve, children: isPending ? "Processing…" : "Reserve Now" }), isCancellable && (_jsx(Text, { className: "text-center text-xs text-zinc-500 dark:text-zinc-400", children: "\u2713 Free cancellation before production" }))] }), tags.length > 0 && (_jsx(Div, { className: "border-t border-zinc-200 dark:border-zinc-700 pt-4", children: _jsx("div", { className: "flex flex-wrap gap-1.5", children: tags.map((tag) => (_jsx("span", { className: "rounded-full bg-zinc-100 dark:bg-zinc-800 px-2.5 py-1 text-xs text-zinc-600 dark:text-zinc-300", children: tag }, tag))) }) })), _jsx(Div, { className: "border-t border-zinc-200 dark:border-zinc-700 pt-4", children: _jsx("div", { className: "flex flex-wrap gap-4 justify-center text-center", children: [
|
|
30
|
+
return (_jsxs(Div, { className: "space-y-4", children: [price !== null && (_jsxs(Div, { children: [_jsx(Text, { className: "text-2xl font-bold text-zinc-900 dark:text-zinc-50", children: formatCurrency(price, currency) }), depositAmount !== null && (_jsxs(Text, { className: "mt-0.5 text-xs text-zinc-500", children: ["Reserve with ", formatCurrency(depositAmount, currency), depositPercent !== null ? ` (${depositPercent}% deposit)` : ""] }))] })), error && (_jsx(Text, { className: "text-xs text-red-600 dark:text-red-400", children: error })), success && (_jsx(Text, { className: "text-xs text-emerald-600 dark:text-emerald-400", children: "\u2713 Reserved! Redirecting to checkout\u2026" })), _jsx(LoginRequiredModal, { isOpen: showLoginModal, onClose: () => setShowLoginModal(false), message: "You need to be signed in to reserve this pre-order. Please log in or create an account to continue." }), _jsxs(Stack, { gap: "sm", children: [_jsx(Button, { variant: "primary", size: "md", className: "w-full", disabled: isPending, onClick: handleReserve, children: isPending ? "Processing…" : "Reserve Now" }), isCancellable && (_jsx(Text, { className: "text-center text-xs text-zinc-500 dark:text-zinc-400", children: "\u2713 Free cancellation before production" }))] }), tags.length > 0 && (_jsx(Div, { className: "border-t border-zinc-200 dark:border-zinc-700 pt-4", children: _jsx("div", { className: "flex flex-wrap gap-1.5", children: tags.map((tag) => (_jsx("span", { className: "rounded-full bg-zinc-100 dark:bg-zinc-800 px-2.5 py-1 text-xs text-zinc-600 dark:text-zinc-300", children: tag }, tag))) }) })), _jsx(Div, { className: "border-t border-zinc-200 dark:border-zinc-700 pt-4", children: _jsx("div", { className: "flex flex-wrap gap-4 justify-center text-center", children: [
|
|
24
31
|
{ icon: "🔒", label: "Secure\nPayment" },
|
|
25
32
|
{ icon: "📅", label: "Guaranteed\nDelivery" },
|
|
26
33
|
{ icon: "↩", label: "Free\nCancellation" },
|
|
@@ -4,7 +4,7 @@ import { useState, useCallback, useMemo } from "react";
|
|
|
4
4
|
import { Columns, Heart, ShoppingCart, SlidersHorizontal, X } from "lucide-react";
|
|
5
5
|
import { useUrlTable } from "../../../react/hooks/useUrlTable";
|
|
6
6
|
import { useProducts } from "../../products/hooks/useProducts";
|
|
7
|
-
import { Pagination, useToast,
|
|
7
|
+
import { Pagination, useToast, BulkActionBar, ListingToolbar } from "../../../ui";
|
|
8
8
|
import { ACTION_ID, ACTION_META, COMPARE_MAX_ITEMS } from "../../products/constants/action-defs";
|
|
9
9
|
import { CompareOverlay } from "../../products/components/CompareOverlay";
|
|
10
10
|
import { ROUTES } from "../../../next";
|
|
@@ -107,7 +107,9 @@ export function PreOrdersIndexListing({ initialData, categorySlug, brandName })
|
|
|
107
107
|
page: table.getNumber("page", 1),
|
|
108
108
|
perPage: table.getNumber("pageSize", 24),
|
|
109
109
|
listingType: "pre-order",
|
|
110
|
-
|
|
110
|
+
// Hide out-of-stock pre-orders by default. Uses stockQuantity>0 (always-present field).
|
|
111
|
+
// Quota over-sign is intentional — never block pre-orders by availability.
|
|
112
|
+
inStock: showClosed ? undefined : true,
|
|
111
113
|
};
|
|
112
114
|
const { products: preOrders, totalPages, page, isLoading } = useProducts(params, { initialData });
|
|
113
115
|
const commitSearch = useCallback(() => {
|
|
@@ -118,6 +120,8 @@ export function PreOrdersIndexListing({ initialData, categorySlug, brandName })
|
|
|
118
120
|
commitSearch();
|
|
119
121
|
};
|
|
120
122
|
const handleViewToggle = (next) => {
|
|
123
|
+
if (next === "table")
|
|
124
|
+
return;
|
|
121
125
|
setView(next);
|
|
122
126
|
table.set("view", next);
|
|
123
127
|
};
|
|
@@ -147,9 +151,9 @@ export function PreOrdersIndexListing({ initialData, categorySlug, brandName })
|
|
|
147
151
|
}, [localCart, showToast]);
|
|
148
152
|
const selection = useBulkSelection({ items: preOrders, keyExtractor: (p) => p.id });
|
|
149
153
|
const gridClass = "grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-4";
|
|
150
|
-
return (_jsxs("div", { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search pre-orders...", onSearchChange: setSearchInput, onSearchCommit: commitSearch, onSearchKeyDown: handleSearchKeyDown, sortValue: table.get("sort") || "-createdAt", sortOptions: PREORDER_SORT_OPTIONS, onSortChange: (v) => { table.set("sort", v); }, view: view, onViewChange: handleViewToggle, onResetAll: resetAll, hasActiveState: hasActiveState, bulkMode: selection.isSelecting, bulkSelectedCount: selection.selectedCount, bulkTotalCount: preOrders.length, onBulkSelectAll: selection.toggleAll, onBulkClear: selection.clearSelection, extra: _jsxs("label", { className: "flex items-center gap-1.5 cursor-pointer select-none shrink-0", children: [_jsx("span", { className: "hidden sm:inline text-xs text-zinc-600 dark:text-zinc-300 whitespace-nowrap", children: "Show closed" }), _jsx("button", { type: "button", role: "switch", "aria-checked": showClosed, onClick: () => table.set("showClosed", showClosed ? "" : "true"), className: `relative inline-flex h-5 w-9 flex-shrink-0 items-center rounded-full transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1 ${showClosed ? "bg-primary" : "bg-zinc-300 dark:bg-slate-600"}`, children: _jsx("span", { className: `inline-block h-3.5 w-3.5 transform rounded-full bg-white shadow-sm transition-transform duration-200 ${showClosed ? "translate-x-[19px]" : "translate-x-[3px]"}` }) })] }) }),
|
|
154
|
+
return (_jsxs("div", { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search pre-orders...", onSearchChange: setSearchInput, onSearchCommit: commitSearch, onSearchKeyDown: handleSearchKeyDown, sortValue: table.get("sort") || "-createdAt", sortOptions: PREORDER_SORT_OPTIONS, onSortChange: (v) => { table.set("sort", v); }, view: view, onViewChange: handleViewToggle, onResetAll: resetAll, hasActiveState: hasActiveState, bulkMode: selection.isSelecting, bulkSelectedCount: selection.selectedCount, bulkTotalCount: preOrders.length, onBulkSelectAll: selection.toggleAll, onBulkClear: selection.clearSelection, extra: _jsxs("label", { className: "flex items-center gap-1.5 cursor-pointer select-none shrink-0", children: [_jsx("span", { className: "hidden sm:inline text-xs text-zinc-600 dark:text-zinc-300 whitespace-nowrap", children: "Show closed" }), _jsx("button", { type: "button", role: "switch", "aria-checked": showClosed, onClick: () => table.set("showClosed", showClosed ? "" : "true"), className: `relative inline-flex h-5 w-9 flex-shrink-0 items-center rounded-full transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1 ${showClosed ? "bg-primary" : "bg-zinc-300 dark:bg-slate-600"}`, children: _jsx("span", { className: `inline-block h-3.5 w-3.5 transform rounded-full bg-white shadow-sm transition-transform duration-200 ${showClosed ? "translate-x-[19px]" : "translate-x-[3px]"}` }) })] }) }), _jsx(BulkActionBar, { selectedCount: selection.selectedCount, onClearSelection: selection.clearSelection, actions: [
|
|
151
155
|
{
|
|
152
|
-
|
|
156
|
+
id: ACTION_ID.ADD_TO_CART,
|
|
153
157
|
label: ACTION_META[ACTION_ID.ADD_TO_CART].label,
|
|
154
158
|
icon: _jsx(ShoppingCart, { className: "h-3.5 w-3.5" }),
|
|
155
159
|
variant: "primary",
|
|
@@ -164,7 +168,7 @@ export function PreOrdersIndexListing({ initialData, categorySlug, brandName })
|
|
|
164
168
|
},
|
|
165
169
|
},
|
|
166
170
|
{
|
|
167
|
-
|
|
171
|
+
id: ACTION_ID.ADD_TO_WISHLIST,
|
|
168
172
|
label: ACTION_META[ACTION_ID.ADD_TO_WISHLIST].label,
|
|
169
173
|
icon: _jsx(Heart, { className: "h-3.5 w-3.5" }),
|
|
170
174
|
variant: "secondary",
|
|
@@ -179,7 +183,7 @@ export function PreOrdersIndexListing({ initialData, categorySlug, brandName })
|
|
|
179
183
|
},
|
|
180
184
|
},
|
|
181
185
|
{
|
|
182
|
-
|
|
186
|
+
id: ACTION_ID.COMPARE,
|
|
183
187
|
label: ACTION_META[ACTION_ID.COMPARE].label,
|
|
184
188
|
icon: _jsx(Columns, { className: "h-3.5 w-3.5" }),
|
|
185
189
|
variant: "secondary",
|
|
@@ -189,7 +193,7 @@ export function PreOrdersIndexListing({ initialData, categorySlug, brandName })
|
|
|
189
193
|
setCompareIds(ids);
|
|
190
194
|
},
|
|
191
195
|
},
|
|
192
|
-
] }), _jsx(CompareOverlay, { isOpen: compareIds.length > 0, productIds: compareIds, productType: "preorder", onClose: () => {
|
|
196
|
+
] }), totalPages > 1 && (_jsx("div", { className: "sticky top-[calc(var(--header-height,0px)+44px)] z-10 flex justify-center bg-white/95 dark:bg-slate-900/95 backdrop-blur-sm border-b border-zinc-200 dark:border-slate-700 px-3 py-1.5", children: _jsx(Pagination, { currentPage: page, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) })), _jsx("div", { className: "py-6", children: isLoading ? (_jsx("div", { className: gridClass, children: Array.from({ length: 10 }).map((_, i) => (_jsxs("div", { className: "rounded-xl border border-zinc-100 dark:border-slate-700 overflow-hidden animate-pulse", children: [_jsx("div", { className: "aspect-square bg-zinc-200 dark:bg-slate-700" }), _jsxs("div", { className: "p-3 space-y-2", children: [_jsx("div", { className: "h-3 bg-zinc-200 dark:bg-slate-700 rounded w-3/4" }), _jsx("div", { className: "h-3 bg-zinc-200 dark:bg-slate-700 rounded w-1/2" }), _jsx("div", { className: "h-4 bg-zinc-200 dark:bg-slate-700 rounded w-1/3" }), _jsx("div", { className: "h-8 bg-zinc-200 dark:bg-slate-700 rounded" })] })] }, i))) })) : preOrders.length === 0 ? (_jsx("p", { className: "py-12 text-center text-sm text-zinc-500 dark:text-zinc-400", children: "No pre-orders found." })) : view === "list" ? (_jsx("div", { className: "flex flex-col divide-y divide-zinc-100 dark:divide-zinc-800 rounded-xl border border-zinc-100 dark:border-zinc-800", children: preOrders.map((product) => (_jsx(MarketplacePreorderCard, { product: product, variant: "list", hrefBuilder: (p) => String(ROUTES.PUBLIC.PRE_ORDER_DETAIL(p.id)), onAddToCart: handleAddToCart, wishlistActions: wishlistActions, selectable: selection.isSelecting, isSelected: selection.isSelected(product.id), onSelect: (id) => selection.toggle(id) }, product.id))) })) : (_jsx("div", { className: gridClass, children: preOrders.map((product) => (_jsx(MarketplacePreorderCard, { product: product, variant: "grid", hrefBuilder: (p) => String(ROUTES.PUBLIC.PRE_ORDER_DETAIL(p.id)), onAddToCart: handleAddToCart, wishlistActions: wishlistActions, selectable: selection.isSelecting, isSelected: selection.isSelected(product.id), onSelect: (id) => selection.toggle(id) }, product.id))) })) }), _jsx(CompareOverlay, { isOpen: compareIds.length > 0, productIds: compareIds, productType: "preorder", onClose: () => {
|
|
193
197
|
setCompareIds([]);
|
|
194
198
|
selection.clearSelection();
|
|
195
199
|
}, onRemove: (id) => setCompareIds((ids) => ids.filter((i) => i !== id)) }), filterOpen && (_jsxs(_Fragment, { children: [_jsx("div", { className: "fixed inset-0 z-40 bg-black/40", "aria-hidden": "true", onClick: () => setFilterOpen(false) }), _jsxs("div", { className: "fixed inset-y-0 left-0 z-50 flex w-80 flex-col bg-white dark:bg-slate-900 shadow-2xl", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: [_jsxs("span", { className: "flex items-center gap-2 text-base font-semibold text-zinc-900 dark:text-zinc-100", children: [_jsx(SlidersHorizontal, { className: "h-4 w-4" }), "Filters"] }), _jsxs("div", { className: "flex items-center gap-2", children: [activeFilterCount > 0 && (_jsx("button", { type: "button", onClick: clearFilters, className: "text-xs text-zinc-500 hover:text-rose-500 dark:text-zinc-400 transition-colors", children: "Clear all" })), _jsx("button", { type: "button", onClick: () => setFilterOpen(false), "aria-label": "Close filters", className: "rounded-lg p-1.5 text-zinc-500 hover:bg-zinc-100 dark:hover:bg-slate-800 transition-colors", children: _jsx(X, { className: "h-5 w-5" }) })] })] }), _jsx("div", { className: "flex-1 overflow-y-auto px-4 py-4", children: _jsx(PreOrderFilters, { table: pendingTable, currencyPrefix: "\u20B9", categoryOptions: categoryOptions, brandOptions: brandOptions }) }), _jsx("div", { className: "border-t border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: _jsxs("button", { type: "button", onClick: applyFilters, className: "w-full rounded-lg bg-primary py-2.5 text-sm font-semibold text-white hover:bg-primary-600 transition-colors active:scale-[0.98]", children: ["Apply Filters", activeFilterCount > 0 ? ` (${activeFilterCount})` : ""] }) })] })] }))] }));
|
|
@@ -4,7 +4,8 @@ import { useState, useCallback, useMemo } from "react";
|
|
|
4
4
|
import { SlidersHorizontal, X } from "lucide-react";
|
|
5
5
|
import { useUrlTable } from "../../../react/hooks/useUrlTable";
|
|
6
6
|
import { useProducts } from "../hooks/useProducts";
|
|
7
|
-
import { Pagination, useToast, ListingToolbar } from "../../../ui";
|
|
7
|
+
import { Pagination, useToast, ListingToolbar, BulkActionBar } from "../../../ui";
|
|
8
|
+
import { useBulkSelection } from "../../../react/hooks/useBulkSelection";
|
|
8
9
|
import { MarketplaceAuctionGrid } from "../../auctions/components/MarketplaceAuctionGrid";
|
|
9
10
|
import { AuctionFilters } from "../../auctions/components/AuctionFilters";
|
|
10
11
|
import { useGuestWishlist } from "../../wishlist/hooks/useGuestWishlist";
|
|
@@ -105,6 +106,7 @@ export function AuctionsIndexListing({ initialData, categorySlug, brandName }) {
|
|
|
105
106
|
listingType: "auction",
|
|
106
107
|
};
|
|
107
108
|
const { products: auctions, totalPages, page, isLoading } = useProducts(params, { initialData });
|
|
109
|
+
const selection = useBulkSelection({ items: auctions, keyExtractor: (a) => a.id });
|
|
108
110
|
const commitSearch = useCallback(() => {
|
|
109
111
|
table.set("q", searchInput.trim());
|
|
110
112
|
}, [searchInput, table]);
|
|
@@ -113,6 +115,8 @@ export function AuctionsIndexListing({ initialData, categorySlug, brandName }) {
|
|
|
113
115
|
commitSearch();
|
|
114
116
|
};
|
|
115
117
|
const handleViewToggle = (next) => {
|
|
118
|
+
if (next === "table")
|
|
119
|
+
return;
|
|
116
120
|
setView(next);
|
|
117
121
|
table.set("view", next);
|
|
118
122
|
};
|
|
@@ -132,5 +136,30 @@ export function AuctionsIndexListing({ initialData, categorySlug, brandName }) {
|
|
|
132
136
|
isWishlisted: (productId) => wishlistedIds.has(productId),
|
|
133
137
|
};
|
|
134
138
|
const gridClass = "grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-3 gap-4";
|
|
135
|
-
return (_jsxs("div", { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search auctions...", onSearchChange: setSearchInput, onSearchCommit: commitSearch, onSearchKeyDown: handleSearchKeyDown, sortValue: table.get("sort") || "auctionEndDate", sortOptions: AUCTION_SORT_OPTIONS, onSortChange: (v) => { table.set("sort", v); }, view: view, onViewChange: handleViewToggle, onResetAll: resetAll, hasActiveState: hasActiveState, extra: _jsxs("label", { className: "flex items-center gap-1.5 cursor-pointer select-none shrink-0", children: [_jsx("span", { className: "hidden sm:inline text-xs text-zinc-600 dark:text-zinc-300 whitespace-nowrap", children: "Show ended" }), _jsx("button", { type: "button", role: "switch", "aria-checked": showEnded, onClick: () => table.set("showEnded", showEnded ? "" : "true"), className: `relative inline-flex h-5 w-9 flex-shrink-0 items-center rounded-full transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1 ${showEnded ? "bg-primary" : "bg-zinc-300 dark:bg-slate-600"}`, children: _jsx("span", { className: `inline-block h-3.5 w-3.5 transform rounded-full bg-white shadow-sm transition-transform duration-200 ${showEnded ? "translate-x-[19px]" : "translate-x-[3px]"}` }) })] }) }),
|
|
139
|
+
return (_jsxs("div", { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search auctions...", onSearchChange: setSearchInput, onSearchCommit: commitSearch, onSearchKeyDown: handleSearchKeyDown, sortValue: table.get("sort") || "auctionEndDate", sortOptions: AUCTION_SORT_OPTIONS, onSortChange: (v) => { table.set("sort", v); }, view: view, onViewChange: handleViewToggle, onResetAll: resetAll, hasActiveState: hasActiveState, bulkMode: selection.isSelecting, bulkSelectedCount: selection.selectedCount, bulkTotalCount: auctions.length, onBulkSelectAll: selection.toggleAll, onBulkClear: selection.clearSelection, extra: _jsxs("label", { className: "flex items-center gap-1.5 cursor-pointer select-none shrink-0", children: [_jsx("span", { className: "hidden sm:inline text-xs text-zinc-600 dark:text-zinc-300 whitespace-nowrap", children: "Show ended" }), _jsx("button", { type: "button", role: "switch", "aria-checked": showEnded, onClick: () => table.set("showEnded", showEnded ? "" : "true"), className: `relative inline-flex h-5 w-9 flex-shrink-0 items-center rounded-full transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1 ${showEnded ? "bg-primary" : "bg-zinc-300 dark:bg-slate-600"}`, children: _jsx("span", { className: `inline-block h-3.5 w-3.5 transform rounded-full bg-white shadow-sm transition-transform duration-200 ${showEnded ? "translate-x-[19px]" : "translate-x-[3px]"}` }) })] }) }), _jsx(BulkActionBar, { selectedCount: selection.selectedCount, onClearSelection: selection.clearSelection, actions: [
|
|
140
|
+
{
|
|
141
|
+
id: "watchlist",
|
|
142
|
+
label: "Add to Watchlist",
|
|
143
|
+
variant: "primary",
|
|
144
|
+
onClick: () => {
|
|
145
|
+
const selected = auctions.filter((a) => selection.selectedIdSet.has(a.id));
|
|
146
|
+
selected.forEach((a) => {
|
|
147
|
+
wishlistActions.addToWishlist(a.id);
|
|
148
|
+
});
|
|
149
|
+
selection.clearSelection();
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
id: "remove-watchlist",
|
|
154
|
+
label: "Remove from Watchlist",
|
|
155
|
+
variant: "secondary",
|
|
156
|
+
onClick: () => {
|
|
157
|
+
const selected = auctions.filter((a) => selection.selectedIdSet.has(a.id));
|
|
158
|
+
selected.forEach((a) => {
|
|
159
|
+
wishlistActions.removeFromWishlist(a.id);
|
|
160
|
+
});
|
|
161
|
+
selection.clearSelection();
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
] }), totalPages > 1 && (_jsx("div", { className: "sticky top-[calc(var(--header-height,0px)+44px)] z-10 flex justify-center bg-white/95 dark:bg-slate-900/95 backdrop-blur-sm border-b border-zinc-200 dark:border-slate-700 px-3 py-1.5", children: _jsx(Pagination, { currentPage: page, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) })), _jsx("div", { className: "py-6", children: isLoading ? (_jsx("div", { className: gridClass, children: Array.from({ length: 8 }).map((_, i) => (_jsxs("div", { className: "rounded-xl border border-zinc-100 dark:border-slate-700 overflow-hidden animate-pulse", children: [_jsx("div", { className: "aspect-square bg-zinc-200 dark:bg-slate-700" }), _jsxs("div", { className: "p-3 space-y-2", children: [_jsx("div", { className: "h-3 bg-zinc-200 dark:bg-slate-700 rounded w-3/4" }), _jsx("div", { className: "h-4 bg-zinc-200 dark:bg-slate-700 rounded w-1/2" }), _jsx("div", { className: "h-3 bg-zinc-200 dark:bg-slate-700 rounded w-full" }), _jsx("div", { className: "h-8 bg-zinc-200 dark:bg-slate-700 rounded" })] })] }, i))) })) : (_jsx(MarketplaceAuctionGrid, { auctions: auctions, variant: view === "list" ? "list" : "grid", gridClassName: view === "list" ? "flex flex-col gap-4" : gridClass, wishlistActions: wishlistActions })) }), filterOpen && (_jsxs(_Fragment, { children: [_jsx("div", { className: "fixed inset-0 z-40 bg-black/40", "aria-hidden": "true", onClick: () => setFilterOpen(false) }), _jsxs("div", { className: "fixed inset-y-0 left-0 z-50 flex w-80 flex-col bg-white dark:bg-slate-900 shadow-2xl", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: [_jsxs("span", { className: "flex items-center gap-2 text-base font-semibold text-zinc-900 dark:text-zinc-100", children: [_jsx(SlidersHorizontal, { className: "h-4 w-4" }), "Filters"] }), _jsxs("div", { className: "flex items-center gap-2", children: [activeFilterCount > 0 && (_jsx("button", { type: "button", onClick: clearFilters, className: "text-xs text-zinc-500 hover:text-rose-500 dark:text-zinc-400 transition-colors", children: "Clear all" })), _jsx("button", { type: "button", onClick: () => setFilterOpen(false), "aria-label": "Close filters", className: "rounded-lg p-1.5 text-zinc-500 hover:bg-zinc-100 dark:hover:bg-slate-800 transition-colors", children: _jsx(X, { className: "h-5 w-5" }) })] })] }), _jsx("div", { className: "flex-1 overflow-y-auto px-4 py-4", children: _jsx(AuctionFilters, { table: pendingTable, currencyPrefix: "\u20B9", categoryOptions: categoryOptions, brandOptions: brandOptions }) }), _jsx("div", { className: "border-t border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: _jsxs("button", { type: "button", onClick: applyFilters, className: "w-full rounded-lg bg-primary py-2.5 text-sm font-semibold text-white hover:bg-primary-600 transition-colors active:scale-[0.98]", children: ["Apply Filters", activeFilterCount > 0 ? ` (${activeFilterCount})` : ""] }) })] })] }))] }));
|
|
136
165
|
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { useState, useTransition } from "react";
|
|
4
|
-
import { Button, Div, Input, Span, Text } from "../../../ui";
|
|
4
|
+
import { Button, Div, Input, LoginRequiredModal, Span, Text } from "../../../ui";
|
|
5
|
+
import { isAuthError } from "../../../utils/auth-error";
|
|
5
6
|
import { formatCurrency } from "../../../utils/number.formatter";
|
|
6
7
|
export function MakeOfferButton({ productId, listedPrice, currency, minOfferPercent = 70, onMakeOffer, className = "", }) {
|
|
7
8
|
const [state, setState] = useState("idle");
|
|
8
9
|
const [errorMsg, setErrorMsg] = useState("");
|
|
10
|
+
const [showLoginModal, setShowLoginModal] = useState(false);
|
|
9
11
|
const [isPending, startTransition] = useTransition();
|
|
10
12
|
const minOffer = Math.round(listedPrice * (minOfferPercent / 100));
|
|
11
13
|
const defaultOffer = Math.round(listedPrice * 0.9);
|
|
@@ -41,13 +43,19 @@ export function MakeOfferButton({ productId, listedPrice, currency, minOfferPerc
|
|
|
41
43
|
setState("success");
|
|
42
44
|
}
|
|
43
45
|
catch (err) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
if (isAuthError(err)) {
|
|
47
|
+
setState("idle");
|
|
48
|
+
setShowLoginModal(true);
|
|
47
49
|
}
|
|
48
50
|
else {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
52
|
+
if (msg.includes("active offer") || msg.includes("ACTIVE_OFFER")) {
|
|
53
|
+
setState("pending");
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
setErrorMsg(msg || "Could not send offer. Please try again.");
|
|
57
|
+
setState("error");
|
|
58
|
+
}
|
|
51
59
|
}
|
|
52
60
|
}
|
|
53
61
|
});
|
|
@@ -64,5 +72,5 @@ export function MakeOfferButton({ productId, listedPrice, currency, minOfferPerc
|
|
|
64
72
|
if (state === "error") {
|
|
65
73
|
return (_jsxs(Div, { className: `rounded-xl border border-red-200 dark:border-red-800 bg-red-50 dark:bg-red-900/20 p-4 space-y-2 ${className}`, children: [_jsx(Text, { className: "text-xs text-red-600 dark:text-red-400 text-center", children: errorMsg }), _jsxs(Div, { className: "flex gap-2", children: [_jsx(Button, { variant: "ghost", size: "sm", className: "flex-1", onClick: handleCancel, children: "Cancel" }), _jsx(Button, { variant: "secondary", size: "sm", className: "flex-1", onClick: () => setState("confirm"), children: "Try Again" })] })] }));
|
|
66
74
|
}
|
|
67
|
-
return (_jsx(Button, { variant: "ghost", size: "md", className: `w-full border border-zinc-300 dark:border-zinc-600 ${className}`, onClick: handleOpenConfirm, children: "Make Offer" }));
|
|
75
|
+
return (_jsxs(_Fragment, { children: [_jsx(LoginRequiredModal, { isOpen: showLoginModal, onClose: () => setShowLoginModal(false), message: "You need to be signed in to make an offer. Please log in or create an account to continue." }), _jsx(Button, { variant: "ghost", size: "md", className: `w-full border border-zinc-300 dark:border-zinc-600 ${className}`, onClick: handleOpenConfirm, children: "Make Offer" })] }));
|
|
68
76
|
}
|
|
@@ -98,14 +98,15 @@ export function PrizeDrawsIndexListing({ initialData, categorySlug, brandName, s
|
|
|
98
98
|
page: table.getNumber("page", 1),
|
|
99
99
|
perPage: table.getNumber("pageSize", 24),
|
|
100
100
|
listingType: "prize-draw",
|
|
101
|
-
status: showClosed ? undefined : "published",
|
|
102
101
|
};
|
|
103
102
|
const { products: draws, totalPages, page, isLoading } = useProducts(params, { initialData });
|
|
104
103
|
// Client-side filter for prizeRevealStatus until the server query supports it.
|
|
105
104
|
const revealFilter = table.get("prizeRevealStatus");
|
|
106
105
|
const filteredDraws = revealFilter
|
|
107
106
|
? draws.filter((d) => d.prizeRevealStatus === revealFilter)
|
|
108
|
-
:
|
|
107
|
+
: !showClosed
|
|
108
|
+
? draws.filter((d) => d.prizeRevealStatus !== "closed")
|
|
109
|
+
: draws;
|
|
109
110
|
const commitSearch = useCallback(() => {
|
|
110
111
|
table.set("q", searchInput.trim());
|
|
111
112
|
}, [searchInput, table]);
|
|
@@ -117,6 +118,8 @@ export function PrizeDrawsIndexListing({ initialData, categorySlug, brandName, s
|
|
|
117
118
|
return (_jsxs("div", { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search prize draws...", onSearchChange: setSearchInput, onSearchCommit: commitSearch, onSearchKeyDown: handleSearchKeyDown, sortValue: table.get("sort") || "-createdAt", sortOptions: PRIZE_DRAW_SORT_OPTIONS, onSortChange: (v) => {
|
|
118
119
|
table.set("sort", v);
|
|
119
120
|
}, view: view, onViewChange: (v) => {
|
|
121
|
+
if (v === "table")
|
|
122
|
+
return;
|
|
120
123
|
setView(v);
|
|
121
124
|
table.set("view", v);
|
|
122
125
|
}, onResetAll: resetAll, hasActiveState: hasActiveState, extra: _jsxs("label", { className: "flex items-center gap-1.5 cursor-pointer select-none shrink-0", children: [_jsx("span", { className: "hidden sm:inline text-xs text-zinc-600 dark:text-zinc-300 whitespace-nowrap", children: "Show closed" }), _jsx("button", { type: "button", role: "switch", "aria-checked": showClosed, onClick: () => table.set("showClosed", showClosed ? "" : "true"), className: `relative inline-flex h-5 w-9 flex-shrink-0 items-center rounded-full transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1 ${showClosed ? "bg-primary" : "bg-zinc-300 dark:bg-slate-600"}`, children: _jsx("span", { className: `inline-block h-3.5 w-3.5 transform rounded-full bg-white shadow-sm transition-transform duration-200 ${showClosed ? "translate-x-[19px]" : "translate-x-[3px]"}` }) })] }) }), totalPages > 1 && (_jsx("div", { className: "sticky top-[calc(var(--header-height,0px)+44px)] z-10 flex justify-center bg-white/95 dark:bg-slate-900/95 backdrop-blur-sm border-b border-zinc-200 dark:border-slate-700 px-3 py-1.5", children: _jsx(Pagination, { currentPage: page, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) })), _jsx("div", { className: "py-6", children: isLoading ? (_jsx("div", { className: gridClass, children: Array.from({ length: 8 }).map((_, i) => (_jsxs("div", { className: "rounded-xl border border-zinc-100 dark:border-slate-700 overflow-hidden animate-pulse", children: [_jsx("div", { className: "aspect-square bg-zinc-200 dark:bg-slate-700" }), _jsxs("div", { className: "p-3 space-y-2", children: [_jsx("div", { className: "h-3 bg-zinc-200 dark:bg-slate-700 rounded w-3/4" }), _jsx("div", { className: "h-3 bg-zinc-200 dark:bg-slate-700 rounded w-1/2" }), _jsx("div", { className: "h-8 bg-zinc-200 dark:bg-slate-700 rounded" })] })] }, i))) })) : filteredDraws.length === 0 ? (_jsx("p", { className: "py-12 text-center text-sm text-zinc-500 dark:text-zinc-400", children: "No prize draws found." })) : (_jsx("div", { className: gridClass, children: filteredDraws.map((product) => (_jsx(MarketplacePrizeDrawCard, { product: product, variant: view }, product.id))) })) }), filterOpen && (_jsxs(_Fragment, { children: [_jsx("div", { className: "fixed inset-0 z-40 bg-black/40", "aria-hidden": "true", onClick: () => setFilterOpen(false) }), _jsxs("div", { className: "fixed inset-y-0 left-0 z-50 flex w-80 flex-col bg-white dark:bg-slate-900 shadow-2xl", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: [_jsxs("span", { className: "flex items-center gap-2 text-base font-semibold text-zinc-900 dark:text-zinc-100", children: [_jsx(SlidersHorizontal, { className: "h-4 w-4" }), "Filters"] }), _jsxs("div", { className: "flex items-center gap-2", children: [activeFilterCount > 0 && (_jsx("button", { type: "button", onClick: clearFilters, className: "text-xs text-zinc-500 hover:text-rose-500 dark:text-zinc-400 transition-colors", children: "Clear all" })), _jsx("button", { type: "button", onClick: () => setFilterOpen(false), "aria-label": "Close filters", className: "rounded-lg p-1.5 text-zinc-500 hover:bg-zinc-100 dark:hover:bg-slate-800 transition-colors", children: _jsx(X, { className: "h-5 w-5" }) })] })] }), _jsxs("div", { className: "flex-1 overflow-y-auto px-4 py-4 space-y-4", children: [_jsxs("div", { children: [_jsx("label", { htmlFor: "prizeRevealStatusFilter", className: "block text-xs font-semibold text-zinc-700 dark:text-zinc-300 mb-1.5", children: "Reveal status" }), _jsxs("select", { id: "prizeRevealStatusFilter", value: pendingFilters.prizeRevealStatus ?? "", onChange: (e) => setPendingFilters((p) => ({
|
|
@@ -15,7 +15,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
15
15
|
* nor LetItRip admins can influence the outcome.
|
|
16
16
|
*/
|
|
17
17
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
18
|
-
import { Button, Div, Heading, Modal, Stack, Text } from "../../../ui";
|
|
18
|
+
import { Button, Div, Heading, LoginRequiredModal, Modal, Stack, Text } from "../../../ui";
|
|
19
|
+
import { isAuthError } from "../../../utils/auth-error";
|
|
19
20
|
import { PrizeDrawCollage } from "./PrizeDrawCollage";
|
|
20
21
|
const REVEAL_DURATION_MS = 3200; // total animation length
|
|
21
22
|
const CYCLE_INTERVAL_FAST = 80;
|
|
@@ -26,6 +27,7 @@ export function PrizeRevealModal({ open, onClose, items, orderId, productId, onR
|
|
|
26
27
|
const [winner, setWinner] = useState(initialPrizeWon);
|
|
27
28
|
const [errorMessage, setErrorMessage] = useState("");
|
|
28
29
|
const [effectiveRngUrl, setEffectiveRngUrl] = useState(rngSourceUrl);
|
|
30
|
+
const [showLoginModal, setShowLoginModal] = useState(false);
|
|
29
31
|
const cycleTimerRef = useRef(null);
|
|
30
32
|
const endTimerRef = useRef(null);
|
|
31
33
|
// Clean up timers on close/unmount so we don't leak between sessions.
|
|
@@ -83,12 +85,23 @@ export function PrizeRevealModal({ open, onClose, items, orderId, productId, onR
|
|
|
83
85
|
body: JSON.stringify({ orderId }),
|
|
84
86
|
});
|
|
85
87
|
const json = (await res.json());
|
|
86
|
-
if (!res.ok)
|
|
88
|
+
if (!res.ok) {
|
|
89
|
+
if (res.status === 401 || res.status === 403) {
|
|
90
|
+
setPhase("idle");
|
|
91
|
+
setShowLoginModal(true);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
87
94
|
throw new Error("Reveal request failed");
|
|
95
|
+
}
|
|
88
96
|
response = json.data ?? {};
|
|
89
97
|
}
|
|
90
98
|
}
|
|
91
99
|
catch (err) {
|
|
100
|
+
if (isAuthError(err)) {
|
|
101
|
+
setPhase("idle");
|
|
102
|
+
setShowLoginModal(true);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
92
105
|
setPhase("error");
|
|
93
106
|
setErrorMessage(err instanceof Error ? err.message : "Reveal request failed");
|
|
94
107
|
return;
|
|
@@ -117,8 +130,8 @@ export function PrizeRevealModal({ open, onClose, items, orderId, productId, onR
|
|
|
117
130
|
}, REVEAL_DURATION_MS);
|
|
118
131
|
}, [onReveal, orderId, phase, productId, startAnimation]);
|
|
119
132
|
const winnerImg = winner?.images?.[0];
|
|
120
|
-
return (
|
|
121
|
-
|
|
122
|
-
|
|
133
|
+
return (_jsxs(Modal, { isOpen: open, onClose: onClose, title: "Prize Reveal", size: "lg", children: [_jsx(LoginRequiredModal, { isOpen: showLoginModal, onClose: () => setShowLoginModal(false), message: "You need to be signed in to reveal your prize. Please log in or create an account to continue." }), _jsxs(Stack, { gap: "md", children: [phase === "refunded" ? (_jsxs(Div, { className: "rounded border border-yellow-400/40 bg-yellow-50 px-4 py-3 text-yellow-900 dark:bg-yellow-900/30 dark:text-yellow-100", children: [_jsx(Heading, { level: 3, className: "mb-1", children: "Pool exhausted \u2014 you've been refunded" }), _jsx(Text, { className: "text-sm", children: "Every prize in this draw was already claimed by the time your entry rolled. Your order has been marked refunded automatically." })] })) : null, phase === "error" ? (_jsxs(Div, { className: "rounded border border-red-400/40 bg-red-50 px-4 py-3 text-red-900 dark:bg-red-900/30 dark:text-red-100", children: [_jsx(Text, { className: "text-sm font-semibold", children: "Something went wrong" }), _jsx(Text, { className: "text-sm", children: errorMessage })] })) : null, _jsx(PrizeDrawCollage, { items: items, hideWonState: true, highlightItemNumber: highlight }), phase === "idle" ? (_jsx(Button, { type: "button", variant: "primary", size: "lg", onClick: handleRevealClick, children: "\u2728 Reveal my prize" })) : null, phase === "revealing" ? (_jsxs(Div, { className: "rounded bg-[var(--appkit-color-surface-muted)] p-4 text-center", children: [_jsx(Text, { className: "text-lg font-semibold", children: "Rolling\u2026" }), _jsx(Text, { className: "text-sm text-[var(--appkit-color-text-muted)]", children: "The winner was locked by the server before this animation started. Hang tight \u2014 we're just making it look pretty." })] })) : null, phase === "won" && winner ? (_jsxs(Div, { className: "rounded-lg border-2 border-[var(--appkit-color-primary)] bg-[var(--appkit-color-surface)] p-4 text-center", children: [_jsx(Text, { className: "text-xs uppercase tracking-wider text-[var(--appkit-color-text-muted)]", children: "You won" }), _jsxs(Heading, { level: 2, className: "my-2", children: ["#", winner.itemNumber, " \u2014 ", winner.title] }), winnerImg ? (
|
|
134
|
+
/* eslint-disable-next-line @next/next/no-img-element */
|
|
135
|
+
_jsx("img", { src: winnerImg, alt: winner.title, className: "mx-auto max-h-64 rounded" })) : null, winner.estimatedValue != null ? (_jsxs(Text, { className: "mt-2 text-sm text-[var(--appkit-color-text-muted)]", children: ["Estimated value: \u20B9", (winner.estimatedValue / 100).toLocaleString("en-IN")] })) : null] })) : null, _jsxs(Div, { className: "rounded border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface-muted)] px-3 py-2 text-xs text-[var(--appkit-color-text-muted)]", children: [_jsx("strong", { className: "text-[var(--appkit-color-text)]", children: "Fairness guarantee:" }), " ", "Winners are picked by ", _jsx("code", { children: "crypto.randomInt" }), " running on LetItRip's server before the animation starts. The animation is theatrical \u2014 neither the store nor LetItRip staff can influence the outcome.", " ", effectiveRngUrl ? (_jsx("a", { href: effectiveRngUrl, target: "_blank", rel: "noreferrer noopener", className: "underline", children: "View RNG source code \u2192" })) : null] })] })] }));
|
|
123
136
|
}
|
|
124
137
|
export default PrizeRevealModal;
|
|
@@ -182,8 +182,30 @@ export async function ProductDetailPageView({ slug, initialProduct, renderOfferA
|
|
|
182
182
|
: Promise.resolve([]),
|
|
183
183
|
]);
|
|
184
184
|
const reviews = reviewDocs.map(toReview);
|
|
185
|
+
const _now = new Date();
|
|
185
186
|
const relatedItems = relatedDocs
|
|
186
|
-
.filter((r) =>
|
|
187
|
+
.filter((r) => {
|
|
188
|
+
if (r.id === product.id)
|
|
189
|
+
return false;
|
|
190
|
+
const s = r.status;
|
|
191
|
+
if (s && ["sold", "out_of_stock", "archived", "discontinued", "draft"].includes(s))
|
|
192
|
+
return false;
|
|
193
|
+
if (r.isSold === true)
|
|
194
|
+
return false;
|
|
195
|
+
if (r.availableQuantity === 0)
|
|
196
|
+
return false;
|
|
197
|
+
if (r.listingType === "auction" && r.auctionEndDate) {
|
|
198
|
+
const end = r.auctionEndDate;
|
|
199
|
+
const endDate = typeof end.toDate === "function"
|
|
200
|
+
? end.toDate()
|
|
201
|
+
: end instanceof Date ? end : new Date(String(end));
|
|
202
|
+
if (endDate <= _now)
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
if (r.listingType === "prize-draw" && r.prizeRevealStatus === "closed")
|
|
206
|
+
return false;
|
|
207
|
+
return true;
|
|
208
|
+
})
|
|
187
209
|
.slice(0, 8)
|
|
188
210
|
.map(toProductItem);
|
|
189
211
|
const formattedPrice = price !== null ? formatCurrency(price, currency) : null;
|
|
@@ -5,7 +5,7 @@ import { X, ShoppingCart, Heart, SlidersHorizontal, Columns } from "lucide-react
|
|
|
5
5
|
import { useRouter } from "next/navigation";
|
|
6
6
|
import { useUrlTable } from "../../../react/hooks/useUrlTable";
|
|
7
7
|
import { useProducts } from "../hooks/useProducts";
|
|
8
|
-
import { Pagination, useToast,
|
|
8
|
+
import { Pagination, useToast, BulkActionBar, ListingToolbar } from "../../../ui";
|
|
9
9
|
import { ACTION_ID, ACTION_META, COMPARE_MAX_ITEMS } from "../constants/action-defs";
|
|
10
10
|
import { CompareOverlay } from "./CompareOverlay";
|
|
11
11
|
import { ROUTES } from "../../../next";
|
|
@@ -24,7 +24,7 @@ export function ProductsIndexListing({ initialData }) {
|
|
|
24
24
|
const [filterOpen, setFilterOpen] = useState(false);
|
|
25
25
|
const [compareIds, setCompareIds] = useState([]);
|
|
26
26
|
const showSold = table.get("showSold") === "true";
|
|
27
|
-
const [view, setView] = useState(table.get("view") || "
|
|
27
|
+
const [view, setView] = useState(table.get("view") || "grid");
|
|
28
28
|
// Pending filter state — buffered until "Apply Filters" clicked
|
|
29
29
|
const [pendingFilters, setPendingFilters] = useState(() => Object.fromEntries(FILTER_KEYS.map((k) => [k, table.get(k)])));
|
|
30
30
|
const pendingTable = useMemo(() => ({
|
|
@@ -97,7 +97,9 @@ export function ProductsIndexListing({ initialData }) {
|
|
|
97
97
|
page: table.getNumber("page", 1),
|
|
98
98
|
perPage: table.getNumber("pageSize", 24),
|
|
99
99
|
listingType: "standard",
|
|
100
|
-
|
|
100
|
+
// Hide sold-out items by default. Uses stockQuantity>0 (always-present field)
|
|
101
|
+
// instead of status=="published" because sellers don't actively transition status.
|
|
102
|
+
inStock: showSold ? undefined : true,
|
|
101
103
|
};
|
|
102
104
|
const { products, totalPages, page, isLoading } = useProducts(params, { initialData });
|
|
103
105
|
const selection = useBulkSelection({ items: products, keyExtractor: (p) => p.id });
|
|
@@ -109,6 +111,8 @@ export function ProductsIndexListing({ initialData }) {
|
|
|
109
111
|
commitSearch();
|
|
110
112
|
};
|
|
111
113
|
const handleViewToggle = (next) => {
|
|
114
|
+
if (next === "table")
|
|
115
|
+
return;
|
|
112
116
|
setView(next);
|
|
113
117
|
table.set("view", next);
|
|
114
118
|
};
|
|
@@ -143,9 +147,9 @@ export function ProductsIndexListing({ initialData }) {
|
|
|
143
147
|
pushCartOp({ op: "add", productId: product.id, quantity: 1, productTitle: product.title, productImage: product.mainImage, price: product.price });
|
|
144
148
|
router.push(String(ROUTES.USER.CART));
|
|
145
149
|
}, [localCart, router]);
|
|
146
|
-
return (_jsxs("div", { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search products...", onSearchChange: setSearchInput, onSearchCommit: commitSearch, onSearchKeyDown: handleSearchKeyDown, sortValue: table.get("sort") || "-createdAt", sortOptions: PRODUCT_PUBLIC_SORT_OPTIONS, onSortChange: (v) => { table.set("sort", v); }, view: view
|
|
150
|
+
return (_jsxs("div", { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search products...", onSearchChange: setSearchInput, onSearchCommit: commitSearch, onSearchKeyDown: handleSearchKeyDown, sortValue: table.get("sort") || "-createdAt", sortOptions: PRODUCT_PUBLIC_SORT_OPTIONS, onSortChange: (v) => { table.set("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, extra: _jsxs("label", { className: "flex items-center gap-1.5 cursor-pointer select-none shrink-0", children: [_jsx("span", { className: "hidden sm:inline text-xs text-zinc-600 dark:text-zinc-300 whitespace-nowrap", children: "Show sold" }), _jsx("button", { type: "button", role: "switch", "aria-checked": showSold, onClick: () => table.set("showSold", showSold ? "" : "true"), className: `relative inline-flex h-5 w-9 flex-shrink-0 items-center rounded-full transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1 ${showSold ? "bg-primary" : "bg-zinc-300 dark:bg-slate-600"}`, children: _jsx("span", { className: `inline-block h-3.5 w-3.5 transform rounded-full bg-white shadow-sm transition-transform duration-200 ${showSold ? "translate-x-[19px]" : "translate-x-[3px]"}` }) })] }) }), _jsx(BulkActionBar, { selectedCount: selection.selectedCount, onClearSelection: selection.clearSelection, actions: [
|
|
147
151
|
{
|
|
148
|
-
|
|
152
|
+
id: ACTION_ID.ADD_TO_CART,
|
|
149
153
|
label: ACTION_META[ACTION_ID.ADD_TO_CART].label,
|
|
150
154
|
icon: _jsx(ShoppingCart, { className: "h-3.5 w-3.5" }),
|
|
151
155
|
variant: "primary",
|
|
@@ -160,7 +164,7 @@ export function ProductsIndexListing({ initialData }) {
|
|
|
160
164
|
},
|
|
161
165
|
},
|
|
162
166
|
{
|
|
163
|
-
|
|
167
|
+
id: ACTION_ID.ADD_TO_WISHLIST,
|
|
164
168
|
label: ACTION_META[ACTION_ID.ADD_TO_WISHLIST].label,
|
|
165
169
|
icon: _jsx(Heart, { className: "h-3.5 w-3.5" }),
|
|
166
170
|
variant: "secondary",
|
|
@@ -175,7 +179,7 @@ export function ProductsIndexListing({ initialData }) {
|
|
|
175
179
|
},
|
|
176
180
|
},
|
|
177
181
|
{
|
|
178
|
-
|
|
182
|
+
id: ACTION_ID.COMPARE,
|
|
179
183
|
label: ACTION_META[ACTION_ID.COMPARE].label,
|
|
180
184
|
icon: _jsx(Columns, { className: "h-3.5 w-3.5" }),
|
|
181
185
|
variant: "secondary",
|
|
@@ -185,7 +189,7 @@ export function ProductsIndexListing({ initialData }) {
|
|
|
185
189
|
setCompareIds(ids);
|
|
186
190
|
},
|
|
187
191
|
},
|
|
188
|
-
] }), _jsx(CompareOverlay, { isOpen: compareIds.length > 0, productIds: compareIds, productType: "product", onClose: () => {
|
|
192
|
+
] }), totalPages > 1 && (_jsx("div", { className: "sticky top-[calc(var(--header-height,0px)+44px)] z-10 flex justify-center bg-white/95 dark:bg-slate-900/95 backdrop-blur-sm border-b border-zinc-200 dark:border-slate-700 px-3 py-1.5", children: _jsx(Pagination, { currentPage: page, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) })), _jsx("div", { className: "py-6", children: isLoading ? (_jsx("div", { className: "grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-4", children: Array.from({ length: 10 }).map((_, i) => (_jsxs("div", { className: "rounded-xl border border-zinc-100 dark:border-slate-700 overflow-hidden animate-pulse", children: [_jsx("div", { className: "aspect-square bg-zinc-200 dark:bg-slate-700" }), _jsxs("div", { className: "p-3 space-y-2", children: [_jsx("div", { className: "h-3 bg-zinc-200 dark:bg-slate-700 rounded w-3/4" }), _jsx("div", { className: "h-3 bg-zinc-200 dark:bg-slate-700 rounded w-1/2" }), _jsx("div", { className: "h-4 bg-zinc-200 dark:bg-slate-700 rounded w-1/3" })] })] }, i))) })) : (_jsx(ProductGrid, { products: products, getProductHref: (p) => String(ROUTES.PUBLIC.PRODUCT_DETAIL(p.slug || p.id)), view: view === "grid" ? "card" : "list", onWishlistToggle: handleWishlistToggle, wishlistedIds: wishlistedIds, onAddToCart: handleAddToCart, onBuyNow: handleBuyNow, selectionMode: selection.isSelecting, selectedIds: selection.selectedIdSet, onToggleSelect: selection.toggle })) }), _jsx(CompareOverlay, { isOpen: compareIds.length > 0, productIds: compareIds, productType: "product", onClose: () => {
|
|
189
193
|
setCompareIds([]);
|
|
190
194
|
selection.clearSelection();
|
|
191
195
|
}, onRemove: (id) => setCompareIds((ids) => ids.filter((i) => i !== id)) }), filterOpen && (_jsxs(_Fragment, { children: [_jsx("div", { className: "fixed inset-0 z-40 bg-black/40", "aria-hidden": "true", onClick: () => setFilterOpen(false) }), _jsxs("div", { className: "fixed inset-y-0 left-0 z-50 flex w-80 flex-col bg-white dark:bg-slate-900 shadow-2xl", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: [_jsxs("span", { className: "flex items-center gap-2 text-base font-semibold text-zinc-900 dark:text-zinc-100", children: [_jsx(SlidersHorizontal, { className: "h-4 w-4" }), "Filters"] }), _jsxs("div", { className: "flex items-center gap-2", children: [activeFilterCount > 0 && (_jsx("button", { type: "button", onClick: clearFilters, className: "text-xs text-zinc-500 hover:text-rose-500 dark:text-zinc-400 transition-colors", children: "Clear all" })), _jsx("button", { type: "button", onClick: () => setFilterOpen(false), "aria-label": "Close filters", className: "rounded-lg p-1.5 text-zinc-500 hover:bg-zinc-100 dark:hover:bg-slate-800 transition-colors", children: _jsx(X, { className: "h-5 w-5" }) })] })] }), _jsx("div", { className: "flex-1 overflow-y-auto px-4 py-4", children: _jsx(ProductFilters, { table: pendingTable, currencyPrefix: "\u20B9", categoryOptions: categoryOptions }) }), _jsx("div", { className: "border-t border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: _jsxs("button", { type: "button", onClick: applyFilters, className: "w-full rounded-lg bg-primary py-2.5 text-sm font-semibold text-white hover:bg-primary-600 transition-colors active:scale-[0.98]", children: ["Apply Filters", activeFilterCount > 0 ? ` (${activeFilterCount})` : ""] }) })] })] }))] }));
|
|
@@ -13,6 +13,13 @@ export function ReviewsIndexListing({ initialData, variant = "public", }) {
|
|
|
13
13
|
const table = useUrlTable({ defaults: { pageSize: String(PAGE_SIZE), sort: "-createdAt" } });
|
|
14
14
|
const [searchInput, setSearchInput] = useState(table.get("q") || "");
|
|
15
15
|
const [filterOpen, setFilterOpen] = useState(false);
|
|
16
|
+
const [view, setView] = useState(table.get("view") || "grid");
|
|
17
|
+
const handleViewToggle = (next) => {
|
|
18
|
+
if (next === "table")
|
|
19
|
+
return;
|
|
20
|
+
setView(next);
|
|
21
|
+
table.set("view", next);
|
|
22
|
+
};
|
|
16
23
|
const sort = table.get("sort") || "-createdAt";
|
|
17
24
|
const currentPage = table.getNumber("page", 1);
|
|
18
25
|
// Pending filter state — buffered until "Apply Filters" clicked
|
|
@@ -85,5 +92,5 @@ export function ReviewsIndexListing({ initialData, variant = "public", }) {
|
|
|
85
92
|
value: opt.value,
|
|
86
93
|
label: opt.key,
|
|
87
94
|
}));
|
|
88
|
-
return (_jsxs("div", { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search reviews by product name...", onSearchChange: setSearchInput, onSearchCommit: commitSearch, sortValue: sort, sortOptions: sortOptions, onSortChange: (v) => { table.set("sort", v); },
|
|
95
|
+
return (_jsxs("div", { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search reviews by product name...", onSearchChange: setSearchInput, onSearchCommit: commitSearch, sortValue: sort, sortOptions: sortOptions, onSortChange: (v) => { table.set("sort", v); }, view: view, onViewChange: handleViewToggle, onResetAll: resetAll, hasActiveState: hasActiveState }), totalPages > 1 && (_jsx("div", { className: "sticky top-[calc(var(--header-height,0px)+44px)] z-10 flex justify-center bg-white/95 dark:bg-slate-900/95 backdrop-blur-sm border-b border-zinc-200 dark:border-slate-700 px-3 py-1.5", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) })), _jsx("div", { className: "py-6", children: isLoading ? (_jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6", children: Array.from({ length: 6 }).map((_, i) => (_jsx("div", { className: "rounded-xl border border-zinc-100 dark:border-slate-700 overflow-hidden animate-pulse", children: _jsxs("div", { className: "p-4 space-y-3", children: [_jsx("div", { className: "h-4 bg-zinc-200 dark:bg-slate-700 rounded w-3/4" }), _jsx("div", { className: "h-3 bg-zinc-200 dark:bg-slate-700 rounded w-full" }), _jsx("div", { className: "h-3 bg-zinc-200 dark:bg-slate-700 rounded w-2/3" })] }) }, i))) })) : reviews.length === 0 ? (_jsx("p", { className: "py-12 text-center text-sm text-zinc-500 dark:text-zinc-400", children: "No reviews found." })) : view === "list" ? (_jsx("div", { className: "flex flex-col divide-y divide-zinc-100 dark:divide-zinc-800 rounded-xl border border-zinc-100 dark:border-zinc-800", children: reviews.map((review) => (_jsx(ReviewCard, { review: review }, review.id))) })) : (_jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6", children: reviews.map((review) => (_jsx(ReviewCard, { review: review }, review.id))) })) }), filterOpen && (_jsxs(_Fragment, { children: [_jsx("div", { className: "fixed inset-0 z-40 bg-black/40", "aria-hidden": "true", onClick: () => setFilterOpen(false) }), _jsxs("div", { className: "fixed inset-y-0 left-0 z-50 flex w-80 flex-col bg-white dark:bg-slate-900 shadow-2xl", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: [_jsx("span", { className: "flex items-center gap-2 text-base font-semibold text-zinc-900 dark:text-zinc-100", children: "Filters" }), _jsxs("div", { className: "flex items-center gap-2", children: [activeFilterCount > 0 && (_jsx("button", { type: "button", onClick: clearFilters, className: "text-xs text-zinc-500 hover:text-rose-500 dark:text-zinc-400 transition-colors", children: "Clear all" })), _jsx("button", { type: "button", onClick: () => setFilterOpen(false), "aria-label": "Close filters", className: "rounded-lg p-1.5 text-zinc-500 hover:bg-zinc-100 dark:hover:bg-slate-800 transition-colors", children: _jsx(X, { className: "h-5 w-5" }) })] })] }), _jsx("div", { className: "flex-1 overflow-y-auto px-4 py-4", children: _jsx(ReviewFilters, { table: pendingTable, variant: variant }) }), _jsx("div", { className: "border-t border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: _jsxs("button", { type: "button", onClick: applyFilters, className: "w-full rounded-lg bg-primary py-2.5 text-sm font-semibold text-white hover:bg-primary-600 transition-colors active:scale-[0.98]", children: ["Apply Filters", activeFilterCount > 0 ? ` (${activeFilterCount})` : ""] }) })] })] }))] }));
|
|
89
96
|
}
|