// ============================================ // 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 ( ); })}
); }; const next = new Date(viewMonth.getFullYear(), viewMonth.getMonth() + 1, 1); return (
{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 ( ); }; 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) => ( ))}
{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?'}

{data.multiCity && (
{data.stops.map((s, i) => (
{s.nights} {ctx.lang === 'no' ? 'n' : 'n'}
{data.stops.length > 1 && ( )}
))}
{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' ? 'Har dere bestilt fly?' : ctx.lang === 'fr' ? 'Avez-vous réservé le vol ?' : 'Have you booked your flight?'}

{data.flightBooked === 'yes' && (