Refactor ALL
This commit is contained in:
577
public/js/dashboard.js
Normal file
577
public/js/dashboard.js
Normal file
@@ -0,0 +1,577 @@
|
||||
// Supabase configuration
|
||||
const SUPABASE_URL = 'https://lfxlplnypzvjrhftaoog.supabase.co';
|
||||
const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImxmeGxwbG55cHp2anJoZnRhb29nIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDkyMTQ3NzIsImV4cCI6MjA2NDc5MDc3Mn0.XR4preBqWAQ1rT4PFbpkmRdz57BTwIusBI89fIxDHM8';
|
||||
|
||||
// Initialize Supabase client
|
||||
const supabase = window.supabase.createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
|
||||
|
||||
// Global variables
|
||||
let currentUser = null;
|
||||
|
||||
// Check authentication and load dashboard
|
||||
async function initDashboard() {
|
||||
try {
|
||||
// Get current session
|
||||
const { data: { session }, error } = await supabase.auth.getSession();
|
||||
|
||||
if (error) {
|
||||
console.error('Error checking authentication:', error);
|
||||
// Temporarily show dashboard for testing
|
||||
currentUser = { id: 'test-user', email: 'admin@speedrun-arena.com' };
|
||||
displayUserInfo({ email: 'admin@speedrun-arena.com' });
|
||||
showDashboard();
|
||||
// Check times section
|
||||
checkLinkStatusAndLoadTimes();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
// No session, redirect to login
|
||||
window.location.href = '/login';
|
||||
return;
|
||||
}
|
||||
|
||||
// User is authenticated, show dashboard
|
||||
if (session.user) {
|
||||
console.log('User data:', session.user);
|
||||
currentUser = session.user;
|
||||
displayUserInfo(session.user);
|
||||
} else {
|
||||
// Fallback if no user data
|
||||
currentUser = { id: 'test-user', email: 'admin@speedrun-arena.com' };
|
||||
displayUserInfo({ email: 'admin@speedrun-arena.com' });
|
||||
}
|
||||
showDashboard();
|
||||
|
||||
// Load times section
|
||||
checkLinkStatusAndLoadTimes();
|
||||
|
||||
} catch (error) {
|
||||
console.error('An unexpected error occurred:', error);
|
||||
// window.location.href = '/login';
|
||||
}
|
||||
}
|
||||
|
||||
// Display user information
|
||||
function displayUserInfo(user) {
|
||||
const userEmail = document.getElementById('userEmail');
|
||||
const userAvatar = document.getElementById('userAvatar');
|
||||
|
||||
userEmail.textContent = user.email;
|
||||
userAvatar.textContent = user.email.charAt(0).toUpperCase();
|
||||
}
|
||||
|
||||
// Show dashboard content
|
||||
function showDashboard() {
|
||||
document.getElementById('loading').style.display = 'none';
|
||||
document.getElementById('dashboardContent').style.display = 'block';
|
||||
}
|
||||
|
||||
// Logout function
|
||||
async function logout() {
|
||||
try {
|
||||
const { error } = await supabase.auth.signOut();
|
||||
if (error) {
|
||||
console.error('Error logging out:', error);
|
||||
} else {
|
||||
window.location.href = '/';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error during logout:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Listen for auth state changes
|
||||
supabase.auth.onAuthStateChange((event, session) => {
|
||||
if (event === 'SIGNED_OUT' || !session) {
|
||||
window.location.href = '/login';
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize dashboard when page loads
|
||||
initDashboard();
|
||||
|
||||
// Modal functions
|
||||
function openModal(modalId) {
|
||||
document.getElementById(modalId).style.display = 'block';
|
||||
}
|
||||
|
||||
function closeModal(modalId) {
|
||||
document.getElementById(modalId).style.display = 'none';
|
||||
// Reset modal state
|
||||
if (modalId === 'rfidModal') {
|
||||
stopQRScanner();
|
||||
document.getElementById('manualRfidInput').value = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Close modal when clicking outside
|
||||
window.onclick = function(event) {
|
||||
if (event.target.classList.contains('modal')) {
|
||||
closeModal(event.target.id);
|
||||
}
|
||||
}
|
||||
|
||||
// QR Scanner variables
|
||||
let qrStream = null;
|
||||
let qrScanning = false;
|
||||
|
||||
// Show RFID Settings
|
||||
async function showRFIDSettings() {
|
||||
openModal('rfidModal');
|
||||
// Reset scanner state
|
||||
stopQRScanner();
|
||||
}
|
||||
|
||||
// Check link status and load times
|
||||
async function checkLinkStatusAndLoadTimes() {
|
||||
if (!currentUser) {
|
||||
showTimesNotLinked();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Check if user has a linked player
|
||||
const response = await fetch(`/api/user-player/${currentUser.id}`);
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
// User is linked, load times
|
||||
await loadUserTimesSection(result.data);
|
||||
} else {
|
||||
// User is not linked
|
||||
showTimesNotLinked();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking link status:', error);
|
||||
showTimesNotLinked();
|
||||
}
|
||||
}
|
||||
|
||||
// Start QR Scanner
|
||||
async function startQRScanner() {
|
||||
try {
|
||||
// Request camera access
|
||||
qrStream = await navigator.mediaDevices.getUserMedia({
|
||||
video: {
|
||||
facingMode: 'environment', // Use back camera if available
|
||||
width: { ideal: 1280 },
|
||||
height: { ideal: 720 }
|
||||
}
|
||||
});
|
||||
|
||||
const video = document.getElementById('qrVideo');
|
||||
const canvas = document.getElementById('qrCanvas');
|
||||
const context = canvas.getContext('2d');
|
||||
|
||||
video.srcObject = qrStream;
|
||||
video.play();
|
||||
|
||||
// Show camera container and update buttons
|
||||
document.getElementById('cameraContainer').style.display = 'block';
|
||||
document.getElementById('startScanBtn').style.display = 'none';
|
||||
document.getElementById('stopScanBtn').style.display = 'inline-block';
|
||||
document.getElementById('scanningStatus').style.display = 'block';
|
||||
|
||||
qrScanning = true;
|
||||
|
||||
// Start scanning loop
|
||||
video.addEventListener('loadedmetadata', () => {
|
||||
canvas.width = video.videoWidth;
|
||||
canvas.height = video.videoHeight;
|
||||
scanQRCode();
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error accessing camera:', error);
|
||||
showMessage('rfidMessage', 'Kamera-Zugriff fehlgeschlagen. Bitte verwende die manuelle Eingabe.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Stop QR Scanner
|
||||
function stopQRScanner() {
|
||||
qrScanning = false;
|
||||
|
||||
if (qrStream) {
|
||||
qrStream.getTracks().forEach(track => track.stop());
|
||||
qrStream = null;
|
||||
}
|
||||
|
||||
// Reset UI
|
||||
document.getElementById('cameraContainer').style.display = 'none';
|
||||
document.getElementById('startScanBtn').style.display = 'inline-block';
|
||||
document.getElementById('stopScanBtn').style.display = 'none';
|
||||
document.getElementById('scanningStatus').style.display = 'none';
|
||||
}
|
||||
|
||||
// Scan QR Code from video stream
|
||||
function scanQRCode() {
|
||||
if (!qrScanning) return;
|
||||
|
||||
const video = document.getElementById('qrVideo');
|
||||
const canvas = document.getElementById('qrCanvas');
|
||||
const context = canvas.getContext('2d');
|
||||
|
||||
if (video.readyState === video.HAVE_ENOUGH_DATA) {
|
||||
canvas.width = video.videoWidth;
|
||||
canvas.height = video.videoHeight;
|
||||
context.drawImage(video, 0, 0, canvas.width, canvas.height);
|
||||
|
||||
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
|
||||
const code = jsQR(imageData.data, imageData.width, imageData.height);
|
||||
|
||||
if (code) {
|
||||
console.log('QR Code detected:', code.data);
|
||||
handleQRCodeDetected(code.data);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Continue scanning
|
||||
if (qrScanning) {
|
||||
requestAnimationFrame(scanQRCode);
|
||||
}
|
||||
}
|
||||
|
||||
// Format RFID UID to match database format
|
||||
function formatRfidUid(rawUid) {
|
||||
// Remove any existing formatting (spaces, colons, etc.)
|
||||
let cleanUid = rawUid.replace(/[^a-fA-F0-9]/g, '').toUpperCase();
|
||||
|
||||
// Handle different UID lengths
|
||||
if (cleanUid.length === 6) {
|
||||
// Pad 6-digit UID to 8 digits by adding leading zeros
|
||||
cleanUid = '00' + cleanUid;
|
||||
} else if (cleanUid.length === 8) {
|
||||
// Already correct length
|
||||
} else if (cleanUid.length < 6) {
|
||||
// Pad shorter UIDs to 8 digits
|
||||
cleanUid = cleanUid.padStart(8, '0');
|
||||
} else {
|
||||
throw new Error(`Ungültige RFID UID Länge: ${cleanUid.length} Zeichen (unterstützt: 6-8)`);
|
||||
}
|
||||
|
||||
// Format as XX:XX:XX:XX
|
||||
return cleanUid.match(/.{2}/g).join(':');
|
||||
}
|
||||
|
||||
// Handle detected QR code
|
||||
async function handleQRCodeDetected(qrData) {
|
||||
stopQRScanner();
|
||||
|
||||
try {
|
||||
// Extract and format RFID UID from QR code
|
||||
const rawUid = qrData.trim();
|
||||
|
||||
if (!rawUid) {
|
||||
showMessage('rfidMessage', 'QR-Code enthält keine gültige RFID UID', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Format the UID to match database format (XX:XX:XX:XX)
|
||||
const formattedUid = formatRfidUid(rawUid);
|
||||
|
||||
showMessage('rfidMessage', `QR-Code erkannt: ${rawUid} → ${formattedUid}`, 'info');
|
||||
|
||||
// Link the user using the formatted RFID UID
|
||||
await linkUserByRfidUid(formattedUid);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error formatting RFID UID:', error);
|
||||
showMessage('rfidMessage', `Fehler beim Formatieren der RFID UID: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Manual RFID linking
|
||||
async function linkManualRfid() {
|
||||
const rawUid = document.getElementById('manualRfidInput').value.trim();
|
||||
|
||||
if (!rawUid) {
|
||||
showMessage('rfidMessage', 'Bitte gib eine RFID UID ein', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Format the UID to match database format
|
||||
const formattedUid = formatRfidUid(rawUid);
|
||||
|
||||
showMessage('rfidMessage', `Formatiert: ${rawUid} → ${formattedUid}`, 'info');
|
||||
|
||||
await linkUserByRfidUid(formattedUid);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error formatting manual RFID UID:', error);
|
||||
showMessage('rfidMessage', `Fehler beim Formatieren: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Link user by RFID UID (core function)
|
||||
async function linkUserByRfidUid(rfidUid) {
|
||||
if (!currentUser) {
|
||||
showMessage('rfidMessage', 'Benutzer nicht authentifiziert', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// First, find the player with this RFID UID
|
||||
const response = await fetch('/api/link-by-rfid', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
rfiduid: rfidUid,
|
||||
supabase_user_id: currentUser.id
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
showMessage('rfidMessage', `✅ RFID erfolgreich verknüpft!\nSpieler: ${result.data.firstname} ${result.data.lastname}`, 'success');
|
||||
setTimeout(() => {
|
||||
closeModal('rfidModal');
|
||||
// Reload times section after successful linking
|
||||
checkLinkStatusAndLoadTimes();
|
||||
}, 2000);
|
||||
} else {
|
||||
showMessage('rfidMessage', result.message || 'Fehler beim Verknüpfen', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error linking RFID:', error);
|
||||
showMessage('rfidMessage', 'Fehler beim Verknüpfen der RFID', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Show not linked state
|
||||
function showTimesNotLinked() {
|
||||
document.getElementById('timesLoading').style.display = 'none';
|
||||
document.getElementById('timesNotLinked').style.display = 'block';
|
||||
document.getElementById('timesDisplay').style.display = 'none';
|
||||
}
|
||||
|
||||
// Show loading state
|
||||
function showTimesLoading() {
|
||||
document.getElementById('timesLoading').style.display = 'block';
|
||||
document.getElementById('timesNotLinked').style.display = 'none';
|
||||
document.getElementById('timesDisplay').style.display = 'none';
|
||||
}
|
||||
|
||||
// Load user times for the section
|
||||
async function loadUserTimesSection(playerData) {
|
||||
showTimesLoading();
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/user-times/${currentUser.id}`);
|
||||
const times = await response.json();
|
||||
|
||||
// Update stats
|
||||
updateTimesStats(times, playerData);
|
||||
|
||||
// Display times
|
||||
displayUserTimes(times);
|
||||
|
||||
// Show the times display
|
||||
document.getElementById('timesLoading').style.display = 'none';
|
||||
document.getElementById('timesNotLinked').style.display = 'none';
|
||||
document.getElementById('timesDisplay').style.display = 'block';
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading user times:', error);
|
||||
showTimesNotLinked();
|
||||
}
|
||||
}
|
||||
|
||||
// Update stats cards
|
||||
function updateTimesStats(times, playerData) {
|
||||
// Total runs
|
||||
document.getElementById('totalRuns').textContent = times.length;
|
||||
|
||||
// Best time
|
||||
if (times.length > 0) {
|
||||
const bestTimeValue = times.reduce((best, current) => {
|
||||
const currentSeconds = convertTimeToSeconds(current.recorded_time);
|
||||
const bestSeconds = convertTimeToSeconds(best.recorded_time);
|
||||
return currentSeconds < bestSeconds ? current : best;
|
||||
});
|
||||
document.getElementById('bestTime').textContent = formatTime(bestTimeValue.recorded_time);
|
||||
} else {
|
||||
document.getElementById('bestTime').textContent = '--:--';
|
||||
}
|
||||
|
||||
// Unique locations count
|
||||
const uniqueLocations = [...new Set(times.map(time => time.location_name))];
|
||||
document.getElementById('locationsCount').textContent = uniqueLocations.length;
|
||||
|
||||
// Linked player name
|
||||
document.getElementById('linkedPlayer').textContent = `${playerData.firstname} ${playerData.lastname}`;
|
||||
}
|
||||
|
||||
// Display user times in grid
|
||||
function displayUserTimes(times) {
|
||||
const timesGrid = document.getElementById('userTimesGrid');
|
||||
|
||||
if (times.length === 0) {
|
||||
timesGrid.innerHTML = `
|
||||
<div style="grid-column: 1 / -1; text-align: center; padding: 3rem; color: #8892b0;">
|
||||
<h3>Noch keine Zeiten aufgezeichnet</h3>
|
||||
<p>Deine ersten Läufe werden hier angezeigt, sobald du sie abgeschlossen hast!</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Group times by location
|
||||
const timesByLocation = times.reduce((acc, time) => {
|
||||
if (!acc[time.location_name]) {
|
||||
acc[time.location_name] = [];
|
||||
}
|
||||
acc[time.location_name].push(time);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// Generate cards for each location
|
||||
const cards = Object.entries(timesByLocation).map(([locationName, locationTimes], index) => {
|
||||
// Sort times by performance (best first)
|
||||
const sortedTimes = locationTimes.sort((a, b) => {
|
||||
return convertTimeToSeconds(a.recorded_time) - convertTimeToSeconds(b.recorded_time);
|
||||
});
|
||||
|
||||
// Get best time for this location
|
||||
const bestTime = sortedTimes[0];
|
||||
|
||||
// Generate all runs for expanded view
|
||||
const allRunsHtml = sortedTimes.map((run, runIndex) => {
|
||||
let rankBadge = '';
|
||||
let rankClass = '';
|
||||
|
||||
if (runIndex === 0) {
|
||||
rankBadge = '🥇 Beste';
|
||||
rankClass = 'best';
|
||||
} else if (runIndex === 1) {
|
||||
rankBadge = '🥈 2.';
|
||||
rankClass = 'second';
|
||||
} else if (runIndex === 2) {
|
||||
rankBadge = '🥉 3.';
|
||||
rankClass = 'third';
|
||||
} else {
|
||||
rankBadge = `${runIndex + 1}.`;
|
||||
rankClass = '';
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="run-item">
|
||||
<div>
|
||||
<div class="run-time">${formatTime(run.recorded_time)}</div>
|
||||
</div>
|
||||
<div class="run-details">
|
||||
<div>${new Date(run.created_at).toLocaleDateString('de-DE')}</div>
|
||||
<div>${new Date(run.created_at).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })}</div>
|
||||
<span class="run-rank-badge ${rankClass}">${rankBadge}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
return `
|
||||
<div class="user-time-card" onclick="toggleTimeCard(this)" data-location="${locationName}">
|
||||
<div class="card-header">
|
||||
<div class="time-location-name">${locationName}</div>
|
||||
<div class="expand-indicator">▼</div>
|
||||
</div>
|
||||
|
||||
<div class="card-main-content">
|
||||
<div class="time-value-large">${formatTime(bestTime.recorded_time)}</div>
|
||||
<div class="time-date-info">
|
||||
<span>${new Date(bestTime.created_at).toLocaleDateString('de-DE')}</span>
|
||||
<span class="time-rank">${locationTimes.length} Läufe</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="expanded-content">
|
||||
<div class="all-runs-title">Alle Läufe an diesem Standort:</div>
|
||||
${allRunsHtml}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
timesGrid.innerHTML = cards;
|
||||
}
|
||||
|
||||
// Toggle time card expansion
|
||||
function toggleTimeCard(cardElement) {
|
||||
const isExpanded = cardElement.classList.contains('expanded');
|
||||
|
||||
// Close all other cards first
|
||||
document.querySelectorAll('.user-time-card.expanded').forEach(card => {
|
||||
if (card !== cardElement) {
|
||||
card.classList.remove('expanded');
|
||||
}
|
||||
});
|
||||
|
||||
// Toggle current card
|
||||
if (isExpanded) {
|
||||
cardElement.classList.remove('expanded');
|
||||
} else {
|
||||
cardElement.classList.add('expanded');
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to convert time to seconds for comparison
|
||||
function convertTimeToSeconds(timeValue) {
|
||||
if (typeof timeValue === 'string') {
|
||||
// Handle HH:MM:SS format
|
||||
const parts = timeValue.split(':');
|
||||
if (parts.length === 3) {
|
||||
return parseInt(parts[0]) * 3600 + parseInt(parts[1]) * 60 + parseFloat(parts[2]);
|
||||
}
|
||||
// Handle MM:SS format
|
||||
if (parts.length === 2) {
|
||||
return parseInt(parts[0]) * 60 + parseFloat(parts[1]);
|
||||
}
|
||||
}
|
||||
return parseFloat(timeValue) || 0;
|
||||
}
|
||||
|
||||
// Format time interval to readable format
|
||||
function formatTime(interval) {
|
||||
// Postgres interval format: {"hours":0,"minutes":1,"seconds":23.45}
|
||||
if (typeof interval === 'object') {
|
||||
const { hours = 0, minutes = 0, seconds = 0 } = interval;
|
||||
const totalSeconds = hours * 3600 + minutes * 60 + seconds;
|
||||
return formatSeconds(totalSeconds);
|
||||
}
|
||||
|
||||
// Fallback for string format
|
||||
if (typeof interval === 'string') {
|
||||
// Parse format like "00:01:23.45"
|
||||
const parts = interval.split(':');
|
||||
if (parts.length === 3) {
|
||||
const hours = parseInt(parts[0]);
|
||||
const minutes = parseInt(parts[1]);
|
||||
const seconds = parseFloat(parts[2]);
|
||||
const totalSeconds = hours * 3600 + minutes * 60 + seconds;
|
||||
return formatSeconds(totalSeconds);
|
||||
}
|
||||
}
|
||||
|
||||
return interval;
|
||||
}
|
||||
|
||||
function formatSeconds(totalSeconds) {
|
||||
const minutes = Math.floor(totalSeconds / 60);
|
||||
const seconds = (totalSeconds % 60).toFixed(2);
|
||||
|
||||
if (minutes > 0) {
|
||||
return `${minutes}:${seconds.padStart(5, '0')}`;
|
||||
} else {
|
||||
return `${seconds}s`;
|
||||
}
|
||||
}
|
||||
|
||||
// Show message in modal
|
||||
function showMessage(containerId, message, type) {
|
||||
const container = document.getElementById(containerId);
|
||||
container.innerHTML = `<div class="message ${type}">${message}</div>`;
|
||||
}
|
||||
Reference in New Issue
Block a user