diff --git a/app.py b/app.py index 284c977..e261987 100644 --- a/app.py +++ b/app.py @@ -2,8 +2,11 @@ from flask import Flask, render_template, jsonify import requests import os import random -from datetime import datetime, timedelta +from datetime import datetime, timedelta, date +from icalendar import Calendar from config import Config +import pytz +import recurring_ical_events app = Flask(__name__) app.config.from_object(Config) @@ -93,27 +96,76 @@ def get_weather(): @app.route('/api/calendar') def get_calendar(): - """Fetch Google Calendar events.""" + """Fetch Google Calendar events from iCal feed.""" global calendar_cache - # Check cache - now = datetime.now() + # Check cache - use timezone-aware datetime + nz_tz = pytz.timezone('Pacific/Auckland') + now = datetime.now(nz_tz) if (calendar_cache['data'] and calendar_cache['timestamp'] and (now - calendar_cache['timestamp']).total_seconds() < app.config['CALENDAR_UPDATE_INTERVAL']): return jsonify(calendar_cache['data']) - # TODO: Implement Google Calendar API integration - # For now, return placeholder data try: - # This is placeholder data - will be replaced with actual Google Calendar API - events = [ - { - 'title': 'Setup Google Calendar API', - 'start': (datetime.now() + timedelta(hours=2)).isoformat(), - 'end': (datetime.now() + timedelta(hours=3)).isoformat(), - 'location': '' - } - ] + # Fetch iCal feed + ical_url = app.config.get('GOOGLE_CALENDAR_ICAL_URL') + if not ical_url: + return jsonify([]) + + response = requests.get(ical_url, timeout=10) + response.raise_for_status() + + # Parse iCal data + cal = Calendar.from_ical(response.content) + + # Use recurring_ical_events to get all events in the date range (including recurring ones) + cutoff_date = now + timedelta(days=app.config['CALENDAR_DAYS_AHEAD']) + + # Get all events between now and cutoff_date + recurring_events = recurring_ical_events.of(cal).between(now, cutoff_date) + + events = [] + for component in recurring_events: + dtstart = component.get('dtstart') + dtend = component.get('dtend') + summary = str(component.get('summary', 'No Title')) + + if dtstart and dtstart.dt: + # Handle both datetime and date objects + if isinstance(dtstart.dt, datetime): + event_start = dtstart.dt + # Make sure event_start is timezone-aware + if event_start.tzinfo is None: + event_start = nz_tz.localize(event_start) + elif isinstance(dtstart.dt, date): + # For date-only events, create a timezone-aware datetime + event_start = nz_tz.localize(datetime.combine(dtstart.dt, datetime.min.time())) + else: + continue + + # Handle end time + if dtend and dtend.dt: + if isinstance(dtend.dt, datetime): + event_end = dtend.dt + # Make sure event_end is timezone-aware + if event_end.tzinfo is None: + event_end = nz_tz.localize(event_end) + elif isinstance(dtend.dt, date): + event_end = nz_tz.localize(datetime.combine(dtend.dt, datetime.min.time())) + else: + event_end = event_start + timedelta(hours=1) + else: + event_end = event_start + timedelta(hours=1) + + events.append({ + 'title': summary, + 'start': event_start.isoformat(), + 'end': event_end.isoformat(), + 'location': str(component.get('location', '')) + }) + + # Sort events by start time + events.sort(key=lambda x: x['start']) calendar_cache = {'data': events, 'timestamp': now} return jsonify(events) @@ -122,7 +174,7 @@ def get_calendar(): app.logger.error(f"Error fetching calendar: {str(e)}") if calendar_cache['data']: return jsonify(calendar_cache['data']) - return jsonify({'error': 'Unable to fetch calendar data'}), 500 + return jsonify([]) @app.route('/api/background') @@ -184,4 +236,4 @@ if __name__ == '__main__': os.makedirs(app.config['CREDENTIALS_DIR'], exist_ok=True) # Run the app - app.run(host='0.0.0.0', port=5000, debug=True) + app.run(host='0.0.0.0', port=5002, debug=False, use_reloader=False) diff --git a/config.py b/config.py index 5accc5f..021f3d8 100644 --- a/config.py +++ b/config.py @@ -17,7 +17,8 @@ class Config: # Google Calendar settings GOOGLE_CALENDAR_ID = os.getenv('GOOGLE_CALENDAR_ID', '') - CALENDAR_DAYS_AHEAD = int(os.getenv('CALENDAR_DAYS_AHEAD', '7')) + GOOGLE_CALENDAR_ICAL_URL = os.getenv('GOOGLE_CALENDAR_ICAL_URL', '') + CALENDAR_DAYS_AHEAD = int(os.getenv('CALENDAR_DAYS_AHEAD', '5')) # Update intervals (in seconds) IMAGE_ROTATION_INTERVAL = int(os.getenv('IMAGE_ROTATION_INTERVAL', '300')) # 5 minutes diff --git a/requirements.txt b/requirements.txt index 79e7b6f..4d35924 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,5 @@ Flask==3.0.0 -google-auth==2.25.2 -google-auth-oauthlib==1.2.0 -google-auth-httplib2==0.2.0 -google-api-python-client==2.110.0 requests==2.31.0 python-dotenv==1.0.0 +pytz==2024.1 +recurring_ical_events==3.8.0 diff --git a/static/backgrounds/Screenshot 2026-02-15 at 7.38.23 AM.png b/static/backgrounds/Screenshot 2026-02-15 at 7.38.23 AM.png new file mode 100644 index 0000000..5a8c5e5 Binary files /dev/null and b/static/backgrounds/Screenshot 2026-02-15 at 7.38.23 AM.png differ diff --git a/static/css/style.css b/static/css/style.css index 37fec31..640ea5a 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -34,7 +34,7 @@ body { left: 0; width: 100%; height: 100%; - background: rgba(0, 0, 0, 0.4); + background: rgba(0, 0, 0, 0.1); z-index: 1; } @@ -55,7 +55,7 @@ body { justify-content: space-between; align-items: flex-start; margin-bottom: 1rem; - background: rgba(0, 0, 0, 0.3); + background: rgba(0, 0, 0, 0.05); backdrop-filter: blur(10px); padding: 1rem 1.5rem; border-radius: 10px; @@ -150,7 +150,7 @@ body { /* Sections */ section { - background: rgba(0, 0, 0, 0.3); + background: rgba(0, 0, 0, 0.05); backdrop-filter: blur(10px); padding: 1rem 1.5rem; border-radius: 10px; @@ -242,28 +242,75 @@ section h3 { /* Calendar Section */ .events-list { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 0.8rem; + height: 100%; +} + +.day-group { display: flex; flex-direction: column; - gap: 0.6rem; + min-height: 0; +} + +.day-header { + display: flex; + flex-direction: column; + align-items: center; + padding: 0.6rem; + background: rgba(255, 255, 255, 0.1); + border-radius: 8px 8px 0 0; + margin-bottom: 0.5rem; + text-align: center; +} + +.day-name { + font-size: 1rem; + font-weight: 500; + text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5); +} + +.day-date { + font-size: 0.85rem; + opacity: 0.8; + margin-top: 0.2rem; +} + +.day-events { + display: flex; + flex-direction: column; + gap: 0.5rem; + overflow-y: auto; + flex: 1; +} + +.no-events { + font-size: 0.85rem; + opacity: 0.5; + font-style: italic; + padding: 0.5rem; + text-align: center; } .event { background: rgba(255, 255, 255, 0.05); - padding: 0.8rem; - border-radius: 8px; + padding: 0.5rem 0.6rem; + border-radius: 6px; border-left: 3px solid #4a9eff; } .event-time { - font-size: 0.9rem; + font-size: 0.75rem; font-weight: 500; - margin-bottom: 0.3rem; + margin-bottom: 0.2rem; color: #4a9eff; } .event-title { - font-size: 1.1rem; + font-size: 0.9rem; margin-bottom: 0.2rem; + line-height: 1.2; } .event-location { @@ -282,7 +329,7 @@ section h3 { /* Footer */ .footer { margin-top: 1rem; - background: rgba(0, 0, 0, 0.3); + background: rgba(0, 0, 0, 0.05); backdrop-filter: blur(10px); padding: 0.8rem 1.5rem; border-radius: 10px; diff --git a/static/js/app.js b/static/js/app.js index 202bbe9..7299985 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -114,26 +114,97 @@ async function updateCalendar() { return; } + // Group events by day + const eventsByDay = {}; + const today = new Date(); + today.setHours(0, 0, 0, 0); + + // Create 5 days (today + 4 more) + for (let i = 0; i < 5; i++) { + const date = new Date(today); + date.setDate(today.getDate() + i); + const dateKey = date.toISOString().split('T')[0]; + eventsByDay[dateKey] = { + date: date, + events: [] + }; + } + + // Group events by their date (including multi-day events) + events.forEach(event => { + const eventStart = new Date(event.start); + const eventEnd = new Date(event.end); + eventStart.setHours(0, 0, 0, 0); + eventEnd.setHours(0, 0, 0, 0); + + // Check each day in our 5-day view + Object.keys(eventsByDay).forEach(dateKey => { + const dayDate = new Date(eventsByDay[dateKey].date); + dayDate.setHours(0, 0, 0, 0); + + // Include event if this day falls within the event's duration + if (dayDate >= eventStart && dayDate <= eventEnd) { + eventsByDay[dateKey].events.push(event); + } + }); + }); + + // Render events grouped by day eventsContainer.innerHTML = ''; - events.forEach(event => { - const eventElement = document.createElement('div'); - eventElement.className = 'event'; + Object.keys(eventsByDay).sort().forEach(dateKey => { + const dayData = eventsByDay[dateKey]; + const dayElement = document.createElement('div'); + dayElement.className = 'day-group'; - const startTime = new Date(event.start); - const endTime = new Date(event.end); + // Format day header + const dayDate = dayData.date; + const isToday = dayDate.toDateString() === new Date().toDateString(); + const dayName = isToday ? 'Today' : dayDate.toLocaleDateString('en-NZ', { weekday: 'long' }); + const dateStr = dayDate.toLocaleDateString('en-NZ', { day: 'numeric', month: 'short' }); - // Format time - const timeOptions = { hour: '2-digit', minute: '2-digit', weekday: 'short', day: 'numeric', month: 'short' }; - const timeString = startTime.toLocaleDateString('en-NZ', timeOptions); - - eventElement.innerHTML = ` -
${timeString}
-
${event.title}
- ${event.location ? `
📍 ${event.location}
` : ''} + dayElement.innerHTML = ` +
+ ${dayName} + ${dateStr} +
+
`; - eventsContainer.appendChild(eventElement); + const dayEventsContainer = dayElement.querySelector('.day-events'); + + if (dayData.events.length === 0) { + dayEventsContainer.innerHTML = '
No events
'; + } else { + dayData.events.forEach(event => { + const eventElement = document.createElement('div'); + eventElement.className = 'event'; + + const startTime = new Date(event.start); + const endTime = new Date(event.end); + + // Check if it's an all-day event (time is 00:00) + const isAllDay = startTime.getHours() === 0 && startTime.getMinutes() === 0 + && endTime.getHours() === 0 && endTime.getMinutes() === 0; + + let timeString; + if (isAllDay) { + timeString = 'All day'; + } else { + timeString = startTime.toLocaleTimeString('en-NZ', { hour: '2-digit', minute: '2-digit' }); + } + + eventElement.innerHTML = ` +
${timeString}
+
${event.title}
+ ${event.location ? `
📍 ${event.location}
` : ''} + `; + + dayEventsContainer.appendChild(eventElement); + }); + } + + eventsContainer.appendChild(dayElement); }); } catch (error) {