1const gradients = [ 2 "linear-gradient(160deg,#8FB1C4,#4E7288 60%,#2F4B5C)", 3 "linear-gradient(160deg,#E3C48B,#C99552 55%,#A9702F)", 4 "linear-gradient(160deg,#B7C9A8,#7E9C6E 55%,#5A7548)", 5 "linear-gradient(160deg,#D9B7A6,#B87F63 55%,#8F5A40)", 6 "linear-gradient(160deg,#9FB6C7,#5F84A0 55%,#3D5E77)" 7]; 8 9const SEARCH_SYNONYMS = { 10 "private-shore-excursion-villefranche-nice-eze-monaco": "shore excursion cruise ship port tender pier croisière escale bateau paquebot crucero barco puerto villefranche", 11 "private-tour-eze-haute-couture-perfume-creation-workshop-village": "perfume parfum perfume workshop atelier fragonard galimard grasse", 12 "private-tour-nice-eze-village-haute-couture-perfume-creation-workshop": "perfume parfum atelier fragonard galimard", 13 "private-tour-monaco-monte-carlo-eze-haute-couture-perfume-creation-workshop-village": "perfume parfum atelier fragonard galimard", 14 "private-tour-nice-eze-monaco-monte-carlo-optional-perfume-workshop": "perfume parfum atelier", 15 "private-half-day-tour-museum-villa-kerylos-villa-rothschild": "villa kerylos rothschild ephrussi museum musée museo beaulieu cap ferrat garden jardin", 16 "private-full-day-tour-nice-and-its-market-old-town-museum-villa-kerylos-villa-rothschild": "villa kerylos rothschild ephrussi museum musée museo cap ferrat", 17 "private-tour-nice-hidden-river-escape": "nature river rivière rÃo swim baignade hike randonnée", 18 "private-tour-saint-paul-de-vence-hidden-river-escape": "nature river rivière rÃo swim baignade hike randonnée chagall maeght", 19 "private-tour-villages-provence-gourdon-saint-paul-de-vence-tourettes-sur-loup": "provence villages village perché gourdon tourrettes", 20 "private-tour-nice-old-town-market-menton-lemon": "menton lemon citron limón italy italie garden jardin", 21 "private-tour-antibes-cannes-half-day-port-market-film-festival": "cannes croisette festival antibes picasso", 22 "private-tour-cannes-film-festival-antibes-port-saint-paul-de-vence-medieval-village": "cannes croisette festival antibes picasso saint paul", 23 "private-tour-monaco-monte-carlo-casino-panoramas": "monaco monte carlo casino grand prix palace palais", 24 "private-tour-monaco-monte-carlo-casino-eze-village-medieval": "monaco casino eze village", 25}; 26 27const EXP_BASE_PATH = (typeof EXP_LINK_PREFIX !== "undefined") ? EXP_LINK_PREFIX : "experiences/"; 28 29const grid = document.getElementById('expGrid'); 30const gridCount = document.getElementById('gridCount'); 31const emptyState = document.getElementById('emptyState'); 32const resultsPanel = document.getElementById('resultsPanel'); 33const toolbarAction = document.getElementById('clearBtn'); 34 35const RANK_LABELS = { 36 en: {1:"#1 Most Booked", 2:"#2 Most Booked", 3:"#3 Most Booked"}, 37 fr: {1:"N°1 le plus réservé", 2:"N°2 le plus réservé", 3:"N°3 le plus réservé"}, 38 es: {1:"N.º 1 más reservado", 2:"N.º 2 más reservado", 3:"N.º 3 más reservado"} 39}; 40const PRIVATE_LABEL = {en:"Private", fr:"Privé", es:"Privado"}; 41const VEHICLE_INCLUDED_LABEL = {en:"Vehicle Included", fr:"Véhicule inclus", es:"VehÃculo incluido"}; 42const PAGE_LANG_HOME = (document.documentElement.lang === "fr") ? "fr" : (document.documentElement.lang === "es") ? "es" : "en"; 43 44EXPERIENCES.forEach((exp, i) => { 45 const card = document.createElement('a'); 46 card.className = 'exp-card'; 47 card.href = EXP_BASE_PATH + exp.slug + '.html'; 48 card.dataset.locs = exp.locs.join(','); 49 card.dataset.dur = exp.dur; 50 card.dataset.rec = exp.rec ? '1' : '0'; 51 // 10/09 : le slug (mots anglais) + des synonymes multilingues entrent dans la recherche, 52 // pour que "shore", "cruise", "croisière", "villefranche", "perfume"... trouvent la bonne fiche dans les 3 langues. 53 const extra = (SEARCH_SYNONYMS[exp.slug] || '') + ' ' + exp.slug.replace(/[-.]/g, ' '); 54 card.dataset.search = (exp.title + ' ' + exp.locs.map(l=>LOC_LABELS[l]).join(' ') + ' ' + exp.tags.join(' ') + ' ' + extra).toLowerCase(); 55 56 let priceHtml; 57 if (exp.original_price) { 58 priceHtml = '<span class="price"><span style="text-decoration:line-through;color:var(--ink-soft);font-weight:500;">â¬' + exp.original_price + '</span> â¬' + exp.price + ' <span class="per-group">' + UI_STRINGS.perGroup + '</span></span>'; 59 } else if (exp.price) { 60 priceHtml = '<span class="price">â¬' + exp.price + ' <span class="per-group">' + UI_STRINGS.perGroup + '</span></span>'; 61 } else { 62 priceHtml = '<span class="price">' + UI_STRINGS.priceOnRequest + '</span>'; 63 } 64 const primaryLoc = exp.headerLoc || exp.locs[0]; 65 const photo = (typeof LOCATION_IMAGES !== "undefined") ? LOCATION_IMAGES[primaryLoc] : null; 66 const photoUrl = photo ? (photo.url.startsWith("http") ? photo.url : (typeof ASSETS_PREFIX !== "undefined" ? ASSETS_PREFIX : "assets/") + photo.url) : null; 67 const photoPos = (photo && photo.position) ? photo.position : "center";
68 const bgStyle = photo 69 ? `background-image:url('${photoUrl}'), ${gradients[i % gradients.length]};background-size:cover,cover;background-position:${photoPos},center;` 70 : `background:${gradients[i % gradients.length]};`; 71 72 let badgeHtml; 73 if (exp.rank) { 74 badgeHtml = '<span class="badge badge-rank badge-rank-' + exp.rank + '">' + RANK_LABELS[PAGE_LANG_HOME][exp.rank] + '</span>'; 75 } else if (exp.rec) { 76 badgeHtml = '<span class="badge badge-rec">' + (UI_STRINGS.recommendedBadge || 'Recommended') + '</span>'; 77 } else { 78 badgeHtml = '<span></span>'; 79 } 80 81 card.innerHTML = 82 '<div class="exp-photo" style="' + bgStyle + '">' + 83 '<span class="badge badge-tag">' + UI_STRINGS.privateTourBadge + '</span>' + 84 badgeHtml + 85 '</div>' + 86 '<div class="exp-body-compact">' + 87 '<h3 class="exp-title-clean">' + exp.title + '</h3>' + 88 '<div class="exp-meta-row"><span class="exp-duration">' + exp.durLabel + '</span>' + priceHtml + '</div>' + 89 '</div>'; 90 grid.appendChild(card); 91}); 92 93let activeLoc = 'all'; 94let activeDur = 'all'; 95let activeTag = null; 96let defaultView = true; 97let visibleCount = 6; 98let hasScrolledForSearch = false; 99 100function computeMatches(){ 101 const q = document.getElementById('searchInput').value.trim().toLowerCase(); 102 let visible = 0; 103 104 if (defaultView) { 105 const cards = Array.from(document.querySelectorAll('.exp-card')); 106 cards.forEach((card, i) => { 107 const show = i < visibleCount; 108 card.hidden = !show; 109 if (show) visible++; 110 }); 111 return visible; 112 } 113 114 document.querySelectorAll('.exp-card').forEach(card => { 115 const locs = card.dataset.locs.split(','); 116 const matchesLoc = activeLoc === 'all' || locs.includes(activeLoc); 117 const matchesDur = activeDur === 'all' || card.dataset.dur === activeDur; 118 const matchesSearch = !q || card.dataset.search.includes(q); 119 const matchesTag = !activeTag || card.dataset.search.includes(activeTag); 120 const show = matchesLoc && matchesDur && matchesSearch && matchesTag; 121 card.hidden = !show; 122 if (show) visible++; 123 }); 124 return visible; 125} 126 127function updateToolbar(visible){ 128 if (defaultView) { 129 const label = visible === 1 ? UI_STRINGS.recommendedSingular : UI_STRINGS.recommendedPlural; 130 gridCount.textContent = UI_STRINGS.resultsPrefix + ' ' + visible + ' ' + label; 131 if (visibleCount < EXPERIENCES.length) { 132 toolbarAction.hidden = false; 133 toolbarAction.textContent = UI_STRINGS.seeMore || 'See more'; 134 toolbarAction.onclick = () => { 135 visibleCount = Math.min(visibleCount + 3, EXPERIENCES.length); 136 applyFilters({scroll:false}); 137 }; 138 } else { 139 toolbarAction.hidden = true; 140 } 141 } else { 142 toolbarAction.hidden = false; 143 const label = visible === 1 ? UI_STRINGS.experienceSingular : UI_STRINGS.experiencePlural; 144 gridCount.textContent = visible + ' ' + label; 145 toolbarAction.textContent = UI_STRINGS.clearSearch; 146 toolbarAction.onclick = clearSearch; 147 } 148} 149 150function applyFilters(opts){ 151 opts = opts || {}; 152 resultsPanel.classList.add('updating'); 153 requestAnimationFrame(() => { 154 const visible = computeMatches(); 155 updateToolbar(visible); 156 emptyState.style.display = visible === 0 ? 'block' : 'none'; 157 setTimeout(() => resultsPanel.classList.remove('updating'), 30); 158 }); 159 if (opts.scroll && !hasScrolledForSearch) { 160 hasScrolledForSearch = true; 161 setTimeout(() => resultsPanel.scrollIntoView({behavior:'smooth', block:'start'}), 160); 162 } 163} 164 165function clearSearch(){ 166 document.getElementById('searchInput').value = ''; 167 activeLoc = 'all'; activeDur = 'all'; activeTag = null; 168 defaultView = true; 169 hasScrolledForSearch = false; 170 document.querySelectorAll('.chip').forEach(c => c.classList.remove('active')); 171 applyFilters({scroll:false}); 172 document.getElementById('search').scrollIntoView({behavior:'smooth', block:'start'}); 173} 174 175document.getElementById('locChips').addEventListener('click', e => { 176 if (!e.target.matches('.chip')) return; 177 document.querySelectorAll('#locChips .chip').forEach(c => c.classList.remove('active')); 178 e.target.classList.add('active'); 179 activeLoc = e.target.dataset.loc; 180 activeTag = null; 181 defaultView = false;
182 applyFilters({scroll:true}); 183}); 184 185document.getElementById('durChips').addEventListener('click', e => { 186 if (!e.target.matches('.chip')) return; 187 document.querySelectorAll('#durChips .chip').forEach(c => c.classList.remove('active')); 188 e.target.classList.add('active'); 189 activeDur = e.target.dataset.dur; 190 activeTag = null; 191 defaultView = false; 192 applyFilters({scroll:true}); 193}); 194 195document.getElementById('exploreBtn').addEventListener('click', () => { 196 defaultView = false; 197 applyFilters({scroll:true}); 198}); 199 200let typingTimer; 201document.getElementById('searchInput').addEventListener('input', () => { 202 clearTimeout(typingTimer); 203 const delay = defaultView ? 350 : 120; 204 typingTimer = setTimeout(() => { 205 defaultView = false; 206 applyFilters({scroll:true}); 207 }, delay); 208}); 209 210updateToolbar(computeMatches()); 211 212// Support ?duration=short (and similar) to auto-apply a duration filter, 213// used by the vehicle-notice modal's "show me experiences without a vehicle" link. 214(function(){ 215 const params = new URLSearchParams(window.location.search); 216 const wantedDur = params.get('duration'); 217 if (wantedDur) { 218 const chip = document.querySelector('#durChips .chip[data-dur="' + wantedDur + '"]'); 219 if (chip) chip.click(); 220 } 221})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.