@numueg/theme-sdk 0.6.1 → 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 +640 -964
- 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 +482 -907
- 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.1" ;
|
|
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,12 +1491,16 @@ 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 }
|
|
@@ -2123,7 +1691,7 @@ function NuMuProvider({
|
|
|
2123
1691
|
const token = reserveToken();
|
|
2124
1692
|
setLoading(true);
|
|
2125
1693
|
try {
|
|
2126
|
-
await postCartMutation(
|
|
1694
|
+
return await postCartMutation(
|
|
2127
1695
|
endpoint,
|
|
2128
1696
|
body,
|
|
2129
1697
|
buildApplyCart(token),
|
|
@@ -2140,12 +1708,13 @@ function NuMuProvider({
|
|
|
2140
1708
|
async (productId, variantId, quantity) => {
|
|
2141
1709
|
const eventId = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}`;
|
|
2142
1710
|
const qty = quantity || 1;
|
|
2143
|
-
await mutate("/api/cart/add", {
|
|
1711
|
+
const result = await mutate("/api/cart/add", {
|
|
2144
1712
|
product_id: productId,
|
|
2145
1713
|
variant_id: variantId,
|
|
2146
1714
|
quantity: qty,
|
|
2147
1715
|
_event_id: eventId
|
|
2148
1716
|
});
|
|
1717
|
+
if (!result.ok) return result;
|
|
2149
1718
|
try {
|
|
2150
1719
|
if (typeof window !== "undefined") {
|
|
2151
1720
|
window.dispatchEvent(
|
|
@@ -2164,34 +1733,29 @@ function NuMuProvider({
|
|
|
2164
1733
|
}
|
|
2165
1734
|
} catch {
|
|
2166
1735
|
}
|
|
1736
|
+
return result;
|
|
2167
1737
|
},
|
|
2168
1738
|
[mutate]
|
|
2169
1739
|
);
|
|
2170
1740
|
const removeItem = useCallback(
|
|
2171
|
-
|
|
2172
|
-
await mutate("/api/cart/remove", { item_id: itemId });
|
|
2173
|
-
},
|
|
1741
|
+
(itemId) => mutate("/api/cart/remove", { item_id: itemId }),
|
|
2174
1742
|
[mutate]
|
|
2175
1743
|
);
|
|
2176
1744
|
const updateQuantity = useCallback(
|
|
2177
|
-
|
|
2178
|
-
await mutate("/api/cart/update", { item_id: itemId, quantity });
|
|
2179
|
-
},
|
|
1745
|
+
(itemId, quantity) => mutate("/api/cart/update", { item_id: itemId, quantity }),
|
|
2180
1746
|
[mutate]
|
|
2181
1747
|
);
|
|
2182
1748
|
const applyDiscount = useCallback(
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
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),
|
|
2186
1755
|
[mutate]
|
|
2187
1756
|
);
|
|
2188
|
-
const removeDiscount = useCallback(async () => {
|
|
2189
|
-
await mutate("/api/cart/discount", void 0);
|
|
2190
|
-
}, [mutate]);
|
|
2191
1757
|
const updateNote = useCallback(
|
|
2192
|
-
|
|
2193
|
-
await mutate("/api/cart/update", { note });
|
|
2194
|
-
},
|
|
1758
|
+
(note) => mutate("/api/cart/update", { note }),
|
|
2195
1759
|
[mutate]
|
|
2196
1760
|
);
|
|
2197
1761
|
const clearCart = useCallback(async () => {
|
|
@@ -2327,7 +1891,87 @@ function NuMuProvider({
|
|
|
2327
1891
|
defaultNumberFmt
|
|
2328
1892
|
]
|
|
2329
1893
|
);
|
|
2330
|
-
|
|
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 }) }) }) }) }) }) }) }) }) });
|
|
2331
1975
|
}
|
|
2332
1976
|
function ProductProvider({ product, children }) {
|
|
2333
1977
|
return /* @__PURE__ */ jsx(ProductContext.Provider, { value: product, children });
|
|
@@ -2628,6 +2272,7 @@ var ThemeMountBridge = forwardRef(function ThemeMountBridge2({ ctx, mountEl, ren
|
|
|
2628
2272
|
initialProducts: pageData.products,
|
|
2629
2273
|
initialCollections: pageData.collections,
|
|
2630
2274
|
currentTemplate: template,
|
|
2275
|
+
pageTemplate: ctx.page?.template,
|
|
2631
2276
|
children: wrapEntityProviders(app, pageData)
|
|
2632
2277
|
}
|
|
2633
2278
|
);
|
|
@@ -2673,15 +2318,18 @@ function Money({
|
|
|
2673
2318
|
}) {
|
|
2674
2319
|
const { formatMoney } = useLocalization();
|
|
2675
2320
|
const shop = useShop();
|
|
2676
|
-
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;
|
|
2677
2325
|
const showCompare = compareAt != null && compareAt > amount;
|
|
2678
2326
|
const children = [
|
|
2679
|
-
/* @__PURE__ */ jsx("span", { children: formatMoney(amount, ccy) }, "amt")
|
|
2327
|
+
/* @__PURE__ */ jsx("span", { children: formatMoney(toDisplay(amount), ccy) }, "amt")
|
|
2680
2328
|
];
|
|
2681
2329
|
if (showCompare) {
|
|
2682
2330
|
children.push(" ");
|
|
2683
2331
|
children.push(
|
|
2684
|
-
/* @__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")
|
|
2685
2333
|
);
|
|
2686
2334
|
}
|
|
2687
2335
|
return createElement(
|
|
@@ -3417,11 +3065,29 @@ function walk(node) {
|
|
|
3417
3065
|
}
|
|
3418
3066
|
}
|
|
3419
3067
|
function sanitizeHtmlServer(input) {
|
|
3420
|
-
|
|
3421
|
-
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);
|
|
3422
3088
|
s = s.replace(
|
|
3423
|
-
|
|
3424
|
-
(full,
|
|
3089
|
+
/[\s/"'<](href|src)\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/gi,
|
|
3090
|
+
(full, _attr, val) => {
|
|
3425
3091
|
const cleaned = String(val).replace(/^['"]|['"]$/g, "");
|
|
3426
3092
|
return isSafeUrl(cleaned) ? full : "";
|
|
3427
3093
|
}
|
|
@@ -3766,97 +3432,6 @@ function Icon({
|
|
|
3766
3432
|
);
|
|
3767
3433
|
}
|
|
3768
3434
|
|
|
3769
|
-
// src/utils/normalize.ts
|
|
3770
|
-
var DEFAULT_THEME_ID = typeof process !== "undefined" && process.env?.NUMU_DEFAULT_THEME_ID || "modern";
|
|
3771
|
-
function resolveThemeSettings(raw) {
|
|
3772
|
-
const safeRaw = raw ?? {};
|
|
3773
|
-
if (safeRaw.schema_version === 3) {
|
|
3774
|
-
return safeRaw;
|
|
3775
|
-
}
|
|
3776
|
-
const themeBlock = safeRaw.theme || {};
|
|
3777
|
-
const themeId = typeof themeBlock.base_theme === "string" && themeBlock.base_theme || DEFAULT_THEME_ID;
|
|
3778
|
-
const globalSettings = {};
|
|
3779
|
-
for (const key of [
|
|
3780
|
-
"primary_color",
|
|
3781
|
-
"secondary_color",
|
|
3782
|
-
"font_family",
|
|
3783
|
-
"logo_url"
|
|
3784
|
-
]) {
|
|
3785
|
-
if (themeBlock[key] !== void 0) globalSettings[key] = themeBlock[key];
|
|
3786
|
-
}
|
|
3787
|
-
if (safeRaw.identity) globalSettings.identity = safeRaw.identity;
|
|
3788
|
-
const sections = {};
|
|
3789
|
-
const order = [];
|
|
3790
|
-
const hero = safeRaw.hero;
|
|
3791
|
-
if (hero) {
|
|
3792
|
-
sections["hero_1"] = {
|
|
3793
|
-
type: "hero",
|
|
3794
|
-
settings: {
|
|
3795
|
-
// Mirrors backend `normalize_legacy_to_v3` exactly — includes
|
|
3796
|
-
// headline_ar so Arabic content survives the round-trip.
|
|
3797
|
-
headline: hero.headline ?? "",
|
|
3798
|
-
headline_ar: hero.headline_ar ?? "",
|
|
3799
|
-
subtitle: hero.subtitle ?? "",
|
|
3800
|
-
background_image: hero.hero_image_url ?? "",
|
|
3801
|
-
cta_text: hero.cta_text ?? "",
|
|
3802
|
-
cta_link: hero.cta_link ?? ""
|
|
3803
|
-
}
|
|
3804
|
-
};
|
|
3805
|
-
order.push("hero_1");
|
|
3806
|
-
}
|
|
3807
|
-
const products = safeRaw.products;
|
|
3808
|
-
if (products) {
|
|
3809
|
-
sections["featured_1"] = {
|
|
3810
|
-
type: "featured-products",
|
|
3811
|
-
settings: products
|
|
3812
|
-
};
|
|
3813
|
-
order.push("featured_1");
|
|
3814
|
-
}
|
|
3815
|
-
const templates = {};
|
|
3816
|
-
if (Object.keys(sections).length > 0) {
|
|
3817
|
-
templates["home"] = { name: "Home", sections, order };
|
|
3818
|
-
}
|
|
3819
|
-
const sectionGroups = {
|
|
3820
|
-
header: {
|
|
3821
|
-
name: "Header Group",
|
|
3822
|
-
sections: {
|
|
3823
|
-
header_1: {
|
|
3824
|
-
type: "header",
|
|
3825
|
-
settings: safeRaw.header || {}
|
|
3826
|
-
}
|
|
3827
|
-
},
|
|
3828
|
-
order: ["header_1"]
|
|
3829
|
-
},
|
|
3830
|
-
footer: {
|
|
3831
|
-
name: "Footer Group",
|
|
3832
|
-
sections: {
|
|
3833
|
-
footer_1: {
|
|
3834
|
-
type: "footer",
|
|
3835
|
-
settings: safeRaw.footer || {}
|
|
3836
|
-
}
|
|
3837
|
-
},
|
|
3838
|
-
order: ["footer_1"]
|
|
3839
|
-
}
|
|
3840
|
-
};
|
|
3841
|
-
let externalTheme = null;
|
|
3842
|
-
const ext = safeRaw.external_theme;
|
|
3843
|
-
if (ext && typeof ext === "object" && typeof ext.bundle_url === "string" && ext.bundle_url) {
|
|
3844
|
-
externalTheme = {
|
|
3845
|
-
bundle_url: ext.bundle_url,
|
|
3846
|
-
css_url: typeof ext.css_url === "string" ? ext.css_url : null,
|
|
3847
|
-
mode: typeof ext.mode === "string" ? ext.mode : "production"
|
|
3848
|
-
};
|
|
3849
|
-
}
|
|
3850
|
-
return {
|
|
3851
|
-
schema_version: 3,
|
|
3852
|
-
theme_id: themeId,
|
|
3853
|
-
global_settings: globalSettings,
|
|
3854
|
-
templates,
|
|
3855
|
-
section_groups: sectionGroups,
|
|
3856
|
-
external_theme: externalTheme
|
|
3857
|
-
};
|
|
3858
|
-
}
|
|
3859
|
-
|
|
3860
3435
|
// src/utils/federation.ts
|
|
3861
3436
|
function sdkSlot() {
|
|
3862
3437
|
return /* @__PURE__ */ Symbol.for("@numueg/theme-sdk:singleton");
|
|
@@ -4169,6 +3744,6 @@ function buildLocaleBundle(modules) {
|
|
|
4169
3744
|
return bundle;
|
|
4170
3745
|
}
|
|
4171
3746
|
|
|
4172
|
-
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 };
|
|
4173
3748
|
//# sourceMappingURL=index.mjs.map
|
|
4174
3749
|
//# sourceMappingURL=index.mjs.map
|