@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.
Files changed (62) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/index.js +817 -405
  3. package/package.json +2 -1
  4. package/templates/scaffold/index.html +13 -0
  5. package/templates/scaffold/package.json +27 -0
  6. package/templates/scaffold/schemas/sections/about_section.json +23 -0
  7. package/templates/scaffold/schemas/sections/account.json +8 -0
  8. package/templates/scaffold/schemas/sections/cart_summary.json +12 -0
  9. package/templates/scaffold/schemas/sections/categories.json +9 -0
  10. package/templates/scaffold/schemas/sections/featured_collection.json +14 -0
  11. package/templates/scaffold/schemas/sections/footer.json +14 -0
  12. package/templates/scaffold/schemas/sections/frequently_bought.json +10 -0
  13. package/templates/scaffold/schemas/sections/header.json +14 -0
  14. package/templates/scaffold/schemas/sections/hero.json +15 -0
  15. package/templates/scaffold/schemas/sections/image_with_text.json +19 -0
  16. package/templates/scaffold/schemas/sections/marquee.json +9 -0
  17. package/templates/scaffold/schemas/sections/newsletter.json +11 -0
  18. package/templates/scaffold/schemas/sections/not_found.json +12 -0
  19. package/templates/scaffold/schemas/sections/order_confirmation.json +9 -0
  20. package/templates/scaffold/schemas/sections/product_details.json +12 -0
  21. package/templates/scaffold/schemas/sections/product_grid.json +12 -0
  22. package/templates/scaffold/schemas/sections/promo_banner.json +13 -0
  23. package/templates/scaffold/schemas/sections/rich_text.json +17 -0
  24. package/templates/scaffold/schemas/sections/search_results.json +11 -0
  25. package/templates/scaffold/schemas/sections/size_chart.json +9 -0
  26. package/templates/scaffold/schemas/sections/testimonials.json +22 -0
  27. package/templates/scaffold/settings_schema.json +35 -0
  28. package/templates/scaffold/src/dev-entry.tsx +244 -0
  29. package/templates/scaffold/src/lib/CouponForm.tsx +90 -0
  30. package/templates/scaffold/src/lib/EditableText.tsx +178 -0
  31. package/templates/scaffold/src/lib/ProductCard.tsx +99 -0
  32. package/templates/scaffold/src/lib/cartUI.ts +43 -0
  33. package/templates/scaffold/src/lib/i18n.ts +17 -0
  34. package/templates/scaffold/src/lib/section.ts +12 -0
  35. package/templates/scaffold/src/main.tsx +230 -0
  36. package/templates/scaffold/src/sections/Footer.tsx +161 -0
  37. package/templates/scaffold/src/sections/Header.tsx +453 -0
  38. package/templates/scaffold/src/sections/about_section.tsx +104 -0
  39. package/templates/scaffold/src/sections/account.tsx +422 -0
  40. package/templates/scaffold/src/sections/cart_summary.tsx +169 -0
  41. package/templates/scaffold/src/sections/categories.tsx +57 -0
  42. package/templates/scaffold/src/sections/featured_collection.tsx +109 -0
  43. package/templates/scaffold/src/sections/frequently_bought.tsx +187 -0
  44. package/templates/scaffold/src/sections/hero.tsx +133 -0
  45. package/templates/scaffold/src/sections/image_with_text.tsx +105 -0
  46. package/templates/scaffold/src/sections/marquee.tsx +45 -0
  47. package/templates/scaffold/src/sections/newsletter.tsx +79 -0
  48. package/templates/scaffold/src/sections/not_found.tsx +56 -0
  49. package/templates/scaffold/src/sections/order_confirmation.tsx +127 -0
  50. package/templates/scaffold/src/sections/product_details.tsx +517 -0
  51. package/templates/scaffold/src/sections/product_grid.tsx +147 -0
  52. package/templates/scaffold/src/sections/promo_banner.tsx +80 -0
  53. package/templates/scaffold/src/sections/rich_text.tsx +51 -0
  54. package/templates/scaffold/src/sections/search_results.tsx +93 -0
  55. package/templates/scaffold/src/sections/size_chart.tsx +109 -0
  56. package/templates/scaffold/src/sections/testimonials.tsx +112 -0
  57. package/templates/scaffold/styles.css +2404 -0
  58. package/templates/scaffold/templates/error.html +13 -0
  59. package/templates/scaffold/templates/loading.html +11 -0
  60. package/templates/scaffold/theme.json +224 -0
  61. package/templates/scaffold/tsconfig.json +22 -0
  62. package/templates/scaffold/vite.config.ts +16 -0
@@ -0,0 +1,422 @@
1
+ import {
2
+ useState } from "react";
3
+ import {
4
+ useCustomer,
5
+ useCustomerActions,
6
+ useOrders,
7
+ useCustomerAddresses,
8
+ useLocalization,
9
+ } from "@numueg/theme-sdk";
10
+ import { EditableText } from "../lib/EditableText";
11
+ import type { EmpSectionProps } from "../lib/section";
12
+
13
+ interface AccountSettings {
14
+ heading?: string;
15
+ }
16
+
17
+ type Tab = "orders" | "addresses" | "settings";
18
+
19
+ const STATUS_AR: Record<string, string> = {
20
+ pending: "قيد الانتظار",
21
+ confirmed: "مؤكد",
22
+ processing: "قيد التجهيز",
23
+ shipped: "تم الشحن",
24
+ delivered: "تم التوصيل",
25
+ cancelled: "ملغي",
26
+ refunded: "مسترد",
27
+ };
28
+
29
+ /**
30
+ * Account / profile dashboard. Logged-out → login/register guard; logged-in →
31
+ * sidebar (name + logout) + tabs for order history, address book (add/remove)
32
+ * and profile settings. All data + mutations are SDK-native (useCustomer /
33
+ * useOrders / useCustomerAddresses / useCustomerActions). Never blank: shows
34
+ * spinners while loading and empty states when there's nothing.
35
+ */
36
+ export default function Account({ id, settings }: EmpSectionProps) {
37
+ const s = settings as AccountSettings;
38
+ const customer = useCustomer();
39
+
40
+ return (
41
+ <section className="nt-container" style={{ paddingBlock: "2.5rem" }}>
42
+ <EditableText
43
+ as="h1"
44
+ className="nt-display-sm"
45
+ sectionId={id}
46
+ settingId="heading"
47
+ value={s.heading ?? "حسابي"}
48
+ style={{ marginBottom: "2rem" }}
49
+ />
50
+ {customer ? <Dashboard /> : <AuthGuard />}
51
+ </section>
52
+ );
53
+ }
54
+
55
+ /* ───────────────────────── Auth guard (logged out) ───────────────────────── */
56
+ function AuthGuard() {
57
+ const { login, register } = useCustomerActions();
58
+ const [mode, setMode] = useState<"login" | "register">("login");
59
+ const [form, setForm] = useState({
60
+ email: "",
61
+ password: "",
62
+ first_name: "",
63
+ last_name: "",
64
+ });
65
+ const [busy, setBusy] = useState(false);
66
+ const [error, setError] = useState<string | null>(null);
67
+
68
+ const set = (k: keyof typeof form, v: string) =>
69
+ setForm((p) => ({ ...p, [k]: v }));
70
+
71
+ async function submit(e: React.FormEvent) {
72
+ e.preventDefault();
73
+ if (busy) return;
74
+ setBusy(true);
75
+ setError(null);
76
+ try {
77
+ if (mode === "login") {
78
+ await login({ email: form.email, password: form.password });
79
+ } else {
80
+ await register({
81
+ email: form.email,
82
+ password: form.password,
83
+ first_name: form.first_name,
84
+ last_name: form.last_name,
85
+ });
86
+ }
87
+ } catch {
88
+ setError("تعذّر إتمام العملية. تأكد من البيانات وحاول مجددًا.");
89
+ } finally {
90
+ setBusy(false);
91
+ }
92
+ }
93
+
94
+ return (
95
+ <div className="nt-auth">
96
+ <div className="nt-auth__tabs">
97
+ <button
98
+ type="button"
99
+ className="nt-chip"
100
+ aria-pressed={mode === "login"}
101
+ onClick={() => setMode("login")}
102
+ >
103
+ تسجيل الدخول
104
+ </button>
105
+ <button
106
+ type="button"
107
+ className="nt-chip"
108
+ aria-pressed={mode === "register"}
109
+ onClick={() => setMode("register")}
110
+ >
111
+ حساب جديد
112
+ </button>
113
+ </div>
114
+
115
+ <form className="nt-form" onSubmit={submit}>
116
+ {mode === "register" ? (
117
+ <div className="nt-form__row">
118
+ <input
119
+ className="nt-input"
120
+ placeholder="الاسم الأول"
121
+ value={form.first_name}
122
+ onChange={(e) => set("first_name", e.target.value)}
123
+ />
124
+ <input
125
+ className="nt-input"
126
+ placeholder="الاسم الأخير"
127
+ value={form.last_name}
128
+ onChange={(e) => set("last_name", e.target.value)}
129
+ />
130
+ </div>
131
+ ) : null}
132
+ <input
133
+ className="nt-input"
134
+ type="email"
135
+ dir="ltr"
136
+ required
137
+ placeholder="البريد الإلكتروني"
138
+ value={form.email}
139
+ onChange={(e) => set("email", e.target.value)}
140
+ />
141
+ <input
142
+ className="nt-input"
143
+ type="password"
144
+ required
145
+ placeholder="كلمة المرور"
146
+ value={form.password}
147
+ onChange={(e) => set("password", e.target.value)}
148
+ />
149
+ {error ? <p className="nt-coupon__err">{error}</p> : null}
150
+ <button className="nt-btn nt-btn--block" type="submit" disabled={busy}>
151
+ {busy ? "..." : mode === "login" ? "تسجيل الدخول" : "إنشاء الحساب"}
152
+ </button>
153
+ </form>
154
+ </div>
155
+ );
156
+ }
157
+
158
+ /* ───────────────────────── Dashboard (logged in) ───────────────────────── */
159
+ function Dashboard() {
160
+ const customer = useCustomer();
161
+ const { logout } = useCustomerActions();
162
+ const [tab, setTab] = useState<Tab>("orders");
163
+
164
+ const name =
165
+ [customer?.first_name, customer?.last_name].filter(Boolean).join(" ") ||
166
+ customer?.email ||
167
+ "عميل";
168
+
169
+ return (
170
+ <div className="nt-account">
171
+ <aside className="nt-account__side">
172
+ <div className="nt-account__avatar">{name[0]}</div>
173
+ <p className="nt-account__name">{name}</p>
174
+ <p className="nt-account__email">{customer?.email}</p>
175
+ <nav className="nt-account__nav">
176
+ {(
177
+ [
178
+ ["orders", "طلباتي"],
179
+ ["addresses", "العناوين"],
180
+ ["settings", "الإعدادات"],
181
+ ] as Array<[Tab, string]>
182
+ ).map(([key, label]) => (
183
+ <button
184
+ key={key}
185
+ type="button"
186
+ className={`nt-account__navitem${tab === key ? " is-active" : ""}`}
187
+ onClick={() => setTab(key)}
188
+ >
189
+ {label}
190
+ </button>
191
+ ))}
192
+ <button
193
+ type="button"
194
+ className="nt-account__navitem"
195
+ onClick={() => logout()}
196
+ >
197
+ تسجيل الخروج
198
+ </button>
199
+ </nav>
200
+ </aside>
201
+
202
+ <div className="nt-account__main">
203
+ {tab === "orders" ? <OrdersTab /> : null}
204
+ {tab === "addresses" ? <AddressesTab /> : null}
205
+ {tab === "settings" ? <SettingsTab /> : null}
206
+ </div>
207
+ </div>
208
+ );
209
+ }
210
+
211
+ function OrdersTab() {
212
+ const { orders, loading } = useOrders();
213
+ const { formatMoney } = useLocalization();
214
+
215
+ if (loading) return <p className="nt-placeholder">جارٍ التحميل…</p>;
216
+ if (orders.length === 0)
217
+ return <p className="nt-placeholder">لا توجد طلبات بعد.</p>;
218
+
219
+ return (
220
+ <div className="nt-orders">
221
+ {orders.map((o) => (
222
+ <a key={o.id} className="nt-orders__row" href={`/orders/${o.id}`}>
223
+ <div>
224
+ <p className="nt-orders__num">#{o.order_number}</p>
225
+ <p className="nt-orders__meta">
226
+ {STATUS_AR[o.status] || o.status}
227
+ {o.item_count ? ` · ${o.item_count} عنصر` : ""}
228
+ </p>
229
+ </div>
230
+ <span className="nt-orders__total">
231
+ {formatMoney(o.total, o.currency)}
232
+ </span>
233
+ </a>
234
+ ))}
235
+ </div>
236
+ );
237
+ }
238
+
239
+ function AddressesTab() {
240
+ const { addresses, loading, addAddress, deleteAddress } =
241
+ useCustomerAddresses();
242
+ const [adding, setAdding] = useState(false);
243
+ const [busy, setBusy] = useState(false);
244
+ const [form, setForm] = useState({
245
+ first_name: "",
246
+ address_line1: "",
247
+ city: "",
248
+ phone: "",
249
+ });
250
+ const set = (k: keyof typeof form, v: string) =>
251
+ setForm((p) => ({ ...p, [k]: v }));
252
+
253
+ async function save(e: React.FormEvent) {
254
+ e.preventDefault();
255
+ if (busy) return;
256
+ setBusy(true);
257
+ try {
258
+ await addAddress(form);
259
+ setForm({ first_name: "", address_line1: "", city: "", phone: "" });
260
+ setAdding(false);
261
+ } finally {
262
+ setBusy(false);
263
+ }
264
+ }
265
+
266
+ if (loading) return <p className="nt-placeholder">جارٍ التحميل…</p>;
267
+
268
+ return (
269
+ <div>
270
+ {addresses.length === 0 ? (
271
+ <p className="nt-placeholder" style={{ textAlign: "start" }}>
272
+ لا توجد عناوين محفوظة.
273
+ </p>
274
+ ) : (
275
+ <div className="nt-addresses">
276
+ {addresses.map((a) => (
277
+ <div className="nt-address" key={a.id}>
278
+ <div>
279
+ <p className="nt-address__name">
280
+ {a.first_name} {a.last_name}
281
+ {a.is_default ? " · افتراضي" : ""}
282
+ </p>
283
+ <p className="nt-address__line">
284
+ {[a.address_line1, a.city, a.country]
285
+ .filter(Boolean)
286
+ .join("، ")}
287
+ </p>
288
+ {a.phone ? (
289
+ <p className="nt-address__line" dir="ltr">
290
+ {a.phone}
291
+ </p>
292
+ ) : null}
293
+ </div>
294
+ <button
295
+ type="button"
296
+ className="nt-line__remove"
297
+ onClick={() => deleteAddress(a.id)}
298
+ aria-label="حذف"
299
+ >
300
+ حذف
301
+ </button>
302
+ </div>
303
+ ))}
304
+ </div>
305
+ )}
306
+
307
+ {adding ? (
308
+ <form className="nt-form" onSubmit={save} style={{ marginTop: "1.5rem" }}>
309
+ <input
310
+ className="nt-input"
311
+ placeholder="الاسم"
312
+ value={form.first_name}
313
+ onChange={(e) => set("first_name", e.target.value)}
314
+ />
315
+ <input
316
+ className="nt-input"
317
+ placeholder="العنوان"
318
+ required
319
+ value={form.address_line1}
320
+ onChange={(e) => set("address_line1", e.target.value)}
321
+ />
322
+ <div className="nt-form__row">
323
+ <input
324
+ className="nt-input"
325
+ placeholder="المدينة"
326
+ value={form.city}
327
+ onChange={(e) => set("city", e.target.value)}
328
+ />
329
+ <input
330
+ className="nt-input"
331
+ placeholder="الهاتف"
332
+ dir="ltr"
333
+ value={form.phone}
334
+ onChange={(e) => set("phone", e.target.value)}
335
+ />
336
+ </div>
337
+ <div style={{ display: "flex", gap: "0.5rem" }}>
338
+ <button className="nt-btn" type="submit" disabled={busy}>
339
+ {busy ? "..." : "حفظ"}
340
+ </button>
341
+ <button
342
+ className="nt-btn-outline"
343
+ type="button"
344
+ onClick={() => setAdding(false)}
345
+ >
346
+ إلغاء
347
+ </button>
348
+ </div>
349
+ </form>
350
+ ) : (
351
+ <button
352
+ className="nt-btn-outline"
353
+ type="button"
354
+ style={{ marginTop: "1.5rem" }}
355
+ onClick={() => setAdding(true)}
356
+ >
357
+ + إضافة عنوان
358
+ </button>
359
+ )}
360
+ </div>
361
+ );
362
+ }
363
+
364
+ function SettingsTab() {
365
+ const customer = useCustomer();
366
+ const { updateProfile } = useCustomerActions();
367
+ const [form, setForm] = useState({
368
+ first_name: customer?.first_name ?? "",
369
+ last_name: customer?.last_name ?? "",
370
+ phone: customer?.phone ?? "",
371
+ });
372
+ const [busy, setBusy] = useState(false);
373
+ const [saved, setSaved] = useState(false);
374
+ const set = (k: keyof typeof form, v: string) => {
375
+ setForm((p) => ({ ...p, [k]: v }));
376
+ setSaved(false);
377
+ };
378
+
379
+ async function save(e: React.FormEvent) {
380
+ e.preventDefault();
381
+ if (busy) return;
382
+ setBusy(true);
383
+ try {
384
+ await updateProfile(form);
385
+ setSaved(true);
386
+ } finally {
387
+ setBusy(false);
388
+ }
389
+ }
390
+
391
+ return (
392
+ <form className="nt-form" onSubmit={save}>
393
+ <div className="nt-form__row">
394
+ <input
395
+ className="nt-input"
396
+ placeholder="الاسم الأول"
397
+ value={form.first_name}
398
+ onChange={(e) => set("first_name", e.target.value)}
399
+ />
400
+ <input
401
+ className="nt-input"
402
+ placeholder="الاسم الأخير"
403
+ value={form.last_name}
404
+ onChange={(e) => set("last_name", e.target.value)}
405
+ />
406
+ </div>
407
+ <input
408
+ className="nt-input"
409
+ placeholder="الهاتف"
410
+ dir="ltr"
411
+ value={form.phone}
412
+ onChange={(e) => set("phone", e.target.value)}
413
+ />
414
+ <div style={{ display: "flex", alignItems: "center", gap: "1rem" }}>
415
+ <button className="nt-btn" type="submit" disabled={busy}>
416
+ {busy ? "..." : "حفظ التغييرات"}
417
+ </button>
418
+ {saved ? <span className="nt-muted">تم الحفظ ✓</span> : null}
419
+ </div>
420
+ </form>
421
+ );
422
+ }
@@ -0,0 +1,169 @@
1
+ import { useCart, useLocalization } from "@numueg/theme-sdk";
2
+ import { EditableText } from "../lib/EditableText";
3
+ import type { EmpSectionProps } from "../lib/section";
4
+ import { CouponForm } from "../lib/CouponForm";
5
+ import { useT } from "../lib/i18n";
6
+
7
+ interface CartSettings {
8
+ title?: string;
9
+ checkout_label?: string;
10
+ empty_title?: string;
11
+ empty_cta_label?: string;
12
+ empty_cta_link?: string;
13
+ }
14
+
15
+ /** Full-page cart (`/cart`) — mirrors the drawer with more room. */
16
+ export default function CartSummary({ id, settings }: EmpSectionProps) {
17
+ const s = settings as CartSettings;
18
+ const t = useT();
19
+ const { cart, updateQuantity, removeItem, loading } = useCart();
20
+ const { formatMoney } = useLocalization();
21
+
22
+ const items = cart?.items ?? [];
23
+ const currency = cart?.currency;
24
+
25
+ if (items.length === 0) {
26
+ return (
27
+ <section
28
+ className="nt-container"
29
+ style={{ paddingBlock: "5rem", textAlign: "center" }}
30
+ >
31
+ <EditableText
32
+ as="h1"
33
+ className="nt-display-sm"
34
+ sectionId={id}
35
+ settingId="empty_title"
36
+ value={s.empty_title || "السلة فاضية"}
37
+ style={{ marginBottom: "1.5rem" }}
38
+ />
39
+ <a className="nt-btn-outline" href={s.empty_cta_link || "/products"}>
40
+ <EditableText
41
+ as="span"
42
+ sectionId={id}
43
+ settingId="empty_cta_label"
44
+ value={s.empty_cta_label || "متابعة التسوق"}
45
+ />
46
+ </a>
47
+ </section>
48
+ );
49
+ }
50
+
51
+ return (
52
+ <section
53
+ className="nt-container"
54
+ style={{ paddingBlock: "2.5rem", maxWidth: "48rem" }}
55
+ >
56
+ <EditableText
57
+ as="h1"
58
+ className="nt-display-sm"
59
+ sectionId={id}
60
+ settingId="title"
61
+ value={s.title || "سلة التسوق"}
62
+ style={{ marginBottom: "1.5rem" }}
63
+ />
64
+
65
+ <div
66
+ style={{
67
+ border: "1px solid var(--nt-border)",
68
+ borderRadius: "var(--nt-radius)",
69
+ overflow: "hidden",
70
+ background: "var(--nt-card)",
71
+ }}
72
+ >
73
+ {items.map((line) => (
74
+ <div className="nt-line" key={line.id}>
75
+ <div className="nt-line__img">
76
+ {line.image_url ? (
77
+ <img src={line.image_url} alt={line.name} />
78
+ ) : null}
79
+ </div>
80
+ <div className="nt-line__body">
81
+ <div className="nt-line__top">
82
+ <div>
83
+ <p className="nt-line__name">{line.name}</p>
84
+ {line.variant_name ? (
85
+ <p className="nt-line__variant">{line.variant_name}</p>
86
+ ) : null}
87
+ </div>
88
+ <p>{formatMoney(line.price * line.quantity, currency)}</p>
89
+ </div>
90
+ <div className="nt-line__controls">
91
+ <div className="nt-qty">
92
+ <button
93
+ type="button"
94
+ aria-label="تقليل"
95
+ disabled={loading}
96
+ onClick={() => updateQuantity(line.id, line.quantity - 1)}
97
+ >
98
+
99
+ </button>
100
+ <span>{line.quantity}</span>
101
+ <button
102
+ type="button"
103
+ aria-label="زيادة"
104
+ disabled={loading}
105
+ onClick={() => updateQuantity(line.id, line.quantity + 1)}
106
+ >
107
+ +
108
+ </button>
109
+ </div>
110
+ <button
111
+ className="nt-line__remove"
112
+ type="button"
113
+ disabled={loading}
114
+ onClick={() => removeItem(line.id)}
115
+ >
116
+ {t("Remove", "حذف")}
117
+ </button>
118
+ </div>
119
+ </div>
120
+ </div>
121
+ ))}
122
+ </div>
123
+
124
+ <div className="nt-cart__totals">
125
+ <CouponForm />
126
+ <div className="nt-subtotal">
127
+ <span>{t("Subtotal", "المجموع الفرعي")}</span>
128
+ <span>{formatMoney(cart?.subtotal ?? 0, currency)}</span>
129
+ </div>
130
+ {cart?.discount_amount && cart.discount_amount > 0 ? (
131
+ <div className="nt-subtotal nt-discount">
132
+ <span>
133
+ {t("Discount", "الخصم")}
134
+ {cart.discount_code ? ` (${cart.discount_code})` : ""}
135
+ </span>
136
+ <span>−{formatMoney(cart.discount_amount, currency)}</span>
137
+ </div>
138
+ ) : null}
139
+ <div className="nt-subtotal nt-total">
140
+ <span>{t("Total", "الإجمالي")}</span>
141
+ <span>
142
+ {formatMoney(
143
+ cart?.total ??
144
+ (cart?.subtotal ?? 0) - (cart?.discount_amount ?? 0),
145
+ currency,
146
+ )}
147
+ </span>
148
+ </div>
149
+ </div>
150
+
151
+ <div
152
+ style={{
153
+ display: "flex",
154
+ justifyContent: "flex-end",
155
+ marginTop: "1.5rem",
156
+ }}
157
+ >
158
+ <a className="nt-btn" href="/checkout">
159
+ <EditableText
160
+ as="span"
161
+ sectionId={id}
162
+ settingId="checkout_label"
163
+ value={s.checkout_label || t("Checkout", "إتمام الطلب")}
164
+ />
165
+ </a>
166
+ </div>
167
+ </section>
168
+ );
169
+ }
@@ -0,0 +1,57 @@
1
+ import { useCollections } from "@numueg/theme-sdk";
2
+ import { EditableText } from "../lib/EditableText";
3
+ import type { EmpSectionProps } from "../lib/section";
4
+ import { useT } from "../lib/i18n";
5
+
6
+ interface CategoriesSettings {
7
+ title?: string;
8
+ columns_desktop?: number;
9
+ }
10
+
11
+ /**
12
+ * Shop-by-category grid — black square tiles holding the collection image
13
+ * (contained on black) with an uppercase tracked label beneath. Reads the
14
+ * store's collections via the SDK.
15
+ */
16
+ export default function Categories({ id, settings }: EmpSectionProps) {
17
+ const s = settings as CategoriesSettings;
18
+ const t = useT();
19
+ const cols = Math.max(2, Math.min(6, s.columns_desktop ?? 5));
20
+ const { collections } = useCollections({ fetchIfMissing: true });
21
+
22
+ if (collections.length === 0) return null;
23
+
24
+ return (
25
+ <section className="nt-section nt-bg-white">
26
+ <div className="nt-container">
27
+ <EditableText
28
+ as="h2"
29
+ className="nt-heading"
30
+ sectionId={id}
31
+ settingId="title"
32
+ value={s.title ?? t("Shop by category", "تسوق حسب الفئة")}
33
+ style={{ marginBottom: "1.5rem" }}
34
+ />
35
+ <div
36
+ className="nt-catgrid"
37
+ style={{ ["--cols" as string]: cols }}
38
+ >
39
+ {collections.map((c) => (
40
+ <a key={c.id} className="nt-cat" href={`/collections/${c.slug}`}>
41
+ <div className="nt-cat__media">
42
+ {c.image_url ? (
43
+ <img src={c.image_url} alt={c.name} loading="lazy" />
44
+ ) : (
45
+ <span className="nt-cat__placeholder">
46
+ {c.name?.[0] ?? "?"}
47
+ </span>
48
+ )}
49
+ </div>
50
+ <h3 className="nt-cat__name">{c.name}</h3>
51
+ </a>
52
+ ))}
53
+ </div>
54
+ </div>
55
+ </section>
56
+ );
57
+ }