urlMeteor = "https://docs.google.com/spreadsheets/d/1-qndigtab0fshihgIM3E_KIGM17oYKMIz1W5Z1UnriI/export?format=csv&gid=0"bierkarte
Le prix de la pinte de pils dans les bars de Strasbourg. Plutôt demi ? Clique sur un bar pour connaître le prix.
viewof barRecherche = {
// --------------------------------------------------
// Bars uniques et triés
// --------------------------------------------------
const bars = [...new Set(
dataMeteor
.map(d => d.bar)
.filter(Boolean)
)].sort((a, b) => a.localeCompare(b, "fr"));
// --------------------------------------------------
// Fonction : aller au bar sélectionné
// --------------------------------------------------
function allerAuBar(nomBar) {
const bar = dataMeteor.find(d => d.bar === nomBar);
if (!bar) return;
const marker = map.barMarkers?.[bar.bar];
const leafletMap = map.leafletMap;
if (!marker || !leafletMap) return;
const lat = +bar.latitude;
const lon = +bar.longitude;
if (!Number.isFinite(lat) || !Number.isFinite(lon)) return;
// Stoppe une éventuelle animation précédente
leafletMap.stop();
// Ouvre le popup uniquement à la fin du déplacement
leafletMap.once("moveend", () => {
marker.openPopup();
});
leafletMap.flyTo(
[lat, lon],
17,
{
duration: 0.8,
easeLinearity: 0.25
}
);
}
// --------------------------------------------------
// Champ de recherche
// --------------------------------------------------
const input = html`<input
type="text"
placeholder="🔍 Rechercher un bar..."
autocomplete="off"
spellcheck="false"
style="
padding: 0.5em 2.2em 0.5em 0.8em;
width: min(300px, calc(100vw - 30px));
height: 38px;
border: 1px solid #ccc;
border-radius: 8px;
font-size: 14px;
box-sizing: border-box;
outline: none;
background: white;
"
>`;
// --------------------------------------------------
// Bouton effacer
// --------------------------------------------------
const clearButton = html`<button
type="button"
aria-label="Effacer la recherche"
title="Effacer"
style="
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
border: none;
background: transparent;
color: #999;
font-size: 18px;
line-height: 1;
cursor: pointer;
padding: 2px 4px;
display: none;
z-index: 2;
"
>×</button>`;
// --------------------------------------------------
// Liste des résultats
// --------------------------------------------------
const list = html`<div style="
position: fixed;
max-height: 220px;
overflow-y: auto;
overflow-x: hidden;
background: white;
border: 1px solid #ddd;
border-radius: 0 0 8px 8px;
box-shadow: 0 4px 10px rgba(0,0,0,0.10);
display: none;
z-index: 10000;
box-sizing: border-box;
"></div>`;
document.body.appendChild(list);
// --------------------------------------------------
// Wrapper
// --------------------------------------------------
const wrapper = html`<div style="
width: min(300px, calc(100vw - 30px));
margin: 0.5em 0;
position: relative;
">
${input}
${clearButton}
</div>`;
// --------------------------------------------------
// Positionnement intelligent de la liste
// --------------------------------------------------
function positionList() {
const rect = input.getBoundingClientRect();
const gap = 4;
const viewport = window.visualViewport;
const viewportHeight = viewport
? viewport.height
: window.innerHeight;
// ------------------------------------------------
// Position horizontale
// ------------------------------------------------
/*
* On part de la position réelle du champ.
*
* Les 8 px permettent de garder une petite marge
* par rapport aux bords de l'écran.
*/
const margin = 8;
const left = Math.max(
margin,
Math.min(
rect.left,
window.innerWidth - rect.width - margin
)
);
list.style.left = `${left}px`;
list.style.width = `${rect.width}px`;
// ------------------------------------------------
// Espace disponible verticalement
// ------------------------------------------------
const spaceBelow =
viewportHeight - rect.bottom - gap;
const spaceAbove =
rect.top - gap;
const desiredHeight = 220;
// ------------------------------------------------
// Liste sous le champ
// ------------------------------------------------
if (
spaceBelow >= 120 ||
spaceBelow >= spaceAbove
) {
list.style.top =
`${rect.bottom + gap}px`;
list.style.bottom = "auto";
list.style.maxHeight =
`${Math.max(
80,
Math.min(
desiredHeight,
spaceBelow
)
)}px`;
list.style.borderRadius =
"0 0 8px 8px";
}
// ------------------------------------------------
// Liste au-dessus du champ
// ------------------------------------------------
else {
list.style.top = "auto";
list.style.bottom =
`${window.innerHeight - rect.top + gap}px`;
list.style.maxHeight =
`${Math.max(
80,
Math.min(
desiredHeight,
spaceAbove
)
)}px`;
list.style.borderRadius =
"8px 8px 0 0";
}
}
// --------------------------------------------------
// Bouton effacer
// --------------------------------------------------
function updateClearButton() {
clearButton.style.display =
input.value.trim()
? "block"
: "none";
}
// --------------------------------------------------
// Affichage des résultats
// --------------------------------------------------
function render(filter = "") {
list.innerHTML = "";
const query =
filter.trim().toLowerCase();
// Pas de texte
if (!query) {
list.style.display = "none";
return;
}
// Recherche
const matches = bars
.filter(bar =>
bar.toLowerCase().includes(query)
)
.slice(0, 8);
// Aucun résultat
if (matches.length === 0) {
list.style.display = "none";
return;
}
// Positionnement avant affichage
positionList();
list.style.display = "block";
// ------------------------------------------------
// Création des résultats
// ------------------------------------------------
matches.forEach(name => {
const item = html`<div style="
padding: 0.55em 0.8em;
cursor: pointer;
font-size: 14px;
border-bottom: 1px solid #f0f0f0;
color: #333;
background: white;
box-sizing: border-box;
">${name}</div>`;
// Survol desktop
item.onmouseenter = () => {
item.style.background = "#f4f4f4";
};
item.onmouseleave = () => {
item.style.background = "white";
};
// ------------------------------------------------
// Sélection
// ------------------------------------------------
item.onclick = () => {
input.value = name;
list.style.display = "none";
updateClearButton();
allerAuBar(name);
};
list.appendChild(item);
});
// Recalcul après création des résultats
positionList();
}
// --------------------------------------------------
// Saisie
// --------------------------------------------------
input.oninput = () => {
render(input.value);
updateClearButton();
};
// --------------------------------------------------
// Focus
// --------------------------------------------------
input.onfocus = () => {
render(input.value);
updateClearButton();
};
// --------------------------------------------------
// Perte du focus
// --------------------------------------------------
input.onblur = () => {
setTimeout(() => {
list.style.display = "none";
}, 150);
};
// --------------------------------------------------
// Navigation clavier
// --------------------------------------------------
input.onkeydown = event => {
// ----------------------------------------------
// Entrée
// ----------------------------------------------
if (event.key === "Enter") {
const query =
input.value.trim().toLowerCase();
if (!query) return;
const match = bars.find(bar =>
bar.toLowerCase().includes(query)
);
if (match) {
input.value = match;
list.style.display = "none";
updateClearButton();
allerAuBar(match);
}
}
// ----------------------------------------------
// Échap
// ----------------------------------------------
if (event.key === "Escape") {
input.value = "";
list.style.display = "none";
updateClearButton();
input.focus();
}
};
// --------------------------------------------------
// Bouton ×
// --------------------------------------------------
clearButton.onclick = () => {
input.value = "";
list.style.display = "none";
updateClearButton();
input.focus();
};
// --------------------------------------------------
// Repositionnement
// --------------------------------------------------
function updatePosition() {
if (list.style.display === "block") {
positionList();
}
}
window.addEventListener(
"scroll",
updatePosition,
true
);
window.addEventListener(
"resize",
updatePosition
);
// --------------------------------------------------
// Gestion du clavier mobile
// --------------------------------------------------
if (window.visualViewport) {
window.visualViewport.addEventListener(
"resize",
updatePosition
);
window.visualViewport.addEventListener(
"scroll",
updatePosition
);
}
// --------------------------------------------------
// Nettoyage Observable
// --------------------------------------------------
invalidation.then(() => {
list.remove();
window.removeEventListener(
"scroll",
updatePosition,
true
);
window.removeEventListener(
"resize",
updatePosition
);
if (window.visualViewport) {
window.visualViewport.removeEventListener(
"resize",
updatePosition
);
window.visualViewport.removeEventListener(
"scroll",
updatePosition
);
}
});
// --------------------------------------------------
// Valeur initiale
// --------------------------------------------------
wrapper.value = null;
updateClearButton();
return wrapper;
}map = {
// Assurer le chargement du plugin Leaflet.Locate
await new Promise((resolve, reject) => {
if (typeof L.Control.Locate === "undefined") {
const script = document.createElement('script');
script.src = "https://cdn.jsdelivr.net/npm/leaflet.locatecontrol/dist/L.Control.Locate.min.js";
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
} else {
resolve();
}
});
const container = yield htl.html`<div style="height: 72vh;">`;
// carte
const map = L.map(container).setView([48.5839, 7.7455], 14);
// tuile Carto DB
L.tileLayer("https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png", {
attribution: '© OpenStreetMap contributors, © CartoDB',
subdomains: 'abcd',
maxZoom: 20
}).addTo(map);
// bouton pour localiser l'utilisateur
L.control.locate({
strings: {
title: "Montre-moi où je suis !"
}
}).addTo(map);
// stockage des marqueurs et de la carte pour la recherche
container.barMarkers = {};
container.leafletMap = map;
dataMeteor.forEach(bar => {
// création de l'icône avec le prix
const priceIcon = L.divIcon({
className: "price-label",
html: `
<svg width="30" height="39.75" viewBox="0 0 384 512" xmlns="http://www.w3.org/2000/svg" style="overflow: visible; fill:#ffffff; stroke:#333e48; stroke-width:20;">
<path d="M384 192c0 87.4-117 243-168.3 307.2c-12.3 15.3-35.1 15.3-47.4 0C117 435 0 279.4 0 192C0 86 86 0 192 0S384 86 384 192z"/>
</svg>
<div style="
position: absolute;
top: 68%;
left: 37%;
transform: translate(-50%, -55%);
color: #333e48;
font-weight: 700;
font-family: 'Arial Rounded MT Bold', Arial, sans-serif;
font-size: 14px;
pointer-events: none;
user-select: none;
">
${bar.pinte}
</div>
`,
iconSize: [40, 24], // taille approximative
iconAnchor: [16, 45] // Pointe de l'icone sur le point
});
const lat = parseFloat(bar.latitude);
const lon = parseFloat(bar.longitude);
if (!isNaN(lat) && !isNaN(lon)) {
const marker = L.marker([lat, lon], {icon: priceIcon}).addTo(map)
const popup = L.popup({
offset: [0, -30], // décale le popup de 30 pixels vers le haut
maxWidth: 120
}).setContent(`
<strong>${bar.bar}</strong><br>
<small style="color: #777;">${bar.adresse}</small><br>
<strong>${bar.biere}</strong><br>
Demi : <strong>${bar.demi} €</strong><br>
Pinte : <strong>${bar.pinte} €</strong><br>
<small style="color: #777;"><i>Relevé le ${bar.date}</i></small>
`);
marker.bindPopup(popup
);
// enregistrement du marqueur sous le nom du bar
container.barMarkers[bar.bar] = marker;
}
});
}D’après une enquête de terrain aussi rigoureuse que joyeuse