- 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>
266 lines
7.6 KiB
JavaScript
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'));
|
|
});
|
|
})();
|