@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
|
@@ -15,6 +15,8 @@ import { toRecordArray, toRelativeDate, toCurrency, toStringValue, } from "../ho
|
|
|
15
15
|
import { DataListingView } from "./DataListingView";
|
|
16
16
|
import { apiClient } from "../../../http";
|
|
17
17
|
import { RecordStatusTimeline } from "../../status-history/components/RecordStatusTimeline";
|
|
18
|
+
import { TextLink } from "../../../ui";
|
|
19
|
+
import { ROUTES } from "../../../constants/index";
|
|
18
20
|
export function AdminBidsView({ children, ...props }) {
|
|
19
21
|
const queryClient = useQueryClient();
|
|
20
22
|
const { showToast } = useToast();
|
|
@@ -99,7 +101,10 @@ export function AdminBidsView({ children, ...props }) {
|
|
|
99
101
|
const detailBadge = detail ? bidStatusBadge(detail.status) : null;
|
|
100
102
|
return (_jsxs(_Fragment, { children: [_jsx(DataListingView, { config: config }), _jsx(RecordDetailModal, { isOpen: detail !== null, onClose: () => setDetail(null), title: toStringValue(detail?.detail?.productTitle, "Bid"), badges: detailBadge ? [{ label: detailBadge.label }] : undefined, fields: detail
|
|
101
103
|
? buildBidDetailFields(detail.detail, "admin")
|
|
102
|
-
: undefined,
|
|
104
|
+
: undefined, footer: detail ? (
|
|
105
|
+
// The full page โ a modal cannot be linked, bookmarked or reloaded
|
|
106
|
+
// into, which is the whole reason the page exists.
|
|
107
|
+
_jsx(TextLink, { href: String(ROUTES.ADMIN.BID_DETAIL(detail.id)), children: "Open full page \u2192" })) : undefined, extra:
|
|
103
108
|
// A bid moves through more states than any record a buyer owns, and
|
|
104
109
|
// every one of them happened inside a settlement batch with nothing
|
|
105
110
|
// recording it until W18. Absent history renders the empty label,
|
|
@@ -2,5 +2,13 @@ import React from "react";
|
|
|
2
2
|
import type { StackedViewShellProps } from "../../../ui";
|
|
3
3
|
export interface AdminCarouselGroupEditorViewProps extends Omit<StackedViewShellProps, "sections"> {
|
|
4
4
|
onCreated?: (id: string) => void;
|
|
5
|
+
/**
|
|
6
|
+
* Present โ edit that carousel; absent โ create a new one.
|
|
7
|
+
*
|
|
8
|
+
* This component was create-only, which is why `/admin/carousels/[id]/edit`
|
|
9
|
+
* did not exist. Same `featureId?`-style seam `AdminFeatureEditorView` uses.
|
|
10
|
+
*/
|
|
11
|
+
carouselId?: string;
|
|
12
|
+
onSaved?: (id: string) => void;
|
|
5
13
|
}
|
|
6
|
-
export declare function AdminCarouselGroupEditorView({ onCreated, ...rest }: AdminCarouselGroupEditorViewProps): React.JSX.Element;
|
|
14
|
+
export declare function AdminCarouselGroupEditorView({ onCreated, carouselId, onSaved, ...rest }: AdminCarouselGroupEditorViewProps): React.JSX.Element;
|
|
@@ -3,6 +3,7 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
|
|
|
3
3
|
import React from "react";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { useRouter } from "next/navigation";
|
|
6
|
+
import { useQuery } from "@tanstack/react-query";
|
|
6
7
|
import { Button, Form, Row, StackedViewShell, useToast } from "../../../ui";
|
|
7
8
|
import { FieldInput, FieldSelect, FormErrorSummary } from "../../../ui/forms";
|
|
8
9
|
import { useApiMutation } from "@mohasinac/appkit/client";
|
|
@@ -13,28 +14,59 @@ const carouselGroupFormSchema = z.object({
|
|
|
13
14
|
name: z.string().min(1, "Name is required").max(200),
|
|
14
15
|
status: z.enum(["active", "draft"]),
|
|
15
16
|
});
|
|
16
|
-
export function AdminCarouselGroupEditorView({ onCreated, ...rest }) {
|
|
17
|
+
export function AdminCarouselGroupEditorView({ onCreated, carouselId, onSaved, ...rest }) {
|
|
17
18
|
const { showToast } = useToast();
|
|
18
19
|
const router = useRouter();
|
|
20
|
+
const isEdit = Boolean(carouselId);
|
|
19
21
|
const [name, setName] = React.useState("");
|
|
20
22
|
const [status, setStatus] = React.useState("draft");
|
|
23
|
+
// Seed from the single-item GET, never from a cached list row โ a list
|
|
24
|
+
// projection is narrower than the document, and saving would write the
|
|
25
|
+
// missing fields back as their defaults (Root Cause #38).
|
|
26
|
+
const existing = useQuery({
|
|
27
|
+
queryKey: ["admin", "carousel", carouselId],
|
|
28
|
+
queryFn: () => apiClient.get(ADMIN_ENDPOINTS.CAROUSELS_BY_ID(carouselId)),
|
|
29
|
+
enabled: isEdit,
|
|
30
|
+
});
|
|
31
|
+
React.useEffect(() => {
|
|
32
|
+
const doc = existing.data?.data;
|
|
33
|
+
if (!doc)
|
|
34
|
+
return;
|
|
35
|
+
setName(doc.name ?? "");
|
|
36
|
+
setStatus(doc.status === "active" ? "active" : "draft");
|
|
37
|
+
}, [existing.data]);
|
|
21
38
|
const createMutation = useApiMutation({
|
|
22
|
-
|
|
39
|
+
/*
|
|
40
|
+
* PATCH on the edit path, not PUT. `/api/admin/carousels/[id]` exports
|
|
41
|
+
* GET/PATCH/DELETE โ copying `AdminFeatureEditorView`'s `apiClient.put`
|
|
42
|
+
* here would have been a 405, which is exactly what
|
|
43
|
+
* `audit-client-verb-match` exists to catch.
|
|
44
|
+
*/
|
|
45
|
+
mutationFn: () => isEdit
|
|
46
|
+
? apiClient.patch(ADMIN_ENDPOINTS.CAROUSELS_BY_ID(carouselId), { name, status })
|
|
47
|
+
: apiClient.post(ADMIN_ENDPOINTS.CAROUSELS, { name, status }),
|
|
23
48
|
onSuccess: (res) => {
|
|
24
|
-
const id = res?.data?.id;
|
|
25
|
-
showToast("Carousel created.", "success");
|
|
26
|
-
if (id)
|
|
27
|
-
|
|
28
|
-
|
|
49
|
+
const id = res?.data?.id ?? carouselId;
|
|
50
|
+
showToast(isEdit ? "Carousel updated." : "Carousel created.", "success");
|
|
51
|
+
if (!id)
|
|
52
|
+
return;
|
|
53
|
+
if (isEdit) {
|
|
54
|
+
if (onSaved)
|
|
55
|
+
onSaved(id);
|
|
29
56
|
else
|
|
30
57
|
router.push(String(ROUTES.ADMIN.CAROUSEL_DETAIL(id)));
|
|
58
|
+
return;
|
|
31
59
|
}
|
|
60
|
+
if (onCreated)
|
|
61
|
+
onCreated(id);
|
|
62
|
+
else
|
|
63
|
+
router.push(String(ROUTES.ADMIN.CAROUSEL_DETAIL(id)));
|
|
32
64
|
},
|
|
33
65
|
onError: (err) => {
|
|
34
|
-
showToast(err?.message ??
|
|
66
|
+
showToast(err?.message ?? `Failed to ${isEdit ? "update" : "create"} carousel.`, "error");
|
|
35
67
|
},
|
|
36
68
|
});
|
|
37
|
-
return (_jsx(StackedViewShell, { portal: "admin", ...rest, title: "New Named Carousel", sections: [
|
|
69
|
+
return (_jsx(StackedViewShell, { portal: "admin", ...rest, title: isEdit ? "Edit Carousel" : "New Named Carousel", sections: [
|
|
38
70
|
_jsx(Form, { schema: carouselGroupFormSchema, onSubmit: (e) => e.preventDefault(), spacing: "md", children: ({ setFieldError, clearErrors }) => (_jsxs(_Fragment, { children: [_jsx(FieldInput, { name: "name", label: "Carousel name", value: name, onChange: (v) => setName(v), required: true, placeholder: "e.g. Homepage Hero" }), _jsx(FieldSelect, { name: "status", label: "Status", value: status, onChange: (v) => setStatus(v), options: [
|
|
39
71
|
{ value: "draft", label: "Draft" },
|
|
40
72
|
{ value: "active", label: "Active" },
|
|
@@ -45,6 +77,6 @@ export function AdminCarouselGroupEditorView({ onCreated, ...rest }) {
|
|
|
45
77
|
return;
|
|
46
78
|
}
|
|
47
79
|
createMutation.mutate();
|
|
48
|
-
}, children: "Create carousel" }) })] })) }, "carousel-group-form"),
|
|
80
|
+
}, children: isEdit ? "Save changes" : "Create carousel" }) })] })) }, "carousel-group-form"),
|
|
49
81
|
] }));
|
|
50
82
|
}
|
|
@@ -145,5 +145,5 @@ export function AdminCarouselView({ children, onBulkDelete, ...props }) {
|
|
|
145
145
|
] })),
|
|
146
146
|
},
|
|
147
147
|
];
|
|
148
|
-
return (_jsxs(Div, { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search slide titles", onSearchChange: setSearchInput, onSearchCommit: commitSearch, showTableView: true, view: view, onViewChange: (v) => setView(v), onResetAll: resetAll, hasActiveState: hasActiveState }), totalPages > 1 && (_jsx(StickyToolbar, { offset: "header+pagination", tone: "translucent", border: true, padding: "toolbar", children: _jsx(Row, { justify: "center", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) }) })), selection.selectedCount > 0 && bulkActions.length > 0 && (_jsx(BulkActionBar, { selectedCount: selection.selectedCount, actions: bulkActions, onClearSelection: selection.clearSelection })), _jsxs(Div, { paddingX: "x-sm-md", padding: "y-md", children: [errorMessage && (_jsx(Div, { textSize: "sm", className: "mb-4 border border-error/20", color: "error", surface: "danger-surface", padding: "inline", rounded: "xl", children: errorMessage })), _jsx(DataTable, { rows: displayRows, columns: columns, isLoading: isLoading, emptyLabel: "No carousel slides \u2014 use 'New slide' to add one", rowHrefTemplate: "/admin/carousel/{id}/edit" })] }), _jsx(ListingFilterDrawer, { open: filterOpen, onClose: () => setFilterOpen(false), onApply: applyFilters, onClear: clearFilters, activeCount: activeFilterCount, children: _jsxs(Stack, { gap: "sm", children: [_jsx(Text, { className: "tracking-widest", color: "muted", size: "xs", weight: "semibold", transform: "uppercase", children: "Status" }), _jsx(Row, { wrap: true, gap: "sm", children: [{ label: "All", value: "" }, { label: "Active", value: "true" }, { label: "Inactive", value: "false" }].map((opt) => (_jsx(Button, { variant: "ghost", type: "button", onClick: () => setPendingFilters((p) => ({ ...p, active: opt.value })), className: `rounded-full px-[var(--appkit-space-3)] py-[var(--appkit-space-1)] text-[length:var(--appkit-text-xs)] font-medium border transition-colors min-h-0 h-auto ${(pendingFilters.active || "") === opt.value ? "bg-primary text-white border-primary" : "border-[var(--appkit-color-border)] text-[var(--appkit-color-text-muted)] hover:bg-
|
|
148
|
+
return (_jsxs(Div, { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search slide titles", onSearchChange: setSearchInput, onSearchCommit: commitSearch, showTableView: true, view: view, onViewChange: (v) => setView(v), onResetAll: resetAll, hasActiveState: hasActiveState }), totalPages > 1 && (_jsx(StickyToolbar, { offset: "header+pagination", tone: "translucent", border: true, padding: "toolbar", children: _jsx(Row, { justify: "center", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) }) })), selection.selectedCount > 0 && bulkActions.length > 0 && (_jsx(BulkActionBar, { selectedCount: selection.selectedCount, actions: bulkActions, onClearSelection: selection.clearSelection })), _jsxs(Div, { paddingX: "x-sm-md", padding: "y-md", children: [errorMessage && (_jsx(Div, { textSize: "sm", className: "mb-4 border border-error/20", color: "error", surface: "danger-surface", padding: "inline", rounded: "xl", children: errorMessage })), _jsx(DataTable, { rows: displayRows, columns: columns, isLoading: isLoading, emptyLabel: "No carousel slides \u2014 use 'New slide' to add one", rowHrefTemplate: "/admin/carousel/{id}/edit" })] }), _jsx(ListingFilterDrawer, { open: filterOpen, onClose: () => setFilterOpen(false), onApply: applyFilters, onClear: clearFilters, activeCount: activeFilterCount, children: _jsxs(Stack, { gap: "sm", children: [_jsx(Text, { className: "tracking-widest", color: "muted", size: "xs", weight: "semibold", transform: "uppercase", children: "Status" }), _jsx(Row, { wrap: true, gap: "sm", children: [{ label: "All", value: "" }, { label: "Active", value: "true" }, { label: "Inactive", value: "false" }].map((opt) => (_jsx(Button, { variant: "ghost", type: "button", onClick: () => setPendingFilters((p) => ({ ...p, active: opt.value })), className: `rounded-full px-[var(--appkit-space-3)] py-[var(--appkit-space-1)] text-[length:var(--appkit-text-xs)] font-medium border transition-colors min-h-0 h-auto ${(pendingFilters.active || "") === opt.value ? "bg-primary text-white border-primary" : "border-[var(--appkit-color-border)] text-[var(--appkit-color-text-muted)] hover:bg-surface-hover"}`, children: opt.label }, opt.label))) })] }) }), deleteTargetId && (_jsx(ConfirmDeleteModal, { isOpen: true, title: "Delete Carousel Slide", message: "Delete this carousel slide? This cannot be undone.", onConfirm: () => { deleteMutation.mutate(deleteTargetId); setDeleteTargetId(null); }, onClose: () => setDeleteTargetId(null), isDeleting: deleteMutation.isPending }))] }));
|
|
149
149
|
}
|
|
@@ -19,7 +19,7 @@ const __O = {
|
|
|
19
19
|
};
|
|
20
20
|
const PILL_BASE_CLASS = "inline-flex items-center gap-[var(--appkit-space-1-5)] rounded-full border px-[var(--appkit-space-3)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-xs)] font-medium cursor-pointer transition-colors";
|
|
21
21
|
const PILL_CHECKED_CLASS = "bg-primary text-white border-primary";
|
|
22
|
-
const PILL_UNCHECKED_CLASS = "border-[var(--appkit-color-border)] text-[var(--appkit-color-text-muted)] hover:bg-
|
|
22
|
+
const PILL_UNCHECKED_CLASS = "border-[var(--appkit-color-border)] text-[var(--appkit-color-text-muted)] hover:bg-surface-hover";
|
|
23
23
|
const FIELD_LABEL_CLASS = "text-[length:var(--appkit-text-sm)] font-medium text-[var(--appkit-color-text-muted)]";
|
|
24
24
|
const DELETE_CONFIRM_TEXT = "Delete this feature? It will fail if any product still references it.";
|
|
25
25
|
const TOAST = {
|
|
@@ -8,12 +8,13 @@ import { normalizeError } from "../../../errors/normalize";
|
|
|
8
8
|
import { sortBy } from "@mohasinac/appkit/client";
|
|
9
9
|
import React, { useState } from "react";
|
|
10
10
|
import { ListingLayout } from "../../../ui";
|
|
11
|
-
import { Button, Row, SideDrawer, Stack, Text, useToast, } from "../../../ui";
|
|
11
|
+
import { Button, Row, SideDrawer, Stack, Text, TextLink, useToast, } from "../../../ui";
|
|
12
12
|
import { toRecordArray, toRelativeDate, toStringValue, } from "../hooks/useAdminListingData";
|
|
13
13
|
import { DataListingView } from "./DataListingView";
|
|
14
14
|
import { ProductInlineSelect } from "../../seller/components/ProductInlineSelect";
|
|
15
15
|
import { apiClient } from "../../../http";
|
|
16
16
|
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
17
|
+
import { ROUTES } from "../../../next/routing/route-map";
|
|
17
18
|
function ReassignProductsDrawer({ state, onClose, onSaved, }) {
|
|
18
19
|
const [productIds, setProductIds] = useState(state.currentProductIds);
|
|
19
20
|
const [saving, setSaving] = useState(false);
|
|
@@ -75,10 +76,21 @@ export function AdminGroupedListingsView({ children, ...props }) {
|
|
|
75
76
|
})),
|
|
76
77
|
getTotal: (response, mappedRows) => typeof response.total === "number" ? response.total : mappedRows.length,
|
|
77
78
|
buildFilters: () => "",
|
|
79
|
+
/*
|
|
80
|
+
* Admin had NO create path at all โ there was no POST route either,
|
|
81
|
+
* so this could not have been wired before now.
|
|
82
|
+
*
|
|
83
|
+
* `toolbarExtra` rather than `primaryAction`: the latter takes an
|
|
84
|
+
* `onClick(panel)` for opening a create DRAWER, and this create is a
|
|
85
|
+
* page. Same shape `AdminAdsView` uses, and `audit-create-affordance`
|
|
86
|
+
* counts both โ which is exactly why it has to, or four views with a
|
|
87
|
+
* perfectly good create link would read as having none.
|
|
88
|
+
*/
|
|
89
|
+
toolbarExtra: (_jsx(TextLink, { variant: "bare", href: String(ROUTES.ADMIN.GROUPED_LISTINGS_NEW), rounded: "md", paddingX: "sm", size: "sm", weight: "medium", layout: "inline-flex", align: "center", className: "h-9 bg-[var(--appkit-color-surface)] text-[var(--appkit-color-text)]", children: "New group" })),
|
|
78
90
|
// Mirrors the row action button's "Reassign products" drawer so the
|
|
79
91
|
// row itself is clickable, not just the button.
|
|
80
92
|
onRowClick: (row) => openReassign(row),
|
|
81
|
-
renderRowActions: (row) => (_jsx(Button, { size: "sm", variant: "outline", onClick: () => openReassign(row), children: "Reassign products" })),
|
|
93
|
+
renderRowActions: (row) => (_jsxs(Row, { gap: "xs", children: [_jsx(TextLink, { href: String(ROUTES.ADMIN.GROUPED_LISTINGS_EDIT(row.id)), children: "Edit" }), _jsx(Button, { size: "sm", variant: "outline", onClick: () => openReassign(row), children: "Reassign products" })] })),
|
|
82
94
|
}), [refreshKey, openReassign]);
|
|
83
95
|
if (React.Children.count(children) > 0) {
|
|
84
96
|
return (_jsx(ListingLayout, { portal: "admin", ...props, children: children }));
|
|
@@ -17,7 +17,7 @@ const __O = {
|
|
|
17
17
|
function NavLink({ item, isActive, onClick }) {
|
|
18
18
|
return (_jsxs(Link, { href: item.href, onClick: onClick, className: `flex items-center justify-end gap-[var(--appkit-space-2-5)] rounded-lg px-[var(--appkit-space-3)] py-[var(--appkit-space-2)] text-[0.8125rem] font-medium leading-tight transition-colors ${isActive
|
|
19
19
|
? "bg-[var(--appkit-color-surface)] bg-[var(--appkit-color-surface-elevated)] text-[var(--appkit-color-text)]"
|
|
20
|
-
: "text-[var(--appkit-color-text-muted)] hover:bg-
|
|
20
|
+
: "text-[var(--appkit-color-text-muted)] hover:bg-surface-hover hover:text-zinc-800 hover:text-[var(--appkit-color-text-muted)]"}`, children: [item.icon && _jsx(Span, { size: "base", className: "shrink-0 opacity-60", children: item.icon }), _jsx(Span, { className: "truncate", children: item.label })] }));
|
|
21
21
|
}
|
|
22
22
|
function GroupsContent({ groups, activePath, onItemClick, }) {
|
|
23
23
|
// Accordion โ only one group open at a time. Initial pick: the group
|
|
@@ -52,7 +52,7 @@ function AdminCardItem({ row, view, selected, onToggleSelect, onRowClick, render
|
|
|
52
52
|
};
|
|
53
53
|
if (view === "list") {
|
|
54
54
|
return (_jsxs(Row, { gap: "sm", className: [
|
|
55
|
-
"px-[var(--appkit-space-4)] py-[var(--appkit-space-3)] cursor-pointer transition-colors hover:bg-
|
|
55
|
+
"px-[var(--appkit-space-4)] py-[var(--appkit-space-3)] cursor-pointer transition-colors hover:bg-surface-hover",
|
|
56
56
|
selected ? "bg-primary-50/40 dark:bg-primary-900/10" : "",
|
|
57
57
|
].filter(Boolean).join(" "), onClick: handleClick, role: onRowClick ? "button" : undefined, children: [onToggleSelect && (_jsx(Div, { "data-no-row-click": true, className: "shrink-0", onClick: (e) => { e.stopPropagation(); onToggleSelect(row.id); }, children: _jsx(Checkbox, { bare: true, checked: selected, onChange: () => onToggleSelect(row.id), className: "h-4 w-4 rounded border-zinc-300 text-primary accent-primary cursor-pointer", "aria-label": `Select ${row.primary}` }) })), _jsx(RowAvatar, { image: row.image, alt: row.primary, icon: resourceIcon }), _jsxs(Stack, { gap: "none", className: "flex-1 min-w-0", children: [_jsx(Text, { size: "sm", weight: "semibold", className: "truncate", color: "primary", children: row.primary }), _jsx(Text, { size: "xs", color: "muted", className: "truncate", children: row.secondary }), row.barcodeId && (_jsx(Text, { size: "xs", color: "faint", className: "truncate font-mono", children: row.barcodeId }))] }), flags.length > 0 && (_jsx(Row, { gap: "xs", className: "hidden sm:flex shrink-0", children: flags.map(({ key, label, tone }) => (_jsx(Span, { padding: "pill-2xs", weight: "medium", className: `inline-flex text-[10px] ${STATUS_TONE_CLASSES[tone]}`, rounded: "full", children: label }, key))) })), _jsx(StatusBadge, { status: row.status }), _jsx(Span, { size: "xs", color: "muted", className: "hidden sm:block shrink-0 w-24", align: "end", children: row.updatedAt }), renderRowActions && (_jsx(Div, { "data-no-row-click": true, className: "shrink-0", onClick: (e) => e.stopPropagation(), children: renderRowActions(row) }))] }));
|
|
58
58
|
}
|
|
@@ -60,7 +60,7 @@ function AdminCardItem({ row, view, selected, onToggleSelect, onRowClick, render
|
|
|
60
60
|
"border overflow-hidden cursor-pointer transition-all hover:shadow-md hover:-translate-y-0.5",
|
|
61
61
|
selected
|
|
62
62
|
? "border-primary ring-1 ring-primary/20 bg-primary-50/30 dark:bg-primary-900/10"
|
|
63
|
-
: "
|
|
63
|
+
: " border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)]",
|
|
64
64
|
].filter(Boolean).join(" "), onClick: handleClick, role: onRowClick ? "button" : undefined, children: [onToggleSelect && (_jsxs(Row, { gap: "xs", "data-no-row-click": true, paddingY: "t-sm", padding: "x-sm", onClick: (e) => { e.stopPropagation(); onToggleSelect(row.id); }, children: [_jsx(Checkbox, { bare: true, checked: selected, onChange: () => onToggleSelect(row.id), className: "h-4 w-4 rounded border-zinc-300 text-primary accent-primary cursor-pointer", "aria-label": `Select ${row.primary}` }), flags.length > 0 && (_jsx(Row, { gap: "xs", wrap: true, children: flags.map(({ key, label, tone }) => (_jsx(Span, { padding: "pill-2xs", weight: "medium", className: `inline-flex text-[10px] ${STATUS_TONE_CLASSES[tone]}`, rounded: "full", children: label }, key))) }))] })), _jsxs(Stack, { gap: "xs", className: `${__P.p3}.5`, children: [_jsxs(Row, { gap: "sm", align: "center", children: [_jsx(RowAvatar, { image: row.image, alt: row.primary, icon: resourceIcon, size: "12" }), _jsxs(Stack, { gap: "none", className: "min-w-0 flex-1", children: [_jsx(Text, { size: "sm", weight: "semibold", className: "line-clamp-2 leading-snug", color: "primary", children: row.primary }), _jsx(Text, { size: "xs", color: "muted", className: "truncate", children: row.secondary })] })] }), row.barcodeId && (_jsx(Text, { size: "xs", color: "faint", className: "truncate font-mono", children: row.barcodeId })), _jsxs(Row, { justify: "between", gap: "xs", children: [_jsx(StatusBadge, { status: row.status }), _jsx(Span, { color: "muted", className: "text-[11px] shrink-0", children: row.updatedAt })] }), renderRowActions && (_jsx(Row, { "data-no-row-click": true, justify: "end", gap: "xs", className: "border-t border-[var(--appkit-color-border)] -mx-[var(--appkit-space-3)] -mb-[var(--appkit-space-3)] mt-[var(--appkit-space-1)] px-[var(--appkit-space-3)] py-[var(--appkit-space-2)]", onClick: (e) => e.stopPropagation(), children: renderRowActions(row) }))] })] }));
|
|
65
65
|
}
|
|
66
66
|
export function AdminViewCards({ rows, view, isLoading, emptyLabel = "No items found", onRowClick, selectedIdSet, onToggleSelect, renderRowActions, resourceIcon, }) {
|
|
@@ -39,7 +39,7 @@ function SelectableRow({ row, columns, isSelected, onToggle, renderRowActions, o
|
|
|
39
39
|
: undefined;
|
|
40
40
|
const handleKeyDown = handleClick
|
|
41
41
|
? (event) => {
|
|
42
|
-
if (event.key === "Enter" || event.key === "
|
|
42
|
+
if (event.key === "Enter" || event.key === "")
|
|
43
43
|
handleClick();
|
|
44
44
|
}
|
|
45
45
|
: undefined;
|
|
@@ -54,5 +54,5 @@ export function DataTable({ columns: columnsProp, rows, isLoading, sortKey, sort
|
|
|
54
54
|
const allRowsSelected = selectionEnabled && rows.length > 0 && rows.every((r) => selectedIds?.has(r.id));
|
|
55
55
|
return (_jsxs(Div, { surface: "card", className: `${__O.hidden}`, children: [_jsx(Div, { className: `${__O.xAuto}`, children: _jsxs(Table, { "data-testid": "data-table", size: "sm", children: [_jsx(Thead, { children: _jsxs(Tr, { className: "bg-[var(--appkit-color-bg)] dark:bg-[var(--appkit-color-surface-elevated)]", border: "default", children: [selectionEnabled && (_jsx(Th, { padding: "xs-3", scope: "col", className: "w-10", children: onToggleSelectAll && (_jsx(Checkbox, { bare: true, "data-testid": "select-all-checkbox", "aria-label": allRowsSelected ? "Deselect all" : "Select all", checked: allRowsSelected, onChange: () => onToggleSelectAll(!allRowsSelected), className: "h-4 w-4 rounded border-[var(--appkit-color-border)] accent-zinc-900 dark:accent-zinc-100" })) })), columns.map((col) => (_jsxs(Th, { scope: "col", onClick: col.sortable && onSort ? () => onSort(col.key) : undefined, className: `text-left text-[var(--appkit-color-text)] ${col.sortable && onSort ? "cursor-pointer select-none hover:text-primary" : ""} ${COLUMN_PRIORITY_CLASS[resolveColumnPriority(col)]} ${col.className ?? ""}`, padding: "md", weight: "semibold", children: [col.header, col.sortable && sortKey === col.key && (_jsx(Span, { className: "ml-1", children: sortDir === "asc" ? "โ" : "โ" }))] }, col.key))), renderRowActions && _jsx(Th, { padding: "xs-3", scope: "col", className: "w-12" })] }) }), _jsx(Tbody, { children: isLoading ? (Array.from({ length: 5 }).map((_, i) => (_jsxs(Tr, { border: "default", children: [selectionEnabled && _jsx(Td, { padding: "xs-3", className: "w-10" }), columns.map((col) => (_jsx(Td, { padding: "md", className: COLUMN_PRIORITY_CLASS[resolveColumnPriority(col)], children: _jsx(Div, { className: "h-4 w-full animate-pulse bg-neutral-200", rounded: "default" }) }, col.key)))] }, i)))) : rows.length === 0 ? (_jsx(Tr, { children: _jsx(Td, { colSpan: columns.length + (selectionEnabled ? 1 : 0) + (renderRowActions ? 1 : 0), align: "center", color: "faint", paddingX: "md", paddingY: "md", children: emptyLabel }) })) : (rows.map((row) => (_jsx(SelectableRow, { row: row, columns: columns, isSelected: selectedIds?.has(row.id) ?? false, onToggle: onToggleSelect, renderRowActions: renderRowActions, onRowClick: onRowClick, rowHref: typeof rowHrefTemplate === "function"
|
|
56
56
|
? rowHrefTemplate(row)
|
|
57
|
-
: rowHrefTemplate?.replace("{id}", encodeURIComponent(row.id)), selectionEnabled: selectionEnabled }, row.id)))) })] }) }), totalPages > 1 && onPageChange && (_jsx(Row, { className: "border-t border-neutral-200", padding: "inline", align: "center", justify: "end", gap: "sm", children: Array.from({ length: totalPages }, (_, i) => i + 1).map((p) => (_jsx(Button, { onClick: () => onPageChange(p), variant: p === currentPage ? "primary" : "ghost", size: "sm", className: `h-8 w-8 rounded text-[length:var(--appkit-text-xs)] font-medium transition ${p === currentPage ? "bg-neutral-900 text-white" : "text-[var(--appkit-color-text-muted)] text-[var(--appkit-color-text-muted)] hover:bg-
|
|
57
|
+
: rowHrefTemplate?.replace("{id}", encodeURIComponent(row.id)), selectionEnabled: selectionEnabled }, row.id)))) })] }) }), totalPages > 1 && onPageChange && (_jsx(Row, { className: "border-t border-neutral-200", padding: "inline", align: "center", justify: "end", gap: "sm", children: Array.from({ length: totalPages }, (_, i) => i + 1).map((p) => (_jsx(Button, { onClick: () => onPageChange(p), variant: p === currentPage ? "primary" : "ghost", size: "sm", className: `h-8 w-8 rounded text-[length:var(--appkit-text-xs)] font-medium transition ${p === currentPage ? "bg-neutral-900 text-white" : "text-[var(--appkit-color-text-muted)] text-[var(--appkit-color-text-muted)] hover:bg-surface-hover"}`, children: p }, p))) }))] }));
|
|
58
58
|
}
|
|
@@ -254,11 +254,17 @@ export const DEFAULT_SITE_SETTINGS_DATA = {
|
|
|
254
254
|
codDepositPercent: 10,
|
|
255
255
|
codHandlingFeeMin: 200,
|
|
256
256
|
codHandlingFeePercent: 10,
|
|
257
|
-
|
|
257
|
+
// The three buyer add-ons ship ENABLED. They were `false`, which made
|
|
258
|
+
// StoreAddonsPicker return null on both the cart and checkout, so a fresh
|
|
259
|
+
// install had no add-ons at all and no indication why. `undefined` still
|
|
260
|
+
// means disabled everywhere (computeWhatsAppNotifyFee et al. read
|
|
261
|
+
// `if (!rates.XEnabled) return 0`) โ only the DEFAULT changed, so an
|
|
262
|
+
// existing siteSettings document keeps whatever it already stored.
|
|
263
|
+
whatsappNotifyFeeEnabled: true,
|
|
258
264
|
whatsappNotifyFee: 10,
|
|
259
|
-
giftWrapFeeEnabled:
|
|
265
|
+
giftWrapFeeEnabled: true,
|
|
260
266
|
giftWrapFee: 49,
|
|
261
|
-
shipmentProtectionFeeEnabled:
|
|
267
|
+
shipmentProtectionFeeEnabled: true,
|
|
262
268
|
shipmentProtectionFeePercent: 2,
|
|
263
269
|
shipmentProtectionFeeMin: 30,
|
|
264
270
|
sellerShippingFixed: 0,
|
|
@@ -12,6 +12,7 @@ import { bidRepository } from "../repository/bid.repository";
|
|
|
12
12
|
import { productRepository } from "../../products/repository/products.repository";
|
|
13
13
|
import { isAuctionListing } from "../../products/utils/listing-type";
|
|
14
14
|
import { userRepository } from "../../auth/repository/user.repository";
|
|
15
|
+
import { isSoftBanned } from "../../auth/server/checkSoftBan";
|
|
15
16
|
import { unitOfWork } from "../../../core/unit-of-work";
|
|
16
17
|
import { storeRepository } from "../../stores/repository/store.repository";
|
|
17
18
|
import { siteSettingsRepository } from "../../../repositories";
|
|
@@ -28,6 +29,29 @@ import { cartRepository } from "../../cart/repository/cart.repository";
|
|
|
28
29
|
import { AUCTION_BUYOUT_WINDOW_MINUTES, AUCTION_BUYOUT_WINDOW_MS, AUCTION_CHECKOUT_URL, } from "../../../_internal/shared/checkout/lanes";
|
|
29
30
|
import { resolveDate } from "../../../utils";
|
|
30
31
|
// --- Domain Functions ----------------------------------------------------------
|
|
32
|
+
/**
|
|
33
|
+
* Load the bidder's profile and refuse if they carry an active `place_bids`
|
|
34
|
+
* soft ban. Returns the profile so the caller pays for exactly one read.
|
|
35
|
+
*
|
|
36
|
+
* ๐ This belongs in the DOMAIN function, not in a route.
|
|
37
|
+
*
|
|
38
|
+
* Admins can issue a `place_bids` soft ban and `BID_ERROR_CODES.USER_BANNED`
|
|
39
|
+
* has always existed with a display-map entry and client copy โ but nothing
|
|
40
|
+
* ever threw it. The only `isSoftBanned(user, "place_bids")` call in the
|
|
41
|
+
* codebase sat in `POST /api/auctions/[id]/buy-now`, a route with zero callers,
|
|
42
|
+
* while the live path (the `placeBidAction` / `buyNowAction` server actions)
|
|
43
|
+
* checked nothing. A banned user could bid and buy out freely. Enforcing it
|
|
44
|
+
* here means every caller โ action, route, or job โ is covered by
|
|
45
|
+
* construction.
|
|
46
|
+
*/
|
|
47
|
+
async function assertCanBid(userId) {
|
|
48
|
+
const profile = await userRepository.findById(userId);
|
|
49
|
+
if (profile && isSoftBanned(profile, "place_bids")) {
|
|
50
|
+
const ban = profile.softBans?.find((b) => b.action === "place_bids");
|
|
51
|
+
throw new AuthorizationError(`Your account is restricted from bidding. Reason: ${ban?.reason ?? "Policy violation"}.`, { code: BID_ERROR_CODES.USER_BANNED });
|
|
52
|
+
}
|
|
53
|
+
return profile ?? null;
|
|
54
|
+
}
|
|
31
55
|
export async function placeBid(userId, userEmail, input) {
|
|
32
56
|
const { productId, bidAmount, autoMaxBid } = input;
|
|
33
57
|
const product = await productRepository.findById(productId);
|
|
@@ -50,6 +74,9 @@ export async function placeBid(userId, userEmail, input) {
|
|
|
50
74
|
throw new AuthorizationError(ERROR_MESSAGES.BID.OWN_AUCTION);
|
|
51
75
|
}
|
|
52
76
|
}
|
|
77
|
+
// Before any write. The profile is reused below for the bid's denormalised
|
|
78
|
+
// userName/userEmail, so the ban check costs no extra read.
|
|
79
|
+
const profile = await assertCanBid(userId);
|
|
53
80
|
// An auction with no bids yet prices off its starting bid, and that starting
|
|
54
81
|
// bid is itself acceptable โ see ResolveMinBidOptions.hasBids.
|
|
55
82
|
const hasBids = (product.currentBid ?? 0) > 0 || (product.bidCount ?? 0) > 0;
|
|
@@ -122,7 +149,6 @@ export async function placeBid(userId, userEmail, input) {
|
|
|
122
149
|
visibleBid = newCap;
|
|
123
150
|
bumpedPreviousVisible = Math.min(prevCap, target);
|
|
124
151
|
}
|
|
125
|
-
const profile = await userRepository.findById(userId);
|
|
126
152
|
const bid = await bidRepository.create({
|
|
127
153
|
productId,
|
|
128
154
|
productTitle: product.title,
|
|
@@ -229,12 +255,24 @@ export async function placeBid(userId, userEmail, input) {
|
|
|
229
255
|
return { bid };
|
|
230
256
|
}
|
|
231
257
|
export async function buyNowAuction(userId, userName, userEmail, input) {
|
|
232
|
-
const { productId } = input;
|
|
233
|
-
|
|
258
|
+
const { productId: productRef } = input;
|
|
259
|
+
// Auction URLs are slug-based, and this action is reachable from a route
|
|
260
|
+
// param, so accept either. Deliberately id-FIRST rather than
|
|
261
|
+
// `findByIdOrSlug`, which queries by slug first and would therefore cost two
|
|
262
|
+
// reads on the common id path (Rule #6) โ here the fallback query only runs
|
|
263
|
+
// when the id lookup actually misses.
|
|
264
|
+
//
|
|
265
|
+
// This was masked because every seeded auction has `slug === id`. Any auction
|
|
266
|
+
// whose slug differs threw NotFoundError, which the caller then swallowed.
|
|
267
|
+
const product = (await productRepository.findById(productRef)) ??
|
|
268
|
+
(await productRepository.findBySlug(productRef));
|
|
234
269
|
if (!product)
|
|
235
270
|
throw new NotFoundError(ERROR_MESSAGES.BID.AUCTION_NOT_FOUND);
|
|
236
271
|
if (!isAuctionListing(product))
|
|
237
272
|
throw new ValidationError(ERROR_MESSAGES.BID.NOT_AN_AUCTION);
|
|
273
|
+
// ๐ Every downstream write (the bid, the locked cart line, the notification)
|
|
274
|
+
// must key on the canonical document id, never on the ref the caller passed.
|
|
275
|
+
const productId = String(product.id);
|
|
238
276
|
if (product.isSold) {
|
|
239
277
|
throw new ValidationError(ERROR_MESSAGES.BID.AUCTION_ENDED, { code: BID_ERROR_CODES.AUCTION_ENDED });
|
|
240
278
|
}
|
|
@@ -250,6 +288,10 @@ export async function buyNowAuction(userId, userName, userEmail, input) {
|
|
|
250
288
|
throw new AuthorizationError(ERROR_MESSAGES.BID.OWN_AUCTION);
|
|
251
289
|
}
|
|
252
290
|
}
|
|
291
|
+
// Same guard as `placeBid` โ a buyout IS a bid (it writes a real
|
|
292
|
+
// `BidDocument` with `isBuyout: true`), so a `place_bids` ban must block it.
|
|
293
|
+
// The profile is reused for the bid's denormalised name/email below.
|
|
294
|
+
const profile = await assertCanBid(userId);
|
|
253
295
|
const buyNowPrice = typeof product.buyNowPrice === "number" ? product.buyNowPrice : null;
|
|
254
296
|
if (buyNowPrice === null || buyNowPrice <= 0) {
|
|
255
297
|
throw new ValidationError(ERROR_MESSAGES.BID.BUY_NOW_NO_PRICE, { code: BID_ERROR_CODES.BUY_NOW_NO_PRICE });
|
|
@@ -286,7 +328,6 @@ export async function buyNowAuction(userId, userName, userEmail, input) {
|
|
|
286
328
|
// The product's own currency, not the site default โ the cart line, the bid
|
|
287
329
|
// and the listing all have to agree, and settlement uses the bid's currency.
|
|
288
330
|
const currency = product.currency || getDefaultCurrency();
|
|
289
|
-
const profile = await userRepository.findById(userId);
|
|
290
331
|
// A real bid at the buy-now price โ this is what "the bid placed is the
|
|
291
332
|
// buyout price by the buyer" means. It starts `active`/`isWinning: false`
|
|
292
333
|
// like any other bid (bidRepository.create forces both) and is promoted to
|
|
@@ -1,8 +1,23 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
currency: string;
|
|
1
|
+
import { type PlaceBidFormClientProps } from "./PlaceBidFormClient";
|
|
2
|
+
export interface AuctionBottomActionsProps extends PlaceBidFormClientProps {
|
|
4
3
|
bidCount: number;
|
|
5
|
-
isEnded: boolean;
|
|
6
|
-
auctionEndDate: Date | null;
|
|
7
4
|
}
|
|
8
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Mobile/after-scroll CTA bar for an auction.
|
|
7
|
+
*
|
|
8
|
+
* ๐ It owns its own modal rather than pointing at one.
|
|
9
|
+
*
|
|
10
|
+
* Two defects lived here. (a) The single Place Bid action ran
|
|
11
|
+
* `getElementById("auction-bid-form")?.scrollIntoView(...)` โ but that id is
|
|
12
|
+
* set inside `renderBidForm`, which `AuctionDetailView` wraps in
|
|
13
|
+
* `hidden lg:block`. On mobile the target is `display:none`, so the bar's only
|
|
14
|
+
* action did nothing on the only viewport the bar exists for. A DOM id is not
|
|
15
|
+
* a contract; the element it names can be hidden, moved or removed without any
|
|
16
|
+
* signal here. (b) There was no Buy Now action at all, even though
|
|
17
|
+
* `MOBILE_PRIMARY_ACTIONS.auction` has declared `[PLACE_BID, BUY_NOW_AUCTION]`
|
|
18
|
+
* for a long time โ a declared CTA nothing rendered (Root Cause #8's shape).
|
|
19
|
+
*
|
|
20
|
+
* Both actions now open the real form, which is the only place the Buy Now
|
|
21
|
+
* button can report an error or route to `/checkout?lane=auction`.
|
|
22
|
+
*/
|
|
23
|
+
export declare function AuctionBottomActions(props: AuctionBottomActionsProps): import("react").JSX.Element;
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useState } from "react";
|
|
2
4
|
import { useBottomActions } from "../../layout/hooks/useBottomActions";
|
|
3
5
|
import { formatCurrency } from "../../../utils/number.formatter";
|
|
4
6
|
import { ACTION_ID, ACTION_META } from "../../products/constants/action-defs";
|
|
5
7
|
import { useCountdown } from "../../../react";
|
|
8
|
+
import { Modal } from "../../../ui";
|
|
9
|
+
import { isBuyNowAvailable } from "../../../_internal/shared/features/auctions/config";
|
|
10
|
+
import { PlaceBidFormClient, } from "./PlaceBidFormClient";
|
|
6
11
|
function formatCountdownLabel(remaining) {
|
|
7
12
|
if (!remaining)
|
|
8
13
|
return undefined;
|
|
@@ -13,25 +18,51 @@ function formatCountdownLabel(remaining) {
|
|
|
13
18
|
return `Ends in ${hours}h ${minutes}m ${seconds}s`;
|
|
14
19
|
return `Ends in ${minutes}m ${seconds}s`;
|
|
15
20
|
}
|
|
16
|
-
|
|
17
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Mobile/after-scroll CTA bar for an auction.
|
|
23
|
+
*
|
|
24
|
+
* ๐ It owns its own modal rather than pointing at one.
|
|
25
|
+
*
|
|
26
|
+
* Two defects lived here. (a) The single Place Bid action ran
|
|
27
|
+
* `getElementById("auction-bid-form")?.scrollIntoView(...)` โ but that id is
|
|
28
|
+
* set inside `renderBidForm`, which `AuctionDetailView` wraps in
|
|
29
|
+
* `hidden lg:block`. On mobile the target is `display:none`, so the bar's only
|
|
30
|
+
* action did nothing on the only viewport the bar exists for. A DOM id is not
|
|
31
|
+
* a contract; the element it names can be hidden, moved or removed without any
|
|
32
|
+
* signal here. (b) There was no Buy Now action at all, even though
|
|
33
|
+
* `MOBILE_PRIMARY_ACTIONS.auction` has declared `[PLACE_BID, BUY_NOW_AUCTION]`
|
|
34
|
+
* for a long time โ a declared CTA nothing rendered (Root Cause #8's shape).
|
|
35
|
+
*
|
|
36
|
+
* Both actions now open the real form, which is the only place the Buy Now
|
|
37
|
+
* button can report an error or route to `/checkout?lane=auction`.
|
|
38
|
+
*/
|
|
39
|
+
export function AuctionBottomActions(props) {
|
|
40
|
+
const { currentBid, currency, bidCount, isEnded, auctionEndDate, buyNowPrice } = props;
|
|
41
|
+
const [open, setOpen] = useState(false);
|
|
18
42
|
const remaining = useCountdown(auctionEndDate ?? undefined);
|
|
43
|
+
const placeBidMeta = ACTION_META[ACTION_ID.PLACE_BID];
|
|
44
|
+
const buyNowMeta = ACTION_META[ACTION_ID.BUY_NOW_AUCTION];
|
|
45
|
+
// Same single predicate the form and the card use. `!!props.onBuyNow` matters
|
|
46
|
+
// because the preview/demo mount passes no action.
|
|
47
|
+
const buyNowAvailable = isBuyNowAvailable({ buyNowPrice, currentBid, isEnded }) && !!props.onBuyNow;
|
|
19
48
|
useBottomActions(isEnded
|
|
20
49
|
? {}
|
|
21
50
|
: {
|
|
22
51
|
actions: [
|
|
23
|
-
{
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
52
|
+
{ ...placeBidMeta, onClick: () => setOpen(true) },
|
|
53
|
+
...(buyNowAvailable && buyNowPrice !== null
|
|
54
|
+
? [
|
|
55
|
+
{
|
|
56
|
+
...buyNowMeta,
|
|
57
|
+
label: `Buy Now โ ${formatCurrency(buyNowPrice, currency)}`,
|
|
58
|
+
onClick: () => setOpen(true),
|
|
59
|
+
},
|
|
60
|
+
]
|
|
61
|
+
: []),
|
|
31
62
|
],
|
|
32
63
|
secondaryLabel: formatCountdownLabel(remaining),
|
|
33
64
|
infoLabel: `${formatCurrency(currentBid, currency)} ยท ${bidCount} bid${bidCount !== 1 ? "s" : ""}`,
|
|
34
65
|
desktop: "after-scroll",
|
|
35
66
|
});
|
|
36
|
-
return
|
|
67
|
+
return (_jsx(Modal, { isOpen: open, onClose: () => setOpen(false), size: "md", title: "Place your bid", children: _jsx(PlaceBidFormClient, { ...props }) }));
|
|
37
68
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ProductFeatureDocument } from "../../products/schemas/product-features";
|
|
2
|
-
import type { PlaceBidInput } from "./PlaceBidFormClient";
|
|
2
|
+
import type { PlaceBidInput, BidActionEnvelope, BuyNowSuccess } from "./PlaceBidFormClient";
|
|
3
3
|
export interface AuctionDetailPageViewProps {
|
|
4
4
|
id: string;
|
|
5
5
|
/**
|
|
@@ -9,8 +9,13 @@ export interface AuctionDetailPageViewProps {
|
|
|
9
9
|
* When absent, falls back to fetching by slug/id (backward compat).
|
|
10
10
|
*/
|
|
11
11
|
initialAuction?: import("../../products/schemas/firestore").ProductDocument | null;
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Typed against the single `ActionResult` envelope โ see the note on
|
|
14
|
+
* `BidActionEnvelope`. `Promise<unknown>` here is what let a doubly-wrapped
|
|
15
|
+
* result flow through to a client that read the wrong `ok`.
|
|
16
|
+
*/
|
|
17
|
+
onPlaceBid?: (input: PlaceBidInput) => Promise<BidActionEnvelope<unknown>>;
|
|
18
|
+
onBuyNow?: () => Promise<BidActionEnvelope<BuyNowSuccess>>;
|
|
14
19
|
/** SSR-loaded productFeatures (platform + store-scope). See ProductDetailPageView for semantics. */
|
|
15
20
|
productFeatures?: ProductFeatureDocument[];
|
|
16
21
|
}
|
|
@@ -46,7 +46,7 @@ function toDescriptionHtml(raw) {
|
|
|
46
46
|
}
|
|
47
47
|
function renderAuctionInfoPanel(props) {
|
|
48
48
|
const { productId, title, currentBid, currency, bidCount, isEnded, endDate, buyNowPrice, buyNowAvailable, featured, freeShipping, condition, category, categoryName, brand, brandSlug, productFeatures, features, descriptionHtml, safeSeller, storeHref } = props;
|
|
49
|
-
return (_jsxs(Stack, { gap: "md", children: [_jsxs(Div, { children: [_jsxs(Row, { gap: "xs", wrap: true, className: "mb-2", children: [_jsx(Span, { size: "xs", weight: "semibold", className: CLS_LIVE_BADGE, children: "\uD83C\uDFF7\uFE0F Live Auction" }), isEnded ? (_jsx(Span, { color: "error", surface: "danger-surface", size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", children: "Ended" })) : (_jsx(Span, { color: "success", surface: "success-surface", size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", children: "Active" }))] }), _jsx(Heading, { level: 1, className: "leading-snug", smSize: "2xl", color: "primary", size: "xl", weight: "bold", children: title })] }), _jsxs(Div, { children: [_jsx(Text, { className: "mb-0.5", color: "muted", size: "xs", children: "Current bid" }), _jsx(LiveBidPrice, { productId: productId, currentBid: currentBid, bidCount: bidCount, currency: currency, isEnded: isEnded, priceSize: "2xl", badgeSize: "sm" }), endDate && !isEnded && (_jsxs(Text, { className: "mt-1.5", color: "muted", size: "sm", children: ["Ends in ", _jsx(Span, { weight: "medium", color: "muted", children: _jsx(CountdownDisplay, { targetDate: endDate, format: "auto", expiredLabel: "Ended" }) })] })), endDate && _jsxs(Text, { className: "mt-0.5", color: "faint", size: "xs", children: [isEnded ? "Ended" : "Ends", " ", endDate.toLocaleString()] })] }), buyNowAvailable && buyNowPrice !== null && (_jsxs(Row, { align: "center", gap: "sm", className: "border border-[var(--appkit-color-primary-200)] dark:border-[var(--appkit-color-primary-800)] bg-primary-50 dark:bg-primary-900/20", padding: "inlineSm", rounded: "lg", children: [_jsx(Span, { size: "xs", color: "muted", children: "Buy Now:" }), _jsx(Span, { size: "base", weight: "bold", className: "text-primary-700 dark:text-primary-300", children: formatCurrency(buyNowPrice, currency) })] })), _jsx(ProductFeatureBadges, { featured: featured, freeShipping: freeShipping, condition: condition ?? undefined, labels: { featured: "Featured", fasterDelivery: "Faster Delivery", ratedSeller: "Rated Seller", condition: "Condition", conditionNew: "New", conditionUsed: "Used", conditionBroken: "For Parts", conditionRefurbished: "Refurbished", returnable: "Returnable", freeShipping: "Free Shipping", codAvailable: "Cash on Delivery", emiAvailable: "EMI Available", wishlistCount: (n) => `${n} wishlisted`, categoryProductCount: (n, cat) => `${n} in ${cat}` } }), (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-
|
|
49
|
+
return (_jsxs(Stack, { gap: "md", children: [_jsxs(Div, { children: [_jsxs(Row, { gap: "xs", wrap: true, className: "mb-2", children: [_jsx(Span, { size: "xs", weight: "semibold", className: CLS_LIVE_BADGE, children: "\uD83C\uDFF7\uFE0F Live Auction" }), isEnded ? (_jsx(Span, { color: "error", surface: "danger-surface", size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", children: "Ended" })) : (_jsx(Span, { color: "success", surface: "success-surface", size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", children: "Active" }))] }), _jsx(Heading, { level: 1, className: "leading-snug", smSize: "2xl", color: "primary", size: "xl", weight: "bold", children: title })] }), _jsxs(Div, { children: [_jsx(Text, { className: "mb-0.5", color: "muted", size: "xs", children: "Current bid" }), _jsx(LiveBidPrice, { productId: productId, currentBid: currentBid, bidCount: bidCount, currency: currency, isEnded: isEnded, priceSize: "2xl", badgeSize: "sm" }), endDate && !isEnded && (_jsxs(Text, { className: "mt-1.5", color: "muted", size: "sm", children: ["Ends in ", _jsx(Span, { weight: "medium", color: "muted", children: _jsx(CountdownDisplay, { targetDate: endDate, format: "auto", expiredLabel: "Ended" }) })] })), endDate && _jsxs(Text, { className: "mt-0.5", color: "faint", size: "xs", children: [isEnded ? "Ended" : "Ends", " ", endDate.toLocaleString()] })] }), buyNowAvailable && buyNowPrice !== null && (_jsxs(Row, { align: "center", gap: "sm", className: "border border-[var(--appkit-color-primary-200)] dark:border-[var(--appkit-color-primary-800)] bg-primary-50 dark:bg-primary-900/20", padding: "inlineSm", rounded: "lg", children: [_jsx(Span, { size: "xs", color: "muted", children: "Buy Now:" }), _jsx(Span, { size: "base", weight: "bold", className: "text-primary-700 dark:text-primary-300", children: formatCurrency(buyNowPrice, currency) }), _jsx(Span, { size: "xs", color: "faint", children: "\u2014 in \u201CPlace a bid\u201D" })] })), _jsx(ProductFeatureBadges, { featured: featured, freeShipping: freeShipping, condition: condition ?? undefined, labels: { featured: "Featured", fasterDelivery: "Faster Delivery", ratedSeller: "Rated Seller", condition: "Condition", conditionNew: "New", conditionUsed: "Used", conditionBroken: "For Parts", conditionRefurbished: "Refurbished", returnable: "Returnable", freeShipping: "Free Shipping", codAvailable: "Cash on Delivery", emiAvailable: "EMI Available", wishlistCount: (n) => `${n} wishlisted`, categoryProductCount: (n, cat) => `${n} in ${cat}` } }), (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 item" }), _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: "Listed 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" })] }) }))] }));
|
|
50
50
|
}
|
|
51
51
|
/**
|
|
52
52
|
* Store reviews for the auction's seller.
|
|
@@ -204,5 +204,5 @@ export async function AuctionDetailPageView({ id, initialAuction, onPlaceBid, on
|
|
|
204
204
|
slug: typeof r.slug === "string" ? r.slug : undefined,
|
|
205
205
|
}));
|
|
206
206
|
return (_jsxs(Stack, { gap: "xl", children: [related.length > 0 && (_jsx(RelatedProducts, { labels: { title: "Similar Auctions" }, renderGrid: () => (_jsx(MarketplaceAuctionGrid, { auctions: related, gridClassName: "grid grid-cols-2 gap-[var(--appkit-space-4)] sm:grid-cols-3 lg:grid-cols-4" })) })), _jsx(GroupedListingsCarousel, { groups: groups }), _jsx(RelatedItemsSection, { relatedItems: relatedItems, relatedByBrand: relatedByBrand, relatedByTags: relatedByTags, relatedByStore: relatedByStore, categoryLabel: categoryName || category || undefined, brandLabel: brand || undefined, storeLabel: storeName || undefined })] }));
|
|
207
|
-
} }), renderAuctionStoreReviews(storeId), _jsx(AuctionBottomActions, { currentBid: currentBid,
|
|
207
|
+
} }), renderAuctionStoreReviews(storeId), onPlaceBid && (_jsx(AuctionBottomActions, { productId: String(product.id), currentBid: currentBid, startingBid: startingBid, tiers: bidIncrementTiers, minBidIncrementOverride: minBidIncrementOverride, currency: currency, isEnded: isEnded, auctionEndDate: endDate, buyNowPrice: buyNowPrice, bidCount: bidCount, tags: tags, onPlaceBid: onPlaceBid, onBuyNow: onBuyNow }))] })] }));
|
|
208
208
|
}
|