/* ============================================================
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 && (
)}
);
}
/* ---------------- 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) => (
))}
);
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' })}
);
}
/* ---------------- 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) => (
))}
);
}
/* ---------------- CTA band ---------------- */
function CTABand({ lang, navigate }) {
const C = window.ATLAS.config;
return (
{T(lang, 'cta_eyebrow')}
{T(lang, 'cta_title')}
{T(lang, 'cta_sub')}
);
}
/* ---------------- 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) => (
))}
{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}
{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 && (
)}
{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')}
{!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());