add collapsible day-rows and game-cards with dates, auto-collapse past items

This commit is contained in:
efigueroa 2025-09-06 22:30:59 -07:00
parent aef8773d7e
commit 9589a69c9b
4 changed files with 622 additions and 223 deletions

View file

@ -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

View file

@ -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);
@ -636,9 +676,9 @@
--error-color: #e74c3c; --error-color: #e74c3c;
--sidebar-bg: white; --sidebar-bg: white;
--sidebar-shadow: 0 0 30px rgba(0, 0, 0, 0.2); --sidebar-shadow: 0 0 30px rgba(0, 0, 0, 0.2);
} }
[data-theme="dark"] { [data-theme="dark"] {
--bg-gradient-start: #1a1a2e; --bg-gradient-start: #1a1a2e;
--bg-gradient-end: #16213e; --bg-gradient-end: #16213e;
--bg-primary: #1e1e2e; --bg-primary: #1e1e2e;
@ -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);
@ -664,6 +705,7 @@
--error-color: #ff6b6b; --error-color: #ff6b6b;
--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;
}

View file

@ -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>

View file

@ -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 }} &rarr;</a> <a href="/{{ user.username }}/{{ week + 1 }}" class="week-btn">Week {{ week + 1 }} &rarr;</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>