diff --git a/public/pages/dashboard.js b/public/pages/dashboard.js index c6c553b..9df89e3 100644 --- a/public/pages/dashboard.js +++ b/public/pages/dashboard.js @@ -62,10 +62,35 @@ const MEAL_LABELS = { snack: 'Snack', }; +const MEAL_ICONS = { + breakfast: 'sunrise', + lunch: 'sun', + dinner: 'moon', + snack: 'apple', +}; + function initials(name = '') { return name.split(' ').map((w) => w[0]).join('').slice(0, 2).toUpperCase(); } +function widgetHeader(icon, title, count, linkHref, linkLabel = 'Alle') { + const badge = count != null + ? `${count}` + : ''; + return ` +
+ + + ${title} + ${badge} + + + ${linkLabel} + +
+ `; +} + // -------------------------------------------------------- // Skeleton // -------------------------------------------------------- @@ -88,37 +113,52 @@ function skeletonWidget(lines = 3) { // Widget-Renderer // -------------------------------------------------------- -function renderGreeting(user) { +function renderGreeting(user, stats = {}) { + const { urgentCount = 0, todayEventCount = 0, todayMealTitle = null } = stats; + + const statChips = []; + if (urgentCount > 0) + statChips.push(` + + ${urgentCount} dring. Aufgabe${urgentCount > 1 ? 'n' : ''} + `); + if (todayEventCount > 0) + statChips.push(` + + ${todayEventCount} Termin${todayEventCount > 1 ? 'e' : ''} heute + `); + if (todayMealTitle) + statChips.push(` + + Heute: ${todayMealTitle} + `); + return `
-
${greeting(user.display_name)}
-
${formatDate()}
+
+
${greeting(user.display_name)}
+
${formatDate()}
+ ${statChips.length ? `
${statChips.join('')}
` : ''} +
`; } function renderUrgentTasks(tasks) { - const header = ` -
- - - Dringende Aufgaben - - Alle -
- `; - if (!tasks.length) { - return `
${header} -
Keine dringenden Aufgaben. ✓
+ return `
+ ${widgetHeader('check-square', 'Aufgaben', 0, '/tasks')} +
+ +
Alles erledigt
+
`; } const items = tasks.map((t) => { const due = formatDueDate(t.due_date); return ` -
+
${t.title}
@@ -126,106 +166,97 @@ function renderUrgentTasks(tasks) {
${t.assigned_color ? `
- ${initials(t.assigned_name || '')} -
` : ''} + title="${t.assigned_name || ''}">${initials(t.assigned_name || '')}
` : ''}
`; }).join(''); - return `
${header}
${items}
`; + return `
+ ${widgetHeader('check-square', 'Aufgaben', tasks.length, '/tasks')} +
${items}
+
`; } function renderUpcomingEvents(events) { - const header = ` -
- - - Anstehende Termine - - Alle -
- `; - if (!events.length) { - return `
${header} -
Keine anstehenden Termine.
+ return `
+ ${widgetHeader('calendar', 'Termine', 0, '/calendar')} +
+ +
Keine Termine
+
`; } - const items = events.map((e) => ` -
-
-
-
${e.title}
-
- ${e.all_day ? formatDate(new Date(e.start_datetime)) : formatDateTime(e.start_datetime)} - ${e.location ? ` · ${e.location}` : ''} + const today = new Date().toDateString(); + const items = events.map((e) => { + const d = new Date(e.start_datetime); + const isToday = d.toDateString() === today; + const timeStr = e.all_day ? 'Ganztägig' : d.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }) + ' Uhr'; + return ` +
+
+
+
${e.title}
+
+ ${isToday ? 'Heute' : formatDateTime(e.start_datetime).split(',')[0]} + ${timeStr} + ${e.location ? ` · ${e.location}` : ''} +
-
- `).join(''); + `; + }).join(''); - return `
${header}
${items}
`; + return `
+ ${widgetHeader('calendar', 'Termine', events.length, '/calendar')} +
${items}
+
`; } function renderTodayMeals(meals) { - const header = ` -
- - - Heute essen - - Alle -
- `; + const MEAL_ORDER = ['breakfast', 'lunch', 'dinner', 'snack']; - if (!meals.length) { - return `
${header} -
Kein Essensplan für heute.
-
`; - } + const slots = MEAL_ORDER.map((type) => { + const meal = meals.find((m) => m.meal_type === type); + return ` +
+ +
${MEAL_LABELS[type]}
+
${meal ? meal.title : '—'}
+
+ `; + }).join(''); - const items = meals.map((m) => ` -
- ${MEAL_LABELS[m.meal_type]} - ${m.title} -
- `).join(''); - - return `
${header}
${items}
`; + return `
+ ${widgetHeader('utensils', 'Heute essen', null, '/meals', 'Woche')} +
${slots}
+
`; } function renderPinnedNotes(notes) { - const header = ` -
- - - Pinnwand - - Alle -
- `; - if (!notes.length) { - return `
${header} -
Keine angepinnten Notizen.
+ return `
+ ${widgetHeader('pin', 'Pinnwand', 0, '/notes')} +
+ +
Keine angepinnten Notizen
+
`; } const items = notes.map((n) => `
+ style="--note-color:${n.color};"> ${n.title ? `
${n.title}
` : ''}
${n.content}
`).join(''); - return `
${header}
${items}
`; + return `
+ ${widgetHeader('pin', 'Pinnwand', notes.length, '/notes')} +
${items}
+
`; } // -------------------------------------------------------- @@ -234,12 +265,8 @@ function renderPinnedNotes(notes) { const WEATHER_ICON_BASE = 'https://openweathermap.org/img/wn/'; -function weatherIconUrl(icon) { - return `${WEATHER_ICON_BASE}${icon}@2x.png`; -} - function renderWeatherWidget(weather) { - if (!weather) return ''; // Kein API-Key → Widget ausblenden + if (!weather) return ''; const { city, current, forecast } = weather; @@ -249,7 +276,7 @@ function renderWeatherWidget(weather) { return `
${label}
- ${d.desc}
${d.temp_max}° @@ -266,10 +293,10 @@ function renderWeatherWidget(weather) {
${current.desc}
${city}
- Gefühlt ${current.feels_like}° · ${current.humidity}% Luftfeuchtigkeit · Wind ${current.wind_speed} km/h + Gefühlt ${current.feels_like}° · ${current.humidity}% · Wind ${current.wind_speed} km/h
- ${current.desc}
${forecast.length ? `
${forecastHtml}
` : ''} @@ -348,7 +375,7 @@ function initFab(container) { function wireLinks(container) { container.querySelectorAll('[data-route]').forEach((el) => { - if (el.id === 'fab-main' || el.closest('#fab-actions')) return; // FAB separat + if (el.id === 'fab-main' || el.closest('#fab-actions')) return; const go = () => window.oikos.navigate(el.dataset.route); if (el.tagName === 'A') { el.addEventListener('click', (e) => { e.preventDefault(); go(); }); @@ -366,13 +393,14 @@ function wireLinks(container) { // -------------------------------------------------------- export async function render(container, { user }) { - // Sofort Skeleton container.innerHTML = `
-
${greeting(user.display_name)}
-
${formatDate()}
+
+
${greeting(user.display_name)}
+
${formatDate()}
+
${skeletonWidget(3)} ${skeletonWidget(3)} @@ -384,7 +412,6 @@ export async function render(container, { user }) { `; initFab(container); - // Daten laden (Dashboard + Wetter parallel) let data = { upcomingEvents: [], urgentTasks: [], todayMeals: [], pinnedNotes: [] }; let weather = null; try { @@ -399,11 +426,21 @@ export async function render(container, { user }) { window.oikos?.showToast('Dashboard konnte nicht vollständig geladen werden.', 'warning'); } - // Widgets rendern + const today = new Date().toDateString(); + const stats = { + urgentCount: data.urgentTasks?.length ?? 0, + todayEventCount: (data.upcomingEvents ?? []).filter((e) => + new Date(e.start_datetime).toDateString() === today + ).length, + todayMealTitle: (data.todayMeals ?? []).find((m) => m.meal_type === 'lunch')?.title + ?? (data.todayMeals ?? [])[0]?.title + ?? null, + }; + container.innerHTML = `
- ${renderGreeting(user)} + ${renderGreeting(user, stats)} ${renderWeatherWidget(weather)} ${renderUrgentTasks(data.urgentTasks ?? [])} ${renderUpcomingEvents(data.upcomingEvents ?? [])} diff --git a/public/styles/dashboard.css b/public/styles/dashboard.css index d091479..10fd413 100644 --- a/public/styles/dashboard.css +++ b/public/styles/dashboard.css @@ -58,18 +58,18 @@ border-radius: var(--radius-md); padding: var(--space-5) var(--space-6); color: #ffffff; + grid-column: 1 / -1; } -@media (min-width: 768px) { - .widget-greeting { - grid-column: 1 / -1; - } +.widget-greeting__content { + display: flex; + flex-direction: column; + gap: var(--space-1); } .widget-greeting__title { font-size: var(--text-2xl); font-weight: var(--font-weight-bold); - margin-bottom: var(--space-1); } .widget-greeting__date { @@ -77,6 +77,30 @@ opacity: 0.85; } +.widget-greeting__chips { + display: flex; + flex-wrap: wrap; + gap: var(--space-2); + margin-top: var(--space-3); +} + +.greeting-chip { + display: inline-flex; + align-items: center; + gap: 4px; + background: rgba(255, 255, 255, 0.2); + color: #ffffff; + font-size: var(--text-xs); + font-weight: var(--font-weight-medium); + padding: 3px var(--space-2); + border-radius: var(--radius-full); + backdrop-filter: blur(4px); +} + +.greeting-chip--warn { + background: rgba(255, 59, 48, 0.35); +} + /* -------------------------------------------------------- * Basis-Widget (Card) * -------------------------------------------------------- */ @@ -116,6 +140,24 @@ font-size: var(--text-sm); color: var(--color-accent); font-weight: var(--font-weight-medium); + display: inline-flex; + align-items: center; + gap: 2px; +} + +.widget__badge { + display: inline-flex; + align-items: center; + justify-content: center; + background-color: var(--color-accent); + color: #ffffff; + font-size: 10px; + font-weight: var(--font-weight-bold); + min-width: 18px; + height: 18px; + padding: 0 4px; + border-radius: var(--radius-full); + line-height: 1; } .widget__body { @@ -128,6 +170,21 @@ text-align: center; color: var(--color-text-secondary); font-size: var(--text-sm); + display: flex; + flex-direction: column; + align-items: center; + gap: var(--space-2); +} + +/* Widget hover lift (desktop) */ +@media (min-width: 1024px) { + .widget { + transition: transform var(--transition-fast), box-shadow var(--transition-fast); + } + .widget:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-md); + } } /* -------------------------------------------------------- @@ -246,10 +303,137 @@ font-size: var(--text-xs); color: var(--color-text-secondary); margin-top: 2px; + display: flex; + align-items: center; + gap: var(--space-1); +} + +.event-time-badge { + font-size: 10px; + font-weight: var(--font-weight-semibold); + padding: 1px 5px; + border-radius: var(--radius-full); + background-color: var(--color-surface-2); + color: var(--color-text-secondary); +} + +.event-time-badge--today { + background-color: var(--color-accent-light); + color: var(--color-accent); } /* -------------------------------------------------------- - * Essen-Widget + * Essen-Widget (Slot-Grid) + * -------------------------------------------------------- */ +.widget--meals { + overflow: visible; +} + +.meal-slots { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 1px; + background-color: var(--color-border); +} + +.meal-slot { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: var(--space-1); + padding: var(--space-3) var(--space-2); + background-color: var(--color-surface); + cursor: pointer; + transition: background-color var(--transition-fast); + text-align: center; + min-height: 88px; +} + +.meal-slot:hover { + background-color: var(--color-surface-2); +} + +.meal-slot:first-child { + border-bottom-left-radius: var(--radius-md); +} + +.meal-slot:last-child { + border-bottom-right-radius: var(--radius-md); +} + +.meal-slot__icon { + width: 20px; + height: 20px; + color: var(--color-text-disabled); + flex-shrink: 0; +} + +.meal-slot--filled .meal-slot__icon { + color: var(--color-accent); +} + +.meal-slot__type { + font-size: 10px; + font-weight: var(--font-weight-semibold); + color: var(--color-text-disabled); + text-transform: uppercase; + letter-spacing: 0.4px; +} + +.meal-slot--filled .meal-slot__type { + color: var(--color-text-secondary); +} + +.meal-slot__title { + font-size: var(--text-xs); + color: var(--color-text-secondary); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 100%; + padding: 0 var(--space-1); +} + +.meal-slot--filled .meal-slot__title { + color: var(--color-text-primary); + font-weight: var(--font-weight-medium); +} + +/* -------------------------------------------------------- + * Notizen-Grid-Widget + * -------------------------------------------------------- */ +.notes-grid-widget { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: var(--space-3); + padding: var(--space-3) var(--space-4); +} + +@media (min-width: 1024px) { + .notes-grid-widget { + grid-template-columns: repeat(3, 1fr); + } +} + +.note-item { + border-radius: var(--radius-sm); + padding: var(--space-3); + cursor: pointer; + transition: opacity var(--transition-fast), transform var(--transition-fast); + border-left: 3px solid var(--note-color, var(--color-accent)); + background-color: var(--color-surface-2); +} + +.note-item:hover { + opacity: 0.8; + transform: translateY(-1px); +} + +/* legacy (old .note-item had margin-bottom, now grid handles gap) */ + +/* -------------------------------------------------------- + * Alte Essen-Listen-Styles (Fallback, nicht mehr primär) * -------------------------------------------------------- */ .meal-item { display: flex; @@ -290,26 +474,6 @@ text-overflow: ellipsis; } -/* -------------------------------------------------------- - * Notizen-Widget - * -------------------------------------------------------- */ -.note-item { - border-radius: var(--radius-sm); - padding: var(--space-3); - margin-bottom: var(--space-2); - cursor: pointer; - transition: opacity var(--transition-fast); - border-left: 3px solid transparent; -} - -.note-item:last-child { - margin-bottom: 0; -} - -.note-item:hover { - opacity: 0.8; -} - .note-item__title { font-size: var(--text-sm); font-weight: var(--font-weight-semibold);