// ==========================================================
// itooth — main App (dynamic + ProductDetail + search + polish)
// ==========================================================
const { useState, useEffect, useRef, useCallback, useMemo } = React;
// ============ Announcement ============
function Announcement({ lang, messages }) {
const [idx, setIdx] = useState(0);
useEffect(() => {
if (!messages.length) return;
const t = setInterval(() => setIdx((i) => (i + 1) % messages.length), 4000);
return () => clearInterval(t);
}, [messages.length]);
if (!messages.length) return null;
const msg = messages[idx];
return (
setMenuOpen(false)} />
>
);
}
// ============ Hero ============
function Hero({ lang, settings, onShop }) {
const eyebrow = tr(settings, 'hero_eyebrow', lang);
const title1 = tr(settings, 'hero_title1', lang);
const title2 = tr(settings, 'hero_title2', lang);
const stat1 = tr(settings, 'hero_stat1', lang);
const stat2 = tr(settings, 'hero_stat2', lang);
const stat3 = tr(settings, 'hero_stat3', lang);
const S = window.ITOOTH_STRINGS[lang].hero;
return (
{eyebrow}
{title1}
{title2}
{Array.from({ length: 3 }).map((_, i) => (
• {lang === "ar" ? "صنع في العراق" : "Made in Iraq"}
• {lang === "ar" ? "خالي من البارابين" : "Paraben-free"}
• {lang === "ar" ? "مختبر إكلينيكياً" : "Clinically tested"}
• {lang === "ar" ? "فلورايد طبي" : "Medical-grade fluoride"}
• {lang === "ar" ? "تركيبة طبيعية" : "Natural formula"}
• {lang === "ar" ? "نباتي 100%" : "100% vegan"}
))}
);
}
function HeroVisual({ lang }) {
return (
{Array.from({ length: 24 }).map((_, i) => ())}
98%
{lang === "ar" ? "رضا المرضى" : "Patient satisfaction"}
★★★★★
{lang === "ar" ? "2,400+ تقييم" : "2,400+ reviews"}
);
}
// ============ Product card ============
function ProductCard({ product, lang, onAdd, onOpen }) {
const S = window.ITOOTH_STRINGS[lang].product;
const variants = product.variants || [];
const [variant, setVariant] = useState(variants[0] || null);
const [added, setAdded] = useState(false);
const cardRef = useRef(null);
useEffect(() => { setVariant(variants[0] || null); }, [product.id]);
const handleAdd = (e) => {
e?.stopPropagation();
const vname = variant ? tr(variant, 'name', lang) : '';
onAdd({
id: product.id,
slug: product.slug,
name: tr(product, 'name', lang),
image: product.image || product.images?.[0]?.url,
illus_key: product.illus_key,
name_ar: product.name_ar,
name_en: product.name_en,
price: product.price,
variant: vname,
}, cardRef.current);
setAdded(true);
setTimeout(() => setAdded(false), 1400);
};
const name = tr(product, 'name', lang);
const desc = tr(product, 'desc', lang);
const badge = tr(product, 'badge', lang);
const primaryImage = product.image || product.images?.[0]?.url;
return (
onOpen(product)}>
{primaryImage
?
})
:
}
{badge && (
{badge}
)}
({product.review_count || 0})
{name}
{desc}
{variants.length > 1 && (
e.stopPropagation()}>
{variants.slice(0, 4).map((v) => (
))}
)}
{fmt(product.price, lang)} {S.iqd}
{product.old_price && {fmt(product.old_price, lang)}}
);
}
// ============ Shop ============
function Shop({ lang, products, categories, onAdd, onOpen, initialCat, hideHead }) {
const S = window.ITOOTH_STRINGS[lang];
const [activeCat, setActiveCat] = useState(initialCat || "all");
const [sort, setSort] = useState("default");
const gridRef = useRef(null);
const allCats = [{ id: 'all', slug: 'all', name_ar: 'كل المنتجات', name_en: 'All products' }, ...categories];
useEffect(() => { if (initialCat) setActiveCat(initialCat); }, [initialCat]);
const filtered = useMemo(() => {
let list = activeCat === "all" ? products : products.filter((p) => p.category_slug === activeCat);
if (sort === 'price-asc') list = [...list].sort((a, b) => a.price - b.price);
else if (sort === 'price-desc') list = [...list].sort((a, b) => b.price - a.price);
else if (sort === 'rating') list = [...list].sort((a, b) => (b.rating || 0) - (a.rating || 0));
else if (sort === 'newest') list = [...list].sort((a, b) => (b.id - a.id));
return list;
}, [products, activeCat, sort]);
useEffect(() => {
const g = gridRef.current;
if (!g) return;
g.classList.remove('is-ready');
const id = requestAnimationFrame(() => {
requestAnimationFrame(() => g.classList.add('is-ready'));
});
return () => cancelAnimationFrame(id);
}, [filtered]);
return (
{!hideHead && (
{S.sections.shop}
{S.sections.shopTitle}
{S.sections.shopSub}
)}
{allCats.map((c) => (
))}
{filtered.map((p) => (
))}
);
}
// ============ Product slider ============
function ProductSlider({ slider, lang, onAdd, onOpen, dark }) {
const trackRef = useRef(null);
const title = tr(slider, 'title', lang);
const subtitle = tr(slider, 'subtitle', lang);
const scrollBy = (dir) => {
const t = trackRef.current;
if (!t) return;
const card = t.querySelector('.pcard');
const step = card ? card.offsetWidth + 24 : 280;
t.scrollBy({ left: dir * step * (lang === 'ar' ? -1 : 1), behavior: 'smooth' });
};
return (
{lang === 'ar' ? '⚡ مختار' : '⚡ Featured'}
{title}
{subtitle &&
{subtitle}
}
{slider.products.map((p) => (
))}
);
}
// ============ Product Detail Modal ============
function ProductDetail({ lang, product, open, onClose, onAdd }) {
const [active, setActive] = useState(0);
const [variant, setVariant] = useState(null);
const [qty, setQty] = useState(1);
const [tab, setTab] = useState('features');
const [full, setFull] = useState(null);
const sourceRef = useRef(null);
useEffect(() => {
setActive(0); setQty(1); setTab('features');
if (open && product) {
setFull(product); // instant show
window.ITOOTH_API.get('/api/products/id/' + product.id)
.then(setFull).catch(() => {});
}
}, [product?.id, open]);
useEffect(() => {
if (full?.variants?.length) setVariant(full.variants[0]);
}, [full?.variants?.length]);
useEffect(() => {
if (open) {
// Prevent body scroll while keeping the modal scrollable on mobile.
// overflow:hidden on body alone fails on iOS Safari — we also lock position with the current scroll preserved.
const y = window.scrollY;
document.body.dataset.lockY = String(y);
document.body.style.overflow = 'hidden';
} else {
const y = parseInt(document.body.dataset.lockY || '0', 10);
document.body.style.overflow = '';
delete document.body.dataset.lockY;
if (y) window.scrollTo(0, y);
}
return () => { document.body.style.overflow = ''; };
}, [open]);
if (!product) return null;
const p = full || product;
const P = window.ITOOTH_STRINGS[lang].product;
const CH = window.ITOOTH_STRINGS[lang];
const images = [
...(p.image ? [p.image] : []),
...((p.images || []).map((i) => i.url)),
].filter((v, i, arr) => arr.indexOf(v) === i);
const activeImg = images[active] || null;
const name = tr(p, 'name', lang);
const desc = tr(p, 'desc', lang);
const longDesc = tr(p, 'long_desc', lang) || desc;
const features = (tr(p, 'features', lang) || '').split('|').map((s) => s.trim()).filter(Boolean);
const usage = tr(p, 'usage', lang);
const ingredients = tr(p, 'ingredients', lang);
const badge = tr(p, 'badge', lang);
const catName = lang === 'ar' ? (p.category_name_ar || '') : (p.category_name_en || '');
const handleAdd = (close = false) => {
const vname = variant ? tr(variant, 'name', lang) : '';
const src = document.querySelector('.pd__main') || sourceRef.current;
for (let i = 0; i < qty; i++) {
setTimeout(() => {
onAdd({
id: p.id,
slug: p.slug,
name,
image: p.image || p.images?.[0]?.url,
illus_key: p.illus_key,
name_ar: p.name_ar, name_en: p.name_en,
price: p.price,
variant: vname,
}, i === 0 ? src : null);
}, i * 150);
}
if (close) setTimeout(() => onClose(), 900);
};
const submitReview = async (payload) => {
try {
await window.ITOOTH_API.post(`/api/products/${p.id}/reviews`, payload);
window.ITOOTH_FX.toast(lang === 'ar' ? 'شكراً! تم نشر تقييمك' : 'Thanks! Review posted');
const fresh = await window.ITOOTH_API.get('/api/products/id/' + p.id);
setFull(fresh);
} catch (e) { window.ITOOTH_FX.toast(e.message || 'Error', '✗', true); }
};
return (
{ if (e.target === e.currentTarget) onClose(); }}>
{badge &&
{badge}}
{activeImg
?
})
:
}
{images.length > 1 && (
{images.map((img, i) => (
))}
)}
{catName &&
{catName}}
{name}
({p.review_count || 0} {lang === 'ar' ? 'تقييم' : 'reviews'})
{p.sku && SKU: {p.sku}}
{fmt(p.price, lang)} {P.iqd}
{p.old_price && (
<>
{fmt(p.old_price, lang)}
−{Math.round((1 - p.price / p.old_price) * 100)}%
>
)}
{longDesc}
{p.variants?.length > 0 && (
{P.variant}
{p.variants.map((v) => (
))}
)}
{lang === 'ar' ? 'الكمية' : 'Quantity'}
{qty}
{lang === 'ar' ? 'متوفر في المخزون' : 'In stock'}
{features.length > 0 && }
{usage && }
{ingredients && }
{tab === 'features' && (
features.length ? (
{features.map((f, i) => - {f}
)}
) :
{longDesc}
)}
{tab === 'usage' &&
{usage}
}
{tab === 'ingredients' &&
{ingredients}
}
{tab === 'reviews' && (
{(p.reviews || []).length === 0 &&
{lang === 'ar' ? 'لا توجد تقييمات بعد. كن أول من يقيّم!' : 'No reviews yet. Be the first!'}
}
{(p.reviews || []).map((r) => (
-
{r.reviewer_name}
{'★'.repeat(r.rating)}{'☆'.repeat(5 - r.rating)}
{tr(r, 'comment', lang) || r.comment_ar || r.comment_en}
))}
)}
);
}
function ReviewForm({ lang, onSubmit }) {
const [name, setName] = useState('');
const [rating, setRating] = useState(5);
const [comment, setComment] = useState('');
const submit = async (e) => {
e.preventDefault();
if (!name.trim() || !comment.trim()) return;
await onSubmit({ reviewer_name: name, rating, [lang === 'ar' ? 'comment_ar' : 'comment_en']: comment });
setName(''); setRating(5); setComment('');
};
return (
);
}
// ============ About ============
function About({ lang, settings, founders }) {
const S = window.ITOOTH_STRINGS[lang].sections;
const title = tr(settings, 'about_title', lang) || (lang === 'ar' ? 'قصتنا' : 'Our story');
const body = tr(settings, 'about_body', lang) || (lang === 'ar'
? 'itooth علامة عناية بالفم عراقية، طُوّرت بإشراف أطباء أسنان لتقدّم منتجات بجودة طبية وأسعار يصلها الجميع.'
: 'itooth is an Iraqi oral-care brand, developed by dentists to deliver clinical-grade products at accessible prices.');
const hasFounders = (founders || []).length > 0;
return (
{hasFounders && (
{founders.slice(0, 3).map((f, i) => (
{f.image ? (
<>
{tr(f, 'name', lang)}
>
) : (
<>
{f.initials}
DDS
>
)}
))}
)}
{S.aboutEyebrow}
{title}
{body}
{hasFounders && (
{founders.map((f) => (
-
{tr(f, 'name', lang)}
{tr(f, 'role', lang)}
))}
)}
6+{lang === "ar" ? "سنوات ممارسة" : "Years practice"}
12k+{lang === "ar" ? "مريض عالجوه" : "Patients treated"}
9{lang === "ar" ? "منتجات" : "Products"}
);
}
function Routine({ lang, steps }) {
const S = window.ITOOTH_STRINGS[lang];
return (
{S.sections.dailyRoutine}
{S.sections.routineTitle}
{steps.map((s, i) => (
{s.step_num}
{tr(s, 'title', lang)}
{tr(s, 'desc', lang)}
{i < steps.length - 1 &&
}
))}
);
}
function Bundle({ lang, products, onAdd }) {
const S = window.ITOOTH_STRINGS[lang].sections;
const bundleSlugs = ["brush-soft", "paste-mint", "tongue-scraper", "mouthwash"];
const items = bundleSlugs.map((slug) => products.find((p) => p.slug === slug)).filter(Boolean);
if (items.length < 2) return null;
const total = items.reduce((s, p) => s + (p.price || 0), 0);
const bundlePrice = Math.round(total * 0.82);
const cardRef = useRef(null);
const handleAdd = () => {
onAdd({
id: -1, slug: 'starter-bundle',
name: lang === 'ar' ? 'باقة البداية' : 'Starter Bundle',
name_ar: 'باقة البداية', name_en: 'Starter Bundle',
image: null, illus_key: 'bundle',
price: bundlePrice,
variant: lang === 'ar' ? 'الطقم الكامل' : 'Full set',
}, cardRef.current);
};
return (
{S.offer}
{S.bundleTitle}
{S.bundleDesc}
{fmt(bundlePrice, lang)} {lang === "ar" ? "د.ع" : "IQD"}
{fmt(total, lang)}
−18%
{items.slice(0, 4).map((p, i) => (
{p.image ?
})
:
}
))}
);
}
function PostCover({ idx }) {
const palettes = [
{ bg: "#E9F6EE", a: "var(--brand)" },
{ bg: "#111", a: "var(--brand)" },
{ bg: "#F2F6F2", a: "var(--brand-deep)" },
];
const p = palettes[idx % palettes.length];
return (
);
}
function Footer({ lang, settings, logoUrl, navigate }) {
const S = window.ITOOTH_STRINGS[lang].footer;
const tagline = tr(settings, 'footer_tagline', lang);
const pay = tr(settings, 'payment', lang);
const city = tr(settings, 'contact_city', lang);
const phone = settings.contact_phone || '';
const waLink = phone ? `https://wa.me/${phone.replace(/[^\d]/g, '')}` : '#';
const links = lang === 'ar'
? {
col1: [ ['فرش الأسنان','/shop/brush'], ['المعاجين','/shop/paste'], ['التبييض','/shop/whitening'], ['الغسول','/shop/rinse'], ['الأدوات','/shop/tools'] ],
col2: [ ['من نحن','/about'], ['المدونة','/blog'], ['تواصل معنا','/contact'], ['حسابي','/account'] ],
col3: [ ['واتساب', waLink, true], ['البريد الإلكتروني', 'mailto:' + (settings.contact_email || ''), true], ['الهاتف', 'tel:' + phone, true] ],
}
: {
col1: [ ['Toothbrushes','/shop/brush'], ['Pastes','/shop/paste'], ['Whitening','/shop/whitening'], ['Rinse','/shop/rinse'], ['Tools','/shop/tools'] ],
col2: [ ['About','/about'], ['Journal','/blog'], ['Contact','/contact'], ['My account','/account'] ],
col3: [ ['WhatsApp', waLink, true], ['Email', 'mailto:' + (settings.contact_email || ''), true], ['Phone', 'tel:' + phone, true] ],
};
const NavLink = ({ to, children, external }) => external
?
{children}
:
{ e.preventDefault(); navigate(to); }}>{children};
return (
);
}
// ============ Cart ============
function Cart({ lang, open, onClose, items, setItems, onCheckout }) {
const S = window.ITOOTH_STRINGS[lang].cart;
const P = window.ITOOTH_STRINGS[lang].product;
const subtotal = items.reduce((s, it) => s + it.price * it.qty, 0);
const updateQty = (idx, delta) => {
setItems((prev) => {
const next = [...prev];
next[idx] = { ...next[idx], qty: Math.max(1, next[idx].qty + delta) };
return next;
});
};
const remove = (idx) => {
window.ITOOTH_FX.play('remove');
setItems((prev) => prev.filter((_, i) => i !== idx));
};
return (
<>
>
);
}
// ============ Checkout ============
function Checkout({ lang, open, onClose, items, onOrdered, customer, onAccountCreated, navigate }) {
const S = window.ITOOTH_STRINGS[lang].checkout;
const P = window.ITOOTH_STRINGS[lang].product;
const [form, setForm] = useState({ name: '', phone: '', address: '', notes: '', password: '' });
const [busy, setBusy] = useState(false);
const [success, setSuccess] = useState(null);
useEffect(() => {
if (open) {
setSuccess(null);
setForm({
name: customer?.name || '',
phone: customer?.phone || '',
address: [customer?.address, customer?.city].filter(Boolean).join(' — ') || '',
notes: '',
password: '',
});
}
}, [open, customer]);
const total = items.reduce((s, it) => s + it.price * it.qty, 0);
const isGuest = !customer;
const submit = async (e) => {
e.preventDefault();
if (isGuest && (!form.password || form.password.length < 6)) {
window.ITOOTH_FX.toast(lang === 'ar' ? 'كلمة المرور قصيرة (6 أحرف على الأقل)' : 'Password too short', '✗', true);
return;
}
setBusy(true);
try {
const res = await window.ITOOTH_API.createOrder({
customer_name: form.name,
customer_phone: form.phone,
customer_address: form.address,
customer_notes: form.notes,
password: isGuest ? form.password : undefined,
items: items.map((it) => ({
product_id: it.id && it.id > 0 ? it.id : null,
name: tr(it, 'name', lang) || it.name,
variant: it.variant || '',
price: it.price,
qty: it.qty,
})),
});
window.ITOOTH_FX.play('success');
setSuccess(res);
onOrdered && onOrdered(res);
if (res.customer && onAccountCreated) onAccountCreated(res.customer);
} catch (err) { window.ITOOTH_FX.toast(err.message || 'Error', '✗', true); }
finally { setBusy(false); }
};
return (
{ if (e.target === e.currentTarget) onClose(); }}>
{success ? (
{S.success}
{S.successDesc}
{lang === 'ar' ? 'رقم الطلب' : 'Order #'}: {success.id}
{success.customer && success.account_created && (
)}
{success.customer && (
)}
) : (
)}
);
}
// ============ Search overlay ============
function SearchModal({ lang, open, onClose, products, onOpenProduct }) {
const [q, setQ] = useState('');
const inputRef = useRef(null);
useEffect(() => { if (open) { setQ(''); setTimeout(() => inputRef.current?.focus(), 50); } }, [open]);
const results = useMemo(() => {
if (!q) return products.slice(0, 6);
const qq = q.toLowerCase();
return products.filter((p) => (p.name_ar + ' ' + p.name_en + ' ' + (p.desc_ar || '') + ' ' + (p.desc_en || '')).toLowerCase().includes(qq)).slice(0, 10);
}, [q, products]);
const P = window.ITOOTH_STRINGS[lang].product;
return (
<>
setQ(e.target.value)} />
{results.length === 0 ? (
{lang === 'ar' ? 'لا نتائج مطابقة' : 'No matches'}
) : results.map((p) => (
{ onOpenProduct(p); onClose(); }}>
{p.image ?
})
:
}
{tr(p, 'name', lang)}
{tr(p, 'desc', lang)?.slice(0, 60)}
{fmt(p.price, lang)} {P.iqd}
))}
>
);
}
// ============ Product Page (full-page replacement for the modal) ============
function ProductPage({ lang, slug, products, onAdd, navigate }) {
const S = window.ITOOTH_STRINGS[lang];
const P = S.product;
const [product, setProduct] = useState(() => products.find((p) => p.slug === slug) || null);
const [active, setActive] = useState(0);
const [variant, setVariant] = useState(null);
const [qty, setQty] = useState(1);
const [tab, setTab] = useState('features');
const [related, setRelated] = useState([]);
useEffect(() => {
setActive(0); setQty(1); setTab('features');
window.ITOOTH_API.get('/api/products/' + slug)
.then(setProduct)
.catch(() => setProduct(null));
}, [slug]);
useEffect(() => {
if (product?.variants?.length) setVariant(product.variants[0]);
}, [product?.id]);
useEffect(() => {
if (!product || !products?.length) return setRelated([]);
// prefer same-category matches; fall back to any other products so the
// "you may also like" section always has something to show
let r = products.filter((p) => p.id !== product.id && p.category_slug && p.category_slug === product.category_slug);
if (r.length < 4) {
const extras = products.filter((p) => p.id !== product.id && !r.find((x) => x.id === p.id));
r = [...r, ...extras];
}
setRelated(r.slice(0, 4));
}, [product?.id, products]);
if (!product) {
return (
{lang === 'ar' ? 'المنتج غير موجود' : 'Product not found'}
);
}
const p = product;
const images = [
...(p.image ? [p.image] : []),
...((p.images || []).map((i) => i.url)),
].filter((v, i, arr) => arr.indexOf(v) === i);
const activeImg = images[active] || null;
const name = tr(p, 'name', lang);
const desc = tr(p, 'desc', lang);
const longDesc = tr(p, 'long_desc', lang) || desc;
const features = (tr(p, 'features', lang) || '').split('|').map((s) => s.trim()).filter(Boolean);
const usage = tr(p, 'usage', lang);
const ingredients = tr(p, 'ingredients', lang);
const badge = tr(p, 'badge', lang);
const catName = lang === 'ar' ? (p.category_name_ar || '') : (p.category_name_en || '');
const handleAdd = (close = false) => {
const vname = variant ? tr(variant, 'name', lang) : '';
const src = document.querySelector('.pp__main');
for (let i = 0; i < qty; i++) {
setTimeout(() => {
onAdd({
id: p.id, slug: p.slug, name,
image: p.image || p.images?.[0]?.url, illus_key: p.illus_key,
name_ar: p.name_ar, name_en: p.name_en, price: p.price,
variant: vname,
}, i === 0 ? src : null);
}, i * 150);
}
};
const submitReview = async (payload) => {
try {
await window.ITOOTH_API.post(`/api/products/${p.id}/reviews`, payload);
window.ITOOTH_FX.toast(lang === 'ar' ? 'شكراً! تم نشر تقييمك' : 'Thanks!');
const fresh = await window.ITOOTH_API.get('/api/products/' + slug);
setProduct(fresh);
} catch (e) { window.ITOOTH_FX.toast(e.message || 'Error', '✗', true); }
};
return (
{badge &&
{badge}}
{activeImg
?
})
:
}
{images.length > 1 && (
{images.map((img, i) => (
))}
)}
{catName &&
{catName}}
{name}
{fmt(p.price, lang)} {P.iqd}
{p.old_price && (
<>
{fmt(p.old_price, lang)} {P.iqd}
−{Math.round((1 - p.price / p.old_price) * 100)}%
>
)}
{longDesc}
{p.variants?.length > 0 && (
{P.variant} {variant ? — {tr(variant, 'name', lang)} : null}
{p.variants.map((v) => (
))}
)}
{lang === 'ar' ? 'الكمية' : 'Quantity'}
{qty}
{lang === 'ar' ? 'متوفر في المخزون' : 'In stock'}
- 🚚{lang === 'ar' ? 'توصيل خلال 1–3 أيام' : 'Delivery in 1–3 days'}
- 💰{lang === 'ar' ? 'الدفع عند الاستلام' : 'Cash on delivery'}
- 🦷{lang === 'ar' ? 'مطوّر بإشراف أطباء أسنان' : 'Dentist-developed'}
{features.length > 0 && }
{usage && }
{ingredients && }
{tab === 'features' && (
features.length ? (
{features.map((f, i) => - {f}
)}
) :
{longDesc}
)}
{tab === 'usage' &&
{usage}
}
{tab === 'ingredients' &&
{ingredients}
}
{tab === 'reviews' && (
{(p.reviews || []).length === 0 && (
{lang === 'ar' ? 'لا توجد تقييمات بعد. كن أول من يقيّم!' : 'No reviews yet — be the first!'}
)}
{(p.reviews || []).map((r) => (
-
{r.reviewer_name}
{'★'.repeat(r.rating)}{'☆'.repeat(5 - r.rating)}
{tr(r, 'comment', lang) || r.comment_ar || r.comment_en}
))}
)}
{related.length > 0 && (
{lang === 'ar' ? 'منتجات قد تعجبك' : 'You may also like'}
{related.map((rp) => (
navigate('/product/' + np.slug)} />
))}
)}
);
}
// ============ App root ============
// ============ Router ============
const stripBase = (p) => {
const b = window.ITOOTH_BASE || '';
if (b && p.startsWith(b)) return p.slice(b.length) || '/';
return p;
};
const withBase = (p) => (window.ITOOTH_BASE || '') + (p.startsWith('/') ? p : '/' + p);
function useRouter() {
const [route, setRoute] = useState(() => stripBase(window.location.pathname) || '/');
const scrollTop = () => {
// instant scroll — smooth scroll loses the race against React rerender,
// leaving the new page appearing to start at the previous page's offset (often the footer)
window.scrollTo(0, 0);
document.documentElement.scrollTop = 0;
document.body.scrollTop = 0;
};
const navigate = useCallback((to, replace = false) => {
const target = withBase(to);
if (replace) window.history.replaceState({}, '', target);
else window.history.pushState({}, '', target);
setRoute(to);
scrollTop();
}, []);
useEffect(() => {
const onPop = () => { setRoute(stripBase(window.location.pathname) || '/'); scrollTop(); };
window.addEventListener('popstate', onPop);
return () => window.removeEventListener('popstate', onPop);
}, []);
// Re-assert scroll on every route change (covers async content loads pushing layout)
useEffect(() => { scrollTop(); }, [route]);
return { route, navigate };
}
// ============ Page: Home ============
function HomePage({ lang, settings, sliders, products, categories, founders, routineSteps, blogPosts, onAdd, onOpenProduct, onOpenShop, navigate }) {
return (
<>
{sliders.length > 0 && sliders.slice(0, 2).map((slider, i) => (
))}
{products.length > 0 && (
)}
{products.length > 3 &&
}
{routineSteps.length > 0 &&
}
{blogPosts.length > 0 &&
}
>
);
}
function AboutPreview({ lang, settings, founders, navigate }) {
const S = window.ITOOTH_STRINGS[lang];
return (
<>
>
);
}
function BlogPreview({ lang, posts, navigate }) {
const S = window.ITOOTH_STRINGS[lang];
return (
{posts.map((post, i) => (
navigate('/blog/' + post.slug)}>
{post.image ?
})
:
}
{tr(post, 'tag', lang)}
{tr(post, 'title', lang)}
{tr(post, 'excerpt', lang)}
{post.read_time} {S.minRead}
))}
);
}
// ============ Page: Shop ============
function ShopPage({ lang, products, categories, onAdd, onOpenProduct, catSlug }) {
const S = window.ITOOTH_STRINGS[lang];
return (
{S.sections.shop}
{S.sections.shopTitle}
{S.sections.shopSub}
);
}
// ============ Page: About ============
function AboutPage({ lang, settings, founders }) {
const S = window.ITOOTH_STRINGS[lang];
return (
{S.sections.aboutEyebrow}
{tr(settings, 'about_title', lang) || (lang === 'ar' ? 'قصتنا' : 'Our story')}
);
}
// ============ Page: Blog list ============
function BlogListPage({ lang, posts, navigate }) {
const S = window.ITOOTH_STRINGS[lang];
return (
{S.sections.journal}
{S.sections.blogTitle}
{S.sections.blogSub}
{posts.map((post, i) => (
navigate('/blog/' + post.slug)}>
{post.image ?
})
:
}
{tr(post, 'tag', lang)}
{tr(post, 'title', lang)}
{tr(post, 'excerpt', lang)}
{post.read_time} {S.minRead}
))}
);
}
// ============ Page: Blog single ============
function BlogPostPage({ lang, slug, posts, navigate }) {
const S = window.ITOOTH_STRINGS[lang];
const post = posts.find((p) => p.slug === slug);
if (!post) return (
{lang === 'ar' ? 'المقال غير موجود' : 'Article not found'}
);
const title = tr(post, 'title', lang);
const excerpt = tr(post, 'excerpt', lang);
const content = tr(post, 'content', lang);
const tag = tr(post, 'tag', lang);
const idx = posts.indexOf(post);
return (
{post.image ?
})
:
}
{tag && {tag}}
{post.read_time} {S.minRead}
{title}
{excerpt &&
{excerpt}
}
{content ? (
{content}
) : (
{lang === 'ar' ? 'لم يُضف محتوى للمقال بعد.' : 'No article content yet.'}
)}
);
}
// ============ Page: Contact ============
function ContactPage({ lang, settings }) {
const S = window.ITOOTH_STRINGS[lang].contact;
const [form, setForm] = useState({ name: '', email: '', phone: '', subject: '', message: '' });
const [busy, setBusy] = useState(false);
const [sent, setSent] = useState(false);
const phone = settings.contact_phone || '';
const email = settings.contact_email || '';
const city = tr(settings, 'contact_city', lang);
const waLink = phone ? `https://wa.me/${phone.replace(/[^\d]/g, '')}` : '#';
const submit = async (e) => {
e.preventDefault();
setBusy(true);
try {
await window.ITOOTH_API.post('/api/contact', form);
setSent(true);
window.ITOOTH_FX.play('success');
setForm({ name: '', email: '', phone: '', subject: '', message: '' });
} catch (err) { window.ITOOTH_FX.toast(err.message || 'Error', '✗', true); }
finally { setBusy(false); }
};
return (
{lang === 'ar' ? 'تواصل' : 'Contact'}
{S.title}
{S.sub}
);
}
// ============ Page: Customer login ============
function LoginPage({ lang, navigate, onLoggedIn }) {
const S = window.ITOOTH_STRINGS[lang].account;
const [form, setForm] = useState({ phone: '', password: '' });
const [err, setErr] = useState('');
const [busy, setBusy] = useState(false);
const submit = async (e) => {
e.preventDefault();
setErr(''); setBusy(true);
try {
const res = await window.ITOOTH_API.post('/api/customer/login', form);
onLoggedIn(res.customer || { id: res.id, phone: res.phone, name: res.name });
navigate('/account');
window.ITOOTH_FX.play('success');
} catch (e2) { setErr(e2.message || 'Error'); }
finally { setBusy(false); }
};
return (
{S.welcomeBack}
{lang === 'ar' ? 'سجّل دخولك لمتابعة طلباتك' : 'Sign in to track your orders'}
{err &&
{err}
}
{S.dontHaveAccount} { e.preventDefault(); navigate('/account/signup'); }}>{S.signup}
);
}
// ============ Page: Customer signup ============
function SignupPage({ lang, navigate, onLoggedIn }) {
const S = window.ITOOTH_STRINGS[lang].account;
const [form, setForm] = useState({ name: '', phone: '', password: '', password2: '' });
const [err, setErr] = useState('');
const [busy, setBusy] = useState(false);
const submit = async (e) => {
e.preventDefault();
setErr('');
if (form.password !== form.password2) return setErr(S.passwordMismatch);
if (form.password.length < 6) return setErr(S.passwordShort);
setBusy(true);
try {
const res = await window.ITOOTH_API.post('/api/customer/register', {
phone: form.phone, password: form.password, name: form.name,
});
onLoggedIn(res.customer || { id: res.id, phone: res.phone, name: res.name });
navigate('/account');
window.ITOOTH_FX.play('success');
} catch (e2) { setErr(e2.message || 'Error'); }
finally { setBusy(false); }
};
return (
);
}
// ============ Page: Customer account ============
function AccountPage({ lang, customer, onLogout, navigate }) {
const S = window.ITOOTH_STRINGS[lang].account;
const P = window.ITOOTH_STRINGS[lang].product;
const [tab, setTab] = useState('profile');
const [profile, setProfile] = useState(customer);
const [orders, setOrders] = useState([]);
const [busy, setBusy] = useState(false);
const [pwForm, setPwForm] = useState({ current: '', next: '', next2: '' });
useEffect(() => { setProfile(customer); }, [customer?.id]);
useEffect(() => {
window.ITOOTH_API.get('/api/customer/orders').then(setOrders).catch(() => {});
}, []);
const saveProfile = async (e) => {
e.preventDefault();
setBusy(true);
try {
await window.ITOOTH_API.put('/api/customer/me', profile);
window.ITOOTH_FX.toast(lang === 'ar' ? 'تم الحفظ' : 'Saved');
} catch (err) { window.ITOOTH_FX.toast(err.message, '✗', true); }
finally { setBusy(false); }
};
const changePw = async (e) => {
e.preventDefault();
if (pwForm.next !== pwForm.next2) return window.ITOOTH_FX.toast(S.passwordMismatch, '✗', true);
if (pwForm.next.length < 6) return window.ITOOTH_FX.toast(S.passwordShort, '✗', true);
try {
await window.ITOOTH_API.post('/api/customer/change-password', { current: pwForm.current, next: pwForm.next });
window.ITOOTH_FX.toast(lang === 'ar' ? 'تم تغيير كلمة المرور' : 'Password changed');
setPwForm({ current: '', next: '', next2: '' });
} catch (err) { window.ITOOTH_FX.toast(err.message || 'Error', '✗', true); }
};
const logout = async () => {
await window.ITOOTH_API.post('/api/customer/logout');
onLogout();
navigate('/');
window.ITOOTH_FX.toast(S.loggedOut);
};
return (
{S.title}
{lang === 'ar' ? 'أهلاً' : 'Hi'}, {customer.name}
);
}
function App() {
const [lang, setLang] = useState(localStorage.getItem('itooth_lang') || 'ar');
const [settings, setSettings] = useState({});
const [categories, setCategories] = useState([]);
const [products, setProducts] = useState([]);
const [sliders, setSliders] = useState([]);
const [blogPosts, setBlogPosts] = useState([]);
const [founders, setFounders] = useState([]);
const [announcements, setAnnouncements] = useState([]);
const [routineSteps, setRoutineSteps] = useState([]);
const [cart, setCart] = useState([]);
const [cartOpen, setCartOpen] = useState(false);
const [checkoutOpen, setCheckoutOpen] = useState(false);
const [scrolled, setScrolled] = useState(false);
const [showTop, setShowTop] = useState(false);
const [soundOn, setSoundOn] = useState(window.ITOOTH_FX?.soundOn ?? true);
const [searchOpen, setSearchOpen] = useState(false);
const [pdProduct, setPdProduct] = useState(null);
const [customer, setCustomer] = useState(null);
const { route, navigate } = useRouter();
useEffect(() => {
(async () => {
try {
const [s, c, p, sl, b, f, a, r] = await Promise.all([
window.ITOOTH_API.getSettings(),
window.ITOOTH_API.getCategories(),
window.ITOOTH_API.getProducts(),
window.ITOOTH_API.getSliders(),
window.ITOOTH_API.getBlog(),
window.ITOOTH_API.getFounders(),
window.ITOOTH_API.getAnnouncements(),
window.ITOOTH_API.getRoutine(),
]);
setSettings(s); setCategories(c); setProducts(p); setSliders(sl);
setBlogPosts(b); setFounders(f); setAnnouncements(a); setRoutineSteps(r);
applyShade(s.green_shade || '#2D8F5F');
} catch (err) { console.error(err); }
})();
}, []);
useEffect(() => {
document.documentElement.lang = lang;
document.documentElement.dir = lang === 'ar' ? 'rtl' : 'ltr';
document.body.setAttribute('dir', lang === 'ar' ? 'rtl' : 'ltr');
localStorage.setItem('itooth_lang', lang);
}, [lang]);
useEffect(() => {
const onScroll = () => { setScrolled(window.scrollY > 20); setShowTop(window.scrollY > 500); };
window.addEventListener('scroll', onScroll);
return () => window.removeEventListener('scroll', onScroll);
}, []);
useEffect(() => {
const h = (e) => setSoundOn(e.detail);
document.addEventListener('itooth:sound', h);
return () => document.removeEventListener('itooth:sound', h);
}, []);
useEffect(() => {
const onKey = (e) => {
if (e.key === 'Escape') { setPdProduct(null); setSearchOpen(false); }
if ((e.ctrlKey || e.metaKey) && e.key === 'k') { e.preventDefault(); setSearchOpen(true); }
};
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, []);
// Fetch logged-in customer on mount
useEffect(() => {
window.ITOOTH_API.get('/api/customer/me').then(setCustomer).catch(() => setCustomer(null));
}, []);
const applyShade = (hex) => {
const shades = {
"#0F5132": { deep: "#0F5132", base: "#0F5132", soft: "#BAD9C4", pale: "#E8F2EC", ink: "#0A2F1D" },
"#1B6A44": { deep: "#1B6A44", base: "#1B6A44", soft: "#C7E6D3", pale: "#F1F8F4", ink: "#0E3A26" },
"#2D8F5F": { deep: "#1B6A44", base: "#2D8F5F", soft: "#C7E6D3", pale: "#F1F8F4", ink: "#0E3A26" },
"#3AA66D": { deep: "#1F7A4C", base: "#3AA66D", soft: "#CDEBD9", pale: "#F2FAF5", ink: "#113F27" },
"#4CAF50": { deep: "#2E8B43", base: "#4CAF50", soft: "#D4EBD5", pale: "#F3FAF4", ink: "#153A1A" },
"#00A86B": { deep: "#006B45", base: "#00A86B", soft: "#B8E6D0", pale: "#EAF7F0", ink: "#003823" },
};
const s = shades[hex] || shades["#2D8F5F"];
const r = document.documentElement.style;
r.setProperty('--brand', s.base);
r.setProperty('--brand-deep', s.deep);
r.setProperty('--brand-soft', s.soft);
r.setProperty('--brand-pale', s.pale);
r.setProperty('--brand-ink', s.ink);
};
const addToCart = (item, sourceEl) => {
setCart((prev) => {
const existing = prev.findIndex(p => p.id === item.id && p.variant === item.variant);
if (existing >= 0) {
const next = [...prev];
next[existing] = { ...next[existing], qty: next[existing].qty + 1, _isNew: true };
return next;
}
return [...prev, { ...item, qty: 1, _isNew: true }];
});
setTimeout(() => setCart((prev) => prev.map((p) => ({ ...p, _isNew: false }))), 900);
if (sourceEl) window.ITOOTH_FX.flyToCart(sourceEl, item.image);
else window.ITOOTH_FX.play('add');
window.ITOOTH_FX.toast(window.ITOOTH_STRINGS[lang].addedToast);
// pulse the floating cart fab so the user notices the count grew
document.getElementById('cart-fab')?.classList.add('is-pulse');
setTimeout(() => document.getElementById('cart-fab')?.classList.remove('is-pulse'), 800);
};
const cartCount = cart.reduce((s, it) => s + it.qty, 0);
const handleCheckout = () => { setCartOpen(false); setTimeout(() => setCheckoutOpen(true), 280); };
const onOrdered = () => { setCart([]); };
// Route dispatch
let page;
const r = route || '/';
const openProduct = (p) => navigate('/product/' + p.slug);
if (r === '/' || r === '') {
page =
navigate('/shop')} navigate={navigate} />;
} else if (r.startsWith('/product/')) {
page = ;
} else if (r.startsWith('/shop')) {
const catSlug = r === '/shop' ? 'all' : r.split('/')[2] || 'all';
page = ;
} else if (r === '/about') {
page = ;
} else if (r === '/blog') {
page = ;
} else if (r.startsWith('/blog/')) {
page = ;
} else if (r === '/contact') {
page = ;
} else if (r === '/account/login') {
page = ;
} else if (r === '/account/signup') {
page = ;
} else if (r.startsWith('/account')) {
page = customer
? setCustomer(null)} />
: ;
} else {
page = navigate('/shop')} navigate={navigate} />;
}
return (
<>