Print function correction + player analysis fix and player stats fix
This commit is contained in:
@@ -638,14 +638,9 @@ def analyze_player_performance(player_id, archives_data):
|
||||
if score < player_stats['worst_tournament_score']:
|
||||
player_stats['worst_tournament_score'] = score
|
||||
|
||||
# Count shots fired
|
||||
targets = participant.get('targets', {})
|
||||
shots_in_tournament = 0
|
||||
for target in targets.values():
|
||||
if target.get('shot1') is not None:
|
||||
shots_in_tournament += 1
|
||||
if target.get('shot2') is not None:
|
||||
shots_in_tournament += 1
|
||||
# Count shots fired - NOW WITH PROPER COUNTING FOR ALL FORMATS
|
||||
tournament_type = archive.get('tournament_type', '20_targets')
|
||||
shots_in_tournament = count_shots_in_tournament(participant, tournament_type)
|
||||
|
||||
player_stats['total_shots_fired'] += shots_in_tournament
|
||||
|
||||
@@ -655,7 +650,7 @@ def analyze_player_performance(player_id, archives_data):
|
||||
'score': score,
|
||||
'tournament_type': archive['tournament_type'],
|
||||
'completed': completed,
|
||||
'shots_fired': shots_in_tournament
|
||||
'shots_fired': shots_in_tournament # NOW CORRECTLY CALCULATED
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"Error analyzing tournament archive: {e}")
|
||||
@@ -923,48 +918,7 @@ def api_get_archive_stats():
|
||||
return jsonify({'status': 'success', 'stats': stats})
|
||||
except Exception as e:
|
||||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||
@app.route('/api/archive/player/<int:player_id>/performance', methods=['GET'])
|
||||
def api_get_player_performance(player_id):
|
||||
"""API endpoint to get player performance data for charts"""
|
||||
try:
|
||||
archives_data = {
|
||||
'tournaments': get_archived_tournaments(),
|
||||
'leagues': get_archived_leagues()
|
||||
}
|
||||
|
||||
player_stats = analyze_player_performance(player_id, archives_data)
|
||||
|
||||
# Prepare chart data
|
||||
performance_data = {
|
||||
'trend': {
|
||||
'labels': [f'T{i+1}' for i in range(len(player_stats['tournament_scores']))],
|
||||
'data': player_stats['tournament_scores']
|
||||
},
|
||||
'distribution': {
|
||||
'labels': ['0-50', '51-60', '61-70', '71-80', '81-90', '91-100'],
|
||||
'data': [0, 0, 0, 0, 0, 0]
|
||||
}
|
||||
}
|
||||
|
||||
# Calculate score distribution
|
||||
for score in player_stats['tournament_scores']:
|
||||
if score <= 50:
|
||||
performance_data['distribution']['data'][0] += 1
|
||||
elif score <= 60:
|
||||
performance_data['distribution']['data'][1] += 1
|
||||
elif score <= 70:
|
||||
performance_data['distribution']['data'][2] += 1
|
||||
elif score <= 80:
|
||||
performance_data['distribution']['data'][3] += 1
|
||||
elif score <= 90:
|
||||
performance_data['distribution']['data'][4] += 1
|
||||
else:
|
||||
performance_data['distribution']['data'][5] += 1
|
||||
|
||||
return jsonify({'status': 'success', 'performance': performance_data})
|
||||
except Exception as e:
|
||||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/archive/players/with-stats', methods=['GET'])
|
||||
def api_get_players_with_stats():
|
||||
"""API endpoint to get all players with their basic stats"""
|
||||
@@ -2162,7 +2116,504 @@ def get_camera_titles():
|
||||
except Exception as e:
|
||||
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||||
|
||||
@app.route('/api/archive/player/<int:player_id>/shot-accuracy')
|
||||
def get_player_shot_accuracy(player_id):
|
||||
"""
|
||||
Get aggregated shot accuracy data for a player from tournament archive files
|
||||
"""
|
||||
try:
|
||||
# Get player name from JSON file instead of database
|
||||
players_data = load_players()
|
||||
player = None
|
||||
for p in players_data['players']:
|
||||
if p['id'] == player_id:
|
||||
player = p
|
||||
break
|
||||
|
||||
if not player:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': f'Player with ID {player_id} not found'
|
||||
}), 404
|
||||
|
||||
player_name = player['name']
|
||||
|
||||
# Initialize shot counts by tournament type
|
||||
shot_accuracy = {
|
||||
'40 Targets': defaultdict(int),
|
||||
'20 Targets': defaultdict(int),
|
||||
'4 Targets': defaultdict(int)
|
||||
}
|
||||
|
||||
# Path to tournament archives
|
||||
tournament_archives_path = 'tournament_archives'
|
||||
|
||||
# Check if directory exists
|
||||
if not os.path.exists(tournament_archives_path):
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': f'Tournament archives directory not found: {tournament_archives_path}'
|
||||
}), 404
|
||||
|
||||
# Find all tournament archive files
|
||||
archive_files = glob.glob(f"{tournament_archives_path}/tournament_*.json")
|
||||
|
||||
if not archive_files:
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'data': {},
|
||||
'player_name': player_name,
|
||||
'message': 'No tournament archive files found'
|
||||
})
|
||||
|
||||
# Process each tournament archive file
|
||||
for file_path in archive_files:
|
||||
try:
|
||||
with open(file_path, 'r') as f:
|
||||
tournament_data = json.load(f)
|
||||
|
||||
# Check if this tournament has our player
|
||||
participants = tournament_data.get('results', {}).get('participants', {})
|
||||
|
||||
# Find player by ID or name
|
||||
player_data = None
|
||||
for participant_id, participant_info in participants.items():
|
||||
if (str(participant_id) == str(player_id) or
|
||||
participant_info.get('name') == player_name):
|
||||
player_data = participant_info
|
||||
break
|
||||
|
||||
if not player_data or not player_data.get('completed'):
|
||||
continue
|
||||
|
||||
# Determine tournament type
|
||||
tournament_type = determine_tournament_type_from_archive(tournament_data)
|
||||
|
||||
# Extract individual shots - NOW WITH PROPER SHOT COUNTING FOR ALL FORMATS
|
||||
shots = extract_shots_from_player_data(player_data, tournament_type)
|
||||
|
||||
# Debug print to verify shot counts
|
||||
print(f"Player {player_name}, Tournament type: {tournament_type}, Total shots extracted: {len(shots)}")
|
||||
|
||||
# Count shots by value
|
||||
for shot_value in shots:
|
||||
if shot_value == 10:
|
||||
shot_accuracy[tournament_type]['tens'] += 1
|
||||
elif shot_value == 9:
|
||||
shot_accuracy[tournament_type]['nines'] += 1
|
||||
elif shot_value == 8:
|
||||
shot_accuracy[tournament_type]['eights'] += 1
|
||||
elif shot_value == 7:
|
||||
shot_accuracy[tournament_type]['sevens'] += 1
|
||||
elif shot_value == 6:
|
||||
shot_accuracy[tournament_type]['sixes'] += 1
|
||||
elif shot_value == 5:
|
||||
shot_accuracy[tournament_type]['fives'] += 1
|
||||
elif shot_value == 4:
|
||||
shot_accuracy[tournament_type]['fours'] += 1
|
||||
elif shot_value == 3:
|
||||
shot_accuracy[tournament_type]['threes'] += 1
|
||||
elif shot_value == 2:
|
||||
shot_accuracy[tournament_type]['twos'] += 1
|
||||
elif shot_value == 1:
|
||||
shot_accuracy[tournament_type]['ones'] += 1
|
||||
elif shot_value == 0:
|
||||
shot_accuracy[tournament_type]['zeros'] += 1
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error processing tournament file {file_path}: {e}")
|
||||
continue
|
||||
|
||||
# Convert defaultdict to regular dict for JSON serialization
|
||||
result = {}
|
||||
for tournament_type, counts in shot_accuracy.items():
|
||||
if any(counts.values()): # Only include types with data
|
||||
result[tournament_type] = dict(counts)
|
||||
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'data': result,
|
||||
'player_name': player_name,
|
||||
'files_processed': len(archive_files)
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': str(e),
|
||||
'traceback': traceback.format_exc()
|
||||
}), 500
|
||||
|
||||
@app.route('/api/archive/tournament/<tournament_id>/shots')
|
||||
def get_tournament_shots(tournament_id):
|
||||
"""
|
||||
Get individual shot data for a specific tournament from archive
|
||||
"""
|
||||
try:
|
||||
# Tournament ID might be a database ID or the tournament filename/timestamp
|
||||
tournament_archives_path = 'tournament_archives'
|
||||
|
||||
# Try to find tournament file by various methods
|
||||
tournament_file = None
|
||||
|
||||
# Method 1: Direct filename match
|
||||
potential_files = [
|
||||
f"{tournament_archives_path}/tournament_{tournament_id}.json",
|
||||
f"{tournament_archives_path}/{tournament_id}.json"
|
||||
]
|
||||
|
||||
for file_path in potential_files:
|
||||
if os.path.exists(file_path):
|
||||
tournament_file = file_path
|
||||
break
|
||||
|
||||
# Method 2: Search through all tournament files for matching ID
|
||||
if not tournament_file:
|
||||
archive_files = glob.glob(f"{tournament_archives_path}/tournament_*.json")
|
||||
for file_path in archive_files:
|
||||
try:
|
||||
with open(file_path, 'r') as f:
|
||||
data = json.load(f)
|
||||
|
||||
# Check if tournament ID matches
|
||||
tournament_data = data.get('tournament', {})
|
||||
results_data = data.get('results', {})
|
||||
|
||||
if (tournament_data.get('created_at') == tournament_id or
|
||||
results_data.get('tournament_id') == tournament_id or
|
||||
str(tournament_id) in file_path):
|
||||
tournament_file = file_path
|
||||
break
|
||||
except:
|
||||
continue
|
||||
|
||||
if not tournament_file:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'Tournament archive file not found'
|
||||
}), 404
|
||||
|
||||
# Load tournament data
|
||||
with open(tournament_file, 'r') as f:
|
||||
tournament_data = json.load(f)
|
||||
|
||||
# Extract all players' shots
|
||||
participants = tournament_data.get('results', {}).get('participants', {})
|
||||
all_shots_data = {}
|
||||
|
||||
for participant_id, participant_info in participants.items():
|
||||
if participant_info.get('completed'):
|
||||
shots = extract_shots_from_player_data(participant_info)
|
||||
all_shots_data[participant_info.get('name')] = shots
|
||||
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'tournament_data': {
|
||||
'tournament_type': determine_tournament_type_from_archive(tournament_data),
|
||||
'participants': len(participants),
|
||||
'file_path': tournament_file
|
||||
},
|
||||
'shots_by_player': all_shots_data
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': str(e)
|
||||
}), 500
|
||||
|
||||
|
||||
def extract_shots_from_player_data(player_data, tournament_type=None):
|
||||
"""
|
||||
Extract individual shot values from player data in your archive format
|
||||
Now properly handles all tournament formats:
|
||||
- 4 targets: 5 shots each (shot1-shot5) = 20 total shots
|
||||
- 20 targets: 2 shots each (shot1-shot2) = 40 total shots
|
||||
- 40 targets: 2 shots each (shot1-shot2) = 80 total shots
|
||||
"""
|
||||
shots = []
|
||||
targets = player_data.get('targets', {})
|
||||
|
||||
if not targets:
|
||||
return shots
|
||||
|
||||
# Sort targets by number to maintain order
|
||||
target_numbers = sorted([int(k) for k in targets.keys() if k.isdigit()])
|
||||
|
||||
# Determine shots per target based on tournament format
|
||||
if tournament_type and '4' in str(tournament_type):
|
||||
shots_per_target = 5 # 4 targets format: 5 shots each
|
||||
else:
|
||||
# Auto-detect if tournament_type not provided
|
||||
num_targets = len(target_numbers)
|
||||
if num_targets <= 6: # Likely 4 targets format (4 targets + maybe some extras)
|
||||
shots_per_target = 5
|
||||
else: # 20 or 40 targets format
|
||||
shots_per_target = 2
|
||||
|
||||
for target_num in target_numbers:
|
||||
target_data = targets[str(target_num)]
|
||||
|
||||
# Extract shots for this target
|
||||
for shot_num in range(1, shots_per_target + 1):
|
||||
shot_key = f'shot{shot_num}'
|
||||
shot_value = target_data.get(shot_key)
|
||||
|
||||
if shot_value is not None:
|
||||
shots.append(int(shot_value))
|
||||
|
||||
return shots
|
||||
|
||||
def count_shots_in_tournament(participant_data, tournament_type=None):
|
||||
"""
|
||||
Count total shots fired by a participant in a tournament
|
||||
Now properly handles all tournament formats:
|
||||
- 4 targets: 5 shots each = 20 total shots
|
||||
- 20 targets: 2 shots each = 40 total shots
|
||||
- 40 targets: 2 shots each = 80 total shots
|
||||
"""
|
||||
targets = participant_data.get('targets', {})
|
||||
shots_count = 0
|
||||
|
||||
if not targets:
|
||||
return shots_count
|
||||
|
||||
# Determine shots per target based on tournament format
|
||||
if tournament_type and '4' in str(tournament_type):
|
||||
max_shots_per_target = 5 # 4 targets format: 5 shots each
|
||||
else:
|
||||
# Auto-detect if tournament_type not provided
|
||||
target_count = len([k for k in targets.keys() if k.isdigit()])
|
||||
if target_count <= 6: # Likely 4 targets format
|
||||
max_shots_per_target = 5
|
||||
else: # 20 or 40 targets format
|
||||
max_shots_per_target = 2
|
||||
|
||||
for target in targets.values():
|
||||
for shot_num in range(1, max_shots_per_target + 1):
|
||||
shot_key = f'shot{shot_num}'
|
||||
if target.get(shot_key) is not None:
|
||||
shots_count += 1
|
||||
|
||||
return shots_count
|
||||
|
||||
def determine_tournament_type_from_archive(tournament_data):
|
||||
"""
|
||||
Determine tournament type from your archive data structure
|
||||
"""
|
||||
# First check the tournament_type field
|
||||
tournament_info = tournament_data.get('tournament', {})
|
||||
results_info = tournament_data.get('results', {})
|
||||
|
||||
tournament_type = (tournament_info.get('tournament_type') or
|
||||
results_info.get('tournament_type'))
|
||||
|
||||
if tournament_type:
|
||||
if '40' in tournament_type:
|
||||
return '40 Targets'
|
||||
elif '20' in tournament_type:
|
||||
return '20 Targets'
|
||||
elif '4' in tournament_type:
|
||||
return '4 Targets'
|
||||
|
||||
# Fallback: count targets from first completed player
|
||||
participants = results_info.get('participants', {})
|
||||
for participant_info in participants.values():
|
||||
if participant_info.get('completed'):
|
||||
targets = participant_info.get('targets', {})
|
||||
target_count = len([k for k in targets.keys() if k.isdigit()])
|
||||
|
||||
if target_count >= 30: # 40 targets
|
||||
return '40 Targets'
|
||||
elif target_count >= 10: # 20 targets
|
||||
return '20 Targets'
|
||||
elif target_count <= 6: # 4 targets (changed from <= 8 to <= 6)
|
||||
return '4 Targets'
|
||||
break
|
||||
|
||||
# Default fallback
|
||||
return '20 Targets'
|
||||
|
||||
@app.route('/api/debug/player/<int:player_id>')
|
||||
def debug_player_info(player_id):
|
||||
"""
|
||||
Debug endpoint to check player info and archive structure
|
||||
"""
|
||||
try:
|
||||
# Check player exists
|
||||
players_data = load_players()
|
||||
player = None
|
||||
for p in players_data['players']:
|
||||
if p['id'] == player_id:
|
||||
player = p
|
||||
break
|
||||
|
||||
# Check archive directory
|
||||
tournament_archives_path = 'tournament_archives'
|
||||
archive_exists = os.path.exists(tournament_archives_path)
|
||||
archive_files = []
|
||||
|
||||
if archive_exists:
|
||||
archive_files = glob.glob(f"{tournament_archives_path}/tournament_*.json")
|
||||
|
||||
# Check current working directory
|
||||
cwd = os.getcwd()
|
||||
|
||||
# List contents of current directory
|
||||
current_dir_contents = os.listdir('.')
|
||||
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'player_found': player is not None,
|
||||
'player_info': player,
|
||||
'current_working_directory': cwd,
|
||||
'current_dir_contents': current_dir_contents,
|
||||
'archive_directory_exists': archive_exists,
|
||||
'archive_directory_path': tournament_archives_path,
|
||||
'archive_files_found': len(archive_files),
|
||||
'archive_files': [os.path.basename(f) for f in archive_files[:5]] # First 5 files
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': str(e),
|
||||
'traceback': traceback.format_exc()
|
||||
}), 500
|
||||
|
||||
# Debug endpoint to see raw tournament data
|
||||
@app.route('/api/archive/tournament-file/<path:filename>')
|
||||
def get_tournament_file_debug(filename):
|
||||
"""
|
||||
Debug endpoint to see raw tournament file data
|
||||
"""
|
||||
try:
|
||||
tournament_archives_path = 'tournament_archives'
|
||||
file_path = os.path.join(tournament_archives_path, filename)
|
||||
|
||||
if not os.path.exists(file_path):
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'File not found'
|
||||
}), 404
|
||||
|
||||
with open(file_path, 'r') as f:
|
||||
data = json.load(f)
|
||||
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'file_path': file_path,
|
||||
'data': data
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': str(e)
|
||||
}), 500
|
||||
|
||||
@app.route('/api/archive/tournament-leaders', methods=['GET'])
|
||||
def api_get_tournament_leaders():
|
||||
"""API endpoint to get overall tournament leaders by tournament type"""
|
||||
try:
|
||||
tournaments = get_archived_tournaments()
|
||||
|
||||
# Group data by tournament type
|
||||
tournament_types = {
|
||||
'20_targets': {
|
||||
'name': '20 Targets',
|
||||
'description': '20 Targets (2 shots each)',
|
||||
'best_score': {'player_name': None, 'score': 0},
|
||||
'most_tens': {'player_name': None, 'tens': 0},
|
||||
'total_tournaments': 0
|
||||
},
|
||||
'40_targets': {
|
||||
'name': '40 Targets',
|
||||
'description': '40 Targets (2 shots each)',
|
||||
'best_score': {'player_name': None, 'score': 0},
|
||||
'most_tens': {'player_name': None, 'tens': 0},
|
||||
'total_tournaments': 0
|
||||
},
|
||||
'4_targets': {
|
||||
'name': '4 Targets',
|
||||
'description': '4 Targets (5 shots each)',
|
||||
'best_score': {'player_name': None, 'score': 0},
|
||||
'most_tens': {'player_name': None, 'tens': 0},
|
||||
'total_tournaments': 0
|
||||
}
|
||||
}
|
||||
|
||||
for tournament in tournaments:
|
||||
try:
|
||||
data = load_archive_file(tournament['filepath'])
|
||||
if not data:
|
||||
continue
|
||||
|
||||
results = data.get('results', {})
|
||||
participants = results.get('participants', {})
|
||||
tournament_type = tournament.get('tournament_type', '20_targets')
|
||||
|
||||
if tournament_type not in tournament_types or not participants:
|
||||
continue
|
||||
|
||||
tournament_types[tournament_type]['total_tournaments'] += 1
|
||||
|
||||
for player_id, participant in participants.items():
|
||||
if not participant.get('completed'):
|
||||
continue
|
||||
|
||||
player_name = participant.get('name', f'Player {player_id}')
|
||||
|
||||
# Check best score for this tournament type
|
||||
score = participant.get('total_score', 0)
|
||||
if score > tournament_types[tournament_type]['best_score']['score']:
|
||||
tournament_types[tournament_type]['best_score'] = {
|
||||
'player_name': player_name,
|
||||
'score': score
|
||||
}
|
||||
|
||||
# Count 10s for this player in this tournament
|
||||
targets = participant.get('targets', {})
|
||||
tens_count = 0
|
||||
for target in targets.values():
|
||||
if target.get('shot1') == 10:
|
||||
tens_count += 1
|
||||
if target.get('shot2') == 10:
|
||||
tens_count += 1
|
||||
# For 4_targets format, check additional shots
|
||||
for shot_num in range(3, 6): # shot3, shot4, shot5
|
||||
if target.get(f'shot{shot_num}') == 10:
|
||||
tens_count += 1
|
||||
|
||||
if tens_count > tournament_types[tournament_type]['most_tens']['tens']:
|
||||
tournament_types[tournament_type]['most_tens'] = {
|
||||
'player_name': player_name,
|
||||
'tens': tens_count
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error processing tournament {tournament.get('filepath', 'unknown')}: {e}")
|
||||
continue
|
||||
|
||||
# Convert to list format, only include types with data
|
||||
tournament_leaders = []
|
||||
for type_key, type_data in tournament_types.items():
|
||||
if type_data['total_tournaments'] > 0 and type_data['best_score']['player_name']:
|
||||
tournament_leaders.append({
|
||||
'id': type_key,
|
||||
'name': type_data['name'],
|
||||
'description': type_data['description'],
|
||||
'total_tournaments': type_data['total_tournaments'],
|
||||
'best_score': type_data['best_score'],
|
||||
'most_tens': type_data['most_tens']
|
||||
})
|
||||
|
||||
return jsonify({'status': 'success', 'tournament_types': tournament_leaders})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=5000, debug=True)
|
||||
@@ -880,12 +880,94 @@
|
||||
.refresh-indicator.show {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* PRINT STYLES - Clean professional printout */
|
||||
@media print {
|
||||
/* Hide everything except the rankings table */
|
||||
.navbar,
|
||||
.left-column,
|
||||
.tv-refresh-indicator {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Reset body and html for print */
|
||||
html, body {
|
||||
height: auto !important;
|
||||
overflow: visible !important;
|
||||
background: white !important;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
}
|
||||
|
||||
/* Make the container take full width and remove grid */
|
||||
.tv-container {
|
||||
display: block !important;
|
||||
height: auto !important;
|
||||
padding: 0 !important;
|
||||
gap: 0 !important;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* Style the right column for print */
|
||||
.right-column {
|
||||
background: white !important;
|
||||
border-radius: 0 !important;
|
||||
box-shadow: none !important;
|
||||
overflow: visible !important;
|
||||
display: block !important;
|
||||
height: auto !important;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Create a beautiful print header */
|
||||
.table-header {
|
||||
background: white !important;
|
||||
padding: 30px 0 30px 0 !important;
|
||||
border-bottom: 3px solid #007bff !important;
|
||||
text-align: center;
|
||||
margin-bottom: 30px !important;
|
||||
page-break-inside: avoid;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Add logo using CSS */
|
||||
.table-header::before {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 200px;
|
||||
height: 80px;
|
||||
margin: 0 auto 20px auto;
|
||||
background: url('/static/logo.png') no-repeat center center;
|
||||
background-size: contain;
|
||||
/* Fallback for when logo doesn't load */
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/* Add title using CSS */
|
||||
.table-header::after {
|
||||
content: "🎖️ LEAGUE RESULTS";
|
||||
display: block;
|
||||
font-size: 28pt;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
letter-spacing: 2px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
}
|
||||
|
||||
/* Hide the original table title */
|
||||
.table-title {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="navbar">
|
||||
<div class="navbar-brand">
|
||||
<div class="navbar-title">🏆 League Results</div>
|
||||
<div class="navbar-title">🎖️ League Results</div>
|
||||
</div>
|
||||
<div class="navbar-controls">
|
||||
<a href="/" class="nav-btn">← Dashboard</a>
|
||||
@@ -927,7 +1009,7 @@
|
||||
{% if participants and participants|length >= 3 %}
|
||||
<!-- Champion Podium -->
|
||||
<div class="champion-section">
|
||||
<div class="champion-title">🏆 League Champions</div>
|
||||
<div class="champion-title">🎖️ League Champions</div>
|
||||
<div class="champion-container">
|
||||
{% for i in range(3) %}
|
||||
{% set participant = participants[i] %}
|
||||
@@ -1091,11 +1173,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Auto-refresh indicator -->
|
||||
<div class="refresh-indicator" id="refreshIndicator">
|
||||
🔄 Updating...
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const participants = {{ participants|tojson }};
|
||||
const league = {{ league|tojson }};
|
||||
|
||||
@@ -653,19 +653,19 @@
|
||||
<div class="stat-label">Total Tournaments</div>
|
||||
</div>
|
||||
<div class="stat-card target-40">
|
||||
<span class="stat-icon">🎯</span>
|
||||
<span class="stat-icon">💪</span>
|
||||
<div class="stat-value">{{ tournaments|selectattr('tournament_type', 'equalto', '40_targets')|list|length if tournaments else 0 }}</div>
|
||||
<div class="stat-label">40-Target Tournaments</div>
|
||||
</div>
|
||||
<div class="stat-card target-20">
|
||||
<span class="stat-icon">🏹</span>
|
||||
<span class="stat-icon">⚡</span>
|
||||
<div class="stat-value">{{ tournaments|selectattr('tournament_type', 'equalto', '20_targets')|list|length if tournaments else 0 }}</div>
|
||||
<div class="stat-label">20-Target Tournaments</div>
|
||||
</div>
|
||||
<div class="stat-card target-4">
|
||||
<span class="stat-icon">🎪</span>
|
||||
<span class="stat-icon">🎯</span>
|
||||
<div class="stat-value">{{ tournaments|selectattr('tournament_type', 'equalto', '4_targets')|list|length if tournaments else 0 }}</div>
|
||||
<div class="stat-label">4-Target Tournaments</div>
|
||||
<div class="stat-label">4-Target <br> Tournaments</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-icon">👥</span>
|
||||
@@ -679,11 +679,6 @@
|
||||
<div class="section">
|
||||
<div class="section-title">
|
||||
<span>🎖️ League Championships</span>
|
||||
<div class="section-controls">
|
||||
<button class="filter-btn active" onclick="filterLeagues('all')">All</button>
|
||||
<button class="filter-btn" onclick="filterLeagues('completed')">Completed</button>
|
||||
<button class="filter-btn" onclick="filterLeagues('recent')">Recent</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="archive-grid" id="leagues-grid">
|
||||
{% for league in leagues %}
|
||||
@@ -716,12 +711,6 @@
|
||||
</div>
|
||||
<div class="archive-actions">
|
||||
<a href="/archive/league/{{ league.filename }}" class="action-btn view-btn">🏆 View</a>
|
||||
<button class="action-btn edit-btn" onclick="event.stopPropagation(); editArchive('league', '{{ league.filename }}', {{ league|tojson }})">
|
||||
✏️ Edit
|
||||
</button>
|
||||
<button class="action-btn delete-btn" onclick="event.stopPropagation(); deleteArchive('league', '{{ league.filename }}')">
|
||||
🗑️
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -738,12 +727,7 @@
|
||||
{% if tournaments_40 %}
|
||||
<div class="section">
|
||||
<div class="section-title">
|
||||
<span>🎯 40-Target Tournaments</span>
|
||||
<div class="section-controls">
|
||||
<button class="filter-btn active" onclick="filterTournaments('40', 'all')">All</button>
|
||||
<button class="filter-btn" onclick="filterTournaments('40', 'finished')">Finished</button>
|
||||
<button class="filter-btn" onclick="filterTournaments('40', 'recent')">Recent</button>
|
||||
</div>
|
||||
<span>💪 40-Target Tournaments</span>
|
||||
</div>
|
||||
<div class="archive-grid" id="tournaments-40-grid">
|
||||
{% for tournament in tournaments_40 %}
|
||||
@@ -776,12 +760,6 @@
|
||||
</div>
|
||||
<div class="archive-actions">
|
||||
<a href="/archive/tournament/{{ tournament.filename }}" class="action-btn view-btn">📊 View</a>
|
||||
<button class="action-btn edit-btn" onclick="event.stopPropagation(); editArchive('tournament', '{{ tournament.filename }}', {{ tournament|tojson }})">
|
||||
✏️ Edit
|
||||
</button>
|
||||
<button class="action-btn delete-btn" onclick="event.stopPropagation(); deleteArchive('tournament', '{{ tournament.filename }}')">
|
||||
🗑️
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -795,12 +773,7 @@
|
||||
{% if tournaments_20 %}
|
||||
<div class="section">
|
||||
<div class="section-title">
|
||||
<span>🏹 20-Target Tournaments</span>
|
||||
<div class="section-controls">
|
||||
<button class="filter-btn active" onclick="filterTournaments('20', 'all')">All</button>
|
||||
<button class="filter-btn" onclick="filterTournaments('20', 'finished')">Finished</button>
|
||||
<button class="filter-btn" onclick="filterTournaments('20', 'recent')">Recent</button>
|
||||
</div>
|
||||
<span>⚡ 20-Target Tournaments</span>
|
||||
</div>
|
||||
<div class="archive-grid" id="tournaments-20-grid">
|
||||
{% for tournament in tournaments_20 %}
|
||||
@@ -833,12 +806,6 @@
|
||||
</div>
|
||||
<div class="archive-actions">
|
||||
<a href="/archive/tournament/{{ tournament.filename }}" class="action-btn view-btn">📊 View</a>
|
||||
<button class="action-btn edit-btn" onclick="event.stopPropagation(); editArchive('tournament', '{{ tournament.filename }}', {{ tournament|tojson }})">
|
||||
✏️ Edit
|
||||
</button>
|
||||
<button class="action-btn delete-btn" onclick="event.stopPropagation(); deleteArchive('tournament', '{{ tournament.filename }}')">
|
||||
🗑️
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -852,12 +819,7 @@
|
||||
{% if tournaments_4 %}
|
||||
<div class="section">
|
||||
<div class="section-title">
|
||||
<span>🎪 4-Target Tournaments</span>
|
||||
<div class="section-controls">
|
||||
<button class="filter-btn active" onclick="filterTournaments('4', 'all')">All</button>
|
||||
<button class="filter-btn" onclick="filterTournaments('4', 'finished')">Finished</button>
|
||||
<button class="filter-btn" onclick="filterTournaments('4', 'recent')">Recent</button>
|
||||
</div>
|
||||
<span>🎯 4-Target Tournaments</span>
|
||||
</div>
|
||||
<div class="archive-grid" id="tournaments-4-grid">
|
||||
{% for tournament in tournaments_4 %}
|
||||
@@ -890,12 +852,6 @@
|
||||
</div>
|
||||
<div class="archive-actions">
|
||||
<a href="/archive/tournament/{{ tournament.filename }}" class="action-btn view-btn">📊 View</a>
|
||||
<button class="action-btn edit-btn" onclick="event.stopPropagation(); editArchive('tournament', '{{ tournament.filename }}', {{ tournament|tojson }})">
|
||||
✏️ Edit
|
||||
</button>
|
||||
<button class="action-btn delete-btn" onclick="event.stopPropagation(); deleteArchive('tournament', '{{ tournament.filename }}')">
|
||||
🗑️
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -910,11 +866,6 @@
|
||||
<div class="section">
|
||||
<div class="section-title">
|
||||
<span>🏆 Other Tournaments</span>
|
||||
<div class="section-controls">
|
||||
<button class="filter-btn active" onclick="filterTournaments('other', 'all')">All</button>
|
||||
<button class="filter-btn" onclick="filterTournaments('other', 'finished')">Finished</button>
|
||||
<button class="filter-btn" onclick="filterTournaments('other', 'recent')">Recent</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="archive-grid" id="tournaments-other-grid">
|
||||
{% for tournament in tournaments_other %}
|
||||
@@ -947,12 +898,6 @@
|
||||
</div>
|
||||
<div class="archive-actions">
|
||||
<a href="/archive/tournament/{{ tournament.filename }}" class="action-btn view-btn">📊 View</a>
|
||||
<button class="action-btn edit-btn" onclick="event.stopPropagation(); editArchive('tournament', '{{ tournament.filename }}', {{ tournament|tojson }})">
|
||||
✏️ Edit
|
||||
</button>
|
||||
<button class="action-btn delete-btn" onclick="event.stopPropagation(); deleteArchive('tournament', '{{ tournament.filename }}')">
|
||||
🗑️
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1067,67 +1012,6 @@
|
||||
);
|
||||
}
|
||||
|
||||
// Edit function
|
||||
function editArchive(type, filename, data) {
|
||||
editingItem = { type, filename, data };
|
||||
|
||||
document.getElementById('editModalTitle').textContent = `Edit ${type.charAt(0).toUpperCase() + type.slice(1)}`;
|
||||
document.getElementById('editName').value = data.name || `${type} - ${data.created_at?.substring(0, 10) || 'Unknown'}`;
|
||||
|
||||
// For format type, use a default since this isn't typically stored
|
||||
document.getElementById('editType').value = type === 'league' ? 'league' : 'single_elimination';
|
||||
document.getElementById('editNotes').value = data.notes || '';
|
||||
|
||||
// Show/hide target count field - both tournaments and leagues have tournament_type
|
||||
const targetCountGroup = document.getElementById('targetCountGroup');
|
||||
if (type === 'tournament' || type === 'league') {
|
||||
targetCountGroup.style.display = 'flex';
|
||||
document.getElementById('editTargetCount').value = data.tournament_type || '20_targets';
|
||||
} else {
|
||||
targetCountGroup.style.display = 'none';
|
||||
}
|
||||
|
||||
document.getElementById('editModal').classList.add('active');
|
||||
}
|
||||
|
||||
// Save edit function
|
||||
async function saveEdit() {
|
||||
if (!editingItem) return;
|
||||
|
||||
const updatedData = {
|
||||
name: document.getElementById('editName').value,
|
||||
notes: document.getElementById('editNotes').value
|
||||
};
|
||||
|
||||
// For both tournaments and leagues, save the tournament_type (target format)
|
||||
if (editingItem.type === 'tournament' || editingItem.type === 'league') {
|
||||
updatedData.tournament_type = document.getElementById('editTargetCount').value;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/archive/update/${editingItem.type}/${editingItem.filename}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(updatedData)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.status === 'success') {
|
||||
alert('Archive updated successfully');
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Error updating archive: ' + result.message);
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Error updating archive: ' + error.message);
|
||||
}
|
||||
|
||||
closeEditModal();
|
||||
}
|
||||
|
||||
// Filtering functions
|
||||
function filterLeagues(filter) {
|
||||
const cards = document.querySelectorAll('#leagues-grid .archive-card');
|
||||
@@ -1151,30 +1035,6 @@
|
||||
});
|
||||
}
|
||||
|
||||
function filterTournaments(targetCount, filter) {
|
||||
const gridId = targetCount === 'other' ? 'tournaments-other-grid' : `tournaments-${targetCount}-grid`;
|
||||
const cards = document.querySelectorAll(`#${gridId} .archive-card`);
|
||||
const section = document.querySelector(`#${gridId}`).closest('.section');
|
||||
const buttons = section.querySelectorAll('.filter-btn');
|
||||
|
||||
// Update active button
|
||||
buttons.forEach(btn => btn.classList.remove('active'));
|
||||
event.target.classList.add('active');
|
||||
|
||||
// Filter cards
|
||||
cards.forEach(card => {
|
||||
const isFinished = card.dataset.status === 'finished';
|
||||
const date = new Date(card.dataset.date);
|
||||
const isRecent = (Date.now() - date.getTime()) < (30 * 24 * 60 * 60 * 1000); // Last 30 days
|
||||
|
||||
let show = true;
|
||||
if (filter === 'finished' && !isFinished) show = false;
|
||||
if (filter === 'recent' && !isRecent) show = false;
|
||||
|
||||
card.classList.toggle('hidden', !show);
|
||||
});
|
||||
}
|
||||
|
||||
// Modal functions
|
||||
function showModal(title, message, confirmCallback) {
|
||||
document.getElementById('modalTitle').textContent = title;
|
||||
@@ -1211,38 +1071,6 @@
|
||||
closeEditModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize page
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log('📚 Enhanced Archive loaded');
|
||||
console.log('🏆 Total Tournaments:', {{ tournaments|length if tournaments else 0 }});
|
||||
console.log('🎖️ Leagues:', {{ leagues|length if leagues else 0 }});
|
||||
|
||||
{% if tournaments %}
|
||||
const tournaments = {{ tournaments|tojson }};
|
||||
console.log('Tournament data sample:', tournaments[0] || 'No tournaments');
|
||||
|
||||
// Count tournaments by type
|
||||
const tournaments40 = tournaments.filter(t => t.tournament_type === '40_targets');
|
||||
const tournaments20 = tournaments.filter(t => t.tournament_type === '20_targets');
|
||||
const tournaments4 = tournaments.filter(t => t.tournament_type === '4_targets');
|
||||
const tournamentsOther = tournaments.filter(t => !['4_targets', '20_targets', '40_targets'].includes(t.tournament_type));
|
||||
|
||||
console.log('🎯 40-Target Tournaments:', tournaments40.length);
|
||||
console.log('🏹 20-Target Tournaments:', tournaments20.length);
|
||||
console.log('🎪 4-Target Tournaments:', tournaments4.length);
|
||||
console.log('🏆 Other Tournaments:', tournamentsOther.length);
|
||||
|
||||
// Log tournament types for debugging
|
||||
const tournamentTypes = [...new Set(tournaments.map(t => t.tournament_type))];
|
||||
console.log('Available tournament types:', tournamentTypes);
|
||||
{% else %}
|
||||
console.log('🎯 40-Target Tournaments: 0');
|
||||
console.log('🏹 20-Target Tournaments: 0');
|
||||
console.log('🎪 4-Target Tournaments: 0');
|
||||
console.log('🏆 Other Tournaments: 0');
|
||||
{% endif %}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -65,28 +65,6 @@
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.nav-btn {
|
||||
background: #f8f9fa;
|
||||
border: 2px solid #e9ecef;
|
||||
cursor: pointer;
|
||||
padding: 12px 20px;
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
color: #333;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.nav-btn:hover {
|
||||
background: #e9ecef;
|
||||
border-color: #007bff;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||
transform: translateY(-1px);
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.nav-btn.secondary {
|
||||
background: #6c757d;
|
||||
border-color: #495057;
|
||||
@@ -146,10 +124,142 @@
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* Tab Navigation */
|
||||
.tab-navigation {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
margin-bottom: 25px;
|
||||
border-bottom: 2px solid #f1f3f4;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: #666;
|
||||
border-bottom: 3px solid transparent;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
color: #007bff;
|
||||
border-bottom-color: #007bff;
|
||||
}
|
||||
|
||||
.tab-btn:hover {
|
||||
color: #007bff;
|
||||
background: rgba(0, 123, 255, 0.05);
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Tournament Leaders Section */
|
||||
.tournament-leaders {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.tournament-card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid #e9ecef;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tournament-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4px;
|
||||
background: linear-gradient(90deg, #ffc107, #fd7e14);
|
||||
}
|
||||
|
||||
.tournament-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.tournament-name {
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.tournament-date {
|
||||
background: #f8f9fa;
|
||||
color: #666;
|
||||
padding: 4px 10px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.leaders-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.leader-category {
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.category-title {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
color: #666;
|
||||
margin-bottom: 8px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.leader-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.leader-name {
|
||||
font-size: 1.1rem;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.leader-score {
|
||||
font-size: 1.3rem;
|
||||
font-weight: bold;
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.leader-score.tens {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
/* Compact Stats Overview */
|
||||
.stats-overview {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||
gap: 15px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
@@ -453,7 +563,25 @@
|
||||
}
|
||||
|
||||
.stats-overview {
|
||||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||||
grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
|
||||
}
|
||||
|
||||
.tournament-leaders {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.leaders-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.tab-navigation {
|
||||
flex-wrap: wrap;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
padding: 8px 16px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -461,7 +589,7 @@
|
||||
<body>
|
||||
<!-- Navigation Bar -->
|
||||
<div class="navbar">
|
||||
<div class="navbar-title">👤 Player Analysis</div>
|
||||
<div class="navbar-title">🎯 Player Analysis</div>
|
||||
<div class="navbar-controls">
|
||||
<a href="/" class="nav-btn primary">← Dashboard</a>
|
||||
<a href="/archive" class="nav-btn secondary">📚 Archive</a>
|
||||
@@ -473,52 +601,84 @@
|
||||
<div class="stats-overview">
|
||||
<div class="stat-card">
|
||||
<span class="stat-icon">👥</span>
|
||||
<div class="stat-value" id="totalPlayers">{{ players|length }}</div>
|
||||
<div class="stat-value" id="totalPlayers">Loading...</div>
|
||||
<div class="stat-label">Total Players</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-icon">📊</span>
|
||||
<div class="stat-value" id="avgTournaments">{{ overview_stats.avg_tournaments if overview_stats else 0 }}</div>
|
||||
<div class="stat-label">Avg Tournaments</div>
|
||||
<span class="stat-icon">🎯</span>
|
||||
<div class="stat-value" id="totalTournaments">Loading...</div>
|
||||
<div class="stat-label">Total Tournaments</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-icon">🏆</span>
|
||||
<div class="stat-value" id="topScore">{{ overview_stats.top_score if overview_stats else 0 }}</div>
|
||||
<div class="stat-label">Highest Score</div>
|
||||
<div class="stat-value" id="score20Targets">Loading...</div>
|
||||
<div class="stat-label">Best 20 Targets</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-icon">🎖️</span>
|
||||
<div class="stat-value" id="score40Targets">Loading...</div>
|
||||
<div class="stat-label">Best 40 Targets</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-icon">🥇</span>
|
||||
<div class="stat-value" id="score4Targets">Loading...</div>
|
||||
<div class="stat-label">Best 4 Targets</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab Navigation -->
|
||||
<div class="section">
|
||||
<div class="section-title">Select a Player to Analyze</div>
|
||||
|
||||
<!-- Controls -->
|
||||
<div class="controls">
|
||||
<input type="text" class="search-box" id="searchBox" placeholder="🔍 Search players by name...">
|
||||
|
||||
<select class="sort-select" id="sortSelect">
|
||||
<option value="name">Sort by Name</option>
|
||||
<option value="best_score">Sort by Best Score</option>
|
||||
<option value="average_score">Sort by Average Score</option>
|
||||
<option value="total_tournaments">Sort by Total Tournaments</option>
|
||||
<option value="total_leagues">Sort by Total Leagues</option>
|
||||
<option value="total_shots">Sort by Total Shots</option>
|
||||
</select>
|
||||
<div class="tab-navigation">
|
||||
<button class="tab-btn active" onclick="switchTab('tournament-leaders')">🏆 Overall Champions</button>
|
||||
<button class="tab-btn" onclick="switchTab('players')">👥 All Players</button>
|
||||
</div>
|
||||
|
||||
<!-- Players Grid -->
|
||||
<div class="players-grid" id="playersGrid">
|
||||
<div class="loading">
|
||||
<div class="loading-spinner"></div>
|
||||
<p>Loading players...</p>
|
||||
<!-- Tournament Leaders Tab -->
|
||||
<div id="tournament-leaders" class="tab-content active">
|
||||
<div class="section-title">Overall Champions by Tournament Type</div>
|
||||
|
||||
<div class="tournament-leaders" id="tournamentLeaders">
|
||||
<div class="loading">
|
||||
<div class="loading-spinner"></div>
|
||||
<p>Loading tournament data...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Players Tab -->
|
||||
<div id="players" class="tab-content">
|
||||
<div class="section-title">Select a Player to Analyze</div>
|
||||
|
||||
<!-- Controls -->
|
||||
<div class="controls">
|
||||
<input type="text" class="search-box" id="searchBox" placeholder="🔍 Search players by name...">
|
||||
|
||||
<select class="sort-select" id="sortSelect">
|
||||
<option value="name">Sort by Name</option>
|
||||
<option value="best_score">Sort by Best Score</option>
|
||||
<option value="average_score">Sort by Average Score</option>
|
||||
<option value="total_tournaments">Sort by Total Tournaments</option>
|
||||
<option value="total_leagues">Sort by Total Leagues</option>
|
||||
<option value="total_shots">Sort by Total Shots</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Players Grid -->
|
||||
<div class="players-grid" id="playersGrid">
|
||||
<div class="loading">
|
||||
<div class="loading-spinner"></div>
|
||||
<p>Loading players...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Players data from Flask
|
||||
let playersData = {{ players|tojson }};
|
||||
let filteredPlayers = [...playersData];
|
||||
// Global data
|
||||
let playersData = [];
|
||||
let tournamentData = [];
|
||||
let filteredPlayers = [];
|
||||
let currentFilter = 'all';
|
||||
let currentSort = 'name';
|
||||
let playersWithStats = [];
|
||||
@@ -526,9 +686,180 @@
|
||||
// Initialize page
|
||||
function initializePage() {
|
||||
loadPlayerStats();
|
||||
loadTournamentLeaders();
|
||||
setupEventListeners();
|
||||
}
|
||||
|
||||
// Switch between tabs
|
||||
function switchTab(tabName) {
|
||||
// Hide all tab contents
|
||||
document.querySelectorAll('.tab-content').forEach(content => {
|
||||
content.classList.remove('active');
|
||||
});
|
||||
|
||||
// Remove active class from all tab buttons
|
||||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||||
btn.classList.remove('active');
|
||||
});
|
||||
|
||||
// Show selected tab content
|
||||
document.getElementById(tabName).classList.add('active');
|
||||
|
||||
// Add active class to clicked button
|
||||
event.target.classList.add('active');
|
||||
}
|
||||
|
||||
// Load tournament leaders data
|
||||
async function loadTournamentLeaders() {
|
||||
try {
|
||||
const response = await fetch('/api/archive/tournament-leaders');
|
||||
const result = await response.json();
|
||||
|
||||
if (result.status === 'success') {
|
||||
tournamentData = result.tournament_types;
|
||||
renderTournamentLeaders();
|
||||
updateOverallStats();
|
||||
} else {
|
||||
console.error('Failed to load tournament data:', result.message);
|
||||
document.getElementById('tournamentLeaders').innerHTML = `
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">🎯</div>
|
||||
<h3>Unable to Load Tournament Data</h3>
|
||||
<p>${result.message || 'Please try refreshing the page'}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading tournament data:', error);
|
||||
document.getElementById('tournamentLeaders').innerHTML = `
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">🎯</div>
|
||||
<h3>Unable to Load Tournament Data</h3>
|
||||
<p>Please try refreshing the page</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// Render tournament leaders
|
||||
function renderTournamentLeaders() {
|
||||
const leadersContainer = document.getElementById('tournamentLeaders');
|
||||
|
||||
if (tournamentData.length === 0) {
|
||||
leadersContainer.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">🎯</div>
|
||||
<h3>No Tournament Data Available</h3>
|
||||
<p>Tournament results will appear here once available</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
leadersContainer.innerHTML = tournamentData.map(tournamentType => `
|
||||
<div class="tournament-card">
|
||||
<div class="tournament-header">
|
||||
<div class="tournament-name">${tournamentType.name}</div>
|
||||
<div class="tournament-date">${tournamentType.total_tournaments} tournament${tournamentType.total_tournaments !== 1 ? 's' : ''}</div>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; margin-bottom: 15px; color: #666; font-size: 0.9rem; font-weight: 500;">
|
||||
${tournamentType.description}
|
||||
</div>
|
||||
|
||||
<div class="leaders-grid">
|
||||
<div class="leader-category">
|
||||
<div class="category-title">🏆 Best Score</div>
|
||||
<div class="leader-info">
|
||||
<div class="leader-name">${tournamentType.best_score.player_name || 'No data'}</div>
|
||||
<div class="leader-score">${tournamentType.best_score.score || 0}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="leader-category">
|
||||
<div class="category-title">🎪 Most 10s</div>
|
||||
<div class="leader-info">
|
||||
<div class="leader-name">${tournamentType.most_tens.player_name || 'No data'}</div>
|
||||
<div class="leader-score tens">${tournamentType.most_tens.tens || 0} 10s</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// Update overall statistics
|
||||
function updateOverallStats() {
|
||||
let totalTournaments = 0;
|
||||
let score20Targets = 0;
|
||||
let score40Targets = 0;
|
||||
let score4Targets = 0;
|
||||
|
||||
// Calculate statistics across all tournament types
|
||||
tournamentData.forEach(tournamentType => {
|
||||
totalTournaments += tournamentType.total_tournaments || 0;
|
||||
|
||||
// Get best scores for each tournament type
|
||||
if (tournamentType.id === '20_targets' && tournamentType.best_score) {
|
||||
score20Targets = tournamentType.best_score.score || 0;
|
||||
} else if (tournamentType.id === '40_targets' && tournamentType.best_score) {
|
||||
score40Targets = tournamentType.best_score.score || 0;
|
||||
} else if (tournamentType.id === '4_targets' && tournamentType.best_score) {
|
||||
score4Targets = tournamentType.best_score.score || 0;
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('totalTournaments').textContent = totalTournaments;
|
||||
document.getElementById('score20Targets').textContent = score20Targets || '-';
|
||||
document.getElementById('score40Targets').textContent = score40Targets || '-';
|
||||
document.getElementById('score4Targets').textContent = score4Targets || '-';
|
||||
}
|
||||
|
||||
// Get tournament type display name
|
||||
function getTournamentTypeDisplay(tournamentType) {
|
||||
const typeMap = {
|
||||
'20_targets': '20 Targets (2 shots each)',
|
||||
'40_targets': '40 Targets (2 shots each)',
|
||||
'4_targets': '4 Targets (5 shots each)'
|
||||
};
|
||||
return typeMap[tournamentType] || tournamentType;
|
||||
}
|
||||
|
||||
// Format date for display
|
||||
function formatDate(dateString) {
|
||||
if (!dateString) return 'Unknown Date';
|
||||
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
if (isNaN(date.getTime())) {
|
||||
// Try to parse filename format YYYYMMDD_HHMMSS
|
||||
if (dateString.length >= 8) {
|
||||
const year = dateString.substring(0, 4);
|
||||
const month = dateString.substring(4, 6);
|
||||
const day = dateString.substring(6, 8);
|
||||
const parsedDate = new Date(`${year}-${month}-${day}`);
|
||||
if (!isNaN(parsedDate.getTime())) {
|
||||
return parsedDate.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
});
|
||||
}
|
||||
}
|
||||
return 'Unknown Date';
|
||||
}
|
||||
|
||||
return date.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
});
|
||||
} catch (error) {
|
||||
return 'Unknown Date';
|
||||
}
|
||||
}
|
||||
|
||||
// Load player statistics
|
||||
async function loadPlayerStats() {
|
||||
try {
|
||||
@@ -539,9 +870,13 @@
|
||||
playersWithStats = result.players;
|
||||
playersData = playersWithStats;
|
||||
filteredPlayers = [...playersWithStats];
|
||||
|
||||
// Update total players count
|
||||
document.getElementById('totalPlayers').textContent = playersData.length;
|
||||
|
||||
filterAndSortPlayers();
|
||||
} else {
|
||||
console.error('Failed to load player stats');
|
||||
console.error('Failed to load player stats:', result.message);
|
||||
renderPlayersBasic();
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -554,22 +889,12 @@
|
||||
function setupEventListeners() {
|
||||
const searchBox = document.getElementById('searchBox');
|
||||
const sortSelect = document.getElementById('sortSelect');
|
||||
const filterButtons = document.querySelectorAll('.filter-btn');
|
||||
|
||||
searchBox.addEventListener('input', filterAndSortPlayers);
|
||||
sortSelect.addEventListener('change', (e) => {
|
||||
currentSort = e.target.value;
|
||||
filterAndSortPlayers();
|
||||
});
|
||||
|
||||
filterButtons.forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
filterButtons.forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
currentFilter = btn.dataset.filter;
|
||||
filterAndSortPlayers();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Filter and sort players
|
||||
@@ -675,42 +1000,70 @@
|
||||
|
||||
// Render players without stats (fallback)
|
||||
function renderPlayersBasic() {
|
||||
const playersGrid = document.getElementById('playersGrid');
|
||||
|
||||
playersGrid.innerHTML = playersData.map(player => `
|
||||
<div class="player-card ${player.current_player ? 'current-player' : 'archived-player'}"
|
||||
onclick="viewPlayerStats(${player.id})">
|
||||
<div class="player-header">
|
||||
<div class="player-name">${player.name}</div>
|
||||
<div class="player-badge ${player.current_player ? 'current' : 'archived'}">
|
||||
${player.current_player ? 'Current' : 'Archived'}
|
||||
</div>
|
||||
</div>
|
||||
// If we can't get stats, try to get basic player list
|
||||
fetch('/api/players')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const basicPlayers = data.players || [];
|
||||
const playersGrid = document.getElementById('playersGrid');
|
||||
|
||||
<div class="player-stats">
|
||||
<div class="stat-item">
|
||||
<span class="icon">🎯</span>
|
||||
<span class="label">Tournaments:</span>
|
||||
<span class="value">Loading...</span>
|
||||
if (basicPlayers.length === 0) {
|
||||
playersGrid.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">👤</div>
|
||||
<h3>No Players Found</h3>
|
||||
<p>No player data available</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
playersGrid.innerHTML = basicPlayers.map(player => `
|
||||
<div class="player-card ${player.enabled ? 'current-player' : 'archived-player'}"
|
||||
onclick="viewPlayerStats(${player.id})">
|
||||
<div class="player-header">
|
||||
<div class="player-name">${player.name}</div>
|
||||
<div class="player-badge ${player.enabled ? 'current' : 'archived'}">
|
||||
${player.enabled ? 'Current' : 'Archived'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="player-stats">
|
||||
<div class="stat-item">
|
||||
<span class="icon">🎯</span>
|
||||
<span class="label">Tournaments:</span>
|
||||
<span class="value">Loading...</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="icon">🏆</span>
|
||||
<span class="label">Leagues:</span>
|
||||
<span class="value">Loading...</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="icon">📈</span>
|
||||
<span class="label">Best Score:</span>
|
||||
<span class="value">Loading...</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="icon">📊</span>
|
||||
<span class="label">Average:</span>
|
||||
<span class="value">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="icon">🏆</span>
|
||||
<span class="label">Leagues:</span>
|
||||
<span class="value">Loading...</span>
|
||||
`).join('');
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading basic players:', error);
|
||||
const playersGrid = document.getElementById('playersGrid');
|
||||
playersGrid.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">❌</div>
|
||||
<h3>Error Loading Players</h3>
|
||||
<p>Unable to load player data</p>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="icon">📈</span>
|
||||
<span class="label">Best Score:</span>
|
||||
<span class="value">Loading...</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="icon">📊</span>
|
||||
<span class="label">Average:</span>
|
||||
<span class="value">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
// Create mini performance chart for player
|
||||
|
||||
@@ -48,14 +48,14 @@
|
||||
background: #f8f9fa;
|
||||
border: 2px solid #e9ecef;
|
||||
cursor: pointer;
|
||||
padding: 8px 16px;
|
||||
padding: 12px 20px;
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
color: #333;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
font-size: 0.85rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.nav-btn:hover {
|
||||
@@ -87,7 +87,7 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Top Section - Stats and Chart */
|
||||
/* Top Section - Stats and Charts */
|
||||
.top-section {
|
||||
display: grid;
|
||||
grid-template-columns: 320px 1fr;
|
||||
@@ -146,8 +146,8 @@
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Chart Panel */
|
||||
.chart-panel {
|
||||
/* Charts Panel */
|
||||
.charts-panel {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
@@ -156,10 +156,151 @@
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Tournament Type Buttons */
|
||||
.tournament-type-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 15px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.type-btn {
|
||||
background: #f8f9fa;
|
||||
border: 2px solid #e9ecef;
|
||||
padding: 10px 16px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.type-btn:hover {
|
||||
border-color: #007bff;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.type-btn.active {
|
||||
color: white;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.type-btn.active.targets-40 {
|
||||
background: #dc3545;
|
||||
border-color: #dc3545;
|
||||
}
|
||||
|
||||
.type-btn.active.targets-20 {
|
||||
background: #28a745;
|
||||
border-color: #28a745;
|
||||
}
|
||||
|
||||
.type-btn.active.targets-4 {
|
||||
background: #007bff;
|
||||
border-color: #007bff;
|
||||
}
|
||||
|
||||
/* Chart Info Header */
|
||||
.chart-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
padding: 12px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.chart-stats {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.chart-stat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.chart-stat-value {
|
||||
font-size: 1.1rem;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.chart-stat-label {
|
||||
color: #666;
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* Shot Accuracy Stats */
|
||||
.accuracy-stats {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
font-size: 0.8rem;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.accuracy-stat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 4px 8px;
|
||||
background: white;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #e9ecef;
|
||||
min-width: 35px;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.accuracy-stat:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 2px 6px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.accuracy-value {
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.accuracy-label {
|
||||
color: #666;
|
||||
font-size: 0.65rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
min-height: 180px;
|
||||
min-height: 200px;
|
||||
background: white;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #e9ecef;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.no-data {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 200px;
|
||||
color: #999;
|
||||
font-size: 0.9rem;
|
||||
font-style: italic;
|
||||
background: #f8f9fa;
|
||||
border-radius: 6px;
|
||||
border: 1px dashed #dee2e6;
|
||||
}
|
||||
|
||||
/* Bottom Section - History */
|
||||
@@ -412,7 +553,7 @@
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<!-- Top Section - Stats and Chart -->
|
||||
<!-- Top Section - Stats and Charts -->
|
||||
<div class="top-section">
|
||||
<!-- Stats Panel -->
|
||||
<div class="stats-panel">
|
||||
@@ -445,11 +586,92 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Performance Chart -->
|
||||
<div class="chart-panel">
|
||||
<div class="panel-title">📈 Performance Trend</div>
|
||||
<!-- Performance Charts -->
|
||||
<div class="charts-panel">
|
||||
<div class="panel-title">📈 Performance by Tournament Type</div>
|
||||
|
||||
<!-- Tournament Type Buttons -->
|
||||
<div class="tournament-type-buttons">
|
||||
<button class="type-btn active targets-40" data-type="40 Targets">
|
||||
<span>💪</span> 40 Targets
|
||||
</button>
|
||||
<button class="type-btn targets-20" data-type="20 Targets">
|
||||
<span>⚡</span> 20 Targets
|
||||
</button>
|
||||
<button class="type-btn targets-4" data-type="4 Targets">
|
||||
<span>🎯</span> 4 Targets
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Chart Info and Stats -->
|
||||
<div class="chart-info" id="chartInfo">
|
||||
<div class="chart-stats">
|
||||
<div class="chart-stat">
|
||||
<div class="chart-stat-value" id="gameCount">0</div>
|
||||
<div class="chart-stat-label">Games</div>
|
||||
</div>
|
||||
<div class="chart-stat">
|
||||
<div class="chart-stat-value" id="avgScore">0</div>
|
||||
<div class="chart-stat-label">Average</div>
|
||||
</div>
|
||||
<div class="chart-stat">
|
||||
<div class="chart-stat-value" id="bestScore">0</div>
|
||||
<div class="chart-stat-label">Best</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Shot Accuracy Stats -->
|
||||
<div class="accuracy-stats" id="accuracyStats">
|
||||
<div class="accuracy-stat">
|
||||
<div class="accuracy-value" id="tens">0</div>
|
||||
<div class="accuracy-label">10s</div>
|
||||
</div>
|
||||
<div class="accuracy-stat">
|
||||
<div class="accuracy-value" id="nines">0</div>
|
||||
<div class="accuracy-label">9s</div>
|
||||
</div>
|
||||
<div class="accuracy-stat">
|
||||
<div class="accuracy-value" id="eights">0</div>
|
||||
<div class="accuracy-label">8s</div>
|
||||
</div>
|
||||
<div class="accuracy-stat">
|
||||
<div class="accuracy-value" id="sevens">0</div>
|
||||
<div class="accuracy-label">7s</div>
|
||||
</div>
|
||||
<div class="accuracy-stat">
|
||||
<div class="accuracy-value" id="sixes">0</div>
|
||||
<div class="accuracy-label">6s</div>
|
||||
</div>
|
||||
<div class="accuracy-stat">
|
||||
<div class="accuracy-value" id="fives">0</div>
|
||||
<div class="accuracy-label">5s</div>
|
||||
</div>
|
||||
<div class="accuracy-stat">
|
||||
<div class="accuracy-value" id="fours">0</div>
|
||||
<div class="accuracy-label">4s</div>
|
||||
</div>
|
||||
<div class="accuracy-stat">
|
||||
<div class="accuracy-value" id="threes">0</div>
|
||||
<div class="accuracy-label">3s</div>
|
||||
</div>
|
||||
<div class="accuracy-stat">
|
||||
<div class="accuracy-value" id="twos">0</div>
|
||||
<div class="accuracy-label">2s</div>
|
||||
</div>
|
||||
<div class="accuracy-stat">
|
||||
<div class="accuracy-value" id="ones">0</div>
|
||||
<div class="accuracy-label">1s</div>
|
||||
</div>
|
||||
<div class="accuracy-stat">
|
||||
<div class="accuracy-value" id="zeros">0</div>
|
||||
<div class="accuracy-label">0s</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Single Chart Container -->
|
||||
<div class="chart-container">
|
||||
<canvas id="performanceChart"></canvas>
|
||||
<canvas id="tournamentChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -532,42 +754,283 @@
|
||||
// Player data from Flask
|
||||
const playerStats = {{ stats|tojson }};
|
||||
|
||||
// Tournament type configuration
|
||||
const tournamentTypes = {
|
||||
'40 Targets': { color: '#dc3545', icon: '💪', class: 'targets-40' },
|
||||
'20 Targets': { color: '#28a745', icon: '⚡', class: 'targets-20' },
|
||||
'4 Targets': { color: '#007bff', icon: '🎯', class: 'targets-4' }
|
||||
};
|
||||
|
||||
let currentChart = null;
|
||||
let currentTournamentType = '40 Targets'; // Will be updated based on available data
|
||||
let tournamentsByType = {};
|
||||
let shotAccuracyData = {};
|
||||
|
||||
// Initialize page
|
||||
function initializePage() {
|
||||
createPerformanceChart();
|
||||
groupTournamentsByType();
|
||||
setupEventListeners();
|
||||
loadShotAccuracyData();
|
||||
updateChart();
|
||||
}
|
||||
|
||||
// Create performance trend chart
|
||||
function createPerformanceChart() {
|
||||
const ctx = document.getElementById('performanceChart').getContext('2d');
|
||||
const scores = playerStats.tournament_scores || [];
|
||||
// Load shot accuracy data from archive
|
||||
async function loadShotAccuracyData() {
|
||||
try {
|
||||
const playerId = '{{ player.id }}'; // Get player ID from template
|
||||
const response = await fetch(`/api/archive/player/${playerId}/shot-accuracy`);
|
||||
const result = await response.json();
|
||||
|
||||
if (result.status === 'success') {
|
||||
shotAccuracyData = result.data;
|
||||
console.log('Shot accuracy data loaded:', shotAccuracyData);
|
||||
|
||||
// Auto-select the first tournament type with data
|
||||
const availableTypes = Object.keys(shotAccuracyData);
|
||||
if (availableTypes.length > 0) {
|
||||
// Update current type to first available type
|
||||
const firstAvailableType = availableTypes[0];
|
||||
currentTournamentType = firstAvailableType;
|
||||
|
||||
// Update button states
|
||||
updateActiveButton();
|
||||
}
|
||||
|
||||
updateChart(); // Refresh chart with accuracy data
|
||||
} else {
|
||||
console.log('No shot accuracy data available:', result.message);
|
||||
// Still try to show basic tournament data without shot accuracy
|
||||
updateChart();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading shot accuracy data:', error);
|
||||
console.log('API endpoints may not be set up yet. Showing basic tournament data only.');
|
||||
updateChart();
|
||||
}
|
||||
}
|
||||
|
||||
// Update active button based on current tournament type
|
||||
function updateActiveButton() {
|
||||
const typeButtons = document.querySelectorAll('.type-btn');
|
||||
typeButtons.forEach(btn => {
|
||||
btn.classList.remove('active');
|
||||
if (btn.dataset.type === currentTournamentType) {
|
||||
btn.classList.add('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Setup event listeners
|
||||
function setupEventListeners() {
|
||||
const typeButtons = document.querySelectorAll('.type-btn');
|
||||
typeButtons.forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
// Update active button
|
||||
typeButtons.forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
|
||||
// Update current type and chart
|
||||
currentTournamentType = btn.dataset.type;
|
||||
updateChart();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Group tournaments by type based on tournament_type field or shots fired
|
||||
function groupTournamentsByType() {
|
||||
const tournamentHistory = playerStats.tournament_history || [];
|
||||
tournamentsByType = {};
|
||||
|
||||
tournamentHistory.forEach(tournament => {
|
||||
let type;
|
||||
|
||||
// First try to use the tournament_type field if it exists
|
||||
if (tournament.tournament_type) {
|
||||
const typeField = tournament.tournament_type.toLowerCase();
|
||||
if (typeField.includes('40') || typeField.includes('forty')) {
|
||||
type = '40 Targets';
|
||||
} else if (typeField.includes('20') || typeField.includes('twenty')) {
|
||||
type = '20 Targets';
|
||||
} else if (typeField.includes('4') || typeField.includes('four')) {
|
||||
type = '4 Targets';
|
||||
}
|
||||
}
|
||||
|
||||
// If we couldn't determine from tournament_type, fall back to shots_fired
|
||||
if (!type) {
|
||||
const shots = tournament.shots_fired;
|
||||
if (shots >= 30) {
|
||||
type = '40 Targets';
|
||||
} else if (shots >= 10 && shots <= 29) {
|
||||
type = '20 Targets';
|
||||
} else if (shots <= 9) {
|
||||
type = '4 Targets';
|
||||
}
|
||||
}
|
||||
|
||||
// Skip tournaments that don't match our types
|
||||
if (!type) {
|
||||
console.log('Could not categorize tournament:', tournament);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!tournamentsByType[type]) {
|
||||
tournamentsByType[type] = [];
|
||||
}
|
||||
tournamentsByType[type].push(tournament);
|
||||
});
|
||||
|
||||
// Sort each type by date
|
||||
Object.keys(tournamentsByType).forEach(type => {
|
||||
tournamentsByType[type].sort((a, b) => new Date(a.date) - new Date(b.date));
|
||||
});
|
||||
|
||||
console.log('Tournaments grouped by type:', tournamentsByType);
|
||||
console.log('Available tournament types from database:', Object.keys(tournamentsByType));
|
||||
|
||||
if (scores.length === 0) {
|
||||
ctx.font = '14px Arial';
|
||||
ctx.fillStyle = '#666';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('No tournament data available', ctx.canvas.width / 2, ctx.canvas.height / 2);
|
||||
// If no 40 Targets tournaments, try to default to an available type
|
||||
if (!tournamentsByType['40 Targets'] && Object.keys(tournamentsByType).length > 0) {
|
||||
const availableTypes = Object.keys(tournamentsByType);
|
||||
currentTournamentType = availableTypes[0];
|
||||
console.log(`No 40 Targets tournaments found. Defaulting to: ${currentTournamentType}`);
|
||||
updateActiveButton();
|
||||
}
|
||||
}
|
||||
|
||||
// Update chart and statistics for current tournament type
|
||||
function updateChart() {
|
||||
const tournaments = tournamentsByType[currentTournamentType] || [];
|
||||
updateChartInfo(tournaments);
|
||||
createChart(tournaments);
|
||||
}
|
||||
|
||||
// Update chart information and statistics
|
||||
function updateChartInfo(tournaments) {
|
||||
const gameCount = tournaments.length;
|
||||
const scores = tournaments.map(t => t.score);
|
||||
const avgScore = gameCount > 0 ? Math.round(scores.reduce((a, b) => a + b, 0) / gameCount) : 0;
|
||||
const bestScore = gameCount > 0 ? Math.max(...scores) : 0;
|
||||
|
||||
// Update basic stats
|
||||
document.getElementById('gameCount').textContent = gameCount;
|
||||
document.getElementById('avgScore').textContent = avgScore;
|
||||
document.getElementById('bestScore').textContent = bestScore;
|
||||
|
||||
// Update shot accuracy stats (if available in tournament data)
|
||||
updateAccuracyStats(tournaments);
|
||||
}
|
||||
|
||||
// Update shot accuracy statistics
|
||||
function updateAccuracyStats(tournaments) {
|
||||
let tens = 0, nines = 0, eights = 0, sevens = 0, sixes = 0;
|
||||
let fives = 0, fours = 0, threes = 0, twos = 0, ones = 0, zeros = 0;
|
||||
let hasData = false;
|
||||
|
||||
tournaments.forEach(tournament => {
|
||||
// Check if we have shot breakdown data for this tournament
|
||||
if (tournament.shot_breakdown) {
|
||||
hasData = true;
|
||||
const breakdown = tournament.shot_breakdown;
|
||||
tens += breakdown.tens || 0;
|
||||
nines += breakdown.nines || 0;
|
||||
eights += breakdown.eights || 0;
|
||||
sevens += breakdown.sevens || 0;
|
||||
sixes += breakdown.sixes || 0;
|
||||
fives += breakdown.fives || 0;
|
||||
fours += breakdown.fours || 0;
|
||||
threes += breakdown.threes || 0;
|
||||
twos += breakdown.twos || 0;
|
||||
ones += breakdown.ones || 0;
|
||||
zeros += breakdown.zeros || 0;
|
||||
}
|
||||
});
|
||||
|
||||
// Also check if we have aggregated shot accuracy data for this tournament type
|
||||
if (shotAccuracyData && shotAccuracyData[currentTournamentType]) {
|
||||
hasData = true;
|
||||
const typeData = shotAccuracyData[currentTournamentType];
|
||||
tens = typeData.tens || 0;
|
||||
nines = typeData.nines || 0;
|
||||
eights = typeData.eights || 0;
|
||||
sevens = typeData.sevens || 0;
|
||||
sixes = typeData.sixes || 0;
|
||||
fives = typeData.fives || 0;
|
||||
fours = typeData.fours || 0;
|
||||
threes = typeData.threes || 0;
|
||||
twos = typeData.twos || 0;
|
||||
ones = typeData.ones || 0;
|
||||
zeros = typeData.zeros || 0;
|
||||
}
|
||||
|
||||
const accuracyStatsDiv = document.getElementById('accuracyStats');
|
||||
|
||||
if (!hasData) {
|
||||
// Hide the accuracy stats section if no data is available
|
||||
accuracyStatsDiv.style.display = 'none';
|
||||
console.log(`No shot accuracy data available for ${currentTournamentType}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Show the section if data is available
|
||||
accuracyStatsDiv.style.display = 'flex';
|
||||
|
||||
document.getElementById('tens').textContent = tens;
|
||||
document.getElementById('nines').textContent = nines;
|
||||
document.getElementById('eights').textContent = eights;
|
||||
document.getElementById('sevens').textContent = sevens;
|
||||
document.getElementById('sixes').textContent = sixes;
|
||||
document.getElementById('fives').textContent = fives;
|
||||
document.getElementById('fours').textContent = fours;
|
||||
document.getElementById('threes').textContent = threes;
|
||||
document.getElementById('twos').textContent = twos;
|
||||
document.getElementById('ones').textContent = ones;
|
||||
document.getElementById('zeros').textContent = zeros;
|
||||
|
||||
console.log(`Shot accuracy for ${currentTournamentType}:`, {
|
||||
tens, nines, eights, sevens, sixes, fives, fours, threes, twos, ones, zeros
|
||||
});
|
||||
}
|
||||
|
||||
// Create chart for current tournament type
|
||||
function createChart(tournaments) {
|
||||
const canvas = document.getElementById('tournamentChart');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
new Chart(ctx, {
|
||||
// Destroy existing chart if it exists
|
||||
if (currentChart) {
|
||||
currentChart.destroy();
|
||||
}
|
||||
|
||||
if (tournaments.length === 0) {
|
||||
// Show no data message
|
||||
ctx.font = '16px Arial';
|
||||
ctx.fillStyle = '#666';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(`No ${currentTournamentType} tournaments found`, canvas.width / 2, canvas.height / 2);
|
||||
return;
|
||||
}
|
||||
|
||||
const typeConfig = tournamentTypes[currentTournamentType];
|
||||
const scores = tournaments.map(t => t.score);
|
||||
|
||||
currentChart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: scores.map((_, i) => `T${i + 1}`),
|
||||
labels: scores.map((_, i) => `${i + 1}`),
|
||||
datasets: [{
|
||||
label: 'Tournament Score',
|
||||
label: `${currentTournamentType} Score`,
|
||||
data: scores,
|
||||
borderColor: '#28a745',
|
||||
backgroundColor: 'rgba(40, 167, 69, 0.15)',
|
||||
borderColor: typeConfig.color,
|
||||
backgroundColor: typeConfig.color + '20',
|
||||
borderWidth: 3,
|
||||
fill: true,
|
||||
tension: 0.4,
|
||||
pointRadius: 5,
|
||||
pointHoverRadius: 8,
|
||||
pointBackgroundColor: '#28a745',
|
||||
pointBackgroundColor: typeConfig.color,
|
||||
pointBorderColor: '#fff',
|
||||
pointBorderWidth: 2,
|
||||
pointHoverBackgroundColor: '#1e7e34',
|
||||
pointHoverBackgroundColor: typeConfig.color,
|
||||
pointHoverBorderColor: '#fff'
|
||||
}]
|
||||
},
|
||||
@@ -577,17 +1040,52 @@
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(40, 167, 69, 0.9)',
|
||||
backgroundColor: typeConfig.color + 'E6',
|
||||
titleColor: '#fff',
|
||||
bodyColor: '#fff',
|
||||
borderColor: '#28a745',
|
||||
borderColor: typeConfig.color,
|
||||
borderWidth: 2,
|
||||
callbacks: {
|
||||
title: function(context) {
|
||||
return `Tournament ${context[0].dataIndex + 1}`;
|
||||
const tournament = tournaments[context[0].dataIndex];
|
||||
return `Game ${context[0].dataIndex + 1} - ${tournament.date.split(' ')[0]}`;
|
||||
},
|
||||
label: function(context) {
|
||||
return `Score: ${context.parsed.y}`;
|
||||
const tournament = tournaments[context.dataIndex];
|
||||
const labels = [`Score: ${context.parsed.y}`, `Shots: ${tournament.shots_fired}`];
|
||||
|
||||
// Add tournament type if available
|
||||
if (tournament.tournament_type) {
|
||||
labels.push(`Type: ${tournament.tournament_type.replace('_', ' ')}`);
|
||||
}
|
||||
|
||||
// Add shot breakdown if available
|
||||
if (tournament.shot_breakdown) {
|
||||
const breakdown = tournament.shot_breakdown;
|
||||
const shots = [];
|
||||
if (breakdown.tens) shots.push(`10s: ${breakdown.tens}`);
|
||||
if (breakdown.nines) shots.push(`9s: ${breakdown.nines}`);
|
||||
if (breakdown.eights) shots.push(`8s: ${breakdown.eights}`);
|
||||
if (breakdown.sevens) shots.push(`7s: ${breakdown.sevens}`);
|
||||
if (breakdown.sixes) shots.push(`6s: ${breakdown.sixes}`);
|
||||
if (breakdown.fives) shots.push(`5s: ${breakdown.fives}`);
|
||||
if (breakdown.fours) shots.push(`4s: ${breakdown.fours}`);
|
||||
if (breakdown.threes) shots.push(`3s: ${breakdown.threes}`);
|
||||
if (breakdown.twos) shots.push(`2s: ${breakdown.twos}`);
|
||||
if (breakdown.ones) shots.push(`1s: ${breakdown.ones}`);
|
||||
if (breakdown.zeros) shots.push(`0s: ${breakdown.zeros}`);
|
||||
|
||||
if (shots.length > 0) {
|
||||
// Show only top scoring shots to keep tooltip readable
|
||||
const topShots = shots.slice(0, 5);
|
||||
labels.push(topShots.join(', '));
|
||||
if (shots.length > 5) {
|
||||
labels.push('+ more...');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return labels;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -595,17 +1093,21 @@
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
grid: { color: 'rgba(40, 167, 69, 0.1)' },
|
||||
grid: {
|
||||
color: typeConfig.color + '20'
|
||||
},
|
||||
ticks: {
|
||||
color: '#28a745',
|
||||
font: { size: 11, weight: 'bold' }
|
||||
color: typeConfig.color,
|
||||
font: { size: 12, weight: 'bold' }
|
||||
}
|
||||
},
|
||||
x: {
|
||||
grid: { color: 'rgba(40, 167, 69, 0.1)' },
|
||||
grid: {
|
||||
color: typeConfig.color + '20'
|
||||
},
|
||||
ticks: {
|
||||
color: '#28a745',
|
||||
font: { size: 11, weight: 'bold' }
|
||||
color: typeConfig.color,
|
||||
font: { size: 12, weight: 'bold' }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
background: #f5f5f5;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
height: 100vh;
|
||||
overflow: hidden; /* Prevent scrolling for TV display */
|
||||
@@ -652,7 +652,88 @@
|
||||
.tv-refresh-indicator.show {
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
/* PRINT STYLES - Clean professional printout */
|
||||
@media print {
|
||||
/* Hide everything except the rankings table */
|
||||
.navbar,
|
||||
.left-column,
|
||||
.tv-refresh-indicator {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Reset body and html for print */
|
||||
html, body {
|
||||
height: auto !important;
|
||||
overflow: visible !important;
|
||||
background: white !important;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
}
|
||||
|
||||
/* Make the container take full width and remove grid */
|
||||
.tv-container {
|
||||
display: block !important;
|
||||
height: auto !important;
|
||||
padding: 0 !important;
|
||||
gap: 0 !important;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* Style the right column for print */
|
||||
.right-column {
|
||||
background: white !important;
|
||||
border-radius: 0 !important;
|
||||
box-shadow: none !important;
|
||||
overflow: visible !important;
|
||||
display: block !important;
|
||||
height: auto !important;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Create a beautiful print header */
|
||||
.table-header {
|
||||
background: white !important;
|
||||
padding: 30px 0 30px 0 !important;
|
||||
border-bottom: 3px solid #007bff !important;
|
||||
text-align: center;
|
||||
margin-bottom: 30px !important;
|
||||
page-break-inside: avoid;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Add logo using CSS */
|
||||
.table-header::before {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 200px;
|
||||
height: 80px;
|
||||
margin: 0 auto 20px auto;
|
||||
background: url('/static/logo.png') no-repeat center center;
|
||||
background-size: contain;
|
||||
/* Fallback for when logo doesn't load */
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/* Add title using CSS */
|
||||
.table-header::after {
|
||||
content: "🏆 TOURNAMENT RESULTS";
|
||||
display: block;
|
||||
font-size: 28pt;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
letter-spacing: 2px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
}
|
||||
|
||||
/* Hide the original table title */
|
||||
.table-title {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="navbar">
|
||||
@@ -661,9 +742,7 @@
|
||||
</div>
|
||||
<div class="navbar-controls">
|
||||
<a href="/" class="nav-btn">← Dashboard</a>
|
||||
<a href="/tournament/draft" class="nav-btn">📋 Draft</a>
|
||||
<a href="/results/calculator" class="nav-btn primary">🎯 Calculator</a>
|
||||
<button class="nav-btn" onclick="window.print()">🖨️ Print</button>
|
||||
<button class="nav-btn" onclick="printResults()">🖨️ Print</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -867,6 +946,11 @@
|
||||
document.body.offsetHeight; // Trigger reflow
|
||||
document.body.style.display = '';
|
||||
});
|
||||
|
||||
function printResults() {
|
||||
// Trigger print dialog
|
||||
window.print();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1082,21 +1082,21 @@
|
||||
|
||||
<!-- Tournament Type Selection -->
|
||||
<div class="tournament-type-selection">
|
||||
<div class="type-title">🎯 Select Tournament Type</div>
|
||||
<div class="type-title">Select Tournament Type</div>
|
||||
<div class="type-options">
|
||||
<div class="type-option" onclick="selectTournamentType('4_targets')">
|
||||
<input type="radio" name="tournament_type" value="4_targets">
|
||||
<div class="type-name">4 Targets</div>
|
||||
<div class="type-description">Quick format with 4 targets, 5 shots each (20 shots total)</div>
|
||||
<div class="type-name">🎯 4 Targets</div>
|
||||
<div class="type-description">Quick format with 4 targets, 5 shots each <br> (20 shots total)</div>
|
||||
</div>
|
||||
<div class="type-option selected" onclick="selectTournamentType('20_targets')">
|
||||
<input type="radio" name="tournament_type" value="20_targets" checked>
|
||||
<div class="type-name">20 Targets</div>
|
||||
<div class="type-name">⚡ 20 Targets</div>
|
||||
<div class="type-description">Standard format with 20 targets, 2 shots each (40 shots total)</div>
|
||||
</div>
|
||||
<div class="type-option" onclick="selectTournamentType('40_targets')">
|
||||
<input type="radio" name="tournament_type" value="40_targets">
|
||||
<div class="type-name">40 Targets</div>
|
||||
<div class="type-name">💪 40 Targets</div>
|
||||
<div class="type-description">Extended format with 40 targets, 2 shots each (80 shots total)</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1108,7 +1108,7 @@
|
||||
|
||||
<div class="action-buttons">
|
||||
<button class="action-btn success" id="startLeagueBtn" onclick="startLeague()">🏆 Start League (6 Tournaments)</button>
|
||||
<button class="action-btn" id="startSingleBtn" onclick="startSingleTournament()">🎯 Start Single Tournament</button>
|
||||
<button class="action-btn" id="startSingleBtn" onclick="startSingleTournament()">🏅 Start Single Tournament</button>
|
||||
</div>
|
||||
|
||||
<div class="warning" id="warningMessage" style="display: none;">
|
||||
|
||||
Reference in New Issue
Block a user