From cac62c6caf2c0a709a6579144509dd187d914a94 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Fri, 17 Apr 2026 17:14:20 -0400 Subject: [PATCH] feat(briefing): add 2-week upcoming events list below weather card Fetch and display the next 14 days of events in the briefing right column, grouped by day with color dots, time, and location. Fills the empty space below the weather card. Co-Authored-By: Claude Opus 4.6 --- frontend/src/views/BriefingView.vue | 146 ++++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) 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;