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)
78 lines
2.2 KiB
JavaScript
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;
|
|
})();
|