@mohasinac/appkit 4.26.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/bundles/data.d.ts +19 -0
- package/dist/_internal/server/features/bundles/data.js +32 -0
- package/dist/_internal/server/features/bundles/index.d.ts +1 -1
- package/dist/_internal/server/features/bundles/index.js +1 -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 +108 -2
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +235 -13
- 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/server.d.ts +1 -1
- package/dist/server.js +1 -1
- 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
|
@@ -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
|
}
|
|
@@ -32,6 +32,8 @@ export declare const ACTION_ID: {
|
|
|
32
32
|
readonly CANCEL_RESERVATION: "cancel-reservation";
|
|
33
33
|
readonly CHECKOUT: "checkout";
|
|
34
34
|
readonly CONTINUE_TO_VERIFY: "continue-to-verification";
|
|
35
|
+
readonly CONTINUE_TO_PAYMENT: "continue-to-payment";
|
|
36
|
+
readonly CHECKOUT_BACK: "checkout-back";
|
|
35
37
|
readonly VERIFY_OTP: "verify-otp";
|
|
36
38
|
readonly PAY_ONLINE: "pay-online";
|
|
37
39
|
readonly PAY_COD: "pay-cod";
|
|
@@ -74,10 +76,25 @@ export interface ActionMeta {
|
|
|
74
76
|
/** Message shown in LoginRequiredModal. Required when requiresAuth=true. */
|
|
75
77
|
authMessage?: string;
|
|
76
78
|
/**
|
|
77
|
-
* RBAC permission key required to use this action
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
79
|
+
* RBAC permission key required to use this action, checked by `useAuthGate`
|
|
80
|
+
* against `SessionUser.permissions`.
|
|
81
|
+
*
|
|
82
|
+
* 🛑 Two preconditions, and BOTH must hold or the action is a guaranteed
|
|
83
|
+
* denial that looks like a working permission check:
|
|
84
|
+
*
|
|
85
|
+
* 1. The value must be a **colon**-separated key that actually exists in
|
|
86
|
+
* `DEFAULT_ROLES` (`"admin:blog:read"`), not dot-separated. Three entries
|
|
87
|
+
* here carried dot-form keys (`"seller.payouts.request"`,
|
|
88
|
+
* `"seller.reviews.respond"`, `"user.account.delete"`) that match no role;
|
|
89
|
+
* they were removed 2026-08-27. Same family as the 182 guaranteed-403
|
|
90
|
+
* `permission:` lines deleted 2026-08-19.
|
|
91
|
+
* 2. `SessionUser.permissions` must be populated for the acting user — today
|
|
92
|
+
* that happens only on the employee path
|
|
93
|
+
* (`_internal/server/features/auth/permissions.ts`). Neither
|
|
94
|
+
* `buildSessionUser` nor `fetchUserProfileFromServer` sets it, so for an
|
|
95
|
+
* ordinary buyer/seller this field denies unconditionally.
|
|
96
|
+
*
|
|
97
|
+
* Leave it unset unless you have verified both.
|
|
81
98
|
*/
|
|
82
99
|
requiredPermission?: string;
|
|
83
100
|
/**
|
|
@@ -53,6 +53,8 @@ export const ACTION_ID = {
|
|
|
53
53
|
// ── Checkout / navigation CTAs ───────────────────────────────────────────
|
|
54
54
|
CHECKOUT: "checkout",
|
|
55
55
|
CONTINUE_TO_VERIFY: "continue-to-verification",
|
|
56
|
+
CONTINUE_TO_PAYMENT: "continue-to-payment",
|
|
57
|
+
CHECKOUT_BACK: "checkout-back",
|
|
56
58
|
VERIFY_OTP: "verify-otp",
|
|
57
59
|
PAY_ONLINE: "pay-online",
|
|
58
60
|
PAY_COD: "pay-cod",
|
|
@@ -98,6 +100,8 @@ export const ACTION_META = {
|
|
|
98
100
|
// ── Checkout / navigation CTAs ────────────────────────────────────────────
|
|
99
101
|
[ACTION_ID.CHECKOUT]: { id: ACTION_ID.CHECKOUT, label: regLabel("CART", "checkout", "Checkout"), variant: "primary", requiresAuth: true, authMessage: "You need to be signed in to checkout." },
|
|
100
102
|
[ACTION_ID.CONTINUE_TO_VERIFY]: { id: ACTION_ID.CONTINUE_TO_VERIFY, label: regLabel("CHECKOUT", "continue-to-verification", "Continue"), variant: "primary", requiresAuth: true, authMessage: "You need to be signed in to checkout." },
|
|
103
|
+
[ACTION_ID.CONTINUE_TO_PAYMENT]: { id: ACTION_ID.CONTINUE_TO_PAYMENT, label: regLabel("CHECKOUT", "continue-to-payment", "Continue to payment"), variant: "primary", requiresAuth: true, authMessage: "You need to be signed in to checkout." },
|
|
104
|
+
[ACTION_ID.CHECKOUT_BACK]: { id: ACTION_ID.CHECKOUT_BACK, label: regLabel("CHECKOUT", "back", "Back"), variant: "ghost" },
|
|
101
105
|
[ACTION_ID.VERIFY_OTP]: { id: ACTION_ID.VERIFY_OTP, label: regLabel("CHECKOUT", "verify-otp", "Verify & Continue"), variant: "primary", requiresAuth: true, authMessage: "You need to be signed in to checkout." },
|
|
102
106
|
[ACTION_ID.PAY_ONLINE]: { id: ACTION_ID.PAY_ONLINE, label: regLabel("CHECKOUT", "pay-online", "Pay Online"), variant: "primary", requiresAuth: true, authMessage: "You need to be signed in to checkout." },
|
|
103
107
|
[ACTION_ID.PAY_COD]: { id: ACTION_ID.PAY_COD, label: regLabel("CHECKOUT", "pay-cod", "Cash on Delivery"), variant: "secondary", requiresAuth: true, authMessage: "You need to be signed in to checkout." },
|
|
@@ -113,8 +117,8 @@ export const ACTION_META = {
|
|
|
113
117
|
[ACTION_ID.MESSAGE_SELLER]: { id: ACTION_ID.MESSAGE_SELLER, label: "Message Seller", variant: "outline", requiresAuth: true, authMessage: "You need to be signed in to message a seller." },
|
|
114
118
|
// ── Seller CTAs ───────────────────────────────────────────────────────────
|
|
115
119
|
[ACTION_ID.BECOME_SELLER]: { id: ACTION_ID.BECOME_SELLER, label: "Apply as Seller", variant: "primary", requiresAuth: true, authMessage: "You need to be signed in to apply as a seller." },
|
|
116
|
-
[ACTION_ID.REQUEST_PAYOUT]: { id: ACTION_ID.REQUEST_PAYOUT, label: regLabel("STORE", "request-payout", "Request Payout"), variant: "outline", requiresAuth: true, authMessage: "You need to be signed in to request a payout."
|
|
117
|
-
[ACTION_ID.RESPOND_TO_REVIEW]: { id: ACTION_ID.RESPOND_TO_REVIEW, label: regLabel("STORE", "reply-review", "Respond to Review"), variant: "outline", requiresAuth: true, authMessage: "You need to be signed in to respond to a review."
|
|
120
|
+
[ACTION_ID.REQUEST_PAYOUT]: { id: ACTION_ID.REQUEST_PAYOUT, label: regLabel("STORE", "request-payout", "Request Payout"), variant: "outline", requiresAuth: true, authMessage: "You need to be signed in to request a payout." },
|
|
121
|
+
[ACTION_ID.RESPOND_TO_REVIEW]: { id: ACTION_ID.RESPOND_TO_REVIEW, label: regLabel("STORE", "reply-review", "Respond to Review"), variant: "outline", requiresAuth: true, authMessage: "You need to be signed in to respond to a review." },
|
|
118
122
|
// ── User account actions ──────────────────────────────────────────────────
|
|
119
123
|
[ACTION_ID.CANCEL_ORDER]: { id: ACTION_ID.CANCEL_ORDER, label: regLabel("USER", "cancel-order", "Cancel Order"), variant: "danger", requiresAuth: true, authMessage: "You need to be signed in to cancel an order." },
|
|
120
124
|
[ACTION_ID.REQUEST_RETURN]: { id: ACTION_ID.REQUEST_RETURN, label: regLabel("USER", "request-return", "Request Return"), variant: "outline", requiresAuth: true, authMessage: "You need to be signed in to request a return." },
|
|
@@ -125,7 +129,7 @@ export const ACTION_META = {
|
|
|
125
129
|
[ACTION_ID.EDIT_ADDRESS]: { id: ACTION_ID.EDIT_ADDRESS, label: "Edit Address", variant: "ghost", requiresAuth: true, authMessage: "You need to be signed in to edit an address." },
|
|
126
130
|
[ACTION_ID.DELETE_ADDRESS]: { id: ACTION_ID.DELETE_ADDRESS, label: regLabel("USER", "delete-address", "Delete Address"), variant: "danger", requiresAuth: true, authMessage: "You need to be signed in to delete an address." },
|
|
127
131
|
[ACTION_ID.CHANGE_PASSWORD]: { id: ACTION_ID.CHANGE_PASSWORD, label: regLabel("USER", "update-password", "Change Password"), variant: "outline", requiresAuth: true, authMessage: "You need to be signed in to change your password." },
|
|
128
|
-
[ACTION_ID.DELETE_ACCOUNT]: { id: ACTION_ID.DELETE_ACCOUNT, label: "Delete Account", variant: "danger", requiresAuth: true, authMessage: "You need to be signed in to delete your account."
|
|
132
|
+
[ACTION_ID.DELETE_ACCOUNT]: { id: ACTION_ID.DELETE_ACCOUNT, label: "Delete Account", variant: "danger", requiresAuth: true, authMessage: "You need to be signed in to delete your account." },
|
|
129
133
|
};
|
|
130
134
|
// Detail page action groups — ordered top-to-bottom in the right-hand panel
|
|
131
135
|
export const DETAIL_ACTIONS = {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { sieveAnd, sieveFilter, SIEVE_OP
|
|
1
|
+
import { sieveAnd, sieveFilter, SIEVE_OP } from "../../../utils/sieve-builder";
|
|
2
|
+
import { sortBy } from "../../../constants/sort";
|
|
2
3
|
import { useQuery } from "@tanstack/react-query";
|
|
3
4
|
import { apiClient } from "../../../http";
|
|
4
5
|
import { PRODUCT_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
@@ -4,7 +4,8 @@ import { DatabaseError } from "../../../errors";
|
|
|
4
4
|
import { BaseRepository, prepareForFirestore, parseSieveDateValue, } from "../../../providers/db-firebase";
|
|
5
5
|
import { cacheManager } from "../../../core";
|
|
6
6
|
import { serverLogger } from "../../../monitoring";
|
|
7
|
-
import { generateUniqueId, slugify,
|
|
7
|
+
import { generateUniqueId, slugify, generateBarcodeId } from "../../../utils";
|
|
8
|
+
import { buildSearchTxt, parseSearchTxtQuery } from "../../../utils/search-txt";
|
|
8
9
|
import { PRODUCT_COLLECTION, ProductStatusValues } from "../schemas";
|
|
9
10
|
import { PRODUCT_FIELDS } from "../../../constants/field-names";
|
|
10
11
|
/**
|
|
@@ -45,8 +46,21 @@ function buildListingKindClause(kind, inverted) {
|
|
|
45
46
|
const op = inverted ? "!=" : "==";
|
|
46
47
|
return `${PRODUCT_FIELDS.LISTING_TYPE}${op}${canonical}`;
|
|
47
48
|
}
|
|
48
|
-
function
|
|
49
|
-
return
|
|
49
|
+
function buildProductSearchTxt(p) {
|
|
50
|
+
return buildSearchTxt([
|
|
51
|
+
p.title,
|
|
52
|
+
p.description,
|
|
53
|
+
p.brand,
|
|
54
|
+
p.brandSlug,
|
|
55
|
+
p.categoryNames,
|
|
56
|
+
p.tags,
|
|
57
|
+
p.features,
|
|
58
|
+
p.condition,
|
|
59
|
+
p.card?.setName,
|
|
60
|
+
p.card?.cardNumber,
|
|
61
|
+
p.grading?.service,
|
|
62
|
+
p.specifications?.map((s) => `${s.name} ${s.value}`),
|
|
63
|
+
]);
|
|
50
64
|
}
|
|
51
65
|
export class ProductRepository extends BaseRepository {
|
|
52
66
|
constructor() {
|
|
@@ -97,7 +111,7 @@ export class ProductRepository extends BaseRepository {
|
|
|
97
111
|
const merged = { ...current, ...data };
|
|
98
112
|
const updated = await super.update(id, {
|
|
99
113
|
...data,
|
|
100
|
-
|
|
114
|
+
searchTxt: buildProductSearchTxt(merged),
|
|
101
115
|
});
|
|
102
116
|
this.cacheSet(updated);
|
|
103
117
|
return updated;
|
|
@@ -121,7 +135,7 @@ export class ProductRepository extends BaseRepository {
|
|
|
121
135
|
barcodeId,
|
|
122
136
|
slug: id,
|
|
123
137
|
availableQuantity: input.stockQuantity,
|
|
124
|
-
|
|
138
|
+
searchTxt: buildProductSearchTxt(input),
|
|
125
139
|
createdAt: new Date(),
|
|
126
140
|
updatedAt: new Date(),
|
|
127
141
|
};
|
|
@@ -359,17 +373,17 @@ export class ProductRepository extends BaseRepository {
|
|
|
359
373
|
if (opts?.storeId) {
|
|
360
374
|
baseQuery = baseQuery.where(PRODUCT_FIELDS.STORE_ID, "==", opts.storeId);
|
|
361
375
|
}
|
|
362
|
-
const
|
|
363
|
-
if (
|
|
376
|
+
const searchTxt = parseSearchTxtQuery(opts?.search ?? "");
|
|
377
|
+
if (searchTxt.length > 0 && opts?.categoriesIn && opts.categoriesIn.length > 0) {
|
|
364
378
|
// Firestore cannot combine two array operators in one query
|
|
365
379
|
}
|
|
366
|
-
else if (
|
|
367
|
-
baseQuery = baseQuery.where(PRODUCT_FIELDS.
|
|
380
|
+
else if (searchTxt.length === 1) {
|
|
381
|
+
baseQuery = baseQuery.where(PRODUCT_FIELDS.SEARCH_TXT, "array-contains", searchTxt[0]);
|
|
368
382
|
}
|
|
369
|
-
else if (
|
|
370
|
-
baseQuery = baseQuery.where(PRODUCT_FIELDS.
|
|
383
|
+
else if (searchTxt.length > 1) {
|
|
384
|
+
baseQuery = baseQuery.where(PRODUCT_FIELDS.SEARCH_TXT, "array-contains-any", searchTxt);
|
|
371
385
|
}
|
|
372
|
-
if (
|
|
386
|
+
if (searchTxt.length === 0 && opts?.categoriesIn && opts.categoriesIn.length > 0) {
|
|
373
387
|
let categoriesIn = opts.categoriesIn;
|
|
374
388
|
if (categoriesIn.length > ProductRepository.MAX_ARRAY_CONTAINS_ANY) {
|
|
375
389
|
// Products carry their FULL ancestor chain in `categorySlugs`, so a
|
|
@@ -559,7 +573,7 @@ ProductRepository.SIEVE_FIELDS = {
|
|
|
559
573
|
// field had no entry: `throwExceptions:false` means an unknown field is
|
|
560
574
|
// silently skipped. The toggle did nothing at all until 2026-08-21.
|
|
561
575
|
shippingPaidBy: { canFilter: true, canSort: false },
|
|
562
|
-
|
|
576
|
+
searchTxt: { canFilter: true, canSort: false },
|
|
563
577
|
"classified.meetupArea": { canFilter: true, canSort: false },
|
|
564
578
|
"classified.acceptsShipping": { canFilter: true, canSort: false },
|
|
565
579
|
"classified.negotiable": { canFilter: true, canSort: false },
|
|
@@ -349,7 +349,8 @@ export interface ProductDocument extends BaseDocument {
|
|
|
349
349
|
* Auto-generated as `LIR-{8-char-hex}` on product creation when not supplied.
|
|
350
350
|
* Override by scanning a pre-printed sticker during the create/edit flow. */
|
|
351
351
|
barcodeId?: string;
|
|
352
|
-
searchTokens
|
|
352
|
+
/** Normalized edge-n-gram search field. Replaced `searchTokens`. */
|
|
353
|
+
searchTxt?: string[];
|
|
353
354
|
}
|
|
354
355
|
/**
|
|
355
356
|
* Single prize within a prize-draw listing. A draw can have between 3 and 16
|
|
@@ -381,7 +382,7 @@ export declare const ProductStatusValues: {
|
|
|
381
382
|
readonly ARCHIVED: "archived";
|
|
382
383
|
};
|
|
383
384
|
export declare const PRODUCT_COLLECTION: "products";
|
|
384
|
-
export declare const PRODUCT_INDEXED_FIELDS: readonly ["storeId", "status", "category", "featured", "listingType", "isPromoted", "isOnSale", "isSold", "
|
|
385
|
+
export declare const PRODUCT_INDEXED_FIELDS: readonly ["storeId", "status", "category", "featured", "listingType", "isPromoted", "isOnSale", "isSold", "searchTxt", "createdAt", "barcodeId"];
|
|
385
386
|
export declare const DEFAULT_PRODUCT_DATA: Partial<ProductDocument>;
|
|
386
387
|
export declare const PRODUCT_PUBLIC_FIELDS: readonly ["allowOffers", "minOfferPercent", "id", "title", "description", "category", "subcategory", "brand", "price", "currency", "stockQuantity", "availableQuantity", "images", "status", "storeName", "featured", "tags", "specifications", "features", "shippingInfo", "returnPolicy", "listingType", "auctionEndDate", "startingBid", "currentBid", "bidCount", "reservePrice", "buyNowPrice", "minBidIncrement", "autoExtendable", "auctionExtensionMinutes", "auctionShippingPaidBy", "bidsHaveStarted", "grading", "card", "classified", "digitalCode", "liveItem", "catalogProductId", "preOrderDeliveryDate", "preOrderDepositPercent", "preOrderDepositAmount", "preOrderMaxQuantity", "preOrderCurrentCount", "preOrderProductionStatus", "preOrderCancellable", "condition", "insurance", "insuranceCost", "shippingPaidBy", "isPromoted", "isOnSale", "isSold", "slug", "seoTitle", "seoDescription", "seoKeywords", "viewCount", "customFields", "customSections", "sublistingCategoryId", "groupId", "isGroupParent", "groupParentSlug", "groupChildSlugs", "groupTitle", "createdAt"];
|
|
387
388
|
export declare const PRODUCT_UPDATABLE_FIELDS: readonly ["title", "description", "category", "subcategory", "brand", "price", "stockQuantity", "images", "status", "tags", "specifications", "features", "shippingInfo", "returnPolicy", "pickupAddressId", "condition", "insurance", "shippingPaidBy", "autoExtendable", "auctionExtensionMinutes", "auctionShippingPaidBy", "reservePrice", "buyNowPrice", "minBidIncrement", "bidsHaveStarted", "grading", "card", "classified", "digitalCode", "liveItem", "catalogProductId", "listingType", "preOrderDeliveryDate", "preOrderDepositPercent", "preOrderDepositAmount", "preOrderMaxQuantity", "preOrderProductionStatus", "preOrderCancellable", "isOnSale", "isSold", "allowShipBeforeEmiComplete", "printMeta", "seoTitle", "seoDescription", "seoKeywords", "customFields", "customSections", "sublistingCategoryId", "groupId", "isGroupParent", "groupParentSlug", "groupChildSlugs", "groupTitle", "barcodeId"];
|