// ============================================
// Chatbot + Cookies banner + Profile dashboard
// All client-side, no API calls, no tracking.
// ============================================
const { useState: useStateE2, useEffect: useEffectE2, useRef: useRefE2 } = React;
// ──────────────────────────────────────────────────────────────
// COOKIE BANNER — accept / decline, persists in localStorage
// ──────────────────────────────────────────────────────────────
const COOKIE_KEY = 'ms_cookie_choice';
function CookieBanner() {
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 [open, setOpen] = useStateE2(false);
useEffectE2(() => {
if (!localStorage.getItem(COOKIE_KEY)) {
// small delay so it doesn't compete with page entry
const t = setTimeout(() => setOpen(true), 800);
return () => clearTimeout(t);
}
}, []);
const choose = (choice) => {
localStorage.setItem(COOKIE_KEY, choice);
setOpen(false);
};
if (!open) return null;
return (
{tx('We use cookies', 'Vi bruker cookies', 'Nous utilisons des cookies')}
{tx(
"We use a few small files to remember your language, your bookings, and how you use the site. You can say no — the site will still work.",
"Vi bruker noen små filer for å huske språket ditt, bestillingene dine og hvordan du bruker siden. Du kan si nei — siden fungerer fortsatt.",
"Nous utilisons quelques petits fichiers pour mémoriser votre langue, vos réservations et votre utilisation du site. Vous pouvez refuser — le site fonctionnera quand même."
)}
);
}
// ──────────────────────────────────────────────────────────────
// CHATBOT — pre-canned answers from site content
// Floating bubble on bottom-right above the Instagram widget.
// ──────────────────────────────────────────────────────────────
const FAQ = {
en: [
{ q: "How do I book a trip?", a: "Pick an itinerary you like and tap 'Plan this trip', or fill in the form at the bottom. We reply within 24 hours by email or WhatsApp." },
{ q: "What does the price include?", a: "Each itinerary lists what's included (driver, hotels, some meals) and what's not (flights, drinks, tips). Open any trip and scroll to the 'Included' box." },
{ q: "Can you change the trip for me?", a: "Yes — every itinerary is a starting point. We change hotels, pace, length and stops to fit you. Just tell us in the form." },
{ q: "How do I pay?", a: "30% to confirm the booking. The rest 30 days before you travel. We send a safe payment link." },
{ q: "Can I cancel?", a: "Free cancel up to 30 days before. 50% from 30 to 14 days. No refund inside 14 days." },
{ q: "How many people can join?", a: "Minimum 2. Children from 6 unless we say otherwise. Bigger groups — just ask." },
{ q: "Do you handle flights?", a: "We don't sell flights, but we help you find the best route and time. Direct from Oslo to Marrakech runs Nov–Apr." },
{ q: "Where are you based?", a: "In Marrakech. Norwegian-Moroccan team. We answer on WhatsApp from 09 to 22 every day: +212 698 164 331." },
{ q: "Is travel insurance included?", a: "No — we strongly suggest you take one. We can point you to a partner." },
{ q: "Do you speak Norwegian?", a: "Yes. Aladdin (the founder) is Norwegian-Moroccan. Email, WhatsApp and call in Norwegian, English or French." },
],
no: [
{ q: "Hvordan bestiller jeg en tur?", a: "Velg en reise du liker og trykk 'Planlegg denne turen', eller fyll ut skjemaet nederst. Vi svarer innen 24 timer på e-post eller WhatsApp." },
{ q: "Hva er inkludert i prisen?", a: "Hver reise viser hva som er med (sjåfør, hoteller, noen måltider) og hva som ikke er det (fly, drikke, tips). Åpne en reise og se 'Inkludert'-boksen." },
{ q: "Kan dere tilpasse reisen?", a: "Ja — hver reise er et utgangspunkt. Vi endrer hoteller, tempo, lengde og stopp etter deg. Skriv det i skjemaet." },
{ q: "Hvordan betaler jeg?", a: "30 % for å bekrefte. Resten 30 dager før avreise. Vi sender en trygg betalingslenke." },
{ q: "Kan jeg avbestille?", a: "Gratis inntil 30 dager før. 50 % fra 30 til 14 dager. Ingen refusjon innen 14 dager." },
{ q: "Hvor mange kan være med?", a: "Minimum 2. Barn fra 6 år dersom ikke annet er sagt. Større grupper — bare spør." },
{ q: "Ordner dere fly?", a: "Vi selger ikke fly, men hjelper deg finne beste rute og tid. Direkterute Oslo–Marrakech går nov–april." },
{ q: "Hvor holder dere til?", a: "I Marrakech. Norsk-marokkansk team. Vi svarer på WhatsApp 09–22 hver dag: +212 698 164 331." },
{ q: "Er reiseforsikring med?", a: "Nei — vi anbefaler sterkt at du tegner en. Vi kan peke deg mot en partner." },
{ q: "Snakker dere norsk?", a: "Ja. Aladdin (gründeren) er norsk-marokkansk. E-post, WhatsApp og samtale på norsk, engelsk eller fransk." },
],
fr: [
{ q: "Comment réserver un voyage ?", a: "Choisissez un itinéraire et appuyez sur 'Planifier ce voyage', ou remplissez le formulaire en bas. Réponse sous 24 h par e-mail ou WhatsApp." },
{ q: "Que comprend le prix ?", a: "Chaque itinéraire indique ce qui est inclus (chauffeur, hôtels, certains repas) et ce qui ne l'est pas (vols, boissons, pourboires)." },
{ q: "Pouvez-vous adapter le voyage ?", a: "Oui — chaque itinéraire est un point de départ. On change hôtels, rythme, durée, étapes." },
{ q: "Comment je paie ?", a: "30 % pour confirmer. Le solde 30 jours avant le départ. Lien de paiement sécurisé." },
{ q: "Puis-je annuler ?", a: "Annulation gratuite jusqu'à 30 jours avant. 50 % entre 30 et 14 jours. Aucun remboursement à moins de 14 jours." },
{ q: "Combien de personnes ?", a: "Minimum 2. Enfants à partir de 6 ans sauf indication. Grands groupes — demandez." },
{ q: "Gérez-vous les vols ?", a: "Non, mais nous vous aidons à trouver la meilleure route. Direct Oslo-Marrakech nov–avril." },
{ q: "Où êtes-vous ?", a: "À Marrakech. Équipe norvégienne-marocaine. WhatsApp 9 h – 22 h tous les jours : +212 698 164 331." },
{ q: "L'assurance est-elle incluse ?", a: "Non — fortement recommandée. Nous pouvons vous orienter." },
{ q: "Parlez-vous français ?", a: "Oui. Norvégien, anglais, français — par e-mail, WhatsApp ou téléphone." },
],
};
function Chatbot() {
const { useMS, COMPANY } = window.MS_CTX;
const ctx = useMS();
const lang = ctx.lang || 'no';
const tx = (en, no, fr) => lang === 'no' ? no : lang === 'fr' ? fr : en;
const [open, setOpen] = useStateE2(false);
const [messages, setMessages] = useStateE2([
{ from: 'bot', text: tx(
"Hi! I'm the Marrakechstory helper. Ask me anything — or pick a question below.",
"Hei! Jeg er Marrakechstory-hjelperen. Spør om hva som helst — eller velg et spørsmål.",
"Bonjour ! Je suis l'assistant Marrakechstory. Posez-moi une question — ou choisissez-en une."
) },
]);
const [input, setInput] = useStateE2('');
const endRef = useRefE2(null);
const faq = FAQ[lang === 'no' ? 'no' : lang === 'fr' ? 'fr' : 'en'];
useEffectE2(() => {
endRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
const send = (text) => {
if (!text || !text.trim()) return;
const userMsg = { from: 'user', text };
// Find best FAQ match by keyword overlap
const lowered = text.toLowerCase();
const scored = faq.map(item => {
const words = item.q.toLowerCase().split(/\W+/).concat(item.a.toLowerCase().split(/\W+/));
const score = words.filter(w => w.length > 3 && lowered.includes(w)).length;
return { item, score };
}).sort((a, b) => b.score - a.score);
const best = scored[0];
let reply;
if (best.score === 0) {
reply = tx(
"I'll send your question to the team — they answer within 24 h. Or WhatsApp us right away: +212 698 164 331.",
"Jeg sender spørsmålet ditt til teamet — de svarer innen 24 t. Eller WhatsApp oss med en gang: +212 698 164 331.",
"Je transmets votre question à l'équipe — réponse sous 24 h. Ou WhatsApp tout de suite : +212 698 164 331."
);
} else {
reply = best.item.a;
}
setMessages(m => [...m, userMsg, { from: 'bot', text: reply }]);
setInput('');
};
return (
<>
{open && (
{tx(
'No favourites yet. Tap the ♡ on any trip or catalogue card to save it here.',
'Ingen favoritter ennå. Trykk ♡ på en reise eller katalogkort for å lagre den her.',
'Aucun favori. Touchez ♡ sur un voyage ou une fiche pour le sauvegarder ici.'
)}
);
}
// Expose to auth.jsx via window so login can open it
window.MS_ProfilePanel = ProfilePanel;
window.MS_CookieBanner = CookieBanner;
window.MS_Chatbot = Chatbot;