Sprint 0: Design System, App Shell, PWA, zentrales JS-Fundament
This commit is contained in:
parent
756e17faba
commit
84f49fafcf
9 changed files with 2507 additions and 0 deletions
258
backend/static/js/app.js
Normal file
258
backend/static/js/app.js
Normal file
|
|
@ -0,0 +1,258 @@
|
|||
/* ============================================================
|
||||
BAN YARO — App Core
|
||||
Router, State-Management, Navigation, Initialisierung.
|
||||
============================================================ */
|
||||
|
||||
const App = (() => {
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// STATE — zentraler App-Zustand
|
||||
// ----------------------------------------------------------
|
||||
const state = {
|
||||
user: null, // eingeloggter User (oder null)
|
||||
dogs: [], // Hunde des Users
|
||||
activeDog: null, // aktuell gewählter Hund
|
||||
page: 'diary', // aktive Seite
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// SEITENDEFINITIONEN
|
||||
// Jede Seite: { id, title, load() }
|
||||
// load() wird beim ersten Aufruf einmalig ausgeführt
|
||||
// ----------------------------------------------------------
|
||||
const pages = {
|
||||
diary: { title: 'Tagebuch', module: null },
|
||||
health: { title: 'Gesundheit', module: null },
|
||||
'dog-profile': { title: 'Mein Hund', module: null },
|
||||
map: { title: 'Karte', module: null },
|
||||
routes: { title: 'Routen', module: null },
|
||||
places: { title: 'Orte', module: null },
|
||||
events: { title: 'Events', module: null },
|
||||
poison: { title: 'Giftköder-Alarm', module: null },
|
||||
walks: { title: 'Gassi-Treffen', module: null },
|
||||
sitting: { title: 'Sitting', module: null },
|
||||
forum: { title: 'Forum', module: null },
|
||||
wiki: { title: 'Wiki', module: null },
|
||||
knigge: { title: 'Knigge', module: null },
|
||||
movies: { title: 'Filme', module: null },
|
||||
settings: { title: 'Einstellungen', module: null },
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// ROUTER
|
||||
// ----------------------------------------------------------
|
||||
function navigate(pageId, pushHistory = true) {
|
||||
if (!pages[pageId]) return;
|
||||
|
||||
// Aktive Seite ausblenden
|
||||
document.querySelector('.page.active')?.classList.remove('active');
|
||||
document.querySelectorAll('.nav-item.active, .sidebar-item.active')
|
||||
.forEach(el => el.classList.remove('active'));
|
||||
|
||||
// Neue Seite einblenden
|
||||
document.getElementById(`page-${pageId}`)?.classList.add('active');
|
||||
|
||||
// Navigation markieren
|
||||
document.querySelectorAll(`[data-page="${pageId}"]`)
|
||||
.forEach(el => el.classList.add('active'));
|
||||
|
||||
// Header-Titel setzen
|
||||
document.getElementById('header-title').textContent = pages[pageId].title;
|
||||
|
||||
// History
|
||||
if (pushHistory) {
|
||||
history.pushState({ page: pageId }, '', `#${pageId}`);
|
||||
}
|
||||
|
||||
state.page = pageId;
|
||||
UI.scrollTop();
|
||||
|
||||
// Seiten-Modul lazy laden (einmalig)
|
||||
_loadPage(pageId);
|
||||
}
|
||||
|
||||
async function _loadPage(pageId) {
|
||||
const page = pages[pageId];
|
||||
if (page.module) {
|
||||
// Bereits geladen → nur refresh aufrufen wenn vorhanden
|
||||
page.module.refresh?.();
|
||||
return;
|
||||
}
|
||||
|
||||
const container = document.querySelector(`#page-${pageId} .page-body`);
|
||||
if (!container) return;
|
||||
|
||||
// Skeleton während Laden
|
||||
container.innerHTML = UI.skeleton(4);
|
||||
|
||||
try {
|
||||
// Seiten-Script dynamisch laden
|
||||
await _loadScript(`/js/pages/${pageId}.js`);
|
||||
const mod = window[`Page_${pageId.replace(/-/g, '_')}`];
|
||||
if (mod?.init) {
|
||||
await mod.init(container, state);
|
||||
page.module = mod;
|
||||
} else {
|
||||
// Platzhalter wenn Seite noch nicht gebaut
|
||||
container.innerHTML = UI.emptyState({
|
||||
icon: '🚧',
|
||||
title: pages[pageId].title,
|
||||
text: 'Diese Seite ist noch in Entwicklung.',
|
||||
});
|
||||
page.module = {}; // verhindert erneutes Laden
|
||||
}
|
||||
} catch {
|
||||
container.innerHTML = UI.emptyState({
|
||||
icon: '🚧',
|
||||
title: pages[pageId].title,
|
||||
text: 'Diese Seite ist noch in Entwicklung.',
|
||||
});
|
||||
page.module = {};
|
||||
}
|
||||
}
|
||||
|
||||
function _loadScript(src) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (document.querySelector(`script[src="${src}"]`)) { resolve(); return; }
|
||||
const s = document.createElement('script');
|
||||
s.src = src;
|
||||
s.onload = resolve;
|
||||
s.onerror = reject;
|
||||
document.head.appendChild(s);
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// NAVIGATION EVENTS
|
||||
// ----------------------------------------------------------
|
||||
function _bindNavigation() {
|
||||
// Bottom Nav + Sidebar Klicks
|
||||
document.addEventListener('click', e => {
|
||||
const item = e.target.closest('[data-page]');
|
||||
if (item) {
|
||||
navigate(item.dataset.page);
|
||||
return;
|
||||
}
|
||||
|
||||
// + Button
|
||||
if (e.target.closest('#nav-add')) {
|
||||
_showQuickAdd();
|
||||
}
|
||||
});
|
||||
|
||||
// Browser Back/Forward
|
||||
window.addEventListener('popstate', e => {
|
||||
const page = e.state?.page || 'diary';
|
||||
navigate(page, false);
|
||||
});
|
||||
|
||||
// Initial: URL-Hash auslesen
|
||||
const hash = location.hash.replace('#', '');
|
||||
if (hash && pages[hash]) {
|
||||
navigate(hash, false);
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// SCHNELL-HINZUFÜGEN (+ Button)
|
||||
// ----------------------------------------------------------
|
||||
function _showQuickAdd() {
|
||||
UI.modal.open({
|
||||
title: 'Was möchtest du hinzufügen?',
|
||||
body: `
|
||||
<div class="flex flex-col gap-3">
|
||||
<button class="btn btn-secondary w-full" data-quick="diary">
|
||||
📖 Tagebuch-Eintrag
|
||||
</button>
|
||||
<button class="btn btn-secondary w-full" data-quick="health">
|
||||
💉 Gesundheits-Eintrag
|
||||
</button>
|
||||
<button class="btn btn-danger w-full" data-quick="poison">
|
||||
⚠️ Giftköder melden
|
||||
</button>
|
||||
<button class="btn btn-secondary w-full" data-quick="place">
|
||||
📍 Ort hinzufügen
|
||||
</button>
|
||||
<button class="btn btn-nature w-full" data-quick="walk">
|
||||
🦮 Gassi-Treffen erstellen
|
||||
</button>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
|
||||
// Quick-Add Aktionen
|
||||
document.querySelector('#modal-container').addEventListener('click', e => {
|
||||
const btn = e.target.closest('[data-quick]');
|
||||
if (!btn) return;
|
||||
UI.modal.close();
|
||||
const action = btn.dataset.quick;
|
||||
if (action === 'diary') { navigate('diary'); pages['diary'].module?.openNew?.(); }
|
||||
if (action === 'health') { navigate('health'); pages['health'].module?.openNew?.(); }
|
||||
if (action === 'poison') { navigate('poison'); pages['poison'].module?.openNew?.(); }
|
||||
if (action === 'place') { navigate('places'); pages['places'].module?.openNew?.(); }
|
||||
if (action === 'walk') { navigate('walks'); pages['walks'].module?.openNew?.(); }
|
||||
}, { once: true });
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// AUTH
|
||||
// ----------------------------------------------------------
|
||||
async function _checkAuth() {
|
||||
try {
|
||||
const user = await API.auth.me();
|
||||
state.user = user;
|
||||
_onLoggedIn();
|
||||
} catch {
|
||||
_onLoggedOut();
|
||||
}
|
||||
}
|
||||
|
||||
function _onLoggedIn() {
|
||||
document.getElementById('sidebar-username').textContent = state.user.name;
|
||||
_loadDogs();
|
||||
}
|
||||
|
||||
function _onLoggedOut() {
|
||||
state.user = null;
|
||||
// Zeige Login wenn nötig
|
||||
// Für MVP: direkte Weiterleitung zu Einstellungen/Login
|
||||
}
|
||||
|
||||
async function _loadDogs() {
|
||||
try {
|
||||
state.dogs = await API.dogs.list();
|
||||
if (state.dogs.length > 0) {
|
||||
state.activeDog = state.dogs[0];
|
||||
}
|
||||
// Seitenmodule über neuen Hund informieren
|
||||
_notifyDogChange();
|
||||
} catch { /* kein Hund vorhanden */ }
|
||||
}
|
||||
|
||||
function _notifyDogChange() {
|
||||
// Alle geladenen Seiten-Module über Hundwechsel informieren
|
||||
Object.values(pages).forEach(p => p.module?.onDogChange?.(state.activeDog));
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// INITIALISIERUNG
|
||||
// ----------------------------------------------------------
|
||||
async function init() {
|
||||
_bindNavigation();
|
||||
await _checkAuth();
|
||||
|
||||
// Erste Seite laden (Hash oder Standard: diary)
|
||||
const startPage = location.hash.replace('#', '') || 'diary';
|
||||
navigate(pages[startPage] ? startPage : 'diary', false);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// ÖFFENTLICHE API
|
||||
// (andere Module können App.state, App.navigate etc. nutzen)
|
||||
// ----------------------------------------------------------
|
||||
return { init, navigate, state };
|
||||
|
||||
})();
|
||||
|
||||
// App starten
|
||||
document.addEventListener('DOMContentLoaded', () => App.init());
|
||||
Loading…
Add table
Add a link
Reference in a new issue