Compare commits
7 commits
ft-live-sc
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72981d5e8a | ||
|
|
6115b557c5 | ||
|
|
ca702d6bcc | ||
|
|
f4ad413eac | ||
|
|
6d21444746 | ||
|
|
2ae44f64b5 | ||
|
|
763277ca8a |
11 changed files with 1032 additions and 82 deletions
|
|
@ -1,16 +1,16 @@
|
|||
|
||||
# <div align="center"><img src="rosterhash_logo.png" alt="RosterHash Logo" width="200"/></div>
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://quay.io/repository/eddiefigsystems/rosterhash)
|
||||
|
||||
# 🏈 RosterHash
|
||||
|
||||
**Your Fantasy Football Command Center**
|
||||
|
||||
*Track all your leagues, matchups, and players in one dashboard*
|
||||
|
||||
[](https://python.org)
|
||||
[](https://flask.palletsprojects.com/)
|
||||
[](https://docker.com)
|
||||
|
||||
</div>
|
||||
|
||||
|
|
|
|||
4
app.py
4
app.py
|
|
@ -6,6 +6,7 @@ import traceback
|
|||
from services.sleeper_api import SleeperAPI
|
||||
from services.espn_api import ESPNAPI
|
||||
from config import Config
|
||||
from faker import Faker
|
||||
|
||||
# Force unbuffered output for Docker logs
|
||||
os.environ['PYTHONUNBUFFERED'] = '1'
|
||||
|
|
@ -87,6 +88,7 @@ def index():
|
|||
print("DEBUG: Index route accessed", flush=True)
|
||||
# Get the last used username from cookie
|
||||
last_username = request.cookies.get('last_username', '')
|
||||
mySleeperUsername = Faker().user_name()
|
||||
print(f"DEBUG: Last username from cookie: '{last_username}'", flush=True)
|
||||
|
||||
# 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)
|
||||
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')
|
||||
def dashboard_form():
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
services:
|
||||
rosterhash:
|
||||
image: rosterhash:latest
|
||||
image: quay.io/eddiefigsystems/rosterhash:latest
|
||||
ports:
|
||||
- "5000:5000"
|
||||
restart: unless-stopped
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import os
|
|||
|
||||
class Config:
|
||||
# App version
|
||||
VERSION = '1.2.0'
|
||||
VERSION = '1.2.1'
|
||||
|
||||
SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key'
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
Flask==2.3.3
|
||||
requests==2.31.0
|
||||
python-dateutil==2.8.2
|
||||
Faker==37.12.0
|
||||
|
|
|
|||
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();
|
||||
});
|
||||
469
static/style.css
469
static/style.css
|
|
@ -1,17 +1,146 @@
|
|||
.sidebar-link {
|
||||
display: block;
|
||||
padding: 12px 16px;
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 8px;
|
||||
color: var(--text-primary);
|
||||
text-decoration: none;
|
||||
margin-bottom: 10px;
|
||||
transition: all 0.2s ease;
|
||||
.btn-icon {
|
||||
font-size: 18px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.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);
|
||||
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 {
|
||||
|
|
@ -259,6 +388,99 @@
|
|||
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 {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
|
@ -619,12 +841,46 @@
|
|||
}
|
||||
|
||||
.sidebar {
|
||||
width: 85%;
|
||||
right: -85%;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
right: -100%;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: 16px 16px;
|
||||
}
|
||||
|
||||
.sidebar-title {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.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 {
|
||||
|
|
@ -857,14 +1113,17 @@ body {
|
|||
.sidebar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: -400px;
|
||||
width: 400px;
|
||||
right: -420px;
|
||||
width: 420px;
|
||||
height: 100%;
|
||||
background: var(--sidebar-bg);
|
||||
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;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Show sidebar when checkbox is checked */
|
||||
|
|
@ -878,55 +1137,189 @@ body {
|
|||
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 {
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
right: 15px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
font-size: 28px;
|
||||
background: var(--bg-secondary);
|
||||
border: 2px solid var(--border-primary);
|
||||
border-radius: 8px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
padding: 5px;
|
||||
padding: 0;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
opacity: 0.7;
|
||||
background: var(--bg-tertiary);
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.sidebar-content {
|
||||
padding: 60px 30px 30px;
|
||||
padding: 20px 24px;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.sidebar-section {
|
||||
margin-bottom: 35px;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.sidebar-section h3 {
|
||||
color: var(--accent);
|
||||
margin-bottom: 15px;
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 12px;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.theme-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
|
||||
/* Sidebar Details (Collapsible) */
|
||||
.sidebar-details {
|
||||
background: var(--bg-secondary);
|
||||
border: 2px solid var(--border-primary);
|
||||
border-radius: 8px;
|
||||
padding: 12px 20px;
|
||||
border-radius: 10px;
|
||||
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;
|
||||
width: 100%;
|
||||
transition: all 0.2s ease;
|
||||
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);
|
||||
transform: translateY(-2px);
|
||||
transform: translateX(-4px);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
/* Refresh button */
|
||||
|
|
|
|||
|
|
@ -21,52 +21,79 @@
|
|||
|
||||
<!-- Sidebar Menu -->
|
||||
<div class="sidebar" id="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<h2 class="sidebar-title">Menu</h2>
|
||||
<label for="menu-toggle" class="close-btn">✕</label>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-content">
|
||||
<!-- Theme Toggle Section -->
|
||||
<!-- About Section (Collapsible, closed by default) -->
|
||||
<div class="sidebar-section">
|
||||
<h3>Theme</h3>
|
||||
<form method="post" action="/toggle_theme" style="display: inline;">
|
||||
<input type="hidden" name="return_url" value="{{ request.url }}">
|
||||
<button type="submit" class="theme-button">
|
||||
{% if current_theme == 'dark' %}
|
||||
<span>🌙</span>
|
||||
<span>Dark Mode</span>
|
||||
{% else %}
|
||||
<span>☀️</span>
|
||||
<span>Light Mode</span>
|
||||
{% endif %}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Links Section -->
|
||||
<div class="sidebar-section">
|
||||
<h3>Links</h3>
|
||||
<a href="https://sleeper.app" target="_blank" class="sidebar-link">
|
||||
🏈 Sleeper App
|
||||
</a>
|
||||
<a href="https://github.com" target="_blank" class="sidebar-link">
|
||||
💻 Source Code
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- About Section -->
|
||||
<div class="sidebar-section">
|
||||
<h3>About This WebApp</h3>
|
||||
<details class="sidebar-details">
|
||||
<summary>About RosterHash</summary>
|
||||
<div class="about-text">
|
||||
<p>RosterHash gives you an at-a-glance view of your Sleeper leagues player's schedules and status.</p>
|
||||
<p><strong>How to use:</strong></p>
|
||||
<p>At-a-glance view of your Sleeper leagues and player schedules.</p>
|
||||
<p><strong>Features:</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>
|
||||
<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 }}">
|
||||
<button type="submit" class="sidebar-action-btn">
|
||||
{% if current_theme == 'dark' %}
|
||||
<span class="btn-icon">🌙</span>
|
||||
<span>Change to Light Theme</span>
|
||||
{% else %}
|
||||
<span class="btn-icon">☀️</span>
|
||||
<span>Change to Dark Theme</span>
|
||||
{% endif %}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- Sleeper App Link -->
|
||||
<a href="https://sleeper.app" target="_blank" class="sidebar-action-btn">
|
||||
<span class="btn-icon">🏈</span>
|
||||
<span>Sleeper App</span>
|
||||
</a>
|
||||
|
||||
<!-- Source Code Link -->
|
||||
<a href="https://codeberg.org/edfig/RosterHash" target="_blank" class="sidebar-action-btn">
|
||||
<span class="btn-icon">💻</span>
|
||||
<span>Source Code</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -186,5 +213,11 @@
|
|||
});
|
||||
}
|
||||
</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>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -80,6 +80,14 @@
|
|||
{% endfor %}
|
||||
</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 -->
|
||||
<section class="schedule-section">
|
||||
<h2>Week {{ week }} Games
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
</form>
|
||||
|
||||
<div class="example">
|
||||
<p>Example: <code>mySleeperUsername</code></p>
|
||||
<p>Example: <code>{{ mySleeperUsername }}</code></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue