// ========================================================== // 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 (
{tr(msg, 'message', lang)}
); } // ============ Brand mark (logo) ============ function BrandMark({ size = 36, logoUrl, onDark }) { if (logoUrl) { return ( itooth ); } return ( ); } function Stars({ value = 5, size = 14 }) { const v = Math.max(0, Math.min(5, value)); const full = Math.floor(v); const half = v - full >= 0.5; const stars = []; for (let i = 0; i < 5; i++) { if (i < full) stars.push(); else if (i === full && half) stars.push(); else stars.push(); } return {stars}; } // ============ Nav ============ function Nav({ lang, setLang, cartCount, onOpenCart, onOpenSearch, scrolled, logoUrl, route, navigate, customer, soundOn, onToggleSound }) { const S = window.ITOOTH_STRINGS[lang]; const [menuOpen, setMenuOpen] = useState(false); useEffect(() => { if (menuOpen) document.body.style.overflow = 'hidden'; else document.body.style.overflow = ''; return () => { document.body.style.overflow = ''; }; }, [menuOpen]); useEffect(() => { const onResize = () => { if (window.innerWidth > 900) setMenuOpen(false); }; window.addEventListener('resize', onResize); return () => window.removeEventListener('resize', onResize); }, []); const go = (path) => { setMenuOpen(false); navigate(path); }; const accountHref = customer ? '/account' : '/account/login'; const isActive = (p) => route === p || (p !== '/' && route.startsWith(p)); return ( <>
{ e.preventDefault(); go('/'); }}> itooth®
{/* Mobile menu drawer */}
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}

{stat1}
{stat2}
{stat3}
{S.cta2}
{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) => ())}
mouthwash
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 ? {name} : } {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 ? {name} : }
{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 (

{lang === 'ar' ? 'أضف تقييمك' : 'Add your review'}

setName(e.target.value)} />
{[1, 2, 3, 4, 5].map((n) => ( = n ? 'is-on' : ''} onClick={() => setRating(n)}>★ ))}