@mohasinac/appkit 4.26.0 → 4.28.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/lottery/LotteryAdminEditView.d.ts +16 -16
- package/dist/_internal/client/features/lottery/LotteryAdminEditView.js +29 -6
- package/dist/_internal/client/features/lottery/LotteryListView.js +13 -1
- package/dist/_internal/server/features/bundles/data.d.ts +19 -0
- package/dist/_internal/server/features/bundles/data.js +32 -0
- package/dist/_internal/server/features/bundles/index.d.ts +1 -1
- package/dist/_internal/server/features/bundles/index.js +1 -1
- package/dist/_internal/server/features/checkout/actions.js +6 -4
- package/dist/_internal/server/features/classified/metadata.js +3 -1
- package/dist/_internal/server/features/digital-code/metadata.js +3 -1
- package/dist/_internal/server/features/live/metadata.js +3 -1
- package/dist/_internal/server/features/refunds/actions.js +3 -2
- package/dist/_internal/server/features/reviews/actions.js +3 -2
- package/dist/_internal/server/features/seo/sitemap.js +189 -40
- package/dist/_internal/server/jobs/core/listingProcessor.d.ts +0 -1
- package/dist/_internal/server/jobs/core/listingProcessor.js +19 -1
- package/dist/_internal/shared/actions/action-registry.js +14 -0
- package/dist/_internal/shared/checkout/order-math.d.ts +25 -3
- package/dist/_internal/shared/features/checkout/config.d.ts +3 -3
- package/dist/_internal/shared/features/checkout/config.js +6 -3
- package/dist/client.d.ts +32 -2
- package/dist/client.js +37 -2
- package/dist/constants/api-endpoints.d.ts +21 -3
- package/dist/constants/api-endpoints.js +14 -1
- package/dist/constants/field-names.d.ts +2 -2
- package/dist/constants/field-names.js +2 -2
- package/dist/core/newsletter.repository.js +14 -1
- package/dist/features/about/components/FAQPageView.js +1 -1
- package/dist/features/about/components/FeesView.js +1 -1
- package/dist/features/account/components/AddressForm.d.ts +2 -0
- package/dist/features/account/components/AddressForm.js +5 -1
- package/dist/features/account/components/UserBidsView.js +1 -1
- package/dist/features/account/components/UserSidebar.js +1 -1
- package/dist/features/account/components/UserSupportView.js +24 -2
- package/dist/features/account/hooks/useAddresses.d.ts +29 -1
- package/dist/features/account/hooks/useAddresses.js +4 -1
- package/dist/features/account/hooks/useProfile.d.ts +1 -1
- package/dist/features/account/hooks/useProfile.js +1 -1
- package/dist/features/addresses/repository/addresses.repository.d.ts +13 -0
- package/dist/features/addresses/repository/addresses.repository.js +19 -0
- package/dist/features/admin/actions/chat-actions.js +5 -1
- package/dist/features/admin/components/AdminAdsView.js +1 -1
- package/dist/features/admin/components/AdminBidsView.js +6 -1
- package/dist/features/admin/components/AdminCarouselGroupEditorView.d.ts +9 -1
- package/dist/features/admin/components/AdminCarouselGroupEditorView.js +42 -10
- package/dist/features/admin/components/AdminCarouselView.js +1 -1
- package/dist/features/admin/components/AdminFeatureEditorView.js +1 -1
- package/dist/features/admin/components/AdminGroupedListingsView.js +14 -2
- package/dist/features/admin/components/AdminSidebar.js +1 -1
- package/dist/features/admin/components/AdminViewCards.js +2 -2
- package/dist/features/admin/components/DataTable.js +2 -2
- package/dist/features/admin/schemas/firestore.js +9 -3
- package/dist/features/auctions/actions/bid-actions.js +45 -4
- package/dist/features/auctions/components/AuctionBottomActions.d.ts +21 -6
- package/dist/features/auctions/components/AuctionBottomActions.js +42 -11
- package/dist/features/auctions/components/AuctionDetailPageView.d.ts +8 -3
- package/dist/features/auctions/components/AuctionDetailPageView.js +2 -2
- package/dist/features/auctions/components/MarketplaceAuctionCard.js +4 -5
- package/dist/features/auctions/components/PlaceBidFormClient.d.ts +27 -2
- package/dist/features/auctions/components/PlaceBidFormClient.js +42 -20
- package/dist/features/auth/components/SocialAuthButtons.js +1 -1
- package/dist/features/auth/hooks/useAuth.d.ts +23 -1
- package/dist/features/auth/hooks/useAuth.js +27 -1
- package/dist/features/auth/permissions/constants.d.ts +1 -1
- package/dist/features/auth/permissions/constants.js +3 -0
- package/dist/features/auth/repository/session.repository.d.ts +16 -1
- package/dist/features/auth/repository/session.repository.js +23 -0
- package/dist/features/auth/repository/user.repository.js +13 -2
- package/dist/features/blog/components/BlogListView.js +2 -2
- package/dist/features/cart/components/CartPriceBreakdown.d.ts +11 -1
- package/dist/features/cart/components/CartPriceBreakdown.js +3 -3
- package/dist/features/cart/components/StoreAddonsPicker.d.ts +9 -0
- package/dist/features/cart/components/StoreAddonsPicker.js +15 -5
- package/dist/features/cart/components/index.d.ts +1 -1
- package/dist/features/cart/components/index.js +1 -1
- package/dist/features/cart/hooks/useGuestCartMerge.d.ts +8 -1
- package/dist/features/cart/hooks/useGuestCartMerge.js +2 -1
- package/dist/features/cart/utils/line-total.d.ts +25 -0
- package/dist/features/cart/utils/line-total.js +26 -0
- package/dist/features/catalogue/repository/catalogue.repository.d.ts +12 -0
- package/dist/features/catalogue/repository/catalogue.repository.js +15 -0
- package/dist/features/categories/components/CategoryTree.js +2 -2
- package/dist/features/contact/components/ContactForm.js +1 -1
- package/dist/features/copilot/hooks/useCopilotFeedback.d.ts +1 -1
- package/dist/features/copilot/hooks/useCopilotFeedback.js +1 -1
- package/dist/features/events/components/AdminEventEditorView.js +16 -9
- package/dist/features/events/components/AdminEventEntriesView.js +7 -13
- package/dist/features/events/components/EventRafflesSection.js +14 -2
- package/dist/features/events/components/EventStatusBadge.js +1 -1
- package/dist/features/events/components/event-entry-detail-fields.d.ts +24 -0
- package/dist/features/events/components/event-entry-detail-fields.js +13 -0
- package/dist/features/faq/components/ContactCTA.js +1 -1
- package/dist/features/faq/components/FAQHelpfulButtons.js +2 -2
- package/dist/features/faq/repository/faqs.repository.d.ts +16 -1
- package/dist/features/faq/repository/faqs.repository.js +64 -54
- package/dist/features/faq/schemas/firestore.d.ts +3 -2
- package/dist/features/faq/schemas/firestore.js +2 -2
- package/dist/features/faq/schemas/index.d.ts +3 -3
- package/dist/features/faq/schemas/index.js +1 -1
- package/dist/features/filters/FilterFacetSection.js +2 -2
- package/dist/features/forms/Toggle.js +1 -1
- package/dist/features/grouped/components/GroupedListingEditorView.d.ts +18 -0
- package/dist/features/grouped/components/GroupedListingEditorView.js +192 -0
- package/dist/features/homepage/components/AdvertisementBanner.js +2 -2
- package/dist/features/homepage/components/BrandsSection.js +1 -1
- package/dist/features/homepage/components/CTABannerSection.js +1 -1
- package/dist/features/homepage/components/HomepageCustomerReviewsSection.js +1 -1
- package/dist/features/homepage/components/HomepageSectionBoundary.d.ts +39 -0
- package/dist/features/homepage/components/HomepageSectionBoundary.js +49 -0
- package/dist/features/homepage/components/ShopByCategorySection.js +1 -1
- package/dist/features/homepage/components/WelcomeSection.js +1 -1
- package/dist/features/homepage/lib/section-renderer.js +2 -1
- package/dist/features/layout/AppLayoutShell.js +5 -5
- package/dist/features/layout/BottomActions.js +5 -5
- package/dist/features/layout/BottomSheet.js +1 -1
- package/dist/features/layout/MainNavbar.js +1 -1
- package/dist/features/layout/NavbarLayout.js +1 -1
- package/dist/features/layout/TitleBarLayout.js +3 -3
- package/dist/features/layout/hooks/useFormBottomActions.d.ts +11 -1
- package/dist/features/layout/hooks/useFormBottomActions.js +16 -2
- package/dist/features/lottery/schemas/config-write.d.ts +141 -0
- package/dist/features/lottery/schemas/config-write.js +139 -0
- package/dist/features/media/MediaPickerModal.js +3 -1
- package/dist/features/media/MediaVideo.js +4 -2
- package/dist/features/media/components/MediaSlider.js +1 -1
- package/dist/features/orders/components/OrdersList.js +2 -2
- package/dist/features/payments/repository/payout.repository.d.ts +11 -1
- package/dist/features/payments/repository/payout.repository.js +14 -6
- package/dist/features/pre-orders/components/MarketplacePreorderCard.js +3 -2
- package/dist/features/pre-orders/components/PreOrderBadge.js +1 -1
- package/dist/features/pre-orders/components/PreOrderDetailPageView.js +1 -1
- package/dist/features/products/columns/productTableColumns.js +3 -3
- package/dist/features/products/components/CompareOverlay.js +1 -1
- package/dist/features/products/components/PrizeDrawDetailPageView.js +1 -1
- package/dist/features/products/components/PrizeDrawEntryActions.js +2 -2
- package/dist/features/products/components/PrizeDrawsSection.js +5 -1
- package/dist/features/products/components/ProductDetailActions.js +11 -7
- package/dist/features/products/components/ProductDetailPageView.js +3 -3
- package/dist/features/products/components/ProductFeatureBadges.js +2 -2
- package/dist/features/products/components/ProductFeaturesSelector.js +1 -1
- package/dist/features/products/components/ProductFilters.js +15 -8
- package/dist/features/products/components/ProductGrid.js +8 -17
- package/dist/features/products/components/ProductTabs.js +7 -4
- package/dist/features/products/components/ProductTabsShell.js +2 -7
- package/dist/features/products/constants/action-defs.d.ts +21 -4
- package/dist/features/products/constants/action-defs.js +7 -3
- package/dist/features/products/hooks/useRelatedProducts.js +2 -1
- package/dist/features/products/repository/products.repository.d.ts +1 -1
- package/dist/features/products/repository/products.repository.js +27 -13
- package/dist/features/products/schemas/firestore.d.ts +3 -2
- package/dist/features/products/schemas/firestore.js +1 -1
- package/dist/features/reviews/actions/review-actions.d.ts +8 -1
- package/dist/features/reviews/actions/review-actions.js +14 -3
- package/dist/features/reviews/components/ReviewDetailShell.js +10 -4
- package/dist/features/reviews/components/ReviewModal.js +1 -1
- package/dist/features/reviews/repository/reviews.repository.d.ts +25 -1
- package/dist/features/reviews/repository/reviews.repository.js +53 -13
- package/dist/features/reviews/schemas/firestore.d.ts +6 -0
- package/dist/features/scams/components/ScamRegistryView.js +2 -2
- package/dist/features/search/components/Search.js +3 -3
- package/dist/features/search/components/SearchResultsSection.js +1 -1
- package/dist/features/seller/components/SellerAddressesView.js +3 -1
- package/dist/features/seller/components/SellerBidsView.js +6 -1
- package/dist/features/seller/components/SellerSidebar.js +1 -1
- package/dist/features/shell/SectionForm.d.ts +21 -1
- package/dist/features/shell/SectionForm.js +15 -3
- package/dist/features/shell/build-sections.js +10 -1
- package/dist/features/shell/field-ui-meta.d.ts +21 -0
- package/dist/features/shipments/components/AdminShipmentLotItemsView.js +23 -6
- package/dist/features/site-settings/components/ActionPermissionsManager.js +1 -1
- package/dist/features/store-extensions/repository/store-extensions.repositories.d.ts +21 -4
- package/dist/features/store-extensions/repository/store-extensions.repositories.js +24 -4
- package/dist/features/store-extensions/schemas/shipping-config-form.js +13 -0
- package/dist/features/store-extensions/schemas/store-category-form.js +1 -1
- package/dist/features/stores/actions/store-query-actions.js +25 -1
- package/dist/features/stores/components/InteractiveStoreCard.js +1 -1
- package/dist/features/stores/components/StoreHeader.js +1 -1
- package/dist/features/stores/components/StoreNavTabs.js +20 -8
- package/dist/features/support/repository/support.repository.d.ts +12 -0
- package/dist/features/support/repository/support.repository.js +15 -0
- package/dist/features/support/schemas/ticket-create-form.d.ts +33 -0
- package/dist/features/support/schemas/ticket-create-form.js +82 -0
- package/dist/features/tester/seed-data/products-tester-seed-data.js +108 -2
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +235 -13
- package/dist/features/wishlist/components/WishlistHeartButton.d.ts +55 -0
- package/dist/features/wishlist/components/WishlistHeartButton.js +52 -0
- package/dist/features/wishlist/components/index.d.ts +2 -1
- package/dist/features/wishlist/components/index.js +1 -1
- package/dist/index.d.ts +10 -3
- package/dist/index.js +19 -4
- package/dist/monitoring/error-tracking.js +39 -5
- package/dist/monitoring/server-logger.js +70 -32
- package/dist/next/components/ErrorView.js +1 -1
- package/dist/next/routing/route-map.d.ts +39 -7
- package/dist/next/routing/route-map.js +34 -4
- package/dist/providers/db-firebase/base.repository.d.ts +30 -0
- package/dist/providers/db-firebase/base.repository.js +56 -9
- package/dist/react/ErrorBoundary.js +9 -1
- package/dist/react/contexts/SessionContext.js +6 -2
- package/dist/react/hooks/useAuthGate.d.ts +22 -0
- package/dist/react/hooks/useAuthGate.js +86 -7
- package/dist/schemas/registry.d.ts +3 -3
- package/dist/security/index.d.ts +1 -1
- package/dist/security/index.js +1 -1
- package/dist/security/pii-encrypt.d.ts +13 -5
- package/dist/security/pii-encrypt.js +132 -6
- package/dist/security/pii-schemas.d.ts +20 -4
- package/dist/security/pii-schemas.js +23 -5
- package/dist/seed/_helpers/product-search-txt.d.ts +18 -0
- package/dist/seed/_helpers/product-search-txt.js +33 -0
- package/dist/seed/faq-seed-data.d.ts +16 -0
- package/dist/seed/faq-seed-data.js +22 -1
- package/dist/seed/products-art-seed-data.js +49 -7
- package/dist/seed/products-auctions-seed-data.js +12 -4
- package/dist/seed/products-classifieds-seed-data.d.ts +5 -0
- package/dist/seed/products-classifieds-seed-data.js +65 -9
- package/dist/seed/products-digital-codes-seed-data.d.ts +5 -0
- package/dist/seed/products-digital-codes-seed-data.js +65 -9
- package/dist/seed/products-live-items-seed-data.d.ts +5 -0
- package/dist/seed/products-live-items-seed-data.js +33 -5
- package/dist/seed/products-preorders-seed-data.d.ts +1 -1
- package/dist/seed/products-preorders-seed-data.js +6 -2
- package/dist/seed/products-prize-draws-seed-data.d.ts +5 -0
- package/dist/seed/products-prize-draws-seed-data.js +57 -8
- package/dist/seed/products-standard-seed-data.js +6 -2
- package/dist/seed/products-stickers-seed-data.js +49 -7
- package/dist/seed/site-settings-seed-data.js +5 -3
- package/dist/seo/json-ld.d.ts +44 -2
- package/dist/seo/json-ld.js +57 -11
- package/dist/server.d.ts +1 -1
- package/dist/server.js +1 -1
- package/dist/styles.css +368 -90
- package/dist/tailwind-utilities.css +1 -1
- package/dist/tokens/index.d.ts +1 -1
- package/dist/tokens/index.js +1 -1
- package/dist/tokens/themes/default-dark.js +6 -0
- package/dist/tokens/themes/default-light.js +5 -0
- package/dist/tokens/tokens.css +84 -0
- package/dist/ui/DataTable.js +1 -1
- package/dist/ui/DataTable.style.css +3 -3
- package/dist/ui/components/BulkActionBar.style.css +1 -3
- package/dist/ui/components/Button.js +15 -6
- package/dist/ui/components/Dropdown.style.css +1 -4
- package/dist/ui/components/FilterChipGroup.js +1 -1
- package/dist/ui/components/FilterDrawer.style.css +1 -1
- package/dist/ui/components/Icon.d.ts +53 -0
- package/dist/ui/components/Icon.js +41 -0
- package/dist/ui/components/IconButton.d.ts +13 -0
- package/dist/ui/components/IconButton.js +13 -0
- package/dist/ui/components/IconButton.style.css +18 -10
- package/dist/ui/components/ListingLayout.style.css +2 -10
- package/dist/ui/components/ListingToolbar.js +6 -6
- package/dist/ui/components/Modal.js +1 -1
- package/dist/ui/components/PaginatedSelect.style.css +3 -3
- package/dist/ui/components/Pagination.style.css +1 -6
- package/dist/ui/components/RichTextEditor.js +1 -1
- package/dist/ui/components/RowActionMenu.style.css +1 -1
- package/dist/ui/components/SideDrawer.style.css +1 -1
- package/dist/ui/components/Slider.js +1 -1
- package/dist/ui/components/TabBarShell.d.ts +78 -0
- package/dist/ui/components/TabBarShell.js +95 -0
- package/dist/ui/components/Tabs.js +12 -8
- package/dist/ui/components/Tabs.style.css +249 -41
- package/dist/ui/components/ViewToggle.style.css +1 -5
- package/dist/ui/components/tabs-overflow.d.ts +17 -0
- package/dist/ui/components/tabs-overflow.js +142 -0
- package/dist/ui/icons/icon-registry.d.ts +38 -0
- package/dist/ui/icons/icon-registry.js +37 -0
- package/dist/ui/index.d.ts +6 -0
- package/dist/ui/index.js +3 -0
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/media-url.d.ts +27 -4
- package/dist/utils/media-url.js +37 -6
- package/dist/utils/search-txt.d.ts +71 -0
- package/dist/utils/search-txt.js +118 -0
- package/package.json +1 -1
|
@@ -85,7 +85,7 @@ export const faqFirestoreSchema = z.object({
|
|
|
85
85
|
order: z.number().int(),
|
|
86
86
|
priority: z.number().int(),
|
|
87
87
|
tags: z.array(z.string()),
|
|
88
|
-
|
|
88
|
+
searchTxt: z.array(z.string()).optional(),
|
|
89
89
|
relatedFAQs: z.array(z.string()),
|
|
90
90
|
useSiteSettings: z.boolean(),
|
|
91
91
|
variables: z.record(z.string(), z.string()).optional(),
|
|
@@ -77,9 +77,9 @@ export function FilterFacetSection({ title, options, selected, onChange, selecti
|
|
|
77
77
|
if (displayAs === "dropdown") {
|
|
78
78
|
return (_jsxs(Div, { ref: dropdownRef, className: cn("relative inline-block", className), children: [_jsxs(Button, { type: "button", onClick: () => setDropdownOpen((o) => !o), variant: "ghost", className: cn("inline-flex items-center gap-[var(--appkit-space-1-5)] rounded-full border px-[var(--appkit-space-3)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-sm)] font-medium transition-colors", hasValue
|
|
79
79
|
? CLS_OPTION_SELECTED
|
|
80
|
-
: "
|
|
80
|
+
: " bg-[var(--appkit-color-surface)] hover:border-zinc-400 border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] text-[var(--appkit-color-text-muted)] hover:border-[var(--appkit-color-border)]"), children: [title, hasValue && (_jsx(Span, { weight: "bold", className: CLS_OPTION_DOT, children: selected.length })), _jsx("svg", { className: cn("h-3.5 w-3.5 text-zinc-400 transition-transform", dropdownOpen ? "rotate-180" : ""), fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2, "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M19 9l-7 7-7-7" }) })] }), dropdownOpen && (_jsxs(Div, { className: "absolute left-0 top-full z-[var(--appkit-z-dropdown,50)] mt-1 min-w-[180px] max-w-[260px]", surface: "default", rounded: "xl", shadow: "lg", border: "default", children: [searchable && (_jsx(Div, { border: "subtle", className: "border-b", padding: "xs", children: _jsx(Input, { type: "search", placeholder: "Search\u2026", value: search, onChange: (e) => handleSearch(e.target.value), className: "w-full rounded-md border px-[var(--appkit-space-2)] py-[var(--appkit-space-1)] text-[length:var(--appkit-text-xs)] focus:outline-none border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface-input)] text-[var(--appkit-color-text)]" }) })), _jsxs(Div, { className: `max-h-56 ${__O.yAuto}`, padding: "y-2xs", children: [filtered.map((option) => {
|
|
81
81
|
const isSelected = selected.includes(option.value);
|
|
82
|
-
return (_jsxs(Row, { gap: "xs", className: "cursor-pointer py-[0.375rem] hover:bg-[var(--appkit-color-bg)] mx-1", padding: "x-sm", rounded: "lg", onClick: () => toggle(option.value), children: [_jsx(Checkbox, { bare: true, type: selectionMode === "single" ? "radio" : "checkbox", checked: isSelected, onChange: () => toggle(option.value), "aria-label": option.label, className: "h-3.5 w-3.5 flex-shrink-0 cursor-pointer rounded
|
|
82
|
+
return (_jsxs(Row, { gap: "xs", className: "cursor-pointer py-[0.375rem] hover:bg-[var(--appkit-color-bg)] mx-1", padding: "x-sm", rounded: "lg", onClick: () => toggle(option.value), children: [_jsx(Checkbox, { bare: true, type: selectionMode === "single" ? "radio" : "checkbox", checked: isSelected, onChange: () => toggle(option.value), "aria-label": option.label, className: "h-3.5 w-3.5 flex-shrink-0 cursor-pointer rounded text-primary-600 border-[var(--appkit-color-border)] dark:text-secondary-500" }), _jsx(Span, { size: "xs", className: "flex-1 truncate", color: "muted", children: option.label }), option.count !== undefined && (_jsx(Span, { className: "flex-shrink-0 text-[10px] tabular-nums", color: "faint", children: option.count }))] }, option.value));
|
|
83
83
|
}), filtered.length === 0 && (_jsx(Text, { paddingY: "xs", paddingX: "x-sm", color: "faint", size: "xs", children: "No results" }))] }), hasValue && (_jsx(Div, { border: "subtle", className: "border-t p-[0.25rem]", children: _jsx(Button, { type: "button", variant: "ghost", size: "sm", className: CLS_CLEAR_BTN, onClick: () => { onChange([]); onClear?.(); setDropdownOpen(false); }, children: "Clear" }) }))] }))] }));
|
|
84
84
|
}
|
|
85
85
|
// ── Accordion variant (default) ─────────────────────────────────────────
|
|
@@ -45,7 +45,7 @@ export function Toggle({ checked: controlledChecked, defaultChecked = false, onC
|
|
|
45
45
|
const cfg = SIZE_CONFIG[size];
|
|
46
46
|
return (_jsxs(Row, { className: cn("gap-3", className), children: [_jsx(Button, { type: "button", role: "switch", id: toggleId, "aria-checked": checked, disabled: disabled, onClick: handleChange, className: cn("relative inline-flex items-center rounded-full transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-primary-500/30 dark:focus:ring-secondary-400/30 focus:ring-offset-2 dark:focus:ring-offset-slate-900", cfg.track, checked
|
|
47
47
|
? "bg-primary-600 dark:bg-secondary-500"
|
|
48
|
-
: "
|
|
48
|
+
: " bg-[var(--appkit-color-border)]", disabled && "opacity-50 cursor-not-allowed"), children: _jsx(Span, { className: cn("inline-block rounded-full bg-[var(--appkit-color-surface)] shadow-sm transition-transform duration-200", cfg.thumb, checked ? cfg.translateOn : cfg.translateOff) }) }), label && (_jsx("label", { htmlFor: toggleId, className: cn("text-sm font-medium cursor-pointer select-none", checked
|
|
49
49
|
? "text-[var(--appkit-color-text)]"
|
|
50
50
|
: "text-[var(--appkit-color-text-muted)]", disabled && "opacity-50 cursor-not-allowed"), children: label }))] }));
|
|
51
51
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
type Scope = "admin" | "store";
|
|
3
|
+
export interface GroupedListingEditorViewProps {
|
|
4
|
+
scope: Scope;
|
|
5
|
+
/** Present → edit that group; absent → create a new one. */
|
|
6
|
+
groupId?: string;
|
|
7
|
+
/**
|
|
8
|
+
* Store the group belongs to. Required for admin CREATE (an admin owns no
|
|
9
|
+
* store, so there is nothing to derive it from); ignored on the seller side,
|
|
10
|
+
* where the route reads it from the session and a body value would let a
|
|
11
|
+
* seller file a group under someone else's store.
|
|
12
|
+
*/
|
|
13
|
+
fixedStoreId?: string;
|
|
14
|
+
onSaved?: (id: string) => void;
|
|
15
|
+
onCancel?: () => void;
|
|
16
|
+
}
|
|
17
|
+
export declare function GroupedListingEditorView({ scope, groupId, fixedStoreId, onSaved, onCancel, }: GroupedListingEditorViewProps): React.JSX.Element;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
/*
|
|
4
|
+
* WHY: There were TWO hand-rolled grouped-listing forms (the seller's `new` and
|
|
5
|
+
* `[id]/edit` pages), between them covering 5 of the entity's 8 fields —
|
|
6
|
+
* `productIds` was hardcoded to `[]` on create, and `minActiveMembers` and
|
|
7
|
+
* `coverImage` could not be set anywhere at all. Admin had no form.
|
|
8
|
+
* WHAT: One editor for both portals, deriving its sections from
|
|
9
|
+
* `groupedListingFormSchema` so a field added to the schema appears here
|
|
10
|
+
* without a second edit.
|
|
11
|
+
*
|
|
12
|
+
* ## The admin API was the blocker, not this component
|
|
13
|
+
*
|
|
14
|
+
* Pointing an editor at the admin endpoints used to give: create → 405 (there
|
|
15
|
+
* was no admin POST), and every save of `title`/`groupTheme`/`isActive` → a
|
|
16
|
+
* 200 that wrote nothing, because the admin PATCH schema was
|
|
17
|
+
* `z.object({ productIds })` and `z.object()` strips unknown keys. That is
|
|
18
|
+
* Root Cause #40's exact shape — a success response is what the broken version
|
|
19
|
+
* returns, so only a reload reveals it. Both were fixed alongside this file;
|
|
20
|
+
* an `endpointOverride` on the seller form could never have worked.
|
|
21
|
+
*
|
|
22
|
+
* ## Two fields are overridden, the other six are derived
|
|
23
|
+
*
|
|
24
|
+
* `productIds` needs the real product search (`ProductInlineSelect`), and admin
|
|
25
|
+
* create needs a store picker — a grouped listing is store-owned and the seller
|
|
26
|
+
* route takes `storeId` from the session, which an admin has no equivalent of.
|
|
27
|
+
* Everything else comes from `buildSectionsFromSchema`.
|
|
28
|
+
*
|
|
29
|
+
* EXPORTS: GroupedListingEditorView, type GroupedListingEditorViewProps
|
|
30
|
+
*
|
|
31
|
+
* @tag domain:grouped
|
|
32
|
+
* @tag layer:feature
|
|
33
|
+
* @tag pattern:slot-shell
|
|
34
|
+
* @tag access:client
|
|
35
|
+
* @tag consumers:admin/grouped-listings pages,store/grouped-listings pages
|
|
36
|
+
* @tag sideEffects:network
|
|
37
|
+
*/
|
|
38
|
+
import React from "react";
|
|
39
|
+
import { useQuery } from "@tanstack/react-query";
|
|
40
|
+
import { useRouter } from "next/navigation";
|
|
41
|
+
import { Stack, Text, useToast } from "../../../ui";
|
|
42
|
+
import { FieldSelect, FormShellContext, FormErrorSummary, applyZodIssues, useFormShellState, } from "../../../ui/forms";
|
|
43
|
+
import { SectionForm, useSectionFormNav } from "../../shell";
|
|
44
|
+
import { buildSectionsFromSchema } from "../../shell/build-sections";
|
|
45
|
+
import { apiClient } from "../../../http";
|
|
46
|
+
import { ADMIN_ENDPOINTS, SELLER_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
47
|
+
import { ROUTES } from "../../../next/routing/route-map";
|
|
48
|
+
import { normalizeError } from "../../../errors/normalize";
|
|
49
|
+
import { ProductInlineSelect } from "../../seller/components/ProductInlineSelect";
|
|
50
|
+
import { groupedListingFormSchema, } from "../schemas/grouped-listing-form";
|
|
51
|
+
const EMPTY = {
|
|
52
|
+
title: "",
|
|
53
|
+
description: "",
|
|
54
|
+
groupTheme: "generic",
|
|
55
|
+
productIds: [],
|
|
56
|
+
minActiveMembers: 1,
|
|
57
|
+
coverImage: "",
|
|
58
|
+
isActive: true,
|
|
59
|
+
isFeatured: false,
|
|
60
|
+
};
|
|
61
|
+
export function GroupedListingEditorView({ scope, groupId, fixedStoreId, onSaved, onCancel, }) {
|
|
62
|
+
const router = useRouter();
|
|
63
|
+
const { showToast } = useToast();
|
|
64
|
+
const isEdit = Boolean(groupId);
|
|
65
|
+
const isAdmin = scope === "admin";
|
|
66
|
+
const listEndpoint = isAdmin ? ADMIN_ENDPOINTS.GROUPED_LISTINGS : SELLER_ENDPOINTS.GROUPED_LISTINGS;
|
|
67
|
+
const byIdEndpoint = isAdmin
|
|
68
|
+
? ADMIN_ENDPOINTS.GROUPED_LISTING_BY_ID
|
|
69
|
+
: SELLER_ENDPOINTS.GROUPED_LISTING_BY_ID;
|
|
70
|
+
const [values, setValues] = React.useState(EMPTY);
|
|
71
|
+
const [storeId, setStoreId] = React.useState(fixedStoreId ?? "");
|
|
72
|
+
const [saving, setSaving] = React.useState(false);
|
|
73
|
+
/*
|
|
74
|
+
* Seeded from the single-item GET, never from a cached list row — a list
|
|
75
|
+
* projection is narrower than the document, and an editor seeded from one
|
|
76
|
+
* re-sends the fields it never received as their defaults (Root Cause #38).
|
|
77
|
+
*/
|
|
78
|
+
const existing = useQuery({
|
|
79
|
+
queryKey: [scope, "grouped-listing", groupId],
|
|
80
|
+
queryFn: async () => {
|
|
81
|
+
const res = await apiClient.get(byIdEndpoint(groupId));
|
|
82
|
+
const body = res?.data ?? res;
|
|
83
|
+
// The admin GET wraps in `{ item }`; the seller GET returns the doc.
|
|
84
|
+
return (body?.item ?? body);
|
|
85
|
+
},
|
|
86
|
+
enabled: isEdit,
|
|
87
|
+
});
|
|
88
|
+
// Admin create only: an admin has no session store to derive the owner from.
|
|
89
|
+
const stores = useQuery({
|
|
90
|
+
queryKey: ["admin", "grouped-listing-editor", "stores"],
|
|
91
|
+
queryFn: async () => {
|
|
92
|
+
const res = await apiClient.get(`${ADMIN_ENDPOINTS.STORES}?pageSize=200&sorts=storeName`);
|
|
93
|
+
const body = res?.data ?? res;
|
|
94
|
+
return (body?.items ?? []);
|
|
95
|
+
},
|
|
96
|
+
enabled: isAdmin && !isEdit && !fixedStoreId,
|
|
97
|
+
});
|
|
98
|
+
React.useEffect(() => {
|
|
99
|
+
const doc = existing.data;
|
|
100
|
+
if (!doc)
|
|
101
|
+
return;
|
|
102
|
+
setValues({
|
|
103
|
+
title: String(doc.title ?? ""),
|
|
104
|
+
description: String(doc.description ?? ""),
|
|
105
|
+
groupTheme: doc.groupTheme ?? "generic",
|
|
106
|
+
productIds: Array.isArray(doc.productIds) ? doc.productIds.map(String) : [],
|
|
107
|
+
minActiveMembers: Number(doc.minActiveMembers ?? 1),
|
|
108
|
+
coverImage: String(doc.coverImage ?? ""),
|
|
109
|
+
isActive: doc.isActive !== false,
|
|
110
|
+
isFeatured: doc.isFeatured === true,
|
|
111
|
+
});
|
|
112
|
+
if (doc.storeId)
|
|
113
|
+
setStoreId(String(doc.storeId));
|
|
114
|
+
}, [existing.data]);
|
|
115
|
+
const update = React.useCallback((partial) => {
|
|
116
|
+
setValues((prev) => ({ ...prev, ...partial }));
|
|
117
|
+
}, []);
|
|
118
|
+
const needsStorePicker = isAdmin && !isEdit && !fixedStoreId;
|
|
119
|
+
const sections = React.useMemo(() => buildSectionsFromSchema(groupedListingFormSchema, {
|
|
120
|
+
renderers: {
|
|
121
|
+
productIds: ({ values: v, onChange }) => (_jsxs(Stack, { gap: "xs", children: [_jsx(ProductInlineSelect, { scope: scope, multiple: true, value: v.productIds ?? [], onChange: (ids) => onChange({ productIds: ids }), placeholder: "Search products to add\u2026" }), _jsxs(Text, { size: "xs", color: "muted", children: [(v.productIds ?? []).length, " product", (v.productIds ?? []).length === 1 ? "" : "s", " in this group."] })] })),
|
|
122
|
+
},
|
|
123
|
+
}), [scope]);
|
|
124
|
+
/*
|
|
125
|
+
* The nav seam — without it `FormErrorSummary`'s "jump to section" resolves
|
|
126
|
+
* to undefined, which is what it did for every form before W0.
|
|
127
|
+
*/
|
|
128
|
+
const { openIds, setOpenIds, goToSection, fieldToSectionIndex, sectionMeta } = useSectionFormNav(sections, values);
|
|
129
|
+
const { shellCtx, setFieldError, validate } = useFormShellState(groupedListingFormSchema, {
|
|
130
|
+
sections: sectionMeta,
|
|
131
|
+
onGoToSection: goToSection,
|
|
132
|
+
fieldToSectionIndex,
|
|
133
|
+
});
|
|
134
|
+
React.useEffect(() => {
|
|
135
|
+
validate(values);
|
|
136
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
137
|
+
}, [values, validate]);
|
|
138
|
+
const handleSubmit = async () => {
|
|
139
|
+
const parsed = groupedListingFormSchema.safeParse(values);
|
|
140
|
+
if (!parsed.success) {
|
|
141
|
+
applyZodIssues(parsed.error.issues, setFieldError);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (needsStorePicker && !storeId) {
|
|
145
|
+
showToast("Pick the store this group belongs to.", "error");
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
setSaving(true);
|
|
149
|
+
try {
|
|
150
|
+
if (isEdit) {
|
|
151
|
+
await apiClient.patch(byIdEndpoint(groupId), parsed.data);
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
/*
|
|
155
|
+
* `storeId` is sent ONLY by admin. The seller route derives it from the
|
|
156
|
+
* session and rejects it in the body, which is what stops a seller
|
|
157
|
+
* filing a group under another store.
|
|
158
|
+
*/
|
|
159
|
+
const payload = isAdmin ? { ...parsed.data, storeId } : parsed.data;
|
|
160
|
+
const res = await apiClient.post(listEndpoint, payload);
|
|
161
|
+
const created = res?.data?.id;
|
|
162
|
+
if (created && onSaved) {
|
|
163
|
+
showToast("Group created.", "success");
|
|
164
|
+
onSaved(created);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
showToast(isEdit ? "Group updated." : "Group created.", "success");
|
|
169
|
+
if (onSaved)
|
|
170
|
+
onSaved(groupId ?? "");
|
|
171
|
+
else
|
|
172
|
+
router.push(String(isAdmin ? ROUTES.ADMIN.GROUPED_LISTINGS : ROUTES.STORE.GROUPED_LISTINGS));
|
|
173
|
+
}
|
|
174
|
+
catch (err) {
|
|
175
|
+
const e = normalizeError(err);
|
|
176
|
+
showToast(e.message || "Failed to save the group.", "error");
|
|
177
|
+
}
|
|
178
|
+
finally {
|
|
179
|
+
setSaving(false);
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
if (isEdit && existing.isLoading) {
|
|
183
|
+
return _jsx(Text, { color: "muted", children: "Loading\u2026" });
|
|
184
|
+
}
|
|
185
|
+
return (_jsxs(Stack, { gap: "md", children: [needsStorePicker ? (_jsx(FieldSelect, { name: "storeId", label: "Store", required: true, value: storeId, onChange: setStoreId, options: [
|
|
186
|
+
{ value: "", label: "Select a store…" },
|
|
187
|
+
...(stores.data ?? []).map((s) => ({
|
|
188
|
+
value: s.id,
|
|
189
|
+
label: s.storeName ?? s.id,
|
|
190
|
+
})),
|
|
191
|
+
] })) : null, _jsxs(FormShellContext.Provider, { value: shellCtx, children: [_jsx(FormErrorSummary, {}), _jsx(SectionForm, { sections: sections, values: values, onChange: update, onSubmit: handleSubmit, schema: groupedListingFormSchema, openIds: openIds, onOpenChange: setOpenIds, submitLabel: isEdit ? "Save changes" : "Create group", isLoading: saving, onCancel: onCancel })] })] }));
|
|
192
|
+
}
|
|
@@ -11,8 +11,8 @@ const __O = {
|
|
|
11
11
|
hidden: "overflow-hidden",
|
|
12
12
|
};
|
|
13
13
|
const CLS_GRADIENT_BG = "absolute inset-0 bg-[image:var(--appkit-gradient-promotion)]";
|
|
14
|
-
const CLS_CTA_BTN_SM = "bg-[var(--appkit-color-surface)] bg-[var(--appkit-color-surface)] text-indigo-700 hover:bg-
|
|
15
|
-
const CLS_CTA_BTN_LG = "bg-[var(--appkit-color-surface)] bg-[var(--appkit-color-surface)] text-indigo-700 hover:bg-
|
|
14
|
+
const CLS_CTA_BTN_SM = "bg-[var(--appkit-color-surface)] bg-[var(--appkit-color-surface)] text-indigo-700 hover:bg-surface-hover font-semibold gap-[var(--appkit-space-1-5)] flex-shrink-0";
|
|
15
|
+
const CLS_CTA_BTN_LG = "bg-[var(--appkit-color-surface)] bg-[var(--appkit-color-surface)] text-indigo-700 hover:bg-surface-hover font-semibold shadow-lg gap-[var(--appkit-space-2)]";
|
|
16
16
|
// --- Constants ---------------------------------------------------------------
|
|
17
17
|
const CLS_CONTAINER = "w-full max-w-7xl mx-auto";
|
|
18
18
|
// --- Component ---------------------------------------------------------------
|
|
@@ -29,7 +29,7 @@ function BrandLogo({ brand }) {
|
|
|
29
29
|
const iconSrc = brand.display?.icon;
|
|
30
30
|
const coverImage = brand.display?.coverImage;
|
|
31
31
|
const initial = brand.name[0]?.toUpperCase() ?? "?";
|
|
32
|
-
return (_jsxs(Link, { href: ROUTES.PUBLIC.CATEGORY_DETAIL(brand.slug), className: "flex h-32 w-36 flex-col items-center justify-center gap-[var(--appkit-space-2)] rounded-xl border
|
|
32
|
+
return (_jsxs(Link, { href: ROUTES.PUBLIC.CATEGORY_DETAIL(brand.slug), className: "flex h-32 w-36 flex-col items-center justify-center gap-[var(--appkit-space-2)] rounded-xl border bg-[var(--appkit-color-surface)] p-[var(--appkit-space-3)] shadow-sm transition-all hover:border-primary-300 hover:shadow-md border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] md:h-40", children: [iconSrc || coverImage ? (_jsx(Image, { src: resolveMediaUrl(iconSrc ?? coverImage), alt: brand.name, width: 64, height: 64, className: "h-16 w-16 rounded object-contain" })) : (_jsx(Row, { textWeight: "bold", textSize: "sm", className: "h-16 w-16 bg-primary-100 text-primary-700 dark:bg-primary-900 dark:text-primary-300", align: "center", justify: "center", rounded: "lg", children: initial })), _jsx(Text, { className: "w-full truncate", color: "muted", size: "xs", weight: "medium", align: "start", children: brand.name })] }));
|
|
33
33
|
}
|
|
34
34
|
export function BrandsSection({ title = "Top Brands", subtitle, limit = 12, viewMoreHref, viewMoreLabel = "All brands →", className = "", initialItems, cta, filters, autoScroll = true, scrollInterval = 4000, loop = true, }) {
|
|
35
35
|
const themed = { textPrimary: THEMED_TEXT_PRIMARY };
|
|
@@ -3,5 +3,5 @@ import Link from "next/link";
|
|
|
3
3
|
import { Div, Heading, Row, Section, Span, Text } from "../../../ui";
|
|
4
4
|
import { ROUTES } from "../../../next/routing/route-map";
|
|
5
5
|
export function CTABannerSection({ title = "Thousands of collectibles. One marketplace.", subtitle, primaryLabel = "Shop All Products →", primaryHref = String(ROUTES.PUBLIC.PRODUCTS), secondaryLabel = "Browse Auctions →", secondaryHref = String(ROUTES.PUBLIC.AUCTIONS), className = "", }) {
|
|
6
|
-
return (_jsx(Section, { tone: "accent-banner", className: `${className}`, paddingY: "y-4xl", paddingX: "x-md", children: _jsxs(Div, { className: "mx-auto max-w-3xl text-left", children: [_jsx(Span, { className: "mb-4 inline-block", size: "2xl", "aria-hidden": "true", children: "\u2728" }), _jsx(Heading, { color: "inverse", level: 2, mdSize: "3xl", lgSize: "4xl", size: "2xl", weight: "bold", children: title }), subtitle && (_jsx(Text, { color: "inverse", className: "mt-3 /80", size: "base", children: subtitle })), _jsxs(Row, { className: "mt-8", align: "center", justify: "start", gap: "md", wrap: true, children: [_jsx(Link, { href: primaryHref, className: "inline-flex items-center rounded-full bg-white px-[var(--appkit-space-8)] py-[var(--appkit-space-3)] text-[length:var(--appkit-text-sm)] font-semibold text-primary-700 shadow-sm transition-all hover:bg-
|
|
6
|
+
return (_jsx(Section, { tone: "accent-banner", className: `${className}`, paddingY: "y-4xl", paddingX: "x-md", children: _jsxs(Div, { className: "mx-auto max-w-3xl text-left", children: [_jsx(Span, { className: "mb-4 inline-block", size: "2xl", "aria-hidden": "true", children: "\u2728" }), _jsx(Heading, { color: "inverse", level: 2, mdSize: "3xl", lgSize: "4xl", size: "2xl", weight: "bold", children: title }), subtitle && (_jsx(Text, { color: "inverse", className: "mt-3 /80", size: "base", children: subtitle })), _jsxs(Row, { className: "mt-8", align: "center", justify: "start", gap: "md", wrap: true, children: [_jsx(Link, { href: primaryHref, className: "inline-flex items-center rounded-full bg-white px-[var(--appkit-space-8)] py-[var(--appkit-space-3)] text-[length:var(--appkit-text-sm)] font-semibold text-primary-700 shadow-sm transition-all hover:bg-surface-hover hover:shadow-md focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-primary-600", children: primaryLabel }), secondaryHref && (_jsx(Link, { href: secondaryHref, className: "inline-flex items-center rounded-full border-2 border-white/80 px-[var(--appkit-space-8)] py-[var(--appkit-space-3)] text-[length:var(--appkit-text-sm)] font-semibold text-white transition-all hover:bg-white/10 focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-primary-600", children: secondaryLabel }))] })] }) }));
|
|
7
7
|
}
|
|
@@ -5,5 +5,5 @@ import { useHomepageReviews } from "../hooks/useHomepageReviews";
|
|
|
5
5
|
import { ReviewCard } from "../../reviews/components";
|
|
6
6
|
export function HomepageCustomerReviewsSection({ title = "What Our Customers Say", subtitle, viewMoreHref, viewMoreLabel = "See all reviews →", className = "", autoScroll = true, scrollInterval = 4500, loop = true, }) {
|
|
7
7
|
const { data: reviews = [], isLoading } = useHomepageReviews();
|
|
8
|
-
return (_jsx(CustomerReviewsSection, { title: title, subtitle: subtitle, items: reviews, renderItem: (review) => (_jsx(ReviewCard, { review: review, className: "h-full
|
|
8
|
+
return (_jsx(CustomerReviewsSection, { title: title, subtitle: subtitle, items: reviews, renderItem: (review) => (_jsx(ReviewCard, { review: review, className: "h-full shadow-sm border-[var(--appkit-color-border)]" })), viewMoreHref: viewMoreHref, viewMoreLabel: viewMoreLabel, isLoading: isLoading, keyExtractor: (review) => review.id, className: className, autoScroll: autoScroll, scrollInterval: scrollInterval, loop: loop }));
|
|
9
9
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Crash containment for a single homepage section.
|
|
3
|
+
*
|
|
4
|
+
* The homepage renders ~26 independently-fetched sections. Before this existed,
|
|
5
|
+
* a throw inside ANY one of them propagated to `[locale]/error.tsx` and replaced
|
|
6
|
+
* the entire page with "Something went wrong" — which is exactly what one bad
|
|
7
|
+
* import in `PrizeDrawsSection` did in production on 2026-08-26.
|
|
8
|
+
*
|
|
9
|
+
* A section is not load-bearing for the page. Losing one should cost that strip
|
|
10
|
+
* and nothing else, so the fallback renders NOTHING: a visitor sees a homepage
|
|
11
|
+
* missing one row instead of a homepage that is entirely an error message.
|
|
12
|
+
*
|
|
13
|
+
* The failure is still reported — `ErrorBoundary` calls `trackError`, which the
|
|
14
|
+
* app wires to the client-error beacon, so the row lands in `serverErrors` and
|
|
15
|
+
* shows up at /admin/maintenance/server-errors. Silent to the visitor, loud to
|
|
16
|
+
* the operator; the inverse would be the Root Cause #59 swallow.
|
|
17
|
+
*
|
|
18
|
+
* `sectionId` is a plain string on purpose. This is a Client Component rendered
|
|
19
|
+
* from a Server Component, so a function prop (e.g. `onError`) would not be
|
|
20
|
+
* serialisable across that boundary.
|
|
21
|
+
*/
|
|
22
|
+
import { Component, type ErrorInfo, type ReactNode } from "react";
|
|
23
|
+
export interface HomepageSectionBoundaryProps {
|
|
24
|
+
/** Homepage section document id — identifies the strip in the error report. */
|
|
25
|
+
sectionId: string;
|
|
26
|
+
/** Section type, e.g. "prize-draws". */
|
|
27
|
+
sectionType: string;
|
|
28
|
+
children: ReactNode;
|
|
29
|
+
}
|
|
30
|
+
interface State {
|
|
31
|
+
hasError: boolean;
|
|
32
|
+
}
|
|
33
|
+
export declare class HomepageSectionBoundary extends Component<HomepageSectionBoundaryProps, State> {
|
|
34
|
+
constructor(props: HomepageSectionBoundaryProps);
|
|
35
|
+
static getDerivedStateFromError(): State;
|
|
36
|
+
componentDidCatch(error: Error, errorInfo: ErrorInfo): void;
|
|
37
|
+
render(): ReactNode;
|
|
38
|
+
}
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
/**
|
|
3
|
+
* Crash containment for a single homepage section.
|
|
4
|
+
*
|
|
5
|
+
* The homepage renders ~26 independently-fetched sections. Before this existed,
|
|
6
|
+
* a throw inside ANY one of them propagated to `[locale]/error.tsx` and replaced
|
|
7
|
+
* the entire page with "Something went wrong" — which is exactly what one bad
|
|
8
|
+
* import in `PrizeDrawsSection` did in production on 2026-08-26.
|
|
9
|
+
*
|
|
10
|
+
* A section is not load-bearing for the page. Losing one should cost that strip
|
|
11
|
+
* and nothing else, so the fallback renders NOTHING: a visitor sees a homepage
|
|
12
|
+
* missing one row instead of a homepage that is entirely an error message.
|
|
13
|
+
*
|
|
14
|
+
* The failure is still reported — `ErrorBoundary` calls `trackError`, which the
|
|
15
|
+
* app wires to the client-error beacon, so the row lands in `serverErrors` and
|
|
16
|
+
* shows up at /admin/maintenance/server-errors. Silent to the visitor, loud to
|
|
17
|
+
* the operator; the inverse would be the Root Cause #59 swallow.
|
|
18
|
+
*
|
|
19
|
+
* `sectionId` is a plain string on purpose. This is a Client Component rendered
|
|
20
|
+
* from a Server Component, so a function prop (e.g. `onError`) would not be
|
|
21
|
+
* serialisable across that boundary.
|
|
22
|
+
*/
|
|
23
|
+
import { Component } from "react";
|
|
24
|
+
import { trackError, ErrorCategory, ErrorSeverity, } from "../../../monitoring/error-tracking";
|
|
25
|
+
export class HomepageSectionBoundary extends Component {
|
|
26
|
+
constructor(props) {
|
|
27
|
+
super(props);
|
|
28
|
+
this.state = { hasError: false };
|
|
29
|
+
}
|
|
30
|
+
static getDerivedStateFromError() {
|
|
31
|
+
return { hasError: true };
|
|
32
|
+
}
|
|
33
|
+
componentDidCatch(error, errorInfo) {
|
|
34
|
+
trackError(error, ErrorCategory.UI, ErrorSeverity.HIGH, {
|
|
35
|
+
component: "HomepageSection",
|
|
36
|
+
metadata: {
|
|
37
|
+
sectionId: this.props.sectionId,
|
|
38
|
+
sectionType: this.props.sectionType,
|
|
39
|
+
componentStack: errorInfo.componentStack ?? null,
|
|
40
|
+
digest: error.digest ?? null,
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
render() {
|
|
45
|
+
if (this.state.hasError)
|
|
46
|
+
return null;
|
|
47
|
+
return this.props.children;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -23,7 +23,7 @@ function CategoryChip({ category }) {
|
|
|
23
23
|
const coverImage = category.display?.coverImage;
|
|
24
24
|
const initial = category.name[0]?.toUpperCase() ?? "?";
|
|
25
25
|
const productCount = category.metrics?.totalProductCount ?? category.metrics?.productCount ?? 0;
|
|
26
|
-
return (_jsxs(Link, { href: ROUTES.PUBLIC.CATEGORY_DETAIL(category.slug), className: "group flex w-full min-h-[180px] sm:min-h-[220px] flex-col overflow-hidden rounded-xl border
|
|
26
|
+
return (_jsxs(Link, { href: ROUTES.PUBLIC.CATEGORY_DETAIL(category.slug), className: "group flex w-full min-h-[180px] sm:min-h-[220px] flex-col overflow-hidden rounded-xl border bg-[var(--appkit-color-surface)] shadow-sm transition-all hover:border-primary-300 hover:shadow-md border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] dark:hover:border-primary-600", children: [coverImage && isImageUrl(coverImage) ? (_jsx(Div, { className: `aspect-video w-full ${__O.hidden}`, surface: "subtle", children: _jsx(Image, { src: resolveMediaUrl(coverImage), alt: category.name, width: 320, height: 180, className: "h-full w-full object-cover transition-transform duration-300 group-hover:scale-105" }) })) : (_jsx(Div, { surface: "muted", className: "aspect-video w-full" })), _jsxs(Stack, { className: `flex-1 ${__P.p3} text-left`, children: [_jsx(Row, { textWeight: "bold", textSize: "sm", className: "mb-2 h-9 w-9 bg-primary-100 text-primary-700 dark:bg-primary-900 dark:text-primary-300", align: "center", justify: "center", rounded: "lg", children: iconSrc && isImageUrl(iconSrc) ? (_jsx(Image, { src: resolveMediaUrl(iconSrc), alt: "", width: 24, height: 24, className: "h-6 w-6 rounded object-cover", "aria-hidden": true })) : iconSrc ? (_jsx(Span, { size: "lg", "aria-hidden": "true", className: "leading-none", children: iconSrc })) : (initial) }), _jsx(Text, { size: "sm", weight: "semibold", color: "primary", truncate: 2, children: category.name }), _jsxs(Text, { className: "mt-1", color: "muted", size: "xs", children: [productCount.toLocaleString(), " items"] }), _jsx(Text, { className: "mt-auto text-primary dark:text-primary-400 pt-[0.75rem]", size: "xs", weight: "medium", children: "Browse category \u2192" })] })] }));
|
|
27
27
|
}
|
|
28
28
|
const CTA_CLASSES = {
|
|
29
29
|
filled: "rounded-lg bg-[var(--appkit-color-primary)] px-[var(--appkit-space-5)] py-[var(--appkit-space-2)] text-[length:var(--appkit-text-sm)] font-semibold text-white hover:opacity-90",
|
|
@@ -9,5 +9,5 @@ export function WelcomeSection({ title, subtitle, pillLabel, showCTA = true, cta
|
|
|
9
9
|
if (isLoading) {
|
|
10
10
|
return (_jsx(Section, { className: `relative overflow-hidden pt-[0px] pb-[var(--appkit-space-16)] md:pt-[0px] md:pb-[6rem] ${className}`, paddingX: "x-md", children: _jsxs(Div, { className: "animate-pulse max-w-4xl mx-auto text-left", children: [_jsx(Div, { className: "h-6 w-52 mb-6", surface: "subtle", rounded: "full" }), _jsx(Div, { className: "h-20 mb-4 max-w-2xl", surface: "subtle", rounded: "lg" }), _jsx(Div, { className: "h-6 mb-8 max-w-lg", surface: "subtle", rounded: "lg" }), _jsxs(Row, { justify: "start", gap: "md", children: [_jsx(Div, { className: "h-12 w-36", surface: "subtle", rounded: "xl" }), _jsx(Div, { className: "h-12 w-36", surface: "subtle", rounded: "xl" })] })] }) }));
|
|
11
11
|
}
|
|
12
|
-
return (_jsxs(Section, { className: `relative overflow-hidden pt-[0px] pb-[var(--appkit-space-20)] md:pt-[0px] md:pb-[7rem] ${className}`, paddingX: "x-md", children: [_jsx(Div, { className: "pointer-events-none absolute -top-32 -left-32 w-[28rem] h-[28rem] bg-primary/10 blur-3xl", rounded: "full", "aria-hidden": "true" }), _jsx(Div, { className: "pointer-events-none absolute -bottom-40 -right-40 w-[36rem] h-[36rem] bg-secondary/10 dark:bg-secondary/15 blur-3xl", rounded: "full", "aria-hidden": "true" }), _jsx(Div, { className: "relative z-10 max-w-7xl mx-auto", children: _jsxs(Grid, { align: "center", gap: "2xl", className: "grid-cols-1 lg:grid-cols-2 xl:grid-cols-2 2xl:grid-cols-2", children: [_jsxs(Div, { className: "text-left", children: [pillLabel && (_jsx(Div, { children: _jsxs(Span, { layout: "inline-flex", gap: "md", size: "xs", weight: "medium", className: "border border-primary-[500]/30 bg-primary-500/10 px-[1.25rem] py-[0.375rem] tracking-[0.2em] text-primary-700 dark:text-primary-400 backdrop-blur-sm", rounded: "full", transform: "uppercase", children: [_jsx(Span, { className: "w-1.5 h-1.5 bg-primary-500 inline-block", rounded: "full", "aria-hidden": "true" }), pillLabel, _jsx(Span, { className: "w-1.5 h-1.5 bg-primary-500 inline-block", rounded: "full", "aria-hidden": "true" })] }) })), _jsx(Heading, { level: 1, variant: "none", gradient: "brand-tri", className: "mt-4 font-display leading-[1.1] tracking-tight break-words", size: "5xl", mdSize: "6xl", lgSize: "7xl", xlSize: "7xl", children: title }), subtitle && (_jsx(Text, { className: `mt-4 ${themed.textSecondary} max-w-xl leading-relaxed`, size: "xl", children: subtitle })), showCTA && (_jsxs(Row, { wrap: true, gap: "md", className: "mt-8", justify: "start", children: [ctaHref ? (_jsx(TextLink, { rounded: "xl", href: ctaHref, className: "inline-flex items-[center] justify-[center] px-[2rem] py-[0.875rem] !bg-primary hover:!bg-primary-600 text-white dark:!bg-primary dark:hover:!bg-primary-600 dark:text-white btn-glow transition-all hover:scale-[1.02]", size: "base", weight: "bold", children: ctaLabel })) : (_jsx(Button, { variant: "primary", size: "lg", onClick: onCtaClick, children: ctaLabel })), secondaryCtaHref ? (_jsx(TextLink, { rounded: "xl", href: secondaryCtaHref, className: "inline-flex items-[center] justify-[center] border-2 border-cobalt/40 dark:border-cobalt-400/40 px-[2rem] py-[0.875rem] text-cobalt-700 dark:text-cobalt-300 hover:bg-
|
|
12
|
+
return (_jsxs(Section, { className: `relative overflow-hidden pt-[0px] pb-[var(--appkit-space-20)] md:pt-[0px] md:pb-[7rem] ${className}`, paddingX: "x-md", children: [_jsx(Div, { className: "pointer-events-none absolute -top-32 -left-32 w-[28rem] h-[28rem] bg-primary/10 blur-3xl", rounded: "full", "aria-hidden": "true" }), _jsx(Div, { className: "pointer-events-none absolute -bottom-40 -right-40 w-[36rem] h-[36rem] bg-secondary/10 dark:bg-secondary/15 blur-3xl", rounded: "full", "aria-hidden": "true" }), _jsx(Div, { className: "relative z-10 max-w-7xl mx-auto", children: _jsxs(Grid, { align: "center", gap: "2xl", className: "grid-cols-1 lg:grid-cols-2 xl:grid-cols-2 2xl:grid-cols-2", children: [_jsxs(Div, { className: "text-left", children: [pillLabel && (_jsx(Div, { children: _jsxs(Span, { layout: "inline-flex", gap: "md", size: "xs", weight: "medium", className: "border border-primary-[500]/30 bg-primary-500/10 px-[1.25rem] py-[0.375rem] tracking-[0.2em] text-primary-700 dark:text-primary-400 backdrop-blur-sm", rounded: "full", transform: "uppercase", children: [_jsx(Span, { className: "w-1.5 h-1.5 bg-primary-500 inline-block", rounded: "full", "aria-hidden": "true" }), pillLabel, _jsx(Span, { className: "w-1.5 h-1.5 bg-primary-500 inline-block", rounded: "full", "aria-hidden": "true" })] }) })), _jsx(Heading, { level: 1, variant: "none", gradient: "brand-tri", className: "mt-4 font-display leading-[1.1] tracking-tight break-words", size: "5xl", mdSize: "6xl", lgSize: "7xl", xlSize: "7xl", children: title }), subtitle && (_jsx(Text, { className: `mt-4 ${themed.textSecondary} max-w-xl leading-relaxed`, size: "xl", children: subtitle })), showCTA && (_jsxs(Row, { wrap: true, gap: "md", className: "mt-8", justify: "start", children: [ctaHref ? (_jsx(TextLink, { rounded: "xl", href: ctaHref, className: "inline-flex items-[center] justify-[center] px-[2rem] py-[0.875rem] !bg-primary hover:!bg-primary-600 text-white dark:!bg-primary dark:hover:!bg-primary-600 dark:text-white btn-glow transition-all hover:scale-[1.02]", size: "base", weight: "bold", children: ctaLabel })) : (_jsx(Button, { variant: "primary", size: "lg", onClick: onCtaClick, children: ctaLabel })), secondaryCtaHref ? (_jsx(TextLink, { rounded: "xl", href: secondaryCtaHref, className: "inline-flex items-[center] justify-[center] border-2 border-cobalt/40 dark:border-cobalt-400/40 px-[2rem] py-[0.875rem] text-cobalt-700 dark:text-cobalt-300 hover:bg-primary-surface transition-all hover:scale-[1.02]", size: "base", weight: "semibold", children: secondaryCtaLabel })) : (_jsx(Button, { variant: "outline", size: "lg", onClick: onSecondaryCtaClick, children: secondaryCtaLabel }))] })), trustChips.length > 0 && (_jsx(Row, { wrap: true, gap: "sm", className: "mt-6", justify: "start", children: trustChips.map((chip) => (_jsxs(Span, { layout: "inline-flex", gap: "xs", size: "xs", weight: "medium", className: "border border-zinc-[200] dark:border-slate-[700] px-[0.875rem] py-[0.375rem]", rounded: "full", surface: "subtle", color: "muted", children: [chip.emoji, " ", chip.label] }, chip.key))) }))] }), _jsx(Div, { className: "hidden lg:block", children: _jsxs(Div, { className: `relative overflow-hidden aspect-[4/3] bg-[image:var(--appkit-gradient-section-mesh)]`, shadow: "2xl", border: "default", rounded: "3xl", children: [_jsx(Div, { className: "absolute inset-0 bg-[image:var(--appkit-gradient-glass)]" }), _jsxs(Stack, { centered: true, gap: "sm", className: `absolute inset-0 ${flex.center} p-[2.5rem]`, children: [_jsx(SiteMark, { title: brandLogoText || "LetItRip.in", size: "xl" }), _jsx(SiteLogo, { title: brandLogoText || "LetItRip.in", size: "xl" })] })] }) })] }) })] }));
|
|
13
13
|
}
|
|
@@ -28,6 +28,7 @@ import { GoogleReviewsSection } from "../components/GoogleReviewsSection";
|
|
|
28
28
|
// S-SBUNI-3 2026-05-13 — FeaturedBundlesSection rebuilt against
|
|
29
29
|
// categoryType:"bundle" rows on the categories collection.
|
|
30
30
|
import { FeaturedBundlesSection } from "../components/FeaturedBundlesSection";
|
|
31
|
+
import { HomepageSectionBoundary } from "../components/HomepageSectionBoundary";
|
|
31
32
|
import { PrizeDrawsSection } from "../../products/components/PrizeDrawsSection";
|
|
32
33
|
import { EventRafflesSection } from "../../events/components/EventRafflesSection";
|
|
33
34
|
import { CollectionCardsSection } from "../components/CollectionCardsSection";
|
|
@@ -200,6 +201,6 @@ export function renderSection(section, adSlots, newsletterFormSlot, faqItems, sl
|
|
|
200
201
|
if (!sectionElement)
|
|
201
202
|
return null;
|
|
202
203
|
const adSlotKey = AD_SLOT_MAP[section.type];
|
|
203
|
-
return (_jsxs(React.Fragment, { children: [sectionElement, adSlots && adSlotKey !== undefined && adSlotKey in adSlots && adSlots[adSlotKey]] }, section.id));
|
|
204
|
+
return (_jsxs(React.Fragment, { children: [_jsx(HomepageSectionBoundary, { sectionId: section.id, sectionType: section.type, children: sectionElement }), adSlots && adSlotKey !== undefined && adSlotKey in adSlots && adSlots[adSlotKey]] }, section.id));
|
|
204
205
|
}
|
|
205
206
|
export { AnnouncementBar, hashBannerMessage };
|
|
@@ -76,11 +76,11 @@ function SidebarUserHeader({ user, onClose, }) {
|
|
|
76
76
|
}
|
|
77
77
|
: user.photoURL
|
|
78
78
|
? { url: user.photoURL, position: { x: 50, y: 50 }, zoom: 1 }
|
|
79
|
-
: null, size: "md", alt: user.displayName || "User", displayName: user.displayName, email: user.email }), user.role && !isBuyerUser(user) && (_jsx(Row, { textWeight: "bold", className: `absolute -bottom-0.5 -right-0.5 w-4 h-4 border-2 border-white text-white text-[9px] leading-none select-none ${ROLE_DOT_BG_CLASS[user.role] ?? "bg-[var(--appkit-color-text-muted)]"}`, align: "center", justify: "center", rounded: "full", title: user.role.charAt(0).toUpperCase() + user.role.slice(1), "aria-label": user.role, children: user.role.charAt(0).toUpperCase() }))] }), _jsxs(Div, { className: "flex-1 min-w-0", children: [_jsx(Text, { className: "truncate", color: "primary", size: "sm", weight: "medium", children: user.displayName || "User" }), _jsx(Text, { className: "truncate", color: "muted", size: "xs", children: user.email || "" })] })] }), _jsx("button", { type: "button", "aria-label": "Close menu", onClick: onClose, className: "flex-shrink-0 rounded-full p-[var(--appkit-space-2)] text-[var(--appkit-color-text-muted)] hover:bg-
|
|
79
|
+
: null, size: "md", alt: user.displayName || "User", displayName: user.displayName, email: user.email }), user.role && !isBuyerUser(user) && (_jsx(Row, { textWeight: "bold", className: `absolute -bottom-0.5 -right-0.5 w-4 h-4 border-2 border-white text-white text-[9px] leading-none select-none ${ROLE_DOT_BG_CLASS[user.role] ?? "bg-[var(--appkit-color-text-muted)]"}`, align: "center", justify: "center", rounded: "full", title: user.role.charAt(0).toUpperCase() + user.role.slice(1), "aria-label": user.role, children: user.role.charAt(0).toUpperCase() }))] }), _jsxs(Div, { className: "flex-1 min-w-0", children: [_jsx(Text, { className: "truncate", color: "primary", size: "sm", weight: "medium", children: user.displayName || "User" }), _jsx(Text, { className: "truncate", color: "muted", size: "xs", children: user.email || "" })] })] }), _jsx("button", { type: "button", "aria-label": "Close menu", onClick: onClose, className: "flex-shrink-0 rounded-full p-[var(--appkit-space-2)] text-[var(--appkit-color-text-muted)] hover:bg-surface-hover hover:text-zinc-900 text-[var(--appkit-color-text-muted)] dark:hover:text-zinc-100 transition-all hover:rotate-90", children: _jsx("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }) }) })] }));
|
|
80
80
|
}
|
|
81
81
|
/** Sidebar header when no user is logged in — title + close button. */
|
|
82
82
|
function SidebarGuestHeader({ sidebarTitle, onClose, }) {
|
|
83
|
-
return (_jsxs(Row, { align: "center", justify: "between", children: [_jsx(Div, { textWeight: "semibold", textSize: "sm", color: "primary", children: sidebarTitle }), _jsx("button", { type: "button", "aria-label": "Close menu", onClick: onClose, className: "rounded-full p-[var(--appkit-space-2)] text-[var(--appkit-color-text-muted)] hover:bg-
|
|
83
|
+
return (_jsxs(Row, { align: "center", justify: "between", children: [_jsx(Div, { textWeight: "semibold", textSize: "sm", color: "primary", children: sidebarTitle }), _jsx("button", { type: "button", "aria-label": "Close menu", onClick: onClose, className: "rounded-full p-[var(--appkit-space-2)] text-[var(--appkit-color-text-muted)] hover:bg-surface-hover hover:text-zinc-900 text-[var(--appkit-color-text-muted)] dark:hover:text-zinc-100 transition-all hover:rotate-90", children: _jsx("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }) }) })] }));
|
|
84
84
|
}
|
|
85
85
|
function SidebarContent({ sidebarItems, sidebarSections, sidebarPrimaryActions, user, activeHref, profileHref, userOrdersHref, userWishlistHref, userSettingsHref, adminHref, storeHref, sellerHref, sidebarLocaleSlot, showThemeToggleInSidebar, sidebarProfileLabels, theme, activeThemeName, toggleTheme, onLogout, onAfterLogout, }) {
|
|
86
86
|
const hasLegacyItems = sidebarItems.length > 0;
|
|
@@ -100,7 +100,7 @@ function SidebarContent({ sidebarItems, sidebarSections, sidebarPrimaryActions,
|
|
|
100
100
|
storeDashboard: sidebarProfileLabels?.storeDashboard ?? sidebarProfileLabels?.sellerDashboard ?? "Store Dashboard",
|
|
101
101
|
logout: sidebarProfileLabels?.logout ?? "Logout",
|
|
102
102
|
};
|
|
103
|
-
const navItemClass = "flex w-full items-center justify-end gap-[var(--appkit-space-2)] rounded-lg px-[var(--appkit-space-3)] py-[var(--appkit-space-2)] text-[length:var(--appkit-text-sm)]
|
|
103
|
+
const navItemClass = "flex w-full items-center justify-end gap-[var(--appkit-space-2)] rounded-lg px-[var(--appkit-space-3)] py-[var(--appkit-space-2)] text-[length:var(--appkit-text-sm)] transition-colors hover:bg-primary-surface hover:text-primary-800 text-[var(--appkit-color-text-muted)] dark:hover:text-secondary-300";
|
|
104
104
|
const navItemActiveClass = "flex w-full items-center justify-end gap-[var(--appkit-space-2)] rounded-lg px-[var(--appkit-space-3)] py-[var(--appkit-space-2)] text-[length:var(--appkit-text-sm)] font-medium bg-primary-50 text-primary-800 dark:bg-primary-900/25 dark:text-primary-300";
|
|
105
105
|
const profileLinks = [
|
|
106
106
|
{ href: profileHref },
|
|
@@ -122,9 +122,9 @@ function SidebarContent({ sidebarItems, sidebarSections, sidebarPrimaryActions,
|
|
|
122
122
|
return (_jsxs(Stack, { gap: "lg", children: [!isAuthenticated && sidebarPrimaryActions && sidebarPrimaryActions.length > 0 && (_jsx(Stack, { gap: "sm", children: sidebarPrimaryActions.map((action) => (_jsx(TextLink, { href: action.href, variant: "none", className: [
|
|
123
123
|
"block w-full rounded-lg px-[var(--appkit-space-3)] py-[var(--appkit-space-2-5)] text-center text-[length:var(--appkit-text-sm)] font-semibold transition-all duration-200 hover:scale-[1.02] shadow-sm",
|
|
124
124
|
action.variant === "outline"
|
|
125
|
-
? "border
|
|
125
|
+
? "border hover:bg-surface-hover border-[var(--appkit-color-border)] text-[var(--appkit-color-text)]"
|
|
126
126
|
: "bg-primary text-white hover:bg-primary-600 dark:bg-primary dark:hover:bg-primary-600 btn-glow",
|
|
127
|
-
].join(" "), children: action.label }, `${action.href}-${action.label}`))) })), isAuthenticated && (_jsx(CollapsibleNavGroup, { title: labels.sectionTitle, hasActive: !!activeProfileLink, children: _jsxs(Ul, { spacing: "2xs", children: [_jsx(Li, { children: _jsx(TextLink, { href: profileHref, variant: "none", className: activeProfileLink?.href === profileHref ? navItemActiveClass : navItemClass, "aria-current": activeProfileLink?.href === profileHref ? "page" : undefined, children: labels.profile }) }), userOrdersHref && (_jsx(Li, { children: _jsx(TextLink, { href: userOrdersHref, variant: "none", className: activeProfileLink?.href === userOrdersHref ? navItemActiveClass : navItemClass, "aria-current": activeProfileLink?.href === userOrdersHref ? "page" : undefined, children: labels.orders }) })), userWishlistHref && (_jsx(Li, { children: _jsx(TextLink, { href: userWishlistHref, variant: "none", className: activeProfileLink?.href === userWishlistHref ? navItemActiveClass : navItemClass, "aria-current": activeProfileLink?.href === userWishlistHref ? "page" : undefined, children: labels.wishlist }) })), userSettingsHref && (_jsx(Li, { children: _jsx(TextLink, { href: userSettingsHref, variant: "none", className: activeProfileLink?.href === userSettingsHref ? navItemActiveClass : navItemClass, "aria-current": activeProfileLink?.href === userSettingsHref ? "page" : undefined, children: labels.settings }) }))] }) })), isAuthenticated && user?.stats && (_jsxs(Div, { layout: "grid", gap: "2", className: "grid-cols-2", children: [user.stats.totalOrders != null && (_jsxs(Div, { className: CLS_STAT_BOX, children: [_jsx(Text, { className: "leading-none", color: "primary", size: "lg", weight: "bold", children: user.stats.totalOrders }), _jsx(Text, { className: CLS_STAT_LABEL, children: "Orders" })] })), user.stats.reviewsCount != null && (_jsxs(Div, { className: CLS_STAT_BOX, children: [_jsx(Text, { className: "leading-none", color: "primary", size: "lg", weight: "bold", children: user.stats.reviewsCount }), _jsx(Text, { className: CLS_STAT_LABEL, children: "Reviews" })] })), user.stats.auctionsWon != null && (_jsxs(Div, { className: CLS_STAT_BOX, children: [_jsx(Text, { className: "leading-none", color: "primary", size: "lg", weight: "bold", children: user.stats.auctionsWon }), _jsx(Text, { className: CLS_STAT_LABEL, children: "Auctions Won" })] })), user.stats.itemsSold != null && (_jsxs(Div, { className: CLS_STAT_BOX, children: [_jsx(Text, { className: "leading-none", color: "primary", size: "lg", weight: "bold", children: user.stats.itemsSold }), _jsx(Text, { className: CLS_STAT_LABEL, children: "Items Sold" })] }))] })), isAuthenticated && isAdminOrSeller && (adminHref || resolvedStoreHref) && (_jsx(CollapsibleNavGroup, { title: labels.dashboardSectionTitle, hasActive: !!activeDashboardLink, children: _jsxs(Ul, { spacing: "2xs", children: [adminHref && role === "admin" && (_jsx(Li, { children: _jsx(TextLink, { href: adminHref, variant: "none", className: activeDashboardLink?.href === adminHref ? navItemActiveClass : navItemClass, "aria-current": activeDashboardLink?.href === adminHref ? "page" : undefined, children: labels.adminDashboard }) })), resolvedStoreHref && isAdminOrSeller && (_jsx(Li, { children: _jsx(TextLink, { href: resolvedStoreHref, variant: "none", className: activeDashboardLink?.href === resolvedStoreHref ? navItemActiveClass : navItemClass, "aria-current": activeDashboardLink?.href === resolvedStoreHref ? "page" : undefined, children: labels.storeDashboard }) }))] }) })), normalizedSections.map((section, sectionIndex) => (_jsx(CollapsibleSidebarSection, { section: section, navItemClass: navItemClass, navItemActiveClass: navItemActiveClass, activeHref: activeHref }, `sidebar-section-${sectionIndex}`))), (sidebarLocaleSlot || showThemeToggleInSidebar || (isAuthenticated && onLogout)) && (_jsxs(Stack, { border: "default", className: "border-t border-[var(--appkit-color-border-subtle)]", padding: "t-md", gap: "3", children: [sidebarLocaleSlot, showThemeToggleInSidebar && (_jsxs("button", { type: "button", onClick: toggleTheme, className: "flex w-full items-center justify-end gap-[var(--appkit-space-3)] rounded-lg px-[var(--appkit-space-3)] py-[var(--appkit-space-2)] text-[length:var(--appkit-text-sm)] text-[var(--appkit-color-text-muted)] transition-colors hover:bg-primary-
|
|
127
|
+
].join(" "), children: action.label }, `${action.href}-${action.label}`))) })), isAuthenticated && (_jsx(CollapsibleNavGroup, { title: labels.sectionTitle, hasActive: !!activeProfileLink, children: _jsxs(Ul, { spacing: "2xs", children: [_jsx(Li, { children: _jsx(TextLink, { href: profileHref, variant: "none", className: activeProfileLink?.href === profileHref ? navItemActiveClass : navItemClass, "aria-current": activeProfileLink?.href === profileHref ? "page" : undefined, children: labels.profile }) }), userOrdersHref && (_jsx(Li, { children: _jsx(TextLink, { href: userOrdersHref, variant: "none", className: activeProfileLink?.href === userOrdersHref ? navItemActiveClass : navItemClass, "aria-current": activeProfileLink?.href === userOrdersHref ? "page" : undefined, children: labels.orders }) })), userWishlistHref && (_jsx(Li, { children: _jsx(TextLink, { href: userWishlistHref, variant: "none", className: activeProfileLink?.href === userWishlistHref ? navItemActiveClass : navItemClass, "aria-current": activeProfileLink?.href === userWishlistHref ? "page" : undefined, children: labels.wishlist }) })), userSettingsHref && (_jsx(Li, { children: _jsx(TextLink, { href: userSettingsHref, variant: "none", className: activeProfileLink?.href === userSettingsHref ? navItemActiveClass : navItemClass, "aria-current": activeProfileLink?.href === userSettingsHref ? "page" : undefined, children: labels.settings }) }))] }) })), isAuthenticated && user?.stats && (_jsxs(Div, { layout: "grid", gap: "2", className: "grid-cols-2", children: [user.stats.totalOrders != null && (_jsxs(Div, { className: CLS_STAT_BOX, children: [_jsx(Text, { className: "leading-none", color: "primary", size: "lg", weight: "bold", children: user.stats.totalOrders }), _jsx(Text, { className: CLS_STAT_LABEL, children: "Orders" })] })), user.stats.reviewsCount != null && (_jsxs(Div, { className: CLS_STAT_BOX, children: [_jsx(Text, { className: "leading-none", color: "primary", size: "lg", weight: "bold", children: user.stats.reviewsCount }), _jsx(Text, { className: CLS_STAT_LABEL, children: "Reviews" })] })), user.stats.auctionsWon != null && (_jsxs(Div, { className: CLS_STAT_BOX, children: [_jsx(Text, { className: "leading-none", color: "primary", size: "lg", weight: "bold", children: user.stats.auctionsWon }), _jsx(Text, { className: CLS_STAT_LABEL, children: "Auctions Won" })] })), user.stats.itemsSold != null && (_jsxs(Div, { className: CLS_STAT_BOX, children: [_jsx(Text, { className: "leading-none", color: "primary", size: "lg", weight: "bold", children: user.stats.itemsSold }), _jsx(Text, { className: CLS_STAT_LABEL, children: "Items Sold" })] }))] })), isAuthenticated && isAdminOrSeller && (adminHref || resolvedStoreHref) && (_jsx(CollapsibleNavGroup, { title: labels.dashboardSectionTitle, hasActive: !!activeDashboardLink, children: _jsxs(Ul, { spacing: "2xs", children: [adminHref && role === "admin" && (_jsx(Li, { children: _jsx(TextLink, { href: adminHref, variant: "none", className: activeDashboardLink?.href === adminHref ? navItemActiveClass : navItemClass, "aria-current": activeDashboardLink?.href === adminHref ? "page" : undefined, children: labels.adminDashboard }) })), resolvedStoreHref && isAdminOrSeller && (_jsx(Li, { children: _jsx(TextLink, { href: resolvedStoreHref, variant: "none", className: activeDashboardLink?.href === resolvedStoreHref ? navItemActiveClass : navItemClass, "aria-current": activeDashboardLink?.href === resolvedStoreHref ? "page" : undefined, children: labels.storeDashboard }) }))] }) })), normalizedSections.map((section, sectionIndex) => (_jsx(CollapsibleSidebarSection, { section: section, navItemClass: navItemClass, navItemActiveClass: navItemActiveClass, activeHref: activeHref }, `sidebar-section-${sectionIndex}`))), (sidebarLocaleSlot || showThemeToggleInSidebar || (isAuthenticated && onLogout)) && (_jsxs(Stack, { border: "default", className: "border-t border-[var(--appkit-color-border-subtle)]", padding: "t-md", gap: "3", children: [sidebarLocaleSlot, showThemeToggleInSidebar && (_jsxs("button", { type: "button", onClick: toggleTheme, className: "flex w-full items-center justify-end gap-[var(--appkit-space-3)] rounded-lg px-[var(--appkit-space-3)] py-[var(--appkit-space-2)] text-[length:var(--appkit-text-sm)] text-[var(--appkit-color-text-muted)] transition-colors hover:bg-primary-surface hover:text-primary-800", children: [_jsx(Span, { "aria-hidden": "true", children: theme === "dark" ? "☀️" : "🌙" }), activeThemeName] })), isAuthenticated && onLogout && (_jsx("button", { type: "button", onClick: () => { onLogout(); onAfterLogout(); }, className: CLS_LOGOUT_BTN, children: labels.logout }))] }))] }));
|
|
128
128
|
}
|
|
129
129
|
// ─── Main component ────────────────────────────────────────────────────────────
|
|
130
130
|
export function AppLayoutShell({ children, navItems, sidebarItems = [], sidebarSections, sidebarPrimaryActions, sidebarTitle = "Navigation", hiddenNavItems, user, brandName, brandShortName, siteLogoUrl, logoHref, promotionsHref, cartHref, wishlistHref, userId, profileHref, loginHref, registerHref, homeHref, shopHref, footer, searchSlot, searchSlotRenderer, titleBarNavSlot, titleBarNotificationSlot, titleBarDevSlot, titleBarPromoStripText, showThemeToggle = false, suppressDashboardNav = false, hideSidebarToggle = false, onLogout, adminHref, storeHref, sellerHref, userOrdersHref, userWishlistHref, userSettingsHref, sidebarLocaleSlot, showThemeToggleInSidebar = false, sidebarProfileLabels, eventBannerSlot, showBottomNav = true, onTourStart, contentClassName, lightBackground = DEFAULT_LIGHT_BG, darkBackground = DEFAULT_DARK_BG, }) {
|
|
@@ -20,7 +20,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
20
20
|
* Layout rules:
|
|
21
21
|
* - Hidden on lg+ screens by default — desktop shows inline action panels. A page can
|
|
22
22
|
* opt in by passing `desktop: "after-scroll" | "always"` to `useBottomActions`;
|
|
23
|
-
*
|
|
23
|
+
*"after-scroll" brings the bar back once the primary CTA has scrolled out of view.
|
|
24
24
|
* - The bar slides up with a 300 ms ease-out transition; `pointer-events-none`
|
|
25
25
|
* while off-screen.
|
|
26
26
|
*
|
|
@@ -57,10 +57,10 @@ function BulkPickerPanel({ bulkActions, selectedActionId, pickerOpen, onSelect,
|
|
|
57
57
|
return (_jsxs(Button, { role: "option", "aria-selected": isSelected, type: "button", variant: "ghost", disabled: action.disabled || action.loading, onClick: () => onSelect(action.id), className: [
|
|
58
58
|
"w-full flex items-center gap-[var(--appkit-space-3)] px-[var(--appkit-space-5)] py-[var(--appkit-space-3-5)] text-left text-[length:var(--appkit-text-sm)] font-medium transition-colors rounded-none",
|
|
59
59
|
i > 0 ? "border-t border-zinc-100/80 border-[var(--appkit-color-border-subtle)]" : "",
|
|
60
|
-
isSelected ? "
|
|
60
|
+
isSelected ? " bg-[var(--appkit-color-surface-input)]" : "",
|
|
61
61
|
action.variant === "danger"
|
|
62
62
|
? "text-error hover:bg-error-surface"
|
|
63
|
-
: "text-[var(--appkit-color-text)] hover:bg-
|
|
63
|
+
: "text-[var(--appkit-color-text)] hover:bg-surface-hover",
|
|
64
64
|
action.disabled || action.loading ? "opacity-50 cursor-not-allowed" : "",
|
|
65
65
|
]
|
|
66
66
|
.filter(Boolean)
|
|
@@ -204,11 +204,11 @@ export default function BottomActions() {
|
|
|
204
204
|
isVisible ? "" : "pointer-events-none",
|
|
205
205
|
]
|
|
206
206
|
.filter(Boolean)
|
|
207
|
-
.join(" "), children: [isBulkMode && (_jsx(Div, { className: "h-[3px] w-full bg-[image:var(--appkit-gradient-brand-tri)]" })), secondaryLabel && !isBulkMode && (_jsx(Div, { border: "subtle", className: "pt-[var(--appkit-space-2)] pb-[var(--appkit-space-0)] border-b /80", padding: "x-md", children: _jsx(Text, { className: "leading-5 truncate", color: "muted", size: "xs", weight: "semibold", children: secondaryLabel }) })), infoLabel && !isBulkMode && !hasInfoPanel && (_jsx(Div, { border: "subtle", className: "pt-[var(--appkit-space-2)] pb-[var(--appkit-space-0)] border-b /80", padding: "x-md", children: _jsx(Text, { className: "leading-5 truncate", color: "muted", size: "xs", weight: "semibold", children: infoLabel }) })), infoLabel && !isBulkMode && hasInfoPanel && (_jsxs(Button, { type: "button", variant: "ghost", onClick: () => setInfoOpen((o) => !o), "aria-expanded": infoOpen, "aria-controls": infoPanelId, className: "w-full flex items-center justify-between gap-[var(--appkit-space-2)] rounded-none border-b border-[var(--appkit-color-border)]/80 px-[var(--appkit-space-4)] pt-[var(--appkit-space-2)] pb-[var(--appkit-space-1)] min-h-0", children: [_jsx(Text, { className: "leading-5 truncate", color: "muted", size: "xs", weight: "semibold", children: infoLabel }), _jsxs(Span, { className: "flex-shrink-0 flex items-center gap-[var(--appkit-space-1)]", children: [_jsx(Text, { as: "span", size: "xs", color: "muted", children: infoOpen ? "Hide" : "Details" }), infoOpen ? (_jsx(ChevronDown, { className: "w-4 h-4 text-zinc-400", "aria-hidden": "true" })) : (_jsx(ChevronUp, { className: "w-4 h-4 text-zinc-400", "aria-hidden": "true" }))] })] })), _jsx(Row, { className: `${BOTTOM_NAV_HEIGHT}`, gap: "sm", padding: "x-sm", children: isBulkMode && bulk ? (_jsxs(_Fragment, { children: [_jsxs(Button, { rounded: "full", gap: "xs", type: "button", variant: "ghost", onClick: dispatchBulkClear, className: "inline- flex-shrink-0 bg-primary-50 hover:bg-primary-
|
|
207
|
+
.join(" "), children: [isBulkMode && (_jsx(Div, { className: "h-[3px] w-full bg-[image:var(--appkit-gradient-brand-tri)]" })), secondaryLabel && !isBulkMode && (_jsx(Div, { border: "subtle", className: "pt-[var(--appkit-space-2)] pb-[var(--appkit-space-0)] border-b /80", padding: "x-md", children: _jsx(Text, { className: "leading-5 truncate", color: "muted", size: "xs", weight: "semibold", children: secondaryLabel }) })), infoLabel && !isBulkMode && !hasInfoPanel && (_jsx(Div, { border: "subtle", className: "pt-[var(--appkit-space-2)] pb-[var(--appkit-space-0)] border-b /80", padding: "x-md", children: _jsx(Text, { className: "leading-5 truncate", color: "muted", size: "xs", weight: "semibold", children: infoLabel }) })), infoLabel && !isBulkMode && hasInfoPanel && (_jsxs(Button, { type: "button", variant: "ghost", onClick: () => setInfoOpen((o) => !o), "aria-expanded": infoOpen, "aria-controls": infoPanelId, className: "w-full flex items-center justify-between gap-[var(--appkit-space-2)] rounded-none border-b border-[var(--appkit-color-border)]/80 px-[var(--appkit-space-4)] pt-[var(--appkit-space-2)] pb-[var(--appkit-space-1)] min-h-0", children: [_jsx(Text, { className: "leading-5 truncate", color: "muted", size: "xs", weight: "semibold", children: infoLabel }), _jsxs(Span, { className: "flex-shrink-0 flex items-center gap-[var(--appkit-space-1)]", children: [_jsx(Text, { as: "span", size: "xs", color: "muted", children: infoOpen ? "Hide" : "Details" }), infoOpen ? (_jsx(ChevronDown, { className: "w-4 h-4 text-zinc-400", "aria-hidden": "true" })) : (_jsx(ChevronUp, { className: "w-4 h-4 text-zinc-400", "aria-hidden": "true" }))] })] })), _jsx(Row, { className: `${BOTTOM_NAV_HEIGHT}`, gap: "sm", padding: "x-sm", children: isBulkMode && bulk ? (_jsxs(_Fragment, { children: [_jsxs(Button, { rounded: "full", gap: "xs", type: "button", variant: "ghost", onClick: dispatchBulkClear, className: "inline- flex-shrink-0 bg-primary-50 hover:bg-primary-surface active:bg-primary-200 dark:bg-primary-950/30 text-primary-700 dark:text-primary-300 pl-2 pr-3 h-8 border border-primary-200/70 dark:border-primary-800/50 transition-colors min-h-0", "aria-label": "Clear selection", children: [_jsx(X, { className: "w-3.5 h-3.5 flex-shrink-0", "aria-hidden": "true" }), _jsx(Span, { size: "xs", weight: "semibold", className: "tabular-nums whitespace-nowrap leading-none", children: bulk.noun
|
|
208
208
|
? `${bulk.selectedCount} ${bulk.noun}`
|
|
209
209
|
: `${bulk.selectedCount} selected` })] }), bulkActions.length > 0 && (_jsxs(Button, { type: "button", variant: "ghost", onClick: () => setPickerOpen((o) => !o), "aria-haspopup": "listbox", "aria-expanded": pickerOpen, className: [
|
|
210
210
|
"flex-1 min-w-0 h-10 flex items-center gap-[var(--appkit-space-2)] px-[var(--appkit-space-3)] rounded-lg border text-[length:var(--appkit-text-sm)] font-medium transition-colors",
|
|
211
|
-
"
|
|
211
|
+
" hover:bg-[var(--appkit-color-surface)] active:bg-zinc-200 bg-[var(--appkit-color-surface-input)] dark:hover:bg-slate-700/60",
|
|
212
212
|
"border-[var(--appkit-color-border)]",
|
|
213
213
|
selectedAction?.variant === "danger"
|
|
214
214
|
? "text-error"
|
|
@@ -38,5 +38,5 @@ export function BottomSheet({ open, onClose, title, children, className, maxHeig
|
|
|
38
38
|
}, [open, handleEscape]);
|
|
39
39
|
if (!open)
|
|
40
40
|
return null;
|
|
41
|
-
return (_jsxs(Div, { className: "fixed inset-0 z-50 lg:hidden", role: "presentation", children: [_jsx(Div, { surface: "overlay-sm", className: "absolute inset-0 backdrop-blur-sm", "aria-hidden": "true", onClick: onClose }), _jsxs(Stack, { role: "dialog", "aria-modal": "true", "aria-label": title, className: `absolute bottom-0 inset-x-0 rounded-t-2xl border-t border-[var(--appkit-color-border-subtle)] shadow-2xl ${maxHeight} animate-slide-up${className ? ` ${className}` : ""}`, surface: "default", children: [_jsxs(Row, { border: "subtle", justify: "between", className: "border-b shrink-0", padding: "inline", children: [_jsx(Div, { surface: "skeleton", className: "absolute top-2 left-1/2 -translate-x-1/2 w-10 h-1 bg-[var(--appkit-color-border)]", rounded: "full" }), title && (_jsx(Span, { size: "sm", weight: "semibold", className: "mt-2", color: "primary", children: title })), _jsx(Button, { rounded: "lg", type: "button", onClick: onClose, "aria-label": "Close", className: "ml-auto mt-2 p-[var(--appkit-space-1-5)] text-[var(--appkit-color-text-muted)] hover:text-zinc-700
|
|
41
|
+
return (_jsxs(Div, { className: "fixed inset-0 z-50 lg:hidden", role: "presentation", children: [_jsx(Div, { surface: "overlay-sm", className: "absolute inset-0 backdrop-blur-sm", "aria-hidden": "true", onClick: onClose }), _jsxs(Stack, { role: "dialog", "aria-modal": "true", "aria-label": title, className: `absolute bottom-0 inset-x-0 rounded-t-2xl border-t border-[var(--appkit-color-border-subtle)] shadow-2xl ${maxHeight} animate-slide-up${className ? ` ${className}` : ""}`, surface: "default", children: [_jsxs(Row, { border: "subtle", justify: "between", className: "border-b shrink-0", padding: "inline", children: [_jsx(Div, { surface: "skeleton", className: "absolute top-2 left-1/2 -translate-x-1/2 w-10 h-1 bg-[var(--appkit-color-border)]", rounded: "full" }), title && (_jsx(Span, { size: "sm", weight: "semibold", className: "mt-2", color: "primary", children: title })), _jsx(Button, { rounded: "lg", type: "button", onClick: onClose, "aria-label": "Close", className: "ml-auto mt-2 p-[var(--appkit-space-1-5)] text-[var(--appkit-color-text-muted)] hover:text-zinc-700 text-[var(--appkit-color-text-muted)] hover:text-[var(--appkit-color-text-muted)] hover:bg-surface-hover transition-colors min-h-[44px] min-w-[44px] justify-center", children: _jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [_jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }), _jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })] }) })] }), _jsx(Div, { className: `flex-1 ${__O.yAuto} overscroll-contain pb-[env(safe-area-inset-bottom)]`, children: children })] })] }));
|
|
42
42
|
}
|