@mohasinac/appkit 4.53.2 → 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/server/features/auth/capabilities.js +7 -5
- package/dist/_internal/server/features/checkout/supplier-identity.js +28 -6
- package/dist/_internal/server/features/products/listing-tab-counts.js +81 -25
- package/dist/_internal/server/features/search/index-build/sources.js +43 -2
- 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/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 +21 -0
- package/dist/constants/api-endpoints.js +7 -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/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/components/BrandDetailPageView.js +2 -1
- package/dist/features/categories/components/CategoryDetailPageView.js +2 -1
- 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/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/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/products/api/[id]/route.js +8 -1
- package/dist/features/products/api/route.js +16 -4
- package/dist/features/products/components/AvailabilityScopeHint.js +1 -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 +27 -2
- 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/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/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/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/tester-checklist-seed-data.js +403 -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/server.d.ts +4 -8
- package/dist/server.js +8 -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/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
|
@@ -269,11 +269,11 @@ export const PART_SLOT_CATEGORY = {
|
|
|
269
269
|
// Burst — a driver with the disc moulded in is still a driver.
|
|
270
270
|
"disc-integrated-driver": "category-burst-drivers",
|
|
271
271
|
// Beyblade X — the CX Custom line, which has its own node.
|
|
272
|
-
"lock-chip": "category-x-
|
|
273
|
-
"main-blade": "category-x-
|
|
274
|
-
"assist-blade": "category-x-
|
|
275
|
-
"over-blade": "category-x-
|
|
276
|
-
"metal-blade": "category-x-
|
|
272
|
+
"lock-chip": "category-x-lock-chips",
|
|
273
|
+
"main-blade": "category-x-main-blades",
|
|
274
|
+
"assist-blade": "category-x-assist-blades",
|
|
275
|
+
"over-blade": "category-x-over-blades",
|
|
276
|
+
"metal-blade": "category-x-metal-blades",
|
|
277
277
|
"gear-chip": "category-x-custom-cx",
|
|
278
278
|
// Beyblade X — integrated parts file under the half a buyer shops for.
|
|
279
279
|
"ratchet-integrated-blade": "category-x-blades",
|
|
@@ -290,7 +290,7 @@ export const PART_SLOT_CATEGORY = {
|
|
|
290
290
|
"evolution-gear": "category-original-engine-gears",
|
|
291
291
|
"bit-protector": "category-original-bit-chips",
|
|
292
292
|
cew: "category-original-weight-disks",
|
|
293
|
-
"support-parts": "category-original-parts",
|
|
293
|
+
"support-parts": "category-original-support-parts",
|
|
294
294
|
};
|
|
295
295
|
/** Every slot key, for an index page that must not hand-write the list. */
|
|
296
296
|
export const ALL_PART_SLOTS = Object.values(PART_SLOT_VALUES);
|
|
@@ -23,5 +23,15 @@ export interface MarketplacePreorderCardProps {
|
|
|
23
23
|
onAddToCart?: (product: MarketplacePreorderCardData) => void;
|
|
24
24
|
wishlistActions?: WishlistToggleActions;
|
|
25
25
|
labels?: MarketplacePreorderCardLabels;
|
|
26
|
+
/**
|
|
27
|
+
* Owner-only pencil / clone / bin. Pass `<OwnerQuickActions>`; it renders
|
|
28
|
+
* nothing for a visitor who does not own the record.
|
|
29
|
+
*
|
|
30
|
+
* 🛑 Rendered OUTSIDE this card's own anchor, as a sibling. A `<button>`
|
|
31
|
+
* inside an `<a>` is invalid HTML that browsers resolve by closing the
|
|
32
|
+
* anchor early, which breaks the CARD's navigation rather than just the
|
|
33
|
+
* button's — `stopPropagation` stops a click but cannot fix a parse.
|
|
34
|
+
*/
|
|
35
|
+
renderOwnerActions?: () => React.ReactNode;
|
|
26
36
|
}
|
|
27
|
-
export declare function MarketplacePreorderCard({ product, className, variant, selectable, isSelected, onSelect, inWishlist: initialInWishlist, href, hrefBuilder, onNavigate, onAddToCart, wishlistActions, labels, }: MarketplacePreorderCardProps): import("react").JSX.Element;
|
|
37
|
+
export declare function MarketplacePreorderCard({ product, className, variant, selectable, isSelected, onSelect, inWishlist: initialInWishlist, href, hrefBuilder, onNavigate, onAddToCart, wishlistActions, labels, renderOwnerActions, }: MarketplacePreorderCardProps): import("react").JSX.Element;
|
|
@@ -14,7 +14,7 @@ const CLS_PREORDER_BADGE = "inline-flex items-center rounded-full bg-warning-sol
|
|
|
14
14
|
const CLS_SALE_BADGE = "rounded-full bg-success-solid px-[var(--appkit-space-2)] py-[var(--appkit-space-0-5)] text-[length:var(--appkit-text-xs)] font-bold text-success-on-solid";
|
|
15
15
|
const CLS_TRENDING_BADGE = "rounded-full bg-indigo-600 px-[var(--appkit-space-2)] py-[var(--appkit-space-0-5)] text-[length:var(--appkit-text-xs)] font-bold text-white";
|
|
16
16
|
const CLS_LIMITED_BADGE = "rounded-full bg-info-solid px-[var(--appkit-space-2)] py-[var(--appkit-space-0-5)] text-[length:var(--appkit-text-xs)] font-bold text-info-on-solid";
|
|
17
|
-
import { BaseListingCard, Button, GatedPrice, RichText, Row, Span, Stack, Text, TextLink } from "../../../ui/index.js";
|
|
17
|
+
import { Div, BaseListingCard, Button, GatedPrice, RichText, Row, Span, Stack, Text, TextLink } from "../../../ui/index.js";
|
|
18
18
|
import { normalizeRichTextHtml } from "../../../utils/string.formatter.js";
|
|
19
19
|
import { PreorderBadge } from "./PreorderCard.js";
|
|
20
20
|
import { WishlistHeartButton } from "../../wishlist/components/WishlistHeartButton.js";
|
|
@@ -34,7 +34,7 @@ function resolveHref(product, href, hrefBuilder) {
|
|
|
34
34
|
return hrefBuilder(product);
|
|
35
35
|
return ROUTES.PUBLIC.PRE_ORDER_DETAIL(product.slug ?? product.id);
|
|
36
36
|
}
|
|
37
|
-
export function MarketplacePreorderCard({ product, className = "", variant = "grid", selectable = false, isSelected = false, onSelect, inWishlist: initialInWishlist = false, href, hrefBuilder, onNavigate, onAddToCart, wishlistActions, labels, }) {
|
|
37
|
+
export function MarketplacePreorderCard({ product, className = "", variant = "grid", selectable = false, isSelected = false, onSelect, inWishlist: initialInWishlist = false, href, hrefBuilder, onNavigate, onAddToCart, wishlistActions, labels, renderOwnerActions, }) {
|
|
38
38
|
const router = useRouter();
|
|
39
39
|
const mergedLabels = { ...DEFAULT_LABELS, ...labels };
|
|
40
40
|
const detailHref = resolveHref(product, href, hrefBuilder);
|
|
@@ -61,7 +61,7 @@ export function MarketplacePreorderCard({ product, className = "", variant = "gr
|
|
|
61
61
|
void normalizeError(_err);
|
|
62
62
|
}
|
|
63
63
|
}, [toggle]);
|
|
64
|
-
|
|
64
|
+
const card = (_jsxs(BaseListingCard, { isSelected: isSelected, variant: variant, className: className, onMouseDown: !isSelected ? longPress.onMouseDown : undefined, onMouseUp: !isSelected ? longPress.onMouseUp : undefined, onMouseLeave: !isSelected ? longPress.onMouseLeave : undefined, onTouchStart: !isSelected ? longPress.onTouchStart : undefined, onTouchEnd: !isSelected ? longPress.onTouchEnd : undefined, onTouchCancel: !isSelected ? longPress.onTouchCancel : undefined, children: [_jsxs(BaseListingCard.Hero, { aspect: "square", variant: variant, children: [_jsx(TextLink, { href: detailHref, className: "absolute inset-0 block", children: _jsx(MediaImage, { src: product.mainImage, alt: product.title, size: "card", fallback: "Product image" }) }), _jsxs(Stack, { className: "absolute right-2 top-2", align: "end", gap: "xs", children: [_jsx(Span, { layout: "inline-flex", color: "inverse", size: "xs", weight: "medium", className: "bg-cobalt", rounded: "full", padding: "pill-xs", children: mergedLabels.preOrderBadge }), product.featured && (_jsx(Span, { size: "xs", weight: "medium", className: CLS_PREORDER_BADGE, children: mergedLabels.featuredBadge }))] }), _jsxs(Stack, { className: "absolute left-2 top-2 pointer-events-none", gap: "xs", children: [product.partOfBundleIds && product.partOfBundleIds.length > 0 && (_jsx(Span, { className: CLS_SALE_BADGE, title: product.partOfBundleTitles?.[0] ? `In bundle: ${product.partOfBundleTitles[0]}` : "In a bundle", children: "Bundled" })), product.groupId && (_jsx(Span, { className: CLS_TRENDING_BADGE, title: product.groupTitle ? `Part of set: ${product.groupTitle}` : "Part of a set", children: product.isGroupParent ? "Set Parent" : "In Set" })), product.sublistingCategoryId && (_jsx(Span, { className: CLS_LIMITED_BADGE, title: "Has variants or sub-listings", children: "Has Variants" }))] }), onSelect && (_jsx(BaseListingCard.Checkbox, { selected: isSelected, onSelect: (event) => {
|
|
65
65
|
event.stopPropagation();
|
|
66
66
|
onSelect(product.id, !isSelected);
|
|
67
67
|
}, className: selectable || isSelected ? "opacity-100" : "opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-opacity" }))] }), _jsx(BaseListingCard.Info, { variant: variant, children: variant === "list" ? (
|
|
@@ -69,4 +69,11 @@ export function MarketplacePreorderCard({ product, className = "", variant = "gr
|
|
|
69
69
|
_jsxs(_Fragment, { children: [_jsxs(Row, { className: "min-w-0", align: "start", justify: "between", gap: "sm", children: [_jsx(TextLink, { href: detailHref, className: "min-w-0 flex-1", children: _jsx(Text, { size: "sm", weight: "medium", color: "primary", truncate: 2, children: product.title }) }), wishlistActions && (_jsx(WishlistHeartButton, { size: "inline", inWishlist: inWishlist, addLabel: mergedLabels.addToWishlist, removeLabel: mergedLabels.removeFromWishlist, onToggle: handleWishlist }))] }), _jsx(CardStoreLine, { storeName: product.storeName, storeId: product.storeId }), _jsxs(Row, { align: "center", gap: "sm", wrap: true, children: [_jsx(Text, { className: "text-primary", size: "sm", weight: "bold", children: _jsx(GatedPrice, { children: formatCurrency(product.price, getDefaultCurrency()) }) }), shipDate && _jsx(PreorderBadge, { shipDate: shipDate })] }), _jsx(Button, { type: "button", variant: "primary", size: "sm", textSize: "xs", className: "self-start mt-0.5", onClick: handleNavigate, children: mergedLabels.reserveNow })] })) : (
|
|
70
70
|
/* ── Full grid layout ── */
|
|
71
71
|
_jsxs(_Fragment, { children: [_jsx(TextLink, { href: detailHref, children: _jsx(Text, { size: "sm", weight: "medium", color: "primary", truncate: 2, children: product.title }) }), _jsx(CardStoreLine, { storeName: product.storeName, storeId: product.storeId }), product.description ? (_jsx(RichText, { html: normalizeRichTextHtml(product.description), proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "line-clamp-2 text-[length:var(--appkit-text-xs)] text-[var(--appkit-color-text-muted)]" })) : null, _jsxs(Row, { justify: "between", className: "mt-1", gap: "sm", children: [_jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: _jsx(GatedPrice, { children: formatCurrency(product.price, getDefaultCurrency()) }) }), shipDate && _jsx(PreorderBadge, { shipDate: shipDate })] }), _jsxs(Row, { justify: "between", className: "mt-auto pt-[var(--appkit-space-2)]", gap: "sm", children: [_jsx(Button, { type: "button", variant: "primary", size: "sm", textSize: "xs", className: "flex-1", onClick: handleNavigate, children: mergedLabels.reserveNow }), onAddToCart ? (_jsx(Button, { type: "button", variant: "outline", size: "sm", textSize: "xs", className: "flex-1", onClick: (e) => { e.stopPropagation(); onAddToCart(product); }, children: mergedLabels.addToCart })) : null, wishlistActions ? (_jsx(WishlistHeartButton, { size: "inline", inWishlist: inWishlist, addLabel: mergedLabels.addToWishlist, removeLabel: mergedLabels.removeFromWishlist, onToggle: handleWishlist })) : null] })] })) })] }));
|
|
72
|
+
if (!renderOwnerActions)
|
|
73
|
+
return card;
|
|
74
|
+
/*
|
|
75
|
+
* Sibling of the card, never a child — see the prop's docstring. `z-20`
|
|
76
|
+
* clears this card's own overlays, which all sit at `z-10`.
|
|
77
|
+
*/
|
|
78
|
+
return (_jsxs(Div, { className: "relative h-full", children: [card, _jsx(Div, { className: "absolute right-2 bottom-2 z-20", children: renderOwnerActions() })] }));
|
|
72
79
|
}
|
|
@@ -13,6 +13,7 @@ import { NextResponse } from "next/server.js";
|
|
|
13
13
|
import { z } from "zod";
|
|
14
14
|
import { getProviders } from "../../../../contracts/index.js";
|
|
15
15
|
import { createRouteHandler } from "../../../../next/api/routeHandler.js";
|
|
16
|
+
import { ALL_LISTING_TYPES } from "../../../../_internal/shared/listing-types/feature-flags.js";
|
|
16
17
|
import { mediaFieldSchema } from "../../../media/types/index.js";
|
|
17
18
|
import { storeRepository } from "../../../stores/repository/store.repository.js";
|
|
18
19
|
// SB-UNI-V — bundlesRepository deleted; bundle stock-sync moves to the
|
|
@@ -55,8 +56,14 @@ const productUpdateSchema = z
|
|
|
55
56
|
isPromoted: z.boolean().optional(),
|
|
56
57
|
allowShipBeforeEmiComplete: z.boolean().optional(),
|
|
57
58
|
printMeta: printMetaSchema.optional(),
|
|
59
|
+
/*
|
|
60
|
+
* 🛑 DERIVED from `ALL_LISTING_TYPES`, never restated. This literal was
|
|
61
|
+
* wrong in both directions: `spares` missing (so the type could not be
|
|
62
|
+
* PATCHed onto a listing at all) and `bundle` still present (so a caller
|
|
63
|
+
* could write a value no registry has a branch for). Root Cause #61.
|
|
64
|
+
*/
|
|
58
65
|
listingType: z
|
|
59
|
-
.enum(
|
|
66
|
+
.enum(ALL_LISTING_TYPES)
|
|
60
67
|
.optional(),
|
|
61
68
|
media: z.array(mediaFieldSchema).optional(),
|
|
62
69
|
})
|
|
@@ -16,6 +16,7 @@ import { NextResponse } from "next/server.js";
|
|
|
16
16
|
import { z } from "zod";
|
|
17
17
|
import { getProviders } from "../../../contracts/index.js";
|
|
18
18
|
import { createRouteHandler } from "../../../next/api/routeHandler.js";
|
|
19
|
+
import { ALL_LISTING_TYPES } from "../../../_internal/shared/listing-types/feature-flags.js";
|
|
19
20
|
import { mediaFieldSchema } from "../../media/types/index.js";
|
|
20
21
|
import { printMetaSchema } from "../schemas/index.js";
|
|
21
22
|
import { sanitizeProductsForPublic } from "../utils/sanitize.js";
|
|
@@ -50,11 +51,22 @@ const productMutateSchema = z
|
|
|
50
51
|
sellerName: z.string().optional(),
|
|
51
52
|
sellerEmail: z.string().email().optional(),
|
|
52
53
|
slug: z.string().optional(),
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
/*
|
|
55
|
+
* 🛑 DERIVED from `ALL_LISTING_TYPES`, never restated.
|
|
56
|
+
*
|
|
57
|
+
* The literal that used to sit here is the cautionary tale for why. Its
|
|
58
|
+
* comment correctly explained that `bundle` is absent because SB-UNI-D
|
|
59
|
+
* moved bundles onto `categories` — so the dead value had been noticed and
|
|
60
|
+
* removed — and the list was STILL wrong, because `spares` was added to the
|
|
61
|
+
* union on 2026-10-19 and never reached here. A fix for one half of the
|
|
62
|
+
* drift, documented in a comment, with the other half drifting one field
|
|
63
|
+
* over underneath it.
|
|
64
|
+
*
|
|
65
|
+
* Deriving removes both failure modes at once: a dead value cannot survive
|
|
66
|
+
* and a new member cannot be forgotten. Root Cause #61.
|
|
67
|
+
*/
|
|
56
68
|
listingType: z
|
|
57
|
-
.enum(
|
|
69
|
+
.enum(ALL_LISTING_TYPES)
|
|
58
70
|
.optional(),
|
|
59
71
|
media: z.array(mediaFieldSchema).optional(),
|
|
60
72
|
})
|
|
@@ -21,7 +21,7 @@ export function AvailabilityScopeHint({ total, available, types, }) {
|
|
|
21
21
|
// availabilityTabsFor()[1] is the unavailable scope — "Sold", "Ended" or
|
|
22
22
|
// "Sold & Ended", already derived per type.
|
|
23
23
|
const noun = (availabilityTabsFor(types)[1]?.label ?? "Sold").toLowerCase();
|
|
24
|
-
return (_jsxs(Row, { align: "center", gap: "sm", padding: "sm", surface: "muted", rounded: "lg", border: "subtle", className: "mb-4
|
|
24
|
+
return (_jsxs(Row, { align: "center", gap: "sm", padding: "sm", surface: "muted", rounded: "lg", border: "subtle", wrap: true, className: "mb-4", children: [_jsx(Text, { size: "sm", color: "muted", children: hidden === 1
|
|
25
25
|
? `1 listing here is ${noun} and hidden by the Available filter.`
|
|
26
26
|
: `${hidden} listings here are ${noun} and hidden by the Available filter.` }), _jsx(Button, { variant: "ghost", size: "sm", onClick: () => table.set(TABLE_KEYS.AVAILABILITY, AVAILABILITY_VALUES.ALL), children: "Show all" })] }));
|
|
27
27
|
}
|
|
@@ -14,5 +14,11 @@ export interface InteractiveProductCardProps {
|
|
|
14
14
|
onToggleWishlist?: (productId: string) => void;
|
|
15
15
|
onAddToCart?: (product: ProductItem) => void;
|
|
16
16
|
onBuyNow?: (product: ProductItem) => void;
|
|
17
|
+
/**
|
|
18
|
+
* Owner-only pencil / clone / bin. Forwarded to ProductCard, which renders
|
|
19
|
+
* it OUTSIDE its own and this component's `<Link>` wrappers — see its return
|
|
20
|
+
* statement. Renders nothing for a visitor who does not own the listing.
|
|
21
|
+
*/
|
|
22
|
+
renderOwnerActions?: () => React.ReactNode;
|
|
17
23
|
}
|
|
18
|
-
export declare function InteractiveProductCard({ product, href, className, selectable, isSelected, onSelect, isWishlisted, onToggleWishlist, onAddToCart, onBuyNow, }: InteractiveProductCardProps): React.JSX.Element;
|
|
24
|
+
export declare function InteractiveProductCard({ product, href, className, selectable, isSelected, onSelect, isWishlisted, onToggleWishlist, onAddToCart, onBuyNow, renderOwnerActions, }: InteractiveProductCardProps): React.JSX.Element;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import Link from "next/link.js";
|
|
3
3
|
import { ProductCard } from "./ProductGrid.js";
|
|
4
|
-
|
|
4
|
+
import { Div } from "../../../ui/components/Div.js";
|
|
5
|
+
export function InteractiveProductCard({ product, href, className, selectable, isSelected, onSelect, isWishlisted = false, onToggleWishlist, onAddToCart, onBuyNow, renderOwnerActions, }) {
|
|
5
6
|
// When selection is enabled, render ProductCard directly so its built-in
|
|
6
7
|
// checkbox + long-press (BaseListingCard.Checkbox) work. `href` is still
|
|
7
8
|
// forwarded here (not just in the plain-Link branch below) — ProductCard's
|
|
@@ -12,7 +13,18 @@ export function InteractiveProductCard({ product, href, className, selectable, i
|
|
|
12
13
|
// `onSelect` is provided at all (see Root Cause Pattern: selectable-card
|
|
13
14
|
// navigation must not depend on a selection callback being present).
|
|
14
15
|
if (onSelect) {
|
|
15
|
-
return (_jsx(ProductCard, { product: product, href: href, className: className, isWishlisted: isWishlisted, onAddToWishlist: onToggleWishlist, onAddToCart: onAddToCart, onBuyNow: onBuyNow, selectionMode: selectable, isSelected: isSelected, onSelect: (id) => onSelect(id, !isSelected) }));
|
|
16
|
+
return (_jsx(ProductCard, { product: product, href: href, className: className, isWishlisted: isWishlisted, onAddToWishlist: onToggleWishlist, onAddToCart: onAddToCart, onBuyNow: onBuyNow, selectionMode: selectable, isSelected: isSelected, onSelect: (id) => onSelect(id, !isSelected), renderOwnerActions: renderOwnerActions }));
|
|
16
17
|
}
|
|
17
|
-
|
|
18
|
+
/*
|
|
19
|
+
* 🛑 The owner toolbar is a SIBLING of this Link, not a child of it.
|
|
20
|
+
*
|
|
21
|
+
* ProductCard already keeps the toolbar outside its OWN anchor, but this
|
|
22
|
+
* branch adds a second one around the whole card — so handing the slot down
|
|
23
|
+
* would still nest buttons inside an `<a>`. Rendering it here, over a
|
|
24
|
+
* relative wrapper, is the only placement that escapes both.
|
|
25
|
+
*/
|
|
26
|
+
const card = (_jsx(Link, { href: href, className: "block", children: _jsx(ProductCard, { product: product, className: className, isWishlisted: isWishlisted, onAddToWishlist: onToggleWishlist, onAddToCart: onAddToCart, onBuyNow: onBuyNow }) }));
|
|
27
|
+
if (!renderOwnerActions)
|
|
28
|
+
return card;
|
|
29
|
+
return (_jsxs(Div, { className: "relative h-full", children: [card, _jsx(Div, { className: "absolute right-2 top-14 z-20", children: renderOwnerActions() })] }));
|
|
18
30
|
}
|
|
@@ -17,5 +17,15 @@ export interface MarketplaceBundleCardProps {
|
|
|
17
17
|
hrefBuilder?: (bundle: MarketplaceBundleCardData) => string;
|
|
18
18
|
onNavigate?: (href: string) => void;
|
|
19
19
|
labels?: MarketplaceBundleCardLabels;
|
|
20
|
+
/**
|
|
21
|
+
* Owner-only pencil / clone / bin. Pass `<OwnerQuickActions>`; it renders
|
|
22
|
+
* nothing for a visitor who does not own the record.
|
|
23
|
+
*
|
|
24
|
+
* 🛑 Rendered OUTSIDE this card's own anchor, as a sibling. A `<button>`
|
|
25
|
+
* inside an `<a>` is invalid HTML that browsers resolve by closing the
|
|
26
|
+
* anchor early, which breaks the CARD's navigation rather than just the
|
|
27
|
+
* button's — `stopPropagation` stops a click but cannot fix a parse.
|
|
28
|
+
*/
|
|
29
|
+
renderOwnerActions?: () => React.ReactNode;
|
|
20
30
|
}
|
|
21
|
-
export declare function MarketplaceBundleCard({ bundle, className, variant, selectable, isSelected, onSelect, href, hrefBuilder, onNavigate, labels, }: MarketplaceBundleCardProps): import("react").JSX.Element;
|
|
31
|
+
export declare function MarketplaceBundleCard({ bundle, className, variant, selectable, isSelected, onSelect, href, hrefBuilder, onNavigate, labels, renderOwnerActions, }: MarketplaceBundleCardProps): import("react").JSX.Element;
|
|
@@ -27,7 +27,7 @@ function resolveHref(bundle, href, hrefBuilder) {
|
|
|
27
27
|
return hrefBuilder(bundle);
|
|
28
28
|
return ROUTES.PUBLIC.BUNDLE_DETAIL(bundle.slug);
|
|
29
29
|
}
|
|
30
|
-
export function MarketplaceBundleCard({ bundle, className = "", variant = "grid", selectable = false, isSelected = false, onSelect, href, hrefBuilder, onNavigate, labels, }) {
|
|
30
|
+
export function MarketplaceBundleCard({ bundle, className = "", variant = "grid", selectable = false, isSelected = false, onSelect, href, hrefBuilder, onNavigate, labels, renderOwnerActions, }) {
|
|
31
31
|
const router = useRouter();
|
|
32
32
|
const mergedLabels = { ...DEFAULT_LABELS, ...labels };
|
|
33
33
|
const detailHref = resolveHref(bundle, href, hrefBuilder);
|
|
@@ -49,7 +49,7 @@ export function MarketplaceBundleCard({ bundle, className = "", variant = "grid"
|
|
|
49
49
|
}
|
|
50
50
|
router.push(String(detailHref));
|
|
51
51
|
}, [detailHref, onNavigate, router]);
|
|
52
|
-
|
|
52
|
+
const card = (_jsxs(BaseListingCard, { isSelected: isSelected, variant: variant, className: className, onMouseDown: !isSelected ? longPress.onMouseDown : undefined, onMouseUp: !isSelected ? longPress.onMouseUp : undefined, onMouseLeave: !isSelected ? longPress.onMouseLeave : undefined, onTouchStart: !isSelected ? longPress.onTouchStart : undefined, onTouchEnd: !isSelected ? longPress.onTouchEnd : undefined, onTouchCancel: !isSelected ? longPress.onTouchCancel : undefined, children: [_jsxs(BaseListingCard.Hero, { aspect: "square", variant: variant, children: [_jsx(TextLink, { href: String(detailHref), className: "absolute inset-0 block", children: showCollage ? (_jsx(Div, { layout: "grid", className: `h-full w-full gap-[0.125rem] bg-[var(--appkit-color-surface-muted)] ${collageTiles.length === 2 ? "grid-cols-2 grid-rows-1" : "grid-cols-2 grid-rows-2"}`, children: collageTiles.map((tile, i) => (_jsxs(Div, { className: "relative overflow-hidden bg-[var(--appkit-color-surface-muted)]", children: [_jsx(MediaImage, { src: tile.imageURL, alt: tile.title ?? `${bundle.name} item ${i + 1}`, size: "card" }), i === collageTiles.length - 1 && overflow > 0 && (_jsxs(Row, { textWeight: "semibold", textSize: "sm", surface: "overlay-md", className: "absolute inset-0 text-white", align: "center", justify: "center", children: ["+", overflow] }))] }, `${tile.productId}-${i}`))) })) : cover ? (_jsx(Div, { className: "absolute inset-0 bg-[var(--appkit-color-surface-muted)]", children: _jsx(MediaImage, { src: cover, alt: bundle.name, size: "card" }) })) : (_jsx(Row, { className: "absolute inset-0 bg-[var(--appkit-color-surface-muted)]", align: "center", justify: "center", children: _jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "xs", children: "No image" }) })) }), discount && (_jsx(PricesOnly, { children: _jsx(Span, { size: "xs", weight: "bold", className: "absolute left-2 top-2 bg-success-solid text-success-on-solid", rounded: "full", padding: "pill-xs", children: BUNDLE_COPY.detail.discountBadge(discount.percent) }) })), _jsxs(Stack, { className: "absolute right-2 top-2", align: "end", gap: "xs", children: [_jsx(Span, { size: "xs", weight: "medium", className: CLS_BUNDLE_PILL, children: mergedLabels.bundleBadge }), _jsx(Span, { layout: "inline-flex", color: "inverse", className: `${stock === "out_of_stock" ? "bg-neutral-500 " : CLS_STOCK_OK}`, rounded: "full", padding: "pill-xs", size: "xs", weight: "medium", children: stock === "out_of_stock" ? mergedLabels.outOfStockBadge : mergedLabels.inStockBadge })] }), stock === "out_of_stock" && (_jsx(Row, { surface: "overlay-xs", className: "absolute inset-0", align: "center", justify: "center", children: _jsx(Span, { color: "inverse", size: "xs", weight: "bold", className: "bg-neutral-900/80 tracking-wider", rounded: "default", padding: "pill-md", transform: "uppercase", children: mergedLabels.outOfStockBadge }) })), onSelect && (_jsx(BaseListingCard.Checkbox, { selected: isSelected, onSelect: (event) => {
|
|
53
53
|
event.stopPropagation();
|
|
54
54
|
onSelect(bundle.id, !isSelected);
|
|
55
55
|
}, className: selectable || isSelected
|
|
@@ -58,4 +58,11 @@ export function MarketplaceBundleCard({ bundle, className = "", variant = "grid"
|
|
|
58
58
|
? formatCurrency(price, getDefaultCurrency())
|
|
59
59
|
: "—" }), discount && (_jsx(Text, { size: "xs", color: "muted", className: "line-through", children: formatCurrency(discount.originalTotal, getDefaultCurrency()) }))] }) }) }), _jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "xs", children: mergedLabels.itemsLabel(memberCount) })] }), _jsx(Div, { className: CLS_VIEW_BTN, role: "button", tabIndex: 0, onClick: handleNavigate, onKeyDown: (e) => { if (e.key === "Enter" || e.key === " ")
|
|
60
60
|
handleNavigate(); }, children: "View Bundle" })] })] }));
|
|
61
|
+
if (!renderOwnerActions)
|
|
62
|
+
return card;
|
|
63
|
+
/*
|
|
64
|
+
* Sibling of the card, never a child — see the prop's docstring. `z-20`
|
|
65
|
+
* clears this card's own overlays, which all sit at `z-10`.
|
|
66
|
+
*/
|
|
67
|
+
return (_jsxs(Div, { className: "relative h-full", children: [card, _jsx(Div, { className: "absolute right-2 top-2 z-20", children: renderOwnerActions() })] }));
|
|
61
68
|
}
|
|
@@ -20,5 +20,15 @@ export interface MarketplacePrizeDrawCardProps {
|
|
|
20
20
|
hrefBuilder?: (product: MarketplacePrizeDrawCardData) => string;
|
|
21
21
|
onNavigate?: (href: string) => void;
|
|
22
22
|
labels?: MarketplacePrizeDrawCardLabels;
|
|
23
|
+
/**
|
|
24
|
+
* Owner-only pencil / clone / bin. Pass `<OwnerQuickActions>`; it renders
|
|
25
|
+
* nothing for a visitor who does not own the record.
|
|
26
|
+
*
|
|
27
|
+
* 🛑 Rendered OUTSIDE this card's own anchor, as a sibling. A `<button>`
|
|
28
|
+
* inside an `<a>` is invalid HTML that browsers resolve by closing the
|
|
29
|
+
* anchor early, which breaks the CARD's navigation rather than just the
|
|
30
|
+
* button's — `stopPropagation` stops a click but cannot fix a parse.
|
|
31
|
+
*/
|
|
32
|
+
renderOwnerActions?: () => React.ReactNode;
|
|
23
33
|
}
|
|
24
|
-
export declare function MarketplacePrizeDrawCard({ product, className, variant, selectable, isSelected, onSelect, href, hrefBuilder, onNavigate, labels, }: MarketplacePrizeDrawCardProps): import("react").JSX.Element;
|
|
34
|
+
export declare function MarketplacePrizeDrawCard({ product, className, variant, selectable, isSelected, onSelect, href, hrefBuilder, onNavigate, labels, renderOwnerActions, }: MarketplacePrizeDrawCardProps): import("react").JSX.Element;
|
|
@@ -65,7 +65,7 @@ function formatCountdown(target) {
|
|
|
65
65
|
const minutes = Math.floor((diff % 3600000) / 60000);
|
|
66
66
|
return `${hours}h ${minutes}m`;
|
|
67
67
|
}
|
|
68
|
-
export function MarketplacePrizeDrawCard({ product, className = "", variant = "grid", selectable = false, isSelected = false, onSelect, href, hrefBuilder, onNavigate, labels, }) {
|
|
68
|
+
export function MarketplacePrizeDrawCard({ product, className = "", variant = "grid", selectable = false, isSelected = false, onSelect, href, hrefBuilder, onNavigate, labels, renderOwnerActions, }) {
|
|
69
69
|
const router = useRouter();
|
|
70
70
|
const mergedLabels = { ...DEFAULT_LABELS, ...labels };
|
|
71
71
|
const detailHref = resolveHref(product, href, hrefBuilder);
|
|
@@ -90,7 +90,7 @@ export function MarketplacePrizeDrawCard({ product, className = "", variant = "g
|
|
|
90
90
|
}
|
|
91
91
|
router.push(String(detailHref));
|
|
92
92
|
}, [detailHref, onNavigate, router]);
|
|
93
|
-
|
|
93
|
+
const card = (_jsxs(BaseListingCard, { isSelected: isSelected, variant: variant, className: className, onMouseDown: !isSelected ? longPress.onMouseDown : undefined, onMouseUp: !isSelected ? longPress.onMouseUp : undefined, onMouseLeave: !isSelected ? longPress.onMouseLeave : undefined, onTouchStart: !isSelected ? longPress.onTouchStart : undefined, onTouchEnd: !isSelected ? longPress.onTouchEnd : undefined, onTouchCancel: !isSelected ? longPress.onTouchCancel : undefined, children: [_jsxs(BaseListingCard.Hero, { aspect: "square", variant: variant, children: [_jsx(TextLink, { href: String(detailHref), className: "absolute inset-0 block", children: thumbItems.length > 0 ? (_jsx(Div, { layout: "grid", className: "grid-cols-2 grid-rows-2 gap-[0.125rem] h-full w-full bg-[var(--appkit-color-surface-muted)]", children: Array.from({ length: 4 }).map((_, i) => {
|
|
94
94
|
const it = thumbItems[i];
|
|
95
95
|
const img = it?.images?.[0];
|
|
96
96
|
return (_jsx(Div, { className: "relative overflow-hidden bg-[var(--appkit-color-surface-muted)]", children: img ? (_jsx(MediaImage, { src: img, alt: it?.title ?? `Prize ${i + 1}`, size: "card" })) : null }, `thumb-${i}`));
|
|
@@ -100,4 +100,11 @@ export function MarketplacePrizeDrawCard({ product, className = "", variant = "g
|
|
|
100
100
|
}, className: selectable || isSelected
|
|
101
101
|
? "opacity-100"
|
|
102
102
|
: "opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-opacity" }))] }), _jsxs(BaseListingCard.Info, { variant: variant, children: [_jsx(TextLink, { href: String(detailHref), children: _jsx(Text, { size: "sm", weight: "medium", color: "primary", truncate: 2, children: product.title }) }), _jsx(CardStoreLine, { storeName: product.storeName, storeId: product.storeId }), _jsxs(Row, { justify: "between", className: "mt-1", gap: "sm", children: [_jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: _jsxs(GatedPrice, { label: "Sign in to see the entry price", children: [formatCurrency(pricePerEntry, getDefaultCurrency()), " ", _jsx(Span, { size: "xs", weight: "normal", className: "text-[var(--appkit-color-text-muted)]", children: mergedLabels.pricePerEntryLabel })] }) }), max > 0 ? (_jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "xs", children: mergedLabels.entriesRemainingLabel(remaining, max) })) : null] }), countdown ? (_jsxs(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "xs", children: [status === "pending" ? "Opens in" : "Closes in", " ", countdown] })) : null, _jsx(Button, { type: "button", variant: "primary", size: "sm", textSize: "xs", className: "mt-2 w-full", onClick: handleNavigate, disabled: status === "closed" || remaining === 0, children: mergedLabels.enterDraw })] })] }));
|
|
103
|
+
if (!renderOwnerActions)
|
|
104
|
+
return card;
|
|
105
|
+
/*
|
|
106
|
+
* Sibling of the card, never a child — see the prop's docstring. `z-20`
|
|
107
|
+
* clears this card's own overlays, which all sit at `z-10`.
|
|
108
|
+
*/
|
|
109
|
+
return (_jsxs(Div, { className: "relative h-full", children: [card, _jsx(Div, { className: "absolute right-2 top-2 z-20", children: renderOwnerActions() })] }));
|
|
103
110
|
}
|
|
@@ -31,6 +31,9 @@ import { ctaAnchorProps } from "../../layout/cta-anchor.js";
|
|
|
31
31
|
import { ContextualTrail } from "../../categories/components/ContextualTrail.js";
|
|
32
32
|
import { productCrumbs, withTrailHead } from "../../categories/utils/breadcrumb-chain.js";
|
|
33
33
|
import { getDefaultCurrency } from "../../../core/baseline-resolver.js";
|
|
34
|
+
import { OwnerQuickActions } from "../../admin/components/OwnerQuickActions.js";
|
|
35
|
+
import { SELLER_ENDPOINTS } from "../../../constants/api-endpoints.js";
|
|
36
|
+
import { PurchaseReviewAffordance } from "../../reviews/components/PurchaseReviewAffordance.js";
|
|
34
37
|
import { Button, Container, Div, Heading, Main, PriceDisplay, RichText, Row, Section, Span, Stack, Text, Ul, Ol, Li, SpecTable, Icon, } from "../../../ui/index.js";
|
|
35
38
|
import { normalizeRichTextHtml } from "../../../utils/string.formatter.js";
|
|
36
39
|
import { safeDisplayName } from "../../../security/index.js";
|
|
@@ -350,7 +353,29 @@ export async function ProductDetailPageView({ slug, initialProduct, renderOfferA
|
|
|
350
353
|
price: product.price,
|
|
351
354
|
storeId: product.storeId,
|
|
352
355
|
storeName: product.storeName,
|
|
353
|
-
} }), _jsxs(Container, { size: "xl", padding: "y-lg", children: [_jsx(ProductDetailView, { renderBreadcrumb: () => (_jsxs(Row, { className: "mb-4", align: "center", justify: "between", gap: "sm", wrap: true, children: [_jsx(ContextualTrail, { trails: trails, alternates: { ids: categoryIds, names: categoryIdNames } }), _jsx(ShareButton, { title: title || undefined })] })), renderGallery: () => (_jsx(ProductGalleryClient, { images: images, video: productVideo, productName: title || undefined })), renderInfo: () => (_jsxs(Stack, { gap: "md", children: [
|
|
356
|
+
} }), _jsxs(Container, { size: "xl", padding: "y-lg", children: [_jsx(ProductDetailView, { renderBreadcrumb: () => (_jsxs(Row, { className: "mb-4", align: "center", justify: "between", gap: "sm", wrap: true, children: [_jsx(ContextualTrail, { trails: trails, alternates: { ids: categoryIds, names: categoryIdNames } }), _jsx(ShareButton, { title: title || undefined })] })), renderGallery: () => (_jsx(ProductGalleryClient, { images: images, video: productVideo, productName: title || undefined })), renderInfo: () => (_jsxs(Stack, { gap: "md", children: [_jsx(OwnerQuickActions, { resource: "PRODUCT", placement: "detail", recordLabel: title || String(product.slug ?? product.id), storeId: product.storeId ? String(product.storeId) : undefined, editHref: String(ROUTES.STORE.PRODUCTS_EDIT(String(product.id))), onClone: async () => {
|
|
357
|
+
const res = await fetch(SELLER_ENDPOINTS.PRODUCT_DUPLICATE(String(product.id)), { method: "POST" });
|
|
358
|
+
if (!res.ok)
|
|
359
|
+
return;
|
|
360
|
+
const json = (await res.json());
|
|
361
|
+
const newId = json?.data?.id;
|
|
362
|
+
/*
|
|
363
|
+
* Land on the COPY's editor. The endpoint returns the new
|
|
364
|
+
* draft's id precisely so a one-click clone ends somewhere
|
|
365
|
+
* useful rather than back on the public page, where the
|
|
366
|
+
* draft is invisible.
|
|
367
|
+
*/
|
|
368
|
+
if (newId && typeof window !== "undefined") {
|
|
369
|
+
window.location.assign(String(ROUTES.STORE.PRODUCTS_EDIT(newId)));
|
|
370
|
+
}
|
|
371
|
+
}, onDelete: async () => {
|
|
372
|
+
await fetch(SELLER_ENDPOINTS.PRODUCT_BY_ID(String(product.id)), {
|
|
373
|
+
method: "DELETE",
|
|
374
|
+
});
|
|
375
|
+
if (typeof window !== "undefined") {
|
|
376
|
+
window.location.assign(String(ROUTES.PUBLIC.PRODUCTS));
|
|
377
|
+
}
|
|
378
|
+
} }), _jsxs(Div, { children: [condition && (_jsx(Span, { size: "xs", weight: "medium", className: "mb-2 inline-block", padding: "pill-sm", rounded: "full", surface: "subtle", color: "muted", transform: "capitalize", children: condition === "new"
|
|
354
379
|
? "Brand New"
|
|
355
380
|
: condition === "like_new"
|
|
356
381
|
? "Like New"
|
|
@@ -421,7 +446,7 @@ export async function ProductDetailPageView({ slug, initialProduct, renderOfferA
|
|
|
421
446
|
? () => (_jsx(SublistingCarouselSection, { sublistingCategoryId: sublistingCategoryId, currentListingId: String(product.id) }))
|
|
422
447
|
: undefined, renderGroupSection: groupId
|
|
423
448
|
? () => (_jsx(ShowGroupSection, { groupId: groupId, currentSlug: String(p.slug ?? product.id), isParent: isGroupParent, groupTitle: groupTitle }))
|
|
424
|
-
: undefined, renderTabs: () => (_jsx(ProductTabsShell, { partsContent: compositionContent, 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: allSpecs.length > 0 ? (_jsx(SpecTable, { rows: allSpecs })) : undefined, ingredientsContent: ingredients.length > 0 ? (_jsx(Ul, { spacing: "comfortable", children: ingredients.map((item, i) => (_jsxs(Li, { layout: "flex-start", gap: "2", children: [_jsx(Span, { className: "mt-1 flex-shrink-0 h-1.5 w-1.5 bg-primary-400", rounded: "full" }), item] }, i))) })) : undefined, howToUseContent: howToUse.length > 0 ? (_jsx(Ol, { spacing: "loose", children: howToUse.map((step, i) => (_jsxs(Li, { layout: "flex-start", gap: "3", textSize: "sm", color: "muted", children: [_jsx(Span, { layout: "flex-center", size: "xs", weight: "bold", className: "flex-shrink-0 h-6 w-6 bg-primary-100 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300", rounded: "full", children: i + 1 }), step] }, 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: toCustomSectionTabs(p.customSections) })), renderBundleSection: bundleMemberships.length > 0
|
|
449
|
+
: undefined, renderTabs: () => (_jsx(ProductTabsShell, { partsContent: compositionContent, 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: allSpecs.length > 0 ? (_jsx(SpecTable, { rows: allSpecs })) : undefined, ingredientsContent: ingredients.length > 0 ? (_jsx(Ul, { spacing: "comfortable", children: ingredients.map((item, i) => (_jsxs(Li, { layout: "flex-start", gap: "2", children: [_jsx(Span, { className: "mt-1 flex-shrink-0 h-1.5 w-1.5 bg-primary-400", rounded: "full" }), item] }, i))) })) : undefined, howToUseContent: howToUse.length > 0 ? (_jsx(Ol, { spacing: "loose", children: howToUse.map((step, i) => (_jsxs(Li, { layout: "flex-start", gap: "3", textSize: "sm", color: "muted", children: [_jsx(Span, { layout: "flex-center", size: "xs", weight: "bold", className: "flex-shrink-0 h-6 w-6 bg-primary-100 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300", rounded: "full", children: i + 1 }), step] }, i))) })) : undefined, reviewsContent: _jsxs(Stack, { gap: "md", children: [_jsx(PurchaseReviewAffordance, { productId: String(product.id), storeId: product.storeId ? String(product.storeId) : undefined }), _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: toCustomSectionTabs(p.customSections) })), renderBundleSection: bundleMemberships.length > 0
|
|
425
450
|
? () => (_jsxs(Div, { className: CLS_BUNDLE_BOX, children: [_jsxs(Row, { className: "mb-3", align: "center", gap: "sm", children: [_jsx(Span, { size: "base", className: CLS_BUNDLE_ICON, "aria-hidden": "true", children: "\uD83D\uDCE6" }), _jsxs(Text, { className: CLS_BUNDLE_TITLE, children: ["Part of ", bundleMemberships.length === 1 ? "a bundle" : `${bundleMemberships.length} bundles`] })] }), _jsx(Stack, { gap: "3", children: bundleMemberships.map((b) => (_jsxs(Div, { className: CLS_BUNDLE_ROW, children: [_jsxs(Div, { className: "min-w-0", children: [_jsx(Text, { className: CLS_BUNDLE_LABEL, children: "Included in bundle" }), _jsx(Text, { className: "truncate", color: "primary", size: "sm", weight: "semibold", children: b.title })] }), _jsx(Link, { href: String(ROUTES.PUBLIC.BUNDLE_DETAIL(b.id)), className: CLS_BUNDLE_CTA, children: "View Bundle \u2192" })] }, b.id))) })] }))
|
|
426
451
|
: undefined, renderRelated: () => (_jsxs(Stack, { gap: "xl", children: [_jsx(GroupedListingsCarousel, { groups: groups }), _jsx(RelatedItemsSection, { relatedItems: relatedItems, relatedByBrand: relatedByBrand, relatedByTags: relatedByTags, relatedByStore: relatedByStore, categoryLabel: categoryNames[0] || categoryName || category || undefined, brandLabel: brand || undefined, storeLabel: storeName || undefined })] })) }), renderPrimaryActions
|
|
427
452
|
? renderPrimaryActions({
|
|
@@ -16,8 +16,21 @@ interface ProductCardProps<T extends ProductItem = ProductItem> {
|
|
|
16
16
|
selectionMode?: boolean;
|
|
17
17
|
isSelected?: boolean;
|
|
18
18
|
onSelect?: (id: string) => void;
|
|
19
|
+
/**
|
|
20
|
+
* Owner-only pencil / clone / bin, for an admin or the seller who owns this
|
|
21
|
+
* listing. Pass `<OwnerQuickActions>`; it renders nothing for anyone else.
|
|
22
|
+
*
|
|
23
|
+
* 🛑 Rendered OUTSIDE this card's `<Link>`, at `top-14 right-2` — below the
|
|
24
|
+
* wishlist heart and clear of the checkbox, badge stack and IconOverlay. See
|
|
25
|
+
* the return statement for why it cannot live inside the card body.
|
|
26
|
+
*
|
|
27
|
+
* Suppressed while a selection is active: a bulk-select gesture and a
|
|
28
|
+
* per-record edit are different intents, and offering both at once invites
|
|
29
|
+
* the wrong one.
|
|
30
|
+
*/
|
|
31
|
+
renderOwnerActions?: () => React.ReactNode;
|
|
19
32
|
}
|
|
20
|
-
export declare function ProductCard<T extends ProductItem = ProductItem>({ product, href, onClick, onAddToWishlist, isWishlisted, onAddToCart, onBuyNow, className, selectionMode, isSelected, onSelect, }: ProductCardProps<T>): React.JSX.Element;
|
|
33
|
+
export declare function ProductCard<T extends ProductItem = ProductItem>({ product, href, onClick, onAddToWishlist, isWishlisted, onAddToCart, onBuyNow, className, selectionMode, isSelected, onSelect, renderOwnerActions, }: ProductCardProps<T>): React.JSX.Element;
|
|
21
34
|
export interface ProductCardContext<T extends ProductItem = ProductItem> {
|
|
22
35
|
onClick?: (product: T) => void;
|
|
23
36
|
onWishlistToggle?: (productId: string) => void;
|
|
@@ -45,7 +45,7 @@ const CLS_DISCOUNT_TEXT = "mt-1 text-[11px] font-semibold text-error";
|
|
|
45
45
|
const CLS_DISCOUNT_TEXT_BARE = "text-[10px] text-error";
|
|
46
46
|
const CLS_STAR_BARE = "text-warning";
|
|
47
47
|
const CLS_BID_TEXT = "text-[11px] text-error";
|
|
48
|
-
export function ProductCard({ product, href, onClick, onAddToWishlist, isWishlisted, onAddToCart, onBuyNow, className = "", selectionMode = false, isSelected = false, onSelect, }) {
|
|
48
|
+
export function ProductCard({ product, href, onClick, onAddToWishlist, isWishlisted, onAddToCart, onBuyNow, className = "", selectionMode = false, isSelected = false, onSelect, renderOwnerActions, }) {
|
|
49
49
|
const discount = product.originalPrice && product.originalPrice > product.price
|
|
50
50
|
? Math.round(((product.originalPrice - product.price) / product.originalPrice) *
|
|
51
51
|
100)
|
|
@@ -124,10 +124,27 @@ export function ProductCard({ product, href, onClick, onAddToWishlist, isWishlis
|
|
|
124
124
|
e.preventDefault();
|
|
125
125
|
onAddToCart(product);
|
|
126
126
|
}, className: "justify-center text-[length:var(--appkit-text-xs)]", children: [_jsx("svg", { className: "h-3.5 w-3.5 flex-shrink-0", fill: "none", stroke: "currentColor", strokeWidth: 2, viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M3 3h2l.4 2M7 13h10l4-8H5.4m1.6 8L5 3H3m4 10v7a1 1 0 001 1h8a1 1 0 001-1v-7M9 21h6" }) }), "Cart"] }))] }))] })] })] }));
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
127
|
+
const wrapped = href && !selectionMode ? (_jsx(Link, { href: href, className: "block h-full", children: cardBody })) : (cardBody);
|
|
128
|
+
if (!renderOwnerActions || selectionMode || isSelected)
|
|
129
|
+
return wrapped;
|
|
130
|
+
/*
|
|
131
|
+
* 🛑 OUTSIDE the anchor, as a sibling — never inside `cardBody`.
|
|
132
|
+
*
|
|
133
|
+
* This card wraps its entire body in a `<Link>` whenever `href` is set, and
|
|
134
|
+
* `InteractiveProductCard` wraps it in another one. A `<button>` inside an
|
|
135
|
+
* `<a>` is invalid HTML that browsers resolve by closing the anchor early,
|
|
136
|
+
* which breaks the CARD's navigation rather than just the button's —
|
|
137
|
+
* stopPropagation stops the click but cannot fix the parse.
|
|
138
|
+
*
|
|
139
|
+
* Positioned `top-14 right-2`: directly below the 44px wishlist heart at
|
|
140
|
+
* `top-2 right-2`, so it clears every occupied corner (checkbox and badges
|
|
141
|
+
* top-left, IconOverlay bottom-right) without needing to know the hero's
|
|
142
|
+
* height. The hero is `aspect-square`, so it is always taller than
|
|
143
|
+
* 3.5rem + 44px and the toolbar lands on the photo.
|
|
144
|
+
*
|
|
145
|
+
* `z-20` beats the in-card overlays, which all sit at `z-10`.
|
|
146
|
+
*/
|
|
147
|
+
return (_jsxs(Div, { className: "relative h-full", children: [wrapped, _jsx(Div, { className: "absolute right-2 top-14 z-20", children: renderOwnerActions() })] }));
|
|
131
148
|
}
|
|
132
149
|
function ProductListRow({ product, onClick, onAddToWishlist, isWishlisted, }) {
|
|
133
150
|
const discount = product.originalPrice && product.originalPrice > product.price
|
|
@@ -221,9 +221,9 @@ export function ProductsIndexListing({ initialData, listingTypes = GENERIC_PRODU
|
|
|
221
221
|
itemCount: items.reduce((sum, it) => sum + it.quantity, 0),
|
|
222
222
|
totalValue: items.reduce((sum, it) => sum + (it.price ?? 0) * it.quantity, 0),
|
|
223
223
|
};
|
|
224
|
-
showToast(formatCartAddedMessage(detail, (amount) => formatCurrency(amount, getDefaultCurrency())), "success");
|
|
224
|
+
showToast(formatCartAddedMessage(detail, (amount) => formatCurrency(amount, getDefaultCurrency()), canSeePrices), "success");
|
|
225
225
|
dispatchCartUpdated(detail);
|
|
226
|
-
}, [localCart, showToast]);
|
|
226
|
+
}, [localCart, showToast, canSeePrices]);
|
|
227
227
|
const handleBuyNow = useCallback((product) => {
|
|
228
228
|
requireAuth(ACTION_ID.BUY_NOW, () => {
|
|
229
229
|
const snapshot = {
|