@mohasinac/appkit 4.71.2 → 4.72.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/.build-stamp.json +3 -3
- package/dist/constants/api-endpoints.d.ts +3 -0
- package/dist/constants/api-endpoints.js +6 -0
- package/dist/features/classified/components/ClassifiedDetailPageView.js +2 -2
- package/dist/features/digital-codes/components/DigitalCodeDetailPageView.js +2 -2
- package/dist/features/events/components/EntryVoteButtons.d.ts +19 -0
- package/dist/features/events/components/EntryVoteButtons.js +65 -0
- package/dist/features/events/repository/entry-votes.repository.d.ts +45 -0
- package/dist/features/events/repository/entry-votes.repository.js +154 -0
- package/dist/features/events/schemas/entry-votes.d.ts +134 -0
- package/dist/features/events/schemas/entry-votes.js +114 -0
- package/dist/features/events/schemas/index.d.ts +29 -7
- package/dist/features/events/schemas/index.js +29 -2
- package/dist/features/inventory/components/AdminMoveLedgerView.d.ts +1 -1
- package/dist/features/live/components/LiveItemDetailPageView.js +2 -2
- package/dist/features/pre-orders/components/PreOrderDetailPageView.js +3 -3
- package/dist/features/products/components/ClearanceDetailPageView.js +2 -2
- package/dist/features/products/components/index.d.ts +0 -2
- package/dist/features/products/components/index.js +0 -1
- package/dist/features/products/schemas/index.d.ts +6 -7
- package/dist/features/products/schemas/index.js +20 -2
- package/dist/features/promotions/schemas/firestore.d.ts +17 -1
- package/dist/features/promotions/schemas/firestore.js +22 -0
- package/dist/features/promotions/schemas/index.d.ts +45 -21
- package/dist/features/promotions/schemas/index.js +37 -8
- package/dist/features/promotions/types/index.d.ts +1 -1
- package/dist/features/reviews/schemas/index.d.ts +3 -0
- package/dist/features/reviews/schemas/index.js +25 -0
- package/dist/index.d.ts +0 -2
- package/dist/index.js +0 -3
- package/dist/next/routing/portals.d.ts +22 -0
- package/dist/next/routing/portals.js +43 -0
- package/dist/providers/db-firebase/admin.d.ts +2 -0
- package/dist/providers/db-firebase/admin.js +25 -2
- package/dist/schemas/registry.d.ts +11 -8
- package/dist/styles.css +1 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/ListingDetailShell.d.ts +68 -0
- package/dist/ui/components/ListingDetailShell.js +32 -0
- package/package.json +533 -525
- package/scripts/seed-cli.mjs +32 -2
- package/dist/features/products/components/PreOrderDetailView.d.ts +0 -18
- package/dist/features/products/components/PreOrderDetailView.js +0 -12
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Entry votes — like/dislike on a CONTEST ENTRY, and who cast them.
|
|
3
|
+
*
|
|
4
|
+
* ## Why a sibling of `likes.ts` rather than a field on the entry
|
|
5
|
+
*
|
|
6
|
+
* Deliberately the same shape as `EventLikeDocument`, for the same reasons its
|
|
7
|
+
* header gives: the document id IS the idempotency key, so a repeat vote
|
|
8
|
+
* overwrites itself, a retraction is a `.delete()`, and "how did I vote" is a
|
|
9
|
+
* single `.doc(id).get()` with no query, no index and no transaction.
|
|
10
|
+
*
|
|
11
|
+
* The rejected alternative is the same one: a `votedBy[]` array on the entry.
|
|
12
|
+
* It caps out at Firestore's 1 MiB document limit — a real ceiling for a
|
|
13
|
+
* popular entry and a silent one — and it has no clean un-vote.
|
|
14
|
+
*
|
|
15
|
+
* ## 🛑 The difference from a like: a vote has a VALUE
|
|
16
|
+
*
|
|
17
|
+
* `likes.ts` stores existence — the row is the like. A vote stores a
|
|
18
|
+
* direction, so the row carries `value` and changing your mind is a `.set()`
|
|
19
|
+
* over the same id rather than a delete-then-create. That keeps "one vote per
|
|
20
|
+
* user per entry" true by construction instead of by a rule somebody enforces.
|
|
21
|
+
*
|
|
22
|
+
* ## 🛑 Who may see WHO voted
|
|
23
|
+
*
|
|
24
|
+
* Owner, 2026-11-08: *"only admins see who voted, rest see numbers"*.
|
|
25
|
+
*
|
|
26
|
+
* So the voter list is an admin projection and the counts are the public one.
|
|
27
|
+
* This file stores only `userId` — never a denormalised name or email — for
|
|
28
|
+
* exactly the reason `likes.ts` states: a public surface reads the counts, and
|
|
29
|
+
* the cheapest way to never leak a field is to not store it. An admin screen
|
|
30
|
+
* joins the user's own public projection at read time.
|
|
31
|
+
*
|
|
32
|
+
* ## 🛑 Ranking is a CONFIG RULE, never a fixed formula
|
|
33
|
+
*
|
|
34
|
+
* This file first shipped with `entryScore = likes - dislikes` hardcoded, which
|
|
35
|
+
* is the exact mistake the rework exists to remove: a rule baked into code that
|
|
36
|
+
* an admin then cannot change. There are several legitimate situations — a
|
|
37
|
+
* like-only popularity contest, a net-score vote, a straight turnout count, a
|
|
38
|
+
* jury pick — and the event, not the source, decides which applies.
|
|
39
|
+
*
|
|
40
|
+
* So the rule is a value on the event and `resolveEntryRanking` is the one
|
|
41
|
+
* place it is interpreted. Adding a fifth rule is a case here plus a config
|
|
42
|
+
* option; it is never a second copy of the arithmetic somewhere else.
|
|
43
|
+
*
|
|
44
|
+
* @tag domain:events
|
|
45
|
+
* @tag layer:schema
|
|
46
|
+
*/
|
|
47
|
+
export const EVENT_ENTRY_VOTES_COLLECTION = "eventEntryVotes";
|
|
48
|
+
/**
|
|
49
|
+
* The deterministic id — one row per user per entry.
|
|
50
|
+
*
|
|
51
|
+
* 🛑 `__` matches `eventLikeId`, `courierVoteId` and the tester-response id.
|
|
52
|
+
* Firestore ids may not contain `/`, and a single `_` appears inside real
|
|
53
|
+
* uids; a double underscore does not.
|
|
54
|
+
*/
|
|
55
|
+
export function eventEntryVoteId(entryId, userId) {
|
|
56
|
+
return `${entryId}__${userId}`;
|
|
57
|
+
}
|
|
58
|
+
export const EVENT_ENTRY_VOTE_FIELDS = {
|
|
59
|
+
EVENT_ID: "eventId",
|
|
60
|
+
ENTRY_ID: "entryId",
|
|
61
|
+
USER_ID: "userId",
|
|
62
|
+
VALUE: "value",
|
|
63
|
+
CREATED_AT: "createdAt",
|
|
64
|
+
UPDATED_AT: "updatedAt",
|
|
65
|
+
};
|
|
66
|
+
/** The default when an event does not say. Matches "most liked photo wins". */
|
|
67
|
+
export const DEFAULT_ENTRY_RANKING = "most-likes";
|
|
68
|
+
/**
|
|
69
|
+
* The ONE place a ranking rule is interpreted.
|
|
70
|
+
*
|
|
71
|
+
* Returns the sort key, descending — so the gallery, the scoreboard, the winner
|
|
72
|
+
* resolution and any export all order entries identically. Two screens
|
|
73
|
+
* disagreeing about who is winning is the failure this prevents.
|
|
74
|
+
*
|
|
75
|
+
* `manual` returns 0 for every entry on purpose: with no computed order, the
|
|
76
|
+
* caller must fall back to the tie-break (earliest first) rather than invent
|
|
77
|
+
* one, and the UI shows a human decision is pending.
|
|
78
|
+
*/
|
|
79
|
+
export function resolveEntryRanking(rule, entry) {
|
|
80
|
+
switch (rule) {
|
|
81
|
+
case "most-likes":
|
|
82
|
+
return entry.likes;
|
|
83
|
+
case "net-score":
|
|
84
|
+
return entry.likes - entry.dislikes;
|
|
85
|
+
case "most-votes":
|
|
86
|
+
return entry.likes + entry.dislikes;
|
|
87
|
+
case "earliest":
|
|
88
|
+
/*
|
|
89
|
+
* Negated so that, like every other rule, a HIGHER key sorts first —
|
|
90
|
+
* callers sort one way and need no per-rule direction flag.
|
|
91
|
+
*/
|
|
92
|
+
return -entry.submittedAt.getTime();
|
|
93
|
+
case "manual":
|
|
94
|
+
return 0;
|
|
95
|
+
default:
|
|
96
|
+
/*
|
|
97
|
+
* An unknown rule means config names something this build does not
|
|
98
|
+
* implement — a newer rule set against an older deploy. Rank everything
|
|
99
|
+
* equally and let the tie-break order it, which degrades to "earliest
|
|
100
|
+
* first" rather than throwing on a public page. The admin screen is
|
|
101
|
+
* where an unimplemented rule should be surfaced, not the gallery.
|
|
102
|
+
*/
|
|
103
|
+
return 0;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* The published tie-break: the earlier entry wins.
|
|
108
|
+
*
|
|
109
|
+
* Stated here because it has to be announced BEFORE a contest closes. Deciding
|
|
110
|
+
* a tie afterwards, however fairly, looks like deciding it to suit someone.
|
|
111
|
+
*/
|
|
112
|
+
export function entryTieBreak(a, b) {
|
|
113
|
+
return a.submittedAt.getTime() - b.submittedAt.getTime();
|
|
114
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export * from "./firestore";
|
|
2
|
+
export * from "./entry-votes";
|
|
2
3
|
import { z } from "zod";
|
|
3
4
|
/**
|
|
4
5
|
* 🛑 WRITE enum — the current spellings only. The three chance-based types
|
|
@@ -11,7 +12,28 @@ import { z } from "zod";
|
|
|
11
12
|
* `normalizeEventType`, and rewritten by
|
|
12
13
|
* `appkit/scripts/migrate-collector-drop-rename.mjs`.
|
|
13
14
|
*/
|
|
14
|
-
|
|
15
|
+
/**
|
|
16
|
+
* 🛑 OPEN, not a closed enum. Owner, 2026-11-08: *"all event types are also
|
|
17
|
+
* configs … they should not be fixed, nothing in the code should be fixed"*
|
|
18
|
+
* and *"the types we have now are the DEFAULT TEMPLATES"*.
|
|
19
|
+
*
|
|
20
|
+
* The eight below are the shipped templates, kept as a named list so the
|
|
21
|
+
* common ones are discoverable and so a typo in one of THEM is still caught.
|
|
22
|
+
* Anything else is accepted as a slug and resolved against the event-type
|
|
23
|
+
* config (`seed/event-types-seed-data.ts`).
|
|
24
|
+
*
|
|
25
|
+
* The rejected shape was the previous one: a closed `z.enum` that 400s an
|
|
26
|
+
* unknown value. It made a new kind of event — a photo contest judged by votes,
|
|
27
|
+
* say — an enum edit, a schema change and a deploy, which is precisely the
|
|
28
|
+
* coupling the rework removes. The same change is owed to coupons, listings
|
|
29
|
+
* and reviews, which are all still closed today.
|
|
30
|
+
*
|
|
31
|
+
* The slug shape is still enforced, because an unvalidated string here becomes
|
|
32
|
+
* a URL segment and a Firestore query value: lowercase, digits and hyphens,
|
|
33
|
+
* 2-40 characters.
|
|
34
|
+
*/
|
|
35
|
+
export declare const DEFAULT_EVENT_TYPE_TEMPLATES: readonly ["sale", "offer", "poll", "survey", "feedback", "giveaway", "bonus-spin", "random-draw"];
|
|
36
|
+
export declare const eventTypeSchema: z.ZodString;
|
|
15
37
|
export declare const eventStatusSchema: z.ZodEnum<["draft", "active", "paused", "ended", "cancelled"]>;
|
|
16
38
|
export declare const saleConfigSchema: z.ZodObject<{
|
|
17
39
|
discountPercent: z.ZodNumber;
|
|
@@ -448,7 +470,7 @@ export declare const giveawayConfigClientSchema: z.ZodObject<{
|
|
|
448
470
|
export declare const eventItemSchema: z.ZodObject<{
|
|
449
471
|
id: z.ZodString;
|
|
450
472
|
slug: z.ZodOptional<z.ZodString>;
|
|
451
|
-
type: z.
|
|
473
|
+
type: z.ZodString;
|
|
452
474
|
title: z.ZodString;
|
|
453
475
|
description: z.ZodString;
|
|
454
476
|
status: z.ZodEnum<["draft", "active", "paused", "ended", "cancelled"]>;
|
|
@@ -954,7 +976,7 @@ export declare const eventItemSchema: z.ZodObject<{
|
|
|
954
976
|
title: string;
|
|
955
977
|
id: string;
|
|
956
978
|
status: "cancelled" | "draft" | "active" | "paused" | "ended";
|
|
957
|
-
type:
|
|
979
|
+
type: string;
|
|
958
980
|
description: string;
|
|
959
981
|
createdAt: string;
|
|
960
982
|
updatedAt: string;
|
|
@@ -1111,7 +1133,7 @@ export declare const eventItemSchema: z.ZodObject<{
|
|
|
1111
1133
|
title: string;
|
|
1112
1134
|
id: string;
|
|
1113
1135
|
status: "cancelled" | "draft" | "active" | "paused" | "ended";
|
|
1114
|
-
type:
|
|
1136
|
+
type: string;
|
|
1115
1137
|
description: string;
|
|
1116
1138
|
createdAt: string;
|
|
1117
1139
|
updatedAt: string;
|
|
@@ -1264,7 +1286,7 @@ export declare const eventItemSchema: z.ZodObject<{
|
|
|
1264
1286
|
}>;
|
|
1265
1287
|
/** Base Zod schema for list-query parameters. */
|
|
1266
1288
|
export declare const eventListParamsSchema: z.ZodObject<{
|
|
1267
|
-
type: z.ZodOptional<z.
|
|
1289
|
+
type: z.ZodOptional<z.ZodString>;
|
|
1268
1290
|
status: z.ZodOptional<z.ZodEnum<["draft", "active", "paused", "ended", "cancelled"]>>;
|
|
1269
1291
|
q: z.ZodOptional<z.ZodString>;
|
|
1270
1292
|
page: z.ZodOptional<z.ZodNumber>;
|
|
@@ -1274,14 +1296,14 @@ export declare const eventListParamsSchema: z.ZodObject<{
|
|
|
1274
1296
|
sort?: string | undefined;
|
|
1275
1297
|
q?: string | undefined;
|
|
1276
1298
|
status?: "cancelled" | "draft" | "active" | "paused" | "ended" | undefined;
|
|
1277
|
-
type?:
|
|
1299
|
+
type?: string | undefined;
|
|
1278
1300
|
page?: number | undefined;
|
|
1279
1301
|
perPage?: number | undefined;
|
|
1280
1302
|
}, {
|
|
1281
1303
|
sort?: string | undefined;
|
|
1282
1304
|
q?: string | undefined;
|
|
1283
1305
|
status?: "cancelled" | "draft" | "active" | "paused" | "ended" | undefined;
|
|
1284
|
-
type?:
|
|
1306
|
+
type?: string | undefined;
|
|
1285
1307
|
page?: number | undefined;
|
|
1286
1308
|
perPage?: number | undefined;
|
|
1287
1309
|
}>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export * from "./firestore.js";
|
|
2
|
+
export * from "./entry-votes.js";
|
|
2
3
|
import { z } from "zod";
|
|
3
4
|
import { mediaUrlSchema } from "../../../validation/schemas.js";
|
|
4
5
|
import { mediaFieldSchema } from "../../media/types/index.js";
|
|
@@ -23,7 +24,27 @@ const eventCoverImageSchema = z
|
|
|
23
24
|
* `normalizeEventType`, and rewritten by
|
|
24
25
|
* `appkit/scripts/migrate-collector-drop-rename.mjs`.
|
|
25
26
|
*/
|
|
26
|
-
|
|
27
|
+
/**
|
|
28
|
+
* 🛑 OPEN, not a closed enum. Owner, 2026-11-08: *"all event types are also
|
|
29
|
+
* configs … they should not be fixed, nothing in the code should be fixed"*
|
|
30
|
+
* and *"the types we have now are the DEFAULT TEMPLATES"*.
|
|
31
|
+
*
|
|
32
|
+
* The eight below are the shipped templates, kept as a named list so the
|
|
33
|
+
* common ones are discoverable and so a typo in one of THEM is still caught.
|
|
34
|
+
* Anything else is accepted as a slug and resolved against the event-type
|
|
35
|
+
* config (`seed/event-types-seed-data.ts`).
|
|
36
|
+
*
|
|
37
|
+
* The rejected shape was the previous one: a closed `z.enum` that 400s an
|
|
38
|
+
* unknown value. It made a new kind of event — a photo contest judged by votes,
|
|
39
|
+
* say — an enum edit, a schema change and a deploy, which is precisely the
|
|
40
|
+
* coupling the rework removes. The same change is owed to coupons, listings
|
|
41
|
+
* and reviews, which are all still closed today.
|
|
42
|
+
*
|
|
43
|
+
* The slug shape is still enforced, because an unvalidated string here becomes
|
|
44
|
+
* a URL segment and a Firestore query value: lowercase, digits and hyphens,
|
|
45
|
+
* 2-40 characters.
|
|
46
|
+
*/
|
|
47
|
+
export const DEFAULT_EVENT_TYPE_TEMPLATES = [
|
|
27
48
|
"sale",
|
|
28
49
|
"offer",
|
|
29
50
|
"poll",
|
|
@@ -32,7 +53,13 @@ export const eventTypeSchema = z.enum([
|
|
|
32
53
|
"giveaway",
|
|
33
54
|
"bonus-spin",
|
|
34
55
|
"random-draw",
|
|
35
|
-
]
|
|
56
|
+
];
|
|
57
|
+
export const eventTypeSchema = z
|
|
58
|
+
.string()
|
|
59
|
+
.trim()
|
|
60
|
+
.min(2)
|
|
61
|
+
.max(40)
|
|
62
|
+
.regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "Use a lowercase slug, e.g. photo-contest");
|
|
36
63
|
export const eventStatusSchema = z.enum(["draft", "active", "paused", "ended", "cancelled"]);
|
|
37
64
|
export const saleConfigSchema = z.object({
|
|
38
65
|
discountPercent: z.number(),
|
|
@@ -7,6 +7,6 @@ export interface AdminMoveLedgerViewProps {
|
|
|
7
7
|
export declare function AdminMoveLedgerView({ pageRegionCards }?: AdminMoveLedgerViewProps): React.JSX.Element;
|
|
8
8
|
/** The kind chips, derived from the union so a sixth kind cannot be missed. */
|
|
9
9
|
export declare const MOVEMENT_KIND_TABS: {
|
|
10
|
-
id: "move" | "
|
|
10
|
+
id: "move" | "audit" | "receive" | "pick" | "adjust";
|
|
11
11
|
label: string;
|
|
12
12
|
}[];
|
|
@@ -21,7 +21,7 @@ import { Heading, Span, Text } from "../../../ui/components/Typography.js";
|
|
|
21
21
|
import { Main, Nav, Section } from "../../../ui/components/Semantic.js";
|
|
22
22
|
import { RichText } from "../../../ui/rich-text/RichText.js";
|
|
23
23
|
import { SpecTable } from "../../../ui/components/SpecTable.js";
|
|
24
|
-
import {
|
|
24
|
+
import { ListingDetailShell } from "../../../ui/components/ListingDetailShell.js";
|
|
25
25
|
import { ProductGalleryClient } from "../../products/components/ProductGalleryClient.js";
|
|
26
26
|
import { ProductTabsShell } from "../../products/components/ProductTabsShell.js";
|
|
27
27
|
import { toCustomSectionTabs } from "../../products/components/CustomSectionTabContent.js";
|
|
@@ -98,5 +98,5 @@ export async function LiveItemDetailPageView({ slug, initialProduct, renderActio
|
|
|
98
98
|
price: price ?? undefined,
|
|
99
99
|
storeId: typeof p.storeId === "string" ? p.storeId : undefined,
|
|
100
100
|
storeName: storeName ?? undefined,
|
|
101
|
-
} }), _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.LIVE), className: CLS_BREADCRUMB_LINK, children: "Live" }), 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(AdminComposableRegion, { pageId: "live-slug", initialCards: pageRegionCards, functionalSlots: { "pdp-primary-actions": primaryActionsNode } }), _jsx(
|
|
101
|
+
} }), _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.LIVE), className: CLS_BREADCRUMB_LINK, children: "Live" }), 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(AdminComposableRegion, { pageId: "live-slug", initialCards: pageRegionCards, functionalSlots: { "pdp-primary-actions": primaryActionsNode } }), _jsx(ListingDetailShell, { layout: "grid-2", renderGallery: () => (_jsx(SectionVisibilityGate, { surfaceId: "pdp-sections", sectionKey: "gallery", initialVisible: pdpSectionVisibility.gallery !== false, children: _jsx(ProductGalleryClient, { images: images, video: productVideo, productName: title }) })), renderInfo: () => (_jsx(SectionVisibilityGate, { surfaceId: "pdp-sections", sectionKey: "info", initialVisible: pdpSectionVisibility.info !== false, children: _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_ITEM_BADGE, children: "Live Item" }), meta?.vendorVerified && (_jsx(Span, { color: "success", surface: "success-surface", size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", children: "Verified Seller" })), meta?.cites && (_jsxs(Span, { color: "warning", surface: "warning-surface", size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", children: ["CITES: ", meta.cites] }))] }), _jsx(OwnerQuickActions, { resource: "PRODUCT", placement: "detail", recordLabel: String(p.title ?? p.slug ?? p.id), storeId: p.storeId ? String(p.storeId) : undefined, editHref: String(ROUTES.STORE.PRODUCTS_EDIT(String(p.id))) }), _jsx(Heading, { level: 1, className: "leading-snug", smSize: "2xl", color: "primary", size: "xl", weight: "bold", children: title }), meta?.species && (_jsxs(Text, { className: "italic mt-1", color: "muted", size: "sm", children: [meta.species, meta.sex && meta.sex !== "n/a" && ` · ${meta.sex}`, meta.ageMonths !== undefined && ` · ${meta.ageMonths}mo`] }))] }), price !== null && (_jsx(Text, { size: "2xl", weight: "bold", color: "primary", children: _jsx(GatedPrice, { children: formatCurrency(price, currency) }) })), jurisdictions.length > 0 && (_jsxs(Div, { className: CLS_WARN_BOX, children: [_jsx(Text, { className: CLS_WARN_TITLE, children: "Delivery restrictions" }), _jsxs(Text, { className: CLS_WARN_BODY, children: ["This item can only be shipped to: ", jurisdictions.join(", ")] })] })), transport && (_jsxs(Div, { textSize: "sm", border: "subtle", surface: "muted", padding: "md", rounded: "lg", children: [_jsx(Text, { weight: "medium", children: "Transport" }), _jsxs(Text, { className: "text-[var(--appkit-color-text-muted)]", children: ["Method: ", transport.method, transport.handlingFee !== undefined && (_jsx(PricesOnly, { children: ` · Handling: ${formatCurrency(transport.handlingFee, "INR")}` })), transport.insuranceIncluded && " · Insurance included"] })] })), meta?.careInfo && (_jsxs(Div, { textSize: "sm", border: "subtle", surface: "muted", padding: "md", rounded: "lg", children: [_jsx(Text, { weight: "medium", children: "Care information" }), _jsx(Text, { className: "mt-1 text-[var(--appkit-color-text-muted)]", children: meta.careInfo })] })), 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, { border: "subtle", rounded: "xl", surface: "muted", padding: "md", 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: "Sold 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" }))] }) }))] }) })), renderTabs: () => (_jsx(SectionVisibilityGate, { surfaceId: "pdp-sections", sectionKey: "tabs", initialVisible: pdpSectionVisibility.tabs !== false, children: _jsx(ProductTabsShell, { descriptionContent: descriptionHtml ? (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm sm:prose max-w-none dark:prose-invert", className: "text-[var(--appkit-color-text-muted)]" })) : undefined, specsContent: specs.length > 0 ? (_jsx(SpecTable, { rows: specs })) : undefined, reviewsContent: _jsx(ReviewsListingPanel, { source: { kind: "product", productId: product.id }, stateMode: "local", context: "listing", initialData: initialReviews, emptyLabel: "No reviews yet \u2014 be the first to review this product." }), customTabs: toCustomSectionTabs(p.customSections) }) })), renderAside: () => (_jsx(SectionVisibilityGate, { surfaceId: "pdp-sections", sectionKey: "actions", initialVisible: pdpSectionVisibility.actions !== false, children: _jsx(Div, { id: "live-item-buy-bar", children: !hasPrimaryActionsSlotOverride && primaryActionsNode }) })), renderRelated: () => (_jsxs(Stack, { gap: "xl", children: [_jsx(PurchaseReviewAffordance, { productId: String(product.id), storeId: product.storeId ? String(product.storeId) : undefined }), _jsx(SectionVisibilityGate, { surfaceId: "pdp-sections", sectionKey: "grouped-listings", initialVisible: pdpSectionVisibility["grouped-listings"] !== false, children: _jsx(GroupedListingsCarousel, { groups: groups }) }), _jsx(SectionVisibilityGate, { surfaceId: "pdp-sections", sectionKey: "related", initialVisible: pdpSectionVisibility.related !== false, children: _jsx(RelatedItemsSection, { relatedItems: relatedItems, relatedByBrand: relatedByBrand, relatedByTags: relatedByTags, relatedByStore: relatedByStore, categoryLabel: categoryName || category || undefined, brandLabel: brand, storeLabel: storeName ?? undefined }) })] })) }), _jsx(ListingBottomActions, { listingType: "live", anchorId: "live-item-buy-bar", price: price, currency: currency })] })] }));
|
|
102
102
|
}
|
|
@@ -28,7 +28,7 @@ import { Heading, Span, Text } from "../../../ui/components/Typography.js";
|
|
|
28
28
|
import { Main, Nav, Section, Ul, Li } from "../../../ui/components/Semantic.js";
|
|
29
29
|
import { RichText } from "../../../ui/rich-text/RichText.js";
|
|
30
30
|
import { SpecTable } from "../../../ui/components/SpecTable.js";
|
|
31
|
-
import {
|
|
31
|
+
import { ListingDetailShell } from "../../../ui/components/ListingDetailShell.js";
|
|
32
32
|
import { PreOrderBottomActions } from "./PreOrderBottomActions.js";
|
|
33
33
|
import { ProductTabsShell } from "../../products/components/ProductTabsShell.js";
|
|
34
34
|
import { toCustomSectionTabs } from "../../products/components/CustomSectionTabContent.js";
|
|
@@ -185,9 +185,9 @@ export async function PreOrderDetailPageView({ id, initialPreOrder, onReserveNow
|
|
|
185
185
|
price: typeof p.price === "number" ? p.price : undefined,
|
|
186
186
|
storeId: typeof p.storeId === "string" ? p.storeId : undefined,
|
|
187
187
|
storeName: typeof p.storeName === "string" ? p.storeName : undefined,
|
|
188
|
-
} }), _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.PRE_ORDERS), className: CLS_BREADCRUMB_LINK, children: "Pre-Orders" }), 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(AdminComposableRegion, { pageId: "pre-orders-id", initialCards: pageRegionCards, functionalSlots: { "pdp-primary-actions": primaryActionsNode } }), _jsx(
|
|
188
|
+
} }), _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.PRE_ORDERS), className: CLS_BREADCRUMB_LINK, children: "Pre-Orders" }), 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(AdminComposableRegion, { pageId: "pre-orders-id", initialCards: pageRegionCards, functionalSlots: { "pdp-primary-actions": primaryActionsNode } }), _jsx(ListingDetailShell, { layout: "grid-2", renderGallery: () => (_jsx(SectionVisibilityGate, { surfaceId: "pdp-sections", sectionKey: "gallery", initialVisible: pdpSectionVisibility.gallery !== false, children: _jsx(ProductGalleryClient, { images: images, video: productVideo, productName: title }) })), renderInfo: () => (_jsx(SectionVisibilityGate, { surfaceId: "pdp-sections", sectionKey: "info", initialVisible: pdpSectionVisibility.info !== false, children: _jsx(PreOrderInfoSection, { title: title, productId: String(product.id), storeId: product.storeId ? String(product.storeId) : undefined, productionStatus: productionStatus, maxPerUser: maxPerUser, deliveryDate: deliveryDate, featured: featured, freeShipping: freeShipping, condition: condition, isCancellable: isCancellable, finalSale: isFinalSale(p), category: category, categoryName: categoryName, brand: brand, brandSlug: brandSlug, productFeatures: productFeatures, features: features, descriptionHtml: descriptionHtml, safeSeller: safeSeller, storeHref: storeHref }) })), renderSublistingSection: sublistingCategoryId
|
|
189
189
|
? () => (_jsx(SublistingCarouselSection, { sublistingCategoryId: sublistingCategoryId, currentListingId: String(product.id) }))
|
|
190
190
|
: undefined, renderGroupSection: groupId
|
|
191
191
|
? () => (_jsx(ShowGroupSection, { groupId: groupId, currentSlug: String(p.slug ?? product.id), isParent: isGroupParent, groupTitle: groupTitle }))
|
|
192
|
-
: undefined, renderTabs: () => (_jsx(SectionVisibilityGate, { surfaceId: "pdp-sections", sectionKey: "tabs", initialVisible: pdpSectionVisibility.tabs !== false, children: _jsx(ProductTabsShell, { descriptionContent: descriptionHtml ? (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm sm:prose max-w-none dark:prose-invert", className: "text-[var(--appkit-color-text-muted)]" })) : undefined, specsContent: specs.length > 0 ? (_jsx(SpecTable, { rows: specs })) : undefined, customTabs: toCustomSectionTabs(p.customSections) }) })),
|
|
192
|
+
: undefined, renderTabs: () => (_jsx(SectionVisibilityGate, { surfaceId: "pdp-sections", sectionKey: "tabs", initialVisible: pdpSectionVisibility.tabs !== false, children: _jsx(ProductTabsShell, { descriptionContent: descriptionHtml ? (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm sm:prose max-w-none dark:prose-invert", className: "text-[var(--appkit-color-text-muted)]" })) : undefined, specsContent: specs.length > 0 ? (_jsx(SpecTable, { rows: specs })) : undefined, customTabs: toCustomSectionTabs(p.customSections) }) })), renderAside: () => (_jsx(SectionVisibilityGate, { surfaceId: "pdp-sections", sectionKey: "actions", initialVisible: pdpSectionVisibility.actions !== false, children: !hasPrimaryActionsSlotOverride && primaryActionsNode })), renderRelated: () => (_jsxs(Stack, { gap: "xl", children: [_jsx(PurchaseReviewAffordance, { productId: String(product.id), storeId: product.storeId ? String(product.storeId) : undefined }), _jsx(SectionVisibilityGate, { surfaceId: "pdp-sections", sectionKey: "grouped-listings", initialVisible: pdpSectionVisibility["grouped-listings"] !== false, children: _jsx(GroupedListingsCarousel, { groups: groups }) }), _jsx(SectionVisibilityGate, { surfaceId: "pdp-sections", sectionKey: "related", initialVisible: pdpSectionVisibility.related !== false, children: _jsx(RelatedItemsSection, { relatedItems: relatedItems, relatedByBrand: relatedByBrand, relatedByTags: relatedByTags, relatedByStore: relatedByStore, categoryLabel: categoryName || category || undefined, brandLabel: brand || undefined, storeLabel: storeName || undefined }) })] })) }), _jsx(PreOrderBottomActions, { price: price, currency: currency })] })] }));
|
|
193
193
|
}
|
|
@@ -20,7 +20,7 @@ import { Div } from "../../../ui/components/Div.js";
|
|
|
20
20
|
import { Heading, Span, Text } from "../../../ui/components/Typography.js";
|
|
21
21
|
import { Main, Section } from "../../../ui/components/Semantic.js";
|
|
22
22
|
import { RichText } from "../../../ui/rich-text/RichText.js";
|
|
23
|
-
import {
|
|
23
|
+
import { ListingDetailShell } from "../../../ui/components/ListingDetailShell.js";
|
|
24
24
|
import { ClearanceBottomActions } from "./ClearanceBottomActions.js";
|
|
25
25
|
import { ProductTabsShell } from "./ProductTabsShell.js";
|
|
26
26
|
import { ShareButton } from "./ShareButton.js";
|
|
@@ -152,5 +152,5 @@ export async function ClearanceDetailPageView({ id, initialClearance, currentUse
|
|
|
152
152
|
price: pricePerEntry,
|
|
153
153
|
storeId: typeof p.storeId === "string" ? p.storeId : undefined,
|
|
154
154
|
storeName: storeName ?? undefined,
|
|
155
|
-
} }), _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", className: "flex items-center gap-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-xs)] text-[var(--appkit-color-text-muted)] flex-wrap", children: [_jsx(Link, { href: String(ROUTES.HOME), className: "hover:text-primary-600 dark:hover:text-primary-400 transition-colors", children: "Home" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.CLEARANCE), className: "hover:text-primary-600 dark:hover:text-primary-400 transition-colors", children: "Clearance" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Span, { className: "truncate max-w-[200px]", color: "muted", children: title })] }), _jsx(ShareButton, { title: title })] }), _jsx(
|
|
155
|
+
} }), _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", className: "flex items-center gap-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-xs)] text-[var(--appkit-color-text-muted)] flex-wrap", children: [_jsx(Link, { href: String(ROUTES.HOME), className: "hover:text-primary-600 dark:hover:text-primary-400 transition-colors", children: "Home" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.CLEARANCE), className: "hover:text-primary-600 dark:hover:text-primary-400 transition-colors", children: "Clearance" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Span, { className: "truncate max-w-[200px]", color: "muted", children: title })] }), _jsx(ShareButton, { title: title })] }), _jsx(ListingDetailShell, { layout: "grid-2", renderGallery: () => (_jsx(SectionVisibilityGate, { surfaceId: "pdp-sections", sectionKey: "gallery", initialVisible: pdpSectionVisibility.gallery !== false, children: _jsx(ClearanceCollage, { items: items, hideWonState: true }) })), renderInfo: () => (_jsx(SectionVisibilityGate, { surfaceId: "pdp-sections", sectionKey: "info", initialVisible: pdpSectionVisibility.info !== false, children: _jsxs(Stack, { gap: "md", children: [_jsx(OwnerQuickActions, { resource: "PRODUCT", placement: "detail", recordLabel: String(product.title ?? product.id), storeId: product.storeId ? String(product.storeId) : undefined, editHref: String(ROUTES.STORE.PRODUCTS_EDIT(String(product.id))) }), _jsxs(Div, { children: [_jsxs(Row, { gap: "xs", wrap: true, className: "mb-2", children: [_jsx(Span, { size: "xs", weight: "semibold", className: CLS_PRIZE_BADGE, children: "Clearance" }), _jsx(Span, { size: "xs", weight: "semibold", className: `inline-block ${statusClass(revealStatus)}`, padding: "pill-sm", rounded: "full", children: statusLabel(revealStatus) }), maxPerUser !== null && (_jsxs(Span, { size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", surface: "subtle", color: "muted", children: ["Limit: ", maxPerUser, " entries per customer"] })), maxPerUser !== null && userEntriesUsed !== null && (_jsxs(Span, { size: "xs", weight: "semibold", className: CLS_PRIZE_BADGE, children: ["You have used ", userEntriesUsed, "/", maxPerUser] }))] }), _jsx(Heading, { level: 1, className: "leading-snug", smSize: "2xl", color: "primary", size: "xl", weight: "bold", children: title })] }), _jsx(Div, { className: `${__P.p4}`, border: "subtle", rounded: "xl", surface: "muted", children: _jsxs(Stack, { gap: "sm", children: [_jsxs(Row, { justify: "between", align: "center", children: [_jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: "Entries" }), _jsxs(Text, { size: "sm", weight: "semibold", children: [current, " / ", max, " (", remaining, " left)"] })] }), revealStart && (_jsxs(Row, { justify: "between", align: "center", children: [_jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: "Reveal window opens" }), _jsx(Text, { size: "sm", weight: "medium", children: revealStart.toLocaleString() })] })), revealEnd && (_jsxs(Row, { justify: "between", align: "center", children: [_jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: "Reveal window closes" }), _jsx(Text, { size: "sm", weight: "medium", children: revealEnd.toLocaleString() })] }))] }) }), 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: "Sold 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" }))] }) })), _jsx(Disclaimer, { id: "clearance" })] }) })), renderTabs: () => (_jsx(SectionVisibilityGate, { surfaceId: "pdp-sections", sectionKey: "tabs", initialVisible: pdpSectionVisibility.tabs !== false, children: _jsx(ProductTabsShell, { descriptionContent: descriptionHtml ? (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm sm:prose max-w-none dark:prose-invert", className: "text-[var(--appkit-color-text-muted)]" })) : undefined }) })), renderAside: () => (_jsx(SectionVisibilityGate, { surfaceId: "pdp-sections", sectionKey: "actions", initialVisible: pdpSectionVisibility.actions !== false, children: _jsx(Div, { padding: "5", id: "prize-draw-buy-bar", border: "subtle", surface: "muted", rounded: "xl", children: !hasPrimaryActionsSlotOverride && primaryActionsNode }) })), renderRelated: () => (_jsxs(Stack, { gap: "xl", children: [_jsx(PurchaseReviewAffordance, { productId: String(product.id), storeId: product.storeId ? String(product.storeId) : undefined }), _jsx(SectionVisibilityGate, { surfaceId: "pdp-sections", sectionKey: "grouped-listings", initialVisible: pdpSectionVisibility["grouped-listings"] !== false, children: _jsx(GroupedListingsCarousel, { groups: groups }) }), _jsx(SectionVisibilityGate, { surfaceId: "pdp-sections", sectionKey: "related", initialVisible: pdpSectionVisibility.related !== false, children: _jsx(RelatedItemsSection, { relatedItems: relatedItems, relatedByBrand: relatedByBrand, relatedByTags: relatedByTags, relatedByStore: relatedByStore, categoryLabel: categoryName, brandLabel: brand, storeLabel: storeName ?? undefined }) }), _jsx(AdminComposableRegion, { pageId: "clearance-slug", initialCards: pageRegionCards, functionalSlots: { "pdp-primary-actions": primaryActionsNode } })] })) }), _jsx(ClearanceBottomActions, { pricePerEntry: pricePerEntry, currency: currency, closed: revealStatus === "closed" || remaining === 0 })] })] }));
|
|
156
156
|
}
|
|
@@ -36,8 +36,6 @@ export { ProductDetailView } from "./ProductDetailView";
|
|
|
36
36
|
export type { ProductDetailViewProps } from "./ProductDetailView";
|
|
37
37
|
export { CollectorDropDetailView } from "./CollectorDropDetailView";
|
|
38
38
|
export type { CollectorDropDetailViewProps } from "./CollectorDropDetailView";
|
|
39
|
-
export { PreOrderDetailView } from "./PreOrderDetailView";
|
|
40
|
-
export type { PreOrderDetailViewProps } from "./PreOrderDetailView";
|
|
41
39
|
export { ProductInfo } from "./ProductInfo";
|
|
42
40
|
export type { ProductInfoProps } from "./ProductInfo";
|
|
43
41
|
export { ProductTabs } from "./ProductTabs";
|
|
@@ -23,7 +23,6 @@ export { FeatureBadge, FeatureBadgeList } from "./FeatureBadge.js";
|
|
|
23
23
|
export { ProductFeaturesProvider, useProductFeatures, } from "./ProductFeaturesContext.js";
|
|
24
24
|
export { ProductDetailView } from "./ProductDetailView.js";
|
|
25
25
|
export { CollectorDropDetailView } from "./CollectorDropDetailView.js";
|
|
26
|
-
export { PreOrderDetailView } from "./PreOrderDetailView.js";
|
|
27
26
|
export { ProductInfo } from "./ProductInfo.js";
|
|
28
27
|
export { ProductTabs } from "./ProductTabs.js";
|
|
29
28
|
export { BidHistory } from "./BidHistory.js";
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
export * from "./firestore";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import type { ListingType } from "../types/index";
|
|
4
3
|
export declare const productImageSchema: z.ZodObject<{
|
|
5
4
|
url: z.ZodString;
|
|
6
5
|
thumbnailUrl: z.ZodOptional<z.ZodString>;
|
|
@@ -119,7 +118,7 @@ export declare const productItemSchema: z.ZodObject<{
|
|
|
119
118
|
status: z.ZodEnum<["draft", "published", "in_review", "archived"]>;
|
|
120
119
|
condition: z.ZodOptional<z.ZodEnum<["new", "like_new", "good", "fair", "poor", "used", "refurbished", "broken", "graded"]>>;
|
|
121
120
|
madeToOrder: z.ZodOptional<z.ZodBoolean>;
|
|
122
|
-
listingType: z.ZodOptional<z.
|
|
121
|
+
listingType: z.ZodOptional<z.ZodString>;
|
|
123
122
|
maxPerUser: z.ZodOptional<z.ZodNumber>;
|
|
124
123
|
partOfBundleIds: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
125
124
|
partOfBundleTitles: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
@@ -360,7 +359,7 @@ export declare const productItemSchema: z.ZodObject<{
|
|
|
360
359
|
insurance?: boolean | undefined;
|
|
361
360
|
insuranceCost?: number | undefined;
|
|
362
361
|
shippingPaidBy?: "seller" | "buyer" | undefined;
|
|
363
|
-
listingType?:
|
|
362
|
+
listingType?: string | undefined;
|
|
364
363
|
collectorDropEndDate?: string | Date | undefined;
|
|
365
364
|
startingBid?: number | undefined;
|
|
366
365
|
currentBid?: number | undefined;
|
|
@@ -505,7 +504,7 @@ export declare const productItemSchema: z.ZodObject<{
|
|
|
505
504
|
insurance?: boolean | undefined;
|
|
506
505
|
insuranceCost?: number | undefined;
|
|
507
506
|
shippingPaidBy?: "seller" | "buyer" | undefined;
|
|
508
|
-
listingType?:
|
|
507
|
+
listingType?: string | undefined;
|
|
509
508
|
collectorDropEndDate?: string | Date | undefined;
|
|
510
509
|
startingBid?: number | undefined;
|
|
511
510
|
currentBid?: number | undefined;
|
|
@@ -624,7 +623,7 @@ export declare const productListParamsSchema: z.ZodObject<{
|
|
|
624
623
|
minPrice: z.ZodOptional<z.ZodNumber>;
|
|
625
624
|
maxPrice: z.ZodOptional<z.ZodNumber>;
|
|
626
625
|
inStock: z.ZodOptional<z.ZodBoolean>;
|
|
627
|
-
listingType: z.ZodOptional<z.
|
|
626
|
+
listingType: z.ZodOptional<z.ZodString>;
|
|
628
627
|
storeId: z.ZodOptional<z.ZodString>;
|
|
629
628
|
sort: z.ZodOptional<z.ZodString>;
|
|
630
629
|
page: z.ZodOptional<z.ZodNumber>;
|
|
@@ -638,7 +637,7 @@ export declare const productListParamsSchema: z.ZodObject<{
|
|
|
638
637
|
storeId?: string | undefined;
|
|
639
638
|
featured?: boolean | undefined;
|
|
640
639
|
condition?: "new" | "like_new" | "good" | "fair" | "poor" | "used" | "refurbished" | "broken" | "graded" | undefined;
|
|
641
|
-
listingType?:
|
|
640
|
+
listingType?: string | undefined;
|
|
642
641
|
page?: number | undefined;
|
|
643
642
|
minPrice?: number | undefined;
|
|
644
643
|
maxPrice?: number | undefined;
|
|
@@ -652,7 +651,7 @@ export declare const productListParamsSchema: z.ZodObject<{
|
|
|
652
651
|
storeId?: string | undefined;
|
|
653
652
|
featured?: boolean | undefined;
|
|
654
653
|
condition?: "new" | "like_new" | "good" | "fair" | "poor" | "used" | "refurbished" | "broken" | "graded" | undefined;
|
|
655
|
-
listingType?:
|
|
654
|
+
listingType?: string | undefined;
|
|
656
655
|
page?: number | undefined;
|
|
657
656
|
minPrice?: number | undefined;
|
|
658
657
|
maxPrice?: number | undefined;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
export * from "./firestore.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { mediaFieldSchema } from "../../media/types/index.js";
|
|
4
|
-
import { ALL_LISTING_TYPES } from "../../../_internal/shared/listing-types/feature-flags.js";
|
|
5
4
|
import { CLEARANCE_DURATION_DAYS_MIN, CLEARANCE_DURATION_DAYS_MAX, } from "../../../_internal/shared/checkout/rules/_limits.js";
|
|
6
5
|
// --- Sub-schemas --------------------------------------------------------------
|
|
7
6
|
export const productImageSchema = z.object({
|
|
@@ -54,7 +53,26 @@ export const printMetaSchema = z.object({
|
|
|
54
53
|
* Registered in `audit-listing-type-tab-coverage`'s STAYS_DERIVED so nobody
|
|
55
54
|
* re-expands it into a literal for readability.
|
|
56
55
|
*/
|
|
57
|
-
|
|
56
|
+
/*
|
|
57
|
+
* 🛑 OPEN, not a closed enum. Owner, 2026-11-08: *"nothing in the code should
|
|
58
|
+
* be fixed … the types we have now are the DEFAULT TEMPLATES"*, and listings
|
|
59
|
+
* were named explicitly alongside events, coupons and reviews.
|
|
60
|
+
*
|
|
61
|
+
* `ALL_LISTING_TYPES` stays the shipped template list — it still drives the
|
|
62
|
+
* admin picker, the seed and every default — but a site that wants a listing
|
|
63
|
+
* kind we never imagined adds a config row rather than waiting for a release.
|
|
64
|
+
*
|
|
65
|
+
* The SHAPE is still enforced, because this value becomes a URL segment, a
|
|
66
|
+
* Firestore query value and an index key: lowercase, digits, hyphen/underscore
|
|
67
|
+
* separated, 2-40 characters.
|
|
68
|
+
*/
|
|
69
|
+
const SLUG_RE = /^[a-z0-9]+(?:[-_][a-z0-9]+)*$/;
|
|
70
|
+
const LISTING_TYPE_ENUM = z
|
|
71
|
+
.string()
|
|
72
|
+
.trim()
|
|
73
|
+
.min(2)
|
|
74
|
+
.max(40)
|
|
75
|
+
.regex(SLUG_RE, "Use a lowercase slug, e.g. art-and-stickers");
|
|
58
76
|
export const productItemSchema = z.object({
|
|
59
77
|
id: z.string(),
|
|
60
78
|
title: z.string(),
|
|
@@ -181,7 +181,23 @@ export declare const COUPON_FIELDS: {
|
|
|
181
181
|
readonly SELLER: "seller";
|
|
182
182
|
};
|
|
183
183
|
};
|
|
184
|
-
|
|
184
|
+
/**
|
|
185
|
+
* Labels for the SHIPPED coupon types.
|
|
186
|
+
*
|
|
187
|
+
* 🛑 Since `CouponType` opened (2026-11-08) this is no longer exhaustive — a
|
|
188
|
+
* site-defined type has no entry here and a bare lookup yields `undefined`,
|
|
189
|
+
* which renders as an empty chip. Read it through `couponTypeLabel()`.
|
|
190
|
+
*/
|
|
191
|
+
export declare const COUPON_TYPE_LABELS: Record<string, string>;
|
|
192
|
+
/**
|
|
193
|
+
* The label for a coupon type, with a readable fallback.
|
|
194
|
+
*
|
|
195
|
+
* An unknown type is a site-defined one, not an error: it renders its own slug
|
|
196
|
+
* de-slugged ("first_order" -> "First order") rather than blank. A coupon that
|
|
197
|
+
* shows no type at all is worse than one showing a plain name, and it must
|
|
198
|
+
* never throw on a checkout page.
|
|
199
|
+
*/
|
|
200
|
+
export declare function couponTypeLabel(type: string): string;
|
|
185
201
|
export declare const DEFAULT_COUPON_DATA: Partial<CouponDocument>;
|
|
186
202
|
export declare const COUPONS_PUBLIC_FIELDS: readonly ["id", "code", "name", "description", "type", "discount", "validity.startDate", "validity.endDate", "usage.totalLimit", "usage.perUserLimit"];
|
|
187
203
|
export declare const COUPONS_UPDATABLE_FIELDS: readonly ["name", "description", "discount", "bxgy", "tiers", "usage", "validity", "restrictions"];
|
|
@@ -55,6 +55,13 @@ export const COUPON_FIELDS = {
|
|
|
55
55
|
SELLER: "seller",
|
|
56
56
|
},
|
|
57
57
|
};
|
|
58
|
+
/**
|
|
59
|
+
* Labels for the SHIPPED coupon types.
|
|
60
|
+
*
|
|
61
|
+
* 🛑 Since `CouponType` opened (2026-11-08) this is no longer exhaustive — a
|
|
62
|
+
* site-defined type has no entry here and a bare lookup yields `undefined`,
|
|
63
|
+
* which renders as an empty chip. Read it through `couponTypeLabel()`.
|
|
64
|
+
*/
|
|
58
65
|
export const COUPON_TYPE_LABELS = {
|
|
59
66
|
percentage: "Percentage Discount",
|
|
60
67
|
fixed: "Fixed Amount Discount",
|
|
@@ -64,6 +71,21 @@ export const COUPON_TYPE_LABELS = {
|
|
|
64
71
|
// our word for it; "Points back" is what appears on their balance.
|
|
65
72
|
points_earn: "Points Back",
|
|
66
73
|
};
|
|
74
|
+
/**
|
|
75
|
+
* The label for a coupon type, with a readable fallback.
|
|
76
|
+
*
|
|
77
|
+
* An unknown type is a site-defined one, not an error: it renders its own slug
|
|
78
|
+
* de-slugged ("first_order" -> "First order") rather than blank. A coupon that
|
|
79
|
+
* shows no type at all is worse than one showing a plain name, and it must
|
|
80
|
+
* never throw on a checkout page.
|
|
81
|
+
*/
|
|
82
|
+
export function couponTypeLabel(type) {
|
|
83
|
+
const known = COUPON_TYPE_LABELS[type];
|
|
84
|
+
if (known)
|
|
85
|
+
return known;
|
|
86
|
+
const words = type.replace(/[-_]+/g, " ").trim();
|
|
87
|
+
return words ? words.charAt(0).toUpperCase() + words.slice(1) : type;
|
|
88
|
+
}
|
|
67
89
|
export const DEFAULT_COUPON_DATA = {
|
|
68
90
|
usage: { currentUsage: 0 },
|
|
69
91
|
validity: { isActive: true, startDate: new Date(), endDate: undefined },
|