Notifications, div fixes, kekse für last location

This commit is contained in:
2025-09-06 12:37:10 +02:00
parent 61d5ef2e6f
commit 8342d95a13
13 changed files with 1325 additions and 39 deletions

View File

@@ -1374,3 +1374,9 @@ body {
max-width: none;
}
}
.last-location-info small {
color: #b3e5fc;
font-size: 0.85rem;
}

View File

@@ -5,10 +5,68 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SPEEDRUN ARENA - Admin Dashboard</title>
<link rel="icon" type="image/x-icon" href="/pictures/favicon.ico">
<link rel="manifest" href="/manifest.json">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="Ninja Cross">
<link rel="apple-touch-icon" href="/pictures/favicon.ico">
<script src="https://unpkg.com/@supabase/supabase-js@2"></script>
<!-- QR Code Scanner Library -->
<script src="https://unpkg.com/jsqr@1.4.0/dist/jsQR.js"></script>
<link rel="stylesheet" href="/css/dashboard.css">
<!-- Notification Permission Script -->
<script>
// Register Service Worker for iPhone Notifications
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then(function(registration) {
console.log('✅ Service Worker registered:', registration);
})
.catch(function(error) {
console.log('❌ Service Worker registration failed:', error);
});
}
// Request notification permission on page load
if ('Notification' in window) {
if (Notification.permission === 'default') {
Notification.requestPermission().then(function(permission) {
if (permission === 'granted') {
console.log('✅ Notification permission granted');
// Subscribe to push notifications
subscribeToPush();
} else {
console.log('❌ Notification permission denied');
}
});
}
}
// Subscribe to push notifications
async function subscribeToPush() {
try {
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: 'BEl62iUYgUivxIkv69yViEuiBIa40HI6F2B5L4h7Q8Y'
});
// Send subscription to server
await fetch('/api/v1/public/subscribe', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(subscription)
});
console.log('✅ Push subscription successful');
} catch (error) {
console.error('❌ Push subscription failed:', error);
}
}
</script>
</head>
<body>
<div class="main-container">
@@ -34,6 +92,7 @@
<div class="welcome-card">
<h2>Dein Dashboard 🥷</h2>
<p>Willkommen in Deinem Dashboard-Panel! Deine übersichtliche Übersicht aller deiner Läufe.</p>
</div>
<div class="dashboard-grid">
@@ -252,6 +311,6 @@
</footer>
<script src="/js/cookie-consent.js"></script>
<script src="/js/dashboard.js"></script>
<script src="/js/dashboard.js?v=1.1"></script>
</body>
</html>

View File

@@ -6,6 +6,7 @@
<title>Timer Leaderboard</title>
<link rel="icon" type="image/x-icon" href="/pictures/favicon.ico">
<script src="/js/page-tracking.js"></script>
<script src="/js/cookie-utils.js"></script>
<link rel="stylesheet" href="/css/leaderboard.css">
</head>
<body>

105
public/js/cookie-utils.js Normal file
View File

@@ -0,0 +1,105 @@
// Cookie Utility Functions
class CookieManager {
// Set a cookie
static setCookie(name, value, days = 30) {
const expires = new Date();
expires.setTime(expires.getTime() + (days * 24 * 60 * 60 * 1000));
document.cookie = `${name}=${value};expires=${expires.toUTCString()};path=/;SameSite=Lax`;
}
// Get a cookie
static getCookie(name) {
const nameEQ = name + "=";
const ca = document.cookie.split(';');
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) === ' ') c = c.substring(1, c.length);
if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
}
return null;
}
// Delete a cookie
static deleteCookie(name) {
document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 UTC;path=/;`;
}
// Check if cookies are enabled
static areCookiesEnabled() {
try {
this.setCookie('test', 'test');
const enabled = this.getCookie('test') === 'test';
this.deleteCookie('test');
return enabled;
} catch (e) {
return false;
}
}
}
// Location-specific cookie functions
class LocationCookieManager {
static COOKIE_NAME = 'ninjacross_last_location';
static COOKIE_EXPIRY_DAYS = 90; // 3 months
// Save last selected location
static saveLastLocation(locationId, locationName) {
if (!locationId || !locationName) return;
const locationData = {
id: locationId,
name: locationName,
timestamp: new Date().toISOString()
};
try {
CookieManager.setCookie(
this.COOKIE_NAME,
JSON.stringify(locationData),
this.COOKIE_EXPIRY_DAYS
);
console.log('✅ Location saved to cookie:', locationName);
} catch (error) {
console.error('❌ Failed to save location to cookie:', error);
}
}
// Get last selected location
static getLastLocation() {
try {
const cookieValue = CookieManager.getCookie(this.COOKIE_NAME);
if (!cookieValue) return null;
const locationData = JSON.parse(cookieValue);
// Check if cookie is not too old (optional: 30 days max)
const cookieDate = new Date(locationData.timestamp);
const maxAge = 30 * 24 * 60 * 60 * 1000; // 30 days in milliseconds
if (Date.now() - cookieDate.getTime() > maxAge) {
this.clearLastLocation();
return null;
}
return locationData;
} catch (error) {
console.error('❌ Failed to parse location cookie:', error);
this.clearLastLocation();
return null;
}
}
// Clear last location
static clearLastLocation() {
CookieManager.deleteCookie(this.COOKIE_NAME);
console.log('🗑️ Location cookie cleared');
}
// Check if location cookie exists
static hasLastLocation() {
return this.getLastLocation() !== null;
}
}
// Export for use in other scripts
window.CookieManager = CookieManager;
window.LocationCookieManager = LocationCookieManager;

View File

@@ -132,12 +132,13 @@ async function checkLinkStatusAndLoadTimes() {
try {
// Check if user has a linked player
const response = await fetch(`/api/v1/public/user-player/${currentUser.id}`);
const response = await fetch(`/api/v1/public/user-player/${currentUser.id}?t=${Date.now()}`);
if (response.ok) {
const result = await response.json();
// User is linked, load times
await loadUserTimesSection(result.data);
} else {
// User is not linked
showTimesNotLinked();
@@ -362,7 +363,7 @@ async function loadUserTimesSection(playerData) {
showTimesLoading();
try {
const response = await fetch(`/api/v1/public/user-times/${currentUser.id}`);
const response = await fetch(`/api/v1/public/user-times/${currentUser.id}?t=${Date.now()}`);
const result = await response.json();
if (!response.ok) {
@@ -608,14 +609,15 @@ async function loadPlayerAchievements() {
document.getElementById('achievementCategories').style.display = 'none';
document.getElementById('achievementsNotAvailable').style.display = 'none';
// Load player achievements
const response = await fetch(`/api/achievements/player/${currentPlayerId}`);
// Load player achievements (includes all achievements with player status)
const response = await fetch(`/api/achievements/player/${currentPlayerId}?t=${Date.now()}`);
if (!response.ok) {
throw new Error('Failed to load achievements');
throw new Error('Failed to load player achievements');
}
const result = await response.json();
playerAchievements = result.data;
window.allAchievements = result.data;
playerAchievements = result.data.filter(achievement => achievement.is_completed);
// Load achievement statistics
await loadAchievementStats();
@@ -639,7 +641,7 @@ async function loadPlayerAchievements() {
// Load achievement statistics
async function loadAchievementStats() {
try {
const response = await fetch(`/api/achievements/player/${currentPlayerId}/stats`);
const response = await fetch(`/api/achievements/player/${currentPlayerId}/stats?t=${Date.now()}`);
if (response.ok) {
const result = await response.json();
window.achievementStats = result.data;
@@ -665,7 +667,7 @@ function displayAchievementStats() {
function displayAchievements() {
const achievementsGrid = document.getElementById('achievementsGrid');
if (playerAchievements.length === 0) {
if (!window.allAchievements || window.allAchievements.length === 0) {
achievementsGrid.innerHTML = `
<div class="no-achievements">
<div class="no-achievements-icon">🏆</div>
@@ -677,9 +679,9 @@ function displayAchievements() {
}
// Filter achievements by category
let filteredAchievements = playerAchievements;
let filteredAchievements = window.allAchievements;
if (currentAchievementCategory !== 'all') {
filteredAchievements = playerAchievements.filter(achievement =>
filteredAchievements = window.allAchievements.filter(achievement =>
achievement.category === currentAchievementCategory
);
}
@@ -690,6 +692,11 @@ function displayAchievements() {
const progress = achievement.progress || 0;
const earnedAt = achievement.earned_at;
// Debug logging
if (achievement.name === 'Tageskönig') {
console.log('Tageskönig Debug:', { isCompleted, progress, earnedAt });
}
let progressText = '';
if (isCompleted) {
progressText = earnedAt ?
@@ -780,7 +787,7 @@ async function checkPlayerAchievements() {
if (!currentPlayerId) return;
try {
const response = await fetch(`/api/achievements/check/${currentPlayerId}`, {
const response = await fetch(`/api/achievements/check/${currentPlayerId}?t=${Date.now()}`, {
method: 'POST'
});
@@ -831,6 +838,109 @@ function initializeAchievements(playerId) {
loadPlayerAchievements();
}
// Web Notification Functions
function showWebNotification(title, message, icon = '🏆') {
if ('Notification' in window && Notification.permission === 'granted') {
const notification = new Notification(title, {
body: message,
icon: '/pictures/favicon.ico',
badge: '/pictures/favicon.ico',
tag: 'ninjacross-achievement',
requireInteraction: true
});
// Auto-close after 10 seconds
setTimeout(() => {
notification.close();
}, 10000);
// Handle click
notification.onclick = function() {
window.focus();
notification.close();
};
}
}
// Check for best time achievements and show notifications
async function checkBestTimeNotifications() {
try {
const response = await fetch('/api/v1/public/best-times');
const result = await response.json();
if (result.success && result.data) {
const { daily, weekly, monthly } = result.data;
// Check if current player has best times
if (currentPlayerId) {
if (daily && daily.player_id === currentPlayerId) {
showWebNotification(
'🏆 Tageskönig!',
`Glückwunsch! Du hast die beste Zeit des Tages mit ${daily.best_time} erreicht!`,
'👑'
);
}
if (weekly && weekly.player_id === currentPlayerId) {
showWebNotification(
'🏆 Wochenchampion!',
`Fantastisch! Du bist der Wochenchampion mit ${weekly.best_time}!`,
'🏆'
);
}
if (monthly && monthly.player_id === currentPlayerId) {
showWebNotification(
'🏆 Monatsmeister!',
`Unglaublich! Du bist der Monatsmeister mit ${monthly.best_time}!`,
'🥇'
);
}
}
}
} catch (error) {
console.error('Error checking best time notifications:', error);
}
}
// Check for new achievements and show notifications
async function checkAchievementNotifications() {
try {
if (!currentPlayerId) return;
const response = await fetch(`/api/achievements/player/${currentPlayerId}?t=${Date.now()}`);
const result = await response.json();
if (result.success && result.data) {
const newAchievements = result.data.filter(achievement => {
// Check if achievement was earned in the last 5 minutes
const earnedAt = new Date(achievement.earned_at);
const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
return earnedAt > fiveMinutesAgo;
});
if (newAchievements.length > 0) {
newAchievements.forEach(achievement => {
showWebNotification(
`🏆 ${achievement.name}`,
achievement.description,
achievement.icon || '🏆'
);
});
}
}
} catch (error) {
console.error('Error checking achievement notifications:', error);
}
}
// Periodic check for notifications (every 30 seconds)
setInterval(() => {
checkBestTimeNotifications();
checkAchievementNotifications();
}, 30000);
document.addEventListener('DOMContentLoaded', function() {
// Add cookie settings button functionality
const cookieSettingsBtn = document.getElementById('cookie-settings-footer');

View File

@@ -10,6 +10,58 @@ const socket = io();
// Global variable to store locations with coordinates
let locationsData = [];
let lastSelectedLocation = null;
// Cookie Functions (inline implementation)
function setCookie(name, value, days = 30) {
const expires = new Date();
expires.setTime(expires.getTime() + (days * 24 * 60 * 60 * 1000));
document.cookie = `${name}=${value};expires=${expires.toUTCString()};path=/;SameSite=Lax`;
}
function getCookie(name) {
const nameEQ = name + "=";
const ca = document.cookie.split(';');
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) === ' ') c = c.substring(1, c.length);
if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
}
return null;
}
function loadLastSelectedLocation() {
try {
const cookieValue = getCookie('ninjacross_last_location');
if (cookieValue) {
const lastLocation = JSON.parse(cookieValue);
lastSelectedLocation = lastLocation;
console.log('📍 Last selected location loaded:', lastLocation.name);
return lastLocation;
}
} catch (error) {
console.error('Error loading last location:', error);
}
return null;
}
function saveLocationSelection(locationId, locationName) {
try {
// Remove emoji from location name for storage
const cleanName = locationName.replace(/^📍\s*/, '');
const locationData = {
id: locationId,
name: cleanName,
timestamp: new Date().toISOString()
};
setCookie('ninjacross_last_location', JSON.stringify(locationData), 90);
lastSelectedLocation = { id: locationId, name: cleanName };
console.log('💾 Location saved to cookie:', cleanName);
} catch (error) {
console.error('Error saving location:', error);
}
}
// WebSocket Event Handlers
socket.on('connect', () => {
@@ -129,6 +181,23 @@ async function loadLocations() {
locationSelect.appendChild(option);
});
// Load and set last selected location
const lastLocation = loadLastSelectedLocation();
if (lastLocation) {
// Find the option that matches the last location name
const matchingOption = Array.from(locationSelect.options).find(option =>
option.textContent === `📍 ${lastLocation.name}` || option.value === lastLocation.name
);
if (matchingOption) {
locationSelect.value = matchingOption.value;
console.log('📍 Last selected location restored:', lastLocation.name);
// Update the current selection display
updateCurrentSelection();
// Load data for the restored location
loadData();
}
}
} catch (error) {
console.error('Error loading locations:', error);
}
@@ -489,7 +558,15 @@ function updateLeaderboard(data) {
// Event Listeners Setup
function setupEventListeners() {
// Location select event listener
document.getElementById('locationSelect').addEventListener('change', loadData);
document.getElementById('locationSelect').addEventListener('change', function() {
// Save location selection to cookie
const selectedOption = this.options[this.selectedIndex];
if (selectedOption.value) {
saveLocationSelection(selectedOption.value, selectedOption.textContent);
}
// Load data
loadData();
});
// Time tab event listeners
document.querySelectorAll('.time-tab').forEach(tab => {

43
public/manifest.json Normal file
View File

@@ -0,0 +1,43 @@
{
"name": "Ninja Cross Parkour",
"short_name": "NinjaCross",
"description": "Dein persönliches Dashboard für die Ninja Cross Arena",
"start_url": "/index.html",
"display": "standalone",
"background_color": "#667eea",
"theme_color": "#764ba2",
"orientation": "portrait",
"icons": [
{
"src": "/pictures/favicon.ico",
"sizes": "192x192",
"type": "image/x-icon",
"purpose": "any maskable"
},
{
"src": "/pictures/favicon.ico",
"sizes": "512x512",
"type": "image/x-icon",
"purpose": "any maskable"
}
],
"categories": ["sports", "fitness", "entertainment"],
"lang": "de",
"dir": "ltr",
"scope": "/",
"prefer_related_applications": false,
"shortcuts": [
{
"name": "Dashboard",
"short_name": "Dashboard",
"description": "Öffne dein Dashboard",
"url": "/index.html",
"icons": [
{
"src": "/pictures/favicon.ico",
"sizes": "192x192"
}
]
}
]
}

94
public/sw.js Normal file
View File

@@ -0,0 +1,94 @@
// Service Worker für iPhone Notifications
const CACHE_NAME = 'ninjacross-v1';
const urlsToCache = [
'/',
'/index.html',
'/css/leaderboard.css',
'/js/leaderboard.js',
'/pictures/favicon.ico'
];
// Install event
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open(CACHE_NAME)
.then(function(cache) {
return cache.addAll(urlsToCache);
})
);
});
// Fetch event
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request)
.then(function(response) {
// Return cached version or fetch from network
return response || fetch(event.request);
}
)
);
});
// Push event (für iPhone Notifications)
self.addEventListener('push', function(event) {
console.log('Push received:', event);
const options = {
body: 'Du hast eine neue Notification!',
icon: '/pictures/favicon.ico',
badge: '/pictures/favicon.ico',
vibrate: [100, 50, 100],
data: {
dateOfArrival: Date.now(),
primaryKey: 1
},
actions: [
{
action: 'explore',
title: 'Dashboard öffnen',
icon: '/pictures/favicon.ico'
},
{
action: 'close',
title: 'Schließen',
icon: '/pictures/favicon.ico'
}
]
};
if (event.data) {
const data = event.data.json();
options.body = data.body || options.body;
options.title = data.title || 'Ninja Cross';
}
event.waitUntil(
self.registration.showNotification('Ninja Cross', options)
);
});
// Notification click event
self.addEventListener('notificationclick', function(event) {
console.log('Notification clicked:', event);
event.notification.close();
if (event.action === 'explore') {
event.waitUntil(
clients.openWindow('/dashboard.html')
);
}
});
// Background sync (für offline functionality)
self.addEventListener('sync', function(event) {
if (event.tag === 'background-sync') {
event.waitUntil(doBackgroundSync());
}
});
async function doBackgroundSync() {
// Sync data when back online
console.log('Background sync triggered');
}