@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
|
@@ -12,6 +12,6 @@ export function MainNavbar({ navItems, hiddenNavItems = [], inline = false, hasD
|
|
|
12
12
|
icon: item.icon,
|
|
13
13
|
highlighted: item.highlighted,
|
|
14
14
|
}));
|
|
15
|
-
const dashboardToggle = hasDashboardNav && onToggleDashboardNav ? (_jsx("button", { type: "button", onClick: onToggleDashboardNav, "aria-label": "Toggle dashboard navigation", className: "flex items-center justify-center w-8 h-8 rounded-lg text-[var(--appkit-color-text-muted)] hover:bg-
|
|
15
|
+
const dashboardToggle = hasDashboardNav && onToggleDashboardNav ? (_jsx("button", { type: "button", onClick: onToggleDashboardNav, "aria-label": "Toggle dashboard navigation", className: "flex items-center justify-center w-8 h-8 rounded-lg text-[var(--appkit-color-text-muted)] hover:bg-surface-hover hover:text-zinc-800 dark:hover:text-zinc-100 transition-colors", children: _jsxs("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: "M3 5a2 2 0 012-2h14a2 2 0 012 2v14a2 2 0 01-2 2H5a2 2 0 01-2-2V5z" }), _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M9 3v18" })] }) })) : undefined;
|
|
16
16
|
return (_jsx(NavbarLayout, { items: items, activeHref: pathname, inline: inline, rightSlot: dashboardToggle }));
|
|
17
17
|
}
|
|
@@ -12,7 +12,7 @@ function DefaultNavItem({ item, isActive, }) {
|
|
|
12
12
|
? "border border-primary-400/40 dark:border-secondary-400/30 text-primary-700 dark:text-secondary-400 bg-primary-50/80 dark:bg-secondary-900/30 px-[var(--appkit-space-3)] transition-colors duration-150"
|
|
13
13
|
: isActive
|
|
14
14
|
? "bg-primary-50 dark:bg-[var(--appkit-color-surface-elevated)] text-primary-800 dark:text-white font-semibold px-[var(--appkit-space-3)] border-b-2 border-primary-500 dark:border-secondary-400 rounded-none pb-[6px] transition-colors duration-150"
|
|
15
|
-
: "text-[var(--appkit-color-text-muted)] hover:bg-
|
|
15
|
+
: "text-[var(--appkit-color-text-muted)] hover:bg-surface-hover hover:text-zinc-950 dark:hover:text-white transition-colors duration-150 px-[var(--appkit-space-3)]";
|
|
16
16
|
return (_jsxs(Link, { href: item.href, "aria-current": isActive ? "page" : undefined, className: `flex items-center gap-[var(--appkit-space-1-5)] py-[var(--appkit-space-2)] text-[length:var(--appkit-text-sm)] rounded-lg font-medium transition-colors duration-150 whitespace-nowrap ${activeClasses}`, children: [item.icon, _jsx(Span, { children: item.label })] }));
|
|
17
17
|
}
|
|
18
18
|
/**
|
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import Link from "next/link";
|
|
3
3
|
import { Avatar, BlockHeader, Button, Div, Row, Section, SiteLogo, SiteMark, Span } from "../../ui";
|
|
4
4
|
/** Shared icon-button class for all action buttons in the title bar. */
|
|
5
|
-
const iconBtn = "flex items-center justify-center w-9 h-9 rounded-lg text-[var(--appkit-color-text-muted)] hover:bg-primary-
|
|
5
|
+
const iconBtn = "flex items-center justify-center w-9 h-9 rounded-lg text-[var(--appkit-color-text-muted)] hover:bg-primary-surface hover:text-primary-700 dark:hover:text-secondary-400 transition-colors";
|
|
6
6
|
/** Badge counter class for wishlist/cart counts.
|
|
7
7
|
*
|
|
8
8
|
* The horizontal offset lives in `.appkit-hand-badge` (HandMode.style.css), NOT
|
|
@@ -29,7 +29,7 @@ const countBadge = "appkit-hand-badge absolute -top-0.5 flex items-center justif
|
|
|
29
29
|
*/
|
|
30
30
|
export function TitleBarLayout({ onToggleSidebar, sidebarOpen, onSearchToggle, searchOpen, brandName, brandShortName, siteLogoUrl, logoHref, promotionsHref, compareHref, wishlistHref, wishlistCount = 0, cartHref, cartCount = 0, profileHref, unreadNotificationCount = 0, notificationsHref, loginHref, registerHref, user, notificationSlot, devSlot, navSlot, promoStripText, isDark = false, onToggleTheme, onTourStart, hasDashboardNav, onToggleDashboardNav, hideSidebarToggle = false, id = "titlebar", className = "", }) {
|
|
31
31
|
// ── Element builders ────────────────────────────────────────────────────────
|
|
32
|
-
const promotionsEl = promotionsHref ? (_jsxs(Link, { href: promotionsHref, "aria-label": "Today's deals", className: "flex items-center gap-[var(--appkit-space-1)] px-[var(--appkit-space-3)] py-[var(--appkit-space-1)] rounded-full text-[length:var(--appkit-text-xs)] font-bold bg-primary-100 text-primary-700 dark:bg-secondary-900/40 dark:text-secondary-400 hover:bg-primary-
|
|
32
|
+
const promotionsEl = promotionsHref ? (_jsxs(Link, { href: promotionsHref, "aria-label": "Today's deals", className: "flex items-center gap-[var(--appkit-space-1)] px-[var(--appkit-space-3)] py-[var(--appkit-space-1)] rounded-full text-[length:var(--appkit-text-xs)] font-bold bg-primary-100 text-primary-700 dark:bg-secondary-900/40 dark:text-secondary-400 hover:bg-primary-surface transition-colors border border-primary-200/60 dark:border-secondary-700/40", children: [_jsx("svg", { className: "w-3 h-3", viewBox: "0 0 24 24", fill: "currentColor", "aria-hidden": "true", children: _jsx("path", { d: "M21.41 11.58l-9-9C12.05 2.22 11.55 2 11 2H4c-1.1 0-2 .9-2 2v7c0 .55.22 1.05.59 1.42l9 9c.36.37.86.58 1.41.58.55 0 1.05-.21 1.41-.59l7-7c.37-.36.59-.86.59-1.41 0-.55-.23-1.06-.59-1.42zM5.5 7C4.67 7 4 6.33 4 5.5S4.67 4 5.5 4 7 4.67 7 5.5 6.33 7 5.5 7z" }) }), _jsx(Span, { className: "hidden sm:inline", children: "Today's Deals" })] })) : null;
|
|
33
33
|
const themeBtn = onToggleTheme ? (_jsx(Button, { type: "button", variant: "ghost", size: "sm", "aria-label": isDark ? "Switch to light mode" : "Switch to dark mode", onClick: onToggleTheme, className: iconBtn, children: isDark ? (_jsx("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M12 3v1m0 16v1m8.66-9h-1M4.34 12h-1m15.07-6.07-.71.71M6.34 17.66l-.71.71m12.73 0-.71-.71M6.34 6.34l-.71-.71M12 5a7 7 0 1 0 0 14A7 7 0 0 0 12 5z" }) })) : (_jsx("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M21 12.79A9 9 0 1 1 11.21 3a7 7 0 1 0 9.79 9.79z" }) })) })) : null;
|
|
34
34
|
const tourBtn = onTourStart ? (_jsx(Button, { type: "button", variant: "ghost", size: "sm", "aria-label": "Start product tour", onClick: onTourStart, className: iconBtn, children: _jsx("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" }) }) })) : null;
|
|
35
35
|
const hamburgerBtn = !hideSidebarToggle ? (_jsx(Button, { type: "button", variant: "ghost", size: "sm", "aria-label": sidebarOpen ? "Close menu" : hasDashboardNav ? "Open dashboard navigation" : "Open menu", "aria-expanded": sidebarOpen, "aria-controls": "secondary-sidebar", onClick: hasDashboardNav && onToggleDashboardNav ? onToggleDashboardNav : onToggleSidebar, className: iconBtn, children: _jsx("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: sidebarOpen ? (_jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" })) : (_jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M4 6h16M4 12h16M4 18h16" })) }) })) : null;
|
|
@@ -46,7 +46,7 @@ export function TitleBarLayout({ onToggleSidebar, sidebarOpen, onSearchToggle, s
|
|
|
46
46
|
const authButtonsEl = !user && (loginHref || registerHref) ? (
|
|
47
47
|
// Mirrors with TB1 so Register (the primary CTA) keeps the outer, more
|
|
48
48
|
// reachable edge instead of ending up tucked inside after the flip.
|
|
49
|
-
_jsxs(Row, { gap: "xs", reverse: "hand", className: "hidden lg:flex", children: [loginHref && (_jsx(Link, { href: loginHref, className: "px-[var(--appkit-space-3)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-sm)] font-medium text-[var(--appkit-color-text-muted)] hover:text-primary-700 dark:hover:text-secondary-400 transition-colors rounded-lg hover:bg-primary-
|
|
49
|
+
_jsxs(Row, { gap: "xs", reverse: "hand", className: "hidden lg:flex", children: [loginHref && (_jsx(Link, { href: loginHref, className: "px-[var(--appkit-space-3)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-sm)] font-medium text-[var(--appkit-color-text-muted)] hover:text-primary-700 dark:hover:text-secondary-400 transition-colors rounded-lg hover:bg-primary-surface", children: "Sign in" })), registerHref && (_jsx(Link, { href: registerHref, className: "px-[var(--appkit-space-3)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-sm)] font-semibold rounded-lg bg-primary text-white hover:bg-primary/90 transition-colors btn-glow shadow-sm", children: "Register" }))] })) : null;
|
|
50
50
|
const hasTb2 = !!(notificationsEl || wishlistEl || cartEl || profileEl);
|
|
51
51
|
// ── Render ───────────────────────────────────────────────────────────────────
|
|
52
52
|
return (_jsxs(BlockHeader, { id: id, className: `sticky top-0 z-50 bg-[var(--appkit-color-bg)]/95 backdrop-blur-md border-b border-[var(--appkit-color-border-subtle)] shadow-sm ${className}`, children: [promoStripText && (_jsx(Section, { color: "inverse", tone: "accent-banner", className: "text-[length:var(--appkit-text-xs)] text-center font-medium", padding: "y-2xs", children: promoStripText })), _jsxs(Div, { paddingX: "x-page", className: "container mx-auto max-w-[1920px]", children: [_jsxs(Row, { justify: "between", gap: "none", reverse: "hand", className: "relative h-14", children: [_jsx(Row, { gap: "3", children: _jsxs(Link, { href: logoHref, "aria-label": brandName, className: "flex items-center gap-[var(--appkit-space-2)] transition-opacity hover:opacity-80", children: [_jsx(Div, { className: "flex md:hidden", children: _jsx(SiteMark, { title: brandShortName ?? brandName, size: "sm" }) }), _jsx(SiteLogo, { title: brandName, size: "md" })] }) }), _jsx(Row, { className: "hidden md:flex absolute inset-y-0 left-1/2 -translate-x-1/2", align: "center", children: _jsx(Link, { href: logoHref, "aria-label": brandName, className: "flex items-center transition-opacity hover:opacity-80", children: _jsx(SiteMark, { src: siteLogoUrl, title: brandName, size: "lg" }) }) }), _jsxs(Row, { gap: "xs", reverse: "hand", children: [devSlot, navSlot, compareEl, notificationSlot, notificationsEl && _jsx(Div, { className: "hidden lg:flex", children: notificationsEl }), wishlistEl && _jsx(Div, { className: "hidden lg:flex", children: wishlistEl }), cartEl && _jsx(Div, { className: "hidden lg:flex", children: cartEl }), user
|
|
@@ -16,6 +16,16 @@ export interface UseFormBottomActionsOptions {
|
|
|
16
16
|
* `Form` itself renders, so the ambient lookup would find the parent's
|
|
17
17
|
* context or none at all.
|
|
18
18
|
*/
|
|
19
|
+
/**
|
|
20
|
+
* Destructive action for this record, mirrored from `SectionForm`. Pushed
|
|
21
|
+
* to the FRONT of the bar so it is furthest from where the thumb rests on
|
|
22
|
+
* the primary action.
|
|
23
|
+
*/
|
|
24
|
+
destructiveAction?: {
|
|
25
|
+
label: string;
|
|
26
|
+
onClick: () => void;
|
|
27
|
+
disabled?: boolean;
|
|
28
|
+
};
|
|
19
29
|
ctx?: FormShellContextValue;
|
|
20
30
|
}
|
|
21
31
|
/**
|
|
@@ -53,5 +63,5 @@ export interface UseFormBottomActionsOptions {
|
|
|
53
63
|
* overlay tells us, because neither portal leaves a detectable trace in
|
|
54
64
|
* the DOM at the form's position.
|
|
55
65
|
*/
|
|
56
|
-
export declare function useFormBottomActions({ onSubmit, onCancel, submitLabel, cancelLabel, isLoading, disabled, enabled, ctx: ctxProp, }: UseFormBottomActionsOptions): void;
|
|
66
|
+
export declare function useFormBottomActions({ onSubmit, onCancel, submitLabel, cancelLabel, isLoading, disabled, enabled, destructiveAction, ctx: ctxProp, }: UseFormBottomActionsOptions): void;
|
|
57
67
|
export default useFormBottomActions;
|
|
@@ -40,7 +40,7 @@ import { useBottomActions } from "./useBottomActions";
|
|
|
40
40
|
* overlay tells us, because neither portal leaves a detectable trace in
|
|
41
41
|
* the DOM at the form's position.
|
|
42
42
|
*/
|
|
43
|
-
export function useFormBottomActions({ onSubmit, onCancel, submitLabel = "Save", cancelLabel = "Cancel", isLoading = false, disabled = false, enabled = true, ctx: ctxProp, }) {
|
|
43
|
+
export function useFormBottomActions({ onSubmit, onCancel, submitLabel = "Save", cancelLabel = "Cancel", isLoading = false, disabled = false, enabled = true, destructiveAction, ctx: ctxProp, }) {
|
|
44
44
|
const ambient = useContext(FormShellContext);
|
|
45
45
|
const ctx = ctxProp ?? ambient;
|
|
46
46
|
const insideOverlay = useIsInsideOverlay();
|
|
@@ -51,6 +51,20 @@ export function useFormBottomActions({ onSubmit, onCancel, submitLabel = "Save",
|
|
|
51
51
|
if (!active)
|
|
52
52
|
return [];
|
|
53
53
|
const list = [];
|
|
54
|
+
/*
|
|
55
|
+
* Destructive FIRST, so it is furthest from the thumb's resting position
|
|
56
|
+
* on the primary action. It never grows, so Save keeps the width.
|
|
57
|
+
*/
|
|
58
|
+
if (destructiveAction) {
|
|
59
|
+
list.push({
|
|
60
|
+
id: "form-destructive",
|
|
61
|
+
label: destructiveAction.label,
|
|
62
|
+
variant: "danger",
|
|
63
|
+
grow: false,
|
|
64
|
+
onClick: destructiveAction.onClick,
|
|
65
|
+
disabled: destructiveAction.disabled || isLoading,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
54
68
|
if (onCancel) {
|
|
55
69
|
list.push({
|
|
56
70
|
id: "form-cancel",
|
|
@@ -71,7 +85,7 @@ export function useFormBottomActions({ onSubmit, onCancel, submitLabel = "Save",
|
|
|
71
85
|
onClick: () => void onSubmit(),
|
|
72
86
|
});
|
|
73
87
|
return list;
|
|
74
|
-
}, [active, onCancel, cancelLabel, submitLabel, isLoading, disabled, onSubmit]);
|
|
88
|
+
}, [active, onCancel, cancelLabel, submitLabel, isLoading, disabled, onSubmit, destructiveAction]);
|
|
75
89
|
useBottomActions({
|
|
76
90
|
actions,
|
|
77
91
|
// Encodes the count on purpose — see (1) in the header.
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { LotteryConfig } from "../types/index";
|
|
3
|
+
/**
|
|
4
|
+
* What an admin may say about one slot. Booking state is deliberately absent
|
|
5
|
+
* — see the header. `.strict()`, so `isBooked` is a 400.
|
|
6
|
+
*/
|
|
7
|
+
export declare const lotterySlotWriteSchema: z.ZodObject<{
|
|
8
|
+
slotNumber: z.ZodNumber;
|
|
9
|
+
name: z.ZodString;
|
|
10
|
+
image: z.ZodOptional<z.ZodString>;
|
|
11
|
+
price: z.ZodNumber;
|
|
12
|
+
}, "strict", z.ZodTypeAny, {
|
|
13
|
+
name: string;
|
|
14
|
+
price: number;
|
|
15
|
+
slotNumber: number;
|
|
16
|
+
image?: string | undefined;
|
|
17
|
+
}, {
|
|
18
|
+
name: string;
|
|
19
|
+
price: number;
|
|
20
|
+
slotNumber: number;
|
|
21
|
+
image?: string | undefined;
|
|
22
|
+
}>;
|
|
23
|
+
export declare const lotteryConfigWriteSchema: z.ZodEffects<z.ZodEffects<z.ZodObject<{
|
|
24
|
+
slots: z.ZodArray<z.ZodObject<{
|
|
25
|
+
slotNumber: z.ZodNumber;
|
|
26
|
+
name: z.ZodString;
|
|
27
|
+
image: z.ZodOptional<z.ZodString>;
|
|
28
|
+
price: z.ZodNumber;
|
|
29
|
+
}, "strict", z.ZodTypeAny, {
|
|
30
|
+
name: string;
|
|
31
|
+
price: number;
|
|
32
|
+
slotNumber: number;
|
|
33
|
+
image?: string | undefined;
|
|
34
|
+
}, {
|
|
35
|
+
name: string;
|
|
36
|
+
price: number;
|
|
37
|
+
slotNumber: number;
|
|
38
|
+
image?: string | undefined;
|
|
39
|
+
}>, "many">;
|
|
40
|
+
pricingMode: z.ZodEnum<["uniform", "variable"]>;
|
|
41
|
+
uniformPrice: z.ZodOptional<z.ZodNumber>;
|
|
42
|
+
drawWindowDurationMinutes: z.ZodNumber;
|
|
43
|
+
maxPullsPerTransaction: z.ZodNumber;
|
|
44
|
+
maxPullsPerUser: z.ZodNumber;
|
|
45
|
+
}, "strict", z.ZodTypeAny, {
|
|
46
|
+
slots: {
|
|
47
|
+
name: string;
|
|
48
|
+
price: number;
|
|
49
|
+
slotNumber: number;
|
|
50
|
+
image?: string | undefined;
|
|
51
|
+
}[];
|
|
52
|
+
pricingMode: "uniform" | "variable";
|
|
53
|
+
drawWindowDurationMinutes: number;
|
|
54
|
+
maxPullsPerTransaction: number;
|
|
55
|
+
maxPullsPerUser: number;
|
|
56
|
+
uniformPrice?: number | undefined;
|
|
57
|
+
}, {
|
|
58
|
+
slots: {
|
|
59
|
+
name: string;
|
|
60
|
+
price: number;
|
|
61
|
+
slotNumber: number;
|
|
62
|
+
image?: string | undefined;
|
|
63
|
+
}[];
|
|
64
|
+
pricingMode: "uniform" | "variable";
|
|
65
|
+
drawWindowDurationMinutes: number;
|
|
66
|
+
maxPullsPerTransaction: number;
|
|
67
|
+
maxPullsPerUser: number;
|
|
68
|
+
uniformPrice?: number | undefined;
|
|
69
|
+
}>, {
|
|
70
|
+
slots: {
|
|
71
|
+
name: string;
|
|
72
|
+
price: number;
|
|
73
|
+
slotNumber: number;
|
|
74
|
+
image?: string | undefined;
|
|
75
|
+
}[];
|
|
76
|
+
pricingMode: "uniform" | "variable";
|
|
77
|
+
drawWindowDurationMinutes: number;
|
|
78
|
+
maxPullsPerTransaction: number;
|
|
79
|
+
maxPullsPerUser: number;
|
|
80
|
+
uniformPrice?: number | undefined;
|
|
81
|
+
}, {
|
|
82
|
+
slots: {
|
|
83
|
+
name: string;
|
|
84
|
+
price: number;
|
|
85
|
+
slotNumber: number;
|
|
86
|
+
image?: string | undefined;
|
|
87
|
+
}[];
|
|
88
|
+
pricingMode: "uniform" | "variable";
|
|
89
|
+
drawWindowDurationMinutes: number;
|
|
90
|
+
maxPullsPerTransaction: number;
|
|
91
|
+
maxPullsPerUser: number;
|
|
92
|
+
uniformPrice?: number | undefined;
|
|
93
|
+
}>, {
|
|
94
|
+
slots: {
|
|
95
|
+
name: string;
|
|
96
|
+
price: number;
|
|
97
|
+
slotNumber: number;
|
|
98
|
+
image?: string | undefined;
|
|
99
|
+
}[];
|
|
100
|
+
pricingMode: "uniform" | "variable";
|
|
101
|
+
drawWindowDurationMinutes: number;
|
|
102
|
+
maxPullsPerTransaction: number;
|
|
103
|
+
maxPullsPerUser: number;
|
|
104
|
+
uniformPrice?: number | undefined;
|
|
105
|
+
}, {
|
|
106
|
+
slots: {
|
|
107
|
+
name: string;
|
|
108
|
+
price: number;
|
|
109
|
+
slotNumber: number;
|
|
110
|
+
image?: string | undefined;
|
|
111
|
+
}[];
|
|
112
|
+
pricingMode: "uniform" | "variable";
|
|
113
|
+
drawWindowDurationMinutes: number;
|
|
114
|
+
maxPullsPerTransaction: number;
|
|
115
|
+
maxPullsPerUser: number;
|
|
116
|
+
uniformPrice?: number | undefined;
|
|
117
|
+
}>;
|
|
118
|
+
export type LotteryConfigWriteInput = z.infer<typeof lotteryConfigWriteSchema>;
|
|
119
|
+
/** Why a merge was refused. Distinct from a validation failure. */
|
|
120
|
+
export interface LotteryMergeRejection {
|
|
121
|
+
ok: false;
|
|
122
|
+
code: "BOOKED_SLOT_REMOVED";
|
|
123
|
+
message: string;
|
|
124
|
+
slotNumbers: number[];
|
|
125
|
+
}
|
|
126
|
+
export type LotteryMergeResult = {
|
|
127
|
+
ok: true;
|
|
128
|
+
config: LotteryConfig;
|
|
129
|
+
} | LotteryMergeRejection;
|
|
130
|
+
/**
|
|
131
|
+
* Apply an admin's edit to the stored config, carrying every booking across.
|
|
132
|
+
*
|
|
133
|
+
* `existing` is undefined when the lottery is being created, in which case
|
|
134
|
+
* there is nothing to preserve and every slot starts unbooked.
|
|
135
|
+
*
|
|
136
|
+
* `totalSlots` is DERIVED from `slots.length` and never accepted from input —
|
|
137
|
+
* a caller-supplied count that disagrees with the array is exactly the
|
|
138
|
+
* mirror-drift trap (Root Cause #42), and `totalSlots` is what the fullness
|
|
139
|
+
* check reads.
|
|
140
|
+
*/
|
|
141
|
+
export declare function mergeLotteryConfig(input: LotteryConfigWriteInput, existing?: LotteryConfig): LotteryMergeResult;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* WHY: `lotteryConfig` had NO write schema. The only authoring UI
|
|
3
|
+
* (`LotteryAdminEditView`) was orphaned, and the route it would have
|
|
4
|
+
* posted to — `PATCH /api/admin/events/[id]` — is `.passthrough()`, so
|
|
5
|
+
* the whole config wrote through completely unvalidated.
|
|
6
|
+
* WHAT: The write contract, plus `mergeLotteryConfig` — the one function that
|
|
7
|
+
* is allowed to turn an admin's edit into a stored config.
|
|
8
|
+
*
|
|
9
|
+
* ## 🛑 The author-side slot shape CANNOT carry booking state
|
|
10
|
+
*
|
|
11
|
+
* `LotteryAdminEditView.handleSubmit` sent `isBooked: false` and `weight: 0`
|
|
12
|
+
* for every slot, and dropped `bookedByUserId` / `bookedByDisplayName` /
|
|
13
|
+
* `bookedByUserLotteryNumber` entirely. Against a `.passthrough()` PATCH that
|
|
14
|
+
* is not a validation gap — it is a **destructive write**: the first save of
|
|
15
|
+
* a live lottery marks every purchased slot as available again, and the
|
|
16
|
+
* buyers who paid for them are simply gone from the record.
|
|
17
|
+
*
|
|
18
|
+
* So the input type deliberately has NO booking fields at all. An admin
|
|
19
|
+
* editing a lottery is describing PRIZES, not attendance, and the schema
|
|
20
|
+
* refuses to express the latter — a `.strict()` object, so sending
|
|
21
|
+
* `isBooked` is a 400 rather than something a merge has to defend against.
|
|
22
|
+
*
|
|
23
|
+
* `weight` is absent for the same reason: it is derived from price
|
|
24
|
+
* server-side and is never client input.
|
|
25
|
+
*
|
|
26
|
+
* ## Merging is BY `slotNumber`, not by index
|
|
27
|
+
*
|
|
28
|
+
* Array position is not identity. An admin who deletes slot 3 shifts every
|
|
29
|
+
* later slot's index by one, and an index-wise merge would hand slot 7's
|
|
30
|
+
* buyer the prize that used to be slot 8. `slotNumber` is the stable key
|
|
31
|
+
* users actually see and pull against.
|
|
32
|
+
*
|
|
33
|
+
* A slot that is booked cannot be REMOVED either. Dropping it would erase a
|
|
34
|
+
* paid entry with no trace; the merge rejects that rather than silently
|
|
35
|
+
* carrying the slot along, because an admin who thinks they deleted a prize
|
|
36
|
+
* and did not is a worse outcome than an error.
|
|
37
|
+
*
|
|
38
|
+
* EXPORTS: lotteryConfigWriteSchema, lotterySlotWriteSchema,
|
|
39
|
+
* mergeLotteryConfig, type LotteryConfigWriteInput
|
|
40
|
+
*
|
|
41
|
+
* @tag domain:lottery
|
|
42
|
+
* @tag layer:schema
|
|
43
|
+
* @tag pattern:none
|
|
44
|
+
* @tag access:isomorphic
|
|
45
|
+
* @tag consumers:/api/admin/events/[id]/lottery-config,LotteryAdminEditView
|
|
46
|
+
* @tag sideEffects:none
|
|
47
|
+
*/
|
|
48
|
+
import { z } from "zod";
|
|
49
|
+
/** Mirrors `LotteryPricingMode`. */
|
|
50
|
+
const pricingModeSchema = z.enum(["uniform", "variable"]);
|
|
51
|
+
/**
|
|
52
|
+
* What an admin may say about one slot. Booking state is deliberately absent
|
|
53
|
+
* — see the header. `.strict()`, so `isBooked` is a 400.
|
|
54
|
+
*/
|
|
55
|
+
export const lotterySlotWriteSchema = z
|
|
56
|
+
.object({
|
|
57
|
+
slotNumber: z.coerce.number().int().min(1).max(200),
|
|
58
|
+
name: z.string().min(1, "Every slot needs a prize name.").max(200),
|
|
59
|
+
image: z.string().max(2000).optional(),
|
|
60
|
+
price: z.coerce.number().min(0, "A price cannot be negative."),
|
|
61
|
+
})
|
|
62
|
+
.strict();
|
|
63
|
+
export const lotteryConfigWriteSchema = z
|
|
64
|
+
.object({
|
|
65
|
+
slots: z
|
|
66
|
+
.array(lotterySlotWriteSchema)
|
|
67
|
+
.min(1, "A lottery needs at least one slot.")
|
|
68
|
+
.max(200, "A lottery cannot exceed 200 slots."),
|
|
69
|
+
pricingMode: pricingModeSchema,
|
|
70
|
+
uniformPrice: z.coerce.number().min(0).optional(),
|
|
71
|
+
drawWindowDurationMinutes: z.coerce.number().int().min(1).max(1440),
|
|
72
|
+
maxPullsPerTransaction: z.coerce.number().int().min(1).max(200),
|
|
73
|
+
maxPullsPerUser: z.coerce.number().int().min(1).max(200),
|
|
74
|
+
})
|
|
75
|
+
.strict()
|
|
76
|
+
.refine((c) => new Set(c.slots.map((s) => s.slotNumber)).size === c.slots.length, { message: "Two slots share a slot number.", path: ["slots"] })
|
|
77
|
+
.refine((c) => c.pricingMode !== "uniform" || typeof c.uniformPrice === "number", {
|
|
78
|
+
message: "Uniform pricing needs a price.",
|
|
79
|
+
path: ["uniformPrice"],
|
|
80
|
+
});
|
|
81
|
+
/**
|
|
82
|
+
* Apply an admin's edit to the stored config, carrying every booking across.
|
|
83
|
+
*
|
|
84
|
+
* `existing` is undefined when the lottery is being created, in which case
|
|
85
|
+
* there is nothing to preserve and every slot starts unbooked.
|
|
86
|
+
*
|
|
87
|
+
* `totalSlots` is DERIVED from `slots.length` and never accepted from input —
|
|
88
|
+
* a caller-supplied count that disagrees with the array is exactly the
|
|
89
|
+
* mirror-drift trap (Root Cause #42), and `totalSlots` is what the fullness
|
|
90
|
+
* check reads.
|
|
91
|
+
*/
|
|
92
|
+
export function mergeLotteryConfig(input, existing) {
|
|
93
|
+
const priorBySlot = new Map();
|
|
94
|
+
for (const slot of existing?.slots ?? [])
|
|
95
|
+
priorBySlot.set(slot.slotNumber, slot);
|
|
96
|
+
const incoming = new Set(input.slots.map((s) => s.slotNumber));
|
|
97
|
+
const droppedBooked = [...priorBySlot.values()]
|
|
98
|
+
.filter((s) => s.isBooked && !incoming.has(s.slotNumber))
|
|
99
|
+
.map((s) => s.slotNumber);
|
|
100
|
+
if (droppedBooked.length > 0) {
|
|
101
|
+
return {
|
|
102
|
+
ok: false,
|
|
103
|
+
code: "BOOKED_SLOT_REMOVED",
|
|
104
|
+
message: `Slot ${droppedBooked.join(", ")} has already been pulled and cannot be ` +
|
|
105
|
+
`removed. Reopen it first if the pull needs to be undone.`,
|
|
106
|
+
slotNumbers: droppedBooked,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
const slots = input.slots.map((s) => {
|
|
110
|
+
const prior = priorBySlot.get(s.slotNumber);
|
|
111
|
+
return {
|
|
112
|
+
slotNumber: s.slotNumber,
|
|
113
|
+
name: s.name,
|
|
114
|
+
image: s.image?.trim() || undefined,
|
|
115
|
+
price: input.pricingMode === "uniform" ? (input.uniformPrice ?? 0) : s.price,
|
|
116
|
+
// Server-derived, never input. Recomputed by the pricing pass; carried
|
|
117
|
+
// from the prior slot so an edit does not zero a live weighting.
|
|
118
|
+
weight: prior?.weight ?? 0,
|
|
119
|
+
// 🛑 THE WHOLE POINT: booking state comes from the STORED slot, never
|
|
120
|
+
// from the request. A slot the admin renames keeps its buyer.
|
|
121
|
+
isBooked: prior?.isBooked ?? false,
|
|
122
|
+
bookedByUserId: prior?.bookedByUserId,
|
|
123
|
+
bookedByDisplayName: prior?.bookedByDisplayName,
|
|
124
|
+
bookedByUserLotteryNumber: prior?.bookedByUserLotteryNumber,
|
|
125
|
+
};
|
|
126
|
+
});
|
|
127
|
+
return {
|
|
128
|
+
ok: true,
|
|
129
|
+
config: {
|
|
130
|
+
slots,
|
|
131
|
+
totalSlots: slots.length,
|
|
132
|
+
pricingMode: input.pricingMode,
|
|
133
|
+
uniformPrice: input.pricingMode === "uniform" ? input.uniformPrice : undefined,
|
|
134
|
+
drawWindowDurationMinutes: input.drawWindowDurationMinutes,
|
|
135
|
+
maxPullsPerTransaction: input.maxPullsPerTransaction,
|
|
136
|
+
maxPullsPerUser: input.maxPullsPerUser,
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
|
@@ -57,8 +57,10 @@ export function MediaPickerModal({ isOpen, onClose, onSelect, accept = "image/*"
|
|
|
57
57
|
setExistingFiles(data.data.files);
|
|
58
58
|
})
|
|
59
59
|
.catch((err) => {
|
|
60
|
+
// normalizeError covers Zod/Api/Firebase shapes an `instanceof Error`
|
|
61
|
+
// check misses, so those stop collapsing to the generic fallback.
|
|
60
62
|
if (!cancelled)
|
|
61
|
-
setExistingError(err
|
|
63
|
+
setExistingError(normalizeError(err).message || "Failed to load");
|
|
62
64
|
})
|
|
63
65
|
.finally(() => {
|
|
64
66
|
if (!cancelled)
|
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useRef, useEffect } from "react";
|
|
4
4
|
import { Div, Row, Span } from "../../ui";
|
|
5
|
-
import { resolveMediaUrl, getYouTubeVideoId } from "../../utils/media-url";
|
|
5
|
+
import { resolveMediaUrl, resolveVideoUrl, getYouTubeVideoId } from "../../utils/media-url";
|
|
6
6
|
import { useSiteSettings } from "../../core/hooks/useSiteSettings";
|
|
7
7
|
export function MediaVideo({ src, thumbnailUrl, alt = "Video", controls = true, autoPlayMuted = false, loop = false, trimStart, trimEnd, objectFit = "cover", watermark, }) {
|
|
8
8
|
const videoRef = useRef(null);
|
|
9
9
|
const fitClass = objectFit === "contain" ? "object-contain" : "object-cover";
|
|
10
|
-
|
|
10
|
+
// Video src must NOT fall through to /api/media/ext (image-only, 400s on
|
|
11
|
+
// video/mp4). The poster is an image and stays on the normal image path.
|
|
12
|
+
const resolvedSrc = resolveVideoUrl(src);
|
|
11
13
|
const resolvedPoster = resolveMediaUrl(thumbnailUrl);
|
|
12
14
|
const youtubeId = getYouTubeVideoId(src);
|
|
13
15
|
// Apply trimStart on load
|
|
@@ -5,7 +5,7 @@ export function MediaSlider({ value, min = 0, max = 100, step = 1, onChange, onC
|
|
|
5
5
|
// drag/cancel state; nothing here persists across a cancelled touch.
|
|
6
6
|
onTouchEnd: (e) => onChangeEnd?.(parseFloat(e.target.value)), className: [
|
|
7
7
|
"w-full h-2 appearance-none cursor-pointer rounded-full",
|
|
8
|
-
"
|
|
8
|
+
" bg-[var(--appkit-color-border)]",
|
|
9
9
|
"accent-primary-600 dark:accent-secondary-400",
|
|
10
10
|
className,
|
|
11
11
|
]
|
|
@@ -23,14 +23,14 @@ export function OrderCard({ order, onClick, labels = {}, renderActions }) {
|
|
|
23
23
|
day: "numeric",
|
|
24
24
|
})
|
|
25
25
|
: "";
|
|
26
|
-
const statusColor = STATUS_COLORS[order.orderStatus] ?? "bg-[var(--appkit-color-surface)]
|
|
26
|
+
const statusColor = STATUS_COLORS[order.orderStatus] ?? "bg-[var(--appkit-color-surface)] bg-[var(--appkit-color-surface-elevated)] text-[var(--appkit-color-text-muted)]";
|
|
27
27
|
// SB8-F — count unrevealed prize-draw entries to surface a "reveals pending" badge.
|
|
28
28
|
const unrevealedPrizeDraws = order.items.filter((it) => it.listingType === "prize-draw" &&
|
|
29
29
|
it.prizeRevealStatus !== undefined &&
|
|
30
30
|
it.prizeRevealStatus !== "revealed");
|
|
31
31
|
const revealsRemaining = unrevealedPrizeDraws.length;
|
|
32
32
|
return (_jsxs(Div, { padding: "5", role: onClick ? "button" : undefined, tabIndex: onClick ? 0 : undefined, onKeyDown: onClick
|
|
33
|
-
? (e) => (e.key === "Enter" || e.key === "
|
|
33
|
+
? (e) => (e.key === "Enter" || e.key === "") && onClick(order)
|
|
34
34
|
: undefined, onClick: onClick ? () => onClick(order) : undefined, className: `border border-neutral-200 ${onClick ? "cursor-pointer transition hover:shadow-md" : ""}`, rounded: "xl", surface: "default", children: [_jsxs(Row, { wrap: true, align: "start", justify: "between", gap: "3", children: [_jsxs(Div, { children: [_jsxs(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "xs", children: ["Order #", order.id.slice(-8).toUpperCase()] }), date && (_jsx(Text, { className: "mt-0.5", color: "faint", size: "xs", children: date }))] }), _jsx(Span, { size: "xs", weight: "semibold", className: `capitalize ${statusColor}`, rounded: "full", padding: "pill-md", children: labels[order.orderStatus] ?? order.orderStatus.replace(/_/g, " ") })] }), revealsRemaining > 0 && (_jsx(Row, { gap: "sm", className: "mt-2", children: _jsxs(Span, { size: "xs", weight: "semibold", className: CLS_PRIZE_BADGE, children: [revealsRemaining, " ", revealsRemaining === 1 ? "reveal" : "reveals", " pending"] }) })), _jsxs(Row, { wrap: true, gap: "3", className: "mt-4", children: [order.items.slice(0, 3).map((item, i) => (_jsxs(Row, { gap: "sm", children: [item.image && (_jsx(Div, { className: "relative h-10 w-10 flex-shrink-0", rounded: "lg", overflow: "hidden", children: _jsx(MediaImage, { src: item.image, alt: item.title, size: "thumbnail" }) })), _jsxs(Div, { children: [_jsx(Text, { truncate: true, size: "sm", weight: "medium", color: "primary", children: item.title }), _jsxs(Text, { size: "xs", color: "faint", children: ["\u00D7", item.quantity] })] })] }, i))), order.items.length > 3 && (_jsxs(Span, { size: "xs", className: "self-center", color: "faint", children: ["+", order.items.length - 3, " more"] }))] }), _jsxs(Row, { justify: "between", className: "mt-4 border-t border-neutral-100", padding: "t-sm", children: [_jsxs(Span, { size: "sm", className: "text-[var(--appkit-color-text-muted)]", children: [order.currency ?? "", " Total"] }), _jsx(Span, { weight: "semibold", className: "text-[var(--appkit-color-text)]", children: formatCurrency(order.total, order.currency) })] }), renderActions && (_jsx(Row, { justify: "end", gap: "sm", className: "mt-3", onClick: (e) => e.stopPropagation(), children: renderActions(order) }))] }));
|
|
35
35
|
}
|
|
36
36
|
export function OrdersList({ orders, isLoading, onOrderClick, totalPages = 1, currentPage = 1, onPageChange, emptyLabel = "No orders found", renderActions, }) {
|
|
@@ -19,7 +19,17 @@ export declare class PayoutRepository extends BaseRepository<PayoutDocument> {
|
|
|
19
19
|
constructor();
|
|
20
20
|
/** Decrypt PII fields on a payout document after Firestore read */
|
|
21
21
|
private decryptPayout;
|
|
22
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* Encrypt PII, then attach blind indices derived from the plaintext.
|
|
24
|
+
*
|
|
25
|
+
* Previously this reassigned `encrypted = addPiiIndices(data, …)` — which
|
|
26
|
+
* re-reads the ORIGINAL plaintext and returns `{...data, ...indices}` — and
|
|
27
|
+
* then spread that over the ciphertext, so plaintext won and `sellerEmail`
|
|
28
|
+
* and `upiId` were written to Firestore in the clear beside a valid index.
|
|
29
|
+
* `piiIndicesFor` returns the index fields alone, so the ciphertext cannot be
|
|
30
|
+
* overwritten. The identical bug was fixed in UserRepository and never
|
|
31
|
+
* propagated here.
|
|
32
|
+
*/
|
|
23
33
|
private encryptPayoutData;
|
|
24
34
|
/** Override mapDoc to auto-decrypt PII on every Firestore read */
|
|
25
35
|
protected mapDoc<D = PayoutDocument>(snap: import("../../../providers/db-firebase").DocumentSnapshot): D;
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { BaseRepository, prepareForFirestore, parseSieveDateValue, } from "../../../providers/db-firebase";
|
|
8
8
|
import { PAYOUT_COLLECTION, PAYOUT_FIELDS, PAYOUT_TRACKED_FIELDS, createPayoutId } from "../schemas";
|
|
9
9
|
import { withHistory } from "../../../_internal/shared/history/index";
|
|
10
|
-
import { encryptPiiFields, decryptPiiFields,
|
|
10
|
+
import { encryptPiiFields, decryptPiiFields, piiIndicesFor, PAYOUT_PII_FIELDS, PAYOUT_PII_INDEX_MAP, encryptPayoutBankAccount, decryptPayoutBankAccount, } from "../../../security";
|
|
11
11
|
export class PayoutRepository extends BaseRepository {
|
|
12
12
|
constructor() {
|
|
13
13
|
super(PAYOUT_COLLECTION);
|
|
@@ -20,13 +20,21 @@ export class PayoutRepository extends BaseRepository {
|
|
|
20
20
|
}
|
|
21
21
|
return decrypted;
|
|
22
22
|
}
|
|
23
|
-
/**
|
|
23
|
+
/**
|
|
24
|
+
* Encrypt PII, then attach blind indices derived from the plaintext.
|
|
25
|
+
*
|
|
26
|
+
* Previously this reassigned `encrypted = addPiiIndices(data, …)` — which
|
|
27
|
+
* re-reads the ORIGINAL plaintext and returns `{...data, ...indices}` — and
|
|
28
|
+
* then spread that over the ciphertext, so plaintext won and `sellerEmail`
|
|
29
|
+
* and `upiId` were written to Firestore in the clear beside a valid index.
|
|
30
|
+
* `piiIndicesFor` returns the index fields alone, so the ciphertext cannot be
|
|
31
|
+
* overwritten. The identical bug was fixed in UserRepository and never
|
|
32
|
+
* propagated here.
|
|
33
|
+
*/
|
|
24
34
|
encryptPayoutData(data) {
|
|
25
|
-
|
|
26
|
-
encrypted = addPiiIndices(data, PAYOUT_PII_INDEX_MAP);
|
|
27
|
-
encrypted = {
|
|
35
|
+
const encrypted = {
|
|
28
36
|
...encryptPiiFields(data, [...PAYOUT_PII_FIELDS]),
|
|
29
|
-
...
|
|
37
|
+
...piiIndicesFor(data, PAYOUT_PII_INDEX_MAP),
|
|
30
38
|
};
|
|
31
39
|
const access = encrypted;
|
|
32
40
|
if (access.bankAccount) {
|
|
@@ -17,6 +17,7 @@ const CLS_LIMITED_BADGE = "rounded-full bg-info-solid px-[var(--appkit-space-2)]
|
|
|
17
17
|
import { BaseListingCard, Button, RichText, Row, Span, Stack, Text, TextLink } from "../../../ui";
|
|
18
18
|
import { normalizeRichTextHtml } from "../../../utils/string.formatter";
|
|
19
19
|
import { PreorderBadge } from "./PreorderCard";
|
|
20
|
+
import { WishlistHeartButton } from "../../wishlist/components/WishlistHeartButton";
|
|
20
21
|
const DEFAULT_LABELS = {
|
|
21
22
|
preOrderBadge: "Pre-order",
|
|
22
23
|
featuredBadge: "Featured",
|
|
@@ -62,7 +63,7 @@ export function MarketplacePreorderCard({ product, className = "", variant = "gr
|
|
|
62
63
|
onSelect(product.id, !isSelected);
|
|
63
64
|
}, className: selectable || isSelected ? "opacity-100" : "opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-opacity" }))] }), _jsx(BaseListingCard.Info, { variant: variant, children: variant === "list" ? (
|
|
64
65
|
/* ── Compact list layout ── */
|
|
65
|
-
_jsxs(_Fragment, { children: [_jsxs(Row, { className: "min-w-0", align: "start", justify: "between", gap: "sm", children: [_jsx(TextLink, { href: detailHref, className: "min-w-0 flex-1", children: _jsx(Text, { size: "sm", weight: "medium", color: "primary", truncate: 2, children: product.title }) }), wishlistActions && (_jsx(
|
|
66
|
+
_jsxs(_Fragment, { children: [_jsxs(Row, { className: "min-w-0", align: "start", justify: "between", gap: "sm", children: [_jsx(TextLink, { href: detailHref, className: "min-w-0 flex-1", children: _jsx(Text, { size: "sm", weight: "medium", color: "primary", truncate: 2, children: product.title }) }), wishlistActions && (_jsx(WishlistHeartButton, { size: "inline", inWishlist: inWishlist, addLabel: mergedLabels.addToWishlist, removeLabel: mergedLabels.removeFromWishlist, onToggle: handleWishlist }))] }), _jsxs(Row, { align: "center", gap: "sm", wrap: true, children: [_jsx(Text, { className: "text-primary", size: "sm", weight: "bold", children: formatCurrency(product.price, getDefaultCurrency()) }), shipDate && _jsx(PreorderBadge, { shipDate: shipDate })] }), _jsx(Button, { type: "button", variant: "primary", size: "sm", textSize: "xs", className: "self-start mt-0.5", onClick: handleNavigate, children: mergedLabels.reserveNow })] })) : (
|
|
66
67
|
/* ── Full grid layout ── */
|
|
67
|
-
_jsxs(_Fragment, { children: [_jsx(TextLink, { href: detailHref, children: _jsx(Text, { size: "sm", weight: "medium", color: "primary", truncate: 2, children: product.title }) }), product.description ? (_jsx(RichText, { html: normalizeRichTextHtml(product.description), proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "line-clamp-2 text-[length:var(--appkit-text-xs)] text-[var(--appkit-color-text-muted)]" })) : null, _jsxs(Row, { justify: "between", className: "mt-1", gap: "sm", children: [_jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: formatCurrency(product.price, getDefaultCurrency()) }), shipDate && _jsx(PreorderBadge, { shipDate: shipDate })] }), _jsxs(Row, { justify: "between", className: "mt-auto pt-[var(--appkit-space-2)]", gap: "sm", children: [_jsx(Button, { type: "button", variant: "primary", size: "sm", textSize: "xs", className: "flex-1", onClick: handleNavigate, children: mergedLabels.reserveNow }), onAddToCart ? (_jsx(Button, { type: "button", variant: "outline", size: "sm", textSize: "xs", className: "flex-1", onClick: (e) => { e.stopPropagation(); onAddToCart(product); }, children: mergedLabels.addToCart })) : null, wishlistActions ? (_jsx(
|
|
68
|
+
_jsxs(_Fragment, { children: [_jsx(TextLink, { href: detailHref, children: _jsx(Text, { size: "sm", weight: "medium", color: "primary", truncate: 2, children: product.title }) }), product.description ? (_jsx(RichText, { html: normalizeRichTextHtml(product.description), proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "line-clamp-2 text-[length:var(--appkit-text-xs)] text-[var(--appkit-color-text-muted)]" })) : null, _jsxs(Row, { justify: "between", className: "mt-1", gap: "sm", children: [_jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: formatCurrency(product.price, getDefaultCurrency()) }), shipDate && _jsx(PreorderBadge, { shipDate: shipDate })] }), _jsxs(Row, { justify: "between", className: "mt-auto pt-[var(--appkit-space-2)]", gap: "sm", children: [_jsx(Button, { type: "button", variant: "primary", size: "sm", textSize: "xs", className: "flex-1", onClick: handleNavigate, children: mergedLabels.reserveNow }), onAddToCart ? (_jsx(Button, { type: "button", variant: "outline", size: "sm", textSize: "xs", className: "flex-1", onClick: (e) => { e.stopPropagation(); onAddToCart(product); }, children: mergedLabels.addToCart })) : null, wishlistActions ? (_jsx(WishlistHeartButton, { size: "inline", inWishlist: inWishlist, addLabel: mergedLabels.addToWishlist, removeLabel: mergedLabels.removeFromWishlist, onToggle: handleWishlist })) : null] })] })) })] }));
|
|
68
69
|
}
|
|
@@ -5,7 +5,7 @@ const STATUS_MAP = {
|
|
|
5
5
|
pending: { label: "Pending", color: "bg-warning-surface text-warning" },
|
|
6
6
|
confirmed: { label: "Confirmed", color: "bg-info-surface text-info" },
|
|
7
7
|
ready: { label: "Ready", color: "bg-success-surface text-success" },
|
|
8
|
-
fulfilled: { label: "Fulfilled", color: "bg-[var(--appkit-color-surface)]
|
|
8
|
+
fulfilled: { label: "Fulfilled", color: "bg-[var(--appkit-color-surface)] bg-[var(--appkit-color-surface-elevated)] text-[var(--appkit-color-text-muted)]" },
|
|
9
9
|
cancelled: { label: "Cancelled", color: "bg-error-surface text-error" },
|
|
10
10
|
};
|
|
11
11
|
export function PreOrderBadge({ status, className = "" }) {
|
|
@@ -61,7 +61,7 @@ function PreOrderInfoSection({ title, productionStatus, maxPerUser, deliveryDate
|
|
|
61
61
|
emiAvailable: "EMI Available",
|
|
62
62
|
wishlistCount: (n) => `${n} wishlisted`,
|
|
63
63
|
categoryProductCount: (n, cat) => `${n} in ${cat}`,
|
|
64
|
-
} }), (categoryName || category || brand) && (_jsxs(Row, { gap: "sm", wrap: true, children: [category && (_jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category)), className: "inline-flex items-center rounded-full border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] px-[var(--appkit-space-2-5)] py-[var(--appkit-space-1)] text-[length:var(--appkit-text-xs)] font-medium text-[var(--appkit-color-text-muted)] transition-colors hover:border-primary-300 hover:bg-primary-
|
|
64
|
+
} }), (categoryName || category || brand) && (_jsxs(Row, { gap: "sm", wrap: true, children: [category && (_jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category)), className: "inline-flex items-center rounded-full border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] px-[var(--appkit-space-2-5)] py-[var(--appkit-space-1)] text-[length:var(--appkit-text-xs)] font-medium text-[var(--appkit-color-text-muted)] transition-colors hover:border-primary-300 hover:bg-primary-surface hover:text-primary-700 dark:hover:border-primary-700/60 dark:hover:text-primary-400", children: categoryName || category })), !category && categoryName && (_jsx(Span, { layout: "inline-flex", size: "xs", weight: "medium", border: "subtle", rounded: "full", padding: "pill-sm-tall", surface: "muted", color: "muted", children: categoryName })), brand && brandSlug && (_jsx(Link, { href: String(ROUTES.PUBLIC.BRAND_DETAIL(brandSlug)), className: "inline-flex items-center rounded-full border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] px-[var(--appkit-space-2-5)] py-[var(--appkit-space-1)] text-[length:var(--appkit-text-xs)] font-medium text-[var(--appkit-color-text-muted)] transition-colors hover:border-primary-300 hover:bg-primary-surface hover:text-primary-700 dark:hover:border-primary-700/60 dark:hover:text-primary-400", children: brand })), brand && !brandSlug && (_jsx(Span, { layout: "inline-flex", size: "xs", weight: "medium", border: "subtle", rounded: "full", padding: "pill-sm-tall", surface: "muted", color: "muted", children: brand }))] })), productFeatures && features.length > 0 && (_jsx(FeatureBadgeList, { productFeatureIds: features, features: productFeatures })), !productFeatures && features.length > 0 && (_jsxs(Div, { border: "subtle", surface: "muted", padding: "inline", rounded: "xl", children: [_jsx(Text, { className: "mb-2 tracking-wide", color: "muted", size: "xs", weight: "semibold", transform: "uppercase", children: "About this product" }), _jsx(Ul, { spacing: "comfortable", size: "sm", color: "primary", children: features.map((f, i) => (_jsxs(Li, { layout: "flex-start", gap: "2", 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-[length:var(--appkit-text-sm)] leading-relaxed text-[var(--appkit-color-text-muted)] line-clamp-4" })), safeSeller && (_jsx(Div, { className: `${__P.p3}`, border: "subtle", rounded: "xl", surface: "muted", children: _jsxs(Row, { justify: "between", align: "center", children: [_jsxs(Div, { children: [_jsx(Text, { className: "text-[10px] tracking-wide mb-0.5", color: "faint", transform: "uppercase", children: "Sold by" }), _jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: safeSeller })] }), storeHref && (_jsx(Link, { href: storeHref, className: "shrink-0 rounded-lg bg-primary/10 dark:bg-primary/20 px-[var(--appkit-space-3)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-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" }))] }) }))] }));
|
|
65
65
|
}
|
|
66
66
|
function PreOrderBuyBarPanel({ reserveTarget, reservedCount, progressPct, productId, price, currency, depositAmount, depositPercent, isCancellable, tags, onReserveNow, }) {
|
|
67
67
|
return (_jsxs(Stack, { id: "pre-order-buy-bar", className: `${__P.p5}`, border: "subtle", gap: "md", rounded: "xl", surface: "muted", children: [reserveTarget > 0 && (_jsxs(Stack, { gap: "sm", children: [_jsxs(Row, { justify: "between", align: "center", children: [_jsxs(Text, { size: "xs", color: "muted", children: [reservedCount, " of ", reserveTarget, " reserved"] }), _jsxs(Span, { size: "xs", weight: "semibold", className: "text-primary-600 dark:text-primary-400", children: [progressPct, "%"] })] }), _jsx(Div, { className: `h-2 w-full ${__O.hidden}`, rounded: "full", surface: "subtle", children: _jsx(ProgressBarFill, { pct: progressPct, className: "h-full bg-primary transition-all rounded-full" }) })] })), onReserveNow ? (_jsx(PreOrderActionsClient, { productId: productId, price: price, currency: currency, depositAmount: depositAmount, depositPercent: depositPercent, isCancellable: isCancellable, tags: tags, onReserveNow: onReserveNow })) : (_jsxs(_Fragment, { children: [price !== null && (_jsxs(Div, { children: [_jsx(Text, { size: "2xl", weight: "bold", color: "primary", children: formatCurrency(price, currency) }), depositAmount !== null && (_jsxs(Text, { className: "mt-0.5", color: "muted", size: "xs", children: ["Reserve with ", formatCurrency(depositAmount, currency), depositPercent !== null ? ` (${depositPercent}% deposit)` : ""] }))] })), _jsxs(Stack, { gap: "sm", children: [_jsx(Button, { variant: "primary", size: "md", className: "w-full", children: "Reserve Now" }), isCancellable && (_jsx(Text, { size: "xs", align: "start", color: "muted", children: "\u2713 Free cancellation before production" }))] }), tags.length > 0 && (_jsx(Div, { border: "default", className: "border-t", padding: "t-md", children: _jsx(Row, { wrap: true, gap: "xs", children: tags.map((tag) => (_jsx(Span, { size: "xs", rounded: "full", padding: "pill-sm-tall", surface: "subtle", color: "muted", children: tag }, tag))) }) })), _jsx(Div, { border: "default", className: "border-t", padding: "t-md", children: _jsx(Row, { wrap: true, gap: "sm", justify: "center", className: "text-center", children: [
|
|
@@ -7,7 +7,7 @@ const __O = {
|
|
|
7
7
|
};
|
|
8
8
|
const STATUS_STYLES = {
|
|
9
9
|
published: "bg-success-surface text-success dark:bg-success-surface dark:text-success",
|
|
10
|
-
draft: "bg-[var(--appkit-color-surface)]
|
|
10
|
+
draft: "bg-[var(--appkit-color-surface)] bg-[var(--appkit-color-border)] text-[var(--appkit-color-text-muted)]",
|
|
11
11
|
out_of_stock: "bg-warning-surface text-warning dark:bg-warning-surface dark:text-warning",
|
|
12
12
|
discontinued: "bg-error-surface text-error dark:bg-error-surface dark:text-error",
|
|
13
13
|
sold: "bg-primary/10 dark:bg-primary/20 text-primary",
|
|
@@ -54,7 +54,7 @@ export function getProductTableColumns({ labels, onEdit, onDelete, currencyCode,
|
|
|
54
54
|
return (_jsx(Span, { className: [
|
|
55
55
|
"rounded px-[var(--appkit-space-2)] py-[var(--appkit-space-1)] text-[length:var(--appkit-text-xs)] font-medium",
|
|
56
56
|
STATUS_STYLES[status] ??
|
|
57
|
-
"bg-[var(--appkit-color-surface)]
|
|
57
|
+
"bg-[var(--appkit-color-surface)] bg-[var(--appkit-color-border)] text-[var(--appkit-color-text-muted)]",
|
|
58
58
|
].join(" "), children: status.replace("_", " ") }));
|
|
59
59
|
},
|
|
60
60
|
},
|
|
@@ -76,7 +76,7 @@ export function getProductTableColumns({ labels, onEdit, onDelete, currencyCode,
|
|
|
76
76
|
"rounded px-[var(--appkit-space-2)] py-[var(--appkit-space-1)] text-[length:var(--appkit-text-xs)] font-medium",
|
|
77
77
|
featured
|
|
78
78
|
? CLS_LOW_STOCK
|
|
79
|
-
: "bg-[var(--appkit-color-surface)]
|
|
79
|
+
: "bg-[var(--appkit-color-surface)] bg-[var(--appkit-color-border)] text-[var(--appkit-color-text-muted)]",
|
|
80
80
|
].join(" "), children: featured ? labels.yes : labels.no }));
|
|
81
81
|
},
|
|
82
82
|
},
|