banyaro/backend/static/js/pages/reise.js
rene 55069d246b 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)
2026-05-05 17:32:03 +02:00

496 lines
21 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* ============================================================
BAN YARO — Reise mit Hund
Tabs: Checkliste | EU-Länder | Notfälle
============================================================ */
window.Page_reise = (() => {
let _container = null;
let _appState = null;
let _activeTab = 'checkliste';
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>' },
];
const CHECKLIST = [
{
key: 'dokumente',
label: 'Dokumente',
icon: 'file-text',
items: [
'EU-Heimtierausweis (Pflicht innerhalb EU)',
'Impfpass (Tollwut mind. 21 Tage alt)',
'Krankenkassen-Notfallkarte Tierarzt',
'Foto des Hundes (für Vermisst-Fall)',
'Chip-Nummer notiert',
],
},
{
key: 'gesundheit',
label: 'Gesundheit',
icon: 'heartbeat',
items: [
'Zecken-/Flohschutz aufgefrischt',
'Reisekrankheit-Mittel (falls nötig)',
'Medikamente ausreichend eingepackt',
'Tierarzt-Kontakt am Zielort recherchiert',
'Verbandszeug für Hunde',
],
},
{
key: 'ausruestung',
label: 'Ausrüstung',
icon: 'backpack',
items: [
'Leine + Ersatzleine',
'Halsband mit Adressanhänger',
'Transportbox/Reisekorb',
'Lieblingsdecke/Schlafplatz',
'Spielzeug (23 Stück)',
],
},
{
key: 'futter',
label: 'Futter & Wasser',
icon: 'bowl-food',
items: [
'Genug Futter (+ Reserve)',
'Wassernapf + Flasche',
'Futternapf',
'Bekannte Leckerlis',
],
},
{
key: 'auto',
label: 'Im Auto',
icon: 'car',
items: [
'Sicherheitsgurt-Adapter oder Box gesichert',
'Sonnenschutz-Netz für Fenster',
'Pausen alle 2h eingeplant',
],
},
];
const LAENDER = [
{ flag: '🇩🇪', name: 'Deutschland', regel: 'Keine Einschränkungen bei EU-Pass + Chip' },
{ flag: '🇦🇹', name: 'Österreich', regel: 'Gleiche Regeln wie DE, Leinenpflicht in Bergbahnen' },
{ flag: '🇨🇭', name: 'Schweiz', regel: 'Nicht-EU → eigene Einfuhrregeln, Tollwut-Titer-Test', warn: true },
{ flag: '🇮🇹', name: 'Italien', regel: 'Leinenpflicht öffentlich, Maulkorb in öffentlichen Verkehrsmitteln' },
{ flag: '🇫🇷', name: 'Frankreich', regel: 'Manche Strände im Sommer hundeverboten' },
{ flag: '🇬🇷', name: 'Griechenland', regel: 'Hunde erlaubt, kaum Einschränkungen' },
{ flag: '🇭🇷', name: 'Kroatien', regel: 'Viele Strände hundefreundlich, EU-Pass genügt' },
{ flag: '🇬🇧', name: 'Großbritannien', regel: 'Strenge Einreise! PETS-Zertifikat + Tollwut-Impfung + Bandwurm-Behandlung nötig', warn: true },
];
const SOFORTMASSNAHMEN = [
{ icon: 'thermometer-hot', text: 'Hitzschlag: Sofort Schatten, kühlen mit lauwarmem Wasser, Tierarzt rufen' },
{ icon: 'skull', text: 'Vergiftung: Ruhig halten, NICHT erbrechen lassen, sofort Tiergift-Notfall anrufen' },
{ icon: 'drop', text: 'Starke Blutung: Druckverband anlegen, Druck halten, Tierarzt aufsuchen' },
{ icon: 'bone', text: 'Knochenbruch: Ruhigstellen, nicht bewegen, Tierarzt aufsuchen' },
{ icon: 'heartbeat', text: 'Bewusstlosigkeit: Atemwege freihalten, stabile Seitenlage, 112 rufen' },
];
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
// ------------------------------------------------------------------
function _esc(s) {
if (s == null) return '';
return String(s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
function _loadChecked() {
try { return JSON.parse(localStorage.getItem(LS_KEY) || '{}'); }
catch { return {}; }
}
function _saveChecked(state) {
try { localStorage.setItem(LS_KEY, JSON.stringify(state)); }
catch {}
}
function _itemKey(catKey, idx) { return `${catKey}__${idx}`; }
// ------------------------------------------------------------------
// LIFECYCLE
// ------------------------------------------------------------------
function init(container, appState, params = {}) {
_container = container;
_appState = appState;
if (params?.tab && TABS.some(t => t.key === params.tab)) {
_activeTab = params.tab;
}
_render();
}
function refresh() { _renderTabContent(); }
function onDogChange() {}
// ------------------------------------------------------------------
// RENDER
// ------------------------------------------------------------------
function _render() {
_container.innerHTML = `
<div class="by-tabs" id="reise-tabs"></div>
<div id="reise-content"></div>
`;
_renderTabBar();
_renderTabContent();
}
function _renderTabBar() {
const el = _container.querySelector('#reise-tabs');
if (!el) return;
el.innerHTML = TABS.map(t => `
<button class="by-tab${t.key === _activeTab ? ' active' : ''}" data-tab="${t.key}">
${t.icon} ${t.label}
</button>`).join('');
el.querySelectorAll('.by-tab').forEach(btn => {
btn.addEventListener('click', () => {
_activeTab = btn.dataset.tab;
el.querySelectorAll('.by-tab').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
_renderTabContent();
});
});
}
function _renderTabContent() {
const el = _container.querySelector('#reise-content');
if (!el) return;
if (_activeTab === 'checkliste') _renderCheckliste(el);
else if (_activeTab === 'laender') _renderLaender(el);
else if (_activeTab === 'notfall') _renderNotfall(el);
}
// ------------------------------------------------------------------
// TAB 1: CHECKLISTE
// ------------------------------------------------------------------
function _renderCheckliste(el) {
const checked = _loadChecked();
const custom = _loadCustom();
const hidden = _loadHidden();
// 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 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];
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)">
<svg class="ph-icon" style="color:var(--c-primary)" aria-hidden="true">
<use href="/icons/phosphor.svg#${_esc(cat.icon)}"></use>
</svg>
<span style="font-weight:var(--weight-semibold)">${_esc(cat.label)}</span>
</div>
<div style="padding:var(--space-2) var(--space-4)">
${stdRows}${customRows}${addRow}
</div>
</div>`;
}).join('');
el.innerHTML = `
<style>
.reise-check-row {
display:flex;align-items:flex-start;gap:var(--space-3);
padding:var(--space-2) 0;cursor:pointer;
border-bottom:1px solid var(--c-surface-2);
font-size:var(--text-sm);color:var(--c-text);line-height:1.5;
}
.reise-check-row:last-child { border-bottom:none; }
.reise-check-row.done span { text-decoration:line-through;color:var(--c-text-secondary); }
.reise-check-row input[type=checkbox] {
flex-shrink:0;margin-top:2px;width:18px;height:18px;
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
el.querySelectorAll('.reise-cb').forEach(cb => {
cb.addEventListener('change', () => {
const key = cb.dataset.key;
const cur = _loadChecked();
cur[key] = cb.checked;
_saveChecked(cur);
_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();
});
});
el.querySelector('#reise-reset-btn')?.addEventListener('click', () => {
_saveChecked({});
_renderTabContent();
});
}
// ------------------------------------------------------------------
// TAB 2: EU-LÄNDER
// ------------------------------------------------------------------
function _renderLaender(el) {
const cards = LAENDER.map(l => `
<div class="card" style="margin-bottom:var(--space-3);padding:var(--space-4)">
<div style="display:flex;align-items:flex-start;gap:var(--space-3)">
<span style="font-size:2rem;line-height:1">${l.flag}</span>
<div style="flex:1;min-width:0">
<div style="font-weight:var(--weight-semibold);margin-bottom:var(--space-1)">
${_esc(l.name)}
</div>
<div style="font-size:var(--text-sm);color:var(--c-text-secondary)">
${_esc(l.regel)}
</div>
</div>
${l.warn ? `<svg class="ph-icon" style="color:var(--c-warning,#f59e0b);flex-shrink:0;width:20px;height:20px" aria-hidden="true">
<use href="/icons/phosphor.svg#warning"></use>
</svg>` : ''}
</div>
</div>`).join('');
el.innerHTML = `
<div style="margin-bottom:var(--space-4);padding:var(--space-3) var(--space-4);
background:var(--c-surface-2);border-radius:var(--radius-md);
font-size:var(--text-sm);color:var(--c-text-secondary);
display:flex;gap:var(--space-2);align-items:flex-start">
<svg class="ph-icon" style="flex-shrink:0;margin-top:1px" aria-hidden="true">
<use href="/icons/phosphor.svg#info"></use>
</svg>
<span>EU-Heimtierausweis + Mikrochip + gültige Tollwut-Impfung (mindestens 21 Tage alt)
sind Pflicht für alle EU-Reisen. Informationen können sich ändern — immer beim
Zielland-Konsulat oder Tierarzt aktuell prüfen.</span>
</div>
${cards}
`;
}
// ------------------------------------------------------------------
// TAB 3: NOTFÄLLE
// ------------------------------------------------------------------
function _renderNotfall(el) {
const massnahmen = SOFORTMASSNAHMEN.map(m => `
<div style="display:flex;align-items:flex-start;gap:var(--space-3);
padding:var(--space-3) 0;border-bottom:1px solid var(--c-surface-2)">
<svg class="ph-icon" style="color:var(--c-danger,#ef4444);flex-shrink:0;margin-top:1px" aria-hidden="true">
<use href="/icons/phosphor.svg#${_esc(m.icon)}"></use>
</svg>
<span style="font-size:var(--text-sm);color:var(--c-text)">${_esc(m.text)}</span>
</div>`).join('');
el.innerHTML = `
<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);
font-weight:var(--weight-semibold);display:flex;align-items:center;gap:var(--space-2)">
<svg class="ph-icon" style="color:var(--c-danger,#ef4444)" aria-hidden="true">
<use href="/icons/phosphor.svg#phone"></use>
</svg>
Notrufnummern
</div>
<div style="padding:var(--space-4)">
<a href="tel:112" class="btn btn-danger w-full"
style="margin-bottom:var(--space-3);display:flex;align-items:center;
justify-content:center;gap:var(--space-2)">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#phone-call"></use></svg>
112 — EU-Notruf
</a>
<a href="tel:+498919240" class="btn btn-secondary w-full"
style="margin-bottom:var(--space-2);display:flex;align-items:center;
justify-content:center;gap:var(--space-2)">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#phone"></use></svg>
Tiergift-Notruf München
</a>
<div style="text-align:center;font-size:var(--text-xs);color:var(--c-text-secondary)">
+49 89 19240 (Tierärztliche Hochschule)
</div>
</div>
</div>
<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);
font-weight:var(--weight-semibold)">Tierarzt finden</div>
<div style="padding:var(--space-4)">
<button class="btn btn-primary w-full" id="reise-map-btn"
style="display:flex;align-items:center;justify-content:center;gap:var(--space-2)">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#map-pin"></use></svg>
Tierarzt in der Nähe suchen
</button>
</div>
</div>
<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);
font-weight:var(--weight-semibold);display:flex;align-items:center;gap:var(--space-2)">
<svg class="ph-icon" style="color:var(--c-warning,#f59e0b)" aria-hidden="true">
<use href="/icons/phosphor.svg#warning"></use>
</svg>
Sofortmaßnahmen
</div>
<div style="padding:var(--space-2) var(--space-4)">
${massnahmen}
</div>
</div>
`;
el.querySelector('#reise-map-btn')?.addEventListener('click', () => {
App.navigate('map');
});
}
// ------------------------------------------------------------------
// PUBLIC
// ------------------------------------------------------------------
return { init, refresh, onDogChange };
})();