squash this

This commit is contained in:
efigueroa 2025-09-07 12:52:42 -07:00
parent 8aa400eef6
commit 727b5bf83c
13 changed files with 317 additions and 15 deletions

102
README.md Normal file
View file

@ -0,0 +1,102 @@
# <div align="center"><img src="gametime_logo.png" alt="GameTime Logo" width="200"/></div>
<div align="center">
# 🏈 GameTime
**Your Fantasy Football Command Center**
*Track all your leagues, matchups, and players in one sleek dashboard*
[![Python](https://img.shields.io/badge/Python-3.8+-blue.svg)](https://python.org)
[![Flask](https://img.shields.io/badge/Flask-2.3.3-green.svg)](https://flask.palletsprojects.com/)
[![Docker](https://img.shields.io/badge/Docker-Ready-blue.svg)](https://docker.com)
</div>
## 🚀 What is GameTime?
GameTime is your all-in-one fantasy football dashboard that connects to Sleeper and ESPN to give you the ultimate game day experience. See all your leagues, matchups, and live scores in one beautiful interface.
### ✨ Features
- 📊 **Multi-League Dashboard** - View all your Sleeper leagues at once
- 🔴 **Live Score Updates** - Real-time score refreshes during game day
- 📅 **NFL Schedule Integration** - See when your players are playing
- 🌙 **Dark/Light Theme** - Switch themes for day or night viewing
- 📱 **Mobile Responsive** - Looks great on all devices
- 🐳 **Docker Ready** - Easy deployment with containers
## 🏃‍♂️ Quick Start
### Option 1: Local Development
```bash
# Install dependencies
pip install -r requirements.txt
# Run the app
python app.py
# Visit http://localhost:5000
```
### Option 2: Docker
```bash
# Build and run
docker-compose up
# Or run detached
docker-compose up -d
```
## 🎮 How to Use
1. **Enter your Sleeper username** on the homepage
2. **View your dashboard** - see all leagues and current week matchups
3. **Navigate weeks** - check past weeks or look ahead
4. **Live updates** - scores refresh automatically during games
5. **Theme switching** - toggle between light and dark modes
## 🛠 Tech Stack
- **Backend**: Flask (Python)
- **APIs**: Sleeper API, ESPN API
- **Frontend**: HTML, CSS, JavaScript
- **Deployment**: Docker + Docker Compose
## 📁 Project Structure
```
gametime/
├── app.py # Main Flask application
├── config.py # App configuration
├── services/ # API integrations
│ ├── sleeper_api.py # Sleeper API client
│ └── espn_api.py # ESPN API client
├── templates/ # HTML templates
├── static/ # CSS and assets
└── ContainerFile # Docker build config
```
## 🤝 Contributing
Got ideas for GameTime? We'd love to hear them!
1. Fork the repo
2. Create a feature branch
3. Make your changes
4. Submit a pull request
## 📜 License
This project is open source and available under the MIT License.
---
<div align="center">
**Ready for GameTime?** 🏈
*Built with ❤️ for fantasy football fanatics*
</div>

17
app.py
View file

@ -1,4 +1,4 @@
from flask import Flask, render_template, jsonify, request, session, redirect, url_for
from flask import Flask, render_template, jsonify, request, session, redirect, url_for, send_file
from datetime import datetime, timedelta
import os
import sys
@ -59,7 +59,7 @@ def inject_apis():
return dict(
sleeper_api=sleeper_api,
app_version=app.config['VERSION'],
current_theme=session.get('theme', 'light')
current_theme=session.get('theme', 'dark')
)
@app.route('/')
@ -81,7 +81,7 @@ def dashboard_form():
@app.route('/toggle_theme', methods=['POST'])
def toggle_theme():
"""Toggle between light and dark theme"""
current_theme = session.get('theme', 'light')
current_theme = session.get('theme', 'dark')
new_theme = 'dark' if current_theme == 'light' else 'light'
session['theme'] = new_theme
@ -359,6 +359,17 @@ def refresh_data(username, week):
print(f"ERROR: Refresh data exception: {str(e)}", flush=True)
return jsonify({'error': str(e)}), 500
# Static file routes
@app.route('/gametime_logo.png')
def logo():
"""Serve the logo file"""
return send_file('gametime_logo.png', mimetype='image/png')
@app.route('/favicon.ico')
def favicon():
"""Serve the favicon"""
return send_file('gametime_logo.png', mimetype='image/png')
if __name__ == '__main__':
print("DEBUG: Starting Flask app on 0.0.0.0:5000", flush=True)
print(f"DEBUG: GameTime {app.config['VERSION']} ready!", flush=True)

BIN
gametime_logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -76,8 +76,15 @@ class ESPNAPI:
else:
tz_abbr = 'Local'
# Extract team information
competitors = event['competitions'][0]['competitors']
# Extract team information and game status
competition = event['competitions'][0]
competitors = competition['competitors']
# Get game status information
status = competition.get('status', {})
game_state = status.get('type', {}).get('state', 'pre') # pre, in, post
is_live = game_state == 'in'
status_display = status.get('type', {}).get('name', 'Scheduled')
home_team = None
away_team = None
@ -102,7 +109,10 @@ class ESPNAPI:
'home_team': home_team,
'away_team': away_team,
'teams': [home_team, away_team],
'is_past': game_date_local < datetime.now(game_date_local.tzinfo)
'is_past': game_date_local < datetime.now(game_date_local.tzinfo),
'is_live': is_live,
'game_state': game_state,
'status_display': status_display
})
except Exception as e:
print(f"ESPN API: Error processing event {i+1}: {str(e)}", flush=True)

21
static/manifest.json Normal file
View file

@ -0,0 +1,21 @@
{
"name": "GameTime - Fantasy Football Dashboard",
"short_name": "GameTime",
"description": "View your Sleeper fantasy football leagues and player schedules",
"start_url": "/",
"display": "standalone",
"background_color": "#1e1e2e",
"theme_color": "#8b7ff5",
"orientation": "portrait-primary",
"icons": [
{
"src": "/gametime_logo.png",
"sizes": "any",
"type": "image/png",
"purpose": "any maskable"
}
],
"categories": ["sports", "games"],
"scope": "/",
"lang": "en"
}

View file

@ -72,6 +72,14 @@
margin-bottom: 20px;
}
.app-logo {
max-width: 120px;
height: auto;
margin-bottom: 15px;
border-radius: 12px;
box-shadow: var(--shadow-sm);
}
.app-name {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
color: var(--text-primary);
@ -1337,4 +1345,34 @@ body {
opacity: 0.8;
}
/* Live game indicator */
.live-indicator {
display: inline-block;
width: 8px;
height: 8px;
background-color: #ff0000;
border-radius: 50%;
margin-left: 8px;
margin-right: 6px;
animation: pulse-red 1.5s ease-in-out infinite;
}
.live-text {
color: #ff0000;
font-weight: bold;
font-size: 11px;
letter-spacing: 0.5px;
}
@keyframes pulse-red {
0%, 100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.3;
transform: scale(1.2);
}
}

93
static/sw.js Normal file
View file

@ -0,0 +1,93 @@
// GameTime Service Worker
const CACHE_NAME = 'gametime-v1.0.0';
const STATIC_CACHE = [
'/',
'/static/style.css',
'/static/manifest.json',
'/gametime_logo.png',
'/favicon.ico'
];
// Install event - cache static resources
self.addEventListener('install', event => {
console.log('GameTime SW: Installing...');
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => {
console.log('GameTime SW: Caching static resources');
return cache.addAll(STATIC_CACHE);
})
.then(() => self.skipWaiting())
);
});
// Activate event - clean up old caches
self.addEventListener('activate', event => {
console.log('GameTime SW: Activating...');
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (cacheName !== CACHE_NAME) {
console.log('GameTime SW: Deleting old cache:', cacheName);
return caches.delete(cacheName);
}
})
);
}).then(() => self.clients.claim())
);
});
// Fetch event - network first, fallback to cache for static resources
self.addEventListener('fetch', event => {
const { request } = event;
// Skip non-GET requests
if (request.method !== 'GET') return;
// For API calls and dynamic content, use network first
if (request.url.includes('/api/') ||
request.url.includes('/dashboard') ||
request.url.includes('/refresh')) {
event.respondWith(
fetch(request)
.catch(() => {
// If offline, show a basic offline page
if (request.destination === 'document') {
return new Response(`
<html>
<head>
<title>GameTime - Offline</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body style="font-family: sans-serif; text-align: center; padding: 50px;">
<h1>You're offline</h1>
<p>Please check your internet connection and try again.</p>
<button onclick="window.location.reload()">Retry</button>
</body>
</html>
`, {
headers: { 'Content-Type': 'text/html' }
});
}
})
);
} else {
// For static resources, cache first
event.respondWith(
caches.match(request)
.then(response => {
return response || fetch(request)
.then(response => {
// Cache successful responses
if (response.status === 200) {
const responseClone = response.clone();
caches.open(CACHE_NAME)
.then(cache => cache.put(request, responseClone));
}
return response;
});
})
);
}
});

View file

@ -5,6 +5,12 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}GameTime{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<link rel="icon" type="image/png" href="/favicon.ico">
<link rel="apple-touch-icon" href="/gametime_logo.png">
<link rel="manifest" href="{{ url_for('static', filename='manifest.json') }}">
<meta name="theme-color" content="#8b7ff5">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
</head>
<body>
<!-- Hamburger Menu Button - Uses CSS-only solution -->
@ -42,13 +48,13 @@
🏈 Sleeper App
</a>
<a href="https://github.com" target="_blank" class="sidebar-link">
💻 GitHub
💻 Source Code
</a>
</div>
<!-- About Section -->
<div class="sidebar-section">
<h3>About This App</h3>
<h3>About This WebApp</h3>
<div class="about-text">
<p>GameTime gives you an at-a-glance view of your Sleeper leagues player's schedules and status.</p>
<p><strong>How to use:</strong></p>
@ -57,8 +63,9 @@
<li>View all your leagues' matchup scores at the top</li>
<li>See when each of your players plays, grouped by league (indicated by colored borders)</li>
<li>Benched players are right-aligned and greyed out</li>
<li>Games and Days are collapsed if they're over.</li>
</ul>
<p class="cron-note">* * * * 0,1,4</p>
<p class="cron-note">* * * * 4,7,1</p>
</div>
</div>
</div>
@ -165,6 +172,19 @@
gameContent.style.display = 'none';
});
});
// Register service worker for PWA functionality
if ('serviceWorker' in navigator) {
window.addEventListener('load', function() {
navigator.serviceWorker.register('/static/sw.js')
.then(function(registration) {
console.log('GameTime SW: Registered successfully');
})
.catch(function(error) {
console.log('GameTime SW: Registration failed:', error);
});
});
}
</script>
</body>
</html>

