@mohasinac/appkit 4.11.1 → 4.11.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_internal/server/features/checkout/actions.d.ts +4 -0
- package/dist/_internal/server/features/checkout/actions.js +138 -27
- package/dist/_internal/server/features/checkout/data.d.ts +10 -0
- package/dist/_internal/server/features/checkout/locked-lines.d.ts +30 -0
- package/dist/_internal/server/features/checkout/locked-lines.js +116 -0
- package/dist/_internal/server/features/orders/adapters.js +7 -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 +2 -1
- package/dist/_internal/server/features/products/index.js +2 -1
- package/dist/_internal/server/features/products/list-public.d.ts +130 -0
- package/dist/_internal/server/features/products/list-public.js +375 -0
- 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 +89 -16
- package/dist/_internal/server/jobs/core/offerExpiry.js +116 -4
- 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/server/jobs/handlers/messages.d.ts +5 -0
- package/dist/_internal/server/jobs/handlers/messages.js +5 -0
- package/dist/_internal/shared/checkout/lanes.d.ts +41 -0
- package/dist/_internal/shared/checkout/lanes.js +106 -0
- package/dist/_internal/shared/checkout/order-math.d.ts +19 -0
- package/dist/_internal/shared/checkout/order-math.js +30 -6
- 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/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/feature-flags.d.ts +1 -0
- package/dist/_internal/shared/listing-types/feature-flags.js +24 -10
- 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 +7 -2
- package/dist/client.js +8 -2
- 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/NotificationBell.js +1 -0
- package/dist/features/account/components/UserOffersPanel.js +8 -1
- package/dist/features/account/components/UserOrdersView.js +27 -5
- package/dist/features/admin/actions/notification-actions.js +1 -1
- package/dist/features/admin/components/AdminAddressesView.js +3 -1
- package/dist/features/admin/components/AdminCouponEditorView.js +1 -4
- package/dist/features/admin/components/AdminPaymentMethodsView.js +3 -1
- package/dist/features/admin/components/AdminProductsView.js +34 -18
- 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/schemas/firestore.d.ts +2 -1
- package/dist/features/admin/schemas/firestore.js +1 -0
- package/dist/features/auctions/actions/bid-actions.d.ts +11 -1
- package/dist/features/auctions/actions/bid-actions.js +29 -15
- package/dist/features/auctions/components/AuctionBidsTable.js +6 -2
- package/dist/features/auctions/components/AuctionBottomActions.js +1 -0
- package/dist/features/auctions/components/AuctionDetailPageView.js +13 -11
- package/dist/features/auctions/components/AuctionsListView.js +12 -53
- package/dist/features/auctions/components/PlaceBidFormClient.js +10 -1
- package/dist/features/auctions/repository/bid.repository.d.ts +15 -0
- package/dist/features/auctions/repository/bid.repository.js +19 -0
- package/dist/features/auctions/schemas/firestore.d.ts +11 -1
- package/dist/features/auctions/schemas/firestore.js +1 -0
- package/dist/features/cart/actions/cart-actions.d.ts +11 -0
- package/dist/features/cart/actions/cart-actions.js +24 -0
- package/dist/features/cart/repository/cart.repository.d.ts +24 -1
- package/dist/features/cart/repository/cart.repository.js +71 -6
- package/dist/features/cart/schemas/firestore.d.ts +23 -4
- 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 +56 -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 +26 -3
- package/dist/features/layout/BottomActionsContext.d.ts +14 -0
- package/dist/features/layout/BottomActionsContext.js +13 -1
- package/dist/features/layout/hooks/useBottomActions.d.ts +6 -1
- package/dist/features/layout/hooks/useBottomActions.js +4 -1
- package/dist/features/layout/index.d.ts +1 -1
- package/dist/features/live/components/LiveItemDetailPageView.js +7 -6
- package/dist/features/orders/repository/orders.repository.d.ts +4 -15
- package/dist/features/orders/repository/orders.repository.js +4 -27
- package/dist/features/orders/types/index.d.ts +16 -0
- package/dist/features/orders/utils/order-splitter.js +14 -2
- package/dist/features/pre-orders/components/PreOrderBottomActions.d.ts +1 -1
- package/dist/features/pre-orders/components/PreOrderBottomActions.js +3 -19
- package/dist/features/pre-orders/components/PreOrdersListView.js +11 -39
- package/dist/features/products/components/ArtStickersListView.js +10 -49
- 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/ProductDetailActions.js +4 -0
- package/dist/features/products/components/ProductDetailPageView.js +7 -7
- package/dist/features/products/components/ProductsIndexPageView.js +9 -62
- 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/repository/products.repository.d.ts +45 -1
- package/dist/features/products/repository/products.repository.js +67 -8
- 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/actions/offer-actions.d.ts +6 -0
- package/dist/features/seller/actions/offer-actions.js +8 -9
- package/dist/features/seller/components/SellerOffersView.d.ts +6 -1
- package/dist/features/seller/components/SellerOffersView.js +59 -8
- package/dist/features/seller/components/SellerProductsCards.js +2 -2
- package/dist/features/seller/components/SellerProductsView.js +21 -18
- package/dist/features/seller/repository/offer.repository.d.ts +18 -2
- package/dist/features/seller/repository/offer.repository.js +38 -2
- package/dist/features/seller/schemas/firestore.d.ts +7 -2
- package/dist/features/seller/schemas/firestore.js +3 -0
- package/dist/features/seller/schemas/offer-forms.d.ts +12 -0
- package/dist/features/seller/schemas/offer-forms.js +28 -0
- package/dist/features/stores/actions/store-query-actions.js +1 -1
- package/dist/features/stores/components/StoreAuctionsListing.js +5 -11
- package/dist/features/stores/components/StorePreOrdersListing.js +6 -11
- 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 +337 -1
- package/dist/index.d.ts +11 -4
- package/dist/index.js +15 -3
- 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/server-entry.d.ts +1 -0
- package/dist/server-entry.js +1 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.js +3 -0
- package/dist/styles.css +44 -39
- package/dist/tailwind-utilities.css +1 -1
- 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
|
@@ -8,16 +8,36 @@ import { PreOrdersIndexListing } from "../../pre-orders/components/PreOrdersInde
|
|
|
8
8
|
import { PrizeDrawsIndexListing } from "../../products/components/PrizeDrawsIndexListing";
|
|
9
9
|
import { CategoryBundlesListing } from "./CategoryBundlesListing";
|
|
10
10
|
import { CategoryStoresListing } from "./CategoryStoresListing";
|
|
11
|
-
import { CATEGORY_PAGE_TABS } from "../../products/constants/listing-tabs";
|
|
12
|
-
|
|
11
|
+
import { CATEGORY_PAGE_TABS, ART_STICKERS_LISTING_TYPES, } from "../../products/constants/listing-tabs";
|
|
12
|
+
import { ALL_LISTING_TYPES } from "../../../_internal/shared/listing-types/feature-flags";
|
|
13
|
+
import { pluginFor } from "../../../_internal/shared/listing-types/_registry";
|
|
14
|
+
/**
|
|
15
|
+
* Maps CATEGORY_PAGE_TABS id (a `tabSlug`) → listing type / category type key
|
|
16
|
+
* for feature-flag filtering.
|
|
17
|
+
*
|
|
18
|
+
* The listing-type half is DERIVED (2026-08-21). It used to be hand-written
|
|
19
|
+
* and covered only 6 of the 10 tabs — `classifieds`, `digital-codes`, `live`
|
|
20
|
+
* and `art` had no entry, so they skipped the flag check entirely AND (worse)
|
|
21
|
+
* had no render branch below, meaning all four rendered a tab you could click
|
|
22
|
+
* that showed an empty page.
|
|
23
|
+
*/
|
|
13
24
|
const TAB_TYPE_MAP = {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
25
|
+
...Object.fromEntries(ALL_LISTING_TYPES.map((type) => [
|
|
26
|
+
pluginFor(type).tabSlug,
|
|
27
|
+
{ kind: "listing", type },
|
|
28
|
+
])),
|
|
18
29
|
bundles: { kind: "category", type: "bundle" },
|
|
19
30
|
stores: { kind: "entity", type: "stores" },
|
|
20
31
|
};
|
|
32
|
+
/** Listing types rendered by the shared products listing rather than a dedicated one. */
|
|
33
|
+
const GENERIC_TAB_LISTING_TYPES = {
|
|
34
|
+
[pluginFor("standard").tabSlug]: ["standard"],
|
|
35
|
+
[pluginFor("classified").tabSlug]: ["classified"],
|
|
36
|
+
[pluginFor("digital-code").tabSlug]: ["digital-code"],
|
|
37
|
+
[pluginFor("live").tabSlug]: ["live"],
|
|
38
|
+
// The combined Art & Stickers tab spans both types under the `art` slug.
|
|
39
|
+
[pluginFor("art").tabSlug]: ART_STICKERS_LISTING_TYPES,
|
|
40
|
+
};
|
|
21
41
|
export function CategoryDetailTabs({ categorySlug, categoryId, initialProductsData, initialBundles = [], initialStores = [], counts, enabledListingTypes, enabledCategoryTypes, }) {
|
|
22
42
|
const countFor = (id) => {
|
|
23
43
|
switch (id) {
|
|
@@ -51,5 +71,5 @@ export function CategoryDetailTabs({ categorySlug, categoryId, initialProductsDa
|
|
|
51
71
|
});
|
|
52
72
|
const firstTabId = (visibleTabs[0]?.id ?? "products");
|
|
53
73
|
const [activeTab, setActiveTab] = useState(firstTabId);
|
|
54
|
-
return (_jsxs(_Fragment, { children: [_jsx(Tabs, { value: activeTab, onChange: (v) => setActiveTab(v), className: "mb-6", children: _jsx(TabsList, { children: visibleTabs.map((t) => (_jsx(TabsTrigger, { value: t.id, badge: countFor(t.id), children: t.label }, t.id))) }) }), activeTab
|
|
74
|
+
return (_jsxs(_Fragment, { children: [_jsx(Tabs, { value: activeTab, onChange: (v) => setActiveTab(v), className: "mb-6", children: _jsx(TabsList, { children: visibleTabs.map((t) => (_jsx(TabsTrigger, { value: t.id, badge: countFor(t.id), children: t.label }, t.id))) }) }), GENERIC_TAB_LISTING_TYPES[activeTab] && (_jsx(CategoryProductsListing, { categorySlug: categorySlug, categoryId: categoryId, listingTypes: GENERIC_TAB_LISTING_TYPES[activeTab], initialData: activeTab === pluginFor("standard").tabSlug ? initialProductsData : undefined })), activeTab === pluginFor("auction").tabSlug && (_jsx(AuctionsIndexListing, { categorySlug: categorySlug })), activeTab === pluginFor("pre-order").tabSlug && (_jsx(PreOrdersIndexListing, { categorySlug: categorySlug })), activeTab === pluginFor("prize-draw").tabSlug && (_jsx(PrizeDrawsIndexListing, { categorySlug: categorySlug })), activeTab === "bundles" && (_jsx(CategoryBundlesListing, { initialBundles: initialBundles })), activeTab === "stores" && (_jsx(CategoryStoresListing, { stores: initialStores }))] }));
|
|
55
75
|
}
|
|
@@ -4,6 +4,14 @@ export interface CategoryProductsListingProps {
|
|
|
4
4
|
categoryId?: string;
|
|
5
5
|
/** Filter products by brand name (for brand detail pages) */
|
|
6
6
|
brandName?: string;
|
|
7
|
+
/**
|
|
8
|
+
* Listing types this tab spans. Defaults to `["standard"]` — the original,
|
|
9
|
+
* only behaviour. Category/brand pages pass a single non-standard type (or
|
|
10
|
+
* the `["art","stickers"]` pair) so the classifieds / digital-codes / live /
|
|
11
|
+
* art tabs render real content instead of a blank panel: those four tabs
|
|
12
|
+
* existed in CATEGORY_PAGE_TABS but had no render branch at all.
|
|
13
|
+
*/
|
|
14
|
+
listingTypes?: readonly string[];
|
|
7
15
|
initialData?: any;
|
|
8
16
|
}
|
|
9
|
-
export declare function CategoryProductsListing({ categorySlug, categoryId, brandName, initialData, }: CategoryProductsListingProps): React.JSX.Element;
|
|
17
|
+
export declare function CategoryProductsListing({ categorySlug, categoryId, brandName, listingTypes, initialData, }: CategoryProductsListingProps): React.JSX.Element;
|
|
@@ -27,7 +27,7 @@ function renderCategoryProductGrid(props) {
|
|
|
27
27
|
}
|
|
28
28
|
return (_jsx(ProductGrid, { products: products, getProductHref: getProductHref, view: view, emptyLabel: "No products found in this category.", onWishlistToggle: onWishlistToggle, wishlistedIds: wishlistedIds, onAddToCart: onAddToCart }));
|
|
29
29
|
}
|
|
30
|
-
export function CategoryProductsListing({ categorySlug, categoryId, brandName, initialData, }) {
|
|
30
|
+
export function CategoryProductsListing({ categorySlug, categoryId, brandName, listingTypes = ["standard"], initialData, }) {
|
|
31
31
|
const table = useUrlTable({ defaults: { pageSize: "24", sort: "-createdAt" } });
|
|
32
32
|
const { showToast } = useToast();
|
|
33
33
|
const { requireAuth, modalOpen, modalMessage, closeModal } = useAuthGate();
|
|
@@ -48,7 +48,10 @@ export function CategoryProductsListing({ categorySlug, categoryId, brandName, i
|
|
|
48
48
|
sort: table.get("sort") || "-createdAt",
|
|
49
49
|
page: table.getNumber("page", 1),
|
|
50
50
|
perPage: table.getNumber("pageSize", 24),
|
|
51
|
-
|
|
51
|
+
// Pipe-joined OR-group when the tab spans several types (art + stickers);
|
|
52
|
+
// sievejs parses it as a same-field OR and the Firebase adapter upgrades
|
|
53
|
+
// it to a `.where(…, "in", …)` query.
|
|
54
|
+
listingType: listingTypes.join("|"),
|
|
52
55
|
};
|
|
53
56
|
const { products, totalPages, page, isLoading } = useProducts(params, { initialData });
|
|
54
57
|
const commitSearch = useCallback(() => {
|
|
@@ -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
|
}
|
|
@@ -13,6 +13,8 @@ function renderToStaticMarkup(el) {
|
|
|
13
13
|
}
|
|
14
14
|
import { normalizeError } from "../../errors/normalize";
|
|
15
15
|
import { getProviders } from "../../contracts";
|
|
16
|
+
import { createResendProvider } from "../../providers/email-resend/provider";
|
|
17
|
+
import { siteSettingsRepository } from "../admin/repository/site-settings.repository";
|
|
16
18
|
import { serverLogger } from "../../monitoring";
|
|
17
19
|
import { currentYear, formatCurrency, formatDateTime, nowMs, } from "../../utils";
|
|
18
20
|
import { EmailBold, EmailButton, EmailContainer, EmailDivider, EmailDoc, EmailFooter, EmailHeader, EmailLink, EmailRow, } from "../email/primitives";
|
|
@@ -25,9 +27,62 @@ function getSiteUrl() {
|
|
|
25
27
|
function getSupportEmail() {
|
|
26
28
|
return process.env.EMAIL_SUPPORT?.trim() || "support@example.com";
|
|
27
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Resolve an email provider that works in BOTH runtimes.
|
|
32
|
+
*
|
|
33
|
+
* The provider registry is populated by the consumer's `providers.config.ts`,
|
|
34
|
+
* which only runs from the Next.js instrumentation hook. Firebase Functions
|
|
35
|
+
* never call `registerProviders()`, so `getProviders()` throws there —
|
|
36
|
+
* silently failing every Functions-side `sendEmail()` with
|
|
37
|
+
* "Call registerProviders() before getProviders()". That is why the scheduled
|
|
38
|
+
* dailyStatusDigest never delivered: `sendEmail` catches the throw and returns
|
|
39
|
+
* it as `{ error }`, so it surfaced only as a logged "send failed".
|
|
40
|
+
*
|
|
41
|
+
* Falling back to a directly-constructed Resend provider mirrors exactly what
|
|
42
|
+
* `sendNotification()` already does for Functions-side email — Firestore
|
|
43
|
+
* credentials first, env vars second — so both paths resolve identically
|
|
44
|
+
* instead of one runtime silently having no provider at all.
|
|
45
|
+
*/
|
|
46
|
+
async function resolveEmailProvider() {
|
|
47
|
+
try {
|
|
48
|
+
return getProviders().email;
|
|
49
|
+
}
|
|
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);
|
|
55
|
+
const [creds, settings] = await Promise.all([
|
|
56
|
+
siteSettingsRepository.getDecryptedCredentials().catch((err) => {
|
|
57
|
+
void normalizeError(err);
|
|
58
|
+
return {};
|
|
59
|
+
}),
|
|
60
|
+
siteSettingsRepository.getSingleton().catch((err) => {
|
|
61
|
+
void normalizeError(err);
|
|
62
|
+
return null;
|
|
63
|
+
}),
|
|
64
|
+
]);
|
|
65
|
+
// A seeded placeholder ("re_PLACEHOLDER") is truthy but not a real key and
|
|
66
|
+
// must never win over a real env var — same guard as providers.config.ts.
|
|
67
|
+
const seeded = creds.resendApiKey?.trim() ?? "";
|
|
68
|
+
const apiKey = seeded && !seeded.includes("PLACEHOLDER")
|
|
69
|
+
? seeded
|
|
70
|
+
: (process.env.RESEND_API_KEY?.trim() ?? "");
|
|
71
|
+
return createResendProvider({
|
|
72
|
+
apiKey,
|
|
73
|
+
fromEmail: settings?.emailSettings?.fromEmail?.trim() ||
|
|
74
|
+
process.env.EMAIL_FROM?.trim() ||
|
|
75
|
+
"",
|
|
76
|
+
fromName: settings?.emailSettings?.fromName?.trim() ||
|
|
77
|
+
process.env.EMAIL_FROM_NAME?.trim() ||
|
|
78
|
+
"App",
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
28
82
|
export async function sendEmail(opts) {
|
|
29
83
|
try {
|
|
30
|
-
const
|
|
84
|
+
const provider = await resolveEmailProvider();
|
|
85
|
+
const data = await provider.send({
|
|
31
86
|
to: opts.to,
|
|
32
87
|
subject: opts.subject,
|
|
33
88
|
html: typeof opts.html === "string" ? opts.html : "",
|
|
@@ -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
|
*
|
|
@@ -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: [
|
|
@@ -85,7 +89,23 @@ export default function BottomActions() {
|
|
|
85
89
|
const [selectedActionId, setSelectedActionId] = useState(null);
|
|
86
90
|
const containerRef = useRef(null);
|
|
87
91
|
useClickOutside(containerRef, () => setPickerOpen(false));
|
|
88
|
-
const { actions, bulk, infoLabel, secondaryLabel } = state;
|
|
92
|
+
const { actions, bulk, infoLabel, secondaryLabel, desktop } = state;
|
|
93
|
+
// Desktop reveal-on-scroll. Kept as a plain passive listener to match BackToTop —
|
|
94
|
+
// there is no shared scroll-position hook in the codebase and one consumer doesn't
|
|
95
|
+
// justify inventing one. Only armed when a page actually opts into "after-scroll".
|
|
96
|
+
const [scrolledPastFold, setScrolledPastFold] = useState(false);
|
|
97
|
+
const wantsScrollGate = desktop === "after-scroll";
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
if (!wantsScrollGate) {
|
|
100
|
+
setScrolledPastFold(false);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const onScroll = () => setScrolledPastFold(window.scrollY > DESKTOP_REVEAL_SCROLL_PX);
|
|
104
|
+
onScroll();
|
|
105
|
+
window.addEventListener("scroll", onScroll, { passive: true });
|
|
106
|
+
return () => window.removeEventListener("scroll", onScroll);
|
|
107
|
+
}, [wantsScrollGate]);
|
|
108
|
+
const showOnDesktop = desktop === "always" || (wantsScrollGate && scrolledPastFold);
|
|
89
109
|
const isBulkMode = !!(bulk && bulk.selectedCount > 0);
|
|
90
110
|
const bulkActions = bulk?.actions ?? [];
|
|
91
111
|
const pageActions = actions;
|
|
@@ -120,7 +140,10 @@ export default function BottomActions() {
|
|
|
120
140
|
setPickerOpen(false);
|
|
121
141
|
};
|
|
122
142
|
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
|
|
143
|
+
"fixed bottom-[calc(var(--keyboard-inset-height,0px)+var(--bottom-nav-height,4rem))] left-0 right-0",
|
|
144
|
+
// `--bottom-nav-height` resolves to 0px above lg, so the same offset lands the
|
|
145
|
+
// bar on the viewport bottom on desktop with no extra positioning.
|
|
146
|
+
showOnDesktop ? "" : "lg:hidden",
|
|
124
147
|
Z_BOTTOM_ACTIONS,
|
|
125
148
|
BOTTOM_NAV_BG,
|
|
126
149
|
"shadow-[0_-4px_20px_rgba(0,0,0,0.08)] dark:shadow-[0_-4px_20px_rgba(0,0,0,0.30)]",
|
|
@@ -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
|
/**
|
|
@@ -107,6 +119,8 @@ interface BottomActionsContextValue {
|
|
|
107
119
|
setInfoLabel: (label: string | undefined) => void;
|
|
108
120
|
/** Set or clear the secondary label (rendered above infoLabel). */
|
|
109
121
|
setSecondaryLabel: (label: string | undefined) => void;
|
|
122
|
+
/** Set the desktop visibility policy. */
|
|
123
|
+
setDesktopMode: (mode: BottomActionsDesktopMode | undefined) => void;
|
|
110
124
|
/** Clear all state (called on feature unmount). */
|
|
111
125
|
clearAll: () => void;
|
|
112
126
|
}
|
|
@@ -87,6 +87,9 @@ export function BottomActionsProvider({ children, }) {
|
|
|
87
87
|
const setSecondaryLabel = useCallback((secondaryLabel) => {
|
|
88
88
|
setState((prev) => ({ ...prev, secondaryLabel }));
|
|
89
89
|
}, []);
|
|
90
|
+
const setDesktopMode = useCallback((desktop) => {
|
|
91
|
+
setState((prev) => (prev.desktop === desktop ? prev : { ...prev, desktop }));
|
|
92
|
+
}, []);
|
|
90
93
|
const clearAll = useCallback(() => {
|
|
91
94
|
actionCallbacksRef.current = new Map();
|
|
92
95
|
bulkCallbacksRef.current = new Map();
|
|
@@ -102,8 +105,17 @@ export function BottomActionsProvider({ children, }) {
|
|
|
102
105
|
bulkClearRef,
|
|
103
106
|
setInfoLabel,
|
|
104
107
|
setSecondaryLabel,
|
|
108
|
+
setDesktopMode,
|
|
109
|
+
clearAll,
|
|
110
|
+
}), [
|
|
111
|
+
state,
|
|
112
|
+
setActions,
|
|
113
|
+
setBulkConfig,
|
|
114
|
+
setInfoLabel,
|
|
115
|
+
setSecondaryLabel,
|
|
116
|
+
setDesktopMode,
|
|
105
117
|
clearAll,
|
|
106
|
-
|
|
118
|
+
]);
|
|
107
119
|
return (_jsx(BottomActionsContext.Provider, { value: value, children: children }));
|
|
108
120
|
}
|
|
109
121
|
// --- Hook ---------------------------------------------------------------------
|
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
import { type BottomAction, type BottomBulkConfig } from "../BottomActionsContext";
|
|
1
|
+
import { type BottomAction, type BottomActionsDesktopMode, type BottomBulkConfig } from "../BottomActionsContext";
|
|
2
2
|
export interface UseBottomActionsOptions {
|
|
3
3
|
actions?: BottomAction[];
|
|
4
4
|
bulk?: BottomBulkConfig;
|
|
5
5
|
infoLabel?: string;
|
|
6
6
|
/** Rendered as its own row above infoLabel. See BottomActionsState.secondaryLabel. */
|
|
7
7
|
secondaryLabel?: string;
|
|
8
|
+
/**
|
|
9
|
+
* Whether the bar may also show on desktop. Defaults to "hidden" (mobile/tablet only),
|
|
10
|
+
* so pages that don't opt in behave exactly as before.
|
|
11
|
+
*/
|
|
12
|
+
desktop?: BottomActionsDesktopMode;
|
|
8
13
|
}
|
|
9
14
|
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, 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,9 @@ export function useBottomActions(options = {}) {
|
|
|
25
25
|
useEffect(() => {
|
|
26
26
|
setSecondaryLabel(options.secondaryLabel);
|
|
27
27
|
}, [setSecondaryLabel, options.secondaryLabel]);
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
setDesktopMode(options.desktop);
|
|
30
|
+
}, [setDesktopMode, options.desktop]);
|
|
28
31
|
useEffect(() => {
|
|
29
32
|
return () => clearAll();
|
|
30
33
|
}, [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
|
}
|
|
@@ -175,6 +175,10 @@ declare class OrderRepository extends BaseRepository<OrderDocument> {
|
|
|
175
175
|
canFilter: boolean;
|
|
176
176
|
canSort: boolean;
|
|
177
177
|
};
|
|
178
|
+
orderType: {
|
|
179
|
+
canFilter: boolean;
|
|
180
|
+
canSort: boolean;
|
|
181
|
+
};
|
|
178
182
|
};
|
|
179
183
|
listAll(model: SieveModel): Promise<FirebaseSieveResult<OrderDocument>>;
|
|
180
184
|
/**
|
|
@@ -293,21 +297,6 @@ declare class OrderRepository extends BaseRepository<OrderDocument> {
|
|
|
293
297
|
* Cloud Functions: stage a cancellation update into a caller-owned WriteBatch.
|
|
294
298
|
*/
|
|
295
299
|
cancelInBatch(batch: WriteBatch, ref: DocumentReference): void;
|
|
296
|
-
/**
|
|
297
|
-
* Cloud Functions: create an auction-won order inside a caller-owned WriteBatch.
|
|
298
|
-
* Returns the new DocumentReference synchronously so the caller can chain further batch ops.
|
|
299
|
-
*/
|
|
300
|
-
createFromAuction(batch: WriteBatch, input: {
|
|
301
|
-
productId: string;
|
|
302
|
-
productTitle: string;
|
|
303
|
-
userId: string;
|
|
304
|
-
userName: string;
|
|
305
|
-
userEmail: string;
|
|
306
|
-
storeId?: string;
|
|
307
|
-
amount: number;
|
|
308
|
-
currency: string;
|
|
309
|
-
auctionProductId: string;
|
|
310
|
-
}): DocumentReference;
|
|
311
300
|
}
|
|
312
301
|
declare const orderRepository: OrderRepository;
|
|
313
302
|
export { OrderRepository, orderRepository };
|