Files
eventmap/js/main.js
T
dapelzaandClaude Sonnet 5 2aaff8f680 Seed-Daten per Fetch von externem Server laden, UI-Sprachumschalter (DE/EN) und KI-Übersetzungs-Prompt-Generator ergänzen
- Beispieldaten werden nicht mehr eingebettet (data/seed.js entfernt),
  sondern beim ersten Start per fetch(EventMap.config.SEED_DATA_URL) von
  einem externen, selbst gehosteten Server geladen (siehe ADR 0001,
  Revision 2026-07-28).
- Neuer UI-Sprachumschalter (Deutsch/Englisch) für die Bedienoberfläche:
  js/i18n/strings.js + js/i18n/i18n.js, Sprachwahl im Menü, Persistenz über
  settings.language, Retranslation ohne Reload.
- Neuer Menüpunkt "Ausflugsziel übersetzen (KI)": erzeugt einen Prompt, der
  eine externe KI bittet, Name/Beschreibungen eines bestehenden Ortes ins
  Englische, Spanische oder Französische zu übersetzen (js/ai/translatePromptBuilder.js,
  js/ui/translatePromptGenerator.js).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-28 15:16:31 +02:00

266 lines
7.6 KiB
JavaScript

// Einstiegspunkt der Anwendung: initialisiert Store, Karte und Liste.
(function () {
const { initStore } = EventMap.store;
const {
initMap,
centerOnCoords,
requestUserLocation,
startLocationPicking,
stopLocationPicking,
isPickingLocation,
} = EventMap.map;
const {
initList,
initDetail,
initForm,
initManualLocationForm,
initFilterPanel,
initPromptGenerator,
initImport,
} = EventMap.ui;
function showFatalError(message) {
const status = document.getElementById('app-status');
const statusText = document.getElementById('app-status-text');
if (!status || !statusText) return;
statusText.textContent = message;
status.hidden = false;
}
function hideStatus() {
const status = document.getElementById('app-status');
const statusText = document.getElementById('app-status-text');
if (!status || !statusText) return;
status.hidden = true;
statusText.textContent = '';
}
// Verdrahtet den Schließen-Button der Statusmeldung unabhängig vom
// restlichen Bootstrap-Ablauf, damit er auch dann funktioniert, wenn
// bootstrap() selbst fehlschlägt und showFatalError() aus dem
// catch-Handler heraus aufgerufen wird (siehe Gitea Issue #1: die
// Fehlermeldung war bisher dauerhaft nicht schließbar).
function setupStatusClose() {
const closeBtn = document.getElementById('app-status-close');
if (!closeBtn) return;
closeBtn.addEventListener('click', hideStatus);
}
// Aktionen-Menü im Header (Hamburger): fasst die frueher direkt auf der
// Karte liegenden Buttons zusammen, damit sie nicht mehr mit dem
// Leaflet-Zoom-Regler kollidieren und auf dem Smartphone nicht die
// Karte zupflastern.
function setupMenuToggle() {
const toggleBtn = document.getElementById('menu-toggle-btn');
const menu = document.getElementById('app-menu');
if (!toggleBtn || !menu) return;
function closeMenu() {
menu.hidden = true;
toggleBtn.setAttribute('aria-expanded', 'false');
}
function openMenu() {
menu.hidden = false;
toggleBtn.setAttribute('aria-expanded', 'true');
}
toggleBtn.addEventListener('click', () => {
if (menu.hidden) {
openMenu();
} else {
closeMenu();
}
});
// Menü nach Klick auf eine der enthaltenen Aktionen automatisch
// schließen (die jeweilige Aktion selbst wurde durch den eigenen
// Klick-Handler des Buttons bereits vorher ausgelöst, da Events erst
// beim Ziel-Element und danach erst am umschließenden <nav> ankommen).
menu.addEventListener('click', (event) => {
if (event.target.closest('button')) {
closeMenu();
}
});
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape' && !menu.hidden) {
closeMenu();
toggleBtn.focus();
}
});
document.addEventListener('click', (event) => {
if (menu.hidden) return;
if (menu.contains(event.target) || toggleBtn.contains(event.target)) return;
closeMenu();
});
}
function setupLanguageSwitcher() {
const select = document.getElementById('language-select');
if (!select) return;
select.value = EventMap.i18n.getLanguage();
select.addEventListener('change', () => {
EventMap.i18n.setLanguage(select.value);
});
}
function setupGeolocationButton() {
const button = document.getElementById('locate-btn');
if (!button) return;
button.addEventListener('click', () => {
button.disabled = true;
button.textContent = EventMap.i18n.t('menu.locating');
requestUserLocation({
onSuccess: ({ lat, lng }) => {
centerOnCoords(lat, lng, 13);
button.disabled = false;
button.textContent = EventMap.i18n.t('menu.locate');
},
onError: () => {
button.disabled = false;
button.textContent = EventMap.i18n.t('menu.locate');
showFatalError(EventMap.i18n.t('status.geolocationError'));
},
});
});
}
function setupManualLocationButton() {
const button = document.getElementById('manual-location-btn');
if (!button) return;
button.addEventListener('click', () => {
EventMap.ui.openManualLocationForm();
});
}
function setupPickLocationButton() {
const button = document.getElementById('pick-location-btn');
const hint = document.getElementById('map-picking-hint');
const cancelBtn = document.getElementById('map-picking-cancel-btn');
if (!button || !hint || !cancelBtn) return;
function endPicking() {
button.textContent = EventMap.i18n.t('menu.pickLocation');
button.classList.remove('btn--active');
hint.hidden = true;
}
function beginPicking() {
button.textContent = EventMap.i18n.t('menu.pickLocationActive');
button.classList.add('btn--active');
hint.hidden = false;
startLocationPicking({
onPick: ({ lat, lng }) => {
EventMap.store.setState({ userLocation: { lat, lng } });
EventMap.store.updateSettings({ userConsentGeo: true });
endPicking();
},
onCancel: endPicking,
});
}
button.addEventListener('click', () => {
if (isPickingLocation()) {
stopLocationPicking();
endPicking();
} else {
beginPicking();
}
});
cancelBtn.addEventListener('click', () => {
stopLocationPicking();
endPicking();
});
}
function setupAddPlaceButton() {
const button = document.getElementById('add-place-btn');
if (!button) return;
button.addEventListener('click', () => {
EventMap.ui.openPlaceForm();
});
}
function setupPromptGeneratorButton() {
const button = document.getElementById('open-prompt-generator-btn');
if (!button) return;
button.addEventListener('click', () => {
EventMap.ui.openPromptGenerator();
});
}
function setupTranslatePromptGeneratorButton() {
const button = document.getElementById('open-translate-prompt-generator-btn');
if (!button) return;
button.addEventListener('click', () => {
EventMap.ui.openTranslatePromptGenerator();
});
}
function setupImportButton() {
const button = document.getElementById('open-import-btn');
if (!button) return;
button.addEventListener('click', () => {
EventMap.ui.openImport('ai');
});
}
function setupBackupButton() {
const button = document.getElementById('open-backup-btn');
if (!button) return;
button.addEventListener('click', () => {
EventMap.ui.openImport('backup');
});
}
async function bootstrap() {
EventMap.i18n.init();
setupLanguageSwitcher();
const { seedLoadFailed } = await initStore();
initMap('map');
initFilterPanel('filter-panel');
initList('place-list');
initDetail('detail-modal');
initForm('place-form-modal');
initManualLocationForm('manual-location-modal');
initPromptGenerator('prompt-generator-modal');
EventMap.ui.initTranslatePromptGenerator('translate-prompt-generator-modal');
initImport('import-modal');
setupGeolocationButton();
setupManualLocationButton();
setupPickLocationButton();
setupAddPlaceButton();
setupPromptGeneratorButton();
setupTranslatePromptGeneratorButton();
setupImportButton();
setupBackupButton();
if (seedLoadFailed) {
showFatalError(EventMap.i18n.t('status.seedLoadFailed'));
}
}
setupStatusClose();
setupMenuToggle();
bootstrap().catch((err) => {
console.error('EventMap: Initialisierung fehlgeschlagen.', err);
showFatalError(EventMap.i18n.t('status.bootstrapFailed'));
});
})();