Feature: Welcome-Dashboard für eingeloggte User — Hundefoto-Hero, Stats-Chips, Feature-Karten — SW by-v475, APP_VER 452

This commit is contained in:
rene 2026-04-29 06:35:42 +02:00
parent c8ae514c01
commit db386da2c0
5 changed files with 359 additions and 27 deletions

View file

@ -255,24 +255,140 @@ window.Page_welcome = (() => {
}
// ----------------------------------------------------------
// EINGELOGGTE ANSICHT — kompakter Überblick
// HILFSFUNKTIONEN (eingeloggte Ansicht)
// ----------------------------------------------------------
function _relDate(dateStr) {
if (!dateStr) return null;
const diff = Math.floor((Date.now() - new Date(dateStr)) / 86400000);
if (diff === 0) return 'Heute';
if (diff === 1) return 'Gestern';
if (diff > 1 && diff < 14) return `vor ${diff} Tagen`;
if (diff === -1) return 'Morgen';
if (diff < 0 && diff > -14) return `in ${-diff} Tagen`;
return dateStr;
}
function _greeting() {
const h = new Date().getHours();
if (h >= 5 && h < 11) return 'Guten Morgen';
if (h >= 11 && h < 18) return 'Moin';
if (h >= 18 && h < 22) return 'Guten Abend';
return 'Hey';
}
function _appointmentIcon(typ) {
if (!typ) return 'calendar-check';
const t = typ.toLowerCase();
if (t === 'impfung') return 'syringe';
if (t === 'tierarzt') return 'stethoscope';
if (t === 'medikament') return 'pill';
return 'calendar-check';
}
// Die 4 Feature-Karten für die eingeloggte Ansicht
const FEATURE_CARDS = [
{ icon: 'book-open', label: 'Tagebuch', desc: 'Fotos, Notizen, Erinnerungen', page: 'diary' },
{ icon: 'first-aid', label: 'Gesundheit', desc: 'Impfungen, Gewicht, Termine', page: 'health' },
{ icon: 'map-trifold', label: 'Karte', desc: 'Spots & Alerts in deiner Nähe', page: 'map' },
{ icon: 'target', label: 'Training', desc: 'Übungen mit KI-Trainer', page: 'uebungen' },
];
const FEATURE_CARD_PAGES = new Set(FEATURE_CARDS.map(f => f.page));
function _heroHTML(dog, dashData) {
const photoUrl = dashData?.random_photo?.url || null;
const avatarUrl = dog?.foto_url || null;
const dogName = dog ? UI.escape(dog.name) : 'Dein Hund';
const dogRasse = dog?.rasse ? UI.escape(dog.rasse) : '';
const greeting = _greeting();
if (photoUrl) {
return `
<div class="wc-hero wc-hero--photo" id="wc-hero-box"
style="background-image:url('${UI.escape(photoUrl)}')">
<div class="wc-hero-overlay"></div>
${avatarUrl ? `<img src="${UI.escape(avatarUrl)}" class="wc-hero-avatar" alt="${dogName}">` : ''}
<div class="wc-hero-center">
<span class="wc-hero-greeting">${UI.escape(greeting)}</span>
<h1 class="wc-hero-title">${dogName}</h1>
${dogRasse ? `<p class="wc-hero-rasse">${dogRasse}</p>` : ''}
</div>
</div>`;
}
// Fallback: Gradient-Hero
return `
<div class="wc-hero wc-hero--gradient" id="wc-hero-box">
<img src="/icons/icon-180.png" alt="Ban Yaro" class="wc-hero-icon">
<div class="wc-hero-center">
<span class="wc-hero-greeting">${UI.escape(greeting)}</span>
<h1 class="wc-hero-title">${dogName}</h1>
${dogRasse ? `<p class="wc-hero-rasse">${dogRasse}</p>` : ''}
</div>
</div>`;
}
function _chipsHTML(dashData) {
const diaryDate = dashData?.last_diary?.datum;
const diaryText = diaryDate ? (_relDate(diaryDate) || diaryDate) : '—';
const appt = dashData?.next_appointment;
const apptLabel = appt ? UI.escape(appt.bezeichnung) : '—';
const apptDate = appt ? (_relDate(appt.naechstes) || appt.naechstes || '—') : '—';
const apptIcon = _appointmentIcon(appt?.typ);
const weight = dashData?.last_weight;
const weightVal = weight ? `${weight.wert} ${weight.einheit}` : '—';
return `
<div class="wc-chips" id="wc-chips-row">
<div class="wc-chip" data-nav="diary">
<svg class="ph-icon wc-chip-icon" aria-hidden="true"><use href="/icons/phosphor.svg#book-open"></use></svg>
<span class="wc-chip-label">Tagebuch</span>
<span class="wc-chip-val${diaryDate ? '' : ' wc-chip-val--empty'}">${diaryText}</span>
</div>
<div class="wc-chip" data-nav="health">
<svg class="ph-icon wc-chip-icon" aria-hidden="true"><use href="/icons/phosphor.svg#${apptIcon}"></use></svg>
<span class="wc-chip-label">Nächster Termin</span>
<span class="wc-chip-val${appt ? '' : ' wc-chip-val--empty'}">${appt ? apptLabel + ' · ' + apptDate : '—'}</span>
</div>
<div class="wc-chip" data-nav="health">
<svg class="ph-icon wc-chip-icon" aria-hidden="true"><use href="/icons/phosphor.svg#scales"></use></svg>
<span class="wc-chip-label">Gewicht</span>
<span class="wc-chip-val${weight ? '' : ' wc-chip-val--empty'}">${weightVal}</span>
</div>
</div>`;
}
// ----------------------------------------------------------
// EINGELOGGTE ANSICHT — visueller Überblick
// ----------------------------------------------------------
function _renderLoggedIn(isInstalled) {
const dog = _appState?.activeDog || null;
_container.innerHTML = `
<div class="welcome-layout" style="margin:0 auto">
<div class="welcome-layout" id="wc-logged-root">
<div class="wc-hero">
<img src="/icons/icon-180.png" alt="Ban Yaro" class="wc-hero-icon">
<h1 class="wc-hero-title">Ban Yaro</h1>
<p class="wc-hero-sub">
Schön, dass du wieder da bist${_appState?.user?.name ? ', <strong>' + UI.escape(_appState.user.name) + '</strong>' : ''}!
</p>
</div>
${_heroHTML(dog, null)}
<div class="page-container" style="padding:var(--space-6) var(--space-4)">
<h2 class="wc-section-title">Was Ban Yaro kann</h2>
${_chipsHTML(null)}
<div class="page-container wc-li-body">
<div class="wc-feature-grid">
${FEATURE_CARDS.map(f => `
<button class="wc-fcard" data-nav="${f.page}">
<div class="wc-fcard-icon">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#${f.icon}"></use></svg>
</div>
<span class="wc-fcard-title">${f.label}</span>
<span class="wc-fcard-desc">${f.desc}</span>
</button>
`).join('')}
</div>
<h2 class="wc-section-title" style="margin-top:var(--space-6)">Mehr entdecken</h2>
<div class="wc-grid">
${FEATURES.map(f => `
${FEATURES.filter(f => !FEATURE_CARD_PAGES.has(f.page)).map(f => `
<button class="wc-tile" data-nav="${f.page}">
<div class="wc-tile-icon">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#${f.icon}"></use></svg>
@ -296,6 +412,47 @@ window.Page_welcome = (() => {
</div>
</div>
`;
// Asynchrones Dashboard laden
if (dog?.id) {
API.dogs.welcomeDashboard(dog.id).then(dash => {
_updateHeroFromDash(dash, dog);
_updateChipsFromDash(dash);
}).catch(() => { /* Skeleton bleibt sichtbar */ });
}
}
function _updateHeroFromDash(dash, dog) {
const heroBox = _container.querySelector('#wc-hero-box');
if (!heroBox) return;
const photoUrl = dash?.random_photo?.url;
const avatarUrl = dog?.foto_url;
const dogName = dog ? UI.escape(dog.name) : 'Dein Hund';
const dogRasse = dog?.rasse ? UI.escape(dog.rasse) : '';
const greeting = _greeting();
if (photoUrl) {
heroBox.className = 'wc-hero wc-hero--photo';
heroBox.style.backgroundImage = `url('${UI.escape(photoUrl)}')`;
heroBox.innerHTML = `
<div class="wc-hero-overlay"></div>
${avatarUrl ? `<img src="${UI.escape(avatarUrl)}" class="wc-hero-avatar" alt="${dogName}">` : ''}
<div class="wc-hero-center">
<span class="wc-hero-greeting">${UI.escape(greeting)}</span>
<h1 class="wc-hero-title">${dogName}</h1>
${dogRasse ? `<p class="wc-hero-rasse">${dogRasse}</p>` : ''}
</div>`;
}
}
function _updateChipsFromDash(dash) {
const chipsRow = _container.querySelector('#wc-chips-row');
if (!chipsRow) return;
chipsRow.outerHTML = _chipsHTML(dash);
// re-bind data-nav auf den neuen Chips
_container.querySelectorAll('#wc-chips-row [data-nav]').forEach(el => {
el.addEventListener('click', () => App.navigate(el.dataset.nav));
});
}
// ----------------------------------------------------------
@ -494,25 +651,131 @@ window.Page_welcome = (() => {
}
.wc-install-link .ph-icon { width: 12px; height: 12px; }
/* ── Logged-in Hero (kompakt) ──────────────────────────── */
/* ── Logged-in Hero ─────────────────────────────────────── */
.wc-hero {
background: linear-gradient(160deg, var(--c-primary-subtle) 0%, var(--c-bg) 70%);
text-align: center;
padding: var(--space-8) var(--space-4) var(--space-6);
position: relative;
min-height: 220px;
display: flex; align-items: flex-end; justify-content: center;
padding: var(--space-6) var(--space-4) var(--space-5);
overflow: hidden;
border-bottom: 1px solid var(--c-border-light);
text-align: center;
}
.wc-hero-icon {
width: 96px; height: 96px; border-radius: 22px;
box-shadow: 0 8px 28px rgba(0,0,0,0.15);
display: block; margin: 0 auto var(--space-4);
/* Gradient fallback */
.wc-hero--gradient {
background: linear-gradient(160deg, var(--c-primary) 0%, color-mix(in srgb, var(--c-primary) 60%, var(--c-bg)) 100%);
flex-direction: column; align-items: center; justify-content: center;
}
/* Photo variant */
.wc-hero--photo {
background-size: cover; background-position: center;
min-height: 260px;
flex-direction: column; align-items: center; justify-content: flex-end;
}
.wc-hero-overlay {
position: absolute; inset: 0;
background: linear-gradient(to bottom, rgba(0,0,0,0.08) 0%, rgba(0,0,0,0.55) 100%);
pointer-events: none;
}
.wc-hero-avatar {
position: absolute; top: var(--space-4); right: var(--space-4);
width: 52px; height: 52px; border-radius: 50%;
border: 2.5px solid rgba(255,255,255,0.7);
object-fit: cover;
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
z-index: 2;
}
.wc-hero-center {
position: relative; z-index: 2;
display: flex; flex-direction: column; align-items: center;
gap: 2px;
}
.wc-hero-greeting {
font-size: var(--text-sm); font-weight: var(--weight-semibold);
color: rgba(255,255,255,0.82); letter-spacing: 0.04em;
text-transform: uppercase;
}
.wc-hero-title {
font-size: var(--text-2xl); font-weight: var(--weight-bold);
color: var(--c-text); margin: 0 0 var(--space-2);
color: #fff; margin: 0;
text-shadow: 0 2px 8px rgba(0,0,0,0.25);
}
.wc-hero-sub {
font-size: var(--text-base); color: var(--c-text-secondary);
margin: 0 auto; line-height: 1.6; max-width: 420px;
.wc-hero-rasse {
font-size: var(--text-sm); color: rgba(255,255,255,0.72);
margin: 0; line-height: 1.4;
}
.wc-hero-icon {
width: 72px; height: 72px; border-radius: 18px;
box-shadow: 0 6px 20px rgba(0,0,0,0.2);
display: block; margin: 0 auto var(--space-3);
position: relative; z-index: 2;
}
/* ── Stats-Chips ─────────────────────────────────────────── */
.wc-chips {
display: flex; gap: var(--space-3);
padding: var(--space-3) var(--space-4);
overflow-x: auto; scrollbar-width: none;
background: var(--c-surface);
border-bottom: 1px solid var(--c-border-light);
-webkit-overflow-scrolling: touch;
}
.wc-chips::-webkit-scrollbar { display: none; }
.wc-chip {
display: flex; flex-direction: column; gap: 2px;
flex-shrink: 0;
background: var(--c-bg); border: 1px solid var(--c-border-light);
border-radius: var(--radius-lg);
padding: var(--space-3) var(--space-4);
cursor: pointer; min-width: 130px;
transition: background 0.15s;
}
.wc-chip:hover { background: var(--c-surface-2, var(--c-surface)); }
.wc-chip:active { transform: scale(0.97); }
.wc-chip-icon { width: 18px; height: 18px; color: var(--c-primary); margin-bottom: 2px; }
.wc-chip-label {
font-size: var(--text-xs); color: var(--c-text-muted);
font-weight: var(--weight-semibold); text-transform: uppercase;
letter-spacing: 0.05em; white-space: nowrap;
}
.wc-chip-val {
font-size: var(--text-sm); color: var(--c-text);
font-weight: var(--weight-semibold); white-space: nowrap;
overflow: hidden; text-overflow: ellipsis; max-width: 180px;
}
.wc-chip-val--empty { color: var(--c-text-muted); font-weight: normal; }
/* ── Feature-Karten (2×2) ────────────────────────────────── */
.wc-li-body { padding: var(--space-5) var(--space-4); }
.wc-feature-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--space-3);
margin-bottom: var(--space-2);
}
.wc-fcard {
display: flex; flex-direction: column; align-items: flex-start;
gap: var(--space-2); padding: var(--space-4);
background: var(--c-surface); border: 1px solid var(--c-border-light);
border-radius: var(--radius-lg); cursor: pointer; text-align: left;
transition: background 0.15s, transform 0.1s;
}
.wc-fcard:hover { background: var(--c-surface-2, var(--c-surface)); }
.wc-fcard:active { transform: scale(0.97); }
.wc-fcard-icon {
width: 44px; height: 44px; border-radius: var(--radius-md);
background: var(--c-primary-subtle);
display: flex; align-items: center; justify-content: center;
flex-shrink: 0;
}
.wc-fcard-icon .ph-icon { width: 22px; height: 22px; color: var(--c-primary); }
.wc-fcard-title {
font-size: var(--text-sm); font-weight: var(--weight-bold);
color: var(--c-text); line-height: 1.2;
}
.wc-fcard-desc {
font-size: var(--text-xs); color: var(--c-text-secondary);
line-height: 1.4;
}
/* ── Shared ────────────────────────────────────────────── */