From 0718fdd01f6cc10f820352b0c8bb88480d714582 Mon Sep 17 00:00:00 2001 From: Eduardo Figueroa Date: Thu, 4 Sep 2025 21:31:23 -0700 Subject: [PATCH] init --- ContainerFile | 18 ++ app.py | 252 +++++++++++++++++++ compose.yaml | 27 +++ config.py | 20 ++ requirements.txt | 3 + services/__init__.py | 1 + services/espn_api.py | 92 +++++++ services/sleeper_api.py | 72 ++++++ static/style.css | 509 +++++++++++++++++++++++++++++++++++++++ templates/base.html | 57 +++++ templates/dashboard.html | 102 ++++++++ templates/error.html | 11 + templates/index.html | 33 +++ 13 files changed, 1197 insertions(+) create mode 100644 ContainerFile create mode 100644 app.py create mode 100644 compose.yaml create mode 100644 config.py create mode 100644 requirements.txt create mode 100644 services/__init__.py create mode 100644 services/espn_api.py create mode 100644 services/sleeper_api.py create mode 100644 static/style.css create mode 100644 templates/base.html create mode 100644 templates/dashboard.html create mode 100644 templates/error.html create mode 100644 templates/index.html diff --git a/ContainerFile b/ContainerFile new file mode 100644 index 0000000..cc22891 --- /dev/null +++ b/ContainerFile @@ -0,0 +1,18 @@ +FROM python:3.11-slim + +WORKDIR /app + +# Copy requirements and install dependencies +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy application code +COPY . . + +# Create non-root user for security +RUN useradd --create-home --shell /bin/bash app && chown -R app:app /app +USER app + +EXPOSE 5000 + +CMD ["python", "app.py"] diff --git a/app.py b/app.py new file mode 100644 index 0000000..5043ccc --- /dev/null +++ b/app.py @@ -0,0 +1,252 @@ +from flask import Flask, render_template, jsonify +from datetime import datetime +import os +import sys +import traceback +from services.sleeper_api import SleeperAPI +from services.espn_api import ESPNAPI +from config import Config + +# Force unbuffered output for Docker logs +os.environ['PYTHONUNBUFFERED'] = '1' + +print("=== FantasyCron Starting ===", flush=True) +print(f"=== Version: {Config.VERSION} ===", flush=True) + +app = Flask(__name__) +app.config.from_object(Config) + +# Initialize API services +sleeper_api = SleeperAPI() +espn_api = ESPNAPI() + +def get_league_color(league_index): + """Assign colors to leagues in order""" + colors = app.config['LEAGUE_COLORS'] + return colors[league_index % len(colors)] # Cycle through colors + +@app.context_processor +def inject_apis(): + """Make API and version available to all templates""" + return dict(sleeper_api=sleeper_api, app_version=app.config['VERSION']) + +@app.route('/') +def index(): + """Home page with username input""" + print("DEBUG: Index route accessed", flush=True) + return render_template('index.html') + +@app.route('/') +def dashboard_current(username): + """Dashboard for current NFL week""" + print(f"DEBUG: Dashboard current route - username: '{username}'", flush=True) + try: + nfl_state = sleeper_api.get_nfl_state() + current_week = nfl_state.get('display_week', 1) + print(f"DEBUG: Current week: {current_week}", flush=True) + return dashboard(username, current_week) + except Exception as e: + print(f"ERROR: dashboard_current exception - {str(e)}", flush=True) + print(f"ERROR: Full traceback: {traceback.format_exc()}", flush=True) + return render_template('error.html', + message=f"Could not find user '{username}'. Please check the username and try again.") + +@app.route('//') +def dashboard_week(username, week): + """Dashboard for specific week""" + print(f"DEBUG: Dashboard week - username: '{username}', week: {week}", flush=True) + return dashboard(username, week) + +def dashboard(username, week): + """Main dashboard logic - fetch and display user's fantasy data""" + print(f"DEBUG: Dashboard function START - username: '{username}', week: {week}", flush=True) + try: + # Get user info from Sleeper + print(f"DEBUG: Calling get_user() with username: '{username}'", flush=True) + user = sleeper_api.get_user(username) + print(f"DEBUG: get_user() returned: {user}", flush=True) + + if not user: + print(f"WARNING: User lookup failed for username: '{username}'", flush=True) + return render_template('error.html', + message=f"Could not find user '{username}'. Please check the username and try again.") + + print(f"DEBUG: User found - ID: {user.get('user_id')}, Display: {user.get('display_name', 'Unknown')}", flush=True) + + # Get current NFL season + print("DEBUG: Calling get_nfl_state()", flush=True) + nfl_state = sleeper_api.get_nfl_state() + season = nfl_state.get('season', str(datetime.now().year)) + print(f"DEBUG: Using season: {season}", flush=True) + + # Get user's fantasy leagues + user_id = user['user_id'] + print(f"DEBUG: Calling get_user_leagues() with user_id: '{user_id}', season: '{season}'", flush=True) + leagues = sleeper_api.get_user_leagues(user_id, season) + print(f"DEBUG: Found {len(leagues) if leagues else 0} leagues", flush=True) + + if leagues: + for i, league in enumerate(leagues): + print(f"DEBUG: League {i+1}: ID={league.get('league_id')}, Name='{league.get('name')}'", flush=True) + + # Get NFL game schedule for the week + print(f"DEBUG: Calling get_week_schedule() for week {week}, season {season}", flush=True) + try: + schedule = espn_api.get_week_schedule(week, season) + print(f"DEBUG: Schedule retrieved successfully", flush=True) + except Exception as e: + print(f"ERROR: ESPN schedule fetch failed: {str(e)}", flush=True) + schedule = {} + + # Process each league for matchup data + league_data = [] + for i, league in enumerate(leagues): + print(f"DEBUG: ===== Processing league {i+1}/{len(leagues)}: '{league['name']}' =====", flush=True) + league_id = league['league_id'] + + try: + # Get matchups for the current week + print(f"DEBUG: Calling get_matchups() for league {league_id}, week {week}", flush=True) + matchups = sleeper_api.get_matchups(league_id, week) + print(f"DEBUG: Found {len(matchups) if matchups else 0} matchups", flush=True) + + # Get rosters to find user's team + print(f"DEBUG: Calling get_rosters() for league {league_id}", flush=True) + rosters = sleeper_api.get_rosters(league_id) + print(f"DEBUG: Found {len(rosters) if rosters else 0} rosters", flush=True) + + # Find user's roster in this league + user_roster = next((r for r in rosters if r['owner_id'] == user['user_id']), None) + print(f"DEBUG: User roster found: {user_roster is not None}", flush=True) + + if user_roster and matchups: + # Find user's matchup for this week + user_matchup = next((m for m in matchups if m['roster_id'] == user_roster['roster_id']), None) + print(f"DEBUG: User matchup found: {user_matchup is not None}", flush=True) + + # Find opponent's matchup and user info + opponent_matchup = None + opponent_user = None + if user_matchup: + # Find opponent in same matchup + opponent_matchup = next((m for m in matchups if m['matchup_id'] == user_matchup['matchup_id'] and m['roster_id'] != user_roster['roster_id']), None) + print(f"DEBUG: Opponent matchup found: {opponent_matchup is not None}", flush=True) + + if opponent_matchup: + # Get opponent's roster to find owner + opponent_roster = next((r for r in rosters if r['roster_id'] == opponent_matchup['roster_id']), None) + if opponent_roster: + opponent_owner_id = opponent_roster['owner_id'] + # Fetch opponent's user profile + opponent_user = sleeper_api.get_user_by_id(opponent_owner_id) + print(f"DEBUG: Opponent user: {opponent_user.get('display_name') if opponent_user else 'None'}", flush=True) + + # Get all players on user's roster for calendar + all_players = [] + if user_roster and user_roster.get('players'): + players_list = user_roster['players'] + print(f"DEBUG: Processing {len(players_list)} total players for calendar", flush=True) + + for player_id in players_list: + try: + # Get player details from Sleeper API + player = sleeper_api.get_player_info(player_id) + if player: + all_players.append(player) + except Exception as e: + print(f"ERROR: Failed to get player info for {player_id}: {str(e)}", flush=True) + + # Store processed league data + league_info = { + 'league': league, + 'league_color': get_league_color(i), # Assign color by order + 'user_matchup': user_matchup, + 'opponent_matchup': opponent_matchup, + 'opponent_user': opponent_user, + 'user_roster': user_roster, + 'all_players': all_players + } + league_data.append(league_info) + print(f"DEBUG: League '{league['name']}' processed successfully", flush=True) + else: + print(f"DEBUG: Skipping league '{league['name']}' - missing data", flush=True) + + except Exception as e: + print(f"ERROR: Failed to process league '{league['name']}': {str(e)}", flush=True) + print(f"ERROR: League processing traceback: {traceback.format_exc()}", flush=True) + + # Debug output before rendering template + print(f"DEBUG: About to render template with {len(league_data)} leagues", flush=True) + for league_info in league_data: + print(f"DEBUG: League '{league_info['league']['name']}' has {len(league_info['all_players'])} players", flush=True) + + total_game_count = sum(len(games) for games in schedule.values()) + print(f"DEBUG: Schedule has {total_game_count} total games", flush=True) + + try: + # Render dashboard with all collected data + result = render_template('dashboard.html', + user=user, + week=week, + league_data=league_data, + schedule=schedule, + nfl_state=nfl_state) + print(f"DEBUG: Template rendered successfully", flush=True) + return result + except Exception as e: + print(f"ERROR: Template rendering failed: {str(e)}", flush=True) + print(f"ERROR: Template rendering traceback: {traceback.format_exc()}", flush=True) + return render_template('error.html', message=f"Template error: {str(e)}") + + except Exception as e: + print(f"ERROR: Dashboard function exception: {str(e)}", flush=True) + print(f"ERROR: Full dashboard traceback: {traceback.format_exc()}", flush=True) + return render_template('error.html', + message=f"Error loading data: {str(e)}") + +@app.route('/api/refresh//') +def refresh_data(username, week): + """API endpoint for live score updates""" + print(f"DEBUG: Refresh data API - username: '{username}', week: {week}", flush=True) + try: + # Get user and current season + user = sleeper_api.get_user(username) + if not user: + return jsonify({'error': 'User not found'}), 404 + + nfl_state = sleeper_api.get_nfl_state() + season = nfl_state.get('season', str(datetime.now().year)) + leagues = sleeper_api.get_user_leagues(user['user_id'], season) + league_scores = [] + + # Get updated scores for each league + for league in leagues: + matchups = sleeper_api.get_matchups(league['league_id'], week) + rosters = sleeper_api.get_rosters(league['league_id']) + user_roster = next((r for r in rosters if r['owner_id'] == user['user_id']), None) + + if user_roster and matchups: + # Find user and opponent matchups + user_matchup = next((m for m in matchups if m['roster_id'] == user_roster['roster_id']), None) + opponent_matchup = None + if user_matchup: + opponent_matchup = next((m for m in matchups if m['matchup_id'] == user_matchup['matchup_id'] and m['roster_id'] != user_roster['roster_id']), None) + + # Return current points + league_scores.append({ + 'league_id': league['league_id'], + 'league_name': league['name'], + 'user_points': user_matchup['points'] if user_matchup else 0, + 'opponent_points': opponent_matchup['points'] if opponent_matchup else 0 + }) + + return jsonify({'league_scores': league_scores}) + + except Exception as e: + print(f"ERROR: Refresh data exception: {str(e)}", flush=True) + return jsonify({'error': str(e)}), 500 + +if __name__ == '__main__': + print("DEBUG: Starting Flask app on 0.0.0.0:5000", flush=True) + print(f"DEBUG: FantasyCron {app.config['VERSION']} ready!", flush=True) + app.run(host='0.0.0.0', port=5000, debug=True) diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..5002b06 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,27 @@ +services: + fantasycron: + image: fantasycron:latest # Use pre-built image + ports: + - "5000:5000" + environment: + - SECRET_KEY=your-secret-key-here + - DATABASE_URL=sqlite:///fantasy_app.db + volumes: + - ./data:/app/data + restart: unless-stopped + + # Optional PostgreSQL service + # Uncomment to use PostgreSQL instead of SQLite + # postgres: + # image: postgres:13 + # environment: + # POSTGRES_DB: fantasy_app + # POSTGRES_USER: fantasy_user + # POSTGRES_PASSWORD: fantasy_pass + # volumes: + # - postgres_data:/var/lib/postgresql/data + # ports: + # - "5432:5432" + +# volumes: +# postgres_data: diff --git a/config.py b/config.py new file mode 100644 index 0000000..d31dafc --- /dev/null +++ b/config.py @@ -0,0 +1,20 @@ +import os + +class Config: + # App version + VERSION = '0.1.0' + + SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key' + + # Database settings - support SQLite, PostgreSQL, MariaDB + DATABASE_URL = os.environ.get('DATABASE_URL') or 'sqlite:///fantasy_app.db' + + # API settings + SLEEPER_BASE_URL = 'https://api.sleeper.app/v1' + ESPN_BASE_URL = 'https://site.api.espn.com/apis/site/v2/sports/football/nfl' + + # League colors - assigned in order + LEAGUE_COLORS = [ + '#006400', '#00FFFF', '#FF0000', '#FFD700', '#1E90FF', + '#C71585', '#00FF00', '#00FFFF', '#0000FF', '#1E90FF' + ] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..fa08585 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +Flask==2.3.3 +requests==2.31.0 +python-dateutil==2.8.2 diff --git a/services/__init__.py b/services/__init__.py new file mode 100644 index 0000000..7a40bd0 --- /dev/null +++ b/services/__init__.py @@ -0,0 +1 @@ +# Empty file to make services a package diff --git a/services/espn_api.py b/services/espn_api.py new file mode 100644 index 0000000..c944053 --- /dev/null +++ b/services/espn_api.py @@ -0,0 +1,92 @@ +import requests +from datetime import datetime, timedelta + +class ESPNAPI: + BASE_URL = 'https://site.api.espn.com/apis/site/v2/sports/football/nfl' + + def __init__(self): + self.session = requests.Session() + + def get_week_schedule(self, week, season): + """Get NFL schedule for a specific week""" + try: + print(f"ESPN API: Fetching schedule for week {week}, season {season}", flush=True) + + # Get current NFL scoreboard + url = f"{self.BASE_URL}/scoreboard" + response = self.session.get(url) + response.raise_for_status() + data = response.json() + + print(f"ESPN API: Response status: {response.status_code}", flush=True) + + games = [] + if 'events' in data: + 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') + + # Extract team information + competitors = event['competitions'][0]['competitors'] + + home_team = None + away_team = None + + # Identify home and away teams + for comp in competitors: + team_abbrev = comp['team']['abbreviation'] + if comp['homeAway'] == 'home': + home_team = team_abbrev + else: + away_team = team_abbrev + + print(f"ESPN API: {away_team} @ {home_team} at {game_date}", flush=True) + + # Store game data + games.append({ + 'date': game_date, + 'day_of_week': game_date.strftime('%A'), + 'time': game_date.strftime('%I:%M %p'), + 'home_team': home_team, + 'away_team': away_team, + 'teams': [home_team, away_team] + }) + except Exception as e: + print(f"ESPN API: Error processing event {i+1}: {str(e)}", flush=True) + else: + print("ESPN API: No 'events' key found in response", flush=True) + + print(f"ESPN API: Processed {len(games)} games total", flush=True) + schedule = self._organize_by_day(games) + + # Log games by day + for day, day_games in schedule.items(): + print(f"ESPN API: {day}: {len(day_games)} games", flush=True) + + return schedule + + except Exception as e: + print(f"ESPN API: Error fetching schedule: {e}", flush=True) + return {} + + def _organize_by_day(self, games): + """Organize games by day of week - dynamically include all days with games""" + schedule = {} + + # Group games by day + for game in games: + day = game['day_of_week'] + if day not in schedule: + schedule[day] = [] + schedule[day].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 = {} + + for day in sorted(schedule.keys(), key=lambda x: day_order.get(x, 7)): + sorted_schedule[day] = schedule[day] + + return sorted_schedule diff --git a/services/sleeper_api.py b/services/sleeper_api.py new file mode 100644 index 0000000..dacf9be --- /dev/null +++ b/services/sleeper_api.py @@ -0,0 +1,72 @@ +import requests +from datetime import datetime + +class SleeperAPI: + BASE_URL = 'https://api.sleeper.app/v1' + + def __init__(self): + self.session = requests.Session() + self.players_cache = None # Cache player data + self.players_updated = None # Track last update time + + def get_user(self, username): + """Get user info by username""" + try: + response = self.session.get(f"{self.BASE_URL}/user/{username}") + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException: + return None + + def get_user_by_id(self, user_id): + """Get user info by user_id""" + try: + response = self.session.get(f"{self.BASE_URL}/user/{user_id}") + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException: + return None + + def get_nfl_state(self): + """Get current NFL state (week, season, etc)""" + response = self.session.get(f"{self.BASE_URL}/state/nfl") + response.raise_for_status() + return response.json() + + def get_user_leagues(self, user_id, season): + """Get all leagues for a user in a season""" + response = self.session.get(f"{self.BASE_URL}/user/{user_id}/leagues/nfl/{season}") + response.raise_for_status() + return response.json() + + def get_matchups(self, league_id, week): + """Get matchups for a league and week""" + response = self.session.get(f"{self.BASE_URL}/league/{league_id}/matchups/{week}") + response.raise_for_status() + return response.json() + + def get_rosters(self, league_id): + """Get rosters for a league""" + response = self.session.get(f"{self.BASE_URL}/league/{league_id}/rosters") + response.raise_for_status() + return response.json() + + def get_players(self, force_refresh=False): + """Get all NFL players (cached for 24 hours)""" + now = datetime.now() + + # Check if cache is stale or force refresh requested + if (not self.players_cache or not self.players_updated or + (now - self.players_updated).total_seconds() > 86400 or force_refresh): + + response = self.session.get(f"{self.BASE_URL}/players/nfl") + response.raise_for_status() + self.players_cache = response.json() + self.players_updated = now + + return self.players_cache + + def get_player_info(self, player_id): + """Get specific player info""" + players = self.get_players() + return players.get(str(player_id)) diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..7e3c5cd --- /dev/null +++ b/static/style.css @@ -0,0 +1,509 @@ +/* Reset and base styles */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + line-height: 1.6; + color: #333; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + min-height: 100vh; +} + +.container { + max-width: 1200px; + margin: 0 auto; + padding: 20px; +} + +/* Brand styling */ +.brand { + margin-bottom: 20px; +} + +.app-name { + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + color: #2c3e50; + margin-bottom: 8px; + font-size: 2.2rem; + font-weight: 600; + letter-spacing: -1px; +} + +.tagline { + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + color: #667eea; + font-size: 0.9rem; + font-weight: 500; + opacity: 0.8; +} + +/* Welcome page styles */ +.welcome-container { + text-align: center; + background: white; + padding: 40px; + border-radius: 12px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); + margin-top: 100px; + max-width: 400px; + margin-left: auto; + margin-right: auto; +} + +.username-form { + display: flex; + flex-direction: column; + gap: 15px; + margin: 30px 0; +} + +.username-form input { + padding: 15px; + border: 2px solid #e1e8ed; + border-radius: 8px; + font-size: 16px; + transition: border-color 0.3s; +} + +.username-form input:focus { + outline: none; + border-color: #667eea; +} + +.username-form button, .btn { + background: linear-gradient(45deg, #667eea, #764ba2); + color: white; + padding: 15px 20px; + border: none; + border-radius: 8px; + font-size: 16px; + cursor: pointer; + transition: transform 0.2s; + text-decoration: none; + display: inline-block; + text-align: center; +} + +.username-form button:hover, .btn:hover { + transform: translateY(-2px); +} + +.example { + color: #666; + font-size: 14px; +} + +.example code { + background: #f8f9fa; + padding: 2px 6px; + border-radius: 4px; + font-family: monospace; +} + +/* Dashboard header */ +.dashboard-header { + background: white; + padding: 20px; + border-radius: 12px; + margin-bottom: 20px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1); + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 15px; +} + +.dashboard-header h1 { + color: #2c3e50; + font-size: 1.8rem; +} + +.week-nav { + display: flex; + align-items: center; + gap: 15px; +} + +.week-btn { + background: #f8f9fa; + color: #495057; + padding: 8px 16px; + border-radius: 6px; + text-decoration: none; + transition: background 0.2s; +} + +.week-btn:hover { + background: #e9ecef; +} + +.current-week { + font-weight: bold; + color: #667eea; + font-size: 1.1rem; +} + +/* League scores - compact at top */ +.scores-summary { + background: white; + border-radius: 12px; + padding: 15px; + margin-bottom: 30px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1); +} + +.score-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 0; + border-bottom: 1px solid #f1f3f4; +} + +.score-row:last-child { + border-bottom: none; +} + +.league-info { + display: flex; + align-items: center; + gap: 8px; + min-width: 140px; +} + +.league-name { + font-weight: 600; + color: #2c3e50; + font-size: 14px; +} + +/* League color dots */ +.league-dot { + width: 8px; + height: 8px; + border-radius: 50%; + display: inline-block; + flex-shrink: 0; +} + +/* Different sizes for different contexts */ +.league-info .league-dot { + width: 10px; + height: 10px; +} + +.player-pill .league-dot { + width: 6px; + height: 6px; +} + +.score-compact { + display: flex; + align-items: center; + gap: 8px; + font-size: 13px; +} + +.user-name, .opp-name { + font-weight: 500; + color: #495057; + min-width: 80px; +} + +.user-name { + text-align: right; +} + +.score { + font-weight: bold; + color: #667eea; + min-width: 30px; + text-align: center; +} + +.vs-compact { + color: #666; + font-size: 11px; + font-weight: 500; +} + +/* Schedule section - main focus */ +.schedule-section { + flex: 1; +} + +.schedule-section h2 { + color: white; + margin-bottom: 25px; + font-size: 1.8rem; + text-align: center; +} + +.calendar-rows { + max-width: 1200px; + margin: 0 auto; +} + +/* Day row styling */ +.day-row { + background: white; + border-radius: 16px; + margin-bottom: 20px; + box-shadow: 0 6px 24px rgba(0, 0, 0, 0.1); + overflow: hidden; +} + +.day-header { + background: linear-gradient(45deg, #667eea, #764ba2); + color: white; + padding: 15px 20px; +} + +.day-header h3 { + margin: 0; + font-size: 1.2rem; + font-weight: 600; +} + +.day-games { + padding: 20px; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 15px; +} + +/* Game card styling */ +.game-card { + background: #f8f9fa; + border-radius: 12px; + padding: 15px; + border: 1px solid #e9ecef; + transition: all 0.2s ease; +} + +.game-card:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); +} + +.game-info { + margin-bottom: 12px; +} + +.game-time { + font-size: 12px; + color: #667eea; + margin-bottom: 8px; + font-weight: 600; + text-align: center; +} + +.matchup { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + font-size: 14px; + font-weight: 600; +} + +.away-team, .home-team { + color: #2c3e50; +} + +.at { + color: #666; + font-size: 12px; +} + +/* Player pills in calendar */ +.game-players { + display: flex; + flex-wrap: wrap; + gap: 6px; + justify-content: center; + margin-top: 12px; + min-height: 24px; +} + +.player-pill { + display: flex; + align-items: center; + gap: 4px; + padding: 4px 8px; + border-radius: 16px; + font-size: 11px; + border: 2px solid #dee2e6; + background: white; + font-weight: 500; + transition: all 0.2s ease; +} + +.player-pill:hover { + transform: scale(1.05); +} + +.player-pill .pos { + font-weight: bold; + font-size: 9px; + opacity: 0.8; +} + +.player-pill .name { + font-weight: 600; +} + +/* Position colors for pills */ +.player-pill.qb { + border-color: rgb(252, 43, 109); + background: rgba(252, 43, 109, 0.1); + color: rgb(252, 43, 109); +} + +.player-pill.rb { + border-color: rgb(0, 206, 184); + background: rgba(0, 206, 184, 0.1); + color: rgb(0, 206, 184); +} + +.player-pill.wr { + border-color: rgb(0, 186, 255); + background: rgba(0, 186, 255, 0.1); + color: rgb(0, 186, 255); +} + +.player-pill.te { + border-color: rgb(255, 174, 88); + background: rgba(255, 174, 88, 0.1); + color: rgb(255, 174, 88); +} + +.no-games-week { + text-align: center; + color: white; + font-style: italic; + font-size: 16px; + padding: 60px 0; + background: rgba(255, 255, 255, 0.1); + border-radius: 12px; + margin: 20px 0; +} + +/* App footer */ +.app-footer { + text-align: center; + padding: 20px; + margin-top: 40px; +} + +.version { + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 11px; + color: rgba(255, 255, 255, 0.6); + opacity: 0.8; +} + +/* Error page */ +.error-container { + text-align: center; + background: white; + padding: 40px; + border-radius: 12px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); + margin-top: 100px; + max-width: 500px; + margin-left: auto; + margin-right: auto; +} + +.error-container h1 { + color: #e74c3c; + margin-bottom: 20px; +} + +.error-container p { + color: #666; + margin-bottom: 30px; + line-height: 1.6; +} + +/* Mobile responsive styles */ +@media (max-width: 768px) { + .container { + padding: 15px; + } + + .dashboard-header { + flex-direction: column; + text-align: center; + } + + .scores-summary { + padding: 12px; + } + + .score-row { + flex-direction: column; + gap: 8px; + align-items: flex-start; + padding: 12px 0; + } + + .league-info { + min-width: auto; + } + + .score-compact { + align-self: stretch; + justify-content: space-between; + } + + .day-games { + grid-template-columns: 1fr; + gap: 12px; + padding: 15px; + } + + .day-header { + padding: 12px 15px; + } + + .day-header h3 { + font-size: 1.1rem; + } + + .schedule-section h2 { + font-size: 1.5rem; + } + + .welcome-container { + margin-top: 50px; + padding: 30px 20px; + } + + .app-name { + font-size: 1.6rem; + } +} + +@media (max-width: 480px) { + .username-form { + gap: 12px; + } + + .username-form input, + .username-form button { + padding: 12px; + font-size: 14px; + } + + .week-nav { + flex-direction: column; + gap: 10px; + } +} diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..53c8fd3 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,57 @@ + + + + + + {% block title %}FantasyCron{% endblock %} + + + +
+ {% block content %}{% endblock %} +
+ + +
+
FantasyCron v{{ app_version }}
+
+ + + + diff --git a/templates/dashboard.html b/templates/dashboard.html new file mode 100644 index 0000000..302bba3 --- /dev/null +++ b/templates/dashboard.html @@ -0,0 +1,102 @@ +{% extends "base.html" %} + +{% block title %}{{ user.display_name }} - Week {{ week }}{% endblock %} + +{% block content %} + + + + + +
+

