Files
Calender/static/js/app.js
Ludwig Mey 20c9bb55b1 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 <noreply@anthropic.com>
2026-07-27 20:53:36 +12:00

211 lines
7.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const INTERVALS = {
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 activeFilter = 'all';
let filterOptionsBuilt = false;
let lastData = null;
let lastWeatherData = null;
function weatherIcon(code) {
return `<img src="https://openweathermap.org/img/wn/${code}@2x.png" alt="${code}" class="owm-icon">`;
}
function toISODate(date) {
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, '0');
const d = String(date.getDate()).padStart(2, '0');
return `${y}-${m}-${d}`;
}
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}`;
}
document.addEventListener('DOMContentLoaded', () => {
updateTime();
updateWeather();
fetchCalendar();
document.getElementById('member-filter').addEventListener('change', (e) => {
activeFilter = e.target.value;
if (lastData) renderDays(lastData);
});
document.getElementById('add-btn').addEventListener('click', showAddToast);
setInterval(updateTime, INTERVALS.TIME);
setInterval(updateWeather, INTERVALS.WEATHER);
setInterval(fetchCalendar, INTERVALS.CALENDAR);
});
function updateTime() {
const now = new Date();
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
const el = document.getElementById('info-clock');
if (el) el.textContent = `${hours}:${minutes}`;
}
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;
lastWeatherData = data;
renderInfoWeather(data);
} catch (error) {
console.error('Error updating weather:', error);
}
}
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 => `
<div class="forecast-row">
<span class="f-day">${day.date}</span>
<span class="f-icon">${weatherIcon(day.icon)}</span>
<span class="f-temps">
<span class="f-max">${day.temp_max}°</span>
<span class="f-min"> / ${day.temp_min}°</span>
</span>
</div>
`).join('');
}
async function fetchCalendar() {
try {
const response = await fetch(`/api/calendar?start=${toISODate(new Date())}`);
if (!response.ok) throw new Error('Calendar fetch failed');
const data = await response.json();
lastData = data;
document.getElementById('date-range').textContent = formatRangeLabel(data.start, data.end);
buildFilterOptions(data.members);
renderDays(data);
} catch (error) {
console.error('Error updating calendar:', error);
document.getElementById('week-grid').innerHTML = '<div class="event-placeholder">Unable to load calendar</div>';
}
}
function buildFilterOptions(members) {
if (filterOptionsBuilt || !members) return;
const select = document.getElementById('member-filter');
members.forEach(member => {
const option = document.createElement('option');
option.value = member.key;
option.textContent = member.name;
select.appendChild(option);
});
filterOptionsBuilt = true;
}
function filterEvents(events) {
if (activeFilter === 'all') return events;
return events.filter(e => e.member === activeFilter);
}
function formatEventTime(event) {
const start = new Date(event.start);
const end = new Date(event.end);
const isAllDay = start.getHours() === 0 && start.getMinutes() === 0 && end.getHours() === 0 && end.getMinutes() === 0;
if (isAllDay) return 'All day';
return start.toLocaleTimeString('en-NZ', { hour: '2-digit', minute: '2-digit' });
}
function eventCardHTML(event) {
return `
<div class="event-card" style="background:${event.bg}; border-left-color:${event.color};">
<div class="event-time">${formatEventTime(event)}</div>
<div class="event-title">${event.title}</div>
<div class="event-member">${event.member_name}</div>
</div>
`;
}
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');
const events = filterEvents(day.events);
const column = document.createElement('div');
column.className = 'day-column' + (day.date === todayISO ? ' is-today' : '');
column.innerHTML = `
<div class="day-header">
<div class="day-name">${DAY_LABELS[dayDate.getDay()]}</div>
<div class="day-number">${dayDate.getDate()}</div>
<div class="day-count">${events.length} event${events.length === 1 ? '' : 's'}</div>
</div>
<div class="day-events">
${events.map(eventCardHTML).join('')}
</div>
`;
grid.appendChild(column);
});
// 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 = `
<div id="info-clock" class="info-clock">${hours}:${minutes}</div>
<div class="info-weekday">${DAY_LABELS[now.getDay()]}</div>
<div class="info-date">${now.getDate()} ${MONTH_LABELS[now.getMonth()]} ${now.getFullYear()}</div>
<div class="info-divider"></div>
<div class="info-weather-current">
<span id="info-weather-icon">--</span>
<div>
<div id="info-current-temp" class="info-current-temp">--°</div>
<div id="info-current-desc" class="info-current-desc">--</div>
</div>
</div>
<div id="info-forecast" class="info-forecast"></div>
`;
grid.appendChild(panel);
// Populate weather immediately if we already have it
if (lastWeatherData) renderInfoWeather(lastWeatherData);
}
function showAddToast() {
let toast = document.querySelector('.add-toast');
if (!toast) {
toast = document.createElement('div');
toast.className = 'add-toast';
toast.textContent = 'This display is read-only — add events directly in Google Calendar.';
document.body.appendChild(toast);
}
toast.classList.add('visible');
clearTimeout(showAddToast._timer);
showAddToast._timer = setTimeout(() => toast.classList.remove('visible'), 3000);
}