From 20c9bb55b12409b26a162ffb1161f21a107865c7 Mon Sep 17 00:00:00 2001 From: Ludwig Mey Date: Mon, 27 Jul 2026 20:53:36 +1200 Subject: [PATCH] Switch to 5-day rolling view with clock/weather info panel - Show today + next 4 days instead of a fixed Sun-Sat week grid - Replace Next Week column with a right-side info panel (live clock, date, current weather + 3-day forecast) - Simplify topbar to just the view pill, date range, and member filter - Adjust grid to repeat(5, 1fr) 190px Co-Authored-By: Claude Sonnet 4.6 --- app.py | 52 ++++------ static/css/style.css | 234 ++++++++++++++++++++----------------------- static/js/app.js | 120 ++++++++++++---------- templates/index.html | 30 +----- 4 files changed, 199 insertions(+), 237 deletions(-) diff --git a/app.py b/app.py index 9393f5c..0e49129 100644 --- a/app.py +++ b/app.py @@ -112,30 +112,25 @@ def parse_member_and_title(summary, family_members): return None, title -def week_start_for(d): - """Return the Sunday on/before date d (Sunday-starting week).""" - return d - timedelta(days=(d.weekday() + 1) % 7) +NUM_DAYS = 5 -def empty_week_payload(week_start_date, members): +def empty_days_payload(start_date, members): days = [] - for i in range(7): - d = week_start_date + timedelta(days=i) + for i in range(NUM_DAYS): + d = start_date + timedelta(days=i) days.append({'date': d.isoformat(), 'weekday': d.strftime('%A'), 'events': []}) - next_week_start = week_start_date + timedelta(days=7) - next_week_end = next_week_start + timedelta(days=6) return { - 'week_start': week_start_date.isoformat(), - 'week_end': (week_start_date + timedelta(days=6)).isoformat(), + 'start': start_date.isoformat(), + 'end': (start_date + timedelta(days=NUM_DAYS - 1)).isoformat(), 'days': days, - 'next_week': {'start': next_week_start.isoformat(), 'end': next_week_end.isoformat(), 'count': 0, 'events': []}, 'members': members, } @app.route('/api/calendar') def get_calendar(): - """Fetch Google Calendar events from iCal feed for a Sunday-starting week.""" + """Fetch Google Calendar events — today plus the next 4 days.""" global calendar_cache nz_tz = pytz.timezone('Pacific/Auckland') @@ -144,17 +139,17 @@ def get_calendar(): start_param = request.args.get('start') if start_param: try: - week_start_date = week_start_for(datetime.strptime(start_param, '%Y-%m-%d').date()) + start_date = datetime.strptime(start_param, '%Y-%m-%d').date() except ValueError: - week_start_date = week_start_for(now.date()) + start_date = now.date() else: - week_start_date = week_start_for(now.date()) + start_date = now.date() family_members = app.config['FAMILY_MEMBERS'] default_member = app.config['DEFAULT_MEMBER'] members_for_filter = family_members + [default_member] - cache_key = week_start_date.isoformat() + cache_key = start_date.isoformat() if (calendar_cache.get('key') == cache_key and calendar_cache['data'] and calendar_cache['timestamp'] and (now - calendar_cache['timestamp']).total_seconds() < app.config['CALENDAR_UPDATE_INTERVAL']): return jsonify(calendar_cache['data']) @@ -162,16 +157,15 @@ def get_calendar(): try: ical_url = app.config.get('GOOGLE_CALENDAR_ICAL_URL') if not ical_url: - return jsonify(empty_week_payload(week_start_date, members_for_filter)) + return jsonify(empty_days_payload(start_date, members_for_filter)) response = requests.get(ical_url, timeout=10) response.raise_for_status() cal = Calendar.from_ical(response.content) - # Fetch this week + next week, so the "Next Week" preview column has data. - range_start = nz_tz.localize(datetime.combine(week_start_date, datetime.min.time())) - range_end = range_start + timedelta(days=14) + range_start = nz_tz.localize(datetime.combine(start_date, datetime.min.time())) + range_end = range_start + timedelta(days=NUM_DAYS) recurring_events = recurring_ical_events.of(cal).between(range_start, range_end) events = [] @@ -219,25 +213,15 @@ def get_calendar(): events.sort(key=lambda x: x['start']) days = [] - for i in range(7): - d = week_start_date + timedelta(days=i) + for i in range(NUM_DAYS): + d = start_date + timedelta(days=i) day_events = [e for e in events if datetime.fromisoformat(e['start']).date() == d] days.append({'date': d.isoformat(), 'weekday': d.strftime('%A'), 'events': day_events}) - next_week_start = week_start_date + timedelta(days=7) - next_week_end = next_week_start + timedelta(days=6) - next_week_events = [e for e in events if datetime.fromisoformat(e['start']).date() >= next_week_start] - result = { - 'week_start': week_start_date.isoformat(), - 'week_end': (week_start_date + timedelta(days=6)).isoformat(), + 'start': start_date.isoformat(), + 'end': (start_date + timedelta(days=NUM_DAYS - 1)).isoformat(), 'days': days, - 'next_week': { - 'start': next_week_start.isoformat(), - 'end': next_week_end.isoformat(), - 'count': len(next_week_events), - 'events': next_week_events[:5], - }, 'members': members_for_filter, } diff --git a/static/css/style.css b/static/css/style.css index 423bb25..d0ef035 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -16,48 +16,16 @@ html, body { height: 100vh; display: flex; flex-direction: column; - padding: 18px 24px 24px; + padding: 14px 20px 20px; } /* ---------- Top bar ---------- */ .topbar { - display: flex; - align-items: flex-start; - justify-content: space-between; - margin-bottom: 18px; - flex-wrap: wrap; - gap: 12px; -} - -.date-time { - display: flex; - align-items: baseline; - gap: 10px; -} - -.icon-badge { - font-size: 22px; - margin-right: 2px; -} - -.header-date { - font-size: 28px; - font-weight: 700; - color: #1F2430; -} - -.header-time { - font-size: 18px; - font-weight: 500; - color: #8A8F9C; -} - -.nav-row { display: flex; align-items: center; gap: 10px; - margin-top: 10px; + margin-bottom: 14px; } .pill { @@ -83,79 +51,21 @@ html, body { background-repeat: no-repeat; } -.week-nav { - display: flex; - align-items: center; - gap: 6px; - background: #FFFFFF; - border: 1px solid #E3E5EA; - border-radius: 18px; - padding: 4px 8px; -} - -.nav-arrow { - border: none; - background: transparent; - font-size: 18px; - line-height: 1; - color: #4A4E5A; - cursor: pointer; - padding: 4px 8px; - border-radius: 50%; -} - -.nav-arrow:hover { - background: #F0F1F4; -} - -.week-range { +.date-range-label { font-size: 13px; font-weight: 600; color: #4A4E5A; - min-width: 80px; - text-align: center; } -.topbar-right { - display: flex; - align-items: center; - gap: 14px; -} +/* ---------- Topbar right-side items removed — now live in info panel ---------- */ -.weather-chip { - display: flex; - align-items: center; - gap: 6px; - background: #FFFFFF; - border: 1px solid #E3E5EA; - border-radius: 18px; - padding: 6px 14px; -} - -.weather-icon { - width: 22px; - height: 22px; - background-size: contain; - background-repeat: no-repeat; -} - -.current-temp { - font-size: 14px; - font-weight: 700; - color: #4A4E5A; -} - -.wifi-icon { - font-size: 18px; - color: #8A8F9C; -} /* ---------- Week grid ---------- */ .week-grid { flex: 1; display: grid; - grid-template-columns: repeat(7, 1fr) 1.1fr; + grid-template-columns: repeat(5, 1fr) 190px; gap: 12px; min-height: 0; } @@ -232,36 +142,6 @@ html, body { margin-top: 1px; } -.next-week-column { - background: #FFFFFF; - border-radius: 14px; - padding: 14px 12px; - display: flex; - flex-direction: column; - cursor: pointer; -} - -.next-week-column:hover { - background: #F7F8FA; -} - -.next-week-title { - font-size: 14px; - font-weight: 700; - color: #1F2430; -} - -.next-week-range { - font-size: 11px; - color: #A6AAB4; - margin-bottom: 10px; -} - -.next-week-count { - font-size: 13px; - color: #4A4E5A; - font-weight: 600; -} .event-placeholder { grid-column: 1 / -1; @@ -271,6 +151,110 @@ html, body { font-size: 14px; } +/* ---------- Info panel (clock + weather) ---------- */ + +.info-panel { + background: #FFFFFF; + border-radius: 14px; + padding: 18px 14px; + display: flex; + flex-direction: column; + gap: 0; + overflow: hidden; +} + +.info-clock { + font-size: 42px; + font-weight: 700; + color: #1F2430; + letter-spacing: -1px; + line-height: 1; +} + +.info-weekday { + font-size: 13px; + font-weight: 600; + color: #A6AAB4; + text-transform: uppercase; + letter-spacing: 0.05em; + margin-top: 4px; +} + +.info-date { + font-size: 15px; + font-weight: 700; + color: #4A4E5A; + margin-top: 2px; +} + +.info-divider { + height: 1px; + background: #F0F1F4; + margin: 14px 0; +} + +.info-weather-current { + display: flex; + align-items: center; + gap: 8px; +} + +.info-weather-current .owm-icon { + width: 40px; + height: 40px; +} + +.info-current-temp { + font-size: 28px; + font-weight: 700; + color: #1F2430; +} + +.info-current-desc { + font-size: 11px; + color: #A6AAB4; + text-transform: capitalize; + margin-top: 2px; +} + +.info-forecast { + display: flex; + flex-direction: column; + gap: 8px; + margin-top: 12px; +} + +.forecast-row { + display: flex; + align-items: center; + justify-content: space-between; + font-size: 12px; +} + +.forecast-row .f-day { + font-weight: 600; + color: #4A4E5A; + width: 30px; +} + +.forecast-row .f-icon .owm-icon { + width: 22px; + height: 22px; +} + +.forecast-row .f-temps { + text-align: right; + color: #4A4E5A; +} + +.forecast-row .f-temps .f-max { + font-weight: 700; +} + +.forecast-row .f-temps .f-min { + color: #A6AAB4; +} + /* ---------- Add button ---------- */ .add-fab { diff --git a/static/js/app.js b/static/js/app.js index 654b6ac..88d9e9a 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -1,30 +1,19 @@ -// Update intervals (in milliseconds) const INTERVALS = { - TIME: 1000, // 1 second - WEATHER: 900000, // 15 minutes - CALENDAR: 300000 // 5 minutes + TIME: 1000, + WEATHER: 900000, + CALENDAR: 300000 }; const DAY_LABELS = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; const MONTH_LABELS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; -let currentWeekStart = startOfWeek(new Date()); let activeFilter = 'all'; let filterOptionsBuilt = false; -let lastWeekData = null; +let lastData = null; +let lastWeatherData = null; -// Return an OWM icon image tag for a given icon code function weatherIcon(code) { - const src = `https://openweathermap.org/img/wn/${code}@2x.png`; - return `${code}`; -} - -// Sunday-starting week for a given date -function startOfWeek(date) { - const d = new Date(date); - d.setHours(0, 0, 0, 0); - d.setDate(d.getDate() - d.getDay()); - return d; + return `${code}`; } function toISODate(date) { @@ -38,21 +27,20 @@ function formatRangeLabel(startISO, endISO) { const start = new Date(startISO + 'T00:00:00'); const end = new Date(endISO + 'T00:00:00'); const startLabel = `${MONTH_LABELS[start.getMonth()]} ${start.getDate()}`; - const endLabel = end.getMonth() === start.getMonth() ? `${end.getDate()}` : `${MONTH_LABELS[end.getMonth()]} ${end.getDate()}`; - return `${startLabel}-${endLabel}`; + const endLabel = end.getMonth() === start.getMonth() + ? `${end.getDate()}` + : `${MONTH_LABELS[end.getMonth()]} ${end.getDate()}`; + return `${startLabel} – ${endLabel}`; } -// Initialize the application document.addEventListener('DOMContentLoaded', () => { updateTime(); updateWeather(); fetchCalendar(); - document.getElementById('prev-week').addEventListener('click', () => shiftWeek(-7)); - document.getElementById('next-week').addEventListener('click', () => shiftWeek(7)); document.getElementById('member-filter').addEventListener('change', (e) => { activeFilter = e.target.value; - if (lastWeekData) renderWeek(lastWeekData); + if (lastData) renderDays(lastData); }); document.getElementById('add-btn').addEventListener('click', showAddToast); @@ -61,49 +49,61 @@ document.addEventListener('DOMContentLoaded', () => { setInterval(fetchCalendar, INTERVALS.CALENDAR); }); -function shiftWeek(days) { - currentWeekStart = new Date(currentWeekStart); - currentWeekStart.setDate(currentWeekStart.getDate() + days); - fetchCalendar(); -} - -// Update time and date in the header function updateTime() { const now = new Date(); const hours = String(now.getHours()).padStart(2, '0'); const minutes = String(now.getMinutes()).padStart(2, '0'); - document.getElementById('header-time').textContent = `${hours}:${minutes}`; - document.getElementById('header-date').textContent = `${MONTH_LABELS[now.getMonth()]} ${String(now.getDate()).padStart(2, '0')}`; + const el = document.getElementById('info-clock'); + if (el) el.textContent = `${hours}:${minutes}`; } -// Fetch and update weather chip async function updateWeather() { try { const response = await fetch('/api/weather'); if (!response.ok) throw new Error('Weather fetch failed'); - const data = await response.json(); if (data.error || !data.current) return; - - document.getElementById('current-temp').textContent = `${data.current.temp}°`; - document.getElementById('weather-icon').innerHTML = weatherIcon(data.current.icon); + lastWeatherData = data; + renderInfoWeather(data); } catch (error) { console.error('Error updating weather:', error); } } -// Fetch the current week's calendar data and render it +function renderInfoWeather(data) { + const tempEl = document.getElementById('info-current-temp'); + const iconEl = document.getElementById('info-weather-icon'); + const descEl = document.getElementById('info-current-desc'); + const forecastEl = document.getElementById('info-forecast'); + if (!tempEl) return; + + tempEl.textContent = `${data.current.temp}°`; + iconEl.innerHTML = weatherIcon(data.current.icon); + descEl.textContent = data.current.description; + + forecastEl.innerHTML = (data.forecast || []).map(day => ` +
+ ${day.date} + ${weatherIcon(day.icon)} + + ${day.temp_max}° + / ${day.temp_min}° + +
+ `).join(''); +} + async function fetchCalendar() { try { - const response = await fetch(`/api/calendar?start=${toISODate(currentWeekStart)}`); + const response = await fetch(`/api/calendar?start=${toISODate(new Date())}`); if (!response.ok) throw new Error('Calendar fetch failed'); const data = await response.json(); - lastWeekData = data; + lastData = data; - document.getElementById('week-range').textContent = formatRangeLabel(data.week_start, data.week_end); + document.getElementById('date-range').textContent = formatRangeLabel(data.start, data.end); buildFilterOptions(data.members); - renderWeek(data); + renderDays(data); } catch (error) { console.error('Error updating calendar:', error); document.getElementById('week-grid').innerHTML = '
Unable to load calendar
'; @@ -145,11 +145,12 @@ function eventCardHTML(event) { `; } -function renderWeek(data) { +function renderDays(data) { const grid = document.getElementById('week-grid'); grid.innerHTML = ''; const todayISO = toISODate(new Date()); + const now = new Date(); data.days.forEach(day => { const dayDate = new Date(day.date + 'T00:00:00'); @@ -164,22 +165,35 @@ function renderWeek(data) {
${events.length} event${events.length === 1 ? '' : 's'}
- ${events.map(eventCardHTML).join('') || ''} + ${events.map(eventCardHTML).join('')}
`; grid.appendChild(column); }); - const nextWeekEvents = filterEvents(data.next_week.events); - const nextColumn = document.createElement('div'); - nextColumn.className = 'next-week-column'; - nextColumn.innerHTML = ` -
Next Week
-
${formatRangeLabel(data.next_week.start, data.next_week.end)}
-
${data.next_week.count} event${data.next_week.count === 1 ? '' : 's'}
+ // Info panel — clock + date + weather + const hours = String(now.getHours()).padStart(2, '0'); + const minutes = String(now.getMinutes()).padStart(2, '0'); + const panel = document.createElement('div'); + panel.className = 'info-panel'; + panel.innerHTML = ` +
${hours}:${minutes}
+
${DAY_LABELS[now.getDay()]}
+
${now.getDate()} ${MONTH_LABELS[now.getMonth()]} ${now.getFullYear()}
+
+
+ -- +
+
--°
+
--
+
+
+
`; - nextColumn.addEventListener('click', () => shiftWeek(7)); - grid.appendChild(nextColumn); + grid.appendChild(panel); + + // Populate weather immediately if we already have it + if (lastWeatherData) renderInfoWeather(lastWeatherData); } function showAddToast() { diff --git a/templates/index.html b/templates/index.html index 3575325..b90fc8b 100644 --- a/templates/index.html +++ b/templates/index.html @@ -10,31 +10,11 @@
-
-
- 📅 - Loading... - --:-- -
- -
-
-
- - --° -
- 📡 -
+ 5 Days + -- +