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).
94 lines
2.9 KiB
JavaScript
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;
|
|
})();
|