Files
eventmap/js/ui/list.js
T
dapelza ee55fb1cf9 Phase 0+1: Scaffold, Datenmodell, Karte und Liste
Reines HTML5/CSS3/JavaScript ohne Build-Tooling, lauffaehig direkt per
Doppelklick auf index.html (file://, kein Server noetig):

- Datenmodell fuer Ausflugsziele mit allen Feldern aus der Anforderung,
  15 feste Kategorien inkl. Farbe/Icon
- Zentraler Pub/Sub-Store mit LocalStorage-Persistenz und
  Migrations-Grundgeruest
- Interaktive Karte (Leaflet + Leaflet.markercluster, lokal vendored)
  mit kategoriefarbenen Markern, Clustering, Popups, Geolocation-Button
- Kartengrundlage ueber CARTO-Basemaps statt tile.openstreetmap.org, da
  dessen Referer-Pflicht file://-Aufrufe blockiert
- Seed-Daten als eingebettetes Script (data/seed.js) statt JSON-Datei,
  da fetch() auf lokale Dateien unter file:// nicht zuverlaessig
  funktioniert
- Ergebnisliste mit 10 Beispiel-Ausflugszielen, sicher gerendert ueber
  textContent/DOM-APIs statt innerHTML (Vorbereitung fuer spaeteren
  KI-JSON-Import mit Fremddaten)
- Responsives Layout (Desktop nebeneinander, Mobile gestapelt)
2026-07-24 11:24:47 +02:00

78 lines
2.2 KiB
JavaScript

// Ergebnisliste (Cards), rendert aus store.state.places. Filterung folgt
// in einer späteren Phase - aktuell werden alle Places angezeigt.
// Rendering ausschließlich über textContent/DOM-APIs (el()), niemals
// innerHTML mit Place-Daten.
(function () {
const { subscribe, getState, setState } = EventMap.store;
const { getCategoryById } = EventMap.config;
const { el, clearChildren } = EventMap.utils;
function createCard(place) {
const category = getCategoryById(place.category);
const select = () => setState({ selectedPlaceId: place.id });
return el(
'li',
{
className: 'place-card',
attrs: { tabindex: '0', role: 'button' },
dataset: { placeId: place.id },
on: {
click: select,
keydown: (event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
select();
}
},
},
},
[
el('h3', { className: 'place-card__name', text: place.name }),
el('span', {
className: 'place-card__category',
style: { backgroundColor: category.color },
text: `${category.icon} ${category.label}`,
}),
el('p', { className: 'place-card__desc', text: place.shortDescription }),
]
);
}
function render(container, places) {
clearChildren(container);
if (!places || places.length === 0) {
container.appendChild(
el('p', { className: 'list-empty', text: 'Keine Ausflugsziele vorhanden.' })
);
return;
}
const list = el('ul', { className: 'place-list' });
for (const place of places) {
list.appendChild(createCard(place));
}
container.appendChild(list);
}
/** Initialisiert die Liste im Element mit der übergebenen id und abonniert den Store. */
function initList(containerId) {
const container = document.getElementById(containerId);
if (!container) {
console.error(`EventMap: List-Container #${containerId} nicht gefunden.`);
return;
}
render(container, getState().places);
subscribe((state) => {
render(container, state.places);
});
}
EventMap.ui.initList = initList;
})();