View file

@ -84,16 +84,22 @@
<div class="day-games" id="day-{{ day_key }}">
<!-- Games for this day -->
{% for game in day_info.games %}
<div class="game-card {% if game.is_past %}collapsed{% endif %}" data-game="{{ loop.index }}">
<div class="game-card {% if game.is_past and not game.is_live %}collapsed{% endif %}" data-game="{{ loop.index }}">
<div class="game-header" onclick="toggleGame('{{ day_key }}', '{{ loop.index }}')">
<div class="game-info">
<div class="game-time">{{ game.time }}</div>
<div class="game-time">
{{ game.time }}
{% if game.is_live %}
<span class="live-indicator"></span>
<span class="live-text">LIVE</span>
{% endif %}
</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>
<span class="game-collapse-indicator">{% if game.is_past %}▶{% else %}▼{% endif %}</span>
<span class="game-collapse-indicator">{% if game.is_past and not game.is_live %}▶{% else %}▼{% endif %}</span>
</div>
</div>
<div class="game-content" id="game-{{ day_key }}-{{ loop.index }}">

View file

@ -5,10 +5,11 @@
{% block content %}
<div class="welcome-container">
<div class="brand">
<img src="/gametime_logo.png" alt="GameTime Logo" class="app-logo">
<h1 class="app-name">GameTime</h1>
<div class="tagline">* * * * 0,1,4</div>
<div class="tagline">* * * * 4,7,1</div>
</div>
<p>Enter your Sleeper username to view your fantasy teams</p>
<p>Enter your Sleeper username to get your league schedules.</p>
<!-- Username input form -->
<form class="username-form" method="get" action="/dashboard">
@ -17,7 +18,7 @@
</form>
<div class="example">
<p>Example: <code>sleeperuser</code></p>
<p>Example: <code>@mySleeperUsername</code></p>
</div>
</div>