@numueg/theme-cli 0.5.0 → 0.6.1
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 +825 -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 +541 -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,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
|
+
}
|