Feature: Welten-Onboarding, Wetter-Motivation, UX-Fixes (SW by-v715)
Welten (worlds.js): - Swipe-Hints beim ersten Öffnen (JETZT ← → WELT animiert, einmalig) - Kein-Hund-Onboarding: Feature-Preview-Grid statt leerer Karte - Hintergrund-Foto-Hint: Kamera-Karte wenn noch kein Tagebuchfoto - worlds-back: navigiert zu Welcome wenn kein User eingeloggt - Nach Logout: worlds-back Button sofort ausgeblendet Wetter (wetter.js): - Standort-Fehlerseite zu Motivations-Seite umgebaut - Feature-Preview: Gassi-Score, 7-Tage, Regenradar, Rekorde - CTA: Standort freigeben + Registrieren (nur für Gäste) Settings (settings.js): - Logo in Auth-Form: display:block + margin:0 auto zentriert - Header bleibt sichtbar (FAB/Zurück-Navigation funktioniert) Jobs (jobs.js): - 2-Spalten-Grid auf Mobile: auto-fit statt festes 1fr 1fr - Kein doppeltes Padding im Wrapper Backend: - weather.py, achievements.py: diary JOIN fix (d.user_id → dogs JOIN) - Neue Wetter-Badges: wetter_tapfer, jahreszeiten, schnee - Ernährungs-, Reise-, Ausgaben-Seite: diverse UX-Verbesserungen - Presse-Seite erweitert - Ban Yaro Foto-Assets (WebP + HIRES JPG)
This commit is contained in:
parent
aa4849d947
commit
55069d246b
28 changed files with 719 additions and 198 deletions
|
|
@ -13,6 +13,7 @@ window.Worlds = (() => {
|
|||
let _lastUserId = undefined;
|
||||
let _dogs = []; // gecachte Hundesliste
|
||||
let _dogIdx = 0; // aktuell angezeigter Hund
|
||||
let _hasBgPhoto = false; // Hintergrund-Foto vorhanden?
|
||||
|
||||
// Touch-Tracking
|
||||
const _t = { x:0, y:0, active:false, vert:null, moved:0 };
|
||||
|
|
@ -49,6 +50,55 @@ window.Worlds = (() => {
|
|||
_setupButtons();
|
||||
_goTo(_cur, false);
|
||||
show();
|
||||
_showSwipeHints();
|
||||
}
|
||||
|
||||
function _showSwipeHints() {
|
||||
if (localStorage.getItem('worlds_swipe_seen')) return;
|
||||
localStorage.setItem('worlds_swipe_seen', '1');
|
||||
const ov = document.getElementById('worlds-overlay');
|
||||
if (!ov) return;
|
||||
const hint = document.createElement('div');
|
||||
hint.style.cssText = [
|
||||
'position:absolute;inset:0;pointer-events:none;z-index:55',
|
||||
'display:flex;align-items:center;justify-content:space-between',
|
||||
'padding:0 8px;transition:opacity 1s ease',
|
||||
].join(';');
|
||||
const arrowStyle = `
|
||||
display:flex;flex-direction:column;align-items:center;gap:4px;
|
||||
background:rgba(0,0,0,0.42);backdrop-filter:blur(10px);
|
||||
-webkit-backdrop-filter:blur(10px);
|
||||
border:1px solid rgba(255,255,255,0.18);border-radius:14px;
|
||||
padding:10px 10px;animation:worlds-pulse 1.2s ease infinite alternate;
|
||||
`;
|
||||
hint.innerHTML = `
|
||||
<style>
|
||||
@keyframes worlds-pulse {
|
||||
from { opacity:0.75; transform:translateX(0); }
|
||||
to { opacity:1; transform:translateX(-3px); }
|
||||
}
|
||||
.wsh-right { animation-name:worlds-pulse-r !important; }
|
||||
@keyframes worlds-pulse-r {
|
||||
from { opacity:0.75; transform:translateX(0); }
|
||||
to { opacity:1; transform:translateX(3px); }
|
||||
}
|
||||
</style>
|
||||
<div style="${arrowStyle}">
|
||||
<svg style="width:20px;height:20px;color:white" viewBox="0 0 256 256">
|
||||
<path fill="currentColor" d="M165.66 202.34a8 8 0 0 1-11.32 11.32l-80-80a8 8 0 0 1 0-11.32l80-80a8 8 0 0 1 11.32 11.32L91.31 128Z"/>
|
||||
</svg>
|
||||
<span style="font-size:8px;font-weight:800;letter-spacing:.12em;color:rgba(255,255,255,0.7);text-transform:uppercase">JETZT</span>
|
||||
</div>
|
||||
<div class="wsh-right" style="${arrowStyle}">
|
||||
<svg style="width:20px;height:20px;color:white" viewBox="0 0 256 256">
|
||||
<path fill="currentColor" d="M90.34 53.66a8 8 0 0 1 11.32-11.32l80 80a8 8 0 0 1 0 11.32l-80 80a8 8 0 0 1-11.32-11.32L164.69 128Z"/>
|
||||
</svg>
|
||||
<span style="font-size:8px;font-weight:800;letter-spacing:.12em;color:rgba(255,255,255,0.7);text-transform:uppercase">WELT</span>
|
||||
</div>
|
||||
`;
|
||||
ov.appendChild(hint);
|
||||
setTimeout(() => { hint.style.opacity = '0'; }, 2800);
|
||||
setTimeout(() => hint.remove(), 3900);
|
||||
}
|
||||
|
||||
function show(worldIdx) {
|
||||
|
|
@ -187,7 +237,10 @@ window.Worlds = (() => {
|
|||
|
||||
function _setupButtons() {
|
||||
document.getElementById('worlds-fab')?.addEventListener('click', _openFab);
|
||||
document.getElementById('worlds-back')?.addEventListener('click', () => show());
|
||||
document.getElementById('worlds-back')?.addEventListener('click', () => {
|
||||
if (_state?.user) show();
|
||||
else if (window.App) window.App.navigate('welcome');
|
||||
});
|
||||
document.querySelectorAll('.wdot').forEach((dot, i) => {
|
||||
dot.style.pointerEvents = 'auto';
|
||||
dot.addEventListener('click', () => {
|
||||
|
|
@ -414,7 +467,7 @@ window.Worlds = (() => {
|
|||
{ icon:'wave-sine', color:'#06B6D4', label:'Gewicht messen', sub:'Aktuelles Gewicht eintragen', page:'health' }] },
|
||||
{ icon:'target', label:'Übungen', page:'uebungen',
|
||||
fab:[{ icon:'target', color:'#F59E0B', label:'Training aufzeichnen', sub:'Übung absolviert', page:'uebungen' }] },
|
||||
{ icon:'list-checks', label:'Trainings-\npläne', page:'trainingsplaene',
|
||||
{ icon:'list-checks', label:'Trainingspläne', page:'trainingsplaene',
|
||||
fab:[{ icon:'list-checks', color:'#10B981', label:'Plan erstellen', sub:'Neuen Trainingsplan anlegen', page:'trainingsplaene', action:'openNew' }] },
|
||||
{ icon:'heart', label:'Adoption', page:'adoption',
|
||||
fab:[{ icon:'heart', color:'#EF4444', label:'Hund anbieten', sub:'Zur Adoption freigeben', page:'adoption', action:'openNew' }] },
|
||||
|
|
@ -444,7 +497,7 @@ window.Worlds = (() => {
|
|||
{ icon:'sparkle', label:'Jobs', page:'jobs' },
|
||||
{ icon:'book-open', label:'Knigge', page:'knigge' },
|
||||
{ icon:'film-slate', label:'Filme', page:'movies' },
|
||||
{ icon:'tree-structure', label:'Zucht-\nkartei', page:'zuchthunde', role:'breeder',
|
||||
{ icon:'tree-structure', label:'Zuchtkartei', page:'zuchthunde', role:'breeder',
|
||||
fab:[{ icon:'tree-structure', color:'#8B5CF6', label:'Zuchthund eintragen', sub:'Neuen Hund anlegen', page:'zuchthunde', action:'openNew' }] },
|
||||
{ icon:'notebook', label:'Wurfverw.', page:'litters', role:'breeder',
|
||||
fab:[{ icon:'notebook', color:'#10B981', label:'Wurf anlegen', sub:'Neuen Wurf eintragen', page:'litters', action:'openNew' }] },
|
||||
|
|
@ -452,12 +505,19 @@ window.Worlds = (() => {
|
|||
fab:[{ icon:'sparkle', color:'#EC4899', label:'Social-Post', sub:'Beitrag erstellen', page:'social', action:'openNew' }] },
|
||||
{ icon:'shield-check', label:'Moderation', page:'moderation', role:'mod' },
|
||||
{ icon:'gear', label:'Admin', page:'admin', role:'admin' },
|
||||
// ── NEUE FEATURES ────────────────────────────────────────────
|
||||
{ icon:'fork-knife', label:'Ernährung', page:'ernaehrung',
|
||||
fab:[{ icon:'fork-knife', color:'#F97316', label:'Futter-Tagebuch', sub:'Mahlzeit oder Futtercheck', page:'ernaehrung' }] },
|
||||
{ icon:'airplane', label:'Reise', page:'reise' },
|
||||
{ icon:'smiley', label:'Persönlichkeit', page:'personality' },
|
||||
];
|
||||
|
||||
const _DEFAULT_CONFIG = {
|
||||
jetzt: ['notes','expenses','erste-hilfe','playdate','chat','wetter','settings'],
|
||||
hund: ['diary','health','uebungen','trainingsplaene','adoption','sitting','wiki','wurfboerse'],
|
||||
welt: ['map','forum','friends','walks','poison','recalls','lost','routes','events','jobs','knigge','movies'],
|
||||
jetzt: ['notes','expenses','erste-hilfe','playdate','chat','wetter','social','moderation','admin'],
|
||||
hund: ['diary','health','uebungen','trainingsplaene','adoption','sitting','wiki','wurfboerse',
|
||||
'litters','zuchthunde','ernaehrung','personality'],
|
||||
welt: ['map','forum','friends','walks','poison','recalls','lost','routes','events',
|
||||
'jobs','knigge','movies','reise'],
|
||||
};
|
||||
|
||||
// _cfgCache: wird beim Init aus DB geladen, Fallback localStorage → Default
|
||||
|
|
@ -605,10 +665,11 @@ window.Worlds = (() => {
|
|||
user-select:none;-webkit-tap-highlight-color:transparent;touch-action:none">
|
||||
${!c.pinned ? `
|
||||
<button class="wc-remove" data-page="${c.page}" data-zone="${w}"
|
||||
style="position:absolute;top:-6px;right:-6px;width:18px;height:18px;
|
||||
border-radius:50%;background:#EF4444;border:none;cursor:pointer;
|
||||
display:flex;align-items:center;justify-content:center;z-index:2">
|
||||
<svg class="ph-icon" style="width:9px;height:9px;color:white">
|
||||
style="position:absolute;top:-8px;right:-8px;width:24px;height:24px;
|
||||
border-radius:50%;background:#EF4444;border:2px solid rgba(18,22,32,0.9);
|
||||
cursor:pointer;display:flex;align-items:center;justify-content:center;
|
||||
z-index:2;box-shadow:0 2px 6px rgba(0,0,0,0.5)">
|
||||
<svg class="ph-icon" style="width:13px;height:13px;color:white">
|
||||
<use href="/icons/phosphor.svg#x"></use>
|
||||
</svg>
|
||||
</button>` : `
|
||||
|
|
@ -781,11 +842,19 @@ window.Worlds = (() => {
|
|||
const track = document.getElementById('worlds-track');
|
||||
if (!track) return;
|
||||
if (url) {
|
||||
const img = new Image();
|
||||
img.onload = () => { track.style.backgroundImage = `url('${url}')`; track.style.backgroundSize = '100% auto'; track.style.backgroundPosition = '0 40%'; track.style.backgroundRepeat = 'no-repeat'; };
|
||||
img.onerror = () => _applyBgImage(null);
|
||||
img.src = url;
|
||||
const toLoad = new Image();
|
||||
toLoad.onload = () => {
|
||||
_hasBgPhoto = true;
|
||||
track.style.backgroundImage = `url('${url}')`;
|
||||
track.style.backgroundSize = '100% auto';
|
||||
track.style.backgroundPosition = '0 40%';
|
||||
track.style.backgroundRepeat = 'no-repeat';
|
||||
document.getElementById('wh-photo-hint')?.remove();
|
||||
};
|
||||
toLoad.onerror = () => _applyBgImage(null);
|
||||
toLoad.src = url;
|
||||
} else {
|
||||
_hasBgPhoto = false;
|
||||
track.style.backgroundImage = 'linear-gradient(160deg,#1a1f35 0%,#16213e 33%,#1a2535 67%,#0f1921 100%)';
|
||||
track.style.backgroundSize = '100% 100%';
|
||||
}
|
||||
|
|
@ -839,26 +908,29 @@ window.Worlds = (() => {
|
|||
const greet = hour < 5 ? 'Gute Nacht' : hour < 12 ? 'Guten Morgen' : hour < 18 ? 'Hallo' : 'Guten Abend';
|
||||
const firstName = user?.name?.split(' ')[0] || '';
|
||||
const dayStr = new Date().toLocaleDateString('de-DE', { weekday:'long', day:'numeric', month:'long' });
|
||||
const stale = isOffline && staleMin > 5
|
||||
const stale = isOffline && staleMin > 5
|
||||
? `<span style="font-size:9px;opacity:0.5;margin-left:6px">· Offline</span>` : '';
|
||||
const weatherLine = w
|
||||
? `${Math.round(w.temp_c)}° ${_esc(w.desc?.split(' ')[0] || '')} · ${Math.round(w.wind_kmh || 0)} km/h · ${w.precip_prob || 0}% Regen`
|
||||
: '';
|
||||
|
||||
// Streak für 3er-Chip-Zeile
|
||||
let streakVal = '—', streakCol = 'rgba(255,255,255,0.4)';
|
||||
if (user && dog) {
|
||||
try {
|
||||
const sr = await _cachedGet(`streak_${dog.id}`, `/streak/${dog.id}`);
|
||||
const s = sr.data;
|
||||
const streak = s?.current_streak || 0;
|
||||
const trainedToday = s?.last_training_date === new Date().toISOString().slice(0,10);
|
||||
streakCol = trainedToday ? '#10B981' : (streak > 0 ? '#F59E0B' : 'rgba(255,255,255,0.4)');
|
||||
streakVal = streak > 0
|
||||
? (trainedToday ? `✓ ${streak} Tage` : `🔥 ${streak} Tage`)
|
||||
: (trainedToday ? '✓ Heute' : 'Heute starten');
|
||||
} catch {}
|
||||
// Gassi-Score aus Wetterdaten berechnen
|
||||
function _calcGassiScore(wd) {
|
||||
if (!wd) return null;
|
||||
let s = 10;
|
||||
const t = wd.temp_c ?? 20, p = wd.precip_prob ?? 0, wind = wd.wind_kmh ?? 0;
|
||||
if (t > 30) s -= 3; else if (t > 25) s -= 1; else if (t < 0) s -= 3; else if (t < 5) s -= 1;
|
||||
if (p > 70) s -= 3; else if (p > 40) s -= 2; else if (p > 20) s -= 1;
|
||||
if (wind > 60) s -= 2; else if (wind > 40) s -= 1;
|
||||
if (wd.thunderstorm) s -= 3;
|
||||
return Math.max(1, Math.min(10, s));
|
||||
}
|
||||
const gassiScore = _calcGassiScore(w);
|
||||
const gassiColor = gassiScore >= 8 ? '#10B981' : gassiScore >= 5 ? '#F59E0B' : '#EF4444';
|
||||
const weatherEmoji = !w ? '🌤️'
|
||||
: w.thunderstorm ? '⛈️'
|
||||
: (w.precip_prob ?? 0) > 70 ? '🌧️'
|
||||
: (w.precip_prob ?? 0) > 30 ? '🌦️'
|
||||
: (w.temp_c ?? 20) > 28 ? '☀️🔥'
|
||||
: (w.temp_c ?? 20) < 2 ? '🌨️'
|
||||
: '☀️';
|
||||
|
||||
// Alert-Reminder
|
||||
const alertHtml = alertList.slice(0,1).map(a => `
|
||||
|
|
@ -901,7 +973,7 @@ window.Worlds = (() => {
|
|||
<div class="world-info-title">
|
||||
${_esc(greet)}${firstName ? `, <span style="color:var(--c-primary)">${_esc(firstName)}</span>` : ''}${stale}
|
||||
</div>
|
||||
<div class="world-info-sub">${_esc(dayStr)}${weatherLine ? ' · ' + weatherLine : ''}${totalKm != null ? ' · ' + totalKm + ' km' : ''}</div>
|
||||
<div class="world-info-sub">${_esc(dayStr)}</div>
|
||||
</div>
|
||||
${user ? userAvatarHtml : ''}
|
||||
</div>
|
||||
|
|
@ -909,17 +981,25 @@ window.Worlds = (() => {
|
|||
${alertHtml}
|
||||
${user && dog ? `
|
||||
<div class="wj-chip-row">
|
||||
<div class="wj-chip" data-wnav="uebungen">
|
||||
<svg class="ph-icon" style="width:1.2rem;height:1.2rem;color:${streakCol}">
|
||||
<use href="/icons/phosphor.svg#target"></use></svg>
|
||||
<span class="wj-chip-label">Streak</span>
|
||||
<span class="wj-chip-val">${streakVal}</span>
|
||||
<div class="wj-chip" data-wnav="wetter" style="${gassiScore ? `border-color:${gassiColor}44;background:${gassiColor}12;` : ''}">
|
||||
<div style="display:flex;align-items:center;gap:6px;width:100%">
|
||||
<span style="font-size:1.4rem;line-height:1;flex-shrink:0">${weatherEmoji}</span>
|
||||
<div style="flex:1;min-width:0">
|
||||
<div style="font-size:9px;color:rgba(255,255,255,0.55);font-weight:600;letter-spacing:.05em;text-transform:uppercase">Gassi-Score</div>
|
||||
<div style="display:flex;align-items:baseline;gap:3px;margin-top:1px">
|
||||
<span style="font-size:1.25rem;font-weight:800;color:${gassiColor};line-height:1">${gassiScore ?? '—'}</span>
|
||||
${gassiScore ? `<span style="font-size:var(--text-xs);color:rgba(255,255,255,0.4);font-weight:600">/10</span>` : ''}
|
||||
</div>
|
||||
${w ? `<div style="font-size:9px;color:rgba(255,255,255,0.5);margin-top:1px">${Math.round(w.temp_c ?? 0)}° · ${w.precip_prob ?? 0}% Regen</div>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wj-chip" data-wnav="routes">
|
||||
<svg class="ph-icon" style="width:1.2rem;height:1.2rem;color:var(--c-primary)">
|
||||
<use href="/icons/phosphor.svg#path"></use></svg>
|
||||
<span class="wj-chip-label">Gassirunde</span>
|
||||
<span class="wj-chip-val" id="wj-route-val">…</span>
|
||||
${totalKm != null ? `<span style="font-size:9px;color:rgba(255,255,255,0.4);margin-top:1px">∑ ${totalKm} km</span>` : ''}
|
||||
</div>
|
||||
<div class="wj-chip" data-wnav="uebungen">
|
||||
<svg class="ph-icon" style="width:1.2rem;height:1.2rem;color:var(--c-primary)">
|
||||
|
|
@ -1018,13 +1098,41 @@ window.Worlds = (() => {
|
|||
const dogs = dogsRes.data || [];
|
||||
|
||||
if (!dogs.length) {
|
||||
const features = [
|
||||
{ icon:'book-open', color:'#8B5CF6', title:'Tagebuch', sub:'Fotos & Erlebnisse' },
|
||||
{ icon:'heartbeat', color:'#EF4444', title:'Gesundheit', sub:'Impfungen & Gewicht' },
|
||||
{ icon:'target', color:'#F59E0B', title:'Training', sub:'104 Übungen' },
|
||||
{ icon:'books', color:'#10B981', title:'Wiki', sub:'Alle Rassen' },
|
||||
{ icon:'paw-print', color:'#3B82F6', title:'Gassi', sub:'Routen & GPS' },
|
||||
{ icon:'currency-eur',color:'#06B6D4',title:'Ausgaben', sub:'Budget im Blick' },
|
||||
];
|
||||
el.innerHTML = `
|
||||
<div class="world-info-card" style="text-align:center">
|
||||
<div style="font-size:4rem;margin-bottom:12px">🐶</div>
|
||||
<div class="world-info-title">Noch kein Hund angelegt</div>
|
||||
<div class="world-info-sub" style="margin-bottom:20px">Erstelle das Profil deines Hundes</div>
|
||||
<button class="btn btn-primary" onclick="Worlds.navigateTo('dog-profile')">Hund anlegen</button>
|
||||
<div class="world-top">
|
||||
<div class="world-info-card" style="text-align:center">
|
||||
<div style="font-size:3.2rem;margin-bottom:10px">🐶</div>
|
||||
<div class="world-info-title">Dein Hund wartet!</div>
|
||||
<div class="world-info-sub" style="margin-bottom:16px">
|
||||
Lege ein Profil an und schalte alle Features frei
|
||||
</div>
|
||||
<button class="btn btn-primary" style="width:100%" onclick="Worlds.navigateTo('dog-profile')">
|
||||
Hund anlegen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="world-bottom">
|
||||
<div class="world-section-label">Was dich erwartet</div>
|
||||
<div class="world-chips-grid">
|
||||
${features.map(f => `
|
||||
<div class="world-chip" style="opacity:0.7;cursor:default">
|
||||
<svg class="ph-icon" style="width:1.4rem;height:1.4rem;color:${f.color}">
|
||||
<use href="/icons/phosphor.svg#${f.icon}"></use>
|
||||
</svg>
|
||||
<span class="world-chip-label">${f.title}</span>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>`;
|
||||
el.querySelectorAll('[data-wnav]').forEach(e => e.addEventListener('click', () => navigateTo(e.dataset.wnav)));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1093,6 +1201,25 @@ window.Worlds = (() => {
|
|||
</div>
|
||||
</div>
|
||||
<div class="world-bottom">
|
||||
${!_hasBgPhoto ? `
|
||||
<div id="wh-photo-hint" data-wnav="diary"
|
||||
style="background:rgba(0,0,0,0.32);backdrop-filter:blur(12px);
|
||||
-webkit-backdrop-filter:blur(12px);border:1px solid rgba(255,255,255,0.12);
|
||||
border-radius:16px;padding:11px 14px;display:flex;align-items:center;
|
||||
gap:10px;cursor:pointer;color:white;flex-shrink:0">
|
||||
<svg class="ph-icon" style="width:1.1rem;height:1.1rem;color:rgba(196,132,58,0.9);flex-shrink:0">
|
||||
<use href="/icons/phosphor.svg#camera"></use>
|
||||
</svg>
|
||||
<div>
|
||||
<div style="font-size:var(--text-xs);font-weight:700;color:rgba(255,255,255,0.85)">
|
||||
Hintergrund-Foto hinzufügen
|
||||
</div>
|
||||
<div style="font-size:10px;color:rgba(255,255,255,0.45);margin-top:1px">
|
||||
Tagebuchfotos erscheinen hier als Panorama
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
<div class="world-section-label">Alles über ${_esc(dog.name)}</div>
|
||||
<div class="world-chips-grid">
|
||||
${chips.map(c => _chip(c.icon, c.label, c.page)).join('')}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue