Files
eventmap/js/ui/list.js
dapelza dac952fbf9 Phase 4: Filter- und Suchfunktion
Neues Modul js/filters/ mit reiner Filterlogik (filterEngine.js:
applyFilters(), getDistanceKm()) und UI (filterPanel.js) fuer alle Filter
aus anforderung.md Abschnitt 3: Kategorie, Entfernung (mit "Standort
verwenden"-Button), Alter der Kinder, Kosten, Indoor/Outdoor, 5
Ausstattungsmerkmale, Aufenthaltsdauer, Mindestbewertung sowie
Freitextsuche ueber Name/Ort/Region/Adresse. Trefferzaehler, "Filter
zuruecksetzen" und "Auf Ergebnisse zoomen" ergaenzt.

Store erweitert um updateFilters()/getFilteredPlaces() (Selector).
Karte und Liste rendern jetzt die gefilterte statt die volle
Ausflugsziel-Liste; die Karten-Rerender-Optimierung vergleicht dafuer
die Trefferliste per Id-Schluessel statt nur state.places, damit reine
Filter-/Standortaenderungen ohne Places-Aenderung erkannt werden.

Layout: Filter-Panel als aufklappbares <details> auf Mobile, eigener
Grid-Bereich neben der Karte auf Desktop. docs/plan.md entsprechend
fortgeschrieben (Phase 2-5 als umgesetzt markiert, Phase 5
Navigation war bereits Teil von Phase 2).
2026-07-24 12:21:07 +02:00

94 lines
2.9 KiB
JavaScript

// Ergebnisliste (Cards), rendert die gefilterte Trefferliste
// (EventMap.store.getFilteredPlaces()) statt state.places direkt, damit
// Kategorie-/Entfernungs-/etc.-Filter aus js/filters/ berücksichtigt werden.
// Rendering ausschließlich über textContent/DOM-APIs (el()), niemals
// innerHTML mit Place-Daten.
(function () {
const { subscribe, getState, setState, getFilteredPlaces } = EventMap.store;
const { getCategoryById } = EventMap.config;
const { getDistanceKm } = EventMap.filters;
const { el, clearChildren } = EventMap.utils;
function formatDistance(km) {
return km < 10 ? `${km.toFixed(1)} km entfernt` : `${Math.round(km)} km entfernt`;
}
function createCard(place, userLocation) {
const category = getCategoryById(place.category);
const select = () => setState({ selectedPlaceId: place.id });
const children = [
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 }),
];
const distanceKm = getDistanceKm(place, userLocation);
if (distanceKm !== null) {
children.push(el('p', { className: 'place-card__distance', text: formatDistance(distanceKm) }));
}
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();
}
},
},
},
children
);
}
function render(container, filteredPlaces, totalPlacesCount, userLocation) {
clearChildren(container);
if (!filteredPlaces || filteredPlaces.length === 0) {
const message =
totalPlacesCount > 0
? 'Keine Ausflugsziele gefunden. Filter anpassen?'
: 'Keine Ausflugsziele vorhanden.';
container.appendChild(el('p', { className: 'list-empty', text: message }));
return;
}
const list = el('ul', { className: 'place-list' });
for (const place of filteredPlaces) {
list.appendChild(createCard(place, userLocation));
}
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;
}
const renderFromState = (state) => {
render(container, getFilteredPlaces(), state.places.length, state.userLocation);
};
renderFromState(getState());
subscribe(renderFromState);
}
EventMap.ui.initList = initList;
})();