Compare commits

...

2 commits

Author SHA1 Message Date
0643cf87cc Marketing: Ban Yaro Blues Hymne live (v1295) + Album/Reel-Backlog 2026-06-14 21:41:14 +02:00
d0a76e1b54 Ban Yaro Blues — Hymne in der WELT-Welt
Eigener Song (KI-Demo via Suno) als Marken-Hymne. Dezente Player-Karte unter
dem Tageszitat; preload=none → 6 MB MP3 lädt erst bei Play, der SW cacht sie
danach für offline. Der Banner ist einmalige Einladung und verschwindet nach
erstem Hören (durchgehört oder >30s + Pause); danach dezenter runder Play-Button
unten links als Gegenspieler zum FAB, nur in WELT. Audio-Element zentral in
index.html → übersteht Welt-Wechsel & Re-Renders.

„Gehört" wird hybrid gemerkt: localStorage (sofort/offline) + DB-Flag
anthem_heard am User (neue Spalte, über /auth/me, gesetzt via
POST /api/profile/anthem-heard) — geräte- und deploy-übergreifend, damit der
Banner nicht erneut nervt.
2026-06-14 21:33:23 +02:00
12 changed files with 151 additions and 18 deletions

View file

@ -37,8 +37,10 @@ Legende: 🟢 läuft/erledigt · 🟡 angefangen · ⬜ offen · 💡 Idee ·
- [ ] **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.
- [ ] **Ban-Yaro-Album** — hin und wieder einen Suno-Song ergänzen, Player unten → Album-Modal (alle Lieder). Voraussetzung: **Suno Pro** (kommerzielle Rechte). Bonus-Asset: Song + App-Screenshots → **Reel/YouTube-Video** für Social.
## ✅ Erledigt
- [x] **Eigener Marken-Song „Ban Yaro Blues"** als Hymne in der WELT-Welt (Suno-KI-Demo, Player unten links, Banner einmalig) — 14.06., Prod v1295. ⚠️ Suno-Lizenz: Free = nicht-kommerziell → für saubere App-Nutzung **Suno Pro** nötig (Songs unter Abo neu rendern)
- [x] 1000 Flyer A5 (zweiseitig) gedruckt — 03.06.2026
- [x] iOS-App nativ gebaut + **im App Store freigegeben** (Ban Yaro Go, 09.06.) — Details im Repo `banyaro-ios`
- [x] Landing-Promotion für „Ban Yaro Go" LIVE (iOS-Abschnitt + Profil, eigenes braunes App-Store-Badge; Hero bewusst ohne Badge) — 09.06., Prod v1278

View file

@ -1 +1 @@
1292
1295

View file

@ -605,6 +605,7 @@ def _migrate(conn_factory):
("notes", "location_name", "TEXT"),
("notes", "parent_label", "TEXT"),
("users", "notes_ki_enabled", "INTEGER NOT NULL DEFAULT 1"),
("users", "anthem_heard", "INTEGER NOT NULL DEFAULT 0"),
# Züchter-Rolle
("users", "breeder_status", "TEXT"),
# Würfe: Verknüpfung mit Zuchtkartei-Hunden + Welfare

View file

@ -398,7 +398,7 @@ async def me(user=Depends(get_current_user)):
bio, wohnort, erfahrung, social_link,
profil_sichtbarkeit, avatar_url, created_at,
is_founder, is_partner, founder_number, is_founder_pending,
notes_ki_enabled, gassi_stunde_push,
notes_ki_enabled, gassi_stunde_push, anthem_heard,
preferred_theme, subscription_tier,
subscription_expires_at, subscription_cancelled_at, needs_dog_selection,
billing_address, geburtstag

View file

@ -50,6 +50,16 @@ def _load_user(user_id: int) -> dict:
return data
@router.post("/anthem-heard", status_code=204)
async def mark_anthem_heard(user=Depends(get_current_user)):
"""Merkt server-seitig, dass die Hymne gehört wurde — geräteübergreifend und
übersteht Cache-Clear bzw. die iOS-Trennung von PWA- und Safari-localStorage,
damit der Banner nach einem Deploy nicht erneut auftaucht."""
with db() as conn:
conn.execute("UPDATE users SET anthem_heard=1 WHERE id=?", (user["id"],))
return None
@router.patch("")
async def update_profile(data: ProfileUpdate, user=Depends(get_current_user)):
fields = data.model_dump(exclude_none=True)

View file

@ -8267,6 +8267,29 @@ svg.empty-state-icon {
}
#worlds-fab:active { transform: scale(0.92); box-shadow: 0 2px 10px rgba(196,132,58,0.3); }
/* Hymne-Button — Gegenspieler zum FAB, unten links, nur WELT + nach erstem Hören */
#worlds-anthem {
position: fixed;
bottom: calc(env(safe-area-inset-bottom, 16px) + 16px);
left: 20px;
width: 54px; height: 54px;
border-radius: 50%;
background: rgba(22,26,30,0.66);
-webkit-backdrop-filter: blur(8px);
backdrop-filter: blur(8px);
color: #fff;
border: 1px solid rgba(255,255,255,0.20);
cursor: pointer;
z-index: 60;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 18px rgba(0,0,0,0.40);
transition: transform 0.12s, background 0.2s;
}
#worlds-anthem:active { transform: scale(0.92); }
#worlds-anthem.playing { background: var(--c-primary); border-color: transparent; }
/* Header + Bottom-Nav: vollständig entfernt — Welten übernehmen Navigation */
#app-header { display: none !important; }
#bottom-nav { display: none !important; }

View file

@ -86,14 +86,14 @@
<title>Ban Yaro</title>
<!-- Theme + theme-color Statusleiste vor CSS setzen -->
<script src="/js/boot-early.js?v=1292"></script>
<script src="/js/boot-early.js?v=1295"></script>
<!-- CSS: Reihenfolge ist wichtig — ?v= zwingt Browser zur Neuladung -->
<link rel="stylesheet" href="/css/design-system.css?v=1292">
<link rel="stylesheet" href="/css/layout.css?v=1292">
<link rel="stylesheet" href="/css/components.css?v=1292">
<link rel="stylesheet" href="/css/utilities.css?v=1292">
<link rel="stylesheet" href="/css/lists.css?v=1292">
<link rel="stylesheet" href="/css/design-system.css?v=1295">
<link rel="stylesheet" href="/css/layout.css?v=1295">
<link rel="stylesheet" href="/css/components.css?v=1295">
<link rel="stylesheet" href="/css/utilities.css?v=1295">
<link rel="stylesheet" href="/css/lists.css?v=1295">
</head>
<body>
@ -596,6 +596,10 @@
<div class="world-panel" id="wp-hund"><div id="wh-content"></div></div>
<div class="world-panel" id="wp-welt"><div id="ww-content"></div></div>
</div>
<button id="worlds-anthem" class="hidden" aria-label="Hymne abspielen" title="Ban Yaro Blues — unsere Hymne">
<svg class="ph-icon" aria-hidden="true" style="width:22px;height:22px"><use href="/icons/phosphor.svg#play"></use></svg>
</button>
<audio id="anthem-audio" src="/sounds/ban-yaro-blues.mp3" preload="none"></audio>
<button id="worlds-fab" aria-label="Hinzufügen">
<svg class="offline-paw" viewBox="0 0 256 256" aria-hidden="true" style="width:24px;height:24px">
<!-- 5 Sub-Pfade einzeln einfärbbar via .paw-elem; Default: weiß auf orange -->
@ -620,12 +624,12 @@
<div id="modal-container"></div>
<!-- JS: Reihenfolge ist wichtig — erst Basis, dann Features -->
<script src="/js/api.js?v=1292"></script>
<script src="/js/ui.js?v=1292"></script>
<script src="/js/app.js?v=1292"></script>
<script src="/js/worlds.js?v=1292"></script>
<script src="/js/offline-indicator.js?v=1292"></script>
<script src="/js/contact-form.js?v=1292"></script>
<script src="/js/api.js?v=1295"></script>
<script src="/js/ui.js?v=1295"></script>
<script src="/js/app.js?v=1295"></script>
<script src="/js/worlds.js?v=1295"></script>
<script src="/js/offline-indicator.js?v=1295"></script>
<script src="/js/contact-form.js?v=1295"></script>
<!-- Feature-Seiten werden lazy geladen -->
@ -635,7 +639,7 @@
<!-- Boot: Offline-Banner + SW-Registration (extrahiert für CSP) -->
<script src="/js/boot.js?v=1292"></script>
<script src="/js/boot.js?v=1295"></script>
</body>

View file

@ -3,7 +3,7 @@
Router, State-Management, Navigation, Initialisierung.
============================================================ */
const APP_VER = '1292'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const APP_VER = '1295'; // ← 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;

View file

@ -220,6 +220,7 @@ window.Worlds = (() => {
track.style.transform = `translateX(${-_cur * (100 / 3)}%)`;
_updateDots();
_updateFab();
_anthem.updateButton();
// Karte neu rendern nachdem Transition abgeschlossen
if (_cur === 2 && _map) {
setTimeout(() => _map.invalidateSize(), animated ? 380 : 50);
@ -1933,6 +1934,83 @@ window.Worlds = (() => {
{ t:"Ein Hund braucht keinen Kalender, er weiß genau, wann du nach Hause kommst.", a:"Unbekannt" },
];
// ── HYMNE (Ban Yaro Blues) ───────────────────────────────────
// Banner in der WELT-Welt lädt zum ersten Hören ein und verschwindet danach.
// Ab dann: dezenter runder Play-Button unten links (Gegenspieler zum FAB).
// Audio-Element lebt zentral in index.html → übersteht Re-Renders & Welt-Wechsel.
const _anthem = (() => {
const KEY = 'by_anthem_heard';
let _bound = false;
const _audio = () => document.getElementById('anthem-audio');
// Gehört? Server-Flag (geräteübergreifend, deploy-fest) ODER lokal (sofort/offline).
const heard = () => {
if (_state?.user?.anthem_heard) return true;
try { return localStorage.getItem(KEY) === '1'; } catch (_) { return false; }
};
function _markHeard() {
try { localStorage.setItem(KEY, '1'); } catch (_) {}
if (!_state?.user?.anthem_heard) { // server-seitig genau einmal merken
if (_state?.user) _state.user.anthem_heard = 1;
try { API.post('/profile/anthem-heard', {}).catch(() => {}); } catch (_) {}
}
document.getElementById('ww-anthem-card')?.classList.add('hidden'); // Banner live ausblenden
updateButton();
}
function _sync() {
const a = _audio();
const playing = !!(a && !a.paused && !a.ended);
document.getElementById('ww-anthem-icon')?.querySelector('use')
?.setAttribute('href', `/icons/phosphor.svg#${playing ? 'pause' : 'play'}`);
const sub = document.getElementById('ww-anthem-sub');
if (sub) sub.textContent = playing ? 'läuft … (tippen zum Pausieren)' : 'unsere Hymne · anhören';
const btn = document.getElementById('worlds-anthem');
if (btn) {
btn.classList.toggle('playing', playing);
btn.querySelector('use')?.setAttribute('href', `/icons/phosphor.svg#${playing ? 'pause' : 'play'}`);
}
}
function _bindAudio() {
if (_bound) return;
const a = _audio();
if (!a) return;
_bound = true;
a.addEventListener('play', _sync);
a.addEventListener('pause', () => { if (a.currentTime >= 30) _markHeard(); _sync(); });
a.addEventListener('ended', () => { _markHeard(); _sync(); });
}
function toggle() {
const a = _audio();
if (!a) return;
if (a.paused) a.play().catch(() => {});
else a.pause();
}
function updateButton() {
document.getElementById('worlds-anthem')?.classList.toggle('hidden', !(_cur === 2 && heard()));
}
// Bei jedem WELT-Render aufrufen: Audio-Listener sichern, Klicks binden, Status setzen.
function initWelt() {
_bindAudio();
const card = document.getElementById('ww-anthem-card');
if (card && !card._anthemBound) {
card._anthemBound = true;
card.addEventListener('click', toggle);
card.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggle(); } });
}
const btn = document.getElementById('worlds-anthem');
if (btn && !btn._anthemBound) { btn._anthemBound = true; btn.addEventListener('click', toggle); }
_sync();
updateButton();
}
return { heard, toggle, updateButton, initWelt };
})();
function _renderWelt() {
const el = document.getElementById('ww-content');
if (!el) return;
@ -1959,6 +2037,19 @@ window.Worlds = (() => {
${_esc(quote.a)}
</div>
</div>
${_anthem.heard() ? '' : `
<div class="world-info-card" id="ww-anthem-card" role="button" tabindex="0"
style="margin-top:10px;display:flex;align-items:center;gap:14px;cursor:pointer">
<div style="width:42px;height:42px;border-radius:50%;background:rgba(255,255,255,0.12);
display:flex;align-items:center;justify-content:center;flex-shrink:0">
<svg class="ph-icon" id="ww-anthem-icon" style="width:20px;height:20px;color:#fff" aria-hidden="true"><use href="/icons/phosphor.svg#play"></use></svg>
</div>
<div style="flex:1;min-width:0">
<div style="font-size:var(--text-sm);font-weight:600;color:#fff">Ban Yaro Blues</div>
<div id="ww-anthem-sub" style="font-size:11px;color:rgba(255,255,255,0.45);margin-top:2px">unsere Hymne · anhören</div>
</div>
<svg class="ph-icon" style="width:15px;height:15px;color:rgba(255,255,255,0.3);flex-shrink:0" aria-hidden="true"><use href="/icons/phosphor.svg#music-notes"></use></svg>
</div>`}
</div>
<div class="world-bottom">
<div class="world-chips-grid">
@ -1972,6 +2063,8 @@ window.Worlds = (() => {
</div>
`;
el.querySelectorAll('[data-wnav]').forEach(e => e.addEventListener('click', () => navigateTo(e.dataset.wnav)));
_anthem.initWelt();
}
// ── HELPERS ──────────────────────────────────────────────────

View file

@ -4,7 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="color-scheme" content="light dark">
<script src="/js/landing-init.js?v=1292"></script>
<script src="/js/landing-init.js?v=1295"></script>
<title>Ban Yaro — Die Hunde-App für Deutschland, Österreich & Schweiz</title>
<meta name="description" content="Ban Yaro: Die kostenlose All-in-One Hunde-App für DACH. Tagebuch, Giftköder-Alarm, Training mit KI, Forum, Wurfbörse, Stammbaum, Inzucht-Check — DSGVO-konform, offline-fähig, direkt im Browser oder als native iPhone-App (Ban Yaro Go).">
<meta name="keywords" content="Hunde App, Hunde Community, Wurfbörse, Züchter, Welpen kaufen, Stammbaum Hund, Inzuchtkoeffizient, Hundezucht, Impfpass Hund, Giftköder Alarm, Gassi Community, Hundetraining App, Hunde Forum, Hunde KI, Hundefilm Datenbank, Welpen Marktplatz">

Binary file not shown.

View file

@ -4,7 +4,7 @@
============================================================ */
// ← EINZIGE Stelle für die Version — STATIC_ASSETS und CACHE_VERSION leiten sich ab
const VER = '1292';
const VER = '1295';
const CACHE_VERSION = `by-v${VER}`;
const CACHE_STATIC = `${CACHE_VERSION}-static`;
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten