@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.
Files changed (169) hide show
  1. package/PROVIDERS.md +190 -0
  2. package/dist/.build-stamp.json +3 -3
  3. package/dist/_internal/client/features/lottery/LotteryAdminSlotView.js +2 -1
  4. package/dist/_internal/server/features/checkout/actions.js +43 -8
  5. package/dist/_internal/server/features/orders/actions.js +2 -1
  6. package/dist/_internal/server/features/orders/adapters.js +18 -0
  7. package/dist/_internal/server/features/orders/balance.js +2 -1
  8. package/dist/_internal/server/features/orders/cancellation-refund.d.ts +0 -89
  9. package/dist/_internal/server/features/orders/cancellation-refund.js +3 -2
  10. package/dist/_internal/server/features/payouts/actions.js +2 -1
  11. package/dist/_internal/server/features/points/award.js +3 -2
  12. package/dist/_internal/server/features/protection-claims/actions.js +2 -1
  13. package/dist/_internal/server/features/refunds/actions.js +3 -2
  14. package/dist/_internal/server/features/shipping/courier-block.js +5 -2
  15. package/dist/_internal/server/features/shipping/reconcile.js +3 -2
  16. package/dist/_internal/server/features/shipping/reimbursement.js +3 -2
  17. package/dist/_internal/server/features/stores/adapters.d.ts +1 -1
  18. package/dist/_internal/server/features/stores/adapters.js +12 -2
  19. package/dist/_internal/server/jobs/core/dailyStatusDigest.js +7 -3
  20. package/dist/_internal/server/jobs/core/emiInstallmentReminder.js +2 -1
  21. package/dist/_internal/server/jobs/core/obligationSweep.js +5 -4
  22. package/dist/_internal/server/jobs/core/onScamReportCreate.js +2 -1
  23. package/dist/_internal/server/jobs/core/paymentReconcile.js +5 -4
  24. package/dist/_internal/shared/checkout/rules/auction.rule.js +25 -7
  25. package/dist/_internal/shared/checkout/rules/prize-draw.rule.js +26 -8
  26. package/dist/_internal/shared/config/schema.d.ts +15 -0
  27. package/dist/_internal/shared/content/published-values.d.ts +2 -2
  28. package/dist/_internal/shared/content/published-values.js +31 -12
  29. package/dist/_internal/shared/features/auctions/errors.js +2 -1
  30. package/dist/_internal/shared/features/orders/refund-copy.js +3 -2
  31. package/dist/_internal/shared/features/promotions/errors.js +2 -1
  32. package/dist/constants/field-names.d.ts +23 -0
  33. package/dist/constants/field-names.js +23 -0
  34. package/dist/constants/storage-keys.d.ts +90 -0
  35. package/dist/constants/storage-keys.js +119 -0
  36. package/dist/contracts/index.d.ts +1 -1
  37. package/dist/contracts/payment.d.ts +42 -0
  38. package/dist/contracts/registry.d.ts +8 -0
  39. package/dist/contracts/tax.d.ts +118 -0
  40. package/dist/contracts/tax.js +3 -0
  41. package/dist/core/brand-resolver.d.ts +92 -0
  42. package/dist/core/brand-resolver.js +120 -0
  43. package/dist/core/brand-text.d.ts +15 -0
  44. package/dist/core/brand-text.js +73 -0
  45. package/dist/core/hooks/useNewsletterSubscribe.d.ts +30 -0
  46. package/dist/core/hooks/useNewsletterSubscribe.js +121 -0
  47. package/dist/features/about/components/DeveloperView.js +2 -1
  48. package/dist/features/account/components/BuyerAccountGuideView.js +2 -1
  49. package/dist/features/account/components/BuyerAuctionsGuideView.js +4 -3
  50. package/dist/features/account/components/BuyerOrdersGuideView.js +6 -5
  51. package/dist/features/account/components/BuyerShoppingGuideView.js +3 -2
  52. package/dist/features/account/components/UserOffersPanel.js +2 -1
  53. package/dist/features/account/hooks/useCollapsedSections.js +5 -3
  54. package/dist/features/account/schemas/index.js +1 -1
  55. package/dist/features/admin/actions/notification-actions.js +9 -2
  56. package/dist/features/admin/components/AdminAnalyticsGuideView.js +2 -1
  57. package/dist/features/admin/components/AdminBundlesView.js +2 -1
  58. package/dist/features/admin/components/AdminCatalogGuideView.js +2 -1
  59. package/dist/features/admin/components/AdminContentGuideView.js +2 -1
  60. package/dist/features/admin/components/AdminFaqEditorView.js +2 -1
  61. package/dist/features/admin/components/AdminGatewayEventsView.js +2 -1
  62. package/dist/features/admin/components/AdminGuideHubView.js +2 -1
  63. package/dist/features/admin/components/AdminMoneyCasesView.js +2 -1
  64. package/dist/features/admin/components/AdminOrdersGuideView.js +2 -1
  65. package/dist/features/admin/components/AdminProductsView.js +3 -2
  66. package/dist/features/admin/components/AdminSectionsView.js +3 -2
  67. package/dist/features/admin/components/AdminSiteSettingsView.js +8 -7
  68. package/dist/features/admin/components/AdminStoresGuideView.js +2 -1
  69. package/dist/features/admin/components/AdminSupportTicketDetailView.js +2 -1
  70. package/dist/features/admin/components/AdminTeamGuideView.js +2 -1
  71. package/dist/features/admin/components/AdminTrustGuideView.js +3 -2
  72. package/dist/features/admin/components/AdminUsersGuideView.js +2 -1
  73. package/dist/features/admin/components/sections/adminSectionsTypes.js +1 -1
  74. package/dist/features/auth/actions/user-slug.js +21 -2
  75. package/dist/features/auth/hooks/useAuth.js +11 -4
  76. package/dist/features/cart/utils/pending-ops.js +16 -3
  77. package/dist/features/catalogue/utils/product-from-item.d.ts +18 -2
  78. package/dist/features/catalogue/utils/product-from-item.js +23 -4
  79. package/dist/features/categories/components/BundleItemsPicker.js +2 -1
  80. package/dist/features/categories/components/CategoryDetailPageView.js +2 -1
  81. package/dist/features/categories/components/ContextualTrail.d.ts +9 -2
  82. package/dist/features/categories/components/ContextualTrail.js +11 -3
  83. package/dist/features/categories/components/LastCategoryBeacon.js +2 -2
  84. package/dist/features/checkout/actions/checkout-value-otp-actions.js +9 -1
  85. package/dist/features/contact/email.js +11 -12
  86. package/dist/features/email/notification-templates.js +4 -5
  87. package/dist/features/email/render-static-markup.d.ts +34 -0
  88. package/dist/features/email/render-static-markup.js +4 -0
  89. package/dist/features/events/utils/event-charts.js +2 -1
  90. package/dist/features/homepage/components/AnnouncementBar.js +5 -3
  91. package/dist/features/homepage/components/WelcomeSection.d.ts +3 -1
  92. package/dist/features/homepage/components/WelcomeSection.js +2 -2
  93. package/dist/features/homepage/lib/section-renderer.d.ts +8 -0
  94. package/dist/features/homepage/lib/section-renderer.js +1 -1
  95. package/dist/features/layout/AppLayoutShell.d.ts +3 -1
  96. package/dist/features/layout/AppLayoutShell.js +2 -2
  97. package/dist/features/layout/TitleBarLayout.d.ts +8 -1
  98. package/dist/features/layout/TitleBarLayout.js +2 -2
  99. package/dist/features/media/MediaVideo.js +2 -1
  100. package/dist/features/orders/actions/refund-actions.js +3 -2
  101. package/dist/features/orders/components/RefundRequestView.js +15 -2
  102. package/dist/features/orders/repository/orders.repository.js +2 -1
  103. package/dist/features/orders/types/index.d.ts +14 -0
  104. package/dist/features/orders/utils/payment-proof-message.js +5 -2
  105. package/dist/features/price-index/components/PriceIndexToolView.js +6 -5
  106. package/dist/features/pricing/components/AdminPricingView.js +2 -1
  107. package/dist/features/print/components/labels.js +2 -1
  108. package/dist/features/products/components/CityCollectiblesView.js +2 -1
  109. package/dist/features/products/components/MakeOfferButton.js +2 -1
  110. package/dist/features/products/components/PrizeDrawCollage.js +3 -2
  111. package/dist/features/products/components/PrizeRevealModal.js +2 -1
  112. package/dist/features/products/repository/products.repository.js +2 -1
  113. package/dist/features/promotions/components/CouponCard.js +3 -2
  114. package/dist/features/promotions/components/CouponHelpDetails.js +2 -1
  115. package/dist/features/promotions/constants/coupon-help.d.ts +1 -1
  116. package/dist/features/promotions/constants/coupon-help.js +8 -1
  117. package/dist/features/scams/components/ScamAwarenessModal.js +2 -1
  118. package/dist/features/scams/components/ScamProfileView.js +5 -3
  119. package/dist/features/scams/components/ScamRegistryView.js +2 -1
  120. package/dist/features/scams/constants/scam-types.d.ts +13 -1
  121. package/dist/features/scams/constants/scam-types.js +67 -23
  122. package/dist/features/scams/schemas/firestore.js +1 -1
  123. package/dist/features/search/components/Search.js +5 -1
  124. package/dist/features/seller/actions/offer-actions.js +47 -9
  125. package/dist/features/seller/components/DispatchForm.js +3 -2
  126. package/dist/features/seller/components/PrintCenterView.js +4 -2
  127. package/dist/features/seller/components/SellerBundlesView.js +2 -1
  128. package/dist/features/seller/components/SellerPayoutSettingsView.js +3 -2
  129. package/dist/features/seller/components/SellerProductShell.js +3 -2
  130. package/dist/features/seller/components/SellerProductsView.js +4 -3
  131. package/dist/features/store-extensions/repository/store-extensions.repositories.d.ts +30 -4
  132. package/dist/features/store-extensions/repository/store-extensions.repositories.js +46 -7
  133. package/dist/features/store-extensions/schemas/firestore.d.ts +22 -3
  134. package/dist/features/store-extensions/schemas/firestore.js +20 -0
  135. package/dist/features/stores/components/StoreCapabilitiesGuideView.js +7 -6
  136. package/dist/features/stores/components/StoreFinanceGuideView.js +6 -5
  137. package/dist/features/stores/components/StoreGuideHubView.js +2 -1
  138. package/dist/features/stores/components/StoreListingsGuideView.js +2 -1
  139. package/dist/features/stores/components/StoreOrdersGuideView.js +5 -4
  140. package/dist/features/stores/components/StoreSettingsGuideView.js +2 -1
  141. package/dist/features/support/schemas/index.d.ts +2 -2
  142. package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.d.ts +1 -1
  143. package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.js +3 -2
  144. package/dist/features/whatsapp-bot/helpers/whatsapp.js +5 -2
  145. package/dist/index.d.ts +0 -1
  146. package/dist/index.js +0 -2
  147. package/dist/providers/payment-manual/index.d.ts +11 -0
  148. package/dist/providers/payment-manual/index.js +13 -0
  149. package/dist/providers/payment-phonepe/index.d.ts +23 -7
  150. package/dist/providers/payment-phonepe/index.js +15 -0
  151. package/dist/providers/payment-razorpay/index.d.ts +13 -0
  152. package/dist/providers/payment-razorpay/index.js +21 -1
  153. package/dist/providers/tax-india-gst/index.d.ts +45 -0
  154. package/dist/providers/tax-india-gst/index.js +78 -0
  155. package/dist/react/hooks/useScrollToTop.d.ts +5 -0
  156. package/dist/react/hooks/useScrollToTop.js +70 -0
  157. package/dist/schemas/registry.d.ts +2 -2
  158. package/dist/seo/json-ld.js +25 -4
  159. package/dist/ui/components/SiteLogo.d.ts +20 -2
  160. package/dist/ui/components/SiteLogo.js +28 -6
  161. package/dist/ui/components/SiteMark.d.ts +1 -1
  162. package/dist/ui/components/SiteMark.js +21 -6
  163. package/dist/utils/date.formatter.d.ts +20 -0
  164. package/dist/utils/date.formatter.js +25 -0
  165. package/dist/utils/number.formatter.d.ts +47 -0
  166. package/dist/utils/number.formatter.js +79 -1
  167. package/dist/validation/phone.validator.d.ts +0 -1
  168. package/dist/validation/phone.validator.js +17 -3
  169. 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 ?? "letitrip.in" }) }));
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 ₹${netRefund} (original ₹${grossRefund} minus ₹${feeDeducted} gateway fee) is being processed.`
79
- : `Your full refund of ₹${grossRefund} is being processed.`,
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
- const PUBLISHED_VALUES = resolvePublishedValues();
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, PUBLISHED_VALUES) }), "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 }))] })),
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 ₹${event.amount.toFixed(2)} exceeds the ₹${remaining.toFixed(2)} still refundable on this order.`);
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
- const amount = totalAmount.toLocaleString("en-IN");
16
- return `💳 Payment proof submitted! ${buyerName} uploaded proof for "${productTitle}" (₹${amount}). Order #${orderId}. Please review within 2 hours or it auto-confirms: ${reviewUrl}`;
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 LetItRip", row.windowDays ? ` over the last ${row.windowDays} days` : "", row.computedAt ? ` · as of ${new Date(row.computedAt).toLocaleDateString("en-IN")}` : ""] })] })) : (
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
- ? "Nothing has sold here on LetItRip yet, so the figures above are asking prices elsewhere rather than our own sales."
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
- : "No completed sales recorded here yet. Prices appear once something sells on LetItRip." }))] }));
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" })] }), _jsx(Text, { color: "muted", children: "Pick any bey, part or set to see what it actually sold for on LetItRip \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
+ 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 LetItRip \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
+ _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 `₹${value.toLocaleString("en-IN")}`;
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 && (_jsxs(Text, { size: "xs", color: "muted", children: ["\u20B9", product.price.toLocaleString("en-IN")] })), product.boxCode && (_jsx(Text, { size: "xs", weight: "semibold", children: product.boxCode }))] })] }));
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 LetItRip 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}` }), _jsx(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 LetItRip'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
+ _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) : `₹${n.toLocaleString()}`);
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. ₹${it.estimatedValue.toLocaleString("en-IN")}`
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. \u20B9", it.estimatedValue.toLocaleString("en-IN")] }) })) : null] })] }, `collage-${it.itemNumber}`));
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 LetItRip's server the moment your prize is assigned. Neither the store nor LetItRip staff can influence the outcome.", " ", rngSourceUrl ? (_jsx(Anchor, { href: rngSourceUrl, tone: "none", underline: "always", children: "View RNG source code \u2192" })) : null] })] }) }));
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: "LetItRip",
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 `₹${n.discountValue.toFixed(0)} ${labels.off}`;
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, ": \u20B9", n.minOrderAmount.toFixed(0)] })) : 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
+ 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 LetItRip 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."];
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 LetItRip coupon, on top of any store coupons.",
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))]" }), _jsx(Span, { children: "Before you start \u2014 Stay Safe on LetItRip" })] }), 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 LetItRip →" }) }), children: _jsxs(Stack, { gap: "md", children: [_jsx(Text, { variant: "secondary", size: "sm", children: "LetItRip 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
+ 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 LetItRip moderation on ", formatDate(scammer.verifiedAt)] })] }) }))] }));
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);