@mohasinac/appkit 2.6.1 → 2.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_internal/client/features/layout/DashboardLayoutClient.d.ts +38 -0
- package/dist/_internal/client/features/layout/DashboardLayoutClient.js +75 -0
- package/dist/_internal/client/features/layout/RoleGuard.d.ts +15 -0
- package/dist/_internal/client/features/layout/RoleGuard.js +25 -0
- package/dist/_internal/client/features/layout/index.d.ts +5 -0
- package/dist/_internal/client/features/layout/index.js +4 -0
- package/dist/_internal/server/features/brands/actions.d.ts +3 -3
- package/dist/_internal/server/features/brands/actions.js +72 -5
- package/dist/_internal/server/features/brands/data.d.ts +8 -8
- package/dist/_internal/server/features/brands/data.js +10 -11
- package/dist/_internal/server/features/brands/service.d.ts +2 -2
- package/dist/_internal/server/features/brands/service.js +5 -5
- package/dist/_internal/server/features/categories/og.d.ts +33 -0
- package/dist/_internal/server/features/categories/og.js +75 -0
- package/dist/_internal/server/features/checkout/actions.d.ts +24 -0
- package/dist/_internal/server/features/checkout/actions.js +442 -13
- package/dist/_internal/server/features/checkout/index.d.ts +1 -1
- package/dist/_internal/server/features/checkout/index.js +1 -1
- package/dist/_internal/server/features/checkout/prize-bundle-gates.d.ts +59 -0
- package/dist/_internal/server/features/checkout/prize-bundle-gates.js +99 -0
- package/dist/_internal/server/features/grouped/data.js +12 -5
- package/dist/_internal/server/features/homepage/data.d.ts +1 -1
- package/dist/_internal/server/features/homepage/data.js +2 -2
- package/dist/_internal/server/features/media/contextGuards.d.ts +52 -0
- package/dist/_internal/server/features/media/contextGuards.js +198 -0
- package/dist/_internal/server/features/orders/adapters.js +12 -0
- package/dist/_internal/server/features/products/data.d.ts +1 -1
- package/dist/_internal/server/features/sublisting-categories/data.d.ts +1 -1
- package/dist/_internal/server/features/sublisting-categories/data.js +2 -2
- package/dist/_internal/server/jobs/handlers/assignSpinPrize.d.ts +24 -0
- package/dist/_internal/server/jobs/handlers/assignSpinPrize.js +86 -0
- package/dist/_internal/server/jobs/handlers/bundleStockSync.d.ts +18 -0
- package/dist/_internal/server/jobs/handlers/bundleStockSync.js +80 -0
- package/dist/_internal/server/jobs/handlers/index.d.ts +8 -0
- package/dist/_internal/server/jobs/handlers/index.js +13 -0
- package/dist/_internal/server/jobs/handlers/listingProcessor.js +13 -3
- package/dist/_internal/server/jobs/handlers/onProductStockChange.d.ts +17 -0
- package/dist/_internal/server/jobs/handlers/onProductStockChange.js +136 -0
- package/dist/_internal/server/jobs/handlers/onProductWrite.js +17 -1
- package/dist/_internal/server/jobs/handlers/prizeRevealClose.d.ts +9 -0
- package/dist/_internal/server/jobs/handlers/prizeRevealClose.js +29 -0
- package/dist/_internal/server/jobs/handlers/prizeRevealExpiry.d.ts +10 -0
- package/dist/_internal/server/jobs/handlers/prizeRevealExpiry.js +58 -0
- package/dist/_internal/server/jobs/handlers/prizeRevealOpen.d.ts +10 -0
- package/dist/_internal/server/jobs/handlers/prizeRevealOpen.js +65 -0
- package/dist/_internal/server/jobs/handlers/prizeRevealReminder.d.ts +9 -0
- package/dist/_internal/server/jobs/handlers/prizeRevealReminder.js +45 -0
- package/dist/_internal/server/jobs/handlers/triggerEventRaffle.d.ts +30 -0
- package/dist/_internal/server/jobs/handlers/triggerEventRaffle.js +94 -0
- package/dist/_internal/shared/features/brands/schema.d.ts +3 -3
- package/dist/_internal/shared/features/cart/schema.d.ts +8 -8
- package/dist/_internal/shared/features/cart/schema.js +1 -1
- package/dist/_internal/shared/features/categories/bundle-config.d.ts +17 -0
- package/dist/_internal/shared/features/categories/bundle-config.js +17 -0
- package/dist/_internal/shared/features/layout/config.d.ts +35 -0
- package/dist/_internal/shared/features/layout/config.js +58 -0
- package/dist/_internal/shared/features/layout/index.d.ts +3 -0
- package/dist/_internal/shared/features/layout/index.js +2 -0
- package/dist/_internal/shared/features/layout/types.d.ts +137 -0
- package/dist/_internal/shared/features/layout/types.js +13 -0
- package/dist/_internal/shared/features/products/types.d.ts +1 -1
- package/dist/_internal/shared/listing-types/_registry.d.ts +57 -0
- package/dist/_internal/shared/listing-types/_registry.js +28 -0
- package/dist/_internal/shared/listing-types/auction/config.d.ts +7 -0
- package/dist/_internal/shared/listing-types/auction/config.js +8 -0
- package/dist/_internal/shared/listing-types/auction/ctas.d.ts +1 -0
- package/dist/_internal/shared/listing-types/auction/ctas.js +2 -0
- package/dist/_internal/shared/listing-types/auction/og.d.ts +1 -0
- package/dist/_internal/shared/listing-types/auction/og.js +1 -0
- package/dist/_internal/shared/listing-types/auction/schema.d.ts +1 -0
- package/dist/_internal/shared/listing-types/auction/schema.js +1 -0
- package/dist/_internal/shared/listing-types/auction/seed-factory.d.ts +1 -0
- package/dist/_internal/shared/listing-types/auction/seed-factory.js +1 -0
- package/dist/_internal/shared/listing-types/capabilities.d.ts +41 -0
- package/dist/_internal/shared/listing-types/capabilities.js +75 -0
- package/dist/_internal/shared/listing-types/pre-order/config.d.ts +7 -0
- package/dist/_internal/shared/listing-types/pre-order/config.js +8 -0
- package/dist/_internal/shared/listing-types/pre-order/ctas.d.ts +1 -0
- package/dist/_internal/shared/listing-types/pre-order/ctas.js +2 -0
- package/dist/_internal/shared/listing-types/pre-order/og.d.ts +1 -0
- package/dist/_internal/shared/listing-types/pre-order/og.js +1 -0
- package/dist/_internal/shared/listing-types/pre-order/schema.d.ts +1 -0
- package/dist/_internal/shared/listing-types/pre-order/schema.js +1 -0
- package/dist/_internal/shared/listing-types/pre-order/seed-factory.d.ts +1 -0
- package/dist/_internal/shared/listing-types/pre-order/seed-factory.js +1 -0
- package/dist/_internal/shared/listing-types/prize-draw/config.d.ts +7 -0
- package/dist/_internal/shared/listing-types/prize-draw/config.js +8 -0
- package/dist/_internal/shared/listing-types/prize-draw/ctas.d.ts +1 -0
- package/dist/_internal/shared/listing-types/prize-draw/ctas.js +2 -0
- package/dist/_internal/shared/listing-types/prize-draw/og.d.ts +1 -0
- package/dist/_internal/shared/listing-types/prize-draw/og.js +1 -0
- package/dist/_internal/shared/listing-types/prize-draw/schema.d.ts +1 -0
- package/dist/_internal/shared/listing-types/prize-draw/schema.js +1 -0
- package/dist/_internal/shared/listing-types/prize-draw/seed-factory.d.ts +1 -0
- package/dist/_internal/shared/listing-types/prize-draw/seed-factory.js +1 -0
- package/dist/_internal/shared/listing-types/standard/config.d.ts +7 -0
- package/dist/_internal/shared/listing-types/standard/config.js +8 -0
- package/dist/_internal/shared/listing-types/standard/ctas.d.ts +1 -0
- package/dist/_internal/shared/listing-types/standard/ctas.js +3 -0
- package/dist/_internal/shared/listing-types/standard/og.d.ts +1 -0
- package/dist/_internal/shared/listing-types/standard/og.js +1 -0
- package/dist/_internal/shared/listing-types/standard/schema.d.ts +1 -0
- package/dist/_internal/shared/listing-types/standard/schema.js +1 -0
- package/dist/_internal/shared/listing-types/standard/seed-factory.d.ts +1 -0
- package/dist/_internal/shared/listing-types/standard/seed-factory.js +1 -0
- package/dist/_internal/shared/media/limits.d.ts +33 -0
- package/dist/_internal/shared/media/limits.js +97 -0
- package/dist/_internal/shared/schema-versions.d.ts +76 -0
- package/dist/_internal/shared/schema-versions.js +82 -0
- package/dist/client.d.ts +9 -0
- package/dist/client.js +7 -0
- package/dist/constants/api-endpoints.d.ts +6 -3
- package/dist/constants/api-endpoints.js +2 -1
- package/dist/errors/messages.d.ts +1 -1
- package/dist/errors/messages.js +1 -1
- package/dist/features/account/migrations.d.ts +2 -0
- package/dist/features/account/migrations.js +10 -0
- package/dist/features/admin/components/AdminMediaView.js +1 -1
- package/dist/features/admin/components/AdminProductsView.js +7 -3
- package/dist/features/admin/migrations.d.ts +2 -0
- package/dist/features/admin/migrations.js +10 -0
- package/dist/features/admin/types/product.types.d.ts +1 -1
- package/dist/features/auctions/components/MarketplaceAuctionCard.d.ts +1 -1
- package/dist/features/auctions/migrations.d.ts +2 -0
- package/dist/features/auctions/migrations.js +10 -0
- package/dist/features/auctions/schemas/index.d.ts +3 -3
- package/dist/features/auctions/schemas/index.js +1 -1
- package/dist/features/auth/migrations.d.ts +2 -0
- package/dist/features/auth/migrations.js +10 -0
- package/dist/features/blog/migrations.d.ts +2 -0
- package/dist/features/blog/migrations.js +10 -0
- package/dist/features/brands/migrations.d.ts +2 -0
- package/dist/features/brands/migrations.js +10 -0
- package/dist/features/bundles/components/BundlesByCategoryListing.d.ts +6 -0
- package/dist/features/bundles/components/BundlesByCategoryListing.js +50 -0
- package/dist/features/bundles/components/index.d.ts +2 -0
- package/dist/features/bundles/components/index.js +1 -0
- package/dist/features/bundles/migrations.d.ts +2 -0
- package/dist/features/bundles/migrations.js +10 -0
- package/dist/features/bundles/schemas/index.d.ts +1 -0
- package/dist/features/bundles/schemas/index.js +1 -0
- package/dist/features/bundles/schemas/zod.d.ts +377 -0
- package/dist/features/bundles/schemas/zod.js +71 -0
- package/dist/features/cart/migrations.d.ts +2 -0
- package/dist/features/cart/migrations.js +10 -0
- package/dist/features/cart/schemas/firestore.d.ts +2 -2
- package/dist/features/categories/components/BrandDetailPageView.js +35 -4
- package/dist/features/categories/components/BrandDetailTabs.d.ts +5 -1
- package/dist/features/categories/components/BrandDetailTabs.js +22 -8
- package/dist/features/categories/components/CategoryBundlesListing.d.ts +6 -0
- package/dist/features/categories/components/CategoryBundlesListing.js +74 -0
- package/dist/features/categories/components/CategoryDetailPageView.js +29 -4
- package/dist/features/categories/components/CategoryDetailTabs.d.ts +5 -1
- package/dist/features/categories/components/CategoryDetailTabs.js +22 -8
- package/dist/features/categories/migrations.d.ts +2 -0
- package/dist/features/categories/migrations.js +10 -0
- package/dist/features/categories/repository/categories.repository.d.ts +29 -0
- package/dist/features/categories/repository/categories.repository.js +83 -0
- package/dist/features/categories/schemas/firestore.d.ts +59 -2
- package/dist/features/categories/schemas/firestore.js +6 -0
- package/dist/features/categories/types/index.d.ts +11 -3
- package/dist/features/events/migrations.d.ts +2 -0
- package/dist/features/events/migrations.js +10 -0
- package/dist/features/faq/migrations.d.ts +2 -0
- package/dist/features/faq/migrations.js +10 -0
- package/dist/features/grouped/migrations.d.ts +2 -0
- package/dist/features/grouped/migrations.js +10 -0
- package/dist/features/grouped/schemas/firestore.d.ts +29 -10
- package/dist/features/grouped/schemas/firestore.js +10 -5
- package/dist/features/history/migrations.d.ts +2 -0
- package/dist/features/history/migrations.js +10 -0
- package/dist/features/homepage/hooks/useFeaturedAuctions.js +2 -2
- package/dist/features/homepage/hooks/useFeaturedPreOrders.js +2 -2
- package/dist/features/homepage/lib/section-renderer.js +5 -3
- package/dist/features/media/AvatarUpload.js +6 -28
- package/dist/features/media/hooks/useMedia.d.ts +31 -15
- package/dist/features/media/hooks/useMedia.js +48 -13
- package/dist/features/media/upload/ImageUpload.js +1 -1
- package/dist/features/media/upload/MediaUploadField.js +1 -1
- package/dist/features/messages/migrations.d.ts +2 -0
- package/dist/features/messages/migrations.js +10 -0
- package/dist/features/orders/components/OrdersList.js +10 -1
- package/dist/features/orders/migrations.d.ts +2 -0
- package/dist/features/orders/migrations.js +10 -0
- package/dist/features/orders/repository/orders.repository.d.ts +16 -0
- package/dist/features/orders/repository/orders.repository.js +49 -0
- package/dist/features/orders/schemas/firestore.d.ts +8 -0
- package/dist/features/orders/types/index.d.ts +12 -0
- package/dist/features/orders/utils/order-splitter.d.ts +2 -2
- package/dist/features/orders/utils/order-splitter.js +5 -0
- package/dist/features/payments/migrations.d.ts +2 -0
- package/dist/features/payments/migrations.js +10 -0
- package/dist/features/pre-orders/components/PreOrderDetailPageView.js +4 -1
- package/dist/features/products/actions/product-actions.d.ts +1 -1
- package/dist/features/products/api/[id]/route.js +34 -0
- package/dist/features/products/api/route.js +1 -19
- package/dist/features/products/components/CompareOverlay.d.ts +1 -1
- package/dist/features/products/components/MarketplacePrizeDrawCard.d.ts +24 -0
- package/dist/features/products/components/MarketplacePrizeDrawCard.js +102 -0
- package/dist/features/products/components/PrizeDrawCollage.d.ts +32 -0
- package/dist/features/products/components/PrizeDrawCollage.js +22 -0
- package/dist/features/products/components/PrizeDrawDetailPageView.d.ts +27 -0
- package/dist/features/products/components/PrizeDrawDetailPageView.js +118 -0
- package/dist/features/products/components/PrizeDrawEntryActions.d.ts +19 -0
- package/dist/features/products/components/PrizeDrawEntryActions.js +48 -0
- package/dist/features/products/components/PrizeDrawItemsEditor.d.ts +13 -0
- package/dist/features/products/components/PrizeDrawItemsEditor.js +97 -0
- package/dist/features/products/components/PrizeDrawsIndexListing.d.ts +8 -0
- package/dist/features/products/components/PrizeDrawsIndexListing.js +128 -0
- package/dist/features/products/components/PrizeDrawsListingView.d.ts +15 -0
- package/dist/features/products/components/PrizeDrawsListingView.js +49 -0
- package/dist/features/products/components/PrizeRevealModal.d.ts +34 -0
- package/dist/features/products/components/PrizeRevealModal.js +124 -0
- package/dist/features/products/components/ProductDetailPageView.js +13 -1
- package/dist/features/products/components/ProductForm.js +35 -2
- package/dist/features/products/components/ProductGrid.js +3 -1
- package/dist/features/products/components/index.d.ts +16 -0
- package/dist/features/products/components/index.js +8 -0
- package/dist/features/products/constants/listing-tabs.d.ts +113 -0
- package/dist/features/products/constants/listing-tabs.js +43 -0
- package/dist/features/products/index.d.ts +1 -0
- package/dist/features/products/index.js +1 -0
- package/dist/features/products/migrations.d.ts +2 -0
- package/dist/features/products/migrations.js +10 -0
- package/dist/features/products/repository/products.repository.d.ts +11 -7
- package/dist/features/products/repository/products.repository.js +49 -24
- package/dist/features/products/schemas/firestore.d.ts +3 -3
- package/dist/features/products/schemas/firestore.js +2 -2
- package/dist/features/products/schemas/index.d.ts +5 -5
- package/dist/features/products/schemas/index.js +3 -1
- package/dist/features/products/schemas/product-features.validators.d.ts +6 -6
- package/dist/features/products/types/index.d.ts +17 -1
- package/dist/features/products/utils/listing-type.d.ts +7 -4
- package/dist/features/products/utils/listing-type.js +8 -4
- package/dist/features/promotions/actions/coupon-actions.d.ts +1 -1
- package/dist/features/promotions/hooks/useCouponValidate.d.ts +1 -1
- package/dist/features/promotions/migrations.d.ts +2 -0
- package/dist/features/promotions/migrations.js +10 -0
- package/dist/features/promotions/repository/coupons.repository.d.ts +1 -1
- package/dist/features/promotions/schemas/index.d.ts +2 -2
- package/dist/features/reviews/migrations.d.ts +2 -0
- package/dist/features/reviews/migrations.js +10 -0
- package/dist/features/scams/migrations.d.ts +2 -0
- package/dist/features/scams/migrations.js +10 -0
- package/dist/features/search/api/route.d.ts +1 -1
- package/dist/features/search/api/route.js +3 -3
- package/dist/features/search/components/Search.d.ts +1 -1
- package/dist/features/search/schemas/index.d.ts +3 -3
- package/dist/features/search/schemas/index.js +3 -1
- package/dist/features/search/types/index.d.ts +2 -2
- package/dist/features/seller/components/SellerProductShell.d.ts +1 -1
- package/dist/features/seller/components/SellerProductsView.js +20 -6
- package/dist/features/seller/migrations.d.ts +2 -0
- package/dist/features/seller/migrations.js +10 -0
- package/dist/features/seller/schemas/index.d.ts +2 -2
- package/dist/features/stores/components/StoreBundlesPageView.d.ts +12 -0
- package/dist/features/stores/components/StoreBundlesPageView.js +24 -0
- package/dist/features/stores/components/StoreDetailLayoutView.js +15 -3
- package/dist/features/stores/components/StorePrizeDrawsPageView.d.ts +11 -0
- package/dist/features/stores/components/StorePrizeDrawsPageView.js +27 -0
- package/dist/features/stores/components/index.d.ts +2 -0
- package/dist/features/stores/migrations.d.ts +2 -0
- package/dist/features/stores/migrations.js +10 -0
- package/dist/features/stores/schemas/index.d.ts +2 -2
- package/dist/features/stores/types/index.d.ts +1 -1
- package/dist/features/sublisting/migrations.d.ts +2 -0
- package/dist/features/sublisting/migrations.js +10 -0
- package/dist/features/sublisting/schemas/firestore.d.ts +2 -0
- package/dist/features/support/migrations.d.ts +2 -0
- package/dist/features/support/migrations.js +10 -0
- package/dist/features/wishlist/migrations.d.ts +2 -0
- package/dist/features/wishlist/migrations.js +10 -0
- package/dist/features/wishlist/types/index.d.ts +1 -1
- package/dist/index.d.ts +26 -18
- package/dist/index.js +41 -24
- package/dist/jobs.d.ts +1 -1
- package/dist/jobs.js +4 -0
- package/dist/next/routing/route-map.d.ts +4 -0
- package/dist/next/routing/route-map.js +2 -0
- package/dist/providers/db-firebase/admin-app-lite.js +21 -0
- package/dist/providers/db-firebase/filter-aliases.d.ts +2 -2
- package/dist/repositories/index.d.ts +0 -5
- package/dist/repositories/index.js +5 -4
- package/dist/seed/actions/demo-seed-actions.d.ts +1 -1
- package/dist/seed/categories-seed-data.js +1105 -6
- package/dist/seed/faq-seed-data.js +160 -0
- package/dist/seed/grouped-listings-seed-data.js +32 -32
- package/dist/seed/homepage-sections-seed-data.js +52 -6
- package/dist/seed/index.d.ts +1 -3
- package/dist/seed/index.js +4 -3
- package/dist/seed/manifest.js +8 -13
- package/dist/seed/products-prize-draws-seed-data.d.ts +17 -0
- package/dist/seed/products-prize-draws-seed-data.js +313 -0
- package/dist/seo/json-ld.d.ts +1 -1
- package/dist/server-entry.d.ts +2 -2
- package/dist/server-entry.js +5 -3
- package/dist/server.d.ts +9 -2
- package/dist/server.js +11 -5
- package/dist/tailwind-utilities.css +1 -1
- package/dist/validation/schemas.d.ts +8 -8
- package/package.json +1 -1
- package/scripts/seed-cli.mjs +2 -4
|
@@ -19,6 +19,22 @@ declare class OrderRepository extends BaseRepository<OrderDocument> {
|
|
|
19
19
|
cancelOrder(orderId: string, reason: string, refundAmount?: number): Promise<OrderDocument>;
|
|
20
20
|
findRecentByUser(userId: string): Promise<OrderDocument[]>;
|
|
21
21
|
hasUserPurchased(userId: string, productId: string): Promise<boolean>;
|
|
22
|
+
/**
|
|
23
|
+
* SB6-B — count this user's "active" orders for a given product. Used by
|
|
24
|
+
* the order-creation API to enforce `product.maxPerUser` on pre-orders +
|
|
25
|
+
* prize draws. Active = pending / confirmed / processing / shipped /
|
|
26
|
+
* delivered. Cancelled + refunded are intentionally excluded.
|
|
27
|
+
*
|
|
28
|
+
* Firestore caveat: an `in` over the 5-status set keeps this a single
|
|
29
|
+
* round-trip; we filter in memory if the active set ever grows past 10.
|
|
30
|
+
*/
|
|
31
|
+
countByUserAndProduct(userId: string, productId: string): Promise<number>;
|
|
32
|
+
/**
|
|
33
|
+
* SB6-B — count this user's active orders for a given bundle. Same
|
|
34
|
+
* active-status filter as `countByUserAndProduct`. Bundle orders set
|
|
35
|
+
* `order.bundleId` at creation time (SB3 / SB6-C).
|
|
36
|
+
*/
|
|
37
|
+
countByUserAndBundle(userId: string, bundleId: string): Promise<number>;
|
|
22
38
|
deleteByUser(userId: string): Promise<number>;
|
|
23
39
|
static readonly SELLER_SIEVE_FIELDS: {
|
|
24
40
|
id: {
|
|
@@ -6,13 +6,30 @@ import { createOrderId, ORDER_COLLECTION, OrderStatusValues, } from "../schemas"
|
|
|
6
6
|
const ORDER_FIELDS = {
|
|
7
7
|
USER_ID: "userId",
|
|
8
8
|
PRODUCT_ID: "productId",
|
|
9
|
+
BUNDLE_ID: "bundleId",
|
|
9
10
|
STATUS: "status",
|
|
10
11
|
STATUS_VALUES: {
|
|
12
|
+
PENDING: "pending",
|
|
11
13
|
CONFIRMED: "confirmed",
|
|
14
|
+
PROCESSING: "processing",
|
|
12
15
|
SHIPPED: "shipped",
|
|
13
16
|
DELIVERED: "delivered",
|
|
17
|
+
CANCELLED: "cancelled",
|
|
18
|
+
REFUNDED: "refunded",
|
|
14
19
|
},
|
|
15
20
|
};
|
|
21
|
+
/**
|
|
22
|
+
* Statuses that count toward a user's per-product / per-bundle purchase
|
|
23
|
+
* allowance (SB6 maxPerUser enforcement). Cancelled and refunded orders are
|
|
24
|
+
* intentionally excluded — the inventory was returned to circulation.
|
|
25
|
+
*/
|
|
26
|
+
const ACTIVE_ALLOWANCE_STATUSES = [
|
|
27
|
+
ORDER_FIELDS.STATUS_VALUES.PENDING,
|
|
28
|
+
ORDER_FIELDS.STATUS_VALUES.CONFIRMED,
|
|
29
|
+
ORDER_FIELDS.STATUS_VALUES.PROCESSING,
|
|
30
|
+
ORDER_FIELDS.STATUS_VALUES.SHIPPED,
|
|
31
|
+
ORDER_FIELDS.STATUS_VALUES.DELIVERED,
|
|
32
|
+
];
|
|
16
33
|
class OrderRepository extends BaseRepository {
|
|
17
34
|
constructor() {
|
|
18
35
|
super(ORDER_COLLECTION);
|
|
@@ -109,6 +126,38 @@ class OrderRepository extends BaseRepository {
|
|
|
109
126
|
.get();
|
|
110
127
|
return snapshot.docs.some((doc) => purchasedStatuses.has(doc.data()[ORDER_FIELDS.STATUS]));
|
|
111
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* SB6-B — count this user's "active" orders for a given product. Used by
|
|
131
|
+
* the order-creation API to enforce `product.maxPerUser` on pre-orders +
|
|
132
|
+
* prize draws. Active = pending / confirmed / processing / shipped /
|
|
133
|
+
* delivered. Cancelled + refunded are intentionally excluded.
|
|
134
|
+
*
|
|
135
|
+
* Firestore caveat: an `in` over the 5-status set keeps this a single
|
|
136
|
+
* round-trip; we filter in memory if the active set ever grows past 10.
|
|
137
|
+
*/
|
|
138
|
+
async countByUserAndProduct(userId, productId) {
|
|
139
|
+
const snapshot = await this.db
|
|
140
|
+
.collection(this.collection)
|
|
141
|
+
.where(ORDER_FIELDS.USER_ID, "==", userId)
|
|
142
|
+
.where(ORDER_FIELDS.PRODUCT_ID, "==", productId)
|
|
143
|
+
.where(ORDER_FIELDS.STATUS, "in", ACTIVE_ALLOWANCE_STATUSES)
|
|
144
|
+
.get();
|
|
145
|
+
return snapshot.size;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* SB6-B — count this user's active orders for a given bundle. Same
|
|
149
|
+
* active-status filter as `countByUserAndProduct`. Bundle orders set
|
|
150
|
+
* `order.bundleId` at creation time (SB3 / SB6-C).
|
|
151
|
+
*/
|
|
152
|
+
async countByUserAndBundle(userId, bundleId) {
|
|
153
|
+
const snapshot = await this.db
|
|
154
|
+
.collection(this.collection)
|
|
155
|
+
.where(ORDER_FIELDS.USER_ID, "==", userId)
|
|
156
|
+
.where(ORDER_FIELDS.BUNDLE_ID, "==", bundleId)
|
|
157
|
+
.where(ORDER_FIELDS.STATUS, "in", ACTIVE_ALLOWANCE_STATUSES)
|
|
158
|
+
.get();
|
|
159
|
+
return snapshot.size;
|
|
160
|
+
}
|
|
112
161
|
async deleteByUser(userId) {
|
|
113
162
|
try {
|
|
114
163
|
const snapshot = await this.getCollection()
|
|
@@ -53,6 +53,14 @@ export interface OrderDocumentItem {
|
|
|
53
53
|
quantity: number;
|
|
54
54
|
unitPrice: number;
|
|
55
55
|
totalPrice: number;
|
|
56
|
+
/** SB8-F — set when the item is a prize-draw entry; drives the reveals badge. */
|
|
57
|
+
listingType?: "standard" | "auction" | "pre-order" | "prize-draw";
|
|
58
|
+
/** SB8-F — per-item reveal status; flips through pending → open → revealed/closed. */
|
|
59
|
+
prizeRevealStatus?: "pending" | "open" | "closed" | "revealed";
|
|
60
|
+
/** SB8-F — ISO timestamp; deadline by which the buyer must claim the prize. */
|
|
61
|
+
prizeRevealDeadline?: string;
|
|
62
|
+
/** SB8-F — set after the reveal API picks a winner. */
|
|
63
|
+
revealedItemNumber?: number;
|
|
56
64
|
}
|
|
57
65
|
/** One applied discount/coupon saved on the order for accounting and display */
|
|
58
66
|
export interface AppliedOrderDiscount {
|
|
@@ -23,6 +23,18 @@ export interface OrderItem {
|
|
|
23
23
|
currency?: string;
|
|
24
24
|
storeId?: string;
|
|
25
25
|
attributes?: Record<string, string>;
|
|
26
|
+
/** Listing kind at the time of order — needed for prize-draw UI hints (SB8-F). */
|
|
27
|
+
listingType?: "standard" | "auction" | "pre-order" | "prize-draw";
|
|
28
|
+
/**
|
|
29
|
+
* Per-item prize-draw reveal status (SB8-F). Populated by the checkout
|
|
30
|
+
* actions when listingType === "prize-draw". Used to render the
|
|
31
|
+
* "X reveals pending" badge on user orders.
|
|
32
|
+
*/
|
|
33
|
+
prizeRevealStatus?: "pending" | "open" | "closed" | "revealed";
|
|
34
|
+
/** ISO timestamp — when the user must claim their reveal before forfeit. */
|
|
35
|
+
prizeRevealDeadline?: string;
|
|
36
|
+
/** Set after the reveal endpoint picks a winner — the prize item index. */
|
|
37
|
+
revealedItemNumber?: number;
|
|
26
38
|
}
|
|
27
39
|
export interface OrderTimeline {
|
|
28
40
|
status: OrderStatus;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type OrderType = "standard" | "preorder" | "auction" | "offer";
|
|
1
|
+
export type OrderType = "standard" | "preorder" | "auction" | "offer" | "prize-draw";
|
|
2
2
|
export interface OrderGroup<T> {
|
|
3
3
|
items: T[];
|
|
4
4
|
orderType: OrderType;
|
|
@@ -18,7 +18,7 @@ export declare function splitCartIntoOrderGroups<T extends {
|
|
|
18
18
|
item: {
|
|
19
19
|
itemId: string;
|
|
20
20
|
storeId?: string;
|
|
21
|
-
listingType?: "standard" | "auction" | "pre-order" | "prize-draw"
|
|
21
|
+
listingType?: "standard" | "auction" | "pre-order" | "prize-draw";
|
|
22
22
|
isOffer?: boolean;
|
|
23
23
|
};
|
|
24
24
|
}>(checks: T[]): OrderGroup<T>[];
|
|
@@ -23,6 +23,11 @@ export function splitCartIntoOrderGroups(checks) {
|
|
|
23
23
|
key = `offer:${item.itemId}`;
|
|
24
24
|
orderType = "offer";
|
|
25
25
|
}
|
|
26
|
+
else if (item.listingType === "prize-draw") {
|
|
27
|
+
// Each prize-draw entry is its own order (single reveal per order).
|
|
28
|
+
key = `prize-draw:${item.itemId}`;
|
|
29
|
+
orderType = "prize-draw";
|
|
30
|
+
}
|
|
26
31
|
else if (item.listingType === "pre-order") {
|
|
27
32
|
key = `preorder:${item.storeId ?? "unknown"}`;
|
|
28
33
|
orderType = "preorder";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// SB-UNI X3 — migrations shell for payout.
|
|
2
|
+
// v1 schema is current; no migrations needed yet.
|
|
3
|
+
//
|
|
4
|
+
// To add a migration:
|
|
5
|
+
// 1. Bump the relevant SCHEMA_VERSIONS entry in
|
|
6
|
+
// `_internal/shared/schema-versions.ts` (e.g. `payments: 2`)
|
|
7
|
+
// 2. Add a row below keyed by the FROM-version, returning the upgraded doc
|
|
8
|
+
// 3. Repository wraps reads with `migrateDocument(doc, current, migrations)`
|
|
9
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
10
|
+
export const migrations = {};
|
|
@@ -74,6 +74,9 @@ export async function PreOrderDetailPageView({ id, initialPreOrder, onReserveNow
|
|
|
74
74
|
? p.preOrderProductionStatus
|
|
75
75
|
: null;
|
|
76
76
|
const isCancellable = p.preOrderCancellable === true;
|
|
77
|
+
const maxPerUser = typeof p.maxPerUser === "number" && p.maxPerUser > 0
|
|
78
|
+
? p.maxPerUser
|
|
79
|
+
: null;
|
|
77
80
|
const condition = typeof p.condition === "string" ? p.condition : null;
|
|
78
81
|
const featured = p.featured === true;
|
|
79
82
|
const shippingPaidBy = p.shippingPaidBy;
|
|
@@ -111,7 +114,7 @@ export async function PreOrderDetailPageView({ id, initialPreOrder, onReserveNow
|
|
|
111
114
|
price: typeof p.price === "number" ? p.price : undefined,
|
|
112
115
|
storeId: typeof p.storeId === "string" ? p.storeId : undefined,
|
|
113
116
|
storeName: typeof p.storeName === "string" ? p.storeName : undefined,
|
|
114
|
-
} }), _jsxs(Container, { size: "xl", className: "px-4 py-6", children: [_jsxs("div", { className: "mb-4 flex items-center justify-between flex-wrap gap-2", children: [_jsxs("nav", { "aria-label": "Breadcrumb", className: "flex items-center gap-1.5 text-xs text-zinc-500 dark:text-zinc-400 flex-wrap", children: [_jsx(Link, { href: String(ROUTES.HOME), className: "hover:text-primary-600 transition-colors", children: "Home" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.PRE_ORDERS), className: "hover:text-primary-600 transition-colors", children: "Pre-Orders" }), category && (_jsxs(_Fragment, { children: [_jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category)), className: "hover:text-primary-600 transition-colors", children: categoryName || category })] })), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Span, { className: "text-zinc-700 dark:text-zinc-300 truncate max-w-[200px]", children: title })] }), _jsx(ShareButton, { title: title })] }), _jsx(PreOrderDetailView, { renderGallery: () => (_jsx(ProductGalleryClient, { images: images, productName: title })), renderInfo: () => (_jsxs(Stack, { gap: "md", children: [_jsxs(Div, { children: [_jsxs(Row, { gap: "xs", className: "mb-2 flex-wrap", children: [_jsx(Span, { className: "inline-block rounded-full bg-indigo-100 dark:bg-indigo-900/30 px-2.5 py-0.5 text-xs font-semibold text-indigo-700 dark:text-indigo-300", children: "Pre-Order" }), productionStatus && (_jsx(Span, { className: "inline-block rounded-full bg-zinc-100 dark:bg-zinc-800 px-2.5 py-0.5 text-xs font-medium text-zinc-600 dark:text-zinc-300", children: PRODUCTION_STATUS_LABELS[productionStatus] ?? productionStatus }))] }), _jsx(Heading, { level: 1, className: "text-xl font-bold leading-snug text-zinc-900 dark:text-zinc-50 sm:text-2xl", children: title })] }), deliveryDate && (_jsxs(Row, { align: "center", gap: "xs", className: "text-sm text-zinc-600 dark:text-zinc-400", children: [_jsx(Span, { children: "\uD83D\uDCC5" }), _jsx(Span, { children: "Estimated delivery:" }), _jsx(Span, { className: "font-medium", children: deliveryDate.toLocaleDateString(undefined, { year: "numeric", month: "long" }) })] })), _jsx(ProductFeatureBadges, { featured: featured, freeShipping: freeShipping, condition: condition ?? undefined, returnable: isCancellable, labels: {
|
|
117
|
+
} }), _jsxs(Container, { size: "xl", className: "px-4 py-6", children: [_jsxs("div", { className: "mb-4 flex items-center justify-between flex-wrap gap-2", children: [_jsxs("nav", { "aria-label": "Breadcrumb", className: "flex items-center gap-1.5 text-xs text-zinc-500 dark:text-zinc-400 flex-wrap", children: [_jsx(Link, { href: String(ROUTES.HOME), className: "hover:text-primary-600 transition-colors", children: "Home" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.PRE_ORDERS), className: "hover:text-primary-600 transition-colors", children: "Pre-Orders" }), category && (_jsxs(_Fragment, { children: [_jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category)), className: "hover:text-primary-600 transition-colors", children: categoryName || category })] })), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Span, { className: "text-zinc-700 dark:text-zinc-300 truncate max-w-[200px]", children: title })] }), _jsx(ShareButton, { title: title })] }), _jsx(PreOrderDetailView, { renderGallery: () => (_jsx(ProductGalleryClient, { images: images, productName: title })), renderInfo: () => (_jsxs(Stack, { gap: "md", children: [_jsxs(Div, { children: [_jsxs(Row, { gap: "xs", className: "mb-2 flex-wrap", children: [_jsx(Span, { className: "inline-block rounded-full bg-indigo-100 dark:bg-indigo-900/30 px-2.5 py-0.5 text-xs font-semibold text-indigo-700 dark:text-indigo-300", children: "Pre-Order" }), productionStatus && (_jsx(Span, { className: "inline-block rounded-full bg-zinc-100 dark:bg-zinc-800 px-2.5 py-0.5 text-xs font-medium text-zinc-600 dark:text-zinc-300", children: PRODUCTION_STATUS_LABELS[productionStatus] ?? productionStatus })), maxPerUser !== null && (_jsxs(Span, { className: "inline-block rounded-full bg-amber-100 dark:bg-amber-900/30 px-2.5 py-0.5 text-xs font-medium text-amber-800 dark:text-amber-200", children: ["Limit: ", maxPerUser, " per customer"] }))] }), _jsx(Heading, { level: 1, className: "text-xl font-bold leading-snug text-zinc-900 dark:text-zinc-50 sm:text-2xl", children: title })] }), deliveryDate && (_jsxs(Row, { align: "center", gap: "xs", className: "text-sm text-zinc-600 dark:text-zinc-400", children: [_jsx(Span, { children: "\uD83D\uDCC5" }), _jsx(Span, { children: "Estimated delivery:" }), _jsx(Span, { className: "font-medium", children: deliveryDate.toLocaleDateString(undefined, { year: "numeric", month: "long" }) })] })), _jsx(ProductFeatureBadges, { featured: featured, freeShipping: freeShipping, condition: condition ?? undefined, returnable: isCancellable, labels: {
|
|
115
118
|
featured: "Featured",
|
|
116
119
|
fasterDelivery: "Faster Delivery",
|
|
117
120
|
ratedSeller: "Rated Seller",
|
|
@@ -6,7 +6,7 @@ export interface ProductListActionParams {
|
|
|
6
6
|
page?: number;
|
|
7
7
|
pageSize?: number;
|
|
8
8
|
/** Canonical listing-kind discriminator (SB1-G Phase 4). */
|
|
9
|
-
listingType?: "standard" | "auction" | "pre-order" | "prize-draw"
|
|
9
|
+
listingType?: "standard" | "auction" | "pre-order" | "prize-draw";
|
|
10
10
|
featured?: boolean;
|
|
11
11
|
storeId?: string;
|
|
12
12
|
categoriesIn?: string[];
|
|
@@ -15,7 +15,14 @@ import { getProviders } from "../../../../contracts";
|
|
|
15
15
|
import { createRouteHandler } from "../../../../next";
|
|
16
16
|
import { mediaFieldSchema } from "../../../media/types/index";
|
|
17
17
|
import { storeRepository } from "../../../stores/repository/store.repository";
|
|
18
|
+
// SB-UNI-V — bundlesRepository deleted; bundle stock-sync moves to the
|
|
19
|
+
// `onProductStockChange` Firebase Function (see functions/src/bundle-stock-sync.ts).
|
|
18
20
|
import { sanitizeProductForPublic } from "../../utils/sanitize";
|
|
21
|
+
const UNAVAILABLE_PRODUCT_STATUSES = new Set([
|
|
22
|
+
"sold",
|
|
23
|
+
"out_of_stock",
|
|
24
|
+
"discontinued",
|
|
25
|
+
]);
|
|
19
26
|
function getRepo() {
|
|
20
27
|
const { db } = getProviders();
|
|
21
28
|
if (!db)
|
|
@@ -102,10 +109,36 @@ export const PATCH = createRouteHandler({
|
|
|
102
109
|
if (!isOwner && !isModerator && !isAdmin) {
|
|
103
110
|
return NextResponse.json({ success: false, error: "Not authorized to update this product" }, { status: 403 });
|
|
104
111
|
}
|
|
112
|
+
// SB4 — once any prize in a prize-draw listing has been won, the listing
|
|
113
|
+
// is frozen. Sellers can no longer edit it; admins inherit the same lock
|
|
114
|
+
// (anything they need to change should go through a new listing or a
|
|
115
|
+
// dedicated remediation script).
|
|
116
|
+
const prizeItems = product.prizeDrawItems ??
|
|
117
|
+
[];
|
|
118
|
+
const anyPrizeWon = prizeItems.some((it) => it?.isWon);
|
|
119
|
+
if (product.listingType === "prize-draw" &&
|
|
120
|
+
anyPrizeWon) {
|
|
121
|
+
return NextResponse.json({
|
|
122
|
+
success: false,
|
|
123
|
+
error: "This prize-draw listing is locked because at least one prize has been revealed. Clone it into a new listing if you want to rerun the draw.",
|
|
124
|
+
}, { status: 409 });
|
|
125
|
+
}
|
|
105
126
|
const updated = await repo.update(id, {
|
|
106
127
|
...body,
|
|
107
128
|
updatedAt: new Date().toISOString(),
|
|
108
129
|
});
|
|
130
|
+
// SB1-H stock-sync — if this PATCH transitioned the product to an
|
|
131
|
+
// unavailable status, propagate `isSold` to every bundle that lists it.
|
|
132
|
+
const beforeStatus = product.status;
|
|
133
|
+
const afterStatus = body?.status;
|
|
134
|
+
const becameUnavailable = typeof afterStatus === "string" &&
|
|
135
|
+
UNAVAILABLE_PRODUCT_STATUSES.has(afterStatus) &&
|
|
136
|
+
beforeStatus !== afterStatus;
|
|
137
|
+
// SB-UNI-V — bundle stock-sync moved to onProductStockChange Function;
|
|
138
|
+
// the API route no longer fires fire-and-forget. The product write
|
|
139
|
+
// triggers the Function via Firestore onWrite. Reference to
|
|
140
|
+
// `becameUnavailable` retained for telemetry callers.
|
|
141
|
+
void becameUnavailable;
|
|
109
142
|
return NextResponse.json({ success: true, data: updated });
|
|
110
143
|
},
|
|
111
144
|
});
|
|
@@ -134,6 +167,7 @@ export const DELETE = createRouteHandler({
|
|
|
134
167
|
status: "discontinued",
|
|
135
168
|
updatedAt: new Date().toISOString(),
|
|
136
169
|
});
|
|
170
|
+
// SB-UNI-V — bundle stock-sync handled by onProductStockChange Function.
|
|
137
171
|
return NextResponse.json({ success: true });
|
|
138
172
|
},
|
|
139
173
|
});
|
|
@@ -43,8 +43,6 @@ const productMutateSchema = z
|
|
|
43
43
|
tags: z.array(z.string()).optional(),
|
|
44
44
|
featured: z.boolean().optional(),
|
|
45
45
|
isPromoted: z.boolean().optional(),
|
|
46
|
-
isAuction: z.boolean().optional(),
|
|
47
|
-
isPreOrder: z.boolean().optional(),
|
|
48
46
|
sellerId: z.string().optional(),
|
|
49
47
|
sellerName: z.string().optional(),
|
|
50
48
|
sellerEmail: z.string().email().optional(),
|
|
@@ -73,10 +71,7 @@ const SAFE_PRODUCT_FILTER_FIELDS = new Set([
|
|
|
73
71
|
"storeId",
|
|
74
72
|
"title",
|
|
75
73
|
"price",
|
|
76
|
-
// SB1-G — canonical discriminator first; legacy booleans retained transitional.
|
|
77
74
|
"listingType",
|
|
78
|
-
"isAuction",
|
|
79
|
-
"isPreOrder",
|
|
80
75
|
"featured",
|
|
81
76
|
"isPromoted",
|
|
82
77
|
"stockQuantity",
|
|
@@ -126,24 +121,11 @@ function buildFilters(url) {
|
|
|
126
121
|
const inStock = param(url, "inStock");
|
|
127
122
|
if (inStock === "true")
|
|
128
123
|
parts.push("stockQuantity>0");
|
|
129
|
-
// SB1-G —
|
|
130
|
-
// the canonical listingType clause. ?listingType=auction|pre-order|standard
|
|
131
|
-
// takes precedence when present.
|
|
124
|
+
// SB1-G — canonical discriminator. Public URL accepts only ?listingType=X.
|
|
132
125
|
const listingTypeParam = param(url, "listingType");
|
|
133
|
-
const isAuction = param(url, "isAuction");
|
|
134
|
-
const isPreOrder = param(url, "isPreOrder");
|
|
135
126
|
if (listingTypeParam) {
|
|
136
127
|
parts.push(`listingType==${listingTypeParam}`);
|
|
137
128
|
}
|
|
138
|
-
else if (isAuction === "true") {
|
|
139
|
-
parts.push("listingType==auction");
|
|
140
|
-
}
|
|
141
|
-
else if (isPreOrder === "true") {
|
|
142
|
-
parts.push("listingType==pre-order");
|
|
143
|
-
}
|
|
144
|
-
else if (isAuction === "false" && isPreOrder === "false") {
|
|
145
|
-
parts.push("listingType==standard");
|
|
146
|
-
}
|
|
147
129
|
const featured = param(url, "featured");
|
|
148
130
|
if (featured === "true")
|
|
149
131
|
parts.push("featured==true");
|
|
@@ -15,7 +15,7 @@ export interface CompareProductLike {
|
|
|
15
15
|
storeName?: string;
|
|
16
16
|
storeSlug?: string;
|
|
17
17
|
/** Canonical discriminator (SB1-G Phase 4). */
|
|
18
|
-
listingType?: "standard" | "auction" | "pre-order" | "prize-draw"
|
|
18
|
+
listingType?: "standard" | "auction" | "pre-order" | "prize-draw";
|
|
19
19
|
features?: string[];
|
|
20
20
|
}
|
|
21
21
|
interface CompareFieldLabels {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ProductItem } from "../types";
|
|
2
|
+
export type MarketplacePrizeDrawCardData = ProductItem;
|
|
3
|
+
export interface MarketplacePrizeDrawCardLabels {
|
|
4
|
+
prizeDrawBadge?: string;
|
|
5
|
+
pendingBadge?: string;
|
|
6
|
+
openBadge?: string;
|
|
7
|
+
closedBadge?: string;
|
|
8
|
+
entriesRemainingLabel?: (remaining: number, max: number) => string;
|
|
9
|
+
pricePerEntryLabel?: string;
|
|
10
|
+
enterDraw?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface MarketplacePrizeDrawCardProps {
|
|
13
|
+
product: MarketplacePrizeDrawCardData;
|
|
14
|
+
className?: string;
|
|
15
|
+
variant?: "grid" | "list";
|
|
16
|
+
selectable?: boolean;
|
|
17
|
+
isSelected?: boolean;
|
|
18
|
+
onSelect?: (id: string, selected: boolean) => void;
|
|
19
|
+
href?: string;
|
|
20
|
+
hrefBuilder?: (product: MarketplacePrizeDrawCardData) => string;
|
|
21
|
+
onNavigate?: (href: string) => void;
|
|
22
|
+
labels?: MarketplacePrizeDrawCardLabels;
|
|
23
|
+
}
|
|
24
|
+
export declare function MarketplacePrizeDrawCard({ product, className, variant, selectable, isSelected, onSelect, href, hrefBuilder, onNavigate, labels, }: MarketplacePrizeDrawCardProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useCallback } from "react";
|
|
4
|
+
import { useRouter } from "next/navigation";
|
|
5
|
+
import { useLongPress } from "../../../react/hooks/useLongPress";
|
|
6
|
+
import { ROUTES } from "../../../next";
|
|
7
|
+
import { formatCurrency } from "../../../utils";
|
|
8
|
+
import { getDefaultCurrency } from "../../../core/baseline-resolver";
|
|
9
|
+
import { BaseListingCard, Button, Div, Row, Span, Text, TextLink, } from "../../../ui";
|
|
10
|
+
import { THEME_CONSTANTS } from "../../../tokens";
|
|
11
|
+
const DEFAULT_LABELS = {
|
|
12
|
+
prizeDrawBadge: "Prize draw",
|
|
13
|
+
pendingBadge: "Reveal pending",
|
|
14
|
+
openBadge: "Reveal open",
|
|
15
|
+
closedBadge: "Draw closed",
|
|
16
|
+
entriesRemainingLabel: (remaining, max) => `${remaining}/${max} entries left`,
|
|
17
|
+
pricePerEntryLabel: "per entry",
|
|
18
|
+
enterDraw: "Enter draw",
|
|
19
|
+
};
|
|
20
|
+
function statusVariant(status) {
|
|
21
|
+
switch (status) {
|
|
22
|
+
case "open":
|
|
23
|
+
return "bg-emerald-600 text-white";
|
|
24
|
+
case "closed":
|
|
25
|
+
return "bg-zinc-500 text-white";
|
|
26
|
+
case "pending":
|
|
27
|
+
default:
|
|
28
|
+
return "bg-amber-500 text-white";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function statusLabel(status, labels) {
|
|
32
|
+
switch (status) {
|
|
33
|
+
case "open":
|
|
34
|
+
return labels.openBadge;
|
|
35
|
+
case "closed":
|
|
36
|
+
return labels.closedBadge;
|
|
37
|
+
case "pending":
|
|
38
|
+
default:
|
|
39
|
+
return labels.pendingBadge;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function resolveHref(product, href, hrefBuilder) {
|
|
43
|
+
if (href)
|
|
44
|
+
return href;
|
|
45
|
+
if (hrefBuilder)
|
|
46
|
+
return hrefBuilder(product);
|
|
47
|
+
return ROUTES.PUBLIC.PRIZE_DRAW_DETAIL(product.slug ?? product.id);
|
|
48
|
+
}
|
|
49
|
+
function formatCountdown(target) {
|
|
50
|
+
if (!target)
|
|
51
|
+
return null;
|
|
52
|
+
const t = target instanceof Date ? target.getTime() : new Date(target).getTime();
|
|
53
|
+
if (Number.isNaN(t))
|
|
54
|
+
return null;
|
|
55
|
+
const diff = t - Date.now();
|
|
56
|
+
if (diff <= 0)
|
|
57
|
+
return null;
|
|
58
|
+
const days = Math.floor(diff / 86400000);
|
|
59
|
+
const hours = Math.floor((diff % 86400000) / 3600000);
|
|
60
|
+
if (days > 0)
|
|
61
|
+
return `${days}d ${hours}h`;
|
|
62
|
+
const minutes = Math.floor((diff % 3600000) / 60000);
|
|
63
|
+
return `${hours}h ${minutes}m`;
|
|
64
|
+
}
|
|
65
|
+
export function MarketplacePrizeDrawCard({ product, className = "", variant = "grid", selectable = false, isSelected = false, onSelect, href, hrefBuilder, onNavigate, labels, }) {
|
|
66
|
+
const router = useRouter();
|
|
67
|
+
const mergedLabels = { ...DEFAULT_LABELS, ...labels };
|
|
68
|
+
const detailHref = resolveHref(product, href, hrefBuilder);
|
|
69
|
+
const longPress = useLongPress(() => onSelect?.(product.id, !isSelected));
|
|
70
|
+
const items = product.prizeDrawItems ?? [];
|
|
71
|
+
const thumbItems = items.slice(0, 4);
|
|
72
|
+
const status = product.prizeRevealStatus;
|
|
73
|
+
const max = product.prizeMaxEntries ?? 0;
|
|
74
|
+
const current = product.prizeCurrentEntries ?? 0;
|
|
75
|
+
const remaining = Math.max(0, max - current);
|
|
76
|
+
const pricePerEntry = product.pricePerEntry ?? product.price;
|
|
77
|
+
const countdownTarget = status === "pending"
|
|
78
|
+
? product.prizeRevealWindowStart
|
|
79
|
+
: status === "open"
|
|
80
|
+
? product.prizeRevealWindowEnd
|
|
81
|
+
: undefined;
|
|
82
|
+
const countdown = formatCountdown(countdownTarget);
|
|
83
|
+
const handleNavigate = useCallback(() => {
|
|
84
|
+
if (onNavigate) {
|
|
85
|
+
onNavigate(String(detailHref));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
router.push(String(detailHref));
|
|
89
|
+
}, [detailHref, onNavigate, router]);
|
|
90
|
+
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, children: [_jsxs(BaseListingCard.Hero, { aspect: "square", variant: variant, children: [_jsx(TextLink, { href: String(detailHref), className: "absolute inset-0 block", children: thumbItems.length > 0 ? (_jsx(Div, { className: "grid grid-cols-2 grid-rows-2 gap-0.5 h-full w-full bg-[var(--appkit-color-surface-muted)]", children: Array.from({ length: 4 }).map((_, i) => {
|
|
91
|
+
const it = thumbItems[i];
|
|
92
|
+
const img = it?.images?.[0];
|
|
93
|
+
return (_jsx(Div, { className: "relative overflow-hidden bg-[var(--appkit-color-surface-muted)]", children: img ? (
|
|
94
|
+
/* eslint-disable-next-line @next/next/no-raw-media-elements, @next/next/no-img-element */
|
|
95
|
+
_jsx("img", { src: img, alt: it?.title ?? `Prize ${i + 1}`, loading: "lazy", className: "absolute inset-0 h-full w-full object-cover" })) : null }, `thumb-${i}`));
|
|
96
|
+
}) })) : (_jsx(Div, { className: "absolute inset-0 flex items-center justify-center bg-[var(--appkit-color-surface-muted)]", children: _jsx(Text, { className: "text-xs text-[var(--appkit-color-text-muted)]", children: "No prizes" }) })) }), _jsxs(Div, { className: "absolute right-2 top-2 flex flex-col items-end gap-1", children: [_jsx(Span, { className: "inline-flex items-center rounded-full bg-fuchsia-600 px-2 py-0.5 text-xs font-medium text-white", children: mergedLabels.prizeDrawBadge }), _jsx(Span, { className: `inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${statusVariant(status)}`, children: statusLabel(status, mergedLabels) })] }), status === "closed" ? (_jsx(Div, { className: "absolute inset-0 flex items-center justify-center bg-black/40", children: _jsx(Span, { className: "rounded bg-zinc-900/80 px-3 py-1 text-xs font-bold uppercase tracking-wider text-white", children: mergedLabels.closedBadge }) })) : null, onSelect && (_jsx(BaseListingCard.Checkbox, { selected: isSelected, onSelect: (event) => {
|
|
97
|
+
event.stopPropagation();
|
|
98
|
+
onSelect(product.id, !isSelected);
|
|
99
|
+
}, className: selectable || isSelected
|
|
100
|
+
? "opacity-100"
|
|
101
|
+
: "opacity-0 group-hover:opacity-100 transition-opacity" }))] }), _jsxs(BaseListingCard.Info, { variant: variant, children: [_jsx(TextLink, { href: String(detailHref), children: _jsx(Text, { className: `${THEME_CONSTANTS.utilities.textClamp2} text-sm font-medium text-zinc-900 dark:text-zinc-100`, children: product.title }) }), _jsxs(Row, { justify: "between", className: "mt-1 gap-2", children: [_jsxs(Text, { className: "text-sm font-semibold text-zinc-900 dark:text-zinc-100", children: [formatCurrency(pricePerEntry, getDefaultCurrency()), " ", _jsx(Span, { className: "text-xs font-normal text-[var(--appkit-color-text-muted)]", children: mergedLabels.pricePerEntryLabel })] }), max > 0 ? (_jsx(Text, { className: "text-xs text-[var(--appkit-color-text-muted)]", children: mergedLabels.entriesRemainingLabel(remaining, max) })) : null] }), countdown ? (_jsxs(Text, { className: "text-xs text-[var(--appkit-color-text-muted)]", children: [status === "pending" ? "Opens in" : "Closes in", " ", countdown] })) : null, _jsx(Button, { type: "button", variant: "primary", size: "sm", className: "mt-2 w-full text-xs", onClick: handleNavigate, disabled: status === "closed" || remaining === 0, children: mergedLabels.enterDraw })] })] }));
|
|
102
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PrizeDrawCollage (SB4-B)
|
|
3
|
+
*
|
|
4
|
+
* Read-only display of a prize-draw's full prize pool. Each cell shows the
|
|
5
|
+
* primary image, item-number badge, and title. Items with `isWon === true`
|
|
6
|
+
* are dimmed under a diagonal overlay with a "Won" label so the public
|
|
7
|
+
* collage truthfully reflects what's still up for grabs.
|
|
8
|
+
*
|
|
9
|
+
* Optional `highlightItemNumber` (passed by the reveal modal during animation)
|
|
10
|
+
* adds an emphasis ring to the cell that just won.
|
|
11
|
+
*/
|
|
12
|
+
import type { PrizeDrawItem } from "../schemas/firestore";
|
|
13
|
+
export interface PrizeDrawCollageProps {
|
|
14
|
+
items: PrizeDrawItem[];
|
|
15
|
+
highlightItemNumber?: number;
|
|
16
|
+
/** Custom click handler — e.g. open detail modal for an item. */
|
|
17
|
+
onItemClick?: (item: PrizeDrawItem) => void;
|
|
18
|
+
/** Defaults to "Won". Use for localisation. */
|
|
19
|
+
wonLabel?: string;
|
|
20
|
+
/**
|
|
21
|
+
* Public buyer surfaces pass `true` so the diagonal "Won" overlay never
|
|
22
|
+
* renders — otherwise potential buyers would see their favorite prize is
|
|
23
|
+
* already gone and drop out. The seller / admin / winner views leave this
|
|
24
|
+
* `false` (default) to show real pool state.
|
|
25
|
+
*
|
|
26
|
+
* The product adapter for public reads should ALSO strip `isWon` before
|
|
27
|
+
* sending the items array client-side; this prop is the visual fallback.
|
|
28
|
+
*/
|
|
29
|
+
hideWonState?: boolean;
|
|
30
|
+
}
|
|
31
|
+
export declare function PrizeDrawCollage({ items, highlightItemNumber, onItemClick, wonLabel, hideWonState, }: PrizeDrawCollageProps): import("react/jsx-runtime").JSX.Element;
|
|
32
|
+
export default PrizeDrawCollage;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Div, Text } from "../../../ui";
|
|
3
|
+
export function PrizeDrawCollage({ items, highlightItemNumber, onItemClick, wonLabel = "Won", hideWonState = false, }) {
|
|
4
|
+
if (!items.length) {
|
|
5
|
+
return (_jsx(Div, { className: "rounded border border-dashed border-[var(--appkit-color-border)] p-6 text-center", children: _jsx(Text, { className: "text-sm text-[var(--appkit-color-text-muted)]", children: "No prizes configured yet." }) }));
|
|
6
|
+
}
|
|
7
|
+
return (_jsx(Div, { className: "grid grid-cols-2 gap-3 sm:grid-cols-3 md:grid-cols-4", children: items.map((it) => {
|
|
8
|
+
const cover = it.images?.[0];
|
|
9
|
+
const isHighlight = highlightItemNumber === it.itemNumber;
|
|
10
|
+
const Cell = onItemClick ? "button" : "div";
|
|
11
|
+
return (_jsxs(Cell, { type: onItemClick ? "button" : undefined, onClick: onItemClick ? () => onItemClick(it) : undefined, className: [
|
|
12
|
+
"group relative overflow-hidden rounded-lg border bg-[var(--appkit-color-surface)] text-left transition-transform",
|
|
13
|
+
"border-[var(--appkit-color-border)]",
|
|
14
|
+
isHighlight
|
|
15
|
+
? "ring-2 ring-offset-2 ring-[var(--appkit-color-primary)] scale-[1.02]"
|
|
16
|
+
: "hover:scale-[1.01]",
|
|
17
|
+
].join(" "), children: [_jsxs(Div, { className: "relative aspect-square w-full", children: [cover ? (
|
|
18
|
+
/* eslint-disable-next-line @next/next/no-img-element */
|
|
19
|
+
_jsx("img", { src: cover, alt: it.title || `Prize #${it.itemNumber}`, className: "absolute inset-0 h-full w-full object-cover", loading: "lazy" })) : (_jsx(Div, { className: "absolute inset-0 flex items-center justify-center bg-[var(--appkit-color-surface-muted)]", children: _jsx(Text, { className: "text-xs text-[var(--appkit-color-text-muted)]", children: "No image" }) })), _jsxs(Div, { className: "absolute left-2 top-2 rounded bg-black/70 px-1.5 py-0.5 text-xs font-semibold text-white", children: ["#", it.itemNumber] }), it.isWon && !hideWonState ? (_jsxs(_Fragment, { children: [_jsx(Div, { "aria-hidden": true, className: "absolute inset-0 bg-gradient-to-br from-black/60 via-black/30 to-black/60 mix-blend-multiply" }), _jsx(Div, { className: "absolute inset-0 flex items-center justify-center", children: _jsx(Text, { className: "rotate-[-12deg] rounded bg-red-600 px-3 py-1 text-xs font-bold uppercase tracking-wider text-white shadow", children: wonLabel }) })] })) : null] }), _jsxs(Div, { className: "p-2", children: [_jsx(Text, { className: "line-clamp-2 text-sm font-medium", children: it.title || `Prize #${it.itemNumber}` }), it.estimatedValue != null ? (_jsxs(Text, { className: "text-xs text-[var(--appkit-color-text-muted)]", children: ["est. \u20B9", (it.estimatedValue / 100).toLocaleString("en-IN")] })) : null] })] }, `collage-${it.itemNumber}`));
|
|
20
|
+
}) }));
|
|
21
|
+
}
|
|
22
|
+
export default PrizeDrawCollage;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ProductDocument } from "../schemas/firestore";
|
|
2
|
+
export interface PrizeDrawDetailPageViewProps {
|
|
3
|
+
id: string;
|
|
4
|
+
/**
|
|
5
|
+
* Pre-fetched product document from the page's server data layer.
|
|
6
|
+
* When provided, the internal repository call is skipped — deduplicating
|
|
7
|
+
* the fetch with generateMetadata() via React.cache().
|
|
8
|
+
*/
|
|
9
|
+
initialPrizeDraw?: ProductDocument | null;
|
|
10
|
+
/**
|
|
11
|
+
* Authenticated buyer's uid. When set, the view server-fetches the
|
|
12
|
+
* buyer's existing-entry count for this draw and renders the SB6-D
|
|
13
|
+
* personalised "You have X/Y entries used" badge.
|
|
14
|
+
*/
|
|
15
|
+
currentUserId?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Public prize-draw detail page (SB4-G).
|
|
19
|
+
*
|
|
20
|
+
* Server-fetches the prize-draw product, strips `isWon` from each item before
|
|
21
|
+
* passing it to `PrizeDrawCollage` so public buyers stay unspoiled, and
|
|
22
|
+
* delegates the buy-bar to the client `PrizeDrawEntryActions` which surfaces
|
|
23
|
+
* the `NonRefundableConsentModal` before any add-to-cart happens.
|
|
24
|
+
*
|
|
25
|
+
* Reuses the `PreOrderDetailView` (grid-2) shell for layout parity.
|
|
26
|
+
*/
|
|
27
|
+
export declare function PrizeDrawDetailPageView({ id, initialPrizeDraw, currentUserId, }: PrizeDrawDetailPageViewProps): Promise<import("react/jsx-runtime").JSX.Element>;
|