// ============================================
// LIVE ITINERARY BUILDER
// Left: questions. Right: animated timeline preview.
// ============================================
const { useState: useSF, useEffect: useEF, useMemo: useMF } = React;
const If = window.MS_I;
const ACT_EMOJI = {
arrival: '✈️', medina: '🕌', food: '🍽️', agafay: '🐪',
atlas: '🏔️', spa: '🛁', balloon: '🎈', quad: '🏍️',
shopping: '🛍️', photo: '📸', sahara: '🌵', essaouira: '🌊',
imperial: '🏛️', pool: '☀️', departure: '✈️',
};
const ACT_POOL = {
arrival: {
title: { no: "Ankomst & velkomst", en: "Arrival & welcome", fr: "Arrivée & bienvenue" },
desc: {
no: "Privat henting på flyplassen, innsjekk på din riad i medinaen, og myntete på taket ved gylne timen.",
en: "Private airport pickup, check-in at your hand-picked riad in the medina, and mint tea on the rooftop at golden hour.",
fr: "Accueil privé à l'aéroport, arrivée dans votre riad de charme au cœur de la médina, thé à la menthe sur le toit-terrasse.",
},
chips: { no: ["Privat transfer", "Riad i medinaen", "Velkomstmiddag"], en: ["Private transfer", "Riad in medina", "Welcome dinner"], fr: ["Transfert privé", "Riad médina", "Dîner de bienvenue"] },
stay: "Riad El Fenn", icon: "Plane",
},
medina: {
title: { no: "Medina & paladser", en: "Medina & palaces", fr: "Médina & palais" },
desc: {
no: "En lokal historiker tar deg gjennom Ben Youssef, Bahia-palasset, Saadiernes graver og krydderkvartalet.",
en: "A local historian walks you through Ben Youssef, Bahia Palace, the Saadian Tombs and the spice quarter.",
fr: "Un historien local vous guide à travers Ben Youssef, le palais Bahia, les tombeaux saadiens et le quartier des épices.",
},
chips: { no: ["Bahia-palasset", "Ben Youssef", "Souker"], en: ["Bahia Palace", "Ben Youssef", "Souks"], fr: ["Palais Bahia", "Ben Youssef", "Souks"] },
stay: "Riad El Fenn", icon: "Compass",
},
food: {
title: { no: "Matkurs & souk", en: "Cooking class & food souk", fr: "Cours de cuisine & souk" },
desc: {
no: "Handle krydder med en lokal kokk, og lag tagine og pastilla i et tradisjonelt hjem.",
en: "Shop the spice souk with a local chef, then cook a tagine and pastilla in a traditional dar.",
fr: "Achetez les épices avec un chef local, puis préparez tagine et pastilla dans une dar traditionnelle.",
},
chips: { no: ["Krydderkurs", "Matlaging", "Pastilla"], en: ["Spice tour", "Cooking class", "Pastilla"], fr: ["Cours d'épices", "Cuisine", "Pastilla"] },
stay: "Riad El Fenn", icon: "Utensils",
},
agafay: {
title: { no: "Agafay-ørkenen", en: "Agafay stone desert", fr: "Désert d'Agafay" },
desc: {
no: "Kjør 45 min til Agafay. Kamelritt ved solnedgang og middag rundt bålet under stjernehimmelen.",
en: "Drive 45min into the Agafay stone desert. Camel sundown and a fire-lit dinner under the stars.",
fr: "45min jusqu'au désert d'Agafay. Coucher de soleil à dos de chameau et dîner autour du feu sous les étoiles.",
},
chips: { no: ["Kameltur", "Stjernemiddag", "Bål"], en: ["Camel ride", "Star dinner", "Bonfire"], fr: ["Chameau", "Dîner étoilé", "Feu de bois"] },
stay: "Scarabeo Camp · Agafay", icon: "Tent",
},
atlas: {
title: { no: "Atlasfjellene & berbiske landsbyer", en: "Atlas Mountains & Berber villages", fr: "Atlas & villages berbères" },
desc: {
no: "90 min inn i Atlas. Vandring mellom berbiske landsbyer med hjemmelaget lunsj.",
en: "90min into the Atlas. Hike between Berber villages with a home-cooked lunch.",
fr: "1h30 dans l'Atlas. Randonnée entre villages berbères avec déjeuner préparé chez l'habitant.",
},
chips: { no: ["Vandring", "Lokal lunsj", "Toubkal-utsikt"], en: ["Hike", "Local lunch", "Toubkal viewpoint"], fr: ["Randonnée", "Déjeuner local", "Vue Toubkal"] },
stay: "Kasbah du Toubkal", icon: "Mountain",
},
spa: {
title: { no: "Hammam & spa", en: "Hammam & spa ritual", fr: "Hammam & rituel spa" },
desc: {
no: "Tradisjonell hammam med svart såpe, peeling og argan-massasje.",
en: "Traditional hammam — black soap exfoliation and full argan oil massage.",
fr: "Hammam traditionnel — gommage au savon noir et massage à l'huile d'argan.",
},
chips: { no: ["Svart såpe", "Peeling", "Argan-massasje"], en: ["Black soap", "Exfoliation", "Argan massage"], fr: ["Savon noir", "Gommage", "Massage argan"] },
stay: "Riad El Fenn", icon: "Sparkle",
},
balloon: {
title: { no: "Luftballong ved soloppgang", en: "Hot-air balloon at sunrise", fr: "Montgolfière au lever du soleil" },
desc: {
no: "Lett over Agafay mens Atlas tar imot første lys. Berbisk frokost ved landing.",
en: "Lift off as the Atlas catches first light. Berber breakfast on landing.",
fr: "Décollez quand l'Atlas reçoit la première lumière. Petit-déjeuner berbère à l'atterrissage.",
},
chips: { no: ["Soloppgang", "Ballong", "Berbisk frokost"], en: ["Sunrise", "Balloon", "Berber breakfast"], fr: ["Lever du soleil", "Ballon", "Petit-déjeuner berbère"] },
stay: "Riad El Fenn", icon: "Sun",
},
quad: {
title: { no: "Quad eller buggy", en: "Quad biking or buggy", fr: "Quad ou buggy" },
desc: {
no: "Tre timer i palmeoasen eller Lalla Takerkoust med buggy.",
en: "Three hours through the palm grove or Lalla Takerkoust by buggy.",
fr: "Trois heures dans la palmeraie ou à Lalla Takerkoust en buggy.",
},
chips: { no: ["Quad", "Off-road", "Palmeoase"], en: ["Quad", "Off-road", "Palm grove"], fr: ["Quad", "Hors-piste", "Palmeraie"] },
stay: "Riad El Fenn", icon: "Compass",
},
shopping: {
title: { no: "Privat shopping i soukene", en: "Private souk shopping", fr: "Shopping privé dans les souks" },
desc: {
no: "En lokal stylist tar deg til pålitelige håndverkere – tepper, lamper, lær.",
en: "A bilingual buyer takes you to her trusted artisans — rugs, lamps, leather.",
fr: "Une acheteuse bilingue vous emmène chez ses artisans de confiance — tapis, lampes, cuir.",
},
chips: { no: ["Tepper", "Lamper", "Lær"], en: ["Rugs", "Lamps", "Leather"], fr: ["Tapis", "Lampes", "Cuir"] },
stay: "Riad El Fenn", icon: "Sparkle",
},
photo: {
title: { no: "Foto-vandring i medinaen", en: "Medina photography walk", fr: "Balade photo dans la médina" },
desc: {
no: "Tre timer med en lokal fotograf – skjulte riader, lysrom, fargemakere, taksolnedganger.",
en: "Three hours with a local photographer — hidden riads, light pockets, dye-makers, rooftop sunsets.",
fr: "Trois heures avec un photographe local — riads cachés, jeux de lumière, teinturiers, couchers de soleil.",
},
chips: { no: ["Skjulte riader", "Fargemakere", "Tak-solnedgang"], en: ["Hidden riads", "Dye-makers", "Rooftop sunset"], fr: ["Riads cachés", "Teinturiers", "Couchers de soleil"] },
stay: "Riad El Fenn", icon: "Camera",
},
sahara: {
title: { no: "Sahara – dyner & luksusleir", en: "Sahara — dunes & luxury camp", fr: "Sahara — dunes & camp de luxe" },
desc: {
no: "4x4 over Erg Chebbi, kameltur ved solnedgang, middag og trommer ved luksusleir.",
en: "4x4 across Erg Chebbi, sunset camel trek, dinner and drums at a luxury dune camp.",
fr: "4x4 sur l'Erg Chebbi, chameau au coucher du soleil, dîner et tambours au camp de luxe.",
},
chips: { no: ["4x4", "Kameltur", "Luksusleir"], en: ["4x4", "Camel trek", "Luxury camp"], fr: ["4x4", "Caravane", "Camp de luxe"] },
stay: "Erg Chebbi Luxury Camp", icon: "Tent",
},
essaouira: {
title: { no: "Essaouira – Atlanterhavet", en: "Essaouira — Atlantic coast", fr: "Essaouira — côte atlantique" },
desc: {
no: "To timer vest til vindsurf-byen – fiske-lunsj på havna, medina-murer, retur ved solnedgang.",
en: "2h drive west to the windsurf capital — fish lunch on the harbour, medina walls, return at sunset.",
fr: "2h vers l'ouest jusqu'à la capitale du windsurf — déjeuner poissons sur le port, remparts, retour.",
},
chips: { no: ["Kystkjøring", "Fiske-lunsj", "Havmurer"], en: ["Coast drive", "Fish lunch", "Sea walls"], fr: ["Route côtière", "Déjeuner poissons", "Remparts"] },
stay: "Riad El Fenn", icon: "Sun",
},
imperial: {
title: { no: "Imperialbyen Fes", en: "Imperial Fes", fr: "Fès, ville impériale" },
desc: {
no: "Full guidet dag i verdens største bilfrie medina – garveriene, madrasaene, håndverkerne.",
en: "Full guided day in the world's largest car-free medina — tanneries, madrasas, artisans.",
fr: "Journée guidée dans la plus grande médina piétonne du monde — tanneries, médersas, artisans.",
},
chips: { no: ["Garverier", "Madrasaer", "Håndverkere"], en: ["Tanneries", "Madrasas", "Artisans"], fr: ["Tanneries", "Médersas", "Artisans"] },
stay: "Riad Fes", icon: "Compass",
},
pool: {
title: { no: "Bassengdag på Beldi", en: "Pool day at Beldi", fr: "Journée piscine à Beldi" },
desc: {
no: "Tre basseng omgitt av olivenlunder og rosenhager. Lang, lat lunsj.",
en: "Three pools surrounded by olive groves and rose gardens. Long, lazy lunch.",
fr: "Trois piscines entre oliviers et roseraies. Long déjeuner langoureux.",
},
chips: { no: ["3 basseng", "Olivenhage", "Lang lunsj"], en: ["3 pools", "Olive groves", "Long lunch"], fr: ["3 piscines", "Oliviers", "Long déjeuner"] },
stay: "Riad El Fenn", icon: "Sun",
},
departure: {
title: { no: "Avreise & farvel", en: "Departure & farewell", fr: "Départ & au revoir" },
desc: {
no: "Siste shopping, eller en time på taket før privat transfer til flyplassen.",
en: "Last-minute shopping, or an hour on the rooftop before a private transfer to the airport.",
fr: "Derniers achats ou une heure sur le toit avant le transfert privé à l'aéroport.",
},
chips: { no: ["Fri tid", "Sjåfør", "Flyplass"], en: ["Free time", "Driver", "Airport"], fr: ["Temps libre", "Chauffeur", "Aéroport"] },
stay: "—", icon: "Plane",
},
};
const FLOAT_EMOJIS = ['✈️','🐪','🌅','🕌','🏔️','🎈','🛍️','🌴','⭐','🌊','🍵','🔥','📸','🏛️','🌺'];
function buildItinerary(days, interests) {
if (!days) return [];
const seq = ['arrival'];
if (days >= 2) seq.push('medina');
if (interests.includes('food') && days >= 2) seq.push('food');
if (days >= 3) seq.push('agafay');
if ((days >= 4 || interests.includes('hike')) && days >= 4) seq.push('atlas');
if (interests.includes('balloon') && days >= 4) seq.push('balloon');
if (interests.includes('photo') && days >= 3) seq.push('photo');
if (interests.includes('spa') && days >= 3) seq.push('spa');
if (interests.includes('shop') && days >= 3) seq.push('shopping');
if (interests.includes('quad') && days >= 4) seq.push('quad');
if (days >= 6 || interests.includes('coast')) seq.push('essaouira');
if (days >= 7) { seq.push('sahara'); seq.push('sahara'); }
if (days >= 9 || interests.includes('imperial')) seq.push('imperial');
if (days >= 12) { seq.push('imperial'); }
if (days >= 14) { seq.push('essaouira'); seq.push('pool'); }
if (days >= 18) { seq.push('spa'); seq.push('photo'); seq.push('shopping'); }
if (days >= 22) { seq.push('balloon'); seq.push('atlas'); seq.push('agafay'); }
seq.push('departure');
// Keep arrival and departure fixed; fill/trim the middle
const arr = seq[0];
const dep = seq[seq.length - 1];
let middle = seq.slice(1, -1);
const filler = ['medina', 'pool', 'spa', 'food', 'shopping', 'photo', 'agafay', 'atlas'];
let fi = 0;
while (middle.length < days - 2) { middle.push(filler[fi % filler.length]); fi++; }
if (middle.length > days - 2) middle = middle.slice(0, days - 2);
return [arr, ...middle, ...(days > 1 ? [dep] : [])].map(key => ({ key }));
}
// ───────────────────────────────────────────────────────────────
// Booking.com-style two-month range calendar.
// Hides past dates. Click start → click end. Click again to reset.
// ───────────────────────────────────────────────────────────────
function RangeCalendar({ start, end, onChange, lang = 'no' }) {
const today = new Date(); today.setHours(0, 0, 0, 0);
const initial = start ? new Date(start) : today;
const [viewMonth, setViewMonth] = useSF(new Date(initial.getFullYear(), initial.getMonth(), 1));
const [hover, setHover] = useSF(null);
const monthName = (d) => d.toLocaleDateString(
lang === 'no' ? 'no-NO' : lang === 'fr' ? 'fr-FR' : 'en-GB',
{ month: 'long', year: 'numeric' }
);
const weekdayLabels = lang === 'no'
? ['M','T','O','T','F','L','S']
: lang === 'fr' ? ['L','M','M','J','V','S','D']
: ['M','T','W','T','F','S','S'];
const buildMonth = (anchor) => {
const first = new Date(anchor.getFullYear(), anchor.getMonth(), 1);
// Monday-first
const offset = (first.getDay() + 6) % 7;
const daysInMonth = new Date(anchor.getFullYear(), anchor.getMonth() + 1, 0).getDate();
const cells = [];
for (let i = 0; i < offset; i++) cells.push(null);
for (let d = 1; d <= daysInMonth; d++) {
cells.push(new Date(anchor.getFullYear(), anchor.getMonth(), d));
}
return cells;
};
const fmt = (d) => d ? d.toISOString().slice(0, 10) : '';
const startD = start ? new Date(start) : null;
const endD = end ? new Date(end) : null;
const hoverD = hover ? new Date(hover) : null;
const cellState = (d) => {
if (!d) return '';
if (d < today) return 'past';
const s = fmt(d);
if (startD && fmt(startD) === s) return 'start';
if (endD && fmt(endD) === s) return 'end';
if (startD && endD && d > startD && d < endD) return 'in';
if (startD && !endD && hoverD && d > startD && d <= hoverD) return 'in-hover';
return '';
};
const handleClick = (d) => {
if (!d || d < today) return;
if (!startD || (startD && endD)) {
onChange(fmt(d), '');
} else if (d < startD) {
onChange(fmt(d), '');
} else {
onChange(fmt(startD), fmt(d));
}
};
const renderMonth = (anchor) => {
const cells = buildMonth(anchor);
return (
{monthName(anchor)}
{weekdayLabels.map((w, i) =>
{w}
)}
{cells.map((d, i) => {
const state = cellState(d);
return (
handleClick(d)}
onMouseEnter={() => d && setHover(fmt(d))}
onMouseLeave={() => setHover(null)}>
{d ? d.getDate() : ''}
);
})}
);
};
const next = new Date(viewMonth.getFullYear(), viewMonth.getMonth() + 1, 1);
return (
{
const prev = new Date(viewMonth.getFullYear(), viewMonth.getMonth() - 1, 1);
if (prev >= new Date(today.getFullYear(), today.getMonth(), 1)) setViewMonth(prev);
}}>‹
setViewMonth(new Date(viewMonth.getFullYear(), viewMonth.getMonth() + 1, 1))
}>›
{renderMonth(viewMonth)}
{renderMonth(next)}
);
}
function ItineraryBuilder() {
const { useT, useMS, usePrice, COMPANY } = window.MS_CTX;
const t = useT();
const ctx = useMS();
const price = usePrice();
const [data, setData] = useSF({
duration: 0,
travellers: { adults: 0, children: 0, infants: 0 },
accommodation: '',
pace: '',
interests: [],
occasion: '',
avoid: '',
notes: '',
budget: '',
startDate: '',
flex: 'flex3',
name: '',
email: '',
phone: '',
country: '',
bookedAccom: false,
bookedAccomAddr: '',
bookedTransport: false,
bookedActivities: false,
endDate: '',
arriveCity: '',
departCity: '',
multiCity: false,
stops: [],
tripType: '',
transport: '',
vanSeats: 7,
rentalCar: '',
flightBooked: '',
flightDetails: '',
daySchedule: [],
});
// Form owns its own state — no two-way sync with global context.
// (Previous sync caused an infinite render loop because ctx.travellers
// was re-created on every parent render.)
// Auto-fill from logged-in user + saved profile so guests don't retype.
// Runs once on mount (and stays stable — guarded by the `||` chain so it
// never overwrites a value the user has already typed).
useEF(() => {
const user = window.MS_Auth_User;
let profile = {};
try { profile = JSON.parse(localStorage.getItem('ms_profile_data') || '{}'); } catch {}
if (!user && !profile.name) return;
setData(d => ({
...d,
name: d.name || profile.name || user?.name || '',
email: d.email || profile.email || user?.email || '',
phone: d.phone || profile.phone || '',
country: d.country || profile.country || (ctx.lang === 'no' ? 'Norge' : ''),
}));
}, []);
// Track which Smak categories are expanded ("first line" by default)
const [smakOpen, setSmakOpen] = useSF({});
const toggleSmak = (key) => setSmakOpen(p => ({ ...p, [key]: !p[key] }));
// Day-by-day Smak picker — current day index + per-day picks helper
const [activeDay, setActiveDay] = useSF(0);
const dayPick = (dayIdx, slot, value) => setData(p => {
const next = [...p.daySchedule];
while (next.length <= dayIdx) next.push({ activities: [], wellness: [], pool: [], restaurant: '' });
const cur = next[dayIdx];
if (slot === 'restaurant') {
next[dayIdx] = { ...cur, restaurant: cur.restaurant === value ? '' : value };
} else {
const list = cur[slot] || [];
next[dayIdx] = { ...cur, [slot]: list.includes(value) ? list.filter(x => x !== value) : [...list, value] };
}
return { ...p, daySchedule: next };
});
// Pick up booking context from itinerary "Take as-is" / Tweak handoffs
const [bookingCtx, setBookingCtx] = useSF(() => window.MS_BookingContext || null);
useEF(() => {
const sync = () => {
const c = window.MS_BookingContext;
if (!c) return;
setBookingCtx(c);
setData(d => ({
...d,
duration: c.duration || d.duration,
tripType: c.tripType || d.tripType,
// Itinerary already defines pace/interests/stay — leave them at sensible defaults
}));
// Always start at step 1 so the user reviews dates, travellers, style, etc.
setStep(0);
};
sync();
window.addEventListener('ms:booking-context', sync);
return () => window.removeEventListener('ms:booking-context', sync);
}, []);
const clearBookingCtx = () => {
window.MS_BookingContext = null;
setBookingCtx(null);
};
const upd = (k, v) => setData(p => ({ ...p, [k]: v }));
const toggle = (k, v) => setData(p => ({ ...p, [k]: p[k].includes(v) ? p[k].filter(x => x !== v) : [...p[k], v] }));
const updTrav = (k, delta) => setData(p => ({ ...p, travellers: { ...p.travellers, [k]: Math.max(0, p.travellers[k] + delta) } }));
const [step, setStep] = useSF(0);
const [sent, setSent] = useSF(false);
// Smart, condensed steps. When the user picked a ready-made itinerary,
// we already know duration/pace/interests — so we only ask for dates + contact.
const allSteps = [
{ id: 'contact', label: ctx.lang === 'no' ? 'Kontakt' : ctx.lang === 'fr' ? 'Contact' : 'Contact' },
{ id: 'when', label: ctx.lang === 'no' ? 'Når' : ctx.lang === 'fr' ? 'Quand' : 'When' },
{ id: 'who', label: ctx.lang === 'no' ? 'Hvem reiser?' : ctx.lang === 'fr' ? 'Qui voyage ?' : 'Who is going?' },
{ id: 'style', label: ctx.lang === 'no' ? 'Stil' : ctx.lang === 'fr' ? 'Style' : 'Style' },
{ id: 'taste', label: ctx.lang === 'no' ? 'Smak' : ctx.lang === 'fr' ? 'Goûts' : 'Taste' },
{ id: 'extra', label: ctx.lang === 'no' ? 'Det lille ekstra' : ctx.lang === 'fr' ? 'Le petit plus' : 'Little extras' },
{ id: 'send', label: ctx.lang === 'no' ? 'Send' : ctx.lang === 'fr' ? 'Envoyer' : 'Send' },
];
const steps = allSteps;
const next = () => setStep(s => Math.min(s + 1, steps.length - 1));
const prev = () => setStep(s => Math.max(s - 1, 0));
const cid = steps[Math.min(step, steps.length - 1)]?.id || 'when';
const show = (...ids) => ids.includes(cid);
const itinerary = useMF(() => buildItinerary(data.duration, data.interests), [data.duration, data.interests]);
const generatePDF = () => {
const totalPax = data.travellers.adults + data.travellers.children + data.travellers.infants;
const endDate = (() => {
const d = new Date(data.startDate);
d.setDate(d.getDate() + data.duration - 1);
return d.toLocaleDateString('no-NO', { day: 'numeric', month: 'long', year: 'numeric' });
})();
const startFmt = data.startDate
? new Date(data.startDate).toLocaleDateString('no-NO', { day: 'numeric', month: 'long', year: 'numeric' })
: '—';
const html = `
MarrakechStory
Premium reise i Marokko
Reiseplan — ${data.name || 'Gjest'}
${startFmt} – ${endDate} · ${data.duration} dager · ${totalPax} reisende
🏨 ${data.accommodation}
⚡ ${data.pace}
✨ ${data.budget}
Dag-for-dag
${itinerary.map((d, i) => {
const act = ACT_POOL[d.key] || ACT_POOL.medina;
const dayDate = new Date(data.startDate);
dayDate.setDate(dayDate.getDate() + i);
const dateStr = dayDate.toLocaleDateString('no-NO', { weekday: 'long', day: 'numeric', month: 'short' });
return `
${i+1}
${dateStr}
${act.title[ctx.lang]}
${act.desc[ctx.lang]}
${act.chips[ctx.lang].map(c => `${c} `).join('')}
`;
}).join('')}
${data.notes ? `
Notater: ${data.notes}
` : ''}
MarrakechStory · Marrakechstory@outlook.com · +212 6 943 45 354
www.marrakechstory.com
`;
if (window.html2pdf) {
const el = document.createElement('div');
el.innerHTML = html;
document.body.appendChild(el);
window.html2pdf().set({
margin: 0,
filename: `MarrakechStory-Reiseplan-${data.name || 'gjest'}.pdf`,
html2canvas: { scale: 2, useCORS: true },
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' },
}).from(el).save().then(() => document.body.removeChild(el));
}
};
const buildSummary = () => {
const totalPax = data.travellers.adults + data.travellers.children + data.travellers.infants;
const lines = [];
if (bookingCtx) {
lines.push(`Reise: ${bookingCtx.title} (${bookingCtx.duration} dager)`);
} else {
lines.push(`Varighet: ${data.duration} dager`);
lines.push(`Stil: ${data.accommodation} · ${data.pace} · ${data.budget}`);
if (data.interests.length) lines.push(`Interesser: ${data.interests.join(', ')}`);
}
lines.push(`Reisende: ${data.travellers.adults}v ${data.travellers.children}b ${data.travellers.infants}s (${totalPax} totalt)`);
lines.push(`Periode: ${data.startDate}${data.endDate ? ' → ' + data.endDate : ''} (${data.flex})`);
lines.push(`Fly: lander i ${data.arriveCity}, hjem fra ${data.departCity}`);
if (data.occasion) lines.push(`Anledning: ${data.occasion}`);
if (data.notes) lines.push(`Notater: ${data.notes}`);
lines.push(`Kontakt: ${data.name} · ${data.email} · ${data.phone}`);
return lines.join('\n');
};
const sendWhatsapp = () => {
if (!window.MS_Auth_User && window.MS_Auth_Prompt) window.MS_Auth_Prompt('register');
const msg = encodeURIComponent(
(ctx.lang === 'no' ? 'Hei Marrakech Story! ' : 'Hi Marrakech Story! ') +
(ctx.lang === 'no' ? 'Jeg vil booke:\n\n' : 'I would like to book:\n\n') +
buildSummary()
);
try {
const prev = JSON.parse(localStorage.getItem('ms_requests') || '[]');
prev.unshift({ when: new Date().toISOString(), via: 'whatsapp', ctx: bookingCtx?.title || null, data });
localStorage.setItem('ms_requests', JSON.stringify(prev.slice(0, 20)));
} catch {}
// Fire-and-forget persistence to Supabase
if (window.MS_submitForm) {
window.MS_submitForm('itinerary', { ...data, bookingCtx }, { via: 'whatsapp' });
}
window.open(`https://wa.me/212698164331?text=${msg}`, '_blank');
setSent(true);
};
const send = () => {
// Prompt account creation if not logged in
if (!window.MS_Auth_User && window.MS_Auth_Prompt) {
window.MS_Auth_Prompt('register');
}
// Generate PDF
generatePDF();
const totalPax = data.travellers.adults + data.travellers.children + data.travellers.infants;
const body = encodeURIComponent(
`Hei Marrakech Story,\n\nJeg vil planlegge en reise — detaljer fra reiseplanleggeren:\n\n` +
`— VARIGHET —\n${data.duration} dager\n\n` +
`— REISENDE —\n${data.travellers.adults} voksne · ${data.travellers.children} barn · ${data.travellers.infants} spedbarn (${totalPax} totalt)\n\n` +
`— DATOER —\nStart: ${data.startDate} (${data.flex})\n\n` +
`— STIL —\nOvernatting: ${data.accommodation}\nTempo: ${data.pace}\nBudsjett: ${data.budget}\n\n` +
`— INTERESSER —\n${data.interests.join(', ') || 'åpent'}\n\n` +
`— ANLEDNING —\n${data.occasion || '—'}\n\n` +
`— UNNGÅ —\n${data.avoid || '—'}\n\n` +
`— NOTATER —\n${data.notes || '—'}\n\n` +
`— UTKAST REISEPLAN —\n${itinerary.map((d, i) => `Dag ${i + 1}: ${(ACT_POOL[d.key]?.title?.[ctx.lang]) || d.key}`).join('\n')}\n\n` +
`— KONTAKT —\n${data.name}\n${data.email}\n${data.phone}\n${data.country}\n\nGleder meg til å høre fra dere!\n`
);
const subject = encodeURIComponent(`Ny reiseforespørsel — ${data.name || 'gjest'} · ${data.duration} dager`);
// Fire-and-forget persistence to Supabase
if (window.MS_submitForm) {
window.MS_submitForm('itinerary', { ...data, bookingCtx }, { via: 'email' });
}
window.location.href = `mailto:${COMPANY.email}?subject=${subject}&body=${body}`;
setSent(true);
};
const OptCard = ({ field, value, ttl, sub, ico, multi }) => {
const isActive = multi ? data[field].includes(value) : data[field] === value;
return (
multi ? toggle(field, value) : upd(field, value)}>
{ico && {ico} }
{ttl}
{sub && {sub} }
);
};
const durLabel = (n, lang) => {
if (lang === 'no') return n === 1 ? 'dag' : 'dager';
if (lang === 'fr') return n === 1 ? 'jour' : 'jours';
return n === 1 ? 'day' : 'days';
};
const presets = [
{ d: 4, label: { no: '4d · Lang helg', en: '4d · Weekend', fr: '4j · Weekend' } },
{ d: 7, label: { no: '7d · Klassisk', en: '7d · Classic', fr: '7j · Classique' } },
{ d: 10, label: { no: '10d · Premium', en: '10d · Premium', fr: '10j · Premium' } },
{ d: 14, label: { no: '14d · Grand Tour', en: '14d · Grand Tour', fr: '14j · Grand Tour' } },
{ d: 21, label: { no: '21d · Utvidet', en: '21d · Extended', fr: '21j · Étendu' } },
{ d: 30, label: { no: '30d · Hele Marokko', en: '30d · Full Morocco', fr: '30j · Maroc entier' } },
];
return (
{t('itin_eyebrow')}
{t('itin_title_a')} {t('itin_title_b')} {t('itin_title_c')}
{t('itin_sub')}
{window.MS_FavouritesQuickAdd &&
}
{/* LEFT: form */}
{!sent && bookingCtx && (
{ctx.lang === 'no' ? 'Du bestiller:' : ctx.lang === 'fr' ? 'Vous réservez :' : 'You are booking:'}
{bookingCtx.title}
{bookingCtx.duration} {ctx.lang === 'no' ? 'dager' : ctx.lang === 'fr' ? 'jours' : 'days'}
{bookingCtx.priceEur ? ` · ${price(bookingCtx.priceEur * 1.4)}` : ''}
✕
)}
{!sent && (
<>
{steps.map((s, i) => (
setStep(i)}>
{i < step ? : i + 1}
{s.label}
))}
{show('when') && (
{ctx.lang === 'no' ? 'Velg periode' : ctx.lang === 'fr' ? 'Choisissez la période' : 'Choose your period'}
{
upd('startDate', s);
upd('endDate', e);
ctx.setDates({ ...ctx.dates, dep: s });
if (s && e) {
const diff = Math.max(1, Math.round((new Date(e) - new Date(s)) / 86400000) + 1);
upd('duration', diff);
}
}}
/>
{data.startDate && data.endDate && (
{data.duration} {ctx.lang === 'no' ? (data.duration === 1 ? 'dag' : 'dager') : ctx.lang === 'fr' ? 'jours' : 'days'}
{' · '}
{Math.max(0, data.duration - 1)} {ctx.lang === 'no' ? 'netter' : ctx.lang === 'fr' ? 'nuits' : 'nights'}
)}
{/* Multi-city — only if longer than 4 days */}
{data.startDate && data.endDate && data.duration > 4 && (() => {
const cityOptions = ['Marrakech', 'Essaouira', 'Fes', 'Casablanca', 'Chefchaouen', 'Rabat', 'Tangier', 'Agadir', 'Merzouga (Sahara)', 'Ouarzazate', 'Ait Ben Haddou', 'Atlas Mountains'];
const totalNights = Math.max(0, data.duration - 1);
const used = data.stops.reduce((s, x) => s + (parseInt(x.nights) || 0), 0);
const remaining = totalNights - used;
return (
{ctx.lang === 'no' ? 'Ønsker du å besøke flere byer?' : ctx.lang === 'fr' ? 'Visiter plusieurs villes ?' : 'Visit multiple cities?'}
setData(p => ({ ...p, multiCity: false, stops: [{ city: 'Marrakech', nights: totalNights }] }))}>
{ctx.lang === 'no' ? 'Kun Marrakech' : ctx.lang === 'fr' ? 'Marrakech seulement' : 'Marrakech only'}
setData(p => ({ ...p, multiCity: true }))}>
{ctx.lang === 'no' ? 'Flere byer' : ctx.lang === 'fr' ? 'Plusieurs villes' : 'Multi-city'}
{data.multiCity && (
{data.stops.map((s, i) => (
setData(p => ({ ...p, stops: p.stops.map((x, j) => j === i ? { ...x, city: e.target.value } : x) }))}>
{cityOptions.map(c => {c} )}
setData(p => ({ ...p, stops: p.stops.map((x, j) => j === i ? { ...x, nights: Math.max(0, (parseInt(x.nights) || 0) - 1) } : x) }))
}>−
{s.nights} {ctx.lang === 'no' ? 'n' : 'n'}
setData(p => ({ ...p, stops: p.stops.map((x, j) => j === i ? { ...x, nights: (parseInt(x.nights) || 0) + 1 } : x) }))
}>+
{data.stops.length > 1 && (
setData(p => ({ ...p, stops: p.stops.filter((_, j) => j !== i) }))
}>✕
)}
))}
setData(p => ({ ...p, stops: [...p.stops, { city: 'Essaouira', nights: 1 }] }))
}>
+ {ctx.lang === 'no' ? 'Legg til by' : ctx.lang === 'fr' ? 'Ajouter une ville' : 'Add city'}
{ctx.lang === 'no'
? `${used} / ${totalNights} netter fordelt`
: ctx.lang === 'fr'
? `${used} / ${totalNights} nuits réparties`
: `${used} / ${totalNights} nights allocated`}
{remaining !== 0 && (
· {remaining > 0
? (ctx.lang === 'no' ? `${remaining} igjen` : ctx.lang === 'fr' ? `${remaining} restant` : `${remaining} left`)
: (ctx.lang === 'no' ? `${-remaining} for mange` : ctx.lang === 'fr' ? `${-remaining} en trop` : `${-remaining} too many`)
}
)}
)}
);
})()}
{ctx.lang === 'no' ? 'Fly inn / fly ut' : ctx.lang === 'fr' ? 'Arrivée / départ' : 'Arrival / departure'}
{ctx.lang === 'no' ? 'Lander i' : ctx.lang === 'fr' ? 'Atterrissage à' : 'Landing in'}
upd('arriveCity', e.target.value)}>
{ctx.lang === 'no' ? 'Velg…' : ctx.lang === 'fr' ? 'Choisir…' : 'Choose…'}
Marrakech (RAK)
Casablanca (CMN)
Agadir (AGA)
Fes (FEZ)
Tangier (TNG)
Rabat (RBA)
Essaouira (ESU)
Ouarzazate (OZZ)
{ctx.lang === 'no' ? 'Reiser hjem fra' : ctx.lang === 'fr' ? 'Départ de' : 'Departing from'}
upd('departCity', e.target.value)}>
{ctx.lang === 'no' ? 'Velg…' : ctx.lang === 'fr' ? 'Choisir…' : 'Choose…'}
Marrakech (RAK)
Casablanca (CMN)
Agadir (AGA)
Fes (FEZ)
Tangier (TNG)
Rabat (RBA)
Essaouira (ESU)
Ouarzazate (OZZ)
{ctx.lang === 'no' ? 'Har dere bestilt fly?' : ctx.lang === 'fr' ? 'Avez-vous réservé le vol ?' : 'Have you booked your flight?'}
upd('flightBooked', 'yes')}>
{ctx.lang === 'no' ? 'Ja' : 'Yes'}
upd('flightBooked', 'no')}>
{ctx.lang === 'no' ? 'Nei' : 'No'}
{data.flightBooked === 'yes' && (
{ctx.lang === 'no' ? 'Flydetaljer (selskap, flynr, tider)' : ctx.lang === 'fr' ? 'Détails du vol (compagnie, n°, horaires)' : 'Flight details (airline, number, times)'}
)}
)}
{show('who') && (
{ctx.lang === 'no' ? 'Hvilken type reise?' : ctx.lang === 'fr' ? 'Quel type de voyage ?' : 'What kind of trip?'}
🧳} />
💑} />
👨👩👧} />
👥} />
🤝} />
💍} />
{t('itin_step_who')}
{[
{ k: 'adults', lbl: t('itin_adults'), sub: t('itin_adults_sub') },
{ k: 'children', lbl: t('itin_kids'), sub: t('itin_kids_sub') },
{ k: 'infants', lbl: t('itin_infants'), sub: t('itin_infants_sub') },
].map(x => (
updTrav(x.k, -1)}>
{data.travellers[x.k]}
updTrav(x.k, 1)}>
))}
{ctx.lang === 'no' ? 'Har du allerede bestilt noe?' : ctx.lang === 'fr' ? 'Avez-vous déjà réservé quelque chose ?' : 'Have you already booked anything?'}
)}
{show('style') && (
{t('itin_step_stay')}
} />
} />
} />
} />
} />
} />
{t('itin_budget')}
)}
{show('style') && (() => {
const D = window.MS_DATA || {};
const cars = D.TRANSPORT || [];
return (
{ctx.lang === 'no' ? 'Transport' : 'Transport'}
} />
} />
} />
{data.transport === 'driver-van' && (
{ctx.lang === 'no' ? 'Seter i van' : ctx.lang === 'fr' ? 'Places dans le van' : 'Van seats'}
{ctx.lang === 'no' ? '7, 9, 12, 17…' : '7, 9, 12, 17…'}
upd('vanSeats', Math.max(4, data.vanSeats - 1))}>
{data.vanSeats}
upd('vanSeats', Math.min(22, data.vanSeats + 1))}>
)}
{data.transport === 'rental' && (
{cars.map((c, i) => (
upd('rentalCar', c.name)}>
{ e.currentTarget.style.display = 'none'; }} />
{c.name}
{c.cuisine}
{c.price}
))}
)}
);
})()}
{show('style') && (
)}
{show('taste') && (() => {
const D = window.MS_DATA || {};
if (!data.duration || data.duration < 1) {
return (
{t('itin_step_int')}
{ctx.lang === 'no'
? '← Gå tilbake til Når-steget og velg en periode først, så fyller du reisen dag for dag her.'
: '← Go back to the When step and pick a date range first — then fill your trip day by day here.'}
);
}
const totalDays = data.duration;
const curDay = Math.min(activeDay, totalDays - 1);
const day = data.daySchedule[curDay] || { activities: [], wellness: [], pool: [], restaurant: '' };
const dayDate = data.startDate ? (() => {
const d = new Date(data.startDate);
d.setDate(d.getDate() + curDay);
return d.toLocaleDateString(ctx.lang === 'no' ? 'no-NO' : ctx.lang === 'fr' ? 'fr-FR' : 'en-GB', { weekday: 'short', day: 'numeric', month: 'short' });
})() : '';
const FIRST = 6;
const Chip = ({ slot, value, label, active }) => (
dayPick(curDay, slot, value)}>
{label}
);
const Section = ({ id, title, meta, items, slot, picked }) => {
const key = `${id}-${curDay}`;
const open = !!smakOpen[key];
const shown = open ? items : items.slice(0, FIRST);
const more = items.length - FIRST;
return (
{title}
{meta != null && {meta} }
{shown.map((it, i) => (
))}
{more > 0 && (
toggleSmak(key)}>
{open
? (ctx.lang === 'no' ? 'Vis færre' : 'Show less')
: (ctx.lang === 'no' ? `Vis flere (+${more})` : `Show more (+${more})`)}
)}
);
};
const restaurantStyles = [
{ v: 'r:traditional', label: ctx.lang === 'no' ? '🍲 Tradisjonell marokkansk' : '🍲 Traditional Moroccan' },
{ v: 'r:fine', label: ctx.lang === 'no' ? '🍷 Fine dining' : '🍷 Fine dining' },
{ v: 'r:rooftop', label: ctx.lang === 'no' ? '🌇 Tak / terrasse' : '🌇 Rooftop / terrasse' },
{ v: 'r:festive', label: ctx.lang === 'no' ? '🎉 Festlig' : '🎉 Festive' },
{ v: 'r:international',label: ctx.lang === 'no' ? '🌍 Internasjonal' : '🌍 International' },
{ v: 'r:asian', label: ctx.lang === 'no' ? '🥢 Asiatisk' : '🥢 Asian' },
{ v: 'r:brunch', label: ctx.lang === 'no' ? '🥐 Brunsj & kafé' : '🥐 Brunch & café' },
{ v: 'r:bar', label: ctx.lang === 'no' ? '🍸 Bar & lounge' : '🍸 Bar & lounge' },
{ v: 'r:club', label: ctx.lang === 'no' ? '🌙 Nattklubb' : '🌙 Nightclub' },
];
const acts = (D.ACTIVITIES || []).map(a => ({ v: `a:${a.name}`, label: a.name }));
const wellness = [
{ v: 'spa-hammam', label: '🛁 Hammam' },
{ v: 'spa-massage', label: ctx.lang === 'no' ? '💆 Massasje' : '💆 Massage' },
{ v: 'spa-beauty', label: ctx.lang === 'no' ? '💅 Skjønnhetssalong' : '💅 Beauty salon' },
{ v: 'spa-yoga', label: ctx.lang === 'no' ? '🧘 Yoga / meditasjon' : '🧘 Yoga / meditation' },
];
const agafayPool = [
...(D.CAMPS || []).map(c => ({ v: `c:${c.name}`, label: `🏜️ ${c.name}` })),
...(D.POOLS || []).map(p => ({ v: `p:${p.name}`, label: `☀️ ${p.name}` })),
];
return (
{ctx.lang === 'no' ? 'Fyll inn dag for dag' : ctx.lang === 'fr' ? 'Remplissez jour par jour' : 'Fill the trip day by day'}
setActiveDay(d => Math.max(0, d - 1))}
disabled={curDay === 0}>‹
{Array.from({ length: totalDays }, (_, i) => {
const dd = data.daySchedule[i];
const filled = dd && (dd.activities.length || dd.wellness.length || dd.pool.length || dd.restaurant);
return (
setActiveDay(i)}>
{ctx.lang === 'no' ? `Dag ${i + 1}` : `Day ${i + 1}`}
);
})}
setActiveDay(d => Math.min(totalDays - 1, d + 1))}
disabled={curDay >= totalDays - 1}>›
{ctx.lang === 'no' ? `Dag ${curDay + 1} av ${totalDays}` : `Day ${curDay + 1} of ${totalDays}`}
{dayDate && ` · ${dayDate}`}
);
})()}
{show('extra') && (
{t('itin_step_extra')}
{t('itin_special')}
upd('occasion', e.target.value)} placeholder={t('itin_special_ph')} />
{t('itin_avoid')}
{t('itin_notes')}
)}
{show('send') && (
{ctx.lang === 'no' ? 'Klar til å sende' : ctx.lang === 'fr' ? 'Prêt à envoyer' : 'Ready to send'}
{ctx.lang === 'no'
? 'Sjekk reiseplanen til høyre. Vi tar kontakt innen 24 timer og foredler detaljene sammen med deg.'
: ctx.lang === 'fr'
? 'Vérifiez l\'itinéraire à droite. Nous vous répondons sous 24 h pour affiner ensemble.'
: 'Review your trip on the right. We reply within 24 hours and refine the details with you.'}
{ctx.lang === 'no' ? '✅ Vi svarer innen 24 timer' : '✅ We reply within 24 hours'}
{ctx.lang === 'no' ? '✅ Ingen forskudd kreves' : '✅ No prepayment required'}
{ctx.lang === 'no' ? '✅ Du kan endre alt før bekreftelse' : '✅ You can change everything before confirming'}
)}
{show('contact') && (
)}
{t('itin_back')}
{t('itin_step')} {step + 1} {t('itin_of')} {steps.length}
{step < steps.length - 1 ? (
{t('itin_next')}
) : (
{ctx.lang === 'no' ? 'Send via WhatsApp' : ctx.lang === 'fr' ? 'Envoyer par WhatsApp' : 'Send via WhatsApp'} →
)}
>
)}
{sent && (
{t('itin_sent_title')}
{t('itin_sent_sub')}
{ setSent(false); setStep(0); }}>{t('itin_sent_again')}
)}
{/* RIGHT: animated timeline preview */}
{/* floating travel emojis background */}
{FLOAT_EMOJIS.map((e, i) => (
{e}
))}
{(() => {
const hasDates = !!(data.startDate && data.endDate);
const totalPax = data.travellers.adults + data.travellers.children + data.travellers.infants;
const hasAnyChoice = hasDates || totalPax > 0 || data.tripType || data.accommodation || data.budget || data.pace || data.transport || data.interests.length || data.multiCity || data.arriveCity || data.departCity || data.name || data.email;
const fmtDate = (d) => d ? new Date(d).toLocaleDateString(ctx.lang === 'no' ? 'no-NO' : ctx.lang === 'fr' ? 'fr-FR' : 'en-GB', { day: 'numeric', month: 'short' }) : '';
const tripLabel = { solo: ctx.lang === 'no' ? 'Solo' : 'Solo', couple: ctx.lang === 'no' ? 'Par' : 'Couple', family: ctx.lang === 'no' ? 'Familie' : 'Family', group: ctx.lang === 'no' ? 'Gruppe' : 'Group', team: 'Team building', wedding: ctx.lang === 'no' ? 'Bryllup' : 'Wedding' }[data.tripType];
const accLabel = { riad: 'Riad', luxury: ctx.lang === 'no' ? 'Luksushotell' : 'Luxury hotel', villa: ctx.lang === 'no' ? 'Privat villa' : 'Private villa', camp: ctx.lang === 'no' ? 'Ørkenleir' : 'Desert camp', mix: ctx.lang === 'no' ? 'Bland det' : 'Mix', surprise: ctx.lang === 'no' ? 'Overrask oss' : 'Surprise' }[data.accommodation];
const budgetLabel = { mid: ctx.lang === 'no' ? 'Komfort' : 'Comfort', premium: 'Premium', luxury: ctx.lang === 'no' ? 'Luksus' : 'Luxury' }[data.budget];
const paceLabel = { slow: ctx.lang === 'no' ? 'Rolig' : 'Slow', balanced: ctx.lang === 'no' ? 'Balansert' : 'Balanced', packed: ctx.lang === 'no' ? 'Pakket' : 'Packed' }[data.pace];
const transportLabel = {
'driver-sedan': ctx.lang === 'no' ? 'Sjåfør · Sedan' : 'Driver · Sedan',
'driver-van': ctx.lang === 'no' ? `Sjåfør · Van (${data.vanSeats} seter)` : `Driver · Van (${data.vanSeats} seats)`,
'rental': data.rentalCar ? `${ctx.lang === 'no' ? 'Leiebil' : 'Rental'} · ${data.rentalCar}` : (ctx.lang === 'no' ? 'Leiebil' : 'Rental car'),
}[data.transport];
const hasContact = !!(data.name || data.email || data.phone || data.country);
// Map specific lodge names → generic city/area tag for the preview
const genericStay = (key) => {
if (['atlas'].includes(key)) return ctx.lang === 'no' ? 'Lodge i Atlas' : 'Lodge in Atlas';
if (['agafay'].includes(key)) return ctx.lang === 'no' ? 'Leir i Agafay' : 'Camp in Agafay';
if (['sahara'].includes(key)) return ctx.lang === 'no' ? 'Leir i Sahara' : 'Camp in Sahara';
if (['imperial'].includes(key)) return ctx.lang === 'no' ? 'Riad i Fes' : 'Riad in Fes';
if (['essaouira'].includes(key)) return ctx.lang === 'no' ? 'Riad i Essaouira' : 'Riad in Essaouira';
return ctx.lang === 'no' ? 'Riad i Marrakech' : 'Riad in Marrakech';
};
return (
<>
{hasContact && (
{(data.name || data.email || '?')[0].toUpperCase()}
{data.name || (ctx.lang === 'no' ? 'Gjest' : 'Guest')}
{data.email && 📧 {data.email} }
{data.phone && 📞 {data.phone} }
{data.country && 🌍 {data.country} }
)}
{t('itin_preview_title')}
{hasDates ? (
<>
{data.duration} {durLabel(data.duration, ctx.lang)}
{totalPax > 0 && <> · {totalPax} {ctx.lang === 'no' ? 'reisende' : ctx.lang === 'fr' ? 'voyageurs' : 'travellers'} >}
>
) : (
{ctx.lang === 'no' ? 'Velg dato…' : ctx.lang === 'fr' ? 'Choisir la date…' : 'Pick your dates…'}
)}
{t('itin_preview_sub')}
{hasDates && (
{t('itin_preview_arrival')}
{fmtDate(data.startDate)}
{t('itin_preview_departure')}
{fmtDate(data.endDate)}
)}
{!hasAnyChoice && (
🗺️
{ctx.lang === 'no' ? 'Reiseplanen din vises her — start med å velge dato til venstre →' : ctx.lang === 'fr' ? 'Votre itinéraire apparaîtra ici — commencez par choisir une date →' : 'Your itinerary will appear here — start by picking dates on the left →'}
)}
{hasAnyChoice && (
{(data.arriveCity || data.departCity) && (
{ctx.lang === 'no' ? '✈️ Fly' : '✈️ Flights'}
{data.arriveCity || '—'} → {data.departCity || data.arriveCity || '—'}
)}
{tripLabel && (
{ctx.lang === 'no' ? '👤 Type' : '👤 Trip'}
{tripLabel}
)}
{totalPax > 0 && (
{ctx.lang === 'no' ? '🧳 Reisende' : '🧳 Travellers'}
{data.travellers.adults > 0 && `${data.travellers.adults} ${ctx.lang === 'no' ? 'voksne' : 'adults'}`}
{data.travellers.children > 0 && `, ${data.travellers.children} ${ctx.lang === 'no' ? 'barn' : 'children'}`}
{data.travellers.infants > 0 && `, ${data.travellers.infants} ${ctx.lang === 'no' ? 'spedbarn' : 'infants'}`}
)}
{accLabel && (
{ctx.lang === 'no' ? '🏨 Overnatting' : '🏨 Stay'}
{accLabel}
)}
{budgetLabel && (
{ctx.lang === 'no' ? '💎 Budsjett' : '💎 Budget'}
{budgetLabel}
)}
{transportLabel && (
{ctx.lang === 'no' ? '🚗 Transport' : '🚗 Transport'}
{transportLabel}
)}
{paceLabel && (
{ctx.lang === 'no' ? '⚡ Tempo' : '⚡ Pace'}
{paceLabel}
)}
{data.multiCity && data.stops.length > 0 && (
{ctx.lang === 'no' ? '📍 Byer' : '📍 Cities'}
{data.stops.map(s => `${s.city} (${s.nights}n)`).join(' → ')}
)}
{data.interests.length > 0 && (
{ctx.lang === 'no' ? '✨ Interesser' : '✨ Interests'}
{data.interests.length} {ctx.lang === 'no' ? 'valgt' : ctx.lang === 'fr' ? 'sélectionnés' : 'picked'}
)}
{data.occasion && (
{ctx.lang === 'no' ? '🎉 Anledning' : '🎉 Occasion'}
{data.occasion}
)}
{data.notes && (
{ctx.lang === 'no' ? '📝 Notater' : '📝 Notes'}
{data.notes}
)}
)}
{hasDates && (() => {
const stripPrefix = (v) => v.replace(/^[a-z]+:/, '');
const restLabel = (v) => ({
'r:traditional': ctx.lang === 'no' ? 'Tradisjonell marokkansk' : 'Traditional Moroccan',
'r:fine': 'Fine dining',
'r:rooftop': ctx.lang === 'no' ? 'Tak / terrasse' : 'Rooftop / terrasse',
'r:festive': ctx.lang === 'no' ? 'Festlig' : 'Festive',
'r:international': ctx.lang === 'no' ? 'Internasjonal' : 'International',
'r:asian': ctx.lang === 'no' ? 'Asiatisk' : 'Asian',
'r:brunch': ctx.lang === 'no' ? 'Brunsj / kafé' : 'Brunch / café',
'r:bar': 'Bar & lounge',
'r:club': ctx.lang === 'no' ? 'Nattklubb' : 'Nightclub',
}[v] || stripPrefix(v));
const wellnessLabel = (v) => ({
'spa-hammam': 'Hammam',
'spa-massage': ctx.lang === 'no' ? 'Massasje' : 'Massage',
'spa-beauty': ctx.lang === 'no' ? 'Skjønnhetssalong' : 'Beauty salon',
'spa-yoga': 'Yoga',
}[v] || stripPrefix(v));
const hasAnyDayPick = data.daySchedule.some(d => d && (d.activities?.length || d.wellness?.length || d.pool?.length || d.restaurant));
if (!hasAnyDayPick) {
return (
{ctx.lang === 'no' ? '→ Gå til Smak-steget og fyll inn dagene dine' : '→ Go to the Taste step to fill in your days'}
);
}
return (
{Array.from({ length: data.duration }, (_, i) => {
const day = data.daySchedule[i] || { activities: [], wellness: [], pool: [], restaurant: '' };
const isEmpty = !(day.activities.length || day.wellness.length || day.pool.length || day.restaurant);
const dayDate = new Date(data.startDate);
dayDate.setDate(dayDate.getDate() + i);
const dateStr = dayDate.toLocaleDateString(ctx.lang === 'no' ? 'no-NO' : ctx.lang === 'fr' ? 'fr-FR' : 'en-GB', { weekday: 'short', day: 'numeric', month: 'short' });
const stayKey = day.pool.find(p => p.startsWith('c:')) ? 'agafay' : 'marrakech';
return (
{isEmpty ? '·' : '✦'}
{i < data.duration - 1 &&
}
{ctx.lang === 'no' ? 'Dag' : 'Day'} {i + 1}
{dateStr}
{isEmpty ? (
{ctx.lang === 'no' ? 'Ingen valg ennå' : 'Nothing picked yet'}
) : (
<>
{day.activities.length > 0 && (
🎯 {ctx.lang === 'no' ? 'Aktiviteter' : 'Activities'}
{day.activities.map((v, j) => {stripPrefix(v)} )}
)}
{day.wellness.length > 0 && (
💆 {ctx.lang === 'no' ? 'Velvære' : 'Wellness'}
{day.wellness.map((v, j) => {wellnessLabel(v)} )}
)}
{day.pool.length > 0 && (
🏜️☀️ {ctx.lang === 'no' ? 'Agafay / Basseng' : 'Agafay / Pool'}
{day.pool.map((v, j) => {stripPrefix(v)} )}
)}
{day.restaurant && (
🍽️ {ctx.lang === 'no' ? 'Middag' : 'Dinner'}
{restLabel(day.restaurant)}
)}
>
)}
{genericStay(stayKey)}
);
})}
);
})()}
>
);
})()}
);
}
window.MS_Form = ItineraryBuilder;