@numueg/theme-sdk 0.6.0 → 0.9.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/CHANGELOG.md +144 -0
- package/dist/chunk-3C37JX2X.cjs +207 -0
- package/dist/chunk-3C37JX2X.cjs.map +1 -0
- package/dist/chunk-JIAA7TGR.mjs +397 -0
- package/dist/chunk-JIAA7TGR.mjs.map +1 -0
- package/dist/chunk-PUR3FRGQ.mjs +183 -0
- package/dist/chunk-PUR3FRGQ.mjs.map +1 -0
- package/dist/chunk-TBSNHHFH.cjs +96 -0
- package/dist/chunk-TBSNHHFH.cjs.map +1 -0
- package/dist/chunk-X3J4GDI7.cjs +408 -0
- package/dist/chunk-X3J4GDI7.cjs.map +1 -0
- package/dist/chunk-XF2FGIVS.mjs +94 -0
- package/dist/chunk-XF2FGIVS.mjs.map +1 -0
- package/dist/{entities-6MGANln7.d.mts → entities-B8378GKp.d.mts} +12 -0
- package/dist/{entities-6MGANln7.d.ts → entities-B8378GKp.d.ts} +12 -0
- package/dist/index.cjs +643 -965
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +215 -82
- package/dist/index.d.ts +215 -82
- package/dist/index.mjs +485 -908
- package/dist/index.mjs.map +1 -1
- package/dist/{mount-BGumg1JM.d.mts → mount-CM4kGg9w.d.ts} +87 -14
- package/dist/{mount-Bt-4ken5.d.ts → mount-NLjFNyBv.d.mts} +87 -14
- package/dist/normalize.cjs +7 -91
- package/dist/normalize.cjs.map +1 -1
- package/dist/normalize.d.mts +1 -1
- package/dist/normalize.d.ts +1 -1
- package/dist/normalize.mjs +1 -92
- package/dist/normalize.mjs.map +1 -1
- package/dist/{theme-D8MOopvi.d.mts → theme-CNTB4KnU.d.mts} +29 -3
- package/dist/{theme-D8MOopvi.d.ts → theme-CNTB4KnU.d.ts} +29 -3
- package/dist/types.d.mts +2 -2
- package/dist/types.d.ts +2 -2
- package/dist/v2-compat.cjs +6 -163
- package/dist/v2-compat.cjs.map +1 -1
- package/dist/v2-compat.d.mts +1 -1
- package/dist/v2-compat.d.ts +1 -1
- package/dist/v2-compat.mjs +2 -159
- package/dist/v2-compat.mjs.map +1 -1
- package/dist/validation.cjs +43 -403
- package/dist/validation.cjs.map +1 -1
- package/dist/validation.d.mts +1 -1
- package/dist/validation.d.ts +1 -1
- package/dist/validation.mjs +1 -395
- package/dist/validation.mjs.map +1 -1
- package/dist/verify.cjs +6 -21
- package/dist/verify.cjs.map +1 -1
- package/dist/verify.d.mts +3 -3
- package/dist/verify.d.ts +3 -3
- package/dist/verify.mjs +3 -20
- package/dist/verify.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1,494 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
export { resolveThemeSettings } from './chunk-XF2FGIVS.mjs';
|
|
2
|
+
export { KNOWN_SETTING_TYPES, KNOWN_TEMPLATES, REQUIRED_TEMPLATES, SDK_VERSION, THEME_CONTRACT_VERSION, mergeResults, validateBuiltManifest, validateManifest, validateSectionSchema, validateSettingsAgainstSchema } from './chunk-JIAA7TGR.mjs';
|
|
3
|
+
import { ShopContext, ThemeSettingsContext, CurrentTemplateContext, PageContext, LocalizationContext, CurrencyContext, CartContext, CustomerContext, NavigationContext, CollectionContext, ProductContext, useShop, useLocalization, useCustomer, useThemeSettings } from './chunk-PUR3FRGQ.mjs';
|
|
4
|
+
export { CartContext, CollectionContext, CurrencyContext, CustomerContext, LocalizationContext, NavigationContext, PageContext, ProductContext, ShopContext, ThemeSettingsContext, useCollections, useCustomer, useDirection, useFieldTranslation, useLocale, useLocalization, useNumberFormat, usePage, useProducts, useShop, useThemeSettings, useTranslation } from './chunk-PUR3FRGQ.mjs';
|
|
5
|
+
import { createContext, forwardRef, useState, useImperativeHandle, useEffect, useCallback, useMemo, useRef, useContext, useSyncExternalStore, StrictMode, createElement, Component } from 'react';
|
|
2
6
|
import { hydrateRoot, createRoot } from 'react-dom/client';
|
|
3
7
|
import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
|
|
4
8
|
|
|
5
9
|
// src/types/theme.ts
|
|
6
10
|
var MAX_BLOCK_DEPTH = 5;
|
|
7
|
-
|
|
8
|
-
// src/validation/index.ts
|
|
9
|
-
var THEME_CONTRACT_VERSION = 1;
|
|
10
|
-
var SDK_VERSION = "0.6.0" ;
|
|
11
|
-
var REQUIRED_TEMPLATES = [
|
|
12
|
-
"home",
|
|
13
|
-
"product",
|
|
14
|
-
"collection",
|
|
15
|
-
"cart",
|
|
16
|
-
"page",
|
|
17
|
-
"search",
|
|
18
|
-
"404"
|
|
19
|
-
];
|
|
20
|
-
var KNOWN_TEMPLATES = /* @__PURE__ */ new Set([
|
|
21
|
-
...REQUIRED_TEMPLATES,
|
|
22
|
-
"blog",
|
|
23
|
-
"article",
|
|
24
|
-
"policies",
|
|
25
|
-
"password",
|
|
26
|
-
"account",
|
|
27
|
-
"checkout"
|
|
28
|
-
]);
|
|
29
|
-
var KNOWN_SETTING_TYPES = /* @__PURE__ */ new Set([
|
|
30
|
-
"text",
|
|
31
|
-
"textarea",
|
|
32
|
-
"richtext",
|
|
33
|
-
"number",
|
|
34
|
-
"range",
|
|
35
|
-
"color",
|
|
36
|
-
"checkbox",
|
|
37
|
-
"select",
|
|
38
|
-
"radio",
|
|
39
|
-
"font",
|
|
40
|
-
"image_picker",
|
|
41
|
-
"url",
|
|
42
|
-
"product",
|
|
43
|
-
"product_list",
|
|
44
|
-
"collection",
|
|
45
|
-
"collection_list",
|
|
46
|
-
"header",
|
|
47
|
-
"paragraph",
|
|
48
|
-
"html",
|
|
49
|
-
"date",
|
|
50
|
-
"time",
|
|
51
|
-
"video_picker",
|
|
52
|
-
"color_scheme",
|
|
53
|
-
"page_picker",
|
|
54
|
-
"blog_picker",
|
|
55
|
-
"link_list_picker",
|
|
56
|
-
"variant_picker",
|
|
57
|
-
"file_upload",
|
|
58
|
-
"icon_picker",
|
|
59
|
-
"icon"
|
|
60
|
-
]);
|
|
61
|
-
var PRESENTATIONAL_SETTING_TYPES = /* @__PURE__ */ new Set([
|
|
62
|
-
"header",
|
|
63
|
-
"paragraph",
|
|
64
|
-
"html"
|
|
65
|
-
]);
|
|
66
|
-
var SECTION_TYPE_RE = /^[a-z][a-z0-9_-]*$/;
|
|
67
|
-
var THEME_ID_RE = /^[a-z0-9][a-z0-9_-]*[a-z0-9]$/;
|
|
68
|
-
var SEMVER_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-.]+)?(?:\+[0-9A-Za-z-.]+)?$/;
|
|
69
|
-
function isObject(v) {
|
|
70
|
-
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
71
|
-
}
|
|
72
|
-
function isNonEmptyString(v) {
|
|
73
|
-
return typeof v === "string" && v.trim().length > 0;
|
|
74
|
-
}
|
|
75
|
-
var IssueBag = class {
|
|
76
|
-
constructor() {
|
|
77
|
-
this.issues = [];
|
|
78
|
-
}
|
|
79
|
-
err(code, message, path) {
|
|
80
|
-
this.issues.push({ level: "error", code, message, path });
|
|
81
|
-
}
|
|
82
|
-
warn(code, message, path) {
|
|
83
|
-
this.issues.push({ level: "warning", code, message, path });
|
|
84
|
-
}
|
|
85
|
-
result() {
|
|
86
|
-
return {
|
|
87
|
-
valid: !this.issues.some((i) => i.level === "error"),
|
|
88
|
-
issues: this.issues
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
};
|
|
92
|
-
function collectPresetSectionTypes(manifest) {
|
|
93
|
-
const types = /* @__PURE__ */ new Set();
|
|
94
|
-
const presets = manifest.presets;
|
|
95
|
-
if (!isObject(presets)) return types;
|
|
96
|
-
const buckets = [presets.templates, presets.section_groups];
|
|
97
|
-
for (const bucket of buckets) {
|
|
98
|
-
if (!isObject(bucket)) continue;
|
|
99
|
-
for (const entry of Object.values(bucket)) {
|
|
100
|
-
if (!isObject(entry)) continue;
|
|
101
|
-
const sections = entry.sections;
|
|
102
|
-
const instances = Array.isArray(sections) ? sections : isObject(sections) ? Object.values(sections) : [];
|
|
103
|
-
for (const inst of instances) {
|
|
104
|
-
if (isObject(inst) && isNonEmptyString(inst.type)) types.add(inst.type);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
return types;
|
|
109
|
-
}
|
|
110
|
-
function validateSettingInto(bag, setting, pathPrefix, seenIds) {
|
|
111
|
-
if (!isObject(setting)) {
|
|
112
|
-
bag.err("schema.setting.invalid", "Setting must be an object.", pathPrefix);
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
const type = setting.type;
|
|
116
|
-
if (!isNonEmptyString(type)) {
|
|
117
|
-
bag.err("schema.setting.type.missing", "Setting is missing a `type`.", pathPrefix);
|
|
118
|
-
} else if (!KNOWN_SETTING_TYPES.has(type)) {
|
|
119
|
-
bag.warn(
|
|
120
|
-
"schema.setting.type.unknown",
|
|
121
|
-
`Unknown setting type "${type}" \u2014 the customizer will fall back to a text input.`,
|
|
122
|
-
pathPrefix
|
|
123
|
-
);
|
|
124
|
-
}
|
|
125
|
-
const presentational = isNonEmptyString(type) && PRESENTATIONAL_SETTING_TYPES.has(type);
|
|
126
|
-
if (!presentational) {
|
|
127
|
-
if (!isNonEmptyString(setting.id)) {
|
|
128
|
-
bag.err("schema.setting.id.missing", "Setting is missing an `id`.", pathPrefix);
|
|
129
|
-
} else {
|
|
130
|
-
if (seenIds.has(setting.id)) {
|
|
131
|
-
bag.err(
|
|
132
|
-
"schema.setting.id.duplicate",
|
|
133
|
-
`Duplicate setting id "${setting.id}".`,
|
|
134
|
-
pathPrefix
|
|
135
|
-
);
|
|
136
|
-
}
|
|
137
|
-
seenIds.add(setting.id);
|
|
138
|
-
}
|
|
139
|
-
if (!isNonEmptyString(setting.label)) {
|
|
140
|
-
bag.warn("schema.setting.label.missing", "Setting has no `label`.", pathPrefix);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
if (type === "select" || type === "radio") {
|
|
144
|
-
const options = setting.options;
|
|
145
|
-
if (!Array.isArray(options) || options.length === 0) {
|
|
146
|
-
bag.err(
|
|
147
|
-
"schema.setting.options.missing",
|
|
148
|
-
`"${type}" setting "${String(setting.id)}" must declare a non-empty \`options\` array.`,
|
|
149
|
-
pathPrefix
|
|
150
|
-
);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
if (type === "range") {
|
|
154
|
-
if (typeof setting.min !== "number" || typeof setting.max !== "number") {
|
|
155
|
-
bag.err(
|
|
156
|
-
"schema.setting.range.bounds",
|
|
157
|
-
`"range" setting "${String(setting.id)}" must declare numeric \`min\` and \`max\`.`,
|
|
158
|
-
pathPrefix
|
|
159
|
-
);
|
|
160
|
-
} else if (setting.min >= setting.max) {
|
|
161
|
-
bag.err(
|
|
162
|
-
"schema.setting.range.order",
|
|
163
|
-
`"range" setting "${String(setting.id)}" has min >= max.`,
|
|
164
|
-
pathPrefix
|
|
165
|
-
);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
function validateSectionSchema(schema, opts = {}) {
|
|
170
|
-
const bag = new IssueBag();
|
|
171
|
-
const where = opts.filenameType ? `${opts.filenameType}.json` : "section schema";
|
|
172
|
-
if (!isObject(schema)) {
|
|
173
|
-
bag.err("schema.invalid", "Section schema must be a JSON object.", where);
|
|
174
|
-
return bag.result();
|
|
175
|
-
}
|
|
176
|
-
const type = schema.type;
|
|
177
|
-
if (!isNonEmptyString(type)) {
|
|
178
|
-
bag.err("schema.type.missing", "Section schema is missing a `type`.", where);
|
|
179
|
-
} else {
|
|
180
|
-
if (!SECTION_TYPE_RE.test(type)) {
|
|
181
|
-
bag.err(
|
|
182
|
-
"schema.type.format",
|
|
183
|
-
`Section type "${type}" must match ${SECTION_TYPE_RE} (lowercase, start with a letter).`,
|
|
184
|
-
where
|
|
185
|
-
);
|
|
186
|
-
}
|
|
187
|
-
if (opts.filenameType && type !== opts.filenameType) {
|
|
188
|
-
bag.err(
|
|
189
|
-
"schema.type.filename_mismatch",
|
|
190
|
-
`Schema type "${type}" must equal its filename "${opts.filenameType}" (component-filename = schema-type convention).`,
|
|
191
|
-
where
|
|
192
|
-
);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
if (!isNonEmptyString(schema.name)) {
|
|
196
|
-
bag.err("schema.name.missing", "Section schema is missing a `name`.", where);
|
|
197
|
-
}
|
|
198
|
-
if (schema.settings === void 0) {
|
|
199
|
-
bag.warn("schema.settings.missing", "Section schema has no `settings`.", where);
|
|
200
|
-
} else if (!Array.isArray(schema.settings)) {
|
|
201
|
-
bag.err("schema.settings.invalid", "`settings` must be an array.", where);
|
|
202
|
-
} else {
|
|
203
|
-
const seen = /* @__PURE__ */ new Set();
|
|
204
|
-
schema.settings.forEach(
|
|
205
|
-
(s, i) => validateSettingInto(bag, s, `${where}.settings[${i}]`, seen)
|
|
206
|
-
);
|
|
207
|
-
}
|
|
208
|
-
if (schema.blocks !== void 0) {
|
|
209
|
-
if (!Array.isArray(schema.blocks)) {
|
|
210
|
-
bag.err("schema.blocks.invalid", "`blocks` must be an array.", where);
|
|
211
|
-
} else {
|
|
212
|
-
schema.blocks.forEach((b, i) => {
|
|
213
|
-
if (!isObject(b) || !isNonEmptyString(b.type)) {
|
|
214
|
-
bag.err("schema.block.type.missing", `Block[${i}] is missing a \`type\`.`, where);
|
|
215
|
-
return;
|
|
216
|
-
}
|
|
217
|
-
if (!SECTION_TYPE_RE.test(b.type)) {
|
|
218
|
-
bag.err(
|
|
219
|
-
"schema.block.type.format",
|
|
220
|
-
`Block type "${b.type}" must match ${SECTION_TYPE_RE}.`,
|
|
221
|
-
where
|
|
222
|
-
);
|
|
223
|
-
}
|
|
224
|
-
if (Array.isArray(b.settings)) {
|
|
225
|
-
const seen = /* @__PURE__ */ new Set();
|
|
226
|
-
b.settings.forEach(
|
|
227
|
-
(s, j) => validateSettingInto(bag, s, `${where}.blocks[${i}].settings[${j}]`, seen)
|
|
228
|
-
);
|
|
229
|
-
}
|
|
230
|
-
});
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
return bag.result();
|
|
234
|
-
}
|
|
235
|
-
function validateSettingsAgainstSchema(settings, schema, pathPrefix = "settings") {
|
|
236
|
-
const bag = new IssueBag();
|
|
237
|
-
if (!isObject(settings)) {
|
|
238
|
-
bag.err("instance.settings.invalid", "Section settings must be an object.", pathPrefix);
|
|
239
|
-
return bag.result();
|
|
240
|
-
}
|
|
241
|
-
const defs = Array.isArray(schema.settings) ? schema.settings : [];
|
|
242
|
-
const byId = /* @__PURE__ */ new Map();
|
|
243
|
-
for (const d of defs) if (isNonEmptyString(d?.id)) byId.set(d.id, d);
|
|
244
|
-
for (const [key, value] of Object.entries(settings)) {
|
|
245
|
-
const def = byId.get(key);
|
|
246
|
-
if (!def) {
|
|
247
|
-
bag.warn(
|
|
248
|
-
"instance.setting.unknown",
|
|
249
|
-
`Setting "${key}" is not declared in the "${String(schema.type)}" schema.`,
|
|
250
|
-
`${pathPrefix}.${key}`
|
|
251
|
-
);
|
|
252
|
-
continue;
|
|
253
|
-
}
|
|
254
|
-
if (value === null || value === void 0) continue;
|
|
255
|
-
const p = `${pathPrefix}.${key}`;
|
|
256
|
-
if ((def.type === "select" || def.type === "radio") && Array.isArray(def.options)) {
|
|
257
|
-
const allowed = def.options.map((o) => o.value);
|
|
258
|
-
if (typeof value === "string" && !allowed.includes(value)) {
|
|
259
|
-
bag.err(
|
|
260
|
-
"instance.setting.option.invalid",
|
|
261
|
-
`"${key}" = "${value}" is not one of: ${allowed.join(", ")}.`,
|
|
262
|
-
p
|
|
263
|
-
);
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
if (def.type === "range" && typeof value === "number") {
|
|
267
|
-
if (typeof def.min === "number" && value < def.min) {
|
|
268
|
-
bag.err("instance.setting.range.under", `"${key}" = ${value} is below min ${def.min}.`, p);
|
|
269
|
-
}
|
|
270
|
-
if (typeof def.max === "number" && value > def.max) {
|
|
271
|
-
bag.err("instance.setting.range.over", `"${key}" = ${value} is above max ${def.max}.`, p);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
if (def.type === "checkbox" && typeof value !== "boolean") {
|
|
275
|
-
bag.warn("instance.setting.type.mismatch", `"${key}" should be a boolean.`, p);
|
|
276
|
-
}
|
|
277
|
-
if ((def.type === "number" || def.type === "range") && typeof value !== "number") {
|
|
278
|
-
bag.warn("instance.setting.type.mismatch", `"${key}" should be a number.`, p);
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
return bag.result();
|
|
282
|
-
}
|
|
283
|
-
function validateManifestCore(bag, m) {
|
|
284
|
-
if (!isNonEmptyString(m.id)) {
|
|
285
|
-
bag.err("manifest.id.missing", "theme.json is missing `id`.", "id");
|
|
286
|
-
} else if (!THEME_ID_RE.test(m.id)) {
|
|
287
|
-
bag.err(
|
|
288
|
-
"manifest.id.format",
|
|
289
|
-
`Theme id "${m.id}" must be lowercase alphanumeric with dashes/underscores (no leading/trailing separator).`,
|
|
290
|
-
"id"
|
|
291
|
-
);
|
|
292
|
-
}
|
|
293
|
-
const name = m.name;
|
|
294
|
-
const nameOk = isNonEmptyString(name) || isObject(name) && Object.values(name).some((v) => isNonEmptyString(v));
|
|
295
|
-
if (!nameOk) {
|
|
296
|
-
bag.err("manifest.name.missing", "theme.json is missing a non-empty `name`.", "name");
|
|
297
|
-
}
|
|
298
|
-
if (!isNonEmptyString(m.version)) {
|
|
299
|
-
bag.err("manifest.version.missing", "theme.json is missing `version`.", "version");
|
|
300
|
-
} else if (!SEMVER_RE.test(m.version)) {
|
|
301
|
-
bag.err("manifest.version.invalid", `Version "${m.version}" is not valid semver.`, "version");
|
|
302
|
-
}
|
|
303
|
-
if (!isNonEmptyString(m.author)) {
|
|
304
|
-
bag.err("manifest.author.missing", "theme.json is missing `author`.", "author");
|
|
305
|
-
}
|
|
306
|
-
if (m.min_sdk_version !== void 0 && !SEMVER_RE.test(String(m.min_sdk_version))) {
|
|
307
|
-
bag.warn("manifest.min_sdk_version.invalid", "`min_sdk_version` is not valid semver.", "min_sdk_version");
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
function validateManifest(manifest, ctx = {}) {
|
|
311
|
-
const bag = new IssueBag();
|
|
312
|
-
if (!isObject(manifest)) {
|
|
313
|
-
bag.err("manifest.invalid", "theme.json must be a JSON object.", "theme.json");
|
|
314
|
-
return bag.result();
|
|
315
|
-
}
|
|
316
|
-
validateManifestCore(bag, manifest);
|
|
317
|
-
const presets = manifest.presets;
|
|
318
|
-
if (!isObject(presets) || Object.keys(presets).length === 0) {
|
|
319
|
-
bag.warn("manifest.presets.empty", "theme.json has no presets \u2014 merchants start with an empty page.", "presets");
|
|
320
|
-
} else {
|
|
321
|
-
if (ctx.sectionTypes) {
|
|
322
|
-
for (const type of collectPresetSectionTypes(manifest)) {
|
|
323
|
-
if (!ctx.sectionTypes.has(type)) {
|
|
324
|
-
bag.err(
|
|
325
|
-
"manifest.preset.unknown_section",
|
|
326
|
-
`Preset references section type "${type}" with no schemas/sections/${type}.json.`,
|
|
327
|
-
"presets"
|
|
328
|
-
);
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
const templates = isObject(presets.templates) ? presets.templates : {};
|
|
333
|
-
for (const t of REQUIRED_TEMPLATES) {
|
|
334
|
-
if (!(t in templates)) {
|
|
335
|
-
bag.warn(
|
|
336
|
-
"manifest.template.missing",
|
|
337
|
-
`No preset for required template "${t}" \u2014 the storefront will fall back to its built-in.`,
|
|
338
|
-
`presets.templates.${t}`
|
|
339
|
-
);
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
return bag.result();
|
|
344
|
-
}
|
|
345
|
-
function validateBuiltManifest(builtManifest, importMap, opts = {}) {
|
|
346
|
-
const bag = new IssueBag();
|
|
347
|
-
if (!isObject(builtManifest)) {
|
|
348
|
-
bag.err("built.invalid", "manifest.json must be a JSON object.", "manifest.json");
|
|
349
|
-
return bag.result();
|
|
350
|
-
}
|
|
351
|
-
validateManifestCore(bag, builtManifest);
|
|
352
|
-
const schemas = builtManifest.section_schemas;
|
|
353
|
-
const shipped = new Set(isObject(schemas) ? Object.keys(schemas) : []);
|
|
354
|
-
if (!isObject(schemas)) {
|
|
355
|
-
bag.warn("built.section_schemas.missing", "manifest.json has no `section_schemas`.", "section_schemas");
|
|
356
|
-
}
|
|
357
|
-
for (const type of collectPresetSectionTypes(builtManifest)) {
|
|
358
|
-
if (!shipped.has(type)) {
|
|
359
|
-
bag.err(
|
|
360
|
-
"built.preset.unknown_section",
|
|
361
|
-
`Preset references section type "${type}" not present in section_schemas.`,
|
|
362
|
-
"presets"
|
|
363
|
-
);
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
if (isObject(schemas)) {
|
|
367
|
-
for (const [type, schema] of Object.entries(schemas)) {
|
|
368
|
-
const r = validateSectionSchema(schema, { filenameType: type });
|
|
369
|
-
for (const issue of r.issues) bag.issues.push(issue);
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
if (!isNonEmptyString(builtManifest.plugin_version)) {
|
|
373
|
-
bag.warn("built.plugin_version.missing", "manifest.json has no `plugin_version`.", "plugin_version");
|
|
374
|
-
}
|
|
375
|
-
if (importMap !== void 0) {
|
|
376
|
-
if (!isObject(importMap)) {
|
|
377
|
-
bag.err("importmap.invalid", "import-map.json must be a JSON object.", "import-map.json");
|
|
378
|
-
} else {
|
|
379
|
-
const cv = importMap.contract_version;
|
|
380
|
-
if (typeof cv !== "number") {
|
|
381
|
-
bag.warn(
|
|
382
|
-
"importmap.contract_version.missing",
|
|
383
|
-
"import-map.json has no numeric `contract_version` \u2014 built by an older plugin.",
|
|
384
|
-
"contract_version"
|
|
385
|
-
);
|
|
386
|
-
} else if (typeof opts.hostContractVersion === "number" && cv > opts.hostContractVersion) {
|
|
387
|
-
bag.err(
|
|
388
|
-
"importmap.contract_version.incompatible",
|
|
389
|
-
`Theme built for contract v${cv}; this platform supports up to v${opts.hostContractVersion}.`,
|
|
390
|
-
"contract_version"
|
|
391
|
-
);
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
return bag.result();
|
|
396
|
-
}
|
|
397
|
-
function mergeResults(...results) {
|
|
398
|
-
const issues = results.flatMap((r) => r.issues);
|
|
399
|
-
return { valid: !issues.some((i) => i.level === "error"), issues };
|
|
400
|
-
}
|
|
401
|
-
var ShopContext = createContext(null);
|
|
402
|
-
var ProductContext = createContext(null);
|
|
403
|
-
var CollectionContext = createContext(null);
|
|
404
|
-
var CartContext = createContext(null);
|
|
405
|
-
var CustomerContext = createContext(null);
|
|
406
|
-
var ThemeSettingsContext = createContext(null);
|
|
407
|
-
var LocalizationContext = createContext(null);
|
|
408
|
-
var PageContext = createContext(null);
|
|
409
|
-
var CurrentTemplateContext = createContext("home");
|
|
410
|
-
var NavigationContext = createContext(
|
|
411
|
-
{}
|
|
412
|
-
);
|
|
413
|
-
function useLocalization() {
|
|
414
|
-
const ctx = useContext(LocalizationContext);
|
|
415
|
-
if (!ctx) throw new Error("useLocalization must be used within NuMuProvider");
|
|
416
|
-
return ctx;
|
|
417
|
-
}
|
|
418
|
-
function useDirection() {
|
|
419
|
-
const { direction } = useLocalization();
|
|
420
|
-
return direction;
|
|
421
|
-
}
|
|
422
|
-
function useLocale() {
|
|
423
|
-
const { locale } = useLocalization();
|
|
424
|
-
return locale;
|
|
425
|
-
}
|
|
426
|
-
function useTranslation() {
|
|
427
|
-
const { translations, locale } = useLocalization();
|
|
428
|
-
return {
|
|
429
|
-
t: (key, fallback) => translations[key] || fallback || key,
|
|
430
|
-
locale
|
|
431
|
-
};
|
|
432
|
-
}
|
|
433
|
-
function useFieldTranslation(entity, field) {
|
|
434
|
-
const { locale } = useLocalization();
|
|
435
|
-
if (!entity) return void 0;
|
|
436
|
-
const translated = entity.attributes && typeof entity.attributes === "object" ? entity.attributes[`${field}_${locale}`] : void 0;
|
|
437
|
-
if (typeof translated === "string" && translated) return translated;
|
|
438
|
-
const base = entity[field];
|
|
439
|
-
if (typeof base === "string") return base;
|
|
440
|
-
if (base == null) return void 0;
|
|
441
|
-
return String(base);
|
|
442
|
-
}
|
|
443
|
-
function useNumberFormat() {
|
|
444
|
-
const { formatNumber } = useLocalization();
|
|
445
|
-
return formatNumber;
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
// src/hooks/useShop.ts
|
|
449
|
-
var DEFAULT_PROTOCOL = typeof window !== "undefined" ? window.location.protocol.replace(":", "") : "https";
|
|
450
|
-
function resolveDomain(store) {
|
|
451
|
-
if (store.domain) return store.domain;
|
|
452
|
-
if (store.subdomain) {
|
|
453
|
-
const platform = typeof window !== "undefined" && window.__NUMU_PLATFORM_DOMAIN || "numueg.app";
|
|
454
|
-
return `${store.subdomain}.${platform}`;
|
|
455
|
-
}
|
|
456
|
-
return store.slug;
|
|
457
|
-
}
|
|
458
|
-
function useShop() {
|
|
459
|
-
const ctx = useContext(ShopContext);
|
|
460
|
-
if (!ctx) throw new Error("useShop must be used within NuMuProvider");
|
|
461
|
-
const locale = useLocale();
|
|
462
|
-
return useMemo(() => {
|
|
463
|
-
const domain = resolveDomain(ctx);
|
|
464
|
-
const settings = ctx.settings;
|
|
465
|
-
const localePrefixEnabled = Boolean(
|
|
466
|
-
settings && settings.locale_url_prefix_enabled
|
|
467
|
-
);
|
|
468
|
-
const defaultLocale = ctx.default_language || "en";
|
|
469
|
-
const prefixedLocales = settings?.locale_url_prefix_locales || null;
|
|
470
|
-
const shouldPrefix = (l) => {
|
|
471
|
-
if (!localePrefixEnabled) return false;
|
|
472
|
-
if (prefixedLocales) return prefixedLocales.includes(l);
|
|
473
|
-
return l !== defaultLocale;
|
|
474
|
-
};
|
|
475
|
-
const formatUrl = (path) => {
|
|
476
|
-
if (!path) return `${DEFAULT_PROTOCOL}://${domain}/`;
|
|
477
|
-
if (/^https?:\/\//i.test(path)) return path;
|
|
478
|
-
if (path.startsWith("//")) return `${DEFAULT_PROTOCOL}:${path}`;
|
|
479
|
-
let normalized = path.startsWith("/") ? path : `/${path}`;
|
|
480
|
-
if (locale && shouldPrefix(locale)) {
|
|
481
|
-
const prefix = `/${locale}/`;
|
|
482
|
-
const root = `/${locale}`;
|
|
483
|
-
if (normalized !== root && !normalized.startsWith(prefix)) {
|
|
484
|
-
normalized = `${root}${normalized}`;
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
return `${DEFAULT_PROTOCOL}://${domain}${normalized}`;
|
|
488
|
-
};
|
|
489
|
-
return { ...ctx, domain, formatUrl };
|
|
490
|
-
}, [ctx, locale]);
|
|
491
|
-
}
|
|
492
11
|
function useProduct() {
|
|
493
12
|
const ctx = useContext(ProductContext);
|
|
494
13
|
if (!ctx) throw new Error("useProduct must be used within ProductProvider");
|
|
@@ -510,20 +29,9 @@ function useCart() {
|
|
|
510
29
|
if (!ctx) throw new Error("useCart must be used within NuMuProvider");
|
|
511
30
|
return ctx;
|
|
512
31
|
}
|
|
513
|
-
function useCustomer() {
|
|
514
|
-
return useContext(CustomerContext);
|
|
515
|
-
}
|
|
516
|
-
function useThemeSettings() {
|
|
517
|
-
const ctx = useContext(ThemeSettingsContext);
|
|
518
|
-
if (!ctx) throw new Error("useThemeSettings must be used within NuMuProvider");
|
|
519
|
-
return ctx;
|
|
520
|
-
}
|
|
521
32
|
function useCurrentTemplate() {
|
|
522
33
|
return useContext(CurrentTemplateContext);
|
|
523
34
|
}
|
|
524
|
-
function usePage() {
|
|
525
|
-
return useContext(PageContext);
|
|
526
|
-
}
|
|
527
35
|
var SectionContext = createContext(null);
|
|
528
36
|
function useSection() {
|
|
529
37
|
const ctx = useContext(SectionContext);
|
|
@@ -533,13 +41,57 @@ function useSection() {
|
|
|
533
41
|
function useSectionOptional() {
|
|
534
42
|
return useContext(SectionContext);
|
|
535
43
|
}
|
|
44
|
+
var EMPTY = Object.freeze([]);
|
|
45
|
+
function useSectionGroup(group) {
|
|
46
|
+
const settings = useContext(ThemeSettingsContext);
|
|
47
|
+
return useMemo(() => {
|
|
48
|
+
const grp = settings?.section_groups?.[group];
|
|
49
|
+
if (!grp || !Array.isArray(grp.order) || grp.order.length === 0) {
|
|
50
|
+
return EMPTY;
|
|
51
|
+
}
|
|
52
|
+
const sections = grp.sections ?? {};
|
|
53
|
+
const out = [];
|
|
54
|
+
for (const id of grp.order) {
|
|
55
|
+
const instance = sections[id];
|
|
56
|
+
if (!instance) continue;
|
|
57
|
+
out.push({ id, ...instance });
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}, [settings, group]);
|
|
61
|
+
}
|
|
62
|
+
function useCurrency() {
|
|
63
|
+
const ctx = useContext(CurrencyContext);
|
|
64
|
+
const shop = useShop();
|
|
65
|
+
const fallback = useMemo(() => {
|
|
66
|
+
const ccy = shop?.currency || "EGP";
|
|
67
|
+
return {
|
|
68
|
+
base: ccy,
|
|
69
|
+
selected: ccy,
|
|
70
|
+
presentment: [ccy],
|
|
71
|
+
rates: {},
|
|
72
|
+
autoConvert: false,
|
|
73
|
+
loading: false,
|
|
74
|
+
setSelected: () => {
|
|
75
|
+
},
|
|
76
|
+
convert: (cents) => cents
|
|
77
|
+
};
|
|
78
|
+
}, [shop?.currency]);
|
|
79
|
+
return ctx ?? fallback;
|
|
80
|
+
}
|
|
536
81
|
|
|
537
82
|
// src/hooks/useMoney.ts
|
|
538
83
|
function useMoney(currencyOverride) {
|
|
539
84
|
const { formatMoney } = useLocalization();
|
|
540
85
|
const shop = useShop();
|
|
541
|
-
const
|
|
542
|
-
return (amount) =>
|
|
86
|
+
const { selected, base, autoConvert, convert } = useCurrency();
|
|
87
|
+
return (amount) => {
|
|
88
|
+
const shouldConvert = autoConvert && !currencyOverride && !!selected && selected !== base;
|
|
89
|
+
if (shouldConvert) {
|
|
90
|
+
const converted = convert(Math.round(amount * 100), selected) / 100;
|
|
91
|
+
return formatMoney(converted, selected);
|
|
92
|
+
}
|
|
93
|
+
return formatMoney(amount, currencyOverride || shop?.currency);
|
|
94
|
+
};
|
|
543
95
|
}
|
|
544
96
|
var DEFAULT_WIDTHS = [320, 480, 640, 768, 1024, 1280, 1600, 1920];
|
|
545
97
|
var DEFAULT_SIZES = "(min-width: 1024px) 25vw, (min-width: 640px) 50vw, 100vw";
|
|
@@ -553,81 +105,6 @@ function useImage(src, opts = {}) {
|
|
|
553
105
|
return { src, srcSet, sizes, alt };
|
|
554
106
|
}, [src, sizes, widths, alt]);
|
|
555
107
|
}
|
|
556
|
-
function useProducts(opts = {}) {
|
|
557
|
-
const { limit, fetchIfMissing = false } = opts;
|
|
558
|
-
const page = usePage();
|
|
559
|
-
const shop = useShop();
|
|
560
|
-
const initial = page?.data?.products ?? null;
|
|
561
|
-
const [products, setProducts] = useState(initial ?? []);
|
|
562
|
-
const [loading, setLoading] = useState(
|
|
563
|
-
initial == null && fetchIfMissing
|
|
564
|
-
);
|
|
565
|
-
const [error, setError] = useState(null);
|
|
566
|
-
useEffect(() => {
|
|
567
|
-
if (initial != null) return;
|
|
568
|
-
if (!fetchIfMissing) return;
|
|
569
|
-
if (!shop?.id) return;
|
|
570
|
-
let cancelled = false;
|
|
571
|
-
(async () => {
|
|
572
|
-
try {
|
|
573
|
-
const params = new URLSearchParams({ store_id: shop.id });
|
|
574
|
-
if (limit) params.set("limit", String(limit));
|
|
575
|
-
const res = await fetch(`/api/products?${params.toString()}`);
|
|
576
|
-
if (!res.ok) throw new Error(`/api/products \u2192 ${res.status}`);
|
|
577
|
-
const data = await res.json();
|
|
578
|
-
if (cancelled) return;
|
|
579
|
-
setProducts(data.products ?? []);
|
|
580
|
-
setLoading(false);
|
|
581
|
-
} catch (err) {
|
|
582
|
-
if (cancelled) return;
|
|
583
|
-
setError(err instanceof Error ? err : new Error(String(err)));
|
|
584
|
-
setLoading(false);
|
|
585
|
-
}
|
|
586
|
-
})();
|
|
587
|
-
return () => {
|
|
588
|
-
cancelled = true;
|
|
589
|
-
};
|
|
590
|
-
}, [initial, fetchIfMissing, limit, shop?.id]);
|
|
591
|
-
const sliced = limit != null ? products.slice(0, limit) : products;
|
|
592
|
-
return { products: sliced, loading, error };
|
|
593
|
-
}
|
|
594
|
-
function useCollections(opts = {}) {
|
|
595
|
-
const { limit, fetchIfMissing = false } = opts;
|
|
596
|
-
const page = usePage();
|
|
597
|
-
const shop = useShop();
|
|
598
|
-
const initial = page?.data?.collections ?? null;
|
|
599
|
-
const [collections, setCollections] = useState(initial ?? []);
|
|
600
|
-
const [loading, setLoading] = useState(
|
|
601
|
-
initial == null && fetchIfMissing
|
|
602
|
-
);
|
|
603
|
-
const [error, setError] = useState(null);
|
|
604
|
-
useEffect(() => {
|
|
605
|
-
if (initial != null) return;
|
|
606
|
-
if (!fetchIfMissing) return;
|
|
607
|
-
if (!shop?.id) return;
|
|
608
|
-
let cancelled = false;
|
|
609
|
-
(async () => {
|
|
610
|
-
try {
|
|
611
|
-
const params = new URLSearchParams({ store_id: shop.id });
|
|
612
|
-
const res = await fetch(`/api/collections?${params.toString()}`);
|
|
613
|
-
if (!res.ok) throw new Error(`/api/collections \u2192 ${res.status}`);
|
|
614
|
-
const data = await res.json();
|
|
615
|
-
if (cancelled) return;
|
|
616
|
-
setCollections(data.collections ?? []);
|
|
617
|
-
setLoading(false);
|
|
618
|
-
} catch (err) {
|
|
619
|
-
if (cancelled) return;
|
|
620
|
-
setError(err instanceof Error ? err : new Error(String(err)));
|
|
621
|
-
setLoading(false);
|
|
622
|
-
}
|
|
623
|
-
})();
|
|
624
|
-
return () => {
|
|
625
|
-
cancelled = true;
|
|
626
|
-
};
|
|
627
|
-
}, [initial, fetchIfMissing, shop?.id]);
|
|
628
|
-
const sliced = limit != null ? collections.slice(0, limit) : collections;
|
|
629
|
-
return { collections: sliced, loading, error };
|
|
630
|
-
}
|
|
631
108
|
var NOOP_ACTIONS = {
|
|
632
109
|
login: async () => ({}),
|
|
633
110
|
register: async () => ({}),
|
|
@@ -997,7 +474,7 @@ function useNavigation(handle, options) {
|
|
|
997
474
|
}, [handle, hostItems === null]);
|
|
998
475
|
return { items, loading, error };
|
|
999
476
|
}
|
|
1000
|
-
var
|
|
477
|
+
var EMPTY2 = {
|
|
1001
478
|
products: [],
|
|
1002
479
|
collections: [],
|
|
1003
480
|
pages: [],
|
|
@@ -1012,7 +489,7 @@ function useSearch(query, options = {}) {
|
|
|
1012
489
|
debounceMs = 200,
|
|
1013
490
|
immediate = false
|
|
1014
491
|
} = options;
|
|
1015
|
-
const [results, setResults] = useState(
|
|
492
|
+
const [results, setResults] = useState(EMPTY2);
|
|
1016
493
|
const [loading, setLoading] = useState(false);
|
|
1017
494
|
const [error, setError] = useState(null);
|
|
1018
495
|
const lastRequestId = useRef(0);
|
|
@@ -1020,7 +497,7 @@ function useSearch(query, options = {}) {
|
|
|
1020
497
|
if (typeof window === "undefined") return;
|
|
1021
498
|
const trimmed = query.trim();
|
|
1022
499
|
if (!trimmed) {
|
|
1023
|
-
setResults(
|
|
500
|
+
setResults(EMPTY2);
|
|
1024
501
|
setLoading(false);
|
|
1025
502
|
setError(null);
|
|
1026
503
|
return;
|
|
@@ -1038,7 +515,7 @@ function useSearch(query, options = {}) {
|
|
|
1038
515
|
});
|
|
1039
516
|
if (requestId !== lastRequestId.current) return;
|
|
1040
517
|
if (!res.ok) {
|
|
1041
|
-
setResults(
|
|
518
|
+
setResults(EMPTY2);
|
|
1042
519
|
return;
|
|
1043
520
|
}
|
|
1044
521
|
const json = await res.json();
|
|
@@ -1054,7 +531,7 @@ function useSearch(query, options = {}) {
|
|
|
1054
531
|
} catch (err) {
|
|
1055
532
|
if (requestId !== lastRequestId.current) return;
|
|
1056
533
|
setError(err instanceof Error ? err : new Error(String(err)));
|
|
1057
|
-
setResults(
|
|
534
|
+
setResults(EMPTY2);
|
|
1058
535
|
} finally {
|
|
1059
536
|
if (requestId === lastRequestId.current) setLoading(false);
|
|
1060
537
|
}
|
|
@@ -1159,63 +636,218 @@ function useAnalytics() {
|
|
|
1159
636
|
);
|
|
1160
637
|
return useMemo(() => ({ track }), [track]);
|
|
1161
638
|
}
|
|
639
|
+
var cache2 = /* @__PURE__ */ new Map();
|
|
640
|
+
function buildSnapshot(entry) {
|
|
641
|
+
return {
|
|
642
|
+
data: entry.data,
|
|
643
|
+
error: entry.error,
|
|
644
|
+
isLoading: !entry.settled,
|
|
645
|
+
isValidating: entry.inFlight
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
function commit(entry) {
|
|
649
|
+
entry.snapshot = buildSnapshot(entry);
|
|
650
|
+
entry.subscribers.forEach((cb) => cb());
|
|
651
|
+
}
|
|
652
|
+
function getEntry(key, initialData) {
|
|
653
|
+
let entry = cache2.get(key);
|
|
654
|
+
if (!entry) {
|
|
655
|
+
entry = {
|
|
656
|
+
data: initialData,
|
|
657
|
+
error: void 0,
|
|
658
|
+
settled: false,
|
|
659
|
+
inFlight: false,
|
|
660
|
+
seq: 0,
|
|
661
|
+
lastFetchAt: 0,
|
|
662
|
+
promise: void 0,
|
|
663
|
+
controller: void 0,
|
|
664
|
+
fetcher: void 0,
|
|
665
|
+
subscribers: /* @__PURE__ */ new Set(),
|
|
666
|
+
snapshot: {
|
|
667
|
+
data: initialData,
|
|
668
|
+
error: void 0,
|
|
669
|
+
isLoading: true,
|
|
670
|
+
isValidating: false
|
|
671
|
+
}
|
|
672
|
+
};
|
|
673
|
+
cache2.set(key, entry);
|
|
674
|
+
}
|
|
675
|
+
return entry;
|
|
676
|
+
}
|
|
677
|
+
function revalidateResource(key, fetcher, opts) {
|
|
678
|
+
const entry = getEntry(key);
|
|
679
|
+
entry.fetcher = fetcher;
|
|
680
|
+
if (!opts?.force && entry.promise) return entry.promise;
|
|
681
|
+
const mySeq = ++entry.seq;
|
|
682
|
+
if (entry.controller) entry.controller.abort();
|
|
683
|
+
const controller = new AbortController();
|
|
684
|
+
entry.controller = controller;
|
|
685
|
+
entry.inFlight = true;
|
|
686
|
+
commit(entry);
|
|
687
|
+
const run = (async () => {
|
|
688
|
+
try {
|
|
689
|
+
const data = await fetcher(controller.signal);
|
|
690
|
+
if (mySeq !== entry.seq) return;
|
|
691
|
+
entry.data = data;
|
|
692
|
+
entry.error = void 0;
|
|
693
|
+
entry.settled = true;
|
|
694
|
+
entry.lastFetchAt = Date.now();
|
|
695
|
+
} catch (err) {
|
|
696
|
+
if (controller.signal.aborted) return;
|
|
697
|
+
if (mySeq !== entry.seq) return;
|
|
698
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
699
|
+
entry.error = error;
|
|
700
|
+
entry.settled = true;
|
|
701
|
+
entry.lastFetchAt = Date.now();
|
|
702
|
+
opts?.onError?.(error, key);
|
|
703
|
+
} finally {
|
|
704
|
+
if (mySeq === entry.seq) {
|
|
705
|
+
entry.inFlight = false;
|
|
706
|
+
entry.controller = void 0;
|
|
707
|
+
commit(entry);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
})();
|
|
711
|
+
entry.promise = run;
|
|
712
|
+
void run.finally(() => {
|
|
713
|
+
if (entry.promise === run) entry.promise = void 0;
|
|
714
|
+
});
|
|
715
|
+
return run;
|
|
716
|
+
}
|
|
717
|
+
function mutateResource(key, next, opts) {
|
|
718
|
+
const entry = getEntry(key);
|
|
719
|
+
const value = typeof next === "function" ? next(entry.data) : next;
|
|
720
|
+
entry.data = value;
|
|
721
|
+
entry.error = void 0;
|
|
722
|
+
entry.settled = true;
|
|
723
|
+
commit(entry);
|
|
724
|
+
if (opts?.revalidate && entry.fetcher) {
|
|
725
|
+
void revalidateResource(key, entry.fetcher, { force: true });
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
function readResource(key) {
|
|
729
|
+
return cache2.get(key)?.data;
|
|
730
|
+
}
|
|
731
|
+
var NOOP = () => {
|
|
732
|
+
};
|
|
733
|
+
function useCachedResource(key, fetcher, options) {
|
|
734
|
+
const enabled = options?.enabled !== false && key != null && key !== "";
|
|
735
|
+
const revalidateOnMount = options?.revalidateOnMount !== false;
|
|
736
|
+
const dedupeIntervalMs = options?.dedupeIntervalMs ?? 2e3;
|
|
737
|
+
const cacheKey = enabled ? key : "";
|
|
738
|
+
const fetcherRef = useRef(fetcher);
|
|
739
|
+
fetcherRef.current = fetcher;
|
|
740
|
+
const onErrorRef = useRef(options?.onError);
|
|
741
|
+
onErrorRef.current = options?.onError;
|
|
742
|
+
const initialDataRef = useRef(options?.initialData);
|
|
743
|
+
const fallbackRef = useRef(null);
|
|
744
|
+
if (fallbackRef.current === null) {
|
|
745
|
+
fallbackRef.current = {
|
|
746
|
+
data: initialDataRef.current,
|
|
747
|
+
error: void 0,
|
|
748
|
+
isLoading: enabled && revalidateOnMount,
|
|
749
|
+
isValidating: false
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
const subscribe = useCallback(
|
|
753
|
+
(cb) => {
|
|
754
|
+
if (!enabled) return NOOP;
|
|
755
|
+
const entry = getEntry(cacheKey, initialDataRef.current);
|
|
756
|
+
entry.subscribers.add(cb);
|
|
757
|
+
return () => {
|
|
758
|
+
entry.subscribers.delete(cb);
|
|
759
|
+
};
|
|
760
|
+
},
|
|
761
|
+
[cacheKey, enabled]
|
|
762
|
+
);
|
|
763
|
+
const getSnapshot = useCallback(() => {
|
|
764
|
+
if (!enabled) return fallbackRef.current;
|
|
765
|
+
return getEntry(cacheKey, initialDataRef.current).snapshot;
|
|
766
|
+
}, [cacheKey, enabled]);
|
|
767
|
+
const getServerSnapshot = useCallback(
|
|
768
|
+
() => fallbackRef.current,
|
|
769
|
+
[]
|
|
770
|
+
);
|
|
771
|
+
const state = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
|
772
|
+
useEffect(() => {
|
|
773
|
+
if (!enabled) return;
|
|
774
|
+
const entry = getEntry(cacheKey, initialDataRef.current);
|
|
775
|
+
const fresh = entry.settled && Date.now() - entry.lastFetchAt < dedupeIntervalMs;
|
|
776
|
+
if (revalidateOnMount && !fresh) {
|
|
777
|
+
void revalidateResource(
|
|
778
|
+
cacheKey,
|
|
779
|
+
(signal) => fetcherRef.current(signal),
|
|
780
|
+
{ onError: (e, k) => onErrorRef.current?.(e, k) }
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
}, [cacheKey, enabled, revalidateOnMount, dedupeIntervalMs]);
|
|
784
|
+
const mutate = useCallback(
|
|
785
|
+
(next, opts) => {
|
|
786
|
+
if (!enabled) return;
|
|
787
|
+
mutateResource(cacheKey, next, opts);
|
|
788
|
+
},
|
|
789
|
+
[cacheKey, enabled]
|
|
790
|
+
);
|
|
791
|
+
const revalidate = useCallback(() => {
|
|
792
|
+
if (!enabled) return Promise.resolve();
|
|
793
|
+
return revalidateResource(
|
|
794
|
+
cacheKey,
|
|
795
|
+
(signal) => fetcherRef.current(signal),
|
|
796
|
+
{ force: true, onError: (e, k) => onErrorRef.current?.(e, k) }
|
|
797
|
+
);
|
|
798
|
+
}, [cacheKey, enabled]);
|
|
799
|
+
return {
|
|
800
|
+
data: state.data,
|
|
801
|
+
error: state.error,
|
|
802
|
+
isLoading: state.isLoading,
|
|
803
|
+
isValidating: state.isValidating,
|
|
804
|
+
mutate,
|
|
805
|
+
revalidate
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// src/hooks/useApp.ts
|
|
1162
810
|
function useApp(slug) {
|
|
1163
811
|
const shop = useShop();
|
|
1164
|
-
const
|
|
1165
|
-
const
|
|
1166
|
-
|
|
1167
|
-
setState({ data: null, loading: false, available: false, error: null });
|
|
1168
|
-
return;
|
|
1169
|
-
}
|
|
1170
|
-
setState((s) => ({ ...s, loading: true, error: null }));
|
|
1171
|
-
try {
|
|
812
|
+
const key = slug ? `numu:app:${shop.id}:${slug}` : "";
|
|
813
|
+
const fetcher = useCallback(
|
|
814
|
+
async (signal) => {
|
|
1172
815
|
const url = `/api/storefront/apps/${encodeURIComponent(slug)}`;
|
|
1173
816
|
const res = await fetch(url, {
|
|
1174
817
|
credentials: "include",
|
|
1175
|
-
headers: { Accept: "application/json" }
|
|
818
|
+
headers: { Accept: "application/json" },
|
|
819
|
+
signal
|
|
1176
820
|
});
|
|
1177
821
|
if (res.status === 404) {
|
|
1178
|
-
|
|
1179
|
-
data: null,
|
|
1180
|
-
loading: false,
|
|
1181
|
-
available: false,
|
|
1182
|
-
error: null
|
|
1183
|
-
});
|
|
1184
|
-
return;
|
|
822
|
+
return { payload: null, available: false };
|
|
1185
823
|
}
|
|
1186
824
|
if (!res.ok) {
|
|
1187
825
|
throw new Error(`useApp(${slug}) failed: HTTP ${res.status}`);
|
|
1188
826
|
}
|
|
1189
827
|
const body = await res.json();
|
|
1190
|
-
const payload = body.data;
|
|
1191
|
-
if (!payload) {
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
}
|
|
1214
|
-
}, [slug, shop.id]);
|
|
1215
|
-
useEffect(() => {
|
|
1216
|
-
void fetchApp();
|
|
1217
|
-
}, [fetchApp]);
|
|
1218
|
-
return { ...state, refresh: fetchApp };
|
|
828
|
+
const payload = body.data ?? null;
|
|
829
|
+
if (!payload) return { payload: null, available: false };
|
|
830
|
+
return {
|
|
831
|
+
payload,
|
|
832
|
+
available: payload.available !== false
|
|
833
|
+
};
|
|
834
|
+
},
|
|
835
|
+
[slug]
|
|
836
|
+
);
|
|
837
|
+
const { data, isLoading, error, revalidate } = useCachedResource(key, fetcher, {
|
|
838
|
+
enabled: Boolean(slug),
|
|
839
|
+
initialData: { payload: null, available: false }
|
|
840
|
+
});
|
|
841
|
+
const refresh = useCallback(async () => {
|
|
842
|
+
await revalidate();
|
|
843
|
+
}, [revalidate]);
|
|
844
|
+
return {
|
|
845
|
+
data: error ? null : data?.payload ?? null,
|
|
846
|
+
loading: isLoading,
|
|
847
|
+
available: error ? false : data?.available ?? false,
|
|
848
|
+
error: error ?? null,
|
|
849
|
+
refresh
|
|
850
|
+
};
|
|
1219
851
|
}
|
|
1220
852
|
function storageKey(storeId) {
|
|
1221
853
|
return `numu_wishlist_${storeId}`;
|
|
@@ -1231,109 +863,95 @@ function readLocal(storeId) {
|
|
|
1231
863
|
return [];
|
|
1232
864
|
}
|
|
1233
865
|
}
|
|
1234
|
-
function
|
|
866
|
+
function persistWishlist(storeId, items) {
|
|
1235
867
|
if (typeof window === "undefined") return;
|
|
1236
|
-
|
|
1237
|
-
window.localStorage.setItem(storageKey(storeId), JSON.stringify(items));
|
|
1238
|
-
} catch {
|
|
1239
|
-
}
|
|
868
|
+
window.localStorage.setItem(storageKey(storeId), JSON.stringify(items));
|
|
1240
869
|
}
|
|
1241
870
|
function sameItem(a, productId, variantId) {
|
|
1242
871
|
return a.product_id === productId && (a.variant_id ?? null) === (variantId ?? null);
|
|
1243
872
|
}
|
|
873
|
+
var EMPTY3 = [];
|
|
1244
874
|
function useWishlist(storeId) {
|
|
1245
|
-
const
|
|
1246
|
-
const
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
875
|
+
const key = storageKey(storeId);
|
|
876
|
+
const fetcher = useCallback(
|
|
877
|
+
() => Promise.resolve(readLocal(storeId)),
|
|
878
|
+
[storeId]
|
|
879
|
+
);
|
|
880
|
+
const { data, isLoading, mutate } = useCachedResource(
|
|
881
|
+
key,
|
|
882
|
+
fetcher,
|
|
883
|
+
{ initialData: EMPTY3 }
|
|
884
|
+
);
|
|
885
|
+
const items = data ?? EMPTY3;
|
|
1253
886
|
const has = useCallback(
|
|
1254
887
|
(productId, variantId) => items.some((it) => sameItem(it, productId, variantId)),
|
|
1255
888
|
[items]
|
|
1256
889
|
);
|
|
890
|
+
const applyOptimistic = useCallback(
|
|
891
|
+
(compute) => {
|
|
892
|
+
const prev = readResource(key) ?? EMPTY3;
|
|
893
|
+
const next = compute(prev);
|
|
894
|
+
if (next === null) return;
|
|
895
|
+
mutate(next, { revalidate: false });
|
|
896
|
+
try {
|
|
897
|
+
persistWishlist(storeId, next);
|
|
898
|
+
} catch {
|
|
899
|
+
mutate(prev, { revalidate: false });
|
|
900
|
+
}
|
|
901
|
+
},
|
|
902
|
+
[key, storeId, mutate]
|
|
903
|
+
);
|
|
1257
904
|
const addToWishlist = useCallback(
|
|
1258
|
-
(productId, variantId) =>
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
905
|
+
(productId, variantId) => applyOptimistic(
|
|
906
|
+
(prev) => prev.some((it) => sameItem(it, productId, variantId)) ? null : [
|
|
907
|
+
...prev,
|
|
908
|
+
{
|
|
909
|
+
product_id: productId,
|
|
910
|
+
variant_id: variantId ?? null,
|
|
911
|
+
added_at: Date.now()
|
|
1262
912
|
}
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
product_id: productId,
|
|
1267
|
-
variant_id: variantId ?? null,
|
|
1268
|
-
added_at: Date.now()
|
|
1269
|
-
}
|
|
1270
|
-
];
|
|
1271
|
-
writeLocal(storeId, next);
|
|
1272
|
-
return next;
|
|
1273
|
-
});
|
|
1274
|
-
},
|
|
1275
|
-
[storeId]
|
|
913
|
+
]
|
|
914
|
+
),
|
|
915
|
+
[applyOptimistic]
|
|
1276
916
|
);
|
|
1277
917
|
const removeFromWishlist = useCallback(
|
|
1278
|
-
(productId, variantId) => {
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
[
|
|
918
|
+
(productId, variantId) => applyOptimistic((prev) => {
|
|
919
|
+
const next = prev.filter((it) => !sameItem(it, productId, variantId));
|
|
920
|
+
return next.length === prev.length ? null : next;
|
|
921
|
+
}),
|
|
922
|
+
[applyOptimistic]
|
|
923
|
+
);
|
|
924
|
+
const clear = useCallback(
|
|
925
|
+
() => applyOptimistic((prev) => prev.length === 0 ? null : []),
|
|
926
|
+
[applyOptimistic]
|
|
1286
927
|
);
|
|
1287
|
-
|
|
1288
|
-
setItems([]);
|
|
1289
|
-
writeLocal(storeId, []);
|
|
1290
|
-
}, [storeId]);
|
|
1291
|
-
return { items, loading, has, addToWishlist, removeFromWishlist, clear };
|
|
928
|
+
return { items, loading: isLoading, has, addToWishlist, removeFromWishlist, clear };
|
|
1292
929
|
}
|
|
930
|
+
var EMPTY4 = [];
|
|
1293
931
|
function useRelatedProducts(productId, options = {}) {
|
|
1294
932
|
const limit = options.limit ?? 4;
|
|
1295
|
-
const
|
|
1296
|
-
const
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
let cancelled = false;
|
|
1307
|
-
setLoading(true);
|
|
1308
|
-
setError(null);
|
|
1309
|
-
void (async () => {
|
|
1310
|
-
try {
|
|
1311
|
-
const res = await fetch(
|
|
1312
|
-
`/api/storefront/products/${encodeURIComponent(productId)}/related?limit=${limit}`,
|
|
1313
|
-
{ cache: "no-store" }
|
|
1314
|
-
);
|
|
1315
|
-
if (!res.ok) {
|
|
1316
|
-
if (cancelled) return;
|
|
1317
|
-
setItems([]);
|
|
1318
|
-
return;
|
|
1319
|
-
}
|
|
1320
|
-
const json = await res.json();
|
|
1321
|
-
const list = Array.isArray(json) ? json : Array.isArray(json.data) ? json.data : Array.isArray(json.items) ? json.items : [];
|
|
1322
|
-
if (cancelled) return;
|
|
1323
|
-
setItems(list);
|
|
1324
|
-
} catch (err) {
|
|
1325
|
-
if (cancelled) return;
|
|
1326
|
-
setError(err instanceof Error ? err : new Error(String(err)));
|
|
1327
|
-
setItems([]);
|
|
1328
|
-
} finally {
|
|
1329
|
-
if (!cancelled) setLoading(false);
|
|
933
|
+
const key = productId ? `numu:related:${productId}:${limit}` : "";
|
|
934
|
+
const fetcher = useCallback(
|
|
935
|
+
async (signal) => {
|
|
936
|
+
const res = await fetch(
|
|
937
|
+
`/api/storefront/products/${encodeURIComponent(
|
|
938
|
+
productId
|
|
939
|
+
)}/related?limit=${limit}`,
|
|
940
|
+
{ cache: "no-store", signal }
|
|
941
|
+
);
|
|
942
|
+
if (!res.ok) {
|
|
943
|
+
return EMPTY4;
|
|
1330
944
|
}
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
945
|
+
const json = await res.json();
|
|
946
|
+
return Array.isArray(json) ? json : Array.isArray(json.data) ? json.data : Array.isArray(json.items) ? json.items : EMPTY4;
|
|
947
|
+
},
|
|
948
|
+
[productId, limit]
|
|
949
|
+
);
|
|
950
|
+
const { data, isLoading, error } = useCachedResource(key, fetcher, {
|
|
951
|
+
enabled: Boolean(productId),
|
|
952
|
+
initialData: EMPTY4
|
|
953
|
+
});
|
|
954
|
+
return { items: data ?? EMPTY4, loading: isLoading, error: error ?? null };
|
|
1337
955
|
}
|
|
1338
956
|
function useProductSizeChart(productOverride) {
|
|
1339
957
|
const ctxProduct = useProductOptional();
|
|
@@ -1358,86 +976,6 @@ function resolveSizeChart(productAttributes, storeSettings) {
|
|
|
1358
976
|
if (hasRows(storeDefault)) return storeDefault;
|
|
1359
977
|
return null;
|
|
1360
978
|
}
|
|
1361
|
-
var COOKIE_NAME = "numu_currency";
|
|
1362
|
-
var COOKIE_MAX_AGE = 60 * 60 * 24 * 30;
|
|
1363
|
-
function readCookie(name) {
|
|
1364
|
-
if (typeof document === "undefined") return null;
|
|
1365
|
-
const match = document.cookie.match(
|
|
1366
|
-
new RegExp(`(?:^|;\\s*)${name}=([^;]+)`)
|
|
1367
|
-
);
|
|
1368
|
-
return match ? decodeURIComponent(match[1]) : null;
|
|
1369
|
-
}
|
|
1370
|
-
function writeCookie(name, value) {
|
|
1371
|
-
if (typeof document === "undefined") return;
|
|
1372
|
-
document.cookie = `${name}=${encodeURIComponent(value)}; path=/; max-age=${COOKIE_MAX_AGE}; SameSite=Lax`;
|
|
1373
|
-
}
|
|
1374
|
-
function useCurrency() {
|
|
1375
|
-
const shop = useShop();
|
|
1376
|
-
const [config, setConfig] = useState(null);
|
|
1377
|
-
const [loading, setLoading] = useState(true);
|
|
1378
|
-
const [selected, setSelectedState] = useState("");
|
|
1379
|
-
useEffect(() => {
|
|
1380
|
-
let cancelled = false;
|
|
1381
|
-
(async () => {
|
|
1382
|
-
try {
|
|
1383
|
-
const res = await fetch(`/api/storefront/currencies`, {
|
|
1384
|
-
credentials: "include",
|
|
1385
|
-
headers: { Accept: "application/json" }
|
|
1386
|
-
});
|
|
1387
|
-
if (!res.ok) throw new Error(`currencies: HTTP ${res.status}`);
|
|
1388
|
-
const body = await res.json();
|
|
1389
|
-
if (cancelled) return;
|
|
1390
|
-
setConfig(body.data);
|
|
1391
|
-
const cookie = readCookie(COOKIE_NAME);
|
|
1392
|
-
const valid = cookie && body.data.presentment.includes(cookie) ? cookie : null;
|
|
1393
|
-
setSelectedState(valid || body.data.default_presentment || body.data.base);
|
|
1394
|
-
} catch {
|
|
1395
|
-
if (cancelled) return;
|
|
1396
|
-
setConfig(null);
|
|
1397
|
-
setSelectedState(shop.currency || "EGP");
|
|
1398
|
-
} finally {
|
|
1399
|
-
if (!cancelled) setLoading(false);
|
|
1400
|
-
}
|
|
1401
|
-
})();
|
|
1402
|
-
return () => {
|
|
1403
|
-
cancelled = true;
|
|
1404
|
-
};
|
|
1405
|
-
}, [shop.id, shop.currency]);
|
|
1406
|
-
const rates = useMemo(() => {
|
|
1407
|
-
const out = {};
|
|
1408
|
-
if (!config) return out;
|
|
1409
|
-
for (const [k, v] of Object.entries(config.rates)) {
|
|
1410
|
-
const n = Number.parseFloat(v);
|
|
1411
|
-
if (Number.isFinite(n)) out[k] = n;
|
|
1412
|
-
}
|
|
1413
|
-
return out;
|
|
1414
|
-
}, [config]);
|
|
1415
|
-
const convert = useCallback(
|
|
1416
|
-
(cents, target) => {
|
|
1417
|
-
if (!config) return cents;
|
|
1418
|
-
const to = target || selected;
|
|
1419
|
-
if (!to || to === config.base) return cents;
|
|
1420
|
-
const rate = rates[to];
|
|
1421
|
-
if (!rate || !Number.isFinite(rate)) return cents;
|
|
1422
|
-
return Math.round(cents * rate);
|
|
1423
|
-
},
|
|
1424
|
-
[config, rates, selected]
|
|
1425
|
-
);
|
|
1426
|
-
const setSelected = useCallback((currency) => {
|
|
1427
|
-
setSelectedState(currency);
|
|
1428
|
-
writeCookie(COOKIE_NAME, currency);
|
|
1429
|
-
}, []);
|
|
1430
|
-
return {
|
|
1431
|
-
base: config?.base || shop.currency || "EGP",
|
|
1432
|
-
selected: selected || config?.base || shop.currency || "EGP",
|
|
1433
|
-
presentment: config?.presentment || [shop.currency || "EGP"],
|
|
1434
|
-
rates,
|
|
1435
|
-
autoConvert: Boolean(config?.auto_convert),
|
|
1436
|
-
loading,
|
|
1437
|
-
setSelected,
|
|
1438
|
-
convert
|
|
1439
|
-
};
|
|
1440
|
-
}
|
|
1441
979
|
|
|
1442
980
|
// src/utils/variants.ts
|
|
1443
981
|
function findVariantByOptions(product, selection) {
|
|
@@ -1893,6 +1431,19 @@ function readCsrfCookie() {
|
|
|
1893
1431
|
const match = document.cookie.match(/(?:^|;\s*)numu_csrf=([^;]+)/);
|
|
1894
1432
|
return match ? decodeURIComponent(match[1]) : null;
|
|
1895
1433
|
}
|
|
1434
|
+
var CURRENCY_COOKIE_NAME = "numu_currency";
|
|
1435
|
+
var CURRENCY_COOKIE_MAX_AGE = 60 * 60 * 24 * 30;
|
|
1436
|
+
function readCurrencyCookie() {
|
|
1437
|
+
if (typeof document === "undefined") return null;
|
|
1438
|
+
const match = document.cookie.match(
|
|
1439
|
+
new RegExp(`(?:^|;\\s*)${CURRENCY_COOKIE_NAME}=([^;]+)`)
|
|
1440
|
+
);
|
|
1441
|
+
return match ? decodeURIComponent(match[1]) : null;
|
|
1442
|
+
}
|
|
1443
|
+
function writeCurrencyCookie(value) {
|
|
1444
|
+
if (typeof document === "undefined") return;
|
|
1445
|
+
document.cookie = `${CURRENCY_COOKIE_NAME}=${encodeURIComponent(value)}; path=/; max-age=${CURRENCY_COOKIE_MAX_AGE}; SameSite=Lax`;
|
|
1446
|
+
}
|
|
1896
1447
|
async function postCartMutation(endpoint, body, applyCart, reserveToken) {
|
|
1897
1448
|
const headers = {
|
|
1898
1449
|
"Content-Type": "application/json"
|
|
@@ -1906,9 +1457,21 @@ async function postCartMutation(endpoint, body, applyCart, reserveToken) {
|
|
|
1906
1457
|
headers,
|
|
1907
1458
|
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
1908
1459
|
});
|
|
1909
|
-
if (!res.ok)
|
|
1460
|
+
if (!res.ok) {
|
|
1461
|
+
let message;
|
|
1462
|
+
try {
|
|
1463
|
+
const err = await res.json();
|
|
1464
|
+
if (err && typeof err === "object") {
|
|
1465
|
+
const m = err.message ?? err.error ?? err.detail;
|
|
1466
|
+
if (typeof m === "string") message = m;
|
|
1467
|
+
}
|
|
1468
|
+
} catch {
|
|
1469
|
+
}
|
|
1470
|
+
return { ok: false, status: res.status, message };
|
|
1471
|
+
}
|
|
1910
1472
|
const json = await res.json();
|
|
1911
1473
|
applyCart(unwrapCart(json));
|
|
1474
|
+
return { ok: true, status: res.status };
|
|
1912
1475
|
}
|
|
1913
1476
|
function NuMuProvider({
|
|
1914
1477
|
store,
|
|
@@ -1918,6 +1481,7 @@ function NuMuProvider({
|
|
|
1918
1481
|
locale: initialLocale,
|
|
1919
1482
|
translations: initialTranslations,
|
|
1920
1483
|
currentTemplate = "home",
|
|
1484
|
+
pageTemplate,
|
|
1921
1485
|
initialProducts,
|
|
1922
1486
|
initialCollections,
|
|
1923
1487
|
navigation,
|
|
@@ -1927,17 +1491,21 @@ function NuMuProvider({
|
|
|
1927
1491
|
() => ({
|
|
1928
1492
|
type: currentTemplate,
|
|
1929
1493
|
title: store?.name ?? "",
|
|
1494
|
+
// I3 — resolved alternate template key (e.g. "product.wholesale"),
|
|
1495
|
+
// surfaced via usePage()?.template. Omitted when undefined so a
|
|
1496
|
+
// page on its default template exposes no `template` field.
|
|
1497
|
+
...pageTemplate ? { template: pageTemplate } : {},
|
|
1930
1498
|
data: {
|
|
1931
1499
|
products: initialProducts ?? [],
|
|
1932
1500
|
collections: initialCollections ?? []
|
|
1933
1501
|
}
|
|
1934
1502
|
}),
|
|
1935
|
-
[currentTemplate, store?.name, initialProducts, initialCollections]
|
|
1503
|
+
[currentTemplate, pageTemplate, store?.name, initialProducts, initialCollections]
|
|
1936
1504
|
);
|
|
1937
1505
|
const [cart, setCart] = useState(
|
|
1938
1506
|
initialCart || { ...EMPTY_CART, currency: store.currency }
|
|
1939
1507
|
);
|
|
1940
|
-
const [loading, setLoading] = useState(
|
|
1508
|
+
const [loading, setLoading] = useState(!initialCart);
|
|
1941
1509
|
const [locale, setLocale] = useState(() => {
|
|
1942
1510
|
if (initialLocale) return initialLocale;
|
|
1943
1511
|
if (typeof document !== "undefined") {
|
|
@@ -2094,6 +1662,8 @@ function NuMuProvider({
|
|
|
2094
1662
|
setCart(normalizeCartFromServer(data));
|
|
2095
1663
|
}
|
|
2096
1664
|
} catch {
|
|
1665
|
+
} finally {
|
|
1666
|
+
if (!cancelled) setLoading(false);
|
|
2097
1667
|
}
|
|
2098
1668
|
})();
|
|
2099
1669
|
return () => {
|
|
@@ -2121,7 +1691,7 @@ function NuMuProvider({
|
|
|
2121
1691
|
const token = reserveToken();
|
|
2122
1692
|
setLoading(true);
|
|
2123
1693
|
try {
|
|
2124
|
-
await postCartMutation(
|
|
1694
|
+
return await postCartMutation(
|
|
2125
1695
|
endpoint,
|
|
2126
1696
|
body,
|
|
2127
1697
|
buildApplyCart(token),
|
|
@@ -2138,12 +1708,13 @@ function NuMuProvider({
|
|
|
2138
1708
|
async (productId, variantId, quantity) => {
|
|
2139
1709
|
const eventId = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}`;
|
|
2140
1710
|
const qty = quantity || 1;
|
|
2141
|
-
await mutate("/api/cart/add", {
|
|
1711
|
+
const result = await mutate("/api/cart/add", {
|
|
2142
1712
|
product_id: productId,
|
|
2143
1713
|
variant_id: variantId,
|
|
2144
1714
|
quantity: qty,
|
|
2145
1715
|
_event_id: eventId
|
|
2146
1716
|
});
|
|
1717
|
+
if (!result.ok) return result;
|
|
2147
1718
|
try {
|
|
2148
1719
|
if (typeof window !== "undefined") {
|
|
2149
1720
|
window.dispatchEvent(
|
|
@@ -2162,34 +1733,29 @@ function NuMuProvider({
|
|
|
2162
1733
|
}
|
|
2163
1734
|
} catch {
|
|
2164
1735
|
}
|
|
1736
|
+
return result;
|
|
2165
1737
|
},
|
|
2166
1738
|
[mutate]
|
|
2167
1739
|
);
|
|
2168
1740
|
const removeItem = useCallback(
|
|
2169
|
-
|
|
2170
|
-
await mutate("/api/cart/remove", { item_id: itemId });
|
|
2171
|
-
},
|
|
1741
|
+
(itemId) => mutate("/api/cart/remove", { item_id: itemId }),
|
|
2172
1742
|
[mutate]
|
|
2173
1743
|
);
|
|
2174
1744
|
const updateQuantity = useCallback(
|
|
2175
|
-
|
|
2176
|
-
await mutate("/api/cart/update", { item_id: itemId, quantity });
|
|
2177
|
-
},
|
|
1745
|
+
(itemId, quantity) => mutate("/api/cart/update", { item_id: itemId, quantity }),
|
|
2178
1746
|
[mutate]
|
|
2179
1747
|
);
|
|
2180
1748
|
const applyDiscount = useCallback(
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
1749
|
+
(code) => mutate("/api/cart/discount", { code }),
|
|
1750
|
+
[mutate]
|
|
1751
|
+
);
|
|
1752
|
+
const removeDiscount = useCallback(
|
|
1753
|
+
// DELETE — postCartMutation interprets undefined body as DELETE.
|
|
1754
|
+
() => mutate("/api/cart/discount", void 0),
|
|
2184
1755
|
[mutate]
|
|
2185
1756
|
);
|
|
2186
|
-
const removeDiscount = useCallback(async () => {
|
|
2187
|
-
await mutate("/api/cart/discount", void 0);
|
|
2188
|
-
}, [mutate]);
|
|
2189
1757
|
const updateNote = useCallback(
|
|
2190
|
-
|
|
2191
|
-
await mutate("/api/cart/update", { note });
|
|
2192
|
-
},
|
|
1758
|
+
(note) => mutate("/api/cart/update", { note }),
|
|
2193
1759
|
[mutate]
|
|
2194
1760
|
);
|
|
2195
1761
|
const clearCart = useCallback(async () => {
|
|
@@ -2325,7 +1891,87 @@ function NuMuProvider({
|
|
|
2325
1891
|
defaultNumberFmt
|
|
2326
1892
|
]
|
|
2327
1893
|
);
|
|
2328
|
-
|
|
1894
|
+
const [currencyConfig, setCurrencyConfig] = useState(
|
|
1895
|
+
null
|
|
1896
|
+
);
|
|
1897
|
+
const [currencyLoading, setCurrencyLoading] = useState(true);
|
|
1898
|
+
const [currencySelected, setCurrencySelected] = useState("");
|
|
1899
|
+
useEffect(() => {
|
|
1900
|
+
if (typeof window === "undefined") return;
|
|
1901
|
+
let cancelled = false;
|
|
1902
|
+
void (async () => {
|
|
1903
|
+
try {
|
|
1904
|
+
const res = await fetch("/api/storefront/currencies", {
|
|
1905
|
+
credentials: "include",
|
|
1906
|
+
headers: { Accept: "application/json" }
|
|
1907
|
+
});
|
|
1908
|
+
if (!res.ok) throw new Error(`currencies: HTTP ${res.status}`);
|
|
1909
|
+
const body = await res.json();
|
|
1910
|
+
if (cancelled) return;
|
|
1911
|
+
setCurrencyConfig(body.data);
|
|
1912
|
+
const cookie = readCurrencyCookie();
|
|
1913
|
+
const valid = cookie && body.data.presentment?.includes(cookie) ? cookie : null;
|
|
1914
|
+
setCurrencySelected(
|
|
1915
|
+
valid || body.data.default_presentment || body.data.base
|
|
1916
|
+
);
|
|
1917
|
+
} catch {
|
|
1918
|
+
if (cancelled) return;
|
|
1919
|
+
setCurrencyConfig(null);
|
|
1920
|
+
setCurrencySelected(store.currency || "EGP");
|
|
1921
|
+
} finally {
|
|
1922
|
+
if (!cancelled) setCurrencyLoading(false);
|
|
1923
|
+
}
|
|
1924
|
+
})();
|
|
1925
|
+
return () => {
|
|
1926
|
+
cancelled = true;
|
|
1927
|
+
};
|
|
1928
|
+
}, [store.id, store.currency]);
|
|
1929
|
+
const currencyRates = useMemo(() => {
|
|
1930
|
+
const out = {};
|
|
1931
|
+
if (!currencyConfig) return out;
|
|
1932
|
+
for (const [k, v] of Object.entries(currencyConfig.rates)) {
|
|
1933
|
+
const n = Number.parseFloat(v);
|
|
1934
|
+
if (Number.isFinite(n)) out[k] = n;
|
|
1935
|
+
}
|
|
1936
|
+
return out;
|
|
1937
|
+
}, [currencyConfig]);
|
|
1938
|
+
const convertCurrency = useCallback(
|
|
1939
|
+
(cents, target) => {
|
|
1940
|
+
if (!currencyConfig) return cents;
|
|
1941
|
+
const to = target || currencySelected;
|
|
1942
|
+
if (!to || to === currencyConfig.base) return cents;
|
|
1943
|
+
const rate = currencyRates[to];
|
|
1944
|
+
if (!rate || !Number.isFinite(rate)) return cents;
|
|
1945
|
+
return Math.round(cents * rate);
|
|
1946
|
+
},
|
|
1947
|
+
[currencyConfig, currencyRates, currencySelected]
|
|
1948
|
+
);
|
|
1949
|
+
const setCurrency = useCallback((currency) => {
|
|
1950
|
+
setCurrencySelected(currency);
|
|
1951
|
+
writeCurrencyCookie(currency);
|
|
1952
|
+
}, []);
|
|
1953
|
+
const currencyValue = useMemo(
|
|
1954
|
+
() => ({
|
|
1955
|
+
base: currencyConfig?.base || store.currency || "EGP",
|
|
1956
|
+
selected: currencySelected || currencyConfig?.base || store.currency || "EGP",
|
|
1957
|
+
presentment: currencyConfig?.presentment || [store.currency || "EGP"],
|
|
1958
|
+
rates: currencyRates,
|
|
1959
|
+
autoConvert: Boolean(currencyConfig?.auto_convert),
|
|
1960
|
+
loading: currencyLoading,
|
|
1961
|
+
setSelected: setCurrency,
|
|
1962
|
+
convert: convertCurrency
|
|
1963
|
+
}),
|
|
1964
|
+
[
|
|
1965
|
+
currencyConfig,
|
|
1966
|
+
currencySelected,
|
|
1967
|
+
currencyRates,
|
|
1968
|
+
currencyLoading,
|
|
1969
|
+
store.currency,
|
|
1970
|
+
setCurrency,
|
|
1971
|
+
convertCurrency
|
|
1972
|
+
]
|
|
1973
|
+
);
|
|
1974
|
+
return /* @__PURE__ */ jsx(ShopContext.Provider, { value: store, children: /* @__PURE__ */ jsx(ThemeSettingsContext.Provider, { value: themeSettings, children: /* @__PURE__ */ jsx(CurrentTemplateContext.Provider, { value: currentTemplate, children: /* @__PURE__ */ jsx(PageContext.Provider, { value: pageValue, children: /* @__PURE__ */ jsx(LocalizationContext.Provider, { value: localization, children: /* @__PURE__ */ jsx(CurrencyContext.Provider, { value: currencyValue, children: /* @__PURE__ */ jsx(CartContext.Provider, { value: cartValue, children: /* @__PURE__ */ jsx(CustomerContext.Provider, { value: customerState, children: /* @__PURE__ */ jsx(CustomerActionsContext.Provider, { value: customerActions, children: /* @__PURE__ */ jsx(NavigationContext.Provider, { value: navigation ?? {}, children }) }) }) }) }) }) }) }) }) });
|
|
2329
1975
|
}
|
|
2330
1976
|
function ProductProvider({ product, children }) {
|
|
2331
1977
|
return /* @__PURE__ */ jsx(ProductContext.Provider, { value: product, children });
|
|
@@ -2626,6 +2272,7 @@ var ThemeMountBridge = forwardRef(function ThemeMountBridge2({ ctx, mountEl, ren
|
|
|
2626
2272
|
initialProducts: pageData.products,
|
|
2627
2273
|
initialCollections: pageData.collections,
|
|
2628
2274
|
currentTemplate: template,
|
|
2275
|
+
pageTemplate: ctx.page?.template,
|
|
2629
2276
|
children: wrapEntityProviders(app, pageData)
|
|
2630
2277
|
}
|
|
2631
2278
|
);
|
|
@@ -2671,15 +2318,18 @@ function Money({
|
|
|
2671
2318
|
}) {
|
|
2672
2319
|
const { formatMoney } = useLocalization();
|
|
2673
2320
|
const shop = useShop();
|
|
2674
|
-
const
|
|
2321
|
+
const { selected, base, autoConvert, convert } = useCurrency();
|
|
2322
|
+
const shouldConvert = autoConvert && !currency && !!selected && selected !== base;
|
|
2323
|
+
const ccy = shouldConvert ? selected : currency || shop?.currency;
|
|
2324
|
+
const toDisplay = (major) => shouldConvert ? convert(Math.round(major * 100), selected) / 100 : major;
|
|
2675
2325
|
const showCompare = compareAt != null && compareAt > amount;
|
|
2676
2326
|
const children = [
|
|
2677
|
-
/* @__PURE__ */ jsx("span", { children: formatMoney(amount, ccy) }, "amt")
|
|
2327
|
+
/* @__PURE__ */ jsx("span", { children: formatMoney(toDisplay(amount), ccy) }, "amt")
|
|
2678
2328
|
];
|
|
2679
2329
|
if (showCompare) {
|
|
2680
2330
|
children.push(" ");
|
|
2681
2331
|
children.push(
|
|
2682
|
-
/* @__PURE__ */ jsx("s", { style: { opacity: 0.6 }, children: formatMoney(compareAt, ccy) }, "cmp")
|
|
2332
|
+
/* @__PURE__ */ jsx("s", { style: { opacity: 0.6 }, children: formatMoney(toDisplay(compareAt), ccy) }, "cmp")
|
|
2683
2333
|
);
|
|
2684
2334
|
}
|
|
2685
2335
|
return createElement(
|
|
@@ -3415,11 +3065,29 @@ function walk(node) {
|
|
|
3415
3065
|
}
|
|
3416
3066
|
}
|
|
3417
3067
|
function sanitizeHtmlServer(input) {
|
|
3418
|
-
|
|
3419
|
-
s =
|
|
3068
|
+
if (!input) return "";
|
|
3069
|
+
let s = input;
|
|
3070
|
+
let prev;
|
|
3071
|
+
const DANGEROUS = [
|
|
3072
|
+
/<script\b[\s\S]*?<\/script\s*>/gi,
|
|
3073
|
+
/<style\b[\s\S]*?<\/style\s*>/gi,
|
|
3074
|
+
/<iframe\b[\s\S]*?<\/iframe\s*>/gi,
|
|
3075
|
+
/<object\b[\s\S]*?<\/object\s*>/gi,
|
|
3076
|
+
/<embed\b[\s\S]*?<\/embed\s*>/gi,
|
|
3077
|
+
/<\/?(?:script|style|iframe|object|embed|link|meta|form|input)\b[^>]*>/gi
|
|
3078
|
+
];
|
|
3079
|
+
do {
|
|
3080
|
+
prev = s;
|
|
3081
|
+
for (const re of DANGEROUS) s = s.replace(re, "");
|
|
3082
|
+
} while (s !== prev);
|
|
3083
|
+
const HANDLER = /[\s/"'<]on\w+\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/gi;
|
|
3084
|
+
do {
|
|
3085
|
+
prev = s;
|
|
3086
|
+
s = s.replace(HANDLER, "");
|
|
3087
|
+
} while (s !== prev);
|
|
3420
3088
|
s = s.replace(
|
|
3421
|
-
|
|
3422
|
-
(full,
|
|
3089
|
+
/[\s/"'<](href|src)\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/gi,
|
|
3090
|
+
(full, _attr, val) => {
|
|
3423
3091
|
const cleaned = String(val).replace(/^['"]|['"]$/g, "");
|
|
3424
3092
|
return isSafeUrl(cleaned) ? full : "";
|
|
3425
3093
|
}
|
|
@@ -3764,97 +3432,6 @@ function Icon({
|
|
|
3764
3432
|
);
|
|
3765
3433
|
}
|
|
3766
3434
|
|
|
3767
|
-
// src/utils/normalize.ts
|
|
3768
|
-
var DEFAULT_THEME_ID = typeof process !== "undefined" && process.env?.NUMU_DEFAULT_THEME_ID || "modern";
|
|
3769
|
-
function resolveThemeSettings(raw) {
|
|
3770
|
-
const safeRaw = raw ?? {};
|
|
3771
|
-
if (safeRaw.schema_version === 3) {
|
|
3772
|
-
return safeRaw;
|
|
3773
|
-
}
|
|
3774
|
-
const themeBlock = safeRaw.theme || {};
|
|
3775
|
-
const themeId = typeof themeBlock.base_theme === "string" && themeBlock.base_theme || DEFAULT_THEME_ID;
|
|
3776
|
-
const globalSettings = {};
|
|
3777
|
-
for (const key of [
|
|
3778
|
-
"primary_color",
|
|
3779
|
-
"secondary_color",
|
|
3780
|
-
"font_family",
|
|
3781
|
-
"logo_url"
|
|
3782
|
-
]) {
|
|
3783
|
-
if (themeBlock[key] !== void 0) globalSettings[key] = themeBlock[key];
|
|
3784
|
-
}
|
|
3785
|
-
if (safeRaw.identity) globalSettings.identity = safeRaw.identity;
|
|
3786
|
-
const sections = {};
|
|
3787
|
-
const order = [];
|
|
3788
|
-
const hero = safeRaw.hero;
|
|
3789
|
-
if (hero) {
|
|
3790
|
-
sections["hero_1"] = {
|
|
3791
|
-
type: "hero",
|
|
3792
|
-
settings: {
|
|
3793
|
-
// Mirrors backend `normalize_legacy_to_v3` exactly — includes
|
|
3794
|
-
// headline_ar so Arabic content survives the round-trip.
|
|
3795
|
-
headline: hero.headline ?? "",
|
|
3796
|
-
headline_ar: hero.headline_ar ?? "",
|
|
3797
|
-
subtitle: hero.subtitle ?? "",
|
|
3798
|
-
background_image: hero.hero_image_url ?? "",
|
|
3799
|
-
cta_text: hero.cta_text ?? "",
|
|
3800
|
-
cta_link: hero.cta_link ?? ""
|
|
3801
|
-
}
|
|
3802
|
-
};
|
|
3803
|
-
order.push("hero_1");
|
|
3804
|
-
}
|
|
3805
|
-
const products = safeRaw.products;
|
|
3806
|
-
if (products) {
|
|
3807
|
-
sections["featured_1"] = {
|
|
3808
|
-
type: "featured-products",
|
|
3809
|
-
settings: products
|
|
3810
|
-
};
|
|
3811
|
-
order.push("featured_1");
|
|
3812
|
-
}
|
|
3813
|
-
const templates = {};
|
|
3814
|
-
if (Object.keys(sections).length > 0) {
|
|
3815
|
-
templates["home"] = { name: "Home", sections, order };
|
|
3816
|
-
}
|
|
3817
|
-
const sectionGroups = {
|
|
3818
|
-
header: {
|
|
3819
|
-
name: "Header Group",
|
|
3820
|
-
sections: {
|
|
3821
|
-
header_1: {
|
|
3822
|
-
type: "header",
|
|
3823
|
-
settings: safeRaw.header || {}
|
|
3824
|
-
}
|
|
3825
|
-
},
|
|
3826
|
-
order: ["header_1"]
|
|
3827
|
-
},
|
|
3828
|
-
footer: {
|
|
3829
|
-
name: "Footer Group",
|
|
3830
|
-
sections: {
|
|
3831
|
-
footer_1: {
|
|
3832
|
-
type: "footer",
|
|
3833
|
-
settings: safeRaw.footer || {}
|
|
3834
|
-
}
|
|
3835
|
-
},
|
|
3836
|
-
order: ["footer_1"]
|
|
3837
|
-
}
|
|
3838
|
-
};
|
|
3839
|
-
let externalTheme = null;
|
|
3840
|
-
const ext = safeRaw.external_theme;
|
|
3841
|
-
if (ext && typeof ext === "object" && typeof ext.bundle_url === "string" && ext.bundle_url) {
|
|
3842
|
-
externalTheme = {
|
|
3843
|
-
bundle_url: ext.bundle_url,
|
|
3844
|
-
css_url: typeof ext.css_url === "string" ? ext.css_url : null,
|
|
3845
|
-
mode: typeof ext.mode === "string" ? ext.mode : "production"
|
|
3846
|
-
};
|
|
3847
|
-
}
|
|
3848
|
-
return {
|
|
3849
|
-
schema_version: 3,
|
|
3850
|
-
theme_id: themeId,
|
|
3851
|
-
global_settings: globalSettings,
|
|
3852
|
-
templates,
|
|
3853
|
-
section_groups: sectionGroups,
|
|
3854
|
-
external_theme: externalTheme
|
|
3855
|
-
};
|
|
3856
|
-
}
|
|
3857
|
-
|
|
3858
3435
|
// src/utils/federation.ts
|
|
3859
3436
|
function sdkSlot() {
|
|
3860
3437
|
return /* @__PURE__ */ Symbol.for("@numueg/theme-sdk:singleton");
|
|
@@ -4167,6 +3744,6 @@ function buildLocaleBundle(modules) {
|
|
|
4167
3744
|
return bundle;
|
|
4168
3745
|
}
|
|
4169
3746
|
|
|
4170
|
-
export { AddToCartButton, Block,
|
|
3747
|
+
export { AddToCartButton, Block, CollectionCard, CollectionProvider, CurrencySwitcher, EditableImage, EditableText, Form, HeroMedia, ICON_NAMES, Icon, IconMap, Image, LOGO_SHAPE_OPTIONS, LOGO_SIZE_OPTIONS, Link, LocaleSwitcher, Logo, MAX_BLOCK_DEPTH, Money, NuMuProvider, ProductCard, ProductProvider, RichText, Section, SectionContext, applyGlobalStyleTokens, applyImageTransform, asImageTransform, assetUrl, availableValues, buildLocaleBundle, buildThemeElement, clearSdkSingleton, collectBlocks, collectSections, computeGlobalStyleTokens, defaultVariant, defineBlock, defineSection, defineThemeEntry, dynamicSource, findVariantByOptions, flattenMessages, focalSrc, getReactSingleton, getSdkSingleton, isDefinedBlock, isDefinedSection, isDynamicSource, isSdkAvailable, logoImgStyle, logoStyleTokens, mountTheme, pickTranslations, registerReactSingleton, registerSdkSingleton, resolveDynamicValue, resolveFontStack, resolveSettingsMap, resolveSizeChart, resolveSourcePath, sanitizeHtml, useAnalytics, useApp, useCachedResource, useCart, useCheckout, useCollection, useCollectionOptional, useCurrency, useCurrentTemplate, useCustomerActions, useCustomerAddresses, useGiftCardBalance, useImage, useMoney, useNavigation, useOrder, useOrders, useProduct, useProductOptional, useProductSizeChart, useRelatedProducts, useReorder, useResolvedSettings, useSearch, useSection, useSectionGroup, useSectionOptional, useShippingRates, useVariantSelection, useWishlist };
|
|
4171
3748
|
//# sourceMappingURL=index.mjs.map
|
|
4172
3749
|
//# sourceMappingURL=index.mjs.map
|