init
This commit is contained in:
commit
0718fdd01f
13 changed files with 1197 additions and 0 deletions
18
ContainerFile
Normal file
18
ContainerFile
Normal file
|
|
@ -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"]
|
||||
252
app.py
Normal file
252
app.py
Normal file
|
|
@ -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('/<username>')
|
||||
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('/<username>/<int:week>')
|
||||
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/<username>/<int:week>')
|
||||
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)
|
||||
27
compose.yaml
Normal file
27
compose.yaml
Normal file
|
|
@ -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:
|
||||
20
config.py
Normal file
20
config.py
Normal file
|
|
@ -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'
|
||||
]
|
||||
3
requirements.txt
Normal file
3
requirements.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
Flask==2.3.3
|
||||
requests==2.31.0
|
||||
python-dateutil==2.8.2
|
||||
1
services/__init__.py
Normal file
1
services/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
# Empty file to make services a package
|
||||
92
services/espn_api.py
Normal file
92
services/espn_api.py
Normal file
|
|
@ -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
|
||||
72
services/sleeper_api.py
Normal file
72
services/sleeper_api.py
Normal file
|
|
@ -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))
|
||||
509
static/style.css
Normal file
509
static/style.css
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
57
templates/base.html
Normal file
57
templates/base.html
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}FantasyCron{% endblock %}</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
|
||||
<!-- App version footer -->
|
||||
<footer class="app-footer">
|
||||
<div class="version">FantasyCron v{{ app_version }}</div>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
function refreshScores() {
|
||||
// Get current page info
|
||||
const username = document.querySelector('meta[name="username"]')?.content;
|
||||
const week = document.querySelector('meta[name="week"]')?.content;
|
||||
|
||||
if (username && week) {
|
||||
// Fetch updated scores from API
|
||||
fetch(`/api/refresh/${username}/${week}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.league_scores) {
|
||||
// Update score displays
|
||||
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 {
|
||||
setInterval(refreshScores, 21600000); // Every 6 hours otherwise
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
102
templates/dashboard.html
Normal file
102
templates/dashboard.html
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ user.display_name }} - Week {{ week }}{% endblock %}
|
||||
|
||||
{% 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 class="dashboard-header">
|
||||
<h1>{{ user.display_name }}</h1>
|
||||
<div class="week-nav">
|
||||
<a href="/{{ user.username }}/{{ week - 1 }}" class="week-btn">← Week {{ week - 1 }}</a>
|
||||
<span class="current-week">Week {{ week }}</span>
|
||||
<a href="/{{ user.username }}/{{ week + 1 }}" class="week-btn">Week {{ week + 1 }} →</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Compact league scores at top -->
|
||||
<section class="scores-summary">
|
||||
{% for league_info in league_data %}
|
||||
<div class="score-row">
|
||||
<div class="league-info">
|
||||
<!-- League color dot -->
|
||||
<span class="league-dot" style="background-color: {{ league_info.league_color }};"></span>
|
||||
<span class="league-name">{{ league_info.league.name }}</span>
|
||||
</div>
|
||||
<div class="score-compact">
|
||||
<span class="user-name">{{ user.display_name }}</span>
|
||||
<span class="score" id="user-score-{{ league_info.league.league_id }}">
|
||||
{{ league_info.user_matchup.points|round(1) if league_info.user_matchup else '0.0' }}
|
||||
</span>
|
||||
<span class="vs-compact">vs</span>
|
||||
<span class="score" id="opp-score-{{ league_info.league.league_id }}">
|
||||
{{ league_info.opponent_matchup.points|round(1) if league_info.opponent_matchup else '0.0' }}
|
||||
</span>
|
||||
<span class="opp-name">{{ league_info.opponent_user.display_name if league_info.opponent_user else 'Opponent' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</section>
|
||||
|
||||
<!-- Main calendar section -->
|
||||
<section class="schedule-section">
|
||||
<h2>Week {{ week }} Games</h2>
|
||||
<div class="calendar-rows">
|
||||
<!-- Check if schedule has games -->
|
||||
{% set has_games = false %}
|
||||
{% for day, games in schedule.items() if games %}
|
||||
{% set has_games = true %}
|
||||
{% endfor %}
|
||||
|
||||
<!-- Loop through days that have games -->
|
||||
{% for day, games in schedule.items() if games %}
|
||||
<div class="day-row">
|
||||
<div class="day-header">
|
||||
<h3>{{ day }}</h3>
|
||||
</div>
|
||||
<div class="day-games">
|
||||
<!-- Games for this day -->
|
||||
{% for game in games %}
|
||||
<div class="game-card">
|
||||
<div class="game-info">
|
||||
<div class="game-time">{{ game.time }}</div>
|
||||
<div class="matchup">
|
||||
<span class="away-team">{{ game.away_team }}</span>
|
||||
<span class="at">@</span>
|
||||
<span class="home-team">{{ game.home_team }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Show user's players in this game -->
|
||||
<div class="game-players">
|
||||
{% for league_info in league_data %}
|
||||
{% for player in league_info.all_players %}
|
||||
{% if player.team in game.teams %}
|
||||
<div class="player-pill {{ player.fantasy_positions[0]|lower if player.fantasy_positions else 'flex' }}">
|
||||
<!-- League color dot -->
|
||||
<span class="league-dot" style="background-color: {{ league_info.league_color }};"></span>
|
||||
<span class="pos">{{ player.fantasy_positions[0] if player.fantasy_positions else 'FLEX' }}</span>
|
||||
<span class="name">{{ player.last_name }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<!-- Show message if no games found -->
|
||||
{% if not has_games %}
|
||||
<div class="no-games-week">
|
||||
<p>No games found for week {{ week }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
11
templates/error.html
Normal file
11
templates/error.html
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Error{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="error-container">
|
||||
<h1>Error</h1>
|
||||
<p>{{ message }}</p>
|
||||
<a href="/" class="btn">Back to Home</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
33
templates/index.html
Normal file
33
templates/index.html
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}FantasyCron - Enter Username{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="welcome-container">
|
||||
<div class="brand">
|
||||
<h1 class="app-name">FantasyCron</h1>
|
||||
<div class="tagline">* * * * 0,1,4</div>
|
||||
</div>
|
||||
<p>Enter your Sleeper username to view your fantasy teams</p>
|
||||
|
||||
<!-- Username input form -->
|
||||
<form class="username-form" onsubmit="goToUser(event)">
|
||||
<input type="text" id="username" placeholder="Enter Sleeper username" required>
|
||||
<button type="submit">View Dashboard</button>
|
||||
</form>
|
||||
|
||||
<div class="example">
|
||||
<p>Example: <code>sleeperuser</code></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function goToUser(event) {
|
||||
event.preventDefault(); // Prevent form submission
|
||||
const username = document.getElementById('username').value.trim();
|
||||
if (username) {
|
||||
window.location.href = `/${username}`; // Navigate to user dashboard
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
Loading…
Reference in a new issue