From 9589a69c9b9f53c3e7d4bce908490ab9f8ebce2d Mon Sep 17 00:00:00 2001 From: efigueroa Date: Sat, 6 Sep 2025 22:30:59 -0700 Subject: [PATCH] add collapsible day-rows and game-cards with dates, auto-collapse past items --- services/espn_api.py | 76 ++++--- static/style.css | 431 +++++++++++++++++++++++++++++++++------ templates/base.html | 197 +++++++++--------- templates/dashboard.html | 141 +++++++++---- 4 files changed, 622 insertions(+), 223 deletions(-) diff --git a/services/espn_api.py b/services/espn_api.py index c944053..34c601d 100644 --- a/services/espn_api.py +++ b/services/espn_api.py @@ -1,5 +1,6 @@ import requests -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone +import zoneinfo class ESPNAPI: BASE_URL = 'https://site.api.espn.com/apis/site/v2/sports/football/nfl' @@ -7,7 +8,7 @@ class ESPNAPI: def __init__(self): self.session = requests.Session() - def get_week_schedule(self, week, season): + def get_week_schedule(self, week, season, user_timezone='America/Los_Angeles'): """Get NFL schedule for a specific week""" try: print(f"ESPN API: Fetching schedule for week {week}, season {season}", flush=True) @@ -25,8 +26,28 @@ class ESPNAPI: print(f"ESPN API: Found {len(data['events'])} events", flush=True) for i, event in enumerate(data['events']): try: - # Parse the game date - game_date = datetime.strptime(event['date'], '%Y-%m-%dT%H:%MZ') + # Parse the game date as UTC + game_date_utc = datetime.strptime(event['date'], '%Y-%m-%dT%H:%MZ').replace(tzinfo=timezone.utc) + + # Convert to user's timezone (default PST) + try: + user_tz = zoneinfo.ZoneInfo(user_timezone) + except: + # Fallback to PST if timezone is invalid + user_tz = zoneinfo.ZoneInfo('America/Los_Angeles') + user_timezone = 'America/Los_Angeles' + + game_date_local = game_date_utc.astimezone(user_tz) + + # Get timezone abbreviation + tz_abbr = game_date_local.strftime('%Z') + if not tz_abbr: # Fallback if %Z doesn't work + if 'Los_Angeles' in user_timezone or 'Pacific' in user_timezone: + tz_abbr = 'PST' if game_date_local.dst() == timedelta(0) else 'PDT' + elif 'New_York' in user_timezone or 'Eastern' in user_timezone: + tz_abbr = 'EST' if game_date_local.dst() == timedelta(0) else 'EDT' + else: + tz_abbr = 'Local' # Extract team information competitors = event['competitions'][0]['competitors'] @@ -42,16 +63,19 @@ class ESPNAPI: else: away_team = team_abbrev - print(f"ESPN API: {away_team} @ {home_team} at {game_date}", flush=True) + print(f"ESPN API: {away_team} @ {home_team} at {game_date_local.strftime('%I:%M %p')} {tz_abbr}", flush=True) - # Store game data + # Store game data with user's local times games.append({ - 'date': game_date, - 'day_of_week': game_date.strftime('%A'), - 'time': game_date.strftime('%I:%M %p'), + 'date': game_date_utc, # Keep UTC for reference + 'date_local': game_date_local, + 'day_of_week': game_date_local.strftime('%A'), + 'date_formatted': game_date_local.strftime('%m/%d'), + 'time': f"{game_date_local.strftime('%I:%M %p')} {tz_abbr}", 'home_team': home_team, 'away_team': away_team, - 'teams': [home_team, away_team] + 'teams': [home_team, away_team], + 'is_past': game_date_local < datetime.now(game_date_local.tzinfo) }) except Exception as e: print(f"ESPN API: Error processing event {i+1}: {str(e)}", flush=True) @@ -72,21 +96,27 @@ class ESPNAPI: return {} def _organize_by_day(self, games): - """Organize games by day of week - dynamically include all days with games""" + """Organize games by day, sorted chronologically with date info""" schedule = {} + # Sort games by date first + games_sorted = sorted(games, key=lambda x: x['date_local']) + # Group games by day - for game in games: - day = game['day_of_week'] - if day not in schedule: - schedule[day] = [] - schedule[day].append(game) + for game in games_sorted: + day_key = f"{game['day_of_week']} {game['date_formatted']}" + if day_key not in schedule: + schedule[day_key] = { + 'day_name': game['day_of_week'], + 'date': game['date_formatted'], + 'date_obj': game['date_local'].date(), + 'is_past': game['date_local'].date() < datetime.now(game['date_local'].tzinfo).date(), + 'games': [] + } + schedule[day_key]['games'].append(game) - # Sort days by chronological order (Monday=0, Sunday=6) - day_order = {'Monday': 0, 'Tuesday': 1, 'Wednesday': 2, 'Thursday': 3, 'Friday': 4, 'Saturday': 5, 'Sunday': 6} - sorted_schedule = {} + # Sort games within each day by time + for day_info in schedule.values(): + day_info['games'].sort(key=lambda x: x['date_local']) - for day in sorted(schedule.keys(), key=lambda x: day_order.get(x, 7)): - sorted_schedule[day] = schedule[day] - - return sorted_schedule + return schedule diff --git a/static/style.css b/static/style.css index 0e2e167..29a6570 100644 --- a/static/style.css +++ b/static/style.css @@ -65,10 +65,7 @@ transition: opacity 0.3s ease, visibility 0.3s ease; } -.sidebar-overlay.active { - opacity: 1; - visibility: visible; -} +/* Removed .sidebar-overlay.active - now handled by checkbox state */ /* Brand styling */ .brand { @@ -608,62 +605,107 @@ flex-direction: column; gap: 10px; } -}/* Theme Variables */ -:root { - /* Light theme (default) */ - --bg-gradient-start: #667eea; - --bg-gradient-end: #764ba2; - --bg-primary: white; - --bg-secondary: #f8f9fa; - --bg-tertiary: #e9ecef; - --text-primary: #333; - --text-secondary: #495057; - --text-muted: #666; - --text-light: white; - --player-name-color: #000; - --border-primary: #e1e8ed; - --border-secondary: #dee2e6; - --border-light: #f1f3f4; - --shadow-sm: 0 4px 16px rgba(0, 0, 0, 0.1); - --shadow-md: 0 6px 24px rgba(0, 0, 0, 0.1); - --shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.1); - --accent: #667eea; - --header-gradient-start: #667eea; - --header-gradient-end: #764ba2; - --overlay-light: rgba(255, 255, 255, 0.1); - --overlay-medium: rgba(255, 255, 255, 0.6); - --footer-text: rgba(255, 255, 255, 0.6); - --error-color: #e74c3c; - --sidebar-bg: white; - --sidebar-shadow: 0 0 30px rgba(0, 0, 0, 0.2); } -[data-theme="dark"] { - --bg-gradient-start: #1a1a2e; - --bg-gradient-end: #16213e; - --bg-primary: #1e1e2e; - --bg-secondary: #2a2a3e; - --bg-tertiary: #35354a; - --text-primary: #e0e0e0; - --text-secondary: #b0b0b0; - --text-muted: #888; - --text-light: #e0e0e0; - --player-name-color: #e0e0e0; - --border-primary: #3a3a4e; - --border-secondary: #4a4a5e; - --border-light: #2a2a3e; - --shadow-sm: 0 4px 16px rgba(0, 0, 0, 0.3); - --shadow-md: 0 6px 24px rgba(0, 0, 0, 0.4); - --shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.5); - --accent: #8b7ff5; - --header-gradient-start: #8b7ff5; - --header-gradient-end: #a085e6; - --overlay-light: rgba(0, 0, 0, 0.2); - --overlay-medium: rgba(0, 0, 0, 0.3); - --footer-text: rgba(255, 255, 255, 0.4); - --error-color: #ff6b6b; - --sidebar-bg: #1e1e2e; - --sidebar-shadow: 0 0 30px rgba(0, 0, 0, 0.5); +/* Original Theme Variables with improved light-dark() support */ +:root { + color-scheme: light dark; + + /* Theme colors using light-dark() where supported */ + --bg-gradient-start: light-dark(#667eea, #1a1a2e); + --bg-gradient-end: light-dark(#764ba2, #16213e); + --bg-primary: light-dark(white, #1e1e2e); + --bg-secondary: light-dark(#f8f9fa, #2a2a3e); + --bg-tertiary: light-dark(#e9ecef, #35354a); + + --text-primary: light-dark(#333, #e0e0e0); + --text-secondary: light-dark(#495057, #b0b0b0); + --text-muted: light-dark(#666, #888); + --text-light: light-dark(white, #e0e0e0); + --player-name-color: light-dark(#000, #e0e0e0); + + --border-primary: light-dark(#e1e8ed, #3a3a4e); + --border-secondary: light-dark(#dee2e6, #4a4a5e); + --border-light: light-dark(#f1f3f4, #2a2a3e); + + --shadow-sm: light-dark(0 4px 16px rgba(0, 0, 0, 0.1), 0 4px 16px rgba(0, 0, 0, 0.3)); + --shadow-md: light-dark(0 6px 24px rgba(0, 0, 0, 0.1), 0 6px 24px rgba(0, 0, 0, 0.4)); + --shadow-lg: light-dark(0 8px 32px rgba(0, 0, 0, 0.1), 0 8px 32px rgba(0, 0, 0, 0.5)); + + --accent: light-dark(#667eea, #8b7ff5); + --accent-hover: light-dark(#5a6fd8, #9d8ff7); + --header-gradient-start: light-dark(#667eea, #8b7ff5); + --header-gradient-end: light-dark(#764ba2, #a085e6); + + --overlay-light: light-dark(rgba(255, 255, 255, 0.1), rgba(0, 0, 0, 0.2)); + --overlay-medium: light-dark(rgba(255, 255, 255, 0.6), rgba(0, 0, 0, 0.3)); + --footer-text: light-dark(rgba(255, 255, 255, 0.6), rgba(255, 255, 255, 0.4)); + --error-color: light-dark(#e74c3c, #ff6b6b); + + --sidebar-bg: var(--bg-primary); + --sidebar-shadow: light-dark(0 0 30px rgba(0, 0, 0, 0.2), 0 0 30px rgba(0, 0, 0, 0.5)); +} + +/* Fallback for browsers that don't support light-dark() */ +@supports not (color: light-dark(white, black)) { + :root { + /* Light theme (original colors) */ + --bg-gradient-start: #667eea; + --bg-gradient-end: #764ba2; + --bg-primary: white; + --bg-secondary: #f8f9fa; + --bg-tertiary: #e9ecef; + --text-primary: #333; + --text-secondary: #495057; + --text-muted: #666; + --text-light: white; + --player-name-color: #000; + --border-primary: #e1e8ed; + --border-secondary: #dee2e6; + --border-light: #f1f3f4; + --shadow-sm: 0 4px 16px rgba(0, 0, 0, 0.1); + --shadow-md: 0 6px 24px rgba(0, 0, 0, 0.1); + --shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.1); + --accent: #667eea; + --accent-hover: #5a6fd8; + --header-gradient-start: #667eea; + --header-gradient-end: #764ba2; + --overlay-light: rgba(255, 255, 255, 0.1); + --overlay-medium: rgba(255, 255, 255, 0.6); + --footer-text: rgba(255, 255, 255, 0.6); + --error-color: #e74c3c; + --sidebar-bg: white; + --sidebar-shadow: 0 0 30px rgba(0, 0, 0, 0.2); + } + + [data-theme="dark"] { + --bg-gradient-start: #1a1a2e; + --bg-gradient-end: #16213e; + --bg-primary: #1e1e2e; + --bg-secondary: #2a2a3e; + --bg-tertiary: #35354a; + --text-primary: #e0e0e0; + --text-secondary: #b0b0b0; + --text-muted: #888; + --text-light: #e0e0e0; + --player-name-color: #e0e0e0; + --border-primary: #3a3a4e; + --border-secondary: #4a4a5e; + --border-light: #2a2a3e; + --shadow-sm: 0 4px 16px rgba(0, 0, 0, 0.3); + --shadow-md: 0 6px 24px rgba(0, 0, 0, 0.4); + --shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.5); + --accent: #8b7ff5; + --accent-hover: #9d8ff7; + --header-gradient-start: #8b7ff5; + --header-gradient-end: #a085e6; + --overlay-light: rgba(0, 0, 0, 0.2); + --overlay-medium: rgba(0, 0, 0, 0.3); + --footer-text: rgba(255, 255, 255, 0.4); + --error-color: #ff6b6b; + --sidebar-bg: #1e1e2e; + --sidebar-shadow: 0 0 30px rgba(0, 0, 0, 0.5); + } } /* Reset and base styles */ @@ -688,7 +730,11 @@ body { padding: 20px; } -/* Hamburger Menu Button */ +/* Pure CSS Hamburger Menu Button */ +.menu-toggle-checkbox { + display: none; +} + .menu-toggle { position: fixed; top: 20px; @@ -731,10 +777,17 @@ body { overflow-y: auto; } -.sidebar.active { +/* Show sidebar when checkbox is checked */ +.menu-toggle-checkbox:checked ~ .sidebar { right: 0; } +/* Show overlay when checkbox is checked */ +.menu-toggle-checkbox:checked ~ .sidebar-overlay { + opacity: 1; + visibility: visible; +} + .close-btn { position: absolute; top: 15px; @@ -786,4 +839,262 @@ body { transform: translateY(-2px); } +/* Refresh button */ +.refresh-btn { + background: var(--accent); + color: white; + border: none; + border-radius: 6px; + padding: 8px 16px; + font-size: 14px; + cursor: pointer; + transition: all 0.2s ease; + margin-left: 10px; +} + +.refresh-btn:hover { + background: var(--accent-hover); + transform: translateY(-1px); +} + +/* Timezone info */ +.timezone-info { + font-size: 0.8rem; + color: var(--text-secondary); + font-weight: normal; + opacity: 0.8; +} + +/* Theme button styling */ +.theme-button { + display: flex; + align-items: center; + gap: 10px; + background: var(--bg-secondary); + border: 2px solid var(--border-primary); + border-radius: 8px; + padding: 12px 20px; + cursor: pointer; + width: 100%; + transition: all 0.2s ease; + color: var(--text-primary); + font-size: 15px; +} + +.theme-button:hover { + background: var(--bg-tertiary); + transform: translateY(-2px); +} + +/* Player pill styling - starters vs bench */ +.player-pill.starter { + /* Starters: normal styling */ + opacity: 1; + font-weight: 500; +} + +.player-pill.bench { + /* Bench players: greyed out */ + opacity: 0.6; + font-weight: 400; + color: var(--text-muted); +} + +.player-pill.bench .pos, +.player-pill.bench .name { + color: var(--text-muted); + opacity: 0.8; +} + +/* League player group styling - use space-between to separate starters and bench */ +.league-player-group { + display: flex; + flex-wrap: wrap; + gap: 8px; + padding: 12px; + background: var(--bg-secondary); + border-radius: 8px; + margin-bottom: 8px; + justify-content: space-between; /* Push bench players to the right */ + align-items: flex-start; +} + +/* Create a container for starters (left side) */ +.starters-container { + display: flex; + flex-wrap: wrap; + gap: 8px; + justify-content: flex-start; +} + +/* Create a container for bench players (right side) */ +.bench-container { + display: flex; + flex-wrap: wrap; + gap: 8px; + justify-content: flex-end; + margin-left: auto; /* Push to right */ +} + +/* Win/Loss Indicators */ +.match-indicator { + display: flex; + align-items: center; + gap: 4px; + padding: 4px 8px; + border-radius: 12px; + font-size: 12px; + font-weight: 600; + margin-right: 12px; + min-width: 32px; + justify-content: center; +} + +.match-indicator.winning { + background-color: #10b981; + color: white; +} + +.match-indicator.losing { + background-color: #ef4444; + color: white; +} + +.match-indicator.tied { + background-color: #6b7280; + color: white; +} + +.indicator-icon { + font-size: 14px; +} + +.indicator-text { + font-weight: 700; + font-size: 11px; + letter-spacing: 0.5px; +} + +/* Score row styling based on status */ +.score-row.winning { + background: linear-gradient(90deg, rgba(16, 185, 129, 0.1) 0%, transparent 100%); + border-left: 3px solid #10b981; +} + +.score-row.losing { + background: linear-gradient(90deg, rgba(239, 68, 68, 0.1) 0%, transparent 100%); + border-left: 3px solid #ef4444; +} + +.score-row.tied { + background: linear-gradient(90deg, rgba(107, 114, 128, 0.1) 0%, transparent 100%); + border-left: 3px solid #6b7280; +} + +/* User score color based on status */ +.user-score.winning { + color: #10b981; + font-weight: 600; +} + +.user-score.losing { + color: #ef4444; + font-weight: 600; +} + +.user-score.tied { + color: #6b7280; + font-weight: 600; +} + +/* Collapsible Day and Game Styling */ +.day-header { + cursor: pointer; + user-select: none; + transition: background-color 0.2s ease; + padding: 12px; + border-radius: 8px; +} + +.day-header:hover { + background-color: var(--bg-secondary); +} + +.day-header-content { + display: flex; + justify-content: space-between; + align-items: center; +} + +.collapse-indicator { + font-size: 14px; + color: var(--text-secondary); + transition: transform 0.2s ease; + min-width: 20px; + text-align: center; +} + +.game-header { + cursor: pointer; + user-select: none; + transition: background-color 0.2s ease; + border-radius: 8px; + padding: 8px; +} + +.game-header:hover { + background-color: var(--bg-tertiary); +} + +.game-header .game-info { + display: flex; + justify-content: space-between; + align-items: center; +} + +.game-collapse-indicator { + font-size: 12px; + color: var(--text-secondary); + transition: transform 0.2s ease; + min-width: 16px; + text-align: center; + margin-left: 12px; +} + +/* Collapsed states */ +.day-row.collapsed .day-games { + display: none; +} + +.game-card.collapsed .game-content { + display: none; +} + +/* Smooth transitions */ +.day-games, +.game-content { + transition: opacity 0.3s ease; +} + +/* Past date/game styling */ +.day-row.collapsed .day-header { + opacity: 0.7; +} + +.game-card.collapsed .game-header { + opacity: 0.7; +} + +/* Improved game card structure */ +.game-card { + border: 1px solid var(--border-light); + border-radius: 8px; + margin-bottom: 12px; + background: var(--bg-primary); +} + +.game-content { + padding: 0 12px 12px 12px; +} + diff --git a/templates/base.html b/templates/base.html index 0d8a86b..4cc46e3 100644 --- a/templates/base.html +++ b/templates/base.html @@ -7,23 +7,32 @@ - - +