@mohasinac/appkit 4.11.2 → 4.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_internal/server/features/checkout/actions.d.ts +40 -3
- package/dist/_internal/server/features/checkout/actions.js +232 -47
- package/dist/_internal/server/features/checkout/data.d.ts +10 -0
- package/dist/_internal/server/features/orders/adapters.js +20 -0
- package/dist/_internal/server/features/products/data.d.ts +12 -3
- package/dist/_internal/server/features/products/data.js +55 -4
- package/dist/_internal/server/features/products/index.d.ts +1 -1
- package/dist/_internal/server/features/products/index.js +1 -1
- package/dist/_internal/server/features/products/list-public.d.ts +47 -0
- package/dist/_internal/server/features/products/list-public.js +131 -6
- package/dist/_internal/server/features/reviews/data.d.ts +1 -1
- package/dist/_internal/server/features/reviews/data.js +3 -2
- package/dist/_internal/server/features/tester/visibility.js +3 -3
- package/dist/_internal/server/jobs/core/auctionSettlement.js +5 -2
- package/dist/_internal/server/jobs/core/testerSandboxCleanup.d.ts +1 -1
- package/dist/_internal/server/jobs/core/testerSandboxCleanup.js +4 -1
- package/dist/_internal/server/jobs/core/testerSandboxRefresh.js +2 -0
- package/dist/_internal/server/jobs/handlers/_helpers.js +4 -1
- package/dist/_internal/shared/checkout/lanes.d.ts +20 -0
- package/dist/_internal/shared/checkout/lanes.js +28 -0
- package/dist/_internal/shared/features/checkout/config.d.ts +4 -0
- package/dist/_internal/shared/features/checkout/config.js +8 -0
- package/dist/_internal/shared/features/promotions/schema.d.ts +0 -16
- package/dist/_internal/shared/features/promotions/schema.js +0 -2
- package/dist/_internal/shared/features/reviews/config.d.ts +8 -1
- package/dist/_internal/shared/features/reviews/config.js +8 -1
- package/dist/_internal/shared/fees/calculator.d.ts +34 -2
- package/dist/_internal/shared/fees/calculator.js +45 -1
- package/dist/_internal/shared/listing-types/_registry.d.ts +88 -1
- package/dist/_internal/shared/listing-types/_registry.js +63 -1
- package/dist/_internal/shared/listing-types/art/config.d.ts +59 -0
- package/dist/_internal/shared/listing-types/art/config.js +12 -0
- package/dist/_internal/shared/listing-types/auction/config.d.ts +53 -0
- package/dist/_internal/shared/listing-types/auction/config.js +17 -0
- package/dist/_internal/shared/listing-types/classified/config.d.ts +59 -0
- package/dist/_internal/shared/listing-types/classified/config.js +14 -0
- package/dist/_internal/shared/listing-types/digital-code/config.d.ts +59 -0
- package/dist/_internal/shared/listing-types/digital-code/config.js +12 -0
- package/dist/_internal/shared/listing-types/live/config.d.ts +59 -0
- package/dist/_internal/shared/listing-types/live/config.js +14 -0
- package/dist/_internal/shared/listing-types/pre-order/config.d.ts +110 -0
- package/dist/_internal/shared/listing-types/pre-order/config.js +14 -0
- package/dist/_internal/shared/listing-types/prize-draw/config.d.ts +44 -0
- package/dist/_internal/shared/listing-types/prize-draw/config.js +10 -0
- package/dist/_internal/shared/listing-types/standard/config.d.ts +59 -0
- package/dist/_internal/shared/listing-types/standard/config.js +9 -0
- package/dist/_internal/shared/listing-types/stickers/config.d.ts +59 -0
- package/dist/_internal/shared/listing-types/stickers/config.js +12 -0
- package/dist/client.d.ts +12 -2
- package/dist/client.js +8 -2
- package/dist/constants/api-endpoints.d.ts +6 -0
- package/dist/constants/api-endpoints.js +2 -0
- package/dist/constants/field-names.d.ts +12 -0
- package/dist/constants/field-names.js +12 -0
- package/dist/constants/table-keys.d.ts +2 -0
- package/dist/constants/table-keys.js +2 -0
- package/dist/features/account/components/UserOrdersView.js +27 -5
- package/dist/features/admin/components/AdminAddressesView.js +21 -11
- package/dist/features/admin/components/AdminArtView.d.ts +9 -0
- package/dist/features/admin/components/AdminArtView.js +11 -35
- package/dist/features/admin/components/AdminClassifiedView.d.ts +9 -0
- package/dist/features/admin/components/AdminClassifiedView.js +11 -35
- package/dist/features/admin/components/AdminCouponEditorView.js +1 -4
- package/dist/features/admin/components/AdminDigitalCodesView.d.ts +9 -0
- package/dist/features/admin/components/AdminDigitalCodesView.js +11 -42
- package/dist/features/admin/components/AdminLiveView.d.ts +9 -0
- package/dist/features/admin/components/AdminLiveView.js +11 -41
- package/dist/features/admin/components/AdminOrderEditorView.d.ts +8 -1
- package/dist/features/admin/components/AdminOrderEditorView.js +4 -3
- package/dist/features/admin/components/AdminOrdersView.js +14 -1
- package/dist/features/admin/components/AdminPaymentMethodsView.js +18 -11
- package/dist/features/admin/components/AdminProductsView.js +34 -18
- package/dist/features/admin/components/AdminSiteConfigGuideView.js +1 -1
- package/dist/features/admin/components/AdminSiteSettingsView.js +4 -2
- package/dist/features/admin/components/AdminStickersView.d.ts +9 -0
- package/dist/features/admin/components/AdminStickersView.js +11 -35
- package/dist/features/admin/constants/filter-tabs.d.ts +26 -9
- package/dist/features/admin/constants/filter-tabs.js +22 -9
- package/dist/features/admin/hooks/useAdminListing.js +7 -2
- package/dist/features/admin/schemas/firestore.d.ts +5 -0
- package/dist/features/admin/schemas/firestore.js +1 -0
- package/dist/features/auctions/actions/bid-actions.js +2 -2
- package/dist/features/auctions/components/AuctionBidsTable.js +1 -1
- package/dist/features/auctions/components/AuctionBottomActions.js +1 -0
- package/dist/features/auctions/components/AuctionDetailPageView.js +13 -11
- package/dist/features/cart/components/CartDrawer.d.ts +9 -1
- package/dist/features/cart/components/CartDrawer.js +4 -3
- package/dist/features/cart/components/CartPriceBreakdown.d.ts +61 -0
- package/dist/features/cart/components/CartPriceBreakdown.js +25 -0
- package/dist/features/cart/components/StoreAddonsPicker.d.ts +47 -0
- package/dist/features/cart/components/StoreAddonsPicker.js +21 -0
- package/dist/features/cart/components/index.d.ts +4 -0
- package/dist/features/cart/components/index.js +2 -0
- package/dist/features/cart/repository/cart.repository.d.ts +20 -2
- package/dist/features/cart/repository/cart.repository.js +49 -0
- package/dist/features/cart/schemas/firestore.d.ts +26 -2
- package/dist/features/categories/components/BrandDetailTabs.js +31 -11
- package/dist/features/categories/components/CategoryDetailTabs.js +27 -7
- package/dist/features/categories/components/CategoryProductsListing.d.ts +9 -1
- package/dist/features/categories/components/CategoryProductsListing.js +5 -2
- package/dist/features/classified/components/ClassifiedDetailPageView.js +2 -1
- package/dist/features/contact/email.js +5 -1
- package/dist/features/digital-codes/components/DigitalCodeDetailPageView.js +17 -16
- package/dist/features/homepage/components/FeaturedResultsSection.js +1 -1
- package/dist/features/homepage/components/SectionCarousel.js +1 -1
- package/dist/features/homepage/components/ShopByCategorySection.js +1 -1
- package/dist/features/layout/AppLayoutShell.js +9 -2
- package/dist/features/layout/BottomActions.js +62 -6
- package/dist/features/layout/BottomActionsContext.d.ts +26 -0
- package/dist/features/layout/BottomActionsContext.js +18 -1
- package/dist/features/layout/hooks/useBottomActions.d.ts +12 -1
- package/dist/features/layout/hooks/useBottomActions.js +14 -1
- package/dist/features/layout/index.d.ts +1 -1
- package/dist/features/live/components/LiveItemDetailPageView.js +7 -6
- package/dist/features/orders/components/OrderAddonBadges.d.ts +36 -0
- package/dist/features/orders/components/OrderAddonBadges.js +28 -0
- package/dist/features/orders/components/index.d.ts +1 -0
- package/dist/features/orders/components/index.js +1 -0
- package/dist/features/orders/repository/orders.repository.d.ts +36 -0
- package/dist/features/orders/repository/orders.repository.js +18 -0
- package/dist/features/orders/types/index.d.ts +38 -0
- package/dist/features/pre-orders/components/PreOrderBottomActions.d.ts +1 -1
- package/dist/features/pre-orders/components/PreOrderBottomActions.js +3 -19
- package/dist/features/products/components/ArtStickersListView.js +5 -4
- package/dist/features/products/components/AuctionsIndexListing.js +1 -2
- package/dist/features/products/components/ListingBottomActions.d.ts +28 -0
- package/dist/features/products/components/ListingBottomActions.js +25 -0
- package/dist/features/products/components/PrizeDrawBottomActions.d.ts +1 -1
- package/dist/features/products/components/PrizeDrawBottomActions.js +3 -21
- package/dist/features/products/components/PrizeDrawsListingView.js +18 -33
- package/dist/features/products/components/ProductDetailActions.js +4 -0
- package/dist/features/products/components/ProductDetailPageView.js +7 -7
- package/dist/features/products/components/ProductsIndexListing.js +87 -11
- package/dist/features/products/components/ProductsIndexPageView.js +14 -2
- package/dist/features/products/config/listing-type-listing-config.d.ts +31 -0
- package/dist/features/products/config/listing-type-listing-config.js +74 -0
- package/dist/features/products/constants/listing-tabs.d.ts +43 -198
- package/dist/features/products/constants/listing-tabs.js +71 -58
- package/dist/features/products/constants/sieve.d.ts +104 -1
- package/dist/features/products/constants/sieve.js +27 -7
- package/dist/features/products/hooks/useProducts.js +14 -0
- package/dist/features/products/repository/products.repository.d.ts +45 -1
- package/dist/features/products/repository/products.repository.js +26 -3
- package/dist/features/products/types/index.d.ts +15 -0
- package/dist/features/products/utils/listing-badge-variant.d.ts +20 -1
- package/dist/features/products/utils/listing-badge-variant.js +26 -1
- package/dist/features/products/utils/listing-type.d.ts +24 -0
- package/dist/features/products/utils/listing-type.js +48 -0
- package/dist/features/promotions/actions/coupon-actions.d.ts +7 -0
- package/dist/features/promotions/actions/coupon-actions.js +12 -1
- package/dist/features/promotions/actions/coupon-stacking.d.ts +24 -0
- package/dist/features/promotions/actions/coupon-stacking.js +21 -0
- package/dist/features/promotions/actions/index.d.ts +1 -0
- package/dist/features/promotions/actions/index.js +1 -0
- package/dist/features/promotions/actions/seller-coupon-actions.d.ts +0 -1
- package/dist/features/promotions/actions/seller-coupon-actions.js +0 -1
- package/dist/features/promotions/components/CouponHelpDetails.d.ts +20 -0
- package/dist/features/promotions/components/CouponHelpDetails.js +17 -0
- package/dist/features/promotions/components/CouponsIndexListing.js +2 -1
- package/dist/features/promotions/components/index.d.ts +2 -0
- package/dist/features/promotions/components/index.js +1 -0
- package/dist/features/promotions/constants/coupon-help.d.ts +27 -0
- package/dist/features/promotions/constants/coupon-help.js +42 -0
- package/dist/features/promotions/repository/coupons.repository.d.ts +10 -1
- package/dist/features/promotions/repository/coupons.repository.js +22 -8
- package/dist/features/promotions/schemas/firestore.d.ts +0 -1
- package/dist/features/promotions/schemas/firestore.js +1 -1
- package/dist/features/promotions/schemas/index.d.ts +0 -20
- package/dist/features/promotions/schemas/index.js +0 -1
- package/dist/features/reviews/actions/review-actions.js +5 -1
- package/dist/features/reviews/components/ReviewDetailPageView.js +2 -1
- package/dist/features/reviews/components/ReviewFilters.d.ts +7 -1
- package/dist/features/reviews/components/ReviewFilters.js +4 -3
- package/dist/features/reviews/components/ReviewsIndexListing.d.ts +6 -0
- package/dist/features/reviews/components/ReviewsIndexListing.js +9 -109
- package/dist/features/reviews/components/ReviewsListingPanel.d.ts +41 -0
- package/dist/features/reviews/components/ReviewsListingPanel.js +226 -0
- package/dist/features/reviews/components/index.d.ts +2 -0
- package/dist/features/reviews/components/index.js +1 -0
- package/dist/features/reviews/hooks/useReviews.d.ts +8 -0
- package/dist/features/reviews/hooks/useReviews.js +14 -6
- package/dist/features/reviews/repository/reviews.repository.d.ts +29 -1
- package/dist/features/reviews/repository/reviews.repository.js +77 -41
- package/dist/features/reviews/types/index.d.ts +8 -1
- package/dist/features/seller/components/SellerOrdersView.js +2 -1
- package/dist/features/seller/components/SellerProductsCards.js +2 -2
- package/dist/features/seller/components/SellerProductsView.js +21 -18
- package/dist/features/stores/actions/store-query-actions.js +1 -1
- package/dist/features/stores/components/StoreArtStickersPageView.d.ts +21 -0
- package/dist/features/stores/components/StoreArtStickersPageView.js +30 -0
- package/dist/features/stores/components/StoreAuctionsListing.js +5 -11
- package/dist/features/stores/components/StoreAuctionsPageView.d.ts +4 -1
- package/dist/features/stores/components/StoreAuctionsPageView.js +17 -17
- package/dist/features/stores/components/StoreClassifiedsListing.js +7 -0
- package/dist/features/stores/components/StoreClassifiedsPageView.d.ts +4 -1
- package/dist/features/stores/components/StoreClassifiedsPageView.js +16 -12
- package/dist/features/stores/components/StoreDetailLayoutView.js +4 -3
- package/dist/features/stores/components/StoreDigitalCodesListing.js +5 -0
- package/dist/features/stores/components/StoreDigitalCodesPageView.d.ts +4 -1
- package/dist/features/stores/components/StoreDigitalCodesPageView.js +16 -12
- package/dist/features/stores/components/StoreLiveItemsListing.js +7 -0
- package/dist/features/stores/components/StoreLiveItemsPageView.d.ts +4 -1
- package/dist/features/stores/components/StoreLiveItemsPageView.js +16 -12
- package/dist/features/stores/components/StorePreOrdersListing.js +6 -11
- package/dist/features/stores/components/StorePreOrdersPageView.d.ts +4 -1
- package/dist/features/stores/components/StorePreOrdersPageView.js +17 -17
- package/dist/features/stores/components/StorePrizeDrawsPageView.d.ts +4 -8
- package/dist/features/stores/components/StorePrizeDrawsPageView.js +17 -22
- package/dist/features/stores/components/StoreProductsListing.d.ts +7 -1
- package/dist/features/stores/components/StoreProductsListing.js +3 -2
- package/dist/features/stores/components/StoreProductsPageView.d.ts +1 -1
- package/dist/features/stores/components/StoreProductsPageView.js +16 -21
- package/dist/features/stores/components/StoreReviewsListing.d.ts +4 -0
- package/dist/features/stores/components/StoreReviewsListing.js +7 -108
- package/dist/features/stores/schemas/index.d.ts +2 -2
- package/dist/features/tester/seed-data/coupons-tester-seed-data.js +3 -3
- package/dist/features/tester/seed-data/index.d.ts +1 -0
- package/dist/features/tester/seed-data/index.js +1 -0
- package/dist/features/tester/seed-data/offers-tester-seed-data.d.ts +2 -0
- package/dist/features/tester/seed-data/offers-tester-seed-data.js +61 -0
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +939 -1
- package/dist/index.d.ts +9 -3
- package/dist/index.js +11 -2
- package/dist/next/routing/route-map.d.ts +2 -0
- package/dist/next/routing/route-map.js +5 -0
- package/dist/react/hooks/useListingTypeFlags.d.ts +12 -19
- package/dist/react/hooks/useListingTypeFlags.js +4 -9
- package/dist/schemas/registry.d.ts +0 -12
- package/dist/seed/claimed-coupons-seed-data.js +3 -3
- package/dist/seed/coupons-seed-data.js +29 -10
- package/dist/seed/faq-seed-data.js +2 -2
- package/dist/seed/manifest.js +2 -2
- package/dist/seed/reviews-seed-data.js +52 -0
- package/dist/server-entry.d.ts +1 -0
- package/dist/server-entry.js +1 -0
- package/dist/styles.css +55 -39
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/Checkbox.d.ts +10 -1
- package/dist/ui/components/Checkbox.js +14 -2
- package/dist/ui/components/Checkbox.style.css +11 -0
- package/dist/ui/components/FilterChipGroup.d.ts +21 -3
- package/dist/ui/components/FilterChipGroup.js +33 -4
- package/dist/ui/components/HorizontalScroller.d.ts +1 -2
- package/dist/ui/components/HorizontalScroller.js +9 -5
- package/dist/ui/components/HorizontalScroller.style.css +43 -38
- package/package.json +1 -1
|
@@ -21,6 +21,7 @@ import { computeRelatedItems } from "../../../_internal/server/features/products
|
|
|
21
21
|
import { GroupedListingsCarousel } from "../../grouped/components/GroupedListingsCarousel";
|
|
22
22
|
import { getGroupsWithItemsForProduct } from "../../../_internal/server/features/grouped/data";
|
|
23
23
|
import { ClassifiedContactSellerPanel } from "./ClassifiedContactSellerPanel";
|
|
24
|
+
import { ListingBottomActions } from "../../products/components/ListingBottomActions";
|
|
24
25
|
function toDescriptionHtml(raw) {
|
|
25
26
|
if (!raw)
|
|
26
27
|
return "";
|
|
@@ -65,5 +66,5 @@ export async function ClassifiedDetailPageView({ slug, initialProduct }) {
|
|
|
65
66
|
id: s.id,
|
|
66
67
|
label: s.title,
|
|
67
68
|
content: _jsx(CustomSectionTabContent, { section: s }),
|
|
68
|
-
})) })), renderBuyBar: () => (_jsx(Stack, { id: "classified-contact-bar", className: "p-[var(--appkit-space-5)]", border: "subtle", gap: "md", rounded: "xl", surface: "muted", children: _jsx(ClassifiedContactSellerPanel, { productId: String(product.id), onContactSeller: startClassifiedConversationAction }) })), renderRelated: () => (_jsxs(Stack, { gap: "xl", children: [_jsx(GroupedListingsCarousel, { groups: groups }), _jsx(RelatedItemsSection, { relatedItems: relatedItems, relatedByBrand: relatedByBrand, relatedByTags: relatedByTags, relatedByStore: relatedByStore, categoryLabel: categoryName || category || undefined, brandLabel: brand, storeLabel: storeName ?? undefined })] })) })] })] }));
|
|
69
|
+
})) })), renderBuyBar: () => (_jsx(Stack, { id: "classified-contact-bar", className: "p-[var(--appkit-space-5)]", border: "subtle", gap: "md", rounded: "xl", surface: "muted", children: _jsx(ClassifiedContactSellerPanel, { productId: String(product.id), onContactSeller: startClassifiedConversationAction }) })), renderRelated: () => (_jsxs(Stack, { gap: "xl", children: [_jsx(GroupedListingsCarousel, { groups: groups }), _jsx(RelatedItemsSection, { relatedItems: relatedItems, relatedByBrand: relatedByBrand, relatedByTags: relatedByTags, relatedByStore: relatedByStore, categoryLabel: categoryName || category || undefined, brandLabel: brand, storeLabel: storeName ?? undefined })] })) }), _jsx(ListingBottomActions, { listingType: "classified", anchorId: "classified-contact-bar", price: price, currency: currency })] })] }));
|
|
69
70
|
}
|
|
@@ -47,7 +47,11 @@ async function resolveEmailProvider() {
|
|
|
47
47
|
try {
|
|
48
48
|
return getProviders().email;
|
|
49
49
|
}
|
|
50
|
-
catch {
|
|
50
|
+
catch (err) {
|
|
51
|
+
// Not an error condition — "no email provider registered yet" is the
|
|
52
|
+
// expected signal on the Functions runtime, and the fallback below builds
|
|
53
|
+
// one from credentials. Normalized anyway so the catch is never bare.
|
|
54
|
+
void normalizeError(err);
|
|
51
55
|
const [creds, settings] = await Promise.all([
|
|
52
56
|
siteSettingsRepository.getDecryptedCredentials().catch((err) => {
|
|
53
57
|
void normalizeError(err);
|
|
@@ -16,10 +16,11 @@ import { CustomSectionTabContent } from "../../products/components/CustomSection
|
|
|
16
16
|
import { ShareButton } from "../../products/components/ShareButton";
|
|
17
17
|
import { HistoryTracker } from "../../history/components/HistoryTracker";
|
|
18
18
|
import { RelatedItemsSection } from "../../products/components/RelatedItemsSection";
|
|
19
|
-
import { computeRelatedItems,
|
|
19
|
+
import { computeRelatedItems, getReviewPageForProduct } from "../../../_internal/server/features/products/data";
|
|
20
20
|
import { GroupedListingsCarousel } from "../../grouped/components/GroupedListingsCarousel";
|
|
21
21
|
import { getGroupsWithItemsForProduct } from "../../../_internal/server/features/grouped/data";
|
|
22
|
-
import {
|
|
22
|
+
import { ReviewsListingPanel } from "../../reviews/components/ReviewsListingPanel";
|
|
23
|
+
import { ListingBottomActions } from "../../products/components/ListingBottomActions";
|
|
23
24
|
function toDescriptionHtml(raw) {
|
|
24
25
|
if (!raw)
|
|
25
26
|
return "";
|
|
@@ -50,10 +51,10 @@ export async function DigitalCodeDetailPageView({ slug, initialProduct, renderPr
|
|
|
50
51
|
const specs = Array.isArray(p.specifications) ? p.specifications : [];
|
|
51
52
|
const customSections = Array.isArray(p.customSections) ? p.customSections : [];
|
|
52
53
|
const descriptionHtml = toDescriptionHtml(p.description);
|
|
53
|
-
const [{ relatedItems, relatedByBrand, relatedByTags, relatedByStore }, groups,
|
|
54
|
+
const [{ relatedItems, relatedByBrand, relatedByTags, relatedByStore }, groups, initialReviews] = await Promise.all([
|
|
54
55
|
computeRelatedItems(product),
|
|
55
56
|
getGroupsWithItemsForProduct(product.id),
|
|
56
|
-
|
|
57
|
+
getReviewPageForProduct(product.id),
|
|
57
58
|
]);
|
|
58
59
|
return (_jsxs(Main, { children: [_jsx(HistoryTracker, { productId: String(p.id ?? p.slug ?? ""), productType: "product", snapshot: {
|
|
59
60
|
title,
|
|
@@ -61,19 +62,19 @@ export async function DigitalCodeDetailPageView({ slug, initialProduct, renderPr
|
|
|
61
62
|
price: price ?? undefined,
|
|
62
63
|
storeId: typeof p.storeId === "string" ? p.storeId : undefined,
|
|
63
64
|
storeName: storeName ?? undefined,
|
|
64
|
-
} }), _jsxs(Container, { size: "xl", padding: "y-lg", children: [_jsxs(Row, { className: "mb-4", align: "center", justify: "between", gap: "sm", wrap: true, children: [_jsxs(Nav, { "aria-label": "Breadcrumb", layout: "flex-wrap", gap: "2xs", textSize: "xs", color: "muted", children: [_jsx(Link, { href: String(ROUTES.HOME), className: CLS_BREADCRUMB_LINK, children: "Home" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.DIGITAL_CODES), className: CLS_BREADCRUMB_LINK, children: "Digital Codes" }), category && (_jsxs(_Fragment, { children: [_jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category)), className: CLS_BREADCRUMB_LINK, children: categoryName || category })] })), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Span, { className: "truncate max-w-[200px]", color: "muted", children: title })] }), _jsx(ShareButton, { title: title })] }), _jsx(PreOrderDetailView, { renderGallery: () => _jsx(ProductGalleryClient, { images: images, productName: title }), renderInfo: () => (_jsxs(Stack, { gap: "md", children: [_jsxs(Div, { children: [_jsxs(Row, { gap: "xs", wrap: true, className: "mb-2", children: [_jsx(Span, { size: "xs", weight: "semibold", className: CLS_DIGITALCODE_BADGE, children: "Digital Code" }), codesLeft > 0 ? (_jsxs(Span, { size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", surface: "subtle", color: "muted", children: [codesLeft, " available"] })) : (_jsx(Span, { size: "xs", weight: "semibold", className: "inline-block", padding: "pill-sm", rounded: "full", surface: "danger-surface", color: "error", children: "Sold out" }))] }), _jsx(Heading, { level: 1, className: "leading-snug", smSize: "2xl", color: "primary", size: "xl", weight: "bold", children: title })] }), price !== null && (_jsx(Text, { size: "2xl", weight: "bold", color: "primary", children: formatCurrency(price, currency) })), meta?.redemptionInstructions && (_jsx(Text, { size: "xs", color: "muted", children: meta.redemptionInstructions })), 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, { border: "subtle", rounded: "xl", surface: "muted", padding: "md", 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" }))] }) }))] })), renderTabs: () => (_jsx(ProductTabsShell, { descriptionContent: descriptionHtml ? (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm sm:prose max-w-none dark:prose-invert", className: "text-[var(--appkit-color-text-muted)]" })) : undefined, specsContent: specs.length > 0 ? (_jsx(Dl, { divide: "subtle", rounded: "xl", border: "subtle", className: "overflow-hidden", children: specs.map((s, i) => (_jsxs(Row, { gap: "md", oddEven: "zebra", surface: "default", padding: "inline", children: [_jsx(Dt, { className: "w-36 flex-shrink-0", color: "primary", weight: "medium", children: s.name }), _jsxs(Dd, { className: "flex-1", color: "muted", children: [s.value, s.unit ? ` ${s.unit}` : ""] })] }, i))) })) : undefined, reviewsContent: _jsx(
|
|
65
|
+
} }), _jsxs(Container, { size: "xl", padding: "y-lg", children: [_jsxs(Row, { className: "mb-4", align: "center", justify: "between", gap: "sm", wrap: true, children: [_jsxs(Nav, { "aria-label": "Breadcrumb", layout: "flex-wrap", gap: "2xs", textSize: "xs", color: "muted", children: [_jsx(Link, { href: String(ROUTES.HOME), className: CLS_BREADCRUMB_LINK, children: "Home" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.DIGITAL_CODES), className: CLS_BREADCRUMB_LINK, children: "Digital Codes" }), category && (_jsxs(_Fragment, { children: [_jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category)), className: CLS_BREADCRUMB_LINK, children: categoryName || category })] })), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Span, { className: "truncate max-w-[200px]", color: "muted", children: title })] }), _jsx(ShareButton, { title: title })] }), _jsx(PreOrderDetailView, { renderGallery: () => _jsx(ProductGalleryClient, { images: images, productName: title }), renderInfo: () => (_jsxs(Stack, { gap: "md", children: [_jsxs(Div, { children: [_jsxs(Row, { gap: "xs", wrap: true, className: "mb-2", children: [_jsx(Span, { size: "xs", weight: "semibold", className: CLS_DIGITALCODE_BADGE, children: "Digital Code" }), codesLeft > 0 ? (_jsxs(Span, { size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", surface: "subtle", color: "muted", children: [codesLeft, " available"] })) : (_jsx(Span, { size: "xs", weight: "semibold", className: "inline-block", padding: "pill-sm", rounded: "full", surface: "danger-surface", color: "error", children: "Sold out" }))] }), _jsx(Heading, { level: 1, className: "leading-snug", smSize: "2xl", color: "primary", size: "xl", weight: "bold", children: title })] }), price !== null && (_jsx(Text, { size: "2xl", weight: "bold", color: "primary", children: formatCurrency(price, currency) })), meta?.redemptionInstructions && (_jsx(Text, { size: "xs", color: "muted", children: meta.redemptionInstructions })), 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, { border: "subtle", rounded: "xl", surface: "muted", padding: "md", 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" }))] }) }))] })), renderTabs: () => (_jsx(ProductTabsShell, { descriptionContent: descriptionHtml ? (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm sm:prose max-w-none dark:prose-invert", className: "text-[var(--appkit-color-text-muted)]" })) : undefined, specsContent: specs.length > 0 ? (_jsx(Dl, { divide: "subtle", rounded: "xl", border: "subtle", className: "overflow-hidden", children: specs.map((s, i) => (_jsxs(Row, { gap: "md", oddEven: "zebra", surface: "default", padding: "inline", children: [_jsx(Dt, { className: "w-36 flex-shrink-0", color: "primary", weight: "medium", children: s.name }), _jsxs(Dd, { className: "flex-1", color: "muted", children: [s.value, s.unit ? ` ${s.unit}` : ""] })] }, i))) })) : undefined, reviewsContent: _jsx(ReviewsListingPanel, { source: { kind: "product", productId: product.id }, stateMode: "local", context: "listing", initialData: initialReviews, emptyLabel: "No reviews yet \u2014 be the first to review this product." }), customTabs: customSections.map((s) => ({
|
|
65
66
|
id: s.id,
|
|
66
67
|
label: s.title,
|
|
67
68
|
content: _jsx(CustomSectionTabContent, { section: s }),
|
|
68
|
-
})) })), renderBuyBar: () => renderPrimaryActions?.({
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
69
|
+
})) })), renderBuyBar: () => (_jsx(Div, { id: "digital-code-buy-bar", children: renderPrimaryActions?.({
|
|
70
|
+
productId: String(product.id),
|
|
71
|
+
productSlug: String(p.slug ?? product.id),
|
|
72
|
+
productTitle: title,
|
|
73
|
+
productImage: images[0],
|
|
74
|
+
price,
|
|
75
|
+
currency,
|
|
76
|
+
storeId: typeof p.storeId === "string" ? p.storeId : undefined,
|
|
77
|
+
storeName: storeName ?? undefined,
|
|
78
|
+
inStock: codesLeft > 0,
|
|
79
|
+
}) })), renderRelated: () => (_jsxs(Stack, { gap: "xl", children: [_jsx(GroupedListingsCarousel, { groups: groups }), _jsx(RelatedItemsSection, { relatedItems: relatedItems, relatedByBrand: relatedByBrand, relatedByTags: relatedByTags, relatedByStore: relatedByStore, categoryLabel: categoryName || category || undefined, brandLabel: brand, storeLabel: storeName ?? undefined })] })) }), _jsx(ListingBottomActions, { listingType: "digital-code", anchorId: "digital-code-buy-bar", price: price, currency: currency, unavailable: codesLeft <= 0 })] })] }));
|
|
79
80
|
}
|
|
@@ -13,5 +13,5 @@ export function FeaturedResultsSection({ title, subtitle, pillLabel, ornamentLab
|
|
|
13
13
|
afterImage: item.afterImage,
|
|
14
14
|
caption: item.caption ?? "",
|
|
15
15
|
sortOrder: item.sortOrder ?? i,
|
|
16
|
-
} }, item.id ?? `result-${i}`) })), keyExtractor: (item, i) => item.id ?? `result-${i}`, perView: CAROUSEL_PER_VIEW.cards, gap: 24, minItemWidth: 260, snapToItems: true, showArrows: true,
|
|
16
|
+
} }, item.id ?? `result-${i}`) })), keyExtractor: (item, i) => item.id ?? `result-${i}`, perView: CAROUSEL_PER_VIEW.cards, gap: 24, minItemWidth: 260, snapToItems: true, showArrows: true, loop: true }) })] }));
|
|
17
17
|
}
|
|
@@ -48,5 +48,5 @@ export function SectionCarousel({ title, description, headingVariant = "editoria
|
|
|
48
48
|
// Double-row grids (e.g. the homepage Auctions section's
|
|
49
49
|
// rows:2 config) get the full-height sliver arrows — much
|
|
50
50
|
// easier to hit than a small circle when the track is tall.
|
|
51
|
-
arrowStyle: rows > 1 ? "full-height" : "circle", snapToItems: true,
|
|
51
|
+
arrowStyle: rows > 1 ? "full-height" : "circle", snapToItems: true, showScrollbar: false, pauseOnHover: true })), viewMoreHref && !isLoading && (_jsx(Row, { className: "mt-6", justify: hand === "left" ? "end" : "start", children: _jsx(TextLink, { rounded: "lg", paddingX: "xl", paddingY: "sm", href: viewMoreHref, shadow: useLightText ? "lg" : "none", className: "inline-flex items-[center] gap-[0.375rem] transition-colors bg-primary text-white hover:bg-primary-600", size: "sm", weight: "semibold", children: viewMoreLabel }) }))] })] }));
|
|
52
52
|
}
|
|
@@ -69,5 +69,5 @@ export function ShopByCategorySection({ title = "Shop by Category", subtitle, li
|
|
|
69
69
|
if (!cat)
|
|
70
70
|
return null;
|
|
71
71
|
return (_jsx(FilterChip, { label: cat.name, active: activeFilter === id, onClick: () => setActiveFilter(activeFilter === id ? "all" : id) }, id));
|
|
72
|
-
})] })), isLoading ? (_jsx(Div, { layout: "flex", gap: "3", className: `${__O.hidden} px-[0.25rem]`, children: Array.from({ length: 6 }).map((_, i) => (_jsx(Div, { className: "flex-none h-[104px] w-[108px] animate-pulse", surface: "subtle", rounded: "xl" }, i))) })) : (_jsx(HorizontalScroller, { items: categories, renderItem: (cat) => _jsx(CategoryChip, { category: cat }), perView: CAROUSEL_PER_VIEW.standard, gap: 16, keyExtractor: (cat) => cat.id, autoScroll: autoScroll, autoScrollInterval: scrollInterval, showArrows: true, snapToItems: true,
|
|
72
|
+
})] })), isLoading ? (_jsx(Div, { layout: "flex", gap: "3", className: `${__O.hidden} px-[0.25rem]`, children: Array.from({ length: 6 }).map((_, i) => (_jsx(Div, { className: "flex-none h-[104px] w-[108px] animate-pulse", surface: "subtle", rounded: "xl" }, i))) })) : (_jsx(HorizontalScroller, { items: categories, renderItem: (cat) => _jsx(CategoryChip, { category: cat }), perView: CAROUSEL_PER_VIEW.standard, gap: 16, keyExtractor: (cat) => cat.id, autoScroll: autoScroll, autoScrollInterval: scrollInterval, showArrows: true, snapToItems: true, showScrollbar: false, loop: loop, pauseOnHover: true })), cta && !isLoading && (_jsx(Div, { className: "mt-6 text-left", children: _jsx(Link, { href: cta.href, className: CTA_CLASSES[cta.variant], children: cta.label }) })), !cta && viewMoreHref && !isLoading && (_jsx(Div, { className: "mt-6 text-left", children: _jsx(Link, { href: viewMoreHref, className: "inline-flex items-center gap-[var(--appkit-space-1)] text-[length:var(--appkit-text-sm)] font-medium text-[var(--appkit-color-primary)] hover:opacity-80", children: viewMoreLabel }) }))] }) }));
|
|
73
73
|
}
|
|
@@ -170,9 +170,16 @@ export function AppLayoutShell({ children, navItems, sidebarItems = [], sidebarS
|
|
|
170
170
|
const handleBeforeDashboardNavToggle = useCallback(() => {
|
|
171
171
|
setSidebarOpen(false);
|
|
172
172
|
}, []);
|
|
173
|
+
// Must stay in sync with BottomActions' own `isVisible` — `secondaryLabel` was missing
|
|
174
|
+
// here, so an auction bar shown by its countdown alone got no bottom clearance.
|
|
173
175
|
const hasBottomActions = bottomActionsState.actions.length > 0 ||
|
|
174
176
|
!!(bottomActionsState.bulk && bottomActionsState.bulk.selectedCount > 0) ||
|
|
175
|
-
!!bottomActionsState.infoLabel
|
|
177
|
+
!!bottomActionsState.infoLabel ||
|
|
178
|
+
!!bottomActionsState.secondaryLabel;
|
|
179
|
+
/** The bar overlays desktop content too once a page opts in, so reserve space there. */
|
|
180
|
+
const hasDesktopBottomActions = hasBottomActions &&
|
|
181
|
+
(bottomActionsState.desktop === "always" ||
|
|
182
|
+
bottomActionsState.desktop === "after-scroll");
|
|
176
183
|
const sidebarContent = (_jsx(SidebarContent, { sidebarItems: sidebarItems, sidebarSections: sidebarSections, sidebarPrimaryActions: sidebarPrimaryActions, user: user, activeHref: pathname ?? "", profileHref: profileHref, userOrdersHref: userOrdersHref, userWishlistHref: userWishlistHref, userSettingsHref: userSettingsHref, adminHref: adminHref, storeHref: storeHref, sellerHref: sellerHref, sidebarLocaleSlot: sidebarLocaleSlot, showThemeToggleInSidebar: showThemeToggleInSidebar, sidebarProfileLabels: sidebarProfileLabels, theme: theme, activeThemeName: activeTheme.name, toggleTheme: toggleTheme, onLogout: onLogout, onAfterLogout: () => setSidebarOpen(false) }));
|
|
177
184
|
const normalizedLightBackground = {
|
|
178
185
|
type: lightBackground.type,
|
|
@@ -192,5 +199,5 @@ export function AppLayoutShell({ children, navItems, sidebarItems = [], sidebarS
|
|
|
192
199
|
opacity: darkBackground.overlay?.opacity ?? 0,
|
|
193
200
|
},
|
|
194
201
|
};
|
|
195
|
-
return (_jsx(_Fragment, { children: _jsxs(Stack, { className: "min-h-screen w-full overflow-x-clip transition-colors duration-300", children: [_jsx(BackgroundRenderer, { mode: theme === "dark" ? "dark" : "light", lightMode: normalizedLightBackground, darkMode: normalizedDarkBackground }), _jsxs(Div, { ref: headerRef, className: "sticky top-0 z-50 w-full", children: [_jsx(TitleBar, { onToggleSidebar: handleTogglePublicSidebar, sidebarOpen: sidebarOpen, onSearchToggle: () => setSearchOpen((prev) => !prev), searchOpen: searchOpen, brandName: brandName, brandShortName: brandShortName, siteLogoUrl: siteLogoUrl, logoHref: logoHref, promotionsHref: promotionsHref, cartHref: cartHref, wishlistHref: wishlistHref, userId: userId, profileHref: profileHref, loginHref: loginHref, registerHref: registerHref, user: user, navSlot: titleBarNavSlot, notificationSlot: titleBarNotificationSlot, devSlot: titleBarDevSlot, promoStripText: titleBarPromoStripText, isDark: theme === "dark", onToggleTheme: showThemeToggle ? toggleTheme : undefined, onTourStart: onTourStart, onBeforeToggleDashboardNav: handleBeforeDashboardNavToggle, suppressDashboardNav: suppressDashboardNav, hideSidebarToggle: hideSidebarToggle }), _jsx(NavbarWithSettings, { navItems: navItems, hiddenNavItems: hiddenNavItems, permissions: authUser?.permissions }), searchOpen && (searchSlotRenderer ? searchSlotRenderer(() => setSearchOpen(false)) : searchSlot), eventBannerSlot] }), _jsx(AutoBreadcrumbs, {}), _jsxs(Div, { layout: "flex", className: "relative w-full flex-1 overflow-x-clip", children: [_jsx(SidebarLayout, { isOpen: sidebarOpen, ariaLabel: "Secondary navigation", header: user ? (_jsx(SidebarUserHeader, { user: user, onClose: () => setSidebarOpen(false) })) : (_jsx(SidebarGuestHeader, { sidebarTitle: sidebarTitle, onClose: () => setSidebarOpen(false) })), onClose: () => setSidebarOpen(false), children: sidebarContent }), _jsx(Main, { id: "main-content", className: `w-full flex-1 flex flex-col ${hasBottomActions ? "mb-28" : "mb-16"} lg:mb-0`, children: _jsx(Div, { padding: "y-lg", className: `flex-1 ${contentClassName ?? "mx-auto w-full max-w-screen-xl px-[var(--appkit-space-5)] md:px-[var(--appkit-space-6)] lg:px-[var(--appkit-space-8)]"}`, children: children }) })] }), _jsx(BackToTop, {}), _jsx(FooterLayout, { ...footer }), _jsx(BottomActions, {}), _jsx(BottomNavbar, { user: user, userId: userId, homeHref: homeHref, shopHref: shopHref, cartHref: cartHref, wishlistHref: wishlistHref, profileHref: profileHref, loginHref: loginHref, onSearchToggle: () => setSearchOpen((prev) => !prev), navItems: navItems, onMoreToggle: hasDashboardNav ? toggleDashboardNav : handleTogglePublicSidebar }), _jsx(UnsavedChangesModal, {})] }) }));
|
|
202
|
+
return (_jsx(_Fragment, { children: _jsxs(Stack, { className: "min-h-screen w-full overflow-x-clip transition-colors duration-300", children: [_jsx(BackgroundRenderer, { mode: theme === "dark" ? "dark" : "light", lightMode: normalizedLightBackground, darkMode: normalizedDarkBackground }), _jsxs(Div, { ref: headerRef, className: "sticky top-0 z-50 w-full", children: [_jsx(TitleBar, { onToggleSidebar: handleTogglePublicSidebar, sidebarOpen: sidebarOpen, onSearchToggle: () => setSearchOpen((prev) => !prev), searchOpen: searchOpen, brandName: brandName, brandShortName: brandShortName, siteLogoUrl: siteLogoUrl, logoHref: logoHref, promotionsHref: promotionsHref, cartHref: cartHref, wishlistHref: wishlistHref, userId: userId, profileHref: profileHref, loginHref: loginHref, registerHref: registerHref, user: user, navSlot: titleBarNavSlot, notificationSlot: titleBarNotificationSlot, devSlot: titleBarDevSlot, promoStripText: titleBarPromoStripText, isDark: theme === "dark", onToggleTheme: showThemeToggle ? toggleTheme : undefined, onTourStart: onTourStart, onBeforeToggleDashboardNav: handleBeforeDashboardNavToggle, suppressDashboardNav: suppressDashboardNav, hideSidebarToggle: hideSidebarToggle }), _jsx(NavbarWithSettings, { navItems: navItems, hiddenNavItems: hiddenNavItems, permissions: authUser?.permissions }), searchOpen && (searchSlotRenderer ? searchSlotRenderer(() => setSearchOpen(false)) : searchSlot), eventBannerSlot] }), _jsx(AutoBreadcrumbs, {}), _jsxs(Div, { layout: "flex", className: "relative w-full flex-1 overflow-x-clip", children: [_jsx(SidebarLayout, { isOpen: sidebarOpen, ariaLabel: "Secondary navigation", header: user ? (_jsx(SidebarUserHeader, { user: user, onClose: () => setSidebarOpen(false) })) : (_jsx(SidebarGuestHeader, { sidebarTitle: sidebarTitle, onClose: () => setSidebarOpen(false) })), onClose: () => setSidebarOpen(false), children: sidebarContent }), _jsx(Main, { id: "main-content", className: `w-full flex-1 flex flex-col ${hasBottomActions ? "mb-28" : "mb-16"} ${hasDesktopBottomActions ? "lg:mb-24" : "lg:mb-0"}`, children: _jsx(Div, { padding: "y-lg", className: `flex-1 ${contentClassName ?? "mx-auto w-full max-w-screen-xl px-[var(--appkit-space-5)] md:px-[var(--appkit-space-6)] lg:px-[var(--appkit-space-8)]"}`, children: children }) })] }), _jsx(BackToTop, {}), _jsx(FooterLayout, { ...footer }), _jsx(BottomActions, {}), _jsx(BottomNavbar, { user: user, userId: userId, homeHref: homeHref, shopHref: shopHref, cartHref: cartHref, wishlistHref: wishlistHref, profileHref: profileHref, loginHref: loginHref, onSearchToggle: () => setSearchOpen((prev) => !prev), navItems: navItems, onMoreToggle: hasDashboardNav ? toggleDashboardNav : handleTogglePublicSidebar }), _jsx(UnsavedChangesModal, {})] }) }));
|
|
196
203
|
}
|
|
@@ -18,7 +18,9 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
18
18
|
* styled with the selected action's variant (danger = red, etc.).
|
|
19
19
|
*
|
|
20
20
|
* Layout rules:
|
|
21
|
-
* - Hidden on lg+ screens
|
|
21
|
+
* - Hidden on lg+ screens by default — desktop shows inline action panels. A page can
|
|
22
|
+
* opt in by passing `desktop: "after-scroll" | "always"` to `useBottomActions`;
|
|
23
|
+
* "after-scroll" brings the bar back once the primary CTA has scrolled out of view.
|
|
22
24
|
* - The bar slides up with a 300 ms ease-out transition; `pointer-events-none`
|
|
23
25
|
* while off-screen.
|
|
24
26
|
*
|
|
@@ -27,7 +29,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
27
29
|
* // Automatically rendered by LayoutClient — no manual usage required.
|
|
28
30
|
* // Features use `useBottomActions` to register their actions.
|
|
29
31
|
*/
|
|
30
|
-
import { useState, useRef, useEffect } from "react";
|
|
32
|
+
import { useState, useRef, useEffect, useId } from "react";
|
|
31
33
|
import { X, ChevronUp, ChevronDown, Check } from "lucide-react";
|
|
32
34
|
import { useBottomActionsContext } from "./BottomActionsContext";
|
|
33
35
|
import { useClickOutside } from "../../react";
|
|
@@ -38,6 +40,8 @@ const Z_BOTTOM_ACTIONS = "z-[var(--appkit-z-bottom-nav)]";
|
|
|
38
40
|
const BOTTOM_NAV_HEIGHT = "h-14";
|
|
39
41
|
const FLEX_CENTER = "flex items-center justify-center";
|
|
40
42
|
const CLS_COUNT_BADGE = "bg-error-surface text-white";
|
|
43
|
+
/** How far the user must scroll before the desktop bar reveals itself. */
|
|
44
|
+
const DESKTOP_REVEAL_SCROLL_PX = 400;
|
|
41
45
|
// ─── Sub-components ───────────────────────────────────────────────────────────
|
|
42
46
|
function BulkPickerPanel({ bulkActions, selectedActionId, pickerOpen, onSelect, }) {
|
|
43
47
|
return (_jsx(Div, { role: "listbox", "aria-label": "Bulk actions", className: [
|
|
@@ -64,6 +68,27 @@ function BulkPickerPanel({ bulkActions, selectedActionId, pickerOpen, onSelect,
|
|
|
64
68
|
.join(" "), children: [action.icon && (_jsx(Span, { className: `flex-shrink-0 w-5 h-5 ${FLEX_CENTER}`, "aria-hidden": "true", children: action.icon })), _jsx(Span, { className: "flex-1 truncate", children: action.label }), isSelected && (_jsx(Check, { className: "w-4 h-4 flex-shrink-0 text-primary-600 dark:text-primary-400", "aria-hidden": "true" }))] }, action.id));
|
|
65
69
|
}) }));
|
|
66
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* Expandable detail that opens UPWARD from the bar.
|
|
73
|
+
*
|
|
74
|
+
* Same recipe as BulkPickerPanel above — absolutely positioned at `bottom-full`
|
|
75
|
+
* so it grows into the space above the bar rather than pushing the page, and
|
|
76
|
+
* animated on max-height/opacity so it can't be tabbed into while closed.
|
|
77
|
+
* Scrolls internally, since a many-store cart's breakdown can outgrow the
|
|
78
|
+
* screen.
|
|
79
|
+
*/
|
|
80
|
+
function InfoPanel({ id, open, children, }) {
|
|
81
|
+
return (_jsx(Div, { id: id, className: [
|
|
82
|
+
"absolute bottom-full left-0 right-0 overflow-hidden",
|
|
83
|
+
BOTTOM_NAV_BG,
|
|
84
|
+
"border-t border-zinc-200/80 border-[var(--appkit-color-border)]/80",
|
|
85
|
+
"shadow-[0_-8px_24px_rgba(0,0,0,0.10)] dark:shadow-[0_-8px_24px_rgba(0,0,0,0.35)]",
|
|
86
|
+
"transition-[max-height,opacity] duration-200 ease-out",
|
|
87
|
+
open ? "max-h-[60vh] opacity-100" : "max-h-0 opacity-0 pointer-events-none",
|
|
88
|
+
]
|
|
89
|
+
.filter(Boolean)
|
|
90
|
+
.join(" "), "aria-hidden": !open, children: _jsx(Div, { className: "max-h-[60vh]", overflow: "y-auto", padding: "md", children: children }) }));
|
|
91
|
+
}
|
|
67
92
|
function PageActionsRow({ pageActions, dispatchAction, }) {
|
|
68
93
|
return (_jsx(_Fragment, { children: pageActions.map((action) => {
|
|
69
94
|
const isIconOnly = !action.label;
|
|
@@ -82,16 +107,44 @@ function PageActionsRow({ pageActions, dispatchAction, }) {
|
|
|
82
107
|
export default function BottomActions() {
|
|
83
108
|
const { state, actionCallbacksRef, bulkCallbacksRef, bulkClearRef } = useBottomActionsContext();
|
|
84
109
|
const [pickerOpen, setPickerOpen] = useState(false);
|
|
110
|
+
const [infoOpen, setInfoOpen] = useState(false);
|
|
85
111
|
const [selectedActionId, setSelectedActionId] = useState(null);
|
|
86
112
|
const containerRef = useRef(null);
|
|
87
|
-
|
|
88
|
-
|
|
113
|
+
const infoPanelId = useId();
|
|
114
|
+
useClickOutside(containerRef, () => {
|
|
115
|
+
setPickerOpen(false);
|
|
116
|
+
setInfoOpen(false);
|
|
117
|
+
});
|
|
118
|
+
const { actions, bulk, infoLabel, secondaryLabel, infoPanel, desktop } = state;
|
|
119
|
+
// Desktop reveal-on-scroll. Kept as a plain passive listener to match BackToTop —
|
|
120
|
+
// there is no shared scroll-position hook in the codebase and one consumer doesn't
|
|
121
|
+
// justify inventing one. Only armed when a page actually opts into "after-scroll".
|
|
122
|
+
const [scrolledPastFold, setScrolledPastFold] = useState(false);
|
|
123
|
+
const wantsScrollGate = desktop === "after-scroll";
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
if (!wantsScrollGate) {
|
|
126
|
+
setScrolledPastFold(false);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const onScroll = () => setScrolledPastFold(window.scrollY > DESKTOP_REVEAL_SCROLL_PX);
|
|
130
|
+
onScroll();
|
|
131
|
+
window.addEventListener("scroll", onScroll, { passive: true });
|
|
132
|
+
return () => window.removeEventListener("scroll", onScroll);
|
|
133
|
+
}, [wantsScrollGate]);
|
|
134
|
+
const showOnDesktop = desktop === "always" || (wantsScrollGate && scrolledPastFold);
|
|
89
135
|
const isBulkMode = !!(bulk && bulk.selectedCount > 0);
|
|
136
|
+
const hasInfoPanel = infoPanel != null && !isBulkMode;
|
|
90
137
|
const bulkActions = bulk?.actions ?? [];
|
|
91
138
|
const pageActions = actions;
|
|
92
139
|
const isVisible = (isBulkMode ? bulkActions.length > 0 || !!bulk : pageActions.length > 0) ||
|
|
93
140
|
!!infoLabel ||
|
|
94
141
|
!!secondaryLabel;
|
|
142
|
+
// A panel left open after its content is withdrawn (bulk mode taking over, or
|
|
143
|
+
// the page clearing it) would sit there as an empty sheet.
|
|
144
|
+
useEffect(() => {
|
|
145
|
+
if (!hasInfoPanel)
|
|
146
|
+
setInfoOpen(false);
|
|
147
|
+
}, [hasInfoPanel]);
|
|
95
148
|
// Keep selectedActionId in sync with available bulk actions
|
|
96
149
|
useEffect(() => {
|
|
97
150
|
if (!isBulkMode) {
|
|
@@ -120,7 +173,10 @@ export default function BottomActions() {
|
|
|
120
173
|
setPickerOpen(false);
|
|
121
174
|
};
|
|
122
175
|
return (_jsxs(Div, { ref: containerRef, role: "toolbar", "aria-label": isBulkMode ? "Bulk actions" : "Page actions", "aria-hidden": !isVisible, className: [
|
|
123
|
-
"fixed bottom-[calc(var(--keyboard-inset-height,0px)+var(--bottom-nav-height,4rem))] left-0 right-0
|
|
176
|
+
"fixed bottom-[calc(var(--keyboard-inset-height,0px)+var(--bottom-nav-height,4rem))] left-0 right-0",
|
|
177
|
+
// `--bottom-nav-height` resolves to 0px above lg, so the same offset lands the
|
|
178
|
+
// bar on the viewport bottom on desktop with no extra positioning.
|
|
179
|
+
showOnDesktop ? "" : "lg:hidden",
|
|
124
180
|
Z_BOTTOM_ACTIONS,
|
|
125
181
|
BOTTOM_NAV_BG,
|
|
126
182
|
"shadow-[0_-4px_20px_rgba(0,0,0,0.08)] dark:shadow-[0_-4px_20px_rgba(0,0,0,0.30)]",
|
|
@@ -128,7 +184,7 @@ export default function BottomActions() {
|
|
|
128
184
|
isVisible ? "translate-y-0" : "translate-y-full pointer-events-none",
|
|
129
185
|
]
|
|
130
186
|
.filter(Boolean)
|
|
131
|
-
.join(" "), "data-section": "bottomactions-div-401", children: [isBulkMode && (_jsx(BulkPickerPanel, { bulkActions: bulkActions, selectedActionId: selectedActionId, pickerOpen: pickerOpen, onSelect: (id) => { setSelectedActionId(id); setPickerOpen(false); } })), 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 && (_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 }) })), _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-100 active:bg-primary-200 dark:bg-primary-950/30 dark:hover:bg-primary-900/50 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
|
|
187
|
+
.join(" "), "data-section": "bottomactions-div-401", children: [isBulkMode && (_jsx(BulkPickerPanel, { bulkActions: bulkActions, selectedActionId: selectedActionId, pickerOpen: pickerOpen, onSelect: (id) => { setSelectedActionId(id); setPickerOpen(false); } })), hasInfoPanel && !isBulkMode && (_jsx(InfoPanel, { id: infoPanelId, open: infoOpen, children: infoPanel })), 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-100 active:bg-primary-200 dark:bg-primary-950/30 dark:hover:bg-primary-900/50 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
|
|
132
188
|
? `${bulk.selectedCount} ${bulk.noun}`
|
|
133
189
|
: `${bulk.selectedCount} selected` })] }), bulkActions.length > 0 && (_jsxs(Button, { type: "button", variant: "ghost", onClick: () => setPickerOpen((o) => !o), "aria-haspopup": "listbox", "aria-expanded": pickerOpen, className: [
|
|
134
190
|
"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",
|
|
@@ -69,9 +69,21 @@ export interface BottomBulkConfig {
|
|
|
69
69
|
/** Bulk action buttons, rendered right of the selection pill. */
|
|
70
70
|
actions: BottomAction[];
|
|
71
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Whether the bar is allowed to show on desktop (>= lg), where it has always been
|
|
74
|
+
* `lg:hidden`.
|
|
75
|
+
*
|
|
76
|
+
* - `"hidden"` (default) — mobile/tablet only, the historical behaviour.
|
|
77
|
+
* - `"after-scroll"` — appears once the user has scrolled past the top of the page,
|
|
78
|
+
* so a detail page's primary CTA comes back after the hero scrolls away.
|
|
79
|
+
* - `"always"` — pinned on desktop from first paint.
|
|
80
|
+
*/
|
|
81
|
+
export type BottomActionsDesktopMode = "hidden" | "after-scroll" | "always";
|
|
72
82
|
export interface BottomActionsState {
|
|
73
83
|
/** Page-level primary actions. */
|
|
74
84
|
actions: BottomAction[];
|
|
85
|
+
/** Desktop visibility policy. Defaults to "hidden" — opt in per page. */
|
|
86
|
+
desktop?: BottomActionsDesktopMode;
|
|
75
87
|
/** Bulk-selection config. Bulk mode activates when selectedCount > 0. */
|
|
76
88
|
bulk?: BottomBulkConfig;
|
|
77
89
|
/**
|
|
@@ -87,6 +99,16 @@ export interface BottomActionsState {
|
|
|
87
99
|
* the auction detail page's live countdown ("Ends in 2d 5h").
|
|
88
100
|
*/
|
|
89
101
|
secondaryLabel?: string;
|
|
102
|
+
/**
|
|
103
|
+
* Optional expandable content revealed ABOVE the bar when the buyer taps the
|
|
104
|
+
* `infoLabel` row, which becomes a disclosure toggle while this is set.
|
|
105
|
+
*
|
|
106
|
+
* Exists so a summarised figure in `infoLabel` ("Total: ₹777.00") can be
|
|
107
|
+
* opened up into the detail behind it without navigating away — the mobile
|
|
108
|
+
* counterpart of a "Show details" expander in a desktop sidebar. Ignored in
|
|
109
|
+
* bulk mode, which owns the same space for its action picker.
|
|
110
|
+
*/
|
|
111
|
+
infoPanel?: React.ReactNode;
|
|
90
112
|
}
|
|
91
113
|
interface BottomActionsContextValue {
|
|
92
114
|
state: BottomActionsState;
|
|
@@ -107,6 +129,10 @@ interface BottomActionsContextValue {
|
|
|
107
129
|
setInfoLabel: (label: string | undefined) => void;
|
|
108
130
|
/** Set or clear the secondary label (rendered above infoLabel). */
|
|
109
131
|
setSecondaryLabel: (label: string | undefined) => void;
|
|
132
|
+
/** Set or clear the expandable panel revealed above the bar. */
|
|
133
|
+
setInfoPanel: (panel: React.ReactNode | undefined) => void;
|
|
134
|
+
/** Set the desktop visibility policy. */
|
|
135
|
+
setDesktopMode: (mode: BottomActionsDesktopMode | undefined) => void;
|
|
110
136
|
/** Clear all state (called on feature unmount). */
|
|
111
137
|
clearAll: () => void;
|
|
112
138
|
}
|
|
@@ -87,6 +87,12 @@ export function BottomActionsProvider({ children, }) {
|
|
|
87
87
|
const setSecondaryLabel = useCallback((secondaryLabel) => {
|
|
88
88
|
setState((prev) => ({ ...prev, secondaryLabel }));
|
|
89
89
|
}, []);
|
|
90
|
+
const setInfoPanel = useCallback((infoPanel) => {
|
|
91
|
+
setState((prev) => ({ ...prev, infoPanel }));
|
|
92
|
+
}, []);
|
|
93
|
+
const setDesktopMode = useCallback((desktop) => {
|
|
94
|
+
setState((prev) => (prev.desktop === desktop ? prev : { ...prev, desktop }));
|
|
95
|
+
}, []);
|
|
90
96
|
const clearAll = useCallback(() => {
|
|
91
97
|
actionCallbacksRef.current = new Map();
|
|
92
98
|
bulkCallbacksRef.current = new Map();
|
|
@@ -102,8 +108,19 @@ export function BottomActionsProvider({ children, }) {
|
|
|
102
108
|
bulkClearRef,
|
|
103
109
|
setInfoLabel,
|
|
104
110
|
setSecondaryLabel,
|
|
111
|
+
setInfoPanel,
|
|
112
|
+
setDesktopMode,
|
|
113
|
+
clearAll,
|
|
114
|
+
}), [
|
|
115
|
+
state,
|
|
116
|
+
setActions,
|
|
117
|
+
setBulkConfig,
|
|
118
|
+
setInfoLabel,
|
|
119
|
+
setSecondaryLabel,
|
|
120
|
+
setInfoPanel,
|
|
121
|
+
setDesktopMode,
|
|
105
122
|
clearAll,
|
|
106
|
-
|
|
123
|
+
]);
|
|
107
124
|
return (_jsx(BottomActionsContext.Provider, { value: value, children: children }));
|
|
108
125
|
}
|
|
109
126
|
// --- Hook ---------------------------------------------------------------------
|
|
@@ -1,9 +1,20 @@
|
|
|
1
|
-
import
|
|
1
|
+
import type React from "react";
|
|
2
|
+
import { type BottomAction, type BottomActionsDesktopMode, type BottomBulkConfig } from "../BottomActionsContext";
|
|
2
3
|
export interface UseBottomActionsOptions {
|
|
3
4
|
actions?: BottomAction[];
|
|
4
5
|
bulk?: BottomBulkConfig;
|
|
5
6
|
infoLabel?: string;
|
|
6
7
|
/** Rendered as its own row above infoLabel. See BottomActionsState.secondaryLabel. */
|
|
7
8
|
secondaryLabel?: string;
|
|
9
|
+
/**
|
|
10
|
+
* Expandable detail revealed above the bar when the infoLabel row is tapped.
|
|
11
|
+
* See BottomActionsState.infoPanel.
|
|
12
|
+
*/
|
|
13
|
+
infoPanel?: React.ReactNode;
|
|
14
|
+
/**
|
|
15
|
+
* Whether the bar may also show on desktop. Defaults to "hidden" (mobile/tablet only),
|
|
16
|
+
* so pages that don't opt in behave exactly as before.
|
|
17
|
+
*/
|
|
18
|
+
desktop?: BottomActionsDesktopMode;
|
|
8
19
|
}
|
|
9
20
|
export declare function useBottomActions(options?: UseBottomActionsOptions): void;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { useEffect, useRef } from "react";
|
|
3
3
|
import { useBottomActionsContext, } from "../BottomActionsContext";
|
|
4
4
|
export function useBottomActions(options = {}) {
|
|
5
|
-
const { setActions, setBulkConfig, setInfoLabel, setSecondaryLabel, clearAll } = useBottomActionsContext();
|
|
5
|
+
const { setActions, setBulkConfig, setInfoLabel, setSecondaryLabel, setInfoPanel, setDesktopMode, clearAll, } = useBottomActionsContext();
|
|
6
6
|
const actionsRef = useRef(options.actions ?? []);
|
|
7
7
|
actionsRef.current = options.actions ?? [];
|
|
8
8
|
const bulkRef = useRef(options.bulk);
|
|
@@ -25,6 +25,19 @@ export function useBottomActions(options = {}) {
|
|
|
25
25
|
useEffect(() => {
|
|
26
26
|
setSecondaryLabel(options.secondaryLabel);
|
|
27
27
|
}, [setSecondaryLabel, options.secondaryLabel]);
|
|
28
|
+
// A ReactNode is a fresh object on every render, so it can't be a dependency
|
|
29
|
+
// without looping. Mirror it through a ref and re-publish whenever anything
|
|
30
|
+
// the panel actually derives from changes — callers pass `infoLabel`, which
|
|
31
|
+
// moves with the same data the panel shows.
|
|
32
|
+
const infoPanelRef = useRef(options.infoPanel);
|
|
33
|
+
infoPanelRef.current = options.infoPanel;
|
|
34
|
+
const hasInfoPanel = options.infoPanel != null;
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
setInfoPanel(infoPanelRef.current);
|
|
37
|
+
}, [setInfoPanel, hasInfoPanel, options.infoLabel, options.secondaryLabel]);
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
setDesktopMode(options.desktop);
|
|
40
|
+
}, [setDesktopMode, options.desktop]);
|
|
28
41
|
useEffect(() => {
|
|
29
42
|
return () => clearAll();
|
|
30
43
|
}, [clearAll]);
|
|
@@ -27,7 +27,7 @@ export type { LayoutClientProps, LayoutProvider } from "./LayoutClient";
|
|
|
27
27
|
export { AppLayoutShell } from "./AppLayoutShell";
|
|
28
28
|
export type { AppLayoutShellProps, AppLayoutShellSidebarLink, AppLayoutShellSidebarSection } from "./AppLayoutShell";
|
|
29
29
|
export { BottomActionsProvider, useBottomActionsContext, } from "./BottomActionsContext";
|
|
30
|
-
export type { BottomAction, BottomBulkConfig, BottomActionsState, } from "./BottomActionsContext";
|
|
30
|
+
export type { BottomAction, BottomBulkConfig, BottomActionsState, BottomActionsDesktopMode, } from "./BottomActionsContext";
|
|
31
31
|
export { useBottomActions } from "./hooks/useBottomActions";
|
|
32
32
|
export type { UseBottomActionsOptions } from "./hooks/useBottomActions";
|
|
33
33
|
export { default as BottomActions } from "./BottomActions";
|
|
@@ -19,10 +19,11 @@ import { CustomSectionTabContent } from "../../products/components/CustomSection
|
|
|
19
19
|
import { ShareButton } from "../../products/components/ShareButton";
|
|
20
20
|
import { HistoryTracker } from "../../history/components/HistoryTracker";
|
|
21
21
|
import { RelatedItemsSection } from "../../products/components/RelatedItemsSection";
|
|
22
|
-
import { computeRelatedItems,
|
|
22
|
+
import { computeRelatedItems, getReviewPageForProduct } from "../../../_internal/server/features/products/data";
|
|
23
23
|
import { GroupedListingsCarousel } from "../../grouped/components/GroupedListingsCarousel";
|
|
24
24
|
import { getGroupsWithItemsForProduct } from "../../../_internal/server/features/grouped/data";
|
|
25
|
-
import {
|
|
25
|
+
import { ReviewsListingPanel } from "../../reviews/components/ReviewsListingPanel";
|
|
26
|
+
import { ListingBottomActions } from "../../products/components/ListingBottomActions";
|
|
26
27
|
function toDescriptionHtml(raw) {
|
|
27
28
|
if (!raw)
|
|
28
29
|
return "";
|
|
@@ -55,10 +56,10 @@ export async function LiveItemDetailPageView({ slug, initialProduct, renderActio
|
|
|
55
56
|
const specs = Array.isArray(p.specifications) ? p.specifications : [];
|
|
56
57
|
const customSections = Array.isArray(p.customSections) ? p.customSections : [];
|
|
57
58
|
const descriptionHtml = toDescriptionHtml(p.description);
|
|
58
|
-
const [{ relatedItems, relatedByBrand, relatedByTags, relatedByStore }, groups,
|
|
59
|
+
const [{ relatedItems, relatedByBrand, relatedByTags, relatedByStore }, groups, initialReviews] = await Promise.all([
|
|
59
60
|
computeRelatedItems(product),
|
|
60
61
|
getGroupsWithItemsForProduct(product.id),
|
|
61
|
-
|
|
62
|
+
getReviewPageForProduct(product.id),
|
|
62
63
|
]);
|
|
63
64
|
return (_jsxs(Main, { children: [_jsx(HistoryTracker, { productId: String(p.id ?? p.slug ?? ""), productType: "product", snapshot: {
|
|
64
65
|
title,
|
|
@@ -66,9 +67,9 @@ export async function LiveItemDetailPageView({ slug, initialProduct, renderActio
|
|
|
66
67
|
price: price ?? undefined,
|
|
67
68
|
storeId: typeof p.storeId === "string" ? p.storeId : undefined,
|
|
68
69
|
storeName: storeName ?? undefined,
|
|
69
|
-
} }), _jsxs(Container, { size: "xl", padding: "y-lg", children: [_jsxs(Row, { className: "mb-4", align: "center", justify: "between", gap: "sm", wrap: true, children: [_jsxs(Nav, { "aria-label": "Breadcrumb", layout: "flex-wrap", gap: "2xs", textSize: "xs", color: "muted", children: [_jsx(Link, { href: String(ROUTES.HOME), className: CLS_BREADCRUMB_LINK, children: "Home" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.LIVE), className: CLS_BREADCRUMB_LINK, children: "Live" }), category && (_jsxs(_Fragment, { children: [_jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category)), className: CLS_BREADCRUMB_LINK, children: categoryName || category })] })), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Span, { className: "truncate max-w-[200px]", color: "muted", children: title })] }), _jsx(ShareButton, { title: title })] }), _jsx(PreOrderDetailView, { renderGallery: () => _jsx(ProductGalleryClient, { images: images, video: productVideo, productName: title }), renderInfo: () => (_jsxs(Stack, { gap: "md", children: [_jsxs(Div, { children: [_jsxs(Row, { gap: "xs", wrap: true, className: "mb-2", children: [_jsx(Span, { size: "xs", weight: "semibold", className: CLS_LIVE_ITEM_BADGE, children: "Live Item" }), meta?.vendorVerified && (_jsx(Span, { color: "success", surface: "success-surface", size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", children: "Verified Seller" })), meta?.cites && (_jsxs(Span, { color: "warning", surface: "warning-surface", size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", children: ["CITES: ", meta.cites] }))] }), _jsx(Heading, { level: 1, className: "leading-snug", smSize: "2xl", color: "primary", size: "xl", weight: "bold", children: title }), meta?.species && (_jsxs(Text, { className: "italic mt-1", color: "muted", size: "sm", children: [meta.species, meta.sex && meta.sex !== "n/a" && ` · ${meta.sex}`, meta.ageMonths !== undefined && ` · ${meta.ageMonths}mo`] }))] }), price !== null && (_jsx(Text, { size: "2xl", weight: "bold", color: "primary", children: formatCurrency(price, currency) })), jurisdictions.length > 0 && (_jsxs(Div, { className: CLS_WARN_BOX, children: [_jsx(Text, { className: CLS_WARN_TITLE, children: "Delivery restrictions" }), _jsxs(Text, { className: CLS_WARN_BODY, children: ["This item can only be shipped to: ", jurisdictions.join(", ")] })] })), transport && (_jsxs(Div, { textSize: "sm", border: "subtle", surface: "muted", padding: "md", rounded: "lg", children: [_jsx(Text, { weight: "medium", children: "Transport" }), _jsxs(Text, { className: "text-muted-foreground", children: ["Method: ", transport.method, transport.handlingFee !== undefined && ` · Handling: ${formatCurrency(transport.handlingFee, "INR")}`, transport.insuranceIncluded && " · Insurance included"] })] })), meta?.careInfo && (_jsxs(Div, { textSize: "sm", border: "subtle", surface: "muted", padding: "md", rounded: "lg", children: [_jsx(Text, { weight: "medium", children: "Care information" }), _jsx(Text, { className: "mt-1 text-muted-foreground", children: meta.careInfo })] })), 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, { border: "subtle", rounded: "xl", surface: "muted", padding: "md", 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" }))] }) }))] })), renderTabs: () => (_jsx(ProductTabsShell, { descriptionContent: descriptionHtml ? (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm sm:prose max-w-none dark:prose-invert", className: "text-[var(--appkit-color-text-muted)]" })) : undefined, specsContent: specs.length > 0 ? (_jsx(Dl, { divide: "subtle", rounded: "xl", border: "subtle", className: "overflow-hidden", children: specs.map((s, i) => (_jsxs(Row, { gap: "md", oddEven: "zebra", surface: "default", padding: "inline", children: [_jsx(Dt, { className: "w-36 flex-shrink-0", color: "primary", weight: "medium", children: s.name }), _jsxs(Dd, { className: "flex-1", color: "muted", children: [s.value, s.unit ? ` ${s.unit}` : ""] })] }, i))) })) : undefined, reviewsContent: _jsx(
|
|
70
|
+
} }), _jsxs(Container, { size: "xl", padding: "y-lg", children: [_jsxs(Row, { className: "mb-4", align: "center", justify: "between", gap: "sm", wrap: true, children: [_jsxs(Nav, { "aria-label": "Breadcrumb", layout: "flex-wrap", gap: "2xs", textSize: "xs", color: "muted", children: [_jsx(Link, { href: String(ROUTES.HOME), className: CLS_BREADCRUMB_LINK, children: "Home" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.LIVE), className: CLS_BREADCRUMB_LINK, children: "Live" }), category && (_jsxs(_Fragment, { children: [_jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category)), className: CLS_BREADCRUMB_LINK, children: categoryName || category })] })), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Span, { className: "truncate max-w-[200px]", color: "muted", children: title })] }), _jsx(ShareButton, { title: title })] }), _jsx(PreOrderDetailView, { renderGallery: () => _jsx(ProductGalleryClient, { images: images, video: productVideo, productName: title }), renderInfo: () => (_jsxs(Stack, { gap: "md", children: [_jsxs(Div, { children: [_jsxs(Row, { gap: "xs", wrap: true, className: "mb-2", children: [_jsx(Span, { size: "xs", weight: "semibold", className: CLS_LIVE_ITEM_BADGE, children: "Live Item" }), meta?.vendorVerified && (_jsx(Span, { color: "success", surface: "success-surface", size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", children: "Verified Seller" })), meta?.cites && (_jsxs(Span, { color: "warning", surface: "warning-surface", size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", children: ["CITES: ", meta.cites] }))] }), _jsx(Heading, { level: 1, className: "leading-snug", smSize: "2xl", color: "primary", size: "xl", weight: "bold", children: title }), meta?.species && (_jsxs(Text, { className: "italic mt-1", color: "muted", size: "sm", children: [meta.species, meta.sex && meta.sex !== "n/a" && ` · ${meta.sex}`, meta.ageMonths !== undefined && ` · ${meta.ageMonths}mo`] }))] }), price !== null && (_jsx(Text, { size: "2xl", weight: "bold", color: "primary", children: formatCurrency(price, currency) })), jurisdictions.length > 0 && (_jsxs(Div, { className: CLS_WARN_BOX, children: [_jsx(Text, { className: CLS_WARN_TITLE, children: "Delivery restrictions" }), _jsxs(Text, { className: CLS_WARN_BODY, children: ["This item can only be shipped to: ", jurisdictions.join(", ")] })] })), transport && (_jsxs(Div, { textSize: "sm", border: "subtle", surface: "muted", padding: "md", rounded: "lg", children: [_jsx(Text, { weight: "medium", children: "Transport" }), _jsxs(Text, { className: "text-muted-foreground", children: ["Method: ", transport.method, transport.handlingFee !== undefined && ` · Handling: ${formatCurrency(transport.handlingFee, "INR")}`, transport.insuranceIncluded && " · Insurance included"] })] })), meta?.careInfo && (_jsxs(Div, { textSize: "sm", border: "subtle", surface: "muted", padding: "md", rounded: "lg", children: [_jsx(Text, { weight: "medium", children: "Care information" }), _jsx(Text, { className: "mt-1 text-muted-foreground", children: meta.careInfo })] })), 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, { border: "subtle", rounded: "xl", surface: "muted", padding: "md", 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" }))] }) }))] })), renderTabs: () => (_jsx(ProductTabsShell, { descriptionContent: descriptionHtml ? (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm sm:prose max-w-none dark:prose-invert", className: "text-[var(--appkit-color-text-muted)]" })) : undefined, specsContent: specs.length > 0 ? (_jsx(Dl, { divide: "subtle", rounded: "xl", border: "subtle", className: "overflow-hidden", children: specs.map((s, i) => (_jsxs(Row, { gap: "md", oddEven: "zebra", surface: "default", padding: "inline", children: [_jsx(Dt, { className: "w-36 flex-shrink-0", color: "primary", weight: "medium", children: s.name }), _jsxs(Dd, { className: "flex-1", color: "muted", children: [s.value, s.unit ? ` ${s.unit}` : ""] })] }, i))) })) : undefined, reviewsContent: _jsx(ReviewsListingPanel, { source: { kind: "product", productId: product.id }, stateMode: "local", context: "listing", initialData: initialReviews, emptyLabel: "No reviews yet \u2014 be the first to review this product." }), customTabs: customSections.map((s) => ({
|
|
70
71
|
id: s.id,
|
|
71
72
|
label: s.title,
|
|
72
73
|
content: _jsx(CustomSectionTabContent, { section: s }),
|
|
73
|
-
})) })), renderBuyBar: () => renderActions?.(product), renderRelated: () => (_jsxs(Stack, { gap: "xl", children: [_jsx(GroupedListingsCarousel, { groups: groups }), _jsx(RelatedItemsSection, { relatedItems: relatedItems, relatedByBrand: relatedByBrand, relatedByTags: relatedByTags, relatedByStore: relatedByStore, categoryLabel: categoryName || category || undefined, brandLabel: brand, storeLabel: storeName ?? undefined })] })) })] })] }));
|
|
74
|
+
})) })), renderBuyBar: () => _jsx(Div, { id: "live-item-buy-bar", children: renderActions?.(product) }), renderRelated: () => (_jsxs(Stack, { gap: "xl", children: [_jsx(GroupedListingsCarousel, { groups: groups }), _jsx(RelatedItemsSection, { relatedItems: relatedItems, relatedByBrand: relatedByBrand, relatedByTags: relatedByTags, relatedByStore: relatedByStore, categoryLabel: categoryName || category || undefined, brandLabel: brand, storeLabel: storeName ?? undefined })] })) }), _jsx(ListingBottomActions, { listingType: "live", anchorId: "live-item-buy-bar", price: price, currency: currency })] })] }));
|
|
74
75
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
/**
|
|
3
|
+
* Icon badges for an order's paid add-ons and applied coupon.
|
|
4
|
+
*
|
|
5
|
+
* These are fulfilment instructions, not decoration: WhatsApp tells the
|
|
6
|
+
* status-change notifier to message this buyer, gift wrap tells the packer to
|
|
7
|
+
* wrap the parcel and enclose the message, shipment protection changes how a
|
|
8
|
+
* loss claim is handled. They are recorded on the order (and filterable in the
|
|
9
|
+
* `orders` Sieve config) so nobody has to keep a separate list of who asked for
|
|
10
|
+
* what — the order IS the record.
|
|
11
|
+
*
|
|
12
|
+
* Rendered on every order surface — buyer detail, seller list + detail, admin
|
|
13
|
+
* list + detail — from one implementation, so a new add-on shows up everywhere
|
|
14
|
+
* at once instead of being wired per screen.
|
|
15
|
+
*/
|
|
16
|
+
export interface OrderAddonBadgesOrder {
|
|
17
|
+
whatsappNotifyAddon?: boolean;
|
|
18
|
+
whatsappNotifyFee?: number;
|
|
19
|
+
giftWrapAddon?: boolean;
|
|
20
|
+
giftWrapFee?: number;
|
|
21
|
+
giftWrapMessage?: string;
|
|
22
|
+
shipmentProtectionAddon?: boolean;
|
|
23
|
+
shipmentProtectionFee?: number;
|
|
24
|
+
couponCode?: string;
|
|
25
|
+
couponDiscount?: number;
|
|
26
|
+
}
|
|
27
|
+
export interface OrderAddonBadgesProps {
|
|
28
|
+
order: OrderAddonBadgesOrder;
|
|
29
|
+
/** `"compact"` — icon chips only, for list rows. `"detail"` — chips + the gift message and fees. */
|
|
30
|
+
variant?: "compact" | "detail";
|
|
31
|
+
/** Hide the coupon chip where the coupon is already shown in a price breakdown. */
|
|
32
|
+
showCoupon?: boolean;
|
|
33
|
+
currency?: string;
|
|
34
|
+
className?: string;
|
|
35
|
+
}
|
|
36
|
+
export declare function OrderAddonBadges({ order, variant, showCoupon, currency, className, }: OrderAddonBadgesProps): React.JSX.Element | null;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Gift, MessageCircle, ShieldCheck, Tag } from "lucide-react";
|
|
4
|
+
import { Row, Span, Stack, Text } from "../../../ui";
|
|
5
|
+
import { formatCurrency } from "../../../utils/number.formatter";
|
|
6
|
+
const CHIP_BASE = "inline-flex items-center gap-[var(--appkit-space-1)] rounded-full px-[var(--appkit-space-2)] py-[var(--appkit-space-0-5)] text-[length:var(--appkit-text-xs)] font-medium";
|
|
7
|
+
function Chip({ icon, label, tone, title, }) {
|
|
8
|
+
const toneCls = tone === "success"
|
|
9
|
+
? "bg-success-surface text-success"
|
|
10
|
+
: tone === "info"
|
|
11
|
+
? "bg-info-surface text-info"
|
|
12
|
+
: "bg-primary-50 text-primary-700 dark:bg-primary-950/40 dark:text-primary-300";
|
|
13
|
+
return (_jsxs(Span, { className: `${CHIP_BASE} ${toneCls}`, title: title, children: [_jsx(Span, { className: "flex-shrink-0", "aria-hidden": "true", children: icon }), label] }));
|
|
14
|
+
}
|
|
15
|
+
export function OrderAddonBadges({ order, variant = "compact", showCoupon = true, currency = "INR", className = "", }) {
|
|
16
|
+
const money = (v) => (typeof v === "number" && v > 0 ? formatCurrency(v, currency) : undefined);
|
|
17
|
+
const hasCoupon = showCoupon && !!order.couponCode;
|
|
18
|
+
const hasAny = order.whatsappNotifyAddon ||
|
|
19
|
+
order.giftWrapAddon ||
|
|
20
|
+
order.shipmentProtectionAddon ||
|
|
21
|
+
hasCoupon;
|
|
22
|
+
if (!hasAny)
|
|
23
|
+
return null;
|
|
24
|
+
const isDetail = variant === "detail";
|
|
25
|
+
return (_jsxs(Stack, { gap: "xs", className: `min-w-0 ${className}`, children: [_jsxs(Row, { gap: "xs", align: "center", wrap: true, className: "min-w-0", children: [order.whatsappNotifyAddon && (_jsx(Chip, { icon: _jsx(MessageCircle, { size: 12 }), label: isDetail && money(order.whatsappNotifyFee) ? `WhatsApp updates · ${money(order.whatsappNotifyFee)}` : "WhatsApp updates", tone: "success", title: "Buyer opted into WhatsApp order-status updates \u2014 status changes on this order should notify them on WhatsApp." })), order.giftWrapAddon && (_jsx(Chip, { icon: _jsx(Gift, { size: 12 }), label: isDetail && money(order.giftWrapFee) ? `Gift wrap · ${money(order.giftWrapFee)}` : "Gift wrap", tone: "brand", title: "This parcel must be gift-wrapped before dispatch." })), order.shipmentProtectionAddon && (_jsx(Chip, { icon: _jsx(ShieldCheck, { size: 12 }), label: isDetail && money(order.shipmentProtectionFee) ? `Protected · ${money(order.shipmentProtectionFee)}` : "Protected", tone: "info", title: "Shipment protection purchased \u2014 loss or damage in transit is covered." })), hasCoupon && (_jsx(Chip, { icon: _jsx(Tag, { size: 12 }), label: isDetail && money(order.couponDiscount)
|
|
26
|
+
? `${order.couponCode} · −${money(order.couponDiscount)}`
|
|
27
|
+
: order.couponCode, tone: "success", title: `Coupon ${order.couponCode} applied to this order.` }))] }), isDetail && order.giftWrapAddon && order.giftWrapMessage && (_jsxs(Stack, { gap: "xs", className: "min-w-0", children: [_jsx(Text, { size: "xs", color: "muted", weight: "semibold", transform: "uppercase", className: "tracking-wide", children: "Gift message" }), _jsx(Text, { size: "sm", color: "primary", className: "min-w-0 whitespace-pre-wrap break-words", children: order.giftWrapMessage })] }))] }));
|
|
28
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { OrderCard, OrdersList } from "./OrdersList";
|
|
2
|
+
export { OrderAddonBadges, type OrderAddonBadgesOrder, type OrderAddonBadgesProps, } from "./OrderAddonBadges";
|
|
2
3
|
export { MarketplaceOrderCard, type MarketplaceOrderCardLabels, type MarketplaceOrderCardLinks, type MarketplaceOrderCardOrder, type MarketplaceOrderCardProps, } from "./MarketplaceOrderCard";
|
|
3
4
|
export { OrderFilters, ORDER_FILTER_KEYS, ORDER_ADMIN_SORT_OPTIONS, ORDER_SELLER_SORT_OPTIONS, ORDER_USER_SORT_OPTIONS, getOrderFilterKeys, getOrderSortOptions, } from "./OrderFilters";
|
|
4
5
|
export type { OrderFiltersProps, OrderFilterVariant } from "./OrderFilters";
|