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)
|
|
@ -8061,28 +8061,35 @@ svg.empty-state-icon {
|
|||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border-radius: 16px;
|
||||
padding: 14px 6px 11px;
|
||||
padding: 12px 6px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
color: white;
|
||||
transition: background 0.12s, transform 0.1s;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
user-select: none;
|
||||
min-height: 80px; /* alle Chips gleich hoch */
|
||||
}
|
||||
.world-chip:active {
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
transform: scale(0.93);
|
||||
}
|
||||
.world-chip svg { color: white; }
|
||||
.world-chip svg { color: white; flex-shrink: 0; }
|
||||
.world-chip-label {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
line-height: 1.2;
|
||||
max-height: 2.4em; /* max. 2 Zeilen */
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
/* Chip-Umrandung je Welt */
|
||||
|
|
|
|||
BIN
backend/static/img/banyaro/fruehling_playdate.webp
Normal file
|
After Width: | Height: | Size: 256 KiB |
BIN
backend/static/img/banyaro/herbst_bach.webp
Normal file
|
After Width: | Height: | Size: 446 KiB |
BIN
backend/static/img/banyaro/herbst_baum.webp
Normal file
|
After Width: | Height: | Size: 261 KiB |
|
After Width: | Height: | Size: 3.8 MiB |
BIN
backend/static/img/banyaro/hires/banyaro_herbst_bach_hires.jpg
Normal file
|
After Width: | Height: | Size: 20 MiB |
BIN
backend/static/img/banyaro/hires/banyaro_herbst_baum_hires.jpg
Normal file
|
After Width: | Height: | Size: 17 MiB |
BIN
backend/static/img/banyaro/hires/banyaro_winter_schnee_hires.jpg
Normal file
|
After Width: | Height: | Size: 437 KiB |
BIN
backend/static/img/banyaro/winter_schnee.webp
Normal file
|
After Width: | Height: | Size: 84 KiB |
|
|
@ -93,9 +93,9 @@
|
|||
</script>
|
||||
|
||||
<!-- CSS: Reihenfolge ist wichtig — ?v= zwingt Browser zur Neuladung -->
|
||||
<link rel="stylesheet" href="/css/design-system.css?v=700">
|
||||
<link rel="stylesheet" href="/css/layout.css?v=700">
|
||||
<link rel="stylesheet" href="/css/components.css?v=700">
|
||||
<link rel="stylesheet" href="/css/design-system.css?v=709">
|
||||
<link rel="stylesheet" href="/css/layout.css?v=709">
|
||||
<link rel="stylesheet" href="/css/components.css?v=709">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
|
|
@ -574,7 +574,7 @@
|
|||
<script src="/js/api.js?v=94"></script>
|
||||
<script src="/js/ui.js?v=94"></script>
|
||||
<script src="/js/app.js?v=94"></script>
|
||||
<script src="/js/worlds.js?v=700"></script>
|
||||
<script src="/js/worlds.js?v=715"></script>
|
||||
|
||||
<!-- Feature-Seiten werden lazy geladen -->
|
||||
|
||||
|
|
@ -676,5 +676,6 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
Router, State-Management, Navigation, Initialisierung.
|
||||
============================================================ */
|
||||
|
||||
const APP_VER = '700'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||
const APP_VER = '715'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||
const APP_VERSION = '1.3.0'; // ← semantische Version, wird bei make release gesetzt
|
||||
const IS_STAGING = location.hostname === 'staging.banyaro.app';
|
||||
|
||||
|
|
@ -484,6 +484,9 @@ const App = (() => {
|
|||
navigate('onboarding');
|
||||
}
|
||||
|
||||
// Drei Welten nach Login starten (falls noch nicht initialisiert)
|
||||
if (window.Worlds) window.Worlds.init(state);
|
||||
|
||||
_showVerifyBanner();
|
||||
_updateNotifBadge();
|
||||
_updateChatBadge();
|
||||
|
|
@ -559,7 +562,8 @@ const App = (() => {
|
|||
|
||||
_updateHeaderUserBtn(false);
|
||||
|
||||
// Nicht eingeloggte User immer zur Welcome-Seite
|
||||
window.Worlds?.hide();
|
||||
document.getElementById('worlds-back')?.classList.remove('worlds-back-visible');
|
||||
navigate('welcome', false);
|
||||
}
|
||||
|
||||
|
|
@ -855,11 +859,8 @@ const App = (() => {
|
|||
}
|
||||
|
||||
const startPage = (hashPage && pages[hashPage]) ? hashPage : 'welcome';
|
||||
// Nicht eingeloggte User immer zur Welcome-Seite — auch bei direktem Link auf Forum, Map etc.
|
||||
navigate(state.user ? startPage : 'welcome', false, hashParams);
|
||||
|
||||
// Drei Welten nach initialer Navigation starten (damit hide() in navigate() sie nicht gleich killt)
|
||||
if (window.Worlds) window.Worlds.init(state);
|
||||
if (window.Worlds && state.user) window.Worlds.init(state);
|
||||
}
|
||||
|
||||
async function _handleInvite(token) {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ window.Worlds = (() => {
|
|||
let _lastUserId = undefined;
|
||||
let _dogs = []; // gecachte Hundesliste
|
||||
let _dogIdx = 0; // aktuell angezeigter Hund
|
||||
let _hasBgPhoto = false; // Hintergrund-Foto vorhanden?
|
||||
|
||||
// Touch-Tracking
|
||||
const _t = { x:0, y:0, active:false, vert:null, moved:0 };
|
||||
|
|
@ -49,6 +50,55 @@ window.Worlds = (() => {
|
|||
_setupButtons();
|
||||
_goTo(_cur, false);
|
||||
show();
|
||||
_showSwipeHints();
|
||||
}
|
||||
|
||||
function _showSwipeHints() {
|
||||
if (localStorage.getItem('worlds_swipe_seen')) return;
|
||||
localStorage.setItem('worlds_swipe_seen', '1');
|
||||
const ov = document.getElementById('worlds-overlay');
|
||||
if (!ov) return;
|
||||
const hint = document.createElement('div');
|
||||
hint.style.cssText = [
|
||||
'position:absolute;inset:0;pointer-events:none;z-index:55',
|
||||
'display:flex;align-items:center;justify-content:space-between',
|
||||
'padding:0 8px;transition:opacity 1s ease',
|
||||
].join(';');
|
||||
const arrowStyle = `
|
||||
display:flex;flex-direction:column;align-items:center;gap:4px;
|
||||
background:rgba(0,0,0,0.42);backdrop-filter:blur(10px);
|
||||
-webkit-backdrop-filter:blur(10px);
|
||||
border:1px solid rgba(255,255,255,0.18);border-radius:14px;
|
||||
padding:10px 10px;animation:worlds-pulse 1.2s ease infinite alternate;
|
||||
`;
|
||||
hint.innerHTML = `
|
||||
<style>
|
||||
@keyframes worlds-pulse {
|
||||
from { opacity:0.75; transform:translateX(0); }
|
||||
to { opacity:1; transform:translateX(-3px); }
|
||||
}
|
||||
.wsh-right { animation-name:worlds-pulse-r !important; }
|
||||
@keyframes worlds-pulse-r {
|
||||
from { opacity:0.75; transform:translateX(0); }
|
||||
to { opacity:1; transform:translateX(3px); }
|
||||
}
|
||||
</style>
|
||||
<div style="${arrowStyle}">
|
||||
<svg style="width:20px;height:20px;color:white" viewBox="0 0 256 256">
|
||||
<path fill="currentColor" d="M165.66 202.34a8 8 0 0 1-11.32 11.32l-80-80a8 8 0 0 1 0-11.32l80-80a8 8 0 0 1 11.32 11.32L91.31 128Z"/>
|
||||
</svg>
|
||||
<span style="font-size:8px;font-weight:800;letter-spacing:.12em;color:rgba(255,255,255,0.7);text-transform:uppercase">JETZT</span>
|
||||
</div>
|
||||
<div class="wsh-right" style="${arrowStyle}">
|
||||
<svg style="width:20px;height:20px;color:white" viewBox="0 0 256 256">
|
||||
<path fill="currentColor" d="M90.34 53.66a8 8 0 0 1 11.32-11.32l80 80a8 8 0 0 1 0 11.32l-80 80a8 8 0 0 1-11.32-11.32L164.69 128Z"/>
|
||||
</svg>
|
||||
<span style="font-size:8px;font-weight:800;letter-spacing:.12em;color:rgba(255,255,255,0.7);text-transform:uppercase">WELT</span>
|
||||
</div>
|
||||
`;
|
||||
ov.appendChild(hint);
|
||||
setTimeout(() => { hint.style.opacity = '0'; }, 2800);
|
||||
setTimeout(() => hint.remove(), 3900);
|
||||
}
|
||||
|
||||
function show(worldIdx) {
|
||||
|
|
@ -187,7 +237,10 @@ window.Worlds = (() => {
|
|||
|
||||
function _setupButtons() {
|
||||
document.getElementById('worlds-fab')?.addEventListener('click', _openFab);
|
||||
document.getElementById('worlds-back')?.addEventListener('click', () => show());
|
||||
document.getElementById('worlds-back')?.addEventListener('click', () => {
|
||||
if (_state?.user) show();
|
||||
else if (window.App) window.App.navigate('welcome');
|
||||
});
|
||||
document.querySelectorAll('.wdot').forEach((dot, i) => {
|
||||
dot.style.pointerEvents = 'auto';
|
||||
dot.addEventListener('click', () => {
|
||||
|
|
@ -414,7 +467,7 @@ window.Worlds = (() => {
|
|||
{ icon:'wave-sine', color:'#06B6D4', label:'Gewicht messen', sub:'Aktuelles Gewicht eintragen', page:'health' }] },
|
||||
{ icon:'target', label:'Übungen', page:'uebungen',
|
||||
fab:[{ icon:'target', color:'#F59E0B', label:'Training aufzeichnen', sub:'Übung absolviert', page:'uebungen' }] },
|
||||
{ icon:'list-checks', label:'Trainings-\npläne', page:'trainingsplaene',
|
||||
{ icon:'list-checks', label:'Trainingspläne', page:'trainingsplaene',
|
||||
fab:[{ icon:'list-checks', color:'#10B981', label:'Plan erstellen', sub:'Neuen Trainingsplan anlegen', page:'trainingsplaene', action:'openNew' }] },
|
||||
{ icon:'heart', label:'Adoption', page:'adoption',
|
||||
fab:[{ icon:'heart', color:'#EF4444', label:'Hund anbieten', sub:'Zur Adoption freigeben', page:'adoption', action:'openNew' }] },
|
||||
|
|
@ -444,7 +497,7 @@ window.Worlds = (() => {
|
|||
{ icon:'sparkle', label:'Jobs', page:'jobs' },
|
||||
{ icon:'book-open', label:'Knigge', page:'knigge' },
|
||||
{ icon:'film-slate', label:'Filme', page:'movies' },
|
||||
{ icon:'tree-structure', label:'Zucht-\nkartei', page:'zuchthunde', role:'breeder',
|
||||
{ icon:'tree-structure', label:'Zuchtkartei', page:'zuchthunde', role:'breeder',
|
||||
fab:[{ icon:'tree-structure', color:'#8B5CF6', label:'Zuchthund eintragen', sub:'Neuen Hund anlegen', page:'zuchthunde', action:'openNew' }] },
|
||||
{ icon:'notebook', label:'Wurfverw.', page:'litters', role:'breeder',
|
||||
fab:[{ icon:'notebook', color:'#10B981', label:'Wurf anlegen', sub:'Neuen Wurf eintragen', page:'litters', action:'openNew' }] },
|
||||
|
|
@ -452,12 +505,19 @@ window.Worlds = (() => {
|
|||
fab:[{ icon:'sparkle', color:'#EC4899', label:'Social-Post', sub:'Beitrag erstellen', page:'social', action:'openNew' }] },
|
||||
{ icon:'shield-check', label:'Moderation', page:'moderation', role:'mod' },
|
||||
{ icon:'gear', label:'Admin', page:'admin', role:'admin' },
|
||||
// ── NEUE FEATURES ────────────────────────────────────────────
|
||||
{ icon:'fork-knife', label:'Ernährung', page:'ernaehrung',
|
||||
fab:[{ icon:'fork-knife', color:'#F97316', label:'Futter-Tagebuch', sub:'Mahlzeit oder Futtercheck', page:'ernaehrung' }] },
|
||||
{ icon:'airplane', label:'Reise', page:'reise' },
|
||||
{ icon:'smiley', label:'Persönlichkeit', page:'personality' },
|
||||
];
|
||||
|
||||
const _DEFAULT_CONFIG = {
|
||||
jetzt: ['notes','expenses','erste-hilfe','playdate','chat','wetter','settings'],
|
||||
hund: ['diary','health','uebungen','trainingsplaene','adoption','sitting','wiki','wurfboerse'],
|
||||
welt: ['map','forum','friends','walks','poison','recalls','lost','routes','events','jobs','knigge','movies'],
|
||||
jetzt: ['notes','expenses','erste-hilfe','playdate','chat','wetter','social','moderation','admin'],
|
||||
hund: ['diary','health','uebungen','trainingsplaene','adoption','sitting','wiki','wurfboerse',
|
||||
'litters','zuchthunde','ernaehrung','personality'],
|
||||
welt: ['map','forum','friends','walks','poison','recalls','lost','routes','events',
|
||||
'jobs','knigge','movies','reise'],
|
||||
};
|
||||
|
||||
// _cfgCache: wird beim Init aus DB geladen, Fallback localStorage → Default
|
||||
|
|
@ -605,10 +665,11 @@ window.Worlds = (() => {
|
|||
user-select:none;-webkit-tap-highlight-color:transparent;touch-action:none">
|
||||
${!c.pinned ? `
|
||||
<button class="wc-remove" data-page="${c.page}" data-zone="${w}"
|
||||
style="position:absolute;top:-6px;right:-6px;width:18px;height:18px;
|
||||
border-radius:50%;background:#EF4444;border:none;cursor:pointer;
|
||||
display:flex;align-items:center;justify-content:center;z-index:2">
|
||||
<svg class="ph-icon" style="width:9px;height:9px;color:white">
|
||||
style="position:absolute;top:-8px;right:-8px;width:24px;height:24px;
|
||||
border-radius:50%;background:#EF4444;border:2px solid rgba(18,22,32,0.9);
|
||||
cursor:pointer;display:flex;align-items:center;justify-content:center;
|
||||
z-index:2;box-shadow:0 2px 6px rgba(0,0,0,0.5)">
|
||||
<svg class="ph-icon" style="width:13px;height:13px;color:white">
|
||||
<use href="/icons/phosphor.svg#x"></use>
|
||||
</svg>
|
||||
</button>` : `
|
||||
|
|
@ -781,11 +842,19 @@ window.Worlds = (() => {
|
|||
const track = document.getElementById('worlds-track');
|
||||
if (!track) return;
|
||||
if (url) {
|
||||
const img = new Image();
|
||||
img.onload = () => { track.style.backgroundImage = `url('${url}')`; track.style.backgroundSize = '100% auto'; track.style.backgroundPosition = '0 40%'; track.style.backgroundRepeat = 'no-repeat'; };
|
||||
img.onerror = () => _applyBgImage(null);
|
||||
img.src = url;
|
||||
const toLoad = new Image();
|
||||
toLoad.onload = () => {
|
||||
_hasBgPhoto = true;
|
||||
track.style.backgroundImage = `url('${url}')`;
|
||||
track.style.backgroundSize = '100% auto';
|
||||
track.style.backgroundPosition = '0 40%';
|
||||
track.style.backgroundRepeat = 'no-repeat';
|
||||
document.getElementById('wh-photo-hint')?.remove();
|
||||
};
|
||||
toLoad.onerror = () => _applyBgImage(null);
|
||||
toLoad.src = url;
|
||||
} else {
|
||||
_hasBgPhoto = false;
|
||||
track.style.backgroundImage = 'linear-gradient(160deg,#1a1f35 0%,#16213e 33%,#1a2535 67%,#0f1921 100%)';
|
||||
track.style.backgroundSize = '100% 100%';
|
||||
}
|
||||
|
|
@ -839,26 +908,29 @@ window.Worlds = (() => {
|
|||
const greet = hour < 5 ? 'Gute Nacht' : hour < 12 ? 'Guten Morgen' : hour < 18 ? 'Hallo' : 'Guten Abend';
|
||||
const firstName = user?.name?.split(' ')[0] || '';
|
||||
const dayStr = new Date().toLocaleDateString('de-DE', { weekday:'long', day:'numeric', month:'long' });
|
||||
const stale = isOffline && staleMin > 5
|
||||
const stale = isOffline && staleMin > 5
|
||||
? `<span style="font-size:9px;opacity:0.5;margin-left:6px">· Offline</span>` : '';
|
||||
const weatherLine = w
|
||||
? `${Math.round(w.temp_c)}° ${_esc(w.desc?.split(' ')[0] || '')} · ${Math.round(w.wind_kmh || 0)} km/h · ${w.precip_prob || 0}% Regen`
|
||||
: '';
|
||||
|
||||
// Streak für 3er-Chip-Zeile
|
||||
let streakVal = '—', streakCol = 'rgba(255,255,255,0.4)';
|
||||
if (user && dog) {
|
||||
try {
|
||||
const sr = await _cachedGet(`streak_${dog.id}`, `/streak/${dog.id}`);
|
||||
const s = sr.data;
|
||||
const streak = s?.current_streak || 0;
|
||||
const trainedToday = s?.last_training_date === new Date().toISOString().slice(0,10);
|
||||
streakCol = trainedToday ? '#10B981' : (streak > 0 ? '#F59E0B' : 'rgba(255,255,255,0.4)');
|
||||
streakVal = streak > 0
|
||||
? (trainedToday ? `✓ ${streak} Tage` : `🔥 ${streak} Tage`)
|
||||
: (trainedToday ? '✓ Heute' : 'Heute starten');
|
||||
} catch {}
|
||||
// Gassi-Score aus Wetterdaten berechnen
|
||||
function _calcGassiScore(wd) {
|
||||
if (!wd) return null;
|
||||
let s = 10;
|
||||
const t = wd.temp_c ?? 20, p = wd.precip_prob ?? 0, wind = wd.wind_kmh ?? 0;
|
||||
if (t > 30) s -= 3; else if (t > 25) s -= 1; else if (t < 0) s -= 3; else if (t < 5) s -= 1;
|
||||
if (p > 70) s -= 3; else if (p > 40) s -= 2; else if (p > 20) s -= 1;
|
||||
if (wind > 60) s -= 2; else if (wind > 40) s -= 1;
|
||||
if (wd.thunderstorm) s -= 3;
|
||||
return Math.max(1, Math.min(10, s));
|
||||
}
|
||||
const gassiScore = _calcGassiScore(w);
|
||||
const gassiColor = gassiScore >= 8 ? '#10B981' : gassiScore >= 5 ? '#F59E0B' : '#EF4444';
|
||||
const weatherEmoji = !w ? '🌤️'
|
||||
: w.thunderstorm ? '⛈️'
|
||||
: (w.precip_prob ?? 0) > 70 ? '🌧️'
|
||||
: (w.precip_prob ?? 0) > 30 ? '🌦️'
|
||||
: (w.temp_c ?? 20) > 28 ? '☀️🔥'
|
||||
: (w.temp_c ?? 20) < 2 ? '🌨️'
|
||||
: '☀️';
|
||||
|
||||
// Alert-Reminder
|
||||
const alertHtml = alertList.slice(0,1).map(a => `
|
||||
|
|
@ -901,7 +973,7 @@ window.Worlds = (() => {
|
|||
<div class="world-info-title">
|
||||
${_esc(greet)}${firstName ? `, <span style="color:var(--c-primary)">${_esc(firstName)}</span>` : ''}${stale}
|
||||
</div>
|
||||
<div class="world-info-sub">${_esc(dayStr)}${weatherLine ? ' · ' + weatherLine : ''}${totalKm != null ? ' · ' + totalKm + ' km' : ''}</div>
|
||||
<div class="world-info-sub">${_esc(dayStr)}</div>
|
||||
</div>
|
||||
${user ? userAvatarHtml : ''}
|
||||
</div>
|
||||
|
|
@ -909,17 +981,25 @@ window.Worlds = (() => {
|
|||
${alertHtml}
|
||||
${user && dog ? `
|
||||
<div class="wj-chip-row">
|
||||
<div class="wj-chip" data-wnav="uebungen">
|
||||
<svg class="ph-icon" style="width:1.2rem;height:1.2rem;color:${streakCol}">
|
||||
<use href="/icons/phosphor.svg#target"></use></svg>
|
||||
<span class="wj-chip-label">Streak</span>
|
||||
<span class="wj-chip-val">${streakVal}</span>
|
||||
<div class="wj-chip" data-wnav="wetter" style="${gassiScore ? `border-color:${gassiColor}44;background:${gassiColor}12;` : ''}">
|
||||
<div style="display:flex;align-items:center;gap:6px;width:100%">
|
||||
<span style="font-size:1.4rem;line-height:1;flex-shrink:0">${weatherEmoji}</span>
|
||||
<div style="flex:1;min-width:0">
|
||||
<div style="font-size:9px;color:rgba(255,255,255,0.55);font-weight:600;letter-spacing:.05em;text-transform:uppercase">Gassi-Score</div>
|
||||
<div style="display:flex;align-items:baseline;gap:3px;margin-top:1px">
|
||||
<span style="font-size:1.25rem;font-weight:800;color:${gassiColor};line-height:1">${gassiScore ?? '—'}</span>
|
||||
${gassiScore ? `<span style="font-size:var(--text-xs);color:rgba(255,255,255,0.4);font-weight:600">/10</span>` : ''}
|
||||
</div>
|
||||
${w ? `<div style="font-size:9px;color:rgba(255,255,255,0.5);margin-top:1px">${Math.round(w.temp_c ?? 0)}° · ${w.precip_prob ?? 0}% Regen</div>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wj-chip" data-wnav="routes">
|
||||
<svg class="ph-icon" style="width:1.2rem;height:1.2rem;color:var(--c-primary)">
|
||||
<use href="/icons/phosphor.svg#path"></use></svg>
|
||||
<span class="wj-chip-label">Gassirunde</span>
|
||||
<span class="wj-chip-val" id="wj-route-val">…</span>
|
||||
${totalKm != null ? `<span style="font-size:9px;color:rgba(255,255,255,0.4);margin-top:1px">∑ ${totalKm} km</span>` : ''}
|
||||
</div>
|
||||
<div class="wj-chip" data-wnav="uebungen">
|
||||
<svg class="ph-icon" style="width:1.2rem;height:1.2rem;color:var(--c-primary)">
|
||||
|
|
@ -1018,13 +1098,41 @@ window.Worlds = (() => {
|
|||
const dogs = dogsRes.data || [];
|
||||
|
||||
if (!dogs.length) {
|
||||
const features = [
|
||||
{ icon:'book-open', color:'#8B5CF6', title:'Tagebuch', sub:'Fotos & Erlebnisse' },
|
||||
{ icon:'heartbeat', color:'#EF4444', title:'Gesundheit', sub:'Impfungen & Gewicht' },
|
||||
{ icon:'target', color:'#F59E0B', title:'Training', sub:'104 Übungen' },
|
||||
{ icon:'books', color:'#10B981', title:'Wiki', sub:'Alle Rassen' },
|
||||
{ icon:'paw-print', color:'#3B82F6', title:'Gassi', sub:'Routen & GPS' },
|
||||
{ icon:'currency-eur',color:'#06B6D4',title:'Ausgaben', sub:'Budget im Blick' },
|
||||
];
|
||||
el.innerHTML = `
|
||||
<div class="world-info-card" style="text-align:center">
|
||||
<div style="font-size:4rem;margin-bottom:12px">🐶</div>
|
||||
<div class="world-info-title">Noch kein Hund angelegt</div>
|
||||
<div class="world-info-sub" style="margin-bottom:20px">Erstelle das Profil deines Hundes</div>
|
||||
<button class="btn btn-primary" onclick="Worlds.navigateTo('dog-profile')">Hund anlegen</button>
|
||||
<div class="world-top">
|
||||
<div class="world-info-card" style="text-align:center">
|
||||
<div style="font-size:3.2rem;margin-bottom:10px">🐶</div>
|
||||
<div class="world-info-title">Dein Hund wartet!</div>
|
||||
<div class="world-info-sub" style="margin-bottom:16px">
|
||||
Lege ein Profil an und schalte alle Features frei
|
||||
</div>
|
||||
<button class="btn btn-primary" style="width:100%" onclick="Worlds.navigateTo('dog-profile')">
|
||||
Hund anlegen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="world-bottom">
|
||||
<div class="world-section-label">Was dich erwartet</div>
|
||||
<div class="world-chips-grid">
|
||||
${features.map(f => `
|
||||
<div class="world-chip" style="opacity:0.7;cursor:default">
|
||||
<svg class="ph-icon" style="width:1.4rem;height:1.4rem;color:${f.color}">
|
||||
<use href="/icons/phosphor.svg#${f.icon}"></use>
|
||||
</svg>
|
||||
<span class="world-chip-label">${f.title}</span>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>`;
|
||||
el.querySelectorAll('[data-wnav]').forEach(e => e.addEventListener('click', () => navigateTo(e.dataset.wnav)));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1093,6 +1201,25 @@ window.Worlds = (() => {
|
|||
</div>
|
||||
</div>
|
||||
<div class="world-bottom">
|
||||
${!_hasBgPhoto ? `
|
||||
<div id="wh-photo-hint" data-wnav="diary"
|
||||
style="background:rgba(0,0,0,0.32);backdrop-filter:blur(12px);
|
||||
-webkit-backdrop-filter:blur(12px);border:1px solid rgba(255,255,255,0.12);
|
||||
border-radius:16px;padding:11px 14px;display:flex;align-items:center;
|
||||
gap:10px;cursor:pointer;color:white;flex-shrink:0">
|
||||
<svg class="ph-icon" style="width:1.1rem;height:1.1rem;color:rgba(196,132,58,0.9);flex-shrink:0">
|
||||
<use href="/icons/phosphor.svg#camera"></use>
|
||||
</svg>
|
||||
<div>
|
||||
<div style="font-size:var(--text-xs);font-weight:700;color:rgba(255,255,255,0.85)">
|
||||
Hintergrund-Foto hinzufügen
|
||||
</div>
|
||||
<div style="font-size:10px;color:rgba(255,255,255,0.45);margin-top:1px">
|
||||
Tagebuchfotos erscheinen hier als Panorama
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
<div class="world-section-label">Alles über ${_esc(dog.name)}</div>
|
||||
<div class="world-chips-grid">
|
||||
${chips.map(c => _chip(c.icon, c.label, c.page)).join('')}
|
||||
|
|
|
|||
|
|
@ -226,9 +226,90 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Foto-Galerie -->
|
||||
<section>
|
||||
<div class="section-label">Fotos — zur redaktionellen Verwendung freigegeben</div>
|
||||
<div style="display:grid;grid-template-columns:repeat(2,1fr);gap:12px;margin-bottom:8px">
|
||||
|
||||
<!-- Herbst am Bach -->
|
||||
<div style="border-radius:var(--radius);overflow:hidden;background:var(--white);border:1px solid var(--border)">
|
||||
<div style="position:relative;aspect-ratio:4/3">
|
||||
<img src="/img/banyaro/herbst_bach.webp" alt="Ban Yaro am Bach im Herbst"
|
||||
style="width:100%;height:100%;object-fit:cover;display:block">
|
||||
</div>
|
||||
<div style="padding:10px 12px">
|
||||
<div style="font-size:.8rem;font-weight:700;margin-bottom:2px">Herbst am Bach</div>
|
||||
<div style="font-size:.7rem;color:var(--muted);margin-bottom:8px">8064 × 6048 px · 20 MB JPEG</div>
|
||||
<a href="/img/banyaro/hires/banyaro_herbst_bach_hires.jpg"
|
||||
download="banyaro-herbst-bach-hires.jpg"
|
||||
style="display:inline-flex;align-items:center;gap:5px;background:var(--primary);color:white;
|
||||
font-size:.72rem;font-weight:700;padding:5px 12px;border-radius:6px;text-decoration:none">
|
||||
↓ Hi-Res herunterladen
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Winter im Schnee -->
|
||||
<div style="border-radius:var(--radius);overflow:hidden;background:var(--white);border:1px solid var(--border)">
|
||||
<div style="position:relative;aspect-ratio:4/3">
|
||||
<img src="/img/banyaro/winter_schnee.webp" alt="Ban Yaro im Schnee"
|
||||
style="width:100%;height:100%;object-fit:cover;display:block">
|
||||
</div>
|
||||
<div style="padding:10px 12px">
|
||||
<div style="font-size:.8rem;font-weight:700;margin-bottom:2px">Winter im Schnee</div>
|
||||
<div style="font-size:.7rem;color:var(--muted);margin-bottom:8px">Original-Auflösung · JPEG</div>
|
||||
<a href="/img/banyaro/hires/banyaro_winter_schnee_hires.jpg"
|
||||
download="banyaro-winter-schnee-hires.jpg"
|
||||
style="display:inline-flex;align-items:center;gap:5px;background:var(--primary);color:white;
|
||||
font-size:.72rem;font-weight:700;padding:5px 12px;border-radius:6px;text-decoration:none">
|
||||
↓ Hi-Res herunterladen
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Frühling & Playdate -->
|
||||
<div style="border-radius:var(--radius);overflow:hidden;background:var(--white);border:1px solid var(--border)">
|
||||
<div style="position:relative;aspect-ratio:4/3">
|
||||
<img src="/img/banyaro/fruehling_playdate.webp" alt="Ban Yaro spielt im Frühling"
|
||||
style="width:100%;height:100%;object-fit:cover;display:block">
|
||||
</div>
|
||||
<div style="padding:10px 12px">
|
||||
<div style="font-size:.8rem;font-weight:700;margin-bottom:2px">Frühling & Playdate</div>
|
||||
<div style="font-size:.7rem;color:var(--muted);margin-bottom:8px">3199 × 2648 px · 3,8 MB JPEG</div>
|
||||
<a href="/img/banyaro/hires/banyaro_fruehling_playdate_hires.jpg"
|
||||
download="banyaro-fruehling-playdate-hires.jpg"
|
||||
style="display:inline-flex;align-items:center;gap:5px;background:var(--primary);color:white;
|
||||
font-size:.72rem;font-weight:700;padding:5px 12px;border-radius:6px;text-decoration:none">
|
||||
↓ Hi-Res herunterladen
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Herbst & Neugier -->
|
||||
<div style="border-radius:var(--radius);overflow:hidden;background:var(--white);border:1px solid var(--border)">
|
||||
<div style="position:relative;aspect-ratio:4/3">
|
||||
<img src="/img/banyaro/herbst_baum.webp" alt="Ban Yaro neugierig am Baum"
|
||||
style="width:100%;height:100%;object-fit:cover;display:block">
|
||||
</div>
|
||||
<div style="padding:10px 12px">
|
||||
<div style="font-size:.8rem;font-weight:700;margin-bottom:2px">Herbst & Neugier</div>
|
||||
<div style="font-size:.7rem;color:var(--muted);margin-bottom:8px">8064 × 6048 px · 17 MB JPEG</div>
|
||||
<a href="/img/banyaro/hires/banyaro_herbst_baum_hires.jpg"
|
||||
download="banyaro-herbst-baum-hires.jpg"
|
||||
style="display:inline-flex;align-items:center;gap:5px;background:var(--primary);color:white;
|
||||
font-size:.72rem;font-weight:700;padding:5px 12px;border-radius:6px;text-decoration:none">
|
||||
↓ Hi-Res herunterladen
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<p style="font-size:.78rem;color:var(--muted)">Alle Fotos: Ban Yaro (Kromfohrländer) · Fotograf: René Degelmann · Zur redaktionellen Verwendung freigegeben</p>
|
||||
</section>
|
||||
|
||||
<!-- Screenshots -->
|
||||
<section>
|
||||
<div class="section-label">Screenshots — zur redaktionellen Verwendung freigegeben</div>
|
||||
<div class="section-label">App-Screenshots — zur redaktionellen Verwendung freigegeben</div>
|
||||
<div class="download-grid">
|
||||
<a class="download-card" href="/img/screenshots/screen-1.jpg" download="banyaro-tagebuch.jpg">
|
||||
<img class="thumb" src="/img/screenshots/screen-1.jpg" alt="Tagebuch">
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
Offline-Cache + Push Notifications + Tile-Cache
|
||||
============================================================ */
|
||||
|
||||
const CACHE_VERSION = 'by-v700';
|
||||
const CACHE_VERSION = 'by-v715';
|
||||
const CACHE_STATIC = `${CACHE_VERSION}-static`;
|
||||
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
|
||||
const CACHE_API = 'ban-yaro-api-v1'; // API-Response-Cache
|
||||
|
|
|
|||