squash this
This commit is contained in:
parent
8aa400eef6
commit
727b5bf83c
13 changed files with 317 additions and 15 deletions
102
README.md
Normal file
102
README.md
Normal 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*
|
||||
|
||||
[](https://python.org)
|
||||
[](https://flask.palletsprojects.com/)
|
||||
[](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
17
app.py
|
|
@ -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
BIN
gametime_logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 190 KiB |
BIN
services/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
services/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
services/__pycache__/espn_api.cpython-313.pyc
Normal file
BIN
services/__pycache__/espn_api.cpython-313.pyc
Normal file
Binary file not shown.
BIN
services/__pycache__/sleeper_api.cpython-313.pyc
Normal file
BIN
services/__pycache__/sleeper_api.cpython-313.pyc
Normal file
Binary file not shown.
|
|
@ -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
21
static/manifest.json
Normal 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"
|
||||
}
|
||||
|
|
@ -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
93
static/sw.js
Normal 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;
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 }}">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue