@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.
- package/dist/_internal/server/features/checkout/actions.js +9 -1
- package/dist/_internal/server/features/checkout/expand-order-items.js +9 -0
- package/dist/_internal/server/features/media/contextGuards.d.ts +3 -3
- package/dist/_internal/server/features/media/contextGuards.js +19 -5
- package/dist/_internal/server/features/orders/actions.d.ts +9 -0
- package/dist/_internal/server/features/orders/actions.js +47 -4
- package/dist/_internal/server/features/orders/adapters.js +10 -0
- package/dist/_internal/server/features/refunds/actions.d.ts +9 -1
- package/dist/_internal/server/features/refunds/actions.js +36 -3
- package/dist/_internal/server/features/site-settings/adapters.js +1 -1
- package/dist/_internal/shared/features/orders/refund-copy.d.ts +12 -0
- package/dist/_internal/shared/features/orders/refund-copy.js +12 -0
- package/dist/_internal/shared/features/orders/return-reasons.d.ts +59 -0
- package/dist/_internal/shared/features/orders/return-reasons.js +96 -0
- package/dist/_internal/shared/features/orders/schema.d.ts +30 -0
- package/dist/_internal/shared/features/orders/schema.js +20 -0
- package/dist/_internal/shared/features/products/schema.d.ts +12 -0
- package/dist/_internal/shared/features/products/schema.js +4 -1
- package/dist/_internal/shared/media/limits.d.ts +14 -0
- package/dist/_internal/shared/media/limits.js +29 -0
- package/dist/client.d.ts +9 -1
- package/dist/client.js +19 -1
- package/dist/features/about/components/PolicyPageView.js +1 -1
- package/dist/features/admin/components/AdminProductEditorView.js +6 -1
- package/dist/features/admin/components/AdminSiteSettingsView.js +2 -6
- package/dist/features/admin/components/index.d.ts +0 -2
- package/dist/features/admin/components/index.js +0 -1
- package/dist/features/admin/schemas/admin-editor-forms.js +1 -1
- package/dist/features/admin/schemas/firestore.d.ts +0 -2
- package/dist/features/auctions/components/AuctionDetailPageView.js +4 -3
- package/dist/features/contact/components/ContactForm.js +46 -31
- package/dist/features/media/upload/MediaUploadList.d.ts +16 -1
- package/dist/features/media/upload/MediaUploadList.js +58 -8
- package/dist/features/orders/components/RefundRequestView.d.ts +26 -10
- package/dist/features/orders/components/RefundRequestView.js +108 -24
- package/dist/features/orders/schemas/firestore.d.ts +42 -0
- package/dist/features/orders/types/index.d.ts +24 -0
- package/dist/features/pre-orders/components/PreOrderDetailPageView.js +5 -3
- package/dist/features/products/components/ProductDetailPageView.js +14 -2
- package/dist/features/products/components/ProductFeatureBadges.d.ts +9 -1
- package/dist/features/products/components/ProductFeatureBadges.js +17 -1
- package/dist/features/products/components/ProductForm.js +46 -10
- package/dist/features/products/components/ProductGrid.js +10 -1
- package/dist/features/products/constants/final-sale.d.ts +40 -0
- package/dist/features/products/constants/final-sale.js +40 -0
- package/dist/features/products/index.d.ts +1 -0
- package/dist/features/products/index.js +1 -0
- package/dist/features/products/schemas/firestore.d.ts +19 -2
- package/dist/features/products/schemas/firestore.js +4 -0
- package/dist/features/products/types/index.d.ts +2 -0
- package/dist/features/seller/components/SellerProductShell.d.ts +7 -0
- package/dist/features/seller/components/SellerProductShell.js +82 -11
- package/dist/features/seller/index.d.ts +1 -0
- package/dist/features/seller/index.js +1 -0
- package/dist/features/seller/schemas/product-form.d.ts +304 -0
- package/dist/features/seller/schemas/product-form.js +132 -0
- package/dist/features/seller/utils/product-draft-mapping.d.ts +117 -0
- package/dist/features/seller/utils/product-draft-mapping.js +147 -0
- package/dist/features/shell/SectionForm.js +62 -7
- package/dist/features/shell/form-sections.js +4 -0
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +200 -0
- package/dist/index.d.ts +6 -3
- package/dist/index.js +12 -3
- package/dist/next/routing/route-map.d.ts +2 -0
- package/dist/next/routing/route-map.js +1 -0
- package/dist/schemas/registry.d.ts +271 -0
- package/dist/schemas/registry.js +9 -0
- package/dist/seed/_helpers/final-sale-fixtures.d.ts +35 -0
- package/dist/seed/_helpers/final-sale-fixtures.js +61 -0
- package/dist/seed/products-art-seed-data.js +9 -1
- package/dist/seed/products-auctions-seed-data.js +2 -1
- package/dist/seed/products-classifieds-seed-data.js +2 -1
- package/dist/seed/products-digital-codes-seed-data.js +2 -1
- package/dist/seed/products-live-items-seed-data.js +2 -1
- package/dist/seed/products-preorders-seed-data.d.ts +2 -138
- package/dist/seed/products-preorders-seed-data.js +2 -1
- package/dist/seed/products-prize-draws-seed-data.js +2 -1
- package/dist/seed/products-standard-seed-data.js +2 -1
- package/dist/seed/products-stickers-seed-data.js +9 -1
- package/dist/server.d.ts +6 -2
- package/dist/server.js +15 -2
- package/dist/styles.css +1 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/tokens/motion.d.ts +12 -0
- package/dist/tokens/motion.js +12 -0
- package/dist/ui/components/CollapsibleSection.d.ts +18 -1
- package/dist/ui/components/CollapsibleSection.js +20 -3
- package/dist/ui/components/Motion.js +17 -1
- 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(
|
|
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
|
|
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,
|
|
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
|
|
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 {
|
|
4
|
+
import { useCallback, useMemo, useState } from "react";
|
|
5
5
|
import { z } from "zod";
|
|
6
|
-
import { Button, Div,
|
|
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
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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
|
}
|