diff --git a/frontend/src/views/BriefingView.vue b/frontend/src/views/BriefingView.vue index 31560a3..d5d56bc 100644 --- a/frontend/src/views/BriefingView.vue +++ b/frontend/src/views/BriefingView.vue @@ -16,7 +16,9 @@ import { postRssReaction, deleteRssReaction, getNewsItems, + listEvents, type BriefingConversation, + type EventEntry, } from '@/api/client' import type { NewsItem } from '@/types/news' @@ -103,6 +105,55 @@ async function refreshWeather() { finally { refreshingWeather.value = false } } +// Upcoming events (right column, below weather) +const upcomingEvents = ref([]) + +interface GroupedDay { + label: string + dateKey: string + events: EventEntry[] +} + +const groupedEvents = computed(() => { + const groups = new Map() + const today = new Date() + today.setHours(0, 0, 0, 0) + + for (const ev of upcomingEvents.value) { + const d = new Date(ev.start_dt) + const key = d.toISOString().slice(0, 10) + if (!groups.has(key)) groups.set(key, []) + groups.get(key)!.push(ev) + } + + const result: GroupedDay[] = [] + for (const [key, events] of groups) { + const d = new Date(key + 'T00:00:00') + const diff = Math.round((d.getTime() - today.getTime()) / 86_400_000) + let label: string + if (diff === 0) label = 'Today' + else if (diff === 1) label = 'Tomorrow' + else label = d.toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric' }) + result.push({ label, dateKey: key, events }) + } + return result +}) + +function formatEventTime(ev: EventEntry): string { + if (ev.all_day) return 'All day' + const d = new Date(ev.start_dt) + return d.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }) +} + +async function loadEvents() { + try { + const now = new Date() + const end = new Date(now) + end.setDate(end.getDate() + 14) + upcomingEvents.value = await listEvents(now.toISOString(), end.toISOString()) + } catch { /* silent */ } +} + // News panel (right column) const newsItems = ref([]) @@ -126,6 +177,7 @@ async function loadAll() { loadWeather(), loadNews(), loadCurrentConditions(), + loadEvents(), ]) conversations.value = convList if (today) { @@ -314,6 +366,27 @@ onMounted(async () => { /> + +
+
+
Upcoming
+ Calendar → +
+
+
+
{{ group.label }}
+
+ + + {{ ev.title }} + {{ formatEventTime(ev) }} + {{ ev.location }} + +
+
+
+
+
@@ -541,6 +614,79 @@ onMounted(async () => { font-weight: 600; } +/* ─── Upcoming events ─────────────────────────────────────── */ +.events-section { + padding: 0.75rem 1rem; + border-top: 1px solid var(--color-border); +} +.events-cal-link { + font-size: 0.75rem; + color: var(--color-text-muted); + text-decoration: none; +} +.events-cal-link:hover { color: var(--color-primary); } +.events-list { + display: flex; + flex-direction: column; + gap: 0.6rem; +} +.events-day-group { + display: flex; + flex-direction: column; + gap: 0.2rem; +} +.events-day-label { + font-size: 0.72rem; + font-weight: 700; + color: var(--color-text-muted); + text-transform: uppercase; + letter-spacing: 0.03em; + padding-bottom: 0.15rem; +} +.event-row { + display: flex; + align-items: flex-start; + gap: 0.4rem; + padding: 0.25rem 0.4rem; + border-radius: 6px; +} +.event-row:hover { + background: color-mix(in srgb, var(--color-primary) 6%, transparent); +} +.event-dot { + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--color-primary); + flex-shrink: 0; + margin-top: 5px; +} +.event-body { + display: flex; + flex-direction: column; + gap: 0.05rem; + min-width: 0; +} +.event-title { + font-size: 0.82rem; + font-weight: 600; + color: var(--color-text); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.event-time { + font-size: 0.72rem; + color: var(--color-text-muted); +} +.event-loc { + font-size: 0.7rem; + color: var(--color-text-muted); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + .news-section { flex: 1; overflow-y: auto;