@mohasinac/appkit 2.7.59 → 2.8.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/client/features/filters/filter-load-options.d.ts +4 -5
- package/dist/_internal/server/features/auctions/og.js +8 -32
- package/dist/_internal/server/features/blog/og.js +10 -49
- package/dist/_internal/server/features/checkout/actions.js +6 -0
- package/dist/_internal/server/features/classified/og.js +9 -51
- package/dist/_internal/server/features/digital-code/actions.d.ts +11 -0
- package/dist/_internal/server/features/digital-code/actions.js +30 -0
- package/dist/_internal/server/features/digital-code/index.d.ts +1 -0
- package/dist/_internal/server/features/digital-code/index.js +1 -0
- package/dist/_internal/server/features/digital-code/og.js +9 -51
- package/dist/_internal/server/features/events/og.js +10 -49
- package/dist/_internal/server/features/item-requests/data.d.ts +17 -0
- package/dist/_internal/server/features/item-requests/data.js +20 -0
- package/dist/_internal/server/features/item-requests/index.d.ts +2 -0
- package/dist/_internal/server/features/item-requests/index.js +2 -0
- package/dist/_internal/server/features/item-requests/og.d.ts +40 -0
- package/dist/_internal/server/features/item-requests/og.js +136 -0
- package/dist/_internal/server/features/live/actions.d.ts +5 -0
- package/dist/_internal/server/features/live/actions.js +24 -0
- package/dist/_internal/server/features/live/index.d.ts +1 -0
- package/dist/_internal/server/features/live/index.js +1 -0
- package/dist/_internal/server/features/live/og.js +9 -51
- package/dist/_internal/server/features/media/contextGuards.js +19 -8
- package/dist/_internal/server/features/pre-orders/og.js +8 -32
- package/dist/_internal/server/features/prize-draws/data.d.ts +17 -0
- package/dist/_internal/server/features/prize-draws/data.js +20 -0
- package/dist/_internal/server/features/prize-draws/index.d.ts +2 -0
- package/dist/_internal/server/features/prize-draws/index.js +2 -0
- package/dist/_internal/server/features/prize-draws/og.d.ts +30 -0
- package/dist/_internal/server/features/prize-draws/og.js +50 -0
- package/dist/_internal/server/features/products/og.js +8 -33
- package/dist/_internal/server/features/seo/og-layout.d.ts +42 -0
- package/dist/_internal/server/features/seo/og-layout.js +69 -0
- package/dist/_internal/server/features/stores/og.js +8 -41
- package/dist/_internal/server/features/sublisting-categories/og.js +11 -42
- package/dist/client.d.ts +10 -6
- package/dist/client.js +7 -3
- package/dist/constants/index.d.ts +4 -0
- package/dist/constants/index.js +2 -0
- package/dist/constants/notification-types.d.ts +20 -0
- package/dist/constants/notification-types.js +37 -0
- package/dist/constants/pagination-presets.d.ts +39 -0
- package/dist/constants/pagination-presets.js +38 -0
- package/dist/errors/messages.d.ts +0 -4
- package/dist/errors/messages.js +0 -4
- package/dist/features/account/components/UserOffersPanel.js +1 -1
- package/dist/features/account/components/UserSupportView.d.ts +1 -2
- package/dist/features/account/components/UserSupportView.js +9 -1
- package/dist/features/admin/actions/admin-actions.js +16 -1
- package/dist/features/admin/components/AdminAllEventEntriesView.d.ts +2 -3
- package/dist/features/admin/components/AdminAllEventEntriesView.js +50 -83
- package/dist/features/admin/components/AdminBidsView.d.ts +2 -3
- package/dist/features/admin/components/AdminBidsView.js +64 -98
- package/dist/features/admin/components/AdminBlogView.d.ts +3 -7
- package/dist/features/admin/components/AdminBlogView.js +53 -83
- package/dist/features/admin/components/AdminBrandsView.d.ts +2 -3
- package/dist/features/admin/components/AdminBrandsView.js +84 -83
- package/dist/features/admin/components/AdminBundlesView.js +82 -125
- package/dist/features/admin/components/AdminCarouselView.d.ts +2 -2
- package/dist/features/admin/components/AdminCarouselView.js +17 -52
- package/dist/features/admin/components/AdminCartsView.d.ts +2 -3
- package/dist/features/admin/components/AdminCartsView.js +39 -88
- package/dist/features/admin/components/AdminCategoriesView.d.ts +3 -3
- package/dist/features/admin/components/AdminCategoriesView.js +98 -92
- package/dist/features/admin/components/AdminCategoryEditorView.js +2 -2
- package/dist/features/admin/components/AdminClassifiedView.d.ts +3 -0
- package/dist/features/admin/components/AdminClassifiedView.js +44 -0
- package/dist/features/admin/components/AdminContactView.d.ts +2 -2
- package/dist/features/admin/components/AdminContactView.js +104 -111
- package/dist/features/admin/components/AdminCouponsView.d.ts +3 -6
- package/dist/features/admin/components/AdminCouponsView.js +66 -86
- package/dist/features/admin/components/AdminDigitalCodesView.d.ts +3 -0
- package/dist/features/admin/components/AdminDigitalCodesView.js +51 -0
- package/dist/features/admin/components/AdminFaqsView.d.ts +4 -4
- package/dist/features/admin/components/AdminFaqsView.js +77 -33
- package/dist/features/admin/components/AdminFeaturesView.d.ts +2 -3
- package/dist/features/admin/components/AdminFeaturesView.js +57 -70
- package/dist/features/admin/components/AdminGroupedListingsView.d.ts +3 -0
- package/dist/features/admin/components/AdminGroupedListingsView.js +43 -0
- package/dist/features/admin/components/AdminHistoryView.d.ts +2 -3
- package/dist/features/admin/components/AdminHistoryView.js +35 -52
- package/dist/features/admin/components/AdminListingScaffold.d.ts +2 -2
- package/dist/features/admin/components/AdminListingScaffold.js +3 -3
- package/dist/features/admin/components/AdminLiveView.d.ts +3 -0
- package/dist/features/admin/components/AdminLiveView.js +50 -0
- package/dist/features/admin/components/AdminNewsletterView.d.ts +2 -2
- package/dist/features/admin/components/AdminNewsletterView.js +67 -97
- package/dist/features/admin/components/AdminNotificationsView.d.ts +2 -3
- package/dist/features/admin/components/AdminNotificationsView.js +67 -93
- package/dist/features/admin/components/AdminOrderEditorView.js +10 -10
- package/dist/features/admin/components/AdminOrdersView.d.ts +2 -3
- package/dist/features/admin/components/AdminOrdersView.js +93 -107
- package/dist/features/admin/components/AdminPayoutsView.d.ts +2 -3
- package/dist/features/admin/components/AdminPayoutsView.js +51 -91
- package/dist/features/admin/components/AdminPrizeDrawsView.d.ts +2 -3
- package/dist/features/admin/components/AdminPrizeDrawsView.js +39 -76
- package/dist/features/admin/components/AdminProductEditorView.js +3 -3
- package/dist/features/admin/components/AdminProductsView.d.ts +4 -4
- package/dist/features/admin/components/AdminProductsView.js +167 -163
- package/dist/features/admin/components/AdminReturnRequestsView.d.ts +1 -1
- package/dist/features/admin/components/AdminReturnRequestsView.js +64 -66
- package/dist/features/admin/components/AdminReviewsView.d.ts +3 -7
- package/dist/features/admin/components/AdminReviewsView.js +94 -110
- package/dist/features/admin/components/AdminScammersView.d.ts +2 -3
- package/dist/features/admin/components/AdminScammersView.js +53 -97
- package/dist/features/admin/components/AdminSectionsView.js +1 -1
- package/dist/features/admin/components/AdminSessionsView.d.ts +2 -3
- package/dist/features/admin/components/AdminSessionsView.js +62 -89
- package/dist/features/admin/components/AdminStoreAddressesView.d.ts +1 -1
- package/dist/features/admin/components/AdminStoreAddressesView.js +39 -53
- package/dist/features/admin/components/AdminStoreEditorView.js +21 -8
- package/dist/features/admin/components/AdminStoresView.d.ts +2 -3
- package/dist/features/admin/components/AdminStoresView.js +67 -99
- package/dist/features/admin/components/AdminSublistingCategoriesView.d.ts +1 -1
- package/dist/features/admin/components/AdminSublistingCategoriesView.js +28 -44
- package/dist/features/admin/components/AdminSupportTicketDetailView.d.ts +10 -1
- package/dist/features/admin/components/AdminSupportTicketDetailView.js +191 -5
- package/dist/features/admin/components/AdminSupportTicketsView.d.ts +2 -3
- package/dist/features/admin/components/AdminSupportTicketsView.js +65 -96
- package/dist/features/admin/components/AdminTeamView.d.ts +2 -2
- package/dist/features/admin/components/AdminTeamView.js +40 -74
- package/dist/features/admin/components/AdminUserEditorView.d.ts +11 -1
- package/dist/features/admin/components/AdminUserEditorView.js +80 -6
- package/dist/features/admin/components/AdminUsersView.d.ts +2 -3
- package/dist/features/admin/components/AdminUsersView.js +108 -100
- package/dist/features/admin/components/AdminViewCards.d.ts +1 -1
- package/dist/features/admin/components/AdminWishlistsView.d.ts +2 -3
- package/dist/features/admin/components/AdminWishlistsView.js +36 -62
- package/dist/features/admin/components/DataListingView.d.ts +122 -0
- package/dist/features/admin/components/DataListingView.js +66 -0
- package/dist/features/admin/components/index.d.ts +10 -4
- package/dist/features/admin/components/index.js +6 -2
- package/dist/features/admin/constants/filter-tabs.d.ts +23 -17
- package/dist/features/admin/constants/filter-tabs.js +21 -17
- package/dist/features/admin/hooks/useAdminListing.d.ts +1 -0
- package/dist/features/admin/hooks/useAdminListing.js +1 -1
- package/dist/features/admin/schemas/firestore.js +6 -7
- package/dist/features/auctions/server.d.ts +0 -1
- package/dist/features/auctions/server.js +0 -1
- package/dist/features/auth/actions/realtime-token-actions.js +1 -1
- package/dist/features/auth/repository/user.repository.js +1 -1
- package/dist/features/cart/repository/cart.repository.d.ts +4 -1
- package/dist/features/cart/repository/cart.repository.js +12 -0
- package/dist/features/checkout/hooks/useCheckoutApi.d.ts +0 -2
- package/dist/features/checkout/index.d.ts +1 -1
- package/dist/features/events/components/AdminEventsView.d.ts +3 -3
- package/dist/features/events/components/AdminEventsView.js +50 -86
- package/dist/features/events/components/EventOfferCard.d.ts +19 -0
- package/dist/features/events/components/EventOfferCard.js +29 -0
- package/dist/features/events/components/EventRaffleEntryForm.d.ts +25 -0
- package/dist/features/events/components/EventRaffleEntryForm.js +37 -0
- package/dist/features/events/components/EventRafflesSection.d.ts +1 -4
- package/dist/features/events/components/EventRafflesSection.js +17 -6
- package/dist/features/events/components/EventSaleBanner.d.ts +21 -0
- package/dist/features/events/components/EventSaleBanner.js +18 -0
- package/dist/features/events/components/index.d.ts +6 -0
- package/dist/features/events/components/index.js +4 -0
- package/dist/features/events/repository/event-entry.repository.d.ts +5 -0
- package/dist/features/events/repository/event-entry.repository.js +7 -0
- package/dist/features/filters/AsyncFacetSection.d.ts +1 -1
- package/dist/features/homepage/components/FAQSection.d.ts +0 -5
- package/dist/features/homepage/lib/live-stats.js +1 -1
- package/dist/features/homepage/schemas/firestore.d.ts +0 -2
- package/dist/features/layout/index.d.ts +0 -2
- package/dist/features/layout/index.js +0 -1
- package/dist/features/pre-orders/server.d.ts +0 -1
- package/dist/features/pre-orders/server.js +0 -1
- package/dist/features/products/components/GroupSettingsPanel.js +2 -2
- package/dist/features/products/components/PrizeDrawItemsEditor.js +2 -2
- package/dist/features/products/components/PrizeDrawsSection.d.ts +1 -4
- package/dist/features/products/components/PrizeDrawsSection.js +17 -6
- package/dist/features/products/components/PrizeRevealModal.js +1 -1
- package/dist/features/products/components/ProductCardMetadataSection.d.ts +11 -0
- package/dist/features/products/components/ProductCardMetadataSection.js +25 -0
- package/dist/features/products/components/ProductDetailPageView.js +3 -1
- package/dist/features/products/components/ProductFeaturesSelector.js +1 -1
- package/dist/features/products/components/ProductGradingTab.d.ts +13 -0
- package/dist/features/products/components/ProductGradingTab.js +25 -0
- package/dist/features/products/components/ProductGrid.js +14 -2
- package/dist/features/products/components/ProductTabsShell.d.ts +5 -1
- package/dist/features/products/components/ProductTabsShell.js +6 -1
- package/dist/features/products/components/index.d.ts +4 -0
- package/dist/features/products/components/index.js +3 -0
- package/dist/features/products/constants/action-defs.d.ts +5 -0
- package/dist/features/products/constants/action-defs.js +52 -32
- package/dist/features/products/constants/sieve.d.ts +24 -4
- package/dist/features/products/constants/sieve.js +20 -11
- package/dist/features/products/schemas/firestore.d.ts +17 -0
- package/dist/features/products/schemas/product-features.validators.d.ts +2 -2
- package/dist/features/products/schemas/product-templates.d.ts +2 -4
- package/dist/features/products/types/index.d.ts +4 -0
- package/dist/features/promotions/server.d.ts +0 -1
- package/dist/features/promotions/server.js +0 -1
- package/dist/features/search/api/route.js +44 -1
- package/dist/features/search/components/Search.js +2 -0
- package/dist/features/search/hooks/useNavSuggestions.d.ts +5 -1
- package/dist/features/search/hooks/useNavSuggestions.js +31 -4
- package/dist/features/seller/components/BrandInlineSelect.js +2 -2
- package/dist/features/seller/components/CategoryInlineSelect.js +2 -2
- package/dist/features/seller/components/SellerAddressesView.js +1 -1
- package/dist/features/seller/components/SellerAuctionsView.d.ts +3 -3
- package/dist/features/seller/components/SellerAuctionsView.js +60 -90
- package/dist/features/seller/components/SellerBidsView.js +1 -1
- package/dist/features/seller/components/SellerBundlesView.js +64 -89
- package/dist/features/seller/components/SellerClassifiedView.js +67 -90
- package/dist/features/seller/components/SellerCouponEditorView.js +1 -1
- package/dist/features/seller/components/SellerCouponsView.d.ts +2 -2
- package/dist/features/seller/components/SellerCouponsView.js +45 -84
- package/dist/features/seller/components/SellerDigitalCodesView.js +63 -88
- package/dist/features/seller/components/SellerGroupedListingsView.js +33 -49
- package/dist/features/seller/components/SellerLiveView.js +63 -84
- package/dist/features/seller/components/SellerOffersPanel.js +1 -1
- package/dist/features/seller/components/SellerOffersView.d.ts +2 -2
- package/dist/features/seller/components/SellerOffersView.js +37 -69
- package/dist/features/seller/components/SellerOrdersView.d.ts +2 -2
- package/dist/features/seller/components/SellerOrdersView.js +4 -4
- package/dist/features/seller/components/SellerPayoutMethodsView.js +1 -1
- package/dist/features/seller/components/SellerPayoutsView.d.ts +2 -2
- package/dist/features/seller/components/SellerPayoutsView.js +34 -68
- package/dist/features/seller/components/SellerPreOrdersView.d.ts +2 -2
- package/dist/features/seller/components/SellerPreOrdersView.js +49 -82
- package/dist/features/seller/components/SellerPrizeDrawsView.d.ts +2 -2
- package/dist/features/seller/components/SellerPrizeDrawsView.js +49 -82
- package/dist/features/seller/components/SellerProductShell.d.ts +1 -1
- package/dist/features/seller/components/SellerProductsView.d.ts +2 -2
- package/dist/features/seller/components/SellerProductsView.js +15 -10
- package/dist/features/seller/components/SellerShippingConfigsView.js +1 -1
- package/dist/features/seller/components/SellerSidebar.d.ts +0 -4
- package/dist/features/seller/components/SellerSidebar.js +0 -2
- package/dist/features/seller/components/SellerStoreCategoriesView.js +1 -1
- package/dist/features/seller/components/SellerTemplatesView.js +2 -2
- package/dist/features/seller/components/SellersListView.d.ts +1 -2
- package/dist/features/seller/components/analytics/SellerAnalyticsStats.d.ts +5 -0
- package/dist/features/seller/components/analytics/SellerAnalyticsStats.js +2 -8
- package/dist/features/seller/components/index.d.ts +0 -3
- package/dist/features/seller/components/index.js +0 -2
- package/dist/features/seller/server.d.ts +0 -1
- package/dist/features/seller/server.js +0 -1
- package/dist/features/shell/FormShell.d.ts +1 -1
- package/dist/features/shell/FormShell.js +4 -1
- package/dist/features/shell/field-groups/ImageFieldGroup.d.ts +18 -0
- package/dist/features/shell/field-groups/ImageFieldGroup.js +15 -0
- package/dist/features/shell/field-groups/SeoFieldGroup.d.ts +22 -0
- package/dist/features/shell/field-groups/SeoFieldGroup.js +16 -0
- package/dist/features/shell/field-groups/StatusFieldGroup.d.ts +35 -0
- package/dist/features/shell/field-groups/StatusFieldGroup.js +16 -0
- package/dist/features/shell/field-groups/TitleDescriptionGroup.d.ts +40 -0
- package/dist/features/shell/field-groups/TitleDescriptionGroup.js +16 -0
- package/dist/features/shell/field-groups/index.d.ts +13 -0
- package/dist/features/shell/field-groups/index.js +9 -0
- package/dist/features/shell/index.d.ts +2 -0
- package/dist/features/shell/index.js +1 -0
- package/dist/features/stores/components/StoreCapabilitiesGuideView.d.ts +1 -2
- package/dist/features/stores/components/StoreFinanceGuideView.d.ts +1 -2
- package/dist/features/stores/components/StoreHeader.js +2 -1
- package/dist/features/stores/components/StoreListingsGuideView.d.ts +1 -2
- package/dist/features/stores/components/StoreOrdersGuideView.d.ts +1 -2
- package/dist/features/stores/components/StoreScopedSearch.d.ts +16 -0
- package/dist/features/stores/components/StoreScopedSearch.js +27 -0
- package/dist/features/stores/components/StoreSettingsGuideView.d.ts +1 -2
- package/dist/features/stores/server.d.ts +0 -1
- package/dist/features/stores/server.js +0 -1
- package/dist/features/support/schemas/firestore.d.ts +22 -1
- package/dist/features/support/schemas/firestore.js +10 -0
- package/dist/features/wishlist/server.d.ts +0 -1
- package/dist/features/wishlist/server.js +0 -1
- package/dist/index.d.ts +9 -27
- package/dist/index.js +14 -50
- package/dist/next/routing/route-map.d.ts +8 -0
- package/dist/next/routing/route-map.js +4 -0
- package/dist/react/hooks/useListingTypeFlags.d.ts +32 -0
- package/dist/react/hooks/useListingTypeFlags.js +19 -0
- package/dist/security/index.d.ts +1 -1
- package/dist/security/index.js +1 -1
- package/dist/security/pii-redact.d.ts +8 -0
- package/dist/security/pii-redact.js +10 -1
- package/dist/seed/grouped-listings-seed-data.d.ts +4 -0
- package/dist/seed/products-preorders-seed-data.d.ts +4 -0
- package/dist/seed/products-prize-draws-seed-data.js +3 -0
- package/dist/seed/site-settings-seed-data.js +5 -5
- package/dist/seed/sub-listings-seed-data.d.ts +4 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +4 -0
- package/dist/styles.css +54 -395
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/BaseListingCard.d.ts +10 -0
- package/dist/ui/components/BaseListingCard.js +17 -1
- package/dist/ui/components/DetailPageGallery.d.ts +32 -0
- package/dist/ui/components/DetailPageGallery.js +47 -0
- package/dist/ui/components/DetailPageHero.d.ts +30 -0
- package/dist/ui/components/DetailPageHero.js +22 -0
- package/dist/ui/components/DetailPageTabs.d.ts +34 -0
- package/dist/ui/components/DetailPageTabs.js +38 -0
- package/dist/ui/components/HorizontalScroller.style.css +15 -2
- package/dist/ui/components/ListingLayout.d.ts +11 -2
- package/dist/ui/components/ListingLayout.js +12 -6
- package/dist/ui/components/PaginatedSelect.d.ts +66 -0
- package/dist/ui/components/PaginatedSelect.js +201 -0
- package/dist/ui/components/PaginatedSelect.style.css +254 -0
- package/dist/ui/components/Semantic.d.ts +0 -4
- package/dist/ui/components/Semantic.js +0 -2
- package/dist/ui/components/SettingsSection.d.ts +47 -0
- package/dist/ui/components/SettingsSection.js +38 -0
- package/dist/ui/components/index.style.css +1 -4
- package/dist/ui/index.d.ts +12 -14
- package/dist/ui/index.js +6 -7
- package/dist/utils/id-generators.d.ts +1 -18
- package/dist/utils/id-generators.js +51 -18
- package/dist/validation/schemas.d.ts +25 -0
- package/dist/validation/schemas.js +28 -0
- package/package.json +1 -1
|
@@ -2,16 +2,27 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import Link from "next/link";
|
|
3
3
|
import { Container, Heading, Section, Stack, Text, } from "../../../ui";
|
|
4
4
|
import { ROUTES } from "../../../next";
|
|
5
|
+
import { eventRepository } from "../repository/events.repository";
|
|
6
|
+
import { EventCard } from "./EventCard";
|
|
5
7
|
/**
|
|
6
8
|
* Renders a live-raffle/spin-wheel strip on the homepage.
|
|
7
|
-
*
|
|
8
|
-
* Reads events with `hasRaffle=true` and status in {"active","upcoming"}. The
|
|
9
|
-
* `hasRaffle` flag is added later with the raffle feature; for now the
|
|
10
|
-
* component renders an empty state and the section ships disabled in seed.
|
|
9
|
+
* W1-38 (2026-05-23): fetches events with hasRaffle=true and renders the EventCard grid.
|
|
11
10
|
*/
|
|
12
11
|
export async function EventRafflesSection({ config, }) {
|
|
13
12
|
const title = config.title ?? "Live Raffles & Spin Wheels";
|
|
14
13
|
const subtitle = config.subtitle ?? "Participate in community events and win prizes";
|
|
15
|
-
const
|
|
16
|
-
|
|
14
|
+
const limit = 6;
|
|
15
|
+
let events = [];
|
|
16
|
+
try {
|
|
17
|
+
const result = await eventRepository.list({
|
|
18
|
+
filters: "hasRaffle==true,status==active",
|
|
19
|
+
sorts: "-startsAt",
|
|
20
|
+
pageSize: limit,
|
|
21
|
+
});
|
|
22
|
+
events = (result.items ?? []);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
events = [];
|
|
26
|
+
}
|
|
27
|
+
return (_jsx(Section, { className: "py-10", children: _jsx(Container, { size: "xl", children: _jsxs(Stack, { gap: "md", children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Heading, { level: 2, className: "text-2xl font-semibold text-zinc-900 dark:text-zinc-50", children: title }), subtitle ? (_jsx(Text, { className: "text-sm text-zinc-500 dark:text-zinc-400", children: subtitle })) : null] }), events.length === 0 ? (_jsxs(Stack, { align: "center", gap: "sm", className: "rounded-2xl border border-dashed border-zinc-300 px-6 py-12 text-center dark:border-slate-700", children: [_jsx(Text, { className: "text-sm text-zinc-500 dark:text-zinc-400", children: "No upcoming raffles \u2014 keep an eye on the events page." }), _jsx(Link, { href: String(ROUTES.PUBLIC.EVENTS), className: "text-sm font-medium text-primary hover:underline", children: "Browse all events \u2192" })] })) : (_jsx("div", { className: "fluid-grid-card gap-3", children: events.map((event) => (_jsx(EventCard, { event: event }, event.id))) }))] }) }) }));
|
|
17
28
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface EventSaleBannerProps {
|
|
2
|
+
/** Discount percent (e.g. 20 = 20% off). */
|
|
3
|
+
discountPercent?: number;
|
|
4
|
+
/** Coupon code (when the sale is gated by a code). */
|
|
5
|
+
couponCode?: string;
|
|
6
|
+
/** Sale headline override. */
|
|
7
|
+
title?: string;
|
|
8
|
+
/** Sale subheading override. */
|
|
9
|
+
subtitle?: string;
|
|
10
|
+
/** Filter the catalog page to a specific category / tag. */
|
|
11
|
+
filterParam?: {
|
|
12
|
+
key: string;
|
|
13
|
+
value: string;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* `EventSaleBanner` — W1-18 — sale event UI noted as missing in the plan.
|
|
18
|
+
* Renders a hero banner with the discount, optional coupon, and a CTA to the
|
|
19
|
+
* filtered products catalog.
|
|
20
|
+
*/
|
|
21
|
+
export declare function EventSaleBanner({ discountPercent, couponCode, title, subtitle, filterParam, }: EventSaleBannerProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import Link from "next/link";
|
|
4
|
+
import { Div, Heading, Stack, Text } from "../../../ui";
|
|
5
|
+
import { Button } from "../../../ui/components/Button";
|
|
6
|
+
import { ROUTES } from "../../../next/routing/route-map";
|
|
7
|
+
/**
|
|
8
|
+
* `EventSaleBanner` — W1-18 — sale event UI noted as missing in the plan.
|
|
9
|
+
* Renders a hero banner with the discount, optional coupon, and a CTA to the
|
|
10
|
+
* filtered products catalog.
|
|
11
|
+
*/
|
|
12
|
+
export function EventSaleBanner({ discountPercent, couponCode, title, subtitle, filterParam, }) {
|
|
13
|
+
const headline = title ?? (discountPercent ? `${discountPercent}% off, today only` : "Special sale");
|
|
14
|
+
const href = filterParam
|
|
15
|
+
? `${ROUTES.PUBLIC.PRODUCTS}?${encodeURIComponent(filterParam.key)}=${encodeURIComponent(filterParam.value)}`
|
|
16
|
+
: `${ROUTES.PUBLIC.PRODUCTS}?onSale=true`;
|
|
17
|
+
return (_jsx(Div, { className: "rounded-2xl bg-gradient-to-br from-[var(--appkit-color-primary)] to-[var(--appkit-color-secondary)] p-6 text-white shadow-lg", children: _jsxs(Stack, { gap: "md", children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Heading, { level: 2, className: "text-3xl font-bold", children: headline }), subtitle ? _jsx(Text, { className: "text-base text-white/90", children: subtitle }) : null] }), couponCode ? (_jsxs(Div, { className: "inline-flex items-center gap-3 rounded-lg bg-white/10 px-4 py-2 backdrop-blur", children: [_jsx("span", { className: "text-xs uppercase tracking-wide opacity-80", children: "Code" }), _jsx("span", { className: "font-mono text-lg font-bold", children: couponCode })] })) : null, _jsx(Div, { children: _jsx(Link, { href: href, children: _jsx(Button, { variant: "secondary", children: "Shop the sale" }) }) })] }) }));
|
|
18
|
+
}
|
|
@@ -29,3 +29,9 @@ export { EventRaffleWinnerView } from "./EventRaffleWinnerView";
|
|
|
29
29
|
export type { EventRaffleWinnerViewProps } from "./EventRaffleWinnerView";
|
|
30
30
|
export { SpinWheelView } from "./SpinWheelView";
|
|
31
31
|
export type { SpinWheelViewProps } from "./SpinWheelView";
|
|
32
|
+
export { EventRaffleEntryForm } from "./EventRaffleEntryForm";
|
|
33
|
+
export type { EventRaffleEntryFormProps } from "./EventRaffleEntryForm";
|
|
34
|
+
export { EventSaleBanner } from "./EventSaleBanner";
|
|
35
|
+
export type { EventSaleBannerProps } from "./EventSaleBanner";
|
|
36
|
+
export { EventOfferCard } from "./EventOfferCard";
|
|
37
|
+
export type { EventOfferCardProps } from "./EventOfferCard";
|
|
@@ -15,3 +15,7 @@ export { EventPollWidget } from "./EventPollWidget";
|
|
|
15
15
|
export { EventRafflesSection } from "./EventRafflesSection";
|
|
16
16
|
export { EventRaffleWinnerView } from "./EventRaffleWinnerView";
|
|
17
17
|
export { SpinWheelView } from "./SpinWheelView";
|
|
18
|
+
// W1-18: Event participation flows (raffle entry / sale banner / offer coupon)
|
|
19
|
+
export { EventRaffleEntryForm } from "./EventRaffleEntryForm";
|
|
20
|
+
export { EventSaleBanner } from "./EventSaleBanner";
|
|
21
|
+
export { EventOfferCard } from "./EventOfferCard";
|
|
@@ -7,6 +7,11 @@ declare class EventEntryRepository extends BaseRepository<EventEntryDocument> {
|
|
|
7
7
|
protected mapDoc<D = EventEntryDocument>(snap: import("../../../providers/db-firebase").DocumentSnapshot): D;
|
|
8
8
|
listForEvent(eventId: string, model: SieveModel): Promise<FirebaseSieveResult<EventEntryDocument>>;
|
|
9
9
|
listForUser(userId: string, model: SieveModel): Promise<FirebaseSieveResult<EventEntryDocument>>;
|
|
10
|
+
/**
|
|
11
|
+
* W1-42 — admin-facing list (all events, all users). Filterable on eventId,
|
|
12
|
+
* userId, reviewStatus, submittedAt; sortable on submittedAt + points.
|
|
13
|
+
*/
|
|
14
|
+
list(model: SieveModel): Promise<FirebaseSieveResult<EventEntryDocument>>;
|
|
10
15
|
hasUserEntered(eventId: string, userId: string): Promise<boolean>;
|
|
11
16
|
countUserEntries(eventId: string, userId: string): Promise<number>;
|
|
12
17
|
getLeaderboard(eventId: string, limit?: number): Promise<import("../types").LeaderboardEntry[]>;
|
|
@@ -23,6 +23,13 @@ class EventEntryRepository extends BaseRepository {
|
|
|
23
23
|
baseQuery: this.getCollection().where(EVENT_ENTRY_FIELDS.USER_ID, "==", userId),
|
|
24
24
|
});
|
|
25
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* W1-42 — admin-facing list (all events, all users). Filterable on eventId,
|
|
28
|
+
* userId, reviewStatus, submittedAt; sortable on submittedAt + points.
|
|
29
|
+
*/
|
|
30
|
+
async list(model) {
|
|
31
|
+
return this.sieveQuery(model, EventEntryRepository.SIEVE_FIELDS);
|
|
32
|
+
}
|
|
26
33
|
async hasUserEntered(eventId, userId) {
|
|
27
34
|
try {
|
|
28
35
|
const snapshot = await this.getCollection()
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { FacetOption } from "./FilterFacetSection";
|
|
2
|
-
import type { AsyncPage } from "../../ui/components/
|
|
2
|
+
import type { AsyncPage } from "../../ui/components/PaginatedSelect";
|
|
3
3
|
export interface AsyncFacetSectionProps {
|
|
4
4
|
title: string;
|
|
5
5
|
selected: string[];
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import React from "react";
|
|
2
1
|
export interface FAQTab {
|
|
3
2
|
value: string;
|
|
4
3
|
label: string;
|
|
@@ -26,10 +25,6 @@ export interface FAQSectionProps {
|
|
|
26
25
|
viewMoreLabel?: string;
|
|
27
26
|
hasMore?: boolean;
|
|
28
27
|
moreCount?: number;
|
|
29
|
-
/** @deprecated Tabs are now derived internally or from the tabs prop */
|
|
30
|
-
activeTab?: string;
|
|
31
|
-
/** @deprecated Use showCategoryTabs + tabs props instead */
|
|
32
|
-
renderTabs?: () => React.ReactNode;
|
|
33
28
|
className?: string;
|
|
34
29
|
}
|
|
35
30
|
export declare function FAQSection({ title, subtitle, items, tabs: tabsProp, showCategoryTabs, allowMultipleOpen, defaultOpenCount, viewMoreHref, viewMoreLabel, hasMore, moreCount, className, }: FAQSectionProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -70,7 +70,7 @@ export async function fetchLiveStats(requests) {
|
|
|
70
70
|
tasks.push((async () => {
|
|
71
71
|
try {
|
|
72
72
|
const db = getAdminDb();
|
|
73
|
-
|
|
73
|
+
const ref = db.collection(collection);
|
|
74
74
|
const query = filterField !== undefined && filterValue !== undefined
|
|
75
75
|
? ref.where(filterField, "==", filterValue)
|
|
76
76
|
: ref;
|
|
@@ -61,8 +61,6 @@ export interface CarouselCard {
|
|
|
61
61
|
/** Legacy — kept for backward compat; use `zone` instead */
|
|
62
62
|
gridCol?: 1 | 2 | 3;
|
|
63
63
|
}
|
|
64
|
-
/** @deprecated Use CarouselCard. This alias is kept for backward compatibility. */
|
|
65
|
-
export type GridCard = CarouselCard;
|
|
66
64
|
export type GridCardCreateInput = Omit<CarouselCard, "id">;
|
|
67
65
|
/** A named carousel that holds up to MAX_SLIDES_PER_CAROUSEL slide references. */
|
|
68
66
|
export interface CarouselDocument {
|
|
@@ -22,8 +22,6 @@ export { LocaleSwitcher } from "./LocaleSwitcher";
|
|
|
22
22
|
export type { LocaleSwitcherProps, LocaleSwitcherOption, } from "./LocaleSwitcher";
|
|
23
23
|
export { BackToTop, SkipToMain } from "./BackToTop";
|
|
24
24
|
export type { BackToTopProps } from "./BackToTop";
|
|
25
|
-
export { ListingLayout } from "./ListingLayout";
|
|
26
|
-
export type { ListingLayoutProps, ListingLayoutLabels } from "./ListingLayout";
|
|
27
25
|
export { LayoutClient } from "./LayoutClient";
|
|
28
26
|
export type { LayoutClientProps, LayoutProvider } from "./LayoutClient";
|
|
29
27
|
export { AppLayoutShell } from "./AppLayoutShell";
|
|
@@ -12,7 +12,6 @@ export { AutoBreadcrumbs } from "./AutoBreadcrumbs";
|
|
|
12
12
|
export { Breadcrumbs, BreadcrumbItem } from "./Breadcrumbs";
|
|
13
13
|
export { LocaleSwitcher } from "./LocaleSwitcher";
|
|
14
14
|
export { BackToTop, SkipToMain } from "./BackToTop";
|
|
15
|
-
export { ListingLayout } from "./ListingLayout";
|
|
16
15
|
export { LayoutClient } from "./LayoutClient";
|
|
17
16
|
export { AppLayoutShell } from "./AppLayoutShell";
|
|
18
17
|
export { BottomActionsProvider, useBottomActionsContext, } from "./BottomActionsContext";
|
|
@@ -1,7 +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 { Button, ConfirmDeleteModal, Div, FormField, Heading, Modal, Row, Select, SideDrawer, Stack, Text, Tabs, TabsContent, TabsList, TabsTrigger,
|
|
4
|
+
import { Button, ConfirmDeleteModal, Div, FormField, Heading, Modal, Row, Select, SideDrawer, Stack, Text, Tabs, TabsContent, TabsList, TabsTrigger, PaginatedSelect, useToast, } from "../../../ui";
|
|
5
5
|
import { apiClient } from "../../../http";
|
|
6
6
|
const CONDITION_OPTIONS = [
|
|
7
7
|
{ value: "new", label: "New" },
|
|
@@ -204,5 +204,5 @@ export function GroupSettingsPanel({ productId, productSlug, groupId, isGroupPar
|
|
|
204
204
|
_jsx("img", { src: child.images[0], alt: child.title, className: "w-8 h-8 rounded-full object-cover border border-zinc-200 dark:border-zinc-700" })) : (_jsx("div", { className: "w-8 h-8 rounded-full bg-zinc-100 dark:bg-zinc-800" })), _jsx(Text, { className: "text-sm text-zinc-800 dark:text-zinc-200 flex-1 truncate", children: child.title }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => requestUnlinkChild(child.id), isLoading: loading, className: "text-red-500 hover:text-red-600 text-xs", children: "Unlink" })] }, child.id)))] }))] }), _jsxs(Row, { gap: "sm", className: "flex-wrap", children: [_jsx(Button, { type: "button", variant: "secondary", size: "sm", onClick: () => setShowAddModal(true), children: "Add child listing" }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: requestDissolveGroup, isLoading: loading, className: "text-red-500 hover:text-red-600", children: "Dissolve group" })] })] })), !isGroupParent && groupParentSlug && (_jsxs(Stack, { gap: "xs", children: [_jsxs(Text, { className: "text-sm text-zinc-600 dark:text-zinc-400", children: ["Part of:", " ", _jsx("span", { className: "font-medium text-zinc-800 dark:text-zinc-200", children: groupTitle ?? groupParentSlug })] }), _jsxs(Text, { className: "text-xs text-zinc-500 dark:text-zinc-400", children: ["Parent listing: ", _jsx("code", { className: "font-mono", children: groupParentSlug })] }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: requestLeaveGroup, isLoading: loading, className: "text-red-500 hover:text-red-600 w-fit", children: "Leave group" })] }))] })), (groupChildSlugs?.length ?? 0) >= 4 ? (_jsx(SideDrawer, { isOpen: showAddModal, onClose: () => setShowAddModal(false), title: "Add child listing", children: _jsx(AddChildContent, { addTab: addTab, setAddTab: setAddTab, createForm: createForm, setCreateForm: setCreateForm, productSlug: productSlug, linkTarget: linkTarget, setLinkTarget: setLinkTarget, loadLinkOptions: loadLinkOptions, onAddCreate: addCreateChild, onAddLink: addLinkChild, loading: loading }) })) : (_jsx(Modal, { open: showAddModal, onClose: () => setShowAddModal(false), title: "Add child listing", size: "lg", children: _jsx(AddChildContent, { addTab: addTab, setAddTab: setAddTab, createForm: createForm, setCreateForm: setCreateForm, productSlug: productSlug, linkTarget: linkTarget, setLinkTarget: setLinkTarget, loadLinkOptions: loadLinkOptions, onAddCreate: addCreateChild, onAddLink: addLinkChild, loading: loading }) })), confirmAction && (_jsx(ConfirmDeleteModal, { isOpen: true, title: confirmAction.title, message: confirmAction.message, onConfirm: confirmAction.onConfirm, onClose: () => setConfirmAction(null), isDeleting: loading }))] }));
|
|
205
205
|
}
|
|
206
206
|
function AddChildContent({ addTab, setAddTab, createForm, setCreateForm, productSlug, linkTarget, setLinkTarget, loadLinkOptions, onAddCreate, onAddLink, loading, }) {
|
|
207
|
-
return (_jsx(Stack, { gap: "md", children: _jsxs(Tabs, { value: addTab, onChange: (v) => setAddTab(v), children: [_jsxs(TabsList, { children: [_jsx(TabsTrigger, { value: "create", children: "Create new child" }), _jsx(TabsTrigger, { value: "link", children: "Link existing" })] }), _jsx(TabsContent, { value: "create", children: _jsxs(Stack, { gap: "sm", className: "mt-4", children: [_jsx(FormField, { name: "childTitle", label: "Title *", type: "text", value: createForm.title, onChange: (v) => setCreateForm({ ...createForm, title: v }), placeholder: `${productSlug}-part` }), _jsx(FormField, { name: "childPrice", label: "Price (\u20B9) *", type: "number", value: createForm.price, onChange: (v) => setCreateForm({ ...createForm, price: v }), placeholder: "0" }), _jsx(Select, { label: "Condition", value: createForm.condition, onValueChange: (v) => setCreateForm({ ...createForm, condition: v }), options: CONDITION_OPTIONS }), _jsx(Text, { className: "text-xs text-zinc-400 dark:text-zinc-400/80", children: "Other fields (category, brand, shipping, return policy) are inherited from this parent listing. Need more control? Edit the full listing after saving." }), _jsx(Button, { type: "button", onClick: onAddCreate, isLoading: loading, disabled: !createForm.title || !createForm.price, children: "Create and link child" })] }) }), _jsx(TabsContent, { value: "link", children: _jsxs(Stack, { gap: "sm", className: "mt-4", children: [_jsx(Text, { className: "text-xs text-zinc-500 dark:text-zinc-400", children: "Search your existing products or pre-orders. Auctions cannot be linked." }), _jsx(
|
|
207
|
+
return (_jsx(Stack, { gap: "md", children: _jsxs(Tabs, { value: addTab, onChange: (v) => setAddTab(v), children: [_jsxs(TabsList, { children: [_jsx(TabsTrigger, { value: "create", children: "Create new child" }), _jsx(TabsTrigger, { value: "link", children: "Link existing" })] }), _jsx(TabsContent, { value: "create", children: _jsxs(Stack, { gap: "sm", className: "mt-4", children: [_jsx(FormField, { name: "childTitle", label: "Title *", type: "text", value: createForm.title, onChange: (v) => setCreateForm({ ...createForm, title: v }), placeholder: `${productSlug}-part` }), _jsx(FormField, { name: "childPrice", label: "Price (\u20B9) *", type: "number", value: createForm.price, onChange: (v) => setCreateForm({ ...createForm, price: v }), placeholder: "0" }), _jsx(Select, { label: "Condition", value: createForm.condition, onValueChange: (v) => setCreateForm({ ...createForm, condition: v }), options: CONDITION_OPTIONS }), _jsx(Text, { className: "text-xs text-zinc-400 dark:text-zinc-400/80", children: "Other fields (category, brand, shipping, return policy) are inherited from this parent listing. Need more control? Edit the full listing after saving." }), _jsx(Button, { type: "button", onClick: onAddCreate, isLoading: loading, disabled: !createForm.title || !createForm.price, children: "Create and link child" })] }) }), _jsx(TabsContent, { value: "link", children: _jsxs(Stack, { gap: "sm", className: "mt-4", children: [_jsx(Text, { className: "text-xs text-zinc-500 dark:text-zinc-400", children: "Search your existing products or pre-orders. Auctions cannot be linked." }), _jsx(PaginatedSelect, { value: linkTarget, onChange: (v) => setLinkTarget(v), loadOptions: loadLinkOptions, placeholder: "Search listings\u2026", searchPlaceholder: "Type title or slug\u2026", noResultsText: "No matching listings found", ariaLabel: "Listing to link" }), _jsx(Button, { type: "button", onClick: onAddLink, isLoading: loading, disabled: !linkTarget, children: "Link to group" })] }) })] }) }));
|
|
208
208
|
}
|
|
@@ -84,7 +84,7 @@ export function PrizeDrawItemsEditor({ items, onChange, onUploadImage, onUploadV
|
|
|
84
84
|
const nextImgs = (cur.images ?? []).filter((_, i) => i !== slot);
|
|
85
85
|
update(index, { images: nextImgs });
|
|
86
86
|
};
|
|
87
|
-
return (_jsxs(Stack, { gap: "md", children: [_jsxs(Row, { justify: "between", align: "center", children: [_jsxs(Heading, { level: 3, children: ["Prize Items (", items.length, ")"] }), _jsxs(Button, { type: "button", variant: "secondary", onClick: addItem, disabled: anyWon || items.length >= MAX_ITEMS, children: ["+ Add prize (", items.length, "/", MAX_ITEMS, ")"] })] }), _jsxs(Text, { className: "text-sm text-[var(--appkit-color-text-muted)]", children: ["Add between ", MIN_ITEMS, " and ", MAX_ITEMS, " prizes. Each entry will reveal exactly one of these. Items marked won during a prior reveal cannot be edited or removed."] }), anyWon ? (_jsxs(Div, { className: "rounded border border-
|
|
87
|
+
return (_jsxs(Stack, { gap: "md", children: [_jsxs(Row, { justify: "between", align: "center", children: [_jsxs(Heading, { level: 3, children: ["Prize Items (", items.length, ")"] }), _jsxs(Button, { type: "button", variant: "secondary", onClick: addItem, disabled: anyWon || items.length >= MAX_ITEMS, children: ["+ Add prize (", items.length, "/", MAX_ITEMS, ")"] })] }), _jsxs(Text, { className: "text-sm text-[var(--appkit-color-text-muted)]", children: ["Add between ", MIN_ITEMS, " and ", MAX_ITEMS, " prizes. Each entry will reveal exactly one of these. Items marked won during a prior reveal cannot be edited or removed."] }), anyWon ? (_jsxs(Div, { className: "rounded border border-error/40 bg-error-surface px-3 py-2 text-sm text-error", children: [_jsx(Span, { weight: "bold", children: "Draw locked." }), " At least one prize has been revealed \u2014 this listing can no longer be edited. To run a similar draw, clone it into a new prize-draw listing."] })) : warning ? (_jsx(Div, { className: "rounded border border-yellow-400/40 bg-yellow-50 px-3 py-2 text-sm text-yellow-900 dark:bg-yellow-900/30 dark:text-yellow-100", children: warning })) : null, _jsx(Stack, { gap: "md", children: items.map((it, index) => {
|
|
88
88
|
const locked = it.isWon || anyWon;
|
|
89
89
|
return (_jsxs(Div, { className: `relative rounded-lg border border-[var(--appkit-color-border)] p-4 ${locked ? "opacity-60" : ""}`, children: [locked ? (_jsx(Div, { className: "absolute inset-0 z-10 flex items-center justify-center bg-black/10 dark:bg-black/40 rounded-lg pointer-events-none", children: _jsx(Text, { className: "rounded bg-red-600 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-white", children: "Won \u2014 locked" }) })) : null, _jsxs(Row, { justify: "between", align: "center", className: "mb-3", children: [_jsxs(Heading, { level: 4, children: ["Prize #", it.itemNumber] }), _jsxs(Row, { gap: "xs", children: [_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => move(index, -1), disabled: locked || index === 0, "aria-label": "Move up", children: "\u2191" }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => move(index, 1), disabled: locked || index === items.length - 1, "aria-label": "Move down", children: "\u2193" }), _jsx(Button, { type: "button", variant: "danger", size: "sm", onClick: () => removeItem(index), disabled: locked || items.length <= MIN_ITEMS, children: "Remove" })] })] }), _jsxs(Stack, { gap: "sm", children: [_jsx(FormField, { name: `item-${index}-title`, label: "Title", type: "text", value: it.title, onChange: (v) => update(index, { title: v }), disabled: locked, placeholder: "e.g. PSA 9 Charizard Base Set Holo" }), _jsx(FormField, { name: `item-${index}-description`, label: "Description (optional)", type: "textarea", value: it.description ?? "", onChange: (v) => update(index, { description: v }), disabled: locked, placeholder: "What makes this prize special?" }), _jsxs(Row, { gap: "md", children: [_jsx(Div, { className: "flex-1", children: _jsx(FormField, { name: `item-${index}-condition`, label: "Condition", type: "select", value: it.condition, onChange: (v) => update(index, { condition: v }), disabled: locked, options: CONDITION_OPTIONS }) }), _jsx(Div, { className: "flex-1", children: _jsx(FormField, { name: `item-${index}-value`, label: "Estimated value (\u20B9)", type: "number", value: it.estimatedValue != null
|
|
90
90
|
? String(Math.round(it.estimatedValue / 100))
|
|
@@ -94,6 +94,6 @@ export function PrizeDrawItemsEditor({ items, onChange, onUploadImage, onUploadV
|
|
|
94
94
|
const existing = (it.images ?? [])[slot];
|
|
95
95
|
return (_jsxs(Div, { className: "w-40", children: [_jsx(ImageUpload, { currentImage: existing, label: existing ? "Replace" : `Image ${slot + 1}`, onUpload: (file) => onUploadImage(file, it.itemNumber), onChange: (url) => setImage(index, slot, url) }), existing && !locked ? (_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => removeImage(index, slot), children: "Remove" })) : null] }, slot));
|
|
96
96
|
}) })] }), onUploadVideo ? (_jsxs(Stack, { gap: "xs", children: [_jsx(Text, { className: "text-sm font-medium", children: "Video (optional)" }), _jsx("input", { type: "file", accept: "video/*", disabled: locked, onChange: (e) => handleVideoFileChange(e, index, it.itemNumber) }), it.video?.url ? (_jsxs(Text, { className: "text-xs text-[var(--appkit-color-text-muted)] truncate", children: ["Current: ", it.video.url] })) : null] })) : null] })] }, `prize-item-${it.itemNumber}-${index}`));
|
|
97
|
-
}) }), items.length < MIN_ITEMS ? (_jsxs(Div, { className: "rounded border border-
|
|
97
|
+
}) }), items.length < MIN_ITEMS ? (_jsxs(Div, { className: "rounded border border-error/40 bg-error-surface px-3 py-2 text-sm text-error", children: ["At least ", MIN_ITEMS, " prizes are required."] })) : null] }));
|
|
98
98
|
}
|
|
99
99
|
export default PrizeDrawItemsEditor;
|
|
@@ -4,9 +4,6 @@ export interface PrizeDrawsSectionProps {
|
|
|
4
4
|
}
|
|
5
5
|
/**
|
|
6
6
|
* Renders a prize-draws strip on the homepage.
|
|
7
|
-
*
|
|
8
|
-
* Reads products with `listingType="prize-draw"` and `prizeRevealStatus` in the
|
|
9
|
-
* configured set. Those fields are added later in the prize-draw block; for now
|
|
10
|
-
* the component renders an empty state and the section ships disabled in seed.
|
|
7
|
+
* W1-38 (2026-05-23): fetches active prize-draw listings from productRepository.
|
|
11
8
|
*/
|
|
12
9
|
export declare function PrizeDrawsSection({ config, }: PrizeDrawsSectionProps): Promise<import("react/jsx-runtime").JSX.Element>;
|
|
@@ -2,16 +2,27 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import Link from "next/link";
|
|
3
3
|
import { Container, Heading, Section, Stack, Text, } from "../../../ui";
|
|
4
4
|
import { ROUTES } from "../../../next";
|
|
5
|
+
import { productRepository } from "../repository/products.repository";
|
|
6
|
+
import { InteractiveProductCard } from "./InteractiveProductCard";
|
|
5
7
|
/**
|
|
6
8
|
* Renders a prize-draws strip on the homepage.
|
|
7
|
-
*
|
|
8
|
-
* Reads products with `listingType="prize-draw"` and `prizeRevealStatus` in the
|
|
9
|
-
* configured set. Those fields are added later in the prize-draw block; for now
|
|
10
|
-
* the component renders an empty state and the section ships disabled in seed.
|
|
9
|
+
* W1-38 (2026-05-23): fetches active prize-draw listings from productRepository.
|
|
11
10
|
*/
|
|
12
11
|
export async function PrizeDrawsSection({ config, }) {
|
|
13
12
|
const title = config.title ?? "Prize Draws";
|
|
14
13
|
const subtitle = config.subtitle ?? "Enter for a chance to win rare collectibles";
|
|
15
|
-
const
|
|
16
|
-
|
|
14
|
+
const limit = 8;
|
|
15
|
+
let draws = [];
|
|
16
|
+
try {
|
|
17
|
+
const result = await productRepository.list({
|
|
18
|
+
filters: "listingType==prize-draw,status==published",
|
|
19
|
+
sorts: "-createdAt",
|
|
20
|
+
pageSize: limit,
|
|
21
|
+
});
|
|
22
|
+
draws = (result.items ?? []);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
draws = [];
|
|
26
|
+
}
|
|
27
|
+
return (_jsx(Section, { className: "py-10", children: _jsx(Container, { size: "xl", children: _jsxs(Stack, { gap: "md", children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Heading, { level: 2, className: "text-2xl font-semibold text-zinc-900 dark:text-zinc-50", children: title }), subtitle ? (_jsx(Text, { className: "text-sm text-zinc-500 dark:text-zinc-400", children: subtitle })) : null] }), draws.length === 0 ? (_jsxs(Stack, { align: "center", gap: "sm", className: "rounded-2xl border border-dashed border-zinc-300 px-6 py-12 text-center dark:border-slate-700", children: [_jsx(Text, { className: "text-sm text-zinc-500 dark:text-zinc-400", children: "No active prize draws \u2014 new draws are announced regularly." }), _jsx(Link, { href: String(ROUTES.PUBLIC.AUCTIONS), className: "text-sm font-medium text-primary hover:underline", children: "Browse live auctions \u2192" })] })) : (_jsx("div", { className: "fluid-grid-card gap-3", children: draws.map((draw) => (_jsx(InteractiveProductCard, { product: draw, href: String(ROUTES.PUBLIC.PRODUCT_DETAIL(draw.slug ?? draw.id ?? "")) }, draw.id))) }))] }) }) }));
|
|
17
28
|
}
|
|
@@ -143,7 +143,7 @@ export function PrizeRevealModal({ open, onClose, items, orderId, productId, onR
|
|
|
143
143
|
}, REVEAL_DURATION_MS);
|
|
144
144
|
}, [onReveal, orderId, phase, productId, startAnimation]);
|
|
145
145
|
const winnerImg = winner?.images?.[0];
|
|
146
|
-
return (_jsxs(Modal, { isOpen: open, onClose: onClose, title: "Prize Reveal", size: "lg", children: [_jsx(LoginRequiredModal, { isOpen: showLoginModal, onClose: () => setShowLoginModal(false), message: "You need to be signed in to reveal your prize. Please log in or create an account to continue." }), _jsxs(Stack, { gap: "md", children: [phase === "refunded" ? (_jsxs(Div, { className: "rounded border border-yellow-400/40 bg-yellow-50 px-4 py-3 text-yellow-900 dark:bg-yellow-900/30 dark:text-yellow-100", children: [_jsx(Heading, { level: 3, className: "mb-1", children: "Pool exhausted \u2014 you've been refunded" }), _jsx(Text, { className: "text-sm", children: "Every prize in this draw was already claimed by the time your entry rolled. Your order has been marked refunded automatically." })] })) : null, phase === "error" ? (_jsxs(Div, { className: "rounded border border-
|
|
146
|
+
return (_jsxs(Modal, { isOpen: open, onClose: onClose, title: "Prize Reveal", size: "lg", children: [_jsx(LoginRequiredModal, { isOpen: showLoginModal, onClose: () => setShowLoginModal(false), message: "You need to be signed in to reveal your prize. Please log in or create an account to continue." }), _jsxs(Stack, { gap: "md", children: [phase === "refunded" ? (_jsxs(Div, { className: "rounded border border-yellow-400/40 bg-yellow-50 px-4 py-3 text-yellow-900 dark:bg-yellow-900/30 dark:text-yellow-100", children: [_jsx(Heading, { level: 3, className: "mb-1", children: "Pool exhausted \u2014 you've been refunded" }), _jsx(Text, { className: "text-sm", children: "Every prize in this draw was already claimed by the time your entry rolled. Your order has been marked refunded automatically." })] })) : null, phase === "error" ? (_jsxs(Div, { className: "rounded border border-error/40 bg-error-surface px-4 py-3 text-error", children: [_jsx(Text, { className: "text-sm font-semibold", children: "Something went wrong" }), _jsx(Text, { className: "text-sm", children: errorMessage })] })) : null, _jsx(PrizeDrawCollage, { items: items, hideWonState: true, highlightItemNumber: highlight }), phase === "idle" ? (_jsx(Button, { type: "button", variant: "primary", size: "lg", onClick: handleRevealClick, children: "\u2728 Reveal my prize" })) : null, phase === "revealing" ? (_jsxs(Div, { className: "rounded bg-[var(--appkit-color-surface-muted)] p-4 text-center", children: [_jsx(Text, { className: "text-lg font-semibold", children: "Rolling\u2026" }), _jsx(Text, { className: "text-sm text-[var(--appkit-color-text-muted)]", children: "The winner was locked by the server before this animation started. Hang tight \u2014 we're just making it look pretty." })] })) : null, phase === "won" && winner ? (_jsxs(Div, { className: "rounded-lg border-2 border-[var(--appkit-color-primary)] bg-[var(--appkit-color-surface)] p-4 text-center", children: [_jsx(Text, { className: "text-xs uppercase tracking-wider text-[var(--appkit-color-text-muted)]", children: "You won" }), _jsxs(Heading, { level: 2, className: "my-2", children: ["#", winner.itemNumber, " \u2014 ", winner.title] }), winnerImg ? (
|
|
147
147
|
/* eslint-disable-next-line @next/next/no-img-element */
|
|
148
148
|
_jsx("img", { src: winnerImg, alt: winner.title, className: "mx-auto max-h-64 rounded" })) : null, winner.estimatedValue != null ? (_jsxs(Text, { className: "mt-2 text-sm text-[var(--appkit-color-text-muted)]", children: ["Estimated value: \u20B9", (winner.estimatedValue / 100).toLocaleString("en-IN")] })) : null] })) : null, _jsxs(Div, { className: "rounded border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface-muted)] px-3 py-2 text-xs text-[var(--appkit-color-text-muted)]", children: [_jsx(Span, { weight: "bold", className: "text-[var(--appkit-color-text)]", children: "Fairness guarantee:" }), " ", "Winners are picked by ", _jsx("code", { children: "crypto.randomInt" }), " running on LetItRip's server before the animation starts. The animation is theatrical \u2014 neither the store nor LetItRip staff can influence the outcome.", " ", effectiveRngUrl ? (_jsx("a", { href: effectiveRngUrl, target: "_blank", rel: "noreferrer noopener", className: "underline", children: "View RNG source code \u2192" })) : null] })] })] }));
|
|
149
149
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ProductCardMetadata } from "../schemas/firestore";
|
|
2
|
+
export interface ProductCardMetadataSectionProps {
|
|
3
|
+
card: ProductCardMetadata;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* `ProductCardMetadataSection` — W1-34 — renders single-card aftermarket
|
|
7
|
+
* metadata (set name / year / card number / rarity / language). Designed to
|
|
8
|
+
* be embedded inside the "Specifications" tab content; rendered as a key/value
|
|
9
|
+
* table for predictable line-up on PDPs.
|
|
10
|
+
*/
|
|
11
|
+
export declare function ProductCardMetadataSection({ card }: ProductCardMetadataSectionProps): import("react/jsx-runtime").JSX.Element | null;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { Div, Heading, Stack, Text } from "../../../ui";
|
|
5
|
+
/**
|
|
6
|
+
* `ProductCardMetadataSection` — W1-34 — renders single-card aftermarket
|
|
7
|
+
* metadata (set name / year / card number / rarity / language). Designed to
|
|
8
|
+
* be embedded inside the "Specifications" tab content; rendered as a key/value
|
|
9
|
+
* table for predictable line-up on PDPs.
|
|
10
|
+
*/
|
|
11
|
+
export function ProductCardMetadataSection({ card }) {
|
|
12
|
+
const rows = [];
|
|
13
|
+
rows.push(["Set", card.setName]);
|
|
14
|
+
if (card.setYear)
|
|
15
|
+
rows.push(["Year", String(card.setYear)]);
|
|
16
|
+
if (card.cardNumber)
|
|
17
|
+
rows.push(["Card #", card.cardNumber]);
|
|
18
|
+
if (card.rarity)
|
|
19
|
+
rows.push(["Rarity", card.rarity]);
|
|
20
|
+
if (card.language)
|
|
21
|
+
rows.push(["Language", card.language.toUpperCase()]);
|
|
22
|
+
if (rows.length === 0)
|
|
23
|
+
return null;
|
|
24
|
+
return (_jsxs(Stack, { gap: "sm", children: [_jsx(Heading, { level: 3, className: "text-sm font-semibold uppercase tracking-wide text-zinc-500 dark:text-zinc-400", children: "Card details" }), _jsx(Div, { className: "grid grid-cols-2 gap-y-1 text-sm sm:grid-cols-[140px_1fr]", children: rows.map(([label, value]) => (_jsxs(React.Fragment, { children: [_jsx(Text, { className: "text-zinc-500 dark:text-zinc-400", children: label }), _jsx(Text, { className: "font-medium text-zinc-900 dark:text-zinc-100", children: value })] }, label))) })] }));
|
|
25
|
+
}
|
|
@@ -262,7 +262,9 @@ export async function ProductDetailPageView({ slug, initialProduct, renderOfferA
|
|
|
262
262
|
codAvailable: "Cash on Delivery",
|
|
263
263
|
wishlistCount: (n) => `${n} wishlisted`,
|
|
264
264
|
categoryProductCount: (n, cat) => `${n} in ${cat}`,
|
|
265
|
-
} }), productFeatures && features.length > 0 && (_jsx(FeatureBadgeList, { productFeatureIds: features, features: productFeatures })), !productFeatures && features.length > 0 && (_jsxs(Div, { className: "rounded-xl border border-zinc-100 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-900/60 px-4 py-3", children: [_jsx(Text, { className: "mb-2 text-xs font-semibold uppercase tracking-wide text-zinc-500 dark:text-zinc-400", children: "About this product" }), _jsx("ul", { className: "space-y-1.5", children: features.map((f, i) => (_jsxs("li", { className: "flex items-start gap-2 text-sm text-zinc-700 dark:text-zinc-300", children: [_jsx(Span, { className: "mt-0.5 flex-shrink-0 text-primary-500", children: "\u2022" }), f] }, i))) })] })), descriptionHtml && (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "text-sm leading-relaxed text-zinc-600 dark:text-zinc-400 line-clamp-4" })), safeSeller && (_jsx(Div, { className: "rounded-xl border border-zinc-100 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-900/60 p-3", children: _jsxs(Row, { justify: "between", align: "center", children: [_jsxs(Div, { children: [_jsx(Text, { className: "text-[10px] uppercase tracking-wide text-zinc-400 dark:text-zinc-400 mb-0.5", children: "Sold by" }), _jsx(Text, { className: "text-sm font-semibold text-zinc-800 dark:text-zinc-200", children: safeSeller })
|
|
265
|
+
} }), productFeatures && features.length > 0 && (_jsx(FeatureBadgeList, { productFeatureIds: features, features: productFeatures })), !productFeatures && features.length > 0 && (_jsxs(Div, { className: "rounded-xl border border-zinc-100 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-900/60 px-4 py-3", children: [_jsx(Text, { className: "mb-2 text-xs font-semibold uppercase tracking-wide text-zinc-500 dark:text-zinc-400", children: "About this product" }), _jsx("ul", { className: "space-y-1.5", children: features.map((f, i) => (_jsxs("li", { className: "flex items-start gap-2 text-sm text-zinc-700 dark:text-zinc-300", children: [_jsx(Span, { className: "mt-0.5 flex-shrink-0 text-primary-500", children: "\u2022" }), f] }, i))) })] })), descriptionHtml && (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "text-sm leading-relaxed text-zinc-600 dark:text-zinc-400 line-clamp-4" })), safeSeller && (_jsx(Div, { className: "rounded-xl border border-zinc-100 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-900/60 p-3", children: _jsxs(Row, { justify: "between", align: "center", children: [_jsxs(Div, { children: [_jsx(Text, { className: "text-[10px] uppercase tracking-wide text-zinc-400 dark:text-zinc-400 mb-0.5", children: "Sold by" }), _jsx(Text, { className: "text-sm font-semibold text-zinc-800 dark:text-zinc-200", children: safeSeller }), typeof p.storeRating === "number" && p.storeRating > 0 && (_jsx(Row, { gap: "xs", align: "center", className: "mt-0.5", children: _jsxs(Text, { size: "xs", color: "muted", children: [_jsx("span", { className: "text-warning", "aria-hidden": "true", children: "\u2605" }), " ", p.storeRating.toFixed(1), typeof p.storeReviewCount === "number" &&
|
|
266
|
+
p.storeReviewCount > 0 &&
|
|
267
|
+
` · ${p.storeReviewCount} reviews`] }) }))] }), storeHref && (_jsx(Link, { href: storeHref, className: "shrink-0 rounded-lg bg-primary/10 dark:bg-primary/20 px-3 py-1.5 text-xs font-semibold text-primary-700 dark:text-primary-300 hover:bg-primary/20 dark:hover:bg-primary/30 transition-colors", children: "Visit Store \u2192" }))] }) }))] })), renderActions: () => (_jsxs(Div, { className: "rounded-xl border border-zinc-100 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-900/60 p-5 space-y-4", children: [formattedPrice && (_jsxs(Div, { children: [_jsxs(Row, { align: "baseline", gap: "sm", wrap: true, children: [_jsx(Text, { className: "text-2xl font-bold text-zinc-900 dark:text-zinc-50", children: formattedPrice }), formattedOriginal && discount && (_jsxs(_Fragment, { children: [_jsx(Span, { className: "text-sm text-zinc-400 line-through dark:text-zinc-400", children: formattedOriginal }), _jsxs(Span, { className: "rounded-full bg-red-500 px-2 py-0.5 text-xs font-bold text-white", children: ["-", discount, "%"] })] }))] }), inStock && effectiveStock !== null && effectiveStock <= 10 && (_jsxs(Text, { className: "mt-1 text-xs text-warning", children: ["Only ", effectiveStock, " left \u2014 order soon!"] }))] })), _jsxs(Stack, { gap: "sm", children: [renderPrimaryActions ? (renderPrimaryActions({
|
|
266
268
|
productId: product.id,
|
|
267
269
|
productSlug: product.slug ?? slug,
|
|
268
270
|
productTitle: product.title,
|
|
@@ -12,7 +12,7 @@ const ROW_BASE_CLASS = "flex items-start gap-2 rounded-lg border px-3 py-2 text-
|
|
|
12
12
|
const ROW_CHECKED_CLASS = "border-primary bg-primary/5 dark:bg-primary/10";
|
|
13
13
|
const ROW_DISABLED_CLASS = "border-zinc-200 dark:border-slate-700 opacity-50 cursor-not-allowed";
|
|
14
14
|
const ROW_DEFAULT_CLASS = "border-zinc-200 dark:border-slate-700 hover:bg-zinc-50 dark:hover:bg-slate-800";
|
|
15
|
-
const OVER_LIMIT_BANNER_CLASS = "rounded-lg border border-
|
|
15
|
+
const OVER_LIMIT_BANNER_CLASS = "rounded-lg border border-error/20 bg-error-surface px-3 py-2 text-xs text-error";
|
|
16
16
|
const EMPTY_STATE_CLASS = "rounded-lg border border-zinc-200 dark:border-slate-700 bg-white dark:bg-slate-900 px-3 py-4 text-xs text-zinc-500 dark:text-zinc-400 text-center";
|
|
17
17
|
function applyProductType(list, productType) {
|
|
18
18
|
if (productType === "all")
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ProductGrading } from "../schemas/firestore";
|
|
2
|
+
export interface ProductGradingTabProps {
|
|
3
|
+
grading: ProductGrading;
|
|
4
|
+
/** Optional resolved media URL for the slab/grade image. */
|
|
5
|
+
slabImageUrl?: string | null;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* `ProductGradingTab` — W1-34 — renders TCG slab grading metadata
|
|
9
|
+
* (`product.grading` ProductGrading). Used as the `gradingContent` slot of
|
|
10
|
+
* `ProductTabsShell`. Always renders the service + grade prominently; cert
|
|
11
|
+
* lookup link is wired for PSA/BGS where the URL pattern is well-known.
|
|
12
|
+
*/
|
|
13
|
+
export declare function ProductGradingTab({ grading, slabImageUrl }: ProductGradingTabProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { Div, Heading, Stack, Text } from "../../../ui";
|
|
4
|
+
import { Badge } from "../../../ui/components/Badge";
|
|
5
|
+
const PSA_CERT_URL = (cert) => `https://www.psacard.com/cert/${cert}`;
|
|
6
|
+
const BGS_CERT_URL = (cert) => `https://www.beckett.com/grading/card-lookup/${cert}`;
|
|
7
|
+
function buildCertLookupUrl(service, certNumber) {
|
|
8
|
+
if (!certNumber)
|
|
9
|
+
return null;
|
|
10
|
+
if (service === "PSA")
|
|
11
|
+
return PSA_CERT_URL(certNumber);
|
|
12
|
+
if (service === "BGS")
|
|
13
|
+
return BGS_CERT_URL(certNumber);
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* `ProductGradingTab` — W1-34 — renders TCG slab grading metadata
|
|
18
|
+
* (`product.grading` ProductGrading). Used as the `gradingContent` slot of
|
|
19
|
+
* `ProductTabsShell`. Always renders the service + grade prominently; cert
|
|
20
|
+
* lookup link is wired for PSA/BGS where the URL pattern is well-known.
|
|
21
|
+
*/
|
|
22
|
+
export function ProductGradingTab({ grading, slabImageUrl }) {
|
|
23
|
+
const lookupUrl = buildCertLookupUrl(grading.service, grading.certNumber);
|
|
24
|
+
return (_jsxs(Stack, { gap: "lg", children: [_jsxs(Div, { className: "flex items-center gap-4", children: [_jsxs(Div, { className: "flex flex-col items-center justify-center rounded-2xl border-2 border-[var(--appkit-color-primary)] px-6 py-4 min-w-[120px]", children: [_jsx(Text, { className: "text-xs font-bold uppercase tracking-wide text-zinc-500 dark:text-zinc-400", children: grading.service }), _jsx(Text, { className: "text-4xl font-extrabold", children: grading.grade })] }), _jsxs(Stack, { gap: "xs", className: "flex-1", children: [_jsxs(Heading, { level: 3, className: "text-base font-semibold", children: ["Graded by ", grading.service] }), grading.certNumber ? (_jsxs(Text, { className: "text-sm text-zinc-600 dark:text-zinc-400", children: ["Cert #", grading.certNumber, lookupUrl ? (_jsxs(_Fragment, { children: [" — ", _jsx("a", { href: lookupUrl, target: "_blank", rel: "noopener noreferrer", className: "text-[var(--appkit-color-primary)] hover:underline", children: "Verify" })] })) : null] })) : null] })] }), slabImageUrl ? (_jsx(Div, { className: "overflow-hidden rounded-2xl border border-zinc-200 dark:border-zinc-700", children: _jsx("img", { src: slabImageUrl, alt: "Slab certificate image", className: "w-full h-auto" }) })) : null, grading.attributes && Object.keys(grading.attributes).length > 0 ? (_jsxs(Stack, { gap: "sm", children: [_jsx(Heading, { level: 3, className: "text-sm font-semibold uppercase tracking-wide text-zinc-500 dark:text-zinc-400", children: "Subgrades & attributes" }), _jsx(Div, { className: "flex flex-wrap gap-2", children: Object.entries(grading.attributes).map(([key, value]) => (_jsxs(Badge, { variant: "default", children: [key, ": ", value] }, key))) })] })) : null] }));
|
|
25
|
+
}
|
|
@@ -49,7 +49,7 @@ export function ProductCard({ product, href, onClick, onAddToWishlist, isWishlis
|
|
|
49
49
|
.filter(Boolean)
|
|
50
50
|
.join(" "), children: [_jsxs(Div, { className: "relative overflow-hidden bg-zinc-100 dark:bg-slate-800 aspect-square", children: [product.mainImage ? (_jsx(MediaImage, { src: product.mainImage, alt: product.title, size: "card", className: "transition-transform duration-500 group-hover:scale-105" })) : (_jsx(Div, { className: "h-full w-full flex items-center justify-center bg-gradient-to-br from-zinc-100 to-zinc-200 dark:from-slate-800 dark:to-slate-700", children: _jsx(Span, { className: "text-4xl opacity-30", children: "\uD83D\uDECD\uFE0F" }) })), onSelect && (_jsx(BaseListingCard.Checkbox, { selected: isSelected, onSelect: (e) => { e.preventDefault(); onSelect(product.id); }, label: isSelected ? "Deselect" : "Select", position: "top-2 left-2", className: selectionMode || isSelected ? "opacity-100" : "opacity-0 group-hover:opacity-100 transition-opacity" })), _jsx(Div, { className: `absolute top-2 flex flex-col gap-1 ${onSelect ? "left-10" : "left-2"}`, children: !selectionMode && !isSelected && (_jsxs(_Fragment, { children: [discount && (_jsxs(Span, { className: "rounded-full bg-rose-500 px-2 py-0.5 text-[10px] font-bold text-white shadow-sm", children: ["-", discount, "%"] })), typeBadge && (_jsx(Span, { className: `rounded-full px-2 py-0.5 text-[10px] font-bold shadow-sm ${typeBadge.cls}`, children: typeBadge.label })), product.partOfBundleIds && product.partOfBundleIds.length > 0 && (_jsx(Span, { className: "rounded-full bg-teal-600 px-2 py-0.5 text-[10px] font-bold text-white shadow-sm", title: product.partOfBundleTitles && product.partOfBundleTitles.length > 0
|
|
51
51
|
? `In bundle: ${product.partOfBundleTitles[0]}`
|
|
52
|
-
: "In a bundle", children: "Bundled" })), product.groupId && (_jsx(Span, { className: "rounded-full bg-indigo-600 px-2 py-0.5 text-[10px] font-bold text-white shadow-sm", title: product.groupTitle ? `Part of set: ${product.groupTitle}` : "Part of a set", children: product.isGroupParent ? "Set Parent" : "In Set" })), product.sublistingCategoryId && (_jsx(Span, { className: "rounded-full bg-sky-600 px-2 py-0.5 text-[10px] font-bold text-white shadow-sm", title: "Has variants or sub-listings", children: "Has Variants" }))] })) }), onAddToWishlist && (_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: (e) => {
|
|
52
|
+
: "In a bundle", children: "Bundled" })), product.groupId && (_jsx(Span, { className: "rounded-full bg-indigo-600 px-2 py-0.5 text-[10px] font-bold text-white shadow-sm", title: product.groupTitle ? `Part of set: ${product.groupTitle}` : "Part of a set", children: product.isGroupParent ? "Set Parent" : "In Set" })), product.sublistingCategoryId && (_jsx(Span, { className: "rounded-full bg-sky-600 px-2 py-0.5 text-[10px] font-bold text-white shadow-sm", title: "Has variants or sub-listings", children: "Has Variants" }))] })) }), _jsx(BaseListingCard.IconOverlay, { groupIcon: product.groupIcon, sublistingIcon: product.sublistingIcon, position: "bottom-2 right-2" }), onAddToWishlist && (_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: (e) => {
|
|
53
53
|
e.stopPropagation();
|
|
54
54
|
e.preventDefault();
|
|
55
55
|
onAddToWishlist(product.id);
|
|
@@ -59,7 +59,19 @@ export function ProductCard({ product, href, onClick, onAddToWishlist, isWishlis
|
|
|
59
59
|
isWishlisted
|
|
60
60
|
? "bg-rose-500 text-white hover:bg-rose-600"
|
|
61
61
|
: "bg-white/90 dark:bg-slate-800/90 text-zinc-500 dark:text-zinc-400 hover:text-rose-500 hover:bg-white dark:hover:bg-slate-800",
|
|
62
|
-
].join(" "), children: _jsx("svg", { className: "h-4 w-4", fill: isWishlisted ? "currentColor" : "none", stroke: "currentColor", strokeWidth: 2, viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12z" }) }) }))] }), _jsxs(Div, { className: "flex flex-1 flex-col p-3 pt-2.5", children: [_jsx(Text, { className: `${THEME_CONSTANTS.utilities.textClamp2} text-sm font-semibold text-zinc-900 dark:text-white leading-snug`, children: product.title }), ((
|
|
62
|
+
].join(" "), children: _jsx("svg", { className: "h-4 w-4", fill: isWishlisted ? "currentColor" : "none", stroke: "currentColor", strokeWidth: 2, viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12z" }) }) }))] }), _jsxs(Div, { className: "flex flex-1 flex-col p-3 pt-2.5", children: [_jsx(Text, { className: `${THEME_CONSTANTS.utilities.textClamp2} text-sm font-semibold text-zinc-900 dark:text-white leading-snug`, children: product.title }), (() => {
|
|
63
|
+
const categories = Array.isArray(product.categoryNames)
|
|
64
|
+
? product.categoryNames.filter(Boolean)
|
|
65
|
+
: product.categoryName
|
|
66
|
+
? [product.categoryName]
|
|
67
|
+
: [];
|
|
68
|
+
if (!categories.length && !product.brand)
|
|
69
|
+
return null;
|
|
70
|
+
const MAX_CHIPS = 3;
|
|
71
|
+
const visible = categories.slice(0, MAX_CHIPS);
|
|
72
|
+
const overflow = Math.max(0, categories.length - MAX_CHIPS);
|
|
73
|
+
return (_jsxs(Row, { className: "mt-1 gap-1 flex-wrap", children: [visible.map((name) => (_jsxs(Span, { className: "inline-flex items-center gap-0.5 rounded-full bg-zinc-100 dark:bg-zinc-800 px-1.5 py-0.5 text-[10px] text-zinc-600 dark:text-zinc-400 truncate max-w-[100px]", children: [_jsx("svg", { className: "h-2.5 w-2.5 flex-shrink-0", fill: "none", stroke: "currentColor", strokeWidth: 2, viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M3 7a2 2 0 012-2h14a2 2 0 012 2v10a2 2 0 01-2 2H5a2 2 0 01-2-2V7z" }) }), name] }, name))), overflow > 0 && (_jsxs(Span, { className: "inline-flex items-center rounded-full bg-zinc-100 dark:bg-zinc-800 px-1.5 py-0.5 text-[10px] font-medium text-zinc-600 dark:text-zinc-400", title: categories.slice(MAX_CHIPS).join(", "), children: ["+", overflow] })), product.brand && (_jsxs(Span, { className: "inline-flex items-center gap-0.5 rounded-full bg-zinc-100 dark:bg-zinc-800 px-1.5 py-0.5 text-[10px] text-zinc-600 dark:text-zinc-400 truncate max-w-[90px]", children: [_jsx("svg", { className: "h-2.5 w-2.5 flex-shrink-0", fill: "none", stroke: "currentColor", strokeWidth: 2, viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" }) }), product.brand] }))] }));
|
|
74
|
+
})(), (() => {
|
|
63
75
|
const seller = safeDisplayName(product.storeName, "");
|
|
64
76
|
return seller ? (_jsxs(Text, { className: "mt-0.5 text-[11px] text-zinc-400 dark:text-zinc-400 truncate", children: ["by ", seller] })) : null;
|
|
65
77
|
})(), product.rating !== undefined && (_jsxs(Row, { className: "mt-1 gap-1", children: [_jsx(Span, { className: "text-[11px] text-amber-400", children: "\u2605" }), _jsxs(Span, { className: "text-[11px] text-zinc-500 dark:text-zinc-400", children: [product.rating.toFixed(1), product.reviewCount ? ` (${product.reviewCount})` : ""] })] })), _jsxs(Div, { className: "mt-auto pt-2", children: [_jsxs(Row, { align: "baseline", className: "gap-2", children: [_jsx(Span, { className: "text-base font-bold text-primary dark:text-primary-400", children: formatCurrency(product.price, getDefaultCurrency()) }), product.originalPrice && product.originalPrice > product.price && (_jsx(Span, { className: "text-xs text-zinc-400 line-through dark:text-zinc-400", children: formatCurrency(product.originalPrice, getDefaultCurrency()) }))] }), (() => {
|
|
@@ -7,11 +7,15 @@ export interface CustomTabDef {
|
|
|
7
7
|
export interface ProductTabsShellProps {
|
|
8
8
|
descriptionContent?: React.ReactNode;
|
|
9
9
|
specsContent?: React.ReactNode;
|
|
10
|
+
/** W1-34 — grading tab content (PSA/BGS/CGC etc). Pass non-null to surface the tab. */
|
|
11
|
+
gradingContent?: React.ReactNode;
|
|
10
12
|
ingredientsContent?: React.ReactNode;
|
|
11
13
|
howToUseContent?: React.ReactNode;
|
|
12
14
|
reviewsContent?: React.ReactNode;
|
|
15
|
+
/** W1-34 — Q&A tab content. Pass non-null to surface the tab. */
|
|
16
|
+
qaContent?: React.ReactNode;
|
|
13
17
|
/** Additional tabs from custom sections — rendered after standard tabs */
|
|
14
18
|
customTabs?: CustomTabDef[];
|
|
15
19
|
className?: string;
|
|
16
20
|
}
|
|
17
|
-
export declare function ProductTabsShell({ descriptionContent, specsContent, ingredientsContent, howToUseContent, reviewsContent, customTabs, className, }: ProductTabsShellProps): import("react/jsx-runtime").JSX.Element | null;
|
|
21
|
+
export declare function ProductTabsShell({ descriptionContent, specsContent, gradingContent, ingredientsContent, howToUseContent, reviewsContent, qaContent, customTabs, className, }: ProductTabsShellProps): import("react/jsx-runtime").JSX.Element | null;
|
|
@@ -4,17 +4,22 @@ import { useState } from "react";
|
|
|
4
4
|
const STATIC_TABS = [
|
|
5
5
|
{ id: "description", label: "Description" },
|
|
6
6
|
{ id: "specs", label: "Specifications" },
|
|
7
|
+
{ id: "grading", label: "Grading" },
|
|
7
8
|
{ id: "ingredients", label: "Ingredients" },
|
|
8
9
|
{ id: "howToUse", label: "How to Use" },
|
|
9
10
|
{ id: "reviews", label: "Reviews" },
|
|
11
|
+
// W1-34 — Q&A tab; consumers pass `qaContent` to surface it.
|
|
12
|
+
{ id: "qa", label: "Q&A" },
|
|
10
13
|
];
|
|
11
|
-
export function ProductTabsShell({ descriptionContent, specsContent, ingredientsContent, howToUseContent, reviewsContent, customTabs = [], className = "", }) {
|
|
14
|
+
export function ProductTabsShell({ descriptionContent, specsContent, gradingContent, ingredientsContent, howToUseContent, reviewsContent, qaContent, customTabs = [], className = "", }) {
|
|
12
15
|
const staticContentMap = {
|
|
13
16
|
description: descriptionContent,
|
|
14
17
|
specs: specsContent,
|
|
18
|
+
grading: gradingContent,
|
|
15
19
|
ingredients: ingredientsContent,
|
|
16
20
|
howToUse: howToUseContent,
|
|
17
21
|
reviews: reviewsContent,
|
|
22
|
+
qa: qaContent,
|
|
18
23
|
};
|
|
19
24
|
const visibleStatic = STATIC_TABS.filter((t) => staticContentMap[t.id] != null);
|
|
20
25
|
const visibleCustom = customTabs.filter((t) => t.content != null);
|
|
@@ -54,6 +54,10 @@ export { ProductGalleryClient } from "./ProductGalleryClient";
|
|
|
54
54
|
export type { ProductGalleryClientProps } from "./ProductGalleryClient";
|
|
55
55
|
export { ProductTabsShell } from "./ProductTabsShell";
|
|
56
56
|
export type { ProductTabsShellProps, CustomTabDef } from "./ProductTabsShell";
|
|
57
|
+
export { ProductGradingTab } from "./ProductGradingTab";
|
|
58
|
+
export type { ProductGradingTabProps } from "./ProductGradingTab";
|
|
59
|
+
export { ProductCardMetadataSection } from "./ProductCardMetadataSection";
|
|
60
|
+
export type { ProductCardMetadataSectionProps } from "./ProductCardMetadataSection";
|
|
57
61
|
export { CustomFieldsEditor } from "./CustomFieldsEditor";
|
|
58
62
|
export type { CustomFieldsEditorProps } from "./CustomFieldsEditor";
|
|
59
63
|
export { CustomSectionsEditor } from "./CustomSectionsEditor";
|
|
@@ -32,6 +32,9 @@ export { MakeOfferButton } from "./MakeOfferButton";
|
|
|
32
32
|
export { RelatedProducts } from "./RelatedProducts";
|
|
33
33
|
export { ProductGalleryClient } from "./ProductGalleryClient";
|
|
34
34
|
export { ProductTabsShell } from "./ProductTabsShell";
|
|
35
|
+
// W1-34: Product detail missing sections
|
|
36
|
+
export { ProductGradingTab } from "./ProductGradingTab";
|
|
37
|
+
export { ProductCardMetadataSection } from "./ProductCardMetadataSection";
|
|
35
38
|
export { CustomFieldsEditor } from "./CustomFieldsEditor";
|
|
36
39
|
export { CustomSectionsEditor } from "./CustomSectionsEditor";
|
|
37
40
|
export { CustomSectionTabContent } from "./CustomSectionTabContent";
|
|
@@ -9,6 +9,11 @@
|
|
|
9
9
|
* Client — useAuthGate() shows LoginRequiredModal for Tier 1 (public) actions
|
|
10
10
|
* Server — checkActionAllowed() enforces the same guards on every server action
|
|
11
11
|
* Admin — ActionPermissionsManager toggles defaultEnabled via siteSettings.actionConfig
|
|
12
|
+
*
|
|
13
|
+
* Label source of truth: `ACTIONS.{RESOURCE}["..."].label` in
|
|
14
|
+
* _internal/shared/actions/action-registry.ts. `ACTION_META` derives its labels
|
|
15
|
+
* via `regLabel()` where a registry counterpart exists; entries with no
|
|
16
|
+
* counterpart keep an inline literal until a registry leaf is added (W1-5).
|
|
12
17
|
*/
|
|
13
18
|
export type ActionVariant = "primary" | "secondary" | "outline" | "ghost" | "danger" | "warning";
|
|
14
19
|
export declare const ACTION_ID: {
|