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.
@@ -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);
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 383 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 398 B |
|
After Width: | Height: | Size: 982 B |
|
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"}
|
||||
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
@@ -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');
|
||||
});
|
||||
});
|
||||
}
|
||||