@mohasinac/appkit 4.70.0 → 4.71.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/PROVIDERS.md +190 -0
- package/dist/.build-stamp.json +3 -3
- package/dist/_internal/client/features/lottery/LotteryAdminSlotView.js +2 -1
- package/dist/_internal/server/features/checkout/actions.js +43 -8
- package/dist/_internal/server/features/orders/actions.js +2 -1
- package/dist/_internal/server/features/orders/adapters.js +18 -0
- package/dist/_internal/server/features/orders/balance.js +2 -1
- package/dist/_internal/server/features/orders/cancellation-refund.d.ts +0 -89
- package/dist/_internal/server/features/orders/cancellation-refund.js +3 -2
- package/dist/_internal/server/features/payouts/actions.js +2 -1
- package/dist/_internal/server/features/points/award.js +3 -2
- package/dist/_internal/server/features/protection-claims/actions.js +2 -1
- package/dist/_internal/server/features/refunds/actions.js +3 -2
- package/dist/_internal/server/features/shipping/courier-block.js +5 -2
- package/dist/_internal/server/features/shipping/reconcile.js +3 -2
- package/dist/_internal/server/features/shipping/reimbursement.js +3 -2
- package/dist/_internal/server/features/stores/adapters.d.ts +1 -1
- package/dist/_internal/server/features/stores/adapters.js +12 -2
- package/dist/_internal/server/jobs/core/dailyStatusDigest.js +7 -3
- package/dist/_internal/server/jobs/core/emiInstallmentReminder.js +2 -1
- package/dist/_internal/server/jobs/core/obligationSweep.js +5 -4
- package/dist/_internal/server/jobs/core/onScamReportCreate.js +2 -1
- package/dist/_internal/server/jobs/core/paymentReconcile.js +5 -4
- package/dist/_internal/shared/checkout/rules/auction.rule.js +25 -7
- package/dist/_internal/shared/checkout/rules/prize-draw.rule.js +26 -8
- package/dist/_internal/shared/config/schema.d.ts +15 -0
- package/dist/_internal/shared/content/published-values.d.ts +2 -2
- package/dist/_internal/shared/content/published-values.js +31 -12
- package/dist/_internal/shared/features/auctions/errors.js +2 -1
- package/dist/_internal/shared/features/orders/refund-copy.js +3 -2
- package/dist/_internal/shared/features/promotions/errors.js +2 -1
- package/dist/constants/field-names.d.ts +23 -0
- package/dist/constants/field-names.js +23 -0
- package/dist/constants/storage-keys.d.ts +90 -0
- package/dist/constants/storage-keys.js +119 -0
- package/dist/contracts/index.d.ts +1 -1
- package/dist/contracts/payment.d.ts +42 -0
- package/dist/contracts/registry.d.ts +8 -0
- package/dist/contracts/tax.d.ts +118 -0
- package/dist/contracts/tax.js +3 -0
- package/dist/core/brand-resolver.d.ts +92 -0
- package/dist/core/brand-resolver.js +120 -0
- package/dist/core/brand-text.d.ts +15 -0
- package/dist/core/brand-text.js +73 -0
- package/dist/core/hooks/useNewsletterSubscribe.d.ts +30 -0
- package/dist/core/hooks/useNewsletterSubscribe.js +121 -0
- package/dist/features/about/components/DeveloperView.js +2 -1
- package/dist/features/account/components/BuyerAccountGuideView.js +2 -1
- package/dist/features/account/components/BuyerAuctionsGuideView.js +4 -3
- package/dist/features/account/components/BuyerOrdersGuideView.js +6 -5
- package/dist/features/account/components/BuyerShoppingGuideView.js +3 -2
- package/dist/features/account/components/UserOffersPanel.js +2 -1
- package/dist/features/account/hooks/useCollapsedSections.js +5 -3
- package/dist/features/account/schemas/index.js +1 -1
- package/dist/features/admin/actions/notification-actions.js +9 -2
- package/dist/features/admin/components/AdminAnalyticsGuideView.js +2 -1
- package/dist/features/admin/components/AdminBundlesView.js +2 -1
- package/dist/features/admin/components/AdminCatalogGuideView.js +2 -1
- package/dist/features/admin/components/AdminContentGuideView.js +2 -1
- package/dist/features/admin/components/AdminFaqEditorView.js +2 -1
- package/dist/features/admin/components/AdminGatewayEventsView.js +2 -1
- package/dist/features/admin/components/AdminGuideHubView.js +2 -1
- package/dist/features/admin/components/AdminMoneyCasesView.js +2 -1
- package/dist/features/admin/components/AdminOrdersGuideView.js +2 -1
- package/dist/features/admin/components/AdminProductsView.js +3 -2
- package/dist/features/admin/components/AdminSectionsView.js +3 -2
- package/dist/features/admin/components/AdminSiteSettingsView.js +8 -7
- package/dist/features/admin/components/AdminStoresGuideView.js +2 -1
- package/dist/features/admin/components/AdminSupportTicketDetailView.js +2 -1
- package/dist/features/admin/components/AdminTeamGuideView.js +2 -1
- package/dist/features/admin/components/AdminTrustGuideView.js +3 -2
- package/dist/features/admin/components/AdminUsersGuideView.js +2 -1
- package/dist/features/admin/components/sections/adminSectionsTypes.js +1 -1
- package/dist/features/auth/actions/user-slug.js +21 -2
- package/dist/features/auth/hooks/useAuth.js +11 -4
- package/dist/features/cart/utils/pending-ops.js +16 -3
- package/dist/features/catalogue/utils/product-from-item.d.ts +18 -2
- package/dist/features/catalogue/utils/product-from-item.js +23 -4
- package/dist/features/categories/components/BundleItemsPicker.js +2 -1
- package/dist/features/categories/components/CategoryDetailPageView.js +2 -1
- package/dist/features/categories/components/ContextualTrail.d.ts +9 -2
- package/dist/features/categories/components/ContextualTrail.js +11 -3
- package/dist/features/categories/components/LastCategoryBeacon.js +2 -2
- package/dist/features/checkout/actions/checkout-value-otp-actions.js +9 -1
- package/dist/features/contact/email.js +11 -12
- package/dist/features/email/notification-templates.js +4 -5
- package/dist/features/email/render-static-markup.d.ts +34 -0
- package/dist/features/email/render-static-markup.js +4 -0
- package/dist/features/events/utils/event-charts.js +2 -1
- package/dist/features/homepage/components/AnnouncementBar.js +5 -3
- package/dist/features/homepage/components/WelcomeSection.d.ts +3 -1
- package/dist/features/homepage/components/WelcomeSection.js +2 -2
- package/dist/features/homepage/lib/section-renderer.d.ts +8 -0
- package/dist/features/homepage/lib/section-renderer.js +1 -1
- package/dist/features/layout/AppLayoutShell.d.ts +3 -1
- package/dist/features/layout/AppLayoutShell.js +2 -2
- package/dist/features/layout/TitleBarLayout.d.ts +8 -1
- package/dist/features/layout/TitleBarLayout.js +2 -2
- package/dist/features/media/MediaVideo.js +2 -1
- package/dist/features/orders/actions/refund-actions.js +3 -2
- package/dist/features/orders/components/RefundRequestView.js +15 -2
- package/dist/features/orders/repository/orders.repository.js +2 -1
- package/dist/features/orders/types/index.d.ts +14 -0
- package/dist/features/orders/utils/payment-proof-message.js +5 -2
- package/dist/features/price-index/components/PriceIndexToolView.js +6 -5
- package/dist/features/pricing/components/AdminPricingView.js +2 -1
- package/dist/features/print/components/labels.js +2 -1
- package/dist/features/products/components/CityCollectiblesView.js +2 -1
- package/dist/features/products/components/MakeOfferButton.js +2 -1
- package/dist/features/products/components/PrizeDrawCollage.js +3 -2
- package/dist/features/products/components/PrizeRevealModal.js +2 -1
- package/dist/features/products/repository/products.repository.js +2 -1
- package/dist/features/promotions/components/CouponCard.js +3 -2
- package/dist/features/promotions/components/CouponHelpDetails.js +2 -1
- package/dist/features/promotions/constants/coupon-help.d.ts +1 -1
- package/dist/features/promotions/constants/coupon-help.js +8 -1
- package/dist/features/scams/components/ScamAwarenessModal.js +2 -1
- package/dist/features/scams/components/ScamProfileView.js +5 -3
- package/dist/features/scams/components/ScamRegistryView.js +2 -1
- package/dist/features/scams/constants/scam-types.d.ts +13 -1
- package/dist/features/scams/constants/scam-types.js +67 -23
- package/dist/features/scams/schemas/firestore.js +1 -1
- package/dist/features/search/components/Search.js +5 -1
- package/dist/features/seller/actions/offer-actions.js +47 -9
- package/dist/features/seller/components/DispatchForm.js +3 -2
- package/dist/features/seller/components/PrintCenterView.js +4 -2
- package/dist/features/seller/components/SellerBundlesView.js +2 -1
- package/dist/features/seller/components/SellerPayoutSettingsView.js +3 -2
- package/dist/features/seller/components/SellerProductShell.js +3 -2
- package/dist/features/seller/components/SellerProductsView.js +4 -3
- package/dist/features/store-extensions/repository/store-extensions.repositories.d.ts +30 -4
- package/dist/features/store-extensions/repository/store-extensions.repositories.js +46 -7
- package/dist/features/store-extensions/schemas/firestore.d.ts +22 -3
- package/dist/features/store-extensions/schemas/firestore.js +20 -0
- package/dist/features/stores/components/StoreCapabilitiesGuideView.js +7 -6
- package/dist/features/stores/components/StoreFinanceGuideView.js +6 -5
- package/dist/features/stores/components/StoreGuideHubView.js +2 -1
- package/dist/features/stores/components/StoreListingsGuideView.js +2 -1
- package/dist/features/stores/components/StoreOrdersGuideView.js +5 -4
- package/dist/features/stores/components/StoreSettingsGuideView.js +2 -1
- package/dist/features/support/schemas/index.d.ts +2 -2
- package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.d.ts +1 -1
- package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.js +3 -2
- package/dist/features/whatsapp-bot/helpers/whatsapp.js +5 -2
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -2
- package/dist/providers/payment-manual/index.d.ts +11 -0
- package/dist/providers/payment-manual/index.js +13 -0
- package/dist/providers/payment-phonepe/index.d.ts +23 -7
- package/dist/providers/payment-phonepe/index.js +15 -0
- package/dist/providers/payment-razorpay/index.d.ts +13 -0
- package/dist/providers/payment-razorpay/index.js +21 -1
- package/dist/providers/tax-india-gst/index.d.ts +45 -0
- package/dist/providers/tax-india-gst/index.js +78 -0
- package/dist/react/hooks/useScrollToTop.d.ts +5 -0
- package/dist/react/hooks/useScrollToTop.js +70 -0
- package/dist/schemas/registry.d.ts +2 -2
- package/dist/seo/json-ld.js +25 -4
- package/dist/ui/components/SiteLogo.d.ts +20 -2
- package/dist/ui/components/SiteLogo.js +28 -6
- package/dist/ui/components/SiteMark.d.ts +1 -1
- package/dist/ui/components/SiteMark.js +21 -6
- package/dist/utils/date.formatter.d.ts +20 -0
- package/dist/utils/date.formatter.js +25 -0
- package/dist/utils/number.formatter.d.ts +47 -0
- package/dist/utils/number.formatter.js +79 -1
- package/dist/validation/phone.validator.d.ts +0 -1
- package/dist/validation/phone.validator.js +17 -3
- package/package.json +17 -3
|
@@ -56,7 +56,7 @@ const nudgeWhenCarrying = (count) => count > 0 ? "appkit-nav-icon--has-items" :
|
|
|
56
56
|
*
|
|
57
57
|
* Receives all domain data as props — zero domain imports.
|
|
58
58
|
*/
|
|
59
|
-
export function TitleBarLayout({ onToggleSidebar, sidebarOpen, onSearchToggle, searchOpen, brandName, brandShortName, siteLogoUrl, logoHref, promotionsHref, compareHref, wishlistHref, wishlistCount = 0, cartHref, cartCount = 0, profileHref, unreadNotificationCount = 0, notificationsHref, messagesHref, unreadMessageCount = 0, loginHref, registerHref, user, notificationSlot, devSlot, navSlot, promoStripText, isDark = false, onToggleTheme, hasDashboardNav, onToggleDashboardNav, hideSidebarToggle = false, id = "titlebar", className = "", }) {
|
|
59
|
+
export function TitleBarLayout({ onToggleSidebar, sidebarOpen, onSearchToggle, searchOpen, brandName, brandTld, brandShortName, siteLogoUrl, logoHref, promotionsHref, compareHref, wishlistHref, wishlistCount = 0, cartHref, cartCount = 0, profileHref, unreadNotificationCount = 0, notificationsHref, messagesHref, unreadMessageCount = 0, loginHref, registerHref, user, notificationSlot, devSlot, navSlot, promoStripText, isDark = false, onToggleTheme, hasDashboardNav, onToggleDashboardNav, hideSidebarToggle = false, id = "titlebar", className = "", }) {
|
|
60
60
|
// ── Element builders ────────────────────────────────────────────────────────
|
|
61
61
|
const promotionsEl = promotionsHref ? (_jsxs(Link, { href: promotionsHref, "aria-label": "Today's deals", className: "flex items-center gap-[var(--appkit-space-1)] px-[var(--appkit-space-3)] py-[var(--appkit-space-1)] rounded-full text-[length:var(--appkit-text-xs)] font-bold bg-primary-100 text-primary-700 dark:bg-secondary-900/40 dark:text-secondary-400 hover:bg-primary-surface transition-colors border border-primary-200/60 dark:border-secondary-700/40", children: [_jsx(Icon, { name: "deals", size: "md", filled: true }), _jsx(Span, { className: "hidden sm:inline", children: "Today's Deals" })] })) : null;
|
|
62
62
|
const themeBtn = onToggleTheme ? (_jsx(IconButton, { size: "touch", variant: "ghost", "aria-label": isDark ? "Switch to light mode" : "Switch to dark mode", onClick: onToggleTheme, className: iconBtn, icon: _jsx(Icon, { name: isDark ? "themeLight" : "themeDark", size: "lg" }) })) : null;
|
|
@@ -81,7 +81,7 @@ export function TitleBarLayout({ onToggleSidebar, sidebarOpen, onSearchToggle, s
|
|
|
81
81
|
_jsxs(Row, { gap: "xs", reverse: "hand", className: "hidden lg:flex", children: [loginHref && (_jsx(Link, { href: loginHref, className: "px-[var(--appkit-space-3)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-sm)] font-medium text-[var(--appkit-color-text-muted)] hover:text-primary-700 dark:hover:text-secondary-400 transition-colors rounded-lg hover:bg-primary-surface", children: "Sign in" })), registerHref && (_jsx(Link, { href: registerHref, className: "px-[var(--appkit-space-3)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-sm)] font-semibold rounded-lg bg-primary text-white hover:bg-primary/90 transition-colors btn-glow shadow-sm", children: "Register" }))] })) : null;
|
|
82
82
|
const hasTb2 = !!(notificationsEl || messagesEl || wishlistEl || cartEl || profileEl);
|
|
83
83
|
// ── Render ───────────────────────────────────────────────────────────────────
|
|
84
|
-
return (_jsxs(BlockHeader, { id: id, className: `sticky top-0 z-50 bg-[var(--appkit-color-bg)]/95 backdrop-blur-md border-b border-[var(--appkit-color-border-subtle)] shadow-sm ${className}`, children: [promoStripText && (_jsx(Section, { color: "inverse", tone: "accent-banner", className: "text-[length:var(--appkit-text-xs)] text-center font-medium", padding: "y-2xs", children: promoStripText })), _jsxs(Div, { paddingX: "x-page", className: "container mx-auto max-w-[1920px]", children: [_jsxs(Row, { gap: "none", reverse: "hand", className: "relative h-14", children: [_jsx(Row, { gap: "3", reverse: "hand", className: "flex-1", children: _jsx(Link, { href: logoHref, "aria-label": brandName, className: "flex items-center gap-[var(--appkit-space-2)] transition-opacity hover:opacity-80", children: _jsx(SiteLogo, { title: brandName, size: "md" }) }) }), _jsx(Row, { className: "flex flex-none", align: "center", children: _jsx(Link, { href: logoHref, "aria-label": brandName, className: "flex items-center transition-opacity hover:opacity-80", children: _jsx(SiteMark, { src: siteLogoUrl, title: brandShortName ?? brandName, size: "lg" }) }) }), _jsxs(Row, { gap: "xs", reverse: "hand", justify: "end", className: "flex-1", children: [devSlot, navSlot, compareEl, notificationSlot, notificationsEl && _jsx(Div, { className: "hidden lg:flex", children: notificationsEl }), messagesEl && _jsx(Div, { className: "hidden lg:flex", children: messagesEl }), wishlistEl && _jsx(Div, { className: "hidden lg:flex", children: wishlistEl }), cartEl && _jsx(Div, { className: "hidden lg:flex", children: cartEl }), user
|
|
84
|
+
return (_jsxs(BlockHeader, { id: id, className: `sticky top-0 z-50 bg-[var(--appkit-color-bg)]/95 backdrop-blur-md border-b border-[var(--appkit-color-border-subtle)] shadow-sm ${className}`, children: [promoStripText && (_jsx(Section, { color: "inverse", tone: "accent-banner", className: "text-[length:var(--appkit-text-xs)] text-center font-medium", padding: "y-2xs", children: promoStripText })), _jsxs(Div, { paddingX: "x-page", className: "container mx-auto max-w-[1920px]", children: [_jsxs(Row, { gap: "none", reverse: "hand", className: "relative h-14", children: [_jsx(Row, { gap: "3", reverse: "hand", className: "flex-1", children: _jsx(Link, { href: logoHref, "aria-label": brandName, className: "flex items-center gap-[var(--appkit-space-2)] transition-opacity hover:opacity-80", children: _jsx(SiteLogo, { title: brandName, wordmark: brandName, tld: brandTld, size: "md" }) }) }), _jsx(Row, { className: "flex flex-none", align: "center", children: _jsx(Link, { href: logoHref, "aria-label": brandName, className: "flex items-center transition-opacity hover:opacity-80", children: _jsx(SiteMark, { src: siteLogoUrl, title: brandShortName ?? brandName, size: "lg" }) }) }), _jsxs(Row, { gap: "xs", reverse: "hand", justify: "end", className: "flex-1", children: [devSlot, navSlot, compareEl, notificationSlot, notificationsEl && _jsx(Div, { className: "hidden lg:flex", children: notificationsEl }), messagesEl && _jsx(Div, { className: "hidden lg:flex", children: messagesEl }), wishlistEl && _jsx(Div, { className: "hidden lg:flex", children: wishlistEl }), cartEl && _jsx(Div, { className: "hidden lg:flex", children: cartEl }), user
|
|
85
85
|
? profileEl && _jsx(Div, { className: "hidden lg:flex", children: profileEl })
|
|
86
86
|
: authButtonsEl ?? (profileEl && _jsx(Div, { className: "hidden lg:flex", children: profileEl })), searchBtn, _jsx(Div, { className: "hidden sm:flex", children: promotionsEl }), themeBtn, hamburgerBtn] })] }), hasTb2 && (
|
|
87
87
|
// `justify="end"` is CORRECT alongside `reverse="hand"` — do not
|
|
@@ -6,6 +6,7 @@ import { Row } from "../../ui/components/Layout.js";
|
|
|
6
6
|
import { Span } from "../../ui/components/Typography.js";
|
|
7
7
|
import { resolveMediaUrl, resolveVideoUrl, getYouTubeVideoId } from "../../utils/media-url.js";
|
|
8
8
|
import { useSiteSettings } from "../../core/hooks/useSiteSettings.js";
|
|
9
|
+
import { getBrandDomain } from "../../core/brand-resolver.js";
|
|
9
10
|
export function MediaVideo({ src, thumbnailUrl, alt = "Video", controls = true, autoPlayMuted = false, loop = false, trimStart, trimEnd, objectFit = "cover", watermark, }) {
|
|
10
11
|
const videoRef = useRef(null);
|
|
11
12
|
const fitClass = objectFit === "contain" ? "object-contain" : "object-cover";
|
|
@@ -135,6 +136,6 @@ function MediaVideoWatermarkLayer({ config }) {
|
|
|
135
136
|
if (config.type === "image" && config.imageUrl) {
|
|
136
137
|
return (_jsx(Div, { className: "z-10", style: containerStyle, "aria-hidden": "true", children: _jsx("img", { src: resolveMediaUrl(config.imageUrl), alt: "", className: "block w-full h-auto" }) }));
|
|
137
138
|
}
|
|
138
|
-
return (_jsx(Div, { className: "z-10", style: containerStyle, "aria-hidden": "true", children: _jsx(Span, { className: "block text-white drop-shadow-[0_1px_2px_rgba(0,0,0,0.7)]", style: { fontSize: "0.8em", fontWeight: 700, textAlign: "right" }, children: config.text ??
|
|
139
|
+
return (_jsx(Div, { className: "z-10", style: containerStyle, "aria-hidden": "true", children: _jsx(Span, { className: "block text-white drop-shadow-[0_1px_2px_rgba(0,0,0,0.7)]", style: { fontSize: "0.8em", fontWeight: 700, textAlign: "right" }, children: config.text ?? getBrandDomain() }) }));
|
|
139
140
|
}
|
|
140
141
|
export default MediaVideo;
|
|
@@ -18,6 +18,7 @@ import { siteSettingsRepository } from "../../admin/repository/site-settings.rep
|
|
|
18
18
|
*/
|
|
19
19
|
import { platformFeeWithheld, refundCapacity, } from "../../../_internal/shared/checkout/refundable-base.js";
|
|
20
20
|
import { sendNotification } from "../../admin/actions/notification-actions.js";
|
|
21
|
+
import { formatMoney } from "../../../utils/number.formatter.js";
|
|
21
22
|
const DEFAULT_GST_PERCENT = 18;
|
|
22
23
|
export async function issuePartialRefund(adminUid, orderId, deductFees, refundNote) {
|
|
23
24
|
const order = await orderRepository.findById(orderId);
|
|
@@ -75,8 +76,8 @@ export async function issuePartialRefund(adminUid, orderId, deductFees, refundNo
|
|
|
75
76
|
priority: "high",
|
|
76
77
|
title: "Refund Initiated",
|
|
77
78
|
message: deductFees
|
|
78
|
-
? `Your refund of
|
|
79
|
-
: `Your full refund of
|
|
79
|
+
? `Your refund of ${formatMoney(netRefund)} (original ${formatMoney(grossRefund)} minus ${formatMoney(feeDeducted)} gateway fee) is being processed.`
|
|
80
|
+
: `Your full refund of ${formatMoney(grossRefund)} is being processed.`,
|
|
80
81
|
relatedId: orderId,
|
|
81
82
|
relatedType: "order",
|
|
82
83
|
storeId: order.storeId,
|
|
@@ -40,7 +40,20 @@ const ACK_COUNT = REFUND_COPY.request.acknowledgments.length;
|
|
|
40
40
|
* on `refundBusinessDaysMin` in the registry), so passing settings would imply
|
|
41
41
|
* an editable number that does not exist.
|
|
42
42
|
*/
|
|
43
|
-
|
|
43
|
+
/*
|
|
44
|
+
* 🛑 A FUNCTION, not a hoisted `const`.
|
|
45
|
+
*
|
|
46
|
+
* It was `const PUBLISHED_VALUES = resolvePublishedValues()` at module scope,
|
|
47
|
+
* which was harmless only by luck: the comment above correctly notes the sole
|
|
48
|
+
* token in this copy is the refund timeline. But `resolvePublishedValues()`
|
|
49
|
+
* now resolves `siteName`/`siteDomain` from the brand the consumer injects at
|
|
50
|
+
* boot, and a module-level call runs at IMPORT time — so the moment anyone
|
|
51
|
+
* adds `{{siteName}}` to an acknowledgment, it would silently render appkit's
|
|
52
|
+
* neutral "Site" inside something a buyer is agreeing to.
|
|
53
|
+
*
|
|
54
|
+
* Resolved per render instead. Same values, no capture.
|
|
55
|
+
*/
|
|
56
|
+
const publishedValues = () => resolvePublishedValues();
|
|
44
57
|
export function RefundRequestView({ order, onSubmitRequest, isLoading = false, className = "", }) {
|
|
45
58
|
/*
|
|
46
59
|
* Does anything in this order carry the final-sale term? Read from the
|
|
@@ -101,7 +114,7 @@ export function RefundRequestView({ order, onSubmitRequest, isLoading = false, c
|
|
|
101
114
|
* buyer is agreeing to is the strongest reason the unknown-key
|
|
102
115
|
* rule leaves `{{…}}` VISIBLE rather than blanking it.
|
|
103
116
|
*/
|
|
104
|
-
label: _jsx(Text, { size: "sm", className: "leading-snug", children: resolvePublishedCopy(text,
|
|
117
|
+
label: _jsx(Text, { size: "sm", className: "leading-snug", children: resolvePublishedCopy(text, publishedValues()) }), "aria-label": `Acknowledgment ${i + 1}`, "aria-invalid": errors.acknowledged ? "true" : undefined }, i))), errors.acknowledged && (_jsx(Text, { size: "sm", variant: "error", role: "alert", children: errors.acknowledged }))] })),
|
|
105
118
|
},
|
|
106
119
|
], [order.total, order.currency, hasFinalSaleLine, allowedReasons]);
|
|
107
120
|
const nav = useSectionFormNav(sections, draft);
|
|
@@ -19,6 +19,7 @@ import { isLegalOrderTransition, describeIllegalTransition, } from "../constants
|
|
|
19
19
|
import { reconcileObligations } from "../../../_internal/shared/obligations/deadlines.js";
|
|
20
20
|
import { obligationsRepository } from "../../obligations/repository/obligations.repository.js";
|
|
21
21
|
import { ORDER_FIELDS } from "../../../constants/field-names.js";
|
|
22
|
+
import { formatMoney } from "../../../utils/number.formatter.js";
|
|
22
23
|
/**
|
|
23
24
|
* An illegal status transition was refused.
|
|
24
25
|
*
|
|
@@ -711,7 +712,7 @@ class OrderRepository extends BaseRepository {
|
|
|
711
712
|
*/
|
|
712
713
|
if (committed + event.amount > opts.refundableBase + opts.epsilon) {
|
|
713
714
|
const remaining = Math.max(0, opts.refundableBase - committed);
|
|
714
|
-
throw new ValidationError(`Refund of
|
|
715
|
+
throw new ValidationError(`Refund of ${formatMoney(event.amount)} exceeds the ${formatMoney(remaining)} still refundable on this order.`);
|
|
715
716
|
}
|
|
716
717
|
tx.update(docRef, {
|
|
717
718
|
refunds: [...existing, { ...event, status: "reserved" }],
|
|
@@ -201,6 +201,20 @@ export interface Order {
|
|
|
201
201
|
supplierGstin?: string;
|
|
202
202
|
supplierLegalName?: string;
|
|
203
203
|
supplierAddress?: string;
|
|
204
|
+
/**
|
|
205
|
+
* The State whose tax applies — Rule 46(n).
|
|
206
|
+
*
|
|
207
|
+
* 🛑 Declared here for the same reason as the EMI fields above, and it is
|
|
208
|
+
* the same Root Cause #57 shape: `orderDocumentToOrder` mapped the
|
|
209
|
+
* CGST/SGST/IGST split while dropping the field that DECIDES which of them
|
|
210
|
+
* is correct, so the HTML invoice could show an IGST line with nothing on
|
|
211
|
+
* the page justifying it. The PDF reads `OrderDocument` directly and always
|
|
212
|
+
* printed it, so the two invoices for one order disagreed.
|
|
213
|
+
*
|
|
214
|
+
* The State NAME is what the rule asks for; the numeric State code is a
|
|
215
|
+
* GSTR-1/e-invoice convention and is deliberately not stored here.
|
|
216
|
+
*/
|
|
217
|
+
placeOfSupply?: string;
|
|
204
218
|
total: number;
|
|
205
219
|
currency: string;
|
|
206
220
|
/**
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { formatMoney } from "../../../utils/number.formatter.js";
|
|
1
2
|
/**
|
|
2
3
|
* Tier PP — message body for the buyer/seller "share for review" `wa.me`
|
|
3
4
|
* link on the order payment page.
|
|
@@ -12,6 +13,8 @@
|
|
|
12
13
|
*/
|
|
13
14
|
export function buildPaymentProofReviewMessage(input) {
|
|
14
15
|
const { orderId, buyerName, productTitle, totalAmount, reviewUrl } = input;
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
// `formatMoney` groups and places the symbol from the market profile; the
|
|
17
|
+
// intermediate `toLocaleString("en-IN")` this replaced hardcoded the locale.
|
|
18
|
+
const amount = formatMoney(totalAmount);
|
|
19
|
+
return `💳 Payment proof submitted! ${buyerName} uploaded proof for "${productTitle}" (${amount}). Order #${orderId}. Please review within 2 hours or it auto-confirms: ${reviewUrl}`;
|
|
17
20
|
}
|
|
@@ -25,6 +25,7 @@ import { formatCurrency } from "../../../utils/number.formatter.js";
|
|
|
25
25
|
import { normalizeError } from "../../../errors/normalize.js";
|
|
26
26
|
import { toUserMessage } from "../../../errors/error-display-map.js";
|
|
27
27
|
import { PRICE_LOOKUP_INDEX_TYPES, priceLookupTargetFor } from "../lookup-target.js";
|
|
28
|
+
import { getBrandName } from "../../../core/brand-resolver.js";
|
|
28
29
|
/** Human labels. Falls back to the raw value so a new condition still renders. */
|
|
29
30
|
const CONDITION_LABEL = {
|
|
30
31
|
new: "New",
|
|
@@ -198,7 +199,7 @@ function MarketSignInPrompt() {
|
|
|
198
199
|
}
|
|
199
200
|
function ResultCard({ row, threshold, market, canSeeMarket, }) {
|
|
200
201
|
return (_jsxs(Stack, { gap: "md", padding: "card", rounded: "lg", surface: "card", border: "subtle", className: "min-w-0", children: [_jsxs(Row, { gap: "md", align: "start", className: "min-w-0", children: [row.coverImage && (_jsx(Div, { rounded: "lg", className: `w-16 h-16 flex-shrink-0 ${__O.hidden}`, children: _jsx(MediaImage, { src: row.coverImage, alt: row.name, size: "thumbnail" }) })), _jsxs(Stack, { gap: "none", className: "flex-1 min-w-0", children: [_jsx(Heading, { level: 3, children: row.name }), row.path && (_jsx(Text, { size: "xs", color: "muted", className: "min-w-0 break-words", children: row.path }))] })] }), (!row.bands || row.bands.length === 0) &&
|
|
201
|
-
(market ? _jsx(MarketCard, { market: market }) : canSeeMarket ? null : _jsx(MarketSignInPrompt, {})), row.bands && row.bands.length > 0 ? (_jsxs(_Fragment, { children: [_jsx(Stack, { gap: "sm", className: "min-w-0", children: row.bands.map((b) => (_jsx(BandCard, { band: b, threshold: threshold }, b.listingType))) }), market ? _jsx(MarketCard, { market: market }) : canSeeMarket ? null : _jsx(MarketSignInPrompt, {}), _jsxs(Text, { size: "xs", color: "muted", children: ["Completed sales on
|
|
202
|
+
(market ? _jsx(MarketCard, { market: market }) : canSeeMarket ? null : _jsx(MarketSignInPrompt, {})), row.bands && row.bands.length > 0 ? (_jsxs(_Fragment, { children: [_jsx(Stack, { gap: "sm", className: "min-w-0", children: row.bands.map((b) => (_jsx(BandCard, { band: b, threshold: threshold }, b.listingType))) }), market ? _jsx(MarketCard, { market: market }) : canSeeMarket ? null : _jsx(MarketSignInPrompt, {}), _jsxs(Text, { size: "xs", color: "muted", children: ["Completed sales on ", getBrandName(), row.windowDays ? ` over the last ${row.windowDays} days` : "", row.computedAt ? ` · as of ${new Date(row.computedAt).toLocaleDateString("en-IN")}` : ""] })] })) : (
|
|
202
203
|
/*
|
|
203
204
|
* 🛑 "No sales recorded yet" — never ₹0, and never a blank card. The
|
|
204
205
|
* absence of a band means nothing has sold here, which is a fact about
|
|
@@ -211,7 +212,7 @@ function ResultCard({ row, threshold, market, canSeeMarket, }) {
|
|
|
211
212
|
* this card now says.
|
|
212
213
|
*/
|
|
213
214
|
_jsx(Text, { size: "sm", color: "muted", children: market
|
|
214
|
-
?
|
|
215
|
+
? `Nothing has sold here on ${getBrandName()} yet, so the figures above are asking prices elsewhere rather than our own sales.`
|
|
215
216
|
: canSeeMarket
|
|
216
217
|
? /*
|
|
217
218
|
* 🛑 BOTH halves are empty here, and saying so is the point.
|
|
@@ -223,7 +224,7 @@ function ResultCard({ row, threshold, market, canSeeMarket, }) {
|
|
|
223
224
|
* states and only one of them is about our marketplace.
|
|
224
225
|
*/
|
|
225
226
|
"No price evidence for this one yet — nothing sold here, and no asking prices recorded for it either. It appears as soon as either exists."
|
|
226
|
-
:
|
|
227
|
+
: `No completed sales recorded here yet. Prices appear once something sells on ${getBrandName()}.` }))] }));
|
|
227
228
|
}
|
|
228
229
|
/** Stable identity — a `useMemo` dependency inside `useSiteSearch`. */
|
|
229
230
|
const identityLabel = (typeKey) => typeKey;
|
|
@@ -477,14 +478,14 @@ export function PriceIndexToolView({ initialQuery = "", initialData = null, sear
|
|
|
477
478
|
: null;
|
|
478
479
|
const threshold = data?.minConfidentSamples ?? 3;
|
|
479
480
|
const hasQuery = (data?.query ?? "").length > 0;
|
|
480
|
-
return (_jsx(Section, { padding: "y-lg", children: _jsx(Container, { size: "lg", children: _jsxs(Stack, { gap: "lg", className: "min-w-0", children: [_jsxs(Stack, { gap: "xs", className: "min-w-0", children: [_jsxs(Row, { gap: "sm", align: "center", children: [_jsx(TrendingUp, { className: "w-5 h-5 text-primary-600", "aria-hidden": "true" }), _jsx(Heading, { level: 1, children: "What it sold for" })] }),
|
|
481
|
+
return (_jsx(Section, { padding: "y-lg", children: _jsx(Container, { size: "lg", children: _jsxs(Stack, { gap: "lg", className: "min-w-0", children: [_jsxs(Stack, { gap: "xs", className: "min-w-0", children: [_jsxs(Row, { gap: "sm", align: "center", children: [_jsx(TrendingUp, { className: "w-5 h-5 text-primary-600", "aria-hidden": "true" }), _jsx(Heading, { level: 1, children: "What it sold for" })] }), _jsxs(Text, { color: "muted", children: ["Pick any bey, part or set to see what it actually sold for on", getBrandName(), " \u2014 the range, the median, the average and the most recent sale, broken down by condition \u2014 alongside what other Indian shops are asking for the same thing."] })] }), dropdownUsable ? (_jsxs(Stack, { gap: "xs", className: "min-w-0", children: [_jsx(PaginatedSelect, { value: picked?.id ?? null, onChange: (value, option) => setPicked(value && option ? { id: value, label: option.label } : null), loadOptions: loadOptions, placeholder: "Pick a bey, part or set\u2026", searchPlaceholder: "Type a name, code or part \u2014 e.g. Dragoon G, 1B, rubber flat", noResultsText: "Nothing in our catalogue matches that.", ariaLabel: "Search the catalogue for a price lookup" }), _jsx(Text, { size: "xs", color: "muted", children: "Models, parts and spares, eras and brands \u2014 straight from our own catalogue, so what you pick is a real thing we can price." })] })) : (
|
|
481
482
|
/*
|
|
482
483
|
* 🛑 The fallback, for a disabled or failed search artifact. Without
|
|
483
484
|
* it a kill-switch flip would leave an empty combobox and no way to
|
|
484
485
|
* search at all — the dropdown has no options without the artifact,
|
|
485
486
|
* but the server-side `searchTxt` path still works.
|
|
486
487
|
*/
|
|
487
|
-
_jsx(Form, { onSubmit: (e) => e.preventDefault(), children: () => (_jsxs(Row, { gap: "sm", align: "end", wrap: true, className: "min-w-0", children: [_jsx(Div, { className: "flex-1 min-w-[16rem]", children: _jsx(FieldInput, { name: "q", label: "Search", value: draft, onChange: setDraft, placeholder: "e.g. Dragoon G, Storm Pegasus, rubber flat\u2026" }) }), _jsxs(Button, { type: "submit", isLoading: loading, onClick: () => run(draft), children: [_jsx(Search, { className: "w-4 h-4", "aria-hidden": "true" }), "Look up"] })] })) })), error && (_jsx(Text, { size: "sm", color: "error", role: "alert", children: error })), !hasQuery && !loading && !error && (_jsxs(Stack, { gap: "xs", padding: "card", rounded: "lg", surface: "muted", className: "min-w-0", children: [_jsx(Text, { weight: "semibold", children: "Two numbers, and the gap matters" }), _jsxs(Text, { size: "sm", color: "muted", children: [_jsx(Span, { weight: "semibold", children: "Sold" }), " is a completed sale on
|
|
488
|
+
_jsx(Form, { onSubmit: (e) => e.preventDefault(), children: () => (_jsxs(Row, { gap: "sm", align: "end", wrap: true, className: "min-w-0", children: [_jsx(Div, { className: "flex-1 min-w-[16rem]", children: _jsx(FieldInput, { name: "q", label: "Search", value: draft, onChange: setDraft, placeholder: "e.g. Dragoon G, Storm Pegasus, rubber flat\u2026" }) }), _jsxs(Button, { type: "submit", isLoading: loading, onClick: () => run(draft), children: [_jsx(Search, { className: "w-4 h-4", "aria-hidden": "true" }), "Look up"] })] })) })), error && (_jsx(Text, { size: "sm", color: "error", role: "alert", children: error })), !hasQuery && !loading && !error && (_jsxs(Stack, { gap: "xs", padding: "card", rounded: "lg", surface: "muted", className: "min-w-0", children: [_jsx(Text, { weight: "semibold", children: "Two numbers, and the gap matters" }), _jsxs(Text, { size: "sm", color: "muted", children: [_jsx(Span, { weight: "semibold", children: "Sold" }), " is a completed sale on", getBrandName(), " \u2014 not an asking price and not a guess. We publish a figure only once ", threshold, " or more sales support it, and the index is recomputed once a day, so it is deliberately a day behind."] }), _jsxs(Text, { size: "sm", color: "muted", children: [_jsx(Span, { weight: "semibold", children: "Asked" }), " is what other Indian shops list the same thing for, measured across fifteen catalogues.", canSeeMarket
|
|
488
489
|
? " Shown beside our own sales — never averaged with them, because an ask is not a sale."
|
|
489
490
|
: " Sign in to see it beside our own sales."] })] })), hasQuery && data && data.results.length === 0 && !loading && (_jsxs(Text, { color: "muted", children: ["Nothing matched \u201C", data.query, "\u201D. Try a shorter term, or the model name on its own."] })), data && data.results.length > 0 && (_jsxs(Stack, { gap: "md", className: "min-w-0", children: [data.results.map((row) => (_jsx(ResultCard, { row: row, threshold: threshold, market: marketById.get(row.categoryId), canSeeMarket: canSeeMarket }, row.categoryId))), data.truncated && (_jsxs(Text, { size: "sm", color: "muted", children: ["Showing the closest ", data.results.length, " matches. Narrow the search to see others."] }))] }))] }) }) }));
|
|
490
491
|
}
|
|
@@ -16,6 +16,7 @@ import { normalizeError } from "../../../errors/normalize.js";
|
|
|
16
16
|
import { toUserMessage } from "../../../errors/error-display-map.js";
|
|
17
17
|
import { ACTIONS } from "../../../_internal/shared/actions/action-registry.js";
|
|
18
18
|
import { RATE_TIER_ORDER, } from "../../../_internal/shared/features/pricing/rate-registry.js";
|
|
19
|
+
import { formatMoney } from "../../../utils/number.formatter.js";
|
|
19
20
|
const TIER_LABEL = {
|
|
20
21
|
statutory: "Statutory",
|
|
21
22
|
policy: "Policy ceiling",
|
|
@@ -45,7 +46,7 @@ function formatRate(value, unit) {
|
|
|
45
46
|
case "percent":
|
|
46
47
|
return `${value}%`;
|
|
47
48
|
case "rupees":
|
|
48
|
-
return
|
|
49
|
+
return formatMoney(value);
|
|
49
50
|
case "days":
|
|
50
51
|
return value === 1 ? "1 day" : `${value} days`;
|
|
51
52
|
case "minutes":
|
|
@@ -42,6 +42,7 @@ import { Row, Stack } from "../../../ui/components/Layout.js";
|
|
|
42
42
|
import { Text } from "../../../ui/components/Typography.js";
|
|
43
43
|
import { normalizeError } from "../../../errors/normalize.js";
|
|
44
44
|
import { MM_PER_INCH, SAFE_MODULE_MM, labelFitsProfile, sheetPositionMm, } from "../../../_internal/shared/features/print/label-profiles.js";
|
|
45
|
+
import { formatMoney } from "../../../utils/number.formatter.js";
|
|
45
46
|
/** CSS px per inch is 96 by definition — not a screen measurement. */
|
|
46
47
|
const CSS_PX_PER_INCH = 96;
|
|
47
48
|
/** Millimetres of paper to CSS pixels. The only unit bridge in this file. */
|
|
@@ -158,7 +159,7 @@ export function ProductLabel({ profile, product, }) {
|
|
|
158
159
|
return (_jsxs(LabelBox, { profile: profile, children: [code && !refuse ? (_jsx(Barcode, { value: code,
|
|
159
160
|
// A third of the label's height, which leaves room for the name and
|
|
160
161
|
// price without the bars becoming too short to scan at an angle.
|
|
161
|
-
heightMm: Math.max(6, profile.heightMm * 0.42) })) : code ? (_jsxs(Text, { size: "xs", color: "error", children: ["Code too long for ", profile.widthMm, "\u00D7", profile.heightMm, " mm"] })) : (_jsx(Text, { size: "xs", color: "muted", children: "No barcode" })), _jsx(Text, { size: "xs", weight: "medium", truncate: 1, children: product.name }), _jsxs(Row, { gap: "xs", justify: "center", wrap: true, children: [product.price != null && (
|
|
162
|
+
heightMm: Math.max(6, profile.heightMm * 0.42) })) : code ? (_jsxs(Text, { size: "xs", color: "error", children: ["Code too long for ", profile.widthMm, "\u00D7", profile.heightMm, " mm"] })) : (_jsx(Text, { size: "xs", color: "muted", children: "No barcode" })), _jsx(Text, { size: "xs", weight: "medium", truncate: 1, children: product.name }), _jsxs(Row, { gap: "xs", justify: "center", wrap: true, children: [product.price != null && (_jsx(Text, { size: "xs", color: "muted", children: formatMoney(product.price) })), product.boxCode && (_jsx(Text, { size: "xs", weight: "semibold", children: product.boxCode }))] })] }));
|
|
162
163
|
}
|
|
163
164
|
/**
|
|
164
165
|
* A box label: the code, large, plus what it held WHEN PRINTED.
|
|
@@ -14,6 +14,7 @@ import { listPublicProducts } from "../../../_internal/server/features/products/
|
|
|
14
14
|
import { hidePublicTestData } from "../../../_internal/server/features/tester/visibility.js";
|
|
15
15
|
import { MEETUP_CITIES } from "../constants/meetup-cities.js";
|
|
16
16
|
import { ProductGrid } from "./ProductGrid.js";
|
|
17
|
+
import { getBrandName } from "../../../core/brand-resolver.js";
|
|
17
18
|
const __P = {
|
|
18
19
|
p5: "p-[var(--appkit-space-5)]",
|
|
19
20
|
};
|
|
@@ -83,5 +84,5 @@ export async function CityCollectiblesView({ city }) {
|
|
|
83
84
|
* grid under a confident headline, which reads as a broken page and
|
|
84
85
|
* is also the doorway pattern this page type is penalised for.
|
|
85
86
|
*/
|
|
86
|
-
_jsxs(Alert, { variant: "info", children: [_jsx(Text, { weight: "semibold", children: `Nobody is selling in ${city.city} right now` }), _jsx(Text, { size: "sm", children: `Classified listings come and go quickly — this page lists them whenever a seller names ${city.city} as their meet-up point. In the meantime, everything on
|
|
87
|
+
_jsxs(Alert, { variant: "info", children: [_jsx(Text, { weight: "semibold", children: `Nobody is selling in ${city.city} right now` }), _jsx(Text, { size: "sm", children: `Classified listings come and go quickly — this page lists them whenever a seller names ${city.city} as their meet-up point. In the meantime, everything on ${getBrandName()} ships nationwide.` }), _jsxs(Row, { gap: "md", wrap: true, className: "mt-3", children: [_jsx(TextLink, { href: String(ROUTES.PUBLIC.PRODUCTS), children: "Browse everything" }), _jsx(TextLink, { href: String(ROUTES.PUBLIC.CLASSIFIED), children: "All classifieds" })] })] })) }) }), _jsx(Section, { padding: "y-xl", children: _jsxs(Div, { className: page.container.lg, children: [_jsx(Stack, { gap: "md", children: _jsxs(Div, { className: __P.p5, surface: "card", rounded: "xl", border: "subtle", children: [_jsx(Heading, { level: 2, size: "lg", weight: "semibold", className: "mb-2", children: `Meeting a seller in ${city.city}` }), _jsxs(Text, { color: "muted", children: ["Meet somewhere public and busy, in daylight. Bring the exact amount if you are paying cash, and check the item against its listing photos before you hand anything over \u2014 a classified sale is between you and the seller, so ", getBrandName(), "'s buyer protection does not cover a cash handover."] }), _jsxs(Row, { gap: "md", wrap: true, className: "mt-3", children: [_jsx(TextLink, { href: String(ROUTES.PUBLIC.SPOT_A_FAKE), children: "How to spot a fake" }), _jsx(TextLink, { href: String(ROUTES.PUBLIC.SCAMS), children: "Scam registry" })] })] }) }), _jsx(Heading, { level: 2, size: "lg", weight: "semibold", className: "mt-8 mb-3", children: "Other cities" }), _jsx(Grid, { cols: "navTiles", gap: "sm", children: others.map((c) => (_jsx(TextLink, { href: String(ROUTES.PUBLIC.CITY_COLLECTIBLES(c.slug)), children: c.city }, c.slug))) })] }) })] }));
|
|
87
88
|
}
|
|
@@ -19,6 +19,7 @@ import { minOfferAmount } from "../../../_internal/shared/features/offers/config
|
|
|
19
19
|
import { isAuthError } from "../../../utils/auth-error.js";
|
|
20
20
|
import { formatCurrency } from "../../../utils/number.formatter.js";
|
|
21
21
|
import { normalizeError } from "../../../errors/normalize.js";
|
|
22
|
+
import { formatMoney } from "../../../utils/number.formatter.js";
|
|
22
23
|
const CLS_SUCCESS_BOX = "rounded-xl border border-success dark:border-success bg-success-surface p-[var(--appkit-space-4)] text-left space-y-1";
|
|
23
24
|
const CLS_PENDING_BOX = "rounded-xl border border-warning dark:border-warning bg-warning-surface dark:bg-warning-surface p-[var(--appkit-space-4)] text-left space-y-1";
|
|
24
25
|
const CLS_PENDING_TITLE = "text-[length:var(--appkit-text-sm)] font-medium text-warning dark:text-warning";
|
|
@@ -50,7 +51,7 @@ export function MakeOfferButton({ productId, listedPrice, currency, bounds, list
|
|
|
50
51
|
* button, which is correct for the signed-out visitor it describes.
|
|
51
52
|
*/
|
|
52
53
|
const viewerOwnsListing = Boolean(listingStoreId) && session?.user?.storeId === listingStoreId;
|
|
53
|
-
const fmt = (n) => (currency ? formatCurrency(n, currency) :
|
|
54
|
+
const fmt = (n) => (currency ? formatCurrency(n, currency) : formatMoney(n));
|
|
54
55
|
// The SAME bounds the server enforces — passed in, never recomputed, so the
|
|
55
56
|
// input can neither seed nor accept an amount `makeOffer` would reject.
|
|
56
57
|
const schema = makeOfferFormSchema({ listedPrice, bounds, formatAmount: fmt });
|
|
@@ -25,6 +25,7 @@ import { Text } from "../../../ui/components/Typography.js";
|
|
|
25
25
|
import { useCanSeePrices } from "../../../react/hooks/useCanSeePrices.js";
|
|
26
26
|
import { MediaImage } from "../../media/MediaImage.js";
|
|
27
27
|
import { ImageLightbox } from "../../../ui/components/ImageLightbox.js";
|
|
28
|
+
import { formatMoney } from "../../../utils/number.formatter.js";
|
|
28
29
|
const __P = {
|
|
29
30
|
p6: "p-[var(--appkit-space-6)]",
|
|
30
31
|
};
|
|
@@ -50,7 +51,7 @@ function toGalleryImages(items, canSeePrices) {
|
|
|
50
51
|
// own estimate is gated in the JSX below and this would otherwise print it
|
|
51
52
|
// again one click away.
|
|
52
53
|
sub: canSeePrices && it.estimatedValue != null
|
|
53
|
-
? `est.
|
|
54
|
+
? `est. ${formatMoney(it.estimatedValue)}`
|
|
54
55
|
: undefined,
|
|
55
56
|
}));
|
|
56
57
|
}
|
|
@@ -84,7 +85,7 @@ export function PrizeDrawCollage({ items, highlightItemNumber, onItemClick, wonL
|
|
|
84
85
|
// The prize's estimated value is a money amount like any
|
|
85
86
|
// other — hidden rather than prompted, because the entry
|
|
86
87
|
// price beside it already carries the sign-in gate.
|
|
87
|
-
_jsx(PricesOnly, { children: _jsxs(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "xs", children: ["est.
|
|
88
|
+
_jsx(PricesOnly, { children: _jsxs(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "xs", children: ["est. ", formatMoney(it.estimatedValue)] }) })) : null] })] }, `collage-${it.itemNumber}`));
|
|
88
89
|
}) }), _jsx(ImageLightbox, { images: galleryImages, activeIndex: lightboxIndex, onClose: () => setLightboxIndex(null), onNavigate: (i) => setLightboxIndex(i), showThumbnails: true })] }));
|
|
89
90
|
}
|
|
90
91
|
export default PrizeDrawCollage;
|
|
@@ -17,6 +17,7 @@ import { PricesOnly } from "../../../ui/components/GatedPrice.js";
|
|
|
17
17
|
import { Stack } from "../../../ui/components/Layout.js";
|
|
18
18
|
import { MediaImage } from "../../media/MediaImage.js";
|
|
19
19
|
import { PrizeDrawCollage } from "./PrizeDrawCollage.js";
|
|
20
|
+
import { getBrandName } from "../../../core/brand-resolver.js";
|
|
20
21
|
const __P = {
|
|
21
22
|
p4: "p-[var(--appkit-space-4)]",
|
|
22
23
|
};
|
|
@@ -31,6 +32,6 @@ export function PrizeRevealModal({ open, onClose, items, initialPrizeWon, reveal
|
|
|
31
32
|
// audit-content-alignment-ok: informational reveal-status panel, not marketing content
|
|
32
33
|
_jsxs(Div, { className: `bg-[var(--appkit-color-bg)] ${__P.p4} text-center`, rounded: "default", children: [_jsx(Text, { size: "lg", weight: "semibold", children: "Reveal pending" }), _jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: PENDING_COPY[revealMode] })] })) : (
|
|
33
34
|
// audit-content-alignment-ok: theatrical reveal-modal centerpiece, not marketing content
|
|
34
|
-
_jsxs(Div, { className: `border-2 border-[var(--appkit-color-primary)] bg-[var(--appkit-color-surface)] ${__P.p4} text-center`, rounded: "lg", children: [_jsx(Text, { className: "tracking-wider text-[var(--appkit-color-text-muted)]", size: "xs", transform: "uppercase", children: "You won" }), _jsxs(Heading, { level: 2, className: "my-2", children: ["#", winner.itemNumber, " \u2014 ", winner.title] }), winnerImg ? (_jsx(Div, { className: "relative mx-auto h-64 w-64 max-w-full", rounded: "md", overflow: "hidden", children: _jsx(MediaImage, { src: winnerImg, alt: winner.title, size: "hero", objectFit: "contain" }) })) : null, winner.estimatedValue != null ? (_jsx(PricesOnly, { children: _jsxs(Text, { className: "mt-2 text-[var(--appkit-color-text-muted)]", size: "sm", children: ["Estimated value: \u20B9", winner.estimatedValue.toLocaleString("en-IN")] }) })) : null] })), _jsxs(Div, { textSize: "xs", className: "border border-[var(--appkit-color-border)] bg-[var(--appkit-color-bg)] text-[var(--appkit-color-text-muted)]", padding: "inlineSm", rounded: "default", children: [_jsx(Span, { weight: "bold", children: "Fairness guarantee:" }), " ", "Winners are picked by ", _jsx("code", { children: "crypto.randomInt" }), " running on
|
|
35
|
+
_jsxs(Div, { className: `border-2 border-[var(--appkit-color-primary)] bg-[var(--appkit-color-surface)] ${__P.p4} text-center`, rounded: "lg", children: [_jsx(Text, { className: "tracking-wider text-[var(--appkit-color-text-muted)]", size: "xs", transform: "uppercase", children: "You won" }), _jsxs(Heading, { level: 2, className: "my-2", children: ["#", winner.itemNumber, " \u2014 ", winner.title] }), winnerImg ? (_jsx(Div, { className: "relative mx-auto h-64 w-64 max-w-full", rounded: "md", overflow: "hidden", children: _jsx(MediaImage, { src: winnerImg, alt: winner.title, size: "hero", objectFit: "contain" }) })) : null, winner.estimatedValue != null ? (_jsx(PricesOnly, { children: _jsxs(Text, { className: "mt-2 text-[var(--appkit-color-text-muted)]", size: "sm", children: ["Estimated value: \u20B9", winner.estimatedValue.toLocaleString("en-IN")] }) })) : null] })), _jsxs(Div, { textSize: "xs", className: "border border-[var(--appkit-color-border)] bg-[var(--appkit-color-bg)] text-[var(--appkit-color-text-muted)]", padding: "inlineSm", rounded: "default", children: [_jsx(Span, { weight: "bold", children: "Fairness guarantee:" }), " ", "Winners are picked by ", _jsx("code", { children: "crypto.randomInt" }), " running on", getBrandName(), "'s server the moment your prize is assigned. Neither the store nor ", getBrandName(), " staff can influence the outcome.", " ", rngSourceUrl ? (_jsx(Anchor, { href: rngSourceUrl, tone: "none", underline: "always", children: "View RNG source code \u2192" })) : null] })] }) }));
|
|
35
36
|
}
|
|
36
37
|
export default PrizeRevealModal;
|
|
@@ -52,6 +52,7 @@ import { interpolate } from "../../../_internal/shared/templating/placeholders.j
|
|
|
52
52
|
import { ALL_LISTING_TYPES } from "../../../_internal/shared/listing-types/feature-flags.js";
|
|
53
53
|
import { pluginFor } from "../../../_internal/shared/listing-types/_registry.js";
|
|
54
54
|
import { PRODUCT_FIELDS } from "../../../constants/field-names.js";
|
|
55
|
+
import { getBrandName } from "../../../core/brand-resolver.js";
|
|
55
56
|
/**
|
|
56
57
|
* Canonical listing-kind tokens used in `listingType`. Note: the public Sieve
|
|
57
58
|
* alias accepts the legacy "preorder" / "product" spelling; we normalise to
|
|
@@ -137,7 +138,7 @@ function interpolateDefaults(template, input, derived) {
|
|
|
137
138
|
category: names[0],
|
|
138
139
|
categoryPath: names.length ? [...names].reverse().join(" › ") : undefined,
|
|
139
140
|
series: names[names.length - 2],
|
|
140
|
-
siteName:
|
|
141
|
+
siteName: getBrandName(),
|
|
141
142
|
}).text;
|
|
142
143
|
}
|
|
143
144
|
/**
|
|
@@ -13,6 +13,7 @@ import { Row, Stack } from "../../../ui/components/Layout.js";
|
|
|
13
13
|
import { Span, Text } from "../../../ui/components/Typography.js";
|
|
14
14
|
import { useLongPress } from "../../../react/hooks/useLongPress.js";
|
|
15
15
|
import { ACCOUNT_ENDPOINTS } from "../../../constants/api-endpoints.js";
|
|
16
|
+
import { formatMoney } from "../../../utils/number.formatter.js";
|
|
16
17
|
const CLS_TOGGLE_ON = "h-4 w-4 text-success dark:text-success";
|
|
17
18
|
const CLS_DELETE_BTN = "p-[var(--appkit-space-1-5)] text-[var(--appkit-color-text-muted)] hover:bg-error-surface dark:hover:bg-error-surface hover:text-error dark:hover:text-error transition-colors disabled:opacity-50";
|
|
18
19
|
const TYPE_COLORS = {
|
|
@@ -82,7 +83,7 @@ function formatDiscount(n, labels) {
|
|
|
82
83
|
if (n.type === "percentage")
|
|
83
84
|
return `${n.discountValue}% ${labels.off}`;
|
|
84
85
|
if (n.type === "fixed")
|
|
85
|
-
return
|
|
86
|
+
return `${formatMoney(n.discountValue)} ${labels.off}`;
|
|
86
87
|
if (n.type === "free_shipping")
|
|
87
88
|
return labels.freeShipping;
|
|
88
89
|
if (n.type === "buy_x_get_y")
|
|
@@ -190,7 +191,7 @@ export function CouponCard({ coupon, labels: labelsProp, onCopy, className = "",
|
|
|
190
191
|
setBusy(null);
|
|
191
192
|
}
|
|
192
193
|
};
|
|
193
|
-
return (_jsxs(Stack, { className: `group relative h-full border-2 ${colors.card} ${isSelected ? "ring-2 ring-primary" : ""} ${!n.isActive ? "opacity-70" : ""} ${className}`, rounded: "xl", padding: "md", onMouseDown: onSelect && !isSelected ? longPress.onMouseDown : undefined, onMouseUp: onSelect && !isSelected ? longPress.onMouseUp : undefined, onMouseLeave: onSelect && !isSelected ? longPress.onMouseLeave : undefined, onTouchStart: onSelect && !isSelected ? longPress.onTouchStart : undefined, onTouchEnd: onSelect && !isSelected ? longPress.onTouchEnd : undefined, onTouchCancel: onSelect && !isSelected ? longPress.onTouchCancel : undefined, children: [onSelect && (_jsx(BaseListingCard.Checkbox, { selected: isSelected, onSelect: (e) => { e.preventDefault(); onSelect(n.id, !isSelected); }, label: isSelected ? "Deselect coupon" : "Select coupon", position: "top-2 right-2", className: selectable || isSelected ? "opacity-100" : "opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-opacity" })), (hasAdminActions || n.scope) && (_jsxs(Row, { gap: "xs", wrap: true, className: "mb-2", children: [_jsx(Badge, { variant: n.isActive ? "active" : "inactive", size: "xs", className: "text-[10px] uppercase tracking-wide", children: n.isActive ? labels.active : labels.inactive }), n.scope && (_jsx(Span, { layout: "inline-flex", weight: "semibold", className: "bg-neutral-200/70 text-[10px] tracking-wide bg-[var(--appkit-color-border)]/60", rounded: "full", padding: "pill-xs", color: "primary", transform: "uppercase", children: n.scope }))] })), _jsxs(Div, { className: "mb-3", children: [_jsx(Text, { className: "font-extrabold tracking-tight leading-none", size: "2xl", children: discountLabel }), n.name && (_jsx(Text, { className: "mt-0.5 opacity-80", size: "sm", weight: "medium", children: n.name }))] }), n.description && (_jsx(Text, { className: "opacity-60 mb-3", size: "xs", children: n.description })), _jsxs(Row, { className: `border border-dashed ${colors.code}`, align: "center", gap: "sm", rounded: "lg", padding: "inlineSm", children: [_jsx(Span, { size: "sm", weight: "bold", className: "flex-1 font-mono tracking-widest select-all", transform: "uppercase", children: n.code || "—" }), n.code && (_jsx(Button, { variant: "outline", type: "button", onClick: handleCopy, rounded: "md", paddingX: "sm", paddingY: "none", textSize: "xs", weight: "semibold", className: "shrink-0 hover:opacity-80 active:scale-95 border-current/20", "aria-label": "Copy coupon code", children: copied ? labels.copied : labels.copy }))] }), showClaim && n.code && n.isActive && (_jsx(Div, { className: "mt-2", children: _jsx(Button, { variant: "primary", type: "button", onClick: handleClaim, disabled: claiming, rounded: "md", paddingX: "md", paddingY: "xs", textSize: "xs", weight: "semibold", className: "w-full active:scale-95 bg-[var(--appkit-color-primary-700)] hover:bg-[var(--appkit-color-primary-800)]", "aria-label": `Claim coupon ${n.code} and apply at checkout`, children: claiming ? labels.claiming : labels.claim }) })), _jsxs(Row, { textSize: "xs", wrap: true, gap: "sm", className: "opacity-60 mt-2", children: [n.minOrderAmount ? (_jsxs(Span, { children: [labels.minOrder, ":
|
|
194
|
+
return (_jsxs(Stack, { className: `group relative h-full border-2 ${colors.card} ${isSelected ? "ring-2 ring-primary" : ""} ${!n.isActive ? "opacity-70" : ""} ${className}`, rounded: "xl", padding: "md", onMouseDown: onSelect && !isSelected ? longPress.onMouseDown : undefined, onMouseUp: onSelect && !isSelected ? longPress.onMouseUp : undefined, onMouseLeave: onSelect && !isSelected ? longPress.onMouseLeave : undefined, onTouchStart: onSelect && !isSelected ? longPress.onTouchStart : undefined, onTouchEnd: onSelect && !isSelected ? longPress.onTouchEnd : undefined, onTouchCancel: onSelect && !isSelected ? longPress.onTouchCancel : undefined, children: [onSelect && (_jsx(BaseListingCard.Checkbox, { selected: isSelected, onSelect: (e) => { e.preventDefault(); onSelect(n.id, !isSelected); }, label: isSelected ? "Deselect coupon" : "Select coupon", position: "top-2 right-2", className: selectable || isSelected ? "opacity-100" : "opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-opacity" })), (hasAdminActions || n.scope) && (_jsxs(Row, { gap: "xs", wrap: true, className: "mb-2", children: [_jsx(Badge, { variant: n.isActive ? "active" : "inactive", size: "xs", className: "text-[10px] uppercase tracking-wide", children: n.isActive ? labels.active : labels.inactive }), n.scope && (_jsx(Span, { layout: "inline-flex", weight: "semibold", className: "bg-neutral-200/70 text-[10px] tracking-wide bg-[var(--appkit-color-border)]/60", rounded: "full", padding: "pill-xs", color: "primary", transform: "uppercase", children: n.scope }))] })), _jsxs(Div, { className: "mb-3", children: [_jsx(Text, { className: "font-extrabold tracking-tight leading-none", size: "2xl", children: discountLabel }), n.name && (_jsx(Text, { className: "mt-0.5 opacity-80", size: "sm", weight: "medium", children: n.name }))] }), n.description && (_jsx(Text, { className: "opacity-60 mb-3", size: "xs", children: n.description })), _jsxs(Row, { className: `border border-dashed ${colors.code}`, align: "center", gap: "sm", rounded: "lg", padding: "inlineSm", children: [_jsx(Span, { size: "sm", weight: "bold", className: "flex-1 font-mono tracking-widest select-all", transform: "uppercase", children: n.code || "—" }), n.code && (_jsx(Button, { variant: "outline", type: "button", onClick: handleCopy, rounded: "md", paddingX: "sm", paddingY: "none", textSize: "xs", weight: "semibold", className: "shrink-0 hover:opacity-80 active:scale-95 border-current/20", "aria-label": "Copy coupon code", children: copied ? labels.copied : labels.copy }))] }), showClaim && n.code && n.isActive && (_jsx(Div, { className: "mt-2", children: _jsx(Button, { variant: "primary", type: "button", onClick: handleClaim, disabled: claiming, rounded: "md", paddingX: "md", paddingY: "xs", textSize: "xs", weight: "semibold", className: "w-full active:scale-95 bg-[var(--appkit-color-primary-700)] hover:bg-[var(--appkit-color-primary-800)]", "aria-label": `Claim coupon ${n.code} and apply at checkout`, children: claiming ? labels.claiming : labels.claim }) })), _jsxs(Row, { textSize: "xs", wrap: true, gap: "sm", className: "opacity-60 mt-2", children: [n.minOrderAmount ? (_jsxs(Span, { children: [labels.minOrder, ": ", formatMoney(n.minOrderAmount)] })) : null, expiry && (_jsxs(Span, { children: [labels.expires, ": ", expiry] })), n.usageLimit ? (_jsxs(Span, { children: [n.usageCount, "/", n.usageLimit, " ", labels.used] })) : n.usageCount > 0 ? (_jsxs(Span, { children: [n.usageCount, " ", labels.used] })) : null] }), hasAdminActions && (_jsxs(Row, { gap: "xs", justify: "end", className: "mt-auto", padding: "t-sm", children: [onEdit && (_jsx(IconButton, { type: "button", onClick: () => onEdit(n.id), title: labels.edit, "aria-label": labels.edit, variant: "ghost", rounded: "lg", className: "text-[var(--appkit-color-text-muted)]", children: _jsx(Pencil, { className: "h-4 w-4" }) })), onToggleActive && (_jsx(IconButton, { type: "button", onClick: handleToggle, disabled: busy === "toggle", title: n.isActive ? labels.deactivate : labels.activate, "aria-label": n.isActive ? labels.deactivate : labels.activate, variant: "ghost", rounded: "lg", className: "text-[var(--appkit-color-text-muted)]", children: n.isActive
|
|
194
195
|
? _jsx(ToggleRight, { className: CLS_TOGGLE_ON })
|
|
195
196
|
: _jsx(ToggleLeft, { className: "h-4 w-4" }) })), onDelete && (_jsx(IconButton, { type: "button", onClick: () => setDeleteConfirmOpen(true), disabled: busy === "delete", title: labels.delete, "aria-label": labels.delete, variant: "ghost", rounded: "lg", className: CLS_DELETE_BTN, children: _jsx(Trash2, { className: "h-4 w-4" }) }))] })), deleteConfirmOpen && (_jsx(ConfirmDeleteModal, { isOpen: true, title: "Delete Coupon", message: labels.deleteConfirm, onConfirm: handleDeleteConfirmed, onClose: () => setDeleteConfirmOpen(false), isDeleting: busy === "delete" }))] }));
|
|
196
197
|
}
|
|
@@ -4,6 +4,7 @@ import { Details, Summary } from "../../../ui/components/Details.js";
|
|
|
4
4
|
import { Stack } from "../../../ui/components/Layout.js";
|
|
5
5
|
import { Text, Span } from "../../../ui/components/Typography.js";
|
|
6
6
|
import { Ul, Li } from "../../../ui/components/Semantic.js";
|
|
7
|
+
import { withBrand, withBrandAll } from "../../../core/brand-text.js";
|
|
7
8
|
import { COUPON_HELP } from "../constants/coupon-help.js";
|
|
8
9
|
function HelpSection({ heading, items }) {
|
|
9
10
|
return (_jsxs(Stack, { gap: "dense", children: [_jsx(Text, { size: "sm", weight: "semibold", children: heading }), _jsx(Ul, { marker: "disc", indent: "md", spacing: "tight", size: "sm", children: items.map((item) => (_jsx(Li, { color: "muted", children: item }, item))) })] }));
|
|
@@ -16,5 +17,5 @@ function HelpSection({ heading, items }) {
|
|
|
16
17
|
* listing render identical text.
|
|
17
18
|
*/
|
|
18
19
|
export function CouponHelpDetails({ showRevalidationNote = false, defaultOpen = false, name, }) {
|
|
19
|
-
return (_jsxs(Details, { tone: "card", padding: "none", defaultOpen: defaultOpen, name: name, children: [_jsxs(Summary, { paddingX: "x-md", paddingY: "y-md", size: "sm", weight: "semibold", layout: "flex", align: "center", justify: "between", children: [_jsx(Span, { children: COUPON_HELP.title }), _jsx(Span, { size: "xs", color: "muted", children: "Stacking rules" })] }), _jsxs(Stack, { gap: "comfortable", padding: "md", children: [_jsx(HelpSection, { heading: COUPON_HELP.stackingHeading, items: COUPON_HELP.stackingRules }), _jsx(HelpSection, { heading: COUPON_HELP.applicationHeading, items: COUPON_HELP.applicationRules }), _jsx(HelpSection, { heading: COUPON_HELP.failureHeading, items: COUPON_HELP.failureReasons }), showRevalidationNote && (_jsx(Text, { size: "xs", color: "muted", children: COUPON_HELP.revalidationNote }))] })] }));
|
|
20
|
+
return (_jsxs(Details, { tone: "card", padding: "none", defaultOpen: defaultOpen, name: name, children: [_jsxs(Summary, { paddingX: "x-md", paddingY: "y-md", size: "sm", weight: "semibold", layout: "flex", align: "center", justify: "between", children: [_jsx(Span, { children: COUPON_HELP.title }), _jsx(Span, { size: "xs", color: "muted", children: "Stacking rules" })] }), _jsxs(Stack, { gap: "comfortable", padding: "md", children: [_jsx(HelpSection, { heading: COUPON_HELP.stackingHeading, items: withBrandAll(COUPON_HELP.stackingRules) }), _jsx(HelpSection, { heading: COUPON_HELP.applicationHeading, items: withBrandAll(COUPON_HELP.applicationRules) }), _jsx(HelpSection, { heading: COUPON_HELP.failureHeading, items: withBrandAll(COUPON_HELP.failureReasons) }), showRevalidationNote && (_jsx(Text, { size: "xs", color: "muted", children: withBrand(COUPON_HELP.revalidationNote) }))] })] }));
|
|
20
21
|
}
|
|
@@ -15,7 +15,7 @@ export declare const COUPON_HELP: {
|
|
|
15
15
|
readonly title: "How coupons work";
|
|
16
16
|
/** The stacking rules — the part buyers most often get wrong. */
|
|
17
17
|
readonly stackingHeading: "You can use more than one coupon";
|
|
18
|
-
readonly stackingRules: readonly ["One store coupon per store. If your cart has items from three stores, you can apply each of those stores' coupons at the same time.", "Plus one platform-wide
|
|
18
|
+
readonly stackingRules: readonly ["One store coupon per store. If your cart has items from three stores, you can apply each of those stores' coupons at the same time.", "Plus one platform-wide {{siteName}} coupon, on top of any store coupons.", "Plus one points offer, which stacks with everything else — it earns you points rather than discounting, so it never takes the place of a discount coupon.", "A second coupon for the same store, a second platform-wide coupon, or a second points offer will be declined — remove the first one to swap it."];
|
|
19
19
|
/** How the discount is actually distributed. */
|
|
20
20
|
readonly applicationHeading: "Where each discount lands";
|
|
21
21
|
readonly applicationRules: readonly ["A store coupon only discounts that store's items.", "A platform-wide coupon is spread proportionally across every store in your cart.", "Orders are placed per store, so each store's order shows its own share of the discount.", "A points offer earns points on what you pay after any discounts, so the two interact rather than ignoring each other."];
|
|
@@ -11,13 +11,20 @@
|
|
|
11
11
|
* enforces the stacking rules described here, and with the seeded FAQ
|
|
12
12
|
* `faq-coupons-and-discounts`.
|
|
13
13
|
*/
|
|
14
|
+
/*
|
|
15
|
+
* 🛑 Carries a `{{siteName}}` token rather than a literal. This is module-level
|
|
16
|
+
* data, so a template literal here would resolve at IMPORT time — before the
|
|
17
|
+
* consumer's `configureBrand()` has run — and permanently capture appkit's
|
|
18
|
+
* neutral default. `CouponHelpDetails` resolves it at render. See
|
|
19
|
+
* `core/brand-text.ts`.
|
|
20
|
+
*/
|
|
14
21
|
export const COUPON_HELP = {
|
|
15
22
|
title: "How coupons work",
|
|
16
23
|
/** The stacking rules — the part buyers most often get wrong. */
|
|
17
24
|
stackingHeading: "You can use more than one coupon",
|
|
18
25
|
stackingRules: [
|
|
19
26
|
"One store coupon per store. If your cart has items from three stores, you can apply each of those stores' coupons at the same time.",
|
|
20
|
-
"Plus one platform-wide
|
|
27
|
+
"Plus one platform-wide {{siteName}} coupon, on top of any store coupons.",
|
|
21
28
|
// D51's third bucket, stated for the buyer. Without this line the rule
|
|
22
29
|
// above reads as "one platform coupon" and a buyer with a points offer
|
|
23
30
|
// applied would reasonably think it was using up that slot.
|
|
@@ -15,6 +15,7 @@ import { apiClient } from "../../../http/index.js";
|
|
|
15
15
|
import { ACCOUNT_ENDPOINTS } from "../../../constants/api-endpoints.js";
|
|
16
16
|
import { SCAM_CATEGORIES } from "../constants/scam-types.js";
|
|
17
17
|
import { ROUTES } from "../../../next/routing/route-map.js";
|
|
18
|
+
import { getBrandName } from "../../../core/brand-resolver.js";
|
|
18
19
|
const CATEGORY_ICONS = {
|
|
19
20
|
price_manipulation: _jsx(IndianRupee, { className: "h-5 w-5" }),
|
|
20
21
|
social_engineering: _jsx(UserX, { className: "h-5 w-5" }),
|
|
@@ -34,5 +35,5 @@ export function ScamAwarenessModal({ isOpen, onAcknowledged }) {
|
|
|
34
35
|
onAcknowledged();
|
|
35
36
|
},
|
|
36
37
|
});
|
|
37
|
-
return (_jsx(Modal, { isOpen: isOpen, onClose: () => { }, showCloseButton: false, size: "lg", title: _jsxs(Row, { gap: "sm", align: "center", children: [_jsx(Shield, { className: "h-5 w-5 text-[color:var(--appkit-color-warning,theme(colors.amber.500))]" }),
|
|
38
|
+
return (_jsx(Modal, { isOpen: isOpen, onClose: () => { }, showCloseButton: false, size: "lg", title: _jsxs(Row, { gap: "sm", align: "center", children: [_jsx(Shield, { className: "h-5 w-5 text-[color:var(--appkit-color-warning,theme(colors.amber.500))]" }), _jsxs(Span, { children: ["Before you start \u2014 Stay Safe on ", getBrandName()] })] }), actions: _jsx(Row, { gap: "sm", justify: "end", className: "w-full", children: _jsx(Button, { variant: "primary", size: "md", disabled: !checked || mutation.isPending, isLoading: mutation.isPending, onClick: () => mutation.mutate(), children: mutation.isPending ? "Saving…" : `Continue to ${getBrandName()} →` }) }), children: _jsxs(Stack, { gap: "md", children: [_jsxs(Text, { variant: "secondary", size: "sm", children: [getBrandName(), " connects you with verified sellers, but collectibles markets attract scams. Take 60 seconds to learn the most common patterns \u2014 it could save your money."] }), _jsx(Div, { layout: "grid", gap: "3", className: "grid-cols-1 sm:grid-cols-2", children: SCAM_CATEGORIES.map((cat) => (_jsxs(Div, { layout: "flex", gap: "3", rounded: "lg", padding: "sm", className: "border border-[color:var(--appkit-color-border,theme(colors.zinc.200))] bg-[color:var(--appkit-color-surface,theme(colors.zinc.50))]", children: [_jsx(Span, { layout: "flex-center", className: "mt-0.5 h-8 w-8 shrink-0 bg-[color:var(--appkit-color-warning,theme(colors.amber.500))]/10 text-[color:var(--appkit-color-warning,theme(colors.amber.600))]", rounded: "md", children: CATEGORY_ICONS[cat.id] ?? _jsx(Shield, { className: "h-4 w-4" }) }), _jsxs(Stack, { gap: "none", children: [_jsx(Text, { weight: "semibold", size: "sm", children: cat.label }), _jsx(Text, { variant: "secondary", className: "leading-relaxed", size: "xs", children: cat.description })] })] }, cat.id))) }), _jsxs(Row, { textSize: "sm", gap: "md", wrap: true, children: [_jsx(TextLink, { variant: "bare", href: String(ROUTES.PUBLIC.SCAM_TYPES), target: "_blank", rel: "noopener noreferrer", className: "text-[color:var(--appkit-color-primary,theme(colors.blue.600))] hover:underline", children: "See all 27 scam types \u2192" }), _jsx(TextLink, { variant: "bare", href: String(ROUTES.PUBLIC.SCAM_FAQS), target: "_blank", rel: "noopener noreferrer", className: "text-[color:var(--appkit-color-primary,theme(colors.blue.600))] hover:underline", children: "Common scam FAQs \u2192" })] }), mutation.isError && (_jsx(Text, { variant: "error", size: "sm", children: "Something went wrong \u2014 please try again." })), _jsx(Checkbox, { id: "scam-awareness-ack", checked: checked, onChange: (e) => setChecked(e.target.checked), label: "I have read the above and understand the risks of buying and selling collectibles online." })] }) }));
|
|
38
39
|
}
|
|
@@ -16,9 +16,11 @@ import { EmptyState } from "../../../ui/components/EmptyState.js";
|
|
|
16
16
|
import { Heading, Span, Text } from "../../../ui/components/Typography.js";
|
|
17
17
|
import { Main, Section } from "../../../ui/components/Semantic.js";
|
|
18
18
|
import { MediaImage } from "../../media/MediaImage.js";
|
|
19
|
+
import { withBrand } from "../../../core/brand-text.js";
|
|
19
20
|
import { SCAM_PLATFORM_LABELS, SCAMMER_STATUS_LABELS, SOCIAL_PLATFORM_LABELS, CONTEST_TYPE_LABELS, } from "../schemas/firestore.js";
|
|
20
21
|
import { SCAM_TYPE_LABELS, getScamType } from "../constants/scam-types.js";
|
|
21
22
|
import { ROUTES } from "../../../next/routing/route-map.js";
|
|
23
|
+
import { getBrandName } from "../../../core/brand-resolver.js";
|
|
22
24
|
function formatDate(d) {
|
|
23
25
|
if (!d)
|
|
24
26
|
return "";
|
|
@@ -42,10 +44,10 @@ function statusVariant(status) {
|
|
|
42
44
|
}
|
|
43
45
|
// ─── Sub-components ───────────────────────────────────────────────────────────
|
|
44
46
|
function ScammerHeaderCard({ scammer }) {
|
|
45
|
-
return (_jsx(Card, { variant: "elevated", padding: "lg", children: _jsxs(Stack, { gap: "md", children: [_jsx(Disclaimer, { id: "scam-registry" }), _jsxs(Row, { justify: "between", gap: "md", align: "start", wrap: true, children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Heading, { level: 1, weight: "bold", size: "2xl", children: scammer.displayNames[0] }), scammer.displayNames.length > 1 && (_jsxs(Text, { variant: "secondary", size: "sm", children: ["Also known as: ", scammer.displayNames.slice(1).join(", ")] }))] }), _jsx(Badge, { variant: statusVariant(scammer.status), children: SCAMMER_STATUS_LABELS[scammer.status] ?? scammer.status })] }), _jsxs(Row, { gap: "xs", wrap: true, children: [_jsx(Badge, { variant: "warning", children: SCAM_TYPE_LABELS[scammer.scamType] ?? scammer.scamType }), _jsxs(Badge, { variant: "default", children: ["via ", SCAM_PLATFORM_LABELS[scammer.scamPlatform] ?? scammer.scamPlatform] }), scammer.tags.includes("repeat_offender") && (_jsx(Badge, { variant: "danger", children: "Repeat Offender" }))] }), _jsxs(Row, { gap: "md", wrap: true, children: [_jsxs(Text, { variant: "secondary", layout: "flex", gap: "1.5", size: "sm", children: [_jsx(Eye, { className: "h-4 w-4" }), scammer.views, " views"] }), _jsxs(Text, { variant: "secondary", layout: "flex", gap: "1.5", size: "sm", children: [_jsx(Users, { className: "h-4 w-4" }), scammer.incidentCount + 1, " victim", scammer.incidentCount !== 0 ? "s" : "", " reported"] }), _jsxs(Text, { variant: "secondary", layout: "flex", gap: "1.5", size: "sm", children: [_jsx(Calendar, { className: "h-4 w-4" }), "Reported ", formatDate(scammer.createdAt)] }), scammer.amountLost ? (_jsxs(Text, { className: "text-[color:var(--appkit-color-danger,theme(colors.red.600))]", size: "sm", weight: "medium", children: [formatCurrency(scammer.amountLost), " lost (primary incident)"] })) : null] })] }) }));
|
|
47
|
+
return (_jsx(Card, { variant: "elevated", padding: "lg", children: _jsxs(Stack, { gap: "md", children: [_jsx(Disclaimer, { id: "scam-registry" }), _jsxs(Row, { justify: "between", gap: "md", align: "start", wrap: true, children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Heading, { level: 1, weight: "bold", size: "2xl", children: scammer.displayNames[0] }), scammer.displayNames.length > 1 && (_jsxs(Text, { variant: "secondary", size: "sm", children: ["Also known as: ", scammer.displayNames.slice(1).join(", ")] }))] }), _jsx(Badge, { variant: statusVariant(scammer.status), children: SCAMMER_STATUS_LABELS[scammer.status] ?? scammer.status })] }), _jsxs(Row, { gap: "xs", wrap: true, children: [_jsx(Badge, { variant: "warning", children: SCAM_TYPE_LABELS[scammer.scamType] ?? scammer.scamType }), _jsxs(Badge, { variant: "default", children: ["via ", withBrand(SCAM_PLATFORM_LABELS[scammer.scamPlatform] ?? scammer.scamPlatform)] }), scammer.tags.includes("repeat_offender") && (_jsx(Badge, { variant: "danger", children: "Repeat Offender" }))] }), _jsxs(Row, { gap: "md", wrap: true, children: [_jsxs(Text, { variant: "secondary", layout: "flex", gap: "1.5", size: "sm", children: [_jsx(Eye, { className: "h-4 w-4" }), scammer.views, " views"] }), _jsxs(Text, { variant: "secondary", layout: "flex", gap: "1.5", size: "sm", children: [_jsx(Users, { className: "h-4 w-4" }), scammer.incidentCount + 1, " victim", scammer.incidentCount !== 0 ? "s" : "", " reported"] }), _jsxs(Text, { variant: "secondary", layout: "flex", gap: "1.5", size: "sm", children: [_jsx(Calendar, { className: "h-4 w-4" }), "Reported ", formatDate(scammer.createdAt)] }), scammer.amountLost ? (_jsxs(Text, { className: "text-[color:var(--appkit-color-danger,theme(colors.red.600))]", size: "sm", weight: "medium", children: [formatCurrency(scammer.amountLost), " lost (primary incident)"] })) : null] })] }) }));
|
|
46
48
|
}
|
|
47
49
|
function ScammerIncidentsSection({ incidents }) {
|
|
48
|
-
return (_jsxs(Stack, { gap: "sm", children: [_jsx(Row, { justify: "between", align: "center", children: _jsxs(Heading, { level: 2, size: "base", weight: "semibold", children: ["Additional Incidents", incidents.length > 0 && ` (${incidents.length})`] }) }), incidents.length === 0 ? (_jsx(EmptyState, { icon: _jsx(FileText, { className: "h-8 w-8" }), title: "No additional incidents yet", description: "Other victims' verified reports linked to this profile will appear here." })) : (_jsx(Stack, { gap: "sm", children: incidents.map((inc) => (_jsx(Card, { variant: "outlined", padding: "md", children: _jsxs(Stack, { gap: "xs", children: [_jsxs(Row, { justify: "between", align: "start", gap: "sm", wrap: true, children: [_jsxs(Row, { gap: "xs", wrap: true, children: [_jsx(Badge, { variant: "warning", children: SCAM_TYPE_LABELS[inc.scamType] ?? inc.scamType }), _jsxs(Badge, { variant: "default", children: ["via ", SCAM_PLATFORM_LABELS[inc.scamPlatform] ?? inc.scamPlatform] })] }), _jsx(Text, { variant: "secondary", size: "xs", children: formatDate(inc.createdAt) })] }), inc.itemInvolved && (_jsxs(Text, { variant: "secondary", size: "xs", children: ["Item: ", inc.itemInvolved] })), inc.amountLost ? (_jsxs(Text, { className: "text-[color:var(--appkit-color-danger,theme(colors.red.600))]", size: "xs", weight: "medium", children: [formatCurrency(inc.amountLost), " lost"] })) : null, _jsx(Text, { variant: "secondary", className: "leading-relaxed", size: "sm", children: inc.description.length > 200
|
|
50
|
+
return (_jsxs(Stack, { gap: "sm", children: [_jsx(Row, { justify: "between", align: "center", children: _jsxs(Heading, { level: 2, size: "base", weight: "semibold", children: ["Additional Incidents", incidents.length > 0 && ` (${incidents.length})`] }) }), incidents.length === 0 ? (_jsx(EmptyState, { icon: _jsx(FileText, { className: "h-8 w-8" }), title: "No additional incidents yet", description: "Other victims' verified reports linked to this profile will appear here." })) : (_jsx(Stack, { gap: "sm", children: incidents.map((inc) => (_jsx(Card, { variant: "outlined", padding: "md", children: _jsxs(Stack, { gap: "xs", children: [_jsxs(Row, { justify: "between", align: "start", gap: "sm", wrap: true, children: [_jsxs(Row, { gap: "xs", wrap: true, children: [_jsx(Badge, { variant: "warning", children: SCAM_TYPE_LABELS[inc.scamType] ?? inc.scamType }), _jsxs(Badge, { variant: "default", children: ["via ", withBrand(SCAM_PLATFORM_LABELS[inc.scamPlatform] ?? inc.scamPlatform)] })] }), _jsx(Text, { variant: "secondary", size: "xs", children: formatDate(inc.createdAt) })] }), inc.itemInvolved && (_jsxs(Text, { variant: "secondary", size: "xs", children: ["Item: ", inc.itemInvolved] })), inc.amountLost ? (_jsxs(Text, { className: "text-[color:var(--appkit-color-danger,theme(colors.red.600))]", size: "xs", weight: "medium", children: [formatCurrency(inc.amountLost), " lost"] })) : null, _jsx(Text, { variant: "secondary", className: "leading-relaxed", size: "sm", children: inc.description.length > 200
|
|
49
51
|
? `${inc.description.slice(0, 200).trimEnd()}…`
|
|
50
52
|
: inc.description }), _jsxs(Text, { variant: "secondary", size: "xs", children: ["Reported by: ", inc.reportedByAnon ? "Anonymous" : "Verified victim"] })] }) }, inc.id))) }))] }));
|
|
51
53
|
}
|
|
@@ -54,7 +56,7 @@ function ScammerCommentsSection({ comments, isAuthenticated, scammerId, }) {
|
|
|
54
56
|
return (_jsxs(Stack, { gap: "sm", children: [_jsxs(Row, { justify: "between", align: "center", children: [_jsxs(Heading, { level: 2, size: "base", weight: "semibold", children: ["Community Discussion", comments.length > 0 && ` (${comments.length})`] }), isAuthenticated ? (_jsx(Link, { href: String(ROUTES.PUBLIC.SCAM_REPORT), className: "text-[length:var(--appkit-text-xs)] text-[color:var(--appkit-color-primary,theme(colors.blue.600))] hover:underline", children: "Leave a comment" })) : (_jsx(Link, { href: commentLoginHref, className: "text-[length:var(--appkit-text-xs)] text-[color:var(--appkit-color-primary,theme(colors.blue.600))] hover:underline", children: "Sign in to comment" }))] }), comments.length === 0 ? (_jsx(EmptyState, { icon: _jsx(MessageSquare, { className: "h-8 w-8" }), title: "No comments yet", description: "Community comments and victim testimonials appear here." })) : (_jsx(Stack, { gap: "sm", children: comments.map((c) => (_jsx(Card, { variant: "flat", padding: "md", children: _jsxs(Stack, { gap: "xs", children: [_jsxs(Row, { justify: "between", align: "center", gap: "sm", children: [_jsxs(Row, { gap: "xs", align: "center", children: [_jsx(Text, { size: "sm", weight: "medium", children: c.authorDisplayName }), c.authorRole !== "user" && (_jsx(Badge, { variant: "default", className: "text-[10px]", children: c.authorRole })), c.isAccused && c.isAccusedVerified && (_jsx(Badge, { variant: "warning", className: "text-[10px]", children: "Accused" })), c.isVerifiedVictim && (_jsx(Badge, { variant: "success", className: "text-[10px]", children: "Verified Victim" }))] }), _jsx(Text, { variant: "secondary", size: "xs", children: formatDate(c.createdAt) })] }), _jsx(Text, { variant: "secondary", className: "leading-relaxed", size: "sm", children: c.body })] }) }, c.id))) }))] }));
|
|
55
57
|
}
|
|
56
58
|
function ScammerActionsColumn({ scammer, isAuthenticated, reportHref, contestLoginHref, }) {
|
|
57
|
-
return (_jsxs(Stack, { gap: "md", children: [_jsxs(Card, { variant: "elevated", padding: "md", children: [_jsx(CardHeader, { children: _jsx(Heading, { level: 3, className: "tracking-wide", size: "sm", weight: "semibold", transform: "uppercase", children: "Actions" }) }), _jsx(CardBody, { children: _jsxs(Stack, { gap: "sm", children: [isAuthenticated ? (_jsxs(Link, { href: reportHref, className: CLS_OUTLINE_BTN, children: [_jsx(AlertOctagon, { className: "h-4 w-4 text-[color:var(--appkit-color-danger,theme(colors.red.500))]" }), "Report another incident"] })) : (_jsxs(Link, { href: `${String(ROUTES.AUTH.LOGIN)}?redirect=${encodeURIComponent(reportHref)}`, className: CLS_OUTLINE_BTN, children: [_jsx(AlertOctagon, { className: "h-4 w-4 text-[color:var(--appkit-color-danger,theme(colors.red.500))]" }), "Sign in to report an incident"] })), isAuthenticated ? (_jsxs(Link, { href: `/scams/${scammer.id}/contest`, className: CLS_OUTLINE_BTN, children: [_jsx(Flag, { className: "h-4 w-4 text-[color:var(--appkit-color-warning,theme(colors.amber.500))]" }), "Contest this profile"] })) : (_jsxs(Link, { href: contestLoginHref, className: "appkit-button appkit-button--ghost appkit-button--md flex w-full items-center gap-[var(--appkit-space-2)]", children: [_jsx(Flag, { className: "h-4 w-4" }), "Sign in to contest"] })), _jsx(Text, { variant: "secondary", size: "xs", children: "All contest submissions are reviewed by our moderation team before any changes are made." })] }) })] }), _jsxs(Card, { variant: "outlined", padding: "md", children: [_jsx(CardHeader, { children: _jsx(Heading, { level: 3, className: "tracking-wide", size: "sm", weight: "semibold", transform: "uppercase", children: "Contest Options" }) }), _jsx(CardBody, { children: _jsx(Stack, { gap: "xs", as: "ul", children: Object.entries(CONTEST_TYPE_LABELS).map(([, label]) => (_jsxs(Row, { gap: "sm", align: "start", as: "li", children: [_jsx(Span, { className: "mt-1.5 h-1.5 w-1.5 shrink-0 bg-[color:var(--appkit-color-border,theme(colors.zinc.300))]", rounded: "full" }), _jsx(Text, { variant: "secondary", size: "xs", children: label })] }, label))) }) })] }), scammer.verifiedAt && (_jsx(Alert, { variant: "success", compact: true, children: _jsxs(Row, { gap: "xs", align: "center", children: [_jsx(Shield, { className: "h-3.5 w-3.5 shrink-0" }), _jsxs(Text, { size: "xs", weight: "medium", children: ["Verified by
|
|
59
|
+
return (_jsxs(Stack, { gap: "md", children: [_jsxs(Card, { variant: "elevated", padding: "md", children: [_jsx(CardHeader, { children: _jsx(Heading, { level: 3, className: "tracking-wide", size: "sm", weight: "semibold", transform: "uppercase", children: "Actions" }) }), _jsx(CardBody, { children: _jsxs(Stack, { gap: "sm", children: [isAuthenticated ? (_jsxs(Link, { href: reportHref, className: CLS_OUTLINE_BTN, children: [_jsx(AlertOctagon, { className: "h-4 w-4 text-[color:var(--appkit-color-danger,theme(colors.red.500))]" }), "Report another incident"] })) : (_jsxs(Link, { href: `${String(ROUTES.AUTH.LOGIN)}?redirect=${encodeURIComponent(reportHref)}`, className: CLS_OUTLINE_BTN, children: [_jsx(AlertOctagon, { className: "h-4 w-4 text-[color:var(--appkit-color-danger,theme(colors.red.500))]" }), "Sign in to report an incident"] })), isAuthenticated ? (_jsxs(Link, { href: `/scams/${scammer.id}/contest`, className: CLS_OUTLINE_BTN, children: [_jsx(Flag, { className: "h-4 w-4 text-[color:var(--appkit-color-warning,theme(colors.amber.500))]" }), "Contest this profile"] })) : (_jsxs(Link, { href: contestLoginHref, className: "appkit-button appkit-button--ghost appkit-button--md flex w-full items-center gap-[var(--appkit-space-2)]", children: [_jsx(Flag, { className: "h-4 w-4" }), "Sign in to contest"] })), _jsx(Text, { variant: "secondary", size: "xs", children: "All contest submissions are reviewed by our moderation team before any changes are made." })] }) })] }), _jsxs(Card, { variant: "outlined", padding: "md", children: [_jsx(CardHeader, { children: _jsx(Heading, { level: 3, className: "tracking-wide", size: "sm", weight: "semibold", transform: "uppercase", children: "Contest Options" }) }), _jsx(CardBody, { children: _jsx(Stack, { gap: "xs", as: "ul", children: Object.entries(CONTEST_TYPE_LABELS).map(([, label]) => (_jsxs(Row, { gap: "sm", align: "start", as: "li", children: [_jsx(Span, { className: "mt-1.5 h-1.5 w-1.5 shrink-0 bg-[color:var(--appkit-color-border,theme(colors.zinc.300))]", rounded: "full" }), _jsx(Text, { variant: "secondary", size: "xs", children: label })] }, label))) }) })] }), scammer.verifiedAt && (_jsx(Alert, { variant: "success", compact: true, children: _jsxs(Row, { gap: "xs", align: "center", children: [_jsx(Shield, { className: "h-3.5 w-3.5 shrink-0" }), _jsxs(Text, { size: "xs", weight: "medium", children: ["Verified by ", getBrandName(), " moderation on ", formatDate(scammer.verifiedAt)] })] }) }))] }));
|
|
58
60
|
}
|
|
59
61
|
export function ScamProfileView({ scammer, isAuthenticated, incidents = [], comments = [], relatedScammers = [], similarScamReports = [], }) {
|
|
60
62
|
const reportHref = String(ROUTES.PUBLIC.SCAM_REPORT);
|