@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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useState } from "react";
|
|
4
|
+
import { Play } from "lucide-react";
|
|
4
5
|
import { ImageLightbox } from "../../../ui/components/ImageLightbox";
|
|
5
6
|
import { Button, Div, Row, Stack, Text } from "../../../ui";
|
|
6
7
|
import { MediaImage } from "../../media/MediaImage";
|
|
@@ -9,23 +10,41 @@ const __O = {
|
|
|
9
10
|
xAuto: "overflow-x-auto",
|
|
10
11
|
};
|
|
11
12
|
const PlaceholderSvg = () => (_jsxs("svg", { width: "64", height: "64", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.5", "aria-hidden": "true", children: [_jsx("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2" }), _jsx("circle", { cx: "8.5", cy: "8.5", r: "1.5" }), _jsx("path", { d: "M21 15l-5-5L5 21" })] }));
|
|
12
|
-
export function ProductGalleryClient({ images, productName }) {
|
|
13
|
+
export function ProductGalleryClient({ images, video, productName }) {
|
|
13
14
|
const [activeIndex, setActiveIndex] = useState(0);
|
|
14
15
|
const [lightboxIndex, setLightboxIndex] = useState(null);
|
|
15
|
-
const
|
|
16
|
-
src,
|
|
17
|
-
|
|
16
|
+
const slides = [
|
|
17
|
+
...images.map((src) => ({ kind: "image", src })),
|
|
18
|
+
...(video ? [{ kind: "video", src: video.url, poster: video.thumbnailUrl }] : []),
|
|
19
|
+
];
|
|
20
|
+
const lightboxImages = slides.map((slide, i) => ({
|
|
21
|
+
src: slide.src,
|
|
22
|
+
alt: productName
|
|
23
|
+
? `${productName} — ${slide.kind === "video" ? "video" : `image ${i + 1}`}`
|
|
24
|
+
: slide.kind === "video"
|
|
25
|
+
? "Product video"
|
|
26
|
+
: `Product image ${i + 1}`,
|
|
27
|
+
kind: slide.kind,
|
|
28
|
+
poster: slide.kind === "video" ? slide.poster : undefined,
|
|
18
29
|
}));
|
|
19
|
-
if (
|
|
30
|
+
if (slides.length === 0) {
|
|
20
31
|
return (_jsx(Div, { className: `${__O.hidden}`, border: "subtle", rounded: "xl", surface: "muted", children: _jsx(Row, { className: "aspect-square text-[var(--appkit-color-border)] dark:text-[var(--appkit-color-border-subtle)]", align: "center", justify: "center", children: _jsx(PlaceholderSvg, {}) }) }));
|
|
21
32
|
}
|
|
22
|
-
const
|
|
23
|
-
return (_jsxs(Stack, { gap: "3", children: [
|
|
33
|
+
const activeSlide = slides[activeIndex] ?? slides[0];
|
|
34
|
+
return (_jsxs(Stack, { gap: "3", children: [_jsxs(Div, { rounded: "xl", className: "overflow-hidden border border-[var(--appkit-color-border-subtle)] cursor-zoom-in aspect-square w-full relative", onClick: () => setLightboxIndex(activeIndex), role: "button", tabIndex: 0, onKeyDown: (e) => {
|
|
24
35
|
if (e.key === "Enter" || e.key === " ")
|
|
25
36
|
setLightboxIndex(activeIndex);
|
|
26
|
-
}, "aria-label":
|
|
37
|
+
}, "aria-label": activeSlide.kind === "video"
|
|
38
|
+
? `Play ${productName ?? "product"} video`
|
|
39
|
+
: `View ${productName ?? "product"} image in full screen`, children: [_jsx(MediaImage, { src: activeSlide.kind === "video" ? activeSlide.poster : activeSlide.src, alt: activeSlide.kind === "video"
|
|
40
|
+
? productName
|
|
41
|
+
? `${productName} — video`
|
|
42
|
+
: "Product video"
|
|
43
|
+
: productName
|
|
44
|
+
? `${productName} — image ${activeIndex + 1}`
|
|
45
|
+
: `Product image ${activeIndex + 1}`, size: "gallery", className: "transition-transform duration-300 hover:scale-105" }), activeSlide.kind === "video" && (_jsx(Row, { className: "absolute inset-0 pointer-events-none", align: "center", justify: "center", children: _jsx(Row, { className: "h-16 w-16 rounded-full bg-black/50 text-white", align: "center", justify: "center", children: _jsx(Play, { className: "h-7 w-7 fill-current", "aria-hidden": "true" }) }) }))] }), slides.length > 1 && (_jsx(Div, { layout: "flex", gap: "2", className: `${__O.xAuto}`, padding: "b-2xs", children: slides.map((slide, i) => (_jsxs(Button, { variant: "ghost", type: "button", onClick: () => setActiveIndex(i), rounded: "lg", paddingX: "none", paddingY: "none", className: `relative flex-shrink-0 h-16 w-16 overflow-hidden border-2 transition-all${i === activeIndex
|
|
27
46
|
? " border-primary-500"
|
|
28
|
-
: " border-transparent opacity-60 hover:opacity-100"}`, "aria-label": `View image ${i + 1}`, "aria-current": i === activeIndex ? "true" : undefined, children: _jsx(MediaImage, { src: src, alt: `Thumbnail ${i + 1}`, size: "thumbnail" }) }, i))) })),
|
|
47
|
+
: " border-transparent opacity-60 hover:opacity-100"}`, "aria-label": slide.kind === "video" ? "View video" : `View image ${i + 1}`, "aria-current": i === activeIndex ? "true" : undefined, children: [_jsx(MediaImage, { src: slide.kind === "video" ? slide.poster : slide.src, alt: slide.kind === "video" ? "Video thumbnail" : `Thumbnail ${i + 1}`, size: "thumbnail" }), slide.kind === "video" && (_jsx(Row, { className: "absolute inset-0 pointer-events-none bg-black/25", align: "center", justify: "center", children: _jsx(Play, { className: "h-5 w-5 text-white fill-current", "aria-hidden": "true" }) }))] }, i))) })), slides.length > 1 && (
|
|
29
48
|
// audit-content-alignment-ok: functional image-counter label under the gallery, not page content
|
|
30
|
-
_jsxs(Text, { size: "xs", align: "center", color: "faint", children: [activeIndex + 1, " / ",
|
|
49
|
+
_jsxs(Text, { size: "xs", align: "center", color: "faint", children: [activeIndex + 1, " / ", slides.length] })), _jsx(ImageLightbox, { images: lightboxImages, activeIndex: lightboxIndex, onClose: () => setLightboxIndex(null), onNavigate: (i) => setLightboxIndex(i) })] }));
|
|
31
50
|
}
|
|
@@ -11,6 +11,7 @@ import { useAuthGate } from "../../../react/hooks/useAuthGate";
|
|
|
11
11
|
import { ACTION_ID, ACTION_META, COMPARE_MAX_ITEMS } from "../constants/action-defs";
|
|
12
12
|
import { CompareOverlay } from "./CompareOverlay";
|
|
13
13
|
import { ROUTES } from "../../../next";
|
|
14
|
+
import { pluginFor } from "../../../_internal/shared/listing-types/_registry";
|
|
14
15
|
import { ProductGrid, ProductFilters, PRODUCT_PUBLIC_SORT_OPTIONS } from ".";
|
|
15
16
|
import { useGuestCart } from "../../cart/hooks/useGuestCart";
|
|
16
17
|
import { useGuestWishlist } from "../../wishlist/hooks/useGuestWishlist";
|
|
@@ -241,7 +242,7 @@ export function ProductsIndexListing({ initialData, listingTypes = GENERIC_PRODU
|
|
|
241
242
|
setCompareIds(ids);
|
|
242
243
|
},
|
|
243
244
|
},
|
|
244
|
-
] }), totalPages > 1 && (_jsx(StickyToolbar, { offset: "header+pagination", tone: "translucent", border: true, padding: "toolbar", children: _jsx(Row, { justify: "center", children: _jsx(Pagination, { currentPage: page, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) }) })), _jsx(Div, { padding: "y-lg", children: isLoading ? (_jsx(Div, { layout: "grid", gap: "4", className: "grid-cols-2 sm:grid-cols-3 lg:grid-cols-4", children: Array.from({ length: 10 }).map((_, i) => (_jsxs(Div, { className: `${__O.hidden} animate-pulse`, border: "subtle", rounded: "xl", children: [_jsx(Div, { className: "aspect-square", surface: "subtle" }), _jsxs(Stack, { className: `${__P.p3}`, gap: "sm", children: [_jsx(Div, { className: "h-3 w-3/4", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-3 w-1/2", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-4 w-1/3", surface: "subtle", rounded: "default" })] })] }, i))) })) : (_jsx(ProductGrid, { products: products, getProductHref: (p) =>
|
|
245
|
+
] }), totalPages > 1 && (_jsx(StickyToolbar, { offset: "header+pagination", tone: "translucent", border: true, padding: "toolbar", children: _jsx(Row, { justify: "center", children: _jsx(Pagination, { currentPage: page, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) }) })), _jsx(Div, { padding: "y-lg", children: isLoading ? (_jsx(Div, { layout: "grid", gap: "4", className: "grid-cols-2 sm:grid-cols-3 lg:grid-cols-4", children: Array.from({ length: 10 }).map((_, i) => (_jsxs(Div, { className: `${__O.hidden} animate-pulse`, border: "subtle", rounded: "xl", children: [_jsx(Div, { className: "aspect-square", surface: "subtle" }), _jsxs(Stack, { className: `${__P.p3}`, gap: "sm", children: [_jsx(Div, { className: "h-3 w-3/4", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-3 w-1/2", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-4 w-1/3", surface: "subtle", rounded: "default" })] })] }, i))) })) : (_jsx(ProductGrid, { products: products, getProductHref: (p) => pluginFor(p.listingType ?? "standard").detailRoute(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: () => {
|
|
245
246
|
setCompareIds([]);
|
|
246
247
|
selection.clearSelection();
|
|
247
248
|
}, onRemove: (id) => setCompareIds((ids) => ids.filter((i) => i !== id)) }), _jsx(FilterDrawer, { open: filterOpen, onOpen: openFilters, onClose: () => setFilterOpen(false), onApply: applyFilters, onReset: onFilterClear, activeCount: filterActiveCount, hideTrigger: true, children: _jsx(ProductFilters, { table: pendingTable, currencyPrefix: "\u20B9", categoryOptions: categoryOptions, brandOptions: brandOptions, featureOptions: featureOptions }) }), _jsx(LoginRequiredModal, { isOpen: modalOpen, onClose: closeModal, message: modalMessage })] }));
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
-
import {
|
|
3
|
+
import { pluginFor } from "../../../_internal/shared/listing-types/_registry";
|
|
4
4
|
import { SectionCarousel } from "../../homepage/components/SectionCarousel";
|
|
5
5
|
import { ProductCard } from "./ProductGrid";
|
|
6
6
|
import { CAROUSEL_PER_VIEW } from "../../homepage/constants/carousel-per-view";
|
|
7
7
|
export function RelatedProductsCarousel({ items, title = "You might also like", }) {
|
|
8
8
|
if (items.length === 0)
|
|
9
9
|
return null;
|
|
10
|
-
return (_jsx(SectionCarousel, { title: title, pillLabel: "Related Products", headingVariant: "editorial", items: items, isLoading: false, perView: CAROUSEL_PER_VIEW.standard, keyExtractor: (item) => item.id, renderItem: (item) => (_jsx(ProductCard, { product: item, href: item.slug || item.id ?
|
|
10
|
+
return (_jsx(SectionCarousel, { title: title, pillLabel: "Related Products", headingVariant: "editorial", items: items, isLoading: false, perView: CAROUSEL_PER_VIEW.standard, keyExtractor: (item) => item.id, renderItem: (item) => (_jsx(ProductCard, { product: item, href: item.slug || item.id ? pluginFor(item.listingType ?? "standard").detailRoute(item.slug || item.id) : undefined })) }));
|
|
11
11
|
}
|
|
@@ -28,7 +28,7 @@ function GroupTableRow({ member }) {
|
|
|
28
28
|
const href = memberHref(member);
|
|
29
29
|
const price = formatCurrency(member.price, member.currency ?? "INR");
|
|
30
30
|
const image = member.images?.[0] ?? "";
|
|
31
|
-
return (_jsxs(Tr, { className: "last:border-0", border: "subtle", children: [_jsx(Td, { paddingSide: "pr-sm", padding: "xs-tall", children: _jsx(Div, { className: `w-10 h-10 ${__O.hidden}`, rounded: "full", border: "default", children: _jsx(MediaImage, { src: image, alt: member.title, size: "thumbnail" }) }) }), _jsxs(Td, { paddingSide: "pr-sm", padding: "xs-tall", children: [_jsx(Text, { className: "line-clamp-2", color: "primary", size: "sm", weight: "medium", children: member.title }), member.isGroupParent && (_jsx(Span, { weight: "semibold", className: "text-[10px] text-[var(--appkit-color-primary)]", children: "Parent" }))] }), _jsx(Td, { paddingSide: "pr-sm", padding: "xs-tall", children: _jsx(Text, { size: "sm", color: "muted", children: price }) }), _jsx(Td, { paddingSide: "pr-sm", padding: "xs-tall", children: _jsx(Text, { size: "xs", transform: "capitalize", color: "muted", children: member.condition ?? "—" }) }), _jsx(Td, { padding: "xs-tall", children: _jsx(Link, { href: href, className: "text-[length:var(--appkit-text-xs)] text-[var(--appkit-color-primary)] hover:underline", children: "View \
|
|
31
|
+
return (_jsxs(Tr, { className: "last:border-0", border: "subtle", children: [_jsx(Td, { paddingSide: "pr-sm", padding: "xs-tall", children: _jsx(Div, { className: `w-10 h-10 ${__O.hidden}`, rounded: "full", border: "default", children: _jsx(MediaImage, { src: image, alt: member.title, size: "thumbnail" }) }) }), _jsxs(Td, { paddingSide: "pr-sm", padding: "xs-tall", children: [_jsx(Text, { className: "line-clamp-2", color: "primary", size: "sm", weight: "medium", children: member.title }), member.isGroupParent && (_jsx(Span, { weight: "semibold", className: "text-[10px] text-[var(--appkit-color-primary)]", children: "Parent" }))] }), _jsx(Td, { paddingSide: "pr-sm", padding: "xs-tall", children: _jsx(Text, { size: "sm", color: "muted", children: price }) }), _jsx(Td, { paddingSide: "pr-sm", padding: "xs-tall", children: _jsx(Text, { size: "xs", transform: "capitalize", color: "muted", children: member.condition ?? "—" }) }), _jsx(Td, { padding: "xs-tall", children: _jsx(Link, { href: href, className: "text-[length:var(--appkit-text-xs)] text-[var(--appkit-color-primary)] hover:underline", children: "View \u2192" }) })] }));
|
|
32
32
|
}
|
|
33
33
|
export function ShowGroupSection({ groupId, currentSlug, isParent, groupTitle }) {
|
|
34
34
|
const [open, setOpen] = useState(false);
|
|
@@ -54,7 +54,7 @@ export function ShowGroupSection({ groupId, currentSlug, isParent, groupTitle })
|
|
|
54
54
|
return (_jsxs(_Fragment, { children: [_jsxs(Div, { className: `${__O.hidden}`, surface: "muted", rounded: "xl", border: "default", children: [_jsxs(Row, { role: "button", tabIndex: 0, onClick: () => setOpen((v) => !v), onKeyDown: (e) => { if (e.key === "Enter" || e.key === " ") {
|
|
55
55
|
e.preventDefault();
|
|
56
56
|
setOpen((v) => !v);
|
|
57
|
-
} }, justify: "between", paddingX: "x-md", paddingY: "y-sm", className: "w-full cursor-pointer text-left hover:bg-[var(--appkit-color-surface-elevated)]/70 transition-colors", "aria-expanded": open, children: [_jsxs(Row, { align: "center", gap: "xs", children: [_jsx(Span, { size: "xs", className: "mr-1", color: "faint", children: open ? "
|
|
57
|
+
} }, justify: "between", paddingX: "x-md", paddingY: "y-sm", className: "w-full cursor-pointer text-left hover:bg-[var(--appkit-color-surface-elevated)]/70 transition-colors", "aria-expanded": open, children: [_jsxs(Row, { align: "center", gap: "xs", children: [_jsx(Span, { size: "xs", className: "mr-1", color: "faint", children: open ? "▼" : "▶" }), _jsx(Text, { size: "sm", weight: "medium", color: "primary", children: parentLabel }), _jsx(Span, { size: "xs", className: "ml-1", rounded: "full", padding: "pill-xs", surface: "subtle", color: "muted", children: members.length })] }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: (e) => { e.stopPropagation(); setShowAll(true); }, className: "text-[length:var(--appkit-text-xs)] text-[var(--appkit-color-primary)] hover:underline ml-3 flex-shrink-0", children: "View whole group \u2192" })] }), open && (_jsx(Div, { className: `pb-[1rem] pt-[0.25rem] ${__O.xAuto}`, padding: "x-md", children: _jsx(Row, { gap: "3", className: "min-w-0", children: [...members]
|
|
58
58
|
.sort((a, b) => (b.isGroupParent ? 1 : 0) - (a.isGroupParent ? 1 : 0))
|
|
59
59
|
.map((m) => (_jsx(MemberThumb, { member: m, isCurrent: m.slug === currentSlug || m.id === currentSlug }, m.id))) }) }))] }), useDrawer ? (_jsx(SideDrawer, { isOpen: showAll, onClose: () => setShowAll(false), title: label, children: tableContent })) : (_jsx(Modal, { open: showAll, onClose: () => setShowAll(false), title: label, children: tableContent }))] }));
|
|
60
60
|
}
|
|
@@ -54,5 +54,5 @@ export function SublistingCarouselSection({ sublistingCategoryId, currentListing
|
|
|
54
54
|
return (_jsxs(Div, { className: `${__O.hidden}`, surface: "muted", rounded: "xl", border: "default", children: [_jsxs(Row, { role: "button", tabIndex: 0, onClick: () => setOpen((v) => !v), onKeyDown: (e) => { if (e.key === "Enter" || e.key === " ") {
|
|
55
55
|
e.preventDefault();
|
|
56
56
|
setOpen((v) => !v);
|
|
57
|
-
} }, justify: "between", paddingX: "x-md", paddingY: "y-sm", className: "w-full cursor-pointer text-left hover:bg-[var(--appkit-color-surface-elevated)]/70 transition-colors", "aria-expanded": open, children: [_jsxs(Row, { align: "center", gap: "xs", children: [_jsx(Span, { size: "xs", className: "mr-1", color: "faint", children: open ? "â–¼" : "â–¶" }), _jsxs(Text, { size: "sm", weight: "medium", color: "primary", children: ["More listings like this:", " ", _jsx(Span, { className: "text-[var(--appkit-color-primary)]", children: label })] }), _jsx(Span, { size: "xs", className: "ml-1", rounded: "full", padding: "pill-xs", surface: "subtle", color: "muted", children: listings.length })] }), category && (_jsx(Link, { href: String(ROUTES.PUBLIC.SUBLISTING_CATEGORY(category.slug)), onClick: (e) => e.stopPropagation(), className: "text-[length:var(--appkit-text-xs)] text-[var(--appkit-color-primary)] hover:underline ml-3 flex-shrink-0", children: "View all \
|
|
57
|
+
} }, justify: "between", paddingX: "x-md", paddingY: "y-sm", className: "w-full cursor-pointer text-left hover:bg-[var(--appkit-color-surface-elevated)]/70 transition-colors", "aria-expanded": open, children: [_jsxs(Row, { align: "center", gap: "xs", children: [_jsx(Span, { size: "xs", className: "mr-1", color: "faint", children: open ? "â–¼" : "â–¶" }), _jsxs(Text, { size: "sm", weight: "medium", color: "primary", children: ["More listings like this:", " ", _jsx(Span, { className: "text-[var(--appkit-color-primary)]", children: label })] }), _jsx(Span, { size: "xs", className: "ml-1", rounded: "full", padding: "pill-xs", surface: "subtle", color: "muted", children: listings.length })] }), category && (_jsx(Link, { href: String(ROUTES.PUBLIC.SUBLISTING_CATEGORY(category.slug)), onClick: (e) => e.stopPropagation(), className: "text-[length:var(--appkit-text-xs)] text-[var(--appkit-color-primary)] hover:underline ml-3 flex-shrink-0", children: "View all \u2192" }))] }), open && (_jsx(Div, { className: `pb-[1rem] pt-[0.25rem] ${__O.xAuto}`, padding: "x-md", children: _jsx(Row, { gap: "3", className: "min-w-0", children: listings.map((listing) => (_jsx(ListingThumb, { listing: listing, isCurrent: listing.id === currentListingId }, listing.id))) }) }))] }));
|
|
58
58
|
}
|
|
@@ -12,7 +12,9 @@ export type { PrizeDrawItemsEditorProps } from "./PrizeDrawItemsEditor";
|
|
|
12
12
|
export { PrizeDrawCollage } from "./PrizeDrawCollage";
|
|
13
13
|
export type { PrizeDrawCollageProps } from "./PrizeDrawCollage";
|
|
14
14
|
export { PrizeRevealModal } from "./PrizeRevealModal";
|
|
15
|
-
export type { PrizeRevealModalProps,
|
|
15
|
+
export type { PrizeRevealModalProps, PrizeRevealResult, } from "./PrizeRevealModal";
|
|
16
|
+
export { PrizeDrawWinnerMappingView } from "./PrizeDrawWinnerMappingView";
|
|
17
|
+
export type { PrizeDrawWinnerMappingViewProps, PrizeDrawWinnerMappingRow, } from "./PrizeDrawWinnerMappingView";
|
|
16
18
|
export { MarketplacePrizeDrawCard } from "./MarketplacePrizeDrawCard";
|
|
17
19
|
export type { MarketplacePrizeDrawCardProps, MarketplacePrizeDrawCardData, MarketplacePrizeDrawCardLabels, } from "./MarketplacePrizeDrawCard";
|
|
18
20
|
export { MarketplaceBundleCard } from "./MarketplaceBundleCard";
|
|
@@ -51,7 +53,7 @@ export type { MakeOfferButtonProps } from "./MakeOfferButton";
|
|
|
51
53
|
export { RelatedProducts } from "./RelatedProducts";
|
|
52
54
|
export type { RelatedProductsProps } from "./RelatedProducts";
|
|
53
55
|
export { ProductGalleryClient } from "./ProductGalleryClient";
|
|
54
|
-
export type { ProductGalleryClientProps } from "./ProductGalleryClient";
|
|
56
|
+
export type { ProductGalleryClientProps, ProductGalleryVideo } from "./ProductGalleryClient";
|
|
55
57
|
export { ProductTabsShell } from "./ProductTabsShell";
|
|
56
58
|
export type { ProductTabsShellProps, CustomTabDef } from "./ProductTabsShell";
|
|
57
59
|
export { ProductGradingTab } from "./ProductGradingTab";
|
|
@@ -6,6 +6,7 @@ export { NonRefundableConsentModal } from "./NonRefundableConsentModal";
|
|
|
6
6
|
export { PrizeDrawItemsEditor } from "./PrizeDrawItemsEditor";
|
|
7
7
|
export { PrizeDrawCollage } from "./PrizeDrawCollage";
|
|
8
8
|
export { PrizeRevealModal } from "./PrizeRevealModal";
|
|
9
|
+
export { PrizeDrawWinnerMappingView } from "./PrizeDrawWinnerMappingView";
|
|
9
10
|
export { MarketplacePrizeDrawCard } from "./MarketplacePrizeDrawCard";
|
|
10
11
|
export { MarketplaceBundleCard } from "./MarketplaceBundleCard";
|
|
11
12
|
export { PrizeDrawsIndexListing } from "./PrizeDrawsIndexListing";
|
|
@@ -32,7 +32,6 @@ export declare const ACTION_ID: {
|
|
|
32
32
|
readonly CANCEL_RESERVATION: "cancel-reservation";
|
|
33
33
|
readonly CHECKOUT: "checkout";
|
|
34
34
|
readonly CONTINUE_TO_VERIFY: "continue-to-verification";
|
|
35
|
-
readonly SEND_OTP: "send-otp";
|
|
36
35
|
readonly VERIFY_OTP: "verify-otp";
|
|
37
36
|
readonly PAY_ONLINE: "pay-online";
|
|
38
37
|
readonly PAY_COD: "pay-cod";
|
|
@@ -53,7 +53,6 @@ export const ACTION_ID = {
|
|
|
53
53
|
// ── Checkout / navigation CTAs ───────────────────────────────────────────
|
|
54
54
|
CHECKOUT: "checkout",
|
|
55
55
|
CONTINUE_TO_VERIFY: "continue-to-verification",
|
|
56
|
-
SEND_OTP: "send-otp",
|
|
57
56
|
VERIFY_OTP: "verify-otp",
|
|
58
57
|
PAY_ONLINE: "pay-online",
|
|
59
58
|
PAY_COD: "pay-cod",
|
|
@@ -99,7 +98,6 @@ export const ACTION_META = {
|
|
|
99
98
|
// ── Checkout / navigation CTAs ────────────────────────────────────────────
|
|
100
99
|
[ACTION_ID.CHECKOUT]: { id: ACTION_ID.CHECKOUT, label: regLabel("CART", "checkout", "Checkout"), variant: "primary", requiresAuth: true, authMessage: "You need to be signed in to checkout." },
|
|
101
100
|
[ACTION_ID.CONTINUE_TO_VERIFY]: { id: ACTION_ID.CONTINUE_TO_VERIFY, label: regLabel("CHECKOUT", "continue-to-verification", "Continue"), variant: "primary", requiresAuth: true, authMessage: "You need to be signed in to checkout." },
|
|
102
|
-
[ACTION_ID.SEND_OTP]: { id: ACTION_ID.SEND_OTP, label: regLabel("CHECKOUT", "send-otp", "Send Code"), variant: "primary", requiresAuth: true, authMessage: "You need to be signed in to checkout." },
|
|
103
101
|
[ACTION_ID.VERIFY_OTP]: { id: ACTION_ID.VERIFY_OTP, label: regLabel("CHECKOUT", "verify-otp", "Verify & Continue"), variant: "primary", requiresAuth: true, authMessage: "You need to be signed in to checkout." },
|
|
104
102
|
[ACTION_ID.PAY_ONLINE]: { id: ACTION_ID.PAY_ONLINE, label: regLabel("CHECKOUT", "pay-online", "Pay Online"), variant: "primary", requiresAuth: true, authMessage: "You need to be signed in to checkout." },
|
|
105
103
|
[ACTION_ID.PAY_COD]: { id: ACTION_ID.PAY_COD, label: regLabel("CHECKOUT", "pay-cod", "Cash on Delivery"), variant: "secondary", requiresAuth: true, authMessage: "You need to be signed in to checkout." },
|
|
@@ -18,6 +18,12 @@ export declare class ProductRepository extends BaseRepository<ProductDocument> {
|
|
|
18
18
|
findByStatus(status: ProductStatus): Promise<ProductDocument[]>;
|
|
19
19
|
findFeatured(): Promise<ProductDocument[]>;
|
|
20
20
|
findByCategory(category: string): Promise<ProductDocument[]>;
|
|
21
|
+
/**
|
|
22
|
+
* Published products sharing at least one tag with the given list — "you
|
|
23
|
+
* might also like" style discovery. `array-contains-any` caps at 10 values,
|
|
24
|
+
* so only the first 10 tags are used.
|
|
25
|
+
*/
|
|
26
|
+
findByTagsOverlap(tags: string[], limit?: number): Promise<ProductDocument[]>;
|
|
21
27
|
findByBarcodeId(barcodeId: string): Promise<ProductDocument | null>;
|
|
22
28
|
findBySlug(slug: string): Promise<ProductDocument | undefined>;
|
|
23
29
|
findByIdOrSlug(idOrSlug: string): Promise<ProductDocument | undefined>;
|
|
@@ -145,6 +145,21 @@ export class ProductRepository extends BaseRepository {
|
|
|
145
145
|
.get();
|
|
146
146
|
return snap.docs.map((d) => this.mapDoc(d));
|
|
147
147
|
}
|
|
148
|
+
/**
|
|
149
|
+
* Published products sharing at least one tag with the given list — "you
|
|
150
|
+
* might also like" style discovery. `array-contains-any` caps at 10 values,
|
|
151
|
+
* so only the first 10 tags are used.
|
|
152
|
+
*/
|
|
153
|
+
async findByTagsOverlap(tags, limit = 8) {
|
|
154
|
+
if (tags.length === 0)
|
|
155
|
+
return [];
|
|
156
|
+
const snap = await this.getCollection()
|
|
157
|
+
.where(PRODUCT_FIELDS.STATUS, "==", ProductStatusValues.PUBLISHED)
|
|
158
|
+
.where(PRODUCT_FIELDS.TAGS, "array-contains-any", tags.slice(0, 10))
|
|
159
|
+
.limit(limit)
|
|
160
|
+
.get();
|
|
161
|
+
return snap.docs.map((d) => this.mapDoc(d));
|
|
162
|
+
}
|
|
148
163
|
async findByBarcodeId(barcodeId) {
|
|
149
164
|
const docs = await this.findBy("barcodeId", barcodeId);
|
|
150
165
|
return docs[0] ?? null;
|
|
@@ -226,10 +241,14 @@ export class ProductRepository extends BaseRepository {
|
|
|
226
241
|
return snapshot.docs.map((doc) => this.mapDoc(doc));
|
|
227
242
|
}
|
|
228
243
|
async findByGroupId(groupId) {
|
|
244
|
+
// Tier GP: groups may mix standard products + pre-orders (never auctions).
|
|
229
245
|
const snapshot = await this.db
|
|
230
246
|
.collection(this.collection)
|
|
231
247
|
.where("groupId", "==", groupId)
|
|
232
|
-
.where(PRODUCT_FIELDS.LISTING_TYPE, "
|
|
248
|
+
.where(PRODUCT_FIELDS.LISTING_TYPE, "in", [
|
|
249
|
+
LISTING_TYPE_VALUES.STANDARD,
|
|
250
|
+
LISTING_TYPE_VALUES.PRE_ORDER,
|
|
251
|
+
])
|
|
233
252
|
.get();
|
|
234
253
|
return snapshot.docs.map((doc) => this.mapDoc(doc));
|
|
235
254
|
}
|
|
@@ -263,18 +263,40 @@ export interface ProductDocument extends BaseDocument {
|
|
|
263
263
|
pricePerEntry?: number;
|
|
264
264
|
prizeMaxEntries?: number;
|
|
265
265
|
prizeCurrentEntries?: number;
|
|
266
|
+
/**
|
|
267
|
+
* Draw start, set once at creation (= createdAt) — never seller-edited.
|
|
268
|
+
* `prizeRevealWindowEnd` is derived from this + `prizeDrawDurationDays`.
|
|
269
|
+
*/
|
|
266
270
|
prizeRevealWindowStart?: Date;
|
|
271
|
+
/** Hard expiry — `prizeRevealWindowStart` + `prizeDrawDurationDays` days. */
|
|
267
272
|
prizeRevealWindowEnd?: Date;
|
|
273
|
+
/**
|
|
274
|
+
* "pending" is legacy-only (no longer written by new listings — reveal is
|
|
275
|
+
* fully automatic, there's no seller-facing delayed-start concept anymore;
|
|
276
|
+
* draft→published is how a seller holds a draw back). New listings go
|
|
277
|
+
* straight to "open"; "closed" once every eligible order has been revealed.
|
|
278
|
+
*/
|
|
268
279
|
prizeRevealStatus?: "pending" | "open" | "closed";
|
|
269
|
-
|
|
280
|
+
/**
|
|
281
|
+
* Seller-facing duration input, 1–15 days (PRIZE_DRAW_DURATION_DAYS_MIN/MAX
|
|
282
|
+
* in checkout/rules/_limits.ts). Drives `prizeRevealWindowEnd` at creation.
|
|
283
|
+
*/
|
|
284
|
+
prizeDrawDurationDays?: number;
|
|
270
285
|
/** Public proof-of-fairness file (commit-reveal scheme). */
|
|
271
286
|
prizeGithubFileUrl?: string;
|
|
272
287
|
/**
|
|
273
288
|
* Draw mode for prize-draw listings.
|
|
274
|
-
* "reveal" = classic
|
|
289
|
+
* "reveal" = classic automatic-reveal flow (default, backward-compat).
|
|
275
290
|
* "lottery" = user self-pull with weighted random slot assignment.
|
|
276
291
|
*/
|
|
277
292
|
prizeDrawMode?: "reveal" | "lottery";
|
|
293
|
+
/**
|
|
294
|
+
* Reveal trigger for "reveal" mode draws (irrelevant for "lottery" mode).
|
|
295
|
+
* "instant" — winner assigned the moment the order's payment is confirmed.
|
|
296
|
+
* "scheduled" — winner assigned automatically at whichever comes first:
|
|
297
|
+
* `prizeRevealWindowEnd` or full sellout (`prizeCurrentEntries === prizeMaxEntries`).
|
|
298
|
+
*/
|
|
299
|
+
prizeRevealMode?: "instant" | "scheduled";
|
|
278
300
|
/**
|
|
279
301
|
* Full lottery config. Only set when prizeDrawMode === "lottery".
|
|
280
302
|
* Includes server-only price/weight fields — stripped by adapter before any client response.
|
|
@@ -333,6 +355,8 @@ export interface PrizeDrawItem {
|
|
|
333
355
|
/** Per-slot price in decimal rupees for lottery mode. Lower price = higher weight = higher chance. */
|
|
334
356
|
prizeSlotPrice?: number;
|
|
335
357
|
isWon: boolean;
|
|
358
|
+
/** Order id this item was assigned to — set alongside isWon:true. Admin/seller-only mapping (never public) so the seller knows which physical item ships to which order. */
|
|
359
|
+
wonByOrderId?: string;
|
|
336
360
|
}
|
|
337
361
|
/** Runtime-accessible product status values — use instead of bare string literals. */
|
|
338
362
|
export declare const ProductStatusValues: {
|
|
@@ -149,6 +149,7 @@ export declare const productItemSchema: z.ZodObject<{
|
|
|
149
149
|
condition: z.ZodString;
|
|
150
150
|
estimatedValue: z.ZodOptional<z.ZodNumber>;
|
|
151
151
|
isWon: z.ZodBoolean;
|
|
152
|
+
wonByOrderId: z.ZodOptional<z.ZodString>;
|
|
152
153
|
}, "strip", z.ZodTypeAny, {
|
|
153
154
|
title: string;
|
|
154
155
|
images: string[];
|
|
@@ -161,6 +162,7 @@ export declare const productItemSchema: z.ZodObject<{
|
|
|
161
162
|
} | undefined;
|
|
162
163
|
description?: string | undefined;
|
|
163
164
|
estimatedValue?: number | undefined;
|
|
165
|
+
wonByOrderId?: string | undefined;
|
|
164
166
|
}, {
|
|
165
167
|
title: string;
|
|
166
168
|
images: string[];
|
|
@@ -173,6 +175,7 @@ export declare const productItemSchema: z.ZodObject<{
|
|
|
173
175
|
} | undefined;
|
|
174
176
|
description?: string | undefined;
|
|
175
177
|
estimatedValue?: number | undefined;
|
|
178
|
+
wonByOrderId?: string | undefined;
|
|
176
179
|
}>, "many">>;
|
|
177
180
|
pricePerEntry: z.ZodOptional<z.ZodNumber>;
|
|
178
181
|
prizeMaxEntries: z.ZodOptional<z.ZodNumber>;
|
|
@@ -180,7 +183,8 @@ export declare const productItemSchema: z.ZodObject<{
|
|
|
180
183
|
prizeRevealWindowStart: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodDate]>>;
|
|
181
184
|
prizeRevealWindowEnd: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodDate]>>;
|
|
182
185
|
prizeRevealStatus: z.ZodOptional<z.ZodEnum<["pending", "open", "closed"]>>;
|
|
183
|
-
|
|
186
|
+
prizeDrawDurationDays: z.ZodOptional<z.ZodNumber>;
|
|
187
|
+
prizeRevealMode: z.ZodOptional<z.ZodEnum<["instant", "scheduled"]>>;
|
|
184
188
|
prizeGithubFileUrl: z.ZodOptional<z.ZodString>;
|
|
185
189
|
inStock: z.ZodOptional<z.ZodBoolean>;
|
|
186
190
|
stockCount: z.ZodOptional<z.ZodNumber>;
|
|
@@ -423,6 +427,7 @@ export declare const productItemSchema: z.ZodObject<{
|
|
|
423
427
|
} | undefined;
|
|
424
428
|
description?: string | undefined;
|
|
425
429
|
estimatedValue?: number | undefined;
|
|
430
|
+
wonByOrderId?: string | undefined;
|
|
426
431
|
}[] | undefined;
|
|
427
432
|
pricePerEntry?: number | undefined;
|
|
428
433
|
prizeMaxEntries?: number | undefined;
|
|
@@ -430,8 +435,9 @@ export declare const productItemSchema: z.ZodObject<{
|
|
|
430
435
|
prizeRevealWindowStart?: string | Date | undefined;
|
|
431
436
|
prizeRevealWindowEnd?: string | Date | undefined;
|
|
432
437
|
prizeRevealStatus?: "closed" | "open" | "pending" | undefined;
|
|
433
|
-
|
|
438
|
+
prizeDrawDurationDays?: number | undefined;
|
|
434
439
|
prizeGithubFileUrl?: string | undefined;
|
|
440
|
+
prizeRevealMode?: "instant" | "scheduled" | undefined;
|
|
435
441
|
media?: {
|
|
436
442
|
type: "video" | "image" | "file";
|
|
437
443
|
url: string;
|
|
@@ -559,6 +565,7 @@ export declare const productItemSchema: z.ZodObject<{
|
|
|
559
565
|
} | undefined;
|
|
560
566
|
description?: string | undefined;
|
|
561
567
|
estimatedValue?: number | undefined;
|
|
568
|
+
wonByOrderId?: string | undefined;
|
|
562
569
|
}[] | undefined;
|
|
563
570
|
pricePerEntry?: number | undefined;
|
|
564
571
|
prizeMaxEntries?: number | undefined;
|
|
@@ -566,8 +573,9 @@ export declare const productItemSchema: z.ZodObject<{
|
|
|
566
573
|
prizeRevealWindowStart?: string | Date | undefined;
|
|
567
574
|
prizeRevealWindowEnd?: string | Date | undefined;
|
|
568
575
|
prizeRevealStatus?: "closed" | "open" | "pending" | undefined;
|
|
569
|
-
|
|
576
|
+
prizeDrawDurationDays?: number | undefined;
|
|
570
577
|
prizeGithubFileUrl?: string | undefined;
|
|
578
|
+
prizeRevealMode?: "instant" | "scheduled" | undefined;
|
|
571
579
|
media?: {
|
|
572
580
|
type: "video" | "image" | "file";
|
|
573
581
|
url: string;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export * from "./firestore";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { mediaFieldSchema } from "../../media/types/index";
|
|
4
|
+
import { PRIZE_DRAW_DURATION_DAYS_MIN, PRIZE_DRAW_DURATION_DAYS_MAX, } from "../../../_internal/shared/checkout/rules/_limits";
|
|
4
5
|
// --- Sub-schemas --------------------------------------------------------------
|
|
5
6
|
export const productImageSchema = z.object({
|
|
6
7
|
url: z.string(),
|
|
@@ -96,8 +97,9 @@ export const productItemSchema = z.object({
|
|
|
96
97
|
condition: z.string(),
|
|
97
98
|
estimatedValue: z.number().optional(),
|
|
98
99
|
isWon: z.boolean(),
|
|
100
|
+
wonByOrderId: z.string().optional(),
|
|
99
101
|
}))
|
|
100
|
-
.min(
|
|
102
|
+
.min(2)
|
|
101
103
|
.max(16)
|
|
102
104
|
.optional(),
|
|
103
105
|
pricePerEntry: z.number().positive().optional(),
|
|
@@ -106,7 +108,13 @@ export const productItemSchema = z.object({
|
|
|
106
108
|
prizeRevealWindowStart: z.union([z.string(), z.date()]).optional(),
|
|
107
109
|
prizeRevealWindowEnd: z.union([z.string(), z.date()]).optional(),
|
|
108
110
|
prizeRevealStatus: z.enum(["pending", "open", "closed"]).optional(),
|
|
109
|
-
|
|
111
|
+
prizeDrawDurationDays: z
|
|
112
|
+
.number()
|
|
113
|
+
.int()
|
|
114
|
+
.min(PRIZE_DRAW_DURATION_DAYS_MIN)
|
|
115
|
+
.max(PRIZE_DRAW_DURATION_DAYS_MAX)
|
|
116
|
+
.optional(),
|
|
117
|
+
prizeRevealMode: z.enum(["instant", "scheduled"]).optional(),
|
|
110
118
|
prizeGithubFileUrl: z.string().url().optional(),
|
|
111
119
|
inStock: z.boolean().optional(),
|
|
112
120
|
stockCount: z.number().optional(),
|
|
@@ -136,7 +136,8 @@ export interface ProductItem {
|
|
|
136
136
|
prizeRevealWindowStart?: string | Date;
|
|
137
137
|
prizeRevealWindowEnd?: string | Date;
|
|
138
138
|
prizeRevealStatus?: "pending" | "open" | "closed";
|
|
139
|
-
|
|
139
|
+
prizeDrawDurationDays?: number;
|
|
140
|
+
prizeRevealMode?: "instant" | "scheduled";
|
|
140
141
|
prizeGithubFileUrl?: string;
|
|
141
142
|
maxPerUser?: number;
|
|
142
143
|
/** Winner's display name for ended auctions — shown masked in public UI */
|
|
@@ -4,6 +4,7 @@ import { reviewRepository } from "../../../repositories";
|
|
|
4
4
|
import { Container, Div, Heading, Main, Nav, Section, Span, Text } from "../../../ui";
|
|
5
5
|
import { ROUTES } from "../../../next";
|
|
6
6
|
import { ReviewDetailShell } from "./ReviewDetailShell";
|
|
7
|
+
import { ReviewCard } from "./ReviewsList";
|
|
7
8
|
export async function ReviewDetailPageView({ id }) {
|
|
8
9
|
const doc = await reviewRepository.findById(id).catch(() => null);
|
|
9
10
|
const review = doc;
|
|
@@ -12,5 +13,11 @@ export async function ReviewDetailPageView({ id }) {
|
|
|
12
13
|
if (!review || review.status !== "approved") {
|
|
13
14
|
return (_jsx(Main, { children: _jsx(Section, { padding: "y-6xl", children: _jsx(Container, { size: "sm", children: _jsxs(Div, { className: "text-left", children: [_jsx(Text, { className: "mb-4", size: "5xl", "aria-hidden": "true", children: "\uD83D\uDD0D" }), _jsx(Heading, { color: "inverse", level: 1, className: "text-[var(--appkit-color-text)] dark: mb-2", size: "2xl", weight: "bold", children: "Review not found" }), _jsx(Text, { className: "text-[var(--appkit-color-text-muted)] mb-6", size: "sm", children: "This review may have been removed or is no longer available." }), _jsx(Link, { href: String(ROUTES.PUBLIC.REVIEWS), className: "inline-flex items-center gap-[var(--appkit-space-2)] rounded-lg bg-primary px-[var(--appkit-space-5)] py-[var(--appkit-space-2-5)] text-[length:var(--appkit-text-sm)] font-medium text-white hover:bg-primary-600 transition-colors", children: "\u2190 Back to Reviews" })] }) }) }) }));
|
|
14
15
|
}
|
|
15
|
-
|
|
16
|
+
const [sameProductDocs, sameStoreDocs] = await Promise.all([
|
|
17
|
+
reviewRepository.findApprovedByProduct(review.productId).catch(() => []),
|
|
18
|
+
storeSlug ? reviewRepository.findApprovedByStore(storeSlug).catch(() => []) : Promise.resolve([]),
|
|
19
|
+
]);
|
|
20
|
+
const sameProductReviews = sameProductDocs.filter((r) => r.id !== review.id).slice(0, 6);
|
|
21
|
+
const sameStoreReviews = sameStoreDocs.filter((r) => r.id !== review.id).slice(0, 6);
|
|
22
|
+
return (_jsxs(Main, { children: [_jsx(Div, { paddingY: "y-xs-tall", className: "border-b border-neutral-100 bg-neutral-50", padding: "x-md", children: _jsxs(Nav, { layout: "flex", gap: "2xs", textSize: "xs", color: "faint", className: "mx-auto max-w-3xl", "aria-label": "Breadcrumb", children: [_jsx(Link, { href: String(ROUTES.HOME), className: "hover:text-primary transition-colors", children: "Home" }), _jsx(Span, { children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.REVIEWS), className: "hover:text-primary transition-colors", children: "Reviews" }), _jsx(Span, { children: "/" }), _jsx(Span, { className: "text-neutral-700 truncate max-w-[200px]", children: review.title ?? review.id })] }) }), _jsx(ReviewDetailShell, { review: review, storeHref: storeSlug ? String(ROUTES.PUBLIC.STORE_DETAIL(storeSlug)) : null }), sameProductReviews.length > 0 && (_jsx(Section, { padding: "y-lg", border: "subtle", className: "border-t", children: _jsxs(Container, { size: "sm", children: [_jsxs(Heading, { level: 2, className: "mb-4", size: "lg", weight: "semibold", children: ["More reviews for ", review.productTitle ?? "this product"] }), _jsx(Div, { layout: "grid", gap: "4", className: "sm:grid-cols-2", children: sameProductReviews.map((r) => (_jsx(ReviewCard, { review: r, context: "listing" }, r.id))) })] }) })), sameStoreReviews.length > 0 && (_jsx(Section, { padding: "y-lg", border: "subtle", className: "border-t", children: _jsxs(Container, { size: "sm", children: [_jsxs(Heading, { level: 2, className: "mb-4", size: "lg", weight: "semibold", children: ["More reviews for ", review.storeName ?? "this store"] }), _jsx(Div, { layout: "grid", gap: "4", className: "sm:grid-cols-2", children: sameStoreReviews.map((r) => (_jsx(ReviewCard, { review: r, context: "store" }, r.id))) })] }) }))] }));
|
|
16
23
|
}
|
|
@@ -24,6 +24,7 @@ import { maskName } from "../../../security";
|
|
|
24
24
|
import { getDefaultLocale } from "../../../core/baseline-resolver";
|
|
25
25
|
import { normalizeRichTextHtml } from "../../../utils/string.formatter";
|
|
26
26
|
import { ROUTES } from "../../../next";
|
|
27
|
+
import { pluginFor } from "../../../_internal/shared/listing-types/_registry";
|
|
27
28
|
import { apiClient } from "../../../http";
|
|
28
29
|
import { REVIEW_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
29
30
|
export function ReviewDetailShell({ review, storeHref }) {
|
|
@@ -84,7 +85,7 @@ export function ReviewDetailShell({ review, storeHref }) {
|
|
|
84
85
|
};
|
|
85
86
|
// ── Links ───────────────────────────────────────────────────────────────────
|
|
86
87
|
const productHref = review.productId
|
|
87
|
-
?
|
|
88
|
+
? pluginFor(review.listingType ?? "standard").detailRoute(review.productId)
|
|
88
89
|
: null;
|
|
89
90
|
const sellerHref = storeHref ?? null;
|
|
90
91
|
const reviewerProfileId = review.userSlug ?? review.userId;
|
|
@@ -7,6 +7,7 @@ import { maskName } from "../../../security";
|
|
|
7
7
|
import { getDefaultLocale } from "../../../core/baseline-resolver";
|
|
8
8
|
import { normalizeRichTextHtml } from "../../../utils/string.formatter";
|
|
9
9
|
import { ROUTES } from "../../../next";
|
|
10
|
+
import { pluginFor } from "../../../_internal/shared/listing-types/_registry";
|
|
10
11
|
export function ReviewCard({ review, context = "general", className = "" }) {
|
|
11
12
|
const date = review.createdAt
|
|
12
13
|
? new Date(review.createdAt).toLocaleDateString(getDefaultLocale(), {
|
|
@@ -19,7 +20,7 @@ export function ReviewCard({ review, context = "general", className = "" }) {
|
|
|
19
20
|
const initials = displayName.charAt(0).toUpperCase();
|
|
20
21
|
const reviewHref = String(ROUTES.PUBLIC.REVIEW_DETAIL(review.id));
|
|
21
22
|
const productHref = review.productId
|
|
22
|
-
?
|
|
23
|
+
? pluginFor(review.listingType ?? "standard").detailRoute(review.productId)
|
|
23
24
|
: null;
|
|
24
25
|
const profileHref = !review.isAnonymous && review.userId
|
|
25
26
|
? String(ROUTES.PUBLIC.PROFILE(review.userId))
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import type { ReviewStatus } from "../types";
|
|
5
5
|
import type { BaseDocument } from "../../../_internal/shared/types/base-document";
|
|
6
|
+
import type { ListingType } from "../../products/types/index";
|
|
6
7
|
/** Runtime-accessible review status values — use instead of bare string literals. */
|
|
7
8
|
export declare const ReviewStatusValues: {
|
|
8
9
|
readonly PENDING: "pending";
|
|
@@ -19,6 +20,8 @@ export interface ReviewVideoField {
|
|
|
19
20
|
export interface ReviewDocument extends BaseDocument {
|
|
20
21
|
productId: string;
|
|
21
22
|
productTitle: string;
|
|
23
|
+
/** Denormalized from the product at review-creation time — drives the correct detail-page link (e.g. prize-draw items link to /prize-draws/[slug], not /products/[slug]). */
|
|
24
|
+
listingType?: ListingType;
|
|
22
25
|
storeId?: string;
|
|
23
26
|
storeName?: string;
|
|
24
27
|
userId: string;
|
|
@@ -37,6 +37,7 @@ export declare const reviewFirestoreSchema: z.ZodObject<{
|
|
|
37
37
|
id: z.ZodString;
|
|
38
38
|
productId: z.ZodString;
|
|
39
39
|
productTitle: z.ZodString;
|
|
40
|
+
listingType: z.ZodOptional<z.ZodString>;
|
|
40
41
|
storeId: z.ZodOptional<z.ZodString>;
|
|
41
42
|
storeName: z.ZodOptional<z.ZodString>;
|
|
42
43
|
userId: z.ZodString;
|
|
@@ -126,6 +127,7 @@ export declare const reviewFirestoreSchema: z.ZodObject<{
|
|
|
126
127
|
} | undefined;
|
|
127
128
|
storeId?: string | undefined;
|
|
128
129
|
featured?: boolean | undefined;
|
|
130
|
+
listingType?: string | undefined;
|
|
129
131
|
images?: string[] | undefined;
|
|
130
132
|
storeName?: string | undefined;
|
|
131
133
|
userAvatar?: string | undefined;
|
|
@@ -173,6 +175,7 @@ export declare const reviewFirestoreSchema: z.ZodObject<{
|
|
|
173
175
|
} | undefined;
|
|
174
176
|
storeId?: string | undefined;
|
|
175
177
|
featured?: boolean | undefined;
|
|
178
|
+
listingType?: string | undefined;
|
|
176
179
|
images?: string[] | undefined;
|
|
177
180
|
storeName?: string | undefined;
|
|
178
181
|
userAvatar?: string | undefined;
|
|
@@ -241,6 +244,7 @@ export declare const reviewSchema: z.ZodObject<{
|
|
|
241
244
|
id: z.ZodString;
|
|
242
245
|
productId: z.ZodString;
|
|
243
246
|
productTitle: z.ZodOptional<z.ZodString>;
|
|
247
|
+
listingType: z.ZodOptional<z.ZodString>;
|
|
244
248
|
storeSlug: z.ZodOptional<z.ZodString>;
|
|
245
249
|
storeName: z.ZodOptional<z.ZodString>;
|
|
246
250
|
userId: z.ZodString;
|
|
@@ -303,6 +307,7 @@ export declare const reviewSchema: z.ZodObject<{
|
|
|
303
307
|
createdAt?: string | undefined;
|
|
304
308
|
updatedAt?: string | undefined;
|
|
305
309
|
featured?: boolean | undefined;
|
|
310
|
+
listingType?: string | undefined;
|
|
306
311
|
images?: {
|
|
307
312
|
url: string;
|
|
308
313
|
thumbnailUrl?: string | undefined;
|
|
@@ -333,6 +338,7 @@ export declare const reviewSchema: z.ZodObject<{
|
|
|
333
338
|
createdAt?: string | undefined;
|
|
334
339
|
updatedAt?: string | undefined;
|
|
335
340
|
featured?: boolean | undefined;
|
|
341
|
+
listingType?: string | undefined;
|
|
336
342
|
images?: {
|
|
337
343
|
url: string;
|
|
338
344
|
thumbnailUrl?: string | undefined;
|
|
@@ -14,6 +14,7 @@ export const reviewFirestoreSchema = z.object({
|
|
|
14
14
|
id: z.string(),
|
|
15
15
|
productId: z.string(),
|
|
16
16
|
productTitle: z.string(),
|
|
17
|
+
listingType: z.string().optional(),
|
|
17
18
|
storeId: z.string().optional(),
|
|
18
19
|
storeName: z.string().optional(),
|
|
19
20
|
userId: z.string(),
|
|
@@ -72,6 +73,7 @@ export const reviewSchema = z.object({
|
|
|
72
73
|
id: z.string(),
|
|
73
74
|
productId: z.string(),
|
|
74
75
|
productTitle: z.string().optional(),
|
|
76
|
+
listingType: z.string().optional(),
|
|
75
77
|
storeSlug: z.string().optional(),
|
|
76
78
|
storeName: z.string().optional(),
|
|
77
79
|
userId: z.string(),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ListingType } from "../../products/types/index";
|
|
1
2
|
export type ReviewStatus = "pending" | "approved" | "rejected";
|
|
2
3
|
export interface ReviewImage {
|
|
3
4
|
url: string;
|
|
@@ -14,6 +15,8 @@ export interface Review {
|
|
|
14
15
|
id: string;
|
|
15
16
|
productId: string;
|
|
16
17
|
productTitle?: string;
|
|
18
|
+
/** Denormalized from the product at review-creation time — drives the correct detail-page link. */
|
|
19
|
+
listingType?: ListingType;
|
|
17
20
|
storeSlug?: string;
|
|
18
21
|
storeName?: string;
|
|
19
22
|
userId: string;
|
|
@@ -3,7 +3,10 @@ export interface ScammerProfilePageData {
|
|
|
3
3
|
scammer: ScammerDocument;
|
|
4
4
|
incidents: ScammerIncidentDocument[];
|
|
5
5
|
comments: ScammerCommentDocument[];
|
|
6
|
+
/** Admin-curated cross-links — confirmed to be the same person under a different alias. */
|
|
6
7
|
relatedScammers: ScammerDocument[];
|
|
8
|
+
/** Other verified profiles with the same scamType — pattern similarity only, never identity. */
|
|
9
|
+
similarScamReports: ScammerDocument[];
|
|
7
10
|
}
|
|
8
11
|
export interface ScammerListResult {
|
|
9
12
|
items: ScammerDocument[];
|