@mohasinac/appkit 4.27.0 → 4.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_internal/client/features/lottery/LotteryAdminEditView.d.ts +16 -16
- package/dist/_internal/client/features/lottery/LotteryAdminEditView.js +29 -6
- package/dist/_internal/client/features/lottery/LotteryListView.js +13 -1
- package/dist/_internal/server/features/checkout/actions.js +6 -4
- package/dist/_internal/server/features/classified/metadata.js +3 -1
- package/dist/_internal/server/features/digital-code/metadata.js +3 -1
- package/dist/_internal/server/features/live/metadata.js +3 -1
- package/dist/_internal/server/features/refunds/actions.js +3 -2
- package/dist/_internal/server/features/reviews/actions.js +3 -2
- package/dist/_internal/server/features/seo/sitemap.js +189 -40
- package/dist/_internal/server/jobs/core/listingProcessor.d.ts +0 -1
- package/dist/_internal/server/jobs/core/listingProcessor.js +19 -1
- package/dist/_internal/shared/actions/action-registry.js +14 -0
- package/dist/_internal/shared/checkout/order-math.d.ts +25 -3
- package/dist/_internal/shared/features/checkout/config.d.ts +3 -3
- package/dist/_internal/shared/features/checkout/config.js +6 -3
- package/dist/client.d.ts +32 -2
- package/dist/client.js +37 -2
- package/dist/constants/api-endpoints.d.ts +21 -3
- package/dist/constants/api-endpoints.js +14 -1
- package/dist/constants/field-names.d.ts +2 -2
- package/dist/constants/field-names.js +2 -2
- package/dist/core/newsletter.repository.js +14 -1
- package/dist/features/about/components/FAQPageView.js +1 -1
- package/dist/features/about/components/FeesView.js +1 -1
- package/dist/features/account/components/AddressForm.d.ts +2 -0
- package/dist/features/account/components/AddressForm.js +5 -1
- package/dist/features/account/components/UserBidsView.js +1 -1
- package/dist/features/account/components/UserSidebar.js +1 -1
- package/dist/features/account/components/UserSupportView.js +24 -2
- package/dist/features/account/hooks/useAddresses.d.ts +29 -1
- package/dist/features/account/hooks/useAddresses.js +4 -1
- package/dist/features/account/hooks/useProfile.d.ts +1 -1
- package/dist/features/account/hooks/useProfile.js +1 -1
- package/dist/features/addresses/repository/addresses.repository.d.ts +13 -0
- package/dist/features/addresses/repository/addresses.repository.js +19 -0
- package/dist/features/admin/actions/chat-actions.js +5 -1
- package/dist/features/admin/components/AdminAdsView.js +1 -1
- package/dist/features/admin/components/AdminBidsView.js +6 -1
- package/dist/features/admin/components/AdminCarouselGroupEditorView.d.ts +9 -1
- package/dist/features/admin/components/AdminCarouselGroupEditorView.js +42 -10
- package/dist/features/admin/components/AdminCarouselView.js +1 -1
- package/dist/features/admin/components/AdminFeatureEditorView.js +1 -1
- package/dist/features/admin/components/AdminGroupedListingsView.js +14 -2
- package/dist/features/admin/components/AdminSidebar.js +1 -1
- package/dist/features/admin/components/AdminViewCards.js +2 -2
- package/dist/features/admin/components/DataTable.js +2 -2
- package/dist/features/admin/schemas/firestore.js +9 -3
- package/dist/features/auctions/actions/bid-actions.js +45 -4
- package/dist/features/auctions/components/AuctionBottomActions.d.ts +21 -6
- package/dist/features/auctions/components/AuctionBottomActions.js +42 -11
- package/dist/features/auctions/components/AuctionDetailPageView.d.ts +8 -3
- package/dist/features/auctions/components/AuctionDetailPageView.js +2 -2
- package/dist/features/auctions/components/MarketplaceAuctionCard.js +4 -5
- package/dist/features/auctions/components/PlaceBidFormClient.d.ts +27 -2
- package/dist/features/auctions/components/PlaceBidFormClient.js +42 -20
- package/dist/features/auth/components/SocialAuthButtons.js +1 -1
- package/dist/features/auth/hooks/useAuth.d.ts +23 -1
- package/dist/features/auth/hooks/useAuth.js +27 -1
- package/dist/features/auth/permissions/constants.d.ts +1 -1
- package/dist/features/auth/permissions/constants.js +3 -0
- package/dist/features/auth/repository/session.repository.d.ts +16 -1
- package/dist/features/auth/repository/session.repository.js +23 -0
- package/dist/features/auth/repository/user.repository.js +13 -2
- package/dist/features/blog/components/BlogListView.js +2 -2
- package/dist/features/cart/components/CartPriceBreakdown.d.ts +11 -1
- package/dist/features/cart/components/CartPriceBreakdown.js +3 -3
- package/dist/features/cart/components/StoreAddonsPicker.d.ts +9 -0
- package/dist/features/cart/components/StoreAddonsPicker.js +15 -5
- package/dist/features/cart/components/index.d.ts +1 -1
- package/dist/features/cart/components/index.js +1 -1
- package/dist/features/cart/hooks/useGuestCartMerge.d.ts +8 -1
- package/dist/features/cart/hooks/useGuestCartMerge.js +2 -1
- package/dist/features/cart/utils/line-total.d.ts +25 -0
- package/dist/features/cart/utils/line-total.js +26 -0
- package/dist/features/catalogue/repository/catalogue.repository.d.ts +12 -0
- package/dist/features/catalogue/repository/catalogue.repository.js +15 -0
- package/dist/features/categories/components/CategoryTree.js +2 -2
- package/dist/features/contact/components/ContactForm.js +1 -1
- package/dist/features/copilot/hooks/useCopilotFeedback.d.ts +1 -1
- package/dist/features/copilot/hooks/useCopilotFeedback.js +1 -1
- package/dist/features/events/components/AdminEventEditorView.js +16 -9
- package/dist/features/events/components/AdminEventEntriesView.js +7 -13
- package/dist/features/events/components/EventRafflesSection.js +14 -2
- package/dist/features/events/components/EventStatusBadge.js +1 -1
- package/dist/features/events/components/event-entry-detail-fields.d.ts +24 -0
- package/dist/features/events/components/event-entry-detail-fields.js +13 -0
- package/dist/features/faq/components/ContactCTA.js +1 -1
- package/dist/features/faq/components/FAQHelpfulButtons.js +2 -2
- package/dist/features/faq/repository/faqs.repository.d.ts +16 -1
- package/dist/features/faq/repository/faqs.repository.js +64 -54
- package/dist/features/faq/schemas/firestore.d.ts +3 -2
- package/dist/features/faq/schemas/firestore.js +2 -2
- package/dist/features/faq/schemas/index.d.ts +3 -3
- package/dist/features/faq/schemas/index.js +1 -1
- package/dist/features/filters/FilterFacetSection.js +2 -2
- package/dist/features/forms/Toggle.js +1 -1
- package/dist/features/grouped/components/GroupedListingEditorView.d.ts +18 -0
- package/dist/features/grouped/components/GroupedListingEditorView.js +192 -0
- package/dist/features/homepage/components/AdvertisementBanner.js +2 -2
- package/dist/features/homepage/components/BrandsSection.js +1 -1
- package/dist/features/homepage/components/CTABannerSection.js +1 -1
- package/dist/features/homepage/components/HomepageCustomerReviewsSection.js +1 -1
- package/dist/features/homepage/components/HomepageSectionBoundary.d.ts +39 -0
- package/dist/features/homepage/components/HomepageSectionBoundary.js +49 -0
- package/dist/features/homepage/components/ShopByCategorySection.js +1 -1
- package/dist/features/homepage/components/WelcomeSection.js +1 -1
- package/dist/features/homepage/lib/section-renderer.js +2 -1
- package/dist/features/layout/AppLayoutShell.js +5 -5
- package/dist/features/layout/BottomActions.js +5 -5
- package/dist/features/layout/BottomSheet.js +1 -1
- package/dist/features/layout/MainNavbar.js +1 -1
- package/dist/features/layout/NavbarLayout.js +1 -1
- package/dist/features/layout/TitleBarLayout.js +3 -3
- package/dist/features/layout/hooks/useFormBottomActions.d.ts +11 -1
- package/dist/features/layout/hooks/useFormBottomActions.js +16 -2
- package/dist/features/lottery/schemas/config-write.d.ts +141 -0
- package/dist/features/lottery/schemas/config-write.js +139 -0
- package/dist/features/media/MediaPickerModal.js +3 -1
- package/dist/features/media/MediaVideo.js +4 -2
- package/dist/features/media/components/MediaSlider.js +1 -1
- package/dist/features/orders/components/OrdersList.js +2 -2
- package/dist/features/payments/repository/payout.repository.d.ts +11 -1
- package/dist/features/payments/repository/payout.repository.js +14 -6
- package/dist/features/pre-orders/components/MarketplacePreorderCard.js +3 -2
- package/dist/features/pre-orders/components/PreOrderBadge.js +1 -1
- package/dist/features/pre-orders/components/PreOrderDetailPageView.js +1 -1
- package/dist/features/products/columns/productTableColumns.js +3 -3
- package/dist/features/products/components/CompareOverlay.js +1 -1
- package/dist/features/products/components/PrizeDrawDetailPageView.js +1 -1
- package/dist/features/products/components/PrizeDrawEntryActions.js +2 -2
- package/dist/features/products/components/PrizeDrawsSection.js +5 -1
- package/dist/features/products/components/ProductDetailActions.js +11 -7
- package/dist/features/products/components/ProductDetailPageView.js +3 -3
- package/dist/features/products/components/ProductFeatureBadges.js +2 -2
- package/dist/features/products/components/ProductFeaturesSelector.js +1 -1
- package/dist/features/products/components/ProductFilters.js +15 -8
- package/dist/features/products/components/ProductGrid.js +8 -17
- package/dist/features/products/components/ProductTabs.js +7 -4
- package/dist/features/products/components/ProductTabsShell.js +2 -7
- package/dist/features/products/constants/action-defs.d.ts +21 -4
- package/dist/features/products/constants/action-defs.js +7 -3
- package/dist/features/products/hooks/useRelatedProducts.js +2 -1
- package/dist/features/products/repository/products.repository.d.ts +1 -1
- package/dist/features/products/repository/products.repository.js +27 -13
- package/dist/features/products/schemas/firestore.d.ts +3 -2
- package/dist/features/products/schemas/firestore.js +1 -1
- package/dist/features/reviews/actions/review-actions.d.ts +8 -1
- package/dist/features/reviews/actions/review-actions.js +14 -3
- package/dist/features/reviews/components/ReviewDetailShell.js +10 -4
- package/dist/features/reviews/components/ReviewModal.js +1 -1
- package/dist/features/reviews/repository/reviews.repository.d.ts +25 -1
- package/dist/features/reviews/repository/reviews.repository.js +53 -13
- package/dist/features/reviews/schemas/firestore.d.ts +6 -0
- package/dist/features/scams/components/ScamRegistryView.js +2 -2
- package/dist/features/search/components/Search.js +3 -3
- package/dist/features/search/components/SearchResultsSection.js +1 -1
- package/dist/features/seller/components/SellerAddressesView.js +3 -1
- package/dist/features/seller/components/SellerBidsView.js +6 -1
- package/dist/features/seller/components/SellerSidebar.js +1 -1
- package/dist/features/shell/SectionForm.d.ts +21 -1
- package/dist/features/shell/SectionForm.js +15 -3
- package/dist/features/shell/build-sections.js +10 -1
- package/dist/features/shell/field-ui-meta.d.ts +21 -0
- package/dist/features/shipments/components/AdminShipmentLotItemsView.js +23 -6
- package/dist/features/site-settings/components/ActionPermissionsManager.js +1 -1
- package/dist/features/store-extensions/repository/store-extensions.repositories.d.ts +21 -4
- package/dist/features/store-extensions/repository/store-extensions.repositories.js +24 -4
- package/dist/features/store-extensions/schemas/shipping-config-form.js +13 -0
- package/dist/features/store-extensions/schemas/store-category-form.js +1 -1
- package/dist/features/stores/actions/store-query-actions.js +25 -1
- package/dist/features/stores/components/InteractiveStoreCard.js +1 -1
- package/dist/features/stores/components/StoreHeader.js +1 -1
- package/dist/features/stores/components/StoreNavTabs.js +20 -8
- package/dist/features/support/repository/support.repository.d.ts +12 -0
- package/dist/features/support/repository/support.repository.js +15 -0
- package/dist/features/support/schemas/ticket-create-form.d.ts +33 -0
- package/dist/features/support/schemas/ticket-create-form.js +82 -0
- package/dist/features/tester/seed-data/products-tester-seed-data.js +2 -2
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +138 -1
- package/dist/features/wishlist/components/WishlistHeartButton.d.ts +55 -0
- package/dist/features/wishlist/components/WishlistHeartButton.js +52 -0
- package/dist/features/wishlist/components/index.d.ts +2 -1
- package/dist/features/wishlist/components/index.js +1 -1
- package/dist/index.d.ts +10 -3
- package/dist/index.js +19 -4
- package/dist/monitoring/error-tracking.js +39 -5
- package/dist/monitoring/server-logger.js +70 -32
- package/dist/next/components/ErrorView.js +1 -1
- package/dist/next/routing/route-map.d.ts +39 -7
- package/dist/next/routing/route-map.js +34 -4
- package/dist/providers/db-firebase/base.repository.d.ts +30 -0
- package/dist/providers/db-firebase/base.repository.js +56 -9
- package/dist/react/ErrorBoundary.js +9 -1
- package/dist/react/contexts/SessionContext.js +6 -2
- package/dist/react/hooks/useAuthGate.d.ts +22 -0
- package/dist/react/hooks/useAuthGate.js +86 -7
- package/dist/schemas/registry.d.ts +3 -3
- package/dist/security/index.d.ts +1 -1
- package/dist/security/index.js +1 -1
- package/dist/security/pii-encrypt.d.ts +13 -5
- package/dist/security/pii-encrypt.js +132 -6
- package/dist/security/pii-schemas.d.ts +20 -4
- package/dist/security/pii-schemas.js +23 -5
- package/dist/seed/_helpers/product-search-txt.d.ts +18 -0
- package/dist/seed/_helpers/product-search-txt.js +33 -0
- package/dist/seed/faq-seed-data.d.ts +16 -0
- package/dist/seed/faq-seed-data.js +22 -1
- package/dist/seed/products-art-seed-data.js +49 -7
- package/dist/seed/products-auctions-seed-data.js +12 -4
- package/dist/seed/products-classifieds-seed-data.d.ts +5 -0
- package/dist/seed/products-classifieds-seed-data.js +65 -9
- package/dist/seed/products-digital-codes-seed-data.d.ts +5 -0
- package/dist/seed/products-digital-codes-seed-data.js +65 -9
- package/dist/seed/products-live-items-seed-data.d.ts +5 -0
- package/dist/seed/products-live-items-seed-data.js +33 -5
- package/dist/seed/products-preorders-seed-data.d.ts +1 -1
- package/dist/seed/products-preorders-seed-data.js +6 -2
- package/dist/seed/products-prize-draws-seed-data.d.ts +5 -0
- package/dist/seed/products-prize-draws-seed-data.js +57 -8
- package/dist/seed/products-standard-seed-data.js +6 -2
- package/dist/seed/products-stickers-seed-data.js +49 -7
- package/dist/seed/site-settings-seed-data.js +5 -3
- package/dist/seo/json-ld.d.ts +44 -2
- package/dist/seo/json-ld.js +57 -11
- package/dist/styles.css +368 -90
- package/dist/tailwind-utilities.css +1 -1
- package/dist/tokens/index.d.ts +1 -1
- package/dist/tokens/index.js +1 -1
- package/dist/tokens/themes/default-dark.js +6 -0
- package/dist/tokens/themes/default-light.js +5 -0
- package/dist/tokens/tokens.css +84 -0
- package/dist/ui/DataTable.js +1 -1
- package/dist/ui/DataTable.style.css +3 -3
- package/dist/ui/components/BulkActionBar.style.css +1 -3
- package/dist/ui/components/Button.js +15 -6
- package/dist/ui/components/Dropdown.style.css +1 -4
- package/dist/ui/components/FilterChipGroup.js +1 -1
- package/dist/ui/components/FilterDrawer.style.css +1 -1
- package/dist/ui/components/Icon.d.ts +53 -0
- package/dist/ui/components/Icon.js +41 -0
- package/dist/ui/components/IconButton.d.ts +13 -0
- package/dist/ui/components/IconButton.js +13 -0
- package/dist/ui/components/IconButton.style.css +18 -10
- package/dist/ui/components/ListingLayout.style.css +2 -10
- package/dist/ui/components/ListingToolbar.js +6 -6
- package/dist/ui/components/Modal.js +1 -1
- package/dist/ui/components/PaginatedSelect.style.css +3 -3
- package/dist/ui/components/Pagination.style.css +1 -6
- package/dist/ui/components/RichTextEditor.js +1 -1
- package/dist/ui/components/RowActionMenu.style.css +1 -1
- package/dist/ui/components/SideDrawer.style.css +1 -1
- package/dist/ui/components/Slider.js +1 -1
- package/dist/ui/components/TabBarShell.d.ts +78 -0
- package/dist/ui/components/TabBarShell.js +95 -0
- package/dist/ui/components/Tabs.js +12 -8
- package/dist/ui/components/Tabs.style.css +249 -41
- package/dist/ui/components/ViewToggle.style.css +1 -5
- package/dist/ui/components/tabs-overflow.d.ts +17 -0
- package/dist/ui/components/tabs-overflow.js +142 -0
- package/dist/ui/icons/icon-registry.d.ts +38 -0
- package/dist/ui/icons/icon-registry.js +37 -0
- package/dist/ui/index.d.ts +6 -0
- package/dist/ui/index.js +3 -0
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/media-url.d.ts +27 -4
- package/dist/utils/media-url.js +37 -6
- package/dist/utils/search-txt.d.ts +71 -0
- package/dist/utils/search-txt.js +118 -0
- package/package.json +1 -1
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useRef, useEffect } from "react";
|
|
4
4
|
import { Div, Row, Span } from "../../ui";
|
|
5
|
-
import { resolveMediaUrl, getYouTubeVideoId } from "../../utils/media-url";
|
|
5
|
+
import { resolveMediaUrl, resolveVideoUrl, getYouTubeVideoId } from "../../utils/media-url";
|
|
6
6
|
import { useSiteSettings } from "../../core/hooks/useSiteSettings";
|
|
7
7
|
export function MediaVideo({ src, thumbnailUrl, alt = "Video", controls = true, autoPlayMuted = false, loop = false, trimStart, trimEnd, objectFit = "cover", watermark, }) {
|
|
8
8
|
const videoRef = useRef(null);
|
|
9
9
|
const fitClass = objectFit === "contain" ? "object-contain" : "object-cover";
|
|
10
|
-
|
|
10
|
+
// Video src must NOT fall through to /api/media/ext (image-only, 400s on
|
|
11
|
+
// video/mp4). The poster is an image and stays on the normal image path.
|
|
12
|
+
const resolvedSrc = resolveVideoUrl(src);
|
|
11
13
|
const resolvedPoster = resolveMediaUrl(thumbnailUrl);
|
|
12
14
|
const youtubeId = getYouTubeVideoId(src);
|
|
13
15
|
// Apply trimStart on load
|
|
@@ -5,7 +5,7 @@ export function MediaSlider({ value, min = 0, max = 100, step = 1, onChange, onC
|
|
|
5
5
|
// drag/cancel state; nothing here persists across a cancelled touch.
|
|
6
6
|
onTouchEnd: (e) => onChangeEnd?.(parseFloat(e.target.value)), className: [
|
|
7
7
|
"w-full h-2 appearance-none cursor-pointer rounded-full",
|
|
8
|
-
"
|
|
8
|
+
" bg-[var(--appkit-color-border)]",
|
|
9
9
|
"accent-primary-600 dark:accent-secondary-400",
|
|
10
10
|
className,
|
|
11
11
|
]
|
|
@@ -23,14 +23,14 @@ export function OrderCard({ order, onClick, labels = {}, renderActions }) {
|
|
|
23
23
|
day: "numeric",
|
|
24
24
|
})
|
|
25
25
|
: "";
|
|
26
|
-
const statusColor = STATUS_COLORS[order.orderStatus] ?? "bg-[var(--appkit-color-surface)]
|
|
26
|
+
const statusColor = STATUS_COLORS[order.orderStatus] ?? "bg-[var(--appkit-color-surface)] bg-[var(--appkit-color-surface-elevated)] text-[var(--appkit-color-text-muted)]";
|
|
27
27
|
// SB8-F — count unrevealed prize-draw entries to surface a "reveals pending" badge.
|
|
28
28
|
const unrevealedPrizeDraws = order.items.filter((it) => it.listingType === "prize-draw" &&
|
|
29
29
|
it.prizeRevealStatus !== undefined &&
|
|
30
30
|
it.prizeRevealStatus !== "revealed");
|
|
31
31
|
const revealsRemaining = unrevealedPrizeDraws.length;
|
|
32
32
|
return (_jsxs(Div, { padding: "5", role: onClick ? "button" : undefined, tabIndex: onClick ? 0 : undefined, onKeyDown: onClick
|
|
33
|
-
? (e) => (e.key === "Enter" || e.key === "
|
|
33
|
+
? (e) => (e.key === "Enter" || e.key === "") && onClick(order)
|
|
34
34
|
: undefined, onClick: onClick ? () => onClick(order) : undefined, className: `border border-neutral-200 ${onClick ? "cursor-pointer transition hover:shadow-md" : ""}`, rounded: "xl", surface: "default", children: [_jsxs(Row, { wrap: true, align: "start", justify: "between", gap: "3", children: [_jsxs(Div, { children: [_jsxs(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "xs", children: ["Order #", order.id.slice(-8).toUpperCase()] }), date && (_jsx(Text, { className: "mt-0.5", color: "faint", size: "xs", children: date }))] }), _jsx(Span, { size: "xs", weight: "semibold", className: `capitalize ${statusColor}`, rounded: "full", padding: "pill-md", children: labels[order.orderStatus] ?? order.orderStatus.replace(/_/g, " ") })] }), revealsRemaining > 0 && (_jsx(Row, { gap: "sm", className: "mt-2", children: _jsxs(Span, { size: "xs", weight: "semibold", className: CLS_PRIZE_BADGE, children: [revealsRemaining, " ", revealsRemaining === 1 ? "reveal" : "reveals", " pending"] }) })), _jsxs(Row, { wrap: true, gap: "3", className: "mt-4", children: [order.items.slice(0, 3).map((item, i) => (_jsxs(Row, { gap: "sm", children: [item.image && (_jsx(Div, { className: "relative h-10 w-10 flex-shrink-0", rounded: "lg", overflow: "hidden", children: _jsx(MediaImage, { src: item.image, alt: item.title, size: "thumbnail" }) })), _jsxs(Div, { children: [_jsx(Text, { truncate: true, size: "sm", weight: "medium", color: "primary", children: item.title }), _jsxs(Text, { size: "xs", color: "faint", children: ["\u00D7", item.quantity] })] })] }, i))), order.items.length > 3 && (_jsxs(Span, { size: "xs", className: "self-center", color: "faint", children: ["+", order.items.length - 3, " more"] }))] }), _jsxs(Row, { justify: "between", className: "mt-4 border-t border-neutral-100", padding: "t-sm", children: [_jsxs(Span, { size: "sm", className: "text-[var(--appkit-color-text-muted)]", children: [order.currency ?? "", " Total"] }), _jsx(Span, { weight: "semibold", className: "text-[var(--appkit-color-text)]", children: formatCurrency(order.total, order.currency) })] }), renderActions && (_jsx(Row, { justify: "end", gap: "sm", className: "mt-3", onClick: (e) => e.stopPropagation(), children: renderActions(order) }))] }));
|
|
35
35
|
}
|
|
36
36
|
export function OrdersList({ orders, isLoading, onOrderClick, totalPages = 1, currentPage = 1, onPageChange, emptyLabel = "No orders found", renderActions, }) {
|
|
@@ -19,7 +19,17 @@ export declare class PayoutRepository extends BaseRepository<PayoutDocument> {
|
|
|
19
19
|
constructor();
|
|
20
20
|
/** Decrypt PII fields on a payout document after Firestore read */
|
|
21
21
|
private decryptPayout;
|
|
22
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* Encrypt PII, then attach blind indices derived from the plaintext.
|
|
24
|
+
*
|
|
25
|
+
* Previously this reassigned `encrypted = addPiiIndices(data, …)` — which
|
|
26
|
+
* re-reads the ORIGINAL plaintext and returns `{...data, ...indices}` — and
|
|
27
|
+
* then spread that over the ciphertext, so plaintext won and `sellerEmail`
|
|
28
|
+
* and `upiId` were written to Firestore in the clear beside a valid index.
|
|
29
|
+
* `piiIndicesFor` returns the index fields alone, so the ciphertext cannot be
|
|
30
|
+
* overwritten. The identical bug was fixed in UserRepository and never
|
|
31
|
+
* propagated here.
|
|
32
|
+
*/
|
|
23
33
|
private encryptPayoutData;
|
|
24
34
|
/** Override mapDoc to auto-decrypt PII on every Firestore read */
|
|
25
35
|
protected mapDoc<D = PayoutDocument>(snap: import("../../../providers/db-firebase").DocumentSnapshot): D;
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { BaseRepository, prepareForFirestore, parseSieveDateValue, } from "../../../providers/db-firebase";
|
|
8
8
|
import { PAYOUT_COLLECTION, PAYOUT_FIELDS, PAYOUT_TRACKED_FIELDS, createPayoutId } from "../schemas";
|
|
9
9
|
import { withHistory } from "../../../_internal/shared/history/index";
|
|
10
|
-
import { encryptPiiFields, decryptPiiFields,
|
|
10
|
+
import { encryptPiiFields, decryptPiiFields, piiIndicesFor, PAYOUT_PII_FIELDS, PAYOUT_PII_INDEX_MAP, encryptPayoutBankAccount, decryptPayoutBankAccount, } from "../../../security";
|
|
11
11
|
export class PayoutRepository extends BaseRepository {
|
|
12
12
|
constructor() {
|
|
13
13
|
super(PAYOUT_COLLECTION);
|
|
@@ -20,13 +20,21 @@ export class PayoutRepository extends BaseRepository {
|
|
|
20
20
|
}
|
|
21
21
|
return decrypted;
|
|
22
22
|
}
|
|
23
|
-
/**
|
|
23
|
+
/**
|
|
24
|
+
* Encrypt PII, then attach blind indices derived from the plaintext.
|
|
25
|
+
*
|
|
26
|
+
* Previously this reassigned `encrypted = addPiiIndices(data, …)` — which
|
|
27
|
+
* re-reads the ORIGINAL plaintext and returns `{...data, ...indices}` — and
|
|
28
|
+
* then spread that over the ciphertext, so plaintext won and `sellerEmail`
|
|
29
|
+
* and `upiId` were written to Firestore in the clear beside a valid index.
|
|
30
|
+
* `piiIndicesFor` returns the index fields alone, so the ciphertext cannot be
|
|
31
|
+
* overwritten. The identical bug was fixed in UserRepository and never
|
|
32
|
+
* propagated here.
|
|
33
|
+
*/
|
|
24
34
|
encryptPayoutData(data) {
|
|
25
|
-
|
|
26
|
-
encrypted = addPiiIndices(data, PAYOUT_PII_INDEX_MAP);
|
|
27
|
-
encrypted = {
|
|
35
|
+
const encrypted = {
|
|
28
36
|
...encryptPiiFields(data, [...PAYOUT_PII_FIELDS]),
|
|
29
|
-
...
|
|
37
|
+
...piiIndicesFor(data, PAYOUT_PII_INDEX_MAP),
|
|
30
38
|
};
|
|
31
39
|
const access = encrypted;
|
|
32
40
|
if (access.bankAccount) {
|
|
@@ -17,6 +17,7 @@ const CLS_LIMITED_BADGE = "rounded-full bg-info-solid px-[var(--appkit-space-2)]
|
|
|
17
17
|
import { BaseListingCard, Button, RichText, Row, Span, Stack, Text, TextLink } from "../../../ui";
|
|
18
18
|
import { normalizeRichTextHtml } from "../../../utils/string.formatter";
|
|
19
19
|
import { PreorderBadge } from "./PreorderCard";
|
|
20
|
+
import { WishlistHeartButton } from "../../wishlist/components/WishlistHeartButton";
|
|
20
21
|
const DEFAULT_LABELS = {
|
|
21
22
|
preOrderBadge: "Pre-order",
|
|
22
23
|
featuredBadge: "Featured",
|
|
@@ -62,7 +63,7 @@ export function MarketplacePreorderCard({ product, className = "", variant = "gr
|
|
|
62
63
|
onSelect(product.id, !isSelected);
|
|
63
64
|
}, className: selectable || isSelected ? "opacity-100" : "opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-opacity" }))] }), _jsx(BaseListingCard.Info, { variant: variant, children: variant === "list" ? (
|
|
64
65
|
/* ── Compact list layout ── */
|
|
65
|
-
_jsxs(_Fragment, { children: [_jsxs(Row, { className: "min-w-0", align: "start", justify: "between", gap: "sm", children: [_jsx(TextLink, { href: detailHref, className: "min-w-0 flex-1", children: _jsx(Text, { size: "sm", weight: "medium", color: "primary", truncate: 2, children: product.title }) }), wishlistActions && (_jsx(
|
|
66
|
+
_jsxs(_Fragment, { children: [_jsxs(Row, { className: "min-w-0", align: "start", justify: "between", gap: "sm", children: [_jsx(TextLink, { href: detailHref, className: "min-w-0 flex-1", children: _jsx(Text, { size: "sm", weight: "medium", color: "primary", truncate: 2, children: product.title }) }), wishlistActions && (_jsx(WishlistHeartButton, { size: "inline", inWishlist: inWishlist, addLabel: mergedLabels.addToWishlist, removeLabel: mergedLabels.removeFromWishlist, onToggle: handleWishlist }))] }), _jsxs(Row, { align: "center", gap: "sm", wrap: true, children: [_jsx(Text, { className: "text-primary", size: "sm", weight: "bold", children: formatCurrency(product.price, getDefaultCurrency()) }), shipDate && _jsx(PreorderBadge, { shipDate: shipDate })] }), _jsx(Button, { type: "button", variant: "primary", size: "sm", textSize: "xs", className: "self-start mt-0.5", onClick: handleNavigate, children: mergedLabels.reserveNow })] })) : (
|
|
66
67
|
/* ── Full grid layout ── */
|
|
67
|
-
_jsxs(_Fragment, { children: [_jsx(TextLink, { href: detailHref, children: _jsx(Text, { size: "sm", weight: "medium", color: "primary", truncate: 2, children: product.title }) }), product.description ? (_jsx(RichText, { html: normalizeRichTextHtml(product.description), proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "line-clamp-2 text-[length:var(--appkit-text-xs)] text-[var(--appkit-color-text-muted)]" })) : null, _jsxs(Row, { justify: "between", className: "mt-1", gap: "sm", children: [_jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: formatCurrency(product.price, getDefaultCurrency()) }), shipDate && _jsx(PreorderBadge, { shipDate: shipDate })] }), _jsxs(Row, { justify: "between", className: "mt-auto pt-[var(--appkit-space-2)]", gap: "sm", children: [_jsx(Button, { type: "button", variant: "primary", size: "sm", textSize: "xs", className: "flex-1", onClick: handleNavigate, children: mergedLabels.reserveNow }), onAddToCart ? (_jsx(Button, { type: "button", variant: "outline", size: "sm", textSize: "xs", className: "flex-1", onClick: (e) => { e.stopPropagation(); onAddToCart(product); }, children: mergedLabels.addToCart })) : null, wishlistActions ? (_jsx(
|
|
68
|
+
_jsxs(_Fragment, { children: [_jsx(TextLink, { href: detailHref, children: _jsx(Text, { size: "sm", weight: "medium", color: "primary", truncate: 2, children: product.title }) }), product.description ? (_jsx(RichText, { html: normalizeRichTextHtml(product.description), proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "line-clamp-2 text-[length:var(--appkit-text-xs)] text-[var(--appkit-color-text-muted)]" })) : null, _jsxs(Row, { justify: "between", className: "mt-1", gap: "sm", children: [_jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: formatCurrency(product.price, getDefaultCurrency()) }), shipDate && _jsx(PreorderBadge, { shipDate: shipDate })] }), _jsxs(Row, { justify: "between", className: "mt-auto pt-[var(--appkit-space-2)]", gap: "sm", children: [_jsx(Button, { type: "button", variant: "primary", size: "sm", textSize: "xs", className: "flex-1", onClick: handleNavigate, children: mergedLabels.reserveNow }), onAddToCart ? (_jsx(Button, { type: "button", variant: "outline", size: "sm", textSize: "xs", className: "flex-1", onClick: (e) => { e.stopPropagation(); onAddToCart(product); }, children: mergedLabels.addToCart })) : null, wishlistActions ? (_jsx(WishlistHeartButton, { size: "inline", inWishlist: inWishlist, addLabel: mergedLabels.addToWishlist, removeLabel: mergedLabels.removeFromWishlist, onToggle: handleWishlist })) : null] })] })) })] }));
|
|
68
69
|
}
|
|
@@ -5,7 +5,7 @@ const STATUS_MAP = {
|
|
|
5
5
|
pending: { label: "Pending", color: "bg-warning-surface text-warning" },
|
|
6
6
|
confirmed: { label: "Confirmed", color: "bg-info-surface text-info" },
|
|
7
7
|
ready: { label: "Ready", color: "bg-success-surface text-success" },
|
|
8
|
-
fulfilled: { label: "Fulfilled", color: "bg-[var(--appkit-color-surface)]
|
|
8
|
+
fulfilled: { label: "Fulfilled", color: "bg-[var(--appkit-color-surface)] bg-[var(--appkit-color-surface-elevated)] text-[var(--appkit-color-text-muted)]" },
|
|
9
9
|
cancelled: { label: "Cancelled", color: "bg-error-surface text-error" },
|
|
10
10
|
};
|
|
11
11
|
export function PreOrderBadge({ status, className = "" }) {
|
|
@@ -61,7 +61,7 @@ function PreOrderInfoSection({ title, productionStatus, maxPerUser, deliveryDate
|
|
|
61
61
|
emiAvailable: "EMI Available",
|
|
62
62
|
wishlistCount: (n) => `${n} wishlisted`,
|
|
63
63
|
categoryProductCount: (n, cat) => `${n} in ${cat}`,
|
|
64
|
-
} }), (categoryName || category || brand) && (_jsxs(Row, { gap: "sm", wrap: true, children: [category && (_jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category)), className: "inline-flex items-center rounded-full border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] px-[var(--appkit-space-2-5)] py-[var(--appkit-space-1)] text-[length:var(--appkit-text-xs)] font-medium text-[var(--appkit-color-text-muted)] transition-colors hover:border-primary-300 hover:bg-primary-
|
|
64
|
+
} }), (categoryName || category || brand) && (_jsxs(Row, { gap: "sm", wrap: true, children: [category && (_jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category)), className: "inline-flex items-center rounded-full border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] px-[var(--appkit-space-2-5)] py-[var(--appkit-space-1)] text-[length:var(--appkit-text-xs)] font-medium text-[var(--appkit-color-text-muted)] transition-colors hover:border-primary-300 hover:bg-primary-surface hover:text-primary-700 dark:hover:border-primary-700/60 dark:hover:text-primary-400", children: categoryName || category })), !category && categoryName && (_jsx(Span, { layout: "inline-flex", size: "xs", weight: "medium", border: "subtle", rounded: "full", padding: "pill-sm-tall", surface: "muted", color: "muted", children: categoryName })), brand && brandSlug && (_jsx(Link, { href: String(ROUTES.PUBLIC.BRAND_DETAIL(brandSlug)), className: "inline-flex items-center rounded-full border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] px-[var(--appkit-space-2-5)] py-[var(--appkit-space-1)] text-[length:var(--appkit-text-xs)] font-medium text-[var(--appkit-color-text-muted)] transition-colors hover:border-primary-300 hover:bg-primary-surface hover:text-primary-700 dark:hover:border-primary-700/60 dark:hover:text-primary-400", children: brand })), brand && !brandSlug && (_jsx(Span, { layout: "inline-flex", size: "xs", weight: "medium", border: "subtle", rounded: "full", padding: "pill-sm-tall", surface: "muted", color: "muted", children: brand }))] })), productFeatures && features.length > 0 && (_jsx(FeatureBadgeList, { productFeatureIds: features, features: productFeatures })), !productFeatures && features.length > 0 && (_jsxs(Div, { border: "subtle", surface: "muted", padding: "inline", rounded: "xl", children: [_jsx(Text, { className: "mb-2 tracking-wide", color: "muted", size: "xs", weight: "semibold", transform: "uppercase", children: "About this product" }), _jsx(Ul, { spacing: "comfortable", size: "sm", color: "primary", children: features.map((f, i) => (_jsxs(Li, { layout: "flex-start", gap: "2", children: [_jsx(Span, { className: "mt-0.5 flex-shrink-0 text-primary-500", children: "\u2022" }), f] }, i))) })] })), descriptionHtml && (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "text-[length:var(--appkit-text-sm)] leading-relaxed text-[var(--appkit-color-text-muted)] line-clamp-4" })), safeSeller && (_jsx(Div, { className: `${__P.p3}`, border: "subtle", rounded: "xl", surface: "muted", children: _jsxs(Row, { justify: "between", align: "center", children: [_jsxs(Div, { children: [_jsx(Text, { className: "text-[10px] tracking-wide mb-0.5", color: "faint", transform: "uppercase", children: "Sold by" }), _jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: safeSeller })] }), storeHref && (_jsx(Link, { href: storeHref, className: "shrink-0 rounded-lg bg-primary/10 dark:bg-primary/20 px-[var(--appkit-space-3)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-xs)] font-semibold text-primary-700 dark:text-primary-300 hover:bg-primary/20 dark:hover:bg-primary/30 transition-colors", children: "Visit Store \u2192" }))] }) }))] }));
|
|
65
65
|
}
|
|
66
66
|
function PreOrderBuyBarPanel({ reserveTarget, reservedCount, progressPct, productId, price, currency, depositAmount, depositPercent, isCancellable, tags, onReserveNow, }) {
|
|
67
67
|
return (_jsxs(Stack, { id: "pre-order-buy-bar", className: `${__P.p5}`, border: "subtle", gap: "md", rounded: "xl", surface: "muted", children: [reserveTarget > 0 && (_jsxs(Stack, { gap: "sm", children: [_jsxs(Row, { justify: "between", align: "center", children: [_jsxs(Text, { size: "xs", color: "muted", children: [reservedCount, " of ", reserveTarget, " reserved"] }), _jsxs(Span, { size: "xs", weight: "semibold", className: "text-primary-600 dark:text-primary-400", children: [progressPct, "%"] })] }), _jsx(Div, { className: `h-2 w-full ${__O.hidden}`, rounded: "full", surface: "subtle", children: _jsx(ProgressBarFill, { pct: progressPct, className: "h-full bg-primary transition-all rounded-full" }) })] })), onReserveNow ? (_jsx(PreOrderActionsClient, { productId: productId, price: price, currency: currency, depositAmount: depositAmount, depositPercent: depositPercent, isCancellable: isCancellable, tags: tags, onReserveNow: onReserveNow })) : (_jsxs(_Fragment, { children: [price !== null && (_jsxs(Div, { children: [_jsx(Text, { size: "2xl", weight: "bold", color: "primary", children: formatCurrency(price, currency) }), depositAmount !== null && (_jsxs(Text, { className: "mt-0.5", color: "muted", size: "xs", children: ["Reserve with ", formatCurrency(depositAmount, currency), depositPercent !== null ? ` (${depositPercent}% deposit)` : ""] }))] })), _jsxs(Stack, { gap: "sm", children: [_jsx(Button, { variant: "primary", size: "md", className: "w-full", children: "Reserve Now" }), isCancellable && (_jsx(Text, { size: "xs", align: "start", color: "muted", children: "\u2713 Free cancellation before production" }))] }), tags.length > 0 && (_jsx(Div, { border: "default", className: "border-t", padding: "t-md", children: _jsx(Row, { wrap: true, gap: "xs", children: tags.map((tag) => (_jsx(Span, { size: "xs", rounded: "full", padding: "pill-sm-tall", surface: "subtle", color: "muted", children: tag }, tag))) }) })), _jsx(Div, { border: "default", className: "border-t", padding: "t-md", children: _jsx(Row, { wrap: true, gap: "sm", justify: "center", className: "text-center", children: [
|
|
@@ -7,7 +7,7 @@ const __O = {
|
|
|
7
7
|
};
|
|
8
8
|
const STATUS_STYLES = {
|
|
9
9
|
published: "bg-success-surface text-success dark:bg-success-surface dark:text-success",
|
|
10
|
-
draft: "bg-[var(--appkit-color-surface)]
|
|
10
|
+
draft: "bg-[var(--appkit-color-surface)] bg-[var(--appkit-color-border)] text-[var(--appkit-color-text-muted)]",
|
|
11
11
|
out_of_stock: "bg-warning-surface text-warning dark:bg-warning-surface dark:text-warning",
|
|
12
12
|
discontinued: "bg-error-surface text-error dark:bg-error-surface dark:text-error",
|
|
13
13
|
sold: "bg-primary/10 dark:bg-primary/20 text-primary",
|
|
@@ -54,7 +54,7 @@ export function getProductTableColumns({ labels, onEdit, onDelete, currencyCode,
|
|
|
54
54
|
return (_jsx(Span, { className: [
|
|
55
55
|
"rounded px-[var(--appkit-space-2)] py-[var(--appkit-space-1)] text-[length:var(--appkit-text-xs)] font-medium",
|
|
56
56
|
STATUS_STYLES[status] ??
|
|
57
|
-
"bg-[var(--appkit-color-surface)]
|
|
57
|
+
"bg-[var(--appkit-color-surface)] bg-[var(--appkit-color-border)] text-[var(--appkit-color-text-muted)]",
|
|
58
58
|
].join(" "), children: status.replace("_", " ") }));
|
|
59
59
|
},
|
|
60
60
|
},
|
|
@@ -76,7 +76,7 @@ export function getProductTableColumns({ labels, onEdit, onDelete, currencyCode,
|
|
|
76
76
|
"rounded px-[var(--appkit-space-2)] py-[var(--appkit-space-1)] text-[length:var(--appkit-text-xs)] font-medium",
|
|
77
77
|
featured
|
|
78
78
|
? CLS_LOW_STOCK
|
|
79
|
-
: "bg-[var(--appkit-color-surface)]
|
|
79
|
+
: "bg-[var(--appkit-color-surface)] bg-[var(--appkit-color-border)] text-[var(--appkit-color-text-muted)]",
|
|
80
80
|
].join(" "), children: featured ? labels.yes : labels.no }));
|
|
81
81
|
},
|
|
82
82
|
},
|
|
@@ -9,7 +9,7 @@ const __O = {
|
|
|
9
9
|
hidden: "overflow-hidden",
|
|
10
10
|
};
|
|
11
11
|
const REL_NOOPENER = "noopener noreferrer";
|
|
12
|
-
const CLS_REMOVE_BTN = "shrink-0 text-zinc-400 hover:
|
|
12
|
+
const CLS_REMOVE_BTN = "shrink-0 text-zinc-400 hover:text-error hover:bg-surface-hover";
|
|
13
13
|
import Link from "next/link";
|
|
14
14
|
import { X } from "lucide-react";
|
|
15
15
|
import { Button, Div, Heading, IconButton, Row, Span, Stack, Text } from "../../../ui";
|
|
@@ -44,7 +44,7 @@ function statusClass(s) {
|
|
|
44
44
|
case "open":
|
|
45
45
|
return "bg-success-surface text-success";
|
|
46
46
|
case "closed":
|
|
47
|
-
return "
|
|
47
|
+
return " bg-[var(--appkit-color-border)] text-[var(--appkit-color-text-muted)]";
|
|
48
48
|
case "pending":
|
|
49
49
|
default:
|
|
50
50
|
return "bg-warning-surface text-warning";
|
|
@@ -18,7 +18,7 @@ import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
|
|
|
18
18
|
*/
|
|
19
19
|
export function PrizeDrawEntryActions({ productId, productSlug, title, pricePerEntry, currency, remainingEntries, revealStatus, prizeGithubFileUrl, }) {
|
|
20
20
|
const router = useRouter();
|
|
21
|
-
const { requireAuth, modalOpen, modalMessage, closeModal } = useAuthGate();
|
|
21
|
+
const { requireAuth, isAuthResolving, modalOpen, modalMessage, closeModal } = useAuthGate();
|
|
22
22
|
const [consentOpen, setConsentOpen] = useState(false);
|
|
23
23
|
const closed = revealStatus === "closed" || remainingEntries === 0;
|
|
24
24
|
const handleEnter = useCallback(() => {
|
|
@@ -31,5 +31,5 @@ export function PrizeDrawEntryActions({ productId, productSlug, title, pricePerE
|
|
|
31
31
|
const slug = productSlug ?? productId;
|
|
32
32
|
router.push(`${String(ROUTES.USER.CHECKOUT)}?directItem=${encodeURIComponent(slug)}&type=prize-draw`);
|
|
33
33
|
}, [productSlug, productId, router]);
|
|
34
|
-
return (_jsxs(Stack, { gap: "md", children: [_jsxs(Text, { size: "2xl", weight: "bold", color: "primary", children: [formatCurrency(pricePerEntry, currency), _jsx(Text, { as: "span", className: "ml-1 text-[var(--appkit-color-text-muted)]", size: "xs", weight: "normal", children: "per entry" })] }), _jsx(Button, { action: closed ? undefined : ACTIONS.PRIZE_DRAW["buy-now"], variant: "primary", size: "md", className: "w-full", disabled: closed, onClick: handleEnter, children: closed ? "Draw closed" : undefined }), prizeGithubFileUrl ? (_jsx(Anchor, { href: prizeGithubFileUrl, tone: "none", underline: "hover", size: "xs", weight: "medium", className: "text-left text-primary-600 underline-offset-4 dark:text-primary-400", children: "View RNG source code on GitHub \u2192" })) : null, _jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "xs", align: "start", children: "Winners chosen by Node.js crypto.randomInt \u2014 fully auditable. Entries are locked once paid; refunds only if the prize pool is exhausted." }), _jsx(NonRefundableConsentModal, { open: consentOpen, listingType: "prize-draw", itemTitle: title, priceLabel: formatCurrency(pricePerEntry, currency), onCancel: () => setConsentOpen(false), onConfirm: handleConfirm }), _jsx(LoginRequiredModal, { isOpen: modalOpen, onClose: closeModal, message: modalMessage })] }));
|
|
34
|
+
return (_jsxs(Stack, { gap: "md", children: [_jsxs(Text, { size: "2xl", weight: "bold", color: "primary", children: [formatCurrency(pricePerEntry, currency), _jsx(Text, { as: "span", className: "ml-1 text-[var(--appkit-color-text-muted)]", size: "xs", weight: "normal", children: "per entry" })] }), _jsx(Button, { action: closed ? undefined : ACTIONS.PRIZE_DRAW["buy-now"], variant: "primary", size: "md", className: "w-full", disabled: closed || isAuthResolving, onClick: handleEnter, children: closed ? "Draw closed" : isAuthResolving ? "Checking…" : undefined }), prizeGithubFileUrl ? (_jsx(Anchor, { href: prizeGithubFileUrl, tone: "none", underline: "hover", size: "xs", weight: "medium", className: "text-left text-primary-600 underline-offset-4 dark:text-primary-400", children: "View RNG source code on GitHub \u2192" })) : null, _jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "xs", align: "start", children: "Winners chosen by Node.js crypto.randomInt \u2014 fully auditable. Entries are locked once paid; refunds only if the prize pool is exhausted." }), _jsx(NonRefundableConsentModal, { open: consentOpen, listingType: "prize-draw", itemTitle: title, priceLabel: formatCurrency(pricePerEntry, currency), onCancel: () => setConsentOpen(false), onConfirm: handleConfirm }), _jsx(LoginRequiredModal, { isOpen: modalOpen, onClose: closeModal, message: modalMessage })] }));
|
|
35
35
|
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
|
|
2
|
+
// Import from the DEFINING module, never `@mohasinac/appkit/client` — that entry
|
|
3
|
+
// is `"use client"`, so in this async Server Component `sortBy` would resolve to
|
|
4
|
+
// a client-reference proxy and calling it throws during the server render
|
|
5
|
+
// (Root Cause #18). That is exactly what took `section-prize-draws` down.
|
|
6
|
+
import { sortBy } from "../../../constants/sort";
|
|
3
7
|
import { Container, Div, Heading, Section, Stack, Text, } from "../../../ui";
|
|
4
8
|
import { ROUTES } from "../../../next";
|
|
5
9
|
import { listPublicProducts } from "../../../_internal/server/features/products/list-public";
|
|
@@ -2,7 +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 { useRouter } from "next/navigation";
|
|
5
|
-
import { Button, Stack, LoginRequiredModal } from "../../../ui";
|
|
5
|
+
import { Button, Stack, LoginRequiredModal, Icon } from "../../../ui";
|
|
6
6
|
import { useToast } from "../../../ui/components/Toast";
|
|
7
7
|
import { useAddToCart } from "../../cart/hooks/useAddToCart";
|
|
8
8
|
import { apiClient } from "../../../http";
|
|
@@ -22,9 +22,13 @@ import { normalizeError } from "../../../errors/normalize";
|
|
|
22
22
|
export function ProductDetailActions({ productId, productTitle, productImage, price, currency, storeId, storeName, inStock, variant = "desktop", }) {
|
|
23
23
|
const router = useRouter();
|
|
24
24
|
const { showToast } = useToast();
|
|
25
|
-
const { requireAuth, modalOpen, modalMessage, closeModal } = useAuthGate();
|
|
25
|
+
const { requireAuth, isAuthResolving, modalOpen, modalMessage, closeModal } = useAuthGate();
|
|
26
26
|
const [busy, setBusy] = useState(null);
|
|
27
27
|
const [wishlisted, setWishlisted] = useState(false);
|
|
28
|
+
// `isAuthResolving` covers the window where the session has not settled and a
|
|
29
|
+
// click would be parked by useAuthGate rather than acted on. Without it the
|
|
30
|
+
// button looks live, swallows the click, and appears to do nothing.
|
|
31
|
+
const blocked = busy !== null || isAuthResolving;
|
|
28
32
|
// Success toast (item + updated count/total) is now shown by useAddToCart
|
|
29
33
|
// itself — centralised so every add-to-cart call site gets the same
|
|
30
34
|
// richer toast, not just this one.
|
|
@@ -98,24 +102,24 @@ export function ProductDetailActions({ productId, productTitle, productImage, pr
|
|
|
98
102
|
actions: [
|
|
99
103
|
{
|
|
100
104
|
id: ACTION_ID.ADD_TO_WISHLIST,
|
|
101
|
-
label: wishlisted ? "
|
|
105
|
+
label: wishlisted ? "Saved" : "Wishlist",
|
|
102
106
|
variant: "ghost",
|
|
103
107
|
grow: false,
|
|
104
|
-
disabled:
|
|
108
|
+
disabled: blocked || wishlisted,
|
|
105
109
|
onClick: handleWishlist,
|
|
106
110
|
},
|
|
107
111
|
{
|
|
108
112
|
id: ACTION_ID.ADD_TO_CART,
|
|
109
113
|
label: busy === "cart" ? "Adding…" : "Add to Cart",
|
|
110
114
|
variant: "outline",
|
|
111
|
-
disabled: !inStock ||
|
|
115
|
+
disabled: !inStock || blocked,
|
|
112
116
|
onClick: () => handleAddToCart(),
|
|
113
117
|
},
|
|
114
118
|
{
|
|
115
119
|
id: ACTION_ID.BUY_NOW,
|
|
116
120
|
label: !inStock ? "Out of Stock" : busy === "buy" ? "Loading…" : "Buy Now",
|
|
117
121
|
variant: "primary",
|
|
118
|
-
disabled: !inStock ||
|
|
122
|
+
disabled: !inStock || blocked,
|
|
119
123
|
onClick: handleBuyNow,
|
|
120
124
|
},
|
|
121
125
|
],
|
|
@@ -132,5 +136,5 @@ export function ProductDetailActions({ productId, productTitle, productImage, pr
|
|
|
132
136
|
if (variant === "mobile") {
|
|
133
137
|
return _jsx(_Fragment, { children: authModal });
|
|
134
138
|
}
|
|
135
|
-
return (_jsxs(_Fragment, { children: [authModal, _jsxs(Stack, { gap: "sm", children: [_jsx(Button, { variant: "primary", size: "md", className: "w-full", disabled: !inStock ||
|
|
139
|
+
return (_jsxs(_Fragment, { children: [authModal, _jsxs(Stack, { gap: "sm", children: [_jsx(Button, { variant: "primary", size: "md", className: "w-full", disabled: !inStock || blocked, onClick: handleBuyNow, children: !inStock ? "Out of Stock" : busy === "buy" ? "Loading…" : "Buy Now" }), _jsx(Button, { variant: "secondary", size: "md", className: "w-full", disabled: !inStock || blocked, onClick: () => handleAddToCart(), children: !inStock ? "Out of Stock" : busy === "cart" ? "Adding…" : "Add to Cart" }), _jsxs(Button, { variant: "ghost", size: "md", className: "w-full", disabled: blocked || wishlisted, onClick: handleWishlist, children: [_jsx(Icon, { name: "wishlist", size: "md", filled: wishlisted }), wishlisted ? "In Wishlist" : "Add to Wishlist"] })] })] }));
|
|
136
140
|
}
|
|
@@ -22,7 +22,7 @@ const CLS_BUNDLE_LABEL = "text-[length:var(--appkit-text-xs)] text-success dark:
|
|
|
22
22
|
const CLS_BUNDLE_CTA = "flex-shrink-0 rounded-lg bg-success-solid hover:brightness-110 px-[var(--appkit-space-3)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-xs)] font-semibold text-success-on-solid transition-colors";
|
|
23
23
|
import { ROUTES } from "../../../next";
|
|
24
24
|
import { getDefaultCurrency } from "../../../core/baseline-resolver";
|
|
25
|
-
import { Button, Container, Div, Heading, Main, Nav, PriceDisplay, RichText, Row, Section, Span, Stack, Text, Ul, Ol, Li, Dl, Dt, Dd, } from "../../../ui";
|
|
25
|
+
import { Button, Container, Div, Heading, Main, Nav, PriceDisplay, RichText, Row, Section, Span, Stack, Text, Ul, Ol, Li, Dl, Dt, Dd, Icon, } from "../../../ui";
|
|
26
26
|
import { normalizeRichTextHtml } from "../../../utils/string.formatter";
|
|
27
27
|
import { safeDisplayName } from "../../../security";
|
|
28
28
|
import { ReviewsListingPanel } from "../../reviews/components/ReviewsListingPanel";
|
|
@@ -204,7 +204,7 @@ export async function ProductDetailPageView({ slug, initialProduct, renderOfferA
|
|
|
204
204
|
? ` — only ${effectiveStock} left`
|
|
205
205
|
: ""] })] })) : (_jsxs(Span, { layout: "inline-flex", gap: "xs", className: `w-fit .5 ${inStock ? CLS_STOCK_IN : CLS_STOCK_OUT}`, rounded: "full", padding: "pill-md", size: "xs", weight: "medium", children: [inStock ? "✓ In Stock" : "✗ Out of Stock", inStock && effectiveStock !== null && effectiveStock <= 10
|
|
206
206
|
? ` — only ${effectiveStock} left`
|
|
207
|
-
: ""] })), (categorySlugs.length > 0 || subcategory || brand) && (_jsxs(Row, { gap: "sm", wrap: true, children: [categorySlugs.map((slug, i) => (_jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(slug)), className: "inline-flex items-center rounded-full border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] px-[var(--appkit-space-2-5)] py-[var(--appkit-space-1)] text-[length:var(--appkit-text-xs)] font-medium text-[var(--appkit-color-text-muted)] transition-colors hover:border-primary-300 hover:bg-primary-
|
|
207
|
+
: ""] })), (categorySlugs.length > 0 || subcategory || brand) && (_jsxs(Row, { gap: "sm", wrap: true, children: [categorySlugs.map((slug, i) => (_jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(slug)), className: "inline-flex items-center rounded-full border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] px-[var(--appkit-space-2-5)] py-[var(--appkit-space-1)] text-[length:var(--appkit-text-xs)] font-medium text-[var(--appkit-color-text-muted)] transition-colors hover:border-primary-300 hover:bg-primary-surface hover:text-primary-700 dark:hover:border-primary-700/60 dark:hover:text-primary-400", children: categoryNames[i] ?? slug }, slug))), subcategory && (_jsx(Span, { layout: "inline-flex", size: "xs", weight: "medium", border: "subtle", rounded: "full", padding: "pill-sm-tall", surface: "muted", color: "muted", transform: "capitalize", children: subcategory })), brand && brandSlug && (_jsx(Link, { href: String(ROUTES.PUBLIC.BRAND_DETAIL(brandSlug)), className: "inline-flex items-center rounded-full border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] px-[var(--appkit-space-2-5)] py-[var(--appkit-space-1)] text-[length:var(--appkit-text-xs)] font-medium text-[var(--appkit-color-text-muted)] transition-colors hover:border-primary-300 hover:bg-primary-surface hover:text-primary-700 dark:hover:border-primary-700/60 dark:hover:text-primary-400", children: brand })), brand && !brandSlug && (_jsx(Span, { layout: "inline-flex", size: "xs", weight: "medium", border: "subtle", rounded: "full", padding: "pill-sm-tall", surface: "muted", color: "muted", children: brand }))] })), bundleMemberships.length > 0 && (_jsx(Row, { gap: "sm", wrap: true, children: bundleMemberships.map((b) => (_jsxs(Link, { href: String(ROUTES.PUBLIC.BUNDLE_DETAIL(b.id)), className: CLS_BUNDLE_PILL, children: [_jsx(Span, { "aria-hidden": "true", children: "\uD83D\uDCE6" }), "In bundle: ", b.title] }, b.id))) })), _jsx(ProductFeatureBadges, { featured: featured, freeShipping: freeShipping, codAvailable: codEnabled === true, codHref: String(ROUTES.PUBLIC.HOW_CHECKOUT_WORKS), emiAvailable: emiEnabled === true, emiHref: String(ROUTES.PUBLIC.HOW_EMI_WORKS), condition: condition ?? undefined, returnable: returnPolicy != null && returnPolicy.length > 0, labels: {
|
|
208
208
|
featured: "Featured",
|
|
209
209
|
fasterDelivery: "Faster Delivery",
|
|
210
210
|
ratedSeller: "Rated Seller",
|
|
@@ -232,7 +232,7 @@ export async function ProductDetailPageView({ slug, initialProduct, renderOfferA
|
|
|
232
232
|
storeName: typeof p.storeName === "string" ? p.storeName : undefined,
|
|
233
233
|
inStock,
|
|
234
234
|
variant: "desktop",
|
|
235
|
-
})) : (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "primary", size: "md", className: "w-full", disabled: true, title: ACTION_NOT_WIRED, children: inStock ? "Buy Now" : "Out of Stock" }), _jsx(Button, { variant: "secondary", size: "md", className: "w-full", disabled: true, title: ACTION_NOT_WIRED, children: inStock ? "Add to Cart" : "Out of Stock" }),
|
|
235
|
+
})) : (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "primary", size: "md", className: "w-full", disabled: true, title: ACTION_NOT_WIRED, children: inStock ? "Buy Now" : "Out of Stock" }), _jsx(Button, { variant: "secondary", size: "md", className: "w-full", disabled: true, title: ACTION_NOT_WIRED, children: inStock ? "Add to Cart" : "Out of Stock" }), _jsxs(Button, { variant: "ghost", size: "md", className: "w-full", disabled: true, title: ACTION_NOT_WIRED, children: [_jsx(Icon, { name: "wishlist", size: "md" }), "Add to Wishlist"] })] })), allowOffers && offerableType && offersEnabled && price !== null && renderOfferAction?.({
|
|
236
236
|
productId: product.id,
|
|
237
237
|
price,
|
|
238
238
|
currency,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Fragment } from "react";
|
|
3
|
-
import { Row, Span, TextLink } from "../../../ui";
|
|
3
|
+
import { Row, Span, TextLink, Icon } from "../../../ui";
|
|
4
4
|
export function ProductFeatureBadges({ featured, fasterDelivery, ratedSeller, condition, returnable, freeShipping, wishlistCount, categoryProductCount, categoryName, codAvailable, codHref, emiAvailable, emiHref, labels, formatCount, categoryBadgeClassName, }) {
|
|
5
5
|
const badges = [];
|
|
6
6
|
if (featured) {
|
|
@@ -89,7 +89,7 @@ export function ProductFeatureBadges({ featured, fasterDelivery, ratedSeller, co
|
|
|
89
89
|
if (wishlistCount && wishlistCount > 0) {
|
|
90
90
|
badges.push({
|
|
91
91
|
key: "wishlist",
|
|
92
|
-
icon: _jsx(
|
|
92
|
+
icon: _jsx(Icon, { name: "wishlist", size: "xs", filled: true }),
|
|
93
93
|
label: labels.wishlistCount(wishlistCount),
|
|
94
94
|
colorClass: "text-pink-700 dark:text-pink-300",
|
|
95
95
|
bgClass: "bg-pink-50 dark:bg-pink-900/30 border-pink-200 dark:border-pink-800",
|
|
@@ -11,7 +11,7 @@ const SECTION_LABEL_CLASS = "text-[var(--appkit-font-size-2xs,11px)] font-semibo
|
|
|
11
11
|
const ROW_BASE_CLASS = "flex items-start gap-[var(--appkit-space-2)] rounded-lg border px-[var(--appkit-space-3)] py-[var(--appkit-space-2)] text-[length:var(--appkit-text-xs)] cursor-pointer transition-colors";
|
|
12
12
|
const ROW_CHECKED_CLASS = "border-primary bg-primary/5 dark:bg-primary/10";
|
|
13
13
|
const ROW_DISABLED_CLASS = "border-[var(--appkit-color-border)] opacity-50 cursor-not-allowed";
|
|
14
|
-
const ROW_DEFAULT_CLASS = "border-[var(--appkit-color-border)] hover:bg-
|
|
14
|
+
const ROW_DEFAULT_CLASS = "border-[var(--appkit-color-border)] hover:bg-surface-hover";
|
|
15
15
|
const OVER_LIMIT_BANNER_CLASS = "rounded-lg border border-error/20 bg-error-surface px-[var(--appkit-space-3)] py-[var(--appkit-space-2)] text-[length:var(--appkit-text-xs)] text-error";
|
|
16
16
|
const EMPTY_STATE_CLASS = "rounded-lg border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] px-[var(--appkit-space-3)] py-[var(--appkit-space-4)] text-[length:var(--appkit-text-xs)] text-[var(--appkit-color-text-muted)] text-left";
|
|
17
17
|
function applyProductType(list, productType) {
|
|
@@ -75,12 +75,21 @@ export function ProductFilters({ table, categoryOptions = [], brandOptions = [],
|
|
|
75
75
|
{ value: PRODUCT_FIELDS.CONDITION_VALUES.REFURBISHED, label: t("conditionRefurbished") },
|
|
76
76
|
{ value: PRODUCT_FIELDS.CONDITION_VALUES.BROKEN, label: t("conditionBroken") },
|
|
77
77
|
];
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
78
|
+
const resolvedVariant = variant ?? (showStatus ? "admin" : "public");
|
|
79
|
+
const shouldShowStatus = resolvedVariant !== "public" || showStatus;
|
|
80
|
+
// Built ONLY when the status facet actually renders. This used to be
|
|
81
|
+
// constructed unconditionally at the top of the component, so every public
|
|
82
|
+
// listing that never shows a status facet still called t() four times —
|
|
83
|
+
// five such listings mount ProductFilters, which is how one missing key
|
|
84
|
+
// (`statusInReview`) produced a MISSING_MESSAGE storm on every page load.
|
|
85
|
+
const defaultStatusOptions = shouldShowStatus
|
|
86
|
+
? [
|
|
87
|
+
{ value: PRODUCT_FIELDS.STATUS_VALUES.PUBLISHED, label: t("statusPublished") },
|
|
88
|
+
{ value: PRODUCT_FIELDS.STATUS_VALUES.DRAFT, label: t("statusDraft") },
|
|
89
|
+
{ value: PRODUCT_FIELDS.STATUS_VALUES.IN_REVIEW, label: t("statusInReview") },
|
|
90
|
+
{ value: PRODUCT_FIELDS.STATUS_VALUES.ARCHIVED, label: t("statusArchived") },
|
|
91
|
+
]
|
|
92
|
+
: [];
|
|
84
93
|
const splitPipe = (key) => table.get(key) ? table.get(key).split("|").filter(Boolean) : [];
|
|
85
94
|
const selectedCategories = splitPipe(TABLE_KEYS.CATEGORY);
|
|
86
95
|
const selectedBrands = splitPipe(TABLE_KEYS.BRAND);
|
|
@@ -90,7 +99,5 @@ export function ProductFilters({ table, categoryOptions = [], brandOptions = [],
|
|
|
90
99
|
const selectedStatuses = splitPipe(TABLE_KEYS.STATUS);
|
|
91
100
|
const selectedSublistings = splitPipe(TABLE_KEYS.SUBLISTING_CATEGORY);
|
|
92
101
|
const selectedFeatures = splitPipe(TABLE_KEYS.FEATURES);
|
|
93
|
-
const resolvedVariant = variant ?? (showStatus ? "admin" : "public");
|
|
94
|
-
const shouldShowStatus = resolvedVariant !== "public" || showStatus;
|
|
95
102
|
return (_jsxs(Div, { children: [loadCategoryOptions ? (_jsx(AsyncFacetSection, { title: t("category"), loadOptions: loadCategoryOptions, selected: selectedCategories, onChange: (vals) => table.set(TABLE_KEYS.CATEGORY, vals.join("|")), defaultCollapsed: false })) : categoryOptions.length > 0 ? (_jsx(FilterFacetSection, { title: t("category"), options: categoryOptions, selected: selectedCategories, onChange: (vals) => table.set(TABLE_KEYS.CATEGORY, vals.join("|")), searchable: true, defaultCollapsed: categoryOptions.length > 6 })) : null, _jsx(FilterFacetSection, { title: t("condition"), options: conditionOptions, selected: selectedConditions, onChange: (vals) => table.set(TABLE_KEYS.CONDITION, vals.join("|")), searchable: false, defaultCollapsed: false }), _jsx(RangeFilter, { title: t("priceRange"), minValue: table.get(TABLE_KEYS.MIN_PRICE), maxValue: table.get(TABLE_KEYS.MAX_PRICE), onMinChange: (v) => table.set(TABLE_KEYS.MIN_PRICE, v), onMaxChange: (v) => table.set(TABLE_KEYS.MAX_PRICE, v), prefix: currencyPrefix, showSlider: true, minBound: 0, maxBound: 500000, step: 500, minPlaceholder: t("minPrice"), maxPlaceholder: t("maxPrice"), defaultCollapsed: false }), loadBrandOptions ? (_jsx(AsyncFacetSection, { title: t("brand"), loadOptions: loadBrandOptions, selected: selectedBrands, onChange: (vals) => table.set(TABLE_KEYS.BRAND, vals.join("|")), defaultCollapsed: false })) : brandOptions.length > 0 ? (_jsx(FilterFacetSection, { title: t("brand"), options: brandOptions, selected: selectedBrands, onChange: (vals) => table.set(TABLE_KEYS.BRAND, vals.join("|")), searchable: brandOptions.length > 4, defaultCollapsed: brandOptions.length > 6 })) : null, loadStoreOptions ? (_jsx(AsyncFacetSection, { title: t("store"), loadOptions: loadStoreOptions, selected: selectedSellers, onChange: (vals) => table.set(TABLE_KEYS.STORE_ID, vals.join("|")), defaultCollapsed: false })) : resolvedStoreOptions.length > 0 ? (_jsx(FilterFacetSection, { title: t("store"), options: resolvedStoreOptions, selected: selectedSellers, onChange: (vals) => table.set(TABLE_KEYS.STORE_ID, vals.join("|")), searchable: resolvedStoreOptions.length > 4, defaultCollapsed: resolvedStoreOptions.length > 6 })) : null, showShipping && (_jsx(SwitchFilter, { title: t("shipping"), label: t("freeShippingOnly"), checked: table.get(TABLE_KEYS.FREE_SHIPPING) === "true", onChange: (v) => table.set(TABLE_KEYS.FREE_SHIPPING, v ? "true" : ""), defaultCollapsed: false })), tagOptions.length > 0 && (_jsx(FilterFacetSection, { title: t("tags"), options: tagOptions, selected: selectedTags, onChange: (vals) => table.set(TABLE_KEYS.TAGS, vals.join("|")), searchable: tagOptions.length > 4, defaultCollapsed: tagOptions.length > 6 })), shouldShowStatus && (_jsx(FilterFacetSection, { title: t("status"), options: statusOptions ?? defaultStatusOptions, selected: selectedStatuses, onChange: (vals) => table.set(TABLE_KEYS.STATUS, vals.join("|")), searchable: false, defaultCollapsed: false })), sublistingCategoryOptions.length > 0 && (_jsx(FilterFacetSection, { title: "Sublisting Type", options: sublistingCategoryOptions, selected: selectedSublistings, onChange: (vals) => table.set(TABLE_KEYS.SUBLISTING_CATEGORY, vals.join("|")), searchable: sublistingCategoryOptions.length > 4, defaultCollapsed: false })), showBundleFilter && (_jsx(SwitchFilter, { title: "Bundles", label: "Part of a bundle only", checked: table.get(TABLE_KEYS.IS_PART_OF_BUNDLE) === "true", onChange: (v) => table.set(TABLE_KEYS.IS_PART_OF_BUNDLE, v ? "true" : ""), defaultCollapsed: false })), featureOptions.length > 0 && (_jsx(FilterFacetSection, { title: "Features", options: featureOptions, selected: selectedFeatures, onChange: (vals) => table.set(TABLE_KEYS.FEATURES, vals.join("|")), searchable: featureOptions.length > 4, defaultCollapsed: featureOptions.length > 4 }))] }));
|
|
96
103
|
}
|
|
@@ -15,6 +15,7 @@ import { useProductFeatures } from "./ProductFeaturesContext";
|
|
|
15
15
|
import { PRODUCT_FEATURE_CARD_MAX_VISIBLE } from "../constants/product-features.constants";
|
|
16
16
|
import { normalizeListingType } from "../utils/listing-type";
|
|
17
17
|
import { pluginFor } from "../../../_internal/shared/listing-types/_registry";
|
|
18
|
+
import { WishlistHeartButton } from "../../wishlist/components/WishlistHeartButton";
|
|
18
19
|
const __P = {
|
|
19
20
|
p3: "p-[var(--appkit-space-3)]",
|
|
20
21
|
};
|
|
@@ -30,15 +31,11 @@ const CLS_BADGE_LIMITED = "rounded-full bg-info-solid px-[var(--appkit-space-2)]
|
|
|
30
31
|
// inverts with the theme. `warning` because success/info/error are each already
|
|
31
32
|
// taken by a sibling pill in this same stack.
|
|
32
33
|
const CLS_BADGE_OFFERABLE = "rounded-full bg-warning-solid px-[var(--appkit-space-2)] py-[var(--appkit-space-0-5)] text-[10px] font-bold text-warning-on-solid shadow-sm";
|
|
33
|
-
const CLS_HEART_ACTIVE = "bg-[var(--appkit-color-surface-elevated)]/90 text-error hover:bg-[var(--appkit-color-surface-elevated)]";
|
|
34
|
-
const CLS_HEART_IDLE = "bg-[var(--appkit-color-surface-elevated)]/90 text-[var(--appkit-color-text-muted)] hover:text-error hover:bg-[var(--appkit-color-surface-elevated)]";
|
|
35
34
|
const CLS_STAR = "text-[11px] text-warning";
|
|
36
35
|
const CLS_DISCOUNT_TEXT = "mt-1 text-[11px] font-semibold text-error";
|
|
37
36
|
const CLS_DISCOUNT_TEXT_BARE = "text-[10px] text-error";
|
|
38
37
|
const CLS_STAR_BARE = "text-warning";
|
|
39
38
|
const CLS_BID_TEXT = "text-[11px] text-error";
|
|
40
|
-
const CLS_HEART_ROSE_ACTIVE = "text-error";
|
|
41
|
-
const CLS_HEART_ROSE_IDLE = "text-zinc-300 dark:text-[var(--appkit-color-text-muted)] hover:text-error";
|
|
42
39
|
export function ProductCard({ product, href, onClick, onAddToWishlist, isWishlisted, onAddToCart, onBuyNow, className = "", selectionMode = false, isSelected = false, onSelect, }) {
|
|
43
40
|
const discount = product.originalPrice && product.originalPrice > product.price
|
|
44
41
|
? Math.round(((product.originalPrice - product.price) / product.originalPrice) *
|
|
@@ -55,9 +52,9 @@ export function ProductCard({ product, href, onClick, onAddToWishlist, isWishlis
|
|
|
55
52
|
? (e) => { e.preventDefault(); e.stopPropagation(); onSelect?.(product.id); }
|
|
56
53
|
: onClick && !href ? () => onClick(product) : undefined;
|
|
57
54
|
const cardBody = (_jsxs(Div, { role: selectionMode ? "checkbox" : onClick ? "button" : undefined, "aria-checked": selectionMode ? isSelected : undefined, tabIndex: selectionMode || (onClick && !href) ? 0 : undefined, onKeyDown: selectionMode
|
|
58
|
-
? (e) => (e.key === "Enter" || e.key === "
|
|
55
|
+
? (e) => (e.key === "Enter" || e.key === "") && onSelect?.(product.id)
|
|
59
56
|
: onClick && !href
|
|
60
|
-
? (e) => (e.key === "Enter" || e.key === "
|
|
57
|
+
? (e) => (e.key === "Enter" || e.key === "") && onClick(product)
|
|
61
58
|
: undefined, onClick: handleCardClick, ...(!selectionMode ? longPress : {}), className: [
|
|
62
59
|
"group relative flex h-full flex-col overflow-hidden",
|
|
63
60
|
"rounded-2xl border bg-[var(--appkit-color-surface)]",
|
|
@@ -73,17 +70,11 @@ export function ProductCard({ product, href, onClick, onAddToWishlist, isWishlis
|
|
|
73
70
|
.filter(Boolean)
|
|
74
71
|
.join(" "), children: [_jsxs(Div, { className: `relative ${__O.hidden} aspect-square`, surface: "subtle", children: [product.mainImage ? (_jsx(MediaImage, { src: product.mainImage, alt: product.title, size: "card", className: "transition-transform duration-500 group-hover:scale-105" })) : (_jsx(Row, { surface: "muted", className: "h-full w-full", align: "center", justify: "center", children: _jsx(Span, { className: "opacity-30", size: "4xl", children: "\uD83D\uDECD\uFE0F" }) })), onSelect && (_jsx(BaseListingCard.Checkbox, { selected: isSelected, onSelect: (e) => { e.preventDefault(); onSelect(product.id); }, label: isSelected ? "Deselect" : "Select", position: "top-2 left-2", className: selectionMode || isSelected ? "opacity-100" : "opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-opacity" })), _jsx(Stack, { className: `absolute top-2 ${onSelect ? "left-10" : "left-2"}`, gap: "xs", children: !selectionMode && !isSelected && (_jsxs(_Fragment, { children: [discount && (_jsxs(Span, { weight: "bold", className: CLS_BADGE_NEW, children: ["-", discount, "%"] })), typeBadge && (_jsx(Span, { weight: "bold", className: `text-[10px] ${typeBadge.cls}`, rounded: "full", padding: "pill-xs", shadow: "sm", children: typeBadge.label })), product.partOfBundleIds && product.partOfBundleIds.length > 0 && (_jsx(Span, { className: CLS_BADGE_SALE, title: product.partOfBundleTitles && product.partOfBundleTitles.length > 0
|
|
75
72
|
? `In bundle: ${product.partOfBundleTitles[0]}`
|
|
76
|
-
: "In a bundle", children: "Bundled" })), product.groupId && (_jsx(Span, { className: CLS_BADGE_TRENDING, title: product.groupTitle ? `Part of set: ${product.groupTitle}` : "Part of a set", children: product.isGroupParent ? "Set Parent" : "In Set" })), product.allowOffers && (_jsx(Span, { className: CLS_BADGE_OFFERABLE, title: "The seller accepts offers on this listing", children: "Taking Offers" })), product.sublistingCategoryId && (_jsx(Span, { className: CLS_BADGE_LIMITED, title: "Has variants or sub-listings", children: "Has Variants" }))] })) }), _jsx(BaseListingCard.IconOverlay, { groupIcon: product.groupIcon, sublistingIcon: product.sublistingIcon, position: "bottom-2 right-2" }), onAddToWishlist && (_jsx(
|
|
73
|
+
: "In a bundle", children: "Bundled" })), product.groupId && (_jsx(Span, { className: CLS_BADGE_TRENDING, title: product.groupTitle ? `Part of set: ${product.groupTitle}` : "Part of a set", children: product.isGroupParent ? "Set Parent" : "In Set" })), product.allowOffers && (_jsx(Span, { className: CLS_BADGE_OFFERABLE, title: "The seller accepts offers on this listing", children: "Taking Offers" })), product.sublistingCategoryId && (_jsx(Span, { className: CLS_BADGE_LIMITED, title: "Has variants or sub-listings", children: "Has Variants" }))] })) }), _jsx(BaseListingCard.IconOverlay, { groupIcon: product.groupIcon, sublistingIcon: product.sublistingIcon, position: "bottom-2 right-2" }), onAddToWishlist && (_jsx(WishlistHeartButton, { size: "card", variant: "overlay", inWishlist: !!isWishlisted, addLabel: "Add to wishlist", removeLabel: "Remove from wishlist", className: "absolute right-2 top-2", onToggle: (e) => {
|
|
77
74
|
e.stopPropagation();
|
|
78
75
|
e.preventDefault();
|
|
79
76
|
onAddToWishlist(product.id);
|
|
80
|
-
},
|
|
81
|
-
"absolute right-2 top-2 flex h-8 w-8 items-center justify-center rounded-full shadow-sm",
|
|
82
|
-
"transition-all duration-150",
|
|
83
|
-
isWishlisted
|
|
84
|
-
? CLS_HEART_ACTIVE
|
|
85
|
-
: CLS_HEART_IDLE,
|
|
86
|
-
].join(" "), children: _jsx("svg", { className: "h-4 w-4", fill: isWishlisted ? "currentColor" : "none", stroke: "currentColor", strokeWidth: 2, viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12z" }) }) }))] }), _jsxs(Stack, { className: `flex-1 ${__P.p3} pt-[0.625rem]`, children: [_jsx(Text, { className: `leading-snug`, truncate: 2, size: "sm", weight: "semibold", color: "primary", children: product.title }), (() => {
|
|
77
|
+
} }))] }), _jsxs(Stack, { className: `flex-1 ${__P.p3} pt-[0.625rem]`, children: [_jsx(Text, { className: `leading-snug`, truncate: 2, size: "sm", weight: "semibold", color: "primary", children: product.title }), (() => {
|
|
87
78
|
const categories = Array.isArray(product.categoryNames)
|
|
88
79
|
? product.categoryNames.filter(Boolean)
|
|
89
80
|
: product.categoryName
|
|
@@ -142,7 +133,7 @@ function ProductListRow({ product, onClick, onAddToWishlist, isWishlisted, }) {
|
|
|
142
133
|
100)
|
|
143
134
|
: null;
|
|
144
135
|
return (_jsxs(Div, { role: onClick ? "button" : undefined, tabIndex: onClick ? 0 : undefined, onKeyDown: onClick
|
|
145
|
-
? (e) => (e.key === "Enter" || e.key === "
|
|
136
|
+
? (e) => (e.key === "Enter" || e.key === "") && onClick(product)
|
|
146
137
|
: undefined, onClick: onClick ? () => onClick(product) : undefined, className: [
|
|
147
138
|
"flex items-center gap-[var(--appkit-space-3)] p-[var(--appkit-space-2)] sm:p-[var(--appkit-space-3)]",
|
|
148
139
|
"hover:bg-[var(--appkit-color-surface)]/60 transition-colors",
|
|
@@ -160,10 +151,10 @@ function ProductListRow({ product, onClick, onAddToWishlist, isWishlisted, }) {
|
|
|
160
151
|
return (_jsx(Span, { weight: "medium", className: `text-[11px] ${low
|
|
161
152
|
? "text-warning"
|
|
162
153
|
: "text-[var(--appkit-color-text-faint)]"}`, children: low ? `Only ${stock} left` : `${stock} in stock` }));
|
|
163
|
-
})()] })] }), onAddToWishlist && (_jsx(
|
|
154
|
+
})()] })] }), onAddToWishlist && (_jsx(WishlistHeartButton, { size: "inline", inWishlist: !!isWishlisted, addLabel: "Add to wishlist", removeLabel: "Remove from wishlist", onToggle: (e) => {
|
|
164
155
|
e.stopPropagation();
|
|
165
156
|
onAddToWishlist(product.id);
|
|
166
|
-
}
|
|
157
|
+
} }))] }));
|
|
167
158
|
}
|
|
168
159
|
function safeHref(href) {
|
|
169
160
|
if (!href)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import {
|
|
3
|
+
import { Tabs, TabsList, TabsTrigger } from "@mohasinac/appkit/ui";
|
|
4
4
|
import React from "react";
|
|
5
5
|
import { Div } from "../../../ui/components/Div";
|
|
6
6
|
const DEFAULT_TABS = [
|
|
@@ -19,7 +19,10 @@ export function ProductTabs({ renderDescription, renderSpecs, renderReviews, ext
|
|
|
19
19
|
...extraTabs,
|
|
20
20
|
];
|
|
21
21
|
const tabs = allTabs.length > 0 ? allTabs : DEFAULT_TABS;
|
|
22
|
-
return (_jsxs(Div, { className: className, children: [renderTabBar ? (renderTabBar(activeTab, setActiveTab, tabs)) : (
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
return (_jsxs(Div, { className: className, children: [renderTabBar ? (renderTabBar(activeTab, setActiveTab, tabs)) : (
|
|
23
|
+
// Was a hand-rolled <Row> of ghost <Button>s whose ACTIVE state was
|
|
24
|
+
// `border-current text-current` — the inherited colour, i.e. visually
|
|
25
|
+
// identical to inactive. It also had no role="tablist"/role="tab"/
|
|
26
|
+
// aria-selected at all; routing through Tabs supplies all three.
|
|
27
|
+
_jsx(Tabs, { value: activeTab, onChange: setActiveTab, className: "mb-4", children: _jsx(TabsList, { children: tabs.map((t) => (_jsx(TabsTrigger, { value: t.value, children: t.label }, t.value))) }) })), activeTab === "description" && renderDescription?.(), activeTab === "specs" && renderSpecs?.(), activeTab === "reviews" && renderReviews?.(), renderExtraTab?.(activeTab)] }));
|
|
25
28
|
}
|
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useState } from "react";
|
|
4
|
-
import { Button } from "../../../ui/components/Button";
|
|
5
4
|
import { Div } from "../../../ui/components/Div";
|
|
6
|
-
|
|
7
|
-
xAuto: "overflow-x-auto",
|
|
8
|
-
};
|
|
5
|
+
import { Tabs, TabsList, TabsTrigger } from "../../../ui/components/Tabs";
|
|
9
6
|
const STATIC_TABS = [
|
|
10
7
|
{ id: "description", label: "Description" },
|
|
11
8
|
{ id: "specs", label: "Specifications" },
|
|
@@ -40,7 +37,5 @@ export function ProductTabsShell({ descriptionContent, specsContent, gradingCont
|
|
|
40
37
|
if (allTabs.length === 0)
|
|
41
38
|
return null;
|
|
42
39
|
const activeContent = allTabs.find((t) => t.id === activeId)?.content;
|
|
43
|
-
return (_jsxs(Div, { className: `mt-8 ${className}`, children: [_jsx(
|
|
44
|
-
? "border-[var(--appkit-color-primary)] text-[var(--appkit-color-primary)]"
|
|
45
|
-
: "border-transparent text-[var(--appkit-color-text-muted)] hover:text-[var(--appkit-color-text-muted)]"}`, children: t.label }, t.id))) }), _jsx("div", { children: activeContent })] }));
|
|
40
|
+
return (_jsxs(Div, { className: `mt-8 ${className}`, children: [_jsx(Tabs, { value: activeId, onChange: setActiveId, className: "mb-6", children: _jsx(TabsList, { children: allTabs.map((t) => (_jsx(TabsTrigger, { value: t.id, children: t.label }, t.id))) }) }), _jsx("div", { children: activeContent })] }));
|
|
46
41
|
}
|