{{ user.display_name }}

+ +
+ + +
+ {% for league_info in league_data %} +
+
+ + + {{ league_info.league.name }} +
+
+ {{ user.display_name }} + + {{ league_info.user_matchup.points|round(1) if league_info.user_matchup else '0.0' }} + + vs + + {{ league_info.opponent_matchup.points|round(1) if league_info.opponent_matchup else '0.0' }} + + {{ league_info.opponent_user.display_name if league_info.opponent_user else 'Opponent' }} +
+
+ {% endfor %} +
+ + +
+

Week {{ week }} Games

+
+ + {% set has_games = false %} + {% for day, games in schedule.items() if games %} + {% set has_games = true %} + {% endfor %} + + + {% for day, games in schedule.items() if games %} +
+
+

{{ day }}

+
+
+ + {% for game in games %} +
+
+
{{ game.time }}
+
+ {{ game.away_team }} + @ + {{ game.home_team }} +
+
+ + +
+ {% for league_info in league_data %} + {% for player in league_info.all_players %} + {% if player.team in game.teams %} +
+ + + {{ player.fantasy_positions[0] if player.fantasy_positions else 'FLEX' }} + {{ player.last_name }} +
+ {% endif %} + {% endfor %} + {% endfor %} +
+
+ {% endfor %} +
+
+ {% endfor %} + + + {% if not has_games %} +
+

No games found for week {{ week }}

+
+ {% endif %} +
+
+{% endblock %} diff --git a/templates/error.html b/templates/error.html new file mode 100644 index 0000000..112e4bf --- /dev/null +++ b/templates/error.html @@ -0,0 +1,11 @@ +{% extends "base.html" %} + +{% block title %}Error{% endblock %} + +{% block content %} +
+

Error

+

{{ message }}

+ Back to Home +
+{% endblock %} diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..3301585 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,33 @@ +{% extends "base.html" %} + +{% block title %}FantasyCron - Enter Username{% endblock %} + +{% block content %} +
+
+

FantasyCron

+
* * * * 0,1,4
+
+

Enter your Sleeper username to view your fantasy teams

+ + +
+ + +
+ +
+

Example: sleeperuser

+
+
+ + +{% endblock %}