Compare commits
7 commits
ft-live-sc
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72981d5e8a | ||
|
|
6115b557c5 | ||
|
|
ca702d6bcc | ||
|
|
f4ad413eac | ||
|
|
6d21444746 | ||
|
|
2ae44f64b5 | ||
|
|
763277ca8a |
12 changed files with 1059 additions and 86 deletions
|
|
@ -1,16 +1,16 @@
|
||||||
|
|
||||||
# <div align="center"><img src="rosterhash_logo.png" alt="RosterHash Logo" width="200"/></div>
|
# <div align="center"><img src="rosterhash_logo.png" alt="RosterHash Logo" width="200"/></div>
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
|
[](https://quay.io/repository/eddiefigsystems/rosterhash)
|
||||||
|
|
||||||
# 🏈 RosterHash
|
# 🏈 RosterHash
|
||||||
|
|
||||||
**Your Fantasy Football Command Center**
|
**Your Fantasy Football Command Center**
|
||||||
|
|
||||||
*Track all your leagues, matchups, and players in one dashboard*
|
*Track all your leagues, matchups, and players in one dashboard*
|
||||||
|
|
||||||
[](https://python.org)
|
|
||||||
[](https://flask.palletsprojects.com/)
|
|
||||||
[](https://docker.com)
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
4
app.py
4
app.py
|
|
@ -6,6 +6,7 @@ import traceback
|
||||||
from services.sleeper_api import SleeperAPI
|
from services.sleeper_api import SleeperAPI
|
||||||
from services.espn_api import ESPNAPI
|
from services.espn_api import ESPNAPI
|
||||||
from config import Config
|
from config import Config
|
||||||
|
from faker import Faker
|
||||||
|
|
||||||
# Force unbuffered output for Docker logs
|
# Force unbuffered output for Docker logs
|
||||||
os.environ['PYTHONUNBUFFERED'] = '1'
|
os.environ['PYTHONUNBUFFERED'] = '1'
|
||||||
|
|
@ -87,6 +88,7 @@ def index():
|
||||||
print("DEBUG: Index route accessed", flush=True)
|
print("DEBUG: Index route accessed", flush=True)
|
||||||
# Get the last used username from cookie
|
# Get the last used username from cookie
|
||||||
last_username = request.cookies.get('last_username', '')
|
last_username = request.cookies.get('last_username', '')
|
||||||
|
mySleeperUsername = Faker().user_name()
|
||||||
print(f"DEBUG: Last username from cookie: '{last_username}'", flush=True)
|
print(f"DEBUG: Last username from cookie: '{last_username}'", flush=True)
|
||||||
|
|
||||||
# Auto-redirect to dashboard if username cookie exists
|
# Auto-redirect to dashboard if username cookie exists
|
||||||
|
|
@ -94,7 +96,7 @@ def index():
|
||||||
print(f"DEBUG: Auto-redirecting to dashboard for user: '{last_username}'", flush=True)
|
print(f"DEBUG: Auto-redirecting to dashboard for user: '{last_username}'", flush=True)
|
||||||
return redirect(url_for('dashboard_current', username=last_username.strip()))
|
return redirect(url_for('dashboard_current', username=last_username.strip()))
|
||||||
|
|
||||||
return render_template('index.html', last_username=last_username)
|
return render_template('index.html', last_username=last_username, mySleeperUsername=mySleeperUsername)
|
||||||
|
|
||||||
@app.route('/dashboard')
|
@app.route('/dashboard')
|
||||||
def dashboard_form():
|
def dashboard_form():
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
services:
|
services:
|
||||||
rosterhash:
|
rosterhash:
|
||||||
image: rosterhash:latest
|
image: quay.io/eddiefigsystems/rosterhash:latest
|
||||||
ports:
|
ports:
|
||||||
- "5000:5000"
|
- "5000:5000"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import os
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
# App version
|
# App version
|
||||||
VERSION = '1.2.0'
|
VERSION = '1.2.1'
|
||||||
|
|
||||||
SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key'
|
SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
Flask==2.3.3
|
Flask==2.3.3
|
||||||
requests==2.31.0
|
requests==2.31.0
|
||||||
python-dateutil==2.8.2
|
python-dateutil==2.8.2
|
||||||
|
Faker==37.12.0
|
||||||
|
|
|
||||||
|
|
@ -148,16 +148,23 @@ class ESPNAPI:
|
||||||
|
|
||||||
home_team = None
|
home_team = None
|
||||||
away_team = None
|
away_team = None
|
||||||
|
home_score = None
|
||||||
|
away_score = None
|
||||||
|
|
||||||
# Identify home and away teams
|
# Identify home and away teams and extract scores
|
||||||
for comp in competitors:
|
for comp in competitors:
|
||||||
team_abbrev = comp['team']['abbreviation']
|
team_abbrev = comp['team']['abbreviation']
|
||||||
# Normalize team abbreviation to match Sleeper format
|
# Normalize team abbreviation to match Sleeper format
|
||||||
normalized_team = self.normalize_team_abbreviation(team_abbrev)
|
normalized_team = self.normalize_team_abbreviation(team_abbrev)
|
||||||
|
# Get score if available
|
||||||
|
score = comp.get('score')
|
||||||
|
|
||||||
if comp['homeAway'] == 'home':
|
if comp['homeAway'] == 'home':
|
||||||
home_team = normalized_team
|
home_team = normalized_team
|
||||||
|
home_score = score
|
||||||
else:
|
else:
|
||||||
away_team = normalized_team
|
away_team = normalized_team
|
||||||
|
away_score = score
|
||||||
|
|
||||||
print(f"ESPN API: {away_team} @ {home_team} at {game_date_local.strftime('%I:%M %p')} {tz_abbr}", flush=True)
|
print(f"ESPN API: {away_team} @ {home_team} at {game_date_local.strftime('%I:%M %p')} {tz_abbr}", flush=True)
|
||||||
|
|
||||||
|
|
@ -170,6 +177,8 @@ class ESPNAPI:
|
||||||
'time': f"{game_date_local.strftime('%I:%M %p')} {tz_abbr}",
|
'time': f"{game_date_local.strftime('%I:%M %p')} {tz_abbr}",
|
||||||
'home_team': home_team,
|
'home_team': home_team,
|
||||||
'away_team': away_team,
|
'away_team': away_team,
|
||||||
|
'home_score': home_score,
|
||||||
|
'away_score': away_score,
|
||||||
'teams': [home_team, away_team],
|
'teams': [home_team, away_team],
|
||||||
'is_past': game_date_local < (debug_current_time.astimezone(game_date_local.tzinfo) if debug_current_time else datetime.now(game_date_local.tzinfo)),
|
'is_past': game_date_local < (debug_current_time.astimezone(game_date_local.tzinfo) if debug_current_time else datetime.now(game_date_local.tzinfo)),
|
||||||
'is_live': is_live,
|
'is_live': is_live,
|
||||||
|
|
|
||||||
211
static/easter-eggs.js
Normal file
211
static/easter-eggs.js
Normal file
|
|
@ -0,0 +1,211 @@
|
||||||
|
// Easter Eggs for RosterHash
|
||||||
|
// Date-based theme modifications
|
||||||
|
|
||||||
|
class EasterEggs {
|
||||||
|
constructor() {
|
||||||
|
this.today = new Date();
|
||||||
|
this.month = this.today.getMonth() + 1; // 1-12
|
||||||
|
this.day = this.today.getDate();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if today is Thanksgiving (4th Thursday of November)
|
||||||
|
isThanksgiving() {
|
||||||
|
if (this.month !== 11) return false; // Not November
|
||||||
|
|
||||||
|
const year = this.today.getFullYear();
|
||||||
|
let thursdayCount = 0;
|
||||||
|
|
||||||
|
// Count Thursdays in November
|
||||||
|
for (let day = 1; day <= 30; day++) {
|
||||||
|
const date = new Date(year, 10, day); // Month 10 = November
|
||||||
|
if (date.getDay() === 4) { // Thursday
|
||||||
|
thursdayCount++;
|
||||||
|
if (thursdayCount === 4 && day === this.day) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if today is April Fools' Day
|
||||||
|
isAprilFools() {
|
||||||
|
return this.month === 4 && this.day === 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get April Fools team name
|
||||||
|
getAprilFoolsTeamName(abbrev) {
|
||||||
|
const jokeNames = {
|
||||||
|
'SF': '40-Whiners',
|
||||||
|
'LAR': 'Lambs',
|
||||||
|
'LAC': '🔋 Low Battery',
|
||||||
|
'KC': 'Chefs',
|
||||||
|
'BUF': 'Buffalo Wild Wings',
|
||||||
|
'NE': 'Cheaters',
|
||||||
|
'NYJ': 'Just End The Season',
|
||||||
|
'NYG': 'Littles',
|
||||||
|
'DAL': 'America\'s Team (lol)',
|
||||||
|
'PHI': 'Iggles',
|
||||||
|
'WAS': 'Commies',
|
||||||
|
'GB': 'Cheese Heads',
|
||||||
|
'CHI': 'Da Burrs',
|
||||||
|
'MIN': 'Purple People Eaters',
|
||||||
|
'DET': 'Kitties',
|
||||||
|
'TB': 'Tompa Bay',
|
||||||
|
'NO': 'Ain\'ts',
|
||||||
|
'ATL': 'Dirty Birds',
|
||||||
|
'CAR': 'Kittens',
|
||||||
|
'ARI': 'Birdinals',
|
||||||
|
'SEA': 'Rain City Bird Brains',
|
||||||
|
'PIT': 'Stealers',
|
||||||
|
'BAL': 'Purple Birds',
|
||||||
|
'CLE': 'Factory of Sadness',
|
||||||
|
'CIN': 'Bungles',
|
||||||
|
'HOU': 'Texas Cows',
|
||||||
|
'TEN': 'Tacks',
|
||||||
|
'IND': 'Dolts',
|
||||||
|
'JAX': 'Jag-wires',
|
||||||
|
'LV': 'Traitors',
|
||||||
|
'DEN': 'Neigh-bors',
|
||||||
|
'MIA': 'Dolphins (but warmer)',
|
||||||
|
};
|
||||||
|
return jokeNames[abbrev] || abbrev;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply Easter eggs
|
||||||
|
apply() {
|
||||||
|
// Thanksgiving: Replace stars with turkey emoji
|
||||||
|
if (this.isThanksgiving()) {
|
||||||
|
console.log('🦃 Happy Thanksgiving!');
|
||||||
|
this.applyThanksgiving();
|
||||||
|
}
|
||||||
|
|
||||||
|
// April Fools: Replace team names with joke versions
|
||||||
|
if (this.isAprilFools()) {
|
||||||
|
console.log('🤡 April Fools!');
|
||||||
|
this.applyAprilFools();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply Thanksgiving theme
|
||||||
|
applyThanksgiving() {
|
||||||
|
// Replace star emoji in Favorite Teams sidebar section
|
||||||
|
const sidebarSummary = document.querySelector('.sidebar-details summary');
|
||||||
|
if (sidebarSummary && sidebarSummary.textContent.includes('⭐')) {
|
||||||
|
sidebarSummary.innerHTML = sidebarSummary.innerHTML.replace('⭐', '🦃');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace star in main Favorite Teams section
|
||||||
|
const mainFavoritesTitle = document.querySelector('.favorites-section-main h2');
|
||||||
|
if (mainFavoritesTitle && mainFavoritesTitle.textContent.includes('⭐')) {
|
||||||
|
mainFavoritesTitle.innerHTML = mainFavoritesTitle.innerHTML.replace('⭐', '🦃');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add turkey emoji after "Week X Games"
|
||||||
|
const scheduleTitle = document.querySelector('.schedule-section h2');
|
||||||
|
if (scheduleTitle) {
|
||||||
|
const text = scheduleTitle.innerHTML;
|
||||||
|
if (!text.includes('🦃')) {
|
||||||
|
scheduleTitle.innerHTML = text.replace(/Week (\d+) Games/, 'Week $1 Games 🦃');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply April Fools theme
|
||||||
|
applyAprilFools() {
|
||||||
|
// Store reference to this for use in setTimeout
|
||||||
|
const self = this;
|
||||||
|
|
||||||
|
// Wait for favorites to be rendered, then replace team names
|
||||||
|
setTimeout(() => {
|
||||||
|
// Replace team names in favorite team rows (main dashboard)
|
||||||
|
document.querySelectorAll('.favorite-team-name').forEach(el => {
|
||||||
|
const originalName = el.textContent.trim();
|
||||||
|
// Try to find the abbreviation from TEAM_NAMES
|
||||||
|
let abbrev = null;
|
||||||
|
for (const [key, value] of Object.entries(TEAM_NAMES)) {
|
||||||
|
if (value === originalName) {
|
||||||
|
abbrev = key;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (abbrev) {
|
||||||
|
el.textContent = self.getAprilFoolsTeamName(abbrev);
|
||||||
|
el.style.fontFamily = '"Comic Sans MS", "Comic Sans", cursive';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Replace opponent names
|
||||||
|
document.querySelectorAll('.favorite-opponent').forEach(el => {
|
||||||
|
const text = el.textContent.trim();
|
||||||
|
const match = text.match(/^(vs|@) (.+)$/);
|
||||||
|
if (match) {
|
||||||
|
const prefix = match[1];
|
||||||
|
const opponentName = match[2];
|
||||||
|
|
||||||
|
// Find abbreviation
|
||||||
|
let abbrev = null;
|
||||||
|
for (const [key, value] of Object.entries(TEAM_NAMES)) {
|
||||||
|
if (value === opponentName) {
|
||||||
|
abbrev = key;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (abbrev) {
|
||||||
|
el.textContent = `${prefix} ${self.getAprilFoolsTeamName(abbrev)}`;
|
||||||
|
el.style.fontFamily = '"Comic Sans MS", "Comic Sans", cursive';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Replace team names in sidebar favorites list
|
||||||
|
document.querySelectorAll('.favorite-game-card .favorite-matchup').forEach(el => {
|
||||||
|
const originalName = el.textContent.trim();
|
||||||
|
let abbrev = null;
|
||||||
|
for (const [key, value] of Object.entries(TEAM_NAMES)) {
|
||||||
|
if (value === originalName) {
|
||||||
|
abbrev = key;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (abbrev) {
|
||||||
|
el.textContent = self.getAprilFoolsTeamName(abbrev);
|
||||||
|
el.style.fontFamily = '"Comic Sans MS", "Comic Sans", cursive';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add April Fools indicator
|
||||||
|
const header = document.querySelector('.dashboard-header h1');
|
||||||
|
if (header && !header.textContent.includes('🤡')) {
|
||||||
|
header.innerHTML += ' <span style="font-size: 0.8em;">🤡</span>';
|
||||||
|
}
|
||||||
|
}, 500); // Wait for favorites to render
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize and apply Easter eggs when DOM is ready
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const easterEggs = new EasterEggs();
|
||||||
|
easterEggs.apply();
|
||||||
|
|
||||||
|
// Re-apply when favorites are updated (listen for changes)
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
if (easterEggs.isThanksgiving()) {
|
||||||
|
easterEggs.applyThanksgiving();
|
||||||
|
}
|
||||||
|
if (easterEggs.isAprilFools()) {
|
||||||
|
easterEggs.applyAprilFools();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Observe favorites sections for changes
|
||||||
|
const favoritesDisplay = document.getElementById('favorites-display');
|
||||||
|
const favoritesList = document.getElementById('favorites-list');
|
||||||
|
|
||||||
|
if (favoritesDisplay) {
|
||||||
|
observer.observe(favoritesDisplay, { childList: true, subtree: true });
|
||||||
|
}
|
||||||
|
if (favoritesList) {
|
||||||
|
observer.observe(favoritesList, { childList: true, subtree: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
302
static/favorites.js
Normal file
302
static/favorites.js
Normal file
|
|
@ -0,0 +1,302 @@
|
||||||
|
// Favorites Management for RosterHash
|
||||||
|
// Manages up to 4 favorite NFL teams with localStorage persistence (persists across weeks)
|
||||||
|
|
||||||
|
const MAX_FAVORITES = 4;
|
||||||
|
|
||||||
|
// Team abbreviation to common name mapping
|
||||||
|
const TEAM_NAMES = {
|
||||||
|
'ARI': 'Cardinals',
|
||||||
|
'ATL': 'Falcons',
|
||||||
|
'BAL': 'Ravens',
|
||||||
|
'BUF': 'Bills',
|
||||||
|
'CAR': 'Panthers',
|
||||||
|
'CHI': 'Bears',
|
||||||
|
'CIN': 'Bengals',
|
||||||
|
'CLE': 'Browns',
|
||||||
|
'DAL': 'Cowboys',
|
||||||
|
'DEN': 'Broncos',
|
||||||
|
'DET': 'Lions',
|
||||||
|
'GB': 'Packers',
|
||||||
|
'HOU': 'Texans',
|
||||||
|
'IND': 'Colts',
|
||||||
|
'JAX': 'Jaguars',
|
||||||
|
'KC': 'Chiefs',
|
||||||
|
'LV': 'Raiders',
|
||||||
|
'LAC': 'Chargers',
|
||||||
|
'LAR': 'Rams',
|
||||||
|
'MIA': 'Dolphins',
|
||||||
|
'MIN': 'Vikings',
|
||||||
|
'NE': 'Patriots',
|
||||||
|
'NO': 'Saints',
|
||||||
|
'NYG': 'Giants',
|
||||||
|
'NYJ': 'Jets',
|
||||||
|
'PHI': 'Eagles',
|
||||||
|
'PIT': 'Steelers',
|
||||||
|
'SF': '49ers',
|
||||||
|
'SEA': 'Seahawks',
|
||||||
|
'TB': 'Buccaneers',
|
||||||
|
'TEN': 'Titans',
|
||||||
|
'WAS': 'Commanders'
|
||||||
|
};
|
||||||
|
|
||||||
|
class FavoritesManager {
|
||||||
|
constructor() {
|
||||||
|
this.favorites = this.loadFavorites();
|
||||||
|
this.games = [];
|
||||||
|
this.allTeams = new Set();
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get team common name
|
||||||
|
getTeamName(abbrev) {
|
||||||
|
return TEAM_NAMES[abbrev] || abbrev;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get storage key (no week suffix - persists across weeks)
|
||||||
|
getStorageKey() {
|
||||||
|
return `rosterhash_favorite_teams`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load favorites from localStorage
|
||||||
|
loadFavorites() {
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem(this.getStorageKey());
|
||||||
|
return stored ? JSON.parse(stored) : [];
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to load favorites:', e);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save favorites to localStorage
|
||||||
|
saveFavorites() {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(this.getStorageKey(), JSON.stringify(this.favorites));
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to save favorites:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize favorites system
|
||||||
|
init() {
|
||||||
|
// Wait for DOM to be ready
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', () => this.setup());
|
||||||
|
} else {
|
||||||
|
this.setup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup favorites after DOM is loaded
|
||||||
|
setup() {
|
||||||
|
this.extractGamesFromPage();
|
||||||
|
this.extractAllTeams();
|
||||||
|
this.populateTeamsPicker();
|
||||||
|
this.renderFavoritesList();
|
||||||
|
this.renderFavoritesDisplay();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract all games from the schedule on the page
|
||||||
|
extractGamesFromPage() {
|
||||||
|
this.games = [];
|
||||||
|
this.allTeams = new Set();
|
||||||
|
|
||||||
|
// Find all game cards in the schedule
|
||||||
|
const gameCards = document.querySelectorAll('.game-card');
|
||||||
|
|
||||||
|
gameCards.forEach((card, index) => {
|
||||||
|
const timeElement = card.querySelector('.game-time');
|
||||||
|
const awayTeamElement = card.querySelector('.away-team');
|
||||||
|
const homeTeamElement = card.querySelector('.home-team');
|
||||||
|
const awayScoreElement = card.querySelectorAll('.nfl-score')[0];
|
||||||
|
const homeScoreElement = card.querySelectorAll('.nfl-score')[1];
|
||||||
|
const liveIndicator = card.querySelector('.live-indicator');
|
||||||
|
|
||||||
|
if (awayTeamElement && homeTeamElement) {
|
||||||
|
const awayTeam = awayTeamElement.textContent.trim();
|
||||||
|
const homeTeam = homeTeamElement.textContent.trim();
|
||||||
|
|
||||||
|
// Add teams to the set
|
||||||
|
this.allTeams.add(awayTeam);
|
||||||
|
this.allTeams.add(homeTeam);
|
||||||
|
|
||||||
|
const game = {
|
||||||
|
awayTeam: awayTeam,
|
||||||
|
homeTeam: homeTeam,
|
||||||
|
awayScore: awayScoreElement ? awayScoreElement.textContent.trim() : null,
|
||||||
|
homeScore: homeScoreElement ? homeScoreElement.textContent.trim() : null,
|
||||||
|
time: timeElement ? timeElement.textContent.trim() : 'TBD',
|
||||||
|
isLive: !!liveIndicator,
|
||||||
|
matchup: `${awayTeam} @ ${homeTeam}`
|
||||||
|
};
|
||||||
|
|
||||||
|
this.games.push(game);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract all unique teams
|
||||||
|
extractAllTeams() {
|
||||||
|
// Already done in extractGamesFromPage
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate the teams picker in the sidebar
|
||||||
|
populateTeamsPicker() {
|
||||||
|
const picker = document.getElementById('games-picker');
|
||||||
|
if (!picker) return;
|
||||||
|
|
||||||
|
picker.innerHTML = '';
|
||||||
|
|
||||||
|
// Use all teams from TEAM_NAMES instead of only teams with games this week
|
||||||
|
const allTeamAbbrevs = Object.keys(TEAM_NAMES);
|
||||||
|
|
||||||
|
// Sort teams alphabetically by common name
|
||||||
|
const sortedTeams = allTeamAbbrevs.sort((a, b) => {
|
||||||
|
const nameA = TEAM_NAMES[a];
|
||||||
|
const nameB = TEAM_NAMES[b];
|
||||||
|
return nameA.localeCompare(nameB);
|
||||||
|
});
|
||||||
|
|
||||||
|
sortedTeams.forEach(team => {
|
||||||
|
const isSelected = this.favorites.includes(team);
|
||||||
|
const isDisabled = !isSelected && this.favorites.length >= MAX_FAVORITES;
|
||||||
|
const teamName = TEAM_NAMES[team];
|
||||||
|
|
||||||
|
const item = document.createElement('div');
|
||||||
|
item.className = 'game-picker-item';
|
||||||
|
if (isSelected) item.classList.add('selected');
|
||||||
|
if (isDisabled) item.classList.add('disabled');
|
||||||
|
|
||||||
|
item.innerHTML = `
|
||||||
|
<div class="game-picker-matchup">${teamName}</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (!isDisabled || isSelected) {
|
||||||
|
item.addEventListener('click', () => this.toggleFavorite(team));
|
||||||
|
}
|
||||||
|
|
||||||
|
picker.appendChild(item);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle a team as favorite
|
||||||
|
toggleFavorite(team) {
|
||||||
|
const index = this.favorites.indexOf(team);
|
||||||
|
|
||||||
|
if (index > -1) {
|
||||||
|
// Remove from favorites
|
||||||
|
this.favorites.splice(index, 1);
|
||||||
|
} else {
|
||||||
|
// Add to favorites if under limit
|
||||||
|
if (this.favorites.length < MAX_FAVORITES) {
|
||||||
|
this.favorites.push(team);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.saveFavorites();
|
||||||
|
this.populateTeamsPicker();
|
||||||
|
this.renderFavoritesList();
|
||||||
|
this.renderFavoritesDisplay();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render favorites list in sidebar
|
||||||
|
renderFavoritesList() {
|
||||||
|
const list = document.getElementById('favorites-list');
|
||||||
|
if (!list) return;
|
||||||
|
|
||||||
|
if (this.favorites.length === 0) {
|
||||||
|
list.innerHTML = '<p class="favorites-empty">Click teams below to add favorites (max 4)</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
list.innerHTML = '';
|
||||||
|
|
||||||
|
this.favorites.forEach(team => {
|
||||||
|
const teamName = this.getTeamName(team);
|
||||||
|
const card = document.createElement('div');
|
||||||
|
card.className = 'favorite-game-card';
|
||||||
|
|
||||||
|
card.innerHTML = `
|
||||||
|
<div class="favorite-game-header">
|
||||||
|
<div class="favorite-matchup">${teamName}</div>
|
||||||
|
<button class="favorite-remove" onclick="favoritesManager.toggleFavorite('${team}')">✕</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
list.appendChild(card);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render favorites display on main dashboard (stacked rows like fantasy scores)
|
||||||
|
renderFavoritesDisplay() {
|
||||||
|
const section = document.getElementById('favorites-section-main');
|
||||||
|
const display = document.getElementById('favorites-display');
|
||||||
|
|
||||||
|
if (!section || !display) return;
|
||||||
|
|
||||||
|
if (this.favorites.length === 0) {
|
||||||
|
section.classList.remove('has-favorites');
|
||||||
|
display.innerHTML = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
section.classList.add('has-favorites');
|
||||||
|
display.innerHTML = '';
|
||||||
|
|
||||||
|
this.favorites.forEach(team => {
|
||||||
|
// Find the game(s) for this team
|
||||||
|
const teamGames = this.games.filter(g => g.awayTeam === team || g.homeTeam === team);
|
||||||
|
|
||||||
|
teamGames.forEach(game => {
|
||||||
|
const row = document.createElement('div');
|
||||||
|
row.className = 'favorite-team-row';
|
||||||
|
|
||||||
|
const isHome = game.homeTeam === team;
|
||||||
|
const opponent = isHome ? game.awayTeam : game.homeTeam;
|
||||||
|
const teamScore = isHome ? game.homeScore : game.awayScore;
|
||||||
|
const oppScore = isHome ? game.awayScore : game.homeScore;
|
||||||
|
|
||||||
|
const teamName = this.getTeamName(team);
|
||||||
|
const opponentName = this.getTeamName(opponent);
|
||||||
|
|
||||||
|
const scoreDisplay = (teamScore && oppScore)
|
||||||
|
? `<span class="favorite-team-score">${teamScore} - ${oppScore}</span>`
|
||||||
|
: '';
|
||||||
|
|
||||||
|
const liveBadge = game.isLive
|
||||||
|
? '<span class="favorite-live-indicator">LIVE</span>'
|
||||||
|
: '';
|
||||||
|
|
||||||
|
const matchupDisplay = isHome ? `vs ${opponentName}` : `@ ${opponentName}`;
|
||||||
|
|
||||||
|
row.innerHTML = `
|
||||||
|
<div class="favorite-team-info">
|
||||||
|
<span class="favorite-team-name">${teamName}</span>
|
||||||
|
<span class="favorite-opponent">${matchupDisplay}</span>
|
||||||
|
</div>
|
||||||
|
<div class="favorite-team-details">
|
||||||
|
<span class="favorite-game-time">${game.time}</span>
|
||||||
|
${liveBadge}
|
||||||
|
${scoreDisplay}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
display.appendChild(row);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh scores for favorite games (called by page refresh)
|
||||||
|
refreshScores() {
|
||||||
|
this.extractGamesFromPage();
|
||||||
|
this.renderFavoritesList();
|
||||||
|
this.renderFavoritesDisplay();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize favorites manager globally
|
||||||
|
let favoritesManager;
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
favoritesManager = new FavoritesManager();
|
||||||
|
});
|
||||||
477
static/style.css
477
static/style.css
|
|
@ -1,17 +1,146 @@
|
||||||
.sidebar-link {
|
.btn-icon {
|
||||||
display: block;
|
font-size: 18px;
|
||||||
padding: 12px 16px;
|
flex-shrink: 0;
|
||||||
background: var(--bg-secondary);
|
|
||||||
border-radius: 8px;
|
|
||||||
color: var(--text-primary);
|
|
||||||
text-decoration: none;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-link:hover {
|
/* Favorites Section */
|
||||||
|
.favorites-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorites-empty {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 13px;
|
||||||
|
text-align: center;
|
||||||
|
padding: 16px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 2px dashed var(--border-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorite-game-card {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 2px solid var(--accent);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorite-game-card:hover {
|
||||||
background: var(--bg-tertiary);
|
background: var(--bg-tertiary);
|
||||||
transform: translateX(5px);
|
transform: translateY(-2px);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorite-game-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorite-matchup {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorite-remove {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px;
|
||||||
|
font-size: 16px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorite-remove:hover {
|
||||||
|
color: var(--error);
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorite-game-info {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorite-score {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorite-live-badge {
|
||||||
|
background: var(--error);
|
||||||
|
color: white;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Games Picker Section */
|
||||||
|
.games-picker {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-picker-item {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 2px solid var(--border-primary);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-picker-item:hover {
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-picker-item.selected {
|
||||||
|
background: var(--accent);
|
||||||
|
border-color: var(--accent);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-picker-item.disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-picker-item.disabled:hover {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-color: var(--border-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-picker-matchup {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-picker-time {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-picker-item.selected .game-picker-time {
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
}
|
}
|
||||||
|
|
||||||
.about-text {
|
.about-text {
|
||||||
|
|
@ -259,6 +388,99 @@
|
||||||
box-shadow: var(--shadow-sm);
|
box-shadow: var(--shadow-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Favorites Section Main */
|
||||||
|
.favorites-section-main {
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorites-section-main.has-favorites {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorites-section-main h2 {
|
||||||
|
color: var(--accent);
|
||||||
|
margin-bottom: 15px;
|
||||||
|
font-size: 1.3rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorites-display {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorite-team-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 0;
|
||||||
|
border-bottom: 1px solid var(--border-light);
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorite-team-row:hover {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorite-team-row:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorite-team-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorite-team-name {
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--accent);
|
||||||
|
font-size: 14px;
|
||||||
|
min-width: 45px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorite-opponent {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorite-team-details {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorite-game-time {
|
||||||
|
color: var(--text-muted);
|
||||||
|
min-width: 100px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorite-live-indicator {
|
||||||
|
background: var(--error);
|
||||||
|
color: white;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorite-team-score {
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-primary);
|
||||||
|
min-width: 60px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
.score-row {
|
.score-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
@ -426,6 +648,14 @@
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nfl-score {
|
||||||
|
color: var(--accent);
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 15px;
|
||||||
|
min-width: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
.at {
|
.at {
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|
@ -611,12 +841,46 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
width: 85%;
|
width: 100%;
|
||||||
right: -85%;
|
max-width: 100%;
|
||||||
|
right: -100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-header {
|
||||||
|
padding: 16px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-title {
|
||||||
|
font-size: 1.3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-content {
|
.sidebar-content {
|
||||||
padding: 50px 20px 20px;
|
padding: 16px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorites-display {
|
||||||
|
/* Already stacked, no changes needed */
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorite-team-row {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorite-team-details {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorite-game-time {
|
||||||
|
min-width: auto;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorite-team-score {
|
||||||
|
min-width: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-header {
|
.dashboard-header {
|
||||||
|
|
@ -849,14 +1113,17 @@ body {
|
||||||
.sidebar {
|
.sidebar {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: -400px;
|
right: -420px;
|
||||||
width: 400px;
|
width: 420px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: var(--sidebar-bg);
|
background: var(--sidebar-bg);
|
||||||
box-shadow: var(--sidebar-shadow);
|
box-shadow: var(--sidebar-shadow);
|
||||||
transition: right 0.3s ease;
|
transition: right 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
z-index: 1001;
|
z-index: 1001;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Show sidebar when checkbox is checked */
|
/* Show sidebar when checkbox is checked */
|
||||||
|
|
@ -870,55 +1137,189 @@ body {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Sidebar Header */
|
||||||
|
.sidebar-header {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border-bottom: 2px solid var(--border-primary);
|
||||||
|
padding: 20px 24px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 10;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-title {
|
||||||
|
color: var(--accent);
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.close-btn {
|
.close-btn {
|
||||||
position: absolute;
|
background: var(--bg-secondary);
|
||||||
top: 15px;
|
border: 2px solid var(--border-primary);
|
||||||
right: 15px;
|
border-radius: 8px;
|
||||||
background: transparent;
|
width: 36px;
|
||||||
border: none;
|
height: 36px;
|
||||||
font-size: 28px;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 20px;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 5px;
|
padding: 0;
|
||||||
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.close-btn:hover {
|
.close-btn:hover {
|
||||||
opacity: 0.7;
|
background: var(--bg-tertiary);
|
||||||
|
transform: rotate(90deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-content {
|
.sidebar-content {
|
||||||
padding: 60px 30px 30px;
|
padding: 20px 24px;
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-section {
|
.sidebar-section {
|
||||||
margin-bottom: 35px;
|
margin-bottom: 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-section h3 {
|
.sidebar-section h3 {
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
margin-bottom: 15px;
|
margin-bottom: 12px;
|
||||||
font-size: 1.1rem;
|
font-size: 0.95rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-button {
|
|
||||||
display: flex;
|
/* Sidebar Details (Collapsible) */
|
||||||
align-items: center;
|
.sidebar-details {
|
||||||
gap: 10px;
|
|
||||||
background: var(--bg-secondary);
|
background: var(--bg-secondary);
|
||||||
border: 2px solid var(--border-primary);
|
border: 2px solid var(--border-primary);
|
||||||
border-radius: 8px;
|
border-radius: 10px;
|
||||||
padding: 12px 20px;
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-details summary {
|
||||||
|
padding: 12px 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
list-style: none;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-details summary::-webkit-details-marker {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-details summary::after {
|
||||||
|
content: '▼';
|
||||||
|
font-size: 12px;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-details[open] summary::after {
|
||||||
|
transform: rotate(-180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-details summary:hover {
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-details .about-text {
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-top: 1px solid var(--border-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-content {
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-top: 1px solid var(--border-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollable-content {
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom scrollbar for details content */
|
||||||
|
.scrollable-content::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollable-content::-webkit-scrollbar-track {
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollable-content::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--accent);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollable-content::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--accent-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Favorites subsection */
|
||||||
|
.favorites-subsection {
|
||||||
|
margin-top: 16px;
|
||||||
|
padding-top: 16px;
|
||||||
|
border-top: 1px solid var(--border-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.subsection-title {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar Actions Section */
|
||||||
|
.sidebar-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-actions form {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-action-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 2px solid var(--border-primary);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 12px 16px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
font-size: 15px;
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-button:hover {
|
.sidebar-action-btn:hover {
|
||||||
background: var(--bg-tertiary);
|
background: var(--bg-tertiary);
|
||||||
transform: translateY(-2px);
|
transform: translateX(-4px);
|
||||||
|
border-color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Refresh button */
|
/* Refresh button */
|
||||||
|
|
|
||||||
|
|
@ -21,52 +21,79 @@
|
||||||
|
|
||||||
<!-- Sidebar Menu -->
|
<!-- Sidebar Menu -->
|
||||||
<div class="sidebar" id="sidebar">
|
<div class="sidebar" id="sidebar">
|
||||||
<label for="menu-toggle" class="close-btn">✕</label>
|
<div class="sidebar-header">
|
||||||
|
<h2 class="sidebar-title">Menu</h2>
|
||||||
|
<label for="menu-toggle" class="close-btn">✕</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="sidebar-content">
|
<div class="sidebar-content">
|
||||||
<!-- Theme Toggle Section -->
|
<!-- About Section (Collapsible, closed by default) -->
|
||||||
<div class="sidebar-section">
|
<div class="sidebar-section">
|
||||||
<h3>Theme</h3>
|
<details class="sidebar-details">
|
||||||
<form method="post" action="/toggle_theme" style="display: inline;">
|
<summary>About RosterHash</summary>
|
||||||
|
<div class="about-text">
|
||||||
|
<p>At-a-glance view of your Sleeper leagues and player schedules.</p>
|
||||||
|
<p><strong>Features:</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>View all league matchups</li>
|
||||||
|
<li>See player schedules by game</li>
|
||||||
|
<li>Track favorite NFL teams</li>
|
||||||
|
<li>League color-coding</li>
|
||||||
|
</ul>
|
||||||
|
<p class="cron-note">* * * * 4,7,1</p>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Favorite Teams Section (Collapsible, closed by default) -->
|
||||||
|
<div class="sidebar-section">
|
||||||
|
<details class="sidebar-details">
|
||||||
|
<summary>⭐ Favorite Teams</summary>
|
||||||
|
<div class="details-content">
|
||||||
|
<div id="favorites-list" class="favorites-list">
|
||||||
|
<p class="favorites-empty">Click teams below to add favorites (max 4)</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Select Teams subsection -->
|
||||||
|
<div class="favorites-subsection">
|
||||||
|
<h4 class="subsection-title">Select Teams</h4>
|
||||||
|
<div class="scrollable-content">
|
||||||
|
<div id="games-picker" class="games-picker">
|
||||||
|
<!-- Populated by JavaScript -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Actions and Links (No title) -->
|
||||||
|
<div class="sidebar-section sidebar-actions">
|
||||||
|
<!-- Theme Toggle -->
|
||||||
|
<form method="post" action="/toggle_theme">
|
||||||
<input type="hidden" name="return_url" value="{{ request.url }}">
|
<input type="hidden" name="return_url" value="{{ request.url }}">
|
||||||
<button type="submit" class="theme-button">
|
<button type="submit" class="sidebar-action-btn">
|
||||||
{% if current_theme == 'dark' %}
|
{% if current_theme == 'dark' %}
|
||||||
<span>🌙</span>
|
<span class="btn-icon">🌙</span>
|
||||||
<span>Dark Mode</span>
|
<span>Change to Light Theme</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span>☀️</span>
|
<span class="btn-icon">☀️</span>
|
||||||
<span>Light Mode</span>
|
<span>Change to Dark Theme</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Links Section -->
|
<!-- Sleeper App Link -->
|
||||||
<div class="sidebar-section">
|
<a href="https://sleeper.app" target="_blank" class="sidebar-action-btn">
|
||||||
<h3>Links</h3>
|
<span class="btn-icon">🏈</span>
|
||||||
<a href="https://sleeper.app" target="_blank" class="sidebar-link">
|
<span>Sleeper App</span>
|
||||||
🏈 Sleeper App
|
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com" target="_blank" class="sidebar-link">
|
|
||||||
💻 Source Code
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- About Section -->
|
<!-- Source Code Link -->
|
||||||
<div class="sidebar-section">
|
<a href="https://codeberg.org/edfig/RosterHash" target="_blank" class="sidebar-action-btn">
|
||||||
<h3>About This WebApp</h3>
|
<span class="btn-icon">💻</span>
|
||||||
<div class="about-text">
|
<span>Source Code</span>
|
||||||
<p>RosterHash gives you an at-a-glance view of your Sleeper leagues player's schedules and status.</p>
|
</a>
|
||||||
<p><strong>How to use:</strong></p>
|
|
||||||
<ul>
|
|
||||||
<li>Enter your Sleeper username on the home page</li>
|
|
||||||
<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">* * * * 4,7,1</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -186,5 +213,11 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- Favorites Management Script -->
|
||||||
|
<script src="{{ url_for('static', filename='favorites.js') }}"></script>
|
||||||
|
|
||||||
|
<!-- Easter Eggs Script -->
|
||||||
|
<script src="{{ url_for('static', filename='easter-eggs.js') }}"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,14 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- Favorites Section (hidden by default, shown by JavaScript if favorites exist) -->
|
||||||
|
<section class="favorites-section-main" id="favorites-section-main">
|
||||||
|
<h2>⭐ Favorite Teams</h2>
|
||||||
|
<div id="favorites-display" class="favorites-display">
|
||||||
|
<!-- Populated by JavaScript -->
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- Main calendar section -->
|
<!-- Main calendar section -->
|
||||||
<section class="schedule-section">
|
<section class="schedule-section">
|
||||||
<h2>Week {{ week }} Games
|
<h2>Week {{ week }} Games
|
||||||
|
|
@ -132,7 +140,13 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="matchup">
|
<div class="matchup">
|
||||||
<span class="away-team">{{ game.away_team }}</span>
|
<span class="away-team">{{ game.away_team }}</span>
|
||||||
|
{% if game.away_score is not none %}
|
||||||
|
<span class="nfl-score">{{ game.away_score }}</span>
|
||||||
|
{% endif %}
|
||||||
<span class="at">@</span>
|
<span class="at">@</span>
|
||||||
|
{% if game.home_score is not none %}
|
||||||
|
<span class="nfl-score">{{ game.home_score }}</span>
|
||||||
|
{% endif %}
|
||||||
<span class="home-team">{{ game.home_team }}</span>
|
<span class="home-team">{{ game.home_team }}</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="game-collapse-indicator">{% if game.is_past and not game.is_live %}▶{% else %}▼{% endif %}</span>
|
<span class="game-collapse-indicator">{% if game.is_past and not game.is_live %}▶{% else %}▼{% endif %}</span>
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="example">
|
<div class="example">
|
||||||
<p>Example: <code>mySleeperUsername</code></p>
|
<p>Example: <code>{{ mySleeperUsername }}</code></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue