Files
Sdk_TV_app/templates/league_combine.html
T
2025-11-14 17:03:30 +01:00

772 lines
24 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="sl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title data-i18n="league.combine_leagues">Combine Leagues</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/base.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/navbar.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/buttons.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/components.css') }}">
<style>
* {
box-sizing: border-box;
}
html, body {
margin: 0;
padding: 0;
background: #f5f5f5;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
min-height: 100vh;
color: #333;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.navbar {
background: white;
border-bottom: 1px solid #e1e5e9;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
padding: 15px 25px;
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0;
}
.navbar-title {
font-size: 1.8rem;
font-weight: bold;
color: #333;
margin: 0;
display: flex;
align-items: center;
gap: 10px;
}
.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: #28a745;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
transform: translateY(-1px);
color: #28a745;
}
.upload-section {
background: white;
padding: 30px;
border-radius: 10px;
margin-bottom: 20px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.upload-area {
border: 3px dashed #28a745;
border-radius: 10px;
padding: 40px;
text-align: center;
background: #f8f9fa;
cursor: pointer;
transition: all 0.3s;
}
.upload-area:hover {
background: #e9f7ed;
border-color: #1e7e34;
}
.upload-area.drag-over {
background: #d4edda;
border-color: #28a745;
box-shadow: 0 4px 12px rgba(40, 167, 69, 0.2);
}
.file-input {
display: none;
}
.upload-icon {
font-size: 48px;
margin-bottom: 10px;
}
.uploaded-files {
margin-top: 20px;
}
.file-item {
background: #f8f9fa;
padding: 15px;
border-radius: 5px;
margin-bottom: 10px;
display: flex;
justify-content: space-between;
align-items: center;
border-left: 4px solid #28a745;
}
.file-item.error {
border-left-color: #dc3545;
background: #f8d7da;
}
.file-info {
display: flex;
flex-direction: column;
gap: 5px;
}
.file-name {
font-weight: bold;
color: #2c3e50;
}
.file-details {
font-size: 0.9em;
color: #6c757d;
}
.remove-btn {
background: #dc3545;
color: white;
border: none;
padding: 8px 15px;
border-radius: 5px;
cursor: pointer;
transition: background 0.3s;
}
.remove-btn:hover {
background: #c82333;
}
.action-buttons {
display: flex;
gap: 10px;
margin-top: 20px;
}
.btn {
padding: 12px 24px;
border: 2px solid;
border-radius: 8px;
cursor: pointer;
font-size: 16px;
font-weight: bold;
transition: all 0.2s ease;
flex: 1;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.btn-primary {
background: #28a745;
color: white;
border-color: #1e7e34;
}
.btn-primary:hover {
background: #1e7e34;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
transform: translateY(-1px);
}
.btn-primary:disabled {
background: #6c757d;
border-color: #5a6268;
cursor: not-allowed;
opacity: 0.6;
transform: none;
}
.btn-secondary {
background: #f8f9fa;
color: #333;
border-color: #e9ecef;
}
.btn-secondary:hover {
background: #e9ecef;
border-color: #dc3545;
color: #dc3545;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
transform: translateY(-1px);
}
.preview-section {
background: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
display: none;
}
.preview-section.active {
display: block;
}
.info-box {
background: #d4edda;
border-left: 4px solid #28a745;
padding: 15px;
margin-bottom: 20px;
border-radius: 8px;
}
.info-box h3 {
margin: 0 0 10px 0;
color: #155724;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-bottom: 20px;
}
.stat-card {
background: #f8f9fa;
padding: 15px;
border-radius: 8px;
text-align: center;
border-left: 4px solid #28a745;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.stat-value {
font-size: 32px;
font-weight: bold;
color: #28a745;
}
.stat-label {
font-size: 14px;
color: #6c757d;
margin-top: 5px;
}
.mode-btn {
flex: 1;
padding: 12px 20px;
border: 2px solid #28a745;
background: white;
color: #28a745;
border-radius: 8px;
cursor: pointer;
font-weight: bold;
transition: all 0.2s ease;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.mode-btn:hover {
background: #e9f7ed;
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
.mode-btn.active {
background: #28a745;
color: white;
border-color: #1e7e34;
}
</style>
</head>
<body>
<div class="navbar">
<h1 class="navbar-title">🔗 <span data-i18n="league.combine_leagues">Combine Leagues</span></h1>
<a href="/" class="nav-btn"><span data-i18n="navigation.dashboard">Dashboard</span></a>
</div>
<div class="container">
<div class="upload-section">
<div style="margin-bottom: 20px;">
<label style="display: block; margin-bottom: 10px; font-weight: bold; color: #2c3e50;">
<span data-i18n="league.select_mode">Select Mode</span>:
</label>
<div style="display: flex; gap: 10px;">
<button class="mode-btn active" id="combineMode" onclick="switchMode('combine')">
🔗 <span data-i18n="league.combine_mode">Combine Leagues</span>
</button>
<button class="mode-btn" id="convertMode" onclick="switchMode('convert')">
🔄 <span data-i18n="league.convert_mode">Convert Tournaments to League</span>
</button>
</div>
</div>
<div id="combineSection">
<h2>📤 <span data-i18n="league.upload_league_files">Upload League JSON Files</span></h2>
<p style="color: #6c757d; margin-bottom: 20px;">
<span data-i18n="league.combine_leagues_desc">Upload multiple league JSON files to combine them into a single results table.</span>
</p>
</div>
<div id="convertSection" style="display: none;">
<h2>📤 <span data-i18n="league.upload_tournament_files">Upload Tournament JSON Files</span></h2>
<p style="color: #6c757d; margin-bottom: 20px;">
<span data-i18n="league.convert_tournaments_desc">Upload 1-5 tournament JSON files to create a league. You can upload partial leagues as tournaments complete. Final scoring uses best 4 results when all 5 are uploaded.</span>
</p>
</div>
<div class="upload-area" id="uploadArea">
<div class="upload-icon">📁</div>
<p><strong data-i18n="league.click_browse">Click to browse</strong> <span data-i18n="league.drag_drop_files">or drag and drop league JSON files here</span></p>
<p style="font-size: 0.9em; color: #6c757d;"><span data-i18n="league.supports_multiple">Supports multiple file selection</span></p>
<input type="file" id="fileInput" class="file-input" accept=".json" multiple>
</div>
<div class="uploaded-files" id="uploadedFiles"></div>
<div class="action-buttons">
<button class="btn btn-secondary" id="clearBtn" disabled>
🗑️ <span data-i18n="scoring.clear_all">Clear All</span>
</button>
<button class="btn btn-primary" id="processBtn" disabled>
<span id="processBtnText">🔗 <span data-i18n="league.combine_preview">Combine & Preview</span></span>
</button>
</div>
</div>
<div class="preview-section" id="previewSection">
<div class="info-box">
<h3><span data-i18n="league.combined_league_info">Combined League Information</span></h3>
<div id="combinedInfo"></div>
</div>
<div class="stats-grid" id="statsGrid"></div>
<div id="resultsDisplay"></div>
<div class="action-buttons" style="margin-top: 20px;">
<button class="btn btn-secondary" onclick="window.location.reload()">
🔄 <span data-i18n="league.start_over">Start Over</span>
</button>
<button class="btn btn-primary" id="exportBtn">
💾 <span data-i18n="league.export_combined_results">Export Combined Results</span>
</button>
</div>
</div>
</div>
<script src="/static/js/i18n.js"></script>
<script>
let uploadedLeagues = [];
let currentMode = 'combine'; // 'combine' or 'convert'
const uploadArea = document.getElementById('uploadArea');
const fileInput = document.getElementById('fileInput');
const uploadedFilesDiv = document.getElementById('uploadedFiles');
const clearBtn = document.getElementById('clearBtn');
const processBtn = document.getElementById('processBtn');
const processBtnText = document.getElementById('processBtnText');
const previewSection = document.getElementById('previewSection');
// Helper function to get translated text
function t(key) {
if (window.i18n && window.i18n.t) {
return window.i18n.t(key);
}
// Fallback if i18n not loaded yet
return key.split('.').pop();
}
function switchMode(mode) {
currentMode = mode;
// Update button states
document.getElementById('combineMode').classList.toggle('active', mode === 'combine');
document.getElementById('convertMode').classList.toggle('active', mode === 'convert');
// Update sections
document.getElementById('combineSection').style.display = mode === 'combine' ? 'block' : 'none';
document.getElementById('convertSection').style.display = mode === 'convert' ? 'block' : 'none';
// Update button text with translations
processBtnText.innerHTML = mode === 'combine'
? '🔗 <span data-i18n="league.combine_preview">Combine & Preview</span>'
: '🔄 <span data-i18n="league.convert_to_league">Convert to League</span>';
// Re-apply translations after DOM change
if (window.i18n) window.i18n.updatePageTranslations();
// Clear uploaded files
uploadedLeagues = [];
updateFileList();
previewSection.classList.remove('active');
}
// Click to upload
uploadArea.addEventListener('click', () => fileInput.click());
// Drag and drop
uploadArea.addEventListener('dragover', (e) => {
e.preventDefault();
uploadArea.classList.add('drag-over');
});
uploadArea.addEventListener('dragleave', () => {
uploadArea.classList.remove('drag-over');
});
uploadArea.addEventListener('drop', (e) => {
e.preventDefault();
uploadArea.classList.remove('drag-over');
handleFiles(e.dataTransfer.files);
});
// File input change
fileInput.addEventListener('change', (e) => {
handleFiles(e.target.files);
});
function handleFiles(files) {
Array.from(files).forEach(file => {
if (file.type === 'application/json' || file.name.endsWith('.json')) {
const reader = new FileReader();
reader.onload = (e) => {
try {
const data = JSON.parse(e.target.result);
if (currentMode === 'combine') {
// Validate it's a league file (handles both archived and raw formats)
let leagueData = null;
if (data.league && data.league.participants) {
// Archived format (wrapped)
leagueData = data.league;
} else if (data.participants) {
// Raw league format (unwrapped)
leagueData = data;
}
if (leagueData) {
uploadedLeagues.push({
filename: file.name,
data: data,
leagueData: leagueData, // Store the actual league data
valid: true,
type: 'league'
});
} else {
uploadedLeagues.push({
filename: file.name,
error: 'Invalid league file format',
valid: false,
type: 'league'
});
}
} else {
// Convert mode - validate it's a tournament file
if (data.tournament && data.results && data.results.participants) {
uploadedLeagues.push({
filename: file.name,
data: data,
valid: true,
type: 'tournament'
});
} else {
uploadedLeagues.push({
filename: file.name,
error: t('league.invalid_tournament_format'),
valid: false,
type: 'tournament'
});
}
}
updateFileList();
} catch (error) {
uploadedLeagues.push({
filename: file.name,
error: t('league.failed_parse_json'),
valid: false
});
updateFileList();
}
};
reader.readAsText(file);
}
});
}
function updateFileList() {
uploadedFilesDiv.innerHTML = '';
if (uploadedLeagues.length > 0) {
const heading = document.createElement('h3');
heading.textContent = `📋 ${t('league.uploaded_files')} (${uploadedLeagues.length})`;
uploadedFilesDiv.appendChild(heading);
uploadedLeagues.forEach((league, index) => {
const fileItem = document.createElement('div');
fileItem.className = 'file-item' + (league.valid ? '' : ' error');
const fileInfo = document.createElement('div');
fileInfo.className = 'file-info';
const fileName = document.createElement('div');
fileName.className = 'file-name';
fileName.textContent = league.filename;
const fileDetails = document.createElement('div');
fileDetails.className = 'file-details';
if (league.valid) {
if (league.type === 'league') {
const participantCount = Object.keys(league.leagueData.participants).length;
const tournamentCount = league.leagueData.total_tournaments || 0;
fileDetails.textContent = `${t('league.valid_league')} | ${participantCount} ${t('league.participants')} | ${tournamentCount} ${t('league.tournaments')}`;
} else {
const participantCount = Object.keys(league.data.results.participants).length;
const tournamentType = league.data.results.tournament_type || '20_targets';
fileDetails.textContent = `${t('league.valid_tournament')} | ${participantCount} ${t('league.participants')} | ${t('league.type')}: ${tournamentType}`;
}
} else {
fileDetails.textContent = `${league.error}`;
}
fileInfo.appendChild(fileName);
fileInfo.appendChild(fileDetails);
const removeBtn = document.createElement('button');
removeBtn.className = 'remove-btn';
removeBtn.textContent = `${t('league.remove')}`;
removeBtn.onclick = () => removeFile(index);
fileItem.appendChild(fileInfo);
fileItem.appendChild(removeBtn);
uploadedFilesDiv.appendChild(fileItem);
});
clearBtn.disabled = false;
const validCount = uploadedLeagues.filter(l => l.valid).length;
if (currentMode === 'combine') {
processBtn.disabled = validCount < 1; // Allow preview with 1+ files
} else {
// Convert mode requires 1-5 tournament files
processBtn.disabled = validCount < 1 || validCount > 5;
}
} else {
clearBtn.disabled = true;
processBtn.disabled = true;
}
}
function removeFile(index) {
uploadedLeagues.splice(index, 1);
updateFileList();
}
clearBtn.addEventListener('click', () => {
uploadedLeagues = [];
updateFileList();
previewSection.classList.remove('active');
});
processBtn.addEventListener('click', async () => {
const validFiles = uploadedLeagues.filter(l => l.valid);
if (currentMode === 'combine') {
if (validFiles.length < 1) {
alert('Please upload at least 1 valid league file');
return;
}
// Send to backend for combination
const response = await fetch('/api/league/combine', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
leagues: validFiles.map(l => l.leagueData)
})
});
if (response.ok) {
const result = await response.json();
// Store in session and redirect to preview
const previewResponse = await fetch('/league/set-preview', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
league: result.combined_league
})
});
if (previewResponse.ok) {
const previewResult = await previewResponse.json();
window.location.href = previewResult.redirect_url;
} else {
alert('Failed to preview league');
}
} else {
alert('Failed to combine leagues');
}
} else {
// Convert mode
if (validFiles.length < 1 || validFiles.length > 5) {
alert('Please upload 1-5 valid tournament files');
return;
}
// Send to backend for conversion
const response = await fetch('/api/league/convert', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
tournaments: validFiles.map(l => l.data)
})
});
if (response.ok) {
const result = await response.json();
// Store in session and redirect to preview
const previewResponse = await fetch('/league/set-preview', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
league: result.combined_league
})
});
if (previewResponse.ok) {
const previewResult = await previewResponse.json();
window.location.href = previewResult.redirect_url;
} else {
alert('Failed to preview league');
}
} else {
alert('Failed to convert tournaments to league');
}
}
});
function displayCombinedResults(result) {
// Display combined info
const sourceType = result.is_partial !== undefined ?
(result.is_partial ? 'tournament files (Partial League)' : 'tournament files (Complete League)') :
'league files';
document.getElementById('combinedInfo').innerHTML = `
<p><strong>Combined from:</strong> ${result.source_count} ${sourceType}</p>
<p><strong>Tournament Type:</strong> ${result.tournament_type}</p>
<p><strong>Total Participants:</strong> ${result.total_participants}</p>
${result.is_partial ? '<p style="color: #ff9800;"><strong>⚠️ Partial League:</strong> Upload more tournaments to complete the league (5 total needed)</p>' : ''}
`;
// Display stats
const statsGrid = document.getElementById('statsGrid');
statsGrid.innerHTML = `
<div class="stat-card">
<div class="stat-value">${result.total_participants}</div>
<div class="stat-label">Total Players</div>
</div>
<div class="stat-card">
<div class="stat-value">${result.total_tournaments}</div>
<div class="stat-label">Total Tournaments</div>
</div>
<div class="stat-card">
<div class="stat-value">${result.highest_score}</div>
<div class="stat-label">Highest Score</div>
</div>
<div class="stat-card">
<div class="stat-value">${result.avg_score}</div>
<div class="stat-label">Average Score</div>
</div>
`;
// Store combined data for export
window.combinedLeagueData = result.combined_league;
// Redirect to results display with combined data
displayResultsTable(result.participants);
}
function displayResultsTable(participants) {
const resultsDisplay = document.getElementById('resultsDisplay');
let html = `
<h2>🏆 Combined Results</h2>
<table style="width: 100%; border-collapse: collapse; margin-top: 20px;">
<thead>
<tr style="background: #667eea; color: white;">
<th style="padding: 12px; text-align: left;">Rank</th>
<th style="padding: 12px; text-align: left;">Player</th>
<th style="padding: 12px; text-align: center;">Final Score</th>
<th style="padding: 12px; text-align: center;">Total Score</th>
<th style="padding: 12px; text-align: center;">Tournaments</th>
<th style="padding: 12px; text-align: center;">10s</th>
</tr>
</thead>
<tbody>
`;
participants.forEach((p, i) => {
html += `
<tr style="border-bottom: 1px solid #dee2e6; ${i % 2 === 0 ? 'background: #f8f9fa;' : ''}">
<td style="padding: 12px; font-weight: bold;">${i + 1}</td>
<td style="padding: 12px;">${p.name}</td>
<td style="padding: 12px; text-align: center; font-weight: bold; color: #667eea;">${p.final_score}</td>
<td style="padding: 12px; text-align: center;">${p.total_score}</td>
<td style="padding: 12px; text-align: center;">${p.tournaments_participated}</td>
<td style="padding: 12px; text-align: center;">${p.total_tens}</td>
</tr>
`;
});
html += `
</tbody>
</table>
`;
resultsDisplay.innerHTML = html;
}
document.getElementById('exportBtn').addEventListener('click', () => {
if (window.combinedLeagueData) {
// Wrap in archive format to be compatible with results display
const archiveData = {
league: window.combinedLeagueData,
archived_at: new Date().toISOString()
};
const dataStr = JSON.stringify(archiveData, null, 2);
const dataBlob = new Blob([dataStr], { type: 'application/json' });
const url = URL.createObjectURL(dataBlob);
const link = document.createElement('a');
link.href = url;
link.download = `combined_league_${new Date().toISOString().slice(0, 10)}.json`;
link.click();
URL.revokeObjectURL(url);
}
});
</script>
</body>
</html>