From de5aa451c147eb0489e892e804867a739fdb3238 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Sat, 28 Mar 2026 01:53:33 +0000 Subject: [PATCH] Autosave: 20260328-015333 --- .../api-server/src/routes/integrations.ts | 2 +- artifacts/api-server/src/routes/orders.ts | 10 +- artifacts/api-server/tsconfig.json | 9 +- artifacts/extra-store/src/App.tsx | 3979 ++++++++++-- .../extra-store/src/lib/admin-preview-api.ts | 1470 +++++ artifacts/extra-store/src/lib/i18n.ts | 2 + artifacts/extra-store/src/lib/mock-auth.ts | 118 + .../extra-store/src/lib/store-fallback.ts | 980 +++ artifacts/extra-store/src/pages/Admin.tsx | 5632 ++++++++++++++--- artifacts/extra-store/vite.config.ts | 16 +- artifacts/mockup-sandbox/vite.config.ts | 16 +- lib/api-zod/src/index.ts | 1 - 12 files changed, 10484 insertions(+), 1751 deletions(-) create mode 100644 artifacts/extra-store/src/lib/admin-preview-api.ts create mode 100644 artifacts/extra-store/src/lib/mock-auth.ts create mode 100644 artifacts/extra-store/src/lib/store-fallback.ts diff --git a/artifacts/api-server/src/routes/integrations.ts b/artifacts/api-server/src/routes/integrations.ts index 6da9665..abe60a8 100644 --- a/artifacts/api-server/src/routes/integrations.ts +++ b/artifacts/api-server/src/routes/integrations.ts @@ -147,7 +147,7 @@ router.get("/integrations/shein-categories", async (req, res) => { try { let categories: SheinCategory[] = []; let source = "preset"; - let scrapeResult: { success: boolean; error?: string; runId?: string } | null = null; + let scrapeResult: Awaited> | null = null; if (mode === "scrape") { scrapeResult = await fetchSheinCategories(); diff --git a/artifacts/api-server/src/routes/orders.ts b/artifacts/api-server/src/routes/orders.ts index af4d9b1..575e4d1 100644 --- a/artifacts/api-server/src/routes/orders.ts +++ b/artifacts/api-server/src/routes/orders.ts @@ -5,6 +5,10 @@ import { requireAdmin } from "../middleware/auth"; const router: IRouter = Router(); +function getSingleParamValue(value: string | string[] | undefined): string { + return Array.isArray(value) ? value[0] ?? "" : value ?? ""; +} + function generateOrderNumber(): string { const now = Date.now(); const random = Math.floor(Math.random() * 1000).toString().padStart(3, "0"); @@ -41,7 +45,7 @@ router.get("/orders", async (req, res) => { router.get("/orders/:id", async (req, res) => { try { - const id = parseInt(req.params.id); + const id = parseInt(getSingleParamValue(req.params.id), 10); const [order] = await db.select().from(ordersTable).where(eq(ordersTable.id, id)); if (!order) return res.status(404).json({ error: "Order not found" }); res.json(order); @@ -191,7 +195,7 @@ router.post("/orders", async (req, res) => { router.delete("/orders/:id", requireAdmin, async (req, res) => { try { - const id = parseInt(req.params.id); + const id = parseInt(getSingleParamValue(req.params.id), 10); await db.delete(ordersTable).where(eq(ordersTable.id, id)); res.json({ success: true }); } catch (err) { @@ -202,7 +206,7 @@ router.delete("/orders/:id", requireAdmin, async (req, res) => { router.put("/orders/:id/status", async (req, res) => { try { - const id = parseInt(req.params.id); + const id = parseInt(getSingleParamValue(req.params.id), 10); const { status, tracking_number } = req.body; // Fetch current order first diff --git a/artifacts/api-server/tsconfig.json b/artifacts/api-server/tsconfig.json index b60e718..dc84fd5 100644 --- a/artifacts/api-server/tsconfig.json +++ b/artifacts/api-server/tsconfig.json @@ -3,9 +3,14 @@ "compilerOptions": { "outDir": "dist", "rootDir": "src", - "types": ["node"] + "types": [ + "node" + ], + "noImplicitReturns": false }, - "include": ["src"], + "include": [ + "src" + ], "references": [ { "path": "../../lib/db" diff --git a/artifacts/extra-store/src/App.tsx b/artifacts/extra-store/src/App.tsx index 105022e..cd4e71f 100644 --- a/artifacts/extra-store/src/App.tsx +++ b/artifacts/extra-store/src/App.tsx @@ -1,10 +1,44 @@ -import { Switch, Route, Router as WouterRouter, Link, useLocation } from "wouter"; +import { + Switch, + Route, + Router as WouterRouter, + Link, + useLocation, +} from "wouter"; import AdminPage from "./pages/Admin"; -import { QueryClient, QueryClientProvider, useQuery } from "@tanstack/react-query"; -import { useState, useRef, useEffect, createContext, useContext, useCallback } from "react"; +import { + QueryClient, + QueryClientProvider, + useQuery, +} from "@tanstack/react-query"; +import { + useState, + useRef, + useEffect, + createContext, + useContext, + useCallback, +} from "react"; import { motion, AnimatePresence } from "framer-motion"; import { API } from "./lib/api"; +import { + isJsonResponse, + loginPreviewStoreUser, + registerPreviewStoreUser, +} from "./lib/mock-auth"; import { translations, Lang } from "./lib/i18n"; +import { + FEATURED_MENU_ITEMS, + FOOTER_POLICY_LINKS, + FALLBACK_STORE_SETTINGS, + getFallbackCategories, + getFallbackCategoryTree, + getFallbackProduct, + getFallbackProducts, +} from "./lib/store-fallback"; +import { installPreviewAdminApi } from "./lib/admin-preview-api"; + +if (typeof window !== "undefined") installPreviewAdminApi(); // ─── Language Context ───────────────────────────────── interface LangCtx { @@ -14,27 +48,69 @@ interface LangCtx { dir: "rtl" | "ltr"; } const LanguageContext = createContext({ - lang: "ar", setLang: () => {}, - t: k => translations.ar[k], + lang: "ar", + setLang: () => {}, + t: (k) => translations.ar[k], dir: "rtl", }); -function useLang() { return useContext(LanguageContext); } +function useLang() { + return useContext(LanguageContext); +} function LanguageProvider({ children }: { children: React.ReactNode }) { - const [lang, setLangState] = useState(() => (localStorage.getItem("extra_lang") as Lang) ?? "ar"); - const setLang = (l: Lang) => { setLangState(l); localStorage.setItem("extra_lang", l); }; - const t = useCallback((key: keyof typeof translations.ar): string => translations[lang][key] ?? translations.ar[key], [lang]); + const [lang, setLangState] = useState( + () => (localStorage.getItem("extra_lang") as Lang) ?? "ar", + ); + const setLang = (l: Lang) => { + setLangState(l); + localStorage.setItem("extra_lang", l); + }; + const t = useCallback( + (key: keyof typeof translations.ar): string => + translations[lang][key] ?? translations.ar[key], + [lang], + ); const dir: "rtl" | "ltr" = lang === "ar" ? "rtl" : "ltr"; - useEffect(() => { document.documentElement.dir = dir; document.documentElement.lang = lang; }, [dir, lang]); - return {children}; + useEffect(() => { + document.documentElement.dir = dir; + document.documentElement.lang = lang; + }, [dir, lang]); + return ( + + {children} + + ); } function proxyImg(url: string): string { if (!url) return ""; + if ( + /^https?:\/\//i.test(url) || + /^data:image\//i.test(url) || + /^blob:/i.test(url) + ) + return url; return `${API}/image-proxy?url=${encodeURIComponent(url)}`; } -const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 60_000 } } }); +async function fetchJsonOrFallback( + url: string, + fallback: () => T | Promise, +): Promise { + try { + const res = await fetch(url); + const contentType = res.headers.get("content-type") || ""; + if (!res.ok || !contentType.includes("application/json")) + throw new Error("API unavailable"); + return await res.json(); + } catch { + return await fallback(); + } +} + +const queryClient = new QueryClient({ + defaultOptions: { queries: { staleTime: 60_000 } }, +}); // ─── Cart Context ──────────────────────────────────── interface CartItem { @@ -45,21 +121,47 @@ interface CartItem { } interface CartCtx { items: CartItem[]; - addItem: (product: Product, qty: number, color: string | null, size: string | null) => void; - removeItem: (productId: number, color: string | null, size: string | null) => void; - updateQty: (productId: number, color: string | null, size: string | null, qty: number) => void; + addItem: ( + product: Product, + qty: number, + color: string | null, + size: string | null, + ) => void; + removeItem: ( + productId: number, + color: string | null, + size: string | null, + ) => void; + updateQty: ( + productId: number, + color: string | null, + size: string | null, + qty: number, + ) => void; clearCart: () => void; count: number; subtotal: number; } const CartContext = createContext({ - items: [], addItem: () => {}, removeItem: () => {}, updateQty: () => {}, clearCart: () => {}, count: 0, subtotal: 0 + items: [], + addItem: () => {}, + removeItem: () => {}, + updateQty: () => {}, + clearCart: () => {}, + count: 0, + subtotal: 0, }); -function useCart() { return useContext(CartContext); } +function useCart() { + return useContext(CartContext); +} function CartProvider({ children }: { children: React.ReactNode }) { const [items, setItems] = useState(() => { - try { return JSON.parse(localStorage.getItem("extra_cart") || "[]"); } catch { return []; } + try { + return JSON.parse(localStorage.getItem("extra_cart") || "[]"); + } catch { + return []; + } }); const saveItems = useCallback((next: CartItem[]) => { @@ -67,43 +169,103 @@ function CartProvider({ children }: { children: React.ReactNode }) { localStorage.setItem("extra_cart", JSON.stringify(next)); }, []); - const key = (id: number, color: string | null, size: string | null) => `${id}|${color}|${size}`; + const key = (id: number, color: string | null, size: string | null) => + `${id}|${color}|${size}`; - const addItem = useCallback((product: Product, qty: number, color: string | null, size: string | null) => { - setItems(prev => { - const k = key(product.id, color, size); - const exists = prev.find(i => key(i.product.id, i.color, i.size) === k); - const next = exists - ? prev.map(i => key(i.product.id, i.color, i.size) === k ? { ...i, quantity: Math.min(i.quantity + qty, i.product.stock) } : i) - : [...prev, { product, quantity: qty, color, size }]; - localStorage.setItem("extra_cart", JSON.stringify(next)); - return next; - }); - }, []); + const addItem = useCallback( + ( + product: Product, + qty: number, + color: string | null, + size: string | null, + ) => { + setItems((prev) => { + const k = key(product.id, color, size); + const exists = prev.find( + (i) => key(i.product.id, i.color, i.size) === k, + ); + const next = exists + ? prev.map((i) => + key(i.product.id, i.color, i.size) === k + ? { + ...i, + quantity: Math.min(i.quantity + qty, i.product.stock), + } + : i, + ) + : [...prev, { product, quantity: qty, color, size }]; + localStorage.setItem("extra_cart", JSON.stringify(next)); + return next; + }); + }, + [], + ); - const removeItem = useCallback((productId: number, color: string | null, size: string | null) => { - saveItems(items.filter(i => key(i.product.id, i.color, i.size) !== key(productId, color, size))); - }, [items, saveItems]); + const removeItem = useCallback( + (productId: number, color: string | null, size: string | null) => { + saveItems( + items.filter( + (i) => + key(i.product.id, i.color, i.size) !== key(productId, color, size), + ), + ); + }, + [items, saveItems], + ); - const updateQty = useCallback((productId: number, color: string | null, size: string | null, qty: number) => { - if (qty < 1) { removeItem(productId, color, size); return; } - saveItems(items.map(i => key(i.product.id, i.color, i.size) === key(productId, color, size) ? { ...i, quantity: qty } : i)); - }, [items, saveItems, removeItem]); + const updateQty = useCallback( + ( + productId: number, + color: string | null, + size: string | null, + qty: number, + ) => { + if (qty < 1) { + removeItem(productId, color, size); + return; + } + saveItems( + items.map((i) => + key(i.product.id, i.color, i.size) === key(productId, color, size) + ? { ...i, quantity: qty } + : i, + ), + ); + }, + [items, saveItems, removeItem], + ); const clearCart = useCallback(() => saveItems([]), [saveItems]); const count = items.reduce((s, i) => s + i.quantity, 0); - const subtotal = items.reduce((s, i) => s + parseFloat(i.product.price) * i.quantity, 0); + const subtotal = items.reduce( + (s, i) => s + parseFloat(i.product.price) * i.quantity, + 0, + ); return ( - + {children} ); } // ─── Auth Context ──────────────────────────────────── -interface AuthUser { id: number; name: string | null; email: string; } +interface AuthUser { + id: number; + name: string | null; + email: string; +} interface AuthCtx { user: AuthUser | null; token: string | null; @@ -111,97 +273,225 @@ interface AuthCtx { logout: () => void; openAuth: (mode?: "login" | "register") => void; } -const AuthContext = createContext({ user: null, token: null, login: () => {}, logout: () => {}, openAuth: () => {} }); -function useAuth() { return useContext(AuthContext); } +const AuthContext = createContext({ + user: null, + token: null, + login: () => {}, + logout: () => {}, + openAuth: () => {}, +}); +function useAuth() { + return useContext(AuthContext); +} function AuthProvider({ children }: { children: React.ReactNode }) { const [user, setUser] = useState(() => { - try { const u = localStorage.getItem("extra_user"); return u ? JSON.parse(u) : null; } catch { return null; } + try { + const u = localStorage.getItem("extra_user"); + return u ? JSON.parse(u) : null; + } catch { + return null; + } }); - const [token, setToken] = useState(() => localStorage.getItem("extra_token")); + const [token, setToken] = useState(() => + localStorage.getItem("extra_token"), + ); const [authOpen, setAuthOpen] = useState(false); const [authMode, setAuthMode] = useState<"login" | "register">("login"); const login = useCallback((u: AuthUser, t: string) => { - setUser(u); setToken(t); + setUser(u); + setToken(t); localStorage.setItem("extra_user", JSON.stringify(u)); localStorage.setItem("extra_token", t); setAuthOpen(false); }, []); const logout = useCallback(() => { - setUser(null); setToken(null); - localStorage.removeItem("extra_user"); localStorage.removeItem("extra_token"); + setUser(null); + setToken(null); + localStorage.removeItem("extra_user"); + localStorage.removeItem("extra_token"); }, []); const openAuth = useCallback((mode: "login" | "register" = "login") => { - setAuthMode(mode); setAuthOpen(true); + setAuthMode(mode); + setAuthOpen(true); }, []); return ( {children} - setAuthOpen(false)} /> + setAuthOpen(false)} + /> ); } -function AuthDrawer({ open, mode, setMode, onClose }: { open: boolean; mode: "login" | "register"; setMode: (m: "login" | "register") => void; onClose: () => void; }) { +function AuthDrawer({ + open, + mode, + setMode, + onClose, +}: { + open: boolean; + mode: "login" | "register"; + setMode: (m: "login" | "register") => void; + onClose: () => void; +}) { const { login } = useAuth(); const showToast = useShowToast(); const { t, dir } = useLang(); - const [form, setForm] = useState({ name: "", email: "", password: "", confirm: "", remember: false }); + const [form, setForm] = useState({ + name: "", + email: "", + password: "", + confirm: "", + remember: false, + }); const [loading, setLoading] = useState(false); const [showPass, setShowPass] = useState(false); - const set = (k: string, v: string | boolean) => setForm(f => ({ ...f, [k]: v })); + const set = (k: string, v: string | boolean) => + setForm((f) => ({ ...f, [k]: v })); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setLoading(true); try { const endpoint = mode === "login" ? "/auth/login" : "/auth/register"; - const body = mode === "login" - ? { email: form.email, password: form.password, remember_me: form.remember } - : { name: form.name, email: form.email, password: form.password, confirm_password: form.confirm }; - const res = await fetch(`${API}${endpoint}`, { - method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) - }); - const data = await res.json(); - if (!res.ok) { showToast(data.error || "حدث خطأ", "error"); return; } + const body = + mode === "login" + ? { + email: form.email, + password: form.password, + remember_me: form.remember, + } + : { + name: form.name, + email: form.email, + password: form.password, + confirm_password: form.confirm, + }; + + let data: { user: AuthUser; token: string }; + + try { + const res = await fetch(`${API}${endpoint}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); + + if (!isJsonResponse(res)) throw new Error("preview-api-unavailable"); + + const json = await res.json(); + if (!res.ok) { + showToast(json.error || "حدث خطأ", "error"); + return; + } + data = json; + } catch (error) { + if ( + error instanceof Error && + error.message !== "preview-api-unavailable" + ) + throw error; + data = + mode === "login" + ? loginPreviewStoreUser({ + email: form.email, + password: form.password, + }) + : registerPreviewStoreUser({ + name: form.name, + email: form.email, + password: form.password, + confirm_password: form.confirm, + }); + showToast( + "تم استخدام وضع المعاينة لتسجيل الدخول لأن خدمة API غير متاحة حالياً", + "success", + ); + } + login(data.user, data.token); - showToast(mode === "login" ? `${t("welcome_back")} ${data.user.name || data.user.email} 👋` : t("account_created"), "success"); - } catch { showToast(t("server_error"), "error"); } - finally { setLoading(false); } + showToast( + mode === "login" + ? `${t("welcome_back")} ${data.user.name || data.user.email} 👋` + : t("account_created"), + "success", + ); + } catch (error) { + showToast( + error instanceof Error ? error.message : t("server_error"), + "error", + ); + } finally { + setLoading(false); + } }; return ( {open && ( <> - - + - + className={`fixed top-0 ${dir === "rtl" ? "right-0" : "left-0"} h-full w-full max-w-sm bg-[#0f0f0f] border-l border-white/10 z-[201] flex flex-col shadow-2xl`} + dir={dir} + > {/* Header */}
X
- {t("store_name")} + + {t("store_name")} +
-
{/* Tabs */}
- {(["login","register"] as const).map(m => ( - ))} @@ -210,12 +500,35 @@ function AuthDrawer({ open, mode, setMode, onClose }: { open: boolean; mode: "lo {/* Social buttons */}
@@ -227,64 +540,156 @@ function AuthDrawer({ open, mode, setMode, onClose }: { open: boolean; mode: "lo
{/* Form */} -
+ {mode === "register" && (
- - set("name", e.target.value)} + + set("name", e.target.value)} placeholder={t("auth_name_placeholder")} className="w-full bg-white/6 border border-white/12 text-white placeholder-white/25 rounded-xl px-4 py-2.5 text-sm focus:outline-none focus:border-orange-500/60" - style={{ fontSize: "16px" }} /> + style={{ fontSize: "16px" }} + />
)}
- - set("email", e.target.value)} required + + set("email", e.target.value)} + required placeholder="example@email.com" className="w-full bg-white/6 border border-white/12 text-white placeholder-white/25 rounded-xl px-4 py-2.5 text-sm focus:outline-none focus:border-orange-500/60" - style={{ fontSize: "16px" }} dir="ltr" /> + style={{ fontSize: "16px" }} + dir="ltr" + />
- +
- set("password", e.target.value)} required + set("password", e.target.value)} + required placeholder={t("auth_pass_placeholder")} className="w-full bg-white/6 border border-white/12 text-white placeholder-white/25 rounded-xl px-4 py-2.5 pl-10 text-sm focus:outline-none focus:border-orange-500/60" - style={{ fontSize: "16px" }} dir="ltr" /> -
- {mode === "register" &&

{t("auth_password_hint")}

} + {mode === "register" && ( +

+ {t("auth_password_hint")} +

+ )}
{mode === "register" && (
- - set("confirm", e.target.value)} required + + set("confirm", e.target.value)} + required placeholder={t("auth_confirm_placeholder")} className="w-full bg-white/6 border border-white/12 text-white placeholder-white/25 rounded-xl px-4 py-2.5 text-sm focus:outline-none focus:border-orange-500/60" - style={{ fontSize: "16px" }} dir="ltr" /> + style={{ fontSize: "16px" }} + dir="ltr" + />
)} {mode === "login" && ( )} -

- {mode === "login" ? t("auth_no_account") : t("auth_has_account")} -

@@ -296,7 +701,11 @@ function AuthDrawer({ open, mode, setMode, onClose }: { open: boolean; mode: "lo } // ─── Sound Notifications ───────────────────────────── -declare global { interface Window { webkitAudioContext?: typeof AudioContext; } } +declare global { + interface Window { + webkitAudioContext?: typeof AudioContext; + } +} function playSound(type: "success" | "error" | "info" = "success") { try { const ctx = new (window.AudioContext || window.webkitAudioContext!)(); @@ -304,7 +713,12 @@ function playSound(type: "success" | "error" | "info" = "success") { masterGain.connect(ctx.destination); masterGain.gain.setValueAtTime(0.18, ctx.currentTime); - const play = (freq: number, start: number, dur: number, wave: OscillatorType = "sine") => { + const play = ( + freq: number, + start: number, + dur: number, + wave: OscillatorType = "sine", + ) => { const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.connect(gain); @@ -313,15 +727,18 @@ function playSound(type: "success" | "error" | "info" = "success") { osc.frequency.setValueAtTime(freq, ctx.currentTime + start); gain.gain.setValueAtTime(0, ctx.currentTime + start); gain.gain.linearRampToValueAtTime(1, ctx.currentTime + start + 0.01); - gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + start + dur); + gain.gain.exponentialRampToValueAtTime( + 0.001, + ctx.currentTime + start + dur, + ); osc.start(ctx.currentTime + start); osc.stop(ctx.currentTime + start + dur); }; if (type === "success") { - play(523, 0, 0.15); // C5 - play(659, 0.1, 0.15); // E5 - play(784, 0.2, 0.25); // G5 + play(523, 0, 0.15); // C5 + play(659, 0.1, 0.15); // E5 + play(784, 0.2, 0.25); // G5 } else if (type === "error") { play(330, 0, 0.18, "sawtooth"); play(247, 0.15, 0.25, "sawtooth"); @@ -335,45 +752,103 @@ function playSound(type: "success" | "error" | "info" = "success") { } // ─── Toast Notification ────────────────────────────── -interface ToastItem { id: number; msg: string; type?: "success" | "error" | "info"; } +interface ToastItem { + id: number; + msg: string; + type?: "success" | "error" | "info"; +} function useToast() { const [toasts, setToasts] = useState([]); - const show = useCallback((msg: string, type: ToastItem["type"] = "success") => { - const id = Date.now(); - setToasts(t => [...t, { id, msg, type }]); - setTimeout(() => setToasts(t => t.filter(x => x.id !== id)), 3000); - }, []); + const show = useCallback( + (msg: string, type: ToastItem["type"] = "success") => { + const id = Date.now(); + setToasts((t) => [...t, { id, msg, type }]); + setTimeout(() => setToasts((t) => t.filter((x) => x.id !== id)), 3000); + }, + [], + ); const dismiss = useCallback((id: number) => { - setToasts(t => t.filter(x => x.id !== id)); + setToasts((t) => t.filter((x) => x.id !== id)); }, []); return { toasts, show, dismiss }; } -const ToastContext = createContext<{ show: (msg: string, type?: ToastItem["type"]) => void }>({ show: () => {} }); -function useShowToast() { return useContext(ToastContext).show; } +const ToastContext = createContext<{ + show: (msg: string, type?: ToastItem["type"]) => void; +}>({ show: () => {} }); +function useShowToast() { + return useContext(ToastContext).show; +} function ToastProvider({ children }: { children: React.ReactNode }) { const { toasts, show, dismiss } = useToast(); return ( {children} -
- {toasts.map(t => ( +
+ {toasts.map((t) => (
-
- {t.type === "error" - ? - : - } +
+ {t.type === "error" ? ( + + + + ) : ( + + + + )}
{t.msg} -
))} @@ -384,68 +859,145 @@ function ToastProvider({ children }: { children: React.ReactNode }) { // ─── Types ────────────────────────────────────────── interface Category { - id: number; name: string; name_en: string | null; icon: string | null; - parent_id: number | null; sort_order: number | null; - source: string | null; slug: string | null; shein_url: string | null; - image_url: string | null; product_count?: number; + id: number; + name: string; + name_en: string | null; + icon: string | null; + parent_id: number | null; + sort_order: number | null; + source: string | null; + slug: string | null; + shein_url: string | null; + image_url: string | null; + product_count?: number; } interface Product { - id: number; name: string; name_en: string | null; brand: string | null; - price: string; original_price: string | null; - images: string[]; colors: string[]; sizes: string[]; - specs: Record; marketing_points: string[]; - subcategory: string | null; category_id: number; - rating: string; review_count: number; stock: number; - is_trending: boolean; is_bestseller: boolean; is_new: boolean; is_top_rated: boolean; + id: number; + name: string; + name_en: string | null; + brand: string | null; + price: string; + original_price: string | null; + images: string[]; + colors: string[]; + sizes: string[]; + specs: Record; + marketing_points: string[]; + subcategory: string | null; + category_id: number; + rating: string; + review_count: number; + stock: number; + is_trending: boolean; + is_bestseller: boolean; + is_new: boolean; + is_top_rated: boolean; +} +interface ProductsResp { + products: Product[]; + total: number; + page: number; + total_pages: number; } -interface ProductsResp { products: Product[]; total: number; page: number; total_pages: number; } // ─── Extended Types ────────────────────────────────── -interface CategoryNode extends Category { children: Category[]; } +interface CategoryNode extends Category { + children: Category[]; +} // ─── Hooks ────────────────────────────────────────── function useCategories() { return useQuery({ queryKey: ["categories"], - queryFn: () => fetch(`${API}/categories`).then(r => r.json()).then((cats: Category[]) => cats.filter(c => !c.parent_id)) + queryFn: async () => { + const cats = await fetchJsonOrFallback( + `${API}/categories`, + () => Promise.resolve(getFallbackCategories() as Category[]), + ); + return cats.filter((c) => !c.parent_id); + }, }); } function useCategoryTree() { - return useQuery({ queryKey: ["categories-tree"], queryFn: () => fetch(`${API}/categories/tree`).then(r => r.json()) }); + return useQuery({ + queryKey: ["categories-tree"], + queryFn: () => + fetchJsonOrFallback(`${API}/categories/tree`, () => + Promise.resolve(getFallbackCategoryTree() as CategoryNode[]), + ), + }); } function useProducts(params: Record) { - const qs = Object.entries(params).filter(([, v]) => v !== undefined).map(([k, v]) => `${k}=${v}`).join("&"); + const qs = Object.entries(params) + .filter(([, v]) => v !== undefined) + .map(([k, v]) => `${k}=${v}`) + .join("&"); return useQuery({ queryKey: ["products", qs], - queryFn: () => fetch(`${API}/products?${qs}`).then(r => r.json()), + queryFn: () => + fetchJsonOrFallback(`${API}/products?${qs}`, () => + Promise.resolve(getFallbackProducts(params) as ProductsResp), + ), }); } function useProduct(id: number) { - return useQuery({ queryKey: ["product", id], queryFn: () => fetch(`${API}/products/${id}`).then(r => r.json()) }); + return useQuery({ + queryKey: ["product", id], + queryFn: () => + fetchJsonOrFallback(`${API}/products/${id}`, () => + Promise.resolve(getFallbackProduct(id) as Product), + ), + }); } function useStoreSettings() { return useQuery>({ queryKey: ["store-settings"], - queryFn: () => fetch(`${API}/public-settings`).then(r => r.json()), + queryFn: () => + fetchJsonOrFallback>( + `${API}/public-settings`, + () => Promise.resolve(FALLBACK_STORE_SETTINGS), + ), staleTime: 30_000, }); } +function storeCopy( + settings: Record | undefined, + lang: Lang, + key: string, + fallback: string, +) { + const arValue = settings?.[`${key}_ar`] ?? settings?.[key]; + const enValue = settings?.[`${key}_en`]; + return lang === "en" ? enValue || arValue || fallback : arValue || fallback; +} + // ─── Announcement Bar ──────────────────────────────── function AnnouncementBar() { const { data: s } = useStoreSettings(); const { lang } = useLang(); if (!s || s.announcement_enabled !== "true") return null; - const text = (lang === "en" && s.announcement_text_en) - ? s.announcement_text_en - : (s.announcement_text || ""); + const text = + lang === "en" && s.announcement_text_en + ? s.announcement_text_en + : s.announcement_text || ""; const bg = s.announcement_color || "#f97316"; const tc = s.announcement_text_color || "#ffffff"; return ( -
-
- {[0,1,2].map(i => ( - +
+
+ {[0, 1, 2].map((i) => ( + {text} ))} @@ -455,24 +1007,41 @@ function AnnouncementBar() { } // ─── Components ───────────────────────────────────── -function StarRating({ rating, count }: { rating?: string | null; count?: number | null }) { +function StarRating({ + rating, + count, +}: { + rating?: string | null; + count?: number | null; +}) { const r = parseFloat(rating ?? "0") || 0; return (
- {[1,2,3,4,5].map(i => ( - - + {[1, 2, 3, 4, 5].map((i) => ( + + ))}
- {(count ?? 0) > 0 && ({(count ?? 0).toLocaleString("ar-SA")})} + {(count ?? 0) > 0 && ( + + ({(count ?? 0).toLocaleString("ar-SA")}) + + )}
); } function ProductCard({ p }: { p: Product }) { - const discount = p.original_price ? Math.round((1 - parseFloat(p.price) / parseFloat(p.original_price)) * 100) : 0; + const discount = p.original_price + ? Math.round((1 - parseFloat(p.price) / parseFloat(p.original_price)) * 100) + : 0; const img = (Array.isArray(p.images) && p.images[0]) || ""; const [imgLoaded, setImgLoaded] = useState(false); const [imgError, setImgError] = useState(false); @@ -485,7 +1054,7 @@ function ProductCard({ p }: { p: Product }) { e.preventDefault(); e.stopPropagation(); addItem(p, 1, p.colors?.[0] ?? null, p.sizes?.[0] ?? null); - const name = (lang === "en" && p.name_en) ? p.name_en : p.name; + const name = lang === "en" && p.name_en ? p.name_en : p.name; showToast(`${t("added_toast_short")} ${name.substring(0, 30)}...`); setAdded(true); setTimeout(() => setAdded(false), 2000); @@ -502,7 +1071,9 @@ function ProductCard({ p }: { p: Product }) { )} {p.is_new && ( - {t("product_new")} + + {t("product_new")} + )} {img && !imgError ? ( ) : imgError ? (
- - + +
) : null} @@ -528,7 +1109,9 @@ function ProductCard({ p }: { p: Product }) {
{/* Product info — dark */}
- {p.brand && {p.brand}} -

{(lang === "en" && p.name_en) ? p.name_en : p.name}

+ {p.brand && ( + + {p.brand} + + )} +

+ {lang === "en" && p.name_en ? p.name_en : p.name} +

- {parseFloat(p.price).toLocaleString("ar-SA")} {t("currency")} - {p.original_price && parseFloat(p.original_price) > parseFloat(p.price) && ( - {parseFloat(p.original_price).toLocaleString("ar-SA")} - )} + + {parseFloat(p.price).toLocaleString("ar-SA")} {t("currency")} + + {p.original_price && + parseFloat(p.original_price) > parseFloat(p.price) && ( + + {parseFloat(p.original_price).toLocaleString("ar-SA")} + + )}
@@ -553,13 +1147,19 @@ function ProductCard({ p }: { p: Product }) { ); } -function SheinMegaMenu({ tree, onClose }: { tree: CategoryNode[]; onClose: () => void }) { +function SheinMegaMenu({ + tree, + onClose, +}: { + tree: CategoryNode[]; + onClose: () => void; +}) { const [activeSection, setActiveSection] = useState(null); const { t, lang } = useLang(); - const sheinSections = tree.filter(n => n.source === "shein"); + const sheinSections = tree.filter((n) => n.source === "shein"); const active = activeSection ?? sheinSections[0] ?? null; const catName = (c: CategoryNode & { name_en?: string }) => - (lang === "en" && c.name_en) ? c.name_en : c.name; + lang === "en" && c.name_en ? c.name_en : c.name; return (
{/* Left: Category list with thumbnail images */}
- {sheinSections.map(sec => ( + {sheinSections.map((sec) => ( ))} @@ -609,32 +1219,57 @@ function SheinMegaMenu({ tree, onClose }: { tree: CategoryNode[]; onClose: () =>
- {(active as CategoryNode & { image_url?: string }).image_url ? ( - {active.name} + {(active as CategoryNode & { image_url?: string }) + .image_url ? ( + {active.name} ) : ( - {active.icon} + + {active.icon} + )}
-

{catName(active as CategoryNode & { name_en?: string })}

-

{(active as CategoryNode & { name_en?: string }).name_en}

+

+ {catName(active as CategoryNode & { name_en?: string })} +

+

+ { + (active as CategoryNode & { name_en?: string }) + .name_en + } +

- + {t("view_all")}
{active.children && active.children.length > 0 ? (
- {active.children.map(sub => ( + {active.children.map((sub) => ( - {sub.icon} - {catName(sub as CategoryNode & { name_en?: string })} + + {sub.icon} + + + {catName(sub as CategoryNode & { name_en?: string })} + ))}
@@ -648,23 +1283,36 @@ function SheinMegaMenu({ tree, onClose }: { tree: CategoryNode[]; onClose: () =>
{/* Promo panel — active category hero image */} - {active && (active as CategoryNode & { image_url?: string }).image_url && ( -
- -
- {active.name} -
-
- {catName(active as CategoryNode & { name_en?: string })} + {active && + (active as CategoryNode & { image_url?: string }).image_url && ( +
+ +
+ {active.name} +
+
+ + {catName(active as CategoryNode & { name_en?: string })} + +
-
- -
- )} + +
+ )}
@@ -674,26 +1322,50 @@ function SheinMegaMenu({ tree, onClose }: { tree: CategoryNode[]; onClose: () => function Header() { const { data: allCats } = useCategories(); const { data: tree } = useCategoryTree(); + const { data: s } = useStoreSettings(); const [search, setSearch] = useState(""); const [, navigate] = useLocation(); const { count } = useCart(); const { user, logout, openAuth } = useAuth(); const { t, lang, setLang, dir } = useLang(); + const featuredMenu = FEATURED_MENU_ITEMS.map((item) => ({ + ...item, + label: lang === "en" ? item.label_en : item.label_ar, + })); + const storeName = storeCopy(s, lang, "store_name", t("store_name")); + const topBarOffer = storeCopy(s, lang, "top_bar_offer", t("top_bar_offer")); + const searchPlaceholder = storeCopy( + s, + lang, + "header_search_placeholder", + t("search_placeholder"), + ); + const menuStripLabel = storeCopy( + s, + lang, + "menu_strip_label", + lang === "en" ? "Store Menus" : "القوائم", + ); const [sheinOpen, setSheinOpen] = useState(false); const [userMenuOpen, setUserMenuOpen] = useState(false); const navRef = useRef(null); const scrollNav = (d: "left" | "right") => { - if (navRef.current) navRef.current.scrollBy({ left: d === "left" ? -200 : 200, behavior: "smooth" }); + if (navRef.current) + navRef.current.scrollBy({ + left: d === "left" ? -200 : 200, + behavior: "smooth", + }); }; - const extraCats = allCats?.filter(c => !c.source || c.source === "extra") ?? []; - const sheinTree = tree?.filter(n => n.source === "shein") ?? []; + const extraCats = + allCats?.filter((c) => !c.source || c.source === "extra") ?? []; + const sheinTree = tree?.filter((n) => n.source === "shein") ?? []; return (
{/* Top bar */}
- {t("top_bar_offer")} + {topBarOffer}
{/* Main header */}
@@ -701,19 +1373,44 @@ function Header() {
X
- {t("store_name")} + + {storeName} + -
{ e.preventDefault(); navigate(`/category/0?q=${encodeURIComponent(search)}`); }} className="flex-1"> + { + e.preventDefault(); + navigate(`/category/0?q=${encodeURIComponent(search)}`); + }} + className="flex-1" + >
setSearch(e.target.value)} - placeholder={t("search_placeholder")} + value={search} + onChange={(e) => setSearch(e.target.value)} + placeholder={searchPlaceholder} className="w-full bg-white/8 border border-white/12 text-white placeholder-white/35 rounded-xl py-2.5 pr-4 pl-10 text-sm focus:outline-none focus:border-orange-500/60" - style={{ fontSize: "16px" }} dir={lang === "en" ? "ltr" : "rtl"} + style={{ fontSize: "16px" }} + dir={lang === "en" ? "ltr" : "rtl"} /> -
@@ -733,45 +1430,131 @@ function Header() {
{user ? ( <> - {userMenuOpen && ( -
+
-

{user.name || t("user_guest")}

-

{user.email}

+

+ {user.name || t("user_guest")} +

+

+ {user.email} +

- setUserMenuOpen(false)} className="flex items-center gap-3 px-4 py-2.5 text-sm text-white/70 hover:text-white hover:bg-white/5 transition-colors"> - + setUserMenuOpen(false)} + className="flex items-center gap-3 px-4 py-2.5 text-sm text-white/70 hover:text-white hover:bg-white/5 transition-colors" + > + + + {t("user_profile")} - setUserMenuOpen(false)} className="flex items-center gap-3 px-4 py-2.5 text-sm text-white/70 hover:text-white hover:bg-white/5 transition-colors"> - + setUserMenuOpen(false)} + className="flex items-center gap-3 px-4 py-2.5 text-sm text-white/70 hover:text-white hover:bg-white/5 transition-colors" + > + + + {t("user_cart")} -
)} ) : ( -
{/* Cart Icon */} - - - + + + {count > 0 && ( @@ -783,34 +1566,79 @@ function Header() { {/* Category nav */} + +
+
+ + {menuStripLabel} + + {featuredMenu.map((item) => ( + + {item.label} + + ))} +
+
); } function Footer() { - const { t } = useLang(); + const { t, lang } = useLang(); + const { data: footerCats } = useCategories(); + const { data: s } = useStoreSettings(); + const categoryLinks = (footerCats ?? []) + .filter((c) => !c.source || c.source === "extra") + .slice(0, 10); + const policyLinks = FOOTER_POLICY_LINKS.map((item) => ({ + ...item, + label: lang === "en" ? item.label_en : item.label_ar, + })); + const storeName = storeCopy(s, lang, "store_name", t("store_name")); + const storeTagline = storeCopy(s, lang, "store_tagline", t("store_tagline")); + const footerAddress = storeCopy( + s, + lang, + "footer_address", + t("footer_address"), + ); + const footerContactPhone = s?.footer_contact_phone || "920003117"; + const footerCopyright = storeCopy( + s, + lang, + "footer_copyright", + t("footer_copyright"), + ); return (
-
+
X
- {t("store_name")} + {storeName}
-

{t("store_tagline")}

+

{storeTagline}

-

{t("footer_quick_links")}

+

+ {t("footer_quick_links")} +

-

{t("footer_customer_service")}

+

+ {lang === "en" ? "Product Categories" : "فئات المنتجات"} +

-

{t("footer_contact")}

-

920003117

-

{t("footer_address")}

+

+ {lang === "en" ? "Policies & Menus" : "السياسات والقوائم"} +

+ +
+
+

+ {t("footer_contact")} +

+

{footerContactPhone}

+

{footerAddress}

- {t("footer_copyright")} + {footerCopyright}
); @@ -882,41 +1826,49 @@ function Home() { const { t, lang } = useLang(); const { data: allCats } = useCategories(); const { data: trending } = useProducts({ featured: "trending", limit: 10 }); - const { data: bestsellers } = useProducts({ featured: "bestseller", limit: 10 }); + const { data: bestsellers } = useProducts({ + featured: "bestseller", + limit: 10, + }); const { data: newArr } = useProducts({ featured: "new_arrivals", limit: 10 }); + const { data: topRated } = useProducts({ featured: "top_rated", limit: 10 }); const { data: s } = useStoreSettings(); - const extraCats = allCats?.filter(c => !c.source || c.source === "extra") ?? []; - const sheinCats = allCats?.filter(c => c.source === "shein" && !c.parent_id) ?? []; + const extraCats = + allCats?.filter((c) => !c.source || c.source === "extra") ?? []; + const sheinCats = + allCats?.filter((c) => c.source === "shein" && !c.parent_id) ?? []; const accent = s?.hero_accent_color || "#f97316"; const isEn = lang === "en"; const heroBadge = isEn - ? (s?.hero_badge_en || t("hero_badge")) - : (s?.hero_badge_ar || t("hero_badge")); + ? s?.hero_badge_en || t("hero_badge") + : s?.hero_badge_ar || t("hero_badge"); const heroTitle = isEn - ? (s?.hero_title_en || t("hero_title")) - : (s?.hero_title_ar || t("hero_title")); + ? s?.hero_title_en || t("hero_title") + : s?.hero_title_ar || t("hero_title"); const heroSub = isEn - ? (s?.hero_subtitle_en || t("hero_sub")) - : (s?.hero_subtitle_ar || t("hero_sub")); + ? s?.hero_subtitle_en || t("hero_sub") + : s?.hero_subtitle_ar || t("hero_sub"); const heroCta = isEn - ? (s?.hero_cta_en || t("hero_cta")) - : (s?.hero_cta_ar || t("hero_cta")); + ? s?.hero_cta_en || t("hero_cta") + : s?.hero_cta_ar || t("hero_cta"); const heroCtaLink = s?.hero_cta_link || "/category/0"; const heroBgImage = s?.hero_bg_image || ""; // Promo banners let promoBanners: { image_url: string; link: string; title: string }[] = []; - try { promoBanners = JSON.parse(s?.promo_banners || "[]"); } catch {} + try { + promoBanners = JSON.parse(s?.promo_banners || "[]"); + } catch {} const sections = [ { id: "trending", enabled: s?.section_trending_enabled !== "false", title: isEn - ? (s?.section_trending_title_en || t("section_trending_title")) - : (s?.section_trending_title_ar || t("section_trending_title")), + ? s?.section_trending_title_en || t("section_trending_title") + : s?.section_trending_title_ar || t("section_trending_title"), icon: s?.section_trending_icon || "🔥", data: trending?.products, }, @@ -924,8 +1876,8 @@ function Home() { id: "bestseller", enabled: s?.section_bestseller_enabled !== "false", title: isEn - ? (s?.section_bestseller_title_en || t("section_bestseller_title")) - : (s?.section_bestseller_title_ar || t("section_bestseller_title")), + ? s?.section_bestseller_title_en || t("section_bestseller_title") + : s?.section_bestseller_title_ar || t("section_bestseller_title"), icon: s?.section_bestseller_icon || "⭐", data: bestsellers?.products, }, @@ -933,11 +1885,20 @@ function Home() { id: "new", enabled: s?.section_new_enabled !== "false", title: isEn - ? (s?.section_new_title_en || t("section_new_title")) - : (s?.section_new_title_ar || t("section_new_title")), + ? s?.section_new_title_en || t("section_new_title") + : s?.section_new_title_ar || t("section_new_title"), icon: s?.section_new_icon || "✨", data: newArr?.products, }, + { + id: "top-rated", + enabled: s?.section_top_rated_enabled !== "false", + title: isEn + ? s?.section_top_rated_title_en || t("section_top_rated_title") + : s?.section_top_rated_title_ar || t("section_top_rated_title"), + icon: s?.section_top_rated_icon || "🏆", + data: topRated?.products, + }, ]; return ( @@ -946,24 +1907,39 @@ function Home() { {s?.hero_enabled !== "false" && (
{heroBgImage &&
}
-
+
{heroBadge}

{heroTitle}

{heroSub}

- + style={{ backgroundColor: accent }} + > {heroCta}
@@ -973,15 +1949,27 @@ function Home() { {/* Promo Banners */} {promoBanners.length > 0 && (
-
+
{promoBanners.map((b, i) => (
- {b.title} { (e.target as HTMLImageElement).parentElement!.style.display = "none"; }} /> + {b.title} { + ( + e.target as HTMLImageElement + ).parentElement!.style.display = "none"; + }} + /> {b.title && (
- {b.title} + + {b.title} +
)}
@@ -996,19 +1984,24 @@ function Home() { {s?.extra_section_enabled !== "false" && extraCats.length > 0 && (

-
+
X
{isEn - ? (s?.extra_section_title_en || t("section_extra_title")) - : (s?.extra_section_title_ar || t("section_extra_title"))} + ? s?.extra_section_title_en || t("section_extra_title") + : s?.extra_section_title_ar || t("section_extra_title")}

- {extraCats.map(c => ( + {extraCats.map((c) => (
{c.icon} - {(lang === "en" && c.name_en) ? c.name_en : c.name} + + {lang === "en" && c.name_en ? c.name_en : c.name} +
))} @@ -1022,29 +2015,44 @@ function Home() {
- SHEIN + + SHEIN +

{isEn - ? (s?.shein_section_title_en || t("shein_section_title")) - : (s?.shein_section_title_ar || t("shein_section_title"))} + ? s?.shein_section_title_en || t("shein_section_title") + : s?.shein_section_title_ar || t("shein_section_title")} +

+

+ {isEn ? "أزياء، جمال ومنزل" : "Fashion, Beauty & Home"}

-

{isEn ? "أزياء، جمال ومنزل" : "Fashion, Beauty & Home"}

- + {t("view_all")}
- {sheinCats.map(c => ( + {sheinCats.map((c) => ( -
+
{c.image_url ? ( - {c.name} { (e.target as HTMLImageElement).style.display = 'none'; }} /> + onError={(e) => { + (e.target as HTMLImageElement).style.display = "none"; + }} + /> ) : (
{c.icon ?? "🏷️"} @@ -1053,12 +2061,24 @@ function Home() {
- {(isEn && c.name_en) ? c.name_en : c.name} + {isEn && c.name_en ? c.name_en : c.name} - {c.name_en && !isEn && {c.name_en}} + {c.name_en && !isEn && ( + + {c.name_en} + + )}
- {c.slug === 'new-in' &&
NEW
} - {c.slug === 'sale' &&
SALE
} + {c.slug === "new-in" && ( +
+ NEW +
+ )} + {c.slug === "sale" && ( +
+ SALE +
+ )}
))} @@ -1067,23 +2087,35 @@ function Home() { )} {/* Product Sections */} - {sections.filter(sec => sec.enabled && sec.data && sec.data.length > 0).map(sec => ( -
-
-
-

- {sec.icon} {sec.title} -

+ {sections + .filter((sec) => sec.enabled && sec.data && sec.data.length > 0) + .map((sec) => ( +
+
+
+

+ {sec.icon} {sec.title} +

+
+ + {t("section_view_all")} +
- - {t("section_view_all")} - -
-
- {sec.data!.map(p => )} -
-
- ))} +
+ {sec.data!.map((p) => ( + + ))} +
+
+ ))}
); @@ -1098,16 +2130,17 @@ function Category() { const q = urlParams.get("q") || ""; const [sort, setSort] = useState("relevance"); - const [tab, setTab] = useState<"all" | "trending" | "bestseller" | "new_arrivals" | "top_rated">("all"); + const [tab, setTab] = useState< + "all" | "trending" | "bestseller" | "new_arrivals" | "top_rated" + >("all"); const [selectedSubcat, setSelectedSubcat] = useState(null); const { data: cats } = useCategories(); const { data: tree } = useCategoryTree(); - const cat = cats?.find(c => c.id === catId); + const cat = cats?.find((c) => c.id === catId); - const subcats: Category[] = catId > 0 - ? (tree?.find(n => n.id === catId)?.children ?? []) - : []; + const subcats: Category[] = + catId > 0 ? (tree?.find((n) => n.id === catId)?.children ?? []) : []; const queryParams: Record = { page: 1, @@ -1132,39 +2165,82 @@ function Category() {
{/* Back + Breadcrumb */}
- - {t("home")} + + {t("home")} + - {catId === 0 ? t("all_products") : ((lang === "en" && cat?.name_en) ? cat.name_en : (cat?.name ?? "..."))} - {selectedSubcat && <>{selectedSubcat}} + + {catId === 0 + ? t("all_products") + : lang === "en" && cat?.name_en + ? cat.name_en + : (cat?.name ?? "...")} + + {selectedSubcat && ( + <> + + {selectedSubcat} + + )}
{/* Title */}

- {cat?.icon} {catId === 0 ? t("all_products") : ((lang === "en" && cat?.name_en) ? cat.name_en : (cat?.name ?? ""))} - {q && {t("results_for")} "{q}"} + {cat?.icon}{" "} + {catId === 0 + ? t("all_products") + : lang === "en" && cat?.name_en + ? cat.name_en + : (cat?.name ?? "")} + {q && ( + + {t("results_for")} "{q}" + + )}

-

{data?.total ?? 0} {t("products_count")}

+

+ {data?.total ?? 0} {t("products_count")} +

{/* Tabs */}
- {([ - { id: "all", label: t("tab_all") }, - { id: "trending", label: t("tab_trending") }, - { id: "bestseller", label: t("tab_bestseller") }, - { id: "top_rated", label: t("tab_top_rated") }, - { id: "new_arrivals", label: t("tab_new") }, - ] as const).map(tb => ( + {( + [ + { id: "all", label: t("tab_all") }, + { id: "trending", label: t("tab_trending") }, + { id: "bestseller", label: t("tab_bestseller") }, + { id: "top_rated", label: t("tab_top_rated") }, + { id: "new_arrivals", label: t("tab_new") }, + ] as const + ).map((tb) => (
{/* Order Summary */}
-
{t("subtotal")}{subtotal.toFixed(2)} {t("currency")}
+
+ {t("subtotal")} + + {subtotal.toFixed(2)} {t("currency")} + +
{t("shipping")} - {shippingFee === 0 ? {t("free")} : `${shippingFee} ${t("currency")}`} + + {shippingFee === 0 ? ( + + {t("free")} + + ) : ( + `${shippingFee} ${t("currency")}` + )} +
- {t("total")}{finalTotal.toFixed(2)} {t("currency")} + {t("total")} + + {finalTotal.toFixed(2)} {t("currency")} +
- )} {/* Step 2: Payment */} {step === 2 && ( - -

{t("payment_method")}

+ +
+

+ {paymentSectionTitle} +

+ {paymentSectionSubtitle && ( +

+ {paymentSectionSubtitle} +

+ )} +
{/* Apple Pay + Google Pay */}
- -
- {t("pay_with_card")} + + {t("pay_with_card")} +
@@ -2215,15 +4446,25 @@ function CheckoutPage() {
setFormData({...formData, cardNumber: formatCardNumberCO(e.target.value)})} + onChange={(e) => + setFormData({ + ...formData, + cardNumber: formatCardNumberCO(e.target.value), + }) + } className={`${CF} pr-28 font-mono tracking-widest text-lg ${cardError ? "border-red-500 ring-2 ring-red-500/30" : isValidCard ? "border-[#D4AF37] ring-2 ring-[#D4AF37]/30" : ""}`} />
{cardType === "VISA" && ( - VISA + + VISA + )} {cardType === "MASTER" && ( @@ -2238,14 +4479,26 @@ function CheckoutPage() { )} {!cardType && rawCard.length === 0 && ( - {lang === "en" ? "VISA / MC / mada" : "VISA / MC / مدى"} + {lang === "en" + ? "VISA / MC / mada" + : "VISA / MC / مدى"} )}
{cardError && (

- + + + {t("card_invalid")}

)} @@ -2255,14 +4508,33 @@ function CheckoutPage() {
setFormData({...formData, expiry: formatExpiryCO(e.target.value)})} + onChange={(e) => + setFormData({ + ...formData, + expiry: formatExpiryCO(e.target.value), + }) + } className={`${CF} font-mono ${expiryError ? "border-red-500 ring-2 ring-red-500/30" : expiryComplete && isValidExpiry ? "border-[#D4AF37] ring-2 ring-[#D4AF37]/30" : ""}`} /> {expiryError && (

- + + + {t("card_expired")}

)} @@ -2270,29 +4542,61 @@ function CheckoutPage() {
setFormData({...formData, cvv: e.target.value.replace(/\D/g, "").substring(0, 3)})} + onChange={(e) => + setFormData({ + ...formData, + cvv: e.target.value + .replace(/\D/g, "") + .substring(0, 3), + }) + } className={`${CF} font-mono`} />
- + { - const filtered = e.target.value.replace(/[^\u0000-\u007F]/g, "").toUpperCase(); - setFormData({...formData, cardHolder: filtered}); + onChange={(e) => { + const filtered = e.target.value + .replace(/[^\u0000-\u007F]/g, "") + .toUpperCase(); + setFormData({ ...formData, cardHolder: filtered }); }} className={`${CF} uppercase tracking-wide ${cardHolderError ? "border-red-500 ring-2 ring-red-500/30" : formData.cardHolder.trim().length >= 3 && !cardHolderError ? "border-[#D4AF37] ring-2 ring-[#D4AF37]/30" : ""}`} /> {cardHolderError && (

- + + + {t("card_holder_error")}

)} @@ -2302,20 +4606,72 @@ function CheckoutPage() {
{t("payment_total")} - {finalTotal.toFixed(2)} {t("currency")} + + {finalTotal.toFixed(2)} {t("currency")} +
-

{t("incl_shipping")}

+

+ {t("incl_shipping")} +

- -
@@ -2323,18 +4679,44 @@ function CheckoutPage() { {/* Step 3: OTP */} {step === 3 && ( - + {otpSuccess ? ( - +
- + + +
-

✅ {t("payment_success")}

+

+ ✅ {verificationSuccessTitle} +

{placedOrderNumber && (

- {lang === "en" ? "Order Confirmation Code" : "رمز تأكيد الطلب"} + {lang === "en" + ? "Order Confirmation Code" + : "رمز تأكيد الطلب"}

@@ -2342,19 +4724,50 @@ function CheckoutPage() {

- {lang === "en" ? "Keep this code to track your order" : "احتفظ بهذا الرمز لمتابعة طلبك"} + {lang === "en" + ? "Keep this code to track your order" + : "احتفظ بهذا الرمز لمتابعة طلبك"}

)} -

{t("payment_success_msg")}

+

+ {verificationSuccessMsg} +

) : otpLoading ? ( - +
- + + + +
-

{t("verifying")}

-

{t("verifying_msg")}

+

+ {verificationProcessingTitle} +

+

+ {verificationProcessingMsg} +

) : ( - +
- + + +
-

{t("otp_title")}

-

{t("otp_msg")}

+

+ {verificationTitle} +

+

+ {verificationSubtitle} +

setOtp(e.target.value.replace(/\D/g, ""))} + onChange={(e) => + setOtp(e.target.value.replace(/\D/g, "")) + } className="w-full border-2 border-[#333] rounded-xl px-4 py-4 text-center text-3xl tracking-[1em] font-mono focus:border-[#D4AF37] outline-none mb-2 bg-[#0f0f0f] text-white" placeholder="——————" /> -

{t("otp_hint")}

+

+ {verificationHint} +

- {otpTimer > 0 - ? `${t("otp_resend_in")} ${otpTimer} ${t("otp_seconds")}` - : - } + {otpTimer > 0 ? ( + `${t("otp_resend_in")} ${otpTimer} ${t("otp_seconds")}` + ) : ( + + )}

@@ -2403,16 +4861,26 @@ function CheckoutPage() {
)} -
{/* Security Badge */}
- + + + {t("ssl_badge")}
-
); @@ -2425,7 +4893,12 @@ function NotFoundPage() {
404

{t("not_found")}

- {t("back_home")} + + {t("back_home")} +
); } @@ -2440,11 +4913,28 @@ function ProfilePage() { return (
- + + +
-

{t("profile_login_first")}

+

+ {t("profile_login_first")} +

{t("profile_login_sub")}

-
@@ -2455,8 +4945,23 @@ function ProfilePage() { return (
- @@ -2468,38 +4973,107 @@ function ProfilePage() { {initial}
-

{user.name || t("user_default")}

+

+ {user.name || t("user_default")} +

{user.email}

- {t("extra_member")} + + {t("extra_member")} +
{/* Quick Links */} {[ - { icon: "M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z", label: t("my_orders"), sub: t("my_orders_sub"), href: "/category/0" }, - { icon: "M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z", label: t("wishlist"), sub: t("wishlist_sub"), href: "/category/0" }, - { icon: "M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z M15 11a3 3 0 11-6 0 3 3 0 016 0z", label: t("my_addresses"), sub: t("my_addresses_sub"), href: "/" }, - { icon: "M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z", label: t("payment_methods"), sub: t("payment_methods_sub"), href: "/cart" }, - ].map(item => ( - + { + icon: "M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z", + label: t("my_orders"), + sub: t("my_orders_sub"), + href: "/category/0", + }, + { + icon: "M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z", + label: t("wishlist"), + sub: t("wishlist_sub"), + href: "/category/0", + }, + { + icon: "M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z M15 11a3 3 0 11-6 0 3 3 0 016 0z", + label: t("my_addresses"), + sub: t("my_addresses_sub"), + href: "/", + }, + { + icon: "M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z", + label: t("payment_methods"), + sub: t("payment_methods_sub"), + href: "/cart", + }, + ].map((item) => ( +
- + + +

{item.label}

{item.sub}

- + + + ))} {/* Logout */} -
@@ -2511,7 +5085,12 @@ function ProfilePage() { function Router() { const [location] = useLocation(); const isAdmin = location.startsWith("/admin"); - if (isAdmin) return ; + if (isAdmin) + return ( + + + + ); return ( <> @@ -2525,7 +5104,9 @@ function Router() { - + + +