@mohasinac/appkit 4.33.0 → 4.34.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 (89) hide show
  1. package/dist/_internal/server/features/checkout/actions.js +9 -1
  2. package/dist/_internal/server/features/checkout/expand-order-items.js +9 -0
  3. package/dist/_internal/server/features/media/contextGuards.d.ts +3 -3
  4. package/dist/_internal/server/features/media/contextGuards.js +19 -5
  5. package/dist/_internal/server/features/orders/actions.d.ts +9 -0
  6. package/dist/_internal/server/features/orders/actions.js +47 -4
  7. package/dist/_internal/server/features/orders/adapters.js +10 -0
  8. package/dist/_internal/server/features/refunds/actions.d.ts +9 -1
  9. package/dist/_internal/server/features/refunds/actions.js +36 -3
  10. package/dist/_internal/server/features/site-settings/adapters.js +1 -1
  11. package/dist/_internal/shared/features/orders/refund-copy.d.ts +12 -0
  12. package/dist/_internal/shared/features/orders/refund-copy.js +12 -0
  13. package/dist/_internal/shared/features/orders/return-reasons.d.ts +59 -0
  14. package/dist/_internal/shared/features/orders/return-reasons.js +96 -0
  15. package/dist/_internal/shared/features/orders/schema.d.ts +30 -0
  16. package/dist/_internal/shared/features/orders/schema.js +20 -0
  17. package/dist/_internal/shared/features/products/schema.d.ts +12 -0
  18. package/dist/_internal/shared/features/products/schema.js +4 -1
  19. package/dist/_internal/shared/media/limits.d.ts +14 -0
  20. package/dist/_internal/shared/media/limits.js +29 -0
  21. package/dist/client.d.ts +9 -1
  22. package/dist/client.js +19 -1
  23. package/dist/features/about/components/PolicyPageView.js +1 -1
  24. package/dist/features/admin/components/AdminProductEditorView.js +6 -1
  25. package/dist/features/admin/components/AdminSiteSettingsView.js +2 -6
  26. package/dist/features/admin/components/index.d.ts +0 -2
  27. package/dist/features/admin/components/index.js +0 -1
  28. package/dist/features/admin/schemas/admin-editor-forms.js +1 -1
  29. package/dist/features/admin/schemas/firestore.d.ts +0 -2
  30. package/dist/features/auctions/components/AuctionDetailPageView.js +4 -3
  31. package/dist/features/contact/components/ContactForm.js +46 -31
  32. package/dist/features/media/upload/MediaUploadList.d.ts +16 -1
  33. package/dist/features/media/upload/MediaUploadList.js +58 -8
  34. package/dist/features/orders/components/RefundRequestView.d.ts +26 -10
  35. package/dist/features/orders/components/RefundRequestView.js +108 -24
  36. package/dist/features/orders/schemas/firestore.d.ts +42 -0
  37. package/dist/features/orders/types/index.d.ts +24 -0
  38. package/dist/features/pre-orders/components/PreOrderDetailPageView.js +5 -3
  39. package/dist/features/products/components/ProductDetailPageView.js +14 -2
  40. package/dist/features/products/components/ProductFeatureBadges.d.ts +9 -1
  41. package/dist/features/products/components/ProductFeatureBadges.js +17 -1
  42. package/dist/features/products/components/ProductForm.js +46 -10
  43. package/dist/features/products/components/ProductGrid.js +10 -1
  44. package/dist/features/products/constants/final-sale.d.ts +40 -0
  45. package/dist/features/products/constants/final-sale.js +40 -0
  46. package/dist/features/products/index.d.ts +1 -0
  47. package/dist/features/products/index.js +1 -0
  48. package/dist/features/products/schemas/firestore.d.ts +19 -2
  49. package/dist/features/products/schemas/firestore.js +4 -0
  50. package/dist/features/products/types/index.d.ts +2 -0
  51. package/dist/features/seller/components/SellerProductShell.d.ts +7 -0
  52. package/dist/features/seller/components/SellerProductShell.js +82 -11
  53. package/dist/features/seller/index.d.ts +1 -0
  54. package/dist/features/seller/index.js +1 -0
  55. package/dist/features/seller/schemas/product-form.d.ts +304 -0
  56. package/dist/features/seller/schemas/product-form.js +132 -0
  57. package/dist/features/seller/utils/product-draft-mapping.d.ts +117 -0
  58. package/dist/features/seller/utils/product-draft-mapping.js +147 -0
  59. package/dist/features/shell/SectionForm.js +62 -7
  60. package/dist/features/shell/form-sections.js +4 -0
  61. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +200 -0
  62. package/dist/index.d.ts +6 -3
  63. package/dist/index.js +12 -3
  64. package/dist/next/routing/route-map.d.ts +2 -0
  65. package/dist/next/routing/route-map.js +1 -0
  66. package/dist/schemas/registry.d.ts +271 -0
  67. package/dist/schemas/registry.js +9 -0
  68. package/dist/seed/_helpers/final-sale-fixtures.d.ts +35 -0
  69. package/dist/seed/_helpers/final-sale-fixtures.js +61 -0
  70. package/dist/seed/products-art-seed-data.js +9 -1
  71. package/dist/seed/products-auctions-seed-data.js +2 -1
  72. package/dist/seed/products-classifieds-seed-data.js +2 -1
  73. package/dist/seed/products-digital-codes-seed-data.js +2 -1
  74. package/dist/seed/products-live-items-seed-data.js +2 -1
  75. package/dist/seed/products-preorders-seed-data.d.ts +2 -138
  76. package/dist/seed/products-preorders-seed-data.js +2 -1
  77. package/dist/seed/products-prize-draws-seed-data.js +2 -1
  78. package/dist/seed/products-standard-seed-data.js +2 -1
  79. package/dist/seed/products-stickers-seed-data.js +9 -1
  80. package/dist/server.d.ts +6 -2
  81. package/dist/server.js +15 -2
  82. package/dist/styles.css +1 -1
  83. package/dist/tailwind-utilities.css +1 -1
  84. package/dist/tokens/motion.d.ts +12 -0
  85. package/dist/tokens/motion.js +12 -0
  86. package/dist/ui/components/CollapsibleSection.d.ts +18 -1
  87. package/dist/ui/components/CollapsibleSection.js +20 -3
  88. package/dist/ui/components/Motion.js +17 -1
  89. package/package.json +1 -1
@@ -17,6 +17,7 @@ export declare const productInputSchema: z.ZodObject<{
17
17
  seoKeywords: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
18
18
  shippingInfo: z.ZodOptional<z.ZodString>;
19
19
  returnPolicy: z.ZodOptional<z.ZodString>;
20
+ finalSale: z.ZodOptional<z.ZodBoolean>;
20
21
  shippingPaidBy: z.ZodOptional<z.ZodEnum<["seller", "buyer"]>>;
21
22
  insurance: z.ZodOptional<z.ZodBoolean>;
22
23
  insuranceCost: z.ZodOptional<z.ZodNumber>;
@@ -45,6 +46,7 @@ export declare const productInputSchema: z.ZodObject<{
45
46
  seoTitle?: string | undefined;
46
47
  seoDescription?: string | undefined;
47
48
  seoKeywords?: string[] | undefined;
49
+ finalSale?: boolean | undefined;
48
50
  allowOffers?: boolean | undefined;
49
51
  minOfferPercent?: number | undefined;
50
52
  }, {
@@ -69,6 +71,7 @@ export declare const productInputSchema: z.ZodObject<{
69
71
  seoTitle?: string | undefined;
70
72
  seoDescription?: string | undefined;
71
73
  seoKeywords?: string[] | undefined;
74
+ finalSale?: boolean | undefined;
72
75
  allowOffers?: boolean | undefined;
73
76
  minOfferPercent?: number | undefined;
74
77
  }>;
@@ -90,6 +93,7 @@ export declare const productUpdateSchema: z.ZodObject<{
90
93
  seoKeywords: z.ZodOptional<z.ZodOptional<z.ZodArray<z.ZodString, "many">>>;
91
94
  shippingInfo: z.ZodOptional<z.ZodOptional<z.ZodString>>;
92
95
  returnPolicy: z.ZodOptional<z.ZodOptional<z.ZodString>>;
96
+ finalSale: z.ZodOptional<z.ZodOptional<z.ZodBoolean>>;
93
97
  shippingPaidBy: z.ZodOptional<z.ZodOptional<z.ZodEnum<["seller", "buyer"]>>>;
94
98
  insurance: z.ZodOptional<z.ZodOptional<z.ZodBoolean>>;
95
99
  insuranceCost: z.ZodOptional<z.ZodOptional<z.ZodNumber>>;
@@ -118,6 +122,7 @@ export declare const productUpdateSchema: z.ZodObject<{
118
122
  seoTitle?: string | undefined;
119
123
  seoDescription?: string | undefined;
120
124
  seoKeywords?: string[] | undefined;
125
+ finalSale?: boolean | undefined;
121
126
  allowOffers?: boolean | undefined;
122
127
  minOfferPercent?: number | undefined;
123
128
  }, {
@@ -142,6 +147,7 @@ export declare const productUpdateSchema: z.ZodObject<{
142
147
  seoTitle?: string | undefined;
143
148
  seoDescription?: string | undefined;
144
149
  seoKeywords?: string[] | undefined;
150
+ finalSale?: boolean | undefined;
145
151
  allowOffers?: boolean | undefined;
146
152
  minOfferPercent?: number | undefined;
147
153
  }>;
@@ -163,6 +169,7 @@ export declare const auctionInputSchema: z.ZodObject<{
163
169
  seoKeywords: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
164
170
  shippingInfo: z.ZodOptional<z.ZodString>;
165
171
  returnPolicy: z.ZodOptional<z.ZodString>;
172
+ finalSale: z.ZodOptional<z.ZodBoolean>;
166
173
  shippingPaidBy: z.ZodOptional<z.ZodEnum<["seller", "buyer"]>>;
167
174
  insurance: z.ZodOptional<z.ZodBoolean>;
168
175
  insuranceCost: z.ZodOptional<z.ZodNumber>;
@@ -210,6 +217,7 @@ export declare const auctionInputSchema: z.ZodObject<{
210
217
  seoTitle?: string | undefined;
211
218
  seoDescription?: string | undefined;
212
219
  seoKeywords?: string[] | undefined;
220
+ finalSale?: boolean | undefined;
213
221
  allowOffers?: boolean | undefined;
214
222
  minOfferPercent?: number | undefined;
215
223
  }, {
@@ -243,6 +251,7 @@ export declare const auctionInputSchema: z.ZodObject<{
243
251
  seoTitle?: string | undefined;
244
252
  seoDescription?: string | undefined;
245
253
  seoKeywords?: string[] | undefined;
254
+ finalSale?: boolean | undefined;
246
255
  allowOffers?: boolean | undefined;
247
256
  minOfferPercent?: number | undefined;
248
257
  }>;
@@ -264,6 +273,7 @@ export declare const preOrderInputSchema: z.ZodObject<{
264
273
  seoKeywords: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
265
274
  shippingInfo: z.ZodOptional<z.ZodString>;
266
275
  returnPolicy: z.ZodOptional<z.ZodString>;
276
+ finalSale: z.ZodOptional<z.ZodBoolean>;
267
277
  shippingPaidBy: z.ZodOptional<z.ZodEnum<["seller", "buyer"]>>;
268
278
  insurance: z.ZodOptional<z.ZodBoolean>;
269
279
  insuranceCost: z.ZodOptional<z.ZodNumber>;
@@ -305,6 +315,7 @@ export declare const preOrderInputSchema: z.ZodObject<{
305
315
  seoTitle?: string | undefined;
306
316
  seoDescription?: string | undefined;
307
317
  seoKeywords?: string[] | undefined;
318
+ finalSale?: boolean | undefined;
308
319
  allowOffers?: boolean | undefined;
309
320
  minOfferPercent?: number | undefined;
310
321
  }, {
@@ -335,6 +346,7 @@ export declare const preOrderInputSchema: z.ZodObject<{
335
346
  seoTitle?: string | undefined;
336
347
  seoDescription?: string | undefined;
337
348
  seoKeywords?: string[] | undefined;
349
+ finalSale?: boolean | undefined;
338
350
  allowOffers?: boolean | undefined;
339
351
  minOfferPercent?: number | undefined;
340
352
  }>;
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { mediaUrlSchema } from "../../../../validation/schemas";
3
+ import { PRODUCT_MAX_IMAGES } from "../../media/limits";
3
4
  const priceSchema = z.number().min(0, "Price must be non-negative");
4
5
  const conditionSchema = z.enum(["new", "like_new", "good", "fair", "poor", "used", "refurbished", "broken"]);
5
6
  export const productInputSchema = z.object({
@@ -12,7 +13,7 @@ export const productInputSchema = z.object({
12
13
  stockQuantity: z.number().int().min(0).default(0),
13
14
  availableQuantity: z.number().int().min(0).default(0),
14
15
  mainImage: mediaUrlSchema,
15
- images: z.array(mediaUrlSchema).max(20).default([]),
16
+ images: z.array(mediaUrlSchema).max(PRODUCT_MAX_IMAGES).default([]),
16
17
  condition: conditionSchema.optional(),
17
18
  tags: z.array(z.string()).max(20).default([]),
18
19
  seoTitle: z.string().max(70).optional(),
@@ -20,6 +21,8 @@ export const productInputSchema = z.object({
20
21
  seoKeywords: z.array(z.string()).max(10).optional(),
21
22
  shippingInfo: z.string().max(1000).optional(),
22
23
  returnPolicy: z.string().max(1000).optional(),
24
+ // Absent means final sale. No `.default()` — see request-schemas.ts.
25
+ finalSale: z.boolean().optional(),
23
26
  shippingPaidBy: z.enum(["seller", "buyer"]).optional(),
24
27
  insurance: z.boolean().optional(),
25
28
  insuranceCost: z.number().min(0).optional(),
@@ -13,6 +13,20 @@ export declare const MAX_IMAGE_BYTES: number;
13
13
  export declare const MAX_PDF_BYTES: number;
14
14
  export declare const MAX_VIDEO_BYTES: number;
15
15
  export type MediaKind = "image" | "video" | "pdf";
16
+ /** Gallery images per listing. */
17
+ export declare const PRODUCT_MAX_IMAGES = 10;
18
+ /** Videos per listing. Bounded by `ProductDocument.video` being singular. */
19
+ export declare const PRODUCT_MAX_VIDEOS = 1;
20
+ /**
21
+ * Highest legal `index` in a product media filename.
22
+ *
23
+ * NOT the same number as `PRODUCT_MAX_IMAGES`, and conflating them is a live
24
+ * bug: `generateMediaFilename` gives the main image index 1 and the gallery
25
+ * 2..N, so the index space is one larger than the gallery count. The server
26
+ * guard checks the INDEX, which is why a gallery capped at 5 was rejecting its
27
+ * own 5th image with a 400 (it arrived as index 6).
28
+ */
29
+ export declare const PRODUCT_IMAGE_INDEX_MAX: number;
16
30
  export declare const MAX_LABEL: Record<MediaKind, string>;
17
31
  export declare const MAX_BYTES: Record<MediaKind, number>;
18
32
  export declare const ALLOWED_IMAGE_MIMES: readonly ["image/jpeg", "image/jpg", "image/png", "image/gif", "image/webp"];
@@ -12,6 +12,35 @@ export const MEGABYTE = 1024 * 1024;
12
12
  export const MAX_IMAGE_BYTES = 10 * MEGABYTE;
13
13
  export const MAX_PDF_BYTES = 20 * MEGABYTE;
14
14
  export const MAX_VIDEO_BYTES = 50 * MEGABYTE;
15
+ // ---------------------------------------------------------------------------
16
+ // Per-listing media COUNTS.
17
+ //
18
+ // Byte ceilings are above; these are how MANY files one listing may carry.
19
+ // Deliberately FLAT — every listing type gets the same allowance. There is no
20
+ // per-type table and one should not be added: the previous state had six
21
+ // disagreeing numbers (5 in the request schema, 5 in the seller form, 5 in the
22
+ // admin form, 5/5/5 in the server context guards, 12 in the MediaUploadList
23
+ // default, 20 in the internal product schema) and no two of them were reached
24
+ // by the same code path, so which cap a seller actually hit depended on which
25
+ // form they had opened.
26
+ //
27
+ // The video cap is 1 because `ProductDocument.video` is a single field, not an
28
+ // array — raising it is a schema change, not a constant change.
29
+ // ---------------------------------------------------------------------------
30
+ /** Gallery images per listing. */
31
+ export const PRODUCT_MAX_IMAGES = 10;
32
+ /** Videos per listing. Bounded by `ProductDocument.video` being singular. */
33
+ export const PRODUCT_MAX_VIDEOS = 1;
34
+ /**
35
+ * Highest legal `index` in a product media filename.
36
+ *
37
+ * NOT the same number as `PRODUCT_MAX_IMAGES`, and conflating them is a live
38
+ * bug: `generateMediaFilename` gives the main image index 1 and the gallery
39
+ * 2..N, so the index space is one larger than the gallery count. The server
40
+ * guard checks the INDEX, which is why a gallery capped at 5 was rejecting its
41
+ * own 5th image with a 400 (it arrived as index 6).
42
+ */
43
+ export const PRODUCT_IMAGE_INDEX_MAX = PRODUCT_MAX_IMAGES + 1;
15
44
  export const MAX_LABEL = {
16
45
  image: "10MB",
17
46
  video: "50MB",
package/dist/client.d.ts CHANGED
@@ -383,6 +383,7 @@ export type { GuestHistoryItem, GuestHistoryType, UserHistoryItem, HistoryProduc
383
383
  export type { TrackArgs as TrackHistoryArgs } from "./features/history/hooks/useHistory";
384
384
  export { WISHLIST_MAX, HISTORY_MAX, CART_MAX_ITEMS, } from "./constants/limits";
385
385
  export { normalizeListingType, isAuctionListing, isPreOrderListing, isStandardListing, isPrizeDrawListing, isClassifiedListing, isDigitalCodeListing, isLiveListing, isArtListing, isStickersListing, } from "./features/products/utils/listing-type";
386
+ export { isFinalSale, acceptsChangeOfMindReturns, } from "./features/products/constants/final-sale";
386
387
  export { isListingTypeEnabled, isCategoryTypeEnabled, enabledListingTypes, enabledCategoryTypes, ALL_LISTING_TYPES, } from "./_internal/shared/listing-types/feature-flags";
387
388
  export { actionTracker, setActionTrackerSink, resetActionTrackerSink, type ActionEvent, type ActionTrackerSink, } from "./_internal/shared/listing-types/action-tracker";
388
389
  export { cartRequiresShipping, cartIsDigitalOnly, cartIsChatOnly, } from "./_internal/shared/listing-types/cart-shipping";
@@ -397,7 +398,7 @@ export type { ListingTypeCapability } from "./_internal/shared/listing-types/cap
397
398
  export { LISTING_TYPE_REGISTRY, pluginFor, detectListingTypeFromSlug } from "./_internal/shared/listing-types/_registry";
398
399
  export type { ListingTypePlugin } from "./_internal/shared/listing-types/_registry";
399
400
  export type { ListingType } from "./features/products/types/index";
400
- export { MEGABYTE, MAX_IMAGE_BYTES, MAX_PDF_BYTES, MAX_VIDEO_BYTES, MAX_LABEL, MAX_BYTES, ALLOWED_IMAGE_MIMES, ALLOWED_VIDEO_MIMES, ALLOWED_DOC_MIMES, ALLOWED_MIMES, ALLOWED_TYPES_LABEL, MIME_TO_EXT, PDF_MAGIC, VIDEO_CONVERSION_HINTS, classifyMime, isAllowedMime, maxBytesFor, getConversionHint, } from "./_internal/shared/media/limits";
401
+ export { MEGABYTE, MAX_IMAGE_BYTES, MAX_PDF_BYTES, MAX_VIDEO_BYTES, MAX_LABEL, MAX_BYTES, ALLOWED_IMAGE_MIMES, ALLOWED_VIDEO_MIMES, ALLOWED_DOC_MIMES, ALLOWED_MIMES, ALLOWED_TYPES_LABEL, MIME_TO_EXT, PDF_MAGIC, VIDEO_CONVERSION_HINTS, classifyMime, isAllowedMime, maxBytesFor, getConversionHint, PRODUCT_MAX_IMAGES, PRODUCT_MAX_VIDEOS, PRODUCT_IMAGE_INDEX_MAX, } from "./_internal/shared/media/limits";
401
402
  export type { MediaKind, AllowedImageMime, AllowedVideoMime, AllowedDocMime, AllowedMime, } from "./_internal/shared/media/limits";
402
403
  export { ScamAwarenessModal } from "./features/scams/components/ScamAwarenessModal";
403
404
  export type { ScamAwarenessModalProps } from "./features/scams/components/ScamAwarenessModal";
@@ -514,3 +515,10 @@ export { AdminActionIndexView } from "./features/admin/components/AdminActionInd
514
515
  export type { AdminActionIndexViewProps } from "./features/admin/components/AdminActionIndexView";
515
516
  export { SellerSupportView } from "./features/seller/components/SellerSupportView";
516
517
  export type { SellerSupportViewProps } from "./features/seller/components/SellerSupportView";
518
+ export { REFUND_COPY } from "./_internal/shared/features/orders/refund-copy";
519
+ export { RETURN_REASON, FINAL_SALE_EXEMPT_REASONS, RETURN_REASON_LABEL, RETURN_REASON_GROUP_LABEL, isFinalSaleExempt, isReturnReason, selectableReturnReasons, } from "./_internal/shared/features/orders/return-reasons";
520
+ export type { ReturnReason } from "./_internal/shared/features/orders/return-reasons";
521
+ export { RefundRequestView } from "./features/orders/components/RefundRequestView";
522
+ export type { RefundRequestViewProps, RefundRequestSubmission, } from "./features/orders/components/RefundRequestView";
523
+ export { ORDER_RETURN_WINDOW_DAYS, ORDER_RETURN_WINDOW_MS, ORDER_CANCELLABLE_STATUSES, } from "./_internal/shared/features/orders/config";
524
+ export { draftToProductInput, productToDraft, } from "./features/seller/utils/product-draft-mapping";
package/dist/client.js CHANGED
@@ -390,6 +390,9 @@ export { WISHLIST_MAX, HISTORY_MAX, CART_MAX_ITEMS, } from "./constants/limits";
390
390
  // SB1-G canonical listing-type accessors (pure functions, client-safe).
391
391
  // SB-UNI-F 2026-05-13 — Phase 2 predicates surfaced through client barrel.
392
392
  export { normalizeListingType, isAuctionListing, isPreOrderListing, isStandardListing, isPrizeDrawListing, isClassifiedListing, isDigitalCodeListing, isLiveListing, isArtListing, isStickersListing, } from "./features/products/utils/listing-type";
393
+ // Final sale — ABSENT MEANS TRUE, so these are the only legal reads of the
394
+ // `finalSale` field. See the module docstring for why `!p.finalSale` is wrong.
395
+ export { isFinalSale, acceptsChangeOfMindReturns, } from "./features/products/constants/final-sale";
393
396
  // SB-UNI-X4 2026-05-13 — per-type feature-flag helpers (client-safe).
394
397
  export { isListingTypeEnabled, isCategoryTypeEnabled, enabledListingTypes, enabledCategoryTypes, ALL_LISTING_TYPES, } from "./_internal/shared/listing-types/feature-flags";
395
398
  // SB-UNI-X5 2026-05-13 — action telemetry sink (client-safe; defaults to
@@ -409,7 +412,7 @@ export { DashboardLayoutClient, RoleGuard } from "./_internal/client/features/la
409
412
  export { LISTING_TYPE_CAPABILITIES, capabilityFor, canAddToCart, canBid, supportsShipping, requiresVendorVerified, requiresJurisdictionCheck, hasInstantFulfillment, assertNever, } from "./_internal/shared/listing-types/capabilities";
410
413
  export { LISTING_TYPE_REGISTRY, pluginFor, detectListingTypeFromSlug } from "./_internal/shared/listing-types/_registry";
411
414
  // Media upload limits — shared by client uploaders + server sign/finalize routes.
412
- export { MEGABYTE, MAX_IMAGE_BYTES, MAX_PDF_BYTES, MAX_VIDEO_BYTES, MAX_LABEL, MAX_BYTES, ALLOWED_IMAGE_MIMES, ALLOWED_VIDEO_MIMES, ALLOWED_DOC_MIMES, ALLOWED_MIMES, ALLOWED_TYPES_LABEL, MIME_TO_EXT, PDF_MAGIC, VIDEO_CONVERSION_HINTS, classifyMime, isAllowedMime, maxBytesFor, getConversionHint, } from "./_internal/shared/media/limits";
415
+ export { MEGABYTE, MAX_IMAGE_BYTES, MAX_PDF_BYTES, MAX_VIDEO_BYTES, MAX_LABEL, MAX_BYTES, ALLOWED_IMAGE_MIMES, ALLOWED_VIDEO_MIMES, ALLOWED_DOC_MIMES, ALLOWED_MIMES, ALLOWED_TYPES_LABEL, MIME_TO_EXT, PDF_MAGIC, VIDEO_CONVERSION_HINTS, classifyMime, isAllowedMime, maxBytesFor, getConversionHint, PRODUCT_MAX_IMAGES, PRODUCT_MAX_VIDEOS, PRODUCT_IMAGE_INDEX_MAX, } from "./_internal/shared/media/limits";
413
416
  export { ScamAwarenessModal } from "./features/scams/components/ScamAwarenessModal";
414
417
  export { SellerTrustBadge } from "./features/scams/components/SellerTrustBadge";
415
418
  // [CLIENT-ONLY] — Admin panel components for action/nav permission management.
@@ -511,3 +514,18 @@ export { ROLES_TABS, isRolesTabId, ADMIN_SETTINGS_TABS, isAdminSettingsTabId, }
511
514
  export { validateActionIndexEntry, normaliseRoute, } from "./features/search/action-index/validate-entry";
512
515
  export { AdminActionIndexView } from "./features/admin/components/AdminActionIndexView";
513
516
  export { SellerSupportView } from "./features/seller/components/SellerSupportView";
517
+ export { REFUND_COPY } from "./_internal/shared/features/orders/refund-copy";
518
+ // Return / refund reasons — the closed enum the final-sale gate branches on.
519
+ // See the module docstring for why the exempt set is "seller or carrier fault"
520
+ // rather than "serious reasons".
521
+ export { RETURN_REASON, FINAL_SALE_EXEMPT_REASONS, RETURN_REASON_LABEL, RETURN_REASON_GROUP_LABEL, isFinalSaleExempt, isReturnReason, selectableReturnReasons, } from "./_internal/shared/features/orders/return-reasons";
522
+ // Buyer-facing return/refund request form and the window it is gated on.
523
+ // Exported from the CLIENT entry because the page that renders it is a
524
+ // "use client" route — importing it from the main entry there would drag the
525
+ // server graph into the browser bundle (Root Cause #6).
526
+ export { RefundRequestView } from "./features/orders/components/RefundRequestView";
527
+ export { ORDER_RETURN_WINDOW_DAYS, ORDER_RETURN_WINDOW_MS, ORDER_CANCELLABLE_STATUSES, } from "./_internal/shared/features/orders/config";
528
+ // Seller listing form draft <-> product write payload. The flat form keys and
529
+ // the nested document blocks are different shapes, and nothing translated
530
+ // between them — see the module docstring.
531
+ export { draftToProductInput, productToDraft, } from "./features/seller/utils/product-draft-mapping";
@@ -93,5 +93,5 @@ export async function PolicyPageView({ policy, }) {
93
93
  }));
94
94
  return (_jsxs(Div, { className: "-mx-4 md:-mx-6 lg:-mx-8 -mt-6 sm:-mt-8 lg:-mt-10", children: [_jsx(Section, { color: "inverse", tone: "accent-banner", padding: "banner", children: _jsxs(Div, { className: `${page.container.sm}`, children: [_jsx(Heading, { color: "inverse", level: 1, variant: "none", className: "mb-3", children: t("title") }), _jsx(Text, { color: "inverse", variant: "none", className: "/80", size: "sm", children: t("lastUpdated") })] }) }), _jsxs(Div, { className: `${page.container.sm}`, padding: "content-banner", children: [adminHtml ? (
95
95
  /* Admin-set HTML takes priority */
96
- _jsx(Div, { className: "prose prose-neutral dark:prose-invert max-w-none", dangerouslySetInnerHTML: { __html: adminHtml } })) : (_jsxs(_Fragment, { children: [t("intro") && (_jsx(Text, { variant: "secondary", className: "mb-10 leading-relaxed", size: "base", children: t("intro") })), _jsx(Stack, { gap: "xl", children: sections.map((section, i) => (_jsxs(Section, { children: [_jsx(Heading, { level: 2, className: "mb-3", children: section.heading }), _jsx(Text, { variant: "secondary", className: "leading-relaxed whitespace-pre-line", children: section.body })] }, i))) })] })), relatedLinks.length > 0 && (_jsxs(Div, { className: `mt-14 border-t`, border: "default", padding: "t-xl", children: [_jsx(Heading, { level: 3, className: "mb-3 uppercase tracking-wide", color: "muted", size: "sm", weight: "semibold", children: t("relatedTitle") }), _jsx(Row, { wrap: true, gap: "md", children: relatedLinks.map((l) => (_jsx(TextLink, { href: l.href, variant: "muted", size: "sm", children: l.label }, l.href))) })] }))] })] }));
96
+ _jsx(Div, { className: "prose prose-neutral dark:prose-invert max-w-none", dangerouslySetInnerHTML: { __html: adminHtml } })) : (_jsxs(_Fragment, { children: [t.has("intro") && (_jsx(Text, { variant: "secondary", className: "mb-10 leading-relaxed", size: "base", children: t("intro") })), _jsx(Stack, { gap: "xl", children: sections.map((section, i) => (_jsxs(Section, { children: [_jsx(Heading, { level: 2, className: "mb-3", children: section.heading }), _jsx(Text, { variant: "secondary", className: "leading-relaxed whitespace-pre-line", children: section.body })] }, i))) })] })), relatedLinks.length > 0 && (_jsxs(Div, { className: `mt-14 border-t`, border: "default", padding: "t-xl", children: [_jsx(Heading, { level: 3, className: "mb-3 uppercase tracking-wide", color: "muted", size: "sm", weight: "semibold", children: t("relatedTitle") }), _jsx(Row, { wrap: true, gap: "md", children: relatedLinks.map((l) => (_jsx(TextLink, { href: l.href, variant: "muted", size: "sm", children: l.label }, l.href))) })] }))] })] }));
97
97
  }
@@ -7,6 +7,7 @@ import { useQuery } from "@tanstack/react-query";
7
7
  import { Button, Card, CardBody, ConfirmDeleteModal, Div, PaginatedSelect, Stack, StackedViewShell, Tabs, TabsContent, TabsList, TabsTrigger, Text, useToast, Show, StickyToolbar, } from "../../../ui";
8
8
  import { useFormShellState, applyZodIssues, FormErrorSummary, FormShellContext } from "../../../ui/forms";
9
9
  import { SectionForm, useSectionFormNav } from "../../shell";
10
+ import { PRODUCT_MAX_IMAGES } from "../../../_internal/shared/media/limits";
10
11
  // Admin form-input validation — deliberately a small local schema, not the
11
12
  // broader `productItemSchema` (that one requires `id`/`price`/`status` as
12
13
  // non-optional, since it's the full read/display shape used across many
@@ -15,8 +16,12 @@ import { SectionForm, useSectionFormNav } from "../../shell";
15
16
  const adminProductFormSchema = z.object({
16
17
  title: z.string().min(1, "Product title is required"),
17
18
  price: z.number().min(0).optional(),
18
- images: z.array(z.string()).max(5, "Up to 5 gallery images allowed").optional(),
19
+ images: z
20
+ .array(z.string())
21
+ .max(PRODUCT_MAX_IMAGES, `Up to ${PRODUCT_MAX_IMAGES} gallery images allowed`)
22
+ .optional(),
19
23
  video: z.object({ url: z.string(), thumbnailUrl: z.string().optional() }).optional(),
24
+ finalSale: z.boolean().optional(),
20
25
  }).passthrough();
21
26
  import { apiClient } from "../../../http";
22
27
  import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
@@ -251,8 +251,6 @@ export function AdminSiteSettingsView({ labels = {}, ...rest }) {
251
251
  const [settlementGrace, setSettlementGrace] = React.useState(24);
252
252
  // ⑪ Platform Limits
253
253
  const [maxProductsPerStore, setMaxProductsPerStore] = React.useState(100);
254
- const [maxImagesPerProduct, setMaxImagesPerProduct] = React.useState(10);
255
- const [maxVideoSizeMb, setMaxVideoSizeMb] = React.useState(100);
256
254
  const [maxCustomFields, setMaxCustomFields] = React.useState(50);
257
255
  const [maxCustomSections, setMaxCustomSections] = React.useState(3);
258
256
  const [orderCancelWindow, setOrderCancelWindow] = React.useState(24);
@@ -443,8 +441,6 @@ export function AdminSiteSettingsView({ labels = {}, ...rest }) {
443
441
  setAutoExtendWindow(s.auctionConfig?.autoExtendWindowMinutes ?? 5);
444
442
  setSettlementGrace(s.auctionConfig?.settlementGracePeriodHours ?? 24);
445
443
  setMaxProductsPerStore(s.platformLimits?.maxProductsPerStore ?? 100);
446
- setMaxImagesPerProduct(s.platformLimits?.maxImagesPerProduct ?? 10);
447
- setMaxVideoSizeMb(s.platformLimits?.maxVideoSizeMb ?? 100);
448
444
  setMaxCustomFields(s.platformLimits?.maxCustomFieldsPerProduct ?? 50);
449
445
  setMaxCustomSections(s.platformLimits?.maxCustomSectionsPerProduct ?? 3);
450
446
  setOrderCancelWindow(s.platformLimits?.orderCancellationWindowHours ?? 24);
@@ -641,7 +637,7 @@ export function AdminSiteSettingsView({ labels = {}, ...rest }) {
641
637
  shipping: { freeShippingThreshold, defaultCarrier, maxDeliveryRadius },
642
638
  payment: { razorpayEnabled, upiManualEnabled, codEnabled, otpCheckoutThreshold },
643
639
  auctionConfig: { bidIncrementTiers: bidIncrementTiers, autoExtendWindowMinutes: autoExtendWindow, settlementGracePeriodHours: settlementGrace },
644
- platformLimits: { maxProductsPerStore, maxImagesPerProduct, maxVideoSizeMb, maxCustomFieldsPerProduct: maxCustomFields, maxCustomSectionsPerProduct: maxCustomSections, orderCancellationWindowHours: orderCancelWindow },
640
+ platformLimits: { maxProductsPerStore, maxCustomFieldsPerProduct: maxCustomFields, maxCustomSectionsPerProduct: maxCustomSections, orderCancellationWindowHours: orderCancelWindow },
645
641
  legalPages: { terms: termsHtml, privacy: privacyHtml, refundPolicy: refundHtml, shipping: shippingPolicyHtml, cookies: cookieHtml, ethics: ethicsHtml, codeOfConduct: conductHtml },
646
642
  // Both keys are always sent: Firestore's update() replaces a nested map
647
643
  // wholesale, so omitting one here would wipe it. The load-time spread this
@@ -748,7 +744,7 @@ export function AdminSiteSettingsView({ labels = {}, ...rest }) {
748
744
  ...bidIncrementTiers.slice(0, -1),
749
745
  { upTo: 100, increment: 10 },
750
746
  bidIncrementTiers[bidIncrementTiers.length - 1] ?? { upTo: null, increment: 1000 },
751
- ]), children: "+ Add tier" })] }), _jsx(Input, { label: "Auto-extend window (minutes before end)", value: String(autoExtendWindow), onChange: (e) => setAutoExtendWindow(parseInt(e.target.value) || 0), type: "number", min: 0, helperText: "Extend auction end time if a bid arrives within this window." }), _jsx(Input, { label: "Settlement grace period (hours)", value: String(settlementGrace), onChange: (e) => setSettlementGrace(parseInt(e.target.value) || 0), type: "number", min: 1, helperText: "Time winner has to pay before the auction is re-listed." })] }) }), _jsx(TabsContent, { value: "limits", children: _jsx(SettingsTabForm, { id: "limits", label: "Limits", children: _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(Input, { label: "Max products per store", value: String(maxProductsPerStore), onChange: (e) => setMaxProductsPerStore(parseInt(e.target.value) || 0), type: "number", min: 1 }), _jsx(Input, { label: "Max images per product", value: String(maxImagesPerProduct), onChange: (e) => setMaxImagesPerProduct(parseInt(e.target.value) || 0), type: "number", min: 1 }), _jsx(Input, { label: "Max video size (MB)", value: String(maxVideoSizeMb), onChange: (e) => setMaxVideoSizeMb(parseInt(e.target.value) || 0), type: "number", min: 1 }), _jsx(Input, { label: "Max custom fields per product", value: String(maxCustomFields), onChange: (e) => setMaxCustomFields(parseInt(e.target.value) || 0), type: "number", min: 0 }), _jsx(Input, { label: "Max custom sections per product", value: String(maxCustomSections), onChange: (e) => setMaxCustomSections(parseInt(e.target.value) || 0), type: "number", min: 0 }), _jsx(Input, { label: "Order cancellation window (hours)", value: String(orderCancelWindow), onChange: (e) => setOrderCancelWindow(parseInt(e.target.value) || 0), type: "number", min: 0 })] }) }) }), _jsx(TabsContent, { value: "whatsapp", children: _jsxs(SettingsTabForm, { id: "whatsapp", label: "Whatsapp", children: [_jsx(Text, { size: "xs", color: "muted", children: "Platform-level WhatsApp Business Cloud API credentials. Used for automated purchase announcements to admin numbers when orders are placed. Store owners configure their own credentials in Store \u2192 WhatsApp." }), _jsx(Input, { label: "Phone Number ID", value: waPhoneNumberId, onChange: (e) => setWaPhoneNumberId(e.target.value), placeholder: "987654321098765", helperText: "From Meta for Developers \u2192 App \u2192 WhatsApp \u2192 API Setup" }), _jsx(MaskedInput, { label: "Cloud API System User Token", value: waCloudApiToken, onChange: setWaCloudApiToken, placeholder: "EAAxxxxxxxx\u2026", helperText: "Long-lived system user access token with WhatsApp Business permissions" }), _jsx(Input, { label: "Admin Notify Numbers", value: waAdminNotifyNumbers, onChange: (e) => setWaAdminNotifyNumbers(e.target.value), placeholder: "919876543210,918765432109", helperText: "Comma-separated, digits-only, include country code. These receive a WhatsApp message when any order is placed." }), _jsx(Text, { size: "xs", color: "muted", children: "Approved Meta message templates for the buyer-facing WhatsApp order-updates addon (Fees tab). Meta rejects business-initiated free-text WhatsApp messages outside a 24h customer-service window \u2014 a real template name here is required for that notification type to actually deliver. Leave blank to skip WhatsApp for that type (in-app + email still fire)." }), _jsx(Input, { label: "Template language code", value: waTemplateLanguage, onChange: (e) => setWaTemplateLanguage(e.target.value), placeholder: "en", helperText: "BCP-47 code the templates below were approved in, e.g. en or en_US." }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(Input, { label: "Order placed template name", value: waTemplateOrderPlaced, onChange: (e) => setWaTemplateOrderPlaced(e.target.value), placeholder: "order_placed_update" }), _jsx(Input, { label: "Order confirmed template name", value: waTemplateOrderConfirmed, onChange: (e) => setWaTemplateOrderConfirmed(e.target.value), placeholder: "order_confirmed_update" }), _jsx(Input, { label: "Order shipped template name", value: waTemplateOrderShipped, onChange: (e) => setWaTemplateOrderShipped(e.target.value), placeholder: "order_shipped_update" }), _jsx(Input, { label: "Order delivered template name", value: waTemplateOrderDelivered, onChange: (e) => setWaTemplateOrderDelivered(e.target.value), placeholder: "order_delivered_update" }), _jsx(Input, { label: "Order cancelled template name", value: waTemplateOrderCancelled, onChange: (e) => setWaTemplateOrderCancelled(e.target.value), placeholder: "order_cancelled_update" }), _jsx(Input, { label: "Refund initiated template name", value: waTemplateRefundInitiated, onChange: (e) => setWaTemplateRefundInitiated(e.target.value), placeholder: "refund_initiated_update" })] })] }) }), _jsx(TabsContent, { value: "notifications", children: _jsxs(SettingsTabForm, { id: "notifications", label: "Notifications", children: [_jsx(Text, { size: "xs", color: "muted", children: "In-app notifications are always on. Enable external channels below to let the platform fan out to email, WhatsApp, or SMS. Users can further restrict which types they receive." }), _jsxs(Stack, { gap: "xs", surface: "muted", rounded: "lg", border: "default", padding: "md", children: [_jsxs(Row, { justify: "between", gap: "sm", children: [_jsx(Text, { size: "sm", weight: "medium", color: "muted", children: "In-app (notification bell)" }), _jsx(Span, { color: "success", surface: "success-surface", size: "xs", weight: "semibold", rounded: "full", padding: "pill-xs", children: "Always on" })] }), _jsx(Text, { size: "xs", color: "muted", children: "Displayed in the notification bell and inbox. Cannot be disabled." })] }), _jsxs(Stack, { gap: "md", rounded: "lg", border: "default", padding: "md", children: [_jsx(Div, { id: "setting-email-notifications", children: _jsx(Toggle, { label: "Email notifications", checked: notifEmailEnabled, onChange: setNotifEmailEnabled }) }), notifEmailEnabled && (_jsxs(Stack, { gap: "md", className: NOTIF_CHANNEL_INDENT, children: [_jsx(Select, { label: "Minimum priority to send email", options: PRIORITY_OPTIONS, value: notifEmailMinPriority, onValueChange: setNotifEmailMinPriority }), _jsx(Text, { size: "xs", weight: "medium", color: "muted", className: "pt-[0.25rem]", children: "Resend API (for transactional email)" }), _jsx(MaskedInput, { label: "Resend API Key", value: resendApiKey, onChange: setResendApiKey, placeholder: "re_live_\u2026", helperText: "Get your key at resend.com \u2014 used for all transactional notifications." }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(Input, { label: "From email", value: notifFromEmail, onChange: (e) => setNotifFromEmail(e.target.value), placeholder: "noreply@letitrip.in", type: "email" }), _jsx(Input, { label: "From name", value: notifFromName, onChange: (e) => setNotifFromName(e.target.value), placeholder: "LetItRip" })] }), _jsx(Text, { size: "xs", weight: "medium", color: "muted", className: "pt-[0.25rem]", children: "Notification types (leave empty to send all types)" }), _jsx(PaginatedSelect, { multiple: true, options: NOTIF_TYPE_OPTIONS, value: notifEmailTypes, onChange: (values) => setNotifEmailTypes(values), placeholder: "All notification types" })] }))] }), _jsxs(Stack, { gap: "md", rounded: "lg", border: "default", padding: "md", children: [_jsx(Div, { id: "setting-daily-digest", children: _jsx(Toggle, { label: "Daily status digest", checked: digestEnabled, onChange: setDigestEnabled }) }), digestEnabled && (_jsxs(Stack, { gap: "md", className: NOTIF_CHANNEL_INDENT, children: [_jsx(Text, { size: "xs", color: "muted", children: "Sent every morning at 10:00 IST with the previous 24 hours of order activity. Its arrival doubles as the platform health check \u2014 if it stops landing, something needs a look." }), _jsx(Textarea, { label: "Recipients (To)", value: digestRecipients, onChange: (e) => setDigestRecipients(e.target.value), placeholder: "support@letitrip.in\nmohasin@letitrip.in", rows: 4, helperText: "One address per line." }), _jsx(Textarea, { label: "CC recipients", value: digestCcRecipients, onChange: (e) => setDigestCcRecipients(e.target.value), placeholder: "One address per line", rows: 3, helperText: "Optional \u2014 anyone else who should receive a copy." })] }))] }), _jsxs(Stack, { gap: "md", rounded: "lg", border: "default", padding: "md", children: [_jsx(Div, { id: "setting-whatsapp-notifications", children: _jsx(Toggle, { label: "WhatsApp notifications", checked: notifWhatsappEnabled, onChange: setNotifWhatsappEnabled }) }), notifWhatsappEnabled && (_jsxs(Stack, { gap: "md", className: NOTIF_CHANNEL_INDENT, children: [_jsx(Select, { label: "Minimum priority to send WhatsApp", options: PRIORITY_OPTIONS, value: notifWhatsappMinPriority, onValueChange: setNotifWhatsappMinPriority }), _jsx(Toggle, { label: "Enable WhatsApp OTP (for login and verification)", checked: notifWhatsappOtpEnabled, onChange: setNotifWhatsappOtpEnabled }), _jsx(Text, { size: "xs", color: "muted", children: "WhatsApp credentials are configured in the WhatsApp tab (\u246C). OTP messages use the same phone number." }), _jsx(Text, { size: "xs", weight: "medium", color: "muted", className: "pt-[0.25rem]", children: "Notification types (leave empty to send all types)" }), _jsx(PaginatedSelect, { multiple: true, options: NOTIF_TYPE_OPTIONS, value: notifWhatsappTypes, onChange: (values) => setNotifWhatsappTypes(values), placeholder: "All notification types" })] }))] }), _jsxs(Stack, { gap: "md", rounded: "lg", border: "default", padding: "md", children: [_jsx(Toggle, { label: "SMS notifications", checked: notifSmsEnabled, onChange: setNotifSmsEnabled }), notifSmsEnabled && (_jsxs(Stack, { gap: "md", className: NOTIF_CHANNEL_INDENT, children: [_jsx(Select, { label: "Minimum priority to send SMS", options: PRIORITY_OPTIONS, value: notifSmsMinPriority, onValueChange: setNotifSmsMinPriority }), _jsx(Text, { size: "xs", color: "muted", children: "SMS gateway credentials (e.g. Twilio, MSG91) can be configured in the Integrations tab once an SMS provider is connected." })] }))] })] }) }), _jsx(TabsContent, { value: "legal", children: _jsx(SettingsTabForm, { id: "legal", label: "Legal", children: [
747
+ ]), children: "+ Add tier" })] }), _jsx(Input, { label: "Auto-extend window (minutes before end)", value: String(autoExtendWindow), onChange: (e) => setAutoExtendWindow(parseInt(e.target.value) || 0), type: "number", min: 0, helperText: "Extend auction end time if a bid arrives within this window." }), _jsx(Input, { label: "Settlement grace period (hours)", value: String(settlementGrace), onChange: (e) => setSettlementGrace(parseInt(e.target.value) || 0), type: "number", min: 1, helperText: "Time winner has to pay before the auction is re-listed." })] }) }), _jsx(TabsContent, { value: "limits", children: _jsx(SettingsTabForm, { id: "limits", label: "Limits", children: _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(Input, { label: "Max products per store", value: String(maxProductsPerStore), onChange: (e) => setMaxProductsPerStore(parseInt(e.target.value) || 0), type: "number", min: 1 }), _jsx(Input, { label: "Max custom fields per product", value: String(maxCustomFields), onChange: (e) => setMaxCustomFields(parseInt(e.target.value) || 0), type: "number", min: 0 }), _jsx(Input, { label: "Max custom sections per product", value: String(maxCustomSections), onChange: (e) => setMaxCustomSections(parseInt(e.target.value) || 0), type: "number", min: 0 }), _jsx(Input, { label: "Order cancellation window (hours)", value: String(orderCancelWindow), onChange: (e) => setOrderCancelWindow(parseInt(e.target.value) || 0), type: "number", min: 0 })] }) }) }), _jsx(TabsContent, { value: "whatsapp", children: _jsxs(SettingsTabForm, { id: "whatsapp", label: "Whatsapp", children: [_jsx(Text, { size: "xs", color: "muted", children: "Platform-level WhatsApp Business Cloud API credentials. Used for automated purchase announcements to admin numbers when orders are placed. Store owners configure their own credentials in Store \u2192 WhatsApp." }), _jsx(Input, { label: "Phone Number ID", value: waPhoneNumberId, onChange: (e) => setWaPhoneNumberId(e.target.value), placeholder: "987654321098765", helperText: "From Meta for Developers \u2192 App \u2192 WhatsApp \u2192 API Setup" }), _jsx(MaskedInput, { label: "Cloud API System User Token", value: waCloudApiToken, onChange: setWaCloudApiToken, placeholder: "EAAxxxxxxxx\u2026", helperText: "Long-lived system user access token with WhatsApp Business permissions" }), _jsx(Input, { label: "Admin Notify Numbers", value: waAdminNotifyNumbers, onChange: (e) => setWaAdminNotifyNumbers(e.target.value), placeholder: "919876543210,918765432109", helperText: "Comma-separated, digits-only, include country code. These receive a WhatsApp message when any order is placed." }), _jsx(Text, { size: "xs", color: "muted", children: "Approved Meta message templates for the buyer-facing WhatsApp order-updates addon (Fees tab). Meta rejects business-initiated free-text WhatsApp messages outside a 24h customer-service window \u2014 a real template name here is required for that notification type to actually deliver. Leave blank to skip WhatsApp for that type (in-app + email still fire)." }), _jsx(Input, { label: "Template language code", value: waTemplateLanguage, onChange: (e) => setWaTemplateLanguage(e.target.value), placeholder: "en", helperText: "BCP-47 code the templates below were approved in, e.g. en or en_US." }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(Input, { label: "Order placed template name", value: waTemplateOrderPlaced, onChange: (e) => setWaTemplateOrderPlaced(e.target.value), placeholder: "order_placed_update" }), _jsx(Input, { label: "Order confirmed template name", value: waTemplateOrderConfirmed, onChange: (e) => setWaTemplateOrderConfirmed(e.target.value), placeholder: "order_confirmed_update" }), _jsx(Input, { label: "Order shipped template name", value: waTemplateOrderShipped, onChange: (e) => setWaTemplateOrderShipped(e.target.value), placeholder: "order_shipped_update" }), _jsx(Input, { label: "Order delivered template name", value: waTemplateOrderDelivered, onChange: (e) => setWaTemplateOrderDelivered(e.target.value), placeholder: "order_delivered_update" }), _jsx(Input, { label: "Order cancelled template name", value: waTemplateOrderCancelled, onChange: (e) => setWaTemplateOrderCancelled(e.target.value), placeholder: "order_cancelled_update" }), _jsx(Input, { label: "Refund initiated template name", value: waTemplateRefundInitiated, onChange: (e) => setWaTemplateRefundInitiated(e.target.value), placeholder: "refund_initiated_update" })] })] }) }), _jsx(TabsContent, { value: "notifications", children: _jsxs(SettingsTabForm, { id: "notifications", label: "Notifications", children: [_jsx(Text, { size: "xs", color: "muted", children: "In-app notifications are always on. Enable external channels below to let the platform fan out to email, WhatsApp, or SMS. Users can further restrict which types they receive." }), _jsxs(Stack, { gap: "xs", surface: "muted", rounded: "lg", border: "default", padding: "md", children: [_jsxs(Row, { justify: "between", gap: "sm", children: [_jsx(Text, { size: "sm", weight: "medium", color: "muted", children: "In-app (notification bell)" }), _jsx(Span, { color: "success", surface: "success-surface", size: "xs", weight: "semibold", rounded: "full", padding: "pill-xs", children: "Always on" })] }), _jsx(Text, { size: "xs", color: "muted", children: "Displayed in the notification bell and inbox. Cannot be disabled." })] }), _jsxs(Stack, { gap: "md", rounded: "lg", border: "default", padding: "md", children: [_jsx(Div, { id: "setting-email-notifications", children: _jsx(Toggle, { label: "Email notifications", checked: notifEmailEnabled, onChange: setNotifEmailEnabled }) }), notifEmailEnabled && (_jsxs(Stack, { gap: "md", className: NOTIF_CHANNEL_INDENT, children: [_jsx(Select, { label: "Minimum priority to send email", options: PRIORITY_OPTIONS, value: notifEmailMinPriority, onValueChange: setNotifEmailMinPriority }), _jsx(Text, { size: "xs", weight: "medium", color: "muted", className: "pt-[0.25rem]", children: "Resend API (for transactional email)" }), _jsx(MaskedInput, { label: "Resend API Key", value: resendApiKey, onChange: setResendApiKey, placeholder: "re_live_\u2026", helperText: "Get your key at resend.com \u2014 used for all transactional notifications." }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(Input, { label: "From email", value: notifFromEmail, onChange: (e) => setNotifFromEmail(e.target.value), placeholder: "noreply@letitrip.in", type: "email" }), _jsx(Input, { label: "From name", value: notifFromName, onChange: (e) => setNotifFromName(e.target.value), placeholder: "LetItRip" })] }), _jsx(Text, { size: "xs", weight: "medium", color: "muted", className: "pt-[0.25rem]", children: "Notification types (leave empty to send all types)" }), _jsx(PaginatedSelect, { multiple: true, options: NOTIF_TYPE_OPTIONS, value: notifEmailTypes, onChange: (values) => setNotifEmailTypes(values), placeholder: "All notification types" })] }))] }), _jsxs(Stack, { gap: "md", rounded: "lg", border: "default", padding: "md", children: [_jsx(Div, { id: "setting-daily-digest", children: _jsx(Toggle, { label: "Daily status digest", checked: digestEnabled, onChange: setDigestEnabled }) }), digestEnabled && (_jsxs(Stack, { gap: "md", className: NOTIF_CHANNEL_INDENT, children: [_jsx(Text, { size: "xs", color: "muted", children: "Sent every morning at 10:00 IST with the previous 24 hours of order activity. Its arrival doubles as the platform health check \u2014 if it stops landing, something needs a look." }), _jsx(Textarea, { label: "Recipients (To)", value: digestRecipients, onChange: (e) => setDigestRecipients(e.target.value), placeholder: "support@letitrip.in\nmohasin@letitrip.in", rows: 4, helperText: "One address per line." }), _jsx(Textarea, { label: "CC recipients", value: digestCcRecipients, onChange: (e) => setDigestCcRecipients(e.target.value), placeholder: "One address per line", rows: 3, helperText: "Optional \u2014 anyone else who should receive a copy." })] }))] }), _jsxs(Stack, { gap: "md", rounded: "lg", border: "default", padding: "md", children: [_jsx(Div, { id: "setting-whatsapp-notifications", children: _jsx(Toggle, { label: "WhatsApp notifications", checked: notifWhatsappEnabled, onChange: setNotifWhatsappEnabled }) }), notifWhatsappEnabled && (_jsxs(Stack, { gap: "md", className: NOTIF_CHANNEL_INDENT, children: [_jsx(Select, { label: "Minimum priority to send WhatsApp", options: PRIORITY_OPTIONS, value: notifWhatsappMinPriority, onValueChange: setNotifWhatsappMinPriority }), _jsx(Toggle, { label: "Enable WhatsApp OTP (for login and verification)", checked: notifWhatsappOtpEnabled, onChange: setNotifWhatsappOtpEnabled }), _jsx(Text, { size: "xs", color: "muted", children: "WhatsApp credentials are configured in the WhatsApp tab (\u246C). OTP messages use the same phone number." }), _jsx(Text, { size: "xs", weight: "medium", color: "muted", className: "pt-[0.25rem]", children: "Notification types (leave empty to send all types)" }), _jsx(PaginatedSelect, { multiple: true, options: NOTIF_TYPE_OPTIONS, value: notifWhatsappTypes, onChange: (values) => setNotifWhatsappTypes(values), placeholder: "All notification types" })] }))] }), _jsxs(Stack, { gap: "md", rounded: "lg", border: "default", padding: "md", children: [_jsx(Toggle, { label: "SMS notifications", checked: notifSmsEnabled, onChange: setNotifSmsEnabled }), notifSmsEnabled && (_jsxs(Stack, { gap: "md", className: NOTIF_CHANNEL_INDENT, children: [_jsx(Select, { label: "Minimum priority to send SMS", options: PRIORITY_OPTIONS, value: notifSmsMinPriority, onValueChange: setNotifSmsMinPriority }), _jsx(Text, { size: "xs", color: "muted", children: "SMS gateway credentials (e.g. Twilio, MSG91) can be configured in the Integrations tab once an SMS provider is connected." })] }))] })] }) }), _jsx(TabsContent, { value: "legal", children: _jsx(SettingsTabForm, { id: "legal", label: "Legal", children: [
752
748
  ["Terms of Service", termsHtml, setTermsHtml],
753
749
  ["Privacy Policy", privacyHtml, setPrivacyHtml],
754
750
  ["Refund Policy", refundHtml, setRefundHtml],
@@ -127,8 +127,6 @@ export { AdminCarouselEditorView } from "./AdminCarouselEditorView";
127
127
  export type { AdminCarouselEditorViewProps } from "./AdminCarouselEditorView";
128
128
  export { AdminDashboardView } from "./AdminDashboardView";
129
129
  export type { AdminDashboardViewProps } from "./AdminDashboardView";
130
- export { AdminSiteView } from "./AdminSiteView";
131
- export type { AdminSiteViewProps } from "./AdminSiteView";
132
130
  export { AdminSiteSettingsView } from "./AdminSiteSettingsView";
133
131
  export type { AdminSiteSettingsViewProps } from "./AdminSiteSettingsView";
134
132
  export { AdminAnalyticsView } from "./AdminAnalyticsView";
@@ -68,7 +68,6 @@ export { AdminAdEditorView } from "./AdminAdEditorView";
68
68
  export { AdminCarouselView } from "./AdminCarouselView";
69
69
  export { AdminCarouselEditorView } from "./AdminCarouselEditorView";
70
70
  export { AdminDashboardView } from "./AdminDashboardView";
71
- export { AdminSiteView } from "./AdminSiteView";
72
71
  export { AdminSiteSettingsView } from "./AdminSiteSettingsView";
73
72
  export { AdminAnalyticsView } from "./AdminAnalyticsView";
74
73
  export { AdminStatCard } from "./analytics/AdminStatCard";
@@ -22,7 +22,7 @@
22
22
  * @tag layer:schema
23
23
  * @tag pattern:none
24
24
  * @tag access:isomorphic
25
- * @tag consumers:AdminAddressEditorView,AdminStoreEditorView,AdminSiteView
25
+ * @tag consumers:AdminAddressEditorView,AdminStoreEditorView
26
26
  * @tag sideEffects:none
27
27
  */
28
28
  import { z } from "zod";
@@ -231,8 +231,6 @@ export interface SiteSettingsIntegrations {
231
231
  /** Admin-tunable platform caps. Enforced server-side; not buyer-facing. */
232
232
  export interface SiteSettingsPlatformLimits {
233
233
  maxProductsPerStore?: number;
234
- maxImagesPerProduct?: number;
235
- maxVideoSizeMb?: number;
236
234
  maxCustomFieldsPerProduct?: number;
237
235
  maxCustomSectionsPerProduct?: number;
238
236
  orderCancellationWindowHours?: number;
@@ -39,6 +39,7 @@ import { computeRelatedItems } from "../../../_internal/server/features/products
39
39
  import { GroupedListingsCarousel } from "../../grouped/components/GroupedListingsCarousel";
40
40
  import { getGroupsWithItemsForProduct } from "../../../_internal/server/features/grouped/data";
41
41
  import { SublistingCarouselSection } from "../../products/components/SublistingCarouselSection";
42
+ import { isFinalSale } from "../../products/constants/final-sale";
42
43
  function toDescriptionHtml(raw) {
43
44
  if (!raw)
44
45
  return "";
@@ -46,8 +47,8 @@ function toDescriptionHtml(raw) {
46
47
  return normalizeRichTextHtml(s);
47
48
  }
48
49
  function renderAuctionInfoPanel(props) {
49
- const { productId, title, currentBid, currency, bidCount, isEnded, endDate, buyNowPrice, buyNowAvailable, featured, freeShipping, condition, category, categoryName, brand, brandSlug, productFeatures, features, descriptionHtml, safeSeller, storeHref } = props;
50
- return (_jsxs(Stack, { gap: "md", children: [_jsxs(Div, { children: [_jsxs(Row, { gap: "xs", wrap: true, className: "mb-2", children: [_jsx(Span, { size: "xs", weight: "semibold", className: CLS_LIVE_BADGE, children: "\uD83C\uDFF7\uFE0F Live Auction" }), isEnded ? (_jsx(Span, { color: "error", surface: "danger-surface", size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", children: "Ended" })) : (_jsx(Span, { color: "success", surface: "success-surface", size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", children: "Active" }))] }), _jsx(Heading, { level: 1, className: "leading-snug", smSize: "2xl", color: "primary", size: "xl", weight: "bold", children: title })] }), _jsxs(Div, { children: [_jsx(Text, { className: "mb-0.5", color: "muted", size: "xs", children: "Current bid" }), _jsx(LiveBidPrice, { productId: productId, currentBid: currentBid, bidCount: bidCount, currency: currency, isEnded: isEnded, priceSize: "2xl", badgeSize: "sm" }), endDate && !isEnded && (_jsxs(Text, { className: "mt-1.5", color: "muted", size: "sm", children: ["Ends in ", _jsx(Span, { weight: "medium", color: "muted", children: _jsx(CountdownDisplay, { targetDate: endDate, format: "auto", expiredLabel: "Ended" }) })] })), endDate && _jsxs(Text, { className: "mt-0.5", color: "faint", size: "xs", children: [isEnded ? "Ended" : "Ends", " ", endDate.toLocaleString()] })] }), buyNowAvailable && buyNowPrice !== null && (_jsxs(Row, { align: "center", gap: "sm", className: "border border-[var(--appkit-color-primary-200)] dark:border-[var(--appkit-color-primary-800)] bg-primary-50 dark:bg-primary-900/20", padding: "inlineSm", rounded: "lg", children: [_jsx(Span, { size: "xs", color: "muted", children: "Buy Now:" }), _jsx(Span, { size: "base", weight: "bold", className: "text-primary-700 dark:text-primary-300", children: formatCurrency(buyNowPrice, currency) }), _jsx(Span, { size: "xs", color: "faint", children: "\u2014 in \u201CPlace a bid\u201D" })] })), _jsx(ProductFeatureBadges, { featured: featured, freeShipping: freeShipping, condition: condition ?? undefined, labels: { featured: "Featured", fasterDelivery: "Faster Delivery", ratedSeller: "Rated Seller", condition: "Condition", conditionNew: "New", conditionUsed: "Used", conditionBroken: "For Parts", conditionRefurbished: "Refurbished", returnable: "Returnable", freeShipping: "Free Shipping", codAvailable: "Cash on Delivery", emiAvailable: "EMI Available", wishlistCount: (n) => `${n} wishlisted`, categoryProductCount: (n, cat) => `${n} in ${cat}` } }), (categoryName || category || brand) && (_jsxs(Row, { gap: "sm", wrap: true, children: [category && _jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category)), className: "inline-flex items-center rounded-full border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] px-[var(--appkit-space-2-5)] py-[var(--appkit-space-1)] text-[length:var(--appkit-text-xs)] font-medium text-[var(--appkit-color-text-muted)] transition-colors hover:border-primary-300 hover:bg-primary-surface hover:text-primary-700 dark:hover:border-primary-700/60 dark:hover:text-primary-400", children: categoryName || category }), !category && categoryName && _jsx(Span, { layout: "inline-flex", size: "xs", weight: "medium", border: "subtle", rounded: "full", padding: "pill-sm-tall", surface: "muted", color: "muted", children: categoryName }), brand && brandSlug && _jsx(Link, { href: String(ROUTES.PUBLIC.BRAND_DETAIL(brandSlug)), className: "inline-flex items-center rounded-full border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] px-[var(--appkit-space-2-5)] py-[var(--appkit-space-1)] text-[length:var(--appkit-text-xs)] font-medium text-[var(--appkit-color-text-muted)] transition-colors hover:border-primary-300 hover:bg-primary-surface hover:text-primary-700 dark:hover:border-primary-700/60 dark:hover:text-primary-400", children: brand }), brand && !brandSlug && _jsx(Span, { layout: "inline-flex", size: "xs", weight: "medium", border: "subtle", rounded: "full", padding: "pill-sm-tall", surface: "muted", color: "muted", children: brand })] })), productFeatures && features.length > 0 && _jsx(FeatureBadgeList, { productFeatureIds: features, features: productFeatures }), !productFeatures && features.length > 0 && (_jsxs(Div, { border: "subtle", surface: "muted", padding: "inline", rounded: "xl", children: [_jsx(Text, { className: "mb-2 tracking-wide", color: "muted", size: "xs", weight: "semibold", transform: "uppercase", children: "About this item" }), _jsx(Ul, { spacing: "comfortable", size: "sm", color: "primary", children: features.map((f, i) => _jsxs(Li, { layout: "flex-start", gap: "2", children: [_jsx(Span, { className: "mt-0.5 flex-shrink-0 text-primary-500", children: "\u2022" }), f] }, i)) })] })), descriptionHtml && _jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "text-[length:var(--appkit-text-sm)] leading-relaxed text-[var(--appkit-color-text-muted)] line-clamp-4" }), safeSeller && (_jsx(Div, { className: `${__P.p3}`, border: "subtle", rounded: "xl", surface: "muted", children: _jsxs(Row, { justify: "between", align: "center", children: [_jsxs(Div, { children: [_jsx(Text, { className: "text-[10px] tracking-wide mb-0.5", color: "faint", transform: "uppercase", children: "Listed by" }), _jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: safeSeller })] }), storeHref && _jsx(Link, { href: storeHref, className: "shrink-0 rounded-lg bg-primary/10 dark:bg-primary/20 px-[var(--appkit-space-3)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-xs)] font-semibold text-primary-700 dark:text-primary-300 hover:bg-primary/20 dark:hover:bg-primary/30 transition-colors", children: "Visit Store \u2192" })] }) }))] }));
50
+ const { productId, title, currentBid, currency, bidCount, isEnded, endDate, buyNowPrice, buyNowAvailable, featured, freeShipping, condition, finalSale, category, categoryName, brand, brandSlug, productFeatures, features, descriptionHtml, safeSeller, storeHref } = props;
51
+ return (_jsxs(Stack, { gap: "md", children: [_jsxs(Div, { children: [_jsxs(Row, { gap: "xs", wrap: true, className: "mb-2", children: [_jsx(Span, { size: "xs", weight: "semibold", className: CLS_LIVE_BADGE, children: "\uD83C\uDFF7\uFE0F Live Auction" }), isEnded ? (_jsx(Span, { color: "error", surface: "danger-surface", size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", children: "Ended" })) : (_jsx(Span, { color: "success", surface: "success-surface", size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", children: "Active" }))] }), _jsx(Heading, { level: 1, className: "leading-snug", smSize: "2xl", color: "primary", size: "xl", weight: "bold", children: title })] }), _jsxs(Div, { children: [_jsx(Text, { className: "mb-0.5", color: "muted", size: "xs", children: "Current bid" }), _jsx(LiveBidPrice, { productId: productId, currentBid: currentBid, bidCount: bidCount, currency: currency, isEnded: isEnded, priceSize: "2xl", badgeSize: "sm" }), endDate && !isEnded && (_jsxs(Text, { className: "mt-1.5", color: "muted", size: "sm", children: ["Ends in ", _jsx(Span, { weight: "medium", color: "muted", children: _jsx(CountdownDisplay, { targetDate: endDate, format: "auto", expiredLabel: "Ended" }) })] })), endDate && _jsxs(Text, { className: "mt-0.5", color: "faint", size: "xs", children: [isEnded ? "Ended" : "Ends", " ", endDate.toLocaleString()] })] }), buyNowAvailable && buyNowPrice !== null && (_jsxs(Row, { align: "center", gap: "sm", className: "border border-[var(--appkit-color-primary-200)] dark:border-[var(--appkit-color-primary-800)] bg-primary-50 dark:bg-primary-900/20", padding: "inlineSm", rounded: "lg", children: [_jsx(Span, { size: "xs", color: "muted", children: "Buy Now:" }), _jsx(Span, { size: "base", weight: "bold", className: "text-primary-700 dark:text-primary-300", children: formatCurrency(buyNowPrice, currency) }), _jsx(Span, { size: "xs", color: "faint", children: "\u2014 in \u201CPlace a bid\u201D" })] })), _jsx(ProductFeatureBadges, { featured: featured, freeShipping: freeShipping, condition: condition ?? undefined, finalSale: finalSale, labels: { featured: "Featured", fasterDelivery: "Faster Delivery", ratedSeller: "Rated Seller", condition: "Condition", conditionNew: "New", conditionUsed: "Used", conditionBroken: "For Parts", conditionRefurbished: "Refurbished", returnable: "Returnable", finalSale: "Final Sale", freeShipping: "Free Shipping", codAvailable: "Cash on Delivery", emiAvailable: "EMI Available", wishlistCount: (n) => `${n} wishlisted`, categoryProductCount: (n, cat) => `${n} in ${cat}` } }), (categoryName || category || brand) && (_jsxs(Row, { gap: "sm", wrap: true, children: [category && _jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category)), className: "inline-flex items-center rounded-full border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] px-[var(--appkit-space-2-5)] py-[var(--appkit-space-1)] text-[length:var(--appkit-text-xs)] font-medium text-[var(--appkit-color-text-muted)] transition-colors hover:border-primary-300 hover:bg-primary-surface hover:text-primary-700 dark:hover:border-primary-700/60 dark:hover:text-primary-400", children: categoryName || category }), !category && categoryName && _jsx(Span, { layout: "inline-flex", size: "xs", weight: "medium", border: "subtle", rounded: "full", padding: "pill-sm-tall", surface: "muted", color: "muted", children: categoryName }), brand && brandSlug && _jsx(Link, { href: String(ROUTES.PUBLIC.BRAND_DETAIL(brandSlug)), className: "inline-flex items-center rounded-full border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] px-[var(--appkit-space-2-5)] py-[var(--appkit-space-1)] text-[length:var(--appkit-text-xs)] font-medium text-[var(--appkit-color-text-muted)] transition-colors hover:border-primary-300 hover:bg-primary-surface hover:text-primary-700 dark:hover:border-primary-700/60 dark:hover:text-primary-400", children: brand }), brand && !brandSlug && _jsx(Span, { layout: "inline-flex", size: "xs", weight: "medium", border: "subtle", rounded: "full", padding: "pill-sm-tall", surface: "muted", color: "muted", children: brand })] })), productFeatures && features.length > 0 && _jsx(FeatureBadgeList, { productFeatureIds: features, features: productFeatures }), !productFeatures && features.length > 0 && (_jsxs(Div, { border: "subtle", surface: "muted", padding: "inline", rounded: "xl", children: [_jsx(Text, { className: "mb-2 tracking-wide", color: "muted", size: "xs", weight: "semibold", transform: "uppercase", children: "About this item" }), _jsx(Ul, { spacing: "comfortable", size: "sm", color: "primary", children: features.map((f, i) => _jsxs(Li, { layout: "flex-start", gap: "2", children: [_jsx(Span, { className: "mt-0.5 flex-shrink-0 text-primary-500", children: "\u2022" }), f] }, i)) })] })), descriptionHtml && _jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "text-[length:var(--appkit-text-sm)] leading-relaxed text-[var(--appkit-color-text-muted)] line-clamp-4" }), safeSeller && (_jsx(Div, { className: `${__P.p3}`, border: "subtle", rounded: "xl", surface: "muted", children: _jsxs(Row, { justify: "between", align: "center", children: [_jsxs(Div, { children: [_jsx(Text, { className: "text-[10px] tracking-wide mb-0.5", color: "faint", transform: "uppercase", children: "Listed by" }), _jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: safeSeller })] }), storeHref && _jsx(Link, { href: storeHref, className: "shrink-0 rounded-lg bg-primary/10 dark:bg-primary/20 px-[var(--appkit-space-3)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-xs)] font-semibold text-primary-700 dark:text-primary-300 hover:bg-primary/20 dark:hover:bg-primary/30 transition-colors", children: "Visit Store \u2192" })] }) }))] }));
51
52
  }
52
53
  /**
53
54
  * Store reviews for the auction's seller.
@@ -215,7 +216,7 @@ export async function AuctionDetailPageView({ id, initialAuction, onPlaceBid, on
215
216
  price: typeof p.price === "number" ? p.price : undefined,
216
217
  storeId: typeof p.storeId === "string" ? p.storeId : undefined,
217
218
  storeName: typeof p.storeName === "string" ? p.storeName : undefined,
218
- } }), _jsxs(Container, { size: "xl", padding: "y-lg", children: [_jsxs(Row, { className: "mb-4", align: "center", justify: "between", gap: "sm", wrap: true, children: [_jsxs(Nav, { "aria-label": "Breadcrumb", layout: "flex-wrap", gap: "2xs", textSize: "xs", color: "muted", children: [_jsx(Link, { href: String(ROUTES.HOME), className: CLS_BREADCRUMB_LINK, children: "Home" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.AUCTIONS), className: CLS_BREADCRUMB_LINK, children: "Auctions" }), category && (_jsxs(_Fragment, { children: [_jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category)), className: CLS_BREADCRUMB_LINK, children: categoryName || category })] })), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Span, { className: "truncate max-w-[200px]", color: "muted", children: title })] }), _jsx(ShareButton, { title: title })] }), _jsx(AuctionDetailView, { renderGallery: () => (_jsx(ProductGalleryClient, { images: images, video: productVideo, productName: title })), renderInfo: () => renderAuctionInfoPanel({ productId: String(product.id), title, currentBid, currency, bidCount, isEnded, endDate, buyNowPrice, buyNowAvailable, featured, freeShipping, condition, category, categoryName, brand, brandSlug, productFeatures, features, descriptionHtml, safeSeller, storeHref }), renderBidForm: () => onPlaceBid ? (_jsx(Stack, { id: "auction-bid-form", gap: "3", children: _jsxs(Stack, { className: `${__P.p5}`, border: "subtle", gap: "3", rounded: "xl", surface: "muted", children: [_jsxs(Row, { justify: "between", align: "baseline", children: [_jsx(LiveBidPrice, { productId: String(product.id), currentBid: currentBid, bidCount: bidCount, currency: currency, isEnded: isEnded, priceSize: "xl", badgeSize: "xs" }), !isEnded && (_jsx(LiveMinIncrement, { productId: String(product.id), currentBid: currentBid, bidCount: bidCount, currency: currency, isEnded: isEnded, tiers: bidIncrementTiers, minBidIncrementOverride: minBidIncrementOverride }))] }), !isEnded && endDate && (_jsxs(Text, { align: "center", size: "xs", color: "muted", children: ["Ends in ", _jsx(CountdownDisplay, { targetDate: endDate, format: "auto", expiredLabel: "Ended" })] })), _jsx(PlaceBidModalButton, { productId: String(product.id), currentBid: currentBid, startingBid: startingBid, tiers: bidIncrementTiers, minBidIncrementOverride: minBidIncrementOverride, currency: currency, isEnded: isEnded, auctionEndDate: endDate, buyNowPrice: buyNowPrice, bidCount: bidCount, tags: tags, onPlaceBid: onPlaceBid, onBuyNow: onBuyNow, triggerClassName: "w-full" })] }) })) : (
219
+ } }), _jsxs(Container, { size: "xl", padding: "y-lg", children: [_jsxs(Row, { className: "mb-4", align: "center", justify: "between", gap: "sm", wrap: true, children: [_jsxs(Nav, { "aria-label": "Breadcrumb", layout: "flex-wrap", gap: "2xs", textSize: "xs", color: "muted", children: [_jsx(Link, { href: String(ROUTES.HOME), className: CLS_BREADCRUMB_LINK, children: "Home" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.AUCTIONS), className: CLS_BREADCRUMB_LINK, children: "Auctions" }), category && (_jsxs(_Fragment, { children: [_jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category)), className: CLS_BREADCRUMB_LINK, children: categoryName || category })] })), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Span, { className: "truncate max-w-[200px]", color: "muted", children: title })] }), _jsx(ShareButton, { title: title })] }), _jsx(AuctionDetailView, { renderGallery: () => (_jsx(ProductGalleryClient, { images: images, video: productVideo, productName: title })), renderInfo: () => renderAuctionInfoPanel({ productId: String(product.id), title, currentBid, currency, bidCount, isEnded, endDate, buyNowPrice, buyNowAvailable, featured, freeShipping, condition, finalSale: isFinalSale(p), category, categoryName, brand, brandSlug, productFeatures, features, descriptionHtml, safeSeller, storeHref }), renderBidForm: () => onPlaceBid ? (_jsx(Stack, { id: "auction-bid-form", gap: "3", children: _jsxs(Stack, { className: `${__P.p5}`, border: "subtle", gap: "3", rounded: "xl", surface: "muted", children: [_jsxs(Row, { justify: "between", align: "baseline", children: [_jsx(LiveBidPrice, { productId: String(product.id), currentBid: currentBid, bidCount: bidCount, currency: currency, isEnded: isEnded, priceSize: "xl", badgeSize: "xs" }), !isEnded && (_jsx(LiveMinIncrement, { productId: String(product.id), currentBid: currentBid, bidCount: bidCount, currency: currency, isEnded: isEnded, tiers: bidIncrementTiers, minBidIncrementOverride: minBidIncrementOverride }))] }), !isEnded && endDate && (_jsxs(Text, { align: "center", size: "xs", color: "muted", children: ["Ends in ", _jsx(CountdownDisplay, { targetDate: endDate, format: "auto", expiredLabel: "Ended" })] })), _jsx(PlaceBidModalButton, { productId: String(product.id), currentBid: currentBid, startingBid: startingBid, tiers: bidIncrementTiers, minBidIncrementOverride: minBidIncrementOverride, currency: currency, isEnded: isEnded, auctionEndDate: endDate, buyNowPrice: buyNowPrice, bidCount: bidCount, tags: tags, onPlaceBid: onPlaceBid, onBuyNow: onBuyNow, triggerClassName: "w-full" })] }) })) : (
219
220
  /* Read-only bid panel — shown when no bid action is wired (preview/demo) */
220
221
  _jsxs(Stack, { className: `${__P.p5}`, border: "subtle", gap: "md", rounded: "xl", surface: "muted", children: [_jsx(Stack, { gap: "xs", children: _jsxs(Text, { size: "xs", color: "muted", children: ["Starting bid: ", formatCurrency(startingBid, currency), !isEnded && _jsxs(_Fragment, { children: [" \u00B7 min increment ", formatCurrency(initialMinBidIncrement, currency)] })] }) }), _jsxs(Stack, { gap: "sm", children: [_jsx(Input, { type: "number", placeholder: `At least ${formatCurrency(initialMinBid, currency)}`, min: initialMinBid, "aria-label": "Your bid amount", disabled: isEnded }), !isEnded && endDate && (_jsxs(Text, { align: "center", size: "xs", color: "muted", children: ["Ends in ", _jsx(CountdownDisplay, { targetDate: endDate, format: "auto", expiredLabel: "Ended" })] })), _jsx(Button, { variant: "primary", size: "md", className: "w-full", disabled: isEnded, children: isEnded ? "Auction Ended" : "Place Bid" }), buyNowAvailable && buyNowPrice !== null && (_jsxs(Text, { align: "center", size: "xs", color: "muted", children: ["Buy Now available at ", formatCurrency(buyNowPrice, currency)] }))] }), tags.length > 0 && (_jsx(Div, { border: "default", className: "border-t", padding: "t-md", children: _jsx(Row, { wrap: true, gap: "xs", children: tags.map((tag) => (_jsx(Span, { size: "xs", rounded: "full", padding: "pill-sm-tall", surface: "subtle", color: "muted", children: tag }, tag))) }) }))] })), renderMobileBidForm: () => !isEnded && onPlaceBid ? (_jsxs(Stack, { className: `lg:hidden ${__P.p4}`, border: "subtle", gap: "3", rounded: "xl", surface: "muted", children: [_jsx(LiveBidPrice, { productId: String(product.id), currentBid: currentBid, bidCount: bidCount, currency: currency, isEnded: isEnded, priceSize: "base", badgeSize: "xs" }), endDate && (_jsxs(Text, { align: "center", size: "xs", color: "muted", children: ["Ends in ", _jsx(CountdownDisplay, { targetDate: endDate, format: "auto", expiredLabel: "Ended" })] })), _jsx(PlaceBidModalButton, { productId: String(product.id), currentBid: currentBid, startingBid: startingBid, tiers: bidIncrementTiers, minBidIncrementOverride: minBidIncrementOverride, currency: currency, isEnded: isEnded, auctionEndDate: endDate, buyNowPrice: buyNowPrice, bidCount: bidCount, tags: tags, onPlaceBid: onPlaceBid, onBuyNow: onBuyNow, triggerClassName: "w-full" })] })) : !isEnded ? (_jsxs(Div, { className: `${__P.p4} lg:hidden`, border: "subtle", rounded: "xl", surface: "muted", children: [_jsxs(Row, { align: "center", gap: "sm", className: "mb-3", children: [_jsx(Span, { size: "base", weight: "bold", className: "text-primary-600 dark:text-primary-400", children: formatCurrency(currentBid, currency) }), _jsxs(Span, { size: "xs", color: "muted", children: [bidCount, " bids"] })] }), endDate && (_jsxs(Text, { align: "center", className: "mb-3", size: "xs", color: "muted", children: ["Ends in ", _jsx(CountdownDisplay, { targetDate: endDate, format: "auto", expiredLabel: "Ended" })] })), _jsx(Button, { variant: "primary", size: "md", className: "w-full", children: "Place Bid" })] })) : null, renderSublistingSection: sublistingCategoryId
221
222
  ? () => (_jsx(SublistingCarouselSection, { sublistingCategoryId: sublistingCategoryId, currentListingId: String(product.id) }))
@@ -1,17 +1,17 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { normalizeError } from "../../../errors/normalize";
4
- import { useEffect, useState, useCallback } from "react";
4
+ import { useCallback, useMemo, useState } from "react";
5
5
  import { z } from "zod";
6
- import { Button, Div, Form, Heading, Input, Span, Stack, Text, Textarea } from "../../../ui";
7
- import { useFormShellState, FormShellContext, FormErrorSummary } from "../../../ui/forms";
6
+ import { Button, Div, Heading, Stack, Text } from "../../../ui";
7
+ import { useFormShellState, FormShellContext, FormErrorSummary, FieldInput, FieldTextarea, } from "../../../ui/forms";
8
+ import { SectionForm, useSectionFormNav } from "../../shell";
8
9
  const contactFormSchema = z.object({
9
10
  name: z.string().min(1, "Name is required"),
10
11
  email: z.string().email("Enter a valid email address"),
11
12
  subject: z.string().min(1, "Subject is required"),
12
13
  message: z.string().min(10, "Message must be at least 10 characters"),
13
14
  });
14
- const CLS_FIELD_ERROR = "text-error";
15
15
  const CLS_ERROR_BANNER = "rounded-lg bg-error-surface dark:bg-error-surface border border-error dark:border-error px-[var(--appkit-space-4)] py-[var(--appkit-space-3)] text-[length:var(--appkit-text-sm)] text-error dark:text-error";
16
16
  const INITIAL_FORM = {
17
17
  name: "",
@@ -23,30 +23,39 @@ export function ContactForm({ onSubmit, labels = {}, className = "", }) {
23
23
  const [form, setForm] = useState(INITIAL_FORM);
24
24
  const [isSubmitting, setIsSubmitting] = useState(false);
25
25
  const [submitted, setSubmitted] = useState(false);
26
- const [errors, setErrors] = useState({});
27
26
  const [submitError, setSubmitError] = useState(null);
28
- const { shellCtx, validate: validateSchema } = useFormShellState(contactFormSchema);
29
- useEffect(() => {
30
- validateSchema(form);
31
- // eslint-disable-next-line react-hooks/exhaustive-deps
32
- }, [form, validateSchema]);
33
- const validate = () => {
34
- const newErrors = {};
35
- if (!form.name.trim())
36
- newErrors.name = labels.validationRequired ?? "Required";
37
- if (!form.email.trim() || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email))
38
- newErrors.email = labels.validationEmail ?? "Valid email required";
39
- if (!form.subject.trim())
40
- newErrors.subject = labels.validationRequired ?? "Required";
41
- if (!form.message.trim() || form.message.length < 10)
42
- newErrors.message =
43
- labels.validationMessageTooShort ?? "Message too short";
44
- setErrors(newErrors);
45
- return Object.keys(newErrors).length === 0;
46
- };
47
- const handleSubmit = useCallback(async (e) => {
48
- e.preventDefault();
49
- if (!validate())
27
+ const update = useCallback((partial) => setForm((f) => ({ ...f, ...partial })), []);
28
+ /*
29
+ * One section, `required` — so after the 2026-09-04 collapse fix it renders
30
+ * as a plain heading with no chevron and cannot be hidden. A contact form
31
+ * has nothing optional to fold away; the section exists to give the fields
32
+ * an owner for error attribution, not to be collapsible.
33
+ */
34
+ const sections = useMemo(() => [
35
+ {
36
+ id: "message",
37
+ label: labels.title ?? "Send us a message",
38
+ required: true,
39
+ fields: ["name", "email", "subject", "message"],
40
+ render: ({ values, onChange }) => (_jsxs(Stack, { gap: "md", children: [_jsx(FieldInput, { name: "name", label: labels.nameLabel ?? "Your Name", required: true, value: values.name, onChange: (v) => onChange({ name: v }), placeholder: labels.namePlaceholder ?? "Enter your name" }), _jsx(FieldInput, { name: "email", type: "email", label: labels.emailLabel ?? "Email", required: true, value: values.email, onChange: (v) => onChange({ email: v }), placeholder: labels.emailPlaceholder ?? "your@email.com" }), _jsx(FieldInput, { name: "subject", label: labels.subjectLabel ?? "Subject", required: true, value: values.subject, onChange: (v) => onChange({ subject: v }), placeholder: labels.subjectPlaceholder ?? "How can we help?" }), _jsx(FieldTextarea, { name: "message", label: labels.messageLabel ?? "Message", required: true, rows: 5, value: values.message, onChange: (v) => onChange({ message: v }), placeholder: labels.messagePlaceholder ?? "Tell us more…" })] })),
41
+ },
42
+ ], [labels]);
43
+ const nav = useSectionFormNav(sections, form);
44
+ const { shellCtx, validate: validateSchema } = useFormShellState(contactFormSchema, {
45
+ sections: nav.sectionMeta,
46
+ onGoToSection: nav.goToSection,
47
+ fieldToSectionIndex: nav.fieldToSectionIndex,
48
+ });
49
+ const handleSubmit = useCallback(async () => {
50
+ /*
51
+ * The hand-rolled `validate()` this replaces re-stated every rule the
52
+ * schema already had — required-ness, the email regex, the 10-character
53
+ * minimum — in a second place that could drift from it, and wrote the
54
+ * results into a `useState` errors map instead of the form context
55
+ * (Rule #9.2 and #9.4). `validateSchema` parses and pipes the issues to
56
+ * the fields, which is the same check the submit gate now uses.
57
+ */
58
+ if (!validateSchema(form))
50
59
  return;
51
60
  setIsSubmitting(true);
52
61
  setSubmitError(null);
@@ -64,12 +73,18 @@ export function ContactForm({ onSubmit, labels = {}, className = "", }) {
64
73
  }
65
74
  },
66
75
  // eslint-disable-next-line react-hooks/exhaustive-deps
67
- [form, onSubmit, labels]);
76
+ [form, onSubmit, labels, validateSchema]);
68
77
  if (submitted) {
69
78
  return (_jsxs(Div, { className: `text-center ${className}`, padding: "y-3xl", children: [_jsx(Div, { textSize: "5xl", className: "mb-4", children: "\u2705" }), _jsx(Heading, { level: 2, className: "mb-2", children: labels.successTitle ?? "Message Sent!" }), _jsx(Text, { variant: "secondary", className: "mb-6", children: labels.successDescription ?? "We'll get back to you shortly." }), _jsx(Button, { type: "button", variant: "outline", onClick: () => setSubmitted(false), children: labels.sendAnotherButton ?? "Send another message" })] }));
70
79
  }
71
- const field = (id, label, placeholder, multiline) => (_jsxs(Stack, { gap: "xs", children: [_jsx("label", { htmlFor: id, className: "text-[length:var(--appkit-text-sm)] font-medium text-[var(--appkit-color-text-muted)]", children: label }), multiline ? (_jsx(Textarea, { id: id, value: form[id], onChange: (e) => setForm((f) => ({ ...f, [id]: e.target.value })), placeholder: placeholder, rows: 5, className: "w-full rounded-lg border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] px-[var(--appkit-space-3)] py-[var(--appkit-space-2)] text-[length:var(--appkit-text-sm)] outline-none focus:ring-2 ring-primary/20 transition" })) : (_jsx(Input, { id: id, type: id === "email" ? "email" : "text", value: form[id], onChange: (e) => setForm((f) => ({ ...f, [id]: e.target.value })), placeholder: placeholder, className: "h-10" })), errors[id] && _jsx(Span, { size: "xs", className: CLS_FIELD_ERROR, children: errors[id] })] }));
72
- return (_jsx(FormShellContext.Provider, { value: shellCtx, children: _jsxs(Form, { onSubmit: handleSubmit, noValidate: true, className: className, spacing: "md", children: [_jsx(Heading, { level: 2, className: "mb-6", children: labels.title ?? "Send us a message" }), submitError && (_jsx(Div, { className: CLS_ERROR_BANNER, children: submitError })), field("name", labels.nameLabel ?? "Your Name", labels.namePlaceholder ?? "Enter your name"), field("email", labels.emailLabel ?? "Email", labels.emailPlaceholder ?? "your@email.com"), field("subject", labels.subjectLabel ?? "Subject", labels.subjectPlaceholder ?? "How can we help?"), field("message", labels.messageLabel ?? "Message", labels.messagePlaceholder ?? "Tell us more…", true), _jsx(FormErrorSummary, {}), _jsx(Button, { type: "submit", variant: "primary", disabled: isSubmitting, className: "w-full sm:w-auto", children: isSubmitting
80
+ return (_jsx(FormShellContext.Provider, { value: shellCtx, children: _jsxs(Stack, { gap: "md", className: className, children: [submitError && _jsx(Div, { className: CLS_ERROR_BANNER, children: submitError }), _jsx(FormErrorSummary, {}), _jsx(SectionForm, { sections: sections, values: form, onChange: update, onSubmit: handleSubmit, schema: contactFormSchema, openIds: nav.openIds, onOpenChange: nav.setOpenIds, isLoading: isSubmitting, submitLabel: isSubmitting
73
81
  ? (labels.submittingButton ?? "Sending…")
74
- : (labels.submitButton ?? "Send Message") })] }) }));
82
+ : (labels.submitButton ?? "Send Message"),
83
+ /*
84
+ * No pinned mobile bar. This form is embedded in a marketing page
85
+ * beside other content rather than owning the screen, so a Save bar
86
+ * fixed to the bottom of the viewport would follow the visitor
87
+ * around the rest of the page.
88
+ */
89
+ bottomBar: false })] }) }));
75
90
  }