Sprint 12: UI-Vereinheitlichung + Läufigkeits-Tracker
- by-tabs/by-tab: einheitliche Tab/Pill-Navigation in allen Seiten - by-section-label, by-toolbar: einheitliche Section-Labels und Toolbars - Design-Tokens: fehlende --c-amber, --c-primary-soft ergänzt, Fallback-Werte entfernt - sitting.js: sitting-layout für konsistentes flush-Layout (wie walks) - Läufigkeits-Tracker: neuer Health-Tab für Hündinnen mit Zyklusvorhersage, Timeline vergangener Läufigkeiten, Erinnerungen und auto-berechnetem Nächst-Datum - emptyState-Bug: icon-Parameter muss SVG sein, nicht Icon-Name (dog/bell/warning gefixt) - SW-Cache: by-v103, APP_VER: 79
This commit is contained in:
parent
32d630d5a1
commit
b58789373c
30 changed files with 4344 additions and 523 deletions
|
|
@ -3,10 +3,19 @@
|
|||
Router, State-Management, Navigation, Initialisierung.
|
||||
============================================================ */
|
||||
|
||||
const APP_VER = '66'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||
const APP_VER = '79'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||
|
||||
const App = (() => {
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// PWA INSTALL PROMPT — frühzeitig abfangen, bevor es verloren geht
|
||||
// ----------------------------------------------------------
|
||||
let _installPrompt = null;
|
||||
window.addEventListener('beforeinstallprompt', e => {
|
||||
e.preventDefault();
|
||||
_installPrompt = e;
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// STATE — zentraler App-Zustand
|
||||
// ----------------------------------------------------------
|
||||
|
|
@ -23,23 +32,41 @@ const App = (() => {
|
|||
// 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 },
|
||||
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 },
|
||||
lost: { title: 'Verlorener Hund', module: null },
|
||||
friends: { title: 'Freunde', module: null },
|
||||
chat: { title: 'Nachrichten', module: null },
|
||||
welcome: { title: 'Willkommen', module: null },
|
||||
diary: { title: 'Tagebuch', module: null, requiresAuth: true },
|
||||
health: { title: 'Gesundheit', module: null, requiresAuth: true },
|
||||
'dog-profile': { title: 'Mein Hund', module: null, requiresAuth: true },
|
||||
map: { title: 'Karte', module: null },
|
||||
routes: { title: 'Routen', module: null },
|
||||
events: { title: 'Events', module: null },
|
||||
poison: { title: 'Giftköder-Alarm', module: null },
|
||||
walks: { title: 'Gassi-Treffen', module: null, requiresAuth: true },
|
||||
sitting: { title: 'Sitting', module: null, requiresAuth: true },
|
||||
forum: { title: 'Forum', module: null },
|
||||
wiki: { title: 'Wiki', module: null },
|
||||
knigge: { title: 'Knigge', module: null },
|
||||
movies: { title: 'Filme', module: null },
|
||||
trainingsplaene: { title: 'Trainingspläne', module: null },
|
||||
uebungen: { title: 'Übungsbibliothek', module: null },
|
||||
'erste-hilfe': { title: 'Erste Hilfe', module: null },
|
||||
settings: { title: 'Einstellungen', module: null },
|
||||
lost: { title: 'Verlorener Hund', module: null },
|
||||
friends: { title: 'Freunde', module: null, requiresAuth: true },
|
||||
chat: { title: 'Nachrichten', module: null, requiresAuth: true },
|
||||
admin: { title: 'Admin', module: null, requiresAuth: true },
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// AUTH GUARD — Login-Gate Texte pro Seite
|
||||
// ----------------------------------------------------------
|
||||
const AUTH_GATE = {
|
||||
diary: { icon: 'book-open', text: 'Halte besondere Momente mit deinem Hund fest — Spaziergänge, Meilensteine, Fotos.' },
|
||||
health: { icon: 'syringe', text: 'Impfungen, Tierarztbesuche, Medikamente und Allergien deines Hundes immer im Blick.' },
|
||||
'dog-profile':{ icon: 'paw-print', text: 'Erstelle ein Profil für deinen Hund — mit Foto, Bio und NFC-Tag.' },
|
||||
friends: { icon: 'users', text: 'Finde Hundebesitzer in deiner Nähe und baue ein Netzwerk auf.' },
|
||||
chat: { icon: 'chat-circle-dots', text: 'Schreibe direkt mit anderen Hundebesitzern.' },
|
||||
walks: { icon: 'paw-print', text: 'Organisiere Gassi-Treffen und triff andere Hunde in deiner Gegend.' },
|
||||
sitting: { icon: 'house-line', text: 'Finde einen Dogsitter oder biete selbst Sitting an.' },
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
|
@ -78,6 +105,14 @@ events: { title: 'Events', module: null },
|
|||
|
||||
async function _loadPage(pageId, params = {}) {
|
||||
const page = pages[pageId];
|
||||
|
||||
// AUTH GUARD — geschützte Seiten für nicht-eingeloggte User
|
||||
if (page.requiresAuth && !state.user) {
|
||||
const container = document.querySelector(`#page-${pageId} .page-body`);
|
||||
if (container) _renderLoginGate(container, pageId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (page.module) {
|
||||
const hasParams = params && Object.keys(params).length > 0;
|
||||
if (hasParams) {
|
||||
|
|
@ -128,6 +163,80 @@ events: { title: 'Events', module: null },
|
|||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// LOGIN GATE — wird statt Seiteninhalt angezeigt
|
||||
// ----------------------------------------------------------
|
||||
function _renderLoginGate(container, pageId) {
|
||||
const gate = AUTH_GATE[pageId] || { icon: 'lock', text: 'Dieser Bereich ist nur für angemeldete Nutzer.' };
|
||||
const title = pages[pageId]?.title || 'Dieser Bereich';
|
||||
|
||||
container.innerHTML = `
|
||||
<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;
|
||||
min-height:60vh;padding:var(--space-8) var(--space-5);text-align:center;gap:var(--space-5)">
|
||||
|
||||
<!-- Icon -->
|
||||
<div style="width:72px;height:72px;border-radius:50%;
|
||||
background:var(--c-primary-subtle);
|
||||
display:flex;align-items:center;justify-content:center">
|
||||
<svg style="width:36px;height:36px;color:var(--c-primary)" aria-hidden="true">
|
||||
<use href="/icons/phosphor.svg#${_esc(gate.icon)}"></use>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- Text -->
|
||||
<div style="max-width:300px">
|
||||
<h2 style="font-size:var(--text-xl);font-weight:var(--weight-bold);
|
||||
color:var(--c-text);margin:0 0 var(--space-2)">
|
||||
${_esc(title)}
|
||||
</h2>
|
||||
<p style="font-size:var(--text-sm);color:var(--c-text-secondary);
|
||||
line-height:1.6;margin:0">
|
||||
${_esc(gate.text)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- CTAs -->
|
||||
<div style="display:flex;flex-direction:column;gap:var(--space-3);width:100%;max-width:280px">
|
||||
<button class="btn btn-primary" id="gate-login-btn">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#sign-out"></use></svg>
|
||||
Anmelden
|
||||
</button>
|
||||
<button class="btn btn-secondary" id="gate-register-btn">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#user-plus"></use></svg>
|
||||
Kostenlos registrieren
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Hinweis was sonst frei ist -->
|
||||
<p style="font-size:var(--text-xs);color:var(--c-text-muted);margin:0">
|
||||
Karte, Wiki, Übungen, Erste Hilfe und mehr sind ohne Account zugänglich.
|
||||
</p>
|
||||
|
||||
<!-- Install-Hinweis -->
|
||||
<button id="gate-install-hint"
|
||||
style="background:none;border:none;cursor:pointer;padding:0;
|
||||
font-size:var(--text-xs);color:var(--c-primary);
|
||||
display:flex;align-items:center;gap:4px;text-decoration:underline">
|
||||
<svg style="width:13px;height:13px" aria-hidden="true">
|
||||
<use href="/icons/phosphor.svg#download-simple"></use>
|
||||
</svg>
|
||||
Ban Yaro als App installieren
|
||||
</button>
|
||||
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.querySelector('#gate-login-btn')?.addEventListener('click', () => {
|
||||
navigate('settings');
|
||||
});
|
||||
container.querySelector('#gate-register-btn')?.addEventListener('click', () => {
|
||||
navigate('settings');
|
||||
});
|
||||
container.querySelector('#gate-install-hint')?.addEventListener('click', () => {
|
||||
navigate('welcome');
|
||||
});
|
||||
}
|
||||
|
||||
function _loadScript(src) {
|
||||
// Versionierter URL damit SW/Browser-Cache bei jedem Deploy invalidiert wird
|
||||
const versioned = `${src}?v=${APP_VER}`;
|
||||
|
|
@ -153,6 +262,13 @@ events: { title: 'Events', module: null },
|
|||
return;
|
||||
}
|
||||
|
||||
// Sidebar-Logo → Willkommensseite
|
||||
if (e.target.closest('.sidebar-logo-text')) {
|
||||
navigate('welcome');
|
||||
_closeSidebar();
|
||||
return;
|
||||
}
|
||||
|
||||
// Sidebar-User (kein data-page, damit keine Aktiv-Markierung)
|
||||
if (e.target.closest('#sidebar-user')) {
|
||||
navigate('settings');
|
||||
|
|
@ -218,23 +334,31 @@ events: { title: 'Events', module: null },
|
|||
// SCHNELL-HINZUFÜGEN (+ Button)
|
||||
// ----------------------------------------------------------
|
||||
function _showQuickAdd() {
|
||||
const loggedIn = !!state.user;
|
||||
|
||||
const authBtn = (quick, cls, icon, label) => loggedIn
|
||||
? `<button class="btn ${cls} w-full" data-quick="${quick}">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#${icon}"></use></svg> ${label}
|
||||
</button>`
|
||||
: `<button class="btn ${cls} w-full" style="opacity:0.5" data-quick="auth-${quick}">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#lock"></use></svg> ${label}
|
||||
</button>`;
|
||||
|
||||
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">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#book-open"></use></svg> Tagebuch-Eintrag
|
||||
</button>
|
||||
<button class="btn btn-secondary w-full" data-quick="health">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#syringe"></use></svg> Gesundheits-Eintrag
|
||||
</button>
|
||||
${authBtn('diary', 'btn-secondary', 'book-open', 'Tagebuch-Eintrag')}
|
||||
${authBtn('health', 'btn-secondary', 'syringe', 'Gesundheits-Eintrag')}
|
||||
<button class="btn btn-danger w-full" data-quick="poison">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#warning-octagon"></use></svg> Giftköder melden
|
||||
</button>
|
||||
<button class="btn btn-nature w-full" data-quick="walk">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#paw-print"></use></svg> Gassi-Treffen erstellen
|
||||
</button>
|
||||
${authBtn('walk', 'btn-nature', 'paw-print', 'Gassi-Treffen erstellen')}
|
||||
</div>
|
||||
${!loggedIn ? `<p style="font-size:var(--text-xs);color:var(--c-text-muted);text-align:center;margin-top:var(--space-3)">
|
||||
<svg class="ph-icon" style="width:12px;height:12px" aria-hidden="true"><use href="/icons/phosphor.svg#info"></use></svg>
|
||||
Einige Funktionen erfordern einen Account.
|
||||
</p>` : ''}
|
||||
`,
|
||||
});
|
||||
|
||||
|
|
@ -248,10 +372,11 @@ events: { title: 'Events', module: null },
|
|||
// ~300ms später ein synthetisches Click-Event an derselben Position.
|
||||
// Ohne Delay trifft es das neu geöffnete Modal und schließt es sofort.
|
||||
setTimeout(() => {
|
||||
if (action.startsWith('auth-')) { navigate('settings'); return; }
|
||||
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 === 'walk') { navigate('walks'); pages['walks'].module?.openNew?.(); }
|
||||
if (action === 'walk') { navigate('walks'); pages['walks'].module?.openNew?.(); }
|
||||
}, 350);
|
||||
}, { once: true });
|
||||
}
|
||||
|
|
@ -271,6 +396,13 @@ if (action === 'walk') { navigate('walks'); pages['walks'].module?.openNew?.(
|
|||
|
||||
async function _onLoggedIn() {
|
||||
document.getElementById('sidebar-username').textContent = state.user.name;
|
||||
// Admin/Moderator-Item einblenden
|
||||
const adminItem = document.getElementById('sidebar-admin');
|
||||
if (adminItem) {
|
||||
const isMod = state.user.rolle === 'admin' || state.user.rolle === 'moderator'
|
||||
|| state.user.is_moderator;
|
||||
adminItem.style.display = isMod ? '' : 'none';
|
||||
}
|
||||
await _loadDogs();
|
||||
}
|
||||
|
||||
|
|
@ -278,8 +410,22 @@ if (action === 'walk') { navigate('walks'); pages['walks'].module?.openNew?.(
|
|||
state.user = null;
|
||||
state.dogs = [];
|
||||
state.activeDog = null;
|
||||
|
||||
// Gecachte Module geschützter Seiten leeren, damit sie beim nächsten Login
|
||||
// sauber neu initialisiert werden statt den alten Zustand zu refreshen.
|
||||
Object.entries(pages).forEach(([, page]) => {
|
||||
if (page.requiresAuth) page.module = null;
|
||||
});
|
||||
|
||||
_renderDogSwitcher();
|
||||
navigate('settings', false);
|
||||
|
||||
// Wenn aktuelle Seite geschützt ist → zu freier Seite wechseln
|
||||
if (pages[state.page]?.requiresAuth) {
|
||||
navigate('map', false);
|
||||
} else {
|
||||
// Bleib auf der Seite, zeige aber den Gate-Screen
|
||||
_loadPage(state.page);
|
||||
}
|
||||
}
|
||||
|
||||
async function _loadDogs() {
|
||||
|
|
@ -448,7 +594,8 @@ if (action === 'walk') { navigate('walks'); pages['walks'].module?.openNew?.(
|
|||
// ÖFFENTLICHE API
|
||||
// (andere Module können App.state, App.navigate etc. nutzen)
|
||||
// ----------------------------------------------------------
|
||||
return { init, navigate, state, setActiveDog, renderDogSwitcher: _renderDogSwitcher };
|
||||
return { init, navigate, state, setActiveDog, renderDogSwitcher: _renderDogSwitcher,
|
||||
getInstallPrompt: () => _installPrompt };
|
||||
|
||||
})();
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue