545 lines
24 KiB
HTML
545 lines
24 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Club Management - HKFC Men's C Team</title>
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
</head>
|
|
<body>
|
|
<div class="container mt-4">
|
|
<div class="row">
|
|
<div class="col-md-12">
|
|
<h1>Club Management</h1>
|
|
<p class="lead">Manage hockey clubs in the database</p>
|
|
|
|
<div class="mb-3">
|
|
<a href="/admin/clubs/add" class="btn btn-primary">Add New Club</a>
|
|
<button type="button" class="btn btn-info" id="importClubsBtn" onclick="importClubs()">
|
|
<span class="spinner-border spinner-border-sm d-none" id="importSpinner"></span>
|
|
Import from Hockey HK
|
|
</button>
|
|
<button type="button" class="btn btn-outline-info" id="previewClubsBtn" onclick="previewClubs()">
|
|
Preview Clubs
|
|
</button>
|
|
<a href="/admin" class="btn btn-secondary">Back to Admin</a>
|
|
</div>
|
|
|
|
<!-- Import Status -->
|
|
<div id="importStatus" class="alert d-none" role="alert"></div>
|
|
|
|
<!-- Preview Modal -->
|
|
<div class="modal fade" id="previewModal" tabindex="-1" aria-labelledby="previewModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="previewModalLabel">Clubs from Hockey Hong Kong</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div id="previewContent">
|
|
<div class="text-center">
|
|
<div class="spinner-border" role="status">
|
|
<span class="visually-hidden">Loading...</span>
|
|
</div>
|
|
<p>Loading clubs...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
|
<button type="button" class="btn btn-primary" id="confirmImportBtn" onclick="confirmImport()">Import All</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- S3 Browser Modal -->
|
|
<div class="modal fade" id="s3BrowserModal" tabindex="-1" aria-labelledby="s3BrowserModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog modal-xl">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="s3BrowserModalLabel">Browse S3 Storage</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div id="s3BrowserContent">
|
|
<div class="text-center">
|
|
<div class="spinner-border" role="status">
|
|
<span class="visually-hidden">Loading...</span>
|
|
</div>
|
|
<p>Loading S3 contents...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="button" class="btn btn-primary" id="selectS3FileBtn" onclick="selectS3File()" disabled>Select File</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
|
{% if messages %}
|
|
{% for category, message in messages %}
|
|
<div class="alert alert-{{ 'danger' if category == 'error' else 'success' }} alert-dismissible fade show" role="alert">
|
|
{{ message }}
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
</div>
|
|
{% endfor %}
|
|
{% endif %}
|
|
{% endwith %}
|
|
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5>All Clubs</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
{% if clubs %}
|
|
<div class="table-responsive">
|
|
<table class="table table-striped table-hover">
|
|
<thead class="table-dark">
|
|
<tr>
|
|
<th>ID</th>
|
|
<th>Club Name</th>
|
|
<th>Logo</th>
|
|
<th>Logo URL</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for club in clubs %}
|
|
<tr>
|
|
<td>{{ club.id }}</td>
|
|
<td>{{ club.hockey_club }}</td>
|
|
<td>
|
|
{% if club.logo_url %}
|
|
<img src="{{ s3_asset_service.get_logo_url(club.logo_url, club.hockey_club) }}" alt="{{ club.hockey_club }} logo" style="max-height: 40px; max-width: 60px;" onerror="this.style.display='none'">
|
|
{% else %}
|
|
<span class="text-muted">No logo</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
{% if club.logo_url %}
|
|
<a href="{{ club.logo_url }}" target="_blank" class="text-decoration-none small">
|
|
{{ club.logo_url[:50] }}{% if club.logo_url|length > 50 %}...{% endif %}
|
|
</a>
|
|
{% else %}
|
|
<span class="text-muted">No URL</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
<a href="/admin/clubs/edit/{{ club.id }}" class="btn btn-sm btn-outline-primary">Edit</a>
|
|
<form method="POST" action="/admin/clubs/delete/{{ club.id }}" style="display: inline;" onsubmit="return confirm('Are you sure you want to delete this club?')">
|
|
<button type="submit" class="btn btn-sm btn-outline-danger">Delete</button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% else %}
|
|
<div class="alert alert-info">
|
|
<h5>No clubs found</h5>
|
|
<p>There are no clubs in the database. <a href="/admin/clubs/add">Add the first club</a> to get started.</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
|
|
|
<script>
|
|
let previewClubs = [];
|
|
|
|
function showStatus(message, type = 'info') {
|
|
const statusDiv = document.getElementById('importStatus');
|
|
statusDiv.className = `alert alert-${type}`;
|
|
statusDiv.textContent = message;
|
|
statusDiv.classList.remove('d-none');
|
|
|
|
// Auto-hide after 5 seconds
|
|
setTimeout(() => {
|
|
statusDiv.classList.add('d-none');
|
|
}, 5000);
|
|
}
|
|
|
|
// S3 Browser functionality
|
|
let selectedS3File = null;
|
|
let currentS3Path = '';
|
|
|
|
function browseS3() {
|
|
// Reset state
|
|
selectedS3File = null;
|
|
currentS3Path = '';
|
|
|
|
// Show loading state
|
|
document.getElementById('s3BrowserContent').innerHTML = `
|
|
<div class="text-center">
|
|
<div class="spinner-border" role="status">
|
|
<span class="visually-hidden">Loading...</span>
|
|
</div>
|
|
<p>Loading S3 contents...</p>
|
|
</div>
|
|
`;
|
|
|
|
// Show modal
|
|
const s3BrowserModal = new bootstrap.Modal(document.getElementById('s3BrowserModal'));
|
|
s3BrowserModal.show();
|
|
|
|
// Load S3 contents from root of assets folder
|
|
loadS3Contents('');
|
|
}
|
|
|
|
function loadS3Contents(path) {
|
|
fetch(`/admin/api/s3-browser?path=${encodeURIComponent(path)}`)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
displayS3Contents(data);
|
|
} else {
|
|
document.getElementById('s3BrowserContent').innerHTML = `
|
|
<div class="alert alert-danger" role="alert">
|
|
<strong>Error:</strong> ${data.message}
|
|
</div>
|
|
`;
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error:', error);
|
|
document.getElementById('s3BrowserContent').innerHTML = `
|
|
<div class="alert alert-danger" role="alert">
|
|
<strong>Error:</strong> Failed to load S3 contents. Please try again.
|
|
</div>
|
|
`;
|
|
});
|
|
}
|
|
|
|
function displayS3Contents(data) {
|
|
currentS3Path = data.path;
|
|
|
|
let html = `
|
|
<div class="row mb-3">
|
|
<div class="col-12">
|
|
<nav aria-label="breadcrumb">
|
|
<ol class="breadcrumb">
|
|
<li class="breadcrumb-item">
|
|
<a href="#" onclick="loadS3Contents('')">assets</a>
|
|
</li>
|
|
`;
|
|
|
|
// Build breadcrumb
|
|
if (data.path !== '') {
|
|
const pathParts = data.path.split('/').filter(p => p);
|
|
let currentPath = '';
|
|
|
|
pathParts.forEach((part, index) => {
|
|
currentPath += part + '/';
|
|
const isLast = index === pathParts.length - 1;
|
|
html += `<li class="breadcrumb-item ${isLast ? 'active' : ''}">`;
|
|
|
|
if (!isLast) {
|
|
html += `<a href="#" onclick="loadS3Contents('${currentPath}')">${part}</a>`;
|
|
} else {
|
|
html += part;
|
|
}
|
|
html += '</li>';
|
|
});
|
|
}
|
|
|
|
html += `
|
|
</ol>
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
<div class="row">
|
|
`;
|
|
|
|
// Display folders
|
|
if (data.folders.length > 0) {
|
|
data.folders.forEach(folder => {
|
|
html += `
|
|
<div class="col-md-3 mb-3">
|
|
<div class="card h-100 folder-card" onclick="loadS3Contents('${folder.path}')" style="cursor: pointer;">
|
|
<div class="card-body text-center">
|
|
<i class="fas fa-folder fa-3x text-warning mb-2"></i>
|
|
<h6 class="card-title">${folder.name}</h6>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
});
|
|
}
|
|
|
|
// Display files
|
|
if (data.files.length > 0) {
|
|
data.files.forEach(file => {
|
|
const isImage = /\.(jpg|jpeg|png|gif|svg|webp)$/i.test(file.name);
|
|
const fileSize = formatFileSize(file.size);
|
|
|
|
html += `
|
|
<div class="col-md-3 mb-3">
|
|
<div class="card h-100 file-card" onclick="selectS3FileItem('${file.path}', '${file.url}')" style="cursor: pointer;">
|
|
<div class="card-body text-center">
|
|
`;
|
|
|
|
if (isImage) {
|
|
html += `
|
|
<img src="${file.url}" alt="${file.name}" class="img-fluid mb-2" style="max-height: 100px; max-width: 100%; object-fit: contain;">
|
|
`;
|
|
} else {
|
|
html += `
|
|
<i class="fas fa-file fa-3x text-primary mb-2"></i>
|
|
`;
|
|
}
|
|
|
|
html += `
|
|
<h6 class="card-title">${file.name}</h6>
|
|
<small class="text-muted">${fileSize}</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
});
|
|
}
|
|
|
|
if (data.folders.length === 0 && data.files.length === 0) {
|
|
html += `
|
|
<div class="col-12">
|
|
<div class="alert alert-info text-center">
|
|
<i class="fas fa-info-circle"></i> No files or folders found in this directory.
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
html += `
|
|
</div>
|
|
`;
|
|
|
|
document.getElementById('s3BrowserContent').innerHTML = html;
|
|
}
|
|
|
|
function selectS3FileItem(filePath, fileUrl) {
|
|
// Remove previous selection
|
|
document.querySelectorAll('.file-card').forEach(card => {
|
|
card.classList.remove('border-primary');
|
|
});
|
|
|
|
// Add selection to clicked card
|
|
event.currentTarget.classList.add('border-primary');
|
|
|
|
// Store selected file
|
|
selectedS3File = {
|
|
path: filePath,
|
|
url: fileUrl
|
|
};
|
|
|
|
// Enable select button
|
|
document.getElementById('selectS3FileBtn').disabled = false;
|
|
}
|
|
|
|
function selectS3File() {
|
|
if (selectedS3File) {
|
|
// Update the logo URL field in the add club form
|
|
const logoUrlField = document.querySelector('input[name="logo_url"]');
|
|
if (logoUrlField) {
|
|
logoUrlField.value = selectedS3File.path;
|
|
}
|
|
|
|
// Close modal
|
|
const s3BrowserModal = bootstrap.Modal.getInstance(document.getElementById('s3BrowserModal'));
|
|
s3BrowserModal.hide();
|
|
|
|
// Show success message
|
|
showStatus('Logo selected successfully!', 'success');
|
|
}
|
|
}
|
|
|
|
function formatFileSize(bytes) {
|
|
if (bytes === 0) return '0 Bytes';
|
|
const k = 1024;
|
|
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
}
|
|
|
|
function previewClubs() {
|
|
const modal = new bootstrap.Modal(document.getElementById('previewModal'));
|
|
const content = document.getElementById('previewContent');
|
|
|
|
// Show loading state
|
|
content.innerHTML = `
|
|
<div class="text-center">
|
|
<div class="spinner-border" role="status">
|
|
<span class="visually-hidden">Loading...</span>
|
|
</div>
|
|
<p>Loading clubs from Hockey Hong Kong...</p>
|
|
</div>
|
|
`;
|
|
|
|
modal.show();
|
|
|
|
// Fetch clubs
|
|
fetch('/admin/api/clubs')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
previewClubs = data.clubs;
|
|
displayPreviewClubs(data.clubs);
|
|
} else {
|
|
content.innerHTML = `
|
|
<div class="alert alert-warning">
|
|
<h6>Unable to fetch clubs</h6>
|
|
<p>${data.message}</p>
|
|
<p><small>This might be due to website structure changes or network issues.</small></p>
|
|
</div>
|
|
`;
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error:', error);
|
|
content.innerHTML = `
|
|
<div class="alert alert-danger">
|
|
<h6>Error loading clubs</h6>
|
|
<p>There was an error fetching clubs from the Hockey Hong Kong website.</p>
|
|
</div>
|
|
`;
|
|
});
|
|
}
|
|
|
|
function displayPreviewClubs(clubs) {
|
|
const content = document.getElementById('previewContent');
|
|
|
|
if (clubs.length === 0) {
|
|
content.innerHTML = `
|
|
<div class="alert alert-info">
|
|
<h6>No clubs found</h6>
|
|
<p>The website structure may have changed or no clubs are currently listed.</p>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
let html = `
|
|
<div class="alert alert-info">
|
|
<h6>Found ${clubs.length} clubs</h6>
|
|
<p>These clubs will be imported with their full names and abbreviations.</p>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-sm table-striped">
|
|
<thead>
|
|
<tr>
|
|
<th>Club Name</th>
|
|
<th>Abbreviation</th>
|
|
<th>Teams</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
`;
|
|
|
|
clubs.forEach(club => {
|
|
const teams = club.teams ? club.teams.join(', ') : 'N/A';
|
|
html += `
|
|
<tr>
|
|
<td>${club.name}</td>
|
|
<td><span class="badge bg-secondary">${club.abbreviation || 'N/A'}</span></td>
|
|
<td>${teams}</td>
|
|
</tr>
|
|
`;
|
|
});
|
|
|
|
html += `
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
`;
|
|
|
|
content.innerHTML = html;
|
|
}
|
|
|
|
function confirmImport() {
|
|
const importBtn = document.getElementById('confirmImportBtn');
|
|
const spinner = document.getElementById('importSpinner');
|
|
|
|
// Show loading state
|
|
importBtn.disabled = true;
|
|
importBtn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Importing...';
|
|
|
|
// Import clubs
|
|
fetch('/admin/api/import-clubs', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
}
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
showStatus(data.message, 'success');
|
|
// Close modal
|
|
const modal = bootstrap.Modal.getInstance(document.getElementById('previewModal'));
|
|
modal.hide();
|
|
// Reload page to show updated clubs
|
|
setTimeout(() => {
|
|
window.location.reload();
|
|
}, 1000);
|
|
} else {
|
|
showStatus(data.message, 'danger');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error:', error);
|
|
showStatus('Error importing clubs', 'danger');
|
|
})
|
|
.finally(() => {
|
|
importBtn.disabled = false;
|
|
importBtn.innerHTML = 'Import All';
|
|
});
|
|
}
|
|
|
|
function importClubs() {
|
|
const importBtn = document.getElementById('importClubsBtn');
|
|
const spinner = document.getElementById('importSpinner');
|
|
|
|
// Show loading state
|
|
importBtn.disabled = true;
|
|
spinner.classList.remove('d-none');
|
|
|
|
// Import clubs directly
|
|
fetch('/admin/api/import-clubs', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
}
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
showStatus(data.message, 'success');
|
|
// Reload page to show updated clubs
|
|
setTimeout(() => {
|
|
window.location.reload();
|
|
}, 1000);
|
|
} else {
|
|
showStatus(data.message, 'danger');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error:', error);
|
|
showStatus('Error importing clubs', 'danger');
|
|
})
|
|
.finally(() => {
|
|
importBtn.disabled = false;
|
|
spinner.classList.add('d-none');
|
|
});
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|