diff --git a/backend/main.py b/backend/main.py index bed79a5..a883e9b 100644 --- a/backend/main.py +++ b/backend/main.py @@ -325,6 +325,18 @@ MEDIA_DIR = os.getenv("MEDIA_DIR", "/data/media") os.makedirs(MEDIA_DIR, exist_ok=True) app.mount("/media", StaticFiles(directory=MEDIA_DIR), name="media") +APP_VER = "715" # muss mit APP_VER in app.js übereinstimmen + +@app.get("/api/version") +async def app_version(): + """Aktuelle Frontend-Version — wird beim App-Start gecheckt.""" + return Response( + content=f'{{"version":"{APP_VER}"}}', + media_type="application/json", + headers={"Cache-Control": "no-store"}, + ) + + @app.get("/stats/script.js") async def umami_script_proxy(): async with httpx.AsyncClient(timeout=10) as client: diff --git a/backend/media_utils.py b/backend/media_utils.py index 8a8698f..4cb2e28 100644 --- a/backend/media_utils.py +++ b/backend/media_utils.py @@ -178,6 +178,17 @@ def generate_preview(data: bytes, ext: str) -> bytes | None: return None +def get_image_size(data: bytes) -> tuple[int, int] | None: + """Gibt (width, height) eines Bildes zurück, oder None bei Fehler.""" + try: + from PIL import Image, ImageOps + img = Image.open(io.BytesIO(data)) + img = ImageOps.exif_transpose(img) + return img.size # (width, height) + except Exception: + return None + + def preview_url_from(url: str | None) -> str | None: """Leitet die Preview-URL aus der Original-URL ab (fügt _preview vor Extension ein). Gibt None für Videos, PDFs, bereits generierte Previews und leere URLs zurück.""" diff --git a/backend/routes/achievements.py b/backend/routes/achievements.py index 0a2988e..e8d0cba 100644 --- a/backend/routes/achievements.py +++ b/backend/routes/achievements.py @@ -203,11 +203,11 @@ def check_and_award(user_id: int, conn): "SELECT current_streak FROM users WHERE id=?", (user_id,) ).fetchone() - # Wetter-Tapferkeit: Diary-Einträge bei schlechtem Wetter + # Wetter-Tapferkeit: Diary-Einträge bei schlechtem Wetter (über Dog-Join) wetter_row = conn.execute(""" SELECT COUNT(*) AS cnt FROM diary d - LEFT JOIN diary_dogs dd ON dd.diary_id = d.id - WHERE d.user_id = ? + JOIN dogs dog ON dog.id = d.dog_id + WHERE dog.user_id = ? AND d.weather_json IS NOT NULL AND ( CAST(json_extract(d.weather_json, '$.precip_prob') AS INTEGER) > 60 @@ -216,23 +216,28 @@ def check_and_award(user_id: int, conn): ) """, (user_id,)).fetchone() - # Jahreszeiten: Anzahl Jahreszeiten mit mind. 5 Diary-Einträgen + # Jahreszeiten: Anzahl Jahreszeiten mit mind. 5 Diary-Einträgen (über Dog-Join) jahreszeiten_row = conn.execute(""" SELECT - (CASE WHEN (SELECT COUNT(*) FROM diary WHERE user_id=? AND CAST(strftime('%m', datum) AS INTEGER) IN (3,4,5)) >= 5 THEN 1 ELSE 0 END) + - (CASE WHEN (SELECT COUNT(*) FROM diary WHERE user_id=? AND CAST(strftime('%m', datum) AS INTEGER) IN (6,7,8)) >= 5 THEN 1 ELSE 0 END) + - (CASE WHEN (SELECT COUNT(*) FROM diary WHERE user_id=? AND CAST(strftime('%m', datum) AS INTEGER) IN (9,10,11)) >= 5 THEN 1 ELSE 0 END) + - (CASE WHEN (SELECT COUNT(*) FROM diary WHERE user_id=? AND CAST(strftime('%m', datum) AS INTEGER) IN (12,1,2)) >= 5 THEN 1 ELSE 0 END) + (CASE WHEN (SELECT COUNT(*) FROM diary d2 JOIN dogs g2 ON g2.id=d2.dog_id + WHERE g2.user_id=? AND CAST(strftime('%m', d2.datum) AS INTEGER) IN (3,4,5)) >= 5 THEN 1 ELSE 0 END) + + (CASE WHEN (SELECT COUNT(*) FROM diary d2 JOIN dogs g2 ON g2.id=d2.dog_id + WHERE g2.user_id=? AND CAST(strftime('%m', d2.datum) AS INTEGER) IN (6,7,8)) >= 5 THEN 1 ELSE 0 END) + + (CASE WHEN (SELECT COUNT(*) FROM diary d2 JOIN dogs g2 ON g2.id=d2.dog_id + WHERE g2.user_id=? AND CAST(strftime('%m', d2.datum) AS INTEGER) IN (9,10,11)) >= 5 THEN 1 ELSE 0 END) + + (CASE WHEN (SELECT COUNT(*) FROM diary d2 JOIN dogs g2 ON g2.id=d2.dog_id + WHERE g2.user_id=? AND CAST(strftime('%m', d2.datum) AS INTEGER) IN (12,1,2)) >= 5 THEN 1 ELSE 0 END) AS jahreszeiten_score FROM (SELECT 1) """, (user_id, user_id, user_id, user_id)).fetchone() - # Schnee: Diary-Einträge bei Schnee (weathercode 71-77) + # Schnee: Diary-Einträge bei Schnee (weathercode 71-77, über Dog-Join) schnee_row = conn.execute(""" - SELECT COUNT(*) AS cnt FROM diary - WHERE user_id = ? - AND weather_json IS NOT NULL - AND CAST(json_extract(weather_json, '$.weathercode') AS INTEGER) BETWEEN 71 AND 77 + SELECT COUNT(*) AS cnt FROM diary d + JOIN dogs dog ON dog.id = d.dog_id + WHERE dog.user_id = ? + AND d.weather_json IS NOT NULL + AND CAST(json_extract(d.weather_json, '$.weathercode') AS INTEGER) BETWEEN 71 AND 77 """, (user_id,)).fetchone() metrics = { diff --git a/backend/routes/osm.py b/backend/routes/osm.py index de4cb45..998cd4b 100644 --- a/backend/routes/osm.py +++ b/backend/routes/osm.py @@ -280,9 +280,14 @@ class UserPoiIn(BaseModel): ALLOWED_TYPES = { 'waste_basket', 'drinking_water', 'dog_park', 'giftkoeder', # Giftköder (exklusiv, kein Kombi) + 'gefahr', # Allgemeine Gefahr / Hinweis + 'freilauf', # Freilauffläche + 'restaurant', # Hundefreundliches Restaurant / Café + 'shop', # Hundefreundlicher Shop + 'tierarzt', # Tierarzt / Tierklinik + 'hundeschule', # Hundeschule / Trainer 'kotbeutel', # Kotbeutelspender 'bank', # Sitzbank - 'gefahr', # Allgemeine Gefahr / Hinweis 'parkplatz', # Hundefreundlicher Parkplatz 'treffpunkt', # Treffpunkt für Hundehalter 'sonstiges', diff --git a/backend/routes/weather.py b/backend/routes/weather.py index ba45306..2167b19 100644 --- a/backend/routes/weather.py +++ b/backend/routes/weather.py @@ -43,7 +43,8 @@ async def weather_records(user=Depends(get_current_user)): rows = conn.execute(""" SELECT d.datum, d.weather_json, d.titel FROM diary d - WHERE d.user_id = ? AND d.weather_json IS NOT NULL + JOIN dogs dog ON dog.id = d.dog_id + WHERE dog.user_id = ? AND d.weather_json IS NOT NULL ORDER BY d.datum ASC """, (uid,)).fetchall() diff --git a/backend/routes/widget.py b/backend/routes/widget.py index 4af2473..2c04ae8 100644 --- a/backend/routes/widget.py +++ b/backend/routes/widget.py @@ -1,6 +1,6 @@ """BAN YARO — Widget-Snapshot + Tagesspruch Endpoints""" -import json, random +import json from datetime import date from fastapi import APIRouter, Depends, Query from typing import Optional @@ -59,7 +59,8 @@ async def widget_snapshot(user=Depends(get_current_user)): (dog_id,) ).fetchall() - random_photo = dict(random.choice(photos)) if photos else None + day_num = (date.today() - date(2024, 1, 1)).days + random_photo = dict(photos[day_num % len(photos)]) if photos else None # Anzahl überfälliger Erinnerungen overdue = conn.execute( diff --git a/backend/static/css/components.css b/backend/static/css/components.css index 4b46952..d3dcc37 100644 --- a/backend/static/css/components.css +++ b/backend/static/css/components.css @@ -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 */ diff --git a/backend/static/img/banyaro/fruehling_playdate.webp b/backend/static/img/banyaro/fruehling_playdate.webp new file mode 100644 index 0000000..7defe9e Binary files /dev/null and b/backend/static/img/banyaro/fruehling_playdate.webp differ diff --git a/backend/static/img/banyaro/herbst_bach.webp b/backend/static/img/banyaro/herbst_bach.webp new file mode 100644 index 0000000..5b7a594 Binary files /dev/null and b/backend/static/img/banyaro/herbst_bach.webp differ diff --git a/backend/static/img/banyaro/herbst_baum.webp b/backend/static/img/banyaro/herbst_baum.webp new file mode 100644 index 0000000..4ea4312 Binary files /dev/null and b/backend/static/img/banyaro/herbst_baum.webp differ diff --git a/backend/static/img/banyaro/hires/banyaro_fruehling_playdate_hires.jpg b/backend/static/img/banyaro/hires/banyaro_fruehling_playdate_hires.jpg new file mode 100644 index 0000000..eb4e55a Binary files /dev/null and b/backend/static/img/banyaro/hires/banyaro_fruehling_playdate_hires.jpg differ diff --git a/backend/static/img/banyaro/hires/banyaro_herbst_bach_hires.jpg b/backend/static/img/banyaro/hires/banyaro_herbst_bach_hires.jpg new file mode 100644 index 0000000..aab4923 Binary files /dev/null and b/backend/static/img/banyaro/hires/banyaro_herbst_bach_hires.jpg differ diff --git a/backend/static/img/banyaro/hires/banyaro_herbst_baum_hires.jpg b/backend/static/img/banyaro/hires/banyaro_herbst_baum_hires.jpg new file mode 100644 index 0000000..d14e80d Binary files /dev/null and b/backend/static/img/banyaro/hires/banyaro_herbst_baum_hires.jpg differ diff --git a/backend/static/img/banyaro/hires/banyaro_winter_schnee_hires.jpg b/backend/static/img/banyaro/hires/banyaro_winter_schnee_hires.jpg new file mode 100644 index 0000000..155d65b Binary files /dev/null and b/backend/static/img/banyaro/hires/banyaro_winter_schnee_hires.jpg differ diff --git a/backend/static/img/banyaro/winter_schnee.webp b/backend/static/img/banyaro/winter_schnee.webp new file mode 100644 index 0000000..87269d4 Binary files /dev/null and b/backend/static/img/banyaro/winter_schnee.webp differ diff --git a/backend/static/index.html b/backend/static/index.html index 77f0433..242e2b7 100644 --- a/backend/static/index.html +++ b/backend/static/index.html @@ -93,9 +93,9 @@ - - - + + + @@ -574,7 +574,7 @@ - + @@ -676,5 +676,6 @@ } + diff --git a/backend/static/js/app.js b/backend/static/js/app.js index deb05c5..b117f18 100644 --- a/backend/static/js/app.js +++ b/backend/static/js/app.js @@ -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) { diff --git a/backend/static/js/pages/ernaehrung.js b/backend/static/js/pages/ernaehrung.js index ec1951f..be58151 100644 --- a/backend/static/js/pages/ernaehrung.js +++ b/backend/static/js/pages/ernaehrung.js @@ -125,47 +125,64 @@ window.Page_ernaehrung = (() => { el.innerHTML = `
-

