// ============================================
// Booking flows
// 1. QuickBookModal — book one catalog item (no full itinerary needed)
// 2. TweakItineraryModal — open an existing itinerary, add/remove catalog items, send request
// 3. FavouritesQuickAdd — favourites panel surfaced above the planning form
// ============================================
const { useState: useStateB, useEffect: useEffectB, useMemo: useMemoB, useRef: useRefB } = React;
const Ib = window.MS_I;
const WHATSAPP = "212698164331";
// ── Helpers ────────────────────────────────────────────────────
function todayPlusBk(days) {
const d = new Date();
d.setDate(d.getDate() + days);
return d.toISOString().slice(0, 10);
}
function readUserPrefill() {
try {
const user = window.MS_Auth_User || JSON.parse(localStorage.getItem('ms_user') || 'null');
const profile = JSON.parse(localStorage.getItem('ms_profile_data') || '{}');
return {
name: profile.name || user?.name || '',
email: profile.email || user?.email || '',
phone: profile.phone || '',
};
} catch { return { name: '', email: '', phone: '' }; }
}
function whatsappUrl(text) {
return `https://wa.me/${WHATSAPP}?text=${encodeURIComponent(text)}`;
}
// ──────────────────────────────────────────────────────────────
// 1. QUICK-BOOK MODAL — book one catalog item, no full itinerary
// ──────────────────────────────────────────────────────────────
function QuickBookModal({ item, tab, onClose }) {
const { useMS } = window.MS_CTX;
const ctx = useMS();
const lang = ctx.lang || 'no';
const tx = (en, no, fr) => lang === 'no' ? no : lang === 'fr' ? fr : en;
const prefill = readUserPrefill();
const [date, setDate] = useStateB(todayPlusBk(14));
const [people, setPeople] = useStateB(2);
const [name, setName] = useStateB(prefill.name);
const [email, setEmail] = useStateB(prefill.email);
const [phone, setPhone] = useStateB(prefill.phone);
const [notes, setNotes] = useStateB('');
const [needTransport, setNeedTransport] = useStateB(false);
const [pickupAddr, setPickupAddr] = useStateB('');
const [sent, setSent] = useStateB(false);
useEffectB(() => {
const onKey = (e) => { if (e.key === 'Escape') onClose(); };
document.addEventListener('keydown', onKey);
document.body.style.overflow = 'hidden';
return () => { document.removeEventListener('keydown', onKey); document.body.style.overflow = ''; };
}, []);
const dateLabel = new Date(date).toLocaleDateString(lang === 'no' ? 'no-NO' : lang === 'fr' ? 'fr-FR' : 'en-GB',
{ day: 'numeric', month: 'short', year: 'numeric' });
const transportLine = () => needTransport
? tx(`\n• Transport needed: yes${pickupAddr ? ` (pickup: ${pickupAddr})` : ''}`,
`\n• Trenger transport: ja${pickupAddr ? ` (henting: ${pickupAddr})` : ''}`,
`\n• Transport nécessaire : oui${pickupAddr ? ` (prise en charge : ${pickupAddr})` : ''}`)
: '';
const buildMessage = () => {
return tx(
`Hi Marrakech Story, I'd like to book just this:\n\n• ${item.name}\n• Date: ${dateLabel}\n• People: ${people}\n• Name: ${name}\n• Email: ${email}\n• Phone: ${phone}${transportLine()}${notes ? `\n• Notes: ${notes}` : ''}`,
`Hei Marrakech Story, jeg vil bestille kun dette:\n\n• ${item.name}\n• Dato: ${dateLabel}\n• Antall: ${people}\n• Navn: ${name}\n• E-post: ${email}\n• Telefon: ${phone}${transportLine()}${notes ? `\n• Notater: ${notes}` : ''}`,
`Bonjour Marrakech Story, je souhaite réserver uniquement ceci :\n\n• ${item.name}\n• Date : ${dateLabel}\n• Personnes : ${people}\n• Nom : ${name}\n• Email : ${email}\n• Téléphone : ${phone}${transportLine()}${notes ? `\n• Notes : ${notes}` : ''}`
);
};
const persistQuickBook = (via) => {
try {
const reqs = JSON.parse(localStorage.getItem('ms_requests') || '[]');
reqs.push({ type: 'single', item: item.name, tab, date, people, name, email, phone, notes, needTransport, pickupAddr, at: Date.now() });
localStorage.setItem('ms_requests', JSON.stringify(reqs));
} catch {}
if (window.MS_submitForm) {
window.MS_submitForm('quickbook', {
item: item.name, tab, date, people, name, email, phone, notes,
needTransport, pickupAddr, startDate: date, endDate: date, duration: 1
}, { via });
}
};
const sendWhatsapp = () => {
if (!name.trim() || !email.trim()) return;
persistQuickBook('whatsapp');
window.open(whatsappUrl(buildMessage()), '_blank', 'noopener');
setSent(true);
};
const sendEmail = () => {
if (!name.trim() || !email.trim()) return;
const subject = `Booking — ${item.name}`;
const body = buildMessage();
window.location.href = `mailto:marrakechstory@outlook.com?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`;
persistQuickBook('email');
setSent(true);
};
return (
e.stopPropagation()}>
✕
{tx('QUICK BOOK', 'RASK BOOKING', 'RÉSERVATION RAPIDE')}
{item.name}
{sent ? (
✓
{tx('Request sent', 'Forespørsel sendt', 'Demande envoyée')}
{tx(
"We've received your booking request. The team will confirm by email or WhatsApp within 24 hours.",
"Vi har mottatt forespørselen din. Teamet bekrefter på e-post eller WhatsApp innen 24 timer.",
"Nous avons reçu votre demande. L'équipe vous confirmera par e-mail ou WhatsApp sous 24 h."
)}
{tx('Close', 'Lukk', 'Fermer')}
) : (
<>
{tx(
"Just want this one thing? Skip the full planner — give us a date, who's coming, and how to reach you.",
"Bare denne ene? Hopp over hele planleggeren — gi oss en dato, hvem som blir med og kontaktinfo.",
"Juste cela ? Sautez le planificateur — donnez-nous une date, qui vient, et comment vous joindre."
)}
{tx('Date', 'Dato', 'Date')}
setDate(e.target.value)} />
{tx('People', 'Antall', 'Personnes')}
setPeople(p => Math.max(1, p - 1))}>−
{people}
setPeople(p => Math.min(20, p + 1))}>+
{tx('Full name', 'Fullt navn', 'Nom complet')}
setName(e.target.value)} autoComplete="name" />
{tx('Email', 'E-post', 'E-mail')}
setEmail(e.target.value)} autoComplete="email" />
{tx('Phone', 'Telefon', 'Téléphone')}
setPhone(e.target.value)} autoComplete="tel" placeholder="+47 …" />
setNeedTransport(e.target.checked)} />
{tx('I need transportation', 'Jeg trenger transport', 'J\'ai besoin de transport')}
{tx('Transport is not included — tick to add a driver.',
'Transport er ikke inkludert — kryss av om vi skal legge til sjåfør.',
'Le transport n\'est pas inclus — cochez pour ajouter un chauffeur.')}
{needTransport && (
setPickupAddr(e.target.value)}
placeholder={tx('Pickup address (hotel / riad name)', 'Henteadresse (hotell / riad)', 'Adresse de prise en charge')} />
)}
{tx('Notes (optional)', 'Notater (valgfritt)', 'Notes (optionnel)')}
📱 {tx('Send via WhatsApp', 'Send via WhatsApp', 'Envoyer via WhatsApp')}
✉ {tx('Send by email', 'Send på e-post', 'Envoyer par e-mail')}
{tx(
'No payment now — we confirm availability and price, you pay directly to the provider.',
'Ingen betaling nå — vi bekrefter ledighet og pris, du betaler direkte til leverandøren.',
'Aucun paiement maintenant — nous confirmons disponibilité et prix, vous payez directement.'
)}
>
)}
);
}
// ──────────────────────────────────────────────────────────────
// 2. TWEAK ITINERARY MODAL — start from an existing itinerary, customise it
// ──────────────────────────────────────────────────────────────
function TweakItineraryModal({ trip, onClose }) {
const { useMS } = window.MS_CTX;
const ctx = useMS();
const lang = ctx.lang || 'no';
const tx = (en, no, fr) => lang === 'no' ? no : lang === 'fr' ? fr : en;
const prefill = readUserPrefill();
// Build editable day-list from the trip
const initialDays = useMemoB(() => (trip.itinerary || []).map((d, i) => ({
id: `day-${i}`,
day: d.day || i + 1,
route: d.route,
text: d.text,
extras: [], // added catalog items per day
})), [trip]);
const [days, setDays] = useStateB(initialDays);
const [pickerOpenFor, setPickerOpenFor] = useStateB(null); // day id when picker active
const [date, setDate] = useStateB(todayPlusBk(28));
const [name, setName] = useStateB(prefill.name);
const [email, setEmail] = useStateB(prefill.email);
const [phone, setPhone] = useStateB(prefill.phone);
const [notes, setNotes] = useStateB('');
const [sent, setSent] = useStateB(false);
useEffectB(() => {
const onKey = (e) => { if (e.key === 'Escape') onClose(); };
document.addEventListener('keydown', onKey);
document.body.style.overflow = 'hidden';
return () => { document.removeEventListener('keydown', onKey); document.body.style.overflow = ''; };
}, []);
const removeDay = (id) => setDays(ds => ds.filter(d => d.id !== id));
const removeExtra = (dayId, extraIdx) => setDays(ds => ds.map(d =>
d.id !== dayId ? d : { ...d, extras: d.extras.filter((_, i) => i !== extraIdx) }));
const addExtra = (dayId, item, tab) => {
setDays(ds => ds.map(d => d.id !== dayId ? d : { ...d, extras: [...d.extras, { item, tab }] }));
setPickerOpenFor(null);
};
const buildMessage = () => {
const dateLabel = new Date(date).toLocaleDateString(lang === 'no' ? 'no-NO' : lang === 'fr' ? 'fr-FR' : 'en-GB',
{ day: 'numeric', month: 'short', year: 'numeric' });
const tripLines = days.map(d => {
const extrasText = d.extras.map(e => ` + ${e.item.name}`).join('\n');
return `Day ${d.day} — ${d.route}\n ${d.text}${extrasText ? '\n' + extrasText : ''}`;
}).join('\n\n');
return tx(
`Hi Marrakech Story, I'd like to book this trip with my own tweaks:\n\nBase trip: ${trip.title} (${trip.duration})\nStart date: ${dateLabel}\n\nName: ${name}\nEmail: ${email}\nPhone: ${phone}\n\nMy custom day-by-day:\n\n${tripLines}\n\n${notes ? `Notes: ${notes}` : ''}`,
`Hei Marrakech Story, jeg vil bestille denne turen med mine tilpasninger:\n\nBasetur: ${trip.title} (${trip.duration})\nStartdato: ${dateLabel}\n\nNavn: ${name}\nE-post: ${email}\nTelefon: ${phone}\n\nMin tilpassede plan:\n\n${tripLines}\n\n${notes ? `Notater: ${notes}` : ''}`,
`Bonjour Marrakech Story, je souhaite réserver ce voyage avec mes ajustements :\n\nBase : ${trip.title} (${trip.duration})\nDate de début : ${dateLabel}\n\nNom : ${name}\nEmail : ${email}\nTéléphone : ${phone}\n\nMon planning personnalisé :\n\n${tripLines}\n\n${notes ? `Notes : ${notes}` : ''}`
);
};
const sendWhatsapp = () => {
if (!name.trim() || !email.trim()) return;
try {
const reqs = JSON.parse(localStorage.getItem('ms_requests') || '[]');
reqs.push({ type: 'tweaked', baseTrip: trip.slug, days: days.length, extras: days.reduce((s, d) => s + d.extras.length, 0), name, email, at: Date.now() });
localStorage.setItem('ms_requests', JSON.stringify(reqs));
} catch {}
if (window.MS_submitForm) {
window.MS_submitForm('tweak', {
name, email, phone, notes,
baseTrip: trip.slug, baseTitle: trip.title, baseDuration: trip.duration,
startDate: date, duration: days.length, days
}, { via: 'whatsapp' });
}
window.open(whatsappUrl(buildMessage()), '_blank', 'noopener');
setSent(true);
};
return (
e.stopPropagation()}>
✕
— {tx('TWEAK THIS TRIP', 'TILPASS DENNE TUREN', 'PERSONNALISER CE VOYAGE')}
{trip.title}
{tx(
"Remove days you don't want. Add anything from our catalogue. We'll cost it up and confirm.",
"Fjern dager du ikke vil ha. Legg til hva som helst fra katalogen. Vi priser det og bekrefter.",
"Retirez ce que vous ne voulez pas. Ajoutez ce qui vous plaît du catalogue. On chiffre et on confirme."
)}
{sent ? (
✓
{tx('Custom trip request sent', 'Tilpasset reise sendt', 'Demande personnalisée envoyée')}
{tx(
"We've got your custom itinerary. Expect a reply within 24 hours.",
"Vi har mottatt din tilpassede reise. Svar innen 24 timer.",
"Nous avons reçu votre itinéraire. Réponse sous 24 h."
)}
{tx('Close', 'Lukk', 'Fermer')}
) : (
{/* Editable day-by-day */}
{days.map((d) => (
{tx('Day', 'Dag', 'Jour')} {d.day}
{d.route}
removeDay(d.id)} aria-label="Remove day">✕
{d.text}
{d.extras.length > 0 && (
{d.extras.map((e, i) => (
+ {e.item.name}
removeExtra(d.id, i)} aria-label="Remove">×
))}
)}
setPickerOpenFor(d.id)}>
+ {tx('Add from catalogue', 'Legg til fra katalog', 'Ajouter du catalogue')}
))}
{days.length === 0 && (
{tx('No days left. Add one or start fresh.', 'Ingen dager igjen. Legg til en eller start på nytt.', 'Aucun jour. Ajoutez-en un ou recommencez.')}
)}
{/* Contact block */}
{tx('Your details', 'Dine opplysninger', 'Vos coordonnées')}
{tx('Start date', 'Startdato', 'Date de début')}
setDate(e.target.value)} />
{tx('Full name', 'Fullt navn', 'Nom complet')}
setName(e.target.value)} autoComplete="name" />
{tx('Email', 'E-post', 'E-mail')}
setEmail(e.target.value)} autoComplete="email" />
{tx('Phone', 'Telefon', 'Téléphone')}
setPhone(e.target.value)} autoComplete="tel" placeholder="+47 …" />
{tx('Anything else?', 'Annet?', 'Autre chose ?')}
📱 {tx('Send custom trip via WhatsApp', 'Send tilpasset reise via WhatsApp', 'Envoyer via WhatsApp')}
{tx(
`${days.reduce((s, d) => s + d.extras.length, 0)} extras added · ${days.length} days kept`,
`${days.reduce((s, d) => s + d.extras.length, 0)} ekstrapunkter lagt til · ${days.length} dager beholdt`,
`${days.reduce((s, d) => s + d.extras.length, 0)} extras ajoutés · ${days.length} jours conservés`
)}
)}
{pickerOpenFor && (
setPickerOpenFor(null)}
onPick={(item, tab) => addExtra(pickerOpenFor, item, tab)}
lang={lang}
/>
)}
);
}
// ──────────────────────────────────────────────────────────────
// CATALOG PICKER — used by TweakItineraryModal and FavouritesQuickAdd
// ──────────────────────────────────────────────────────────────
function CatalogPicker({ onClose, onPick, lang }) {
const tx = (en, no, fr) => lang === 'no' ? no : lang === 'fr' ? fr : en;
const D = window.MS_DATA || {};
const [tab, setTab] = useStateB('activities');
const [q, setQ] = useStateB('');
const TABS = [
{ id: 'activities', label: tx('Activities', 'Aktiviteter', 'Activités'), data: D.ACTIVITIES || [] },
{ id: 'restaurants', label: tx('Restaurants', 'Restauranter', 'Restaurants'), data: D.RESTAURANTS || [] },
{ id: 'spa', label: tx('Spa', 'Spa', 'Spa'), data: D.SPAS || [] },
{ id: 'pools', label: tx('Pools', 'Basseng', 'Piscines'), data: D.POOLS || [] },
{ id: 'camps', label: tx('Camps', 'Leirer', 'Camps'), data: D.CAMPS || [] },
{ id: 'transport', label: tx('Transport', 'Transport', 'Transport'), data: D.TRANSPORT || [] },
];
const current = TABS.find(t => t.id === tab);
const items = useMemoB(() => {
if (!q.trim()) return current.data.slice(0, 30);
const lq = q.toLowerCase();
return current.data.filter(i => (i.name + ' ' + (i.desc || '') + ' ' + (i.area || '')).toLowerCase().includes(lq)).slice(0, 30);
}, [tab, q, current]);
return (
e.stopPropagation()}>
✕
{tx('Add from the catalogue', 'Legg til fra katalogen', 'Ajouter du catalogue')}
setQ(e.target.value)}
placeholder={tx('Search any activity, riad, spa, restaurant…', 'Søk aktivitet, riad, spa, restaurant …', 'Chercher activité, riad, spa …')} />
{TABS.map(t => (
setTab(t.id)}>{t.label} {t.data.length}
))}
{items.map((it, i) => (
onPick(it, tab)}>
{it.name}
{it.area}
+
))}
{items.length === 0 && (
{tx('No matches.', 'Ingen treff.', 'Aucun résultat.')}
)}
);
}
// ──────────────────────────────────────────────────────────────
// 3. FAVOURITES QUICK-ADD — small panel placed above the planning form
// ──────────────────────────────────────────────────────────────
function FavouritesQuickAdd() {
const { useMS } = window.MS_CTX;
const ctx = useMS();
const lang = ctx.lang || 'no';
const tx = (en, no, fr) => lang === 'no' ? no : lang === 'fr' ? fr : en;
const [favs, setFavs] = useStateB([]);
const [tweakTrip, setTweakTrip] = useStateB(null);
const [pickerOpen, setPickerOpen] = useStateB(false);
useEffectB(() => {
// Build the list of favourites (catalog items) + favourite itineraries
const refresh = () => {
let catFavs = {}, itinFavs = [];
try {
catFavs = JSON.parse(localStorage.getItem('ms_catalog_favs') || '{}');
itinFavs = JSON.parse(localStorage.getItem('ms_user_favs') || '[]');
} catch {}
const D = window.MS_DATA || {};
const arrays = { activities: 'ACTIVITIES', restaurants: 'RESTAURANTS', spa: 'SPAS', camps: 'CAMPS', pools: 'POOLS', transport: 'TRANSPORT' };
const items = [];
Object.entries(arrays).forEach(([tab, k]) => {
(D[k] || []).forEach(it => {
if (catFavs[`${tab}-${it.name}`]) items.push({ kind: 'catalog', tab, item: it });
});
});
(window.MS_ITINERARIES || []).filter(t => itinFavs.includes(t.slug))
.forEach(t => items.push({ kind: 'itinerary', trip: t }));
setFavs(items);
};
refresh();
const onStorage = (e) => { if (e.key === 'ms_catalog_favs' || e.key === 'ms_user_favs') refresh(); };
window.addEventListener('storage', onStorage);
// Also poll lightly because heart clicks happen in the same tab
const t = setInterval(refresh, 1500);
return () => { window.removeEventListener('storage', onStorage); clearInterval(t); };
}, []);
if (favs.length === 0) return null;
return (
— {tx('YOUR FAVOURITES', 'DINE FAVORITTER', 'VOS FAVORIS')}
{tx('Start your trip from what you saved', 'Start reisen fra det du har lagret', 'Démarrez du contenu sauvegardé')}
{favs.length}
{favs.map((f, i) => {
if (f.kind === 'catalog') {
return (
{f.item.name}
{f.item.area}
window.MS_OpenQuickBook?.(f.item, f.tab)}>
{tx('Book this →', 'Bestill →', 'Réserver →')}
);
}
// itinerary fav
return (
{f.trip.title}
{f.trip.duration} · {f.trip.route}
setTweakTrip(f.trip)}>
{tx('Tweak this →', 'Tilpass →', 'Personnaliser →')}
);
})}
{tweakTrip &&
setTweakTrip(null)} />}
);
}
// Globally expose the QuickBook opener so catalog.jsx can fire it
window.MS_OpenQuickBook = null; // wired below via the host component
function QuickBookHost() {
const [current, setCurrent] = useStateB(null); // { item, tab }
useEffectB(() => {
window.MS_OpenQuickBook = (item, tab) => setCurrent({ item, tab });
return () => { window.MS_OpenQuickBook = null; };
}, []);
if (!current) return null;
return setCurrent(null)} />;
}
window.MS_QuickBookHost = QuickBookHost;
window.MS_TweakItineraryModal = TweakItineraryModal;
window.MS_FavouritesQuickAdd = FavouritesQuickAdd;