@mohasinac/appkit 4.2.0 → 4.3.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/auth/actions.d.ts +0 -1
- package/dist/_internal/server/features/auth/actions.js +0 -1
- package/dist/_internal/server/features/bundles/data.d.ts +22 -4
- package/dist/_internal/server/features/bundles/data.js +41 -4
- package/dist/_internal/server/features/bundles/index.d.ts +1 -1
- package/dist/_internal/server/features/bundles/index.js +1 -1
- package/dist/_internal/server/features/checkout/actions.js +7 -78
- package/dist/_internal/server/features/checkout/prize-bundle-gates.d.ts +5 -16
- package/dist/_internal/server/features/checkout/prize-bundle-gates.js +5 -30
- package/dist/_internal/server/features/orders/adapters.js +1 -14
- package/dist/_internal/server/features/products/actions.js +8 -3
- package/dist/_internal/server/features/products/service.d.ts +15 -0
- package/dist/_internal/server/features/products/service.js +41 -0
- package/dist/_internal/server/functions/firestore.d.ts +3 -1
- package/dist/_internal/server/functions/firestore.js +17 -1
- package/dist/_internal/server/functions/scheduled.d.ts +2 -5
- package/dist/_internal/server/functions/scheduled.js +6 -30
- package/dist/_internal/server/jobs/core/bundleStockSync.js +25 -7
- package/dist/_internal/server/jobs/core/countersReconcile.js +7 -1
- package/dist/_internal/server/jobs/core/index.d.ts +4 -4
- package/dist/_internal/server/jobs/core/index.js +4 -4
- package/dist/_internal/server/jobs/core/onPrizeDrawPaymentConfirmed.d.ts +19 -0
- package/dist/_internal/server/jobs/core/onPrizeDrawPaymentConfirmed.js +30 -0
- package/dist/_internal/server/jobs/core/paymentWindowTimeout.d.ts +2 -2
- package/dist/_internal/server/jobs/core/paymentWindowTimeout.js +2 -2
- package/dist/_internal/server/jobs/core/prizeDrawAssignWinner.d.ts +20 -0
- package/dist/_internal/server/jobs/core/prizeDrawAssignWinner.js +113 -0
- package/dist/_internal/server/jobs/core/prizeDrawExpiryReveal.d.ts +14 -0
- package/dist/_internal/server/jobs/core/prizeDrawExpiryReveal.js +74 -0
- package/dist/_internal/server/jobs/core/prizeDrawSoldOutReveal.d.ts +22 -0
- package/dist/_internal/server/jobs/core/prizeDrawSoldOutReveal.js +68 -0
- package/dist/_internal/server/jobs/handlers/index.d.ts +3 -4
- package/dist/_internal/server/jobs/handlers/index.js +8 -6
- package/dist/_internal/server/jobs/handlers/onPrizeDrawPaymentConfirmed.d.ts +3 -0
- package/dist/_internal/server/jobs/handlers/onPrizeDrawPaymentConfirmed.js +11 -0
- package/dist/_internal/server/jobs/handlers/prizeDrawExpiryReveal.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/prizeDrawExpiryReveal.js +2 -0
- package/dist/_internal/server/jobs/handlers/prizeDrawSoldOutReveal.d.ts +3 -0
- package/dist/_internal/server/jobs/handlers/prizeDrawSoldOutReveal.js +4 -0
- package/dist/_internal/shared/actions/action-registry.js +40 -10
- package/dist/_internal/shared/checkout/rules/_defaults.js +1 -2
- package/dist/_internal/shared/checkout/rules/_limits.d.ts +3 -0
- package/dist/_internal/shared/checkout/rules/_limits.js +3 -0
- package/dist/_internal/shared/checkout/rules/_registry.d.ts +2 -1
- package/dist/_internal/shared/checkout/rules/_registry.js +2 -2
- package/dist/_internal/shared/checkout/rules/classified.rule.js +0 -1
- package/dist/_internal/shared/checkout/rules/index.d.ts +1 -1
- package/dist/_internal/shared/checkout/rules/index.js +1 -1
- package/dist/_internal/shared/checkout/rules/prize-draw.rule.js +20 -1
- package/dist/_internal/shared/checkout/rules/types.d.ts +17 -7
- package/dist/_internal/shared/features/categories/bundle-copy.d.ts +2 -0
- package/dist/_internal/shared/features/categories/bundle-copy.js +2 -0
- package/dist/_internal/shared/features/categories/bundle-pricing.d.ts +23 -0
- package/dist/_internal/shared/features/categories/bundle-pricing.js +25 -0
- package/dist/_internal/shared/listing-types/prize-draw/config.js +1 -1
- package/dist/constants/api-endpoints.d.ts +6 -9
- package/dist/constants/api-endpoints.js +5 -5
- package/dist/constants/field-names.d.ts +17 -1
- package/dist/constants/field-names.js +17 -1
- package/dist/features/account/hooks/useNotifications.js +5 -0
- package/dist/features/admin/actions/admin-actions.js +6 -0
- package/dist/features/admin/actions/notification-actions.js +1 -2
- package/dist/features/admin/components/AdminBlogEditorView.js +1 -1
- package/dist/features/admin/components/AdminPrizeDrawsView.js +3 -3
- package/dist/features/admin/components/AdminSiteSettingsView.js +33 -6
- package/dist/features/admin/components/AdminTesterChecklistView.js +74 -22
- package/dist/features/admin/hooks/useAdminListing.d.ts +4 -0
- package/dist/features/admin/hooks/useAdminListing.js +4 -2
- package/dist/features/admin/schemas/firestore.d.ts +13 -5
- package/dist/features/admin/schemas/firestore.js +1 -2
- package/dist/features/auctions/actions/bid-actions.js +3 -1
- package/dist/features/auctions/components/AuctionBidsTable.d.ts +6 -1
- package/dist/features/auctions/components/AuctionBidsTable.js +8 -4
- package/dist/features/auctions/components/AuctionDetailPageView.js +10 -13
- package/dist/features/auctions/components/CollapsibleBidHistory.d.ts +23 -2
- package/dist/features/auctions/components/CollapsibleBidHistory.js +21 -3
- package/dist/features/auctions/components/LiveBidPrice.d.ts +19 -0
- package/dist/features/auctions/components/LiveBidPrice.js +17 -0
- package/dist/features/auctions/components/PlaceBidFormClient.d.ts +1 -1
- package/dist/features/auctions/components/PlaceBidFormClient.js +16 -1
- package/dist/features/auctions/hooks/useBids.d.ts +33 -0
- package/dist/features/auctions/hooks/useBids.js +32 -0
- package/dist/features/auctions/hooks/useLiveAuctionBid.d.ts +20 -0
- package/dist/features/auctions/hooks/useLiveAuctionBid.js +23 -0
- package/dist/features/auth/checkout-value-otp.d.ts +7 -10
- package/dist/features/auth/checkout-value-otp.js +14 -10
- package/dist/features/auth/server.d.ts +0 -1
- package/dist/features/auth/server.js +0 -1
- package/dist/features/blog/api/[slug]/route.d.ts +5 -0
- package/dist/features/blog/api/[slug]/route.js +12 -3
- package/dist/features/blog/components/BlogPostView.d.ts +2 -0
- package/dist/features/blog/components/BlogPostView.js +5 -4
- package/dist/features/blog/hooks/useBlog.d.ts +2 -0
- package/dist/features/blog/hooks/useBlog.js +2 -0
- package/dist/features/blog/repository/blog.repository.d.ts +7 -0
- package/dist/features/blog/repository/blog.repository.js +44 -0
- package/dist/features/blog/schemas/firestore.d.ts +3 -0
- package/dist/features/blog/schemas/firestore.js +1 -0
- package/dist/features/blog/types/index.d.ts +2 -0
- package/dist/features/cart/components/index.d.ts +0 -2
- package/dist/features/cart/components/index.js +0 -1
- package/dist/features/cart/hooks/useCartCount.js +22 -5
- package/dist/features/cart/hooks/useGuestCart.js +11 -1
- package/dist/features/cart/utils/guest-cart.d.ts +4 -0
- package/dist/features/cart/utils/guest-cart.js +10 -0
- package/dist/features/cart/utils/pending-ops.d.ts +17 -0
- package/dist/features/cart/utils/pending-ops.js +46 -5
- package/dist/features/categories/components/BrandDetailPageView.js +6 -2
- package/dist/features/categories/components/BundleDetailView.js +4 -2
- package/dist/features/categories/components/CategoryDetailPageView.js +10 -2
- package/dist/features/categories/schemas/firestore.d.ts +7 -0
- package/dist/features/checkout/actions/index.d.ts +0 -1
- package/dist/features/checkout/actions/index.js +0 -1
- package/dist/features/checkout/schemas/firestore.d.ts +2 -2
- package/dist/features/events/actions/event-actions.d.ts +2 -0
- package/dist/features/events/actions/event-actions.js +5 -0
- package/dist/features/events/components/EventCard.js +1 -2
- package/dist/features/events/components/RelatedEventsCarousel.d.ts +15 -0
- package/dist/features/events/components/RelatedEventsCarousel.js +13 -0
- package/dist/features/events/components/index.d.ts +2 -0
- package/dist/features/events/components/index.js +1 -0
- package/dist/features/events/repository/events.repository.d.ts +6 -0
- package/dist/features/events/repository/events.repository.js +21 -0
- package/dist/features/history/repository/user-history.repository.d.ts +1 -1
- package/dist/features/history/utils/guest-history.d.ts +1 -1
- package/dist/features/homepage/components/FeaturedBundlesSection.js +4 -2
- package/dist/features/homepage/components/FeaturedProductsSection.js +2 -2
- package/dist/features/homepage/components/SectionCarousel.js +1 -1
- package/dist/features/homepage/components/WelcomeSection.js +1 -1
- package/dist/features/layout/AppLayoutShell.js +1 -1
- package/dist/features/layout/NavbarLayout.js +1 -1
- package/dist/features/layout/TitleBarLayout.js +2 -2
- package/dist/features/media/MediaVideo.d.ts +7 -0
- package/dist/features/media/MediaVideo.js +36 -5
- package/dist/features/orders/components/OrdersList.js +1 -5
- package/dist/features/orders/schemas/firestore.d.ts +2 -8
- package/dist/features/orders/schemas/index.d.ts +3 -32
- package/dist/features/orders/schemas/index.js +1 -4
- package/dist/features/orders/types/index.d.ts +6 -9
- package/dist/features/pre-orders/components/MarketplacePreorderCard.js +1 -1
- package/dist/features/pre-orders/components/PreOrderDetailPageView.js +2 -1
- package/dist/features/products/actions/product-actions.js +13 -10
- package/dist/features/products/components/MarketplaceBundleCard.d.ts +1 -1
- package/dist/features/products/components/MarketplaceBundleCard.js +7 -4
- package/dist/features/products/components/PrizeDrawDetailPageView.js +1 -1
- package/dist/features/products/components/PrizeDrawItemsEditor.js +2 -2
- package/dist/features/products/components/PrizeDrawWinnerMappingView.d.ts +14 -0
- package/dist/features/products/components/PrizeDrawWinnerMappingView.js +37 -0
- package/dist/features/products/components/PrizeRevealModal.d.ts +10 -23
- package/dist/features/products/components/PrizeRevealModal.js +18 -147
- package/dist/features/products/components/ProductDetailPageView.js +72 -30
- package/dist/features/products/components/ProductForm.js +7 -14
- package/dist/features/products/components/ProductGalleryClient.d.ts +10 -1
- package/dist/features/products/components/ProductGalleryClient.js +29 -10
- package/dist/features/products/components/ProductsIndexListing.js +2 -1
- package/dist/features/products/components/RelatedProductsCarousel.js +2 -2
- package/dist/features/products/components/ShowGroupSection.js +2 -2
- package/dist/features/products/components/SublistingCarouselSection.js +1 -1
- package/dist/features/products/components/index.d.ts +4 -2
- package/dist/features/products/components/index.js +1 -0
- package/dist/features/products/constants/action-defs.d.ts +0 -1
- package/dist/features/products/constants/action-defs.js +0 -2
- package/dist/features/products/repository/products.repository.d.ts +6 -0
- package/dist/features/products/repository/products.repository.js +20 -1
- package/dist/features/products/schemas/firestore.d.ts +26 -2
- package/dist/features/products/schemas/index.d.ts +11 -3
- package/dist/features/products/schemas/index.js +10 -2
- package/dist/features/products/types/index.d.ts +2 -1
- package/dist/features/reviews/actions/review-actions.js +1 -0
- package/dist/features/reviews/components/ReviewDetailPageView.js +8 -1
- package/dist/features/reviews/components/ReviewDetailShell.js +2 -1
- package/dist/features/reviews/components/ReviewsList.js +2 -1
- package/dist/features/reviews/schemas/firestore.d.ts +3 -0
- package/dist/features/reviews/schemas/index.d.ts +6 -0
- package/dist/features/reviews/schemas/index.js +2 -0
- package/dist/features/reviews/types/index.d.ts +3 -0
- package/dist/features/scams/actions/scam-actions.d.ts +3 -0
- package/dist/features/scams/actions/scam-actions.js +3 -2
- package/dist/features/scams/components/ScamProfileView.d.ts +2 -1
- package/dist/features/scams/components/ScamProfileView.js +2 -2
- package/dist/features/scams/repository/scammer.repository.d.ts +6 -0
- package/dist/features/scams/repository/scammer.repository.js +24 -0
- package/dist/features/seller/actions/seller-actions.js +8 -0
- package/dist/features/seller/components/SellerPrizeDrawsView.js +4 -0
- package/dist/features/stores/components/StoreDetailLayoutView.js +6 -6
- package/dist/features/stores/components/StoreNavTabs.d.ts +10 -1
- package/dist/features/stores/components/StoreNavTabs.js +21 -9
- package/dist/features/tester/actions/index.d.ts +1 -0
- package/dist/features/tester/actions/index.js +1 -0
- package/dist/features/tester/actions/leaderboard-actions.d.ts +3 -0
- package/dist/features/tester/actions/leaderboard-actions.js +5 -0
- package/dist/features/tester/components/AdminTesterFeedbackIssuesView.js +26 -4
- package/dist/features/tester/components/AdminTesterFeedbackListView.js +13 -0
- package/dist/features/tester/components/BugHunterLeaderboardView.d.ts +23 -0
- package/dist/features/tester/components/BugHunterLeaderboardView.js +18 -0
- package/dist/features/tester/components/index.d.ts +2 -0
- package/dist/features/tester/components/index.js +1 -0
- package/dist/features/tester/repository/tester-checklist-item.repository.d.ts +12 -1
- package/dist/features/tester/repository/tester-checklist-item.repository.js +80 -0
- package/dist/features/tester/repository/tester-checklist-response.repository.d.ts +4 -0
- package/dist/features/tester/repository/tester-checklist-response.repository.js +4 -0
- package/dist/features/tester/schemas/firestore.d.ts +22 -1
- package/dist/features/tester/schemas/firestore.js +8 -0
- package/dist/features/tester/seed-data/categories-tester-seed-data.js +42 -7
- package/dist/features/tester/seed-data/events-tester-seed-data.js +23 -0
- package/dist/features/tester/seed-data/orders-tester-seed-data.js +69 -31
- package/dist/features/tester/seed-data/products-tester-seed-data.js +152 -8
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +249 -14
- package/dist/features/wishlist/hooks/useGuestWishlist.js +11 -1
- package/dist/features/wishlist/hooks/useWishlistCount.js +16 -2
- package/dist/features/wishlist/utils/guest-wishlist.d.ts +5 -0
- package/dist/features/wishlist/utils/guest-wishlist.js +11 -0
- package/dist/index.d.ts +19 -22
- package/dist/index.js +25 -58
- package/dist/jobs.d.ts +1 -1
- package/dist/jobs.js +2 -2
- package/dist/next/routing/route-map.d.ts +2 -0
- package/dist/next/routing/route-map.js +1 -0
- package/dist/schemas/registry.d.ts +6 -26
- package/dist/seed/bids-seed-data.js +14 -2
- package/dist/seed/blog-posts-seed-data.js +2 -1
- package/dist/seed/categories-seed-data.js +11 -2
- package/dist/seed/events-seed-data.js +11 -11
- package/dist/seed/faq-seed-data.js +5 -1
- package/dist/seed/products-auctions-seed-data.js +2 -2
- package/dist/seed/products-preorders-seed-data.d.ts +2 -1
- package/dist/seed/products-prize-draws-seed-data.js +56 -10
- package/dist/seed/products-standard-seed-data.js +82 -3
- package/dist/seed/scammers-seed-data.js +75 -2
- package/dist/seed/site-settings-seed-data.js +5 -2
- package/dist/server-entry.d.ts +0 -1
- package/dist/server-entry.js +0 -1
- package/dist/server.d.ts +4 -20
- package/dist/server.js +10 -57
- package/dist/styles.css +1 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/BaseListingCard.js +1 -1
- package/dist/ui/components/ImageLightbox.d.ts +9 -0
- package/dist/ui/components/ImageLightbox.js +6 -3
- package/dist/ui/components/ListingToolbar.d.ts +18 -1
- package/dist/ui/components/ListingToolbar.js +5 -2
- package/dist/ui/components/StickyToolbar.js +13 -8
- package/package.json +1 -1
|
@@ -12,6 +12,8 @@ const CLS_STOCK_OK = "bg-success-surface text-white";
|
|
|
12
12
|
const CLS_VIEW_BTN = "mt-2 w-full cursor-pointer rounded-md bg-violet-600 py-[var(--appkit-space-1-5)] text-center text-[length:var(--appkit-text-xs)] font-semibold text-white transition-colors hover:bg-violet-700 active:scale-[0.98]";
|
|
13
13
|
import { BaseListingCard, Div, Row, Span, Stack, Text, TextLink } from "../../../ui";
|
|
14
14
|
import { MediaImage } from "../../media/MediaImage";
|
|
15
|
+
import { computeBundleDiscount } from "../../../_internal/shared/features/categories/bundle-pricing";
|
|
16
|
+
import { BUNDLE_COPY } from "../../../_internal/shared/features/categories/bundle-copy";
|
|
15
17
|
const DEFAULT_LABELS = {
|
|
16
18
|
bundleBadge: "Bundle",
|
|
17
19
|
inStockBadge: "In stock",
|
|
@@ -34,6 +36,7 @@ export function MarketplaceBundleCard({ bundle, className = "", variant = "grid"
|
|
|
34
36
|
const stock = bundle.bundleStockStatus ?? "in_stock";
|
|
35
37
|
const cover = bundle.display?.coverImage;
|
|
36
38
|
const price = bundle.bundlePrice;
|
|
39
|
+
const discount = computeBundleDiscount(bundle.bundlePrice, bundle.bundleOriginalTotal);
|
|
37
40
|
const collageTiles = (bundle.bundleItemDetails ?? [])
|
|
38
41
|
.filter((d) => Boolean(d.imageURL))
|
|
39
42
|
.slice(0, 4);
|
|
@@ -46,13 +49,13 @@ export function MarketplaceBundleCard({ bundle, className = "", variant = "grid"
|
|
|
46
49
|
}
|
|
47
50
|
router.push(String(detailHref));
|
|
48
51
|
}, [detailHref, onNavigate, router]);
|
|
49
|
-
return (_jsxs(BaseListingCard, { isSelected: isSelected, variant: variant, className: className, onMouseDown: !isSelected ? longPress.onMouseDown : undefined, onMouseUp: !isSelected ? longPress.onMouseUp : undefined, onMouseLeave: !isSelected ? longPress.onMouseLeave : undefined, onTouchStart: !isSelected ? longPress.onTouchStart : undefined, onTouchEnd: !isSelected ? longPress.onTouchEnd : undefined, onTouchCancel: !isSelected ? longPress.onTouchCancel : undefined, children: [_jsxs(BaseListingCard.Hero, { aspect: "square", variant: variant, children: [_jsx(TextLink, { href: String(detailHref), className: "absolute inset-0 block", children: showCollage ? (_jsx(Div, { layout: "grid", className: `h-full w-full gap-[0.125rem] bg-[var(--appkit-color-surface-muted)] ${collageTiles.length === 2 ? "grid-cols-2 grid-rows-1" : "grid-cols-2 grid-rows-2"}`, children: collageTiles.map((tile, i) => (_jsxs(Div, { className: "relative overflow-hidden bg-[var(--appkit-color-surface-muted)]", children: [_jsx(MediaImage, { src: tile.imageURL, alt: tile.title ?? `${bundle.name} item ${i + 1}`, size: "card" }), i === collageTiles.length - 1 && overflow > 0 && (_jsxs(Row, { textWeight: "semibold", textSize: "sm", surface: "overlay-md", className: "absolute inset-0 text-white", align: "center", justify: "center", children: ["+", overflow] }))] }, `${tile.productId}-${i}`))) })) : cover ? (_jsx(Div, { className: "absolute inset-0 bg-[var(--appkit-color-surface-muted)]", children: _jsx(MediaImage, { src: cover, alt: bundle.name, size: "card" }) })) : (_jsx(Row, { className: "absolute inset-0 bg-[var(--appkit-color-surface-muted)]", align: "center", justify: "center", children: _jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "xs", children: "No image" }) })) }), _jsxs(Stack, { className: "absolute right-2 top-2", align: "end", gap: "xs", children: [_jsx(Span, { size: "xs", weight: "medium", className: CLS_BUNDLE_PILL, children: mergedLabels.bundleBadge }), _jsx(Span, { layout: "inline-flex", color: "inverse", className: `${stock === "out_of_stock" ? "bg-neutral-500 " : CLS_STOCK_OK}`, rounded: "full", padding: "pill-xs", size: "xs", weight: "medium", children: stock === "out_of_stock" ? mergedLabels.outOfStockBadge : mergedLabels.inStockBadge })] }), stock === "out_of_stock" && (_jsx(Row, { surface: "overlay-xs", className: "absolute inset-0", align: "center", justify: "center", children: _jsx(Span, { color: "inverse", size: "xs", weight: "bold", className: "bg-neutral-900/80 tracking-wider", rounded: "default", padding: "pill-md", transform: "uppercase", children: mergedLabels.outOfStockBadge }) })), onSelect && (_jsx(BaseListingCard.Checkbox, { selected: isSelected, onSelect: (event) => {
|
|
52
|
+
return (_jsxs(BaseListingCard, { isSelected: isSelected, variant: variant, className: className, onMouseDown: !isSelected ? longPress.onMouseDown : undefined, onMouseUp: !isSelected ? longPress.onMouseUp : undefined, onMouseLeave: !isSelected ? longPress.onMouseLeave : undefined, onTouchStart: !isSelected ? longPress.onTouchStart : undefined, onTouchEnd: !isSelected ? longPress.onTouchEnd : undefined, onTouchCancel: !isSelected ? longPress.onTouchCancel : undefined, children: [_jsxs(BaseListingCard.Hero, { aspect: "square", variant: variant, children: [_jsx(TextLink, { href: String(detailHref), className: "absolute inset-0 block", children: showCollage ? (_jsx(Div, { layout: "grid", className: `h-full w-full gap-[0.125rem] bg-[var(--appkit-color-surface-muted)] ${collageTiles.length === 2 ? "grid-cols-2 grid-rows-1" : "grid-cols-2 grid-rows-2"}`, children: collageTiles.map((tile, i) => (_jsxs(Div, { className: "relative overflow-hidden bg-[var(--appkit-color-surface-muted)]", children: [_jsx(MediaImage, { src: tile.imageURL, alt: tile.title ?? `${bundle.name} item ${i + 1}`, size: "card" }), i === collageTiles.length - 1 && overflow > 0 && (_jsxs(Row, { textWeight: "semibold", textSize: "sm", surface: "overlay-md", className: "absolute inset-0 text-white", align: "center", justify: "center", children: ["+", overflow] }))] }, `${tile.productId}-${i}`))) })) : cover ? (_jsx(Div, { className: "absolute inset-0 bg-[var(--appkit-color-surface-muted)]", children: _jsx(MediaImage, { src: cover, alt: bundle.name, size: "card" }) })) : (_jsx(Row, { className: "absolute inset-0 bg-[var(--appkit-color-surface-muted)]", align: "center", justify: "center", children: _jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "xs", children: "No image" }) })) }), discount && (_jsx(Span, { size: "xs", weight: "bold", color: "inverse", className: "absolute left-2 top-2 bg-success-surface", rounded: "full", padding: "pill-xs", children: BUNDLE_COPY.detail.discountBadge(discount.percent) })), _jsxs(Stack, { className: "absolute right-2 top-2", align: "end", gap: "xs", children: [_jsx(Span, { size: "xs", weight: "medium", className: CLS_BUNDLE_PILL, children: mergedLabels.bundleBadge }), _jsx(Span, { layout: "inline-flex", color: "inverse", className: `${stock === "out_of_stock" ? "bg-neutral-500 " : CLS_STOCK_OK}`, rounded: "full", padding: "pill-xs", size: "xs", weight: "medium", children: stock === "out_of_stock" ? mergedLabels.outOfStockBadge : mergedLabels.inStockBadge })] }), stock === "out_of_stock" && (_jsx(Row, { surface: "overlay-xs", className: "absolute inset-0", align: "center", justify: "center", children: _jsx(Span, { color: "inverse", size: "xs", weight: "bold", className: "bg-neutral-900/80 tracking-wider", rounded: "default", padding: "pill-md", transform: "uppercase", children: mergedLabels.outOfStockBadge }) })), onSelect && (_jsx(BaseListingCard.Checkbox, { selected: isSelected, onSelect: (event) => {
|
|
50
53
|
event.stopPropagation();
|
|
51
54
|
onSelect(bundle.id, !isSelected);
|
|
52
55
|
}, className: selectable || isSelected
|
|
53
56
|
? "opacity-100"
|
|
54
|
-
: "opacity-0 group-hover:opacity-100 transition-opacity" }))] }), _jsxs(BaseListingCard.Info, { variant: variant, children: [_jsx(TextLink, { href: String(detailHref), children: _jsx(Text, { size: "sm", weight: "medium", color: "primary", truncate: 2, children: bundle.name }) }), _jsxs(Row, { justify: "between", className: "mt-1", gap: "sm", children: [_jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: price
|
|
55
|
-
|
|
56
|
-
|
|
57
|
+
: "opacity-0 group-hover:opacity-100 transition-opacity" }))] }), _jsxs(BaseListingCard.Info, { variant: variant, children: [_jsx(TextLink, { href: String(detailHref), children: _jsx(Text, { size: "sm", weight: "medium", color: "primary", truncate: 2, children: bundle.name }) }), _jsxs(Row, { justify: "between", className: "mt-1", gap: "sm", wrap: true, children: [_jsxs(Row, { gap: "xs", align: "center", children: [_jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: price
|
|
58
|
+
? formatCurrency(price, getDefaultCurrency())
|
|
59
|
+
: "—" }), discount && (_jsx(Text, { size: "xs", color: "muted", className: "line-through", children: formatCurrency(discount.originalTotal, getDefaultCurrency()) }))] }), _jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "xs", children: mergedLabels.itemsLabel(memberCount) })] }), _jsx(Div, { className: CLS_VIEW_BTN, role: "button", tabIndex: 0, onClick: handleNavigate, onKeyDown: (e) => { if (e.key === "Enter" || e.key === " ")
|
|
57
60
|
handleNavigate(); }, children: "View Bundle" })] })] }));
|
|
58
61
|
}
|
|
@@ -112,7 +112,7 @@ export async function PrizeDrawDetailPageView({ id, initialPrizeDraw, currentUse
|
|
|
112
112
|
? String(ROUTES.PUBLIC.STORE_DETAIL(storeSlug))
|
|
113
113
|
: null;
|
|
114
114
|
const descriptionHtml = toDescriptionHtml(p.description);
|
|
115
|
-
return (_jsxs(Main, { children: [_jsx(HistoryTracker, { productId: String(p.id ?? p.slug ?? ""), productType: "
|
|
115
|
+
return (_jsxs(Main, { children: [_jsx(HistoryTracker, { productId: String(p.id ?? p.slug ?? ""), productType: "prize-draw", snapshot: {
|
|
116
116
|
title,
|
|
117
117
|
thumb,
|
|
118
118
|
price: pricePerEntry,
|
|
@@ -8,14 +8,14 @@ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
|
8
8
|
* estimatedValue, 1–2 images per item, optional video, and a locked-overlay
|
|
9
9
|
* for items already won during a previous reveal.
|
|
10
10
|
*
|
|
11
|
-
* Min
|
|
11
|
+
* Min 2 items, max 16 (mirrors PrizeDrawItem schema constraints).
|
|
12
12
|
*/
|
|
13
13
|
import { useCallback } from "react";
|
|
14
14
|
import { Button, Div, FormField, Heading, Input, Row, Span, Stack, Text } from "../../../ui";
|
|
15
15
|
const CLS_WARN_BOX = "rounded border border-warning/40 bg-warning-surface px-[var(--appkit-space-3)] py-[var(--appkit-space-2)] text-[length:var(--appkit-text-sm)] text-warning dark:bg-warning-surface dark:text-warning";
|
|
16
16
|
const CLS_WON_BADGE = "rounded bg-error-surface px-[var(--appkit-space-3)] py-[var(--appkit-space-1)] text-[length:var(--appkit-text-xs)] font-semibold uppercase tracking-wide text-white";
|
|
17
17
|
import { ImageUpload } from "../../media";
|
|
18
|
-
const MIN_ITEMS =
|
|
18
|
+
const MIN_ITEMS = 2;
|
|
19
19
|
const MAX_ITEMS = 16;
|
|
20
20
|
const MAX_IMAGES_PER_ITEM = 2;
|
|
21
21
|
const CONDITION_OPTIONS = [
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface PrizeDrawWinnerMappingRow {
|
|
2
|
+
itemNumber: number;
|
|
3
|
+
title: string;
|
|
4
|
+
image?: string;
|
|
5
|
+
isWon: boolean;
|
|
6
|
+
wonByOrderId?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface PrizeDrawWinnerMappingViewProps {
|
|
9
|
+
title?: string;
|
|
10
|
+
items: PrizeDrawWinnerMappingRow[];
|
|
11
|
+
isAdmin: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare function PrizeDrawWinnerMappingView({ title, items, isAdmin, }: PrizeDrawWinnerMappingViewProps): import("react").JSX.Element;
|
|
14
|
+
export default PrizeDrawWinnerMappingView;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* PrizeDrawWinnerMappingView
|
|
5
|
+
*
|
|
6
|
+
* Read-only item→order mapping for a classic reveal-mode prize draw. Shown
|
|
7
|
+
* only to admin and the owning seller (never publicly) so they know which
|
|
8
|
+
* physical item to ship for which order. Mirrors LotteryEntriesView's choice
|
|
9
|
+
* of a small bounded DataTable (≤16 rows per draw) rather than a paginated
|
|
10
|
+
* DataListingView.
|
|
11
|
+
*/
|
|
12
|
+
import { Badge, Container, DataTable, Div, Heading, Row, Section, Stack, Text, TextLink } from "../../../ui";
|
|
13
|
+
import { MediaImage } from "../../media/MediaImage";
|
|
14
|
+
import { ROUTES } from "../../../next";
|
|
15
|
+
export function PrizeDrawWinnerMappingView({ title = "Prize Draw — Winner Mapping", items, isAdmin, }) {
|
|
16
|
+
const columns = [
|
|
17
|
+
{
|
|
18
|
+
key: "itemNumber",
|
|
19
|
+
header: "Item",
|
|
20
|
+
render: (row) => (_jsxs(Row, { gap: "xs", align: "center", children: [row.image ? (_jsx(Div, { className: "relative h-10 w-10 shrink-0", overflow: "hidden", rounded: "default", children: _jsx(MediaImage, { src: row.image, alt: row.title, size: "thumbnail" }) })) : null, _jsxs(Text, { size: "sm", weight: "medium", children: ["#", row.itemNumber, " \u2014 ", row.title] })] })),
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
key: "isWon",
|
|
24
|
+
header: "Status",
|
|
25
|
+
render: (row) => row.isWon ? (_jsx(Badge, { variant: "success", size: "sm", children: "Won" })) : (_jsx(Badge, { variant: "secondary", size: "sm", children: "Unclaimed" })),
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
key: "wonByOrderId",
|
|
29
|
+
header: "Order",
|
|
30
|
+
render: (row) => row.wonByOrderId ? (_jsx(TextLink, { href: String(isAdmin
|
|
31
|
+
? ROUTES.ADMIN.ORDER_DETAIL(row.wonByOrderId)
|
|
32
|
+
: ROUTES.STORE.ORDER_DETAIL(row.wonByOrderId)), children: row.wonByOrderId })) : (_jsx(Text, { size: "sm", color: "muted", children: "\u2014" })),
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
return (_jsx(Container, { children: _jsx(Section, { children: _jsxs(Stack, { gap: "lg", children: [_jsx(Heading, { level: 1, size: "2xl", weight: "bold", children: title }), _jsx(Text, { color: "muted", children: "Visible only to you \u2014 buyers never see which item went to which order. Use this to ship the correct physical item to the correct order." }), _jsx(DataTable, { data: items, columns: columns, keyExtractor: (row) => String(row.itemNumber), emptyMessage: "No prize items." })] }) }) }));
|
|
36
|
+
}
|
|
37
|
+
export default PrizeDrawWinnerMappingView;
|
|
@@ -1,34 +1,21 @@
|
|
|
1
1
|
import type { PrizeDrawItem } from "../schemas/firestore";
|
|
2
|
-
export interface
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
estimatedValue?: number;
|
|
8
|
-
};
|
|
9
|
-
alreadyRevealed?: boolean;
|
|
10
|
-
rngSourceUrl?: string;
|
|
11
|
-
refunded?: true;
|
|
12
|
-
reason?: string;
|
|
2
|
+
export interface PrizeRevealResult {
|
|
3
|
+
itemNumber: number;
|
|
4
|
+
title: string;
|
|
5
|
+
images: string[];
|
|
6
|
+
estimatedValue?: number;
|
|
13
7
|
}
|
|
14
8
|
export interface PrizeRevealModalProps {
|
|
15
9
|
open: boolean;
|
|
16
10
|
onClose: () => void;
|
|
17
11
|
/** Items to render in the collage (won state hidden — buyers don't see prior wins). */
|
|
18
12
|
items: PrizeDrawItem[];
|
|
19
|
-
/**
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
|
|
23
|
-
/** Override the default `/api/prize-draws/[id]/reveal` POST call. */
|
|
24
|
-
onReveal?: (args: {
|
|
25
|
-
orderId: string;
|
|
26
|
-
productId: string;
|
|
27
|
-
}) => Promise<PrizeRevealResponse>;
|
|
28
|
-
/** Already-revealed prize, passed in if the order has `prizeWon` populated. */
|
|
29
|
-
initialPrizeWon?: PrizeRevealResponse["prizeWon"];
|
|
13
|
+
/** Already-assigned prize, from `order.prizeWon`. Undefined while still pending. */
|
|
14
|
+
initialPrizeWon?: PrizeRevealResult;
|
|
15
|
+
/** Drives the pending-state copy — "instant" vs "scheduled" draws explain differently. */
|
|
16
|
+
revealMode?: "instant" | "scheduled";
|
|
30
17
|
/** Public proof-of-fairness URL — shown in the disclaimer. */
|
|
31
18
|
rngSourceUrl?: string;
|
|
32
19
|
}
|
|
33
|
-
export declare function PrizeRevealModal({ open, onClose, items,
|
|
20
|
+
export declare function PrizeRevealModal({ open, onClose, items, initialPrizeWon, revealMode, rngSourceUrl, }: PrizeRevealModalProps): import("react").JSX.Element;
|
|
34
21
|
export default PrizeRevealModal;
|
|
@@ -1,160 +1,31 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
/**
|
|
4
|
-
* PrizeRevealModal (SB4-I)
|
|
4
|
+
* PrizeRevealModal (SB4-I, redesigned for fully-automatic reveal)
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* locked in by the server before the first frame rendered.
|
|
12
|
-
*
|
|
13
|
-
* A permanent disclaimer below the collage explains exactly that — the RNG
|
|
14
|
-
* is `crypto.randomInt`, the source code is on GitHub, and neither the store
|
|
15
|
-
* nor LetItRip admins can influence the outcome.
|
|
6
|
+
* Displays a prize-draw order's reveal state. Reveal is no longer a buyer
|
|
7
|
+
* action — the winner is assigned automatically by assignPrizeDrawWinner(),
|
|
8
|
+
* either the moment payment is confirmed ("instant" mode) or by a scheduled
|
|
9
|
+
* Firebase Function at draw expiry/sellout ("scheduled" mode). This modal is
|
|
10
|
+
* a pure display: "pending" (no result yet) or "won" (order.prizeWon is set).
|
|
16
11
|
*/
|
|
17
|
-
import {
|
|
18
|
-
import { Anchor, Button, Div, Heading, LoginRequiredModal, Modal, Span, Stack, Text, useToast } from "../../../ui";
|
|
12
|
+
import { Anchor, Div, Heading, Modal, Span, Stack, Text } from "../../../ui";
|
|
19
13
|
import { MediaImage } from "../../media/MediaImage";
|
|
20
|
-
import { isAuthError } from "../../../utils/auth-error";
|
|
21
14
|
import { PrizeDrawCollage } from "./PrizeDrawCollage";
|
|
22
|
-
import { PRIZE_DRAW_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
23
|
-
import { normalizeError } from "../../../errors/normalize";
|
|
24
15
|
const __P = {
|
|
25
16
|
p4: "p-[var(--appkit-space-4)]",
|
|
26
17
|
};
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
function
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
async function fetchPrizeReveal(productId, orderId) {
|
|
35
|
-
const res = await fetch(PRIZE_DRAW_ENDPOINTS.REVEAL(productId), {
|
|
36
|
-
method: "POST",
|
|
37
|
-
headers: { "Content-Type": "application/json" },
|
|
38
|
-
body: JSON.stringify({ orderId }),
|
|
39
|
-
});
|
|
40
|
-
const json = (await res.json());
|
|
41
|
-
if (!res.ok) {
|
|
42
|
-
if (isUnauthenticatedResponse(res.status)) {
|
|
43
|
-
return { response: null, unauthenticated: true };
|
|
44
|
-
}
|
|
45
|
-
throw new Error("Reveal request failed");
|
|
46
|
-
}
|
|
47
|
-
return { response: json.data ?? {}, unauthenticated: false };
|
|
48
|
-
}
|
|
49
|
-
export function PrizeRevealModal({ open, onClose, items, orderId, productId, onReveal, initialPrizeWon, rngSourceUrl, }) {
|
|
50
|
-
const [phase, setPhase] = useState(initialPrizeWon ? "won" : "idle");
|
|
51
|
-
const [highlight, setHighlight] = useState(undefined);
|
|
52
|
-
const [winner, setWinner] = useState(initialPrizeWon);
|
|
53
|
-
const [errorMessage, setErrorMessage] = useState("");
|
|
54
|
-
const [effectiveRngUrl, setEffectiveRngUrl] = useState(rngSourceUrl);
|
|
55
|
-
const [showLoginModal, setShowLoginModal] = useState(false);
|
|
56
|
-
const { showToast } = useToast();
|
|
57
|
-
const cycleTimerRef = useRef(null);
|
|
58
|
-
const endTimerRef = useRef(null);
|
|
59
|
-
// Clean up timers on close/unmount so we don't leak between sessions.
|
|
60
|
-
useEffect(() => {
|
|
61
|
-
if (!open) {
|
|
62
|
-
if (cycleTimerRef.current)
|
|
63
|
-
clearTimeout(cycleTimerRef.current);
|
|
64
|
-
if (endTimerRef.current)
|
|
65
|
-
clearTimeout(endTimerRef.current);
|
|
66
|
-
cycleTimerRef.current = null;
|
|
67
|
-
endTimerRef.current = null;
|
|
68
|
-
}
|
|
69
|
-
return () => {
|
|
70
|
-
if (cycleTimerRef.current)
|
|
71
|
-
clearTimeout(cycleTimerRef.current);
|
|
72
|
-
if (endTimerRef.current)
|
|
73
|
-
clearTimeout(endTimerRef.current);
|
|
74
|
-
};
|
|
75
|
-
}, [open]);
|
|
76
|
-
const startAnimation = useCallback((winningItemNumber) => {
|
|
77
|
-
const startedAt = Date.now();
|
|
78
|
-
const tile = (n) => items[n % items.length]?.itemNumber ?? winningItemNumber;
|
|
79
|
-
let step = 0;
|
|
80
|
-
const tick = () => {
|
|
81
|
-
const elapsed = Date.now() - startedAt;
|
|
82
|
-
const remaining = REVEAL_DURATION_MS - elapsed;
|
|
83
|
-
if (remaining <= 0) {
|
|
84
|
-
setHighlight(winningItemNumber);
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
// Decelerate: as we approach the end, the interval stretches.
|
|
88
|
-
const progress = elapsed / REVEAL_DURATION_MS;
|
|
89
|
-
const interval = CYCLE_INTERVAL_FAST +
|
|
90
|
-
(CYCLE_INTERVAL_SLOW - CYCLE_INTERVAL_FAST) * progress;
|
|
91
|
-
step += 1;
|
|
92
|
-
setHighlight(tile(step));
|
|
93
|
-
cycleTimerRef.current = setTimeout(tick, interval);
|
|
94
|
-
};
|
|
95
|
-
tick();
|
|
96
|
-
}, [items]);
|
|
97
|
-
const handleRevealClick = useCallback(async () => {
|
|
98
|
-
if (phase !== "idle")
|
|
99
|
-
return;
|
|
100
|
-
setPhase("revealing");
|
|
101
|
-
setErrorMessage("");
|
|
102
|
-
let response;
|
|
103
|
-
try {
|
|
104
|
-
if (onReveal) {
|
|
105
|
-
response = await onReveal({ orderId, productId });
|
|
106
|
-
}
|
|
107
|
-
else {
|
|
108
|
-
const result = await fetchPrizeReveal(productId, orderId);
|
|
109
|
-
if (result.unauthenticated) {
|
|
110
|
-
setPhase("idle");
|
|
111
|
-
setShowLoginModal(true);
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
response = result.response ?? {};
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
catch (err) {
|
|
118
|
-
void normalizeError(err);
|
|
119
|
-
if (isAuthError(err)) {
|
|
120
|
-
setPhase("idle");
|
|
121
|
-
setShowLoginModal(true);
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
setPhase("error");
|
|
125
|
-
const msg = err instanceof Error ? err.message : "Reveal request failed";
|
|
126
|
-
setErrorMessage(msg);
|
|
127
|
-
showToast(msg, "error");
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
if (response.refunded) {
|
|
131
|
-
setPhase("refunded");
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
if (!response.prizeWon) {
|
|
135
|
-
setPhase("error");
|
|
136
|
-
setErrorMessage("Unexpected reveal response");
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
if (response.rngSourceUrl)
|
|
140
|
-
setEffectiveRngUrl(response.rngSourceUrl);
|
|
141
|
-
setWinner(response.prizeWon);
|
|
142
|
-
if (response.alreadyRevealed) {
|
|
143
|
-
// No theatrics for re-opens — just show the prize.
|
|
144
|
-
setHighlight(response.prizeWon.itemNumber);
|
|
145
|
-
setPhase("won");
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
startAnimation(response.prizeWon.itemNumber);
|
|
149
|
-
endTimerRef.current = setTimeout(() => {
|
|
150
|
-
setPhase("won");
|
|
151
|
-
}, REVEAL_DURATION_MS);
|
|
152
|
-
}, [onReveal, orderId, phase, productId, startAnimation]);
|
|
18
|
+
const PENDING_COPY = {
|
|
19
|
+
instant: "Your prize is being assigned now that payment is confirmed — refresh in a moment.",
|
|
20
|
+
scheduled: "This draw reveals automatically once it closes (at its expiry date) or sells out — check back here after.",
|
|
21
|
+
};
|
|
22
|
+
export function PrizeRevealModal({ open, onClose, items, initialPrizeWon, revealMode = "scheduled", rngSourceUrl, }) {
|
|
23
|
+
const winner = initialPrizeWon;
|
|
153
24
|
const winnerImg = winner?.images?.[0];
|
|
154
|
-
return (
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
25
|
+
return (_jsx(Modal, { isOpen: open, onClose: onClose, title: "Prize Reveal", size: "lg", children: _jsxs(Stack, { gap: "md", children: [_jsx(PrizeDrawCollage, { items: items, hideWonState: true }), !winner ? (
|
|
26
|
+
// audit-content-alignment-ok: informational reveal-status panel, not marketing content
|
|
27
|
+
_jsxs(Div, { className: `bg-[var(--appkit-color-surface-muted)] ${__P.p4} text-center`, rounded: "default", children: [_jsx(Text, { size: "lg", weight: "semibold", children: "Reveal pending" }), _jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: PENDING_COPY[revealMode] })] })) : (
|
|
28
|
+
// audit-content-alignment-ok: theatrical reveal-modal centerpiece, not marketing content
|
|
29
|
+
_jsxs(Div, { className: `border-2 border-[var(--appkit-color-primary)] bg-[var(--appkit-color-surface)] ${__P.p4} text-center`, rounded: "lg", children: [_jsx(Text, { className: "tracking-wider text-[var(--appkit-color-text-muted)]", size: "xs", transform: "uppercase", children: "You won" }), _jsxs(Heading, { level: 2, className: "my-2", children: ["#", winner.itemNumber, " \u2014 ", winner.title] }), winnerImg ? (_jsx(Div, { className: "relative mx-auto h-64 w-64 max-w-full", rounded: "md", overflow: "hidden", children: _jsx(MediaImage, { src: winnerImg, alt: winner.title, size: "hero", objectFit: "contain" }) })) : null, winner.estimatedValue != null ? (_jsxs(Text, { className: "mt-2 text-[var(--appkit-color-text-muted)]", size: "sm", children: ["Estimated value: \u20B9", winner.estimatedValue.toLocaleString("en-IN")] })) : null] })), _jsxs(Div, { textSize: "xs", className: "border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface-muted)] text-[var(--appkit-color-text-muted)]", padding: "inlineSm", rounded: "default", children: [_jsx(Span, { weight: "bold", children: "Fairness guarantee:" }), " ", "Winners are picked by ", _jsx("code", { children: "crypto.randomInt" }), " running on LetItRip's server the moment your prize is assigned. Neither the store nor LetItRip staff can influence the outcome.", " ", rngSourceUrl ? (_jsx(Anchor, { href: rngSourceUrl, tone: "none", underline: "always", children: "View RNG source code \u2192" })) : null] })] }) }));
|
|
159
30
|
}
|
|
160
31
|
export default PrizeRevealModal;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import Link from "next/link";
|
|
3
|
+
import { SIEVE_OP, sieveAnd, sieveFilter, sortBy } from "@mohasinac/appkit";
|
|
3
4
|
import { productRepository, reviewRepository } from "../../../repositories";
|
|
4
5
|
const __P = {
|
|
5
6
|
p3: "p-[var(--appkit-space-3)]",
|
|
@@ -125,6 +126,7 @@ export async function ProductDetailPageView({ slug, initialProduct, renderOfferA
|
|
|
125
126
|
: typeof p.mainImage === "string"
|
|
126
127
|
? [p.mainImage]
|
|
127
128
|
: [];
|
|
129
|
+
const productVideo = p.video;
|
|
128
130
|
const stockQuantity = typeof p.stockQuantity === "number" ? p.stockQuantity : null;
|
|
129
131
|
const availableQuantity = typeof p.availableQuantity === "number"
|
|
130
132
|
? p.availableQuantity
|
|
@@ -139,6 +141,10 @@ export async function ProductDetailPageView({ slug, initialProduct, renderOfferA
|
|
|
139
141
|
const categorySlugs = Array.isArray(p.categorySlugs)
|
|
140
142
|
? p.categorySlugs
|
|
141
143
|
: category ? [category] : [];
|
|
144
|
+
// The real category FK — `category` above is only the deprecated display-name
|
|
145
|
+
// cache (SB-UNI drift, Root Cause #24-class bug: don't route links/queries
|
|
146
|
+
// through it as if it were a slug).
|
|
147
|
+
const primaryCategorySlug = categorySlugs[0] ?? null;
|
|
142
148
|
const categoryNames = Array.isArray(p.categoryNames)
|
|
143
149
|
? p.categoryNames
|
|
144
150
|
: categoryName ? [categoryName] : [];
|
|
@@ -201,49 +207,82 @@ export async function ProductDetailPageView({ slug, initialProduct, renderOfferA
|
|
|
201
207
|
const groupId = typeof p.groupId === "string" ? p.groupId : null;
|
|
202
208
|
const isGroupParent = p.isGroupParent === true;
|
|
203
209
|
const groupTitle = typeof p.groupTitle === "string" ? p.groupTitle : undefined;
|
|
204
|
-
|
|
205
|
-
|
|
210
|
+
const storeId = typeof p.storeId === "string" ? p.storeId : null;
|
|
211
|
+
// -- Fetch reviews + 4 related-listing signals in parallel ------------------
|
|
212
|
+
const [reviewDocs, sameCategoryDocs, sameBrandDocs, tagOverlapDocs, sameStoreDocs] = await Promise.all([
|
|
206
213
|
reviewRepository
|
|
207
214
|
.findApprovedByProduct(product.id)
|
|
208
215
|
.catch(() => []),
|
|
209
|
-
|
|
210
|
-
? productRepository.findByCategory(
|
|
216
|
+
primaryCategorySlug
|
|
217
|
+
? productRepository.findByCategory(primaryCategorySlug).catch(() => [])
|
|
218
|
+
: Promise.resolve([]),
|
|
219
|
+
brand
|
|
220
|
+
? productRepository
|
|
221
|
+
.list({
|
|
222
|
+
filters: sieveAnd(sieveFilter("status", SIEVE_OP.EQ, "published"), sieveFilter("brand", SIEVE_OP.EQ, brand)),
|
|
223
|
+
sorts: sortBy("createdAt", "DESC"),
|
|
224
|
+
page: 1,
|
|
225
|
+
pageSize: 9,
|
|
226
|
+
})
|
|
227
|
+
.then((r) => r.items)
|
|
228
|
+
.catch(() => [])
|
|
229
|
+
: Promise.resolve([]),
|
|
230
|
+
tags.length > 0
|
|
231
|
+
? productRepository.findByTagsOverlap(tags, 9).catch(() => [])
|
|
232
|
+
: Promise.resolve([]),
|
|
233
|
+
storeId
|
|
234
|
+
? productRepository
|
|
235
|
+
.list({
|
|
236
|
+
filters: sieveAnd(sieveFilter("status", SIEVE_OP.EQ, "published"), sieveFilter("storeId", SIEVE_OP.EQ, storeId)),
|
|
237
|
+
sorts: sortBy("createdAt", "DESC"),
|
|
238
|
+
page: 1,
|
|
239
|
+
pageSize: 9,
|
|
240
|
+
})
|
|
241
|
+
.then((r) => r.items)
|
|
242
|
+
.catch(() => [])
|
|
211
243
|
: Promise.resolve([]),
|
|
212
244
|
]);
|
|
213
245
|
const reviews = reviewDocs.map(toReview);
|
|
214
246
|
const _now = new Date();
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
return false;
|
|
224
|
-
if (r.availableQuantity === 0)
|
|
225
|
-
return false;
|
|
226
|
-
if (r.listingType === "auction" && r.auctionEndDate) {
|
|
227
|
-
const end = r.auctionEndDate;
|
|
228
|
-
const endDate = typeof end.toDate === "function"
|
|
229
|
-
? end.toDate()
|
|
230
|
-
: end instanceof Date ? end : new Date(String(end));
|
|
231
|
-
if (endDate <= _now)
|
|
247
|
+
const currentProductId = product.id;
|
|
248
|
+
function toRelatedItems(docs) {
|
|
249
|
+
return docs
|
|
250
|
+
.filter((r) => {
|
|
251
|
+
if (r.id === currentProductId)
|
|
252
|
+
return false;
|
|
253
|
+
const s = r.status;
|
|
254
|
+
if (s && ["sold", "out_of_stock", "archived", "discontinued", "draft"].includes(s))
|
|
232
255
|
return false;
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
256
|
+
if (r.isSold === true)
|
|
257
|
+
return false;
|
|
258
|
+
if (r.availableQuantity === 0)
|
|
259
|
+
return false;
|
|
260
|
+
if (r.listingType === "auction" && r.auctionEndDate) {
|
|
261
|
+
const end = r.auctionEndDate;
|
|
262
|
+
const endDate = typeof end.toDate === "function"
|
|
263
|
+
? end.toDate()
|
|
264
|
+
: end instanceof Date ? end : new Date(String(end));
|
|
265
|
+
if (endDate <= _now)
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
if (r.listingType === "prize-draw" && r.prizeRevealStatus === "closed")
|
|
269
|
+
return false;
|
|
270
|
+
return true;
|
|
271
|
+
})
|
|
272
|
+
.slice(0, 8)
|
|
273
|
+
.map(toProductItem);
|
|
274
|
+
}
|
|
275
|
+
const relatedItems = toRelatedItems(sameCategoryDocs);
|
|
276
|
+
const relatedByBrand = toRelatedItems(sameBrandDocs);
|
|
277
|
+
const relatedByTags = toRelatedItems(tagOverlapDocs);
|
|
278
|
+
const relatedByStore = toRelatedItems(sameStoreDocs);
|
|
240
279
|
return (_jsxs(Main, { children: [_jsx(HistoryTracker, { productId: product.id, productType: "product", snapshot: {
|
|
241
280
|
title: product.title,
|
|
242
281
|
thumb: product.mainImage ?? product.images?.[0],
|
|
243
282
|
price: product.price,
|
|
244
283
|
storeId: product.storeId,
|
|
245
284
|
storeName: product.storeName,
|
|
246
|
-
} }), _jsxs(Container, { size: "xl", padding: "y-lg", children: [_jsx(ProductDetailView, { renderBreadcrumb: () => (_jsxs(Row, { className: "mb-4", align: "center", justify: "between", gap: "sm", wrap: true, children: [_jsxs(Nav, { "aria-label": "Breadcrumb", layout: "flex-wrap", gap: "2xs", textSize: "xs", color: "muted", children: [_jsx(Link, { href: String(ROUTES.HOME), className: CLS_BREADCRUMB_LINK, children: "Home" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.PRODUCTS), className: CLS_BREADCRUMB_LINK, children: "Products" }), category && (_jsxs(_Fragment, { children: [_jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category)), className: CLS_BREADCRUMB_LINK, children: categoryName || category })] })), subcategory && (_jsxs(_Fragment, { children: [_jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Span, { transform: "capitalize", children: subcategory })] }))] }), _jsx(ShareButton, { title: title || undefined })] })), renderGallery: () => (_jsx(ProductGalleryClient, { images: images, productName: title || undefined })), renderInfo: () => (_jsxs(Stack, { gap: "md", children: [_jsxs(Div, { children: [condition && (_jsx(Span, { size: "xs", weight: "medium", className: "mb-2 inline-block", padding: "pill-sm", rounded: "full", surface: "subtle", color: "muted", transform: "capitalize", children: condition === "new"
|
|
285
|
+
} }), _jsxs(Container, { size: "xl", padding: "y-lg", children: [_jsx(ProductDetailView, { renderBreadcrumb: () => (_jsxs(Row, { className: "mb-4", align: "center", justify: "between", gap: "sm", wrap: true, children: [_jsxs(Nav, { "aria-label": "Breadcrumb", layout: "flex-wrap", gap: "2xs", textSize: "xs", color: "muted", children: [_jsx(Link, { href: String(ROUTES.HOME), className: CLS_BREADCRUMB_LINK, children: "Home" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.PRODUCTS), className: CLS_BREADCRUMB_LINK, children: "Products" }), (primaryCategorySlug || category) && (_jsxs(_Fragment, { children: [_jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(primaryCategorySlug ?? category)), className: CLS_BREADCRUMB_LINK, children: categoryName || category })] })), subcategory && (_jsxs(_Fragment, { children: [_jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Span, { transform: "capitalize", children: subcategory })] }))] }), _jsx(ShareButton, { title: title || undefined })] })), renderGallery: () => (_jsx(ProductGalleryClient, { images: images, video: productVideo, productName: title || undefined })), renderInfo: () => (_jsxs(Stack, { gap: "md", children: [_jsxs(Div, { children: [condition && (_jsx(Span, { size: "xs", weight: "medium", className: "mb-2 inline-block", padding: "pill-sm", rounded: "full", surface: "subtle", color: "muted", transform: "capitalize", children: condition === "new"
|
|
247
286
|
? "Brand New"
|
|
248
287
|
: condition === "like_new"
|
|
249
288
|
? "Like New"
|
|
@@ -302,7 +341,10 @@ export async function ProductDetailPageView({ slug, initialProduct, renderOfferA
|
|
|
302
341
|
content: _jsx(CustomSectionTabContent, { section: s }),
|
|
303
342
|
})) })), renderBundleSection: bundleMemberships.length > 0
|
|
304
343
|
? () => (_jsxs(Div, { className: CLS_BUNDLE_BOX, children: [_jsxs(Row, { className: "mb-3", align: "center", gap: "sm", children: [_jsx(Span, { size: "base", className: CLS_BUNDLE_ICON, "aria-hidden": "true", children: "\uD83D\uDCE6" }), _jsxs(Text, { className: CLS_BUNDLE_TITLE, children: ["Part of ", bundleMemberships.length === 1 ? "a bundle" : `${bundleMemberships.length} bundles`] })] }), _jsx(Stack, { gap: "3", children: bundleMemberships.map((b) => (_jsxs(Div, { className: CLS_BUNDLE_ROW, children: [_jsxs(Div, { className: "min-w-0", children: [_jsx(Text, { className: CLS_BUNDLE_LABEL, children: "Included in bundle" }), _jsx(Text, { className: "truncate", color: "primary", size: "sm", weight: "semibold", children: b.title })] }), _jsx(Link, { href: String(ROUTES.PUBLIC.BUNDLE_DETAIL(b.id)), className: CLS_BUNDLE_CTA, children: "View Bundle \u2192" })] }, b.id))) })] }))
|
|
305
|
-
: undefined, renderRelated: () => relatedItems.length > 0
|
|
344
|
+
: undefined, renderRelated: () => relatedItems.length > 0 ||
|
|
345
|
+
relatedByBrand.length > 0 ||
|
|
346
|
+
relatedByTags.length > 0 ||
|
|
347
|
+
relatedByStore.length > 0 ? (_jsxs(Stack, { gap: "xl", children: [relatedItems.length > 0 && (_jsx(RelatedProductsCarousel, { items: relatedItems, title: categoryName || category ? `More in ${categoryName || category}` : "More in this category" })), relatedByBrand.length > 0 && (_jsx(RelatedProductsCarousel, { items: relatedByBrand, title: brand ? `More by ${brand}` : "More by this brand" })), relatedByTags.length > 0 && (_jsx(RelatedProductsCarousel, { items: relatedByTags, title: "You might also like" })), relatedByStore.length > 0 && (_jsx(RelatedProductsCarousel, { items: relatedByStore, title: storeName ? `More from ${storeName}` : "More from this seller" }))] })) : null }), renderPrimaryActions
|
|
306
348
|
? renderPrimaryActions({
|
|
307
349
|
productId: product.id,
|
|
308
350
|
productSlug: product.slug ?? slug,
|
|
@@ -14,6 +14,11 @@ import { normalizeRichTextHtml } from "../../../utils/string.formatter";
|
|
|
14
14
|
import { useListingTypeFlags } from "../../../react/hooks/useListingTypeFlags";
|
|
15
15
|
import { isAuctionListing, isPreOrderListing, isPrizeDrawListing, normalizeListingType, } from "../utils/listing-type";
|
|
16
16
|
import { PrizeDrawItemsEditor } from "./PrizeDrawItemsEditor";
|
|
17
|
+
import { PRIZE_DRAW_DURATION_DAYS_MIN, PRIZE_DRAW_DURATION_DAYS_MAX, } from "../../../_internal/shared/checkout/rules/_limits";
|
|
18
|
+
const PRIZE_REVEAL_MODE_OPTIONS = [
|
|
19
|
+
{ value: "instant", label: "Reveal immediately on purchase" },
|
|
20
|
+
{ value: "scheduled", label: "Reveal at expiry or sellout" },
|
|
21
|
+
];
|
|
17
22
|
// ListingType uses the canonical SB1-G spellings ("standard", "pre-order");
|
|
18
23
|
// ProductFeatureProductType predates it with legacy spellings for 2 values.
|
|
19
24
|
// Replaces the old 2-case ternary that silently mapped every other type
|
|
@@ -174,23 +179,11 @@ export function ProductForm({ product, onChange, isReadonly = false, renderDescr
|
|
|
174
179
|
const anyWon = prizeItems.some((it) => it.isWon);
|
|
175
180
|
const lockedForReveal = anyWon;
|
|
176
181
|
const fieldDisabled = isReadonly || lockedForReveal;
|
|
177
|
-
return (_jsxs(_Fragment, { children: [lockedForReveal ? (_jsx(Alert, { variant: "error", title: "Listing locked", children: "At least one prize has been revealed for this draw \u2014 the listing and its fields are now frozen. To rerun a similar draw, clone it into a new prize-draw listing." })) : null, _jsx(Alert, { variant: "warning", title: "Non-refundable", children: "Prize-draw entries are non-refundable once
|
|
182
|
+
return (_jsxs(_Fragment, { children: [lockedForReveal ? (_jsx(Alert, { variant: "error", title: "Listing locked", children: "At least one prize has been revealed for this draw \u2014 the listing and its fields are now frozen. To rerun a similar draw, clone it into a new prize-draw listing." })) : null, _jsx(Alert, { variant: "warning", title: "Non-refundable", children: "Prize-draw entries are non-refundable once purchased. Buyers see and must accept this notice before paying." }), _jsxs(FormGroup, { columns: 2, children: [_jsx(FormField, { name: "pricePerEntry", label: "Price per entry (\u20B9)", type: "number", value: product.pricePerEntry != null
|
|
178
183
|
? String(product.pricePerEntry)
|
|
179
184
|
: "", onChange: (value) => update({
|
|
180
185
|
pricePerEntry: Math.round((parseFloat(value) || 0) * 100) / 100,
|
|
181
|
-
}), disabled: fieldDisabled, placeholder: "299" }), _jsx(FormField, { name: "prizeMaxEntries", label: "Max entries (pool size)", type: "number", value: String(product.prizeMaxEntries ?? ""), onChange: (value) => update({ prizeMaxEntries: Number(value) || undefined }), disabled: fieldDisabled, placeholder: "100" })] }), _jsxs(FormGroup, { columns: 2, children: [_jsx(FormField, { name: "
|
|
182
|
-
const d = resolveDate(product.prizeRevealWindowStart);
|
|
183
|
-
if (!d)
|
|
184
|
-
return "";
|
|
185
|
-
return d.toISOString().slice(0, 16);
|
|
186
|
-
})(), onChange: (value) => update({ prizeRevealWindowStart: value }), disabled: fieldDisabled }), _jsx(FormField, { name: "prizeRevealWindowEnd", label: "Reveal window end", type: "datetime-local", value: (() => {
|
|
187
|
-
const d = resolveDate(product.prizeRevealWindowEnd);
|
|
188
|
-
if (!d)
|
|
189
|
-
return "";
|
|
190
|
-
return d.toISOString().slice(0, 16);
|
|
191
|
-
})(), onChange: (value) => update({ prizeRevealWindowEnd: value }), disabled: fieldDisabled })] }), _jsxs(FormGroup, { columns: 2, children: [_jsx(FormField, { name: "prizeRevealDeadlineDays", label: "Reveal deadline (days)", type: "number", value: String(product.prizeRevealDeadlineDays ?? 3), onChange: (value) => update({
|
|
192
|
-
prizeRevealDeadlineDays: Number(value) || 3,
|
|
193
|
-
}), disabled: fieldDisabled, placeholder: "3", helpText: "After window opens, buyers have this many days to claim." }), _jsx(FormField, { name: "maxPerUser", label: "Max entries per customer (blank = unlimited)", type: "number", value: String(product.maxPerUser ?? ""), onChange: (value) => update({ maxPerUser: Number(value) || undefined }), disabled: fieldDisabled, placeholder: "" })] }), product.prizeGithubFileUrl ? (_jsx(FormField, { name: "prizeGithubFileUrl", label: "RNG Source Code URL (read-only)", type: "text", value: product.prizeGithubFileUrl, onChange: () => { }, disabled: true, helpText: "Auto-set on first save. Public proof-of-fairness link." })) : null, _jsx(PrizeDrawItemsEditor, { items: (product.prizeDrawItems ?? []), onChange: (items) => update({ prizeDrawItems: items }), onUploadImage: (file) =>
|
|
186
|
+
}), disabled: fieldDisabled, placeholder: "299" }), _jsx(FormField, { name: "prizeMaxEntries", label: "Max entries (pool size)", type: "number", value: String(product.prizeMaxEntries ?? ""), onChange: (value) => update({ prizeMaxEntries: Number(value) || undefined }), disabled: fieldDisabled, placeholder: "100" })] }), _jsxs(FormGroup, { columns: 2, children: [_jsx(FormField, { name: "prizeDrawDurationDays", label: "Draw duration (days)", type: "number", value: String(product.prizeDrawDurationDays ?? ""), onChange: (value) => update({ prizeDrawDurationDays: Number(value) || undefined }), disabled: fieldDisabled, placeholder: "7", helpText: `Draw runs this many days from publish, or until sold out — whichever comes first. ${PRIZE_DRAW_DURATION_DAYS_MIN}–${PRIZE_DRAW_DURATION_DAYS_MAX} days.` }), _jsx(FormField, { name: "prizeRevealMode", label: "Reveal mode", type: "select", value: product.prizeRevealMode ?? "scheduled", onChange: (value) => update({ prizeRevealMode: value }), disabled: fieldDisabled, options: PRIZE_REVEAL_MODE_OPTIONS })] }), _jsxs(FormGroup, { columns: 2, children: [_jsx(FormField, { name: "maxPerUser", label: "Max entries per customer (blank = unlimited)", type: "number", value: String(product.maxPerUser ?? ""), onChange: (value) => update({ maxPerUser: Number(value) || undefined }), disabled: fieldDisabled, placeholder: "" }), _jsx(Div, {})] }), product.prizeGithubFileUrl ? (_jsx(FormField, { name: "prizeGithubFileUrl", label: "RNG Source Code URL (read-only)", type: "text", value: product.prizeGithubFileUrl, onChange: () => { }, disabled: true, helpText: "Auto-set on first save. Public proof-of-fairness link." })) : null, _jsx(PrizeDrawItemsEditor, { items: (product.prizeDrawItems ?? []), onChange: (items) => update({ prizeDrawItems: items }), onUploadImage: (file) =>
|
|
194
187
|
// Caller is expected to wire a real uploader by overriding
|
|
195
188
|
// renderPrizeDrawImageUpload — fall back to data URL preview
|
|
196
189
|
// so the editor still functions in unwired admin contexts.
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
+
/** Product video field shape — mirrors `ProductVideoField` from the products schema. */
|
|
3
|
+
export interface ProductGalleryVideo {
|
|
4
|
+
/** Raw, unwrapped playable video URL — never routed through the (image-only) media proxy. */
|
|
5
|
+
url: string;
|
|
6
|
+
/** Poster frame — a normal image, already resolved/proxied by the caller (e.g. via seedExtMedia). */
|
|
7
|
+
thumbnailUrl?: string;
|
|
8
|
+
}
|
|
2
9
|
export interface ProductGalleryClientProps {
|
|
3
10
|
images: string[];
|
|
11
|
+
/** Optional product video — rendered as a trailing slide/thumbnail; opens in theater mode (lightbox) on click. */
|
|
12
|
+
video?: ProductGalleryVideo;
|
|
4
13
|
productName?: string;
|
|
5
14
|
}
|
|
6
|
-
export declare function ProductGalleryClient({ images, productName }: ProductGalleryClientProps): React.JSX.Element;
|
|
15
|
+
export declare function ProductGalleryClient({ images, video, productName }: ProductGalleryClientProps): React.JSX.Element;
|