@numueg/theme-cli 0.4.1 → 0.6.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 +22 -0
- package/dist/index.js +817 -405
- package/package.json +2 -1
- package/templates/scaffold/index.html +13 -0
- package/templates/scaffold/package.json +27 -0
- package/templates/scaffold/schemas/sections/about_section.json +23 -0
- package/templates/scaffold/schemas/sections/account.json +8 -0
- package/templates/scaffold/schemas/sections/cart_summary.json +12 -0
- package/templates/scaffold/schemas/sections/categories.json +9 -0
- package/templates/scaffold/schemas/sections/featured_collection.json +14 -0
- package/templates/scaffold/schemas/sections/footer.json +14 -0
- package/templates/scaffold/schemas/sections/frequently_bought.json +10 -0
- package/templates/scaffold/schemas/sections/header.json +14 -0
- package/templates/scaffold/schemas/sections/hero.json +15 -0
- package/templates/scaffold/schemas/sections/image_with_text.json +19 -0
- package/templates/scaffold/schemas/sections/marquee.json +9 -0
- package/templates/scaffold/schemas/sections/newsletter.json +11 -0
- package/templates/scaffold/schemas/sections/not_found.json +12 -0
- package/templates/scaffold/schemas/sections/order_confirmation.json +9 -0
- package/templates/scaffold/schemas/sections/product_details.json +12 -0
- package/templates/scaffold/schemas/sections/product_grid.json +12 -0
- package/templates/scaffold/schemas/sections/promo_banner.json +13 -0
- package/templates/scaffold/schemas/sections/rich_text.json +17 -0
- package/templates/scaffold/schemas/sections/search_results.json +11 -0
- package/templates/scaffold/schemas/sections/size_chart.json +9 -0
- package/templates/scaffold/schemas/sections/testimonials.json +22 -0
- package/templates/scaffold/settings_schema.json +35 -0
- package/templates/scaffold/src/dev-entry.tsx +244 -0
- package/templates/scaffold/src/lib/CouponForm.tsx +90 -0
- package/templates/scaffold/src/lib/EditableText.tsx +178 -0
- package/templates/scaffold/src/lib/ProductCard.tsx +99 -0
- package/templates/scaffold/src/lib/cartUI.ts +43 -0
- package/templates/scaffold/src/lib/i18n.ts +17 -0
- package/templates/scaffold/src/lib/section.ts +12 -0
- package/templates/scaffold/src/main.tsx +230 -0
- package/templates/scaffold/src/sections/Footer.tsx +161 -0
- package/templates/scaffold/src/sections/Header.tsx +453 -0
- package/templates/scaffold/src/sections/about_section.tsx +104 -0
- package/templates/scaffold/src/sections/account.tsx +422 -0
- package/templates/scaffold/src/sections/cart_summary.tsx +169 -0
- package/templates/scaffold/src/sections/categories.tsx +57 -0
- package/templates/scaffold/src/sections/featured_collection.tsx +109 -0
- package/templates/scaffold/src/sections/frequently_bought.tsx +187 -0
- package/templates/scaffold/src/sections/hero.tsx +133 -0
- package/templates/scaffold/src/sections/image_with_text.tsx +105 -0
- package/templates/scaffold/src/sections/marquee.tsx +45 -0
- package/templates/scaffold/src/sections/newsletter.tsx +79 -0
- package/templates/scaffold/src/sections/not_found.tsx +56 -0
- package/templates/scaffold/src/sections/order_confirmation.tsx +127 -0
- package/templates/scaffold/src/sections/product_details.tsx +517 -0
- package/templates/scaffold/src/sections/product_grid.tsx +147 -0
- package/templates/scaffold/src/sections/promo_banner.tsx +80 -0
- package/templates/scaffold/src/sections/rich_text.tsx +51 -0
- package/templates/scaffold/src/sections/search_results.tsx +93 -0
- package/templates/scaffold/src/sections/size_chart.tsx +109 -0
- package/templates/scaffold/src/sections/testimonials.tsx +112 -0
- package/templates/scaffold/styles.css +2404 -0
- package/templates/scaffold/templates/error.html +13 -0
- package/templates/scaffold/templates/loading.html +11 -0
- package/templates/scaffold/theme.json +224 -0
- package/templates/scaffold/tsconfig.json +22 -0
- package/templates/scaffold/vite.config.ts +16 -0
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useEffect,
|
|
3
|
+
useRef,
|
|
4
|
+
useState } from "react";
|
|
5
|
+
import {
|
|
6
|
+
useCart,
|
|
7
|
+
useLocalization,
|
|
8
|
+
useNavigation,
|
|
9
|
+
useCollections,
|
|
10
|
+
useCurrency,
|
|
11
|
+
useShop,
|
|
12
|
+
} from "@numueg/theme-sdk";
|
|
13
|
+
import { EditableText } from "../lib/EditableText";
|
|
14
|
+
import type { EmpSectionProps } from "../lib/section";
|
|
15
|
+
import { useCartOpen, openCart, closeCart } from "../lib/cartUI";
|
|
16
|
+
import { CouponForm } from "../lib/CouponForm";
|
|
17
|
+
import { useT } from "../lib/i18n";
|
|
18
|
+
|
|
19
|
+
interface HeaderSettings {
|
|
20
|
+
brand_name?: string;
|
|
21
|
+
logo?: string;
|
|
22
|
+
announcement_text?: string;
|
|
23
|
+
menu_handle?: string;
|
|
24
|
+
show_search?: boolean;
|
|
25
|
+
show_account?: boolean;
|
|
26
|
+
show_cart?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default function Header({
|
|
30
|
+
id,
|
|
31
|
+
settings,
|
|
32
|
+
solidHeader,
|
|
33
|
+
}: EmpSectionProps & { solidHeader?: boolean }) {
|
|
34
|
+
const s = settings as HeaderSettings;
|
|
35
|
+
const t = useT();
|
|
36
|
+
const shop = useShop();
|
|
37
|
+
// The merchant rarely overrides the brand, and the theme's placeholder
|
|
38
|
+
// ("STORE") gets baked into the store's saved customization on activation.
|
|
39
|
+
// Treat that placeholder as "unset" so the real store name always wins.
|
|
40
|
+
const brand =
|
|
41
|
+
s.brand_name && s.brand_name !== "STORE"
|
|
42
|
+
? s.brand_name
|
|
43
|
+
: shop?.name || s.brand_name || "STORE";
|
|
44
|
+
const DEFAULT_LINKS = [
|
|
45
|
+
{ label: t("Home", "الرئيسية"), url: "/" },
|
|
46
|
+
{ label: t("Shop", "المتجر"), url: "/products" },
|
|
47
|
+
{ label: t("Contact", "تواصل"), url: "/pages/contact" },
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
const { cart, updateQuantity, removeItem, loading } = useCart();
|
|
51
|
+
const { formatMoney, locale, setLocale, availableLocales } =
|
|
52
|
+
useLocalization();
|
|
53
|
+
const nav = useNavigation(s.menu_handle || "main-menu");
|
|
54
|
+
const { collections } = useCollections({ limit: 6 });
|
|
55
|
+
const currency = useCurrency();
|
|
56
|
+
const drawerOpen = useCartOpen();
|
|
57
|
+
|
|
58
|
+
const [scrolled, setScrolled] = useState(false);
|
|
59
|
+
const [menuOpen, setMenuOpen] = useState(false);
|
|
60
|
+
const [megaOpen, setMegaOpen] = useState(false);
|
|
61
|
+
const megaTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
|
62
|
+
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
const onScroll = () => setScrolled(window.scrollY > 50);
|
|
65
|
+
onScroll();
|
|
66
|
+
window.addEventListener("scroll", onScroll, { passive: true });
|
|
67
|
+
return () => window.removeEventListener("scroll", onScroll);
|
|
68
|
+
}, []);
|
|
69
|
+
|
|
70
|
+
const items = cart?.items ?? [];
|
|
71
|
+
const count = items.reduce((n, it) => n + it.quantity, 0);
|
|
72
|
+
const cartCurrency = cart?.currency;
|
|
73
|
+
|
|
74
|
+
const links =
|
|
75
|
+
nav.items.length > 0
|
|
76
|
+
? nav.items.map((it) => ({ label: it.title, url: it.url }))
|
|
77
|
+
: DEFAULT_LINKS;
|
|
78
|
+
|
|
79
|
+
const showCurrency = currency.presentment.length > 1;
|
|
80
|
+
|
|
81
|
+
// Language toggle. The store advertises locales via `availableLocales`; when
|
|
82
|
+
// it doesn't, fall back to the Arabic/English pair this store ships with.
|
|
83
|
+
const isAr = typeof locale === "string" && locale.toLowerCase().startsWith("ar");
|
|
84
|
+
const localePair =
|
|
85
|
+
availableLocales.length >= 2 ? availableLocales : ["ar", "en"];
|
|
86
|
+
const nextLocale =
|
|
87
|
+
localePair.find((l) =>
|
|
88
|
+
isAr ? !l.toLowerCase().startsWith("ar") : l.toLowerCase().startsWith("ar"),
|
|
89
|
+
) || (isAr ? "en" : "ar");
|
|
90
|
+
// Label shows the language you'll switch TO, in its own script.
|
|
91
|
+
const switchLabel = isAr ? "EN" : "ع";
|
|
92
|
+
|
|
93
|
+
const enterMega = () => {
|
|
94
|
+
clearTimeout(megaTimer.current);
|
|
95
|
+
setMegaOpen(true);
|
|
96
|
+
};
|
|
97
|
+
const leaveMega = () => {
|
|
98
|
+
megaTimer.current = setTimeout(() => setMegaOpen(false), 180);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<>
|
|
103
|
+
<header
|
|
104
|
+
className={`nt-header${solidHeader || scrolled ? " is-scrolled" : ""}`}
|
|
105
|
+
>
|
|
106
|
+
{s.announcement_text ? (
|
|
107
|
+
<EditableText
|
|
108
|
+
as="div"
|
|
109
|
+
className="nt-announce"
|
|
110
|
+
sectionId={id}
|
|
111
|
+
settingId="announcement_text"
|
|
112
|
+
value={s.announcement_text}
|
|
113
|
+
/>
|
|
114
|
+
) : null}
|
|
115
|
+
|
|
116
|
+
<div className="nt-container">
|
|
117
|
+
<div className="nt-header__bar">
|
|
118
|
+
{/* Left: desktop nav + mobile burger */}
|
|
119
|
+
<nav className="nt-header__nav" aria-label="Primary">
|
|
120
|
+
{links.map((l, i) => {
|
|
121
|
+
const isShop = l.url === "/products";
|
|
122
|
+
if (!isShop) {
|
|
123
|
+
return (
|
|
124
|
+
<a key={i} href={l.url}>
|
|
125
|
+
{l.label}
|
|
126
|
+
</a>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
return (
|
|
130
|
+
<div
|
|
131
|
+
key={i}
|
|
132
|
+
style={{ position: "relative" }}
|
|
133
|
+
onMouseEnter={enterMega}
|
|
134
|
+
onMouseLeave={leaveMega}
|
|
135
|
+
>
|
|
136
|
+
<a href={l.url}>{l.label}</a>
|
|
137
|
+
{megaOpen && collections.length > 0 ? (
|
|
138
|
+
<div
|
|
139
|
+
className="nt-mega"
|
|
140
|
+
onMouseEnter={enterMega}
|
|
141
|
+
onMouseLeave={leaveMega}
|
|
142
|
+
>
|
|
143
|
+
<div className="nt-mega__col">
|
|
144
|
+
<p className="nt-mega__label">{l.label}</p>
|
|
145
|
+
<a
|
|
146
|
+
className="nt-mega__link nt-mega__link--strong"
|
|
147
|
+
href="/products"
|
|
148
|
+
>
|
|
149
|
+
{t("All products", "كل المنتجات")}
|
|
150
|
+
</a>
|
|
151
|
+
{collections.slice(0, 5).map((c) => (
|
|
152
|
+
<a
|
|
153
|
+
key={c.id}
|
|
154
|
+
className="nt-mega__link"
|
|
155
|
+
href={`/collections/${c.slug}`}
|
|
156
|
+
>
|
|
157
|
+
{c.name}
|
|
158
|
+
</a>
|
|
159
|
+
))}
|
|
160
|
+
</div>
|
|
161
|
+
<div className="nt-mega__cards">
|
|
162
|
+
{collections.slice(0, 3).map((c) => (
|
|
163
|
+
<a
|
|
164
|
+
key={c.id}
|
|
165
|
+
className="nt-mega__card"
|
|
166
|
+
href={`/collections/${c.slug}`}
|
|
167
|
+
>
|
|
168
|
+
<span className="nt-mega__cardmedia">
|
|
169
|
+
{c.image_url ? (
|
|
170
|
+
<img src={c.image_url} alt={c.name} />
|
|
171
|
+
) : (
|
|
172
|
+
<span className="nt-cat__placeholder">
|
|
173
|
+
{c.name?.[0] ?? "?"}
|
|
174
|
+
</span>
|
|
175
|
+
)}
|
|
176
|
+
</span>
|
|
177
|
+
<span className="nt-mega__cardname">
|
|
178
|
+
{c.name}
|
|
179
|
+
</span>
|
|
180
|
+
</a>
|
|
181
|
+
))}
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
) : null}
|
|
185
|
+
</div>
|
|
186
|
+
);
|
|
187
|
+
})}
|
|
188
|
+
</nav>
|
|
189
|
+
|
|
190
|
+
<button
|
|
191
|
+
className="nt-burger"
|
|
192
|
+
type="button"
|
|
193
|
+
aria-label="Menu"
|
|
194
|
+
aria-expanded={menuOpen}
|
|
195
|
+
onClick={() => setMenuOpen((v) => !v)}
|
|
196
|
+
>
|
|
197
|
+
{menuOpen ? <IconX /> : <IconMenu />}
|
|
198
|
+
</button>
|
|
199
|
+
|
|
200
|
+
{/* Center: logo */}
|
|
201
|
+
<a className="nt-header__logo" href="/" aria-label={brand}>
|
|
202
|
+
{s.logo ? (
|
|
203
|
+
<img src={s.logo} alt={brand} />
|
|
204
|
+
) : (
|
|
205
|
+
<EditableText
|
|
206
|
+
as="span"
|
|
207
|
+
sectionId={id}
|
|
208
|
+
settingId="brand_name"
|
|
209
|
+
value={brand}
|
|
210
|
+
/>
|
|
211
|
+
)}
|
|
212
|
+
</a>
|
|
213
|
+
|
|
214
|
+
{/* Right: actions */}
|
|
215
|
+
<div className="nt-header__actions">
|
|
216
|
+
<button
|
|
217
|
+
className="nt-langswitch"
|
|
218
|
+
type="button"
|
|
219
|
+
onClick={() => setLocale(nextLocale)}
|
|
220
|
+
aria-label={isAr ? "Switch to English" : "التبديل إلى العربية"}
|
|
221
|
+
title={isAr ? "English" : "العربية"}
|
|
222
|
+
>
|
|
223
|
+
{switchLabel}
|
|
224
|
+
</button>
|
|
225
|
+
|
|
226
|
+
{showCurrency ? (
|
|
227
|
+
<select
|
|
228
|
+
className="nt-header__currency"
|
|
229
|
+
aria-label="Currency"
|
|
230
|
+
value={currency.selected}
|
|
231
|
+
onChange={(e) => currency.setSelected(e.target.value)}
|
|
232
|
+
>
|
|
233
|
+
{currency.presentment.map((c) => (
|
|
234
|
+
<option key={c} value={c}>
|
|
235
|
+
{c}
|
|
236
|
+
</option>
|
|
237
|
+
))}
|
|
238
|
+
</select>
|
|
239
|
+
) : null}
|
|
240
|
+
|
|
241
|
+
{s.show_search !== false ? (
|
|
242
|
+
<a className="nt-iconbtn" href="/search" aria-label="بحث">
|
|
243
|
+
<IconSearch />
|
|
244
|
+
</a>
|
|
245
|
+
) : null}
|
|
246
|
+
|
|
247
|
+
{s.show_account !== false ? (
|
|
248
|
+
<a className="nt-iconbtn" href="/account" aria-label="الحساب">
|
|
249
|
+
<IconUser />
|
|
250
|
+
</a>
|
|
251
|
+
) : null}
|
|
252
|
+
|
|
253
|
+
{s.show_cart !== false ? (
|
|
254
|
+
<button
|
|
255
|
+
className="nt-iconbtn"
|
|
256
|
+
type="button"
|
|
257
|
+
onClick={openCart}
|
|
258
|
+
aria-label={`السلة، ${count} عناصر`}
|
|
259
|
+
>
|
|
260
|
+
<IconBag />
|
|
261
|
+
<span>{count}</span>
|
|
262
|
+
</button>
|
|
263
|
+
) : null}
|
|
264
|
+
</div>
|
|
265
|
+
</div>
|
|
266
|
+
</div>
|
|
267
|
+
|
|
268
|
+
{menuOpen ? (
|
|
269
|
+
<div className="nt-mobilemenu">
|
|
270
|
+
{links.map((l, i) => (
|
|
271
|
+
<a key={i} href={l.url} onClick={() => setMenuOpen(false)}>
|
|
272
|
+
{l.label}
|
|
273
|
+
</a>
|
|
274
|
+
))}
|
|
275
|
+
{collections.slice(0, 5).map((c) => (
|
|
276
|
+
<a
|
|
277
|
+
key={c.id}
|
|
278
|
+
href={`/collections/${c.slug}`}
|
|
279
|
+
onClick={() => setMenuOpen(false)}
|
|
280
|
+
style={{ paddingInlineStart: "1rem", fontWeight: 400 }}
|
|
281
|
+
>
|
|
282
|
+
{c.name}
|
|
283
|
+
</a>
|
|
284
|
+
))}
|
|
285
|
+
</div>
|
|
286
|
+
) : null}
|
|
287
|
+
</header>
|
|
288
|
+
|
|
289
|
+
{/* Cart drawer */}
|
|
290
|
+
<div
|
|
291
|
+
className={`nt-drawer${drawerOpen ? " is-open" : ""}`}
|
|
292
|
+
role="dialog"
|
|
293
|
+
aria-label="السلة"
|
|
294
|
+
aria-hidden={!drawerOpen}
|
|
295
|
+
>
|
|
296
|
+
<div className="nt-drawer__overlay" onClick={closeCart} />
|
|
297
|
+
<div className="nt-drawer__panel">
|
|
298
|
+
<div className="nt-drawer__head">
|
|
299
|
+
<span className="nt-drawer__title">
|
|
300
|
+
{count > 0
|
|
301
|
+
? `${count} ${t("in cart", "عنصر في السلة")}`
|
|
302
|
+
: t("Cart", "السلة")}
|
|
303
|
+
</span>
|
|
304
|
+
<button
|
|
305
|
+
className="nt-drawer__close"
|
|
306
|
+
type="button"
|
|
307
|
+
onClick={closeCart}
|
|
308
|
+
aria-label="إغلاق"
|
|
309
|
+
>
|
|
310
|
+
<IconX />
|
|
311
|
+
</button>
|
|
312
|
+
</div>
|
|
313
|
+
|
|
314
|
+
<div className="nt-drawer__body">
|
|
315
|
+
{items.length === 0 ? (
|
|
316
|
+
<div className="nt-drawer__empty">
|
|
317
|
+
<p>{t("Your cart is empty", "السلة فاضية")}</p>
|
|
318
|
+
</div>
|
|
319
|
+
) : (
|
|
320
|
+
items.map((line) => (
|
|
321
|
+
<div className="nt-line" key={line.id}>
|
|
322
|
+
<div className="nt-line__img">
|
|
323
|
+
{line.image_url ? (
|
|
324
|
+
<img src={line.image_url} alt={line.name} />
|
|
325
|
+
) : null}
|
|
326
|
+
</div>
|
|
327
|
+
<div className="nt-line__body">
|
|
328
|
+
<div className="nt-line__top">
|
|
329
|
+
<div>
|
|
330
|
+
<p className="nt-line__name">{line.name}</p>
|
|
331
|
+
{line.variant_name ? (
|
|
332
|
+
<p className="nt-line__variant">
|
|
333
|
+
{line.variant_name}
|
|
334
|
+
</p>
|
|
335
|
+
) : null}
|
|
336
|
+
</div>
|
|
337
|
+
<p>{formatMoney(line.price * line.quantity, cartCurrency)}</p>
|
|
338
|
+
</div>
|
|
339
|
+
<div className="nt-line__controls">
|
|
340
|
+
<div className="nt-qty">
|
|
341
|
+
<button
|
|
342
|
+
type="button"
|
|
343
|
+
aria-label="تقليل"
|
|
344
|
+
disabled={loading}
|
|
345
|
+
onClick={() =>
|
|
346
|
+
updateQuantity(line.id, line.quantity - 1)
|
|
347
|
+
}
|
|
348
|
+
>
|
|
349
|
+
−
|
|
350
|
+
</button>
|
|
351
|
+
<span>{line.quantity}</span>
|
|
352
|
+
<button
|
|
353
|
+
type="button"
|
|
354
|
+
aria-label="زيادة"
|
|
355
|
+
disabled={loading}
|
|
356
|
+
onClick={() =>
|
|
357
|
+
updateQuantity(line.id, line.quantity + 1)
|
|
358
|
+
}
|
|
359
|
+
>
|
|
360
|
+
+
|
|
361
|
+
</button>
|
|
362
|
+
</div>
|
|
363
|
+
<button
|
|
364
|
+
className="nt-line__remove"
|
|
365
|
+
type="button"
|
|
366
|
+
disabled={loading}
|
|
367
|
+
onClick={() => removeItem(line.id)}
|
|
368
|
+
aria-label="حذف"
|
|
369
|
+
>
|
|
370
|
+
<IconX />
|
|
371
|
+
</button>
|
|
372
|
+
</div>
|
|
373
|
+
</div>
|
|
374
|
+
</div>
|
|
375
|
+
))
|
|
376
|
+
)}
|
|
377
|
+
</div>
|
|
378
|
+
|
|
379
|
+
{items.length > 0 ? (
|
|
380
|
+
<div className="nt-drawer__foot">
|
|
381
|
+
<CouponForm compact />
|
|
382
|
+
<div className="nt-subtotal">
|
|
383
|
+
<span>{t("Subtotal", "المجموع الفرعي")}</span>
|
|
384
|
+
<span>{formatMoney(cart?.subtotal ?? 0, cartCurrency)}</span>
|
|
385
|
+
</div>
|
|
386
|
+
{cart?.discount_amount && cart.discount_amount > 0 ? (
|
|
387
|
+
<div className="nt-subtotal nt-discount">
|
|
388
|
+
<span>
|
|
389
|
+
{t("Discount", "الخصم")}
|
|
390
|
+
{cart.discount_code ? ` (${cart.discount_code})` : ""}
|
|
391
|
+
</span>
|
|
392
|
+
<span>−{formatMoney(cart.discount_amount, cartCurrency)}</span>
|
|
393
|
+
</div>
|
|
394
|
+
) : null}
|
|
395
|
+
<div className="nt-subtotal nt-total">
|
|
396
|
+
<span>{t("Total", "الإجمالي")}</span>
|
|
397
|
+
<span>
|
|
398
|
+
{formatMoney(
|
|
399
|
+
cart?.total ??
|
|
400
|
+
(cart?.subtotal ?? 0) - (cart?.discount_amount ?? 0),
|
|
401
|
+
cartCurrency,
|
|
402
|
+
)}
|
|
403
|
+
</span>
|
|
404
|
+
</div>
|
|
405
|
+
<a className="nt-btn nt-btn--block" href="/checkout">
|
|
406
|
+
{t("Checkout", "إتمام الطلب")}
|
|
407
|
+
</a>
|
|
408
|
+
<button
|
|
409
|
+
className="nt-iconbtn"
|
|
410
|
+
type="button"
|
|
411
|
+
style={{ justifyContent: "center" }}
|
|
412
|
+
onClick={closeCart}
|
|
413
|
+
>
|
|
414
|
+
{t("Continue shopping", "متابعة التسوق")}
|
|
415
|
+
</button>
|
|
416
|
+
</div>
|
|
417
|
+
) : null}
|
|
418
|
+
</div>
|
|
419
|
+
</div>
|
|
420
|
+
</>
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/* ── Inline icons (no runtime icon dep) ── */
|
|
425
|
+
const IconSearch = () => (
|
|
426
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
|
|
427
|
+
<circle cx="11" cy="11" r="7" />
|
|
428
|
+
<path d="m21 21-4.35-4.35" />
|
|
429
|
+
</svg>
|
|
430
|
+
);
|
|
431
|
+
const IconUser = () => (
|
|
432
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
|
|
433
|
+
<circle cx="12" cy="8" r="4" />
|
|
434
|
+
<path d="M4 21a8 8 0 0 1 16 0" />
|
|
435
|
+
</svg>
|
|
436
|
+
);
|
|
437
|
+
const IconBag = () => (
|
|
438
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
|
|
439
|
+
<path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4Z" />
|
|
440
|
+
<path d="M3 6h18" />
|
|
441
|
+
<path d="M16 10a4 4 0 0 1-8 0" />
|
|
442
|
+
</svg>
|
|
443
|
+
);
|
|
444
|
+
const IconMenu = () => (
|
|
445
|
+
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
|
|
446
|
+
<path d="M3 12h18M3 6h18M3 18h18" />
|
|
447
|
+
</svg>
|
|
448
|
+
);
|
|
449
|
+
const IconX = () => (
|
|
450
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
|
|
451
|
+
<path d="M18 6 6 18M6 6l12 12" />
|
|
452
|
+
</svg>
|
|
453
|
+
);
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EditableImage,
|
|
3
|
+
type BlockInstance,
|
|
4
|
+
} from "@numueg/theme-sdk";
|
|
5
|
+
import { EditableText } from "../lib/EditableText";
|
|
6
|
+
import type { EmpSectionProps } from "../lib/section";
|
|
7
|
+
|
|
8
|
+
interface AboutSettings {
|
|
9
|
+
eyebrow?: string;
|
|
10
|
+
title?: string;
|
|
11
|
+
body?: string;
|
|
12
|
+
image?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* About / brand-story block — eyebrow + display heading + body paragraph beside
|
|
17
|
+
* an image, with an optional row of stat highlights (`stat` blocks: value +
|
|
18
|
+
* label). All copy + the image are inline-editable.
|
|
19
|
+
*/
|
|
20
|
+
export default function AboutSection({
|
|
21
|
+
id,
|
|
22
|
+
settings,
|
|
23
|
+
blocks,
|
|
24
|
+
blockOrder,
|
|
25
|
+
}: EmpSectionProps) {
|
|
26
|
+
const s = settings as AboutSettings;
|
|
27
|
+
|
|
28
|
+
const stats = (blockOrder ?? [])
|
|
29
|
+
.map((bid) => ({ bid, block: blocks?.[bid] }))
|
|
30
|
+
.filter(
|
|
31
|
+
(x): x is { bid: string; block: BlockInstance } =>
|
|
32
|
+
!!x.block && !x.block.disabled && x.block.type === "stat",
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<section className="nt-section nt-bg-white">
|
|
37
|
+
<div className="nt-container nt-about">
|
|
38
|
+
<div className="nt-about__copy">
|
|
39
|
+
{s.eyebrow ? (
|
|
40
|
+
<EditableText
|
|
41
|
+
as="p"
|
|
42
|
+
className="nt-label"
|
|
43
|
+
sectionId={id}
|
|
44
|
+
settingId="eyebrow"
|
|
45
|
+
value={s.eyebrow}
|
|
46
|
+
/>
|
|
47
|
+
) : null}
|
|
48
|
+
<EditableText
|
|
49
|
+
as="h2"
|
|
50
|
+
className="nt-display-sm"
|
|
51
|
+
sectionId={id}
|
|
52
|
+
settingId="title"
|
|
53
|
+
value={s.title ?? "صُنع بشغف، يدوم مدى الحياة"}
|
|
54
|
+
/>
|
|
55
|
+
<EditableText
|
|
56
|
+
as="p"
|
|
57
|
+
className="nt-about__body"
|
|
58
|
+
sectionId={id}
|
|
59
|
+
settingId="body"
|
|
60
|
+
value={
|
|
61
|
+
s.body ??
|
|
62
|
+
"بدأنا كاستوديو صغير مستقل، وكبرنا بفضل عملائنا. كل قطعة تُختار بعناية لتجمع بين التصميم النظيف والجودة التي تدوم."
|
|
63
|
+
}
|
|
64
|
+
/>
|
|
65
|
+
{stats.length > 0 ? (
|
|
66
|
+
<div className="nt-about__stats">
|
|
67
|
+
{stats.map(({ bid, block }) => (
|
|
68
|
+
<div className="nt-about__stat" key={bid}>
|
|
69
|
+
<EditableText
|
|
70
|
+
as="span"
|
|
71
|
+
className="nt-about__statvalue"
|
|
72
|
+
sectionId={id}
|
|
73
|
+
blockId={bid}
|
|
74
|
+
settingId="value"
|
|
75
|
+
value={(block.settings.value as string) || "—"}
|
|
76
|
+
/>
|
|
77
|
+
<EditableText
|
|
78
|
+
as="span"
|
|
79
|
+
className="nt-about__statlabel"
|
|
80
|
+
sectionId={id}
|
|
81
|
+
blockId={bid}
|
|
82
|
+
settingId="label"
|
|
83
|
+
value={(block.settings.label as string) || ""}
|
|
84
|
+
/>
|
|
85
|
+
</div>
|
|
86
|
+
))}
|
|
87
|
+
</div>
|
|
88
|
+
) : null}
|
|
89
|
+
</div>
|
|
90
|
+
<div className="nt-about__media">
|
|
91
|
+
<EditableImage
|
|
92
|
+
sectionId={id}
|
|
93
|
+
settingId="image"
|
|
94
|
+
src={
|
|
95
|
+
s.image ||
|
|
96
|
+
"https://images.unsplash.com/photo-1441984904996-e0b6ba687e04?w=1200"
|
|
97
|
+
}
|
|
98
|
+
alt={(s.title as string) || ""}
|
|
99
|
+
/>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
</section>
|
|
103
|
+
);
|
|
104
|
+
}
|