/* ============================================================ GoToMorocco — client multi-page app ============================================================ */ const ACCENTS = { sand: { a: 'oklch(0.81 0.078 80)', d: 'oklch(0.72 0.092 72)', on: 'oklch(0.18 0.02 70)' }, terracotta:{ a: 'oklch(0.68 0.12 46)', d: 'oklch(0.60 0.13 42)', on: 'oklch(0.98 0.01 70)' }, olive: { a: 'oklch(0.76 0.07 120)', d: 'oklch(0.68 0.08 122)', on: 'oklch(0.18 0.02 120)' }, }; function applyAccent(name) { const a = ACCENTS[name] || ACCENTS.sand; const r = document.documentElement.style; r.setProperty('--accent', a.a); r.setProperty('--accent-deep', a.d); r.setProperty('--on-accent', a.on); } function pickText(obj, lang, fallback) { return window.ATLAS.pick && obj ? (window.ATLAS.pick(obj, lang) || fallback) : fallback; } function serviceTitle(lang, s) { return pickText(s.t, lang, T(lang, 'svc_' + s.id + '_t')); } function serviceDesc(lang, s) { return pickText(s.d, lang, T(lang, 'svc_' + s.id + '_d')); } function vehicleTitle(lang, v) { return pickText(v.t, lang, T(lang, 'veh_' + v.id + '_t')); } function vehicleDesc(lang, v) { return pickText(v.d, lang, T(lang, 'veh_' + v.id + '_d')); } /* ---------------- router ---------------- */ function useRoute() { const parse = () => (window.location.hash.replace(/^#\/?/, '') || 'home').split('?')[0]; const [route, setRoute] = useState(parse()); useEffect(() => { const h = () => { setRoute(parse()); window.scrollTo(0, 0); }; window.addEventListener('hashchange', h); return () => window.removeEventListener('hashchange', h); }, []); const navigate = (to) => { window.location.hash = '/' + to; }; return [route, navigate]; } /* ---------------- Header ---------------- */ function Header({ lang, setLang, route, navigate, user, onLogin }) { const [menu, setMenu] = useState(false); const [acct, setAcct] = useState(false); const links = [ { id: 'home', label: 'nav_home' }, { id: 'services', label: 'nav_services' }, { id: 'fleet', label: 'nav_fleet' }, { id: 'events', label: 'nav_events' }, { id: 'news', label: 'nav_news' }, { id: 'track', label: 'nav_track' }, ]; const go = (id) => { navigate(id); setMenu(false); }; return (
{T(lang, 'nav_admin')} {user ? (
{acct && (
setAcct(false)}>
)}
) : ( )}
{menu && (
{links.map(l => setMenu(false)} className={route === l.id ? 'active' : ''}>{T(lang, l.label)})} setMenu(false)}>{T(lang, 'nav_book')} {user ? setMenu(false)}>{T(lang, 'ac_my_requests')} : } {T(lang, 'nav_admin')}
)}
); } /* ---------------- Hero ---------------- */ function Hero({ lang, variant, navigate }) { const titleKey = variant === 'split' ? 'hero_title_b' : variant === 'minimal' ? 'hero_title_c' : 'hero_title_a'; const stats = [ { end: 14, suffix: '', lbl: T(lang, 'stat_years') }, { end: 40, suffix: '+', lbl: T(lang, 'stat_vehicles') }, { end: 25, suffix: '', lbl: T(lang, 'stat_drivers') }, { end: 10, suffix: '', lbl: T(lang, 'stat_cities') }, ]; const content = (
{T(lang, 'hero_eyebrow')}

{T(lang, titleKey)}

{T(lang, 'hero_sub')}

{window.ATLAS.taRating}
{window.ATLAS.taCount.toLocaleString(lang === 'ar' ? 'ar-MA' : 'fr-FR')} {T(lang, 'ta_reviews')} · {T(lang, 'ta_excellent')}
{stats.map((s, i) => (
{s.lbl}
))}
); if (variant === 'split') { return (
{content}
{React.createElement('image-slot', { id: 'hero-split', src: window.ATLAS.photo('hero-split'), placeholder: 'Mercedes / désert', shape: 'rounded', radius: '24' })}
); } return (
{React.createElement('image-slot', { id: 'hero-bg', src: window.ATLAS.photo('hero-bg'), placeholder: 'Photo héro · route / désert marocain', shape: 'rect' })}
{content}
); } /* ---------------- Services ---------------- */ function Services({ lang, navigate, goBook, compact }) { return (
{T(lang, 'svc_eyebrow')}

{T(lang, 'svc_title')}

{T(lang, 'svc_sub')}

{window.ATLAS.services.map(s => ( ))}
); } /* ---------------- Fleet ---------------- */ function Fleet({ lang, goBook }) { const [filter, setFilter] = useState('all'); const cats = [ { id: 'all' }, { id: 'small', match: ['berline', 'luxury'] }, { id: 'group', match: ['van', 'minibus', 'bus'] }, { id: 'suv', match: ['suv'] }, ]; const catLabels = { fr: { all: 'Tous', small: 'Berlines & luxe', group: 'Groupes', suv: '4x4' }, en: { all: 'All', small: 'Sedans & luxury', group: 'Groups', suv: '4x4' }, ar: { all: 'الكل', small: 'سيدان وفاخرة', group: 'مجموعات', suv: 'دفع رباعي' }, }[lang] || {}; const list = window.ATLAS.vehicles.filter(v => { if (filter === 'all') return true; const c = cats.find(x => x.id === filter); return c && c.match && c.match.includes(v.id); }); return (
{T(lang, 'fleet_eyebrow')}

{T(lang, 'fleet_title')}

{T(lang, 'fleet_sub')}

{cats.map(c => ( ))}
{list.map(v => (
{v.cls}

{vehicleTitle(lang, v)}

{vehicleDesc(lang, v)}
{v.seats} {T(lang, 'fleet_seats')} {v.bags} {T(lang, 'fleet_bags')}
{T(lang, 'fleet_from')} {window.ATLAS.fmtMoney(v.priceDay, lang)} {T(lang, 'fleet_perday')}
))}
); } /* ---------------- Features strip ---------------- */ function Features({ lang }) { const items = [ { ic: 'clock', t: 'feat_resp', d: 'feat_resp_d' }, { ic: 'users', t: 'feat_drivers', d: 'feat_drivers_d' }, { ic: 'check', t: 'feat_safe', d: 'feat_safe_d' }, ]; return (
{items.map((f, i) => (

{T(lang, f.t)}

{T(lang, f.d)}

))}
); } /* ---------------- Languages strip ---------------- */ function Languages({ lang }) { const codes = ['IT', 'ES', 'EN', 'FR', 'AR', 'DE']; const counts = {}; window.ATLAS.drivers.forEach(d => d.langs.forEach(l => { counts[l] = (counts[l] || 0) + 1; })); return (
{T(lang, 'lang_eyebrow')}

{T(lang, 'lang_title')}

{T(lang, 'lang_sub')}

{codes.map((c, i) => (
{LANG_NAMES[c][lang] || LANG_NAMES[c].fr} {counts[c] ? {counts[c]} {T(lang, 'lang_guides')} : null}
))}
); } /* ---------------- Destinations ---------------- */ function Destinations({ lang }) { return (
{T(lang, 'dest_eyebrow')}

{T(lang, 'dest_title')}

{window.ATLAS.destinations.map((d, i) => (
{React.createElement('image-slot', { id: 'dest-' + d.id, src: window.ATLAS.photo('dest-' + d.id), placeholder: d[lang] || d.fr, shape: 'rect' })}
{d[lang] || d.fr}
))}
); } /* ---------------- Reviews (TripAdvisor) ---------------- */ function Reviews({ lang }) { const reviews = window.ATLAS.getReviews(lang); return (
{T(lang, 'rv_eyebrow')}

{T(lang, 'rv_title')}

{window.ATLAS.taRating}
{window.ATLAS.taCount.toLocaleString(lang === 'ar' ? 'ar-MA' : 'fr-FR')} {T(lang, 'ta_reviews')} {T(lang, 'ta_choice')}
{reviews.map((r, i) => (
{r.initials}
{r.name}
{r.place}

“{r.text}”

{r.when}
))}
); } /* ---------------- CTA band ---------------- */ function CTABand({ lang, navigate }) { const C = window.ATLAS.config; return (
{T(lang, 'cta_eyebrow')}

{T(lang, 'cta_title')}

{T(lang, 'cta_sub')}

{T(lang, 'cta_wa')} {T(lang, 'cta_call')}
); } /* ---------------- Personnel / corporate transport ---------------- */ function Personnel({ lang, goBook }) { const feats = [ { ic: 'cal', t: 'pers_f1', d: 'pers_f1d' }, { ic: 'check', t: 'pers_f2', d: 'pers_f2d' }, { ic: 'money', t: 'pers_f3', d: 'pers_f3d' }, ]; return (
{React.createElement('image-slot', { id: 'pers-photo', src: window.ATLAS.photo('pers-photo'), placeholder: T(lang, 'pers_photo'), shape: 'rounded', radius: '20' })} {T(lang, 'svc_personnel_t')}
{T(lang, 'pers_eyebrow')}

{T(lang, 'pers_title')}

{T(lang, 'pers_sub')}

{feats.map(f => (

{T(lang, f.t)}

{T(lang, f.d)}

))}
); } /* ---------------- Events ---------------- */ function EventCard({ lang, e, goBook, full }) { const pick = window.ATLAS.pick; return (
{React.createElement('image-slot', { id: e.img, src: window.ATLAS.photo(e.img), placeholder: pick(e.t, lang), shape: 'rect' })} {new Date(e.date + 'T00:00:00').getDate()} {new Date(e.date + 'T00:00:00').toLocaleDateString(window.ATLAS.localeOf(lang), { month: 'short' })}
{e.city} · {window.ATLAS.fmtDate(e.date, lang)}

{pick(e.t, lang)}

{pick(e.d, lang)}

); } function Events({ lang, goBook, page }) { return (
{T(lang, 'ev_eyebrow')}

{T(lang, 'ev_title')}

{T(lang, 'ev_sub')}

{window.ATLAS.events.map(e => )}
); } /* ---------------- News ---------------- */ function NewsCard({ lang, n }) { const pick = window.ATLAS.pick; return (
{React.createElement('image-slot', { id: n.img, src: window.ATLAS.photo(n.img), placeholder: pick(n.t, lang), shape: 'rect' })} {pick(n.tag, lang)}
{window.ATLAS.fmtDate(n.date, lang)}

{pick(n.t, lang)}

{pick(n.d, lang)}

{T(lang, 'news_read')}
); } function News({ lang, page }) { return (
{T(lang, 'news_eyebrow')}

{T(lang, 'news_title')}

{T(lang, 'news_sub')}

{window.ATLAS.news.map(n => )}
); } /* ---------------- Booking page ---------------- */ function BookPage({ lang, prefill, user, navigate, onLogin, setTrackCode }) { const [form, setForm] = useState(Object.assign( { service: '', date: '', time: '', pickup: '', dropoff: '', passengers: 2, vehicle: '', billing: 'monthly', name: '', phone: '', email: '', notes: '' }, prefill || {}, user ? { name: user.name, email: user.email, phone: user.phone } : {} )); const [step, setStep] = useState(0); const [done, setDone] = useState(null); const [errs, setErrs] = useState({}); const [mapOpen, setMapOpen] = useState(false); const steps = ['rq_step1', 'rq_step2', 'rq_step3', 'rq_step4']; const set = (k, v) => setForm(f => Object.assign({}, f, { [k]: v })); const validate = (s) => { const e = {}; if (s === 0 && !form.service) e.service = 1; if (s === 1) { if (!form.date) e.date = 1; if (!form.time) e.time = 1; if (!form.pickup) e.pickup = 1; if (!form.dropoff) e.dropoff = 1; } if (s === 3) { if (!form.name) e.name = 1; if (!form.phone) e.phone = 1; } setErrs(e); return Object.keys(e).length === 0; }; const next = () => { if (validate(step)) setStep(s => Math.min(3, s + 1)); }; const back = () => setStep(s => Math.max(0, s - 1)); const submit = () => { if (!validate(3)) return; const created = window.ATLAS.addRequest({ service: form.service, date: form.date, time: form.time, pickup: form.pickup, dropoff: form.dropoff, passengers: form.passengers || 1, vehicleReq: form.vehicle || null, name: form.name, phone: form.phone, email: form.email, notes: form.notes, lang, billing: form.service === 'personnel' ? form.billing : null, ownerEmail: user ? user.email : null, }); setDone(created); }; if (done) { return (

{T(lang, 'rq_done_title')}

{T(lang, 'rq_done_sub')}
{done.id}

{T(lang, 'rq_done_msg')}

{user ? (
) : (
{T(lang, 'cf_create_track')}
{T(lang, 'trk_guest_note')}
)}
); } return (
{T(lang, 'rq_eyebrow')}

{T(lang, 'rq_title')}

{T(lang, 'rq_sub')}

{steps.map((s, i) => (
{T(lang, s)}
))}
{step === 0 && (
{window.ATLAS.services.map(s => ( ))}
{errs.service &&
{T(lang, 'rq_required')}
} {form.service === 'personnel' && (
{['daily', 'monthly', 'annual'].map(b => ( ))}
{T(lang, 'bill_hint')}
)}
)} {step === 1 && (
set('date', e.target.value)} /> {errs.date && {T(lang, 'rq_required')}}
set('time', e.target.value)} /> {errs.time && {T(lang, 'rq_required')}}
set('pickup', e.target.value)} /> {errs.pickup && {T(lang, 'rq_required')}}
set('dropoff', e.target.value)} /> {errs.dropoff && {T(lang, 'rq_required')}}
{form.passengers || 1}
{window.ATLAS.cities.map(c => {mapOpen && c.name === form.pickup)} initArr={window.ATLAS.mapCities.find(c => c.name === form.dropoff)} onConfirm={({ pickup, dropoff }) => { if (pickup) set('pickup', pickup); if (dropoff) set('dropoff', dropoff); setMapOpen(false); }} onCancel={() => setMapOpen(false)} />}
)} {step === 2 && (
{window.ATLAS.vehicles.map(v => ( ))}
)} {step === 3 && (
set('name', e.target.value)} /> {errs.name && {T(lang, 'rq_required')}}
set('phone', e.target.value)} /> {errs.phone && {T(lang, 'rq_required')}}
set('email', e.target.value)} />
)}
{step < 3 ? : }
); } /* ---------------- Track page ---------------- */ function TrackPage({ lang, initialCode, user, navigate, onLogin }) { const [code, setCode] = useState(initialCode || ''); const [found, setFound] = useState(initialCode ? window.ATLAS.getByCode(initialCode) : null); const [searched, setSearched] = useState(!!initialCode); const [, force] = useState(0); useEffect(() => { const u = window.ATLAS.subscribe(() => force(x => x + 1)); return u; }, []); useEffect(() => { if (initialCode) { setCode(initialCode); setFound(window.ATLAS.getByCode(initialCode)); setSearched(true); } }, [initialCode]); const lookup = () => { setFound(window.ATLAS.getByCode(code)); setSearched(true); }; const fresh = found ? window.ATLAS.getByCode(found.id) : null; return (
{T(lang, 'trk_track')}

{T(lang, 'trk_title')}

setCode(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') lookup(); }} />
{!user && (
{T(lang, 'cf_create_track')}
)} {searched && !fresh &&
{T(lang, 'trk_notfound')}
} {fresh &&
force(x => x + 1)} />
}
); } /* ---------------- Account page ---------------- */ function AccountPage({ lang, user, navigate, onLogin }) { const [sel, setSel] = useState(null); const [, force] = useState(0); useEffect(() => { const u = window.ATLAS.subscribe(() => force(x => x + 1)); return u; }, []); if (!user) { return (

{T(lang, 'ac_account')}

{T(lang, 'cf_create_track')}

); } const reqs = window.ATLAS.getUserRequests(user.email); const current = sel ? window.ATLAS.getByCode(sel) : null; return (
{T(lang, 'ac_welcome')}

{user.name}

{current ? (
force(x => x + 1)} />
) : reqs.length === 0 ? (
{T(lang, 'ac_no_requests')}
) : (
{reqs.map(r => ( ))}
)}
); } /* ---------------- Footer ---------------- */ function Footer({ lang, navigate }) { const C = window.ATLAS.config; return ( ); } /* ---------------- Tweaks ---------------- */ const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "heroVariant": "classic", "accent": "sand", "zellij": true }/*EDITMODE-END*/; /* ---------------- App ---------------- */ function App() { const [lang, setLang] = useLang(); const [t, setTweak] = useTweaks(TWEAK_DEFAULTS); const [route, navigate] = useRoute(); const user = useAuth(); const [bookPrefill, setBookPrefill] = useState(null); const [trackCode, setTrackCode] = useState(''); const [auth, setAuth] = useState(null); // {mode, prefill, claimCode} useEffect(() => { applyAccent(t.accent); }, [t.accent]); useEffect(() => { document.body.classList.toggle('no-zellij', !t.zellij); }, [t.zellij]); // re-render whole tree when currency changes so prices refresh const [, setCurTick] = useState(0); useEffect(() => window.ATLAS.subscribeCurrency(() => setCurTick(x => x + 1)), []); // scroll-reveal (progressive enhancement; re-observe on page change) useEffect(() => { document.body.classList.add('js-ready'); const els = document.querySelectorAll('.js-reveal:not(.in)'); const io = new IntersectionObserver((es) => es.forEach(e => { if (e.isIntersecting) { e.target.classList.add('in'); io.unobserve(e.target); } }), { threshold: 0.12, rootMargin: '0px 0px -40px 0px' }); els.forEach(el => io.observe(el)); return () => io.disconnect(); }, [route]); const goBook = (service, vehicle) => { setBookPrefill({ service: service || '', vehicle: vehicle || '' }); navigate('book'); }; const openLogin = (mode, prefill, claimCode) => setAuth({ mode: mode || 'login', prefill, claimCode }); const onAuthed = (u) => { if (auth && auth.claimCode) { window.ATLAS.claimRequest(auth.claimCode, u.email); navigate('account'); } }; let page; if (route === 'services') page = ; else if (route === 'fleet') page = ; else if (route === 'events') page = ; else if (route === 'news') page = ; else if (route === 'book') page = ; else if (route === 'track') page = ; else if (route === 'account') page = ; else page = (
); return (
{page}
{auth && setAuth(null)} onAuthed={onAuthed} />} setTweak('heroVariant', v)} /> setTweak('zellij', v)} /> { const n = Object.keys(ACCENTS).find(k => ACCENTS[k].a === v) || 'sand'; setTweak('accent', n); }} />
); } window.ATLAS.ready.then(() => ReactDOM.createRoot(document.getElementById('root')).render());