@mohasinac/appkit 4.53.1 → 4.54.0-next.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/.build-stamp.json +5 -5
- package/dist/_internal/client/theme/ThemeProvider.js +15 -1
- package/dist/_internal/server/features/auth/capabilities.js +7 -5
- package/dist/_internal/server/features/categories/price-guidance-resolve.d.ts +66 -0
- package/dist/_internal/server/features/categories/price-guidance-resolve.js +138 -0
- package/dist/_internal/server/features/checkout/supplier-identity.js +28 -6
- package/dist/_internal/server/features/price-index/market.d.ts +57 -0
- package/dist/_internal/server/features/price-index/market.js +120 -0
- package/dist/_internal/server/features/products/list-public.d.ts +12 -0
- package/dist/_internal/server/features/products/list-public.js +10 -1
- package/dist/_internal/server/features/products/listing-tab-counts.d.ts +25 -1
- package/dist/_internal/server/features/products/listing-tab-counts.js +184 -10
- package/dist/_internal/server/features/search/index-build/sources.js +43 -2
- package/dist/_internal/server/features/stores/tabs.d.ts +7 -2
- package/dist/_internal/server/features/stores/tabs.js +5 -3
- package/dist/_internal/server/jobs/core/jobRunners.js +0 -10
- package/dist/_internal/server/jobs/core/onOrderCreate.js +10 -104
- package/dist/_internal/shared/actions/action-registry.d.ts +1 -1
- package/dist/_internal/shared/actions/action-registry.js +128 -0
- package/dist/_internal/shared/features/categories/bundle-copy.d.ts +1 -1
- package/dist/_internal/shared/features/site-settings/supplier-of-record.d.ts +56 -0
- package/dist/_internal/shared/features/site-settings/supplier-of-record.js +57 -0
- package/dist/_internal/shared/listing-types/tiers.d.ts +94 -0
- package/dist/_internal/shared/listing-types/tiers.js +150 -0
- package/dist/_internal/shared/search/match.js +28 -2
- package/dist/cli/index.js +0 -2
- package/dist/client.d.ts +21 -2
- package/dist/client.js +23 -4
- package/dist/constants/api-endpoints.d.ts +28 -0
- package/dist/constants/api-endpoints.js +14 -0
- package/dist/features/account/components/UserOrdersView.js +26 -7
- package/dist/features/addresses/utils/postal-autofill.d.ts +39 -0
- package/dist/features/addresses/utils/postal-autofill.js +54 -0
- package/dist/features/admin/components/AdminStoreEditorView.js +73 -34
- package/dist/features/admin/components/OwnerQuickActions.d.ts +29 -0
- package/dist/features/admin/components/OwnerQuickActions.js +142 -0
- package/dist/features/admin/schemas/admin-editor-forms.d.ts +1 -1
- package/dist/features/admin/schemas/admin-editor-forms.js +4 -1
- package/dist/features/auctions/components/AuctionDetailPageView.js +8 -7
- package/dist/features/auctions/components/MarketplaceAuctionCard.d.ts +11 -1
- package/dist/features/auctions/components/MarketplaceAuctionCard.js +9 -2
- package/dist/features/auth/permissions/constants.d.ts +29 -2
- package/dist/features/auth/permissions/constants.js +68 -1
- package/dist/features/blog/components/BlogFeaturedCard.d.ts +11 -1
- package/dist/features/blog/components/BlogFeaturedCard.js +9 -2
- package/dist/features/cart/components/CartDrawer.js +3 -2
- package/dist/features/cart/components/CartGroupLineRow.js +2 -1
- package/dist/features/cart/hooks/useAddToCart.js +10 -5
- package/dist/features/cart/utils/pending-ops.d.ts +11 -1
- package/dist/features/cart/utils/pending-ops.js +14 -2
- package/dist/features/categories/api/[id]/price-guidance/route.js +1 -24
- package/dist/features/categories/components/BrandDetailPageView.js +15 -6
- package/dist/features/categories/components/BrandDetailTabs.d.ts +7 -1
- package/dist/features/categories/components/BrandDetailTabs.js +18 -3
- package/dist/features/categories/components/BundleCollage.js +3 -2
- package/dist/features/categories/components/BundleItemsPicker.js +3 -4
- package/dist/features/categories/components/CategoryDetailPageView.js +19 -8
- package/dist/features/categories/components/CategoryDetailTabs.d.ts +7 -1
- package/dist/features/categories/components/CategoryDetailTabs.js +29 -7
- package/dist/features/categories/components/CategoryGrid.d.ts +11 -1
- package/dist/features/categories/components/CategoryGrid.js +9 -2
- package/dist/features/checkout/actions/checkout-value-otp-actions.d.ts +10 -5
- package/dist/features/checkout/actions/checkout-value-otp-actions.js +2 -88
- package/dist/features/classified/components/ClassifiedDetailPageView.js +10 -1
- package/dist/features/digital-codes/components/DigitalCodeDetailPageView.js +10 -1
- package/dist/features/events/components/EventCard.d.ts +11 -1
- package/dist/features/events/components/EventCard.js +9 -2
- package/dist/features/layout/AppLayoutShell.js +2 -1
- package/dist/features/layout/BottomActions.d.ts +1 -0
- package/dist/features/layout/BottomActions.js +330 -0
- package/dist/features/layout/BottomChrome.d.ts +45 -0
- package/dist/features/layout/BottomChrome.js +5 -17
- package/dist/features/layout/SideActionDock.js +36 -13
- package/dist/features/layout/index.d.ts +2 -7
- package/dist/features/layout/index.js +7 -7
- package/dist/features/layout/useIsDesktop.d.ts +36 -0
- package/dist/features/layout/useIsDesktop.js +48 -0
- package/dist/features/live/components/LiveItemDetailPageView.js +10 -1
- package/dist/features/media/messages/en.json +67 -67
- package/dist/features/orders/components/YourPurchaseStrip.d.ts +24 -0
- package/dist/features/orders/components/YourPurchaseStrip.js +77 -0
- package/dist/features/orders/components/index.d.ts +2 -1
- package/dist/features/orders/components/index.js +1 -1
- package/dist/features/orders/utils/order-announcement-message.d.ts +18 -0
- package/dist/features/orders/utils/order-announcement-message.js +16 -0
- package/dist/features/orders/utils/payment-proof-message.d.ts +20 -0
- package/dist/features/orders/utils/payment-proof-message.js +17 -0
- package/dist/features/parts/schemas/firestore.d.ts +227 -0
- package/dist/features/parts/schemas/firestore.js +6 -6
- package/dist/features/pre-orders/components/MarketplacePreorderCard.d.ts +11 -1
- package/dist/features/pre-orders/components/MarketplacePreorderCard.js +10 -3
- package/dist/features/pre-orders/components/PreOrderDetailPageView.js +8 -7
- package/dist/features/price-index/components/PriceIndexToolView.d.ts +21 -0
- package/dist/features/price-index/components/PriceIndexToolView.js +230 -27
- package/dist/features/price-index/lookup-target.d.ts +48 -0
- package/dist/features/price-index/lookup-target.js +107 -0
- package/dist/features/products/api/[id]/route.js +8 -1
- package/dist/features/products/api/route.js +16 -4
- package/dist/features/products/components/AvailabilityScopeHint.d.ts +22 -0
- package/dist/features/products/components/AvailabilityScopeHint.js +27 -0
- package/dist/features/products/components/CompareOverlay.d.ts +2 -1
- package/dist/features/products/components/CompareOverlay.js +2 -1
- package/dist/features/products/components/GroupSettingsPanel.js +2 -1
- package/dist/features/products/components/InteractiveProductCard.d.ts +7 -1
- package/dist/features/products/components/InteractiveProductCard.js +16 -4
- package/dist/features/products/components/MarketplaceBundleCard.d.ts +11 -1
- package/dist/features/products/components/MarketplaceBundleCard.js +9 -2
- package/dist/features/products/components/MarketplacePrizeDrawCard.d.ts +11 -1
- package/dist/features/products/components/MarketplacePrizeDrawCard.js +9 -2
- package/dist/features/products/components/ProductDetailPageView.js +34 -4
- package/dist/features/products/components/ProductGrid.d.ts +14 -1
- package/dist/features/products/components/ProductGrid.js +22 -5
- package/dist/features/products/components/ProductsIndexListing.js +2 -2
- package/dist/features/products/components/SublistingCarouselSection.js +2 -1
- package/dist/features/products/repository/products.repository.d.ts +1 -0
- package/dist/features/products/repository/products.repository.js +5 -1
- package/dist/features/products/utils/group-member-image.d.ts +9 -2
- package/dist/features/products/utils/group-member-image.js +2 -1
- package/dist/features/reviews/api/[id]/route.js +83 -23
- package/dist/features/reviews/components/PurchaseReviewAffordance.d.ts +6 -0
- package/dist/features/reviews/components/PurchaseReviewAffordance.js +42 -0
- package/dist/features/reviews/components/ReviewFormDrawer.d.ts +34 -0
- package/dist/features/reviews/components/ReviewFormDrawer.js +127 -0
- package/dist/features/reviews/components/index.d.ts +4 -0
- package/dist/features/reviews/components/index.js +2 -0
- package/dist/features/seller/actions/seller-actions.d.ts +1 -1
- package/dist/features/seller/actions/seller-actions.js +49 -3
- package/dist/features/seller/components/SellerPayoutSettingsView.js +30 -2
- package/dist/features/seller/components/SellerProductShell.d.ts +17 -1
- package/dist/features/seller/components/SellerProductsView.js +3 -2
- package/dist/features/seller/hooks/useStoreListingEntitlements.d.ts +8 -0
- package/dist/features/seller/hooks/useStoreListingEntitlements.js +70 -0
- package/dist/features/stores/components/InteractiveStoreCard.d.ts +11 -1
- package/dist/features/stores/components/InteractiveStoreCard.js +9 -2
- package/dist/features/stores/components/StoreAuctionsListing.js +2 -1
- package/dist/features/stores/components/index.d.ts +0 -2
- package/dist/features/stores/components/index.js +0 -1
- package/dist/features/stores/schemas/index.d.ts +1 -1
- package/dist/features/stores/schemas/index.js +10 -1
- package/dist/features/tester/seed-data/authored/content-discovery__authenticity-checker.js +114 -2
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +415 -0
- package/dist/index.d.ts +8 -27
- package/dist/index.js +14 -35
- package/dist/next/routing/route-map.d.ts +2 -0
- package/dist/next/routing/route-map.js +9 -0
- package/dist/providers/shipping-shiprocket/index.d.ts +98 -0
- package/dist/providers/shipping-shiprocket/index.js +353 -0
- package/dist/schemas/registry.d.ts +2 -2
- package/dist/security/csp.d.ts +16 -1
- package/dist/security/csp.js +5 -3
- package/dist/seed/_helpers/category-forest.js +27 -0
- package/dist/seed/_helpers/category-parts-glossary-map.js +1816 -86
- package/dist/seed/_helpers/category-parts.d.ts +1 -1
- package/dist/seed/_helpers/category-parts.js +13412 -3020
- package/dist/seed/categories-seed-data.js +48 -6
- package/dist/seed/homepage-sections-seed-data.js +41 -28
- package/dist/seed/model-composition-seed-data.js +6 -8
- package/dist/seed/part-glossary/chunk0.d.ts +9 -0
- package/dist/seed/part-glossary/chunk0.js +35044 -0
- package/dist/seed/part-glossary/chunk1.d.ts +9 -0
- package/dist/seed/part-glossary/chunk1.js +39955 -0
- package/dist/seed/part-glossary/chunk2.d.ts +9 -0
- package/dist/seed/part-glossary/chunk2.js +50335 -0
- package/dist/seed/part-glossary/chunk3.d.ts +9 -0
- package/dist/seed/part-glossary/chunk3.js +52386 -0
- package/dist/seed/part-glossary/chunk4.d.ts +9 -0
- package/dist/seed/part-glossary/chunk4.js +50549 -0
- package/dist/seed/part-glossary-seed-data.d.ts +1 -1
- package/dist/seed/part-glossary-seed-data.js +23 -47483
- package/dist/seed/products-beyblade-x-seed-data.js +36 -1
- package/dist/server.d.ts +6 -8
- package/dist/server.js +16 -20
- package/dist/styles.css +516 -358
- package/dist/tailwind-utilities.css +1 -1
- package/dist/tokens/themes/index.d.ts +3 -1
- package/dist/tokens/themes/index.js +11 -1
- package/dist/tokens/themes/technical-dark.d.ts +30 -0
- package/dist/tokens/themes/technical-dark.js +103 -0
- package/dist/tokens/themes/technical-light.d.ts +22 -0
- package/dist/tokens/themes/technical-light.js +89 -0
- package/dist/tokens/tokens.css +155 -0
- package/dist/ui/components/GatedPrice.js +30 -3
- package/dist/ui/components/GatedPrice.style.css +22 -19
- package/dist/ui/components/QuickCreateModal.style.css +78 -78
- package/dist/ui/forms/FormShell.style.css +260 -260
- package/package.json +1 -1
- package/scripts/seed-cli.mjs +133 -27
- package/dist/_internal/server/jobs/core/whatsappCatalogImport.d.ts +0 -27
- package/dist/_internal/server/jobs/core/whatsappCatalogImport.js +0 -172
- package/dist/_internal/server/jobs/core/whatsappNotify.d.ts +0 -35
- package/dist/_internal/server/jobs/core/whatsappNotify.js +0 -140
- package/dist/features/admin/components/AdminWhatsAppGuideView.d.ts +0 -2
- package/dist/features/admin/components/AdminWhatsAppGuideView.js +0 -35
- package/dist/features/orders/components/MarketplaceOrderCard.d.ts +0 -36
- package/dist/features/orders/components/MarketplaceOrderCard.js +0 -57
- package/dist/features/stores/components/StoreWhatsAppGuideView.d.ts +0 -3
- package/dist/features/stores/components/StoreWhatsAppGuideView.js +0 -12
- package/dist/features/whatsapp-bot/api/send-status/route.d.ts +0 -12
- package/dist/features/whatsapp-bot/api/send-status/route.js +0 -38
- package/dist/features/whatsapp-bot/api/webhook/route.d.ts +0 -12
- package/dist/features/whatsapp-bot/api/webhook/route.js +0 -43
- package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.d.ts +0 -6
- package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.js +0 -270
- package/dist/features/whatsapp-bot/components/WhatsAppChatButton.d.ts +0 -9
- package/dist/features/whatsapp-bot/components/WhatsAppChatButton.js +0 -7
- package/dist/features/whatsapp-bot/components/index.d.ts +0 -3
- package/dist/features/whatsapp-bot/components/index.js +0 -2
- package/dist/features/whatsapp-bot/helpers/payment-proof-message.d.ts +0 -14
- package/dist/features/whatsapp-bot/helpers/payment-proof-message.js +0 -17
- package/dist/features/whatsapp-bot/helpers/whatsapp.d.ts +0 -52
- package/dist/features/whatsapp-bot/helpers/whatsapp.js +0 -283
- package/dist/features/whatsapp-bot/index.d.ts +0 -3
- package/dist/features/whatsapp-bot/index.js +0 -3
- package/dist/features/whatsapp-bot/manifest.d.ts +0 -2
- package/dist/features/whatsapp-bot/manifest.js +0 -13
- package/dist/features/whatsapp-bot/messages/en.json +0 -7
- package/dist/features/whatsapp-bot/server.d.ts +0 -7
- package/dist/features/whatsapp-bot/server.js +0 -7
- package/dist/features/whatsapp-bot/types/index.d.ts +0 -126
- package/dist/features/whatsapp-bot/types/index.js +0 -1
|
@@ -319,7 +319,53 @@ export const ROUTE_PERMISSION_MAP = {
|
|
|
319
319
|
"/admin/maintenance/payment-rollbacks": "admin:maintenance:view-payment-rollbacks",
|
|
320
320
|
"/admin/maintenance/analysis": "admin:maintenance:run-analysis",
|
|
321
321
|
};
|
|
322
|
-
/**
|
|
322
|
+
/**
|
|
323
|
+
* Every capability, as a runtime value.
|
|
324
|
+
*
|
|
325
|
+
* 🛑 `Record<StoreCapability, true>` rather than a plain array, for the same
|
|
326
|
+
* reason `ALL_LISTING_TYPES_MAP` is: adding a member to the union without
|
|
327
|
+
* adding it here is then a COMPILE error, not a silent omission. This is what
|
|
328
|
+
* lets `capabilities` be validated as a `z.enum` instead of
|
|
329
|
+
* `z.array(z.string())` — which accepted any string, so a typo'd capability
|
|
330
|
+
* saved successfully and the gate it was meant to open never opened, silently.
|
|
331
|
+
*
|
|
332
|
+
* It also backs the admin editor's grouping, so a new capability cannot be
|
|
333
|
+
* granted in code and remain unreachable from the admin UI.
|
|
334
|
+
*/
|
|
335
|
+
const STORE_CAPABILITY_MAP = {
|
|
336
|
+
advanced_listing_types: true,
|
|
337
|
+
host_auctions: true,
|
|
338
|
+
host_preorders: true,
|
|
339
|
+
create_categories: true,
|
|
340
|
+
suggest_brands: true,
|
|
341
|
+
create_coupons: true,
|
|
342
|
+
bulk_listing_import: true,
|
|
343
|
+
extended_return_window: true,
|
|
344
|
+
verified_seller: true,
|
|
345
|
+
featured_placement: true,
|
|
346
|
+
promotional_banner: true,
|
|
347
|
+
priority_support: true,
|
|
348
|
+
multiple_stores: true,
|
|
349
|
+
custom_store_slug: true,
|
|
350
|
+
api_access: true,
|
|
351
|
+
lower_commission_rate: true,
|
|
352
|
+
early_access_features: true,
|
|
353
|
+
advanced_analytics: true,
|
|
354
|
+
whatsapp_catalog_sync: true,
|
|
355
|
+
};
|
|
356
|
+
export const ALL_STORE_CAPABILITIES = Object.keys(STORE_CAPABILITY_MAP);
|
|
357
|
+
/** Narrow an untrusted string to a real capability. */
|
|
358
|
+
export function isStoreCapability(value) {
|
|
359
|
+
return typeof value === "string" && value in STORE_CAPABILITY_MAP;
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Default capabilities granted to every new store on creation.
|
|
363
|
+
*
|
|
364
|
+
* 🛑 THE one definition. `AdminStoreEditorView` hardcoded its own copy, which
|
|
365
|
+
* is also the `??` fallback on its load path — so the two could disagree about
|
|
366
|
+
* what a store with no `capabilities` field is entitled to, and the admin
|
|
367
|
+
* editor's copy is the one that gets SAVED BACK. Import this; never restate it.
|
|
368
|
+
*/
|
|
323
369
|
export const DEFAULT_STORE_CAPABILITIES = [
|
|
324
370
|
"suggest_brands",
|
|
325
371
|
"create_coupons",
|
|
@@ -352,6 +398,27 @@ export function hasAllPermissions(permissions, required) {
|
|
|
352
398
|
export function hasCapability(capabilities, required) {
|
|
353
399
|
return capabilities.includes(required);
|
|
354
400
|
}
|
|
401
|
+
/**
|
|
402
|
+
* Resolve a store's stored `capabilities` array to its effective set.
|
|
403
|
+
*
|
|
404
|
+
* 🛑 The PURE half of `getStoreCapabilities`, extracted so a caller that
|
|
405
|
+
* already holds the store document does not pay a second Firestore read to ask
|
|
406
|
+
* what it is entitled to (Rule #6). Three WhatsApp routes were reading
|
|
407
|
+
* `store.capabilities ?? []` inline for exactly that reason, which made them a
|
|
408
|
+
* second, divergent reader of the field — they skipped the default merge, so
|
|
409
|
+
* they and `getStoreCapabilities` could disagree about the same store.
|
|
410
|
+
*
|
|
411
|
+
* Note the empty-array case falls back to the defaults, matching
|
|
412
|
+
* `getStoreCapabilities`' long-standing `raw.length > 0` test. That is
|
|
413
|
+
* arguably the wrong call — an admin who deliberately unticks everything gets
|
|
414
|
+
* the defaults handed back — but it is the established behaviour and changing
|
|
415
|
+
* it would silently re-grant capabilities on stores that had them removed.
|
|
416
|
+
*/
|
|
417
|
+
export function resolveStoreCapabilities(raw) {
|
|
418
|
+
if (Array.isArray(raw) && raw.length > 0)
|
|
419
|
+
return raw;
|
|
420
|
+
return [...DEFAULT_STORE_CAPABILITIES];
|
|
421
|
+
}
|
|
355
422
|
// ============================================================================
|
|
356
423
|
// PERMISSION DOMAINS — display grouping (S-ADMIN-7, 2026-08-19)
|
|
357
424
|
// ============================================================================
|
|
@@ -17,5 +17,15 @@ export interface BlogFeaturedCardProps {
|
|
|
17
17
|
showAuthor?: boolean;
|
|
18
18
|
/** Show the cover image. Defaults to `true`. */
|
|
19
19
|
showThumbnail?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Owner-only pencil / clone / bin, for an admin or whoever owns this record.
|
|
22
|
+
* Pass `<OwnerQuickActions>`; it renders nothing for anyone else.
|
|
23
|
+
*
|
|
24
|
+
* 🛑 Rendered OUTSIDE this card's own `<Link>`/`<TextLink>`, as a sibling.
|
|
25
|
+
* A `<button>` inside an `<a>` is invalid HTML that browsers resolve by
|
|
26
|
+
* closing the anchor early, which breaks the CARD's navigation rather than
|
|
27
|
+
* just the button's — `stopPropagation` stops a click but cannot fix a parse.
|
|
28
|
+
*/
|
|
29
|
+
renderOwnerActions?: () => React.ReactNode;
|
|
20
30
|
}
|
|
21
|
-
export declare function BlogFeaturedCard({ post, href, labels, className, selectable, isSelected, onSelect, showReadTime, showAuthor, showThumbnail, }: BlogFeaturedCardProps): React.JSX.Element;
|
|
31
|
+
export declare function BlogFeaturedCard({ post, href, labels, className, selectable, isSelected, onSelect, showReadTime, showAuthor, showThumbnail, renderOwnerActions, }: BlogFeaturedCardProps): React.JSX.Element;
|
|
@@ -20,7 +20,7 @@ const CATEGORY_BADGE = {
|
|
|
20
20
|
community: "bg-warning-surface text-warning dark:bg-warning-surface dark:text-warning",
|
|
21
21
|
};
|
|
22
22
|
const CLS_FEATURED_BADGE = "bg-warning-surface text-warning dark:bg-warning-surface dark:text-warning px-[var(--appkit-space-2)] py-[var(--appkit-space-0-5)] rounded-full";
|
|
23
|
-
export function BlogFeaturedCard({ post, href, labels = {}, className = "", selectable = false, isSelected = false, onSelect, showReadTime = true, showAuthor = true, showThumbnail = true, }) {
|
|
23
|
+
export function BlogFeaturedCard({ post, href, labels = {}, className = "", selectable = false, isSelected = false, onSelect, showReadTime = true, showAuthor = true, showThumbnail = true, renderOwnerActions, }) {
|
|
24
24
|
const longPress = useLongPress(() => onSelect?.(post.id, !isSelected));
|
|
25
25
|
const safeTitle = post.title?.trim() || "Untitled post";
|
|
26
26
|
const coverImageUrl = showThumbnail ? getMediaUrl(post.coverImage) : undefined;
|
|
@@ -31,5 +31,12 @@ export function BlogFeaturedCard({ post, href, labels = {}, className = "", sele
|
|
|
31
31
|
day: "numeric",
|
|
32
32
|
})
|
|
33
33
|
: "";
|
|
34
|
-
|
|
34
|
+
const card = (_jsxs(Article, { rounded: "xl", shadow: "hover-md", className: `group relative h-full overflow-hidden border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] ${isSelected ? "ring-2 ring-primary outline outline-2 outline-primary" : ""} ${className}`, onMouseDown: onSelect && !isSelected ? longPress.onMouseDown : undefined, onMouseUp: onSelect && !isSelected ? longPress.onMouseUp : undefined, onMouseLeave: onSelect && !isSelected ? longPress.onMouseLeave : undefined, onTouchStart: onSelect && !isSelected ? longPress.onTouchStart : undefined, onTouchEnd: onSelect && !isSelected ? longPress.onTouchEnd : undefined, onTouchCancel: onSelect && !isSelected ? longPress.onTouchCancel : undefined, children: [onSelect && (_jsx(BaseListingCard.Checkbox, { selected: isSelected, onSelect: (e) => { e.preventDefault(); onSelect(post.id, !isSelected); }, label: isSelected ? "Deselect post" : "Select post", position: "top-2 left-2", className: selectable || isSelected ? "opacity-100" : "opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-opacity" })), _jsxs(TextLink, { href: href, layout: "flex-col", className: "h-full", children: [showThumbnail && (_jsx(Div, { className: `relative aspect-video ${__O.hidden} flex-shrink-0`, children: coverImageUrl ? (_jsx(MediaImage, { src: coverImageUrl, alt: safeTitle, size: "card", className: "transition-transform duration-300 group-hover:scale-105" })) : (_jsx(Div, { role: "img", "aria-label": safeTitle, surface: "muted", className: "h-full w-full" })) })), _jsxs(Stack, { className: `flex-1 ${__P.p4}`, children: [_jsxs(Row, { gap: "xs", className: "mb-2", wrap: true, children: [_jsx(Span, { size: "xs", weight: "medium", className: `inline-block capitalize ${CATEGORY_BADGE[post.category] ?? "bg-[var(--appkit-color-bg)] text-[var(--appkit-color-text-muted)]"}`, rounded: "full", padding: "pill-xs", children: post.category }), post.isFeatured && (_jsx(Span, { size: "xs", weight: "medium", className: CLS_FEATURED_BADGE, children: labels.featuredBadge ?? "Featured" }))] }), _jsx(Heading, { level: 3, className: `leading-snug mb-1`, color: "primary", truncate: 2, size: "base", weight: "semibold", children: safeTitle }), post.excerpt && (_jsx(Text, { className: `mb-3`, color: "muted", truncate: 2, size: "sm", children: post.excerpt })), _jsxs(Row, { color: "muted", textSize: "xs", className: "mt-auto", gap: "3", wrap: true, children: [showAuthor && post.authorName && (_jsx(Span, { children: safeDisplayName(post.authorName, "Author") })), showReadTime && post.readTimeMinutes != null && (_jsxs(Span, { children: [post.readTimeMinutes, " ", labels.readTime ?? "min read"] })), date && _jsx(Span, { children: date })] })] })] })] }));
|
|
35
|
+
if (!renderOwnerActions)
|
|
36
|
+
return card;
|
|
37
|
+
/*
|
|
38
|
+
* Sibling of the card, never a child — see the prop's docstring. `z-20`
|
|
39
|
+
* beats this card's own overlays, which all sit at `z-10`.
|
|
40
|
+
*/
|
|
41
|
+
return (_jsxs(Div, { className: "relative h-full", children: [card, _jsx(Div, { className: "absolute right-2 top-2 z-20", children: renderOwnerActions() })] }));
|
|
35
42
|
}
|
|
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
3
3
|
import { Aside, Button, Div, Heading, Row, Span, Stack, Text, TextLink } from "../../../ui/index.js";
|
|
4
4
|
import { MediaImage } from "../../media/MediaImage.js";
|
|
5
5
|
import { QuantityStepper } from "../../../ui/components/QuantityStepper.js";
|
|
6
|
+
import { GatedPrice } from "../../../ui/components/GatedPrice.js";
|
|
6
7
|
import { formatCurrency } from "../../../utils/number.formatter.js";
|
|
7
8
|
import { ACTIONS } from "../../../_internal/shared/actions/action-registry.js";
|
|
8
9
|
import { useHandMode } from "../../../_internal/client/hand-mode/index.js";
|
|
@@ -19,11 +20,11 @@ export function CartItemRow({ item, onQtyChange, onRemove, href, isOutOfStock =
|
|
|
19
20
|
return (_jsxs(Div, { layout: "flex", gap: isRow ? "3" : "4", surface: isRow ? "none" : "card", padding: isRow ? "none" : "sm", className: `min-w-0 transition-opacity ${isOutOfStock ? "opacity-60" : ""}`, children: [_jsx(Div, { surface: "muted", className: `relative ${isRow ? "h-16 w-16" : "h-20 w-20"} flex-shrink-0 ${__O.hidden}`, rounded: "lg", children: item.meta.image && (_jsx(MediaImage, { src: item.meta.image, alt: item.meta.title, size: "thumbnail" })) }), _jsxs(Stack, { justify: "between", className: "flex-1 min-w-0", children: [_jsxs(Row, { gap: "xs", className: "min-w-0", align: "start", children: [href ? (_jsx(TextLink, { href: href, target: "_blank", rel: "noopener noreferrer", weight: "medium", className: "text-[var(--appkit-color-text)] hover:underline underline-offset-2 line-clamp-2", children: item.meta.title })) : (_jsx(Text, { truncate: 2, weight: "medium", color: "primary", children: item.meta.title })), isOutOfStock && (_jsx(Span, { padding: "pill-2xs", color: "error", surface: "danger-surface", weight: "semibold", className: "flex-shrink-0 text-[10px] tracking-wide", rounded: "default", transform: "uppercase", children: "Out of Stock" }))] }), item.meta.attributes &&
|
|
20
21
|
Object.keys(item.meta.attributes).length > 0 && (_jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "xs", truncate: 1, children: Object.entries(item.meta.attributes)
|
|
21
22
|
.map(([k, v]) => `${k}: ${v}`)
|
|
22
|
-
.join(", ") })), _jsxs(Row, { justify: "between", className: "min-w-0", gap: "sm", children: [_jsx(Text, { className: "text-[var(--appkit-color-text)]", weight: "semibold", children: formatCurrency(item.meta.price * item.quantity, item.meta.currency) }), onQtyChange && !isOutOfStock && (_jsx(QuantityStepper, { value: item.quantity, onChange: (next) => onQtyChange(item.id, next), min: 1, ariaLabel: `Quantity for ${item.meta.title}`, decrementLabel: ACTIONS.CART["decrease-quantity"].ariaLabel, incrementLabel: ACTIONS.CART["increase-quantity"].ariaLabel })), isOutOfStock && (_jsxs(Span, { size: "xs", color: "muted", children: ["Qty: ", item.quantity] }))] })] }), onRemove && (_jsx(Button, { onClick: () => onRemove(item.id), variant: "ghost", size: "sm", "aria-label": ACTIONS.CART["remove-item"].ariaLabel, className: CLS_REMOVE_BTN, children: "\u2715" }))] }));
|
|
23
|
+
.join(", ") })), _jsxs(Row, { justify: "between", className: "min-w-0", gap: "sm", children: [_jsx(Text, { className: "text-[var(--appkit-color-text)]", weight: "semibold", children: _jsx(GatedPrice, { children: formatCurrency(item.meta.price * item.quantity, item.meta.currency) }) }), onQtyChange && !isOutOfStock && (_jsx(QuantityStepper, { value: item.quantity, onChange: (next) => onQtyChange(item.id, next), min: 1, ariaLabel: `Quantity for ${item.meta.title}`, decrementLabel: ACTIONS.CART["decrease-quantity"].ariaLabel, incrementLabel: ACTIONS.CART["increase-quantity"].ariaLabel })), isOutOfStock && (_jsxs(Span, { size: "xs", color: "muted", children: ["Qty: ", item.quantity] }))] })] }), onRemove && (_jsx(Button, { onClick: () => onRemove(item.id), variant: "ghost", size: "sm", "aria-label": ACTIONS.CART["remove-item"].ariaLabel, className: CLS_REMOVE_BTN, children: "\u2715" }))] }));
|
|
23
24
|
}
|
|
24
25
|
export function CartDrawer({ isOpen, onClose, items, subtotal = 0, currency, isLoading, onQtyChange, onRemove, onCheckout, labels = {}, }) {
|
|
25
26
|
const { hand } = useHandMode();
|
|
26
27
|
if (!isOpen)
|
|
27
28
|
return null;
|
|
28
|
-
return (_jsxs(_Fragment, { children: [_jsx(Div, { surface: "overlay-xs", role: "presentation", className: "fixed inset-0 z-40", onClick: onClose }), _jsx(Aside, { surface: "sidePanel", shadow: "xl", className: `fixed inset-y-0 ${hand === "left" ? "left-0" : "right-0"} z-50 w-full max-w-sm`, children: _jsxs(Stack, { className: "h-full", children: [_jsxs(Row, { justify: "between", border: "bottom", className: __P.p4, reverse: hand === "left", children: [_jsx(Heading, { level: 2, size: "lg", weight: "semibold", children: labels.title ?? "Cart" }), _jsx(Button, { onClick: onClose, variant: "ghost", size: "sm", "aria-label": "Close cart", textColor: "faint", className: "hover:text-[var(--appkit-color-text)] dark:hover:text-[var(--appkit-color-text)]", children: "\u2715" })] }), _jsx(Stack, { className: `flex-1 ${__O.yAuto} ${__P.p4}`, gap: "3", children: isLoading ? (_jsx(Row, { justify: "center", padding: "y-3xl", children: _jsx(Div, { className: "h-8 w-8 animate-spin border-2 border-neutral-300 border-t-neutral-800 dark:border-t-zinc-200", rounded: "full" }) })) : items.length === 0 ? (_jsx(Text, { paddingY: "3xl", className: "text-[var(--appkit-color-text-muted)]", size: "sm", align: "center", children: labels.empty ?? "Your cart is empty" })) : (items.map((item) => (_jsx(CartItemRow, { item: item, onQtyChange: onQtyChange, onRemove: onRemove }, item.id)))) }), items.length > 0 && (_jsxs(Stack, { border: "top", className: __P.p4, gap: "md", children: [_jsxs(Row, { textSize: "sm", justify: "between", children: [_jsx(Span, { className: "text-[var(--appkit-color-text-muted)]", children: labels.subtotal ?? "Subtotal" }), _jsx(Span, { weight: "semibold", children: formatCurrency(subtotal, currency) })] }), onCheckout && (_jsx(Button, { rounded: "xl", onClick: onCheckout, variant: "primary", textSize: "sm", className: "w-full bg-neutral-900 py-[0.75rem] font-[600] text-white transition hover:bg-neutral-800", children: labels.checkout ?? ACTIONS.CART["checkout"].label }))] }))] }) })] }));
|
|
29
|
+
return (_jsxs(_Fragment, { children: [_jsx(Div, { surface: "overlay-xs", role: "presentation", className: "fixed inset-0 z-40", onClick: onClose }), _jsx(Aside, { surface: "sidePanel", shadow: "xl", className: `fixed inset-y-0 ${hand === "left" ? "left-0" : "right-0"} z-50 w-full max-w-sm`, children: _jsxs(Stack, { className: "h-full", children: [_jsxs(Row, { justify: "between", border: "bottom", className: __P.p4, reverse: hand === "left", children: [_jsx(Heading, { level: 2, size: "lg", weight: "semibold", children: labels.title ?? "Cart" }), _jsx(Button, { onClick: onClose, variant: "ghost", size: "sm", "aria-label": "Close cart", textColor: "faint", className: "hover:text-[var(--appkit-color-text)] dark:hover:text-[var(--appkit-color-text)]", children: "\u2715" })] }), _jsx(Stack, { className: `flex-1 ${__O.yAuto} ${__P.p4}`, gap: "3", children: isLoading ? (_jsx(Row, { justify: "center", padding: "y-3xl", children: _jsx(Div, { className: "h-8 w-8 animate-spin border-2 border-neutral-300 border-t-neutral-800 dark:border-t-zinc-200", rounded: "full" }) })) : items.length === 0 ? (_jsx(Text, { paddingY: "3xl", className: "text-[var(--appkit-color-text-muted)]", size: "sm", align: "center", children: labels.empty ?? "Your cart is empty" })) : (items.map((item) => (_jsx(CartItemRow, { item: item, onQtyChange: onQtyChange, onRemove: onRemove }, item.id)))) }), items.length > 0 && (_jsxs(Stack, { border: "top", className: __P.p4, gap: "md", children: [_jsxs(Row, { textSize: "sm", justify: "between", children: [_jsx(Span, { className: "text-[var(--appkit-color-text-muted)]", children: labels.subtotal ?? "Subtotal" }), _jsx(Span, { weight: "semibold", children: _jsx(GatedPrice, { children: formatCurrency(subtotal, currency) }) })] }), onCheckout && (_jsx(Button, { rounded: "xl", onClick: onCheckout, variant: "primary", textSize: "sm", className: "w-full bg-neutral-900 py-[0.75rem] font-[600] text-white transition hover:bg-neutral-800", children: labels.checkout ?? ACTIONS.CART["checkout"].label }))] }))] }) })] }));
|
|
29
30
|
}
|
|
@@ -20,6 +20,7 @@ import { useState } from "react";
|
|
|
20
20
|
import { Button, Div, Row, Span, Stack, Text, TextLink } from "../../../ui/index.js";
|
|
21
21
|
import { QuantityStepper } from "../../../ui/components/QuantityStepper.js";
|
|
22
22
|
import { MediaImage } from "../../media/MediaImage.js";
|
|
23
|
+
import { GatedPrice, PricesOnly } from "../../../ui/components/GatedPrice.js";
|
|
23
24
|
import { formatCurrency } from "../../../utils/number.formatter.js";
|
|
24
25
|
import { ACTIONS } from "../../../_internal/shared/actions/action-registry.js";
|
|
25
26
|
const __O = { hidden: "overflow-hidden" };
|
|
@@ -29,5 +30,5 @@ export function CartGroupLineRow({ id, title, image, currency = "INR", lineKind,
|
|
|
29
30
|
const onePrice = members.reduce((sum, m) => sum + m.unitPrice * m.quantity, 0);
|
|
30
31
|
const lineTotal = onePrice * quantity;
|
|
31
32
|
const unitCount = members.reduce((sum, m) => sum + m.quantity, 0);
|
|
32
|
-
return (_jsxs(Div, { layout: "flex", gap: isRow ? "3" : "4", surface: isRow ? "none" : "card", padding: isRow ? "none" : "sm", className: "min-w-0", children: [_jsx(Div, { surface: "muted", className: `relative ${isRow ? "h-16 w-16" : "h-20 w-20"} flex-shrink-0 ${__O.hidden}`, rounded: "lg", children: image && _jsx(MediaImage, { src: image, alt: title, size: "thumbnail" }) }), _jsxs(Stack, { justify: "between", gap: "xs", className: "flex-1 min-w-0", children: [_jsxs(Row, { gap: "xs", align: "start", className: "min-w-0", children: [href ? (_jsx(TextLink, { href: href, weight: "medium", className: "text-[var(--appkit-color-text)] hover:underline underline-offset-2 line-clamp-2", children: title })) : (_jsx(Text, { truncate: 2, weight: "medium", color: "primary", children: title })), _jsx(Span, { padding: "pill-2xs", surface: "subtle", color: "muted", rounded: "default", className: "flex-shrink-0 text-[10px] tracking-wide", transform: "uppercase", children: lineKind === "bundle" ? "Bundle" : "Group" })] }), _jsxs(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => setIsOpen((v) => !v), "aria-expanded": isOpen, className: "self-start text-[length:var(--appkit-text-xs)] text-[var(--appkit-color-primary)] hover:underline", children: [isOpen ? "▼" : "▶", " ", members.length, " item", members.length === 1 ? "" : "s", unitCount !== members.length ? ` · ${unitCount} units` : ""] }), isOpen && (_jsx(Stack, { gap: "xs", className: "w-full", children: members.map((m) => (_jsxs(Row, { gap: "sm", align: "center", justify: "between", className: "min-w-0", children: [_jsxs(Row, { gap: "sm", align: "center", className: "min-w-0 flex-1", children: [_jsx(Div, { className: `h-10 w-10 flex-shrink-0 ${__O.hidden}`, rounded: "default", surface: "muted", children: m.image && _jsx(MediaImage, { src: m.image, alt: m.title, size: "thumbnail" }) }), _jsxs(Stack, { gap: "none", className: "min-w-0", children: [_jsx(Text, { size: "sm", truncate: 1, color: "primary", children: m.title }), _jsxs(Text, { size: "xs", color: "muted", children: [formatCurrency(m.unitPrice, currency), lineKind === "bundle" ? ` × ${m.quantity}` : ""] })] })] }), lineKind === "group" && onMemberQtyChange && !locked && (_jsxs(Row, { gap: "xs", align: "center", className: "flex-shrink-0", children: [_jsx(QuantityStepper, { value: m.quantity, onChange: (next) => onMemberQtyChange(id, m.productId, next), min: 0, max: maxByProductId?.[m.productId], ariaLabel: `Quantity for ${m.title}`, decrementLabel: ACTIONS.CART["decrease-quantity"].ariaLabel, incrementLabel: ACTIONS.CART["increase-quantity"].ariaLabel }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", "aria-label": ACTIONS.CART["remove-group-member"].ariaLabel, onClick: () => onMemberQtyChange(id, m.productId, 0), className: "text-[var(--appkit-color-text-faint)] transition hover:text-error", children: "\u2715" })] })), lineKind === "bundle" && (_jsxs(Span, { size: "xs", color: "muted", className: "flex-shrink-0", children: ["\u00D7 ", m.quantity] }))] }, m.productId))) })), _jsxs(Row, { justify: "between", gap: "sm", className: "min-w-0", children: [_jsx(Text, { weight: "semibold", className: "text-[var(--appkit-color-text)]", children: formatCurrency(lineTotal, currency) }), lineKind === "bundle" && onQtyChange && !locked && (_jsx(QuantityStepper, { value: quantity, onChange: (next) => onQtyChange(id, next), min: 1, ariaLabel: `Number of copies of ${title}`, decrementLabel: ACTIONS.CART["decrease-quantity"].ariaLabel, incrementLabel: ACTIONS.CART["increase-quantity"].ariaLabel }))] })] }), onRemove && !locked && (_jsx(Button, { onClick: () => onRemove(id), variant: "ghost", size: "sm", "aria-label": ACTIONS.CART["remove-item"].ariaLabel, className: "self-start text-[var(--appkit-color-text-faint)] transition hover:text-error", children: "\u2715" }))] }));
|
|
33
|
+
return (_jsxs(Div, { layout: "flex", gap: isRow ? "3" : "4", surface: isRow ? "none" : "card", padding: isRow ? "none" : "sm", className: "min-w-0", children: [_jsx(Div, { surface: "muted", className: `relative ${isRow ? "h-16 w-16" : "h-20 w-20"} flex-shrink-0 ${__O.hidden}`, rounded: "lg", children: image && _jsx(MediaImage, { src: image, alt: title, size: "thumbnail" }) }), _jsxs(Stack, { justify: "between", gap: "xs", className: "flex-1 min-w-0", children: [_jsxs(Row, { gap: "xs", align: "start", className: "min-w-0", children: [href ? (_jsx(TextLink, { href: href, weight: "medium", className: "text-[var(--appkit-color-text)] hover:underline underline-offset-2 line-clamp-2", children: title })) : (_jsx(Text, { truncate: 2, weight: "medium", color: "primary", children: title })), _jsx(Span, { padding: "pill-2xs", surface: "subtle", color: "muted", rounded: "default", className: "flex-shrink-0 text-[10px] tracking-wide", transform: "uppercase", children: lineKind === "bundle" ? "Bundle" : "Group" })] }), _jsxs(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => setIsOpen((v) => !v), "aria-expanded": isOpen, className: "self-start text-[length:var(--appkit-text-xs)] text-[var(--appkit-color-primary)] hover:underline", children: [isOpen ? "▼" : "▶", " ", members.length, " item", members.length === 1 ? "" : "s", unitCount !== members.length ? ` · ${unitCount} units` : ""] }), isOpen && (_jsx(Stack, { gap: "xs", className: "w-full", children: members.map((m) => (_jsxs(Row, { gap: "sm", align: "center", justify: "between", className: "min-w-0", children: [_jsxs(Row, { gap: "sm", align: "center", className: "min-w-0 flex-1", children: [_jsx(Div, { className: `h-10 w-10 flex-shrink-0 ${__O.hidden}`, rounded: "default", surface: "muted", children: m.image && _jsx(MediaImage, { src: m.image, alt: m.title, size: "thumbnail" }) }), _jsxs(Stack, { gap: "none", className: "min-w-0", children: [_jsx(Text, { size: "sm", truncate: 1, color: "primary", children: m.title }), _jsxs(Text, { size: "xs", color: "muted", children: [_jsx(PricesOnly, { children: formatCurrency(m.unitPrice, currency) }), lineKind === "bundle" ? ` × ${m.quantity}` : ""] })] })] }), lineKind === "group" && onMemberQtyChange && !locked && (_jsxs(Row, { gap: "xs", align: "center", className: "flex-shrink-0", children: [_jsx(QuantityStepper, { value: m.quantity, onChange: (next) => onMemberQtyChange(id, m.productId, next), min: 0, max: maxByProductId?.[m.productId], ariaLabel: `Quantity for ${m.title}`, decrementLabel: ACTIONS.CART["decrease-quantity"].ariaLabel, incrementLabel: ACTIONS.CART["increase-quantity"].ariaLabel }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", "aria-label": ACTIONS.CART["remove-group-member"].ariaLabel, onClick: () => onMemberQtyChange(id, m.productId, 0), className: "text-[var(--appkit-color-text-faint)] transition hover:text-error", children: "\u2715" })] })), lineKind === "bundle" && (_jsxs(Span, { size: "xs", color: "muted", className: "flex-shrink-0", children: ["\u00D7 ", m.quantity] }))] }, m.productId))) })), _jsxs(Row, { justify: "between", gap: "sm", className: "min-w-0", children: [_jsx(Text, { weight: "semibold", className: "text-[var(--appkit-color-text)]", children: _jsx(GatedPrice, { children: formatCurrency(lineTotal, currency) }) }), lineKind === "bundle" && onQtyChange && !locked && (_jsx(QuantityStepper, { value: quantity, onChange: (next) => onQtyChange(id, next), min: 1, ariaLabel: `Number of copies of ${title}`, decrementLabel: ACTIONS.CART["decrease-quantity"].ariaLabel, incrementLabel: ACTIONS.CART["increase-quantity"].ariaLabel }))] })] }), onRemove && !locked && (_jsx(Button, { onClick: () => onRemove(id), variant: "ghost", size: "sm", "aria-label": ACTIONS.CART["remove-item"].ariaLabel, className: "self-start text-[var(--appkit-color-text-faint)] transition hover:text-error", children: "\u2715" }))] }));
|
|
33
34
|
}
|
|
@@ -8,9 +8,10 @@ import { CART_ENDPOINTS } from "../../../constants/api-endpoints.js";
|
|
|
8
8
|
import { dispatchCartUpdated, formatCartAddedMessage } from "../utils/pending-ops.js";
|
|
9
9
|
import { useToast } from "../../../ui/components/Toast.js";
|
|
10
10
|
import { formatCurrency } from "../../../utils/number.formatter.js";
|
|
11
|
+
import { useCanSeePrices } from "../../../react/hooks/useCanSeePrices.js";
|
|
11
12
|
import { getDefaultCurrency } from "../../../core/baseline-resolver.js";
|
|
12
|
-
function showAddedToast(showToast, detail) {
|
|
13
|
-
showToast(formatCartAddedMessage(detail, (amount) => formatCurrency(amount, getDefaultCurrency())), "success");
|
|
13
|
+
function showAddedToast(showToast, detail, canSeePrices) {
|
|
14
|
+
showToast(formatCartAddedMessage(detail, (amount) => formatCurrency(amount, getDefaultCurrency()), canSeePrices), "success");
|
|
14
15
|
}
|
|
15
16
|
/**
|
|
16
17
|
* useAddToCart
|
|
@@ -21,6 +22,10 @@ export function useAddToCart(options) {
|
|
|
21
22
|
const queryClient = useQueryClient();
|
|
22
23
|
const endpoint = options?.endpoint ?? CART_ENDPOINTS.GET;
|
|
23
24
|
const { showToast } = useToast();
|
|
25
|
+
// The guest fallback below fires precisely when the viewer is NOT signed in,
|
|
26
|
+
// so a toast that quoted the cart total was the one place the price gate was
|
|
27
|
+
// guaranteed to be speaking to a guest.
|
|
28
|
+
const { canSeePrices } = useCanSeePrices();
|
|
24
29
|
const optionsRef = useRef(options);
|
|
25
30
|
useEffect(() => {
|
|
26
31
|
optionsRef.current = options;
|
|
@@ -31,7 +36,7 @@ export function useAddToCart(options) {
|
|
|
31
36
|
queryClient.invalidateQueries({ queryKey: ["cart"] });
|
|
32
37
|
const itemCount = data?.itemCount ?? 0;
|
|
33
38
|
const totalValue = data?.subtotal ?? 0;
|
|
34
|
-
showAddedToast(showToast, { productTitle: variables.productTitle, itemCount, totalValue });
|
|
39
|
+
showAddedToast(showToast, { productTitle: variables.productTitle, itemCount, totalValue }, canSeePrices);
|
|
35
40
|
dispatchCartUpdated({ itemCount, totalValue, productTitle: variables.productTitle });
|
|
36
41
|
optionsRef.current?.onSuccess?.();
|
|
37
42
|
},
|
|
@@ -55,14 +60,14 @@ export function useAddToCart(options) {
|
|
|
55
60
|
const guestItems = getGuestCartItems();
|
|
56
61
|
const itemCount = guestItems.reduce((sum, it) => sum + it.quantity, 0);
|
|
57
62
|
const totalValue = guestItems.reduce((sum, it) => sum + (it.price ?? 0) * it.quantity, 0);
|
|
58
|
-
showAddedToast(showToast, { productTitle: data.productTitle, itemCount, totalValue });
|
|
63
|
+
showAddedToast(showToast, { productTitle: data.productTitle, itemCount, totalValue }, canSeePrices);
|
|
59
64
|
dispatchCartUpdated({ itemCount, totalValue, productTitle: data.productTitle });
|
|
60
65
|
optionsRef.current?.onSuccess?.();
|
|
61
66
|
return undefined;
|
|
62
67
|
}
|
|
63
68
|
throw err;
|
|
64
69
|
}
|
|
65
|
-
}, [serverMutation, showToast]);
|
|
70
|
+
}, [serverMutation, showToast, canSeePrices]);
|
|
66
71
|
return {
|
|
67
72
|
mutate,
|
|
68
73
|
isLoading: serverMutation.isPending,
|
|
@@ -30,7 +30,17 @@ export declare function dispatchCartUpdated(detail: CartUpdatedEventDetail): voi
|
|
|
30
30
|
* instead of a bare "Added to cart", so callers only need to format
|
|
31
31
|
* currency, not compose the sentence twice.
|
|
32
32
|
*/
|
|
33
|
-
export declare function formatCartAddedMessage(detail: CartUpdatedEventDetail, formatCurrency: (amount: number) => string
|
|
33
|
+
export declare function formatCartAddedMessage(detail: CartUpdatedEventDetail, formatCurrency: (amount: number) => string,
|
|
34
|
+
/**
|
|
35
|
+
* 🛑 A toast is a STRING, so it cannot be wrapped in `<GatedPrice>` — this is
|
|
36
|
+
* the third form of the guest-price gate, read as a value at the call site
|
|
37
|
+
* (CLAUDE.md § "Guest price gating"). Default `true` so no existing caller
|
|
38
|
+
* silently loses the total; the signed-out callers pass `false`.
|
|
39
|
+
*
|
|
40
|
+
* The COUNT is deliberately still shown. It is not money, and a guest who
|
|
41
|
+
* just added something is owed confirmation that it landed.
|
|
42
|
+
*/
|
|
43
|
+
includeTotal?: boolean): string;
|
|
34
44
|
export type CartOpKind = "add" | "remove";
|
|
35
45
|
export type WishlistOpKind = "add" | "remove";
|
|
36
46
|
export interface CartOp {
|
|
@@ -35,9 +35,21 @@ export function dispatchCartUpdated(detail) {
|
|
|
35
35
|
* instead of a bare "Added to cart", so callers only need to format
|
|
36
36
|
* currency, not compose the sentence twice.
|
|
37
37
|
*/
|
|
38
|
-
export function formatCartAddedMessage(detail, formatCurrency
|
|
38
|
+
export function formatCartAddedMessage(detail, formatCurrency,
|
|
39
|
+
/**
|
|
40
|
+
* 🛑 A toast is a STRING, so it cannot be wrapped in `<GatedPrice>` — this is
|
|
41
|
+
* the third form of the guest-price gate, read as a value at the call site
|
|
42
|
+
* (CLAUDE.md § "Guest price gating"). Default `true` so no existing caller
|
|
43
|
+
* silently loses the total; the signed-out callers pass `false`.
|
|
44
|
+
*
|
|
45
|
+
* The COUNT is deliberately still shown. It is not money, and a guest who
|
|
46
|
+
* just added something is owed confirmation that it landed.
|
|
47
|
+
*/
|
|
48
|
+
includeTotal = true) {
|
|
39
49
|
const itemLabel = detail.productTitle ? `"${detail.productTitle}"` : "Item";
|
|
40
|
-
|
|
50
|
+
const count = `${detail.itemCount} item${detail.itemCount !== 1 ? "s" : ""}`;
|
|
51
|
+
const total = includeTotal ? `, ${formatCurrency(detail.totalValue)} total` : "";
|
|
52
|
+
return `${itemLabel} added to cart — ${count}${total}`;
|
|
41
53
|
}
|
|
42
54
|
function dispatchCartOpsChange() {
|
|
43
55
|
if (typeof window === "undefined")
|
|
@@ -43,30 +43,7 @@ import { getProviders } from "../../../../../contracts/index.js";
|
|
|
43
43
|
import { categoriesRepository } from "../../../repository/categories.repository.js";
|
|
44
44
|
import { isAdminUser } from "../../../../auth/role-predicates.js";
|
|
45
45
|
import { priceGuidanceOnSellerBasis, describePriceGuidanceBasis, } from "../../../../../_internal/shared/features/categories/price-guidance.js";
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Nearest-first ancestor walk for `priceGuidance`, same shape as
|
|
49
|
-
* `description-template/route.ts`'s `resolveInherited` — the leaf's own band
|
|
50
|
-
* wins, and only a leaf with none falls back to the nearest ancestor that has
|
|
51
|
-
* one. ONE bounded `findByIds`, not a sequential walk: a seller form hitting
|
|
52
|
-
* this on every category change should not pay up to eight round trips for it.
|
|
53
|
-
*/
|
|
54
|
-
async function resolveInheritedGuidance(leaf) {
|
|
55
|
-
if (leaf.productDefaults?.priceGuidance)
|
|
56
|
-
return leaf.productDefaults.priceGuidance;
|
|
57
|
-
const parentIds = (leaf.parentIds ?? []).slice(0, MAX_ANCESTORS);
|
|
58
|
-
if (parentIds.length === 0)
|
|
59
|
-
return undefined;
|
|
60
|
-
const nearestFirst = [...parentIds].reverse();
|
|
61
|
-
const ancestors = await categoriesRepository.findByIds(nearestFirst);
|
|
62
|
-
const byId = new Map(ancestors.map((a) => [a.id, a]));
|
|
63
|
-
for (const pid of nearestFirst) {
|
|
64
|
-
const guidance = byId.get(pid)?.productDefaults?.priceGuidance;
|
|
65
|
-
if (guidance)
|
|
66
|
-
return guidance;
|
|
67
|
-
}
|
|
68
|
-
return undefined;
|
|
69
|
-
}
|
|
46
|
+
import { resolveInheritedGuidance, } from "../../../../../_internal/server/features/categories/price-guidance-resolve.js";
|
|
70
47
|
export const GET = createRouteHandler({
|
|
71
48
|
roles: ["seller", "admin", "moderator"],
|
|
72
49
|
handler: async ({ user, params }) => {
|
|
@@ -5,6 +5,7 @@ import Link from "next/link.js";
|
|
|
5
5
|
import { categoriesRepository, productRepository, } from "../../../repositories/index.js";
|
|
6
6
|
import { ROUTES } from "../../../next/routing/route-map.js";
|
|
7
7
|
import { Anchor, Container, Div, Dl, Dt, Dd, Heading, Main, Nav, Section, Span, Stack, Text } from "../../../ui/index.js";
|
|
8
|
+
import { OwnerQuickActions } from "../../admin/components/OwnerQuickActions.js";
|
|
8
9
|
import { DynamicBgDiv } from "../../../ui/components/DynamicBgDiv.js";
|
|
9
10
|
import { MediaImage } from "../../media/MediaImage.js";
|
|
10
11
|
import { RichTextRenderer } from "../../../ui/rich-text/RichTextRenderer.js";
|
|
@@ -60,7 +61,7 @@ export async function BrandDetailPageView({ slug, initialBrand }) {
|
|
|
60
61
|
// therefore never hide, however empty the brand was.
|
|
61
62
|
brandName
|
|
62
63
|
? listingTabCounts(CATEGORY_PAGE_TABS, { brandName })
|
|
63
|
-
: Promise.resolve({}),
|
|
64
|
+
: Promise.resolve({ available: {}, total: {} }),
|
|
64
65
|
// SB-UNI-D — bundles are categoryType:"bundle" rows on the categories
|
|
65
66
|
// collection, tagged to a brand via the real brandSlug field (added
|
|
66
67
|
// 2026-08-21 — previously a fragile seo.keywords string-match heuristic).
|
|
@@ -108,15 +109,23 @@ export async function BrandDetailPageView({ slug, initialBrand }) {
|
|
|
108
109
|
// Keyed by tabSlug. `bundles` is overridden with the brand-scoped figure —
|
|
109
110
|
// `listingTabCounts` cannot narrow bundles by brand (a bundle's brand lives on
|
|
110
111
|
// `brandSlug`, which this page already filters on locally).
|
|
112
|
+
// Badge = available scope; hiding + the header pill = total. See the same
|
|
113
|
+
// split in `CategoryDetailPageView` and `ListingTabCountResult`.
|
|
111
114
|
const counts = {
|
|
112
|
-
...tabCounts,
|
|
113
|
-
products: tabCounts.products ?? brand?.metrics?.productCount ?? 0,
|
|
114
|
-
auctions: tabCounts.auctions ?? brand?.metrics?.auctionCount ?? 0,
|
|
115
|
+
...tabCounts.available,
|
|
116
|
+
products: tabCounts.available.products ?? brand?.metrics?.productCount ?? 0,
|
|
117
|
+
auctions: tabCounts.available.auctions ?? brand?.metrics?.auctionCount ?? 0,
|
|
118
|
+
bundles: brandBundles.length,
|
|
119
|
+
};
|
|
120
|
+
const totals = {
|
|
121
|
+
...tabCounts.total,
|
|
122
|
+
products: tabCounts.total.products ?? brand?.metrics?.productCount ?? 0,
|
|
123
|
+
auctions: tabCounts.total.auctions ?? brand?.metrics?.auctionCount ?? 0,
|
|
115
124
|
bundles: brandBundles.length,
|
|
116
125
|
};
|
|
117
126
|
// Header total — `?? 0` is right here (a pill just doesn't render), unlike the
|
|
118
127
|
// tab bar where an unknown count must keep its tab visible.
|
|
119
|
-
const totalItems = Object.values(
|
|
128
|
+
const totalItems = Object.values(totals).reduce((sum, n) => sum + (n ?? 0), 0);
|
|
120
129
|
/*
|
|
121
130
|
* ── D4: structured data on the brand page ───────────────────────────────
|
|
122
131
|
*
|
|
@@ -168,5 +177,5 @@ export async function BrandDetailPageView({ slug, initialBrand }) {
|
|
|
168
177
|
const ldFaq = brand?.faqs && brand.faqs.length > 0
|
|
169
178
|
? faqJsonLd(brand.faqs.map((f) => ({ question: f.question, answer: f.answer })))
|
|
170
179
|
: null;
|
|
171
|
-
return (_jsxs(Main, { children: [_jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify(ldBreadcrumb) } }), _jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify(ldCollection) } }), ldFaq && (_jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify(ldFaq) } })), _jsxs(Section, { className: `relative ${__O.hidden} ${hasCover ? "min-h-[220px] md:min-h-[280px]" : "bg-[var(--appkit-color-bg)]"}`, children: [hasCover && (_jsxs(_Fragment, { children: [_jsx(Div, { className: "absolute inset-0", children: _jsx(MediaImage, { src: coverImage, alt: "", size: "banner" }) }), _jsx(Div, { surface: "overlay-md", className: "absolute inset-0" })] })), !hasCover && (_jsx(DynamicBgDiv, { color: brandColor, className: "absolute inset-0 opacity-10" })), _jsxs(Div, { className: "relative z-10 max-w-7xl mx-auto", paddingY: hasCover ? "y-3xl" : "y-xl", padding: "x-md", children: [_jsxs(Nav, { layout: "flex", gap: "2xs", textSize: "sm", className: "mb-4", "aria-label": "Breadcrumb", children: [_jsx(Link, { href: String(ROUTES.HOME), className: hasCover ? "text-white/70 hover:text-white transition-colors" : "text-[var(--appkit-color-text-muted)] hover:text-primary-600 dark:hover:text-primary-400 transition-colors", children: "Home" }), _jsx(Span, { className: hasCover ? "text-white/40" : "text-zinc-400", children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.BRANDS), className: hasCover ? "text-white/70 hover:text-white transition-colors" : "text-[var(--appkit-color-text-muted)] hover:text-primary-600 dark:hover:text-primary-400 transition-colors", children: "Brands" }), _jsx(Span, { className: hasCover ? "text-white/40" : "text-zinc-400", children: "/" }), _jsx(Span, { weight: "medium", className: hasCover ? "text-white" : "text-[var(--appkit-color-text)]", children: brand?.name ?? slug })] }), _jsxs(Row, { className: "mb-3", align: "center", gap: "md", children: [brand?.display?.icon && (_jsx(Span, { className: "leading-none", size: "5xl", children: brand.display.icon })), _jsxs(_Fragment, { children: [_jsx(Heading, { color: "inverse", level: 1, className: `${hasCover ? "" : "text-[var(--appkit-color-text)]"}`, size: "3xl", mdSize: "4xl", weight: "bold", children: brand?.name ?? slug }), brand?.description && typeof brand.description === "string" && !brand.description.startsWith("{") && (_jsx(Text, { color: hasCover ? "inverse" : "muted", className: `max-w-2xl mt-1 ${hasCover ? "/80" : ""}`, size: "base", children: brand.description }))] })] }), _jsxs(Row, { gap: "sm", wrap: true, className: "mt-3", children: [(counts.products ?? 0) > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: "inverse", size: "xs", weight: "medium", className: `${hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "bg-primary/10 text-primary-700 dark:text-primary-400"}`, rounded: "full", padding: "pill-sm-tall", children: [counts.products.toLocaleString(), " ", counts.products === 1 ? "product" : "products"] })), (counts.auctions ?? 0) > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: hasCover ? "inverse" : "warning", surface: hasCover ? undefined : "warning-surface", size: "xs", weight: "medium", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "", rounded: "full", padding: "pill-sm-tall", children: [counts.auctions.toLocaleString(), " ", counts.auctions === 1 ? "auction" : "auctions"] })), (counts["pre-orders"] ?? 0) > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: hasCover ? "inverse" : "info", surface: hasCover ? undefined : "info-surface", size: "xs", weight: "medium", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "", rounded: "full", padding: "pill-sm-tall", children: [counts["pre-orders"].toLocaleString(), " ", counts["pre-orders"] === 1 ? "pre-order" : "pre-orders"] })), totalItems === 0 && (_jsx(Span, { size: "sm", className: hasCover ? "text-white/60" : "text-zinc-400", children: "No items listed yet" }))] })] })] }), (brand?.brandWebsite || brand?.brandCountry || brand?.brandFounded) && (_jsx(Section, { border: "subtle", surface: "default", className: "border-b", padding: "y-md", children: _jsx(Container, { size: "xl", children: _jsxs(Stack, { gap: "sm", children: [_jsx(Heading, { level: 2, size: "sm", weight: "semibold", color: "muted", transform: "uppercase", children: "About this brand" }), _jsxs(Dl, { divide: "subtle", rounded: "xl", border: "subtle", className: "overflow-hidden max-w-lg", children: [brand?.brandWebsite && (_jsxs(Row, { gap: "md", oddEven: "zebra", surface: "default", padding: "inline", children: [_jsx(Dt, { className: "w-32 flex-shrink-0", color: "primary", weight: "medium", children: "Website" }), _jsx(Dd, { className: "flex-1", children: _jsx(Anchor, { href: brand.brandWebsite, tone: "brand", underline: "hover", children: brand.brandWebsite }) })] })), brand?.brandCountry && (_jsxs(Row, { gap: "md", oddEven: "zebra", surface: "default", padding: "inline", children: [_jsx(Dt, { className: "w-32 flex-shrink-0", color: "primary", weight: "medium", children: "Country" }), _jsx(Dd, { className: "flex-1", color: "muted", children: brand.brandCountry })] })), brand?.brandFounded && (_jsxs(Row, { gap: "md", oddEven: "zebra", surface: "default", padding: "inline", children: [_jsx(Dt, { className: "w-32 flex-shrink-0", color: "primary", weight: "medium", children: "Founded" }), _jsx(Dd, { className: "flex-1", color: "muted", children: brand.brandFounded })] }))] })] }) }) })), brandContentBody && brandContentBody.trim().length > 0 && (_jsx(Section, { padding: "y-lg", children: _jsx(Container, { size: "xl", children: _jsx(RichTextRenderer, { html: brandContentBody }) }) })), _jsx(CategoryHighlightsAndFaqSection, { highlights: brand?.highlights, faqs: brand?.faqs }), _jsx(Section, { padding: "y-lg", children: _jsx(Container, { size: "xl", children: brandName ? (_jsx(BrandDetailTabs, { brandName: brandName, initialProductsData: productsResult ?? undefined, initialBundles: brandBundles, counts: counts, enabledListingTypes: enabledListingTypes(settings), enabledCategoryTypes: enabledCategoryTypes(settings) })) : (_jsx(Text, { paddingY: "3xl", color: "muted", size: "sm", align: "start", children: "Brand not found." })) }) }), groupedListings.length > 0 && (_jsx(Section, { padding: "y-lg", children: _jsx(Container, { size: "xl", children: _jsx(GroupedListingsCarousel, { groups: groupedListings }) }) })), relatedBrands.length > 0 && (_jsx(Section, { border: "subtle", surface: "default", className: "border-t", padding: "y-lg", children: _jsxs(Container, { size: "xl", children: [_jsx(Heading, { level: 2, className: "mb-4", size: "xl", weight: "semibold", children: "Related Brands" }), _jsx(CategoryGrid, { categories: relatedBrands, getHref: (b) => String(ROUTES.PUBLIC.BRAND_DETAIL(b.slug)) })] }) }))] }));
|
|
180
|
+
return (_jsxs(Main, { children: [_jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify(ldBreadcrumb) } }), _jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify(ldCollection) } }), ldFaq && (_jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify(ldFaq) } })), _jsxs(Section, { className: `relative ${__O.hidden} ${hasCover ? "min-h-[220px] md:min-h-[280px]" : "bg-[var(--appkit-color-bg)]"}`, children: [hasCover && (_jsxs(_Fragment, { children: [_jsx(Div, { className: "absolute inset-0", children: _jsx(MediaImage, { src: coverImage, alt: "", size: "banner" }) }), _jsx(Div, { surface: "overlay-md", className: "absolute inset-0" })] })), !hasCover && (_jsx(DynamicBgDiv, { color: brandColor, className: "absolute inset-0 opacity-10" })), _jsxs(Div, { className: "relative z-10 max-w-7xl mx-auto", paddingY: hasCover ? "y-3xl" : "y-xl", padding: "x-md", children: [_jsxs(Nav, { layout: "flex", gap: "2xs", textSize: "sm", className: "mb-4", "aria-label": "Breadcrumb", children: [_jsx(Link, { href: String(ROUTES.HOME), className: hasCover ? "text-white/70 hover:text-white transition-colors" : "text-[var(--appkit-color-text-muted)] hover:text-primary-600 dark:hover:text-primary-400 transition-colors", children: "Home" }), _jsx(Span, { className: hasCover ? "text-white/40" : "text-zinc-400", children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.BRANDS), className: hasCover ? "text-white/70 hover:text-white transition-colors" : "text-[var(--appkit-color-text-muted)] hover:text-primary-600 dark:hover:text-primary-400 transition-colors", children: "Brands" }), _jsx(Span, { className: hasCover ? "text-white/40" : "text-zinc-400", children: "/" }), _jsx(Span, { weight: "medium", className: hasCover ? "text-white" : "text-[var(--appkit-color-text)]", children: brand?.name ?? slug })] }), _jsxs(Row, { className: "mb-3", align: "center", gap: "md", children: [brand?.display?.icon && (_jsx(Span, { className: "leading-none", size: "5xl", children: brand.display.icon })), _jsxs(_Fragment, { children: [brand?.id && (_jsx(OwnerQuickActions, { resource: "BRAND", placement: "detail", className: "mb-2", recordLabel: brand.name ?? slug, editHref: String(ROUTES.ADMIN.BRANDS_EDIT(brand.id)) })), _jsx(Heading, { color: "inverse", level: 1, className: `${hasCover ? "" : "text-[var(--appkit-color-text)]"}`, size: "3xl", mdSize: "4xl", weight: "bold", children: brand?.name ?? slug }), brand?.description && typeof brand.description === "string" && !brand.description.startsWith("{") && (_jsx(Text, { color: hasCover ? "inverse" : "muted", className: `max-w-2xl mt-1 ${hasCover ? "/80" : ""}`, size: "base", children: brand.description }))] })] }), _jsxs(Row, { gap: "sm", wrap: true, className: "mt-3", children: [(counts.products ?? 0) > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: "inverse", size: "xs", weight: "medium", className: `${hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "bg-primary/10 text-primary-700 dark:text-primary-400"}`, rounded: "full", padding: "pill-sm-tall", children: [counts.products.toLocaleString(), " ", counts.products === 1 ? "product" : "products"] })), (counts.auctions ?? 0) > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: hasCover ? "inverse" : "warning", surface: hasCover ? undefined : "warning-surface", size: "xs", weight: "medium", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "", rounded: "full", padding: "pill-sm-tall", children: [counts.auctions.toLocaleString(), " ", counts.auctions === 1 ? "auction" : "auctions"] })), (counts["pre-orders"] ?? 0) > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: hasCover ? "inverse" : "info", surface: hasCover ? undefined : "info-surface", size: "xs", weight: "medium", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "", rounded: "full", padding: "pill-sm-tall", children: [counts["pre-orders"].toLocaleString(), " ", counts["pre-orders"] === 1 ? "pre-order" : "pre-orders"] })), totalItems === 0 && (_jsx(Span, { size: "sm", className: hasCover ? "text-white/60" : "text-zinc-400", children: "No items listed yet" }))] })] })] }), (brand?.brandWebsite || brand?.brandCountry || brand?.brandFounded) && (_jsx(Section, { border: "subtle", surface: "default", className: "border-b", padding: "y-md", children: _jsx(Container, { size: "xl", children: _jsxs(Stack, { gap: "sm", children: [_jsx(Heading, { level: 2, size: "sm", weight: "semibold", color: "muted", transform: "uppercase", children: "About this brand" }), _jsxs(Dl, { divide: "subtle", rounded: "xl", border: "subtle", className: "overflow-hidden max-w-lg", children: [brand?.brandWebsite && (_jsxs(Row, { gap: "md", oddEven: "zebra", surface: "default", padding: "inline", children: [_jsx(Dt, { className: "w-32 flex-shrink-0", color: "primary", weight: "medium", children: "Website" }), _jsx(Dd, { className: "flex-1", children: _jsx(Anchor, { href: brand.brandWebsite, tone: "brand", underline: "hover", children: brand.brandWebsite }) })] })), brand?.brandCountry && (_jsxs(Row, { gap: "md", oddEven: "zebra", surface: "default", padding: "inline", children: [_jsx(Dt, { className: "w-32 flex-shrink-0", color: "primary", weight: "medium", children: "Country" }), _jsx(Dd, { className: "flex-1", color: "muted", children: brand.brandCountry })] })), brand?.brandFounded && (_jsxs(Row, { gap: "md", oddEven: "zebra", surface: "default", padding: "inline", children: [_jsx(Dt, { className: "w-32 flex-shrink-0", color: "primary", weight: "medium", children: "Founded" }), _jsx(Dd, { className: "flex-1", color: "muted", children: brand.brandFounded })] }))] })] }) }) })), brandContentBody && brandContentBody.trim().length > 0 && (_jsx(Section, { padding: "y-lg", children: _jsx(Container, { size: "xl", children: _jsx(RichTextRenderer, { html: brandContentBody }) }) })), _jsx(CategoryHighlightsAndFaqSection, { highlights: brand?.highlights, faqs: brand?.faqs }), _jsx(Section, { padding: "y-lg", children: _jsx(Container, { size: "xl", children: brandName ? (_jsx(BrandDetailTabs, { brandName: brandName, initialProductsData: productsResult ?? undefined, initialBundles: brandBundles, counts: counts, totals: totals, enabledListingTypes: enabledListingTypes(settings), enabledCategoryTypes: enabledCategoryTypes(settings) })) : (_jsx(Text, { paddingY: "3xl", color: "muted", size: "sm", align: "start", children: "Brand not found." })) }) }), groupedListings.length > 0 && (_jsx(Section, { padding: "y-lg", children: _jsx(Container, { size: "xl", children: _jsx(GroupedListingsCarousel, { groups: groupedListings }) }) })), relatedBrands.length > 0 && (_jsx(Section, { border: "subtle", surface: "default", className: "border-t", padding: "y-lg", children: _jsxs(Container, { size: "xl", children: [_jsx(Heading, { level: 2, className: "mb-4", size: "xl", weight: "semibold", children: "Related Brands" }), _jsx(CategoryGrid, { categories: relatedBrands, getHref: (b) => String(ROUTES.PUBLIC.BRAND_DETAIL(b.slug)) })] }) }))] }));
|
|
172
181
|
}
|
|
@@ -10,10 +10,16 @@ export interface BrandDetailTabsProps {
|
|
|
10
10
|
* inventory (Root Cause #59). Keyed by tab id; the previous hand-named shape
|
|
11
11
|
* covered only 5 of 9 tabs (Root Cause #61).
|
|
12
12
|
*/
|
|
13
|
+
/** Badge counts — what the default (available) scope will render. */
|
|
13
14
|
counts?: Record<string, number | undefined>;
|
|
15
|
+
/**
|
|
16
|
+
* Every published row per tab, all availability scopes. Drives hide-at-zero
|
|
17
|
+
* and the "N ended — show all" hint; see `ListingTabCountResult`.
|
|
18
|
+
*/
|
|
19
|
+
totals?: Record<string, number | undefined>;
|
|
14
20
|
/** Enabled listing types (e.g. ["standard","auction"]). When omitted, all tabs shown. */
|
|
15
21
|
enabledListingTypes?: string[];
|
|
16
22
|
/** Enabled category types (e.g. ["category","brand","bundle"]). When omitted, all tabs shown. */
|
|
17
23
|
enabledCategoryTypes?: string[];
|
|
18
24
|
}
|
|
19
|
-
export declare function BrandDetailTabs({ brandName, initialProductsData, initialBundles, counts, enabledListingTypes, enabledCategoryTypes, }: BrandDetailTabsProps): React.JSX.Element;
|
|
25
|
+
export declare function BrandDetailTabs({ brandName, initialProductsData, initialBundles, counts, totals, enabledListingTypes, enabledCategoryTypes, }: BrandDetailTabsProps): React.JSX.Element;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useState } from "react";
|
|
4
4
|
import { EmptyState, Tabs, TabsList, TabsTrigger } from "../../../ui/index.js";
|
|
5
|
+
import { AvailabilityScopeHint } from "../../products/components/AvailabilityScopeHint.js";
|
|
5
6
|
import { CategoryProductsListing } from "./CategoryProductsListing.js";
|
|
6
7
|
import { AuctionsIndexListing } from "../../products/components/AuctionsIndexListing.js";
|
|
7
8
|
import { PreOrdersIndexListing } from "../../pre-orders/components/PreOrdersIndexListing.js";
|
|
@@ -28,6 +29,18 @@ const TAB_TYPE_MAP = {
|
|
|
28
29
|
])),
|
|
29
30
|
bundles: { kind: "category", type: "bundle" },
|
|
30
31
|
};
|
|
32
|
+
/**
|
|
33
|
+
* The listing types a tab spans, for the availability-scope hint. Derived
|
|
34
|
+
* from the same maps the tabs render from, never hand-written (Root Cause
|
|
35
|
+
* #61).
|
|
36
|
+
*/
|
|
37
|
+
function typesForTab(id) {
|
|
38
|
+
const generic = GENERIC_TAB_LISTING_TYPES[id];
|
|
39
|
+
if (generic)
|
|
40
|
+
return generic;
|
|
41
|
+
const mapping = TAB_TYPE_MAP[id];
|
|
42
|
+
return mapping?.kind === "listing" ? [mapping.type] : [];
|
|
43
|
+
}
|
|
31
44
|
/** Listing types rendered by the shared products listing rather than a dedicated one. */
|
|
32
45
|
const GENERIC_TAB_LISTING_TYPES = {
|
|
33
46
|
[pluginFor("standard").tabSlug]: ["standard"],
|
|
@@ -46,8 +59,9 @@ const GENERIC_TAB_LISTING_TYPES = {
|
|
|
46
59
|
*/
|
|
47
60
|
[pluginFor("spares").tabSlug]: ["spares"],
|
|
48
61
|
};
|
|
49
|
-
export function BrandDetailTabs({ brandName, initialProductsData, initialBundles = [], counts, enabledListingTypes, enabledCategoryTypes, }) {
|
|
62
|
+
export function BrandDetailTabs({ brandName, initialProductsData, initialBundles = [], counts, totals, enabledListingTypes, enabledCategoryTypes, }) {
|
|
50
63
|
const countFor = (id) => counts?.[id];
|
|
64
|
+
const totalFor = (id) => totals?.[id] ?? counts?.[id];
|
|
51
65
|
const visibleTabs = CATEGORY_PAGE_TABS.filter((t) => {
|
|
52
66
|
const mapping = TAB_TYPE_MAP[t.id];
|
|
53
67
|
// A tab with no mapping has no render branch below and would show a blank
|
|
@@ -67,7 +81,8 @@ export function BrandDetailTabs({ brandName, initialProductsData, initialBundles
|
|
|
67
81
|
// Hide a tab only when its count is known and explicitly zero. `undefined`
|
|
68
82
|
// now means the count query FAILED (every tab is counted), so the tab stays
|
|
69
83
|
// visible rather than hiding real listings behind a swallowed error.
|
|
70
|
-
|
|
84
|
+
// TOTAL, not the badge — see the same note in CategoryDetailTabs.
|
|
85
|
+
const count = totalFor(t.id);
|
|
71
86
|
return count === undefined || count > 0;
|
|
72
87
|
});
|
|
73
88
|
const firstTabId = (visibleTabs[0]?.id ?? "products");
|
|
@@ -75,5 +90,5 @@ export function BrandDetailTabs({ brandName, initialProductsData, initialBundles
|
|
|
75
90
|
if (visibleTabs.length === 0) {
|
|
76
91
|
return (_jsx(EmptyState, { title: "Nothing listed here yet", description: "This brand has no active listings right now. Check back soon." }));
|
|
77
92
|
}
|
|
78
|
-
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: "", brandName: brandName, listingTypes: GENERIC_TAB_LISTING_TYPES[activeTab], initialData: activeTab === pluginFor("standard").tabSlug ? initialProductsData : undefined })), activeTab === pluginFor("auction").tabSlug && (_jsx(AuctionsIndexListing, { brandName: brandName })), activeTab === pluginFor("pre-order").tabSlug && (_jsx(PreOrdersIndexListing, { brandName: brandName })), activeTab === pluginFor("prize-draw").tabSlug && (_jsx(PrizeDrawsIndexListing, { brandName: brandName })), activeTab === "bundles" && (_jsx(CategoryBundlesListing, { initialBundles: initialBundles, brandName: brandName }))] }));
|
|
93
|
+
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))) }) }), _jsx(AvailabilityScopeHint, { total: totalFor(activeTab), available: countFor(activeTab), types: typesForTab(activeTab) }), GENERIC_TAB_LISTING_TYPES[activeTab] && (_jsx(CategoryProductsListing, { categorySlug: "", brandName: brandName, listingTypes: GENERIC_TAB_LISTING_TYPES[activeTab], initialData: activeTab === pluginFor("standard").tabSlug ? initialProductsData : undefined })), activeTab === pluginFor("auction").tabSlug && (_jsx(AuctionsIndexListing, { brandName: brandName })), activeTab === pluginFor("pre-order").tabSlug && (_jsx(PreOrdersIndexListing, { brandName: brandName })), activeTab === pluginFor("prize-draw").tabSlug && (_jsx(PrizeDrawsIndexListing, { brandName: brandName })), activeTab === "bundles" && (_jsx(CategoryBundlesListing, { initialBundles: initialBundles, brandName: brandName }))] }));
|
|
79
94
|
}
|
|
@@ -16,10 +16,11 @@ import { MediaImage } from "../../media/MediaImage.js";
|
|
|
16
16
|
import { ImageLightbox } from "../../../ui/components/ImageLightbox.js";
|
|
17
17
|
import { ROUTES } from "../../../next/routing/route-map.js";
|
|
18
18
|
import { formatCurrency } from "../../../utils/number.formatter.js";
|
|
19
|
+
import { productImageUrls } from "../../products/schemas/firestore.js";
|
|
19
20
|
const PLACEHOLDER_EMOJI = "📦";
|
|
20
21
|
function toGalleryImages(members, canSeePrices) {
|
|
21
22
|
return members.map((p, i) => ({
|
|
22
|
-
src: p.mainImage
|
|
23
|
+
src: p.mainImage || productImageUrls(p.images)[0] || "",
|
|
23
24
|
alt: p.title,
|
|
24
25
|
badge: `#${i + 1}`,
|
|
25
26
|
caption: p.title,
|
|
@@ -46,7 +47,7 @@ export function BundleCollage({ members, onItemClick }) {
|
|
|
46
47
|
return null;
|
|
47
48
|
const galleryImages = toGalleryImages(members, canSeePrices);
|
|
48
49
|
return (_jsxs(_Fragment, { children: [_jsx(Grid, { cols: "categoryCards", gap: "3", children: members.map((p, idx) => {
|
|
49
|
-
const cover = p.mainImage
|
|
50
|
+
const cover = p.mainImage || productImageUrls(p.images)[0];
|
|
50
51
|
const href = String(ROUTES.PUBLIC.PRODUCT_DETAIL?.(p.slug ?? p.id) ?? "#");
|
|
51
52
|
return (_jsxs(Div, { layout: "flex-col", className: "group relative overflow-hidden border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] transition-transform hover:scale-[1.01]", rounded: "lg", children: [_jsx(Button, { variant: "ghost", type: "button", onClick: makeBundleItemClickHandler(p, idx, onItemClick, setLightboxIndex), paddingX: "none", paddingY: "none", rounded: "none", className: "relative aspect-square w-full overflow-hidden focus-visible:ring-2 focus-visible:ring-[var(--appkit-color-primary)]", "aria-label": `View ${p.title} in lightbox`, children: _jsxs(Div, { className: "relative aspect-square w-full", children: [cover ? (_jsx(MediaImage, { src: cover, alt: p.title, size: "card", className: "transition-transform group-hover:scale-105" })) : (_jsx(Row, { textSize: "3xl", surface: "muted", className: "absolute inset-0", align: "center", justify: "center", children: PLACEHOLDER_EMOJI })), _jsxs(Div, { textWeight: "semibold", textSize: "xs", surface: "overlay-xl", padding: "chip-2xs", className: "absolute left-2 top-2 text-white", rounded: "default", children: ["#", idx + 1] })] }) }), _jsxs(Div, { padding: "xs", children: [_jsx(Link, { href: href, className: "line-clamp-2 text-[length:var(--appkit-text-sm)] font-medium hover:underline", children: p.title }), _jsx(Text, { className: "mt-0.5 text-[var(--appkit-color-text-muted)]", size: "xs", children: _jsx(GatedPrice, { children: formatCurrency(p.price ?? 0, p.currency ?? "INR") }) })] })] }, p.id));
|
|
52
53
|
}) }), _jsx(ImageLightbox, { images: galleryImages, activeIndex: lightboxIndex, onClose: () => setLightboxIndex(null), onNavigate: (i) => setLightboxIndex(i), showThumbnails: true })] }));
|
|
@@ -18,6 +18,7 @@ import { Badge, Button, Checkbox, Div, Input, Row, Stack, Text } from "../../../
|
|
|
18
18
|
import { BUNDLE_MAX_ITEMS, BUNDLE_MIN_ITEMS, } from "../../../_internal/shared/features/categories/bundle-config.js";
|
|
19
19
|
import { BUNDLE_COPY } from "../../../_internal/shared/features/categories/bundle-copy.js";
|
|
20
20
|
import { PRODUCT_ENDPOINTS } from "../../../constants/api-endpoints.js";
|
|
21
|
+
import { productImageUrls } from "../../products/schemas/firestore.js";
|
|
21
22
|
const __P = {
|
|
22
23
|
p3: "p-[var(--appkit-space-3)]",
|
|
23
24
|
};
|
|
@@ -137,11 +138,9 @@ export async function defaultBundleItemsFetch(query) {
|
|
|
137
138
|
return {
|
|
138
139
|
id,
|
|
139
140
|
title,
|
|
140
|
-
image: typeof rec.mainImage === "string"
|
|
141
|
+
image: typeof rec.mainImage === "string" && rec.mainImage
|
|
141
142
|
? rec.mainImage
|
|
142
|
-
:
|
|
143
|
-
? rec.images[0]
|
|
144
|
-
: undefined,
|
|
143
|
+
: productImageUrls(rec.images)[0],
|
|
145
144
|
price: typeof rec.price === "number" ? rec.price : undefined,
|
|
146
145
|
storeId: typeof rec.storeId === "string" ? rec.storeId : undefined,
|
|
147
146
|
};
|