Release v1.5.0
This commit is contained in:
commit
ed9c482ae5
17 changed files with 837 additions and 267 deletions
|
|
@ -3,8 +3,8 @@
|
|||
Router, State-Management, Navigation, Initialisierung.
|
||||
============================================================ */
|
||||
|
||||
const APP_VER = '727'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||
const APP_VERSION = '1.4.0'; // ← semantische Version, wird bei make release gesetzt
|
||||
const APP_VER = '741'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||
const APP_VERSION = '1.5.0'; // ← semantische Version, wird bei make release gesetzt
|
||||
const IS_STAGING = location.hostname === 'staging.banyaro.app';
|
||||
|
||||
const App = (() => {
|
||||
|
|
@ -43,7 +43,7 @@ const App = (() => {
|
|||
routes: { title: 'Routen', module: null },
|
||||
events: { title: 'Events', module: null },
|
||||
poison: { title: 'Giftköder-Alarm', module: null },
|
||||
walks: { title: 'Gassi-Treffen', module: null, requiresAuth: true },
|
||||
walks: { title: 'Gassi-Treffen', module: null, requiresAuth: true, requiresPro: true },
|
||||
sitting: { title: 'Sitting', module: null, requiresAuth: true },
|
||||
forum: { title: 'Forum', module: null },
|
||||
wiki: { title: 'Wiki', module: null },
|
||||
|
|
@ -51,12 +51,12 @@ const App = (() => {
|
|||
movies: { title: 'Filme', module: null },
|
||||
trainingsplaene: { title: 'Trainingspläne', module: null },
|
||||
uebungen: { title: 'Übungsbibliothek', module: null },
|
||||
notes: { title: 'Notizblock', module: null, requiresAuth: true },
|
||||
notes: { title: 'Notizblock', module: null, requiresAuth: true, requiresPro: true },
|
||||
'erste-hilfe': { title: 'Erste Hilfe', module: null },
|
||||
settings: { title: 'Einstellungen', module: null },
|
||||
lost: { title: 'Verlorener Hund', module: null },
|
||||
friends: { title: 'Freunde', module: null, requiresAuth: true },
|
||||
chat: { title: 'Nachrichten', module: null, requiresAuth: true },
|
||||
friends: { title: 'Freunde', module: null, requiresAuth: true, requiresPro: true },
|
||||
chat: { title: 'Nachrichten', module: null, requiresAuth: true, requiresPro: true },
|
||||
social: { title: 'Social Media', module: null, requiresAuth: true },
|
||||
admin: { title: 'Admin', module: null, requiresAuth: true },
|
||||
moderation: { title: 'Moderation', module: null, requiresAuth: true },
|
||||
|
|
@ -74,14 +74,28 @@ const App = (() => {
|
|||
expenses: { title: 'Ausgaben', module: null, requiresAuth: true },
|
||||
recalls: { title: 'Rückrufe', module: null },
|
||||
adoption: { title: 'Adoption', module: null },
|
||||
playdate: { title: 'Playdate', module: null, requiresAuth: true },
|
||||
playdate: { title: 'Playdate', module: null, requiresAuth: true, requiresPro: true },
|
||||
wetter: { title: 'Wetter', module: null },
|
||||
ernaehrung: { title: 'Ernährung', module: null, requiresAuth: true },
|
||||
ernaehrung: { title: 'Ernährung', module: null, requiresAuth: true, requiresPro: true },
|
||||
personality: { title: 'Persönlichkeitstest', module: null },
|
||||
reise: { title: 'Reise mit Hund', module: null },
|
||||
reise: { title: 'Reise mit Hund', module: null, requiresPro: true },
|
||||
hilfe: { title: 'Hilfe & FAQ', module: null },
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// TIER-CHECK — Frontend-Pendant zu has_pro_access() in auth.py
|
||||
// ----------------------------------------------------------
|
||||
function _hasPro(user) {
|
||||
if (!user) return false;
|
||||
const t = user.subscription_tier || 'standard';
|
||||
// _test-Tiers simulieren ihren Tier ohne Admin-Override — so sieht Admin was echte User sehen
|
||||
if (t.endsWith('_test')) return ['pro_test','breeder_test'].includes(t);
|
||||
// Normale Prüfung: Admin/Mod/Social bekommen immer Pro
|
||||
if (user.rolle === 'admin' || user.rolle === 'moderator') return true;
|
||||
if (user.is_moderator || user.is_social_media) return true;
|
||||
return ['pro','breeder'].includes(t);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// AUTH GUARD — Login-Gate Texte pro Seite
|
||||
// ----------------------------------------------------------
|
||||
|
|
@ -142,6 +156,34 @@ const App = (() => {
|
|||
return;
|
||||
}
|
||||
|
||||
// Pro-Guard — nur wenn User eingeloggt aber kein Pro-Zugang
|
||||
if (page.requiresPro && state.user && !_hasPro(state.user)) {
|
||||
const container = document.querySelector(`#page-${pageId} .page-body`);
|
||||
if (container) {
|
||||
container.innerHTML = `
|
||||
<div style="max-width:420px;margin:40px auto;padding:var(--space-6) var(--space-4);text-align:center">
|
||||
<div style="font-size:3rem;margin-bottom:var(--space-4)">⭐</div>
|
||||
<h2 style="font-size:var(--text-xl);font-weight:800;margin:0 0 var(--space-3)">Ban Yaro Pro</h2>
|
||||
<p style="color:var(--c-text-secondary);margin:0 0 var(--space-6);line-height:1.6">
|
||||
Dieses Feature ist Teil von Ban Yaro Pro — verfügbar wenn wir die nächste Stufe zünden.<br>
|
||||
Du wirst benachrichtigt wenn es soweit ist.
|
||||
</p>
|
||||
<div style="background:var(--c-bg-card);border:1px solid var(--c-border);border-radius:var(--radius-lg);
|
||||
padding:var(--space-4);text-align:left;font-size:var(--text-sm);color:var(--c-text-secondary)">
|
||||
<div style="font-weight:700;margin-bottom:var(--space-2);color:var(--c-text)">Ban Yaro Pro enthält:</div>
|
||||
<ul style="margin:0;padding-left:var(--space-5);display:flex;flex-direction:column;gap:var(--space-1)">
|
||||
<li>Mehrere Hunde verwalten</li>
|
||||
<li>KI-Trainer für personalisiertes Training</li>
|
||||
<li>Direktnachrichten & Freunde</li>
|
||||
<li>Gassi-Treffen, Playdate, Ernährung, Reise</li>
|
||||
<li>Notizblock</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (page.module) {
|
||||
const hasParams = params && Object.keys(params).length > 0;
|
||||
if (hasParams) {
|
||||
|
|
@ -819,6 +861,7 @@ const App = (() => {
|
|||
|
||||
try { localStorage.removeItem('by_wissen_open'); } catch (_) {}
|
||||
|
||||
_initVersionCheck();
|
||||
await _checkAuth();
|
||||
|
||||
// Einladungslink /teilen/{token} → direkt annehmen
|
||||
|
|
@ -916,6 +959,124 @@ const App = (() => {
|
|||
});
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// ----------------------------------------------------------
|
||||
// VERSION-CHECK — persistentes Banner wenn neue Version verfügbar
|
||||
// ----------------------------------------------------------
|
||||
let _updateBannerShown = false;
|
||||
|
||||
async function _checkVersion() {
|
||||
try {
|
||||
const r = await fetch('/api/version', { cache: 'no-store' });
|
||||
if (!r.ok) return;
|
||||
const { version } = await r.json();
|
||||
if (version && version !== APP_VER && !_updateBannerShown) {
|
||||
_updateBannerShown = true;
|
||||
_showUpdateBanner(version);
|
||||
}
|
||||
} catch { /* offline — ignorieren */ }
|
||||
}
|
||||
|
||||
function _showUpdateBanner(newVersion) {
|
||||
const isIos = /iphone|ipad|ipod/i.test(navigator.userAgent);
|
||||
const existing = document.getElementById('app-update-banner');
|
||||
if (existing) return;
|
||||
|
||||
const banner = document.createElement('div');
|
||||
banner.id = 'app-update-banner';
|
||||
banner.style.cssText = [
|
||||
'position:fixed;bottom:calc(env(safe-area-inset-bottom,0px) + 72px);left:12px;right:12px',
|
||||
'z-index:9000;background:var(--c-primary);color:#fff;border-radius:16px',
|
||||
'padding:14px 16px;box-shadow:0 4px 20px rgba(0,0,0,0.3)',
|
||||
'display:flex;flex-direction:column;gap:10px',
|
||||
].join(';');
|
||||
|
||||
banner.innerHTML = `
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;gap:10px">
|
||||
<div>
|
||||
<div style="font-weight:700;font-size:var(--text-sm)">
|
||||
Neue Version verfügbar (v${newVersion})
|
||||
</div>
|
||||
<div style="font-size:var(--text-xs);opacity:0.85;margin-top:2px">
|
||||
Tippe auf Aktualisieren um die neueste Version zu laden.
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;flex-shrink:0">
|
||||
<button id="upd-btn-reload"
|
||||
style="background:rgba(255,255,255,0.2);border:1px solid rgba(255,255,255,0.4);
|
||||
color:#fff;border-radius:10px;padding:8px 14px;cursor:pointer;
|
||||
font-size:var(--text-sm);font-weight:700;white-space:nowrap">
|
||||
Aktualisieren
|
||||
</button>
|
||||
<button id="upd-btn-close"
|
||||
style="background:none;border:none;color:rgba(255,255,255,0.7);
|
||||
cursor:pointer;font-size:1.1rem;padding:4px 6px;line-height:1">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="upd-ios-hint" style="display:none;font-size:var(--text-xs);
|
||||
background:rgba(0,0,0,0.2);border-radius:10px;padding:10px 12px;line-height:1.6">
|
||||
${isIos
|
||||
? `Falls die App nach dem Aktualisieren noch die alte Version zeigt:<br>
|
||||
<strong>1.</strong> Drücke lange auf das App-Icon am Homescreen<br>
|
||||
<strong>2.</strong> Wähle „App entfernen" (nur das Symbol, keine Daten)<br>
|
||||
<strong>3.</strong> Öffne banyaro.app in Safari und füge die App erneut hinzu`
|
||||
: `Falls die Seite noch die alte Version zeigt:<br>
|
||||
Drücke <strong>Cmd+Shift+R</strong> (Mac) bzw. <strong>Ctrl+Shift+R</strong> (Windows/Android Chrome) für einen harten Reload.`}
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(banner);
|
||||
|
||||
banner.querySelector('#upd-btn-close').addEventListener('click', () => banner.remove());
|
||||
|
||||
banner.querySelector('#upd-btn-reload').addEventListener('click', async () => {
|
||||
const btn = banner.querySelector('#upd-btn-reload');
|
||||
btn.textContent = 'Lädt…';
|
||||
btn.disabled = true;
|
||||
sessionStorage.setItem('by_update_reload', APP_VER);
|
||||
try {
|
||||
// SW aktivieren + alle Caches leeren für sauberen Reload
|
||||
const reg = await navigator.serviceWorker?.getRegistration();
|
||||
if (reg?.waiting) reg.waiting.postMessage({ type: 'SKIP_WAITING' });
|
||||
await reg?.update();
|
||||
const keys = await caches.keys();
|
||||
await Promise.all(keys.map(k => caches.delete(k)));
|
||||
} catch { /* ignorieren */ }
|
||||
setTimeout(() => location.reload(), 600);
|
||||
});
|
||||
}
|
||||
|
||||
function _initVersionCheck() {
|
||||
// Beim Start nach 10 Sekunden prüfen (nicht sofort — Prio für Auth)
|
||||
setTimeout(_checkVersion, 10_000);
|
||||
// Dann alle 30 Minuten
|
||||
setInterval(_checkVersion, 30 * 60_000);
|
||||
// Beim Wiedereinstieg in die App
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.visibilityState === 'visible') _checkVersion();
|
||||
});
|
||||
// Nach Reload: war das ein Update-Reload? Falls Version immer noch alt → iOS-Hinweis
|
||||
const reloadVer = sessionStorage.getItem('by_update_reload');
|
||||
if (reloadVer && reloadVer === APP_VER) {
|
||||
// Version hat sich nicht geändert nach Reload → iOS-Cache-Problem
|
||||
sessionStorage.removeItem('by_update_reload');
|
||||
setTimeout(() => {
|
||||
fetch('/api/version', { cache: 'no-store' })
|
||||
.then(r => r.json())
|
||||
.then(({ version }) => {
|
||||
if (version && version !== APP_VER) {
|
||||
_updateBannerShown = true;
|
||||
_showUpdateBanner(version);
|
||||
// iOS-Hinweis sofort aufklappen
|
||||
setTimeout(() => {
|
||||
document.getElementById('upd-ios-hint')?.style.setProperty('display', 'block');
|
||||
}, 300);
|
||||
}
|
||||
}).catch(() => {});
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// ÖFFENTLICHE API
|
||||
// (andere Module können App.state, App.navigate etc. nutzen)
|
||||
|
|
|
|||
|
|
@ -795,6 +795,9 @@ window.Page_admin = (() => {
|
|||
<span style="color:${u.rolle === 'admin' ? 'var(--c-danger)' : u.rolle === 'moderator' ? '#f59e0b' : 'var(--c-text-muted)'}">
|
||||
${_esc(u.rolle)}
|
||||
</span>
|
||||
· <span style="color:${u.subscription_tier && u.subscription_tier !== 'standard' ? 'var(--c-primary)' : 'var(--c-text-muted)'}">
|
||||
${_esc(u.subscription_tier || 'standard')}
|
||||
</span>
|
||||
· ${u.dog_count} Hund${u.dog_count !== 1 ? 'e' : ''}
|
||||
· ${u.thread_count} Threads
|
||||
</div>
|
||||
|
|
@ -823,6 +826,11 @@ window.Page_admin = (() => {
|
|||
title="Rolle ändern">
|
||||
<svg class="ph-icon"><use href="/icons/phosphor.svg#shield"></use></svg>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-ghost adm-tier" data-uid="${u.id}"
|
||||
data-name="${_esc(u.name)}" data-tier="${_esc(u.subscription_tier || 'standard')}"
|
||||
title="Abo-Stufe ändern">
|
||||
<svg class="ph-icon"><use href="/icons/phosphor.svg#star"></use></svg>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-ghost adm-delete" data-uid="${u.id}"
|
||||
data-name="${_esc(u.name)}" title="Löschen"
|
||||
style="color:var(--c-danger)">
|
||||
|
|
@ -847,6 +855,9 @@ window.Page_admin = (() => {
|
|||
el.querySelectorAll('.adm-rolle').forEach(btn => {
|
||||
btn.addEventListener('click', () => _changeRolle(btn.dataset.uid, btn.dataset.name, btn.dataset.rolle));
|
||||
});
|
||||
el.querySelectorAll('.adm-tier').forEach(btn => {
|
||||
btn.addEventListener('click', () => _changeTier(btn.dataset.uid, btn.dataset.name, btn.dataset.tier));
|
||||
});
|
||||
el.querySelectorAll('.adm-delete').forEach(btn => {
|
||||
btn.addEventListener('click', () => _deleteUser(btn.dataset.uid, btn.dataset.name));
|
||||
});
|
||||
|
|
@ -903,6 +914,54 @@ window.Page_admin = (() => {
|
|||
});
|
||||
}
|
||||
|
||||
async function _changeTier(uid, name, currentTier) {
|
||||
const tiers = ['standard', 'pro', 'breeder', 'standard_test', 'pro_test', 'breeder_test'];
|
||||
const tierLabels = {
|
||||
standard: 'Standard (kostenlos)',
|
||||
pro: 'Pro (bezahlt)',
|
||||
breeder: 'Breeder (Züchter)',
|
||||
standard_test: 'Standard Test (intern)',
|
||||
pro_test: 'Pro Test (intern)',
|
||||
breeder_test: 'Breeder Test (intern)',
|
||||
};
|
||||
UI.modal.open({
|
||||
title: `Abo-Stufe ändern: ${name}`,
|
||||
body: `
|
||||
<p style="font-size:var(--text-sm);color:var(--c-text-secondary);margin:0 0 var(--space-4)">
|
||||
Aktuelle Stufe: <strong>${currentTier}</strong>
|
||||
</p>
|
||||
<div style="display:flex;flex-direction:column;gap:var(--space-2)">
|
||||
${tiers.map(t => `
|
||||
<button class="btn ${t === currentTier ? 'btn-primary' : 'btn-secondary'} adm-tier-choice"
|
||||
data-tier="${t}" form="" ${t === currentTier ? 'disabled' : ''}>
|
||||
${tierLabels[t]}${t === currentTier ? ' ✓' : ''}
|
||||
</button>
|
||||
`).join('')}
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
|
||||
document.querySelectorAll('.adm-tier-choice:not([disabled])').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
UI.modal.close();
|
||||
try {
|
||||
await API.patch(`/admin/users/${uid}`, { subscription_tier: btn.dataset.tier });
|
||||
// Eigenes Tier geändert → User-State neu laden + Welten neu rendern
|
||||
if (String(uid) === String(_appState?.user?.id)) {
|
||||
_appState.user.subscription_tier = btn.dataset.tier;
|
||||
if (window.Worlds) {
|
||||
window.Worlds.init(_appState);
|
||||
}
|
||||
UI.toast.success(`Dein Tier ist jetzt ${btn.dataset.tier} — Ansicht aktualisiert.`);
|
||||
} else {
|
||||
UI.toast.success(`${name}: Abo-Stufe ist jetzt ${btn.dataset.tier}.`);
|
||||
}
|
||||
_renderTab();
|
||||
} catch (e) { UI.toast.error(e.message); }
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function _deleteUser(uid, name) {
|
||||
const ok = await UI.modal.confirm({
|
||||
title: `${name} löschen?`,
|
||||
|
|
|
|||
|
|
@ -82,6 +82,16 @@ window.Page_datenschutz = (() => {
|
|||
Plattformsicherheit).
|
||||
</p>`)}
|
||||
|
||||
${sec('Direktnachrichten', `
|
||||
<p style="${S.p}">
|
||||
Nachrichten zwischen Nutzern (z. B. zwischen Hundesitter und Hundeeigentümer oder
|
||||
zwischen Interessenten und Züchtern) werden auf unserem Server gespeichert, bis du
|
||||
das Gespräch oder deinen Account löschst. Admins können gemeldete Nachrichten zur
|
||||
Missbrauchsprüfung einsehen (Art. 6 Abs. 1 lit. f DSGVO — berechtigtes Interesse
|
||||
an Plattformsicherheit). Nachrichten werden nicht an Dritte weitergegeben.
|
||||
Du kannst Gespräche jederzeit selbst löschen.
|
||||
</p>`)}
|
||||
|
||||
${sec('KI-Funktionen', `
|
||||
<p style="${S.p}">
|
||||
Ban Yaro bietet KI-gestützte Funktionen (Trainingsempfehlungen, Terminvorschläge,
|
||||
|
|
@ -99,12 +109,34 @@ window.Page_datenschutz = (() => {
|
|||
<a href="https://www.anthropic.com/privacy" target="_blank" rel="noopener"
|
||||
style="${S.a}">anthropic.com/privacy</a>.
|
||||
</p>
|
||||
<p style="${S.p};margin-top:var(--space-3)">
|
||||
Der <strong>KI-Trainer</strong> analysiert deinen bisherigen Trainingsfortschritt
|
||||
(Übungshistorie, Erfolgsquoten, Streaks) und gibt personalisierte Empfehlungen.
|
||||
Diese Analyse läuft auf unserem lokalen Server in Deutschland — deine Trainingsdaten
|
||||
verlassen dabei nicht unsere Infrastruktur. Es findet kein Training oder Fine-Tuning
|
||||
von KI-Modellen auf Basis deiner Nutzerdaten statt.
|
||||
</p>
|
||||
<p style="${S.p};margin-top:var(--space-3)">
|
||||
KI-Empfehlungen sind Vorschläge und ersetzen keine tierärztliche Beratung.
|
||||
Eine automatisierte Entscheidungsfindung mit rechtlicher Wirkung (Art. 22 DSGVO)
|
||||
findet nicht statt.
|
||||
</p>`)}
|
||||
|
||||
${sec('Wetterdaten (Open-Meteo)', `
|
||||
<p style="${S.p}">
|
||||
Die Wetter-Funktion übermittelt auf Wunsch deine GPS-Koordinaten einmalig an
|
||||
<strong>Open-Meteo</strong> (Österreich, DSGVO-konform), um die lokale
|
||||
Wettervorhersage abzurufen. Es werden ausschließlich anonyme Koordinaten übertragen —
|
||||
keine Account- oder Profildaten. Open-Meteo protokolliert keine personenbezogenen
|
||||
Daten. Die Funktion wird nur aktiv, wenn du deinen Standort im Browser freigibst.
|
||||
Rechtsgrundlage: Einwilligung gem. Art. 6 Abs. 1 lit. a DSGVO.
|
||||
</p>
|
||||
<p style="${S.p};margin-top:var(--space-3)">
|
||||
Datenschutzerklärung von Open-Meteo:
|
||||
<a href="https://open-meteo.com/en/terms" target="_blank" rel="noopener"
|
||||
style="${S.a}">open-meteo.com/en/terms</a>
|
||||
</p>`)}
|
||||
|
||||
${sec('Routenvorschläge (OpenRouteService)', `
|
||||
<p style="${S.p}">
|
||||
Die Funktion <strong>„Routenvorschläge"</strong> berechnet auf Wunsch einen Rundweg
|
||||
|
|
|
|||
|
|
@ -276,6 +276,13 @@ window.Page_settings = (() => {
|
|||
<span>Hilfe & FAQ</span>
|
||||
<span style="margin-left:auto;color:var(--c-text-secondary)">›</span>
|
||||
</div>
|
||||
${!_appState.user?.subscription_tier || _appState.user.subscription_tier === 'standard' || _appState.user.subscription_tier === 'standard_test' ? `
|
||||
<div style="margin:var(--space-3) 0;padding:var(--space-3) var(--space-4);
|
||||
background:rgba(196,132,58,0.1);border-radius:var(--radius-md);
|
||||
font-size:var(--text-xs);color:var(--c-text-secondary)">
|
||||
⭐ <strong>Ban Yaro Pro</strong> kommt bald — mehr Features, mehrere Hunde.
|
||||
</div>
|
||||
` : ''}
|
||||
<div class="sidebar-item" id="settings-logout-btn"
|
||||
style="padding:var(--space-4);border-radius:0;cursor:pointer;
|
||||
color:var(--c-danger)">
|
||||
|
|
|
|||
|
|
@ -87,6 +87,14 @@ window.Page_welcome = (() => {
|
|||
// LANDING PAGE — nicht eingeloggte Besucher
|
||||
// ----------------------------------------------------------
|
||||
function _renderLanding(isInstalled) {
|
||||
// Browser-Besucher (kein PWA) ohne Login → auf /info weiterleiten
|
||||
const isPWA = window.matchMedia('(display-mode: standalone)').matches
|
||||
|| window.navigator.standalone === true;
|
||||
if (!isPWA && !sessionStorage.getItem('by_stay_in_app')) {
|
||||
window.location.replace('/info');
|
||||
return;
|
||||
}
|
||||
|
||||
const hasPrompt = !!App.getInstallPrompt();
|
||||
|
||||
_container.innerHTML = `
|
||||
|
|
@ -108,7 +116,7 @@ window.Page_welcome = (() => {
|
|||
${hasPrompt ? `
|
||||
<button class="btn wc-btn-hero" id="welcome-install-hero-btn">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#download-simple"></use></svg>
|
||||
App installieren
|
||||
Zum Home-Bildschirm hinzufügen
|
||||
</button>
|
||||
` : `
|
||||
<button class="btn wc-btn-hero" id="welcome-register-btn">
|
||||
|
|
@ -218,7 +226,7 @@ window.Page_welcome = (() => {
|
|||
${hasPrompt ? `
|
||||
<button class="btn wc-btn-hero" id="welcome-install-hero-btn2">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#download-simple"></use></svg>
|
||||
App installieren — kostenlos
|
||||
Zum Home-Bildschirm hinzufügen
|
||||
</button>
|
||||
` : `
|
||||
<button class="btn wc-btn-hero" id="welcome-register-btn2">
|
||||
|
|
@ -231,7 +239,7 @@ window.Page_welcome = (() => {
|
|||
${!isInstalled ? `
|
||||
<button class="wc-install-link" id="welcome-install-link">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#download-simple"></use></svg>
|
||||
Installationsanleitung
|
||||
Zum Home-Bildschirm hinzufügen
|
||||
</button>
|
||||
` : ''}
|
||||
</div>
|
||||
|
|
@ -242,7 +250,7 @@ window.Page_welcome = (() => {
|
|||
<div class="card wc-install-card">
|
||||
<div class="wc-install-header">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#download-simple"></use></svg>
|
||||
App installieren
|
||||
Immer griffbereit — kein App Store
|
||||
</div>
|
||||
<div style="padding:var(--space-4)">${_installHTML()}</div>
|
||||
</div>
|
||||
|
|
@ -481,7 +489,7 @@ window.Page_welcome = (() => {
|
|||
<div class="card wc-install-card">
|
||||
<div class="wc-install-header">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#download-simple"></use></svg>
|
||||
App installieren
|
||||
Immer griffbereit — kein App Store
|
||||
</div>
|
||||
<div style="padding:var(--space-4)">${_installHTML()}</div>
|
||||
</div>
|
||||
|
|
@ -1069,11 +1077,11 @@ window.Page_welcome = (() => {
|
|||
if (hasPrompt) {
|
||||
return `
|
||||
<p style="font-size:var(--text-sm);color:var(--c-text-secondary);margin:0 0 var(--space-3);line-height:1.5">
|
||||
Kein App Store nötig — direkt auf den Home-Bildschirm.
|
||||
Ban Yaro immer griffbereit — einmal hinzufügen, dann direkt vom Home-Bildschirm öffnen.
|
||||
</p>
|
||||
<button class="btn btn-primary" id="install-android-btn" style="width:100%;margin-bottom:var(--space-2)">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#download-simple"></use></svg>
|
||||
Ban Yaro installieren
|
||||
Zum Home-Bildschirm hinzufügen
|
||||
</button>`;
|
||||
}
|
||||
|
||||
|
|
@ -1095,7 +1103,7 @@ window.Page_welcome = (() => {
|
|||
if (isIOS && !isSafari) {
|
||||
return `
|
||||
<p style="font-size:var(--text-sm);color:var(--c-text-secondary);margin:0 0 var(--space-4);line-height:1.5">
|
||||
Auf dem iPhone geht die Installation nur über <strong>Safari</strong>.
|
||||
Auf dem iPhone funktioniert das Hinzufügen nur über <strong>Safari</strong>.
|
||||
</p>
|
||||
${_steps([
|
||||
['safari-logo', 'Öffne <strong>Safari</strong> auf deinem iPhone'],
|
||||
|
|
|
|||
|
|
@ -457,14 +457,14 @@ window.Worlds = (() => {
|
|||
// Alle verfügbaren Chips mit Metadaten
|
||||
|
||||
const _ALL_CHIPS = [
|
||||
{ icon:'note-pencil', label:'Notizblock', page:'notes',
|
||||
{ icon:'note-pencil', label:'Notizblock', page:'notes', pro: true,
|
||||
fab:[{ icon:'note-pencil', color:'#10B981', label:'Neue Notiz', sub:'Schnellnotiz erstellen', page:'notes', action:'openNew' }] },
|
||||
{ icon:'currency-eur', label:'Ausgaben', page:'expenses',
|
||||
fab:[{ icon:'currency-eur', color:'#3B82F6', label:'Ausgabe eintragen', sub:'Einmalig oder Dauerauftrag', page:'expenses', action:'openNew' }] },
|
||||
{ icon:'first-aid', label:'Erste Hilfe', page:'erste-hilfe' },
|
||||
{ icon:'handshake', label:'Playdate', page:'playdate',
|
||||
{ icon:'handshake', label:'Playdate', page:'playdate', pro: true,
|
||||
fab:[{ icon:'handshake', color:'#F59E0B', label:'Playdate anfragen', sub:'Treffen mit anderen Hunden', page:'playdate', action:'openNew' }] },
|
||||
{ icon:'chat-circle-dots', label:'Nachrichten', page:'chat' },
|
||||
{ icon:'chat-circle-dots', label:'Nachrichten', page:'chat', pro: true },
|
||||
{ icon:'sun', label:'Wetter', page:'wetter' },
|
||||
|
||||
{ icon:'book-open', label:'Tagebuch', page:'diary',
|
||||
|
|
@ -486,9 +486,9 @@ window.Worlds = (() => {
|
|||
fab:[{ icon:'map-pin', color:'#10B981', label:'Ort vorschlagen', sub:'Neuen POI auf der Karte', page:'map' }] },
|
||||
{ icon:'push-pin', label:'Forum', page:'forum',
|
||||
fab:[{ icon:'push-pin', color:'#8B5CF6', label:'Forum-Beitrag', sub:'Thema oder Frage erstellen', page:'forum', action:'openNew' }] },
|
||||
{ icon:'users', label:'Freunde', page:'friends',
|
||||
{ icon:'users', label:'Freunde', page:'friends', pro: true,
|
||||
fab:[{ icon:'users', color:'#3B82F6', label:'Freund einladen', sub:'Per Link einladen', page:'friends', action:'openNew' }] },
|
||||
{ icon:'paw-print', label:'Gassi', page:'walks',
|
||||
{ icon:'paw-print', label:'Gassi', page:'walks', pro: true,
|
||||
fab:[{ icon:'paw-print', color:'#F59E0B', label:'Gassirunde', sub:'Neue Runde starten', page:'walks', action:'openNew' },
|
||||
{ icon:'paw-print', color:'#10B981', label:'Schnell-Gassi', sub:'Kurze Runde ohne GPS eintragen', page:'walks', action:'quickGassi' }] },
|
||||
{ icon:'skull', label:'Giftköder', page:'poison',
|
||||
|
|
@ -513,9 +513,9 @@ window.Worlds = (() => {
|
|||
{ 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',
|
||||
{ icon:'fork-knife', label:'Ernährung', page:'ernaehrung', pro: true,
|
||||
fab:[{ icon:'fork-knife', color:'#F97316', label:'Futter-Tagebuch', sub:'Mahlzeit oder Futtercheck', page:'ernaehrung' }] },
|
||||
{ icon:'airplane', label:'Reise', page:'reise' },
|
||||
{ icon:'airplane', label:'Reise', page:'reise', pro: true },
|
||||
{ icon:'smiley', label:'Persönlichkeit', page:'personality' },
|
||||
];
|
||||
|
||||
|
|
@ -567,13 +567,31 @@ window.Worlds = (() => {
|
|||
}
|
||||
function _chipAllowed(chip) {
|
||||
const u = _state?.user;
|
||||
const tier = u?.subscription_tier || 'standard';
|
||||
const isTest = tier.endsWith('_test');
|
||||
// Role-Checks (hart — komplett ausblenden)
|
||||
if (!chip?.role) return true;
|
||||
if (chip.role === 'breeder') return u?.rolle === 'breeder' || u?.rolle === 'admin';
|
||||
if (chip.role === 'breeder') {
|
||||
if (isTest) return tier === 'breeder_test';
|
||||
return u?.rolle === 'breeder' || u?.rolle === 'admin';
|
||||
}
|
||||
if (chip.role === 'social') return u?.is_social_media || u?.rolle === 'admin';
|
||||
if (chip.role === 'mod') return u?.rolle === 'admin' || u?.rolle === 'moderator' || u?.is_moderator;
|
||||
if (chip.role === 'admin') return u?.rolle === 'admin';
|
||||
return true;
|
||||
}
|
||||
|
||||
// Gibt true zurück wenn User vollen Pro-Zugriff hat
|
||||
function _hasProAccess() {
|
||||
const u = _state?.user;
|
||||
if (!u) return false;
|
||||
const tier = u.subscription_tier || 'standard';
|
||||
if (tier.endsWith('_test')) return ['pro_test','breeder_test'].includes(tier);
|
||||
if (u.rolle === 'admin' || u.rolle === 'moderator') return true;
|
||||
if (u.is_moderator || u.is_social_media) return true;
|
||||
return ['pro','breeder'].includes(tier);
|
||||
}
|
||||
|
||||
function _chipsForWorld(world) {
|
||||
const pages = _getConfig()[world] || _DEFAULT_CONFIG[world];
|
||||
return pages.map(_chipMeta).filter(c => c && _chipAllowed(c));
|
||||
|
|
@ -869,9 +887,10 @@ window.Worlds = (() => {
|
|||
|
||||
// ── CHIP-HELPER ──────────────────────────────────────────────
|
||||
|
||||
function _chip(icon, label, page) {
|
||||
function _chip(icon, label, page, locked = false) {
|
||||
const style = locked ? 'opacity:0.25;cursor:default;' : '';
|
||||
return `
|
||||
<div class="world-chip" data-wnav="${page}">
|
||||
<div class="world-chip" ${locked ? '' : `data-wnav="${page}"`} style="${style}">
|
||||
<svg class="ph-icon" style="width:1.4rem;height:1.4rem">
|
||||
<use href="/icons/phosphor.svg#${icon}"></use>
|
||||
</svg>
|
||||
|
|
@ -1240,7 +1259,7 @@ window.Worlds = (() => {
|
|||
` : ''}
|
||||
<div class="world-section-label">Alles über ${_esc(dog.name)}</div>
|
||||
<div class="world-chips-grid">
|
||||
${chips.map(c => _chip(c.icon, c.label, c.page)).join('')}
|
||||
${chips.map(c => _chip(c.icon, c.label, c.page, !!(c.pro && !_hasProAccess()))).join('')}
|
||||
</div>
|
||||
<div class="world-footer-links">
|
||||
<span data-wnav="gruender">Die 100 Gründer</span>
|
||||
|
|
@ -1334,7 +1353,7 @@ window.Worlds = (() => {
|
|||
<div class="world-bottom">
|
||||
<div class="world-section-label">Die Welt da draußen</div>
|
||||
<div class="world-chips-grid">
|
||||
${chips.map(c => _chip(c.icon, c.label, c.page)).join('')}
|
||||
${chips.map(c => _chip(c.icon, c.label, c.page, !!(c.pro && !_hasProAccess()))).join('')}
|
||||
</div>
|
||||
<div class="world-footer-links">
|
||||
<span data-wnav="datenschutz">Datenschutz</span>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue