Feature: Welten-Onboarding, Wetter-Motivation, UX-Fixes (SW by-v715)
Welten (worlds.js): - Swipe-Hints beim ersten Öffnen (JETZT ← → WELT animiert, einmalig) - Kein-Hund-Onboarding: Feature-Preview-Grid statt leerer Karte - Hintergrund-Foto-Hint: Kamera-Karte wenn noch kein Tagebuchfoto - worlds-back: navigiert zu Welcome wenn kein User eingeloggt - Nach Logout: worlds-back Button sofort ausgeblendet Wetter (wetter.js): - Standort-Fehlerseite zu Motivations-Seite umgebaut - Feature-Preview: Gassi-Score, 7-Tage, Regenradar, Rekorde - CTA: Standort freigeben + Registrieren (nur für Gäste) Settings (settings.js): - Logo in Auth-Form: display:block + margin:0 auto zentriert - Header bleibt sichtbar (FAB/Zurück-Navigation funktioniert) Jobs (jobs.js): - 2-Spalten-Grid auf Mobile: auto-fit statt festes 1fr 1fr - Kein doppeltes Padding im Wrapper Backend: - weather.py, achievements.py: diary JOIN fix (d.user_id → dogs JOIN) - Neue Wetter-Badges: wetter_tapfer, jahreszeiten, schnee - Ernährungs-, Reise-, Ausgaben-Seite: diverse UX-Verbesserungen - Presse-Seite erweitert - Ban Yaro Foto-Assets (WebP + HIRES JPG)
This commit is contained in:
parent
aa4849d947
commit
55069d246b
28 changed files with 719 additions and 198 deletions
|
|
@ -125,47 +125,64 @@ window.Page_ernaehrung = (() => {
|
|||
|
||||
el.innerHTML = `
|
||||
<div style="padding:var(--space-4) 0">
|
||||
<p style="font-size:var(--text-sm);color:var(--c-text-secondary);margin-bottom:var(--space-4)">
|
||||
Berechne den täglichen Kalorienbedarf deines Hundes.
|
||||
</p>
|
||||
<style>
|
||||
.ern-pill-group { display:flex; gap:8px; flex-wrap:wrap; }
|
||||
.ern-pill {
|
||||
flex:1; min-width:0; padding:10px 8px; border-radius:12px;
|
||||
border:1.5px solid var(--c-border); background:var(--c-bg-card);
|
||||
color:var(--c-text-secondary); font-size:var(--text-xs); font-weight:600;
|
||||
cursor:pointer; text-align:center; transition:all .15s; line-height:1.3;
|
||||
}
|
||||
.ern-pill.active {
|
||||
background:var(--c-primary); color:#fff; border-color:var(--c-primary);
|
||||
}
|
||||
.ern-input-row {
|
||||
display:grid; grid-template-columns:1fr 1fr; gap:var(--space-3);
|
||||
margin-bottom:var(--space-4);
|
||||
}
|
||||
.ern-field { display:flex; flex-direction:column; gap:6px; }
|
||||
.ern-field label { font-size:var(--text-xs); color:var(--c-text-secondary); font-weight:600; text-transform:uppercase; letter-spacing:.04em; }
|
||||
.ern-field input { padding:12px 14px; border-radius:12px; border:1.5px solid var(--c-border); background:var(--c-bg-card); color:var(--c-text); font-size:var(--text-base); font-weight:700; width:100%; box-sizing:border-box; }
|
||||
.ern-section-label { font-size:var(--text-xs); color:var(--c-text-secondary); font-weight:600; text-transform:uppercase; letter-spacing:.04em; margin-bottom:8px; }
|
||||
</style>
|
||||
|
||||
<div class="by-form-group">
|
||||
<label class="by-label">Gewicht (kg)</label>
|
||||
<input id="ern-gewicht" type="number" step="0.1" min="0.5" max="100"
|
||||
class="by-input" value="${_esc(gewichtDefault)}" placeholder="z. B. 15">
|
||||
</div>
|
||||
|
||||
<div class="by-form-group">
|
||||
<label class="by-label">Alter (Jahre)</label>
|
||||
<input id="ern-alter" type="number" step="0.5" min="0" max="25"
|
||||
class="by-input" value="${_esc(alterDefault)}" placeholder="z. B. 3">
|
||||
</div>
|
||||
|
||||
<div class="by-form-group">
|
||||
<label class="by-label">Aktivität</label>
|
||||
<select id="ern-aktivitaet" class="by-select">
|
||||
<option value="gering">Gering (Couch-Hund)</option>
|
||||
<option value="normal" selected>Normal</option>
|
||||
<option value="aktiv">Aktiv</option>
|
||||
<option value="sport">Sehr aktiv (Sport)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="by-form-group">
|
||||
<label class="by-label">Kastriert</label>
|
||||
<div style="display:flex;gap:var(--space-3)">
|
||||
<label style="display:flex;align-items:center;gap:6px;cursor:pointer">
|
||||
<input type="radio" name="ern-kastriert" value="ja"> Ja
|
||||
</label>
|
||||
<label style="display:flex;align-items:center;gap:6px;cursor:pointer">
|
||||
<input type="radio" name="ern-kastriert" value="nein" checked> Nein
|
||||
</label>
|
||||
<!-- Gewicht + Alter nebeneinander -->
|
||||
<div class="ern-input-row">
|
||||
<div class="ern-field">
|
||||
<label>⚖️ Gewicht (kg)</label>
|
||||
<input id="ern-gewicht" type="number" step="0.1" min="0.5" max="100"
|
||||
value="${_esc(gewichtDefault)}" placeholder="15">
|
||||
</div>
|
||||
<div class="ern-field">
|
||||
<label>🎂 Alter (Jahre)</label>
|
||||
<input id="ern-alter" type="number" step="0.5" min="0" max="25"
|
||||
value="${_esc(alterDefault)}" placeholder="3">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary" id="ern-rechner-btn" style="width:100%">
|
||||
<!-- Aktivität als Pill-Buttons -->
|
||||
<div style="margin-bottom:var(--space-4)">
|
||||
<div class="ern-section-label">🏃 Aktivität</div>
|
||||
<div class="ern-pill-group">
|
||||
<button class="ern-pill" data-akt="gering">🛋️ Gemütlich</button>
|
||||
<button class="ern-pill active" data-akt="normal">🚶 Normal</button>
|
||||
<button class="ern-pill" data-akt="aktiv">🏃 Aktiv</button>
|
||||
<button class="ern-pill" data-akt="sport">🏅 Sportlich</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Kastriert als Pill-Buttons -->
|
||||
<div style="margin-bottom:var(--space-5)">
|
||||
<div class="ern-section-label">✂️ Kastriert / Sterilisiert</div>
|
||||
<div class="ern-pill-group">
|
||||
<button class="ern-pill active" data-kas="nein" style="flex:none;width:calc(50% - 4px)">Nein</button>
|
||||
<button class="ern-pill" data-kas="ja" style="flex:none;width:calc(50% - 4px)">Ja</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary" id="ern-rechner-btn" style="width:100%;padding:14px;font-size:var(--text-base)">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#calculator"></use></svg>
|
||||
Berechnen
|
||||
Kalorienbedarf berechnen
|
||||
</button>
|
||||
|
||||
<div id="ern-rechner-result" style="display:none;margin-top:var(--space-5)"></div>
|
||||
|
|
@ -208,13 +225,28 @@ window.Page_ernaehrung = (() => {
|
|||
</div>
|
||||
`;
|
||||
|
||||
// Aktivität Pills
|
||||
el.querySelectorAll('[data-akt]').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
el.querySelectorAll('[data-akt]').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
});
|
||||
});
|
||||
// Kastriert Pills
|
||||
el.querySelectorAll('[data-kas]').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
el.querySelectorAll('[data-kas]').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
el.querySelector('#ern-rechner-btn').addEventListener('click', () => _berechne(el));
|
||||
}
|
||||
|
||||
function _berechne(el) {
|
||||
const gewicht = parseFloat(el.querySelector('#ern-gewicht').value);
|
||||
const aktivitaet = el.querySelector('#ern-aktivitaet').value;
|
||||
const kastriert = el.querySelector('input[name="ern-kastriert"]:checked')?.value === 'ja';
|
||||
const gewicht = parseFloat(el.querySelector('#ern-gewicht').value);
|
||||
const aktivitaet = el.querySelector('[data-akt].active')?.dataset.akt || 'normal';
|
||||
const kastriert = el.querySelector('[data-kas].active')?.dataset.kas === 'ja';
|
||||
|
||||
if (!gewicht || gewicht < 0.5) {
|
||||
UI.toast.warning('Bitte ein gültiges Gewicht eingeben.');
|
||||
|
|
|
|||
|
|
@ -5,15 +5,26 @@
|
|||
|
||||
window.Page_expenses = (() => {
|
||||
|
||||
let _container = null;
|
||||
let _appState = null;
|
||||
let _tab = 'uebersicht';
|
||||
let _container = null;
|
||||
let _appState = null;
|
||||
let _tab = 'uebersicht';
|
||||
let _selectedDogId = null;
|
||||
|
||||
// Cache
|
||||
let _summary = null;
|
||||
let _entries = [];
|
||||
let _statsData = null;
|
||||
|
||||
function _dogParam() {
|
||||
return _selectedDogId ? `?dog_id=${_selectedDogId}` : '';
|
||||
}
|
||||
function _dogParamAnd() {
|
||||
return _selectedDogId ? `&dog_id=${_selectedDogId}` : '';
|
||||
}
|
||||
function _clearCache() {
|
||||
_summary = null; _entries = []; _statsData = null;
|
||||
}
|
||||
|
||||
const TABS = [
|
||||
{ id: 'uebersicht', label: 'Übersicht', icon: 'house-line' },
|
||||
{ id: 'eintraege', label: 'Ausgaben', icon: 'currency-eur' },
|
||||
|
|
@ -38,11 +49,10 @@ window.Page_expenses = (() => {
|
|||
// LIFECYCLE
|
||||
// ----------------------------------------------------------
|
||||
async function init(container, appState) {
|
||||
_container = container;
|
||||
_appState = appState;
|
||||
_summary = null;
|
||||
_entries = [];
|
||||
_statsData = null;
|
||||
_container = container;
|
||||
_appState = appState;
|
||||
_selectedDogId = null;
|
||||
_clearCache();
|
||||
_render();
|
||||
}
|
||||
|
||||
|
|
@ -56,6 +66,16 @@ window.Page_expenses = (() => {
|
|||
// ----------------------------------------------------------
|
||||
// SHELL
|
||||
// ----------------------------------------------------------
|
||||
function _dogSelectorHtml() {
|
||||
const dogs = _appState?.dogs || [];
|
||||
if (dogs.length < 2) return '';
|
||||
const pills = [{ id: null, name: 'Alle' }, ...dogs].map(d => `
|
||||
<button class="exp-dog-pill${_selectedDogId === d.id ? ' active' : ''}" data-dog="${d.id ?? ''}">
|
||||
${d.id ? UI.icon('paw-print') : ''} ${_esc(d.name)}
|
||||
</button>`).join('');
|
||||
return `<div class="exp-dog-selector" id="exp-dog-selector">${pills}</div>`;
|
||||
}
|
||||
|
||||
function _render() {
|
||||
_container.innerHTML = `
|
||||
<div class="by-tabs exp-tabs" id="exp-tabs">
|
||||
|
|
@ -65,6 +85,7 @@ window.Page_expenses = (() => {
|
|||
</button>
|
||||
`).join('')}
|
||||
</div>
|
||||
${_dogSelectorHtml()}
|
||||
<div id="exp-content"></div>
|
||||
<button class="exp-fab" id="exp-fab" title="Neue Ausgabe">
|
||||
${UI.icon('plus')}
|
||||
|
|
@ -81,6 +102,17 @@ window.Page_expenses = (() => {
|
|||
});
|
||||
});
|
||||
|
||||
_container.querySelector('#exp-dog-selector')?.addEventListener('click', e => {
|
||||
const pill = e.target.closest('.exp-dog-pill');
|
||||
if (!pill) return;
|
||||
_selectedDogId = pill.dataset.dog ? parseInt(pill.dataset.dog) : null;
|
||||
_clearCache();
|
||||
_container.querySelectorAll('.exp-dog-pill').forEach(p =>
|
||||
p.classList.toggle('active', p.dataset.dog === (pill.dataset.dog))
|
||||
);
|
||||
_renderTab();
|
||||
});
|
||||
|
||||
_container.querySelector('#exp-fab')
|
||||
?.addEventListener('click', () => _showForm(null));
|
||||
|
||||
|
|
@ -111,7 +143,7 @@ window.Page_expenses = (() => {
|
|||
// ----------------------------------------------------------
|
||||
async function _renderUebersicht(el) {
|
||||
if (!_summary) {
|
||||
_summary = await API.get('/expenses/summary');
|
||||
_summary = await API.get('/expenses/summary' + _dogParam());
|
||||
}
|
||||
const s = _summary;
|
||||
|
||||
|
|
@ -120,14 +152,20 @@ window.Page_expenses = (() => {
|
|||
const trendHtml = _trendHtml(letzteMonat);
|
||||
|
||||
const kacheln = KATEGORIEN.map(k => {
|
||||
const betrag = s.monat[k.id] || 0;
|
||||
const monat = s.monat[k.id] || 0;
|
||||
const jahr = s.jahr[k.id] || 0;
|
||||
const monatLine = monat > 0
|
||||
? `<div class="exp-kachel-jahr">${_fmt(monat)} diesen Monat</div>`
|
||||
: '';
|
||||
return `
|
||||
<div class="exp-kachel">
|
||||
<div class="exp-kachel" data-kat="${k.id}" style="cursor:pointer">
|
||||
<div class="exp-kachel-icon" style="background:${k.color}20;color:${k.color}">
|
||||
${UI.icon(k.icon)}
|
||||
</div>
|
||||
<div class="exp-kachel-betrag" style="color:var(--c-primary)">${_fmt(betrag)}</div>
|
||||
<div class="exp-kachel-betrag" style="color:var(--c-primary)">${_fmt(jahr)}</div>
|
||||
<div class="exp-kachel-label">${k.label}</div>
|
||||
${monatLine}
|
||||
<div class="exp-kachel-add">${UI.icon('plus')} eintragen</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
|
|
@ -146,11 +184,15 @@ window.Page_expenses = (() => {
|
|||
${verlauf}
|
||||
<div style="height:80px"></div>
|
||||
`;
|
||||
|
||||
el.querySelectorAll('.exp-kachel[data-kat]').forEach(k => {
|
||||
k.addEventListener('click', () => _showForm(null, k.dataset.kat));
|
||||
});
|
||||
}
|
||||
|
||||
async function _getLetzteMonateData() {
|
||||
if (!_entries.length) {
|
||||
_entries = await API.get('/expenses?limit=500');
|
||||
_entries = await API.get('/expenses?limit=500' + _dogParamAnd());
|
||||
}
|
||||
const monatMap = {};
|
||||
_entries.forEach(e => {
|
||||
|
|
@ -208,7 +250,7 @@ window.Page_expenses = (() => {
|
|||
// ----------------------------------------------------------
|
||||
async function _renderEintraege(el) {
|
||||
if (!_entries.length) {
|
||||
_entries = await API.get('/expenses?limit=500');
|
||||
_entries = await API.get('/expenses?limit=500' + _dogParamAnd());
|
||||
}
|
||||
|
||||
if (!_entries.length) {
|
||||
|
|
@ -321,6 +363,7 @@ window.Page_expenses = (() => {
|
|||
async function _renderDauerauftraege(el) {
|
||||
let recurring = [];
|
||||
try { recurring = await API.get('/expenses/recurring'); } catch { /* leer */ }
|
||||
if (_selectedDogId) recurring = recurring.filter(r => r.dog_id === _selectedDogId);
|
||||
|
||||
const cards = recurring.map(r => {
|
||||
const k = _kat(r.kategorie);
|
||||
|
|
@ -481,10 +524,10 @@ window.Page_expenses = (() => {
|
|||
// ----------------------------------------------------------
|
||||
async function _renderStatistik(el) {
|
||||
if (!_summary) {
|
||||
_summary = await API.get('/expenses/summary');
|
||||
_summary = await API.get('/expenses/summary' + _dogParam());
|
||||
}
|
||||
if (!_entries.length) {
|
||||
_entries = await API.get('/expenses?limit=500');
|
||||
_entries = await API.get('/expenses?limit=500' + _dogParamAnd());
|
||||
}
|
||||
|
||||
const s = _summary;
|
||||
|
|
@ -637,14 +680,15 @@ window.Page_expenses = (() => {
|
|||
// ----------------------------------------------------------
|
||||
// FORMULAR — Neu / Bearbeiten
|
||||
// ----------------------------------------------------------
|
||||
function _showForm(entry) {
|
||||
function _showForm(entry, preKat) {
|
||||
const isEdit = !!entry;
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const formId = 'exp-form';
|
||||
const selKat = entry?.kategorie || 'sonstiges';
|
||||
const selKat = entry?.kategorie || preKat || 'sonstiges';
|
||||
|
||||
const defaultDogId = entry?.dog_id ?? _selectedDogId;
|
||||
const dogOptions = (_appState.dogs || []).map(d =>
|
||||
`<option value="${d.id}"${entry?.dog_id === d.id ? ' selected' : ''}>${_esc(d.name)}</option>`
|
||||
`<option value="${d.id}"${defaultDogId === d.id ? ' selected' : ''}>${_esc(d.name)}</option>`
|
||||
).join('');
|
||||
|
||||
// Kategorie-Kacheln statt Dropdown
|
||||
|
|
@ -730,6 +774,9 @@ window.Page_expenses = (() => {
|
|||
|
||||
const modal = UI.modal.open({ title: isEdit ? 'Ausgabe bearbeiten' : 'Neue Ausgabe', body, footer });
|
||||
|
||||
// Betrag-Feld fokussieren (besonders beim Schnelleintrag per Kachel)
|
||||
setTimeout(() => modal.querySelector('input[name="betrag"]')?.focus(), 200);
|
||||
|
||||
// Kategorie-Kacheln interaktiv
|
||||
modal.querySelectorAll('.exp-kat-tile').forEach(tile => {
|
||||
tile.addEventListener('click', () => {
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ window.Page_jobs = (() => {
|
|||
}
|
||||
|
||||
_container.innerHTML = `
|
||||
<div style="max-width:640px;margin:0 auto;padding:var(--space-4)">
|
||||
<div style="max-width:640px;margin:0 auto;padding:0;box-sizing:border-box">
|
||||
|
||||
<!-- Header -->
|
||||
<div style="text-align:center;margin-bottom:var(--space-6)">
|
||||
|
|
@ -156,7 +156,7 @@ window.Page_jobs = (() => {
|
|||
value="${u ? _esc(u.email || '') : ''}" placeholder="deine@email.de" required>
|
||||
</div>
|
||||
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3)">
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:var(--space-3)">
|
||||
<div class="form-group" style="margin:0">
|
||||
<label class="form-label">Hunde-Name</label>
|
||||
<input class="form-control" type="text" name="dog_name" placeholder="z. B. Bella">
|
||||
|
|
|
|||
|
|
@ -840,15 +840,21 @@ window.Page_map = (() => {
|
|||
|
||||
// Einzelne Basis-Typen — Mehrfachauswahl möglich (außer giftkoeder = exklusiv)
|
||||
const PIN_TYPES = [
|
||||
{ type: 'giftkoeder', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#skull"></use></svg>', label: 'Giftköder', color: '#DC2626', exclusive: true },
|
||||
{ type: 'waste_basket', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#trash"></use></svg>', label: 'Mülleimer', color: '#6B7280' },
|
||||
{ type: 'kotbeutel', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#bag"></use></svg>', label: 'Kotbeutel', color: '#84A98C' },
|
||||
{ type: 'bank', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#park"></use></svg>', label: 'Sitzbank', color: '#92400E' },
|
||||
{ type: 'drinking_water',icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#drop"></use></svg>', label: 'Wasserstelle',color: '#0EA5E9' },
|
||||
{ type: 'dog_park', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#leaf"></use></svg>', label: 'Hundewiese', color: '#15803D' },
|
||||
{ type: 'parkplatz', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#car"></use></svg>', label: 'Parkplatz', color: '#2563EB' },
|
||||
{ type: 'treffpunkt', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#handshake"></use></svg>', label: 'Treffpunkt', color: '#7C3AED' },
|
||||
{ type: 'sonstiges', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#push-pin"></use></svg>', label: 'Sonstiges', color: '#F59E0B' },
|
||||
{ type: 'giftkoeder', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#skull"></use></svg>', label: 'Giftköder', color: '#DC2626', exclusive: true },
|
||||
{ type: 'gefahr', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#warning"></use></svg>', label: 'Gefahr', color: '#F59E0B' },
|
||||
{ type: 'freilauf', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#dog"></use></svg>', label: 'Freilauf', color: '#22C55E' },
|
||||
{ type: 'dog_park', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#leaf"></use></svg>', label: 'Hundewiese', color: '#15803D' },
|
||||
{ type: 'restaurant', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#fork-knife"></use></svg>', label: 'Restaurant', color: '#F97316' },
|
||||
{ type: 'shop', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#shopping-cart"></use></svg>', label: 'Shop', color: '#3B82F6' },
|
||||
{ type: 'tierarzt', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#first-aid"></use></svg>', label: 'Tierarzt', color: '#EF4444' },
|
||||
{ type: 'hundeschule', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#graduation-cap"></use></svg>', label: 'Hundeschule', color: '#8B5CF6' },
|
||||
{ type: 'waste_basket', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#trash"></use></svg>', label: 'Mülleimer', color: '#6B7280' },
|
||||
{ type: 'kotbeutel', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#bag"></use></svg>', label: 'Kotbeutel', color: '#84A98C' },
|
||||
{ type: 'bank', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#park"></use></svg>', label: 'Sitzbank', color: '#92400E' },
|
||||
{ type: 'drinking_water',icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#drop"></use></svg>', label: 'Wasserstelle',color: '#0EA5E9' },
|
||||
{ type: 'parkplatz', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#car"></use></svg>', label: 'Parkplatz', color: '#2563EB' },
|
||||
{ type: 'treffpunkt', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#handshake"></use></svg>', label: 'Treffpunkt', color: '#7C3AED' },
|
||||
{ type: 'sonstiges', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#push-pin"></use></svg>', label: 'Sonstiges', color: '#F59E0B' },
|
||||
];
|
||||
|
||||
function _confirmPlacement(latlng) {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ window.Page_reise = (() => {
|
|||
const TABS = [
|
||||
{ key: 'checkliste', label: 'Checkliste', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#check-square"></use></svg>' },
|
||||
{ key: 'laender', label: 'EU-Länder', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#globe"></use></svg>' },
|
||||
{ key: 'notfall', label: 'Notfälle', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#first-aid"></use></svg>' },
|
||||
];
|
||||
|
||||
const CHECKLIST = [
|
||||
|
|
@ -94,7 +93,15 @@ window.Page_reise = (() => {
|
|||
{ icon: 'heartbeat', text: 'Bewusstlosigkeit: Atemwege freihalten, stabile Seitenlage, 112 rufen' },
|
||||
];
|
||||
|
||||
const LS_KEY = 'banyaro_reise_checkliste';
|
||||
const LS_KEY = 'banyaro_reise_checkliste';
|
||||
const LS_CUSTOM_KEY = 'banyaro_reise_custom'; // {catKey: ["custom item",...]}
|
||||
const LS_HIDDEN_KEY = 'banyaro_reise_hidden'; // {itemKey: true} — gelöschte Standard-Items
|
||||
let _editMode = false;
|
||||
|
||||
function _loadCustom() { try { return JSON.parse(localStorage.getItem(LS_CUSTOM_KEY) || '{}'); } catch { return {}; } }
|
||||
function _saveCustom(d) { try { localStorage.setItem(LS_CUSTOM_KEY, JSON.stringify(d)); } catch {} }
|
||||
function _loadHidden() { try { return JSON.parse(localStorage.getItem(LS_HIDDEN_KEY) || '{}'); } catch { return {}; } }
|
||||
function _saveHidden(d) { try { localStorage.setItem(LS_HIDDEN_KEY, JSON.stringify(d)); } catch {} }
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Helpers
|
||||
|
|
@ -177,37 +184,79 @@ window.Page_reise = (() => {
|
|||
// ------------------------------------------------------------------
|
||||
function _renderCheckliste(el) {
|
||||
const checked = _loadChecked();
|
||||
const custom = _loadCustom();
|
||||
const hidden = _loadHidden();
|
||||
|
||||
const totalItems = CHECKLIST.reduce((s, c) => s + c.items.length, 0);
|
||||
const doneItems = Object.values(checked).filter(Boolean).length;
|
||||
const pct = totalItems > 0 ? Math.round((doneItems / totalItems) * 100) : 0;
|
||||
|
||||
const progressBar = `
|
||||
<div style="margin-bottom:var(--space-5)">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;
|
||||
margin-bottom:var(--space-2)">
|
||||
<span style="font-size:var(--text-sm);color:var(--c-text-secondary)">
|
||||
${doneItems} von ${totalItems} erledigt
|
||||
</span>
|
||||
<span style="font-size:var(--text-sm);font-weight:var(--weight-semibold);
|
||||
color:var(--c-primary)">${pct}%</span>
|
||||
</div>
|
||||
<div style="height:8px;background:var(--c-surface-2);border-radius:var(--radius-full);overflow:hidden">
|
||||
<div style="height:100%;width:${pct}%;background:var(--c-primary);
|
||||
border-radius:var(--radius-full);transition:width .3s ease"></div>
|
||||
</div>
|
||||
</div>`;
|
||||
// Alle sichtbaren Items zählen
|
||||
let totalItems = 0, doneItems = 0;
|
||||
CHECKLIST.forEach(cat => {
|
||||
cat.items.forEach((_, idx) => {
|
||||
if (!hidden[_itemKey(cat.key, idx)]) {
|
||||
totalItems++;
|
||||
if (checked[_itemKey(cat.key, idx)]) doneItems++;
|
||||
}
|
||||
});
|
||||
(custom[cat.key] || []).forEach((_, i) => {
|
||||
totalItems++;
|
||||
if (checked[`${cat.key}__custom__${i}`]) doneItems++;
|
||||
});
|
||||
});
|
||||
const pct = totalItems > 0 ? Math.round((doneItems / totalItems) * 100) : 0;
|
||||
|
||||
const cats = CHECKLIST.map(cat => {
|
||||
const rows = cat.items.map((item, idx) => {
|
||||
const customItems = custom[cat.key] || [];
|
||||
|
||||
const stdRows = cat.items.map((item, idx) => {
|
||||
if (hidden[_itemKey(cat.key, idx)]) return '';
|
||||
const key = _itemKey(cat.key, idx);
|
||||
const done = !!checked[key];
|
||||
return `<label class="reise-check-row${done ? ' done' : ''}" data-key="${_esc(key)}">
|
||||
if (_editMode) {
|
||||
return `<div class="reise-check-row" style="justify-content:space-between">
|
||||
<span style="flex:1;color:var(--c-text)">${_esc(item)}</span>
|
||||
<button class="reise-del-btn" data-hide="${_esc(key)}"
|
||||
style="background:none;border:none;color:#EF4444;cursor:pointer;padding:4px;flex-shrink:0">
|
||||
<svg class="ph-icon" style="width:1rem;height:1rem"><use href="/icons/phosphor.svg#trash"></use></svg>
|
||||
</button>
|
||||
</div>`;
|
||||
}
|
||||
return `<label class="reise-check-row${done ? ' done' : ''}">
|
||||
<input type="checkbox" class="reise-cb" data-key="${_esc(key)}" ${done ? 'checked' : ''}>
|
||||
<span>${_esc(item)}</span>
|
||||
</label>`;
|
||||
}).join('');
|
||||
|
||||
const customRows = customItems.map((item, i) => {
|
||||
const key = `${cat.key}__custom__${i}`;
|
||||
const done = !!checked[key];
|
||||
if (_editMode) {
|
||||
return `<div class="reise-check-row" style="justify-content:space-between">
|
||||
<span style="flex:1;color:var(--c-primary)">${_esc(item)}</span>
|
||||
<button class="reise-del-custom-btn" data-cat="${_esc(cat.key)}" data-idx="${i}"
|
||||
style="background:none;border:none;color:#EF4444;cursor:pointer;padding:4px;flex-shrink:0">
|
||||
<svg class="ph-icon" style="width:1rem;height:1rem"><use href="/icons/phosphor.svg#trash"></use></svg>
|
||||
</button>
|
||||
</div>`;
|
||||
}
|
||||
return `<label class="reise-check-row${done ? ' done' : ''}">
|
||||
<input type="checkbox" class="reise-cb" data-key="${_esc(key)}" ${done ? 'checked' : ''}>
|
||||
<span style="color:var(--c-primary)">${_esc(item)}</span>
|
||||
</label>`;
|
||||
}).join('');
|
||||
|
||||
const addRow = _editMode ? `
|
||||
<div style="padding:var(--space-2) 0;border-top:1px dashed var(--c-border);margin-top:4px">
|
||||
<div style="display:flex;gap:8px;align-items:center">
|
||||
<input class="reise-add-input" data-cat="${_esc(cat.key)}"
|
||||
style="flex:1;padding:8px 10px;border-radius:8px;border:1px solid var(--c-border);
|
||||
background:var(--c-bg-card);color:var(--c-text);font-size:var(--text-sm)"
|
||||
placeholder="Eigenes Item hinzufügen…">
|
||||
<button class="reise-add-btn btn btn-primary" data-cat="${_esc(cat.key)}"
|
||||
style="padding:8px 12px;flex-shrink:0;font-size:var(--text-sm)">
|
||||
<svg class="ph-icon" style="width:1rem;height:1rem"><use href="/icons/phosphor.svg#plus"></use></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>` : '';
|
||||
|
||||
return `<div class="card" style="margin-bottom:var(--space-4)">
|
||||
<div style="padding:var(--space-3) var(--space-4);border-bottom:1px solid var(--c-border);
|
||||
display:flex;align-items:center;gap:var(--space-2)">
|
||||
|
|
@ -217,20 +266,12 @@ window.Page_reise = (() => {
|
|||
<span style="font-weight:var(--weight-semibold)">${_esc(cat.label)}</span>
|
||||
</div>
|
||||
<div style="padding:var(--space-2) var(--space-4)">
|
||||
${rows}
|
||||
${stdRows}${customRows}${addRow}
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
el.innerHTML = `
|
||||
${progressBar}
|
||||
${cats}
|
||||
<div style="text-align:center;margin-bottom:var(--space-6)">
|
||||
<button class="btn btn-secondary" id="reise-reset-btn">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#arrow-counter-clockwise"></use></svg>
|
||||
Alle zurücksetzen
|
||||
</button>
|
||||
</div>
|
||||
<style>
|
||||
.reise-check-row {
|
||||
display:flex;align-items:flex-start;gap:var(--space-3);
|
||||
|
|
@ -245,6 +286,32 @@ window.Page_reise = (() => {
|
|||
accent-color:var(--c-primary);cursor:pointer;
|
||||
}
|
||||
</style>
|
||||
<!-- Fortschritt + Buttons -->
|
||||
<div style="margin-bottom:var(--space-5)">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:var(--space-2)">
|
||||
<span style="font-size:var(--text-sm);color:var(--c-text-secondary)">${doneItems} von ${totalItems} erledigt</span>
|
||||
<div style="display:flex;gap:8px;align-items:center">
|
||||
<span style="font-size:var(--text-sm);font-weight:700;color:var(--c-primary)">${pct}%</span>
|
||||
<button id="reise-edit-toggle" style="background:${_editMode ? 'var(--c-primary)' : 'var(--c-bg-card)'};
|
||||
color:${_editMode ? '#fff' : 'var(--c-text-secondary)'};border:1.5px solid var(--c-border);
|
||||
border-radius:8px;padding:5px 10px;cursor:pointer;font-size:var(--text-xs);font-weight:600;
|
||||
display:flex;align-items:center;gap:4px">
|
||||
<svg class="ph-icon" style="width:.9rem;height:.9rem"><use href="/icons/phosphor.svg#pencil-simple"></use></svg>
|
||||
${_editMode ? 'Fertig' : 'Bearbeiten'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="height:8px;background:var(--c-surface-2);border-radius:var(--radius-full);overflow:hidden">
|
||||
<div style="height:100%;width:${pct}%;background:var(--c-primary);border-radius:var(--radius-full);transition:width .3s"></div>
|
||||
</div>
|
||||
</div>
|
||||
${cats}
|
||||
${!_editMode ? `<div style="text-align:center;margin-bottom:var(--space-6)">
|
||||
<button class="btn btn-secondary" id="reise-reset-btn">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#arrow-counter-clockwise"></use></svg>
|
||||
Abhaken zurücksetzen
|
||||
</button>
|
||||
</div>` : ''}
|
||||
`;
|
||||
|
||||
// Checkbox events
|
||||
|
|
@ -254,7 +321,55 @@ window.Page_reise = (() => {
|
|||
const cur = _loadChecked();
|
||||
cur[key] = cb.checked;
|
||||
_saveChecked(cur);
|
||||
_renderTabContent(); // re-render to update progress
|
||||
_renderTabContent();
|
||||
});
|
||||
});
|
||||
|
||||
// Edit-Toggle
|
||||
el.querySelector('#reise-edit-toggle')?.addEventListener('click', () => {
|
||||
_editMode = !_editMode;
|
||||
_renderTabContent();
|
||||
});
|
||||
|
||||
// Standard-Item löschen (verstecken)
|
||||
el.querySelectorAll('.reise-del-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const h = _loadHidden();
|
||||
h[btn.dataset.hide] = true;
|
||||
_saveHidden(h);
|
||||
_renderTabContent();
|
||||
});
|
||||
});
|
||||
|
||||
// Custom-Item löschen
|
||||
el.querySelectorAll('.reise-del-custom-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const c = _loadCustom();
|
||||
c[btn.dataset.cat] = (c[btn.dataset.cat] || []).filter((_, i) => i !== parseInt(btn.dataset.idx));
|
||||
_saveCustom(c);
|
||||
_renderTabContent();
|
||||
});
|
||||
});
|
||||
|
||||
// Custom-Item hinzufügen
|
||||
el.querySelectorAll('.reise-add-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const cat = btn.dataset.cat;
|
||||
const input = el.querySelector(`.reise-add-input[data-cat="${cat}"]`);
|
||||
const val = (input?.value || '').trim();
|
||||
if (!val) return;
|
||||
const c = _loadCustom();
|
||||
if (!c[cat]) c[cat] = [];
|
||||
c[cat].push(val);
|
||||
_saveCustom(c);
|
||||
_renderTabContent();
|
||||
});
|
||||
});
|
||||
|
||||
// Enter in Add-Input
|
||||
el.querySelectorAll('.reise-add-input').forEach(input => {
|
||||
input.addEventListener('keydown', e => {
|
||||
if (e.key === 'Enter') el.querySelector(`.reise-add-btn[data-cat="${input.dataset.cat}"]`)?.click();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1424,15 +1424,15 @@ window.Page_settings = (() => {
|
|||
|
||||
_mode = mode;
|
||||
_container.innerHTML = `
|
||||
<div style="max-width:380px;margin:0 auto;padding:var(--space-6) 0">
|
||||
<div style="max-width:380px;width:100%;margin:0 auto;padding:var(--space-6) 0;box-sizing:border-box">
|
||||
|
||||
<!-- Logo -->
|
||||
<div style="text-align:center;margin-bottom:var(--space-6)">
|
||||
<img src="/icons/icon-180.png" alt="Ban Yaro"
|
||||
style="width:72px;height:72px;border-radius:var(--radius-lg);
|
||||
margin-bottom:var(--space-3)">
|
||||
<h1 style="font-size:var(--text-2xl);font-weight:700;margin:0">Ban Yaro</h1>
|
||||
<p style="color:var(--c-text-secondary);margin:var(--space-1) 0 0">
|
||||
display:block;margin:0 auto var(--space-3)">
|
||||
<h1 style="font-size:var(--text-2xl);font-weight:700;margin:0;text-align:center">Ban Yaro</h1>
|
||||
<p style="color:var(--c-text-secondary);margin:var(--space-1) 0 0;text-align:center">
|
||||
Alles rund um deinen Hund
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -497,9 +497,16 @@ window.Page_welcome = (() => {
|
|||
API.dogs.welcomeDashboard(dog.id).then(dash => {
|
||||
_updateHeroFromDash(dash, dog);
|
||||
_updateChipsFromDash(dash);
|
||||
_tryRouteChip(dash); // nach Chips-Update: ggf. Gassirunden-Vorschlag einfügen
|
||||
_tryRouteChip(dash);
|
||||
}).catch(() => { /* Skeleton bleibt sichtbar */ });
|
||||
|
||||
// Hero-Foto stündlich auffrischen (Tageswechsel um Mitternacht sichtbar ohne Reload)
|
||||
setInterval(() => {
|
||||
API.dogs.welcomeDashboard(dog.id)
|
||||
.then(dash => _updateHeroFromDash(dash, dog))
|
||||
.catch(() => {});
|
||||
}, 60 * 60 * 1000);
|
||||
|
||||
// Streak-Widget asynchron laden
|
||||
_loadStreakWidget(dog.id);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -112,22 +112,82 @@ window.Page_wetter = (() => {
|
|||
function _showLocationError() {
|
||||
const body = _container.querySelector('#wttr-body');
|
||||
if (!body) return;
|
||||
const isLoggedIn = !!_appState?.user;
|
||||
|
||||
body.innerHTML = `
|
||||
<div style="text-align:center;padding:var(--space-10) var(--space-4)">
|
||||
<div style="font-size:2.5rem;margin-bottom:var(--space-3)">📍</div>
|
||||
<h3 style="margin-bottom:var(--space-2)">Standort nicht verfügbar</h3>
|
||||
<p style="color:var(--c-text-secondary);margin-bottom:var(--space-5);max-width:300px;margin-inline:auto">
|
||||
Bitte erlaube den Zugriff auf deinen Standort, um die Wettervorhersage zu laden.
|
||||
</p>
|
||||
<button class="btn btn-primary" id="wttr-btn-retry">
|
||||
${UI.icon('map-pin')} Nochmal versuchen
|
||||
</button>
|
||||
<div style="max-width:420px;margin:0 auto;padding:var(--space-6) var(--space-4)">
|
||||
|
||||
<!-- Hero -->
|
||||
<div style="text-align:center;margin-bottom:var(--space-6)">
|
||||
<div style="font-size:4rem;line-height:1;margin-bottom:var(--space-2)">🌤️🐾</div>
|
||||
<h2 style="font-size:var(--text-xl);font-weight:800;margin:0 0 var(--space-2)">
|
||||
Das Gassi-Wetter wartet auf dich
|
||||
</h2>
|
||||
<p style="color:var(--c-text-secondary);font-size:var(--text-sm);margin:0">
|
||||
Erfahre sekundengenau, ob gerade der perfekte Moment für eine Runde ist —
|
||||
zugeschnitten auf dich und deinen Hund.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Feature-Liste -->
|
||||
<div style="display:flex;flex-direction:column;gap:var(--space-3);margin-bottom:var(--space-6)">
|
||||
${[
|
||||
['sun', '#F59E0B', 'Gassi-Score 1–10', 'Wetter bewertet nach Temperatur, Regen und Wind'],
|
||||
['thermometer', '#3B82F6', '7-Tage-Vorschau', 'Plane deine Runden für die ganze Woche voraus'],
|
||||
['drop', '#06B6D4', 'Regenradar stündlich', '24h-Niederschlagstimeline auf einen Blick'],
|
||||
['trophy', '#10B981', 'Wetter-Rekorde', 'Wärmster, nassester und stürmischster Gassi-Tag'],
|
||||
].map(([icon, color, title, sub]) => `
|
||||
<div style="display:flex;align-items:center;gap:var(--space-3);
|
||||
background:var(--c-bg-card);border:1px solid var(--c-border);
|
||||
border-radius:var(--radius-lg);padding:var(--space-3) var(--space-4)">
|
||||
<div style="width:38px;height:38px;border-radius:var(--radius-md);
|
||||
background:${color}18;display:flex;align-items:center;
|
||||
justify-content:center;flex-shrink:0">
|
||||
<svg class="ph-icon" style="width:1.1rem;height:1.1rem;color:${color}">
|
||||
<use href="/icons/phosphor.svg#${icon}"></use>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-weight:700;font-size:var(--text-sm)">${title}</div>
|
||||
<div style="color:var(--c-text-secondary);font-size:var(--text-xs)">${sub}</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
|
||||
<!-- CTAs -->
|
||||
<div style="display:flex;flex-direction:column;gap:var(--space-3)">
|
||||
<button class="btn btn-primary" id="wttr-btn-retry"
|
||||
style="display:flex;align-items:center;justify-content:center;gap:var(--space-2)">
|
||||
<svg class="ph-icon" style="width:1rem;height:1rem">
|
||||
<use href="/icons/phosphor.svg#map-pin"></use>
|
||||
</svg>
|
||||
Standort freigeben & loslegen
|
||||
</button>
|
||||
${!isLoggedIn ? `
|
||||
<button class="btn btn-secondary" id="wttr-btn-login"
|
||||
style="display:flex;align-items:center;justify-content:center;gap:var(--space-2)">
|
||||
<svg class="ph-icon" style="width:1rem;height:1rem">
|
||||
<use href="/icons/phosphor.svg#user"></use>
|
||||
</svg>
|
||||
Kostenlos registrieren
|
||||
</button>
|
||||
<p style="text-align:center;font-size:var(--text-xs);color:var(--c-text-secondary);margin:0">
|
||||
Mit Account werden Rekorde & Gassi-Score für deinen Hund gespeichert.
|
||||
</p>
|
||||
` : ''}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
`;
|
||||
|
||||
body.querySelector('#wttr-btn-retry')?.addEventListener('click', () => {
|
||||
_renderShell();
|
||||
_tryAutoLocate();
|
||||
});
|
||||
body.querySelector('#wttr-btn-login')?.addEventListener('click', () => {
|
||||
if (window.App) App.navigate('settings');
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
|
@ -1007,19 +1067,21 @@ window.Page_wetter = (() => {
|
|||
|
||||
function _recordCard(emoji, title, value, subtitle, color) {
|
||||
return `
|
||||
<div style="background:var(--c-bg-card);border:1px solid var(--c-border);
|
||||
border-radius:var(--radius);padding:var(--space-3) var(--space-3);
|
||||
display:flex;flex-direction:column;gap:2px">
|
||||
<div style="font-size:var(--text-xs);color:var(--c-text-secondary);
|
||||
display:flex;align-items:center;gap:4px;font-weight:600">
|
||||
<div style="background:${color}10;border:1px solid ${color}33;
|
||||
border-radius:var(--radius);padding:var(--space-3);
|
||||
display:flex;flex-direction:column;gap:3px">
|
||||
<div style="font-size:10px;color:var(--c-text-secondary);
|
||||
display:flex;align-items:center;gap:3px;font-weight:700;
|
||||
text-transform:uppercase;letter-spacing:.04em">
|
||||
<span>${emoji}</span>
|
||||
<span>${_esc(title)}</span>
|
||||
</div>
|
||||
<div style="font-size:var(--text-xl);font-weight:800;color:${color};line-height:1.1">
|
||||
<div style="font-size:var(--text-lg);font-weight:800;color:${color};line-height:1.1">
|
||||
${_esc(value)}
|
||||
</div>
|
||||
<div style="font-size:var(--text-xs);color:var(--c-text-secondary);
|
||||
white-space:nowrap;overflow:hidden;text-overflow:ellipsis">
|
||||
<div style="font-size:10px;color:var(--c-text-secondary);
|
||||
overflow:hidden;display:-webkit-box;
|
||||
-webkit-line-clamp:2;-webkit-box-orient:vertical;line-height:1.3">
|
||||
${_esc(subtitle)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue