Feature: Welcome-Dashboard für eingeloggte User — Hundefoto-Hero, Stats-Chips, Feature-Karten — SW by-v475, APP_VER 452
This commit is contained in:
parent
c8ae514c01
commit
db386da2c0
5 changed files with 359 additions and 27 deletions
|
|
@ -108,6 +108,7 @@ const API = (() => {
|
|||
},
|
||||
deletePhoto(id) { return del(`/dogs/${id}/photo`); },
|
||||
getSkills(id) { return get(`/dogs/${id}/skills`); },
|
||||
welcomeDashboard(dogId) { return get(`/dogs/${dogId}/welcome-dashboard`); },
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
Router, State-Management, Navigation, Initialisierung.
|
||||
============================================================ */
|
||||
|
||||
const APP_VER = '451'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||
const APP_VER = '452'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||
|
||||
const App = (() => {
|
||||
|
||||
|
|
|
|||
|
|
@ -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 ────────────────────────────────────────────── */
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue