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 ``;
-}
-
-// 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 `
`;
}
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 => `
+