- Berechne den täglichen Kalorienbedarf deines Hundes. -

+ -
- - -
- -
- - -
- -
- - -
- -
- -
- - + +
+
+ + +
+
+ +
- + + + +
+
+ + +
+ +
+ + +
+
+ + @@ -208,13 +225,28 @@ window.Page_ernaehrung = (() => {
`; + // 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.'); diff --git a/backend/static/js/pages/expenses.js b/backend/static/js/pages/expenses.js index 3bed1dd..c516c40 100644 --- a/backend/static/js/pages/expenses.js +++ b/backend/static/js/pages/expenses.js @@ -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 => ` + `).join(''); + return `
${pills}
`; + } + function _render() { _container.innerHTML = `
@@ -65,6 +85,7 @@ window.Page_expenses = (() => { `).join('')}
+ ${_dogSelectorHtml()}
+ `; + } + return ``; }).join(''); + const customRows = customItems.map((item, i) => { + const key = `${cat.key}__custom__${i}`; + const done = !!checked[key]; + if (_editMode) { + return `
+ ${_esc(item)} + +
`; + } + return ``; + }).join(''); + + const addRow = _editMode ? ` +
+
+ + +
+
` : ''; + return `
@@ -217,20 +266,12 @@ window.Page_reise = (() => { ${_esc(cat.label)}
- ${rows} + ${stdRows}${customRows}${addRow}
`; }).join(''); el.innerHTML = ` - ${progressBar} - ${cats} -
- -
+ +
+
+ ${doneItems} von ${totalItems} erledigt +
+ ${pct}% + +
+
+
+
+
+
+ ${cats} + ${!_editMode ? `
+ +
` : ''} `; // 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(); }); }); diff --git a/backend/static/js/pages/settings.js b/backend/static/js/pages/settings.js index b829dea..ffc47f7 100644 --- a/backend/static/js/pages/settings.js +++ b/backend/static/js/pages/settings.js @@ -1424,15 +1424,15 @@ window.Page_settings = (() => { _mode = mode; _container.innerHTML = ` -
+
Ban Yaro -

Ban Yaro

-

+ display:block;margin:0 auto var(--space-3)"> +

Ban Yaro

+

Alles rund um deinen Hund

diff --git a/backend/static/js/pages/welcome.js b/backend/static/js/pages/welcome.js index e3514d5..fdbdc87 100644 --- a/backend/static/js/pages/welcome.js +++ b/backend/static/js/pages/welcome.js @@ -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); } diff --git a/backend/static/js/pages/wetter.js b/backend/static/js/pages/wetter.js index 755b825..751f410 100644 --- a/backend/static/js/pages/wetter.js +++ b/backend/static/js/pages/wetter.js @@ -112,22 +112,82 @@ window.Page_wetter = (() => { function _showLocationError() { const body = _container.querySelector('#wttr-body'); if (!body) return; + const isLoggedIn = !!_appState?.user; + body.innerHTML = ` -
-
📍
-

Standort nicht verfügbar

-

- Bitte erlaube den Zugriff auf deinen Standort, um die Wettervorhersage zu laden. -

- +
+ + +
+
🌤️🐾
+

+ Das Gassi-Wetter wartet auf dich +

+

+ Erfahre sekundengenau, ob gerade der perfekte Moment für eine Runde ist — + zugeschnitten auf dich und deinen Hund. +

+
+ + +
+ ${[ + ['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]) => ` +
+
+ + + +
+
+
${title}
+
${sub}
+
+
+ `).join('')} +
+ + +
+ + ${!isLoggedIn ? ` + +

+ Mit Account werden Rekorde & Gassi-Score für deinen Hund gespeichert. +

+ ` : ''} +
+
`; + 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 ` -
-
+
+
${emoji} ${_esc(title)}
-
+
${_esc(value)}
-
+
${_esc(subtitle)}
diff --git a/backend/static/js/worlds.js b/backend/static/js/worlds.js index daacde0..d0f6bf2 100644 --- a/backend/static/js/worlds.js +++ b/backend/static/js/worlds.js @@ -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 = ` + +
+ + + + JETZT +
+
+ + + + WELT +
+ `; + 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 ? ` ` : ` @@ -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 ? `· Offline` : ''; - 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 = (() => {
${_esc(greet)}${firstName ? `, ${_esc(firstName)}` : ''}${stale}
-
${_esc(dayStr)}${weatherLine ? ' · ' + weatherLine : ''}${totalKm != null ? ' · ' + totalKm + ' km' : ''}
+
${_esc(dayStr)}
${user ? userAvatarHtml : ''}
@@ -909,17 +981,25 @@ window.Worlds = (() => { ${alertHtml} ${user && dog ? `
-
- - - Streak - ${streakVal} +
+
+ ${weatherEmoji} +
+
Gassi-Score
+
+ ${gassiScore ?? '—'} + ${gassiScore ? `/10` : ''} +
+ ${w ? `
${Math.round(w.temp_c ?? 0)}° · ${w.precip_prob ?? 0}% Regen
` : ''} +
+
Gassirunde + ${totalKm != null ? `∑ ${totalKm} km` : ''}
@@ -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 = ` -
-
🐶
-
Noch kein Hund angelegt
-
Erstelle das Profil deines Hundes
- +
+
+
🐶
+
Dein Hund wartet!
+
+ Lege ein Profil an und schalte alle Features frei +
+ +
+
+
+ +
+ ${features.map(f => ` +
+ + + + ${f.title} +
+ `).join('')} +
`; + el.querySelectorAll('[data-wnav]').forEach(e => e.addEventListener('click', () => navigateTo(e.dataset.wnav))); return; } @@ -1093,6 +1201,25 @@ window.Worlds = (() => {
+ ${!_hasBgPhoto ? ` +
+ + + +
+
+ Hintergrund-Foto hinzufügen +
+
+ Tagebuchfotos erscheinen hier als Panorama +
+
+
+ ` : ''}
${chips.map(c => _chip(c.icon, c.label, c.page)).join('')} diff --git a/backend/static/presse.html b/backend/static/presse.html index 2ea2e54..85ee38d 100644 --- a/backend/static/presse.html +++ b/backend/static/presse.html @@ -226,9 +226,90 @@
+ +
+ +
+ + +
+
+ Ban Yaro am Bach im Herbst +
+
+
Herbst am Bach
+
8064 × 6048 px · 20 MB JPEG
+ + ↓ Hi-Res herunterladen + +
+
+ + +
+
+ Ban Yaro im Schnee +
+
+
Winter im Schnee
+
Original-Auflösung · JPEG
+ + ↓ Hi-Res herunterladen + +
+
+ + +
+
+ Ban Yaro spielt im Frühling +
+
+
Frühling & Playdate
+
3199 × 2648 px · 3,8 MB JPEG
+ + ↓ Hi-Res herunterladen + +
+
+ + +
+
+ Ban Yaro neugierig am Baum +
+
+
Herbst & Neugier
+
8064 × 6048 px · 17 MB JPEG
+ + ↓ Hi-Res herunterladen + +
+
+ +
+

Alle Fotos: Ban Yaro (Kromfohrländer) · Fotograf: René Degelmann · Zur redaktionellen Verwendung freigegeben

+
+
- +