// ============================================ // 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 (
{item.description || item.desc}
{item.slogan &&{item.slogan}
} {tab === 'restaurants' && item.cuisine && ({lang === 'no' ? 'Bestill:' : lang === 'fr' ? 'À commander :' : 'What to order:'} {item.whatToOrder}
)} {item.perk && ({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 →'} )}{t('cat_sub')}
{it.desc}