Add Impressum and QR code management templates

- Created a new Impressum page with legal information and contact details.
- Developed admin link page for linking QR codes to events with form functionality.
- Implemented QR code dashboard for managing QR code sets, including creation and quick actions.
- Added detailed view for QR code sets, allowing addition of QR codes and management actions.
- Introduced static file serving for favicon and related images.
- Established views for static content pages (about, contact, privacy, terms).
- Implemented translation management scripts for compiling and updating translations.
This commit is contained in:
Christian Krakau-Louis
2025-04-14 03:19:41 +02:00
parent 9eef726d9b
commit d595b468e9
43 changed files with 2895 additions and 391 deletions
+158
View File
@@ -0,0 +1,158 @@
/*
* LeagueLedger Main Stylesheet
* Version: 1.0.0
* Date: April 13, 2025
*/
:root {
/* LeagueLedger brand color palette */
--irish-green: #006837;
--golden-ale: #FFB400;
--cream-white: #F5F0E1;
--black-stout: #1A1A1A;
--guinness-red: #B22222;
/* UI colors */
--success: #28a745;
--danger: #dc3545;
--warning: #ffc107;
--info: #17a2b8;
}
/* Base styles */
body {
font-family: 'Open Sans', 'Helvetica Neue', sans-serif;
line-height: 1.6;
color: var(--black-stout);
background-color: #f8f9fa;
}
h1, h2, h3, h4, h5, h6 {
font-family: 'Garamond', 'Georgia', serif;
color: var(--irish-green);
}
/* Custom button styles */
.btn-irish {
background-color: var(--irish-green);
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 0.25rem;
transition: opacity 0.2s ease;
}
.btn-irish:hover {
opacity: 0.9;
}
.btn-ale {
background-color: var(--golden-ale);
color: var(--black-stout);
border: none;
padding: 0.5rem 1rem;
border-radius: 0.25rem;
transition: opacity 0.2s ease;
}
.btn-ale:hover {
opacity: 0.9;
}
/* QR Code styles */
.qr-container {
padding: 1rem;
background-color: white;
border-radius: 0.5rem;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
display: inline-block;
}
.qr-title {
margin-top: 0.5rem;
font-weight: bold;
color: var(--irish-green);
text-align: center;
}
.qr-points {
font-size: 0.9rem;
color: var(--golden-ale);
font-weight: bold;
text-align: center;
}
/* Leaderboard styles */
.leaderboard-table {
width: 100%;
border-collapse: collapse;
}
.leaderboard-table th {
background-color: var(--irish-green);
color: white;
padding: 0.75rem;
text-align: left;
}
.leaderboard-table tr:nth-child(even) {
background-color: var(--cream-white);
}
.leaderboard-table td {
padding: 0.75rem;
border-bottom: 1px solid #e2e8f0;
}
/* Rank badges */
.rank-badge {
display: inline-block;
width: 2rem;
height: 2rem;
line-height: 2rem;
text-align: center;
border-radius: 50%;
font-weight: bold;
}
.rank-1 {
background-color: gold;
color: var(--black-stout);
}
.rank-2 {
background-color: silver;
color: var(--black-stout);
}
.rank-3 {
background-color: #cd7f32; /* bronze */
color: white;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.leaderboard-table thead {
display: none;
}
.leaderboard-table tr {
display: block;
margin-bottom: 1rem;
border: 1px solid #e2e8f0;
border-radius: 0.25rem;
}
.leaderboard-table td {
display: block;
text-align: right;
padding: 0.5rem;
}
.leaderboard-table td::before {
content: attr(data-label);
float: left;
font-weight: bold;
color: var(--irish-green);
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 383 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 398 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

@@ -0,0 +1 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

+191
View File
@@ -0,0 +1,191 @@
/**
* LeagueLedger Main JavaScript
* Version: 1.0.0
* Date: April 13, 2025
*/
// Initialize all components when document is ready
document.addEventListener('DOMContentLoaded', () => {
initializeQrScanner();
setupModalHandlers();
initializeTooltips();
initializeDropdowns();
});
/**
* QR Code Scanner initialization
*/
function initializeQrScanner() {
const scannerContainer = document.getElementById('qr-reader');
if (!scannerContainer) return; // Exit if scanner container doesn't exist
// Check if camera access is available
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
// QR Scanner library configuration
const html5QrCode = new Html5Qrcode("qr-reader");
const config = { fps: 10, qrbox: 250 };
// Remove overlay once scanner is ready
const overlay = document.getElementById('scanner-overlay');
if (overlay) {
overlay.style.display = 'none';
}
// Start scanner
html5QrCode.start(
{ facingMode: "environment" }, // Use rear camera
config,
onScanSuccess,
onScanFailure
);
// Save scanner instance for later use
window.qrScanner = html5QrCode;
} else {
// Camera unavailable, show manual entry form
const manualEntry = document.getElementById('manual-entry-container');
if (manualEntry) {
manualEntry.style.display = 'block';
}
const scannerElement = document.getElementById('scanner-container');
if (scannerElement) {
scannerElement.style.display = 'none';
}
console.warn("Camera access not available");
}
}
/**
* Handle successful QR code scan
*/
function onScanSuccess(decodedText) {
// Stop scanner once code is detected
if (window.qrScanner) {
window.qrScanner.stop();
}
// Extract code from URL if present
let code = decodedText;
if (decodedText.includes('/redeem/')) {
code = decodedText.split('/redeem/').pop();
}
// Redirect to redemption page
window.location.href = '/redeem/' + code;
}
/**
* Handle QR scan errors
*/
function onScanFailure(error) {
// We don't need to show errors for normal operation
console.debug("QR scan error: " + error);
}
/**
* Initialize modal handlers
*/
function setupModalHandlers() {
// Find all elements meant to open modals
const modalOpeners = document.querySelectorAll('[data-modal-target]');
const modalClosers = document.querySelectorAll('[data-modal-close]');
modalOpeners.forEach(opener => {
opener.addEventListener('click', (e) => {
e.preventDefault();
const modalId = opener.getAttribute('data-modal-target');
const modal = document.getElementById(modalId);
if (modal) {
modal.classList.remove('hidden');
}
});
});
modalClosers.forEach(closer => {
closer.addEventListener('click', (e) => {
e.preventDefault();
const modal = closer.closest('.modal');
if (modal) {
modal.classList.add('hidden');
}
});
});
// Close modal when clicking outside
document.addEventListener('click', (e) => {
const modals = document.querySelectorAll('.modal:not(.hidden)');
modals.forEach(modal => {
if (e.target === modal) {
modal.classList.add('hidden');
}
});
});
}
/**
* Initialize tooltips
*/
function initializeTooltips() {
const tooltips = document.querySelectorAll('[data-tooltip]');
tooltips.forEach(tooltip => {
tooltip.addEventListener('mouseenter', (e) => {
const text = tooltip.getAttribute('data-tooltip');
// Create tooltip element
const tooltipEl = document.createElement('div');
tooltipEl.classList.add('tooltip');
tooltipEl.textContent = text;
// Position the tooltip
const rect = tooltip.getBoundingClientRect();
tooltipEl.style.top = (rect.top - 30) + 'px';
tooltipEl.style.left = (rect.left + rect.width/2) + 'px';
// Add to DOM
document.body.appendChild(tooltipEl);
// Save reference to remove it later
tooltip._tooltipElement = tooltipEl;
});
tooltip.addEventListener('mouseleave', () => {
if (tooltip._tooltipElement) {
tooltip._tooltipElement.remove();
tooltip._tooltipElement = null;
}
});
});
}
/**
* Initialize dropdown menus
*/
function initializeDropdowns() {
const dropdowns = document.querySelectorAll('.dropdown-toggle');
dropdowns.forEach(dropdown => {
dropdown.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
const menu = dropdown.nextElementSibling;
if (menu && menu.classList.contains('dropdown-menu')) {
menu.classList.toggle('hidden');
}
});
});
// Close dropdowns when clicking elsewhere
document.addEventListener('click', () => {
const openDropdowns = document.querySelectorAll('.dropdown-menu:not(.hidden)');
openDropdowns.forEach(menu => {
menu.classList.add('hidden');
});
});
}