// ============================================ // Catalog — 6 categories // ============================================ const { useState: useStateC, useMemo: useMemoC, useEffect: useEffectC } = React; const Ic = window.MS_I; // Strip diacritics + lowercase + hyphenate. function msSlugify(s) { return (s || '') .normalize('NFKD') .replace(/[̀-ͯ]/g, '') .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-+|-+$/g, '') .slice(0, 80); } // Tab id → asset folder. Catalog tab "spa" maps to /assets/catalog/spas/. const MS_CAT_DIR = { activities: 'activities', restaurants: 'restaurants', spa: 'spas', camps: 'camps', pools: 'pools', excursions: 'excursions', transport: 'transport', }; // Resolver: try real photo at canonical path first, then AI placeholder, then existing Unsplash URL. // Returns { primary, fallback1, fallback2, isAi }. function msResolveImg(tab, item) { const dir = MS_CAT_DIR[tab] || tab; const slug = item.slug || msSlugify(item.name); const real = `assets/catalog/${dir}/${slug}.jpg`; const placeholder = `assets/catalog/placeholder_ai/${dir}/${slug}/hero.jpg`; const remote = item.img; return { primary: real, placeholder, remote, slug }; } // Img component with onError fallback chain function ResolvedImg({ tab, item, alt = '', className = '', style = {}, srcOverride = null }) { const { primary, placeholder, remote } = msResolveImg(tab, item); const initial = srcOverride || primary; const [src, setSrc] = useStateC(initial); const [stage, setStage] = useStateC(srcOverride ? 'override' : 'primary'); // If srcOverride changes (e.g. user clicks thumbnail), update useEffectC(() => { if (srcOverride) { setSrc(srcOverride); setStage('override'); } }, [srcOverride]); const onError = () => { if (stage === 'override') { setSrc(primary); setStage('primary'); } else if (stage === 'primary') { setSrc(placeholder); setStage('placeholder'); } else if (stage === 'placeholder') { setSrc(remote); setStage('remote'); } }; const isAi = stage === 'placeholder'; const isDev = (typeof window !== 'undefined' && /localhost|127\.0\.0\.1/.test(location.hostname)); return (
{alt} {isAi && isDev && AI placeholder}
); } // Gallery — image carousel with thumbnails. Used in modal when item.images present. function ModalGallery({ tab, item, lang }) { const images = Array.isArray(item.images) && item.images.length > 0 ? item.images : null; const [active, setActive] = useStateC(0); if (!images) { // Single image — original layout return (
{item.tag || item.style || item.cuisine}
); } const total = images.length; const prev = () => setActive(a => (a - 1 + total) % total); const next = () => setActive(a => (a + 1) % total); return (
{item.tag || item.style || item.cuisine} {total > 1 && ( <>
{active + 1} / {total}
)}
{total > 1 && (
{images.map((url, i) => ( ))}
)}
); } function CatalogModal({ item, tab, onClose, lang }) { useEffectC(() => { 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 addToReservation = () => { onClose(); document.getElementById('plan')?.scrollIntoView({ behavior: 'smooth' }); }; return (
e.stopPropagation()}>
{item.rating} ({(item.reviews || 0).toLocaleString()} {lang === 'no' ? 'anmeldelser' : lang === 'fr' ? 'avis' : 'reviews'})

{item.name}

{item.area}
{item.duration &&
{item.duration}
}

{item.description || item.desc}

{item.slogan &&

{item.slogan}

} {tab === 'restaurants' && item.cuisine && (
{item.cuisine} {item.price && {item.price}}
)} {item.style && tab !== 'restaurants' && (
{item.style}
)} {item.atmosphere && (

{item.atmosphere}

)} {item.whatToOrder && (

{lang === 'no' ? 'Bestill:' : lang === 'fr' ? 'À commander :' : 'What to order:'} {item.whatToOrder}

)} {item.perk && (
{lang === 'no' ? 'Marrakech Story-fordel' : lang === 'fr' ? 'Avantage Marrakech Story' : 'Marrakech Story perk'} {item.perk}
)} {(() => { // Detect if transport is already included const haystack = [ ...(item.included || []), ...(item.practical || []), item.desc || '', item.description || '', ].join(' ').toLowerCase(); const hasTransport = /transport|transfer|pickup|round-trip|round trip|hotel pickup|driver|4×4|4x4|shuttle|inkludert.*transport|transport.*inkludert/i.test(haystack); const transportTab = tab === 'transport'; if (transportTab) return null; return (
{hasTransport ? : }
{hasTransport ? (lang === 'no' ? 'Transport inkludert' : lang === 'fr' ? 'Transport inclus' : 'Transport included') : (lang === 'no' ? 'Trenger du transport?' : lang === 'fr' ? 'Besoin d\'un transport ?' : 'Need transport?')}

{hasTransport ? (lang === 'no' ? 'Vi henter deg på hotellet og kjører deg trygt hjem.' : lang === 'fr' ? 'Nous vous prenons à l\'hôtel et vous ramenons en toute sécurité.' : 'We pick you up at your hotel and drive you back safely.') : (lang === 'no' ? 'Transport er ikke med — vi kan ordne privat sjåfør, gi oss beskjed.' : lang === 'fr' ? 'Le transport n\'est pas inclus — nous pouvons organiser un chauffeur privé, dites-le nous.' : 'Transport not included — we can arrange a private driver, just let us know.')}

{!hasTransport && ( { e.preventDefault(); onClose(); document.getElementById('plan')?.scrollIntoView({ behavior: 'smooth' }); }}> {lang === 'no' ? 'Be om transport →' : lang === 'fr' ? 'Demander un transport →' : 'Request transport →'} )}
); })()} {item.included && item.included.length > 0 && (
{lang === 'no' ? 'Inkludert' : lang === 'fr' ? 'Inclus' : 'Included'}
    {item.included.map((inc, i) =>
  • {inc}
  • )}
)} {tab === 'transport' && item.prices && item.prices.length > 0 && (
{lang === 'no' ? 'Pris' : lang === 'fr' ? 'Tarif' : 'Price'}
{item.prices[0].label} {item.prices[0].price}
)} {item.perfectFor && item.perfectFor.length > 0 && (
{lang === 'no' ? 'Perfekt for' : lang === 'fr' ? 'Idéal pour' : 'Perfect for'}
{item.perfectFor.map((pf, i) => {pf})}
)} {/* Offers section removed site-wide — cocktail / promo deals shown on request only */} {item.practical && item.practical.length > 0 && (
{lang === 'no' ? 'Praktisk info' : lang === 'fr' ? 'Infos pratiques' : 'Good to know'}
    {item.practical.map((p, i) =>
  • {p}
  • )}
)}
{item.startingPriceEur ? ( {lang === 'no' ? 'Fra' : lang === 'fr' ? 'Dès' : 'From'} €{item.startingPriceEur} {lang === 'no' ? 'per person' : lang === 'fr' ? 'par personne' : 'per person'} ) : tab !== 'transport' && ( {lang === 'no' ? 'Pris på forespørsel' : lang === 'fr' ? 'Prix sur demande' : 'Price on request'} )}
{item.sourceUrl && ( {lang === 'no' ? 'Kilde' : 'Source'} )} {lang === 'no' ? 'Book på WhatsApp' : lang === 'fr' ? 'Réserver sur WhatsApp' : 'Book on WhatsApp'} →
); } function Catalog() { const D = window.MS_DATA; const { useT, usePrice, useMS } = window.MS_CTX; const t = useT(); const price = usePrice(); const ctx = useMS(); const [tab, setTab] = useStateC('activities'); const [filter, setFilter] = useStateC('All'); const [favs, setFavs] = useStateC({}); const [modal, setModal] = useStateC(null); const [visibleCount, setVisibleCount] = useStateC(8); useEffectC(() => { setVisibleCount(8); }, [tab, filter]); const tabs = [ { id: 'activities', label: t('cat_activities'), icon: , data: D.ACTIVITIES, filters: ['All', 'Discover', 'In the Air', 'Nautical', 'Outdoor'], priceLabel: t('cat_per_person') }, { id: 'restaurants', label: t('cat_restaurants'), icon: , data: D.RESTAURANTS, filters: ['All', 'Fine Dining', 'Traditional Moroccan', 'Rooftop', 'Festive', 'International', 'Asian', 'Brunch', 'Café', 'Bar & Lounge', 'Nightclub'], priceLabel: '' }, { id: 'excursions', label: t('cat_excursions'), icon: , data: D.EXCURSIONS, filters: ['All', 'Day-trip', 'Half-day', 'Multi-day'], priceLabel: t('cat_per_person') }, { id: 'spa', label: t('cat_spa'), icon: , data: D.SPAS, filters: ['All', 'Palace Spa', 'Boutique', 'Medina Hammam', 'Wellness House', 'Medical'], priceLabel: t('cat_per_person') }, { id: 'camps', label: t('cat_camps'), icon: , data: D.CAMPS, filters: ['All', 'Day Pass', 'Overnight', 'Events'], priceLabel: t('cat_per_person') }, { id: 'pools', label: t('cat_pools'), icon: , data: D.POOLS, filters: ['All', 'Palace', 'Boutique', 'Agafay', 'Beach Club', 'Festive', 'Family', 'Women Only', 'Water Park'], priceLabel: t('cat_per_person') }, { id: 'transport', label: t('cat_transport'), icon: , data: D.TRANSPORT, filters: ['All', 'Compact', 'Compact SUV', 'Sedan', 'SUV'], priceLabel: '/ day' }, ]; const current = tabs.find(x => x.id === tab); const items = useMemoC(() => { if (filter === 'All') return current.data; return current.data.filter(i => i.filter === filter); }, [tab, filter, current]); const visibleItems = items.slice(0, visibleCount); const hasMore = visibleCount < items.length; // Sync favs with localStorage so they appear in the profile dashboard useEffectC(() => { try { const stored = JSON.parse(localStorage.getItem('ms_catalog_favs') || '{}'); setFavs(stored); } catch {} }, []); const toggleFav = (key) => setFavs(p => { const next = { ...p, [key]: !p[key] }; if (!next[key]) delete next[key]; localStorage.setItem('ms_catalog_favs', JSON.stringify(next)); return next; }); return (
{t('cat_eyebrow')}

{t('cat_title_a')} {t('cat_title_b')}{(() => { const c = t('cat_title_c'); return c && c !== 'cat_title_c' ? ` ${c}` : ''; })()}

{t('cat_sub')}

{tabs.map(x => ( ))}
{current.filters.map(f => ( ))}
{items.length} {t('cat_results')}
{tab === 'transport' && (
{ctx.lang === 'no' ? 'LANGTIDSRABATT' : ctx.lang === 'fr' ? 'REMISE LONGUE DURÉE' : 'LONG-RENTAL DISCOUNT'}
10+ {ctx.lang === 'no' ? 'dager' : ctx.lang === 'fr' ? 'jours' : 'days'}−5%
20+ {ctx.lang === 'no' ? 'dager' : ctx.lang === 'fr' ? 'jours' : 'days'}−10%
30+ {ctx.lang === 'no' ? 'dager' : ctx.lang === 'fr' ? 'jours' : 'days'}−15%
{ctx.lang === 'no' ? 'Ubegrenset kjørelengde · Gratis levering · Forsikring tilgjengelig' : ctx.lang === 'fr' ? 'Kilométrage illimité · Livraison gratuite · Assurance disponible' : 'Unlimited mileage · Free delivery · Insurance available'}
)}
{visibleItems.map((it, i) => { const key = `${tab}-${it.name}`; return (
setModal({ item: it, tab })} role="button" tabIndex={0} onKeyDown={e => e.key === 'Enter' && setModal({ item: it, tab })} style={{ cursor: 'pointer' }}>
{it.tag || it.style || it.cuisine}
{it.rating} ({(it.reviews || 0).toLocaleString()} reviews)

{it.name}

{it.area} {it.duration && {it.duration}}

{it.desc}

{tab === 'restaurants' ? it.cuisine : it.startingPriceEur ? `${ctx.lang === 'no' ? 'Fra' : ctx.lang === 'fr' ? 'Dès' : 'From'} €${it.startingPriceEur}` : tab === 'transport' ? (it.prices && it.prices[0] ? it.prices[0].price : '') : (ctx.lang === 'no' ? 'På forespørsel' : ctx.lang === 'fr' ? 'Sur demande' : 'On request')}
); })}
{hasMore && (
{visibleCount + 8 < items.length && ( )}
)}
{modal && ( setModal(null)} /> )}
); } window.MS_Catalog = Catalog;