diff --git a/.gitignore b/.gitignore index cbcf3ae..28e4c9f 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,3 @@ __pycache__/ /icons/ .claude/worktrees/ Ban Yaro - Google Play package/ -/unsplash/ diff --git a/MARKETING.md b/MARKETING.md deleted file mode 100644 index d232fe2..0000000 --- a/MARKETING.md +++ /dev/null @@ -1,72 +0,0 @@ -# đŸŸ Ban Yaro — Marketing-Cockpit - -**Single Source of Truth fĂŒrs Marketing.** Vor jeder Aktion hier prĂŒfen, danach updaten — so wird nichts doppelt gemacht, vergessen oder ĂŒbersehen. Pflege: RenĂ© + Claude. - -_Stand: 2026-06-03_ - -> Diese Datei = Planung & Checkliste. FĂŒr **Live-Daten** (User-Meilenstein, Kanal-Tracking) lohnt zusĂ€tzlich ein Marketing-Tab im **Admin-Bereich** — siehe „Ausbau" unten. - -## 📊 Kanal-Überblick -| Kanal / Bereich | Status | NĂ€chster Schritt | -|---|---|---| -| Flyer Print | 🟱 1000 gedruckt (03.06.) | lokal verteilen | -| Flyer Digital | 💡 Idee | Doppelseiten-PDF + Empfehlungs-QR | -| Lokal (Ebersberg) | ⬜ offen | TierĂ€rzte, Hundeschulen, FutterlĂ€den, Tierheim | -| Online-Communities | ⬜ offen | FB-Gruppen Landkreis EBE + nebenan.de | -| Empfehlung / Referral | 🟡 Infra da (`referral_code`) | Empfehlungs-QR + Tracking sichtbar machen | -| Influencer | 🟡 2 Runden (Mai), kaum Resonanz | Runde 3 erst ab ~50 aktiven Usern | -| Presse / Blogs | 🟡 1 Runde, kaum Resonanz | keine Massenwelle; Nische zuerst | -| Verzeichnisse / Listings | ⬜ offen | Product Hunt, PWA-Dirs, Google Business EBE | -| SEO / KI-Auffindbarkeit | 🟡 technisch optimiert | Backlinks (Blog-Testberichte) | -| Landing Page | 🟡 Redesign-Briefing da | 3 Einstiege, Outcomes statt Features | -| App Store (iOS) | 🟱 in Review (1.0 (3), 03.06.) | Freigabe abwarten | -| Play Store (Android) | 🔮 ON HOLD | 12 Closed-Tester / 14 Tage fehlen | -| Merch / NFC-Halsband | 💡 recherchiert | 20 Tags fĂŒr Beta (~33 €) | - -Legende: 🟱 lĂ€uft/erledigt · 🟡 angefangen · ⬜ offen · 💡 Idee · 🔮 blockiert - -## ⏳ Gates / Trigger (nicht zu frĂŒh starten) -- **Influencer & Presse Runde 3** erst ab **~50 aktiven Usern** — vorher zu frĂŒh (Großredaktionen fragen zuerst nach Zahlen). → Bei jeder Session aktuelle User-Zahl checken. -- iOS-App ist nativ gebaut & in Review — **ĂŒberholt** die alte „iOS erst ab 10k via Rork/PWABuilder"-Strategie. - -## 📋 Backlog (konkret als NĂ€chstes) -- [ ] **Flyer lokal verteilen (Ebersberg)** — TierĂ€rzte (Wartezimmer), Hundeschulen/Welpengruppen, FutterlĂ€den, Hundesalons, Tierheim, Hundewiesen-AushĂ€nge, hundefreundliche CafĂ©s. Persönlich erklĂ€ren; AufhĂ€nger: Giftköder-Radar + „Daten in Deutschland". **Lokal bĂŒndeln, nicht streuen** (Community-Dichte fĂŒr Gassi-Treffen/Giftköder). -- [ ] **Digitaler Doppelseiten-Flyer (PDF)** mit **Empfehlungs-QR** fĂŒr Online-/Gruppen-Verteilung. Quelle: `promotion/flyer_a5_*.html`. _Offene Frage: generischer `?ref=empfehlung`-Link vs. pro-User `referral_code`._ -- [ ] **Lokale FB-Gruppen + nebenan.de** — Flyer-Foto + Link posten. -- [ ] **Verzeichnisse** — Product Hunt, progressivewebappstore.com, pwafire.org/directory, Google Business (Ebersberg). -- [ ] **Landing-Page-Redesign** nach Briefing (3 Zielgruppen-Einstiege Hundebesitzer/ZĂŒchter/WelpenkĂ€ufer, Outcomes statt Features, ZĂŒchter-SaaS prominent, Datenschutz als Argument, GrĂŒnder-Story + Foto). -- [ ] **Messung einbauen** — „Wie hast du von uns gehört?" im Onboarding + QR-refs pro Kanal. - -## ✅ Erledigt -- [x] 1000 Flyer A5 (zweiseitig) gedruckt — 03.06.2026 -- [x] iOS-App nativ gebaut + eingereicht (1.0 (3), in Review) — Details im Repo `banyaro-ios` -- [x] Influencer-Outreach Runde 1 (5) + Runde 2 (13) — Mai 2026 -- [x] SEO-Grundlagen (llms.txt, Landing About-Section) - -## 📈 Messung — was bringt wirklich Nutzer? -- **Onboarding-Frage „Wie hast du von uns gehört?"** (1 Klick) = billigste & wichtigste Kontrolle. _(noch einzubauen)_ -- **QR-refs pro Ort/Kanal** (z. B. `banyaro.app/?ref=tierarzt-grafing`) → ab nĂ€chster Flyer-Charge. -- **`referral_code`** (in DB, `routes/auth.py`) → Empfehlungen zĂ€hlbar. -- Aktive User aktuell: _[aus Admin eintragen]_ - -## 🗂 Details je Kanal - -### Influencer -2 Runden im Mai gesendet (`partner@banyaro.app`; DKIM/SPF/DMARC aktiv), **kaum Resonanz** — zu frĂŒh (wenige User), teils falsche Adressen (z. B. GEO → richtig `chefredaktion@geo.de`). -**Runde 3:** keine Massenwelle ohne PR-Agentur; **Hundeschulen/-trainer zuerst** (kleines Netzwerk, empfehlen aktiv Tools, Trainingsfeature ist stark), persönliche Mails, AufhĂ€nger = neue Features + echte Nutzerzahlen. -→ **Wer schon kontaktiert wurde:** AI-Memory `project_influencer_outreach` (Runde 1: verpinscht, missyminzi, wanderlust_samoyed, viviundholly, doguniversity, dogstv; Runde 2: nami.and.tommy, brina.explores, heimatherzen, pfotentick, flummis_diary, verwolft, wildwildwilli, knutini_, ninja.vom.wolfstor, pupsonality, osman_theparson, babybearyuki, dogswiss). **Vor neuer Runde dort prĂŒfen.** - -### Play Store (Android TWA) -PWABuilder-Paket fertig (`Ban Yaro - Google Play package/`, Package `app.banyaro.twa`). **BLOCKER:** Google verlangt 12 Closed-Tester ĂŒber 14 Tage — Tester fehlen (Engpass, nicht die Technik). assetlinks.json + Play-Console-Eintrag stehen bereit. Nicht priorisieren bis Tester da. - -### Merch / NFC-Halsband -Tag recherchiert: **HID Laundry Tag 16 mm** (shopnfc, SKU RE-ICO2-16, ~1 €/Stk ab 500), fĂŒr `banyaro.app/hund/{id}`. Beta: 20 Stk (~33 €) an erste Nutzer. - -### Flyer -Print: A5 zweiseitig, Quelle `promotion/flyer_a5_allgemein.html` + `flyer_a5_rueckseite.html`, QR → banyaro.app. Vorderseite = alle Hundebesitzer, RĂŒckseite stark ZĂŒchter-fokussiert. - -## 🚀 Ausbau: Live-Tool im Admin-Bereich (optional) -Diese Datei deckt Planung/Checkliste ab (Claude pflegt sie). Der **Admin-Bereich** lohnt sich fĂŒr die Teile mit echten Daten: -- **User-Meilenstein-Anzeige** (aktive User) → blendet automatisch den „Outreach Runde 3"-Hinweis ein, sobald ~50 erreicht. -- **Kanal-Tracking**: Auswertung „Wie gehört?" + QR-ref-ZĂ€hler + `referral_code`-Statistik. -- Optional: das Kanal-Board (Status/Backlog) als editierbare Admin-Seite. diff --git a/VERSION b/VERSION index 0948691..41edc23 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1155 \ No newline at end of file +1141 \ No newline at end of file diff --git a/backend/main.py b/backend/main.py index e954c83..df5124d 100644 --- a/backend/main.py +++ b/backend/main.py @@ -511,11 +511,11 @@ async def sitemap(): urls = [ ("https://banyaro.app/", "weekly", "1.0"), ("https://banyaro.app/zuechter", "weekly", "0.9"), - ("https://banyaro.app/wurfboerse", "daily", "0.8"), + ("https://banyaro.app/info", "monthly", "0.8"), + ("https://banyaro.app/presse", "monthly", "0.7"), ("https://banyaro.app/wiki/rassen", "weekly", "0.8"), - ("https://banyaro.app/help", "monthly", "0.7"), ("https://banyaro.app/knigge", "monthly", "0.7"), - ("https://banyaro.app/partner", "monthly", "0.6"), + ("https://banyaro.app/wurfboerse", "daily", "0.8"), ] try: @@ -526,6 +526,12 @@ async def sitemap(): for r in rassen: urls.append((f"https://banyaro.app/wiki/rasse/{r['slug']}", "monthly", "0.7")) + events = conn.execute( + "SELECT id FROM events WHERE datum >= date('now') LIMIT 200" + ).fetchall() + for e in events: + urls.append((f"https://banyaro.app/api/events/{e['id']}", "weekly", "0.5")) + # Öffentliche ZĂŒchter-Profile breeders = conn.execute( "SELECT bp.zwingername FROM breeder_profiles bp " @@ -1342,47 +1348,12 @@ async def public_dog_page(dog_id: int): # ------------------------------------------------------------------ @app.get("/teilen/{token}") async def invite_page(token: str): - from fastapi.responses import HTMLResponse - with open(f"{STATIC_DIR}/index.html", encoding="utf-8") as _f: - _html = _f.read() - _html = _html.replace( - '', - '' - ) - return HTMLResponse(content=_html, headers={"Cache-Control": "no-store, no-cache"}) + return FileResponse(f"{STATIC_DIR}/index.html", headers={"Cache-Control": "no-store, no-cache"}) @app.get("/breeder/{zwingername}") async def breeder_profile_page(zwingername: str): - from fastapi.responses import HTMLResponse - from urllib.parse import unquote - from database import db as _db - import html as _html_mod - name = unquote(zwingername) - desc = f"HundezĂŒchter {_html_mod.escape(name)} auf Ban Yaro — Wurfbörse, Stammbaum und mehr." - try: - with _db() as conn: - bp = conn.execute( - "SELECT bp.rasse, bp.beschreibung FROM breeder_profiles bp " - "JOIN users u ON u.id = bp.user_id WHERE bp.zwingername=? AND u.rolle='breeder' LIMIT 1", - (name,) - ).fetchone() - if bp and bp["beschreibung"]: - desc = _html_mod.escape(bp["beschreibung"][:160]) - except Exception: - pass - with open(f"{STATIC_DIR}/index.html", encoding="utf-8") as _f: - _page = _f.read() - _page = _page.replace( - '', - f'' - ).replace( - 'Ban Yaro', - f'{_html_mod.escape(name)} — HundezĂŒchter auf Ban Yaro' - f'\n ' - f'\n ' - ) - return HTMLResponse(content=_page, headers={"Cache-Control": "no-store, no-cache"}) + return FileResponse(f"{STATIC_DIR}/index.html", headers={"Cache-Control": "no-store, no-cache"}) @app.get("/litters") @@ -1500,7 +1471,6 @@ async def ausweis_page(dog_id: int, request: Request): - Heimtierausweis – {esc(dog["name"])} -
@@ -1765,9 +1727,7 @@ async def help_page(): k for k in by_kat.keys() if k not in KAT_LABEL ] - import json as _json sections_html = "" - faq_items = [] for kat in kat_order: label = KAT_LABEL.get(kat, kat.replace("_", " ").title()) items = "".join( @@ -1776,13 +1736,6 @@ async def help_page(): for a in by_kat[kat] ) sections_html += f'

{_html.escape(label)}

{items}
' - for a in by_kat[kat]: - faq_items.append({ - "@type": "Question", - "name": a["frage"], - "acceptedAnswer": {"@type": "Answer", "text": a["antwort"]} - }) - faq_json_ld = _json.dumps(faq_items, ensure_ascii=False) html = f""" @@ -1791,8 +1744,6 @@ async def help_page(): Hilfe & FAQ — Ban Yaro - - - @@ -1905,8 +1851,6 @@ async def konto_loeschen(): Konto löschen — Ban Yaro - - - - -
-

Wurfbörse

-

Hundewelpen von geprĂŒften ZĂŒchtern

-
-
-

{count_text}

- {litters_html or '

Aktuell keine WĂŒrfe eingetragen.
Schau bald wieder vorbei!

'} - -
- -""" - return HTMLResponse(content=html, headers={"Cache-Control": "max-age=1800"}) - - # SPA Fallback — ALLE nicht-API-Routen gehen zur index.html @app.get("/{full_path:path}") async def spa_fallback(full_path: str): diff --git a/backend/routes/forum.py b/backend/routes/forum.py index 33eb726..f8ee5e0 100644 --- a/backend/routes/forum.py +++ b/backend/routes/forum.py @@ -586,25 +586,6 @@ async def toggle_like(data: LikeBody, user=Depends(get_current_user)): return {"liked": liked, "count": count} -# ------------------------------------------------------------------ -# GET /api/forum/likes/{target_type}/{target_id} — Wer hat geliked? -# ------------------------------------------------------------------ -@router.get("/likes/{target_type}/{target_id}") -async def list_likers(target_type: str, target_id: int): - if target_type not in _LIKE_TABLE: - raise HTTPException(400, "UngĂŒltiger Typ.") - with db() as conn: - rows = conn.execute( - """SELECT u.name AS name, u.founder_number AS founder_number - FROM forum_likes fl - JOIN users u ON u.id = fl.user_id - WHERE fl.target_type = ? AND fl.target_id = ? - ORDER BY fl.id DESC""", - (target_type, target_id) - ).fetchall() - return [dict(r) for r in rows] - - # ------------------------------------------------------------------ # POST /api/forum/report # ------------------------------------------------------------------ diff --git a/backend/routes/osm.py b/backend/routes/osm.py index 71ed5d2..5fc22b9 100644 --- a/backend/routes/osm.py +++ b/backend/routes/osm.py @@ -423,65 +423,3 @@ async def submit_poi_edit(osm_id: str, data: PoiEditCreate, poi[data.field], data.new_value.strip(), user["id"]) ) return {"status": "pending", "message": "Korrektur wurde zur PrĂŒfung eingereicht."} - - -# ------------------------------------------------------------------ -# Geocoding-Proxy GET /api/osm/geocode?q=
 -# Nominatim-Rate-Limit: 1 req/s — serverseitig throttled -# ------------------------------------------------------------------ -_nominatim_sem = asyncio.Semaphore(1) -_nominatim_last = 0.0 - -@router.get('/geocode') -async def geocode_search(q: str = Query(..., min_length=2, max_length=200)): - import time - global _nominatim_last - async with _nominatim_sem: - wait = 1.1 - (time.monotonic() - _nominatim_last) - if wait > 0: - await asyncio.sleep(wait) - _nominatim_last = time.monotonic() - try: - async with httpx.AsyncClient(timeout=6.0) as client: - resp = await client.get( - 'https://nominatim.openstreetmap.org/search', - params={ - 'q': q, - 'format': 'jsonv2', - 'limit': 6, - 'countrycodes': 'de,at,ch', - 'addressdetails': 1, - 'accept-language': 'de', - }, - headers={ - 'User-Agent': _OVERPASS_UA, - 'Referer': 'https://banyaro.app/', - } - ) - resp.raise_for_status() - data = resp.json() - except Exception as e: - logger.warning("Nominatim-Fehler: %s", e) - raise HTTPException(502, "Geocoding nicht verfĂŒgbar") - - out = [] - for r in data[:6]: - addr = r.get('address', {}) - short = ( - addr.get('amenity') or addr.get('shop') or addr.get('leisure') or - addr.get('road') or addr.get('village') or addr.get('town') or - addr.get('city') or r.get('name') or - r.get('display_name', '').split(',')[0] - ) - city = addr.get('city') or addr.get('town') or addr.get('village') or addr.get('municipality') or '' - state = addr.get('state', '') - subtitle = ', '.join(filter(None, [city, state])) - out.append({ - 'lat': float(r['lat']), - 'lon': float(r['lon']), - 'name': short, - 'subtitle': subtitle, - 'full': r.get('display_name', ''), - 'type': r.get('type', ''), - }) - return out diff --git a/backend/static/css/components.css b/backend/static/css/components.css index 1f22ba8..1f724ec 100644 --- a/backend/static/css/components.css +++ b/backend/static/css/components.css @@ -3256,182 +3256,6 @@ html.modal-open { } } -/* Orts-Suche — Panel schiebt von oben rein wenn aktiv */ -.map-search-wrap { - position: absolute; - top: 0; - left: 0; - right: 0; - z-index: 1002; - padding: 8px 12px 4px; - background: rgba(255,255,255,0.97); - backdrop-filter: blur(8px); - box-shadow: 0 3px 14px rgba(0,0,0,0.18); - transform: translateY(-110%); - transition: transform 0.22s ease; - pointer-events: none; -} -.map-search-wrap.active { - transform: translateY(0); - pointer-events: auto; -} -:root[data-theme="dark"] .map-search-wrap { background: rgba(22,22,24,0.97); } -.map-search-row { - display: flex; - align-items: center; - gap: 8px; - background: var(--c-bg, #fff); - border-radius: var(--radius-full); - border: 1px solid var(--c-border, #e5e7eb); - padding: 8px 12px; -} -.map-search-input { - flex: 1; - border: none; - outline: none; - font-size: 15px; - font-family: inherit; - background: transparent; - color: var(--c-text); - min-width: 0; -} -.map-search-input::placeholder { color: #aaa; } -.map-search-clear { - background: none; - border: none; - padding: 4px; - cursor: pointer; - color: #999; - line-height: 1; - flex-shrink: 0; - border-radius: 50%; -} -.map-search-clear:hover { color: var(--c-text); background: var(--c-bg-subtle); } -.map-search-results { - background: var(--c-bg, #fff); - border-radius: 12px; - border: 1px solid var(--c-border, #e5e7eb); - margin-top: 6px; - margin-bottom: 4px; - overflow: hidden; - max-height: 240px; - overflow-y: auto; -} -.map-search-item { - padding: 10px 14px; - cursor: pointer; - border-bottom: 1px solid var(--c-border-light, rgba(0,0,0,0.05)); -} -.map-search-item:last-child { border-bottom: none; } -.map-search-item:hover, -.map-search-item:active { background: var(--c-primary-subtle, #fef3c7); } -.map-search-item-name { - font-size: 13px; - font-weight: 600; - color: var(--c-text); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} -.map-search-item-sub { - font-size: 11px; - color: var(--c-text-secondary); - margin-top: 1px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} -.map-search-loading, -.map-search-empty { - padding: 12px 14px; - font-size: 13px; - color: var(--c-text-secondary); - text-align: center; -} - -/* Speed Dial — Ein Trigger-Button, Sub-Buttons fĂ€chern nach oben auf */ -.map-speed-dial { - position: absolute; - bottom: calc(var(--safe-bottom) + 82px); - right: 20px; - z-index: 1000; - display: flex; - flex-direction: column; - align-items: flex-end; - gap: var(--space-2); -} -.map-sd-items { - display: flex; - flex-direction: column-reverse; /* unterste Item = erstes im DOM */ - align-items: flex-end; - gap: var(--space-2); - pointer-events: none; -} -.map-speed-dial.open .map-sd-items { pointer-events: auto; } - -.map-sd-item { - display: flex; - align-items: center; - gap: 10px; - opacity: 0; - transform: translateY(8px) scale(0.88); - transition: opacity 0.16s ease, transform 0.16s ease; -} -.map-speed-dial.open .map-sd-item { opacity: 1; transform: translateY(0) scale(1); } -.map-speed-dial.open .map-sd-item:nth-child(1) { transition-delay: 0ms; } -.map-speed-dial.open .map-sd-item:nth-child(2) { transition-delay: 50ms; } -.map-speed-dial.open .map-sd-item:nth-child(3) { transition-delay: 100ms; } -.map-speed-dial.open .map-sd-item:nth-child(4) { transition-delay: 150ms; } -.map-speed-dial.open .map-sd-item:nth-child(5) { transition-delay: 200ms; } - -.map-sd-label { - background: rgba(20,20,20,0.72); - color: #fff; - font-size: 12px; - font-weight: 600; - padding: 5px 11px; - border-radius: var(--radius-full); - white-space: nowrap; - backdrop-filter: blur(4px); - pointer-events: none; - letter-spacing: 0.01em; -} -.map-sd-btn { - width: 46px; - height: 46px; - border-radius: 50%; - background: #fff; - color: #C4843A; - border: 2px solid rgba(196,132,58,0.25); - box-shadow: 0 2px 8px rgba(0,0,0,0.22); - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - font-size: 1.1rem; - flex-shrink: 0; - transition: background 0.12s, color 0.12s; - -webkit-tap-highlight-color: transparent; -} -.map-sd-btn:hover, -.map-sd-btn:active { background: #fef3c7; } -.map-sd-btn.active { background: #C4843A; color: #fff; border-color: #C4843A; } -.map-sd-btn.map-fab--pin.active { background: var(--c-danger); border-color: var(--c-danger); color: #fff; } -#map-radar-btn.active { background: #1d4ed8; color: #fff; border-color: #1d4ed8; } -#map-temp-btn.active { background: #dc2626; color: #fff; border-color: #dc2626; } - -.map-sd-trigger { - transition: background 0.15s, transform 0.2s ease; -} -.map-speed-dial.open .map-sd-trigger { - background: #6b4a20; - transform: rotate(90deg); -} -.map-sd-icon-open { display: block; } -.map-sd-icon-close { display: none; } -.map-speed-dial.open .map-sd-icon-open { display: none; } -.map-speed-dial.open .map-sd-icon-close { display: block; } - /* FAB-Gruppe rechts unten — direkt ĂŒber dem ZurĂŒck-Button */ .map-fabs { position: absolute; diff --git a/backend/static/index.html b/backend/static/index.html index da37ccd..a85a5a8 100644 --- a/backend/static/index.html +++ b/backend/static/index.html @@ -86,14 +86,14 @@ Ban Yaro - + - - - - - + + + + + @@ -617,11 +617,11 @@ - - - - - + + + + + @@ -631,7 +631,7 @@ - + diff --git a/backend/static/js/api.js b/backend/static/js/api.js index 7fd420e..de39835 100644 --- a/backend/static/js/api.js +++ b/backend/static/js/api.js @@ -45,12 +45,9 @@ const API = (() => { throw new APIError(msg, 0, 'network'); } - // Versions-Check: Server meldet neue Version → beim nĂ€chsten navigate() aktualisieren. - // Ausnahme: _BY_SW_RELOAD = wir sind gerade von /force-update weitergeleitet worden. - // In dem Fall ist APP_VER kurzzeitig veraltet (SW-Cache lĂ€uft noch aus) — KEIN erneuter - // Pending setzen, sonst entsteht sofort ein Loop beim nĂ€chsten Seitenwechsel. + // Versions-Check: Server meldet neue Version → Banner anzeigen (einmalig) const serverVer = response.headers.get('x-app-version'); - if (serverVer && serverVer !== APP_VER && !window._byUpdatePending && !window._BY_SW_RELOAD) { + if (serverVer && serverVer !== APP_VER && !window._byUpdatePending) { window._byUpdatePending = true; window._byNewVersion = serverVer; } @@ -442,9 +439,6 @@ const API = (() => { like(targetType, targetId) { return post('/forum/like', { target_type: targetType, target_id: targetId }); }, - likers(targetType, targetId) { - return get(`/forum/likes/${targetType}/${targetId}`); - }, report(targetType, targetId, grund) { return post('/forum/report', { target_type: targetType, target_id: targetId, grund }); }, diff --git a/backend/static/js/app.js b/backend/static/js/app.js index ce91fdf..187a0e1 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 = '1155'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '1141'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen const APP_VERSION = '1.6.0'; // ← semantische Version, wird bei make release gesetzt window.APP_VER = APP_VER; // global verfĂŒgbar fĂŒr andere Module (z.B. offline-indicator) window.APP_VERSION = APP_VERSION; diff --git a/backend/static/js/boot.js b/backend/static/js/boot.js index d1ef297..9f54463 100644 --- a/backend/static/js/boot.js +++ b/backend/static/js/boot.js @@ -57,10 +57,7 @@ if ('serviceWorker' in navigator) { if (!sw) return; sw.addEventListener('statechange', function() { if (sw.state === 'activated') { - if (sessionStorage.getItem('by_skip_sw_reload')) { - sessionStorage.removeItem('by_skip_sw_reload'); // einmalig konsumieren - return; - } + if (sessionStorage.getItem('by_skip_sw_reload')) return; window.location.replace('/?_t=' + Date.now()); } }); diff --git a/backend/static/js/pages/diary.js b/backend/static/js/pages/diary.js index 686007c..653b117 100644 --- a/backend/static/js/pages/diary.js +++ b/backend/static/js/pages/diary.js @@ -1306,7 +1306,37 @@ window.Page_diary = (() => {
-
+ + +
+
+ +
+ + +
+
+
+ + ${UI.escape(entry?.location_name || '')} + +
+
+
+ + +
+ +
${dogPickerHtml}
@@ -1508,15 +1538,140 @@ window.Page_diary = (() => { let _locLat = (entry?.gps_lat != null) ? entry.gps_lat : null; let _locLon = (entry?.gps_lon != null) ? entry.gps_lon : null; let _locName = entry?.location_name || null; + let _miniMap = null, _miniMarker = null; - // Location Picker (gemeinsame UI-Komponente) - setTimeout(() => { - const _diaryPicker = UI.locationPicker({ - containerId: 'diary-location-picker', - onSelect: (lat, lon, name) => { _locLat = lat; _locLon = lon; _locName = name; }, + const _pinSvg = ''; + const _mkIcon = () => L.divIcon({ html: _pinSvg, className: '', iconSize: [32,40], iconAnchor: [16,40] }); + + function _setName(name) { + _locName = name; + document.getElementById('diary-location-label').textContent = name; + document.getElementById('diary-location-chip-wrap').style.display = ''; + document.getElementById('diary-location-suggestions').style.display = 'none'; + } + + function _placeMarker(lat, lon) { + if (_miniMarker) { _miniMarker.setLatLng([lat, lon]); return; } + _miniMarker = L.marker([lat, lon], { draggable: false, icon: _mkIcon() }).addTo(_miniMap); + _miniMarker.on('dragend', () => { + const p = _miniMarker.getLatLng(); _locLat = p.lat; _locLon = p.lng; + document.getElementById('diary-location-btn-label').textContent = 'POI suchen'; }); - if (_locLat != null) _diaryPicker.setValue(_locLat, _locLon, _locName); - }, 50); + } + + document.getElementById('diary-location-clear')?.addEventListener('click', () => { + _locName = null; + document.getElementById('diary-location-chip-wrap').style.display = 'none'; + }); + const _clearBtn = document.getElementById('diary-coords-clear'); + let _clearPending = false; + _clearBtn?.addEventListener('click', () => { + if (!_clearPending) { + _clearPending = true; + _clearBtn.textContent = 'Wirklich entfernen?'; + _clearBtn.style.color = 'var(--c-danger)'; + setTimeout(() => { + if (_clearPending) { + _clearPending = false; + _clearBtn.textContent = 'Ort entfernen'; + _clearBtn.style.color = 'var(--c-text-muted)'; + } + }, 3000); + return; + } + _clearPending = false; + _clearBtn.textContent = 'Ort entfernen'; + _clearBtn.style.color = 'var(--c-text-muted)'; + _locLat = null; _locLon = null; _locName = null; + document.getElementById('diary-location-chip-wrap').style.display = 'none'; + document.getElementById('diary-location-suggestions').style.display = 'none'; + document.getElementById('diary-location-btn-label').textContent = 'POI suchen'; + if (_miniMarker) { _miniMarker.remove(); _miniMarker = null; } + if (_miniMap) { _miniMap.setView([48.0, 11.9], 7); _setMapEditing(false); } + }); + + let _mapEditing = false; + + function _setMapEditing(on) { + _mapEditing = on; + const lbl = document.getElementById('diary-map-edit-label'); + if (lbl) lbl.textContent = on ? 'Fertig' : 'Position Àndern'; + if (!_miniMap) return; + if (on) { + if (_miniMarker) _miniMarker.dragging.enable(); + } else { + if (_miniMarker) _miniMarker.dragging.disable(); + } + } + + document.getElementById('diary-map-edit-btn')?.addEventListener('click', () => { + _setMapEditing(!_mapEditing); + }); + + // Karte beim Formular-Open automatisch laden + UI.loadLeaflet().then(() => { + setTimeout(() => { + const lat = _locLat || 48.0, lon = _locLon || 11.9, zoom = _locLat ? 15 : 7; + _miniMap = L.map('diary-map-wrap', { + zoomControl: true, attributionControl: false, + dragging: true, scrollWheelZoom: false, + }).setView([lat, lon], zoom); + L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19 }) + .addTo(_miniMap); + _miniMap.invalidateSize(); + if (_locLat) { + _placeMarker(lat, lon); + _miniMarker.dragging.disable(); // Lesemodus: kein Drag + } + // Klick nur im Edit-Modus + _miniMap.on('click', e => { + if (!_mapEditing) return; + _locLat = e.latlng.lat; _locLon = e.latlng.lng; + _placeMarker(_locLat, _locLon); + if (!_mapEditing) _miniMarker.dragging.disable(); + document.getElementById('diary-location-btn-label').textContent = 'POI suchen'; + }); + }, 150); + }); + + async function _showSuggestions() { + const btn = document.getElementById('diary-location-btn'); + UI.setLoading(btn, true); + try { + let lat = _locLat, lon = _locLon; + if (lat == null || lon == null) { + const pos = await API.getLocation(); + lat = pos.lat; lon = pos.lon; + _locLat = lat; _locLon = lon; + if (_miniMap) { _miniMap.setView([lat, lon], 15); _placeMarker(lat, lon); } + document.getElementById('diary-location-btn-label').textContent = 'POI suchen'; + } + const suggestions = await API.diary.nearby(_appState.activeDog.id, lat, lon); + const sugEl = document.getElementById('diary-location-suggestions'); + if (suggestions.length === 0) { + sugEl.innerHTML = '

Keine Orte in der NĂ€he gefunden.

'; + } else { + sugEl.innerHTML = suggestions.map(s => ` + `).join(''); + sugEl.querySelectorAll('.diary-location-suggestion').forEach(el => { + el.addEventListener('click', () => _setName(el.dataset.name)); + }); + } + sugEl.style.display = ''; + } catch (err) { + UI.toast.error(err?.message?.includes('GPS') || lat == null + ? 'GPS nicht verfĂŒgbar.' : 'Ortssuche fehlgeschlagen.'); + } finally { + UI.setLoading(btn, false); + } + } + + document.getElementById('diary-location-btn')?.addEventListener('click', _showSuggestions); document.getElementById('diary-form-delete')?.addEventListener('click', async () => { const ok = await UI.modal.confirm({ diff --git a/backend/static/js/pages/forum.js b/backend/static/js/pages/forum.js index 512ed6d..e2dfe19 100644 --- a/backend/static/js/pages/forum.js +++ b/backend/static/js/pages/forum.js @@ -640,17 +640,6 @@ function _fmtDate(iso) { } catch (err) { UI.toast.error(err.message); } }); - // Liker-Liste anzeigen (Klick auf die Zahl) - const _thLikeCount = document.getElementById('thread-like-count'); - if (_thLikeCount) { - _thLikeCount.style.cursor = 'pointer'; - _thLikeCount.title = 'Wer hat geliked?'; - _thLikeCount.addEventListener('click', e => { - e.stopPropagation(); - if ((thread.likes || 0) > 0) _showLikers('thread', thread.id); - }); - } - // Report thread document.getElementById('thread-report-btn')?.addEventListener('click', () => { _showReportForm('thread', thread.id); @@ -823,9 +812,9 @@ function _fmtDate(iso) { // Like container.querySelectorAll('.forum-post-like:not([data-bound])').forEach(btn => { btn.dataset.bound = '1'; - const postId = parseInt(btn.dataset.postId); btn.addEventListener('click', async () => { if (!uid) { UI.toast.info('Bitte erst anmelden.'); return; } + const postId = parseInt(btn.dataset.postId); try { const res = await API.forum.like('post', postId); btn.classList.toggle('active', res.liked); @@ -833,16 +822,6 @@ function _fmtDate(iso) { if (countEl) countEl.textContent = res.count; } catch (err) { UI.toast.error(err.message); } }); - // Klick auf die Zahl → Liker-Liste - const countEl = btn.querySelector('.forum-post-like-count'); - if (countEl) { - countEl.style.cursor = 'pointer'; - countEl.title = 'Wer hat geliked?'; - countEl.addEventListener('click', e => { - e.stopPropagation(); - if (parseInt(countEl.textContent) > 0) _showLikers('post', postId); - }); - } }); // Report @@ -895,28 +874,6 @@ function _fmtDate(iso) { }); } - // ---------------------------------------------------------- - // Liker-Liste — wer hat geliked? - // ---------------------------------------------------------- - async function _showLikers(targetType, targetId) { - try { - const likers = await API.forum.likers(targetType, targetId); - if (!likers.length) { UI.toast.info('Noch keine Likes.'); return; } - const rows = likers.map(l => ` -
-
${UI.escape(_initial(l.name))}
- ${UI.escape(l.name || 'Unbekannt')} - ${l.founder_number ? `GrĂŒnder #${l.founder_number}` : ''} -
`).join(''); - UI.modal.open({ - title: `${UI.icon('heart')} ${likers.length} ${likers.length === 1 ? 'Like' : 'Likes'}`, - body: `
${rows}
`, - footer: ``, - }); - document.getElementById('likers-close')?.addEventListener('click', UI.modal.close); - } catch (err) { UI.toast.error(err.message); } - } - // ---------------------------------------------------------- // Report-Formular // ---------------------------------------------------------- diff --git a/backend/static/js/pages/map.js b/backend/static/js/pages/map.js index 00a4324..7b70409 100644 --- a/backend/static/js/pages/map.js +++ b/backend/static/js/pages/map.js @@ -59,7 +59,6 @@ window.Page_map = (() => { treffpunkt: [], community: [], zuechter: [], - hotel: [], }; const VISIBLE_KEY = 'by_map_visible_v1'; @@ -131,10 +130,6 @@ window.Page_map = (() => { interactive: false, }; - // Orts-Suche - let _searchTimer = null; - let _searchMarker = null; - let _overpassTimer = null; let _overpassActive = false; let _ringClosing = false; @@ -215,50 +210,13 @@ window.Page_map = (() => {
- -
-
- - - -
- -
- - -
-
- -
- Mein Standort - -
-
- Ort suchen - -
-
- Marker setzen - -
- ${App.hasPro(_appState?.user) ? ` -
- Regenradar - -
-
- Temperatur - -
- ` : ''} -
- +
+ + ${App.hasPro(_appState?.user) ? ` + + + ` : ''} +
@@ -331,19 +289,7 @@ window.Page_map = (() => { _saveVisible(); }); - // Speed Dial - const _sdEl = document.getElementById('map-speed-dial'); - document.getElementById('map-sd-trigger')?.addEventListener('click', e => { - e.stopPropagation(); - _sdEl?.classList.toggle('open'); - }); - // Klick auf Karte / außerhalb schließt Speed Dial - document.getElementById('central-map')?.addEventListener('pointerdown', () => { - _sdEl?.classList.remove('open'); - }); - document.getElementById('map-locate-btn').addEventListener('click', () => { - _sdEl?.classList.remove('open'); if (_userPos) { _map?.setView([_userPos.lat, _userPos.lon], 16); } else { @@ -351,54 +297,9 @@ window.Page_map = (() => { } }); - document.getElementById('map-pin-btn').addEventListener('click', () => { - _sdEl?.classList.remove('open'); - _togglePlacementMode(); - }); - document.getElementById('map-radar-btn')?.addEventListener('click', () => { - _sdEl?.classList.remove('open'); - _toggleRadar(); - }); - document.getElementById('map-temp-btn')?.addEventListener('click', () => { - _sdEl?.classList.remove('open'); - _toggleTemp(); - }); - - // Suche — FAB öffnet Panel - document.getElementById('map-search-btn')?.addEventListener('click', () => { - document.getElementById('map-speed-dial')?.classList.remove('open'); - const wrap = document.getElementById('map-search-wrap'); - const isOpen = wrap?.classList.contains('active'); - if (isOpen) { - _clearSearch(); - } else { - wrap?.classList.add('active'); - setTimeout(() => document.getElementById('map-search-input')?.focus(), 60); - document.getElementById('map-search-btn')?.classList.add('active'); - } - }); - - const searchInput = document.getElementById('map-search-input'); - const searchResults = document.getElementById('map-search-results'); - - searchInput?.addEventListener('input', () => { - const q = searchInput.value.trim(); - clearTimeout(_searchTimer); - if (q.length < 2) { searchResults.style.display = 'none'; return; } - _searchTimer = setTimeout(() => _runSearch(q), 400); - }); - - searchInput?.addEventListener('keydown', e => { - if (e.key === 'Escape') _clearSearch(); - }); - - document.getElementById('map-search-clear')?.addEventListener('click', _clearSearch); - - // Klick auf Karte schließt Ergebnisse (aber behĂ€lt Marker) - document.getElementById('central-map')?.addEventListener('pointerdown', () => { - searchResults.style.display = 'none'; - searchInput?.blur(); - }); + document.getElementById('map-pin-btn').addEventListener('click', _togglePlacementMode); + document.getElementById('map-radar-btn')?.addEventListener('click', _toggleRadar); + document.getElementById('map-temp-btn')?.addEventListener('click', _toggleTemp); } // ---------------------------------------------------------- @@ -1006,7 +907,7 @@ window.Page_map = (() => { const params = new URLSearchParams({ type: osmType, ...bbox }); try { const pois = await fetch(`/api/osm/pois?${params}`).then(r => r.json()); - const osmCount = (_layers[layerKey] || []).filter(m => !m._ownPlace).length; + const osmCount = _layers[layerKey].filter(m => !m._ownPlace).length; if (pois.length !== osmCount) _replaceOsmMarkers(layerKey, pois); _done++; const pct = Math.round(20 + _done / _total * 80); @@ -1018,14 +919,11 @@ window.Page_map = (() => { const pct = Math.round(20 + _done / _total * 80); const total = Object.values(_layers).flat().filter(m => !m._ownPlace).length; _setOsmStatus(pct < 100 ? `Scanne
` : `${total} Marker`, pct); - return (_layers[layerKey] || []).filter(m => !m._ownPlace).length; + return _layers[layerKey].filter(m => !m._ownPlace).length; } }); - try { - await Promise.all(freshTasks); - } finally { - _overpassActive = false; - } + await Promise.all(freshTasks); + _overpassActive = false; const totalLoaded = Object.values(_layers).flat().filter(m => !m._ownPlace).length; const allHidden = Object.keys(OSM_LAYER_MAP).every(k => _visible[k] === false); @@ -1033,13 +931,10 @@ window.Page_map = (() => { _setOsmStatus('Layer deaktiviert — Liste antippen', 100); } - // Wenn 0 OSM-Marker: Hintergrund-Overpass-Fetch lĂ€uft noch — bis zu 8× nachfragen - // Overpass fĂŒr alle Layer sequential: bis zu ~4min → Retries mĂŒssen das abdecken - if (totalLoaded === 0 && zoom >= 14 && _autoRetryCount < 8) { + // Wenn 0 OSM-Marker: Hintergrund-Fetch lĂ€uft noch — max 3× automatisch nachfragen + if (totalLoaded === 0 && zoom >= 14 && _autoRetryCount < 3) { _autoRetryCount++; - // 10s, 20s, 35s, 50s, 70s, 90s, 120s, 150s - const delays = [10000, 20000, 35000, 50000, 70000, 90000, 120000, 150000]; - const delay = delays[_autoRetryCount - 1] || 120000; + const delay = _autoRetryCount * 30000; // 30s, 60s, 90s _setOsmStatus(`Neue Umgebung – Daten werden geladen
`); setTimeout(() => { if (!_overpassActive) _scheduleOsmLoad(); }, delay); } @@ -2049,92 +1944,6 @@ window.Page_map = (() => { } catch { /* still */ } } - // ---------------------------------------------------------- - // Orts-Suche (Nominatim-Proxy) - // ---------------------------------------------------------- - async function _runSearch(q) { - const resultsEl = document.getElementById('map-search-results'); - if (!resultsEl) return; - resultsEl.innerHTML = '
Suche

'; - resultsEl.style.display = ''; - try { - const data = await API.get(`/osm/geocode?q=${encodeURIComponent(q)}`); - if (!data.length) { - resultsEl.innerHTML = '
Keine Ergebnisse
'; - return; - } - resultsEl.innerHTML = data.map((r, i) => - `
-
${UI.escape(r.name)}
- ${r.subtitle ? `
${UI.escape(r.subtitle)}
` : ''} -
` - ).join(''); - resultsEl.querySelectorAll('.map-search-item').forEach(el => { - el.addEventListener('pointerdown', e => { - e.stopPropagation(); - const r = data[+el.dataset.i]; - _flyToResult(r); - document.getElementById('map-search-input').value = r.name; - document.getElementById('map-search-clear').style.display = ''; - resultsEl.style.display = 'none'; - }); - }); - } catch { - resultsEl.innerHTML = '
Suche nicht verfĂŒgbar
'; - } - } - - function _flyToResult(r) { - if (!_map || !window.L) return; - _searchMarker?.remove(); - _map.flyTo([r.lat, r.lon], 15, { duration: 1.0 }); - _searchMarker = L.marker([r.lat, r.lon], { - icon: L.divIcon({ - className: '', - html: `
- - - - -
`, - iconSize: [32, 32], - iconAnchor: [16, 32], - }), - zIndexOffset: 1000, - }) - .addTo(_map) - .bindPopup(`
${UI.escape(r.name)}
- ${r.subtitle ? `
${UI.escape(r.subtitle)}
` : ''} - `, { maxWidth: 240 }) - .openPopup(); - - setTimeout(() => { - document.getElementById('search-marker-close')?.addEventListener('click', () => { - _clearSearch(); - _searchMarker?.closePopup(); - }); - }, 50); - } - - function _clearSearch() { - const input = document.getElementById('map-search-input'); - const results = document.getElementById('map-search-results'); - const wrap = document.getElementById('map-search-wrap'); - const btn = document.getElementById('map-search-btn'); - if (input) { input.value = ''; input.blur(); } - if (results) results.style.display = 'none'; - wrap?.classList.remove('active'); - btn?.classList.remove('active'); - _searchMarker?.remove(); - _searchMarker = null; - clearTimeout(_searchTimer); - } - return { init, refresh, onDogChange, startRecording: _startRecording, stopRecording: _stopRecording, isRecording: () => _recActive }; })(); diff --git a/backend/static/js/pages/routes.js b/backend/static/js/pages/routes.js index bf3ccaa..0f4e1af 100644 --- a/backend/static/js/pages/routes.js +++ b/backend/static/js/pages/routes.js @@ -1698,10 +1698,6 @@ window.Page_routes = (() => { center: [mid.lat, mid.lon], zoom: 15, zoomControl: false, attributionControl: false, }); - // Container hat im frisch eingefĂŒgten Fixed-Overlay erst jetzt seine - // finale Flex-Höhe — Leaflet muss sie neu vermessen, sonst lĂ€dt es nur - // oben Tiles und der Rest bleibt grau. - _navMap.invalidateSize(); // Route-Polylines: erledigt (grĂŒn) + ausstehend (orange) const doneLine = L.polyline([], { color: '#22c55e', weight: 5, opacity: 0.85 }).addTo(_navMap); @@ -1709,14 +1705,6 @@ window.Page_routes = (() => { _navMap.fitBounds(remainLine.getBounds(), { padding: [20, 20] }); _addRouteArrows(_navMap, track, '#3b82f6'); - // iOS rendert das Flex-Layout teils verzögert — nochmal neu vermessen - // und Ausschnitt erneut anpassen. - setTimeout(() => { - if (!_navMap) return; - _navMap.invalidateSize(); - _navMap.fitBounds(remainLine.getBounds(), { padding: [20, 20] }); - }, 250); - // Start/End-Marker (als Variable damit Reverse sie neu setzen kann) const mkPin = (p, color) => L.circleMarker([p.lat, p.lon], { radius: 8, color: '#fff', weight: 2, fillColor: color, fillOpacity: 1 diff --git a/backend/static/js/pages/walks.js b/backend/static/js/pages/walks.js index dfa11ee..2d2072e 100644 --- a/backend/static/js/pages/walks.js +++ b/backend/static/js/pages/walks.js @@ -897,6 +897,8 @@ window.Page_walks = (() => { let _locLon = v.lon != null ? parseFloat(v.lon) : null; let _locName = v.ort_name || null; + const _pinSvg = ''; + const body = `
@@ -922,7 +924,48 @@ window.Page_walks = (() => {
-
+ + +
+
+ +
+ + +
+
+
+ ${UI.icon('map-pin')} + ${UI.escape(_locName || '')} + +
+
+ +
+ + +
+ + + +
+ + + + +
@@ -953,16 +996,157 @@ window.Page_walks = (() => { document.getElementById('wf-cancel')?.addEventListener('click', UI.modal.close); - // Location Picker - let _wfPicker = null; - setTimeout(() => { - _wfPicker = UI.locationPicker({ - containerId: 'wf-location-picker', - onSelect: (lat, lon, name) => { _locLat = lat; _locLon = lon; _locName = name; }, - }); - if (_locLat != null) _wfPicker.setValue(_locLat, _locLon, _locName); - }, 50); + // --- Mini-Karte --- + let _miniMap = null, _miniMarker = null, _mapEditing = false; + const _mkIcon = () => L.divIcon({ html: _pinSvg, className: '', iconSize: [28, 36], iconAnchor: [14, 36] }); + + function _placeMarker(lat, lon) { + if (_miniMarker) { _miniMarker.setLatLng([lat, lon]); return; } + _miniMarker = L.marker([lat, lon], { draggable: true, icon: _mkIcon() }).addTo(_miniMap); + _miniMarker.on('dragend', () => { + const p = _miniMarker.getLatLng(); + _locLat = p.lat; _locLon = p.lng; + document.getElementById('wf-lat').value = _locLat; + document.getElementById('wf-lon').value = _locLon; + document.getElementById('wf-location-btn-label').textContent = 'POI suchen'; + }); + } + + function _setCoords(lat, lon) { + _locLat = lat; _locLon = lon; + document.getElementById('wf-lat').value = lat; + document.getElementById('wf-lon').value = lon; + } + + function _setName(name) { + _locName = name; + document.getElementById('wf-location-label').textContent = name; + document.getElementById('wf-location-chip-wrap').style.display = ''; + document.getElementById('wf-ort-name').value = name; + document.getElementById('wf-location-suggestions').style.display = 'none'; + } + + UI.loadLeaflet().then(() => { + setTimeout(() => { + const lat = _locLat || 48.0, lon = _locLon || 11.9, zoom = _locLat ? 15 : 7; + _miniMap = L.map('wf-map-wrap', { + zoomControl: true, attributionControl: false, + dragging: true, scrollWheelZoom: false, + }).setView([lat, lon], zoom); + L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19 }) + .addTo(_miniMap); + _miniMap.invalidateSize(); + if (_locLat) _placeMarker(lat, lon); + _miniMap.on('click', e => { + _setCoords(e.latlng.lat, e.latlng.lng); + _placeMarker(_locLat, _locLon); + document.getElementById('wf-location-btn-label').textContent = 'POI suchen'; + }); + document.getElementById('wf-map-pin-here')?.addEventListener('click', () => { + const c = _miniMap.getCenter(); + _setCoords(c.lat, c.lng); + _placeMarker(c.lat, c.lng); + document.getElementById('wf-location-btn-label').textContent = 'POI suchen'; + }); + }, 150); + }); + + // Ort-Name-Chip entfernen + document.getElementById('wf-location-clear')?.addEventListener('click', () => { + _locName = null; + document.getElementById('wf-location-chip-wrap').style.display = 'none'; + document.getElementById('wf-ort-name').value = ''; + }); + + // Koordinaten + Name entfernen (Zwei-Klick) + const clearBtn = document.getElementById('wf-coords-clear'); + let _clearPending = false; + clearBtn?.addEventListener('click', () => { + if (!_clearPending) { + _clearPending = true; + clearBtn.textContent = 'Wirklich entfernen?'; + clearBtn.style.color = 'var(--c-danger)'; + setTimeout(() => { + _clearPending = false; + if (clearBtn) { + clearBtn.textContent = 'Ort entfernen'; + clearBtn.style.color = ''; + } + }, 3000); + return; + } + _clearPending = false; + clearBtn.textContent = 'Ort entfernen'; + clearBtn.style.color = ''; + _locLat = null; _locLon = null; _locName = null; + document.getElementById('wf-lat').value = ''; + document.getElementById('wf-lon').value = ''; + document.getElementById('wf-ort-name').value = ''; + document.getElementById('wf-location-chip-wrap').style.display = 'none'; + document.getElementById('wf-location-suggestions').style.display = 'none'; + document.getElementById('wf-location-btn-label').textContent = 'GPS → POI suchen'; + if (_miniMarker) { _miniMarker.remove(); _miniMarker = null; } + if (_miniMap) { _miniMap.setView([48.0, 11.9], 7); } + }); + + // GPS → POI-Suche (wie diary.js) + async function _showSuggestions() { + const btn = document.getElementById('wf-location-btn'); + UI.setLoading(btn, true); + try { + let lat = _locLat, lon = _locLon; + if (lat == null || lon == null) { + const pos = await API.getLocation({ enableHighAccuracy: true }); + lat = pos.lat; lon = pos.lon; + _setCoords(lat, lon); + if (_miniMap) { + _miniMap.setView([lat, lon], 15); + _placeMarker(lat, lon); + if (_miniMarker) _miniMarker.dragging.disable(); + } + document.getElementById('wf-location-btn-label').textContent = 'POI suchen'; + } + + const suggestions = _appState.user + ? await API.walks.nearby(lat, lon) + : []; + + const sugEl = document.getElementById('wf-location-suggestions'); + if (!suggestions.length) { + sugEl.innerHTML = '

Keine Orte in der NĂ€he gefunden.

'; + } else { + sugEl.innerHTML = suggestions.map(s => ` + `).join(''); + sugEl.querySelectorAll('.diary-location-suggestion').forEach(el => { + el.addEventListener('click', () => { + const slat = parseFloat(el.dataset.lat); + const slon = parseFloat(el.dataset.lon); + _setCoords(slat, slon); + _setName(el.dataset.name); + if (_miniMap) { + _miniMap.setView([slat, slon], 16); + _placeMarker(slat, slon); + if (_miniMarker) _miniMarker.dragging.disable(); + } + }); + }); + } + sugEl.style.display = ''; + } catch (err) { + UI.toast.error(err?.message?.includes('GPS') || _locLat == null + ? 'GPS nicht verfĂŒgbar.' : 'Ortssuche fehlgeschlagen.'); + } finally { + UI.setLoading(btn, false); + } + } + + document.getElementById('wf-location-btn')?.addEventListener('click', _showSuggestions); // Formular absenden document.getElementById('walk-form')?.addEventListener('submit', async e => { diff --git a/backend/static/js/ui.js b/backend/static/js/ui.js index 4b014c2..9f50342 100644 --- a/backend/static/js/ui.js +++ b/backend/static/js/ui.js @@ -453,10 +453,6 @@ const UI = (() => { const isDark = document.documentElement.dataset.theme === 'dark'; if (isDark) tiles.getContainer().style.filter = 'brightness(0.7) invert(1) contrast(0.9) hue-rotate(200deg)'; } - // Safety-Net: Container-GrĂ¶ĂŸe nach Layout neu vermessen. Verhindert - // grau bleibende Bereiche wenn die Karte vor dem finalen Layout erstellt - // wird (z.B. in frisch eingefĂŒgten Overlays mit flex:1). - requestAnimationFrame(() => m.invalidateSize()); return m; }, @@ -877,35 +873,12 @@ const UI = (() => { coordsClear: `${p}-coords-clear`, suggestions: `${p}-suggestions`, pinHere: `${p}-pin-here`, - geoInput: `${p}-geo-input`, - geoClear: `${p}-geo-clear`, - geoResults: `${p}-geo-results`, }; // HTML in den Container rendern function _render(container) { container.innerHTML = `
- -
-
- - - -
- -
'; - geoResults.style.display = ''; - } - try { - const data = await API.get(`/osm/geocode?q=${encodeURIComponent(q)}`); - if (!geoResults) return; - if (!data.length) { - geoResults.innerHTML = '
Keine Ergebnisse
'; - return; - } - geoResults.innerHTML = data.map((r, i) => ` -
-
${escape(r.name)}
- ${r.subtitle ? `
${escape(r.subtitle)}
` : ''} -
`).join(''); - geoResults.querySelectorAll('[data-i]').forEach(el => { - el.addEventListener('pointerdown', e => { - e.preventDefault(); - const r = data[+el.dataset.i]; - _setCoords(r.lat, r.lon); - _setName(r.name); - if (_map) { - _map.flyTo([r.lat, r.lon], 15, { duration: 0.8 }); - _placeMarker(r.lat, r.lon); - } - const lbl = _getEl(ids.locBtnLabel); - if (lbl) lbl.textContent = 'POI suchen'; - geoInput.value = ''; - if (geoClear) geoClear.style.display = 'none'; - geoResults.style.display = 'none'; - onSelect?.(_lat, _lon, _name); - }); - }); - } catch { - if (geoResults) geoResults.innerHTML = '
Suche nicht verfĂŒgbar
'; - } - }, 400); - }); - - geoInput?.addEventListener('keydown', e => { - if (e.key === 'Escape') { - geoInput.value = ''; - if (geoClear) geoClear.style.display = 'none'; - if (geoResults) geoResults.style.display = 'none'; - } - }); - geoClear?.addEventListener('click', () => { - geoInput.value = ''; - geoClear.style.display = 'none'; - if (geoResults) geoResults.style.display = 'none'; - }); - _getEl(ids.mapWrap)?.addEventListener('pointerdown', () => { - if (geoResults) geoResults.style.display = 'none'; - geoInput?.blur(); - }); } // Container initialisieren diff --git a/backend/static/landing.html b/backend/static/landing.html index 15f12fe..59277a8 100644 --- a/backend/static/landing.html +++ b/backend/static/landing.html @@ -4,7 +4,7 @@ - + Ban Yaro — Die Hunde-App fĂŒr Deutschland, Österreich & Schweiz @@ -149,25 +149,6 @@ } - - - - -
- - -
-
-
- -
- Ban Yaro -
-
Ban Yaro
-
Die App fĂŒr Dich und deinen Hund
-
-
- -
-
Die App fĂŒr Dich
und deinen Hund.
-
Tagebuch  Â·  GPS  Â·  KI-Trainer  Â·  Gesundheit  Â·  ZĂŒchter
-
- -
-
- App -
-
-
- - -
-
-
-
-
- - -
-
- -
-
-
📖
-
-
Tagebuch & Galerie
-
Fotos, Notizen und Meilensteine chronologisch auf einen Blick.
-
-
-
-
đŸ—ș
-
-
Gassi-Tracking & Routen
-
GPS-Strecken aufzeichnen, Lieblingsrunden speichern und hundefreundliche Orte entdecken.
-
-
-
-
đŸ€–
-
-
KI-Trainer & Übungen
-
Über 100 Übungen mit persönlichem Protokoll — der KI passt sich deinem Hund an.
-
-
-
-
❀‍đŸ©č
-
-
Gesundheit & Vorsorge
-
Impfungen, Gewicht, Tierarztbesuche und Befunde — immer griffbereit, nie vergessen.
-
-
-
-
🐕
-
-
FĂŒr ZĂŒchter: Stammbaum & WĂŒrfe
-
Ahnentafel, Genetik, Kaufvertrag und Wurfverwaltung — alles an einem Ort.
-
-
-
- - -
- -
- - \ No newline at end of file diff --git a/promotion/flyer_a5_rueckseite.html b/promotion/flyer_a5_rueckseite.html deleted file mode 100644 index a0f454d..0000000 --- a/promotion/flyer_a5_rueckseite.html +++ /dev/null @@ -1,383 +0,0 @@ - - - - -Ban Yaro – Flyer A5 RĂŒckseite - - - -
- - -
-
- Ban Yaro -
-
Ban Yaro
-
Die App fĂŒr Dich und deinen Hund
-
-
-
-
Entdecke
die App
-
RĂŒckseite
-
-
- -
- - -
-
Die App im Überblick
-
-
-
- Tagebuch -
-
Tagebuch & Galerie
-
-
-
- Routen -
-
Routen & Karte
-
-
-
- Wetter -
-
Wetter & Umgebung
-
-
-
- - - -
-
-
⚠
-
-
Giftköder-Radar
-
Aktuell gemeldete Giftköder in deiner NĂ€he — immer im Blick.
-
-
-
-
💬
-
-
Community-Forum
-
Austausch mit anderen Hundehaltern — Fragen, Tipps, Erfahrungen.
-
-
-
- - -
-
-
- -
-
-
🐕
-
-
Speziell fĂŒr ZĂŒchter
-
Professionelle Werkzeuge fĂŒr verantwortungsvolle Zucht
-
-
- -
-
-
🌳
-
-
Stammbaum & Ahnentafel
-
Ahnentafel bis zur 4. Generation, Rassestandard-Vermerke und Linienzucht-Übersicht.
-
-
-
-
đŸŸ
-
-
Wurfverwaltung & Dokumentation
-
WurfgrĂ¶ĂŸe, Geburtsgewichte, Entwicklungsmeilensteine und Abgabedaten je Welpe.
-
-
-
-
🧬
-
-
Genetik & Gesundheitstests
-
HD/ED, DNA-Tests und ZuchttauglichkeitsprĂŒfungen zentral hinterlegen und teilen.
-
-
-
-
📄
-
-
Kaufvertrag & Übergabe
-
Kaufvertrag direkt in der App erstellen, unterzeichnen und als PDF exportieren.
-
-
-
- -
- „Als ZĂŒchterin brauche ich alle Informationen auf einen Blick — Ban Yaro gibt mir genau das." -
-
-
- - - - -
- - \ No newline at end of file diff --git a/promotion/flyer_a5_rueckseite.pdf b/promotion/flyer_a5_rueckseite.pdf deleted file mode 100644 index e6b01c8..0000000 Binary files a/promotion/flyer_a5_rueckseite.pdf and /dev/null differ diff --git a/promotion/flyer_a5_vorderseite.pdf b/promotion/flyer_a5_vorderseite.pdf deleted file mode 100644 index b4c5184..0000000 Binary files a/promotion/flyer_a5_vorderseite.pdf and /dev/null differ