add collapsible day-rows and game-cards with dates, auto-collapse past items
This commit is contained in:
parent
aef8773d7e
commit
9589a69c9b
4 changed files with 622 additions and 223 deletions
|
|
@ -1,5 +1,6 @@
|
||||||
import requests
|
import requests
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta, timezone
|
||||||
|
import zoneinfo
|
||||||
|
|
||||||
class ESPNAPI:
|
class ESPNAPI:
|
||||||
BASE_URL = 'https://site.api.espn.com/apis/site/v2/sports/football/nfl'
|
BASE_URL = 'https://site.api.espn.com/apis/site/v2/sports/football/nfl'
|
||||||
|
|
@ -7,7 +8,7 @@ class ESPNAPI:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.session = requests.Session()
|
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"""
|
"""Get NFL schedule for a specific week"""
|
||||||
try:
|
try:
|
||||||
print(f"ESPN API: Fetching schedule for week {week}, season {season}", flush=True)
|
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)
|
print(f"ESPN API: Found {len(data['events'])} events", flush=True)
|
||||||
for i, event in enumerate(data['events']):
|
for i, event in enumerate(data['events']):
|
||||||
try:
|
try:
|
||||||
# Parse the game date
|
# Parse the game date as UTC
|
||||||
game_date = datetime.strptime(event['date'], '%Y-%m-%dT%H:%MZ')
|
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
|
# Extract team information
|
||||||
competitors = event['competitions'][0]['competitors']
|
competitors = event['competitions'][0]['competitors']
|
||||||
|
|
@ -42,16 +63,19 @@ class ESPNAPI:
|
||||||
else:
|
else:
|
||||||
away_team = team_abbrev
|
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({
|
games.append({
|
||||||
'date': game_date,
|
'date': game_date_utc, # Keep UTC for reference
|
||||||
'day_of_week': game_date.strftime('%A'),
|
'date_local': game_date_local,
|
||||||
'time': game_date.strftime('%I:%M %p'),
|
'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,
|
'home_team': home_team,
|
||||||
'away_team': away_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:
|
except Exception as e:
|
||||||
print(f"ESPN API: Error processing event {i+1}: {str(e)}", flush=True)
|
print(f"ESPN API: Error processing event {i+1}: {str(e)}", flush=True)
|
||||||
|
|
@ -72,21 +96,27 @@ class ESPNAPI:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def _organize_by_day(self, games):
|
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 = {}
|
schedule = {}
|
||||||
|
|
||||||
|
# Sort games by date first
|
||||||
|
games_sorted = sorted(games, key=lambda x: x['date_local'])
|
||||||
|
|
||||||
# Group games by day
|
# Group games by day
|
||||||
for game in games:
|
for game in games_sorted:
|
||||||
day = game['day_of_week']
|
day_key = f"{game['day_of_week']} {game['date_formatted']}"
|
||||||
if day not in schedule:
|
if day_key not in schedule:
|
||||||
schedule[day] = []
|
schedule[day_key] = {
|
||||||
schedule[day].append(game)
|
'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)
|
# Sort games within each day by time
|
||||||
day_order = {'Monday': 0, 'Tuesday': 1, 'Wednesday': 2, 'Thursday': 3, 'Friday': 4, 'Saturday': 5, 'Sunday': 6}
|
for day_info in schedule.values():
|
||||||
sorted_schedule = {}
|
day_info['games'].sort(key=lambda x: x['date_local'])
|
||||||
|
|
||||||
for day in sorted(schedule.keys(), key=lambda x: day_order.get(x, 7)):
|
return schedule
|
||||||
sorted_schedule[day] = schedule[day]
|
|
||||||
|
|
||||||
return sorted_schedule
|
|
||||||
|
|
|
||||||
327
static/style.css
327
static/style.css
|
|
@ -65,10 +65,7 @@
|
||||||
transition: opacity 0.3s ease, visibility 0.3s ease;
|
transition: opacity 0.3s ease, visibility 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-overlay.active {
|
/* Removed .sidebar-overlay.active - now handled by checkbox state */
|
||||||
opacity: 1;
|
|
||||||
visibility: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Brand styling */
|
/* Brand styling */
|
||||||
.brand {
|
.brand {
|
||||||
|
|
@ -608,9 +605,51 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
}/* Theme Variables */
|
}
|
||||||
|
|
||||||
|
/* Original Theme Variables with improved light-dark() support */
|
||||||
:root {
|
:root {
|
||||||
/* Light theme (default) */
|
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-start: #667eea;
|
||||||
--bg-gradient-end: #764ba2;
|
--bg-gradient-end: #764ba2;
|
||||||
--bg-primary: white;
|
--bg-primary: white;
|
||||||
|
|
@ -628,6 +667,7 @@
|
||||||
--shadow-md: 0 6px 24px 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);
|
--shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||||
--accent: #667eea;
|
--accent: #667eea;
|
||||||
|
--accent-hover: #5a6fd8;
|
||||||
--header-gradient-start: #667eea;
|
--header-gradient-start: #667eea;
|
||||||
--header-gradient-end: #764ba2;
|
--header-gradient-end: #764ba2;
|
||||||
--overlay-light: rgba(255, 255, 255, 0.1);
|
--overlay-light: rgba(255, 255, 255, 0.1);
|
||||||
|
|
@ -656,6 +696,7 @@
|
||||||
--shadow-md: 0 6px 24px rgba(0, 0, 0, 0.4);
|
--shadow-md: 0 6px 24px rgba(0, 0, 0, 0.4);
|
||||||
--shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.5);
|
--shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.5);
|
||||||
--accent: #8b7ff5;
|
--accent: #8b7ff5;
|
||||||
|
--accent-hover: #9d8ff7;
|
||||||
--header-gradient-start: #8b7ff5;
|
--header-gradient-start: #8b7ff5;
|
||||||
--header-gradient-end: #a085e6;
|
--header-gradient-end: #a085e6;
|
||||||
--overlay-light: rgba(0, 0, 0, 0.2);
|
--overlay-light: rgba(0, 0, 0, 0.2);
|
||||||
|
|
@ -665,6 +706,7 @@
|
||||||
--sidebar-bg: #1e1e2e;
|
--sidebar-bg: #1e1e2e;
|
||||||
--sidebar-shadow: 0 0 30px rgba(0, 0, 0, 0.5);
|
--sidebar-shadow: 0 0 30px rgba(0, 0, 0, 0.5);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Reset and base styles */
|
/* Reset and base styles */
|
||||||
* {
|
* {
|
||||||
|
|
@ -688,7 +730,11 @@ body {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hamburger Menu Button */
|
/* Pure CSS Hamburger Menu Button */
|
||||||
|
.menu-toggle-checkbox {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.menu-toggle {
|
.menu-toggle {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 20px;
|
top: 20px;
|
||||||
|
|
@ -731,10 +777,17 @@ body {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar.active {
|
/* Show sidebar when checkbox is checked */
|
||||||
|
.menu-toggle-checkbox:checked ~ .sidebar {
|
||||||
right: 0;
|
right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Show overlay when checkbox is checked */
|
||||||
|
.menu-toggle-checkbox:checked ~ .sidebar-overlay {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
.close-btn {
|
.close-btn {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 15px;
|
top: 15px;
|
||||||
|
|
@ -786,4 +839,262 @@ body {
|
||||||
transform: translateY(-2px);
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,23 +7,32 @@
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- Hamburger Menu Button -->
|
<!-- Hamburger Menu Button - Uses CSS-only solution -->
|
||||||
<button class="menu-toggle" onclick="toggleSidebar()" aria-label="Toggle menu">
|
<input type="checkbox" id="menu-toggle" class="menu-toggle-checkbox">
|
||||||
|
<label for="menu-toggle" class="menu-toggle" aria-label="Toggle menu">
|
||||||
<span class="menu-icon">☰</span>
|
<span class="menu-icon">☰</span>
|
||||||
</button>
|
</label>
|
||||||
|
|
||||||
<!-- Sidebar Menu -->
|
<!-- Sidebar Menu -->
|
||||||
<div class="sidebar" id="sidebar">
|
<div class="sidebar" id="sidebar">
|
||||||
<button class="close-btn" onclick="toggleSidebar()">✕</button>
|
<label for="menu-toggle" class="close-btn">✕</label>
|
||||||
|
|
||||||
<div class="sidebar-content">
|
<div class="sidebar-content">
|
||||||
<!-- Theme Toggle Section -->
|
<!-- Theme Toggle Section -->
|
||||||
<div class="sidebar-section">
|
<div class="sidebar-section">
|
||||||
<h3>Theme</h3>
|
<h3>Theme</h3>
|
||||||
<button class="theme-button" onclick="toggleTheme()">
|
<form method="post" action="/toggle_theme" style="display: inline;">
|
||||||
<span id="theme-icon">☀️</span>
|
<input type="hidden" name="return_url" value="{{ request.url }}">
|
||||||
<span id="theme-text">Light Mode</span>
|
<button type="submit" class="theme-button">
|
||||||
|
{% if current_theme == 'dark' %}
|
||||||
|
<span>🌙</span>
|
||||||
|
<span>Dark Mode</span>
|
||||||
|
{% else %}
|
||||||
|
<span>☀️</span>
|
||||||
|
<span>Light Mode</span>
|
||||||
|
{% endif %}
|
||||||
</button>
|
</button>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Links Section -->
|
<!-- Links Section -->
|
||||||
|
|
@ -57,7 +66,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Overlay for sidebar -->
|
<!-- Overlay for sidebar -->
|
||||||
<div class="sidebar-overlay" id="sidebar-overlay" onclick="toggleSidebar()"></div>
|
<label for="menu-toggle" class="sidebar-overlay" id="sidebar-overlay"></label>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
|
|
@ -68,105 +77,95 @@
|
||||||
<div class="version">FantasyCron v{{ app_version }}</div>
|
<div class="version">FantasyCron v{{ app_version }}</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
<!-- Set theme based on server-side session -->
|
||||||
<script>
|
<script>
|
||||||
// Sidebar management
|
// Apply theme immediately to prevent flash
|
||||||
const toggleSidebar = () => {
|
document.documentElement.setAttribute('data-theme', '{{ current_theme }}');
|
||||||
const sidebar = document.getElementById('sidebar');
|
|
||||||
const overlay = document.getElementById('sidebar-overlay');
|
|
||||||
sidebar.classList.toggle('active');
|
|
||||||
overlay.classList.toggle('active');
|
|
||||||
};
|
|
||||||
|
|
||||||
// Theme management
|
// For modern browsers with light-dark() support, also set color-scheme
|
||||||
const getTheme = () => localStorage.getItem('theme') || 'light';
|
document.documentElement.style.colorScheme = '{{ current_theme }}';
|
||||||
|
|
||||||
const setTheme = (theme) => {
|
// Detect user's timezone and send to server if not already set
|
||||||
document.documentElement.setAttribute('data-theme', theme);
|
{% if not session.get('user_timezone') %}
|
||||||
localStorage.setItem('theme', theme);
|
|
||||||
const icon = document.getElementById('theme-icon');
|
|
||||||
const text = document.getElementById('theme-text');
|
|
||||||
if (theme === 'dark') {
|
|
||||||
icon.textContent = '🌙';
|
|
||||||
text.textContent = 'Dark Mode';
|
|
||||||
} else {
|
|
||||||
icon.textContent = '☀️';
|
|
||||||
text.textContent = 'Light Mode';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleTheme = () => {
|
|
||||||
const currentTheme = getTheme();
|
|
||||||
setTheme(currentTheme === 'dark' ? 'light' : 'dark');
|
|
||||||
};
|
|
||||||
|
|
||||||
// Initialize theme on load
|
|
||||||
setTheme(getTheme());
|
|
||||||
|
|
||||||
// Timezone conversion
|
|
||||||
const convertGameTimes = () => {
|
|
||||||
const timeElements = document.querySelectorAll('.game-time[data-utc]');
|
|
||||||
const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||||
|
if (userTimezone) {
|
||||||
|
// Send timezone to server via hidden form submission
|
||||||
|
const form = document.createElement('form');
|
||||||
|
form.method = 'POST';
|
||||||
|
form.action = '/set_timezone';
|
||||||
|
form.style.display = 'none';
|
||||||
|
|
||||||
timeElements.forEach(element => {
|
const timezoneInput = document.createElement('input');
|
||||||
const utcDateStr = element.getAttribute('data-utc');
|
timezoneInput.type = 'hidden';
|
||||||
if (utcDateStr) {
|
timezoneInput.name = 'timezone';
|
||||||
const utcDate = new Date(utcDateStr);
|
timezoneInput.value = userTimezone;
|
||||||
|
|
||||||
// Format time in user's timezone (or PST as fallback)
|
const returnUrlInput = document.createElement('input');
|
||||||
const options = {
|
returnUrlInput.type = 'hidden';
|
||||||
hour: 'numeric',
|
returnUrlInput.name = 'return_url';
|
||||||
minute: '2-digit',
|
returnUrlInput.value = window.location.href;
|
||||||
hour12: true,
|
|
||||||
timeZone: userTimezone || 'America/Los_Angeles'
|
|
||||||
};
|
|
||||||
|
|
||||||
const localTime = utcDate.toLocaleString('en-US', options);
|
form.appendChild(timezoneInput);
|
||||||
element.textContent = localTime;
|
form.appendChild(returnUrlInput);
|
||||||
|
document.body.appendChild(form);
|
||||||
// Add timezone abbreviation on hover
|
form.submit();
|
||||||
const tzOptions = { timeZoneName: 'short', timeZone: userTimezone || 'America/Los_Angeles' };
|
|
||||||
const tzAbbr = utcDate.toLocaleString('en-US', tzOptions).split(' ').pop();
|
|
||||||
element.title = `${localTime} ${tzAbbr}`;
|
|
||||||
}
|
}
|
||||||
});
|
{% endif %}
|
||||||
};
|
|
||||||
|
|
||||||
// Convert times on page load
|
// Collapsible functionality
|
||||||
window.addEventListener('DOMContentLoaded', convertGameTimes);
|
function toggleDay(dayKey) {
|
||||||
|
const dayRow = document.querySelector(`[data-day="${dayKey}"]`);
|
||||||
|
const dayGames = document.getElementById(`day-${dayKey}`);
|
||||||
|
const indicator = dayRow.querySelector('.collapse-indicator');
|
||||||
|
|
||||||
// Score refresh functionality
|
dayRow.classList.toggle('collapsed');
|
||||||
function refreshScores() {
|
|
||||||
const username = document.querySelector('meta[name="username"]')?.content;
|
|
||||||
const week = document.querySelector('meta[name="week"]')?.content;
|
|
||||||
|
|
||||||
if (username && week) {
|
if (dayRow.classList.contains('collapsed')) {
|
||||||
fetch(`/api/refresh/${username}/${week}`)
|
indicator.textContent = '▶';
|
||||||
.then(response => response.json())
|
dayGames.style.display = 'none';
|
||||||
.then(data => {
|
|
||||||
if (data.league_scores) {
|
|
||||||
data.league_scores.forEach(league => {
|
|
||||||
const userScore = document.getElementById(`user-score-${league.league_id}`);
|
|
||||||
const oppScore = document.getElementById(`opp-score-${league.league_id}`);
|
|
||||||
|
|
||||||
if (userScore) userScore.textContent = league.user_points.toFixed(1);
|
|
||||||
if (oppScore) oppScore.textContent = league.opponent_points.toFixed(1);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => console.log('Refresh failed:', error));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set refresh interval based on game day
|
|
||||||
const now = new Date();
|
|
||||||
const dayOfWeek = now.getDay();
|
|
||||||
const isGameTime = dayOfWeek === 4 || dayOfWeek === 0 || dayOfWeek === 1; // Thu, Sun, Mon
|
|
||||||
|
|
||||||
if (isGameTime) {
|
|
||||||
setInterval(refreshScores, 60000); // Every minute during games
|
|
||||||
} else {
|
} else {
|
||||||
setInterval(refreshScores, 21600000); // Every 6 hours otherwise
|
indicator.textContent = '▼';
|
||||||
|
dayGames.style.display = 'block';
|
||||||
|
// Expand all games in this day
|
||||||
|
const gameCards = dayGames.querySelectorAll('.game-card');
|
||||||
|
gameCards.forEach(card => {
|
||||||
|
card.classList.remove('collapsed');
|
||||||
|
const gameIndicator = card.querySelector('.game-collapse-indicator');
|
||||||
|
const gameContent = card.querySelector('.game-content');
|
||||||
|
if (gameIndicator) gameIndicator.textContent = '▼';
|
||||||
|
if (gameContent) gameContent.style.display = 'block';
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleGame(dayKey, gameIndex) {
|
||||||
|
const gameCard = document.querySelector(`[data-day="${dayKey}"] [data-game="${gameIndex}"]`);
|
||||||
|
const gameContent = document.getElementById(`game-${dayKey}-${gameIndex}`);
|
||||||
|
const indicator = gameCard.querySelector('.game-collapse-indicator');
|
||||||
|
|
||||||
|
gameCard.classList.toggle('collapsed');
|
||||||
|
|
||||||
|
if (gameCard.classList.contains('collapsed')) {
|
||||||
|
indicator.textContent = '▶';
|
||||||
|
gameContent.style.display = 'none';
|
||||||
|
} else {
|
||||||
|
indicator.textContent = '▼';
|
||||||
|
gameContent.style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize collapsed states on page load
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Hide content for collapsed day rows
|
||||||
|
document.querySelectorAll('.day-row.collapsed .day-games').forEach(dayGames => {
|
||||||
|
dayGames.style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Hide content for collapsed game cards
|
||||||
|
document.querySelectorAll('.game-card.collapsed .game-content').forEach(gameContent => {
|
||||||
|
gameContent.style.display = 'none';
|
||||||
|
});
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,6 @@
|
||||||
{% block title %}{{ user.display_name }} - Week {{ week }}{% endblock %}
|
{% block title %}{{ user.display_name }} - Week {{ week }}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<!-- Meta tags for JavaScript -->
|
|
||||||
<meta name="username" content="{{ user.username }}">
|
|
||||||
<meta name="week" content="{{ week }}">
|
|
||||||
|
|
||||||
<!-- Header with user name and week navigation -->
|
<!-- Header with user name and week navigation -->
|
||||||
<header class="dashboard-header">
|
<header class="dashboard-header">
|
||||||
|
|
@ -15,25 +12,45 @@
|
||||||
<span class="current-week">Week {{ week }}</span>
|
<span class="current-week">Week {{ week }}</span>
|
||||||
<a href="/{{ user.username }}/{{ week + 1 }}" class="week-btn">Week {{ week + 1 }} →</a>
|
<a href="/{{ user.username }}/{{ week + 1 }}" class="week-btn">Week {{ week + 1 }} →</a>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Server-side refresh -->
|
||||||
|
<div class="refresh-nav">
|
||||||
|
<form method="post" action="/{{ user.username }}/{{ week }}/refresh" style="display: inline;">
|
||||||
|
<button type="submit" class="refresh-btn">🔄 Refresh Scores</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- Compact league scores at top -->
|
<!-- Compact league scores at top -->
|
||||||
<section class="scores-summary">
|
<section class="scores-summary">
|
||||||
{% for league_info in league_data %}
|
{% for league_info in league_data %}
|
||||||
<div class="score-row">
|
<div class="score-row {{ league_info.match_status }}">
|
||||||
<div class="league-info">
|
<div class="league-info">
|
||||||
<!-- League color dot -->
|
<!-- League color dot -->
|
||||||
<span class="league-dot" style="background-color: {{ league_info.league_color }};"></span>
|
<span class="league-dot" style="background-color: {{ league_info.league_color }};"></span>
|
||||||
<span class="league-name">{{ league_info.league.name }}</span>
|
<span class="league-name">{{ league_info.league.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="score-compact">
|
<div class="score-compact">
|
||||||
|
<!-- Win/Loss indicator -->
|
||||||
|
<span class="match-indicator {{ league_info.match_status }}">
|
||||||
|
{% if league_info.match_status == 'winning' %}
|
||||||
|
<span class="indicator-icon">↗️</span>
|
||||||
|
<span class="indicator-text">W</span>
|
||||||
|
{% elif league_info.match_status == 'losing' %}
|
||||||
|
<span class="indicator-icon">↘️</span>
|
||||||
|
<span class="indicator-text">L</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="indicator-icon">↔️</span>
|
||||||
|
<span class="indicator-text">T</span>
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
|
||||||
<span class="user-name">{{ user.display_name }}</span>
|
<span class="user-name">{{ user.display_name }}</span>
|
||||||
<span class="score" id="user-score-{{ league_info.league.league_id }}">
|
<span class="score user-score {{ league_info.match_status }}" id="user-score-{{ league_info.league.league_id }}">
|
||||||
{{ league_info.user_matchup.points|round(1) if league_info.user_matchup else '0.0' }}
|
{{ league_info.user_points|round(1) }}
|
||||||
</span>
|
</span>
|
||||||
<span class="vs-compact">vs</span>
|
<span class="vs-compact">vs</span>
|
||||||
<span class="score" id="opp-score-{{ league_info.league.league_id }}">
|
<span class="score opp-score" id="opp-score-{{ league_info.league.league_id }}">
|
||||||
{{ league_info.opponent_matchup.points|round(1) if league_info.opponent_matchup else '0.0' }}
|
{{ league_info.opponent_points|round(1) }}
|
||||||
</span>
|
</span>
|
||||||
<span class="opp-name">{{ league_info.opponent_user.display_name if league_info.opponent_user else 'Opponent' }}</span>
|
<span class="opp-name">{{ league_info.opponent_user.display_name if league_info.opponent_user else 'Opponent' }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -43,30 +60,43 @@
|
||||||
|
|
||||||
<!-- Main calendar section -->
|
<!-- Main calendar section -->
|
||||||
<section class="schedule-section">
|
<section class="schedule-section">
|
||||||
<h2>Week {{ week }} Games</h2>
|
<h2>Week {{ week }} Games
|
||||||
|
{% if session.get('user_timezone') %}
|
||||||
|
<span class="timezone-info">({{ session.get('user_timezone')|replace('_', ' ')|replace('America/', '') }})</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="timezone-info">(PST/PDT)</span>
|
||||||
|
{% endif %}
|
||||||
|
</h2>
|
||||||
<div class="calendar-rows">
|
<div class="calendar-rows">
|
||||||
<!-- Track if we have any games using namespace -->
|
<!-- Track if we have any games using namespace -->
|
||||||
{% set ns = namespace(has_games=false) %}
|
{% set ns = namespace(has_games=false) %}
|
||||||
|
|
||||||
<!-- Loop through days that have games -->
|
<!-- Loop through days that have games -->
|
||||||
{% for day, games in schedule.items() if games %}
|
{% for day_key, day_info in schedule.items() if day_info.games %}
|
||||||
{% set ns.has_games = true %}
|
{% set ns.has_games = true %}
|
||||||
<div class="day-row">
|
<div class="day-row {% if day_info.is_past %}collapsed{% endif %}" data-day="{{ day_key }}">
|
||||||
<div class="day-header">
|
<div class="day-header" onclick="toggleDay('{{ day_key }}')">
|
||||||
<h3>{{ day }}</h3>
|
<div class="day-header-content">
|
||||||
|
<h3>{{ day_info.day_name }} {{ day_info.date }}</h3>
|
||||||
|
<span class="collapse-indicator">{% if day_info.is_past %}▶{% else %}▼{% endif %}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="day-games">
|
</div>
|
||||||
|
<div class="day-games" id="day-{{ day_key }}">
|
||||||
<!-- Games for this day -->
|
<!-- Games for this day -->
|
||||||
{% for game in games %}
|
{% for game in day_info.games %}
|
||||||
<div class="game-card">
|
<div class="game-card {% if game.is_past %}collapsed{% endif %}" data-game="{{ loop.index }}">
|
||||||
|
<div class="game-header" onclick="toggleGame('{{ day_key }}', '{{ loop.index }}')">
|
||||||
<div class="game-info">
|
<div class="game-info">
|
||||||
<div class="game-time" data-utc="{{ game.date.isoformat() if game.date else '' }}">{{ game.time }}</div>
|
<div class="game-time">{{ game.time }}</div>
|
||||||
<div class="matchup">
|
<div class="matchup">
|
||||||
<span class="away-team">{{ game.away_team }}</span>
|
<span class="away-team">{{ game.away_team }}</span>
|
||||||
<span class="at">@</span>
|
<span class="at">@</span>
|
||||||
<span class="home-team">{{ game.home_team }}</span>
|
<span class="home-team">{{ game.home_team }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<span class="game-collapse-indicator">{% if game.is_past %}▶{% else %}▼{% endif %}</span>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="game-content" id="game-{{ day_key }}-{{ loop.index }}">
|
||||||
|
|
||||||
<!-- Show user's players in this game, grouped by league -->
|
<!-- Show user's players in this game, grouped by league -->
|
||||||
<div class="game-players">
|
<div class="game-players">
|
||||||
|
|
@ -79,17 +109,46 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
{% if players_in_game %}
|
{% if players_in_game %}
|
||||||
<div class="league-player-group" style="border-left: 3px solid {{ league_info.league_color }};">
|
<div class="league-player-group" style="border-left: 5px solid {{ league_info.league_color }}; border-right: 5px solid {{ league_info.league_color }}">
|
||||||
|
<!-- Sort players: starters and bench -->
|
||||||
|
{% set starters = [] %}
|
||||||
|
{% set bench_players = [] %}
|
||||||
{% for player in players_in_game %}
|
{% for player in players_in_game %}
|
||||||
<div class="player-pill {{ player.fantasy_positions[0]|lower if player.fantasy_positions else 'flex' }}">
|
{% if player.get('is_starter', False) %}
|
||||||
|
{% set _ = starters.append(player) %}
|
||||||
|
{% else %}
|
||||||
|
{% set _ = bench_players.append(player) %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<!-- Starters container (left-aligned) -->
|
||||||
|
{% if starters %}
|
||||||
|
<div class="starters-container">
|
||||||
|
{% for player in starters %}
|
||||||
|
<div class="player-pill starter {{ player.fantasy_positions[0]|lower if player.fantasy_positions else 'flex' }}">
|
||||||
<span class="pos">{{ player.fantasy_positions[0] if player.fantasy_positions else 'FLEX' }}</span>
|
<span class="pos">{{ player.fantasy_positions[0] if player.fantasy_positions else 'FLEX' }}</span>
|
||||||
<span class="name">{{ player.last_name }}</span>
|
<span class="name">{{ player.last_name }}</span>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Bench players container (right-aligned) -->
|
||||||
|
{% if bench_players %}
|
||||||
|
<div class="bench-container">
|
||||||
|
{% for player in bench_players %}
|
||||||
|
<div class="player-pill bench {{ player.fantasy_positions[0]|lower if player.fantasy_positions else 'flex' }}">
|
||||||
|
<span class="pos">{{ player.fantasy_positions[0] if player.fantasy_positions else 'FLEX' }}</span>
|
||||||
|
<span class="name">{{ player.last_name }}</span>
|
||||||
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue