summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--PREMISE.md23
-rw-r--r--README.md1
-rw-r--r--app.py956
-rw-r--r--db_builder.py124
-rw-r--r--orchard_league.dbbin0 -> 36864 bytes
-rw-r--r--requirements.txt3
-rw-r--r--static/css/style.css372
-rw-r--r--static/img/logo.pngbin0 -> 6863 bytes
-rw-r--r--static/img/orchards_left.pngbin0 -> 285981 bytes
-rw-r--r--static/img/orchards_right.pngbin0 -> 279472 bytes
-rw-r--r--static/ttf/Decort.ttfbin0 -> 38632 bytes
-rw-r--r--static/ttf/Powell and Geary.ttfbin0 -> 111644 bytes
-rw-r--r--static/uploads/7_drahdiwaberl.pngbin0 -> 409795 bytes
-rw-r--r--templates/admin.html138
-rw-r--r--templates/admin_edit_game.html66
-rw-r--r--templates/admin_manage_teams.html76
-rw-r--r--templates/base.html54
-rw-r--r--templates/game.html185
-rw-r--r--templates/login.html16
-rw-r--r--templates/season.html285
-rw-r--r--templates/team.html65
22 files changed, 2366 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..92afa22
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+__pycache__/
+venv/
diff --git a/PREMISE.md b/PREMISE.md
new file mode 100644
index 0000000..7b1f22d
--- /dev/null
+++ b/PREMISE.md
@@ -0,0 +1,23 @@
+Create a website for a Super Mega Baseball 4 league called "The Orchard League" according to the following requirements:
+
+## Functional Requirements
+
+1. Make all site pages visible to un-authenticated users, except the team management page and the admin pages.
+2. Create a login page with the credentials being a simple username and password. Make sure the password is hashed/encrypted.
+3. Users should have the associated fields besides the username/hashed password: whether the user is an admin, team name, team icon, and seasons being partaken in. Potentially more fields could be required to match additional features
+4. Create a standings page that displays the standings for a given season. Columns will be wins, losses, runs for, runs against, and games back.
+5. Create a schedule page that by default when not logged in shows the full schedule for the current season with a calendar. Games in the calendar should have the teams with their records and icons, visiting team first. Clicking on games within the calendar loads the game page. When a user is logged in, it should show the schedule for the logged in team (if the team is participating in the season). There should be a dropdown on the schedule page that allows the visitor to view the schedules of the whole league or of specific teams.
+6. Create an admin-only season management page that allows admins to create seasons, add teams to seasons, generate schedules, and add/remove game results to a season. Season generation should have the following parameters: total number of games, games per week, number of playoff teams, number of playoff games per series. Even number of playoff teams should result in an even two sided bracket with seeding going top seed vs last seed, 2nd vs 2nd to last, etc. Odd numbers should result in the top seed getting a bye and playing the winner.
+7. Create a game page that displays the scheduled or already completed game. The game page should have the two participating teams and their logos/records with the away team on the left. It should also have the scheduled game date and the score (if any). The users for both participating teams can submit a score/result after or on the scheduled game date. If their submissions agree, the match score is finalized. Result submissions have an optional box score with 9 regular innings with an extra innings box (optional), hits, and errors. This is ignored if the box score contradicts the final score or both users submit box scores that contradict each other. Users of the involved teams can also submit petitions to change the start date, which happens automatically if both their petitions agree. Admins can change any game information at will, but the fields only become editable after pressing a pencil edit button visible only to admins.
+8. Create a user management page where users can change their password, team name, and upload team icons.
+
+## Technical Requirements
+
+1. Use Flask for hosting the website and serving the page templates.
+2. Use Flask-login for authentication.
+3. Use SQLite for storing information.
+4. Create data structures and functions that can be used for generating SQLite tables with ease of iteration and regeneration. Tables may include user logins, teams, seasons, schedules, individual games, etc.
+
+## Stylistic Requirements
+
+1. The website appearance should be evocative of themes of futuristic art deco urbanism growing out of dusty rural agricultural surroundings.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..9913bed
--- /dev/null
+++ b/README.md
@@ -0,0 +1 @@
+init repo
diff --git a/app.py b/app.py
new file mode 100644
index 0000000..826410b
--- /dev/null
+++ b/app.py
@@ -0,0 +1,956 @@
+import os
+import json
+import itertools
+from flask import Flask, render_template, request, redirect, url_for, flash
+from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user
+from werkzeug.security import generate_password_hash, check_password_hash
+from werkzeug.utils import secure_filename
+import sqlite3
+from datetime import datetime, timedelta
+
+app = Flask(__name__)
+app.secret_key = 'super_secret_orchard_key'
+app.config['UPLOAD_FOLDER'] = 'static/uploads'
+
+os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
+
+login_manager = LoginManager()
+login_manager.init_app(app)
+login_manager.login_view = 'login'
+
+DB_FILE = 'orchard_league.db'
+
+def get_db():
+ conn = sqlite3.connect(DB_FILE)
+ conn.row_factory = sqlite3.Row
+ return conn
+
+class User(UserMixin):
+ def __init__(self, id, username, is_admin, team_name, team_icon):
+ self.id = str(id)
+ self.username = username
+ self.is_admin = bool(is_admin)
+ self.team_name = team_name
+ self.team_icon = team_icon
+
+@login_manager.user_loader
+def load_user(user_id):
+ conn = get_db()
+ c = conn.cursor()
+ c.execute('SELECT * FROM Users WHERE id = ?', (user_id,))
+ user_row = c.fetchone()
+ conn.close()
+ if user_row:
+ return User(user_row['id'], user_row['username'], user_row['is_admin'], user_row['team_name'], user_row['team_icon'])
+ return None
+
+@app.route('/')
+def index():
+ season_id = request.args.get('season_id')
+ team_id = request.args.get('team_id')
+ tab = request.args.get('tab', 'overview')
+
+ conn = get_db()
+ c = conn.cursor()
+
+ # Get all seasons for the dropdown
+ c.execute('SELECT id, name FROM Seasons ORDER BY id DESC')
+ seasons = c.fetchall()
+
+ if not seasons:
+ conn.close()
+ return render_template('season.html', seasons=[], selected_season_id=None)
+
+ if not season_id:
+ season_id = seasons[0]['id']
+
+ # Season Overview Data
+ c.execute('SELECT * FROM Seasons WHERE id = ?', (season_id,))
+ season_info = dict(c.fetchone())
+
+ # Season Runtime (Start/End dates)
+ c.execute('SELECT MIN(scheduled_date), MAX(scheduled_date) FROM Games WHERE season_id = ?', (season_id,))
+ runtime = c.fetchone()
+ season_info['start_date'] = runtime[0] if runtime[0] else "N/A"
+ season_info['end_date'] = runtime[1] if runtime[1] else "N/A"
+
+ # Final Placement (if all games are Final and games exist)
+ c.execute("SELECT COUNT(*) as c FROM Games WHERE season_id = ?", (season_id,))
+ has_games = c.fetchone()['c'] > 0
+ c.execute("SELECT COUNT(*) as c FROM Games WHERE season_id = ? AND status != 'Final'", (season_id,))
+ all_final = c.fetchone()['c'] == 0
+ is_finished = has_games and all_final
+ season_info['is_finished'] = is_finished
+
+ # Join Status for current user
+ user_season_status = None
+ if current_user.is_authenticated:
+ c.execute('SELECT status FROM SeasonTeams WHERE season_id = ? AND user_id = ?', (season_id, current_user.id))
+ status_row = c.fetchone()
+ if status_row:
+ user_season_status = status_row['status']
+
+ # Standings Data (Regular Season only)
+ c.execute('''
+ SELECT
+ u.id as user_id, u.team_name, u.team_icon,
+ SUM(CASE WHEN (g.home_team_id = u.id AND g.home_score > g.away_score) OR (g.away_team_id = u.id AND g.away_score > g.home_score) THEN 1 ELSE 0 END) as wins,
+ SUM(CASE WHEN (g.home_team_id = u.id AND g.home_score < g.away_score) OR (g.away_team_id = u.id AND g.away_score < g.home_score) THEN 1 ELSE 0 END) as losses,
+ SUM(CASE WHEN g.home_team_id = u.id THEN g.home_score WHEN g.away_team_id = u.id THEN g.away_score ELSE 0 END) as runs_for,
+ SUM(CASE WHEN g.home_team_id = u.id THEN g.away_score WHEN g.away_team_id = u.id THEN g.home_score ELSE 0 END) as runs_against,
+ (SELECT MIN(scheduled_date) FROM Games WHERE (home_team_id = u.id OR away_team_id = u.id) AND season_id = st.season_id) as first_game
+ FROM SeasonTeams st
+ JOIN Users u ON st.user_id = u.id
+ LEFT JOIN Games g ON (g.home_team_id = u.id OR g.away_team_id = u.id) AND g.season_id = st.season_id AND g.status = 'Final' AND g.is_playoff = 0
+ WHERE st.season_id = ? AND st.status = 'Approved'
+ GROUP BY u.id, u.team_name, u.team_icon
+ ORDER BY wins DESC, runs_for DESC, runs_against ASC, first_game ASC
+ ''', (season_id,))
+ standings_raw = c.fetchall()
+
+ # Clinch Calculation
+ standings = []
+ if standings_raw:
+ top_wins = standings_raw[0]['wins']
+ top_losses = standings_raw[0]['losses']
+ p_count = season_info['playoff_teams'] or 0
+ total_sched = season_info['total_games'] or 0
+
+ # Get remaining games for each team
+ c.execute('''
+ SELECT u.id, COUNT(g.id) as remaining
+ FROM SeasonTeams st
+ JOIN Users u ON st.user_id = u.id
+ LEFT JOIN Games g ON (g.home_team_id = u.id OR g.away_team_id = u.id)
+ AND g.season_id = st.season_id AND g.status = 'Scheduled' AND g.is_playoff = 0
+ WHERE st.season_id = ?
+ GROUP BY u.id
+ ''', (season_id,))
+ remaining_map = {row['id']: row['remaining'] for row in c.fetchall()}
+
+ team_stats = []
+ for row in standings_raw:
+ d = dict(row)
+ d['max_wins'] = d['wins'] + remaining_map.get(d['user_id'], 0)
+ team_stats.append(d)
+
+ # Clinch Playoff Marker
+ # A team clinches if their wins > max possible wins of the team currently in the first 'out' spot
+ clinch_out_threshold = float('inf')
+ if p_count > 0 and len(team_stats) > p_count:
+ clinch_out_threshold = team_stats[p_count]['max_wins']
+
+ # Clinch 1st Seed Marker
+ clinch_1st_threshold = float('inf')
+ if p_count > 0 and len(team_stats) > 1:
+ clinch_1st_threshold = team_stats[1]['max_wins']
+
+ for i, d in enumerate(team_stats):
+ games_back = ((top_wins - d['wins']) + (d['losses'] - top_losses)) / 2.0
+ d['games_back'] = "-" if games_back == 0 else str(games_back)
+
+ d['clinch'] = ""
+ if d['wins'] > clinch_out_threshold:
+ d['clinch'] = "*"
+ if i == 0 and d['wins'] > clinch_1st_threshold:
+ d['clinch'] = "**"
+ standings.append(d)
+
+ # Schedule Data
+ c.execute('SELECT id, team_name FROM Users WHERE is_admin = 0')
+ all_teams = c.fetchall()
+
+ if not team_id and current_user.is_authenticated and not current_user.is_admin:
+ team_id = str(current_user.id)
+ elif not team_id:
+ team_id = 'all'
+
+ query = '''
+ SELECT g.*,
+ a.team_name as away_team, a.team_icon as away_icon,
+ h.team_name as home_team, h.team_icon as home_icon,
+ (SELECT COUNT(*) FROM GameSubmissions gs
+ WHERE g.status != 'Final' AND gs.game_id = g.id
+ AND (
+ (gs.proposed_date IS NOT NULL AND gs.proposed_date != g.scheduled_date) OR
+ (gs.away_score IS NOT NULL AND (g.away_score IS NULL OR gs.away_score != g.away_score)) OR
+ (gs.home_score IS NOT NULL AND (g.home_score IS NULL OR gs.home_score != g.home_score))
+ )
+ ) as pending_proposals
+ FROM Games g
+ LEFT JOIN Users a ON g.away_team_id = a.id
+ LEFT JOIN Users h ON g.home_team_id = h.id
+ WHERE g.season_id = ?
+ '''
+ params = [season_id]
+
+ # We fetch ALL games first, then filter regular season games in Python if needed,
+ # OR we modify the query to include ALL playoff games but only filtered regular games.
+ if team_id and team_id != 'all':
+ query += ' AND (g.is_playoff = 1 OR g.away_team_id = ? OR g.home_team_id = ?)'
+ params.extend([team_id, team_id])
+
+ query += ' ORDER BY g.scheduled_date ASC'
+ c.execute(query, params)
+ games_list = c.fetchall()
+
+ # Get team records for bracket display
+ team_records = {}
+ for row in standings:
+ team_records[row['user_id']] = f"({row['wins']}-{row['losses']})"
+
+ # Group games by date for calendar
+ games_by_date = {}
+ playoff_games = []
+ for g_row in games_list:
+ g = dict(g_row)
+ if g['is_playoff']:
+ g['away_record'] = team_records.get(g['away_team_id'], "")
+ g['home_record'] = team_records.get(g['home_team_id'], "")
+ playoff_games.append(g)
+
+ dt = g['scheduled_date']
+ if dt not in games_by_date:
+ games_by_date[dt] = []
+ games_by_date[dt].append(g)
+
+ # Bracket Logic (using series_id)
+ bracket_series = {}
+ for pg in playoff_games:
+ sid = pg['series_id']
+ if sid not in bracket_series:
+ bracket_series[sid] = pg.copy()
+ c.execute('''
+ SELECT
+ SUM(CASE WHEN away_score > home_score THEN 1 ELSE 0 END) as aw,
+ SUM(CASE WHEN home_score > away_score THEN 1 ELSE 0 END) as hw
+ FROM Games WHERE season_id = ? AND series_id = ? AND status = 'Final'
+ ''', (season_id, sid))
+ s_res = c.fetchone()
+ bracket_series[sid]['away_series_wins'] = s_res['aw'] or 0
+ bracket_series[sid]['home_series_wins'] = s_res['hw'] or 0
+
+ # Group series into rounds
+ bracket_rounds = {}
+ for sid, s_data in bracket_series.items():
+ rnd = s_data['playoff_round']
+ if rnd not in bracket_rounds:
+ bracket_rounds[rnd] = []
+ bracket_rounds[rnd].append(s_data)
+
+ sorted_rounds = sorted(bracket_rounds.items())
+ bracket = [r[1] for r in sorted_rounds]
+
+ # Calendar generation logic
+ import calendar
+ today = datetime.now().strftime('%Y-%m-%d')
+
+ # Determine range of months to show
+ if games_list:
+ dates = [datetime.strptime(g['scheduled_date'], '%Y-%m-%d') for g in games_list if g['scheduled_date'] and g['scheduled_date'] != 'TBD']
+ if dates:
+ min_date = min(dates)
+ max_date = max(dates)
+ else:
+ min_date = datetime.now()
+ max_date = datetime.now()
+ else:
+ min_date = datetime.now()
+ max_date = datetime.now()
+
+ months = []
+ curr = datetime(min_date.year, min_date.month, 1)
+ while curr <= datetime(max_date.year, max_date.month, 1):
+ cal = calendar.Calendar(firstweekday=6) # Sunday start
+ weeks = cal.monthdayscalendar(curr.year, curr.month)
+ months.append({
+ 'name': curr.strftime('%B %Y'),
+ 'year': curr.year,
+ 'month': curr.month,
+ 'weeks': weeks
+ })
+ if curr.month == 12:
+ curr = datetime(curr.year + 1, 1, 1)
+ else:
+ curr = datetime(curr.year, curr.month + 1, 1)
+
+ conn.close()
+ return render_template('season.html',
+ seasons=seasons,
+ selected_season_id=season_id,
+ season_info=season_info,
+ standings=standings,
+ games_by_date=games_by_date,
+ playoff_games=playoff_games,
+ bracket=bracket,
+ months=months,
+ today=today,
+ all_teams=all_teams,
+ selected_team_id=team_id,
+ active_tab=tab,
+ user_season_status=user_season_status)
+
+def run_consensus_check(game_id, c):
+ # Consensus Check: Find most recent from each team
+ c.execute('SELECT away_team_id, home_team_id FROM Games WHERE id = ?', (game_id,))
+ g_teams = c.fetchone()
+ if not g_teams: return
+
+ c.execute('''
+ SELECT * FROM GameSubmissions
+ WHERE game_id = ? AND submitted_by_id = ?
+ ORDER BY id DESC LIMIT 1
+ ''', (game_id, g_teams['away_team_id']))
+ sub_away = c.fetchone()
+
+ c.execute('''
+ SELECT * FROM GameSubmissions
+ WHERE game_id = ? AND submitted_by_id = ?
+ ORDER BY id DESC LIMIT 1
+ ''', (game_id, g_teams['home_team_id']))
+ sub_home = c.fetchone()
+
+ if sub_away and sub_home:
+ # Check score consensus
+ if sub_away['away_score'] is not None and sub_home['away_score'] is not None and \
+ sub_away['home_score'] is not None and sub_home['home_score'] is not None and \
+ sub_away['away_score'] == sub_home['away_score'] and \
+ sub_away['home_score'] == sub_home['home_score']:
+
+ # Consensus reached on score
+ box_score_final = None
+ if sub_away['box_score_json'] and sub_home['box_score_json'] and sub_away['box_score_json'] == sub_home['box_score_json']:
+ bs = json.loads(sub_away['box_score_json'])
+ # bs["away"][10] is the 'runs' column (index 10)
+ if bs["away"][10] == sub_away['away_score'] and bs["home"][10] == sub_away['home_score']:
+ box_score_final = sub_away['box_score_json']
+
+ c.execute('''
+ UPDATE Games
+ SET away_score = ?, home_score = ?, box_score_json = ?, status = 'Final'
+ WHERE id = ?
+ ''', (sub_away['away_score'], sub_away['home_score'], box_score_final, game_id))
+
+ c.execute('DELETE FROM GameSubmissions WHERE game_id = ?', (game_id,))
+
+ check_and_trigger_playoff_updates(game_id, c)
+ flash('Scores agreed! Game finalized.')
+
+ # Check date consensus
+ if sub_away['proposed_date'] and sub_home['proposed_date'] and sub_away['proposed_date'] == sub_home['proposed_date']:
+ c.execute('UPDATE Games SET scheduled_date = ? WHERE id = ?', (sub_away['proposed_date'], game_id))
+ c.execute('DELETE FROM GameSubmissions WHERE game_id = ?', (game_id,))
+ flash('Date change agreed! Game rescheduled.')
+
+@app.route('/game/<int:game_id>/retract', methods=['POST'])
+@login_required
+def retract_submission(game_id):
+ conn = get_db()
+ c = conn.cursor()
+ c.execute('DELETE FROM GameSubmissions WHERE game_id = ? AND submitted_by_id = ?', (game_id, current_user.id))
+ conn.commit()
+ conn.close()
+ flash('Submission retracted.')
+ return redirect(url_for('game', game_id=game_id))
+
+@app.route('/game/<int:game_id>/agree/<int:submission_id>', methods=['POST'])
+@login_required
+def agree_submission(game_id, submission_id):
+ conn = get_db()
+ c = conn.cursor()
+
+ # Get the submission to agree with
+ c.execute('SELECT * FROM GameSubmissions WHERE id = ?', (submission_id,))
+ sub = c.fetchone()
+
+ if not sub or sub['game_id'] != game_id:
+ conn.close()
+ flash('Submission not found.')
+ return redirect(url_for('game', game_id=game_id))
+
+ if str(sub['submitted_by_id']) == str(current_user.id):
+ conn.close()
+ flash('You cannot agree with your own submission.')
+ return redirect(url_for('game', game_id=game_id))
+
+ # Create an identical submission for the current user to trigger consensus
+ c.execute('''
+ INSERT INTO GameSubmissions (game_id, submitted_by_id, away_score, home_score, box_score_json, proposed_date)
+ VALUES (?, ?, ?, ?, ?, ?)
+ ''', (game_id, current_user.id, sub['away_score'], sub['home_score'], sub['box_score_json'], sub['proposed_date']))
+
+ run_consensus_check(game_id, c)
+
+ conn.commit()
+ conn.close()
+ return redirect(url_for('game', game_id=game_id))
+@app.route('/login', methods=['GET', 'POST'])
+def login():
+ if request.method == 'POST':
+ username = request.form['username']
+ password = request.form['password']
+
+ conn = get_db()
+ c = conn.cursor()
+ c.execute('SELECT * FROM Users WHERE username = ?', (username,))
+ user_row = c.fetchone()
+ conn.close()
+
+ if user_row and check_password_hash(user_row['password_hash'], password):
+ user = User(user_row['id'], user_row['username'], user_row['is_admin'], user_row['team_name'], user_row['team_icon'])
+ login_user(user)
+ return redirect(url_for('index'))
+ else:
+ flash('Invalid username or password')
+
+ return render_template('login.html')
+
+@app.route('/logout')
+@login_required
+def logout():
+ logout_user()
+ return redirect(url_for('index'))
+
+@app.route('/game/<int:game_id>')
+def game(game_id):
+ conn = get_db()
+ c = conn.cursor()
+ c.execute('''
+ SELECT g.*,
+ a.team_name as away_team, a.team_icon as away_icon,
+ h.team_name as home_team, h.team_icon as home_icon
+ FROM Games g
+ LEFT JOIN Users a ON g.away_team_id = a.id
+ LEFT JOIN Users h ON g.home_team_id = h.id
+ WHERE g.id = ?
+ ''', (game_id,))
+ game_row = c.fetchone()
+
+ if not game_row:
+ conn.close()
+ return "Game not found", 404
+
+ game_dict = dict(game_row)
+ if game_dict['box_score_json']:
+ game_dict['box_score'] = json.loads(game_dict['box_score_json'])
+ else:
+ game_dict['box_score'] = None
+
+ # Fetch pending submissions
+ c.execute('''
+ SELECT s.*, u.username, u.team_name
+ FROM GameSubmissions s
+ JOIN Users u ON s.submitted_by_id = u.id
+ WHERE s.game_id = ?
+ ORDER BY s.id DESC
+ ''', (game_id,))
+ submissions_raw = c.fetchall()
+ submissions = []
+ for s in submissions_raw:
+ s_dict = dict(s)
+
+ # Hide components that match current game state
+ if s_dict['proposed_date'] == game_dict['scheduled_date']:
+ s_dict['proposed_date'] = None
+
+ if game_dict['status'] == 'Final':
+ if s_dict['away_score'] == game_dict['away_score'] and s_dict['home_score'] == game_dict['home_score']:
+ s_dict['away_score'] = None
+ s_dict['home_score'] = None
+
+ # Only show if there's still something to show
+ if s_dict['proposed_date'] or s_dict['away_score'] is not None:
+ if s_dict['box_score_json']:
+ s_dict['box_score'] = json.loads(s_dict['box_score_json'])
+ else:
+ s_dict['box_score'] = None
+ submissions.append(s_dict)
+
+ conn.close()
+ return render_template('game.html', game=game_dict, submissions=submissions)
+
+def generate_tbd_playoffs(season_id, playoff_teams, series_length, c):
+ # Generates TBD playoff series
+ # For N=4: Series 1 (Semi A), Series 2 (Semi B), Series 3 (Finals)
+ # For N=2: Series 1 (Finals)
+ games_to_create = []
+ wins_needed = (series_length // 2) + 1
+ start_date = datetime.now() + timedelta(days=30)
+
+ series_map = {
+ 2: [1], # Finals
+ 4: [1, 2, 3] # Semis then Finals
+ }
+
+ rounds_map = {
+ 2: {1: 1},
+ 4: {1: 1, 2: 1, 3: 2}
+ }
+
+ active_series = series_map.get(playoff_teams, [])
+ for sid in active_series:
+ rnd = rounds_map[playoff_teams][sid]
+ for g_num in range(1, series_length + 1):
+ is_conditional = 1 if g_num > wins_needed else 0
+ games_to_create.append((
+ season_id, None, None, 'TBD',
+ (start_date + timedelta(days=sid*7 + g_num)).strftime('%Y-%m-%d'),
+ 1, sid, rnd, g_num, is_conditional
+ ))
+
+ c.executemany('''
+ INSERT INTO Games (season_id, away_team_id, home_team_id, status, scheduled_date, is_playoff, series_id, playoff_round, playoff_game_num, is_conditional)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ ''', games_to_create)
+
+def check_and_trigger_playoff_updates(game_id, c):
+ c.execute('SELECT season_id, is_playoff, series_id, playoff_round FROM Games WHERE id = ?', (game_id,))
+ game = c.fetchone()
+ if not game: return
+ season_id = game['season_id']
+
+ if not game['is_playoff']:
+ # Regular season check
+ c.execute("SELECT COUNT(*) as c FROM Games WHERE season_id = ? AND is_playoff = 0 AND status != 'Final'", (season_id,))
+ if c.fetchone()['c'] == 0:
+ c.execute('SELECT playoff_teams FROM Seasons WHERE id = ?', (season_id,))
+ p_teams = c.fetchone()['playoff_teams'] or 0
+
+ c.execute('''
+ SELECT u.id
+ FROM SeasonTeams st
+ JOIN Users u ON st.user_id = u.id
+ LEFT JOIN Games g ON (g.home_team_id = u.id OR g.away_team_id = u.id)
+ AND g.season_id = st.season_id AND g.status = 'Final' AND g.is_playoff = 0
+ WHERE st.season_id = ? AND st.status = 'Approved'
+ GROUP BY u.id ORDER BY SUM(CASE WHEN (g.home_team_id = u.id AND g.home_score > g.away_score) OR (g.away_team_id = u.id AND g.away_score > g.home_score) THEN 1 ELSE 0 END) DESC,
+ SUM(CASE WHEN g.home_team_id = u.id THEN g.home_score ELSE g.away_score END) DESC
+ LIMIT ?
+ ''', (season_id, p_teams))
+ top_teams = [row['id'] for row in c.fetchall()]
+
+ if len(top_teams) == 2:
+ c.execute("UPDATE Games SET away_team_id = ?, home_team_id = ?, status = 'Scheduled' WHERE season_id = ? AND series_id = 1", (top_teams[1], top_teams[0], season_id))
+ elif len(top_teams) == 4:
+ # 1v4 (S1), 2v3 (S2)
+ c.execute("UPDATE Games SET away_team_id = ?, home_team_id = ?, status = 'Scheduled' WHERE season_id = ? AND series_id = 1", (top_teams[3], top_teams[0], season_id))
+ c.execute("UPDATE Games SET away_team_id = ?, home_team_id = ?, status = 'Scheduled' WHERE season_id = ? AND series_id = 2", (top_teams[2], top_teams[1], season_id))
+ else:
+ # Playoff advancement
+ c.execute('SELECT playoff_teams, playoff_series_length FROM Seasons WHERE id = ?', (season_id,))
+ s_info = c.fetchone()
+ wins_needed = (s_info['playoff_series_length'] // 2) + 1
+ sid = game['series_id']
+
+ c.execute('''
+ SELECT
+ SUM(CASE WHEN away_score > home_score THEN 1 ELSE 0 END) as aw,
+ SUM(CASE WHEN home_score > away_score THEN 1 ELSE 0 END) as hw,
+ MAX(away_team_id) as aid, MAX(home_team_id) as hid
+ FROM Games WHERE season_id = ? AND series_id = ? AND status = 'Final'
+ ''', (season_id, sid))
+ res = c.fetchone()
+
+ winner = None
+ if res['aw'] >= wins_needed: winner = res['aid']
+ elif res['hw'] >= wins_needed: winner = res['hid']
+
+ if winner:
+ c.execute("UPDATE Games SET status = 'Canceled' WHERE season_id = ? AND series_id = ? AND status IN ('Scheduled', 'TBD')", (season_id, sid))
+ if s_info['playoff_teams'] == 4:
+ if sid == 1: c.execute("UPDATE Games SET away_team_id = ? WHERE season_id = ? AND series_id = 3", (winner, season_id))
+ elif sid == 2: c.execute("UPDATE Games SET home_team_id = ? WHERE season_id = ? AND series_id = 3", (winner, season_id))
+ c.execute("UPDATE Games SET status = 'Scheduled' WHERE season_id = ? AND series_id = 3 AND away_team_id IS NOT NULL AND home_team_id IS NOT NULL AND status = 'TBD'", (season_id,))
+
+@app.route('/submit_game/<int:game_id>', methods=['POST'])
+@login_required
+def submit_game(game_id):
+ away_score = request.form.get('away_score')
+ home_score = request.form.get('home_score')
+ proposed_date = request.form.get('proposed_date')
+
+ # Collect Box Score from grid (Optional)
+ box_score = {"away": [], "home": []}
+ cols = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "extra", "runs", "hits", "errors"]
+ has_box_data = False
+ for team in ["away", "home"]:
+ for col in cols:
+ val = request.form.get(f"{team}_inn_{col}")
+ if val and val != "0" and val != "": has_box_data = True
+ box_score[team].append(int(val) if val and val.isdigit() else 0)
+
+ box_score_json = json.dumps(box_score) if has_box_data else None
+
+ conn = get_db()
+ c = conn.cursor()
+
+ # Check if user is part of the game
+ c.execute('SELECT away_team_id, home_team_id, status FROM Games WHERE id = ?', (game_id,))
+ game = c.fetchone()
+
+ if not game or game['status'] == 'Final':
+ conn.close()
+ flash('Game not found or already finalized.')
+ return redirect(url_for('game', game_id=game_id))
+
+ if str(current_user.id) not in (str(game['away_team_id']), str(game['home_team_id'])):
+ conn.close()
+ flash('You are not authorized to submit for this game.')
+ return redirect(url_for('game', game_id=game_id))
+
+ # Save submission (Update existing or Insert new)
+ away_score_val = int(away_score) if away_score else None
+ home_score_val = int(home_score) if home_score else None
+
+ c.execute('SELECT * FROM GameSubmissions WHERE game_id = ? AND submitted_by_id = ?', (game_id, current_user.id))
+ existing = c.fetchone()
+
+ if existing:
+ if away_score_val is not None:
+ c.execute('''
+ UPDATE GameSubmissions
+ SET away_score = ?, home_score = ?, box_score_json = ?
+ WHERE id = ?
+ ''', (away_score_val, home_score_val, box_score_json, existing['id']))
+ if proposed_date:
+ c.execute('''
+ UPDATE GameSubmissions
+ SET proposed_date = ?
+ WHERE id = ?
+ ''', (proposed_date, existing['id']))
+ else:
+ c.execute('''
+ INSERT INTO GameSubmissions (game_id, submitted_by_id, away_score, home_score, box_score_json, proposed_date)
+ VALUES (?, ?, ?, ?, ?, ?)
+ ''', (game_id, current_user.id, away_score_val, home_score_val, box_score_json, proposed_date))
+
+ run_consensus_check(game_id, c)
+
+ conn.commit()
+ conn.close()
+ flash('Submission recorded.')
+ return redirect(url_for('game', game_id=game_id))
+
+@app.route('/team', methods=['GET', 'POST'])
+@login_required
+def team():
+ if request.method == 'POST':
+ conn = get_db()
+ c = conn.cursor()
+
+ new_team_name = request.form.get('team_name')
+ if new_team_name:
+ c.execute('UPDATE Users SET team_name = ? WHERE id = ?', (new_team_name, current_user.id))
+ current_user.team_name = new_team_name
+
+ new_password = request.form.get('new_password')
+ if new_password:
+ c.execute('UPDATE Users SET password_hash = ? WHERE id = ?', (generate_password_hash(new_password), current_user.id))
+
+ icon_file = request.files.get('team_icon')
+ if icon_file and icon_file.filename != '':
+ filename = secure_filename(icon_file.filename)
+ filepath = os.path.join(app.config['UPLOAD_FOLDER'], f"{current_user.id}_{filename}")
+ icon_file.save(filepath)
+ c.execute('UPDATE Users SET team_icon = ? WHERE id = ?', (f"{current_user.id}_{filename}", current_user.id))
+ current_user.team_icon = f"{current_user.id}_{filename}"
+
+ conn.commit()
+ conn.close()
+ flash('Team profile updated successfully.')
+ return redirect(url_for('team'))
+
+ conn = get_db()
+ c = conn.cursor()
+ c.execute('SELECT * FROM Seasons')
+ seasons = c.fetchall()
+
+ c.execute('SELECT season_id, status FROM SeasonTeams WHERE user_id = ?', (current_user.id,))
+ my_seasons = {row['season_id']: row['status'] for row in c.fetchall()}
+ conn.close()
+
+ return render_template('team.html', seasons=seasons, my_seasons=my_seasons)
+
+@app.route('/team/join_season/<int:season_id>', methods=['POST'])
+@login_required
+def join_season(season_id):
+ if current_user.is_admin:
+ flash('Admins cannot join seasons as teams.')
+ return redirect(url_for('team'))
+
+ conn = get_db()
+ c = conn.cursor()
+ try:
+ c.execute('INSERT INTO SeasonTeams (season_id, user_id, status) VALUES (?, ?, ?)', (season_id, current_user.id, 'Pending'))
+ conn.commit()
+ flash('Requested to join season.')
+ except sqlite3.IntegrityError:
+ flash('Already requested or joined this season.')
+ finally:
+ conn.close()
+
+ return redirect(url_for('index', season_id=season_id))
+
+@app.route('/admin', methods=['GET', 'POST'])
+@login_required
+def admin():
+ if not current_user.is_admin:
+ return "Access Denied", 403
+
+ conn = get_db()
+ c = conn.cursor()
+
+ if request.method == 'POST':
+ action = request.form.get('action')
+ if action == 'create_season':
+ name = request.form['name']
+ c.execute('INSERT INTO Seasons (name) VALUES (?)', (name,))
+ conn.commit()
+ flash('Season created.')
+
+ elif action == 'create_team':
+ username = request.form['username']
+ password = request.form['password']
+ team_name = request.form['team_name']
+
+ try:
+ c.execute('''
+ INSERT INTO Users (username, password_hash, team_name)
+ VALUES (?, ?, ?)
+ ''', (username, generate_password_hash(password), team_name))
+ conn.commit()
+ flash('Team created.')
+ except sqlite3.IntegrityError:
+ flash('Username already exists.')
+
+ c.execute('SELECT * FROM Seasons')
+ seasons = c.fetchall()
+
+ c.execute('SELECT * FROM Users WHERE is_admin = 0')
+ teams = c.fetchall()
+
+ conn.close()
+ return render_template('admin.html', seasons=seasons, teams=teams)
+
+@app.route('/admin/season/<int:season_id>/manage_teams', methods=['GET', 'POST'])
+@login_required
+def admin_manage_teams(season_id):
+ if not current_user.is_admin:
+ return "Access Denied", 403
+
+ conn = get_db()
+ c = conn.cursor()
+
+ if request.method == 'POST':
+ action = request.form.get('action')
+ user_id = request.form.get('user_id')
+
+ if action == 'approve':
+ c.execute("UPDATE SeasonTeams SET status = 'Approved' WHERE season_id = ? AND user_id = ?", (season_id, user_id))
+ flash('Team approved.')
+ elif action == 'deny' or action == 'remove':
+ c.execute("DELETE FROM SeasonTeams WHERE season_id = ? AND user_id = ?", (season_id, user_id))
+ flash(f"Team {action}d.")
+ elif action == 'add':
+ team_ids = request.form.getlist('team_ids')
+ for tid in team_ids:
+ try:
+ c.execute("INSERT INTO SeasonTeams (season_id, user_id, status) VALUES (?, ?, 'Approved')", (season_id, tid))
+ except sqlite3.IntegrityError:
+ c.execute("UPDATE SeasonTeams SET status = 'Approved' WHERE season_id = ? AND user_id = ?", (season_id, tid))
+ flash('Selected teams added directly.')
+
+ conn.commit()
+ return redirect(url_for('admin_manage_teams', season_id=season_id))
+
+ c.execute('SELECT * FROM Seasons WHERE id = ?', (season_id,))
+ season = c.fetchone()
+
+ c.execute('SELECT * FROM Users WHERE is_admin = 0')
+ all_teams = c.fetchall()
+
+ c.execute('''
+ SELECT u.id, u.team_name, u.username, st.status
+ FROM SeasonTeams st
+ JOIN Users u ON st.user_id = u.id
+ WHERE st.season_id = ?
+ ''', (season_id,))
+ enrolled_data = c.fetchall()
+
+ pending_teams = [t for t in enrolled_data if t['status'] == 'Pending']
+ approved_teams = [t for t in enrolled_data if t['status'] == 'Approved']
+ enrolled_ids = [t['id'] for t in enrolled_data]
+
+ conn.close()
+ return render_template('admin_manage_teams.html', season=season, all_teams=all_teams, pending_teams=pending_teams, approved_teams=approved_teams, enrolled_ids=enrolled_ids)
+
+@app.route('/admin/season/<int:season_id>/delete', methods=['POST'])
+@login_required
+def admin_delete_season(season_id):
+ if not current_user.is_admin:
+ return "Access Denied", 403
+
+ conn = get_db()
+ c = conn.cursor()
+ c.execute('DELETE FROM GameSubmissions WHERE game_id IN (SELECT id FROM Games WHERE season_id = ?)', (season_id,))
+ c.execute('DELETE FROM Games WHERE season_id = ?', (season_id,))
+ c.execute('DELETE FROM SeasonTeams WHERE season_id = ?', (season_id,))
+ c.execute('DELETE FROM Seasons WHERE id = ?', (season_id,))
+ conn.commit()
+ conn.close()
+ flash('Season deleted.')
+ return redirect(url_for('admin'))
+
+@app.route('/admin/season/<int:season_id>/generate_schedule', methods=['POST'])
+@login_required
+def admin_generate_schedule(season_id):
+ if not current_user.is_admin:
+ return "Access Denied", 403
+
+ confirm = request.form.get('confirm') == 'true'
+ total_games_per_team = int(request.form['total_games'])
+ gpw = int(request.form['games_per_week'])
+ p_teams = int(request.form['playoff_teams'])
+ p_series_len = int(request.form['playoff_series_length'])
+
+ conn = get_db()
+ c = conn.cursor()
+
+ # Check if any games are finalized
+ c.execute("SELECT COUNT(*) as c FROM Games WHERE season_id = ? AND status = 'Final'", (season_id,))
+ final_count = c.fetchone()['c']
+
+ if final_count > 0 and not confirm:
+ conn.close()
+ return {"status": "confirm_required", "message": f"There are {final_count} finalized games. Regenerating will delete ALL games. Proceed?"}, 200
+
+ # Update Season Parameters
+ c.execute('''
+ UPDATE Seasons
+ SET total_games = ?, games_per_week = ?, playoff_teams = ?, playoff_series_length = ?
+ WHERE id = ?
+ ''', (total_games_per_team, gpw, p_teams, p_series_len, season_id))
+
+ c.execute('SELECT user_id FROM SeasonTeams WHERE season_id = ? AND status = "Approved"', (season_id,))
+ team_ids = [row['user_id'] for row in c.fetchall()]
+
+ if len(team_ids) < 2:
+ conn.close()
+ flash('Not enough approved teams to generate schedule.')
+ return redirect(url_for('admin'))
+
+ # Delete ALL existing games for this season
+ c.execute("DELETE FROM Games WHERE season_id = ?", (season_id,))
+ c.execute("DELETE FROM GameSubmissions WHERE game_id NOT IN (SELECT id FROM Games)")
+
+ # Per-team schedule generation using the circle method
+ num_teams = len(team_ids)
+
+ if num_teams % 2 != 0:
+ team_ids.append(None) # Add a bye team
+ num_teams += 1
+
+ games_count = {tid: 0 for tid in team_ids if tid is not None}
+ current_teams = list(team_ids)
+ games_to_create = []
+ start_date = datetime.now()
+
+ round_num = 0
+ while any(count < total_games_per_team for count in games_count.values()):
+ for i in range(num_teams // 2):
+ team1 = current_teams[i]
+ team2 = current_teams[num_teams - 1 - i]
+
+ if team1 is not None and team2 is not None:
+ if games_count[team1] < total_games_per_team and games_count[team2] < total_games_per_team:
+ if round_num % 2 == 0: away_id, home_id = team1, team2
+ else: away_id, home_id = team2, team1
+
+ day_offset = int((round_num * 7) / gpw)
+ game_date = (start_date + timedelta(days=day_offset)).strftime('%Y-%m-%d')
+ games_to_create.append((season_id, away_id, home_id, game_date))
+ games_count[team1] += 1
+ games_count[team2] += 1
+
+ current_teams = [current_teams[0]] + [current_teams[-1]] + current_teams[1:-1]
+ round_num += 1
+
+ c.executemany('''
+ INSERT INTO Games (season_id, away_team_id, home_team_id, scheduled_date)
+ VALUES (?, ?, ?, ?)
+ ''', games_to_create)
+
+ # Also generate Playoff TBDs
+ generate_tbd_playoffs(season_id, p_teams, p_series_len, c)
+
+ conn.commit()
+ conn.close()
+ flash(f'Schedule and Playoffs generated.')
+ if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
+ return {"status": "success", "redirect": url_for('admin')}
+ return redirect(url_for('admin'))
+
+@app.route('/admin/game/<int:game_id>/edit', methods=['GET', 'POST'])
+@login_required
+def admin_edit_game(game_id):
+ if not current_user.is_admin:
+ return "Access Denied", 403
+
+ conn = get_db()
+ c = conn.cursor()
+
+ if request.method == 'POST':
+ scheduled_date = request.form.get('scheduled_date')
+ status = request.form.get('status')
+ away_score = request.form.get('away_score')
+ home_score = request.form.get('home_score')
+
+ # Collect Box Score from grid
+ box_score = {"away": [], "home": []}
+ cols = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "extra", "runs", "hits", "errors"]
+ has_box_data = False
+ for team in ["away", "home"]:
+ for col in cols:
+ val = request.form.get(f"{team}_inn_{col}")
+ if val and val != "0" and val != "": has_box_data = True
+ box_score[team].append(int(val) if val and val.isdigit() else 0)
+
+ box_score_json = json.dumps(box_score) if has_box_data else None
+
+ c.execute('''
+ UPDATE Games
+ SET scheduled_date = ?, status = ?, away_score = ?, home_score = ?, box_score_json = ?
+ WHERE id = ?
+ ''', (scheduled_date, status, away_score if away_score else None, home_score if home_score else None, box_score_json, game_id))
+
+ if status == 'Final':
+ check_and_trigger_playoff_updates(game_id, c)
+ c.execute('DELETE FROM GameSubmissions WHERE game_id = ?', (game_id,))
+
+ conn.commit()
+ flash('Game updated.')
+ return redirect(url_for('game', game_id=game_id))
+
+ c.execute('''
+ SELECT g.*, a.team_name as away_team, h.team_name as home_team
+ FROM Games g
+ LEFT JOIN Users a ON g.away_team_id = a.id
+ LEFT JOIN Users h ON g.home_team_id = h.id
+ WHERE g.id = ?
+ ''', (game_id,))
+ game_row = c.fetchone()
+ if not game_row:
+ conn.close()
+ return "Game not found", 404
+
+ game_dict = dict(game_row)
+ if game_dict['box_score_json']:
+ game_dict['box_score'] = json.loads(game_dict['box_score_json'])
+ else:
+ game_dict['box_score'] = None
+
+ conn.close()
+ return render_template('admin_edit_game.html', game=game_dict)
+
+if __name__ == '__main__':
+ app.run(debug=True, port=5000) \ No newline at end of file
diff --git a/db_builder.py b/db_builder.py
new file mode 100644
index 0000000..ed4934b
--- /dev/null
+++ b/db_builder.py
@@ -0,0 +1,124 @@
+import sqlite3
+import json
+from werkzeug.security import generate_password_hash
+
+DB_FILE = 'orchard_league.db'
+
+def get_db():
+ conn = sqlite3.connect(DB_FILE)
+ conn.row_factory = sqlite3.Row
+ return conn
+
+def init_db():
+ conn = get_db()
+ c = conn.cursor()
+
+ # Drop existing tables
+ c.execute('DROP TABLE IF EXISTS GameSubmissions')
+ c.execute('DROP TABLE IF EXISTS Games')
+ c.execute('DROP TABLE IF EXISTS SeasonTeams')
+ c.execute('DROP TABLE IF EXISTS Seasons')
+ c.execute('DROP TABLE IF EXISTS Users')
+
+ # Users Table
+ c.execute('''
+ CREATE TABLE Users (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ username TEXT UNIQUE NOT NULL,
+ password_hash TEXT NOT NULL,
+ is_admin BOOLEAN NOT NULL DEFAULT 0,
+ team_name TEXT,
+ team_icon TEXT
+ )
+ ''')
+
+ # Seasons Table
+ c.execute('''
+ CREATE TABLE Seasons (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ name TEXT NOT NULL,
+ status TEXT NOT NULL DEFAULT 'Scheduled', -- 'Scheduled', 'Active', 'Completed'
+ total_games INTEGER,
+ games_per_week INTEGER,
+ playoff_teams INTEGER,
+ playoff_series_length INTEGER
+ )
+ ''')
+
+ # SeasonTeams Junction Table
+ c.execute('''
+ CREATE TABLE SeasonTeams (
+ season_id INTEGER,
+ user_id INTEGER,
+ status TEXT NOT NULL DEFAULT 'Pending', -- 'Pending', 'Approved'
+ PRIMARY KEY (season_id, user_id),
+ FOREIGN KEY (season_id) REFERENCES Seasons(id),
+ FOREIGN KEY (user_id) REFERENCES Users(id)
+ )
+ ''')
+
+ # Games Table
+ c.execute('''
+ CREATE TABLE Games (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ season_id INTEGER,
+ away_team_id INTEGER,
+ home_team_id INTEGER,
+ scheduled_date TEXT,
+ status TEXT NOT NULL DEFAULT 'Scheduled', -- 'Scheduled', 'Final', 'TBD'
+ away_score INTEGER,
+ home_score INTEGER,
+ box_score_json TEXT,
+ is_playoff BOOLEAN NOT NULL DEFAULT 0,
+ series_id INTEGER,
+ playoff_round INTEGER,
+ playoff_game_num INTEGER,
+ is_conditional BOOLEAN NOT NULL DEFAULT 0,
+ FOREIGN KEY (season_id) REFERENCES Seasons(id),
+ FOREIGN KEY (away_team_id) REFERENCES Users(id),
+ FOREIGN KEY (home_team_id) REFERENCES Users(id)
+ )
+ ''')
+
+ # GameSubmissions Table
+ c.execute('''
+ CREATE TABLE GameSubmissions (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ game_id INTEGER,
+ submitted_by_id INTEGER,
+ away_score INTEGER,
+ home_score INTEGER,
+ box_score_json TEXT,
+ proposed_date TEXT,
+ FOREIGN KEY (game_id) REFERENCES Games(id),
+ FOREIGN KEY (submitted_by_id) REFERENCES Users(id)
+ )
+ ''')
+
+ # Create Initial Admin User
+ admin_password = generate_password_hash('admin')
+ c.execute('''
+ INSERT INTO Users (username, password_hash, is_admin, team_name)
+ VALUES (?, ?, 1, ?)
+ ''', ('admin', admin_password, 'The Adminators'))
+
+ # Create 8 Placeholder Teams
+ teams = [
+ "Yankees", "Giants", "Dodgers", "Cubs",
+ "Red Sox", "Cardinals", "White Sox", "Braves"
+ ]
+ test_password = generate_password_hash('test')
+ for team in teams:
+ username = f"test_{team.lower().replace(' ', '_')}"
+ team_name = f"Test {team}"
+ c.execute('''
+ INSERT INTO Users (username, password_hash, is_admin, team_name)
+ VALUES (?, ?, 0, ?)
+ ''', (username, test_password, team_name))
+
+ conn.commit()
+ conn.close()
+ print("Database initialized successfully.")
+
+if __name__ == '__main__':
+ init_db() \ No newline at end of file
diff --git a/orchard_league.db b/orchard_league.db
new file mode 100644
index 0000000..88bd44f
--- /dev/null
+++ b/orchard_league.db
Binary files differ
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..40801be
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,3 @@
+Flask==3.0.3
+Flask-Login==0.6.3
+Werkzeug==3.0.3 \ No newline at end of file
diff --git a/static/css/style.css b/static/css/style.css
new file mode 100644
index 0000000..51ecbfe
--- /dev/null
+++ b/static/css/style.css
@@ -0,0 +1,372 @@
+:root {
+ --bg-primary: #e0e0e0; /* Light newspaper grey */
+ --bg-secondary: #f4f4f4; /* Lighter grey for content areas */
+ --text-primary: #2b2b2b; /* Dark grey for main text */
+ --text-accent: #111111; /* Almost black for headers */
+ --deco-border: #555555; /* Medium grey for borders */
+ --deco-highlight: #777777; /* Subtle highlight grey */
+ --deco-shadow: #aaaaaa; /* Soft shadow */
+ --newspaper-font: 'Times New Roman', 'Georgia', serif;
+ --sans-font: 'Times New Roman', 'Helvetica Neue', sans-serif;
+}
+
+@font-face {
+ font-family: 'Powell and Geary';
+ src: url('../ttf/Powell and Geary.ttf') format('truetype');
+}
+
+body {
+ font-family: var(--newspaper-font);
+ background-color: var(--bg-primary);
+ color: var(--text-primary);
+ margin: 0;
+ padding: 0;
+ line-height: 1.5;
+}
+
+header {
+ background-color: #ffffff;
+ border-bottom: 5px solid var(--text-accent);
+ border-top: 10px solid var(--text-accent);
+ padding: 1rem 2rem;
+ margin-bottom: 1rem;
+ position: relative;
+}
+
+.logo-container {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 2rem;
+}
+
+.logo-container img {
+ height: 8rem; /* Increased size */
+ width: auto;
+}
+
+.logo-container .text-wrap {
+ text-align: left;
+}
+
+.logo-container h1 {
+ font-family: 'Powell and Geary', var(--sans-font);
+ font-size: 5rem;
+ margin: 0;
+ text-transform: uppercase;
+ letter-spacing: 2px;
+ color: var(--text-accent);
+ font-weight: 900;
+ line-height: 1;
+}
+
+.logo-container .subtitle {
+ font-size: 1.2rem;
+ color: var(--deco-border);
+ text-transform: uppercase;
+ letter-spacing: 1px;
+ margin-top: 0.2rem;
+ font-style: italic;
+ font-family: var(--sans-font);
+ line-height: 1;
+}
+
+nav {
+ margin-top: 1rem;
+ display: flex;
+ justify-content: center;
+ gap: 2rem;
+ border-top: 1px solid var(--deco-border);
+ border-bottom: 1px solid var(--deco-border);
+ padding: 0.5rem 0;
+ background-color: #ffffff;
+}
+
+nav a {
+ color: var(--text-accent);
+ text-decoration: none;
+ text-transform: uppercase;
+ font-weight: bold;
+ font-family: var(--sans-font);
+ font-size: 0.9rem;
+ letter-spacing: 1px;
+}
+
+nav a:hover {
+ text-decoration: underline;
+ color: #000;
+}
+
+main {
+ max-width: 1000px;
+ margin: 0 auto;
+ padding: 2rem;
+ background-color: var(--bg-secondary);
+ border: 1px solid var(--deco-border);
+ box-shadow: 2px 2px 5px var(--deco-shadow);
+}
+
+h2, h3, h4 {
+ font-family: var(--sans-font);
+ color: var(--text-accent);
+ text-transform: uppercase;
+ border-bottom: 2px solid var(--text-accent);
+ padding-bottom: 0.5rem;
+ margin-top: 1.5rem;
+}
+
+table {
+ width: 100%;
+ border-collapse: collapse;
+ margin-top: 1rem;
+ font-family: var(--sans-font);
+}
+
+table th, table td {
+ border: 1px solid var(--deco-border);
+ padding: 8px;
+ text-align: center;
+}
+
+table th {
+ background-color: #dddddd;
+ color: var(--text-accent);
+ text-transform: uppercase;
+ font-size: 0.85rem;
+ font-weight: bold;
+}
+
+table tr:nth-child(even) {
+ background-color: #eeeeee;
+}
+
+form {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+ max-width: 400px;
+ font-family: var(--sans-font);
+}
+
+label {
+ font-weight: bold;
+ color: var(--text-accent);
+ font-size: 0.9rem;
+}
+
+input[type="text"],
+input[type="password"],
+input[type="number"],
+select,
+textarea {
+ padding: 8px;
+ background-color: #ffffff;
+ border: 1px solid var(--deco-border);
+ color: var(--text-primary);
+ font-size: 1rem;
+ font-family: var(--sans-font);
+}
+
+input:focus, select:focus, textarea:focus {
+ outline: none;
+ border-color: var(--text-accent);
+}
+
+button {
+ background-color: var(--text-accent);
+ color: #ffffff;
+ border: none;
+ padding: 8px 16px;
+ text-transform: uppercase;
+ font-weight: bold;
+ font-family: var(--sans-font);
+ cursor: pointer;
+ transition: background-color 0.2s;
+}
+
+button:hover {
+ background-color: #333333;
+}
+
+footer {
+ text-align: center;
+ padding: 1.5rem;
+ font-family: var(--sans-font);
+ color: var(--deco-highlight);
+ font-size: 0.8rem;
+ margin-top: 2rem;
+ border-top: 1px solid var(--deco-border);
+}
+
+.flash-messages {
+ list-style: none;
+ padding: 0;
+ margin-bottom: 2rem;
+}
+
+.flash-messages li {
+ background-color: #f8f8f8;
+ border-left: 4px solid var(--text-accent);
+ padding: 10px;
+ color: var(--text-primary);
+ font-family: var(--sans-font);
+ font-weight: bold;
+ margin-bottom: 0.5rem;
+}
+
+.conditional-game {
+ color: #555;
+ font-style: italic;
+}
+
+/* Calendar Styles */
+.calendar-month {
+ background: #fff;
+ border: 1px solid var(--deco-border);
+ margin-bottom: 2rem;
+ padding: 1rem;
+}
+
+.calendar-month h4 {
+ text-align: center;
+ margin-top: 0;
+ border-bottom: 1px solid #ddd;
+}
+
+.calendar-grid {
+ display: grid;
+ grid-template-columns: repeat(7, 1fr);
+ border-top: 1px solid #ddd;
+ border-left: 1px solid #ddd;
+}
+
+.calendar-day-head {
+ background: #eee;
+ font-weight: bold;
+ text-align: center;
+ padding: 5px;
+ border-right: 1px solid #ddd;
+ border-bottom: 1px solid #ddd;
+ font-size: 0.8rem;
+ text-transform: uppercase;
+}
+
+.calendar-day {
+ min-height: 100px;
+ border-right: 1px solid #ddd;
+ border-bottom: 1px solid #ddd;
+ padding: 5px;
+ background: #fff;
+}
+
+.calendar-day.other-month {
+ background: #f9f9f9;
+}
+
+.calendar-day.today {
+ border: 3px solid #cc0000 !important;
+}
+
+.calendar-day-num {
+ font-weight: bold;
+ font-size: 0.9rem;
+ margin-bottom: 5px;
+ display: block;
+}
+
+.calendar-game-item {
+ font-size: 0.7rem;
+ background: #f0f0f0;
+ margin-bottom: 3px;
+ padding: 3px;
+ border: 1px solid #ccc;
+ cursor: pointer;
+ border-radius: 3px;
+}
+
+.calendar-game-item:hover {
+ background: #e0e0e0;
+}
+
+.team-icon-small {
+ width: 18px;
+ height: 18px;
+ border-radius: 50%;
+ vertical-align: middle;
+}
+
+.team-icon-highlight {
+ border: 2px solid gold !important;
+ box-shadow: 0 0 5px gold;
+}
+
+/* Warning Triangle */
+.pending-icon {
+ color: #cc9900;
+ font-weight: bold;
+ font-size: 1.2rem;
+ margin-left: 5px;
+ cursor: help;
+}
+
+/* Bracket Styles */
+.bracket-container {
+ display: flex;
+ flex-direction: row;
+ gap: 2rem;
+ padding: 2rem;
+ overflow-x: auto;
+ background: #fff;
+ border: 1px solid var(--deco-border);
+ margin-bottom: 2rem;
+}
+
+.bracket-round {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-around;
+ gap: 1rem;
+ min-width: 200px;
+}
+
+.bracket-game {
+ border: 1px solid #ccc;
+ background: #f9f9f9;
+ padding: 0.5rem;
+ font-size: 0.8rem;
+ position: relative;
+}
+
+.bracket-game.winner {
+ border-left: 4px solid var(--text-accent);
+}
+
+.bracket-team {
+ display: flex;
+ justify-content: space-between;
+ padding: 2px 0;
+}
+
+.clinch-marker {
+ color: #cc0000;
+ font-weight: bold;
+ margin-left: 5px;
+}
+
+/* Side Images */
+.side-image {
+ position: fixed;
+ top: 0;
+ height: 100vh;
+ width: auto;
+ z-index: -1;
+ pointer-events: none;
+}
+
+.side-image.left {
+ left: 0;
+}
+
+.side-image.right {
+ right: 0;
+} \ No newline at end of file
diff --git a/static/img/logo.png b/static/img/logo.png
new file mode 100644
index 0000000..6e4f7b7
--- /dev/null
+++ b/static/img/logo.png
Binary files differ
diff --git a/static/img/orchards_left.png b/static/img/orchards_left.png
new file mode 100644
index 0000000..7fbedc9
--- /dev/null
+++ b/static/img/orchards_left.png
Binary files differ
diff --git a/static/img/orchards_right.png b/static/img/orchards_right.png
new file mode 100644
index 0000000..955117f
--- /dev/null
+++ b/static/img/orchards_right.png
Binary files differ
diff --git a/static/ttf/Decort.ttf b/static/ttf/Decort.ttf
new file mode 100644
index 0000000..6c765cc
--- /dev/null
+++ b/static/ttf/Decort.ttf
Binary files differ
diff --git a/static/ttf/Powell and Geary.ttf b/static/ttf/Powell and Geary.ttf
new file mode 100644
index 0000000..7fd7fb3
--- /dev/null
+++ b/static/ttf/Powell and Geary.ttf
Binary files differ
diff --git a/static/uploads/7_drahdiwaberl.png b/static/uploads/7_drahdiwaberl.png
new file mode 100644
index 0000000..9bea221
--- /dev/null
+++ b/static/uploads/7_drahdiwaberl.png
Binary files differ
diff --git a/templates/admin.html b/templates/admin.html
new file mode 100644
index 0000000..19c7bd9
--- /dev/null
+++ b/templates/admin.html
@@ -0,0 +1,138 @@
+{% extends "base.html" %}
+
+{% block content %}
+ <h2>League Administration</h2>
+
+ <div style="display: flex; gap: 2rem; flex-wrap: wrap;">
+ <div style="flex: 1; min-width: 300px;">
+ <h3>Create New Season</h3>
+ <form method="POST">
+ <input type="hidden" name="action" value="create_season">
+ <div>
+ <label for="name">Season Name:</label>
+ <input type="text" id="name" name="name" required>
+ </div>
+ <button type="submit">Create Season</button>
+ </form>
+ </div>
+
+ <div style="flex: 1; min-width: 300px;">
+ <h3>Create Team/User</h3>
+ <form method="POST">
+ <input type="hidden" name="action" value="create_team">
+ <div>
+ <label for="username">Username:</label>
+ <input type="text" id="username" name="username" required>
+ </div>
+ <div>
+ <label for="password">Password:</label>
+ <input type="password" id="password" name="password" required>
+ </div>
+ <div>
+ <label for="team_name">Team Name:</label>
+ <input type="text" id="team_name" name="team_name" required>
+ </div>
+ <button type="submit">Create Team</button>
+ </form>
+ </div>
+ </div>
+
+ <div style="margin-top: 2rem;">
+ <h3>Manage Seasons</h3>
+ {% if seasons %}
+ <table>
+ <tr>
+ <th>ID</th>
+ <th>Name</th>
+ <th>Status</th>
+ <th>Actions</th>
+ </tr>
+ {% for season in seasons %}
+ <tr>
+ <td>{{ season.id }}</td>
+ <td>{{ season.name }}</td>
+ <td>{{ season.status }}</td>
+ <td>
+ <form method="GET" action="{{ url_for('admin_manage_teams', season_id=season.id) }}" style="display:inline;">
+ <button type="submit">Manage Teams</button>
+ </form>
+ <button onclick="toggleScheduleForm({{ season.id }})">Generate Schedule/Playoffs</button>
+
+ <div id="form-{{ season.id }}" style="display:none; background: #f0f0f0; padding: 1rem; border: 1px solid #ccc; margin-top: 10px; text-align: left;">
+ <form id="sched-form-{{ season.id }}" onsubmit="submitGenerate(event, {{ season.id }})">
+ <div>
+ <label>Total Games (per team):</label>
+ <input type="number" name="total_games" value="10" required style="width: 60px;">
+ </div>
+ <div>
+ <label>Games Per Week:</label>
+ <input type="number" name="games_per_week" value="3" required style="width: 60px;">
+ </div>
+ <div>
+ <label>Playoff Teams (2 or 4):</label>
+ <input type="number" name="playoff_teams" value="4" required style="width: 60px;">
+ </div>
+ <div>
+ <label>Series Length (1, 3, 5):</label>
+ <input type="number" name="playoff_series_length" value="3" required style="width: 60px;">
+ </div>
+ <button type="submit" style="margin-top: 10px;">Generate</button>
+ </form>
+ </div>
+ <form method="POST" action="{{ url_for('admin_delete_season', season_id=season.id) }}" style="display:inline;" onsubmit="return confirm('Are you sure you want to delete this season and all its games?')">
+ <button type="submit" style="background-color: #cc0000; color: white;">Delete</button>
+ </form>
+ </td>
+ </tr>
+ {% endfor %}
+ </table>
+
+ <script>
+ function toggleScheduleForm(id) {
+ var f = document.getElementById('form-' + id);
+ f.style.display = f.style.display === 'none' ? 'block' : 'none';
+ }
+
+ function submitGenerate(event, id, confirmed = false) {
+ event.preventDefault();
+ var form = document.getElementById('sched-form-' + id);
+ var formData = new FormData(form);
+ if (confirmed) formData.append('confirm', 'true');
+
+ fetch('/admin/season/' + id + '/generate_schedule', {
+ method: 'POST',
+ body: formData,
+ headers: {'X-Requested-With': 'XMLHttpRequest'}
+ })
+ .then(response => response.json())
+ .then(data => {
+ if (data.status === 'confirm_required') {
+ if (confirm(data.message)) {
+ submitGenerate(event, id, true);
+ }
+ } else if (data.status === 'success') {
+ window.location.href = data.redirect;
+ } else {
+ alert('An error occurred.');
+ }
+ });
+ }
+ </script>
+ {% else %}
+ <p>No seasons created yet.</p>
+ {% endif %}
+ </div>
+
+ <div style="margin-top: 2rem;">
+ <h3>Registered Teams</h3>
+ {% if teams %}
+ <ul>
+ {% for team in teams %}
+ <li>{{ team.team_name }} ({{ team.username }})</li>
+ {% endfor %}
+ </ul>
+ {% else %}
+ <p>No teams registered yet.</p>
+ {% endif %}
+ </div>
+{% endblock %} \ No newline at end of file
diff --git a/templates/admin_edit_game.html b/templates/admin_edit_game.html
new file mode 100644
index 0000000..2651185
--- /dev/null
+++ b/templates/admin_edit_game.html
@@ -0,0 +1,66 @@
+{% extends "base.html" %}
+
+{% block content %}
+ <h2>Admin: Edit Game {{ game.id }}</h2>
+ <p>Matchup: {{ game.away_team if game.away_team else 'TBD' }} @ {{ game.home_team if game.home_team else 'TBD' }}</p>
+
+ <form method="POST" style="max-width: 100%;">
+ <div style="background: var(--bg-primary); padding: 2rem; border: 1px solid var(--deco-border); width: 100%; box-sizing: border-box;">
+ <div style="display: flex; gap: 2rem; flex-wrap: wrap;">
+ <div style="flex: 1; min-width: 200px;">
+ <label for="scheduled_date">Scheduled Date (YYYY-MM-DD):</label>
+ <input type="text" id="scheduled_date" name="scheduled_date" value="{{ game.scheduled_date }}" style="width: 100%;">
+ </div>
+ <div style="flex: 1; min-width: 200px;">
+ <label for="status">Status:</label>
+ <select id="status" name="status" style="width: 100%;">
+ <option value="Scheduled" {% if game.status == 'Scheduled' %}selected{% endif %}>Scheduled</option>
+ <option value="Final" {% if game.status == 'Final' %}selected{% endif %}>Final</option>
+ <option value="TBD" {% if game.status == 'TBD' %}selected{% endif %}>TBD</option>
+ <option value="Canceled" {% if game.status == 'Canceled' %}selected{% endif %}>Canceled</option>
+ </select>
+ </div>
+ </div>
+
+ <div style="display: flex; gap: 2rem; margin-top: 1rem; flex-wrap: wrap;">
+ <div style="flex: 1; min-width: 200px;">
+ <label for="away_score">{{ game.away_team if game.away_team else 'Away' }} Score:</label>
+ <input type="number" id="away_score" name="away_score" value="{{ game.away_score if game.away_score is not none else '' }}" style="width: 100%;">
+ </div>
+ <div style="flex: 1; min-width: 200px;">
+ <label for="home_score">{{ game.home_team if game.home_team else 'Home' }} Score:</label>
+ <input type="number" id="home_score" name="home_score" value="{{ game.home_score if game.home_score is not none else '' }}" style="width: 100%;">
+ </div>
+ </div>
+
+ <div style="margin-top: 2rem; overflow-x: auto;">
+ <label style="display: block; margin-bottom: 0.5rem; font-weight: bold;">Box Score (Optional):</label>
+ <table style="width: 100%; border: 1px solid var(--deco-border); background: #fff; min-width: 600px;">
+ <tr style="background: #eee;">
+ <th>Team</th>
+ <th>1</th><th>2</th><th>3</th><th>4</th><th>5</th><th>6</th><th>7</th><th>8</th><th>9</th><th>Ex</th><th>R</th><th>H</th><th>E</th>
+ </tr>
+ <tr>
+ <td style="font-weight: bold; font-size: 0.8rem; padding: 5px;">{{ game.away_team if game.away_team else 'Away' }}</td>
+ {% set bs_a = game.box_score.away if game.box_score else [0,0,0,0,0,0,0,0,0,0,0,0,0] %}
+ {% for col in ["1", "2", "3", "4", "5", "6", "7", "8", "9", "extra", "runs", "hits", "errors"] %}
+ <td style="padding: 2px;"><input type="number" name="away_inn_{{ col }}" value="{{ bs_a[loop.index0] }}" style="width: 35px; padding: 2px; text-align: center;"></td>
+ {% endfor %}
+ </tr>
+ <tr>
+ <td style="font-weight: bold; font-size: 0.8rem; padding: 5px;">{{ game.home_team if game.home_team else 'Home' }}</td>
+ {% set bs_h = game.box_score.home if game.box_score else [0,0,0,0,0,0,0,0,0,0,0,0,0] %}
+ {% for col in ["1", "2", "3", "4", "5", "6", "7", "8", "9", "extra", "runs", "hits", "errors"] %}
+ <td style="padding: 2px;"><input type="number" name="home_inn_{{ col }}" value="{{ bs_h[loop.index0] }}" style="width: 35px; padding: 2px; text-align: center;"></td>
+ {% endfor %}
+ </tr>
+ </table>
+ </div>
+
+ <div style="margin-top: 2rem; display: flex; gap: 1rem;">
+ <button type="submit" style="padding: 10px 30px;">Update Game</button>
+ <a href="{{ url_for('game', game_id=game.id) }}" style="display: inline-block; background: #ddd; color: #333; padding: 10px 20px; text-decoration: none; font-family: var(--sans-font); font-weight: bold; border: 1px solid #ccc;">Cancel</a>
+ </div>
+ </div>
+ </form>
+{% endblock %} \ No newline at end of file
diff --git a/templates/admin_manage_teams.html b/templates/admin_manage_teams.html
new file mode 100644
index 0000000..669d622
--- /dev/null
+++ b/templates/admin_manage_teams.html
@@ -0,0 +1,76 @@
+{% extends "base.html" %}
+
+{% block content %}
+ <h2>Manage Teams for Season: {{ season.name }}</h2>
+
+ <div style="display: flex; gap: 2rem; flex-wrap: wrap;">
+ <!-- Left Column: Pending and Approved Teams -->
+ <div style="flex: 1; min-width: 300px;">
+ <h3>Pending Requests</h3>
+ {% if pending_teams %}
+ <ul style="list-style: none; padding: 0;">
+ {% for team in pending_teams %}
+ <li style="background: var(--bg-primary); padding: 1rem; margin-bottom: 0.5rem; border: 1px solid var(--deco-border); display: flex; justify-content: space-between; align-items: center;">
+ <span>{{ team.team_name }} ({{ team.username }})</span>
+ <div style="display: flex; gap: 0.5rem;">
+ <form method="POST" style="margin:0; width: auto;">
+ <input type="hidden" name="action" value="approve">
+ <input type="hidden" name="user_id" value="{{ team.id }}">
+ <button type="submit" style="background-color: green; font-size: 0.8rem; padding: 4px 8px;">Approve</button>
+ </form>
+ <form method="POST" style="margin:0; width: auto;">
+ <input type="hidden" name="action" value="deny">
+ <input type="hidden" name="user_id" value="{{ team.id }}">
+ <button type="submit" style="background-color: red; font-size: 0.8rem; padding: 4px 8px;">Deny</button>
+ </form>
+ </div>
+ </li>
+ {% endfor %}
+ </ul>
+ {% else %}
+ <p>No pending requests.</p>
+ {% endif %}
+
+ <h3 style="margin-top: 2rem;">Approved Teams</h3>
+ {% if approved_teams %}
+ <ul style="list-style: none; padding: 0;">
+ {% for team in approved_teams %}
+ <li style="background: var(--bg-primary); padding: 1rem; margin-bottom: 0.5rem; border: 1px solid var(--deco-border); display: flex; justify-content: space-between; align-items: center;">
+ <span>{{ team.team_name }} ({{ team.username }})</span>
+ <form method="POST" style="margin:0; width: auto;">
+ <input type="hidden" name="action" value="remove">
+ <input type="hidden" name="user_id" value="{{ team.id }}">
+ <button type="submit" style="background-color: red; font-size: 0.8rem; padding: 4px 8px;">Remove</button>
+ </form>
+ </li>
+ {% endfor %}
+ </ul>
+ {% else %}
+ <p>No approved teams.</p>
+ {% endif %}
+ </div>
+
+ <!-- Right Column: Add Teams Directly -->
+ <div style="flex: 1; min-width: 300px; background: var(--bg-primary); padding: 2rem; border: 1px solid var(--deco-border);">
+ <h3>Directly Add Teams</h3>
+ <p>Select teams to bypass the request process and approve immediately.</p>
+ <form method="POST">
+ <input type="hidden" name="action" value="add">
+ <div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 1rem; margin-bottom: 1.5rem;">
+ {% for team in all_teams %}
+ {% if team.id not in enrolled_ids %}
+ <label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
+ <input type="checkbox" name="team_ids" value="{{ team.id }}">
+ {{ team.team_name }}
+ </label>
+ {% endif %}
+ {% endfor %}
+ </div>
+ <button type="submit">Add Selected Teams</button>
+ </form>
+ <div style="margin-top: 2rem;">
+ <a href="{{ url_for('admin') }}" style="color: var(--text-accent); text-decoration: underline; font-weight: bold;">&larr; Back to Admin Dashboard</a>
+ </div>
+ </div>
+ </div>
+{% endblock %} \ No newline at end of file
diff --git a/templates/base.html b/templates/base.html
new file mode 100644
index 0000000..9172079
--- /dev/null
+++ b/templates/base.html
@@ -0,0 +1,54 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>The Orchard League</title>
+ <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
+</head>
+<body>
+ <img src="{{ url_for('static', filename='img/orchards_left.png') }}" class="side-image left">
+ <img src="{{ url_for('static', filename='img/orchards_right.png') }}" class="side-image right">
+ <header>
+ <div class="logo-container">
+ <img src="{{ url_for('static', filename='img/logo.png') }}"/>
+ <div class="text-wrap">
+ <h1>THE ORCHARD LEAGUE</h1>
+ <p class="subtitle">Established 2026</p>
+ </div>
+ </div>
+ <nav>
+ <a href="{{ url_for('index') }}">Seasons</a>
+ {% if current_user.is_authenticated %}
+ <a href="{{ url_for('team') }}">Team Management ({{ current_user.team_name }})</a>
+ {% if current_user.is_admin %}
+ <a href="{{ url_for('admin') }}">Admin</a>
+ {% endif %}
+ <a href="{{ url_for('logout') }}">Logout ({{ current_user.username }})</a>
+ {% else %}
+ <a href="{{ url_for('login') }}">Login</a>
+ {% endif %}
+ </nav>
+ </header>
+
+ <main>
+ <div class="flashes">
+ {% with messages = get_flashed_messages() %}
+ {% if messages %}
+ <ul class="flash-messages">
+ {% for message in messages %}
+ <li>{{ message }}</li>
+ {% endfor %}
+ </ul>
+ {% endif %}
+ {% endwith %}
+ </div>
+
+ {% block content %}{% endblock %}
+ </main>
+
+ <footer>
+ <p>&copy; Blessings; WWCH -Chairman of the Orchard League.</p>
+ </footer>
+</body>
+</html> \ No newline at end of file
diff --git a/templates/game.html b/templates/game.html
new file mode 100644
index 0000000..690e16d
--- /dev/null
+++ b/templates/game.html
@@ -0,0 +1,185 @@
+{% extends "base.html" %}
+
+{% block content %}
+ <div style="display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem;">
+ <a href="{{ url_for('index', season_id=game.season_id, tab='schedule') }}"
+ style="text-decoration: none; background: #ddd; color: #333; padding: 5px 15px; font-family: var(--sans-font); font-weight: bold; border: 1px solid #ccc;">
+ &larr; Back to Schedule
+ </a>
+ <h2 style="margin: 0; flex: 1; border: none;">Game Details</h2>
+ </div>
+
+ {% if game %}
+ <div style="display: flex; justify-content: center; gap: 4rem; align-items: center; margin: 2rem 0; background: var(--bg-primary); padding: 2rem; border: 2px solid var(--deco-border);">
+ <div style="text-align: center;">
+ <h3>Away</h3>
+ {% set is_away_team = current_user.is_authenticated and current_user.id|string == game.away_team_id|string %}
+ {% if game.away_icon %}
+ <img src="{{ url_for('static', filename='uploads/' + game.away_icon) }}" style="width:100px; height:100px; border-radius:50%; border: {{ '5px solid gold' if is_away_team else '2px solid var(--text-accent)' }};">
+ {% else %}
+ <div style="width: 100px; height: 100px; border-radius: 50%; background-color: var(--deco-border); display: inline-block; border: {{ '5px solid gold' if is_away_team else 'none' }};"></div>
+ {% endif %}
+ <h4>{{ game.away_team if game.away_team else 'TBD' }}</h4>
+ <div style="font-size: 3rem; font-weight: bold; color: var(--deco-highlight);">{{ game.away_score if game.status == 'Final' else '-' }}</div>
+ </div>
+
+ <div style="text-align: center;">
+ <div style="color: var(--text-accent); font-size: 2rem; margin-bottom: 1rem;">VS</div>
+ <div style="color: var(--text-primary);">Date: {{ game.scheduled_date }}</div>
+ <div style="color: var(--deco-highlight); font-weight: bold; margin-top: 1rem; text-transform: uppercase;">
+ {{ game.status }}
+ {% if game.is_conditional %}<span class="conditional-game" title="Conditional Game">*</span>{% endif %}
+ </div>
+
+ {% if current_user.is_admin %}
+ <div style="margin-top: 1rem;">
+ <a href="{{ url_for('admin_edit_game', game_id=game.id) }}" style="background: var(--deco-border); color: var(--bg-primary); padding: 5px 10px; text-decoration: none;">&#9998; Edit Game</a>
+ </div>
+ {% endif %}
+ </div>
+
+ <div style="text-align: center;">
+ <h3>Home</h3>
+ {% set is_home_team = current_user.is_authenticated and current_user.id|string == game.home_team_id|string %}
+ {% if game.home_icon %}
+ <img src="{{ url_for('static', filename='uploads/' + game.home_icon) }}" style="width:100px; height:100px; border-radius:50%; border: {{ '5px solid gold' if is_home_team else '2px solid var(--text-accent)' }};">
+ {% else %}
+ <div style="width: 100px; height: 100px; border-radius: 50%; background-color: var(--deco-border); display: inline-block; border: {{ '5px solid gold' if is_home_team else 'none' }};"></div>
+ {% endif %}
+ <h4>{{ game.home_team if game.home_team else 'TBD' }}</h4>
+ <div style="font-size: 3rem; font-weight: bold; color: var(--deco-highlight);">{{ game.home_score if game.status == 'Final' else '-' }}</div>
+ </div>
+ </div>
+
+ {% if game.status == 'Final' and game.box_score and (game.box_score.away|sum + game.box_score.home|sum) > 0 %}
+ <div style="margin-top: 2rem; overflow-x: auto;">
+ <h3>Box Score</h3>
+ <table style="width: 100%; font-family: var(--sans-font); border: 2px solid #000;">
+ <tr style="background: #eee;">
+ <th>Team</th>
+ <th>1</th><th>2</th><th>3</th><th>4</th><th>5</th><th>6</th><th>7</th><th>8</th><th>9</th><th>Extra</th>
+ <th style="background: #ddd;">R</th><th>H</th><th>E</th>
+ </tr>
+ <tr>
+ <td style="font-weight: bold;">{{ game.away_team }}</td>
+ {% for val in game.box_score.away %}
+ <td style="{{ 'background: #f9f9f9; font-weight: bold;' if loop.index > 10 else '' }}">{{ val }}</td>
+ {% endfor %}
+ </tr>
+ <tr>
+ <td style="font-weight: bold;">{{ game.home_team }}</td>
+ {% for val in game.box_score.home %}
+ <td style="{{ 'background: #f9f9f9; font-weight: bold;' if loop.index > 10 else '' }}">{{ val }}</td>
+ {% endfor %}
+ </tr>
+ </table>
+ </div>
+ {% endif %}
+
+ {% if game.status != 'Final' and submissions %}
+ <div style="margin-top: 2rem;">
+ <h3>Active Proposals</h3>
+ {% for s in submissions %}
+ {% set show_score = s.away_score is not none and (s.away_score != game.away_score or s.home_score != game.home_score) %}
+ {% set show_date = s.proposed_date and s.proposed_date != game.scheduled_date %}
+
+ {% if show_score or show_date %}
+ <div style="background: #fff; border: 1px solid var(--deco-border); padding: 1rem; margin-bottom: 1rem; display: flex; justify-content: space-between; align-items: flex-start;">
+ <div>
+ <p style="margin: 0;"><strong>From:</strong> {{ s.team_name }} ({{ s.username }})</p>
+ {% if show_score %}
+ <p style="margin: 0.5rem 0 0 0;"><strong>Score:</strong> {{ game.away_team }} {{ s.away_score }} - {{ game.home_team }} {{ s.home_score }}</p>
+ {% if s.box_score %}
+ <details style="margin-top: 0.5rem;">
+ <summary style="cursor: pointer; font-size: 0.85rem; color: #555;">View Proposed Box Score</summary>
+ <table style="width: 100%; font-size: 0.75rem; margin-top: 0.5rem; border: 1px solid #ddd;">
+ <tr style="background: #f5f5f5;">
+ <th>Team</th><th>1</th><th>2</th><th>3</th><th>4</th><th>5</th><th>6</th><th>7</th><th>8</th><th>9</th><th>Ex</th><th>R</th><th>H</th><th>E</th>
+ </tr>
+ <tr><td>Away</td>{% for v in s.box_score.away %}<td>{{ v }}</td>{% endfor %}</tr>
+ <tr><td>Home</td>{% for v in s.box_score.home %}<td>{{ v }}</td>{% endfor %}</tr>
+ </table>
+ </details>
+ {% endif %}
+ {% endif %}
+ {% if show_date %}
+ <p style="margin: 0.5rem 0 0 0;"><strong>Date Proposal:</strong> {{ s.proposed_date }}</p>
+ {% endif %}
+ </div>
+ <div style="display: flex; gap: 0.5rem;">
+ {% if current_user.id|string == s.submitted_by_id|string %}
+ <form method="POST" action="{{ url_for('retract_submission', game_id=game.id) }}">
+ <button type="submit" style="background-color: #cc0000; font-size: 0.8rem; padding: 5px 10px;">Retract</button>
+ </form>
+ {% elif current_user.is_authenticated and (current_user.id|string == game.away_team_id|string or current_user.id|string == game.home_team_id|string) %}
+ <form method="POST" action="{{ url_for('agree_submission', game_id=game.id, submission_id=s.id) }}">
+ <button type="submit" style="background-color: #008800; font-size: 0.8rem; padding: 5px 10px;">Agree & Finalize</button>
+ </form>
+ {% endif %}
+ </div>
+ </div>
+ {% endif %}
+ {% endfor %}
+ </div>
+ {% endif %}
+
+ {% if game.status != 'Final' and current_user.is_authenticated and (current_user.id|string == game.away_team_id|string or current_user.id|string == game.home_team_id|string) %}
+ <div style="margin-top: 2rem; display: flex; flex-direction: column; gap: 2rem;">
+ <!-- Score Submission Form (Full Width) -->
+ <div style="background: var(--bg-primary); padding: 2rem; border: 1px solid var(--deco-border);">
+ <h3>Submit Game Results</h3>
+ <form method="POST" action="{{ url_for('submit_game', game_id=game.id) }}" style="max-width: 100%;">
+ <div style="display: flex; gap: 1rem; margin-bottom: 1rem; max-width: 400px;">
+ <div style="flex: 1;">
+ <label for="away_score" style="display: block; margin-bottom: 0.5rem;">{{ game.away_team }} Score:</label>
+ <input type="number" id="away_score" name="away_score" required style="width: 100%;">
+ </div>
+ <div style="flex: 1;">
+ <label for="home_score" style="display: block; margin-bottom: 0.5rem;">{{ game.home_team }} Score:</label>
+ <input type="number" id="home_score" name="home_score" required style="width: 100%;">
+ </div>
+ </div>
+
+ <div style="margin-bottom: 1rem; overflow-x: auto;">
+ <label style="display: block; margin-bottom: 0.5rem;">Box Score (Optional):</label>
+ <table style="width: 100%; border: 1px solid var(--deco-border); background: #fff;">
+ <tr style="background: #eee;">
+ <th>Team</th>
+ <th>1</th><th>2</th><th>3</th><th>4</th><th>5</th><th>6</th><th>7</th><th>8</th><th>9</th><th>Extra</th><th>R</th><th>H</th><th>E</th>
+ </tr>
+ <tr>
+ <td style="font-weight: bold; font-size: 0.8rem;">{{ game.away_team if game.away_team else 'Away' }}</td>
+ {% for col in ["1", "2", "3", "4", "5", "6", "7", "8", "9", "extra", "runs", "hits", "errors"] %}
+ <td><input type="number" name="away_inn_{{ col }}" value="0" style="width: 35px; padding: 2px; text-align: center;"></td>
+ {% endfor %}
+ </tr>
+ <tr>
+ <td style="font-weight: bold; font-size: 0.8rem;">{{ game.home_team if game.home_team else 'Home' }}</td>
+ {% for col in ["1", "2", "3", "4", "5", "6", "7", "8", "9", "extra", "runs", "hits", "errors"] %}
+ <td><input type="number" name="home_inn_{{ col }}" value="0" style="width: 35px; padding: 2px; text-align: center;"></td>
+ {% endfor %}
+ </tr>
+ </table>
+ </div>
+
+ <button type="submit" style="width: 200px;">Submit Scores</button>
+ </form>
+ </div>
+
+ <!-- Date Change Form -->
+ <div style="background: var(--bg-primary); padding: 2rem; border: 1px solid var(--deco-border); max-width: 400px;">
+ <h3>Propose Date Change</h3>
+ <form method="POST" action="{{ url_for('submit_game', game_id=game.id) }}" style="max-width: 100%;">
+ <div style="margin-bottom: 1rem;">
+ <label for="proposed_date" style="display: block; margin-bottom: 0.5rem;">New Date:</label>
+ <input type="text" id="proposed_date" name="proposed_date" placeholder="YYYY-MM-DD" style="width: 100%;">
+ </div>
+ <button type="submit" style="width: 100%; background-color: var(--deco-highlight); color: white;">Propose Date</button>
+ </form>
+ </div>
+ </div>
+ {% endif %}
+ {% else %}
+ <p>Game not found.</p>
+ {% endif %}
+{% endblock %} \ No newline at end of file
diff --git a/templates/login.html b/templates/login.html
new file mode 100644
index 0000000..f8f19b6
--- /dev/null
+++ b/templates/login.html
@@ -0,0 +1,16 @@
+{% extends "base.html" %}
+
+{% block content %}
+ <h2>Team Owner Login</h2>
+ <form method="POST" action="{{ url_for('login') }}">
+ <div>
+ <label for="username">Username:</label>
+ <input type="text" id="username" name="username" required>
+ </div>
+ <div>
+ <label for="password">Password:</label>
+ <input type="password" id="password" name="password" required>
+ </div>
+ <button type="submit">Login</button>
+ </form>
+{% endblock %} \ No newline at end of file
diff --git a/templates/season.html b/templates/season.html
new file mode 100644
index 0000000..43ca0de
--- /dev/null
+++ b/templates/season.html
@@ -0,0 +1,285 @@
+{% extends "base.html" %}
+
+{% block content %}
+ <div style="margin-bottom: 2rem;">
+ <form method="GET" style="flex-direction: row; align-items: center; max-width: 100%; border: none; padding: 0;">
+ <label for="season_id" style="margin-right: 10px; font-family: var(--sans-font); font-weight: bold; text-transform: uppercase;">Select Season:</label>
+ <select id="season_id" name="season_id" onchange="this.form.submit()" style="padding: 5px 10px;">
+ {% for season in seasons %}
+ <option value="{{ season.id }}" {% if season.id|string == selected_season_id|string %}selected{% endif %}>{{ season.name }}</option>
+ {% endfor %}
+ </select>
+ <input type="hidden" name="tab" value="{{ active_tab }}">
+ </form>
+ </div>
+
+ <!-- Tabs Navigation -->
+ <div style="border-bottom: 2px solid var(--text-accent); margin-bottom: 1rem; display: flex; gap: 5px;">
+ <a href="{{ url_for('index', season_id=selected_season_id, tab='overview') }}"
+ style="padding: 10px 20px; text-decoration: none; font-family: var(--sans-font); font-weight: bold; text-transform: uppercase; border: 1px solid var(--deco-border); border-bottom: none; {{ 'background: white; color: black;' if active_tab == 'overview' else 'background: #ddd; color: #555;' }}">
+ Season Overview
+ </a>
+ <a href="{{ url_for('index', season_id=selected_season_id, tab='standings') }}"
+ style="padding: 10px 20px; text-decoration: none; font-family: var(--sans-font); font-weight: bold; text-transform: uppercase; border: 1px solid var(--deco-border); border-bottom: none; {{ 'background: white; color: black;' if active_tab == 'standings' else 'background: #ddd; color: #555;' }}">
+ Standings
+ </a>
+ <a href="{{ url_for('index', season_id=selected_season_id, tab='schedule') }}"
+ style="padding: 10px 20px; text-decoration: none; font-family: var(--sans-font); font-weight: bold; text-transform: uppercase; border: 1px solid var(--deco-border); border-bottom: none; {{ 'background: white; color: black;' if active_tab == 'schedule' else 'background: #ddd; color: #555;' }}">
+ Schedule
+ </a>
+ <a href="{{ url_for('index', season_id=selected_season_id, tab='playoffs') }}"
+ style="padding: 10px 20px; text-decoration: none; font-family: var(--sans-font); font-weight: bold; text-transform: uppercase; border: 1px solid var(--deco-border); border-bottom: none; {{ 'background: white; color: black;' if active_tab == 'playoffs' else 'background: #ddd; color: #555;' }}">
+ Playoffs
+ </a>
+ </div>
+
+ {% if active_tab == 'overview' %}
+ <div class="tab-content">
+ <h3>{{ season_info.name }} - Overview</h3>
+ <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 2rem; margin-top: 1rem;">
+ <div style="background: white; padding: 1.5rem; border: 1px solid var(--deco-border);">
+ <p><strong>Total Games (Per Team):</strong> {{ season_info.total_games }}</p>
+ <p><strong>Games Per Week:</strong> {{ season_info.games_per_week }}</p>
+ <p><strong>Season Start:</strong> {{ season_info.start_date }}</p>
+ <p><strong>Season End:</strong> {{ season_info.end_date }}</p>
+ {% if season_info.total_games %}
+ <p><strong>Playoff Teams:</strong> {{ season_info.playoff_teams }}</p>
+ {% endif %}
+ <p><strong>Status:</strong> {{ season_info.status }} {% if season_info.is_finished %}(Finished){% else %}(In Progress){% endif %}</p>
+ </div>
+
+ <div style="background: white; padding: 1.5rem; border: 1px solid var(--deco-border);">
+ {% if season_info.is_finished %}
+ <h4>Final Standings</h4>
+ {% if standings %}
+ <table style="margin-top: 0; width: 100%;">
+ {% for row in standings %}
+ <tr>
+ <td>{{ loop.index }}. <strong>{{ row.team_name }}</strong></td>
+ <td>{{ row.wins }} - {{ row.losses }}</td>
+ </tr>
+ {% endfor %}
+ </table>
+ {% else %}
+ <p>No teams enrolled.</p>
+ {% endif %}
+ {% else %}
+ <h4>League Action</h4>
+ <p>The season is currently active or upcoming.</p>
+
+ {% if current_user.is_authenticated and not current_user.is_admin %}
+ {% if user_season_status == 'Approved' %}
+ <div style="background: #f0fff0; border: 1px solid green; padding: 10px; margin-top: 10px;">
+ <p style="color: green; font-weight: bold; margin: 0;">You are currently enrolled in this season.</p>
+ </div>
+ {% elif user_season_status == 'Pending' %}
+ <div style="background: #fffbe6; border: 1px solid orange; padding: 10px; margin-top: 10px;">
+ <p style="color: orange; font-weight: bold; margin: 0;">Your request to join is pending approval.</p>
+ </div>
+ {% else %}
+ <form method="POST" action="{{ url_for('join_season', season_id=selected_season_id) }}">
+ <button type="submit" style="width: 100%; padding: 12px; font-size: 1.1rem;">Apply to Join Season</button>
+ </form>
+ {% endif %}
+ {% elif not current_user.is_authenticated %}
+ <p style="border: 1px solid #ccc; padding: 10px; background: #eee;">
+ <a href="{{ url_for('login') }}" style="font-weight: bold; color: black; text-decoration: underline;">Login</a> to apply for this season.
+ </p>
+ {% endif %}
+ {% endif %}
+ </div>
+ </div>
+ </div>
+
+ {% elif active_tab == 'standings' %}
+ <div class="tab-content">
+ <h3>{{ season_info.name }} - Standings</h3>
+ {% if standings %}
+ <table>
+ <tr>
+ <th>Team</th>
+ <th>Wins (W)</th>
+ <th>Losses (L)</th>
+ <th>Runs For (RF)</th>
+ <th>Runs Against (RA)</th>
+ <th>Games Back (GB)</th>
+ </tr>
+ {% for row in standings %}
+ {% set is_my_team = current_user.is_authenticated and current_user.id|string == row.user_id|string %}
+ {% set is_cutoff = loop.index == season_info.playoff_teams %}
+ <tr style="{{ 'background-color: #ffffcc;' if is_my_team else '' }} {{ 'border-bottom: 4px solid #333;' if is_cutoff else '' }}">
+ <td style="display: flex; align-items: center; justify-content: center; gap: 10px;">
+ {% if row.team_icon %}
+ <img src="{{ url_for('static', filename='uploads/' + row.team_icon) }}" alt="{{ row.team_name }}"
+ class="{{ 'team-icon-highlight' if is_my_team else '' }}"
+ style="width: 30px; height: 30px; border-radius: 50%; border: 1px solid var(--deco-border);">
+ {% else %}
+ <div class="{{ 'team-icon-highlight' if is_my_team else '' }}"
+ style="width: 30px; height: 30px; border-radius: 50%; background-color: var(--deco-highlight); display: inline-block;"></div>
+ {% endif %}
+ {{ row.team_name }}
+ {% if row.clinch %}<span class="clinch-marker" title="Clinched Playoff Spot {{ '(1st Seed)' if row.clinch == '**' else '' }}">{{ row.clinch }}</span>{% endif %}
+ </td>
+ <td>{{ row.wins }}</td>
+ <td>{{ row.losses }}</td>
+ <td>{{ row.runs_for }}</td>
+ <td>{{ row.runs_against }}</td>
+ <td>{{ row.games_back }}</td>
+ </tr>
+ {% endfor %}
+ </table>
+ {% else %}
+ <p>No standings available for the selected season.</p>
+ {% endif %}
+ </div>
+
+ {% elif active_tab == 'schedule' %}
+ <div class="tab-content">
+ <h3>{{ season_info.name }} - Schedule</h3>
+
+ <form method="GET" style="flex-direction: row; align-items: center; max-width: 100%; margin-bottom: 1.5rem; border: none; padding: 0;">
+ <input type="hidden" name="season_id" value="{{ selected_season_id }}">
+ <input type="hidden" name="tab" value="schedule">
+ <label for="team_id" style="margin-right: 10px; font-family: var(--sans-font); font-weight: bold; text-transform: uppercase;">Filter by Team:</label>
+ <select id="team_id" name="team_id" onchange="this.form.submit()" style="padding: 5px 10px;">
+ <option value="all">All Teams</option>
+ {% for team in all_teams %}
+ <option value="{{ team.id }}" {% if team.id|string == selected_team_id|string %}selected{% endif %}>{{ team.team_name }}</option>
+ {% endfor %}
+ </select>
+ </form>
+
+ {% for month in months %}
+ <div class="calendar-month">
+ <h4>{{ month.name }}</h4>
+ <div class="calendar-grid">
+ {% for day in ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] %}
+ <div class="calendar-day-head">{{ day }}</div>
+ {% endfor %}
+
+ {% for week in month.weeks %}
+ {% for day in week %}
+ {% set date_str = "%04d-%02d-%02d" % (month.year, month.month, day) if day != 0 else "" %}
+ <div class="calendar-day {{ 'other-month' if day == 0 else '' }} {{ 'today' if date_str == today else '' }}">
+ {% if day != 0 %}
+ <span class="calendar-day-num">{{ day }}</span>
+ {% if date_str in games_by_date %}
+ {% for game in games_by_date[date_str] %}
+ {% set is_away_mine = current_user.is_authenticated and current_user.id|string == game.away_team_id|string %}
+ {% set is_home_mine = current_user.is_authenticated and current_user.id|string == game.home_team_id|string %}
+ <div class="calendar-game-item" onclick="window.location='{{ url_for('game', game_id=game.id) }}'">
+ <span title="{{ game.away_team if game.away_team else 'TBD' }} @ {{ game.home_team if game.home_team else 'TBD' }}">
+ {% if game.away_icon %}
+ <img src="{{ url_for('static', filename='uploads/' + game.away_icon) }}" class="team-icon-small {{ 'team-icon-highlight' if is_away_mine else '' }}">
+ {% else %}
+ <div class="team-icon-small {{ 'team-icon-highlight' if is_away_mine else '' }}" style="display:inline-block; background:#ccc;"></div>
+ {% endif %}
+ {{ game.away_score if game.status == 'Final' else '' }} @
+ {{ game.home_score if game.status == 'Final' else '' }}
+ {% if game.home_icon %}
+ <img src="{{ url_for('static', filename='uploads/' + game.home_icon) }}" class="team-icon-small {{ 'team-icon-highlight' if is_home_mine else '' }}">
+ {% else %}
+ <div class="team-icon-small {{ 'team-icon-highlight' if is_home_mine else '' }}" style="display:inline-block; background:#ccc;"></div>
+ {% endif %}
+ {% if game.is_conditional %}*{% endif %}
+ {% if game.pending_proposals > 0 %}
+ <span class="pending-icon" title="Game has pending proposals!">&#9888;</span>
+ {% endif %}
+ </span>
+ </div>
+ {% endfor %}
+ {% endif %}
+ {% endif %}
+ </div>
+ {% endfor %}
+ {% endfor %}
+ </div>
+ </div>
+ {% endfor %}
+
+ {% if games_by_date['TBD'] %}
+ <h3>TBD Games</h3>
+ <div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 1rem;">
+ {% for game in games_by_date['TBD'] %}
+ <div class="calendar-game-item" onclick="window.location='{{ url_for('game', game_id=game.id) }}'" style="padding: 1rem; font-size: 0.9rem;">
+ TBD @ TBD (Playoff)
+ </div>
+ {% endfor %}
+ </div>
+ {% endif %}
+ </div>
+ {% elif active_tab == 'playoffs' %}
+ <div class="tab-content">
+ <h3>{{ season_info.name }} - Playoffs</h3>
+
+ {% if bracket %}
+ <div class="bracket-container">
+ {% for round_games in bracket %}
+ <div class="bracket-round">
+ <h4 style="border: none; text-align: center; font-size: 0.9rem;">Round {{ loop.index }}</h4>
+ {% for g in round_games %}
+ <div class="bracket-game {{ 'winner' if g.status == 'Final' }}" onclick="window.location='{{ url_for('game', game_id=g.id) }}'" style="cursor: pointer;">
+ <div class="bracket-team">
+ <span>{{ g.away_team if g.away_team else 'TBD' }} {{ g.away_record if g.away_team else '' }}</span>
+ <span style="font-weight: bold; margin-left: 10px;">{{ g.away_series_wins }}</span>
+ </div>
+ <div class="bracket-team">
+ <span>{{ g.home_team if g.home_team else 'TBD' }} {{ g.home_record if g.home_team else '' }}</span>
+ <span style="font-weight: bold; margin-left: 10px;">{{ g.home_series_wins }}</span>
+ </div>
+ <div style="font-size: 0.6rem; color: #777; margin-top: 3px;">
+ Best of {{ season_info.playoff_series_length }}
+ </div>
+ </div>
+ {% endfor %}
+ </div>
+ {% endfor %}
+ </div>
+ {% else %}
+ <p>Playoff bracket not yet generated.</p>
+ {% endif %}
+
+ <h3>Playoff Schedule</h3>
+ {% if playoff_games %}
+ <div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 1rem;">
+ {% for game in playoff_games %}
+ {% set is_away_mine = current_user.is_authenticated and current_user.id|string == game.away_team_id|string %}
+ {% set is_home_mine = current_user.is_authenticated and current_user.id|string == game.home_team_id|string %}
+ <div style="border: 1px solid var(--deco-border); padding: 1rem; background: white; cursor: pointer;"
+ onclick="window.location='{{ url_for('game', game_id=game.id) }}'">
+ <div style="font-size: 0.8rem; color: var(--deco-highlight); margin-bottom: 0.5rem; font-family: var(--sans-font); font-weight: bold; text-transform: uppercase;">
+ {{ game.scheduled_date }} - {{ game.status }}
+ {% if game.is_conditional %}<span class="conditional-game">*</span>{% endif %}
+ {% if game.pending_proposals > 0 %}<span class="pending-icon">&#9888;</span>{% endif %}
+ </div>
+ <div style="display: flex; justify-content: space-between; align-items: center;">
+ <div style="text-align: center; flex: 1;">
+ {% if game.away_icon %}
+ <img src="{{ url_for('static', filename='uploads/' + game.away_icon) }}" class="team-icon-small {{ 'team-icon-highlight' if is_away_mine else '' }}">
+ {% else %}
+ <div class="team-icon-small {{ 'team-icon-highlight' if is_away_mine else '' }}" style="display:inline-block; background:#ccc; border-radius: 50%;"></div>
+ {% endif %}
+ <br><strong style="font-size: 0.8rem;">{{ game.away_team if game.away_team else 'TBD' }}</strong>
+ <br><span style="font-weight: bold;">{{ game.away_score if game.status == 'Final' else '-' }}</span>
+ </div>
+ <div style="font-weight: bold;">@</div>
+ <div style="text-align: center; flex: 1;">
+ {% if game.home_icon %}
+ <img src="{{ url_for('static', filename='uploads/' + game.home_icon) }}" class="team-icon-small {{ 'team-icon-highlight' if is_home_mine else '' }}">
+ {% else %}
+ <div class="team-icon-small {{ 'team-icon-highlight' if is_home_mine else '' }}" style="display:inline-block; background:#ccc; border-radius: 50%;"></div>
+ {% endif %}
+ <br><strong style="font-size: 0.8rem;">{{ game.home_team if game.home_team else 'TBD' }}</strong>
+ <br><span style="font-weight: bold;">{{ game.home_score if game.status == 'Final' else '-' }}</span>
+ </div>
+ </div>
+ </div>
+ {% endfor %}
+ </div>
+ {% else %}
+ <p>No playoff games scheduled.</p>
+ {% endif %}
+ </div>
+ {% endif %}
+
+{% endblock %} \ No newline at end of file
diff --git a/templates/team.html b/templates/team.html
new file mode 100644
index 0000000..e7098be
--- /dev/null
+++ b/templates/team.html
@@ -0,0 +1,65 @@
+{% extends "base.html" %}
+
+{% block content %}
+ <h2>Team Management: {{ current_user.team_name }}</h2>
+
+ {% if current_user.team_icon %}
+ <div style="margin-bottom: 1rem;">
+ <img src="{{ url_for('static', filename='uploads/' + current_user.team_icon) }}" alt="Team Icon" style="max-width: 150px; border: 1px solid var(--deco-border);">
+ </div>
+ {% endif %}
+
+ <div style="display: flex; gap: 2rem; flex-wrap: wrap;">
+ <div style="flex: 1; min-width: 300px;">
+ <h3>Update Profile</h3>
+ <form method="POST" enctype="multipart/form-data">
+ <div>
+ <label for="team_name">Team Name:</label>
+ <input type="text" id="team_name" name="team_name" value="{{ current_user.team_name }}">
+ </div>
+ <div>
+ <label for="new_password">New Password (leave blank to keep current):</label>
+ <input type="password" id="new_password" name="new_password">
+ </div>
+ <div>
+ <label for="team_icon">Upload Team Icon:</label>
+ <input type="file" id="team_icon" name="team_icon" accept="image/*">
+ </div>
+ <button type="submit">Save Changes</button>
+ </form>
+ </div>
+
+ <div style="flex: 1; min-width: 300px;">
+ <h3>Seasons</h3>
+ {% if seasons %}
+ <ul style="list-style: none; padding: 0;">
+ {% for season in seasons %}
+ <li style="background: var(--bg-primary); padding: 1rem; margin-bottom: 0.5rem; border: 1px solid var(--deco-border); display: flex; justify-content: space-between; align-items: center;">
+ <span><strong>{{ season.name }}</strong> ({{ season.status }})</span>
+
+ {% if not current_user.is_admin %}
+ {% if season.id in my_seasons %}
+ {% if my_seasons[season.id] == 'Approved' %}
+ <span style="color: green; font-weight: bold;">Joined</span>
+ {% else %}
+ <span style="color: orange; font-weight: bold;">Pending Approval</span>
+ {% endif %}
+ {% else %}
+ <form method="POST" action="{{ url_for('join_season', season_id=season.id) }}" style="margin: 0;">
+ <button type="submit" style="padding: 4px 8px; font-size: 0.8rem;">Request to Join</button>
+ </form>
+ {% endif %}
+ {% endif %}
+ </li>
+ {% endfor %}
+ </ul>
+ {% else %}
+ <p>No seasons available right now.</p>
+ {% endif %}
+
+ <h3 style="margin-top: 2rem;">Your Schedule</h3>
+ <p>Below is your team's schedule for the active season.</p>
+ <a href="{{ url_for('index', tab='schedule', team_id=current_user.id) }}" style="display: inline-block; background-color: var(--text-accent); color: white; padding: 8px 16px; text-decoration: none; font-weight: bold;">View Full Team Schedule</a>
+ </div>
+ </div>
+{% endblock %} \ No newline at end of file