This commit is contained in:
2025-09-23 14:13:24 +02:00
commit 58b5e6b074
103 changed files with 44000 additions and 0 deletions

336
public/404.html Normal file
View File

@@ -0,0 +1,336 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>404 - Seite nicht gefunden | NinjaCross</title>
<link rel="icon" type="image/x-icon" href="/pictures/favicon.ico">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
position: relative;
}
/* Animated background particles */
.particles {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
z-index: 1;
}
.particle {
position: absolute;
background: rgba(255, 255, 255, 0.1);
border-radius: 50%;
animation: float 6s ease-in-out infinite;
}
.particle:nth-child(1) { width: 4px; height: 4px; left: 10%; animation-delay: 0s; }
.particle:nth-child(2) { width: 6px; height: 6px; left: 20%; animation-delay: 1s; }
.particle:nth-child(3) { width: 3px; height: 3px; left: 30%; animation-delay: 2s; }
.particle:nth-child(4) { width: 5px; height: 5px; left: 40%; animation-delay: 3s; }
.particle:nth-child(5) { width: 4px; height: 4px; left: 50%; animation-delay: 4s; }
.particle:nth-child(6) { width: 7px; height: 7px; left: 60%; animation-delay: 5s; }
.particle:nth-child(7) { width: 3px; height: 3px; left: 70%; animation-delay: 0.5s; }
.particle:nth-child(8) { width: 5px; height: 5px; left: 80%; animation-delay: 1.5s; }
.particle:nth-child(9) { width: 4px; height: 4px; left: 90%; animation-delay: 2.5s; }
@keyframes float {
0%, 100% { transform: translateY(100vh) rotate(0deg); opacity: 0; }
10% { opacity: 1; }
90% { opacity: 1; }
50% { transform: translateY(-10vh) rotate(180deg); }
}
/* Main container */
.container {
text-align: center;
z-index: 10;
position: relative;
max-width: 600px;
padding: 2rem;
}
/* Ninja emoji with animation */
.ninja-emoji {
font-size: 8rem;
margin-bottom: 1rem;
display: inline-block;
animation: ninja-bounce 2s ease-in-out infinite;
filter: drop-shadow(0 0 20px rgba(0, 255, 255, 0.5));
}
@keyframes ninja-bounce {
0%, 100% { transform: translateY(0) rotate(0deg); }
25% { transform: translateY(-20px) rotate(-5deg); }
50% { transform: translateY(-10px) rotate(0deg); }
75% { transform: translateY(-15px) rotate(5deg); }
}
/* 404 number with glow effect */
.error-code {
font-size: 6rem;
font-weight: bold;
background: linear-gradient(45deg, #00ffff, #ff00ff, #ffff00, #00ffff);
background-size: 400% 400%;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
animation: gradient-shift 3s ease-in-out infinite;
margin-bottom: 1rem;
text-shadow: 0 0 30px rgba(0, 255, 255, 0.5);
}
@keyframes gradient-shift {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
/* Error message */
.error-message {
font-size: 1.5rem;
color: #ffffff;
margin-bottom: 2rem;
opacity: 0;
animation: fade-in-up 1s ease-out 0.5s forwards;
}
@keyframes fade-in-up {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Description */
.description {
font-size: 1.1rem;
color: #b0b0b0;
margin-bottom: 3rem;
line-height: 1.6;
opacity: 0;
animation: fade-in-up 1s ease-out 1s forwards;
}
/* Action buttons */
.actions {
display: flex;
gap: 1rem;
justify-content: center;
flex-wrap: wrap;
opacity: 0;
animation: fade-in-up 1s ease-out 1.5s forwards;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 8px;
font-size: 1rem;
font-weight: 600;
text-decoration: none;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.btn-primary {
background: linear-gradient(135deg, #00ffff, #0080ff);
color: white;
box-shadow: 0 4px 15px rgba(0, 255, 255, 0.3);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0, 255, 255, 0.5);
}
.btn-secondary {
background: transparent;
color: #00ffff;
border: 2px solid #00ffff;
}
.btn-secondary:hover {
background: #00ffff;
color: #1a1a2e;
transform: translateY(-2px);
}
/* Glitch effect for 404 */
.glitch {
position: relative;
display: inline-block;
}
.glitch::before,
.glitch::after {
content: '404';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(45deg, #00ffff, #ff00ff, #ffff00, #00ffff);
background-size: 400% 400%;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.glitch::before {
animation: glitch-1 0.5s infinite;
z-index: -1;
}
.glitch::after {
animation: glitch-2 0.5s infinite;
z-index: -2;
}
@keyframes glitch-1 {
0%, 100% { transform: translate(0); }
20% { transform: translate(-2px, 2px); }
40% { transform: translate(-2px, -2px); }
60% { transform: translate(2px, 2px); }
80% { transform: translate(2px, -2px); }
}
@keyframes glitch-2 {
0%, 100% { transform: translate(0); }
20% { transform: translate(2px, -2px); }
40% { transform: translate(2px, 2px); }
60% { transform: translate(-2px, -2px); }
80% { transform: translate(-2px, 2px); }
}
/* Responsive design */
@media (max-width: 768px) {
.ninja-emoji { font-size: 6rem; }
.error-code { font-size: 4rem; }
.error-message { font-size: 1.2rem; }
.description { font-size: 1rem; }
.actions { flex-direction: column; align-items: center; }
.btn { width: 200px; }
}
/* Loading animation for page load */
.container {
animation: page-load 1s ease-out;
}
@keyframes page-load {
from {
opacity: 0;
transform: scale(0.8);
}
to {
opacity: 1;
transform: scale(1);
}
}
</style>
</head>
<body>
<!-- Animated background particles -->
<div class="particles">
<div class="particle"></div>
<div class="particle"></div>
<div class="particle"></div>
<div class="particle"></div>
<div class="particle"></div>
<div class="particle"></div>
<div class="particle"></div>
<div class="particle"></div>
<div class="particle"></div>
</div>
<div class="container">
<!-- Animated ninja emoji -->
<div class="ninja-emoji">🥷</div>
<!-- Glitchy 404 number -->
<div class="error-code glitch">404</div>
<!-- Error message -->
<h1 class="error-message">Oops! Diese Seite ist im Ninja-Modus verschwunden!</h1>
<!-- Description -->
<p class="description">
Die Seite, die du suchst, hat sich wie ein echter Ninja versteckt.<br>
Vielleicht ist sie auf einer geheimen Mission oder hat sich in der Dunkelheit versteckt.
</p>
<!-- Action buttons -->
<div class="actions">
<a href="/" class="btn btn-primary">🏠 Zur Hauptseite</a>
<a href="/dashboard.html" class="btn btn-secondary">📊 Dashboard</a>
</div>
</div>
<script>
// Add some interactive effects
document.addEventListener('mousemove', (e) => {
const particles = document.querySelectorAll('.particle');
const x = e.clientX / window.innerWidth;
const y = e.clientY / window.innerHeight;
particles.forEach((particle, index) => {
const speed = (index + 1) * 0.5;
const xOffset = (x - 0.5) * speed * 20;
const yOffset = (y - 0.5) * speed * 20;
particle.style.transform = `translate(${xOffset}px, ${yOffset}px)`;
});
});
// Add click effect on ninja emoji
document.querySelector('.ninja-emoji').addEventListener('click', () => {
const ninja = document.querySelector('.ninja-emoji');
ninja.style.animation = 'none';
ninja.style.transform = 'scale(1.2) rotate(360deg)';
setTimeout(() => {
ninja.style.animation = 'ninja-bounce 2s ease-in-out infinite';
ninja.style.transform = '';
}, 500);
});
// Add some console easter egg
console.log(`
🥷 NINJA 404 CONSOLE EASTER EGG 🥷
Du hast die geheime Konsole gefunden!
Hier ist ein Ninja-Haiku für dich:
"Versteckte Seite
Wie ein Ninja in der Nacht
Kehrt bald zurück"
- Der NinjaCross Server
`);
</script>
</body>
</html>

254
public/admin-dashboard.html Normal file
View File

@@ -0,0 +1,254 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin Dashboard - NinjaCross</title>
<link rel="icon" type="image/x-icon" href="/pictures/favicon.ico">
<link rel="stylesheet" href="/css/admin-dashboard.css">
</head>
<body>
<div class="header">
<h1>🛡️ Admin Dashboard</h1>
<div class="user-info">
<span id="username">Loading...</span>
<span id="accessBadge" class="access-badge">Level ?</span>
<button id="generatorBtn" class="btn btn-success" style="display: none;">
🔧 Lizenzgenerator
</button>
<button id="logoutBtn" class="btn btn-danger">Logout</button>
</div>
</div>
<div class="container">
<!-- Statistiken -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-number" id="totalPlayers">-</div>
<div class="stat-label">Spieler</div>
</div>
<div class="stat-card">
<div class="stat-number" id="totalRuns">-</div>
<div class="stat-label">Läufe</div>
</div>
<div class="stat-card">
<div class="stat-number" id="totalLocations">-</div>
<div class="stat-label">Standorte</div>
</div>
<div class="stat-card">
<div class="stat-number" id="totalAdminUsers">-</div>
<div class="stat-label">Admin-Benutzer</div>
</div>
</div>
<!-- Hauptseiten-Besuche -->
<div class="page-stats-section">
<h2>🏠 Hauptseiten-Besuche</h2>
<div class="page-stats-grid">
<div class="page-stat-card">
<h3>Heute</h3>
<div id="todayStats" class="page-stats-content">Lade...</div>
</div>
<div class="page-stat-card">
<h3>Diese Woche</h3>
<div id="weekStats" class="page-stats-content">Lade...</div>
</div>
<div class="page-stat-card">
<h3>Dieser Monat</h3>
<div id="monthStats" class="page-stats-content">Lade...</div>
</div>
<div class="page-stat-card">
<h3>Gesamt</h3>
<div id="totalStats" class="page-stats-content">Lade...</div>
</div>
</div>
<!-- Verlinkungs-Statistiken -->
<div class="link-stats-section">
<h3>🔗 Account-Verknüpfungen</h3>
<div class="link-stats-grid">
<div class="link-stat-card">
<div class="link-stat-number" id="totalPlayersCount">-</div>
<div class="link-stat-label">Gesamt Spieler</div>
</div>
<div class="link-stat-card">
<div class="link-stat-number" id="linkedPlayersCount">-</div>
<div class="link-stat-label">Verknüpfte Spieler</div>
</div>
<div class="link-stat-card">
<div class="link-stat-number" id="linkPercentage">-</div>
<div class="link-stat-label">Verknüpfungsrate</div>
</div>
</div>
</div>
</div>
<!-- Dashboard Cards -->
<div class="dashboard-grid">
<!-- Benutzer-Verwaltung -->
<div class="card">
<h3><span class="icon">👥</span> Benutzer-Verwaltung</h3>
<p>Verwalte Supabase-Benutzer und deren RFID-Verknüpfungen</p>
<button class="btn" onclick="showUserManagement()">Benutzer anzeigen</button>
</div>
<!-- Spieler-Verwaltung -->
<div class="card">
<h3><span class="icon">🏃</span> Spieler-Verwaltung</h3>
<p>Verwalte Spieler und deren RFID-UIDs</p>
<button class="btn" onclick="showPlayerManagement()">Spieler anzeigen</button>
</div>
<!-- Blacklist-Verwaltung -->
<div class="card">
<h3><span class="icon">🚫</span> Blacklist-Verwaltung</h3>
<p>Verwalte verbotene Namen und Begriffe</p>
<button class="btn" onclick="showBlacklistManagement()">Blacklist verwalten</button>
</div>
<!-- Läufe-Verwaltung -->
<div class="card">
<h3><span class="icon">⏱️</span> Läufe-Verwaltung</h3>
<p>Zeige und lösche Läufe</p>
<button class="btn" onclick="showRunManagement()">Läufe anzeigen</button>
</div>
<!-- Standort-Verwaltung -->
<div class="card">
<h3><span class="icon">📍</span> Standort-Verwaltung</h3>
<p>Verwalte Standorte und deren Koordinaten</p>
<button class="btn" onclick="showLocationManagement()">Standorte anzeigen</button>
</div>
<!-- Admin-Benutzer -->
<div class="card">
<h3><span class="icon">🔐</span> Admin-Benutzer</h3>
<p>Verwalte Admin-Benutzer und Zugriffsrechte</p>
<button class="btn" onclick="showAdminUserManagement()">Admins anzeigen</button>
</div>
<!-- Achievement-Verwaltung -->
<div class="card">
<h3><span class="icon">🏆</span> Achievement-Verwaltung</h3>
<p>Verwalte Achievements und Spieler-Achievements</p>
<button class="btn" onclick="showAchievementManagement()">Achievements verwalten</button>
</div>
<!-- System-Info -->
<div class="card">
<h3><span class="icon">📊</span> System-Informationen</h3>
<p>Server-Status und Systemdaten</p>
<button class="btn" onclick="showSystemInfo()">Info anzeigen</button>
</div>
</div>
<!-- Daten-Anzeige Bereich -->
<div id="dataSection" style="display: none;">
<div class="card">
<h3 id="dataTitle">Daten</h3>
<div class="search-container">
<input type="text" id="searchInput" class="search-input" placeholder="Suchen...">
<button class="btn" onclick="refreshData()">🔄 Aktualisieren</button>
<button class="btn btn-success" onclick="showAddModal()"> Hinzufügen</button>
</div>
<div id="dataContent">
<div class="loading">Lade Daten...</div>
</div>
</div>
</div>
</div>
<!-- Modals -->
<div id="addModal" class="modal">
<div class="modal-content">
<span class="close">&times;</span>
<h3 id="modalTitle">Element hinzufügen</h3>
<div class="message" id="modalMessage"></div>
<form id="addForm">
<div id="formFields"></div>
<button type="submit" class="btn">Speichern</button>
<button type="button" class="btn btn-danger" onclick="closeModal()">Abbrechen</button>
</form>
</div>
</div>
<div id="confirmModal" class="modal">
<div class="modal-content">
<span class="close">&times;</span>
<h3>Bestätigung</h3>
<p id="confirmMessage">Sind Sie sicher?</p>
<button id="confirmYes" class="btn btn-danger">Ja, löschen</button>
<button id="confirmNo" class="btn">Abbrechen</button>
</div>
</div>
<!-- Blacklist Management Modal -->
<div id="blacklistModal" class="modal">
<div class="modal-content" style="max-width: 800px;">
<span class="close" onclick="closeModal('blacklistModal')">&times;</span>
<h3>🚫 Blacklist-Verwaltung</h3>
<div class="message" id="blacklistMessage"></div>
<!-- Test Name Section -->
<div style="border: 1px solid #ddd; padding: 1rem; margin-bottom: 1rem; border-radius: 5px;">
<h4>Name testen</h4>
<div style="display: flex; gap: 1rem; margin-bottom: 1rem;">
<input type="text" id="testFirstname" placeholder="Vorname" style="flex: 1; padding: 0.5rem;">
<input type="text" id="testLastname" placeholder="Nachname" style="flex: 1; padding: 0.5rem;">
<button class="btn" onclick="testNameAgainstBlacklist()">Testen</button>
<button class="btn" onclick="testLevenshteinDetailed()" style="background: #9c27b0; color: white;">Levenshtein Details</button>
</div>
<div id="testResult" style="padding: 0.5rem; border-radius: 3px; display: none;"></div>
</div>
<!-- Add New Entry Section -->
<div style="border: 1px solid #ddd; padding: 1rem; margin-bottom: 1rem; border-radius: 5px;">
<h4>Neuen Eintrag hinzufügen</h4>
<div style="display: flex; gap: 1rem; margin-bottom: 1rem;">
<input type="text" id="newTerm" placeholder="Begriff" style="flex: 1; padding: 0.5rem;">
<select id="newCategory" style="flex: 1; padding: 0.5rem;">
<option value="historical">Historisch belastet</option>
<option value="offensive">Beleidigend/anstößig</option>
<option value="titles">Titel/Berufsbezeichnung</option>
<option value="brands">Markenname</option>
<option value="inappropriate">Unpassend</option>
<option value="racial">Rassistisch/ethnisch</option>
<option value="religious">Religiös beleidigend</option>
<option value="disability">Behinderungsbezogen</option>
<option value="leetspeak">Verschleiert</option>
<option value="cyberbullying">Cyberbullying</option>
<option value="drugs">Drogenbezogen</option>
<option value="violence">Gewalt/Bedrohung</option>
</select>
<button class="btn btn-success" onclick="addToBlacklist()">Hinzufügen</button>
</div>
</div>
<!-- Blacklist Content -->
<div id="blacklistContent">
<div class="loading">Lade Blacklist...</div>
</div>
</div>
</div>
<!-- Footer -->
<footer class="footer">
<div class="footer-content">
<div class="footer-links">
<a href="/impressum.html" class="footer-link">Impressum</a>
<a href="/datenschutz.html" class="footer-link">Datenschutz</a>
<button id="cookie-settings-footer" class="footer-link cookie-settings-btn">Cookie-Einstellungen</button>
</div>
<div class="footer-text">
<p>&copy; 2024 NinjaCross. Alle Rechte vorbehalten.</p>
</div>
</div>
</footer>
<!-- Application JavaScript -->
<script src="/js/cookie-consent.js"></script>
<script src="/js/admin-dashboard.js"></script>
</body>
</html>

55
public/adminlogin.html Normal file
View File

@@ -0,0 +1,55 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login - Lizenzgenerator</title>
<link rel="icon" type="image/x-icon" href="/pictures/favicon.ico">
<link rel="stylesheet" href="/css/adminlogin.css">
</head>
<body>
<div class="login-container">
<h1>🔐 Anmeldung</h1>
<form id="loginForm" onsubmit="handleLogin(event)">
<div class="form-group">
<label for="username">Benutzername</label>
<input type="text" id="username" name="username" placeholder="Ihr Benutzername" required>
</div>
<div class="form-group">
<label for="password">Passwort</label>
<input type="password" id="password" name="password" placeholder="Ihr Passwort" required>
</div>
<button type="submit" class="login-btn" id="loginBtn">
<span id="btn-text">Anmelden</span>
</button>
</form>
<div id="error" class="error"></div>
<div id="success" class="success"></div>
<div class="info-text">
Melden Sie sich an, um auf den Lizenzgenerator zuzugreifen.
</div>
</div>
<!-- Footer -->
<footer class="footer">
<div class="footer-content">
<div class="footer-links">
<a href="/impressum.html" class="footer-link">Impressum</a>
<a href="/datenschutz.html" class="footer-link">Datenschutz</a>
<button id="cookie-settings-footer" class="footer-link cookie-settings-btn">Cookie-Einstellungen</button>
</div>
<div class="footer-text">
<p>&copy; 2024 NinjaCross. Alle Rechte vorbehalten.</p>
</div>
</div>
</footer>
<script src="/js/cookie-consent.js"></script>
<script src="/js/adminlogin.js"></script>
</body>
</html>

244
public/agb.html Normal file
View File

@@ -0,0 +1,244 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Allgemeine Geschäftsbedingungen | NinjaCross</title>
<link rel="stylesheet" href="css/dashboard.css">
<style>
.agb-container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
background: #1e293b;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
margin-top: 20px;
color: #e2e8f0;
}
.agb-header {
text-align: center;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 2px solid #334155;
}
.agb-header h1 {
color: #00d4ff;
margin-bottom: 10px;
}
.agb-header .subtitle {
color: #94a3b8;
font-size: 16px;
}
.agb-content {
line-height: 1.6;
color: #e2e8f0;
}
.agb-section {
margin-bottom: 25px;
}
.agb-section h2 {
color: #00d4ff;
font-size: 20px;
margin-bottom: 15px;
padding-bottom: 5px;
border-bottom: 1px solid #334155;
}
.agb-section h3 {
color: #e2e8f0;
font-size: 16px;
margin-bottom: 10px;
margin-top: 20px;
}
.agb-section p {
margin-bottom: 12px;
}
.agb-section ul {
margin-left: 20px;
margin-bottom: 15px;
}
.agb-section li {
margin-bottom: 8px;
}
.highlight-box {
background: #0f172a;
border-left: 4px solid #00d4ff;
padding: 15px;
margin: 20px 0;
border-radius: 0 5px 5px 0;
}
.warning-box {
background: #451a03;
border-left: 4px solid #fbbf24;
padding: 15px;
margin: 20px 0;
border-radius: 0 5px 5px 0;
}
.back-button {
display: inline-block;
background: #00d4ff;
color: #0f172a;
padding: 12px 24px;
text-decoration: none;
border-radius: 5px;
margin-top: 30px;
transition: background-color 0.3s;
font-weight: bold;
}
.back-button:hover {
background: #0891b2;
}
.last-updated {
text-align: center;
color: #94a3b8;
font-style: italic;
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #334155;
}
</style>
</head>
<body style="background: #0f172a; min-height: 100vh; padding: 20px;">
<div class="agb-container">
<div class="agb-header">
<h1>Allgemeine Geschäftsbedingungen</h1>
<p class="subtitle">NinjaCross Parkour System</p>
</div>
<div class="agb-content">
<div class="highlight-box">
<strong>Wichtig:</strong> Durch die Nutzung des NinjaCross Systems stimmen Sie zu, dass Ihre Daten
(Name, Zeiten, Standorte) im öffentlichen Leaderboard angezeigt werden können.
</div>
<div class="agb-section">
<h2>1. Geltungsbereich</h2>
<p>Diese Allgemeinen Geschäftsbedingungen (AGB) gelten für die Nutzung des NinjaCross Parkour Systems
im Schwimmbad. Mit der Registrierung und Nutzung des Systems erkennen Sie diese AGB als verbindlich an.</p>
</div>
<div class="agb-section">
<h2>2. Datenverarbeitung und Datenschutz</h2>
<h3>2.1 Erhebung von Daten</h3>
<p>Wir erheben folgende personenbezogene Daten:</p>
<ul>
<li>Vor- und Nachname</li>
<li>Geburtsdatum (zur Altersberechnung)</li>
<li>RFID-Kartennummer</li>
<li>Laufzeiten und Standortdaten</li>
<li>Zeitstempel der Aktivitäten</li>
</ul>
<h3>2.2 Verwendung der Daten</h3>
<p>Ihre Daten werden für folgende Zwecke verwendet:</p>
<ul>
<li><strong>Leaderboard-Anzeige:</strong> Name, Zeiten und Standorte werden im öffentlichen Leaderboard angezeigt</li>
<li><strong>Leistungsauswertung:</strong> Erfassung und Bewertung Ihrer Parkour-Zeiten</li>
<li><strong>System-Funktionalität:</strong> Zuordnung von Zeiten zu Ihrem Profil über RFID-Karten</li>
<li><strong>Statistiken:</strong> Anonymisierte Auswertungen für Systemverbesserungen</li>
</ul>
<div class="warning-box">
<strong>Wichtiger Hinweis:</strong> Durch die Annahme dieser AGB stimmen Sie ausdrücklich zu,
dass Ihr Name und Ihre Laufzeiten im öffentlichen Leaderboard sichtbar sind.
</div>
</div>
<div class="agb-section">
<h2>3. Leaderboard und Öffentlichkeit</h2>
<p>Das NinjaCross System verfügt über ein öffentlich zugängliches Leaderboard, das folgende Informationen anzeigt:</p>
<ul>
<li>Vollständiger Name der Teilnehmer</li>
<li>Erreichte Laufzeiten</li>
<li>Standort der Aktivität</li>
<li>Datum und Uhrzeit der Aktivität</li>
</ul>
<p><strong>Durch die Nutzung des Systems erklären Sie sich damit einverstanden, dass diese Daten öffentlich angezeigt werden.</strong></p>
</div>
<div class="agb-section">
<h2>4. Ihre Rechte</h2>
<h3>4.1 Recht auf Auskunft</h3>
<p>Sie haben das Recht, Auskunft über die zu Ihrer Person gespeicherten Daten zu verlangen.</p>
<h3>4.2 Recht auf Löschung</h3>
<p>Sie können jederzeit die Löschung Ihrer Daten und Ihres Profils beantragen.</p>
<h3>4.3 Recht auf Widerspruch</h3>
<p>Sie können der Verarbeitung Ihrer Daten für das Leaderboard widersprechen.
In diesem Fall werden Ihre Daten aus dem öffentlichen Leaderboard entfernt,
aber weiterhin für die Systemfunktionalität verwendet.</p>
</div>
<div class="agb-section">
<h2>5. Haftung und Verantwortung</h2>
<p>Die Teilnahme am NinjaCross System erfolgt auf eigene Gefahr. Wir haften nicht für:</p>
<ul>
<li>Verletzungen während der Nutzung der Parkour-Anlage</li>
<li>Verlust oder Diebstahl der RFID-Karte</li>
<li>Technische Ausfälle des Systems</li>
</ul>
</div>
<div class="agb-section">
<h2>6. Systemregeln</h2>
<p>Bei der Nutzung des Systems sind folgende Regeln zu beachten:</p>
<ul>
<li>Keine Manipulation der Zeiterfassung</li>
<li>Respektvoller Umgang mit anderen Teilnehmern</li>
<li>Beachtung der Sicherheitshinweise der Anlage</li>
<li>Keine Verwendung falscher Identitäten</li>
</ul>
</div>
<div class="agb-section">
<h2>7. Änderungen der AGB</h2>
<p>Wir behalten uns vor, diese AGB zu ändern. Wesentliche Änderungen werden Ihnen
mitgeteilt und erfordern Ihre erneute Zustimmung.</p>
</div>
<div class="agb-section">
<h2>8. Kontakt</h2>
<p>Bei Fragen zu diesen AGB oder zum Datenschutz wenden Sie sich an:</p>
<p>
<strong>NinjaCross Team</strong><br>
Schwimmbad Ulm<br>
E-Mail: info@ninjacross.de<br>
Telefon: 0731-123456
</p>
</div>
</div>
<div style="text-align: center;">
<a href="javascript:history.back()" class="back-button">Zurück</a>
</div>
<div class="last-updated">
<p>Stand: September 2024</p>
<p>Diese AGB sind Teil der Registrierung und gelten ab dem Zeitpunkt der Zustimmung.</p>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,753 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', sans-serif;
background: #0a0a0f;
color: #ffffff;
min-height: 100vh;
background-image:
radial-gradient(circle at 20% 80%, #1a1a2e 0%, transparent 50%),
radial-gradient(circle at 80% 20%, #16213e 0%, transparent 50%),
radial-gradient(circle at 40% 40%, #0f3460 0%, transparent 50%);
}
.header {
background: rgba(26, 26, 46, 0.95);
padding: 20px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.header h1 {
color: #ffffff;
font-size: 2em;
}
.user-info {
display: flex;
align-items: center;
gap: 15px;
}
.access-badge {
background: #4CAF50;
color: white;
padding: 5px 10px;
border-radius: 15px;
font-size: 0.8em;
font-weight: bold;
}
.access-badge.level-1 {
background: #ff9800;
}
.access-badge.level-2 {
background: #4CAF50;
}
.btn {
background: #2196F3;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
text-decoration: none;
display: inline-block;
font-size: 0.9em;
transition: all 0.3s ease;
}
.btn:hover {
background: #1976D2;
transform: translateY(-2px);
}
.btn-danger {
background: #f44336;
}
.btn-danger:hover {
background: #d32f2f;
}
.btn-warning {
background: #ff9800;
}
.btn-warning:hover {
background: #f57c00;
}
.btn-success {
background: #4CAF50;
}
.btn-success:hover {
background: #388E3C;
}
.container {
max-width: 1200px;
margin: 30px auto;
padding: 0 20px;
}
.dashboard-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.card {
background: rgba(26, 26, 46, 0.95);
border-radius: 10px;
padding: 20px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
transition: transform 0.3s ease;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.card:hover {
transform: translateY(-5px);
}
.card h3 {
color: #00d4ff;
margin-bottom: 15px;
display: flex;
align-items: center;
gap: 10px;
}
.icon {
font-size: 1.5em;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-bottom: 30px;
}
.stat-card {
background: rgba(26, 26, 46, 0.95);
padding: 20px;
border-radius: 10px;
text-align: center;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.stat-number {
font-size: 2.5em;
font-weight: bold;
color: #00d4ff;
margin-bottom: 5px;
}
.stat-label {
color: #8892b0;
font-size: 0.9em;
}
.data-table {
width: 100%;
border-collapse: collapse;
background: rgba(26, 26, 46, 0.95);
border-radius: 10px;
overflow: hidden;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.data-table th,
.data-table td {
padding: 12px;
text-align: left;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
color: #ffffff;
}
.data-table th {
background: rgba(255, 255, 255, 0.1);
font-weight: bold;
color: #ffffff;
}
.data-table tr:hover {
background: rgba(255, 255, 255, 0.1);
}
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
}
.modal-content {
background-color: rgba(26, 26, 46, 0.95);
margin: 5% auto;
padding: 20px;
border-radius: 10px;
width: 90%;
max-width: 500px;
max-height: 85vh;
overflow-y: auto;
position: relative;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.close {
color: #8892b0;
float: right;
font-size: 28px;
font-weight: bold;
cursor: pointer;
position: absolute;
right: 15px;
top: 10px;
}
.close:hover {
color: #ffffff;
}
/* Blacklist Modal specific styles */
#blacklistModal .modal-content {
max-width: 800px;
max-height: 90vh;
overflow-y: auto;
}
/* Smooth scrolling for modal content */
.modal-content {
scroll-behavior: smooth;
}
/* Custom scrollbar for modal content */
.modal-content::-webkit-scrollbar {
width: 8px;
}
.modal-content::-webkit-scrollbar-track {
background: rgba(30, 41, 59, 0.3);
border-radius: 4px;
}
.modal-content::-webkit-scrollbar-thumb {
background: rgba(100, 116, 139, 0.6);
border-radius: 4px;
}
.modal-content::-webkit-scrollbar-thumb:hover {
background: rgba(100, 116, 139, 0.8);
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
color: #333;
}
.form-group input,
.form-group select {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 1em;
}
.message {
padding: 10px;
border-radius: 5px;
margin-bottom: 15px;
display: none;
}
.message.success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.message.error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.loading {
text-align: center;
padding: 20px;
color: #666;
}
.no-data {
text-align: center;
padding: 40px;
color: #999;
font-style: italic;
}
.action-buttons {
display: flex;
gap: 5px;
}
.btn-small {
padding: 5px 10px;
font-size: 0.8em;
}
.table-container {
max-height: 400px;
overflow-y: auto;
margin-top: 15px;
}
.search-container {
margin-bottom: 20px;
display: flex;
gap: 10px;
align-items: center;
}
.search-input {
flex: 1;
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
}
@media (max-width: 768px) {
.header {
flex-direction: column;
gap: 15px;
text-align: center;
}
.dashboard-grid {
grid-template-columns: 1fr;
}
.stats-grid {
grid-template-columns: repeat(2, 1fr);
}
.search-container {
flex-direction: column;
align-items: stretch;
}
.action-buttons {
flex-direction: column;
}
.modal-content {
margin: 5% auto;
width: 95%;
}
.page-stats-grid {
grid-template-columns: 1fr;
}
.link-stats-grid {
grid-template-columns: repeat(2, 1fr);
}
}
/* Page Statistics Styles */
.page-stats-section {
background: rgba(26, 26, 46, 0.95);
border-radius: 10px;
padding: 25px;
margin: 20px 0;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.page-stats-section h2 {
color: #ffffff;
margin-bottom: 20px;
font-size: 1.5em;
}
.page-stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.page-stat-card {
background: rgba(26, 26, 46, 0.95);
border-radius: 8px;
padding: 20px;
border-left: 4px solid #00d4ff;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.page-stat-card h3 {
color: #ffffff;
margin-bottom: 15px;
font-size: 1.2em;
}
.page-stats-content {
font-size: 0.9em;
line-height: 1.6;
background: rgba(26, 26, 46, 0.95);
border-radius: 8px;
padding: 15px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.page-stats-content .page-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 15px;
margin: 8px 0;
background: rgba(255, 255, 255, 0.05);
border-radius: 6px;
border: 1px solid rgba(255, 255, 255, 0.1);
transition: background-color 0.3s ease;
}
.page-stats-content .page-item:last-child {
border-bottom: none;
}
.page-stats-content .page-item:hover {
background: rgba(255, 255, 255, 0.1);
}
.page-stats-content .page-name {
font-weight: 500;
color: #ffffff;
}
.page-stats-content .page-count {
background: #00d4ff;
color: #00d4ff;
padding: 4px 8px;
border-radius: 12px;
font-size: 0.8em;
font-weight: bold;
}
/* Link Statistics Styles */
.link-stats-section {
background: #1a1a2f;
border-radius: 8px;
padding: 20px;
margin-top: 20px;
}
.link-stats-section h3 {
color: #00d4ff;
margin-bottom: 15px;
font-size: 1.3em;
}
.link-stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 20px;
}
.link-stat-card {
text-align: center;
background: rgba(26, 26, 46, 0.95);
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.link-stat-number {
font-size: 2em;
font-weight: bold;
color: #00d4ff;
margin-bottom: 8px;
}
.link-stat-label {
color: #8892b0;
font-size: 0.9em;
font-weight: 500;
}
@media (max-width: 480px) {
.stats-grid {
grid-template-columns: 1fr;
}
.container {
padding: 0 10px;
}
.card {
padding: 15px;
}
}
/* Footer Styles */
.footer {
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
border-top: 1px solid #2a2a3e;
margin-top: 3rem;
padding: 2rem 0;
}
.footer-content {
max-width: 1200px;
margin: 0 auto;
padding: 0 2rem;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 1rem;
}
.footer-links {
display: flex;
gap: 2rem;
align-items: center;
}
.footer-link {
color: #8892b0;
text-decoration: none;
font-size: 0.9rem;
transition: color 0.3s ease;
background: none;
border: none;
cursor: pointer;
font-family: inherit;
}
.footer-link:hover {
color: #00d4ff;
}
.cookie-settings-btn {
background: none !important;
border: none !important;
padding: 0 !important;
font-size: 0.9rem !important;
}
.footer-text {
color: #6b7280;
font-size: 0.85rem;
}
.footer-text p {
margin: 0;
}
@media (max-width: 768px) {
.footer-content {
flex-direction: column;
text-align: center;
gap: 1.5rem;
}
.footer-links {
flex-wrap: wrap;
justify-content: center;
gap: 1.5rem;
}
}
/* Achievement Management Styles */
.achievement-controls {
display: flex;
gap: 10px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.status-badge {
padding: 4px 8px;
border-radius: 12px;
font-size: 0.8em;
font-weight: 500;
}
.status-badge.active {
background: #4ade80;
color: #000;
}
.status-badge.inactive {
background: #6b7280;
color: #fff;
}
.progress-bar {
position: relative;
background: rgba(255, 255, 255, 0.1);
border-radius: 10px;
height: 20px;
overflow: hidden;
min-width: 100px;
}
.progress-fill {
background: linear-gradient(90deg, #4ade80, #22c55e);
height: 100%;
transition: width 0.3s ease;
}
.progress-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 0.8em;
font-weight: 500;
color: #fff;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
}
.player-achievements {
max-height: 70vh;
overflow-y: auto;
}
.achievement-stats {
display: flex;
gap: 20px;
margin-bottom: 20px;
padding: 15px;
background: rgba(255, 255, 255, 0.05);
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.stat-item {
font-size: 0.9em;
}
.achievements-list {
display: grid;
gap: 15px;
}
.achievement-item {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
padding: 15px;
transition: all 0.3s ease;
}
.achievement-item.completed {
border-color: #4ade80;
background: rgba(74, 222, 128, 0.1);
}
.achievement-item.not-completed {
border-color: #6b7280;
background: rgba(107, 114, 128, 0.1);
}
.achievement-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
}
.achievement-icon {
font-size: 1.5em;
}
.achievement-name {
font-weight: 600;
flex: 1;
}
.achievement-status {
font-size: 1.2em;
}
.achievement-details p {
margin-bottom: 10px;
color: rgba(255, 255, 255, 0.8);
line-height: 1.4;
}
.achievement-meta {
display: flex;
gap: 15px;
margin-bottom: 10px;
flex-wrap: wrap;
}
.achievement-meta span {
font-size: 0.8em;
color: rgba(255, 255, 255, 0.6);
background: rgba(255, 255, 255, 0.1);
padding: 2px 8px;
border-radius: 4px;
}
.achievement-actions {
display: flex;
gap: 10px;
justify-content: flex-end;
}
.achievement-actions .btn {
padding: 6px 12px;
font-size: 0.8em;
}
@media (max-width: 768px) {
.achievement-controls {
flex-direction: column;
}
.achievement-stats {
flex-direction: column;
gap: 10px;
}
.achievement-meta {
flex-direction: column;
gap: 5px;
}
.achievement-actions {
justify-content: center;
}
}

294
public/css/adminlogin.css Normal file
View File

@@ -0,0 +1,294 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
height: 100%;
margin: 0;
padding: 0;
}
body {
font-family: 'Inter', sans-serif;
background: #0a0a0f;
color: #ffffff;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
background-image:
radial-gradient(circle at 20% 80%, #1a1a2e 0%, transparent 50%),
radial-gradient(circle at 80% 20%, #16213e 0%, transparent 50%),
radial-gradient(circle at 40% 40%, #0f3460 0%, transparent 50%);
}
.login-container {
background: rgba(26, 26, 46, 0.95);
backdrop-filter: blur(10px);
border-radius: 20px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
padding: 40px;
max-width: 400px;
width: 100%;
position: relative;
overflow: hidden;
flex-grow: 1;
display: flex;
flex-direction: column;
justify-content: center;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.login-container::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, #667eea, #764ba2, #f093fb, #f5576c);
background-size: 300% 100%;
animation: gradientShift 3s ease infinite;
}
@keyframes gradientShift {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
h1 {
text-align: center;
color: #ffffff;
margin-bottom: 30px;
font-size: 2em;
font-weight: 300;
letter-spacing: -1px;
}
.form-group {
margin-bottom: 25px;
position: relative;
}
label {
display: block;
margin-bottom: 8px;
color: #555;
font-weight: 500;
font-size: 0.95em;
}
input {
width: 100%;
padding: 15px 20px;
border: 2px solid #e0e0e0;
border-radius: 12px;
font-size: 1em;
transition: all 0.3s ease;
background: #fafafa;
font-family: inherit;
}
input:focus {
outline: none;
border-color: #667eea;
background: white;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
transform: translateY(-2px);
}
input:hover {
border-color: #ccc;
}
.login-btn {
width: 100%;
padding: 18px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 12px;
font-size: 1.1em;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
margin-top: 10px;
position: relative;
overflow: hidden;
}
.login-btn:hover {
transform: translateY(-2px);
box-shadow: 0 10px 25px rgba(102, 126, 234, 0.3);
}
.login-btn:active {
transform: translateY(0);
}
.login-btn:disabled {
opacity: 0.7;
cursor: not-allowed;
transform: none;
}
.error {
background: #ffebee;
color: #c62828;
padding: 15px;
border-radius: 8px;
margin-top: 15px;
border-left: 4px solid #f44336;
font-size: 0.9em;
opacity: 0;
transform: translateY(-10px);
transition: all 0.3s ease;
}
.error.show {
opacity: 1;
transform: translateY(0);
}
.success {
background: #e8f5e8;
color: #2e7d32;
padding: 15px;
border-radius: 8px;
margin-top: 15px;
border-left: 4px solid #4caf50;
font-size: 0.9em;
opacity: 0;
transform: translateY(-10px);
transition: all 0.3s ease;
}
.success.show {
opacity: 1;
transform: translateY(0);
}
.loading {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid rgba(255, 255, 255, .3);
border-radius: 50%;
border-top-color: #fff;
animation: spin 1s ease-in-out infinite;
margin-right: 10px;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.info-text {
text-align: center;
color: #666;
font-size: 0.85em;
margin-top: 20px;
line-height: 1.5;
}
@media (max-width: 480px) {
.login-container {
padding: 30px 20px;
margin: 10px;
}
h1 {
font-size: 1.6em;
}
}
/* Footer Styles */
.footer {
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
border-top: 1px solid #2a2a3e;
margin-top: auto;
padding: 2rem 0;
position: relative;
bottom: 0;
width: 100%;
}
.footer-content {
max-width: 1200px;
margin: 0 auto;
padding: 0 2rem;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 1rem;
}
.footer-links {
display: flex;
gap: 2rem;
align-items: center;
}
.footer-link {
color: #8892b0;
text-decoration: none;
font-size: 0.9rem;
transition: color 0.3s ease;
background: none;
border: none;
cursor: pointer;
font-family: inherit;
}
.footer-link:hover {
color: #00d4ff;
}
.cookie-settings-btn {
background: none !important;
border: none !important;
padding: 0 !important;
font-size: 0.9rem !important;
}
.footer-text {
color: #6b7280;
font-size: 0.85rem;
}
.footer-text p {
margin: 0;
}
@media (max-width: 768px) {
.footer-content {
flex-direction: column;
text-align: center;
gap: 1.5rem;
}
.footer-links {
flex-wrap: wrap;
justify-content: center;
gap: 1.5rem;
}
}

1981
public/css/dashboard.css Normal file

File diff suppressed because it is too large Load Diff

500
public/css/generator.css Normal file
View File

@@ -0,0 +1,500 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
height: 100%;
margin: 0;
padding: 0;
}
body {
font-family: 'Inter', sans-serif;
background: #0a0a0f;
color: #ffffff;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
padding: 20px;
background-image:
radial-gradient(circle at 20% 80%, #1a1a2e 0%, transparent 50%),
radial-gradient(circle at 80% 20%, #16213e 0%, transparent 50%),
radial-gradient(circle at 40% 40%, #0f3460 0%, transparent 50%);
}
.container {
background: rgba(26, 26, 46, 0.95);
backdrop-filter: blur(10px);
border-radius: 20px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
padding: 40px;
max-width: 700px;
width: 100%;
position: relative;
overflow: visible;
margin-bottom: 20px;
display: flex;
flex-direction: column;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.container::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, #667eea, #764ba2, #f093fb, #f5576c);
background-size: 300% 100%;
animation: gradientShift 3s ease infinite;
}
@keyframes gradientShift {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
h1 {
text-align: center;
color: #ffffff;
margin-bottom: 30px;
font-size: 2.2em;
font-weight: 300;
letter-spacing: -1px;
}
.form-group {
margin-bottom: 25px;
position: relative;
}
label {
display: block;
margin-bottom: 8px;
color: #555;
font-weight: 500;
font-size: 0.95em;
}
input,
textarea {
width: 100%;
padding: 15px 20px;
border: 2px solid #e0e0e0;
border-radius: 12px;
font-size: 1em;
transition: all 0.3s ease;
background: #fafafa;
font-family: inherit;
}
textarea {
resize: vertical;
min-height: 80px;
}
input:focus,
textarea:focus {
outline: none;
border-color: #667eea;
background: white;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
transform: translateY(-2px);
}
input:hover,
textarea:hover {
border-color: #ccc;
}
.db-config {
background: linear-gradient(135deg, #f8f9ff 0%, #e8f2ff 100%);
border: 2px solid #e3f2fd;
border-radius: 12px;
padding: 20px;
margin-bottom: 25px;
opacity: 0;
transform: translateY(-20px);
transition: all 0.4s ease;
max-height: 0;
overflow: hidden;
}
.db-config.show {
opacity: 1;
transform: translateY(0);
max-height: 1000px;
}
.db-config h3 {
color: #1565c0;
margin-bottom: 15px;
font-size: 1.1em;
}
.tier-notice {
background: linear-gradient(135deg, #fff3e0 0%, #ffe0b2 100%);
border: 2px solid #ffcc02;
border-radius: 8px;
padding: 12px;
margin-top: 10px;
font-size: 0.9em;
color: #f57c00;
text-align: center;
}
.generate-btn {
width: 100%;
padding: 18px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 12px;
font-size: 1.1em;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
margin-top: 10px;
position: relative;
overflow: hidden;
}
.generate-btn:hover {
transform: translateY(-2px);
box-shadow: 0 10px 25px rgba(102, 126, 234, 0.3);
}
.generate-btn:active {
transform: translateY(0);
}
.result-section {
margin-top: 30px;
opacity: 0;
transform: translateY(20px);
transition: all 0.4s ease;
}
.result-section.show {
opacity: 1;
transform: translateY(0);
}
.license-output {
background: linear-gradient(135deg, #f8f9ff 0%, #e8f2ff 100%);
border: 2px solid #e3f2fd;
border-radius: 12px;
padding: 20px;
font-family: 'Courier New', monospace;
font-size: 0.9em;
word-break: break-all;
color: #1565c0;
position: relative;
}
.license-label {
font-family: 'Segoe UI', sans-serif;
font-size: 0.85em;
color: #666;
margin-bottom: 8px;
font-weight: 500;
}
.copy-btn {
width: 100%;
padding: 12px;
background: #4caf50;
color: white;
border: none;
border-radius: 8px;
font-size: 0.95em;
font-weight: 500;
cursor: pointer;
margin-top: 15px;
transition: all 0.3s ease;
}
.copy-btn:hover {
background: #45a049;
transform: translateY(-1px);
}
.copy-btn.copied {
background: #2196f3;
animation: pulse 0.6s;
}
@keyframes pulse {
0% {
transform: scale(1);
}
50% {
transform: scale(1.05);
}
100% {
transform: scale(1);
}
}
.success {
background: #e8f5e8;
color: #2e7d32;
padding: 15px;
border-radius: 8px;
margin-top: 15px;
border-left: 4px solid #4caf50;
font-size: 0.9em;
opacity: 0;
transform: translateY(-10px);
transition: all 0.3s ease;
}
.success.show {
opacity: 1;
transform: translateY(0);
}
.error {
background: #ffebee;
color: #c62828;
padding: 15px;
border-radius: 8px;
margin-top: 15px;
border-left: 4px solid #f44336;
font-size: 0.9em;
opacity: 0;
transform: translateY(-10px);
transition: all 0.3s ease;
}
.error.show {
opacity: 1;
transform: translateY(0);
}
.info-text {
text-align: center;
color: #666;
font-size: 0.85em;
margin-top: 20px;
line-height: 1.5;
}
@media (max-width: 480px) {
.container {
padding: 30px 20px;
margin: 10px;
}
h1 {
font-size: 1.8em;
}
}
.loading {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid rgba(255, 255, 255, .3);
border-radius: 50%;
border-top-color: #fff;
animation: spin 1s ease-in-out infinite;
margin-right: 10px;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* Standortsuche Styles */
.coordinates-display {
animation: slideDown 0.4s ease;
}
.map-container {
animation: slideDown 0.4s ease;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
#mapFrame iframe {
border-radius: 10px;
}
.coordinates-display h4 {
display: flex;
align-items: center;
gap: 8px;
}
.coordinates-display strong {
color: #2e7d32;
}
/* Verbesserte Standortsuche Layouts */
.location-search-container {
display: flex;
gap: 10px;
align-items: stretch;
}
.location-search-container input {
flex: 1;
min-width: 0;
}
.location-search-container button {
white-space: nowrap;
min-width: 120px;
}
/* Responsive Design für Standortsuche */
@media (max-width: 600px) {
.location-search-container {
flex-direction: column;
gap: 15px;
}
.location-search-container button {
min-width: auto;
width: 100%;
}
.coordinates-display .flex-container {
flex-direction: column;
gap: 10px;
}
}
/* Interaktive Karte Styles */
#interactiveMap {
position: relative;
}
#map {
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.leaflet-container {
border-radius: 10px;
}
.leaflet-control-zoom {
border: none;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
.leaflet-control-zoom a {
background: white;
color: #333;
border: 1px solid #ddd;
}
.leaflet-control-zoom a:hover {
background: #f8f9fa;
}
/* Footer Styles */
.footer {
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
border-top: 1px solid #2a2a3e;
margin-top: auto;
padding: 2rem 0;
position: relative;
bottom: 0;
width: 100%;
}
.footer-content {
max-width: 1200px;
margin: 0 auto;
padding: 0 2rem;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 1rem;
}
.footer-links {
display: flex;
gap: 2rem;
align-items: center;
}
.footer-link {
color: #8892b0;
text-decoration: none;
font-size: 0.9rem;
transition: color 0.3s ease;
background: none;
border: none;
cursor: pointer;
font-family: inherit;
}
.footer-link:hover {
color: #00d4ff;
}
.cookie-settings-btn {
background: none !important;
border: none !important;
padding: 0 !important;
font-size: 0.9rem !important;
}
.footer-text {
color: #6b7280;
font-size: 0.85rem;
}
.footer-text p {
margin: 0;
}
@media (max-width: 768px) {
.footer-content {
flex-direction: column;
text-align: center;
gap: 1.5rem;
}
.footer-links {
flex-wrap: wrap;
justify-content: center;
gap: 1.5rem;
}
}

1289
public/css/index.css Normal file

File diff suppressed because it is too large Load Diff

486
public/css/login.css Normal file
View File

@@ -0,0 +1,486 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', sans-serif;
background: #0a0a0f;
color: #ffffff;
min-height: 100vh;
background-image:
radial-gradient(circle at 20% 80%, #1a1a2e 0%, transparent 50%),
radial-gradient(circle at 80% 20%, #16213e 0%, transparent 50%),
radial-gradient(circle at 40% 40%, #0f3460 0%, transparent 50%);
position: relative;
display: flex;
flex-direction: column;
}
.main-content {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem 0;
}
.back-button {
position: fixed;
top: 20px;
right: 20px;
background: rgba(30, 41, 59, 0.95);
backdrop-filter: blur(20px);
border: 1px solid rgba(51, 65, 85, 0.3);
border-radius: 12px;
padding: 12px 20px;
color: #00d4ff;
text-decoration: none;
font-weight: 600;
font-size: 0.9rem;
text-transform: uppercase;
letter-spacing: 0.5px;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
z-index: 1000;
}
.back-button:hover {
background: #00d4ff;
color: #ffffff;
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 212, 255, 0.3);
}
.back-button::before {
content: "← ";
margin-right: 5px;
}
.container {
background: rgba(30, 41, 59, 0.95);
backdrop-filter: blur(20px);
border: 1px solid rgba(51, 65, 85, 0.3);
padding: 2.5rem;
border-radius: 1.5rem;
box-shadow:
0 25px 50px rgba(0, 0, 0, 0.3),
0 0 0 1px rgba(0, 212, 255, 0.1);
width: 100%;
max-width: 420px;
}
.logo {
text-align: center;
margin-bottom: 2rem;
}
.logo h1 {
font-size: 2.5rem;
font-weight: 700;
background: linear-gradient(135deg, #00d4ff, #ff6b35, #ffd700);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 0.5rem;
letter-spacing: -0.02em;
}
.logo p {
color: #94a3b8;
margin-top: 0.5rem;
font-size: 1rem;
font-weight: 400;
}
.form-container {
display: none;
}
.form-container.active {
display: block;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
color: #e2e8f0;
font-weight: 500;
font-size: 0.875rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.form-group input {
width: 100%;
padding: 1rem;
background: #1e293b;
border: 2px solid #334155;
border-radius: 0.75rem;
color: #ffffff;
font-size: 1rem;
font-family: inherit;
transition: all 0.2s ease;
}
.form-group input:focus {
outline: none;
border-color: #00d4ff;
box-shadow: 0 0 0 3px rgba(0, 212, 255, 0.1);
}
.form-group input::placeholder {
color: #64748b;
}
.btn {
width: 100%;
padding: 1rem;
border: none;
border-radius: 0.75rem;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
margin-bottom: 1rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.btn-primary {
background: linear-gradient(135deg, #00d4ff, #0891b2);
color: white;
box-shadow: 0 4px 12px rgba(0, 212, 255, 0.3);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0, 212, 255, 0.4);
}
.btn-secondary {
background: transparent;
color: #00d4ff;
border: 2px solid #00d4ff;
}
.btn-secondary:hover {
background: #00d4ff;
color: #0a0a0f;
}
.toggle-form {
text-align: center;
margin-top: 1rem;
}
.toggle-form button {
background: none;
border: none;
color: #00d4ff;
cursor: pointer;
text-decoration: underline;
font-size: 0.9rem;
transition: color 0.2s ease;
}
.toggle-form button:hover {
color: #0891b2;
}
.message {
padding: 1rem;
border-radius: 0.75rem;
margin-bottom: 1rem;
text-align: center;
font-weight: 500;
border: 1px solid;
}
.message.success {
background: rgba(34, 197, 94, 0.1);
color: #22c55e;
border-color: rgba(34, 197, 94, 0.3);
}
.message.error {
background: rgba(239, 68, 68, 0.1);
color: #ef4444;
border-color: rgba(239, 68, 68, 0.3);
}
.password-reset-container {
display: none;
margin-top: 1rem;
padding: 1rem;
background: rgba(239, 68, 68, 0.05);
border: 1px solid rgba(239, 68, 68, 0.2);
border-radius: 0.75rem;
text-align: center;
}
.password-reset-container.active {
display: block;
}
.password-reset-container p {
color: #ef4444;
margin-bottom: 1rem;
font-size: 0.9rem;
}
.btn-reset {
background: linear-gradient(135deg, #ef4444, #dc2626);
color: white;
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3);
}
.btn-reset:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(239, 68, 68, 0.4);
}
.loading {
display: none;
text-align: center;
color: #94a3b8;
}
.loading.active {
display: block;
}
.spinner {
border: 3px solid #334155;
border-top: 3px solid #00d4ff;
border-radius: 50%;
width: 30px;
height: 30px;
animation: spin 1s linear infinite;
margin: 0 auto 1rem;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Responsive Design */
@media (max-width: 768px) {
.container {
margin: 1rem;
padding: 2rem;
max-width: none;
}
.logo h1 {
font-size: 2rem;
}
.form-group input {
padding: 0.875rem;
}
.btn {
padding: 0.875rem;
}
}
@media (max-width: 480px) {
.container {
margin: 0.5rem;
padding: 1.5rem;
}
.logo h1 {
font-size: 1.75rem;
}
.logo p {
font-size: 0.875rem;
}
.back-button {
top: 15px;
right: 15px;
padding: 10px 15px;
font-size: 0.8rem;
}
}
/* OAuth Container */
.oauth-container {
margin-bottom: 20px;
}
.btn-google {
width: 100%;
background: white;
color: #333;
border: 1px solid #dadce0;
padding: 12px 16px;
border-radius: 8px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
font-size: 0.95rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.btn-google:hover {
background: #f8f9fa;
border-color: #c1c7cd;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.btn-google:active {
transform: translateY(0);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.btn-google svg {
flex-shrink: 0;
}
.btn-discord {
width: 100%;
background: #5865F2;
color: white;
border: 1px solid #4752C4;
padding: 12px 16px;
border-radius: 8px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
font-size: 0.95rem;
box-shadow: 0 1px 3px rgba(88, 101, 242, 0.3);
margin-top: 10px;
}
.btn-discord:hover {
background: #4752C4;
border-color: #3C45A5;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(88, 101, 242, 0.4);
}
.btn-discord:active {
transform: translateY(0);
box-shadow: 0 1px 3px rgba(88, 101, 242, 0.3);
}
.btn-discord svg {
flex-shrink: 0;
}
/* Divider */
.divider {
position: relative;
text-align: center;
margin: 20px 0;
color: #64748b;
font-size: 0.9rem;
}
.divider::before {
content: '';
position: absolute;
top: 50%;
left: 0;
right: 0;
height: 1px;
background: linear-gradient(to right, transparent, #334155, transparent);
}
.divider span {
background: #0a0a0f;
padding: 0 16px;
position: relative;
z-index: 1;
}
/* Footer Styles */
.footer {
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
border-top: 1px solid #2a2a3e;
margin-top: 3rem;
padding: 2rem 0;
}
.footer-content {
max-width: 1200px;
margin: 0 auto;
padding: 0 2rem;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 1rem;
}
.footer-links {
display: flex;
gap: 2rem;
align-items: center;
}
.footer-link {
color: #8892b0;
text-decoration: none;
font-size: 0.9rem;
transition: color 0.3s ease;
background: none;
border: none;
cursor: pointer;
font-family: inherit;
}
.footer-link:hover {
color: #00d4ff;
}
.cookie-settings-btn {
background: none !important;
border: none !important;
padding: 0 !important;
font-size: 0.9rem !important;
}
.footer-text {
color: #6b7280;
font-size: 0.85rem;
}
.footer-text p {
margin: 0;
}
@media (max-width: 768px) {
.footer-content {
flex-direction: column;
text-align: center;
gap: 1.5rem;
}
.footer-links {
flex-wrap: wrap;
justify-content: center;
gap: 1.5rem;
}
}

View File

@@ -0,0 +1,305 @@
/* Reset und Basis-Styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: radial-gradient(ellipse at top, #1e293b 0%, #0f172a 50%, #020617 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
color: #e2e8f0;
line-height: 1.6;
}
.container {
background: rgba(30, 41, 59, 0.95);
backdrop-filter: blur(20px);
border: 1px solid rgba(51, 65, 85, 0.3);
border-radius: 20px;
padding: 40px;
max-width: 500px;
width: 90%;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
text-align: center;
}
.logo {
font-size: 2.5rem;
font-weight: 900;
background: linear-gradient(135deg, #00d4ff, #0891b2);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 10px;
text-transform: uppercase;
letter-spacing: 2px;
}
.tagline {
color: #94a3b8;
font-size: 0.9rem;
margin-bottom: 30px;
text-transform: uppercase;
letter-spacing: 1px;
}
.title {
font-size: 1.8rem;
font-weight: 700;
color: #ffffff;
margin-bottom: 20px;
}
.subtitle {
color: #cbd5e1;
font-size: 1rem;
margin-bottom: 30px;
}
.form-group {
margin-bottom: 20px;
text-align: left;
}
.form-label {
display: block;
color: #e2e8f0;
font-weight: 600;
margin-bottom: 8px;
text-transform: uppercase;
letter-spacing: 0.5px;
font-size: 0.9rem;
}
.form-input {
width: 100%;
padding: 15px 20px;
background: #1e293b;
border: 2px solid #334155;
border-radius: 12px;
color: #ffffff;
font-size: 1rem;
transition: all 0.3s ease;
}
.form-input:focus {
outline: none;
border-color: #00d4ff;
box-shadow: 0 0 0 3px rgba(0, 212, 255, 0.1);
}
.form-input::placeholder {
color: #64748b;
}
.btn {
width: 100%;
padding: 15px 30px;
border: none;
border-radius: 12px;
font-size: 1rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1px;
cursor: pointer;
transition: all 0.3s ease;
margin-bottom: 20px;
}
.btn-primary {
background: linear-gradient(135deg, #00d4ff, #0891b2);
color: #ffffff;
box-shadow: 0 4px 15px rgba(0, 212, 255, 0.3);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 212, 255, 0.4);
}
.btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.btn-secondary {
background: transparent;
color: #00d4ff;
border: 2px solid #00d4ff;
}
.btn-secondary:hover {
background: #00d4ff;
color: #ffffff;
}
.message {
padding: 15px 20px;
border-radius: 12px;
margin-bottom: 20px;
font-weight: 600;
text-align: center;
}
.message.success {
background: rgba(34, 197, 94, 0.1);
border: 1px solid #22c55e;
color: #22c55e;
}
.message.error {
background: rgba(239, 68, 68, 0.1);
border: 1px solid #ef4444;
color: #ef4444;
}
.message.info {
background: rgba(59, 130, 246, 0.1);
border: 1px solid #3b82f6;
color: #3b82f6;
}
.loading {
display: none;
text-align: center;
color: #94a3b8;
}
.spinner {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid #334155;
border-radius: 50%;
border-top-color: #00d4ff;
animation: spin 1s ease-in-out infinite;
margin-right: 10px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.back-link {
color: #00d4ff;
text-decoration: none;
font-size: 0.9rem;
margin-top: 20px;
display: inline-block;
}
.back-link:hover {
color: #0891b2;
}
/* Responsive Design */
@media (max-width: 768px) {
.container {
margin: 20px;
padding: 30px 20px;
}
.logo {
font-size: 2rem;
}
.title {
font-size: 1.5rem;
}
.form-input, .btn {
padding: 12px 15px;
font-size: 0.9rem;
}
}
@media (max-width: 480px) {
.container {
margin: 10px;
padding: 20px 15px;
}
.logo {
font-size: 1.8rem;
}
.title {
font-size: 1.3rem;
}
}
/* Footer Styles */
.footer {
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
border-top: 1px solid #2a2a3e;
margin-top: 3rem;
padding: 2rem 0;
}
.footer-content {
max-width: 1200px;
margin: 0 auto;
padding: 0 2rem;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 1rem;
}
.footer-links {
display: flex;
gap: 2rem;
align-items: center;
}
.footer-link {
color: #8892b0;
text-decoration: none;
font-size: 0.9rem;
transition: color 0.3s ease;
background: none;
border: none;
cursor: pointer;
font-family: inherit;
}
.footer-link:hover {
color: #00d4ff;
}
.cookie-settings-btn {
background: none !important;
border: none !important;
padding: 0 !important;
font-size: 0.9rem !important;
}
.footer-text {
color: #6b7280;
font-size: 0.85rem;
}
.footer-text p {
margin: 0;
}
@media (max-width: 768px) {
.footer-content {
flex-direction: column;
text-align: center;
gap: 1.5rem;
}
.footer-links {
flex-wrap: wrap;
justify-content: center;
gap: 1.5rem;
}
}

902
public/dashboard.html Normal file
View File

@@ -0,0 +1,902 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<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="mobile-web-app-capable" content="yes">
<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);
});
}
// Don't automatically request notification permission
// User must click the button to enable push notifications
// Convert VAPID key from base64url to Uint8Array
function urlBase64ToUint8Array(base64String) {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding)
.replace(/\-/g, '+')
.replace(/_/g, '/');
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
// Convert ArrayBuffer to Base64 string
function arrayBufferToBase64(buffer) {
const bytes = new Uint8Array(buffer);
let binary = '';
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return window.btoa(binary);
}
// Push notification state
let pushSubscription = null;
let pushEnabled = false;
// Subscribe to push notifications
async function subscribeToPush() {
try {
console.log('🔔 Starting push subscription...');
const registration = await navigator.serviceWorker.ready;
const vapidPublicKey = 'BJmNVx0C3XeVxeKGTP9c-Z4HcuZNmdk6QdiLocZgCmb-miCS0ESFO3W2TvJlRhhNAShV63pWA5p36BTVSetyTds';
const applicationServerKey = urlBase64ToUint8Array(vapidPublicKey);
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: applicationServerKey
});
pushSubscription = subscription;
// Player ID wird automatisch vom Server aus der Session geholt
console.log(`📱 Subscribing to push notifications...`);
// Convert ArrayBuffer keys to Base64 strings
const p256dhKey = subscription.getKey('p256dh');
const authKey = subscription.getKey('auth');
// Convert ArrayBuffer to Base64 URL-safe string
const p256dhString = arrayBufferToBase64(p256dhKey);
const authString = arrayBufferToBase64(authKey);
console.log('📱 Converted keys to Base64 strings');
console.log('📱 p256dh length:', p256dhString.length);
console.log('📱 auth length:', authString.length);
// Get current Supabase user ID
const { data: { session } } = await supabase.auth.getSession();
const supabaseUserId = session?.user?.id || null;
// Send subscription to server with player ID
const response = await fetch('/api/v1/public/subscribe', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include', // Include cookies for session
body: JSON.stringify({
endpoint: subscription.endpoint,
keys: {
p256dh: p256dhString,
auth: authString
},
supabaseUserId: supabaseUserId
})
});
const result = await response.json();
if (result.success) {
pushEnabled = true;
updatePushButton();
console.log('✅ Push subscription successful');
// Store player ID for notifications
if (result.playerId) {
localStorage.setItem('pushPlayerId', result.playerId);
}
} else {
throw new Error(result.message);
}
} catch (error) {
console.error('❌ Push subscription failed:', error);
pushEnabled = false;
updatePushButton();
}
}
// Unsubscribe from push notifications
async function unsubscribeFromPush() {
try {
console.log('🔕 Unsubscribing from push notifications...');
// Get player ID from localStorage
const playerId = localStorage.getItem('pushPlayerId');
if (pushSubscription) {
// Store endpoint before unsubscribing
const endpoint = pushSubscription.endpoint;
await pushSubscription.unsubscribe();
pushSubscription = null;
console.log('✅ Local push subscription removed');
// Notify server to remove specific subscription from database
if (playerId && endpoint) {
try {
const response = await fetch('/api/v1/public/unsubscribe', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include', // Include cookies for session
body: JSON.stringify({
playerId: playerId,
endpoint: endpoint
})
});
const result = await response.json();
if (result.success) {
console.log('✅ Server notified - specific subscription removed from database');
} else {
console.warn('⚠️ Server notification failed:', result.message);
}
} catch (error) {
console.warn('⚠️ Failed to notify server:', error);
}
}
}
// Clear stored player ID
localStorage.removeItem('pushPlayerId');
pushEnabled = false;
updatePushButton();
console.log('🔕 Push notifications disabled');
} catch (error) {
console.error('❌ Push unsubscribe failed:', error);
pushEnabled = false;
updatePushButton();
}
}
// Check if user is on iOS
function isIOS() {
return /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
}
// Check if PWA is installed
function isPWAInstalled() {
return window.matchMedia('(display-mode: standalone)').matches ||
window.navigator.standalone === true;
}
// Show iOS PWA installation hint
function showIOSPWAHint() {
if (isIOS() && !isPWAInstalled()) {
const hint = document.createElement('div');
hint.id = 'ios-pwa-hint';
hint.style.cssText = `
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 15px 20px;
border-radius: 10px;
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
z-index: 10000;
max-width: 90%;
text-align: center;
font-size: 14px;
line-height: 1.4;
`;
hint.innerHTML = `
<div style="display: flex; align-items: center; gap: 10px;">
<span style="font-size: 20px;">📱</span>
<div>
<strong>Push-Benachrichtigungen für iOS</strong><br>
<small>Für Push-Benachrichtigungen auf iOS: Tippe auf <span style="background: rgba(255,255,255,0.2); padding: 2px 6px; border-radius: 4px;">📤 Teilen</span> → <span style="background: rgba(255,255,255,0.2); padding: 2px 6px; border-radius: 4px;">Zum Home-Bildschirm hinzufügen</span></small>
</div>
<button onclick="this.parentElement.parentElement.remove()" style="background: none; border: none; color: white; font-size: 18px; cursor: pointer; padding: 0; margin-left: 10px;">✕</button>
</div>
`;
document.body.appendChild(hint);
// Auto-remove after 10 seconds
setTimeout(() => {
if (hint.parentNode) {
hint.remove();
}
}, 10000);
}
}
// Toggle push notifications
async function togglePushNotifications() {
if (pushEnabled) {
await unsubscribeFromPush();
} else {
// Check if iOS and not PWA
if (isIOS() && !isPWAInstalled()) {
showIOSPWAHint();
return;
}
// Check notification permission first
if (Notification.permission === 'denied') {
alert('Push-Benachrichtigungen sind blockiert. Bitte erlaube sie in den Browser-Einstellungen.');
return;
}
if (Notification.permission === 'default') {
const permission = await Notification.requestPermission();
if (permission !== 'granted') {
alert('Push-Benachrichtigungen wurden nicht erlaubt.');
return;
}
}
await subscribeToPush();
}
}
// Update push button appearance
function updatePushButton() {
const button = document.getElementById('pushButton');
if (!button) {
console.log('❌ Push button not found in DOM');
return;
}
console.log(`🔔 Updating push button - Status: ${pushEnabled ? 'ENABLED' : 'DISABLED'}`);
if (pushEnabled) {
button.classList.add('active');
button.setAttribute('data-de', '🔕 Push deaktivieren');
button.setAttribute('data-en', '🔕 Disable Push');
button.textContent = '🔕 Push deaktivieren';
console.log('✅ Button updated to: Push deaktivieren (RED)');
} else {
button.classList.remove('active');
button.setAttribute('data-de', '🔔 Push aktivieren');
button.setAttribute('data-en', '🔔 Enable Push');
button.textContent = '🔔 Push aktivieren';
console.log('✅ Button updated to: Push aktivieren (GREEN)');
}
}
// Check existing push subscription on page load
async function checkPushStatus() {
try {
console.log('🔍 Checking push status...');
if (!('serviceWorker' in navigator)) {
console.log('❌ Service Worker not supported');
pushEnabled = false;
updatePushButton();
return;
}
if (!('PushManager' in window)) {
console.log('❌ Push Manager not supported');
pushEnabled = false;
updatePushButton();
return;
}
const registration = await navigator.serviceWorker.ready;
console.log('✅ Service Worker ready');
const subscription = await registration.pushManager.getSubscription();
console.log('📱 Current subscription:', subscription ? 'EXISTS' : 'NONE');
if (subscription) {
pushSubscription = subscription;
pushEnabled = true;
updatePushButton();
console.log('✅ Existing push subscription found and activated');
// Also check if we have a stored player ID
const storedPlayerId = localStorage.getItem('pushPlayerId');
if (storedPlayerId) {
console.log(`📱 Push subscription linked to player: ${storedPlayerId}`);
}
} else {
pushEnabled = false;
updatePushButton();
console.log(' No existing push subscription found');
}
} catch (error) {
console.error('❌ Error checking push status:', error);
pushEnabled = false;
updatePushButton();
}
}
</script>
</head>
<body>
<div class="main-container">
<!-- Sticky Header Container -->
<div class="sticky-header">
<!-- Language Selector -->
<div class="language-selector">
<select id="languageSelect" onchange="changeLanguage()">
<option value="de" data-flag="🇩🇪">Deutsch</option>
<option value="en" data-flag="🇺🇸">English</option>
</select>
</div>
<div class="nav-buttons">
<div class="user-info">
<div class="user-avatar" id="userAvatar">U</div>
<span id="userEmail">user@example.com</span>
</div>
<button class="btn btn-push" id="pushButton" onclick="togglePushNotifications()" data-de="🔔 Push aktivieren" data-en="🔔 Enable Push">🔔 Push aktivieren</button>
<button class="btn btn-pwa" id="pwaButton" onclick="installPWA()" style="display: none;" data-de="📱 App installieren" data-en="📱 Install App">📱 App installieren</button>
<a href="/" class="btn btn-primary" data-de="Zurück zu Zeiten" data-en="Back to Times">Back to Times</a>
<button class="btn btn-logout" onclick="logout()" data-de="Logout" data-en="Logout">Logout</button>
</div>
</div>
<div class="header-section">
<h1 class="main-title" data-de="DEIN DASHBOARD" data-en="YOUR DASHBOARD">DEIN DASHBOARD</h1>
<p class="tagline" data-de="Verwalte deine Läufe in der NINJACROSS ARENA" data-en="Manage your runs in the NINJACROSS ARENA">Verwalte deine Läufe in der NINJACROSS ARENA</p>
</div>
<div id="loading" class="loading">
<div class="spinner"></div>
<p data-de="Lade dein Dashboard..." data-en="Loading your dashboard...">Lade dein Dashboard...</p>
</div>
<div id="dashboardContent" style="display: none;">
<div class="welcome-card">
<h2 data-de="Dein Dashboard 🥷" data-en="Your Dashboard 🥷">Dein Dashboard 🥷</h2>
<p data-de="Willkommen in Deinem Dashboard-Panel! Deine übersichtliche Übersicht aller deiner Läufe." data-en="Welcome to your Dashboard panel! Your clear overview of all your runs.">Willkommen in Deinem Dashboard-Panel! Deine übersichtliche Übersicht aller deiner Läufe.</p>
</div>
<div class="dashboard-grid">
<div class="card" id="analyticsCard" style="cursor: pointer;">
<h3 data-de="📊 Analytics" data-en="📊 Analytics">📊 Analytics</h3>
<div id="analyticsPreview" style="display: none;">
<div class="analytics-stats">
<div class="mini-stat">
<div class="mini-stat-number" id="avgTimeThisWeek">--:--</div>
<div class="mini-stat-label">Durchschnitt diese Woche</div>
</div>
<div class="mini-stat">
<div class="mini-stat-number" id="improvementThisWeek">+0.0%</div>
<div class="mini-stat-label">Verbesserung</div>
</div>
<div class="mini-stat">
<div class="mini-stat-number" id="runsThisWeek">0</div>
<div class="mini-stat-label">Läufe diese Woche</div>
</div>
</div>
</div>
<p data-de="Verfolge deine Leistung und überwache wichtige Metriken." data-en="Track your performance and monitor important metrics.">Verfolge deine Leistung und überwache wichtige Metriken.</p>
<button class="btn btn-primary" style="margin-top: 1rem;" onclick="event.stopPropagation(); showAnalytics();" data-de="Analytics öffnen" data-en="Open Analytics">Analytics öffnen</button>
</div>
<div class="card" onclick="showSettings()" style="cursor: pointer;">
<h3 data-de="⚙️ Settings" data-en="⚙️ Settings">⚙️ Settings</h3>
<p data-de="Verwalte deine Privatsphäre-Einstellungen und andere Optionen." data-en="Manage your privacy settings and other options.">Verwalte deine Privatsphäre-Einstellungen und andere Optionen.</p>
<button class="btn btn-primary" style="margin-top: 1rem;" onclick="event.stopPropagation(); showSettings();" data-de="Einstellungen öffnen" data-en="Open Settings">Einstellungen öffnen</button>
</div>
<div class="card" onclick="showRFIDSettings()" style="cursor: pointer;">
<h3 data-de="🏷️ RFID Verknüpfung" data-en="🏷️ RFID Linking">🏷️ RFID Verknüpfung</h3>
<p data-de="Verknüpfe deine RFID-Karte mit deinem Account, um deine Zeiten automatisch zu tracken." data-en="Link your RFID card with your account to automatically track your times.">Verknüpfe deine RFID-Karte mit deinem Account, um deine Zeiten automatisch zu tracken.</p>
<button class="btn btn-primary" style="margin-top: 1rem;" onclick="event.stopPropagation(); showRFIDSettings();" data-de="RFID verknüpfen" data-en="Link RFID">RFID verknüpfen</button>
</div>
<div class="card" id="statisticsCard" style="cursor: pointer;">
<h3 data-de="📊 Statistiken" data-en="📊 Statistics">📊 Statistiken</h3>
<div id="statisticsPreview" style="display: none;">
<div class="statistics-stats">
<div class="mini-stat">
<div class="mini-stat-number" id="personalBest">--:--</div>
<div class="mini-stat-label">Persönliche Bestzeit</div>
</div>
<div class="mini-stat">
<div class="mini-stat-number" id="totalRunsCount">0</div>
<div class="mini-stat-label">Gesamte Läufe</div>
</div>
<div class="mini-stat">
<div class="mini-stat-number" id="rankPosition">-</div>
<div class="mini-stat-label">Ranglisten-Position</div>
</div>
</div>
</div>
<p data-de="Detaillierte Statistiken zu deinen Läufen - beste Zeiten, Verbesserungen und Vergleiche." data-en="Detailed statistics about your runs - best times, improvements and comparisons.">Detaillierte Statistiken zu deinen Läufen - beste Zeiten, Verbesserungen und Vergleiche.</p>
<button class="btn btn-primary" style="margin-top: 1rem;" onclick="event.stopPropagation(); showStatistics();" data-de="Statistiken öffnen" data-en="Open Statistics">Statistiken öffnen</button>
</div>
</div>
<!-- User Times Section -->
<div class="times-section">
<div class="times-header">
<h2 data-de="🏃‍♂️ Meine Zeiten" data-en="🏃‍♂️ My Times">🏃‍♂️ Meine Zeiten</h2>
<p data-de="Deine persönlichen Bestzeiten an allen Standorten" data-en="Your personal best times at all locations">Deine persönlichen Bestzeiten an allen Standorten</p>
</div>
<!-- Loading State -->
<div id="timesLoading" class="times-loading" style="display: none;">
<div class="spinner"></div>
<p data-de="Lade deine Zeiten..." data-en="Loading your times...">Lade deine Zeiten...</p>
</div>
<!-- Not Linked State -->
<div id="timesNotLinked" class="times-not-linked">
<div class="not-linked-content">
<div class="not-linked-icon">🔗</div>
<h3 data-de="RFID noch nicht verknüpft" data-en="RFID not linked yet">RFID noch nicht verknüpft</h3>
<p data-de="Um deine persönlichen Zeiten zu sehen, musst du zuerst deine RFID-Karte mit deinem Account verknüpfen." data-en="To see your personal times, you must first link your RFID card with your account.">Um deine persönlichen Zeiten zu sehen, musst du zuerst deine RFID-Karte mit deinem Account verknüpfen.</p>
<button class="btn btn-primary" onclick="showRFIDSettings()" data-de="🏷️ RFID jetzt verknüpfen" data-en="🏷️ Link RFID now">
🏷️ RFID jetzt verknüpfen
</button>
<div class="link-info">
<h4 data-de="So funktioniert's:" data-en="How it works:">So funktioniert's:</h4>
<ol>
<li data-de="Klicke auf \"RFID jetzt verknüpfen\"" data-en="Click on \"Link RFID now\"">Klicke auf "RFID jetzt verknüpfen"</li>
<li data-de="Scanne den QR-Code auf deiner RFID-Karte" data-en="Scan the QR code on your RFID card">Scanne den QR-Code auf deiner RFID-Karte</li>
<li data-de="Fertig! Deine Zeiten werden automatisch hier angezeigt" data-en="Done! Your times will be displayed here automatically">Fertig! Deine Zeiten werden automatisch hier angezeigt</li>
</ol>
</div>
</div>
</div>
<!-- Times Display -->
<div id="timesDisplay" style="display: none;">
<div class="times-stats">
<div class="stat-card">
<div class="stat-number" id="totalRuns">0</div>
<div class="stat-label" data-de="Gesamte Läufe" data-en="Total Runs">Gesamte Läufe</div>
</div>
<div class="stat-card">
<div class="stat-number" id="bestTime">--:--</div>
<div class="stat-label" data-de="Beste Zeit" data-en="Best Time">Beste Zeit</div>
</div>
<div class="stat-card">
<div class="stat-number" id="locationsCount">0</div>
<div class="stat-label" data-de="Standorte" data-en="Locations">Standorte</div>
</div>
<div class="stat-card">
<div class="stat-number" id="linkedPlayer">--</div>
<div class="stat-label" data-de="Verknüpfter Spieler" data-en="Linked Player">Verknüpfter Spieler</div>
</div>
</div>
<div class="times-content">
<div class="times-grid" id="userTimesGrid">
<!-- Times will be populated here -->
</div>
</div>
</div>
</div>
<!-- Analytics Section -->
<div id="analyticsSection" class="analytics-section" style="display: none;">
<div class="section-header">
<h2 data-de="📊 Analytics" data-en="📊 Analytics">📊 Analytics</h2>
<p data-de="Detaillierte Analyse deiner Performance und Trends" data-en="Detailed analysis of your performance and trends">Detaillierte Analyse deiner Performance und Trends</p>
</div>
<!-- Performance Overview -->
<div class="analytics-grid">
<div class="analytics-card">
<h3>📈 Performance-Trends</h3>
<div class="trend-stats">
<div class="trend-item">
<span class="trend-label">Diese Woche:</span>
<span class="trend-value" id="avgTimeThisWeekDetail">--:--</span>
</div>
<div class="trend-item">
<span class="trend-label">Letzte Woche:</span>
<span class="trend-value" id="avgTimeLastWeek">--:--</span>
</div>
<div class="trend-item">
<span class="trend-label">Verbesserung:</span>
<span class="trend-value" id="improvementDetail">+0.0%</span>
</div>
</div>
</div>
<div class="analytics-card">
<h3>🏃‍♂️ Aktivitäts-Heatmap</h3>
<div class="activity-stats">
<div class="activity-item">
<span class="activity-label">Heute:</span>
<span class="activity-value" id="runsToday">0 Läufe</span>
</div>
<div class="activity-item">
<span class="activity-label">Diese Woche:</span>
<span class="activity-value" id="runsThisWeekDetail">0 Läufe</span>
</div>
<div class="activity-item">
<span class="activity-label">Durchschnitt/Tag:</span>
<span class="activity-value" id="avgRunsPerDay">0.0</span>
</div>
</div>
</div>
<div class="analytics-card">
<h3>🏆 Standort-Performance</h3>
<div class="location-stats" id="locationPerformance">
<p data-de="Lade Standort-Daten..." data-en="Loading location data...">Lade Standort-Daten...</p>
</div>
</div>
<div class="analytics-card">
<h3>📅 Monatlicher Fortschritt</h3>
<div class="monthly-stats">
<div class="monthly-item">
<span class="monthly-label">Dieser Monat:</span>
<span class="monthly-value" id="runsThisMonth">0 Läufe</span>
</div>
<div class="monthly-item">
<span class="monthly-label">Letzter Monat:</span>
<span class="monthly-value" id="runsLastMonth">0 Läufe</span>
</div>
<div class="monthly-item">
<span class="monthly-label">Beste Zeit:</span>
<span class="monthly-value" id="bestTimeThisMonth">--:--</span>
</div>
</div>
</div>
</div>
</div>
<!-- Statistics Section -->
<div id="statisticsSection" class="statistics-section" style="display: none;">
<div class="section-header">
<h2 data-de="📊 Statistiken" data-en="📊 Statistics">📊 Statistiken</h2>
<p data-de="Detaillierte Statistiken zu deinen Läufen und Vergleiche" data-en="Detailed statistics about your runs and comparisons">Detaillierte Statistiken zu deinen Läufen und Vergleiche</p>
</div>
<!-- Personal Records -->
<div class="statistics-grid">
<div class="statistics-card">
<h3>🏆 Persönliche Bestzeiten</h3>
<div class="personal-records" id="personalRecords">
<p data-de="Lade Bestzeiten..." data-en="Loading best times...">Lade Bestzeiten...</p>
</div>
</div>
<div class="statistics-card">
<h3>📊 Konsistenz-Metriken</h3>
<div class="consistency-stats">
<div class="consistency-item">
<span class="consistency-label">Durchschnittszeit:</span>
<span class="consistency-value" id="averageTime">--:--</span>
</div>
<div class="consistency-item">
<span class="consistency-label">Standardabweichung:</span>
<span class="consistency-value" id="timeDeviation">--:--</span>
</div>
<div class="consistency-item">
<span class="consistency-label">Konsistenz-Score:</span>
<span class="consistency-value" id="consistencyScore">0%</span>
</div>
</div>
</div>
<div class="statistics-card">
<h3>🏅 Ranglisten-Positionen</h3>
<div class="ranking-stats" id="rankingStats">
<p data-de="Lade Ranglisten..." data-en="Loading rankings...">Lade Ranglisten...</p>
</div>
</div>
<div class="statistics-card">
<h3>📈 Fortschritt-Übersicht</h3>
<div class="progress-stats">
<div class="progress-item">
<span class="progress-label">Gesamte Läufe:</span>
<span class="progress-value" id="totalRunsStats">0</span>
</div>
<div class="progress-item">
<span class="progress-label">Aktive Tage:</span>
<span class="progress-value" id="activeDays">0</span>
</div>
<div class="progress-item">
<span class="progress-label">Standorte besucht:</span>
<span class="progress-value" id="locationsVisited">0</span>
</div>
</div>
</div>
</div>
</div>
<!-- Achievements Section -->
<div class="achievements-section">
<div class="achievements-header">
<h2 data-de="🏆 Meine Achievements" data-en="🏆 My Achievements">🏆 Meine Achievements</h2>
<p data-de="Sammele Punkte und erreiche neue Meilensteine!" data-en="Collect points and reach new milestones!">Sammele Punkte und erreiche neue Meilensteine!</p>
</div>
<!-- Achievement Stats -->
<div class="achievement-stats" id="achievementStats" style="display: none;">
<div class="stat-card achievement-stat">
<div class="stat-number" id="totalPoints">0</div>
<div class="stat-label" data-de="Gesamtpunkte" data-en="Total Points">Gesamtpunkte</div>
</div>
<div class="stat-card achievement-stat">
<div class="stat-number" id="completedAchievements">0</div>
<div class="stat-label" data-de="Abgeschlossen" data-en="Completed">Abgeschlossen</div>
</div>
<div class="stat-card achievement-stat">
<div class="stat-number" id="achievementsToday">0</div>
<div class="stat-label" data-de="Heute erreicht" data-en="Achieved Today">Heute erreicht</div>
</div>
<div class="stat-card achievement-stat">
<div class="stat-number" id="completionPercentage">0%</div>
<div class="stat-label" data-de="Fortschritt" data-en="Progress">Fortschritt</div>
</div>
</div>
<!-- Achievement Categories -->
<div class="achievement-categories" id="achievementCategories" style="display: none;">
<div class="category-tabs">
<button class="category-tab active" onclick="showAchievementCategory('all')" data-category="all" data-de="Alle" data-en="All">Alle</button>
<button class="category-tab" onclick="showAchievementCategory('consistency')" data-category="consistency" data-de="Konsistenz" data-en="Consistency">Konsistenz</button>
<button class="category-tab" onclick="showAchievementCategory('improvement')" data-category="improvement" data-de="Verbesserung" data-en="Improvement">Verbesserung</button>
<button class="category-tab" onclick="showAchievementCategory('seasonal')" data-category="seasonal" data-de="Saisonal" data-en="Seasonal">Saisonal</button>
<button class="category-tab" onclick="showAchievementCategory('monthly')" data-category="monthly" data-de="Monatlich" data-en="Monthly">Monatlich</button>
</div>
<div class="achievements-grid" id="achievementsGrid">
<!-- Achievements will be populated here -->
</div>
</div>
<!-- Achievement Loading State -->
<div id="achievementsLoading" class="achievements-loading" style="display: none;">
<div class="spinner"></div>
<p data-de="Lade deine Achievements..." data-en="Loading your achievements...">Lade deine Achievements...</p>
</div>
<!-- Achievement Not Available State -->
<div id="achievementsNotAvailable" class="achievements-not-available" style="display: none;">
<div class="not-available-content">
<div class="not-available-icon">🏆</div>
<h3 data-de="Achievements noch nicht verfügbar" data-en="Achievements not available yet">Achievements noch nicht verfügbar</h3>
<p data-de="Um Achievements zu sammeln, musst du zuerst deine RFID-Karte mit deinem Account verknüpfen und einige Läufe absolvieren." data-en="To collect achievements, you must first link your RFID card with your account and complete some runs.">Um Achievements zu sammeln, musst du zuerst deine RFID-Karte mit deinem Account verknüpfen und einige Läufe absolvieren.</p>
<button class="btn btn-primary" onclick="showRFIDSettings()" data-de="🏷️ RFID jetzt verknüpfen" data-en="🏷️ Link RFID now">
🏷️ RFID jetzt verknüpfen
</button>
</div>
</div>
</div>
</div>
</div>
<!-- RFID Settings Modal -->
<div id="rfidModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title" data-de="📱 RFID QR-Code Scanner" data-en="📱 RFID QR Code Scanner">📱 RFID QR-Code Scanner</h2>
<span class="close" onclick="closeModal('rfidModal')">&times;</span>
</div>
<div id="rfidMessage"></div>
<!-- QR Scanner Step -->
<div id="qrScannerStep">
<p style="color: #8892b0; margin-bottom: 1.5rem; text-align: center;" data-de="Scanne den QR-Code auf deiner RFID-Karte, um sie mit deinem Account zu verknüpfen." data-en="Scan the QR code on your RFID card to link it with your account.">
Scanne den QR-Code auf deiner RFID-Karte, um sie mit deinem Account zu verknüpfen.
</p>
<!-- Camera Preview -->
<div id="cameraContainer" style="display: none;">
<video id="qrVideo" style="width: 100%; max-width: 400px; border-radius: 0.75rem; margin: 0 auto; display: block;"></video>
<canvas id="qrCanvas" style="display: none;"></canvas>
</div>
<!-- Scanner Controls -->
<div style="text-align: center; margin: 1.5rem 0;">
<button class="btn btn-primary" onclick="startQRScanner()" id="startScanBtn" data-de="📷 Kamera starten" data-en="📷 Start Camera">
📷 Kamera starten
</button>
<button class="btn btn-secondary" onclick="stopQRScanner()" id="stopScanBtn" style="display: none;" data-de="🛑 Scanner stoppen" data-en="🛑 Stop Scanner">
🛑 Scanner stoppen
</button>
</div>
<!-- Manual Input Fallback -->
<div style="border-top: 1px solid #334155; padding-top: 1.5rem; margin-top: 1.5rem;">
<p style="color: #8892b0; text-align: center; margin-bottom: 1rem; font-size: 0.9rem;" data-de="Kamera funktioniert nicht? RFID UID manuell eingeben:" data-en="Camera not working? Enter RFID UID manually:">
Kamera funktioniert nicht? RFID UID manuell eingeben:
</p>
<div class="form-group">
<input type="text" id="manualRfidInput" class="form-input" placeholder="z.B. aaaaaa, FFFFFF oder FF:FF:FF:FF" style="text-align: center; font-family: monospace;" data-de="z.B. aaaaaa, FFFFFF oder FF:FF:FF:FF" data-en="e.g. aaaaaa, FFFFFF or FF:FF:FF:FF">
</div>
<button class="btn btn-secondary" onclick="linkManualRfid()" style="width: 100%;" data-de="Manuell verknüpfen" data-en="Link Manually">
Manuell verknüpfen
</button>
</div>
<!-- Create New Player Section -->
<div id="createPlayerSection" style="border-top: 1px solid #334155; padding-top: 1.5rem; margin-top: 1.5rem;">
<p style="color: #8892b0; text-align: center; margin-bottom: 1rem; font-size: 0.9rem;" data-de="Neuen Spieler mit RFID erstellen:" data-en="Create new player with RFID:">
Neuen Spieler mit RFID erstellen:
</p>
<div class="form-group">
<label for="playerFirstname" style="color: #8892b0; font-size: 0.9rem; margin-bottom: 0.5rem; display: block;" data-de="Vorname:" data-en="First Name:">Vorname:</label>
<input type="text" id="playerFirstname" class="form-input" placeholder="Max" style="text-align: center;">
</div>
<div class="form-group">
<label for="playerLastname" style="color: #8892b0; font-size: 0.9rem; margin-bottom: 0.5rem; display: block;" data-de="Nachname:" data-en="Last Name:">Nachname:</label>
<input type="text" id="playerLastname" class="form-input" placeholder="Mustermann" style="text-align: center;">
</div>
<div class="form-group">
<label for="playerBirthdate" style="color: #8892b0; font-size: 0.9rem; margin-bottom: 0.5rem; display: block;" data-de="Geburtsdatum:" data-en="Birth Date:">Geburtsdatum:</label>
<input type="date" id="playerBirthdate" class="form-input" style="text-align: center;">
</div>
<!-- AGB Section -->
<div class="agb-section" style="background: #1e293b; border: 1px solid #334155; border-radius: 8px; padding: 15px; margin: 15px 0;">
<div class="agb-checkbox" style="display: flex; align-items: flex-start; gap: 10px; margin-bottom: 10px;">
<input type="checkbox" id="agbAccepted" name="agbAccepted" required style="width: auto; margin: 0; margin-top: 3px;">
<label for="agbAccepted" style="color: #e2e8f0; font-size: 0.85rem; line-height: 1.4; margin: 0; font-weight: normal;">
Ich habe die <a href="/agb.html" target="_blank" style="color: #00d4ff; text-decoration: none; font-weight: bold;">Allgemeinen Geschäftsbedingungen</a>
gelesen und stimme zu, dass mein Name und meine Laufzeiten im öffentlichen Leaderboard angezeigt werden.
</label>
</div>
<div class="agb-warning" style="color: #fbbf24; font-size: 0.8rem; margin-top: 10px;">
⚠️ <strong>Wichtig:</strong> Ohne Zustimmung zu den AGB können Sie das System nutzen,
aber Ihre Zeiten werden nicht im öffentlichen Leaderboard angezeigt.
</div>
</div>
<button class="btn btn-primary" onclick="createRfidPlayerRecord()" style="width: 100%;" data-de="Spieler erstellen" data-en="Create Player">
Spieler erstellen
</button>
</div>
<!-- Scanning Status -->
<div id="scanningStatus" style="display: none; text-align: center; color: #00d4ff; margin-top: 1rem;">
<div class="spinner" style="width: 20px; height: 20px; margin: 0 auto 0.5rem;"></div>
<span data-de="Suche nach QR-Code..." data-en="Searching for QR code...">Suche nach QR-Code...</span>
</div>
</div>
</div>
</div>
<!-- Settings Modal -->
<div id="settingsModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title" data-de="⚙️ Einstellungen" data-en="⚙️ Settings">⚙️ Einstellungen</h2>
<span class="close" onclick="closeModal('settingsModal')">&times;</span>
</div>
<div class="settings-content">
<div class="setting-item">
<div class="setting-info">
<h3 data-de="🏆 Leaderboard Sichtbarkeit" data-en="🏆 Leaderboard Visibility">🏆 Leaderboard Sichtbarkeit</h3>
<p data-de="Bestimme, ob deine Zeiten im globalen Leaderboard angezeigt werden sollen." data-en="Determine whether your times should be displayed in the global leaderboard.">Bestimme, ob deine Zeiten im globalen Leaderboard angezeigt werden sollen.</p>
</div>
<div class="setting-control">
<label class="toggle-switch">
<input type="checkbox" id="showInLeaderboard" onchange="updateLeaderboardSetting()">
<span class="toggle-slider"></span>
</label>
</div>
</div>
<div class="setting-description">
<p style="color: #8892b0; font-size: 0.9rem; margin-top: 1rem; padding: 1rem; background: #1e293b; border-radius: 0.5rem;" data-de="<strong>Hinweis:</strong> Wenn diese Option deaktiviert ist, werden deine Zeiten nur in deinem persönlichen Dashboard angezeigt, aber nicht im öffentlichen Leaderboard. Du kannst diese Einstellung jederzeit ändern." data-en="<strong>Note:</strong> If this option is disabled, your times will only be displayed in your personal dashboard, but not in the public leaderboard. You can change this setting at any time.">
<strong>Hinweis:</strong> Wenn diese Option deaktiviert ist, werden deine Zeiten nur in deinem persönlichen Dashboard angezeigt, aber nicht im öffentlichen Leaderboard. Du kannst diese Einstellung jederzeit ändern.
</p>
</div>
<div class="settings-actions">
<button class="btn btn-primary" onclick="saveSettings()" data-de="Einstellungen speichern" data-en="Save Settings">Einstellungen speichern</button>
<button class="btn btn-secondary" onclick="closeModal('settingsModal')" data-de="Abbrechen" data-en="Cancel">Abbrechen</button>
</div>
</div>
</div>
</div>
<!-- Footer -->
<footer class="footer">
<div class="footer-content">
<div class="footer-links">
<a href="/impressum.html" class="footer-link" data-de="Impressum" data-en="Imprint">Impressum</a>
<a href="/datenschutz.html" class="footer-link" data-de="Datenschutz" data-en="Privacy">Datenschutz</a>
<button id="cookie-settings-footer" class="footer-link cookie-settings-btn" data-de="Cookie-Einstellungen" data-en="Cookie Settings">Cookie-Einstellungen</button>
</div>
<div class="footer-text">
<p data-de="&copy; 2024 NinjaCross. Alle Rechte vorbehalten." data-en="&copy; 2024 NinjaCross. All rights reserved.">&copy; 2024 NinjaCross. Alle Rechte vorbehalten.</p>
</div>
</div>
</footer>
<script src="/js/cookie-consent.js"></script>
<script src="/js/dashboard.js?v=1.6"></script>
<script>
// PWA Installation
let deferredPrompt;
// Listen for PWA install prompt
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault();
deferredPrompt = e;
const pwaButton = document.getElementById('pwaButton');
if (pwaButton) {
pwaButton.style.display = 'inline-block';
}
});
// Install PWA
async function installPWA() {
if (deferredPrompt) {
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
console.log(`PWA install outcome: ${outcome}`);
deferredPrompt = null;
const pwaButton = document.getElementById('pwaButton');
if (pwaButton) {
pwaButton.style.display = 'none';
}
} else if (isIOS()) {
// Show iOS installation instructions
showIOSPWAHint();
}
}
// Check if PWA is already installed
window.addEventListener('appinstalled', () => {
console.log('PWA was installed');
const pwaButton = document.getElementById('pwaButton');
if (pwaButton) {
pwaButton.style.display = 'none';
}
});
// Initialize dashboard when page loads
document.addEventListener('DOMContentLoaded', function () {
// Show PWA hint for iOS users
if (isIOS() && !isPWAInstalled()) {
setTimeout(showIOSPWAHint, 2000); // Show after 2 seconds
}
});
</script>
</body>
</html>

362
public/datenschutz.html Normal file
View File

@@ -0,0 +1,362 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Datenschutzerklärung - NinjaCross</title>
<link rel="icon" type="image/x-icon" href="/pictures/favicon.ico">
<link rel="stylesheet" href="/css/leaderboard.css">
<style>
.legal-container {
max-width: 1000px;
margin: 0 auto;
padding: 2rem;
min-height: 100vh;
}
.legal-header {
text-align: center;
margin-bottom: 3rem;
}
.legal-title {
font-size: 3.5rem;
font-weight: 700;
background: linear-gradient(135deg, #00d4ff, #ff6b35, #ffd700);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 0.5rem;
letter-spacing: -0.02em;
}
.legal-subtitle {
font-size: 1.2rem;
color: #8892b0;
font-weight: 300;
}
.legal-content {
background: rgba(30, 41, 59, 0.8);
backdrop-filter: blur(20px);
border: 1px solid rgba(51, 65, 85, 0.3);
border-radius: 20px;
padding: 3rem;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
}
.section {
margin-bottom: 2.5rem;
}
.section h2 {
color: #00d4ff;
font-size: 1.8rem;
margin-bottom: 1rem;
border-bottom: 2px solid #334155;
padding-bottom: 0.5rem;
font-weight: 600;
}
.section h3 {
color: #ff6b35;
font-size: 1.3rem;
margin: 1.5rem 0 0.8rem 0;
font-weight: 500;
}
.section p, .section li {
margin-bottom: 0.8rem;
color: #e2e8f0;
line-height: 1.6;
}
.section ul, .section ol {
margin-left: 1.5rem;
}
.contact-info {
background: rgba(0, 212, 255, 0.1);
border: 1px solid rgba(0, 212, 255, 0.3);
padding: 1.5rem;
border-radius: 12px;
margin: 1rem 0;
}
.contact-info p {
margin-bottom: 0.5rem;
}
.contact-info strong {
color: #00d4ff;
}
.back-button {
display: inline-flex;
align-items: center;
gap: 0.5rem;
background: linear-gradient(135deg, #00d4ff, #0099cc);
color: white;
padding: 12px 24px;
text-decoration: none;
border-radius: 12px;
margin-top: 2rem;
font-weight: 600;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(0, 212, 255, 0.3);
}
.back-button:hover {
background: linear-gradient(135deg, #0099cc, #007aa3);
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 212, 255, 0.4);
}
.highlight-box {
background: rgba(255, 107, 53, 0.1);
border: 1px solid rgba(255, 107, 53, 0.3);
padding: 1.5rem;
border-radius: 12px;
margin: 1.5rem 0;
}
.highlight-box h3 {
color: #ff6b35;
margin-top: 0;
}
.highlight-box p {
color: #e2e8f0;
margin: 0;
}
.cookie-table {
width: 100%;
border-collapse: collapse;
margin: 1.5rem 0;
background: rgba(30, 41, 59, 0.5);
border-radius: 12px;
overflow: hidden;
}
.cookie-table th, .cookie-table td {
border: 1px solid rgba(51, 65, 85, 0.3);
padding: 1rem;
text-align: left;
}
.cookie-table th {
background: rgba(0, 212, 255, 0.1);
font-weight: 600;
color: #00d4ff;
}
.cookie-table td {
color: #e2e8f0;
}
@media (max-width: 768px) {
.legal-container {
padding: 1rem;
}
.legal-content {
padding: 1.5rem;
}
.legal-title {
font-size: 2.5rem;
}
}
</style>
</head>
<body>
<div class="legal-container">
<div class="legal-header">
<h1 class="legal-title">🔒 Datenschutzerklärung</h1>
<p class="legal-subtitle">NinjaCross - Speedrun Arena</p>
</div>
<div class="legal-content">
<div class="section">
<h2>1. Datenschutz auf einen Blick</h2>
<h3>Allgemeine Hinweise</h3>
<p>Die folgenden Hinweise geben einen einfachen Überblick darüber, was mit Ihren personenbezogenen Daten passiert, wenn Sie diese Website besuchen. Personenbezogene Daten sind alle Daten, mit denen Sie persönlich identifiziert werden können. Ausführliche Informationen zum Thema Datenschutz entnehmen Sie unserer unter diesem Text aufgeführten Datenschutzerklärung.</p>
</div>
<div class="section">
<h2>2. Datenerfassung auf dieser Website</h2>
<h3>Wer ist verantwortlich für die Datenerfassung auf dieser Website?</h3>
<p>Die Datenverarbeitung auf dieser Website erfolgt durch den Websitebetreiber. Dessen Kontaktdaten können Sie dem Abschnitt „Hinweis zur Verantwortlichen Stelle" in dieser Datenschutzerklärung entnehmen.</p>
<h3>Wie erfassen wir Ihre Daten?</h3>
<p>Ihre Daten werden zum einen dadurch erhoben, dass Sie uns diese mitteilen. Hierbei kann es sich z. B. um Daten handeln, die Sie in ein Kontaktformular eingeben.</p>
<p>Andere Daten werden automatisch oder nach Ihrer Einwilligung beim Besuch der Website durch unsere IT-Systeme erfasst. Das sind vor allem technische Daten (z. B. Internetbrowser, Betriebssystem oder Uhrzeit des Seitenaufrufs). Die Erfassung dieser Daten erfolgt automatisch, sobald Sie diese Website betreten.</p>
<h3>Wofür nutzen wir Ihre Daten?</h3>
<p>Ein Teil der Daten wird erhoben, um eine fehlerfreie Bereitstellung der Website zu gewährleisten. Andere Daten können zur Analyse Ihres Nutzerverhaltens verwendet werden.</p>
<h3>Welche Rechte haben Sie bezüglich Ihrer Daten?</h3>
<p>Sie haben jederzeit das Recht, unentgeltlich Auskunft über Herkunft, Empfänger und Zweck Ihrer gespeicherten personenbezogenen Daten zu erhalten. Sie haben außerdem ein Recht, die Berichtigung oder Löschung dieser Daten zu verlangen. Wenn Sie eine Einwilligung zur Datenverarbeitung erteilt haben, können Sie diese Einwilligung jederzeit für die Zukunft widerrufen. Außerdem haben Sie das Recht, unter bestimmten Umständen die Einschränkung der Verarbeitung Ihrer personenbezogenen Daten zu verlangen. Des Weiteren steht Ihnen ein Beschwerderecht bei der zuständigen Aufsichtsbehörde zu.</p>
</div>
<div class="section">
<h2>3. Hosting</h2>
<p>Wir hosten die Inhalte unserer Website bei folgendem Anbieter:</p>
<div class="contact-info">
<p><strong>Serverstandort:</strong> Deutschland<br>
<strong>Anbieter:</strong> [Ihr Hosting-Anbieter]<br>
<strong>Datenschutz:</strong> <a href="#" target="_blank" rel="noopener">Datenschutzerklärung des Anbieters</a></p>
</div>
</div>
<div class="section">
<h2>4. Allgemeine Hinweise und Pflichtinformationen</h2>
<h3>Datenschutz</h3>
<p>Die Betreiber dieser Seiten nehmen den Schutz Ihrer persönlichen Daten sehr ernst. Wir behandeln Ihre personenbezogenen Daten vertraulich und entsprechend den gesetzlichen Datenschutzvorschriften sowie dieser Datenschutzerklärung.</p>
<h3>Hinweis zur verantwortlichen Stelle</h3>
<p>Die verantwortliche Stelle für die Datenverarbeitung auf dieser Website ist:</p>
<div class="contact-info">
<p>Max Mustermann<br>
Musterstraße 123<br>
12345 Musterstadt<br>
Deutschland</p>
<p>Telefon: +49 (0) 123 456789<br>
E-Mail: info@ninjacross.de</p>
</div>
<h3>Speicherdauer</h3>
<p>Soweit innerhalb dieser Datenschutzerklärung keine speziellere Speicherdauer genannt wurde, verbleiben Ihre personenbezogenen Daten bei uns, bis der Zweck für die Datenverarbeitung entfällt. Wenn Sie ein berechtigtes Löschersuchen geltend machen oder eine Einwilligung zur Datenverarbeitung widerrufen, werden Ihre Daten gelöscht, sofern wir keine anderen rechtlich zulässigen Gründe für die Speicherung Ihrer personenbezogenen Daten haben (z. B. steuer- oder handelsrechtliche Aufbewahrungsfristen); im letztgenannten Fall erfolgt die Löschung nach Fortfall dieser Gründe.</p>
</div>
<div class="section">
<h2>5. Datenerfassung auf dieser Website</h2>
<h3>Cookies</h3>
<p>Unsere Internetseiten verwenden so genannte „Cookies". Cookies sind kleine Textdateien und richten auf Ihrem Endgerät keinen Schaden an. Sie werden entweder vorübergehend für die Dauer einer Sitzung (Session-Cookies) oder dauerhaft (dauerhafte Cookies) auf Ihrem Endgerät gespeichert. Session-Cookies werden nach Ende Ihres Besuchs automatisch gelöscht. Von dauerhaften Cookies bleibt eine Teil auf Ihrem Endgerät gespeichert, bis Sie diese selbst löschen oder eine automatische Löschung durch Ihren Webbrowser erfolgt.</p>
<p>Teilweise können auch Cookies von Drittanbietern auf Ihrem Endgerät gespeichert werden, wenn Sie unsere Seite betreten (Third-Party-Cookies). Diese ermöglichen uns oder Ihnen die Nutzung bestimmter Dienstleistungen des Drittanbieters (z. B. Cookies zur Abwicklung von Zahlungsdienstleistungen).</p>
<h3>Cookie-Übersicht</h3>
<table class="cookie-table">
<thead>
<tr>
<th>Cookie-Name</th>
<th>Zweck</th>
<th>Speicherdauer</th>
<th>Typ</th>
</tr>
</thead>
<tbody>
<tr>
<td>session</td>
<td>Authentifizierung und Session-Management</td>
<td>24 Stunden</td>
<td>Notwendig</td>
</tr>
<tr>
<td>supabase.auth.token</td>
<td>Benutzer-Authentifizierung (Supabase)</td>
<td>1 Jahr</td>
<td>Funktional</td>
</tr>
<tr>
<td>page_views</td>
<td>Seitenaufruf-Statistiken</td>
<td>30 Tage</td>
<td>Statistik</td>
</tr>
</tbody>
</table>
<h3>Server-Log-Dateien</h3>
<p>Der Provider der Seiten erhebt und speichert automatisch Informationen in so genannten Server-Log-Dateien, die Ihr Browser automatisch an uns übermittelt. Dies sind:</p>
<ul>
<li>Browsertyp und Browserversion</li>
<li>verwendetes Betriebssystem</li>
<li>Referrer URL</li>
<li>Hostname des zugreifenden Rechners</li>
<li>Uhrzeit der Serveranfrage</li>
<li>IP-Adresse</li>
</ul>
<p>Eine Zusammenführung dieser Daten mit anderen Datenquellen wird nicht vorgenommen. Die Erfassung dieser Daten erfolgt auf Grundlage von Art. 6 Abs. 1 lit. f DSGVO.</p>
</div>
<div class="section">
<h2>6. Plugins und Tools</h2>
<h3>Google OAuth</h3>
<p>Diese Website nutzt Google OAuth für die Benutzeranmeldung. Anbieter ist die Google Ireland Limited („Google"), Gordon House, Barrow Street, Dublin 4, Irland.</p>
<p>Wenn Sie sich über Google anmelden, werden Ihre Daten an Google übertragen. Die Nutzung von Google OAuth erfolgt auf Grundlage von Art. 6 Abs. 1 lit. f DSGVO. Weitere Informationen finden Sie in der <a href="https://policies.google.com/privacy" target="_blank" rel="noopener">Datenschutzerklärung von Google</a>.</p>
<h3>Supabase</h3>
<p>Diese Website nutzt Supabase für die Benutzerauthentifizierung und Datenbankdienste. Anbieter ist Supabase Inc., 970 Toa Payoh North #07-04, Singapore 318992.</p>
<p>Weitere Informationen finden Sie in der <a href="https://supabase.com/privacy" target="_blank" rel="noopener">Datenschutzerklärung von Supabase</a>.</p>
</div>
<div class="section">
<h2>7. Ihre Rechte</h2>
<h3>Recht auf Auskunft</h3>
<p>Sie haben das Recht, jederzeit Auskunft über die von uns über Sie gespeicherten personenbezogenen Daten zu verlangen.</p>
<h3>Recht auf Berichtigung</h3>
<p>Sie haben das Recht, die Berichtigung unrichtiger oder die Vervollständigung unvollständiger Daten zu verlangen.</p>
<h3>Recht auf Löschung</h3>
<p>Sie haben das Recht, die Löschung Ihrer personenbezogenen Daten zu verlangen.</p>
<h3>Recht auf Einschränkung der Verarbeitung</h3>
<p>Sie haben das Recht, die Einschränkung der Verarbeitung Ihrer personenbezogenen Daten zu verlangen.</p>
<h3>Recht auf Datenübertragbarkeit</h3>
<p>Sie haben das Recht, die Sie betreffenden personenbezogenen Daten in einem strukturierten, gängigen und maschinenlesbaren Format zu erhalten.</p>
<h3>Widerruf Ihrer Einwilligung zur Datenverarbeitung</h3>
<p>Viele Datenverarbeitungsvorgänge sind nur mit Ihrer ausdrücklichen Einwilligung möglich. Sie können eine bereits erteilte Einwilligung jederzeit widerrufen. Die Rechtmäßigkeit der bis zum Widerruf erfolgten Datenverarbeitung bleibt vom Widerruf unberührt.</p>
<h3>Recht auf Beschwerde bei der zuständigen Aufsichtsbehörde</h3>
<p>Im Falle von Verstößen gegen die DSGVO steht den Betroffenen ein Beschwerderecht bei einer Aufsichtsbehörde, insbesondere in dem Mitgliedstaat ihres gewöhnlichen Aufenthalts, ihres Arbeitsplatzes oder des Orts des mutmaßlichen Verstoßes zu. Das Beschwerderecht besteht unbeschadet anderweitiger verwaltungsrechtlicher oder gerichtlicher Rechtsbehelfe.</p>
</div>
<div class="section">
<h2>8. Kontakt</h2>
<p>Bei Fragen zum Datenschutz wenden Sie sich bitte an:</p>
<div class="contact-info">
<p>E-Mail: datenschutz@ninjacross.de<br>
Telefon: +49 (0) 123 456789</p>
</div>
</div>
<a href="/" class="back-button">← Zurück zur Startseite</a>
</div>
</div>
<!-- Footer -->
<footer class="footer">
<div class="footer-content">
<div class="footer-links">
<a href="/impressum.html" class="footer-link">Impressum</a>
<a href="/datenschutz.html" class="footer-link">Datenschutz</a>
<button id="cookie-settings-footer" class="footer-link cookie-settings-btn">Cookie-Einstellungen</button>
</div>
<div class="footer-text">
<p>&copy; 2024 NinjaCross. Alle Rechte vorbehalten.</p>
</div>
</div>
</footer>
<script src="/js/cookie-consent.js"></script>
<script>
// Add cookie settings button functionality
document.addEventListener('DOMContentLoaded', function() {
const cookieSettingsBtn = document.getElementById('cookie-settings-footer');
if (cookieSettingsBtn) {
cookieSettingsBtn.addEventListener('click', function() {
if (window.cookieConsent) {
window.cookieConsent.resetConsent();
}
});
}
});
</script>
</body>
</html>

View File

@@ -0,0 +1,181 @@
# E-Mail-Client-Kompatibilität Guide
## 🚨 Problem: Farben sehen in E-Mail-Clients komisch aus
E-Mail-Clients sind sehr restriktiv mit CSS und unterstützen oft keine modernen Features. Hier sind die Lösungen:
## ❌ Was E-Mail-Clients NICHT unterstützen:
### CSS-Features
- **Gradients** (`linear-gradient`, `radial-gradient`)
- **Backdrop-Filter** (`backdrop-filter: blur()`)
- **Box-Shadow** (komplexe Schatten)
- **Transforms** (`transform: translateY()`)
- **Custom Fonts** (Google Fonts, Inter)
- **Flexbox/Grid** (begrenzte Unterstützung)
- **CSS-Variablen** (`--custom-property`)
### Farben
- **Transparente Hintergründe** (`rgba()` mit Alpha)
- **Komplexe Farbverläufe**
- **Moderne CSS-Farben** (HSL, etc.)
## ✅ E-Mail-Client-kompatible Alternativen:
### 1. Einfache Hintergrundfarben
```css
/* ❌ Nicht kompatibel */
background: linear-gradient(135deg, #00d4ff, #0891b2);
/* ✅ Kompatibel */
background-color: #00d4ff;
```
### 2. Einfache Borders
```css
/* ❌ Nicht kompatibel */
border: 1px solid rgba(51, 65, 85, 0.3);
/* ✅ Kompatibel */
border: 1px solid #334155;
```
### 3. Standard-Fonts
```css
/* ❌ Nicht kompatibel */
font-family: 'Inter', sans-serif;
/* ✅ Kompatibel */
font-family: Arial, sans-serif;
```
### 4. Einfache Container
```css
/* ❌ Nicht kompatibel */
background: rgba(30, 41, 59, 0.95);
backdrop-filter: blur(20px);
/* ✅ Kompatibel */
background-color: #1e293b;
```
## 🎨 Optimierte Farbpalette für E-Mail:
### Hauptfarben
- **Hintergrund:** `#0a0a0f` (Dunkelblau)
- **Container:** `#1e293b` (Dunkelgrau)
- **Akzent:** `#00d4ff` (Neon-Blau)
- **Text:** `#ffffff` (Weiß)
- **Sekundärtext:** `#cbd5e1` (Hellgrau)
### Status-Farben
- **Erfolg:** `#22c55e` (Grün)
- **Warnung:** `#f59e0b` (Orange)
- **Fehler:** `#ef4444` (Rot)
- **Info:** `#00d4ff` (Blau)
## 📱 Responsive Design für E-Mail:
### Media Queries
```css
@media (max-width: 600px) {
.email-container {
margin: 0 10px;
}
.email-content {
padding: 20px 15px;
}
}
```
### Mobile-First Approach
- **Max-Width:** 600px für Container
- **Padding:** Reduziert auf mobilen Geräten
- **Schriftgrößen:** Angepasst für kleine Bildschirme
## 🔧 Template-Optimierungen:
### 1. Inline-CSS verwenden
```html
<div style="background-color: #1e293b; padding: 20px;">
Inhalt
</div>
```
### 2. Tabellen-Layout für komplexe Strukturen
```html
<table width="100%" cellpadding="0" cellspacing="0">
<tr>
<td style="padding: 20px;">
Inhalt
</td>
</tr>
</table>
```
### 3. Fallback-Farben definieren
```css
background-color: #00d4ff; /* Fallback */
background: linear-gradient(135deg, #00d4ff, #0891b2); /* Modern */
```
## 📊 E-Mail-Client-Test-Matrix:
| Client | Gradients | Backdrop-Filter | Box-Shadow | Custom Fonts |
|--------|-----------|-----------------|------------|--------------|
| Gmail | ❌ | ❌ | ❌ | ❌ |
| Outlook | ❌ | ❌ | ❌ | ❌ |
| Apple Mail | ✅ | ❌ | ✅ | ✅ |
| Thunderbird | ❌ | ❌ | ❌ | ❌ |
| Yahoo Mail | ❌ | ❌ | ❌ | ❌ |
## 🚀 Empfohlene Template-Struktur:
### 1. Kompatible Versionen erstellen
- `welcome-email-compatible.html` - E-Mail-Client-optimiert
- `welcome-email.html` - Moderne Browser-Version
### 2. Fallback-Strategien
- **HTML-Version** für moderne Clients
- **Text-Version** für einfache Clients
- **Kompatible HTML** für E-Mail-Clients
### 3. Testing
- **Litmus** oder **Email on Acid** für E-Mail-Testing
- **Verschiedene Clients** testen
- **Mobile Geräte** berücksichtigen
## 📝 Best Practices:
### 1. Einfache Struktur
- **Minimale CSS** verwenden
- **Inline-Styles** bevorzugen
- **Tabellen-Layout** für komplexe Strukturen
### 2. Farben
- **Hex-Codes** verwenden (`#00d4ff`)
- **Keine Transparenz** (`rgba()` vermeiden)
- **Hoher Kontrast** für Lesbarkeit
### 3. Typografie
- **Web-Safe Fonts** verwenden
- **Fallback-Fonts** definieren
- **Angemessene Schriftgrößen** (mind. 14px)
### 4. Bilder
- **Alt-Text** immer angeben
- **Optimierte Größen** verwenden
- **Fallback-Farben** definieren
## 🎯 Sofortige Lösung:
Verwende die **kompatiblen Versionen** der Templates:
- `welcome-email-compatible.html`
- `reset-password-compatible.html`
Diese verwenden nur E-Mail-Client-kompatible CSS-Features und sollten in allen Clients korrekt angezeigt werden.
---
**Tipp:** Teste immer in verschiedenen E-Mail-Clients, bevor du die Templates produktiv einsetzt! 📧✨

View File

@@ -0,0 +1,157 @@
# NinjaCross E-Mail Templates
Diese E-Mail-Templates sind im gleichen Design wie die NinjaCross-Website erstellt und können in Supabase für die E-Mail-Authentifizierung verwendet werden.
## 📁 Dateien
### Welcome Email (Registrierung)
- `welcome-email.html` - Vollständige HTML-Version mit modernem Design
- `welcome-email-compatible.html` - **EMPFOHLEN** - E-Mail-Client-optimierte Version
- `welcome-email-simple.html` - Vereinfachte HTML-Version für bessere E-Mail-Client-Kompatibilität
- `welcome-email.txt` - Text-Version für E-Mail-Client-Kompatibilität
### Invite User (Einladung)
- `invite-user.html` - HTML-Version für Benutzereinladungen
- `invite-user.txt` - Text-Version für Benutzereinladungen
### Magic Link (Passwortlose Anmeldung)
- `magic-link.html` - HTML-Version für Magic Link Anmeldung
- `magic-link.txt` - Text-Version für Magic Link Anmeldung
### Change Email Address (E-Mail-Adresse ändern)
- `change-email.html` - HTML-Version für E-Mail-Adressen-Änderung
- `change-email.txt` - Text-Version für E-Mail-Adressen-Änderung
### Reset Password (Passwort zurücksetzen)
- `reset-password.html` - HTML-Version für Passwort-Reset
- `reset-password-compatible.html` - E-Mail-Client-optimierte Version
- `reset-password-optimized.html` - **EMPFOHLEN** - Verbesserte Kompatibilität
- `reset-password-table.html` - **MAXIMALE KOMPATIBILITÄT** - Tabellen-basiert
- `reset-password.txt` - Text-Version für Passwort-Reset
### Reauthentication (Erneute Authentifizierung)
- `reauthentication.html` - HTML-Version für erneute Authentifizierung
- `reauthentication.txt` - Text-Version für erneute Authentifizierung
## 🚀 Supabase Setup
### 1. Supabase Dashboard öffnen
1. Gehe zu deinem Supabase-Projekt
2. Navigiere zu **Authentication****Email Templates**
### 2. E-Mail-Templates anpassen
1. Wähle das entsprechende Template aus der Liste:
- **Confirm signup** → `welcome-email-compatible.html` / `welcome-email.txt`
- **Invite user** → `invite-user.html` / `invite-user.txt`
- **Magic Link** → `magic-link.html` / `magic-link.txt`
- **Change Email Address** → `change-email.html` / `change-email.txt`
- **Reset Password** → `reset-password-table.html` / `reset-password.txt`
- **Reauthentication** → `reauthentication.html` / `reauthentication.txt`
2. Ersetze den Standard-HTML-Code mit dem Inhalt aus der entsprechenden `.html` Datei
3. Ersetze den Standard-Text mit dem Inhalt aus der entsprechenden `.txt` Datei
**Empfohlene kompatible Versionen** für bessere E-Mail-Client-Unterstützung
### 3. Template-Variablen
Die folgenden Variablen werden automatisch von Supabase ersetzt:
- `{{ .ConfirmationURL }}` - Link zur E-Mail-Bestätigung/Aktion
- `{{ .SiteURL }}` - URL deiner Website
- `{{ .Email }}` - E-Mail-Adresse des Benutzers
- `{{ .NewEmail }}` - Neue E-Mail-Adresse (nur bei Change Email)
- `{{ .InvitedBy }}` - Name des einladenden Benutzers (nur bei Invite User)
### 4. E-Mail-Provider konfigurieren
Stelle sicher, dass dein E-Mail-Provider in Supabase konfiguriert ist:
- **SMTP Settings** in **Authentication****Settings**
- Oder verwende **Supabase Edge Functions** für erweiterte E-Mail-Funktionen
## 🎨 Design-Features
### Farbpalette
- **Hintergrund:** #0a0a0f (Dunkelblau)
- **Container:** #1e293b (Dunkelgrau)
- **Akzent:** #00d4ff (Neon-Blau)
- **Text:** #e2e8f0 (Hellgrau)
### Typografie
- **Font:** Inter (Google Fonts)
- **Fallback:** Arial, sans-serif
- **Gewichtungen:** 300, 400, 500, 600, 700
### Responsive Design
- **Mobile-optimiert** für alle Bildschirmgrößen
- **Flexible Container** mit max-width
- **Angepasste Schriftgrößen** für mobile Geräte
## 🔧 Anpassungen
### Farben ändern
Suche und ersetze die Hex-Codes in den CSS-Styles:
```css
/* Neon-Blau ändern */
#00d4ff #deine-farbe
#0891b2 #deine-farbe-dunkler
```
### Logo anpassen
Ändere den Logo-Text in der HTML-Datei:
```html
<div class="logo">🥷 DEIN-LOGO</div>
```
### Features hinzufügen/entfernen
Bearbeite den `.features-section` Bereich in der HTML-Datei.
## 📱 E-Mail-Client-Kompatibilität
### Unterstützte Clients
- ✅ Gmail (Web & App)
- ✅ Outlook (Web & App)
- ✅ Apple Mail
- ✅ Thunderbird
- ✅ Yahoo Mail
### Fallback-Strategien
1. **HTML-Version** für moderne Clients
2. **Text-Version** für einfache Clients
3. **Inline-CSS** für bessere Kompatibilität
## 🧪 Testing
### E-Mail-Test
1. Erstelle einen Test-Account
2. Registriere dich mit einer Test-E-Mail
3. Überprüfe das E-Mail-Design in verschiedenen Clients
### Browser-Test
1. Öffne `welcome-email.html` in einem Browser
2. Teste die responsive Darstellung
3. Überprüfe alle Links und Buttons
## 🚨 E-Mail-Client-Kompatibilität
**WICHTIG:** Die ursprünglichen Templates verwenden moderne CSS-Features, die in E-Mail-Clients nicht unterstützt werden.
### Empfohlene Versionen:
- **Welcome Email:** `welcome-email-compatible.html`
- **Reset Password:** `reset-password-table.html` ⭐ (für maximale Kompatibilität)
### Was wurde optimiert:
-**Gradients** → ✅ **Einfache Hintergrundfarben**
-**Backdrop-Filter** → ✅ **Standard-Container**
-**Custom Fonts** → ✅ **Arial, sans-serif**
-**Transparente Farben** → ✅ **Hex-Codes**
Siehe `EMAIL-COMPATIBILITY-GUIDE.md` und `URL-CONFIGURATION-GUIDE.md` für Details.
## 📞 Support
Bei Fragen oder Problemen:
- Überprüfe die Supabase-Dokumentation
- Teste mit verschiedenen E-Mail-Providern
- Verwende E-Mail-Testing-Tools wie Litmus oder Email on Acid
- Verwende die **kompatiblen Versionen** für bessere E-Mail-Client-Unterstützung
---
**Erstellt für NinjaCross Timer Leaderboard** 🥷

View File

@@ -0,0 +1,119 @@
# NinjaCross E-Mail Templates - Übersicht
## 📋 Alle verfügbaren Templates
| Template | HTML-Datei | Text-Datei | Beschreibung |
|----------|------------|------------|--------------|
| **Confirm signup** | `welcome-email.html` | `welcome-email.txt` | Willkommens-E-Mail für neue Registrierungen |
| **Invite user** | `invite-user.html` | `invite-user.txt` | Einladungs-E-Mail für neue Benutzer |
| **Magic Link** | `magic-link.html` | `magic-link.txt` | Passwortlose Anmeldung per Magic Link |
| **Change Email Address** | `change-email.html` | `change-email.txt` | E-Mail-Adressen-Änderung bestätigen |
| **Reset Password** | `reset-password.html` | `reset-password.txt` | Passwort zurücksetzen |
| **Reauthentication** | `reauthentication.html` | `reauthentication.txt` | Erneute Authentifizierung für sensible Aktionen |
## 🎨 Design-Features
### Einheitliche Gestaltung
- **Dunkles Design** mit Neon-Blau-Akzenten (#00d4ff)
- **Gradient-Titel** mit den NinjaCross-Farben
- **Glas-Effekt Container** mit Backdrop-Filter
- **Responsive Design** für alle Geräte
- **Inter-Font** für moderne Typografie
### Sicherheits-Features
- **Zeitlimits** für alle Links (15 Min - 24 Std)
- **Sicherheitshinweise** in jeder E-Mail
- **Klare Call-to-Action Buttons**
- **Warnungen** bei verdächtigen Aktivitäten
## 🔧 Template-Variablen
### Standard-Variablen (alle Templates)
- `{{ .ConfirmationURL }}` - Link zur Bestätigung/Aktion
- `{{ .SiteURL }}` - URL der Website
- `{{ .Email }}` - E-Mail-Adresse des Benutzers
### Spezielle Variablen
- `{{ .NewEmail }}` - Neue E-Mail-Adresse (nur Change Email)
- `{{ .InvitedBy }}` - Name des einladenden Benutzers (nur Invite User)
## 📱 E-Mail-Client-Kompatibilität
### Unterstützte Clients
- ✅ Gmail (Web & App)
- ✅ Outlook (Web & App)
- ✅ Apple Mail
- ✅ Thunderbird
- ✅ Yahoo Mail
- ✅ Mobile E-Mail-Apps
### Fallback-Strategien
1. **HTML-Version** für moderne Clients
2. **Text-Version** für einfache Clients
3. **Inline-CSS** für bessere Kompatibilität
4. **Responsive Design** für mobile Geräte
## 🚀 Supabase Integration
### Setup-Schritte
1. **Supabase Dashboard** → Authentication → Email Templates
2. **Template auswählen** (z.B. "Confirm signup")
3. **HTML-Code ersetzen** mit Inhalt aus `.html` Datei
4. **Text-Code ersetzen** mit Inhalt aus `.txt` Datei
5. **Speichern** und testen
### E-Mail-Provider
- **SMTP Settings** in Authentication → Settings
- **Supabase Edge Functions** für erweiterte Funktionen
- **Custom SMTP** für eigene E-Mail-Server
## 🧪 Testing
### E-Mail-Test
1. **Test-Account** erstellen
2. **Template auslösen** (Registrierung, Reset, etc.)
3. **E-Mail prüfen** in verschiedenen Clients
4. **Links testen** auf Funktionalität
### Browser-Test
1. **HTML-Datei** in Browser öffnen
2. **Responsive Design** testen
3. **Links und Buttons** überprüfen
4. **Design-Konsistenz** sicherstellen
## 📊 Template-Statistiken
### Dateigrößen
- **HTML-Templates**: ~8-12 KB
- **Text-Templates**: ~1-2 KB
- **Gesamt**: ~60 KB für alle Templates
### Performance
- **Ladezeit**: < 1 Sekunde
- **Rendering**: Optimiert für alle Clients
- **Mobile**: Vollständig responsive
## 🔒 Sicherheit
### Link-Sicherheit
- **Zeitlimits**: 15 Min (Reauth) bis 24 Std (Reset)
- **Einmalige Nutzung**: Links werden nach Verwendung ungültig
- **HTTPS**: Alle Links verwenden sichere Verbindungen
### Datenschutz
- **Keine sensiblen Daten** in E-Mail-Inhalten
- **Minimale Informationen** in Templates
- **DSGVO-konform** gestaltet
## 🎯 Nächste Schritte
1. **Supabase konfigurieren** mit neuen Templates
2. **E-Mail-Provider** einrichten
3. **Test-E-Mails** versenden
4. **Design anpassen** falls gewünscht
5. **Monitoring** einrichten für E-Mail-Delivery
---
**Erstellt für NinjaCross Timer Leaderboard** 🥷
**Alle Templates sind bereit für den produktiven Einsatz!**

View File

@@ -0,0 +1,174 @@
# URL-Konfiguration für NinjaCross E-Mails
## 🚨 Problem: Reset-Password E-Mail funktioniert nicht
**Deine Server-URL:** `ninja.reptilfpv.de:3000`
## 🔧 Mögliche Ursachen:
### 1. Supabase URL-Konfiguration
Supabase muss wissen, wohin die Bestätigungslinks führen sollen.
**Lösung:**
1. **Supabase Dashboard****Authentication****URL Configuration**
2. **Site URL** setzen auf: `https://ninja.reptilfpv.de:3000`
3. **Redirect URLs** hinzufügen:
- `https://ninja.reptilfpv.de:3000/**`
- `http://ninja.reptilfpv.de:3000/**` (falls HTTP verwendet wird)
### 2. HTTPS vs HTTP Problem
E-Mail-Clients blockieren oft HTTP-Links in E-Mails.
**Lösung:**
- **HTTPS verwenden** für alle Links
- **SSL-Zertifikat** für `ninja.reptilfpv.de:3000` einrichten
- **Port 3000** in der URL kann problematisch sein
### 3. E-Mail-Client-Sicherheit
Manche E-Mail-Clients blockieren Links mit Ports oder verdächtigen Domains.
**Lösung:**
- **Standard-Ports** verwenden (80 für HTTP, 443 für HTTPS)
- **Subdomain** verwenden: `ninja.reptilfpv.de` ohne Port
- **Reverse Proxy** einrichten (Nginx/Apache)
## 🚀 Empfohlene Lösungen:
### Option 1: HTTPS mit Standard-Port
```
https://ninja.reptilfpv.de
```
- **Port 443** (Standard HTTPS)
- **SSL-Zertifikat** erforderlich
- **Reverse Proxy** (Nginx) einrichten
### Option 2: Subdomain ohne Port
```
https://ninja.reptilfpv.de
```
- **Port 3000** intern weiterleiten
- **Nginx** als Reverse Proxy
- **SSL-Zertifikat** für Subdomain
### Option 3: Hauptdomain verwenden
```
https://reptilfpv.de/ninja
```
- **Hauptdomain** mit Pfad
- **Bessere E-Mail-Client-Kompatibilität**
- **Standard-Ports** verwenden
## 🔧 Nginx Reverse Proxy Setup:
### 1. Nginx-Konfiguration
```nginx
server {
listen 80;
listen 443 ssl;
server_name ninja.reptilfpv.de;
ssl_certificate /path/to/certificate.crt;
ssl_certificate_key /path/to/private.key;
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
}
```
### 2. SSL-Zertifikat
```bash
# Let's Encrypt SSL-Zertifikat
sudo certbot --nginx -d ninja.reptilfpv.de
```
## 📧 E-Mail-Template-Optimierungen:
### 1. Absolute URLs verwenden
```html
<!-- ❌ Relativ -->
<a href="/reset-password">Reset</a>
<!-- ✅ Absolut -->
<a href="https://ninja.reptilfpv.de/reset-password">Reset</a>
```
### 2. HTTPS erzwingen
```html
<!-- ✅ Immer HTTPS verwenden -->
<a href="https://ninja.reptilfpv.de{{ .ConfirmationURL }}">Reset</a>
```
### 3. Fallback-URLs
```html
<!-- ✅ Mit Fallback -->
<a href="{{ .ConfirmationURL }}">Reset</a>
<p>Falls der Link nicht funktioniert: https://ninja.reptilfpv.de</p>
```
## 🧪 Testing:
### 1. E-Mail-Links testen
```bash
# Test-URLs
curl -I https://ninja.reptilfpv.de:3000
curl -I https://ninja.reptilfpv.de
curl -I http://ninja.reptilfpv.de:3000
```
### 2. E-Mail-Client-Test
- **Gmail** - Links in E-Mail testen
- **Outlook** - Sicherheitswarnungen prüfen
- **Apple Mail** - Link-Funktionalität testen
### 3. Supabase-Logs prüfen
- **Authentication Logs** in Supabase Dashboard
- **Fehler-Meldungen** analysieren
- **Redirect-URLs** überprüfen
## 🎯 Sofortige Lösung:
### 1. Supabase konfigurieren
```
Site URL: https://ninja.reptilfpv.de:3000
Redirect URLs:
- https://ninja.reptilfpv.de:3000/**
- https://ninja.reptilfpv.de:3000/auth/callback
```
### 2. Optimierte Templates verwenden
- **`reset-password-optimized.html`** - Verbesserte Kompatibilität
- **`reset-password-table.html`** - Tabellen-basiert für maximale Kompatibilität
### 3. HTTPS einrichten
- **SSL-Zertifikat** für `ninja.reptilfpv.de:3000`
- **Oder** Reverse Proxy mit Standard-Port
## 📞 Debugging:
### 1. E-Mail-Links prüfen
- **Link in E-Mail** kopieren und in Browser testen
- **URL-Struktur** analysieren
- **Redirects** verfolgen
### 2. Supabase-Logs
- **Authentication** → **Logs** in Supabase Dashboard
- **Fehler-Meldungen** suchen
- **URL-Parameter** prüfen
### 3. Browser-Entwicklertools
- **Network-Tab** für Redirects
- **Console** für JavaScript-Fehler
- **Security-Tab** für HTTPS-Probleme
---
**Empfehlung:** Verwende `reset-password-table.html` mit HTTPS und Standard-Port für beste Kompatibilität! 🚀

View File

@@ -0,0 +1,274 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>E-Mail-Adresse ändern - NinjaCross</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', sans-serif;
background: #0a0a0f;
color: #ffffff;
line-height: 1.6;
margin: 0;
padding: 0;
}
.email-container {
max-width: 600px;
margin: 0 auto;
background: #0a0a0f;
background-image:
radial-gradient(circle at 20% 80%, #1a1a2e 0%, transparent 50%),
radial-gradient(circle at 80% 20%, #16213e 0%, transparent 50%),
radial-gradient(circle at 40% 40%, #0f3460 0%, transparent 50%);
}
.email-header {
text-align: center;
padding: 3rem 2rem 2rem;
}
.logo {
font-size: 2.5rem;
font-weight: 700;
background: linear-gradient(135deg, #00d4ff, #ff6b35, #ffd700);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 0.5rem;
letter-spacing: -0.02em;
}
.tagline {
color: #94a3b8;
font-size: 1rem;
font-weight: 400;
}
.email-content {
background: rgba(30, 41, 59, 0.95);
backdrop-filter: blur(20px);
border: 1px solid rgba(51, 65, 85, 0.3);
margin: 0 2rem;
padding: 2.5rem;
border-radius: 1.5rem;
box-shadow:
0 25px 50px rgba(0, 0, 0, 0.3),
0 0 0 1px rgba(0, 212, 255, 0.1);
}
.change-title {
font-size: 1.75rem;
font-weight: 600;
color: #e2e8f0;
text-align: center;
margin-bottom: 1.5rem;
}
.change-message {
color: #cbd5e1;
font-size: 1rem;
margin-bottom: 2rem;
text-align: center;
}
.email-info {
background: rgba(51, 65, 85, 0.3);
border: 1px solid rgba(0, 212, 255, 0.1);
border-radius: 0.75rem;
padding: 1.5rem;
margin-bottom: 2rem;
}
.email-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 0;
border-bottom: 1px solid rgba(51, 65, 85, 0.5);
}
.email-row:last-child {
border-bottom: none;
}
.email-label {
color: #94a3b8;
font-size: 0.9rem;
font-weight: 500;
}
.email-value {
color: #e2e8f0;
font-size: 0.9rem;
font-weight: 600;
}
.cta-button {
display: inline-block;
width: 100%;
padding: 1rem 2rem;
background: linear-gradient(135deg, #00d4ff, #0891b2);
color: white;
text-decoration: none;
border-radius: 0.75rem;
font-weight: 600;
font-size: 1rem;
text-align: center;
text-transform: uppercase;
letter-spacing: 0.05em;
box-shadow: 0 4px 12px rgba(0, 212, 255, 0.3);
transition: all 0.2s ease;
margin-bottom: 2rem;
}
.cta-button:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0, 212, 255, 0.4);
}
.warning-info {
background: rgba(245, 158, 11, 0.1);
border: 1px solid rgba(245, 158, 11, 0.3);
border-radius: 0.75rem;
padding: 1rem;
margin-top: 2rem;
}
.warning-title {
color: #f59e0b;
font-weight: 600;
font-size: 0.9rem;
margin-bottom: 0.5rem;
text-align: center;
}
.warning-text {
color: #fbbf24;
font-size: 0.85rem;
text-align: center;
}
.email-footer {
text-align: center;
padding: 2rem;
color: #64748b;
font-size: 0.875rem;
}
.footer-links {
margin-top: 1rem;
}
.footer-links a {
color: #00d4ff;
text-decoration: none;
margin: 0 1rem;
}
.footer-links a:hover {
color: #0891b2;
}
.divider {
height: 1px;
background: linear-gradient(90deg, transparent, #334155, transparent);
margin: 2rem 0;
}
/* Mobile Responsive */
@media (max-width: 600px) {
.email-content {
margin: 0 1rem;
padding: 2rem;
}
.email-header {
padding: 2rem 1rem 1rem;
}
.logo {
font-size: 2rem;
}
.change-title {
font-size: 1.5rem;
}
.email-row {
flex-direction: column;
align-items: flex-start;
gap: 0.25rem;
}
}
</style>
</head>
<body>
<div class="email-container">
<!-- Header -->
<div class="email-header">
<div class="logo">🥷 NINJACROSS</div>
<div class="tagline">Die ultimative Timer-Rangliste</div>
</div>
<!-- Main Content -->
<div class="email-content">
<h1 class="change-title">E-Mail-Adresse ändern 📧</h1>
<p class="change-message">
Du möchtest deine E-Mail-Adresse ändern. Bestätige die neue E-Mail-Adresse,
um die Änderung abzuschließen.
</p>
<div class="email-info">
<div class="email-row">
<span class="email-label">Aktuelle E-Mail:</span>
<span class="email-value">{{ .Email }}</span>
</div>
<div class="email-row">
<span class="email-label">Neue E-Mail:</span>
<span class="email-value">{{ .NewEmail }}</span>
</div>
</div>
<a href="{{ .ConfirmationURL }}" class="cta-button">
✅ E-Mail-Adresse bestätigen
</a>
<div class="warning-info">
<div class="warning-title">⚠️ Wichtiger Hinweis</div>
<div class="warning-text">
Nach der Bestätigung wird deine neue E-Mail-Adresse für alle zukünftigen
Benachrichtigungen und Anmeldungen verwendet.
</div>
</div>
</div>
<!-- Footer -->
<div class="email-footer">
<p>
Falls du diese Änderung nicht angefordert hast, kannst du diese E-Mail ignorieren.
</p>
<div class="footer-links">
<a href="{{ .SiteURL }}">Zur Website</a>
<a href="{{ .SiteURL }}/support">Support</a>
<a href="{{ .SiteURL }}/privacy">Datenschutz</a>
</div>
<p style="margin-top: 1.5rem; font-size: 0.75rem; color: #64748b;">
© 2024 NinjaCross. Alle Rechte vorbehalten.
</p>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,29 @@
🥷 NINJACROSS - Die ultimative Timer-Rangliste
================================================
E-Mail-Adresse ändern 📧
Du möchtest deine E-Mail-Adresse ändern. Bestätige die neue E-Mail-Adresse,
um die Änderung abzuschließen.
📧 E-Mail-Informationen:
- Aktuelle E-Mail: {{ .Email }}
- Neue E-Mail: {{ .NewEmail }}
✅ E-Mail-Adresse bestätigen:
{{ .ConfirmationURL }}
⚠️ Wichtiger Hinweis:
Nach der Bestätigung wird deine neue E-Mail-Adresse für alle zukünftigen
Benachrichtigungen und Anmeldungen verwendet.
================================================
Falls du diese Änderung nicht angefordert hast, kannst du diese E-Mail ignorieren.
Links:
- Zur Website: {{ .SiteURL }}
- Support: {{ .SiteURL }}/support
- Datenschutz: {{ .SiteURL }}/privacy
© 2024 NinjaCross. Alle Rechte vorbehalten.

View File

@@ -0,0 +1,300 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Einladung zu NinjaCross</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', sans-serif;
background: #0a0a0f;
color: #ffffff;
line-height: 1.6;
margin: 0;
padding: 0;
}
.email-container {
max-width: 600px;
margin: 0 auto;
background: #0a0a0f;
background-image:
radial-gradient(circle at 20% 80%, #1a1a2e 0%, transparent 50%),
radial-gradient(circle at 80% 20%, #16213e 0%, transparent 50%),
radial-gradient(circle at 40% 40%, #0f3460 0%, transparent 50%);
}
.email-header {
text-align: center;
padding: 3rem 2rem 2rem;
}
.logo {
font-size: 2.5rem;
font-weight: 700;
background: linear-gradient(135deg, #00d4ff, #ff6b35, #ffd700);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 0.5rem;
letter-spacing: -0.02em;
}
.tagline {
color: #94a3b8;
font-size: 1rem;
font-weight: 400;
}
.email-content {
background: rgba(30, 41, 59, 0.95);
backdrop-filter: blur(20px);
border: 1px solid rgba(51, 65, 85, 0.3);
margin: 0 2rem;
padding: 2.5rem;
border-radius: 1.5rem;
box-shadow:
0 25px 50px rgba(0, 0, 0, 0.3),
0 0 0 1px rgba(0, 212, 255, 0.1);
}
.invite-title {
font-size: 1.75rem;
font-weight: 600;
color: #e2e8f0;
text-align: center;
margin-bottom: 1.5rem;
}
.invite-message {
color: #cbd5e1;
font-size: 1rem;
margin-bottom: 2rem;
text-align: center;
}
.inviter-info {
background: rgba(51, 65, 85, 0.3);
border: 1px solid rgba(0, 212, 255, 0.1);
border-radius: 0.75rem;
padding: 1.5rem;
margin-bottom: 2rem;
text-align: center;
}
.inviter-name {
font-size: 1.25rem;
font-weight: 600;
color: #00d4ff;
margin-bottom: 0.5rem;
}
.inviter-role {
color: #94a3b8;
font-size: 0.9rem;
}
.cta-button {
display: inline-block;
width: 100%;
padding: 1rem 2rem;
background: linear-gradient(135deg, #00d4ff, #0891b2);
color: white;
text-decoration: none;
border-radius: 0.75rem;
font-weight: 600;
font-size: 1rem;
text-align: center;
text-transform: uppercase;
letter-spacing: 0.05em;
box-shadow: 0 4px 12px rgba(0, 212, 255, 0.3);
transition: all 0.2s ease;
margin-bottom: 2rem;
}
.cta-button:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0, 212, 255, 0.4);
}
.features-section {
margin-top: 2rem;
}
.features-title {
font-size: 1.25rem;
font-weight: 600;
color: #e2e8f0;
margin-bottom: 1rem;
text-align: center;
}
.feature-item {
display: flex;
align-items: center;
margin-bottom: 1rem;
padding: 1rem;
background: rgba(51, 65, 85, 0.3);
border-radius: 0.75rem;
border: 1px solid rgba(0, 212, 255, 0.1);
}
.feature-icon {
font-size: 1.5rem;
margin-right: 1rem;
width: 2rem;
text-align: center;
}
.feature-text {
color: #cbd5e1;
font-size: 0.95rem;
}
.email-footer {
text-align: center;
padding: 2rem;
color: #64748b;
font-size: 0.875rem;
}
.footer-links {
margin-top: 1rem;
}
.footer-links a {
color: #00d4ff;
text-decoration: none;
margin: 0 1rem;
}
.footer-links a:hover {
color: #0891b2;
}
.divider {
height: 1px;
background: linear-gradient(90deg, transparent, #334155, transparent);
margin: 2rem 0;
}
/* Mobile Responsive */
@media (max-width: 600px) {
.email-content {
margin: 0 1rem;
padding: 2rem;
}
.email-header {
padding: 2rem 1rem 1rem;
}
.logo {
font-size: 2rem;
}
.invite-title {
font-size: 1.5rem;
}
.feature-item {
flex-direction: column;
text-align: center;
}
.feature-icon {
margin-right: 0;
margin-bottom: 0.5rem;
}
}
</style>
</head>
<body>
<div class="email-container">
<!-- Header -->
<div class="email-header">
<div class="logo">🥷 NINJACROSS</div>
<div class="tagline">Die ultimative Timer-Rangliste</div>
</div>
<!-- Main Content -->
<div class="email-content">
<h1 class="invite-title">Du wurdest eingeladen! 🎉</h1>
<p class="invite-message">
Du wurdest von einem Administrator zu NinjaCross eingeladen.
Klicke auf den Button unten, um dein Konto zu erstellen und der Community beizutreten.
</p>
<div class="inviter-info">
<div class="inviter-name">{{ .InvitedBy }}</div>
<div class="inviter-role">hat dich zu NinjaCross eingeladen</div>
</div>
<a href="{{ .ConfirmationURL }}" class="cta-button">
🚀 Konto erstellen
</a>
<div class="divider"></div>
<!-- Features Section -->
<div class="features-section">
<h2 class="features-title">Was dich erwartet:</h2>
<div class="feature-item">
<div class="feature-icon">🏃‍♂️</div>
<div class="feature-text">
<strong>Timer-Tracking:</strong> Erfasse deine Zeiten und verfolge deinen Fortschritt
</div>
</div>
<div class="feature-item">
<div class="feature-icon">🏆</div>
<div class="feature-text">
<strong>Leaderboards:</strong> Vergleiche dich mit anderen Spielern und erreiche die Spitze
</div>
</div>
<div class="feature-item">
<div class="feature-icon">📊</div>
<div class="feature-text">
<strong>Statistiken:</strong> Detaillierte Analysen deiner Performance und Verbesserungen
</div>
</div>
<div class="feature-item">
<div class="feature-icon">🌍</div>
<div class="feature-text">
<strong>Multi-Location:</strong> Spiele an verschiedenen Standorten und sammle Erfahrungen
</div>
</div>
</div>
</div>
<!-- Footer -->
<div class="email-footer">
<p>
Falls du diese Einladung nicht erwartet hast, kannst du diese E-Mail ignorieren.
</p>
<div class="footer-links">
<a href="{{ .SiteURL }}">Zur Website</a>
<a href="{{ .SiteURL }}/support">Support</a>
<a href="{{ .SiteURL }}/privacy">Datenschutz</a>
</div>
<p style="margin-top: 1.5rem; font-size: 0.75rem; color: #64748b;">
© 2024 NinjaCross. Alle Rechte vorbehalten.
</p>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,34 @@
🥷 NINJACROSS - Die ultimative Timer-Rangliste
================================================
Du wurdest eingeladen! 🎉
Du wurdest von einem Administrator zu NinjaCross eingeladen.
Klicke auf den Link unten, um dein Konto zu erstellen und der Community beizutreten.
👤 Einladung von: {{ .InvitedBy }}
🚀 Konto erstellen:
{{ .ConfirmationURL }}
Was dich erwartet:
==================
🏃‍♂️ Timer-Tracking: Erfasse deine Zeiten und verfolge deinen Fortschritt
🏆 Leaderboards: Vergleiche dich mit anderen Spielern und erreiche die Spitze
📊 Statistiken: Detaillierte Analysen deiner Performance und Verbesserungen
🌍 Multi-Location: Spiele an verschiedenen Standorten und sammle Erfahrungen
================================================
Falls du diese Einladung nicht erwartet hast, kannst du diese E-Mail ignorieren.
Links:
- Zur Website: {{ .SiteURL }}
- Support: {{ .SiteURL }}/support
- Datenschutz: {{ .SiteURL }}/privacy
© 2024 NinjaCross. Alle Rechte vorbehalten.

View File

@@ -0,0 +1,257 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Magic Link - NinjaCross</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', sans-serif;
background: #0a0a0f;
color: #ffffff;
line-height: 1.6;
margin: 0;
padding: 0;
}
.email-container {
max-width: 600px;
margin: 0 auto;
background: #0a0a0f;
background-image:
radial-gradient(circle at 20% 80%, #1a1a2e 0%, transparent 50%),
radial-gradient(circle at 80% 20%, #16213e 0%, transparent 50%),
radial-gradient(circle at 40% 40%, #0f3460 0%, transparent 50%);
}
.email-header {
text-align: center;
padding: 3rem 2rem 2rem;
}
.logo {
font-size: 2.5rem;
font-weight: 700;
background: linear-gradient(135deg, #00d4ff, #ff6b35, #ffd700);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 0.5rem;
letter-spacing: -0.02em;
}
.tagline {
color: #94a3b8;
font-size: 1rem;
font-weight: 400;
}
.email-content {
background: rgba(30, 41, 59, 0.95);
backdrop-filter: blur(20px);
border: 1px solid rgba(51, 65, 85, 0.3);
margin: 0 2rem;
padding: 2.5rem;
border-radius: 1.5rem;
box-shadow:
0 25px 50px rgba(0, 0, 0, 0.3),
0 0 0 1px rgba(0, 212, 255, 0.1);
}
.magic-title {
font-size: 1.75rem;
font-weight: 600;
color: #e2e8f0;
text-align: center;
margin-bottom: 1.5rem;
}
.magic-message {
color: #cbd5e1;
font-size: 1rem;
margin-bottom: 2rem;
text-align: center;
}
.magic-info {
background: rgba(51, 65, 85, 0.3);
border: 1px solid rgba(0, 212, 255, 0.1);
border-radius: 0.75rem;
padding: 1.5rem;
margin-bottom: 2rem;
text-align: center;
}
.magic-icon {
font-size: 3rem;
margin-bottom: 1rem;
display: block;
}
.magic-description {
color: #94a3b8;
font-size: 0.9rem;
margin-bottom: 1rem;
}
.cta-button {
display: inline-block;
width: 100%;
padding: 1rem 2rem;
background: linear-gradient(135deg, #00d4ff, #0891b2);
color: white;
text-decoration: none;
border-radius: 0.75rem;
font-weight: 600;
font-size: 1rem;
text-align: center;
text-transform: uppercase;
letter-spacing: 0.05em;
box-shadow: 0 4px 12px rgba(0, 212, 255, 0.3);
transition: all 0.2s ease;
margin-bottom: 2rem;
}
.cta-button:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0, 212, 255, 0.4);
}
.security-info {
background: rgba(34, 197, 94, 0.1);
border: 1px solid rgba(34, 197, 94, 0.3);
border-radius: 0.75rem;
padding: 1rem;
margin-top: 2rem;
}
.security-title {
color: #22c55e;
font-weight: 600;
font-size: 0.9rem;
margin-bottom: 0.5rem;
text-align: center;
}
.security-text {
color: #86efac;
font-size: 0.85rem;
text-align: center;
}
.email-footer {
text-align: center;
padding: 2rem;
color: #64748b;
font-size: 0.875rem;
}
.footer-links {
margin-top: 1rem;
}
.footer-links a {
color: #00d4ff;
text-decoration: none;
margin: 0 1rem;
}
.footer-links a:hover {
color: #0891b2;
}
.divider {
height: 1px;
background: linear-gradient(90deg, transparent, #334155, transparent);
margin: 2rem 0;
}
/* Mobile Responsive */
@media (max-width: 600px) {
.email-content {
margin: 0 1rem;
padding: 2rem;
}
.email-header {
padding: 2rem 1rem 1rem;
}
.logo {
font-size: 2rem;
}
.magic-title {
font-size: 1.5rem;
}
.magic-icon {
font-size: 2.5rem;
}
}
</style>
</head>
<body>
<div class="email-container">
<!-- Header -->
<div class="email-header">
<div class="logo">🥷 NINJACROSS</div>
<div class="tagline">Die ultimative Timer-Rangliste</div>
</div>
<!-- Main Content -->
<div class="email-content">
<h1 class="magic-title">Dein Magic Link ist da! ✨</h1>
<p class="magic-message">
Du hast einen Magic Link angefordert. Klicke auf den Button unten,
um dich sicher und ohne Passwort bei NinjaCross anzumelden.
</p>
<div class="magic-info">
<span class="magic-icon">🔗</span>
<div class="magic-description">
Dieser Link ist sicher und führt dich direkt zu deinem Konto
</div>
</div>
<a href="{{ .ConfirmationURL }}" class="cta-button">
🚀 Anmelden
</a>
<div class="security-info">
<div class="security-title">🔒 Sicherheitshinweis</div>
<div class="security-text">
Dieser Link ist nur für dich bestimmt und verfällt nach 24 Stunden.
Teile ihn nicht mit anderen Personen.
</div>
</div>
</div>
<!-- Footer -->
<div class="email-footer">
<p>
Falls du diesen Magic Link nicht angefordert hast, kannst du diese E-Mail ignorieren.
</p>
<div class="footer-links">
<a href="{{ .SiteURL }}">Zur Website</a>
<a href="{{ .SiteURL }}/support">Support</a>
<a href="{{ .SiteURL }}/privacy">Datenschutz</a>
</div>
<p style="margin-top: 1.5rem; font-size: 0.75rem; color: #64748b;">
© 2024 NinjaCross. Alle Rechte vorbehalten.
</p>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,25 @@
🥷 NINJACROSS - Die ultimative Timer-Rangliste
================================================
Dein Magic Link ist da! ✨
Du hast einen Magic Link angefordert. Klicke auf den Link unten,
um dich sicher und ohne Passwort bei NinjaCross anzumelden.
🔗 Magic Link:
{{ .ConfirmationURL }}
🔒 Sicherheitshinweis:
Dieser Link ist nur für dich bestimmt und verfällt nach 24 Stunden.
Teile ihn nicht mit anderen Personen.
================================================
Falls du diesen Magic Link nicht angefordert hast, kannst du diese E-Mail ignorieren.
Links:
- Zur Website: {{ .SiteURL }}
- Support: {{ .SiteURL }}/support
- Datenschutz: {{ .SiteURL }}/privacy
© 2024 NinjaCross. Alle Rechte vorbehalten.

View File

@@ -0,0 +1,327 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Erneute Authentifizierung - NinjaCross</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', sans-serif;
background: #0a0a0f;
color: #ffffff;
line-height: 1.6;
margin: 0;
padding: 0;
}
.email-container {
max-width: 600px;
margin: 0 auto;
background: #0a0a0f;
background-image:
radial-gradient(circle at 20% 80%, #1a1a2e 0%, transparent 50%),
radial-gradient(circle at 80% 20%, #16213e 0%, transparent 50%),
radial-gradient(circle at 40% 40%, #0f3460 0%, transparent 50%);
}
.email-header {
text-align: center;
padding: 3rem 2rem 2rem;
}
.logo {
font-size: 2.5rem;
font-weight: 700;
background: linear-gradient(135deg, #00d4ff, #ff6b35, #ffd700);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 0.5rem;
letter-spacing: -0.02em;
}
.tagline {
color: #94a3b8;
font-size: 1rem;
font-weight: 400;
}
.email-content {
background: rgba(30, 41, 59, 0.95);
backdrop-filter: blur(20px);
border: 1px solid rgba(51, 65, 85, 0.3);
margin: 0 2rem;
padding: 2.5rem;
border-radius: 1.5rem;
box-shadow:
0 25px 50px rgba(0, 0, 0, 0.3),
0 0 0 1px rgba(0, 212, 255, 0.1);
}
.reauth-title {
font-size: 1.75rem;
font-weight: 600;
color: #e2e8f0;
text-align: center;
margin-bottom: 1.5rem;
}
.reauth-message {
color: #cbd5e1;
font-size: 1rem;
margin-bottom: 2rem;
text-align: center;
}
.reauth-info {
background: rgba(51, 65, 85, 0.3);
border: 1px solid rgba(0, 212, 255, 0.1);
border-radius: 0.75rem;
padding: 1.5rem;
margin-bottom: 2rem;
text-align: center;
}
.reauth-icon {
font-size: 3rem;
margin-bottom: 1rem;
display: block;
}
.reauth-description {
color: #94a3b8;
font-size: 0.9rem;
margin-bottom: 1rem;
}
.cta-button {
display: inline-block;
width: 100%;
padding: 1rem 2rem;
background: linear-gradient(135deg, #00d4ff, #0891b2);
color: white;
text-decoration: none;
border-radius: 0.75rem;
font-weight: 600;
font-size: 1rem;
text-align: center;
text-transform: uppercase;
letter-spacing: 0.05em;
box-shadow: 0 4px 12px rgba(0, 212, 255, 0.3);
transition: all 0.2s ease;
margin-bottom: 2rem;
}
.cta-button:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0, 212, 255, 0.4);
}
.security-info {
background: rgba(34, 197, 94, 0.1);
border: 1px solid rgba(34, 197, 94, 0.3);
border-radius: 0.75rem;
padding: 1.5rem;
margin-top: 2rem;
}
.security-title {
color: #22c55e;
font-weight: 600;
font-size: 1rem;
margin-bottom: 1rem;
text-align: center;
}
.security-text {
color: #86efac;
font-size: 0.9rem;
text-align: center;
line-height: 1.6;
}
.warning-info {
background: rgba(245, 158, 11, 0.1);
border: 1px solid rgba(245, 158, 11, 0.3);
border-radius: 0.75rem;
padding: 1rem;
margin-top: 1.5rem;
}
.warning-title {
color: #f59e0b;
font-weight: 600;
font-size: 0.9rem;
margin-bottom: 0.5rem;
text-align: center;
}
.warning-text {
color: #fbbf24;
font-size: 0.85rem;
text-align: center;
}
.token-section {
background: rgba(0, 212, 255, 0.1);
border: 1px solid rgba(0, 212, 255, 0.3);
border-radius: 0.75rem;
padding: 1.5rem;
margin: 2rem 0;
text-align: center;
}
.token-display {
margin-top: 1rem;
}
.token-code {
background: #1e293b;
border: 2px solid #00d4ff;
border-radius: 0.5rem;
padding: 1rem;
font-family: 'Courier New', monospace;
font-size: 1.5rem;
font-weight: 700;
color: #00d4ff;
letter-spacing: 0.1em;
text-align: center;
margin: 0 auto;
max-width: 300px;
box-shadow: 0 0 20px rgba(0, 212, 255, 0.2);
}
.email-footer {
text-align: center;
padding: 2rem;
color: #64748b;
font-size: 0.875rem;
}
.footer-links {
margin-top: 1rem;
}
.footer-links a {
color: #00d4ff;
text-decoration: none;
margin: 0 1rem;
}
.footer-links a:hover {
color: #0891b2;
}
.divider {
height: 1px;
background: linear-gradient(90deg, transparent, #334155, transparent);
margin: 2rem 0;
}
/* Mobile Responsive */
@media (max-width: 600px) {
.email-content {
margin: 0 1rem;
padding: 2rem;
}
.email-header {
padding: 2rem 1rem 1rem;
}
.logo {
font-size: 2rem;
}
.reauth-title {
font-size: 1.5rem;
}
.reauth-icon {
font-size: 2.5rem;
}
}
</style>
</head>
<body>
<div class="email-container">
<!-- Header -->
<div class="email-header">
<div class="logo">🥷 NINJACROSS</div>
<div class="tagline">Die ultimative Timer-Rangliste</div>
</div>
<!-- Main Content -->
<div class="email-content">
<h1 class="reauth-title">Erneute Authentifizierung erforderlich 🔒</h1>
<p class="reauth-message">
Für diese Aktion ist eine erneute Authentifizierung erforderlich.
Klicke auf den Button unten, um deine Identität zu bestätigen.
</p>
<div class="reauth-info">
<span class="reauth-icon">🛡️</span>
<div class="reauth-description">
Diese zusätzliche Sicherheitsmaßnahme schützt dein Konto
</div>
</div>
<!-- Token Section -->
<div class="token-section">
<h2 style="color: #e2e8f0; text-align: center; margin-bottom: 1rem; font-size: 1.25rem;">Bestätigungscode</h2>
<div class="token-display">
<p style="color: #cbd5e1; text-align: center; margin-bottom: 0.5rem;">Gib diesen Code ein:</p>
<div class="token-code">{{ .Token }}</div>
</div>
</div>
<a href="{{ .ConfirmationURL }}" class="cta-button">
🔐 Identität bestätigen
</a>
<div class="security-info">
<div class="security-title">🔒 Warum ist das nötig?</div>
<div class="security-text">
Bestimmte Aktionen wie das Ändern sensibler Daten oder das Zugreifen auf
administrative Funktionen erfordern eine erneute Authentifizierung,
um die Sicherheit deines Kontos zu gewährleisten.
</div>
</div>
<div class="warning-info">
<div class="warning-title">⏰ Zeitlimit</div>
<div class="warning-text">
Dieser Link verfällt nach 15 Minuten. Falls du diese Aktion nicht
angefordert hast, kannst du diese E-Mail ignorieren.
</div>
</div>
</div>
<!-- Footer -->
<div class="email-footer">
<p>
Falls du diese Aktion nicht angefordert hast, kannst du diese E-Mail ignorieren.
</p>
<div class="footer-links">
<a href="{{ .SiteURL }}">Zur Website</a>
<a href="{{ .SiteURL }}/support">Support</a>
<a href="{{ .SiteURL }}/privacy">Datenschutz</a>
</div>
<p style="margin-top: 1.5rem; font-size: 0.75rem; color: #64748b;">
© 2024 NinjaCross. Alle Rechte vorbehalten.
</p>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,30 @@
🥷 NINJACROSS - Die ultimative Timer-Rangliste
================================================
Erneute Authentifizierung erforderlich 🔒
Für diese Aktion ist eine erneute Authentifizierung erforderlich.
Klicke auf den Link unten, um deine Identität zu bestätigen.
🛡️ Identität bestätigen:
{{ .ConfirmationURL }}
🔒 Warum ist das nötig?
Bestimmte Aktionen wie das Ändern sensibler Daten oder das Zugreifen auf
administrative Funktionen erfordern eine erneute Authentifizierung,
um die Sicherheit deines Kontos zu gewährleisten.
⏰ Zeitlimit:
Dieser Link verfällt nach 15 Minuten. Falls du diese Aktion nicht
angefordert hast, kannst du diese E-Mail ignorieren.
================================================
Falls du diese Aktion nicht angefordert hast, kannst du diese E-Mail ignorieren.
Links:
- Zur Website: {{ .SiteURL }}
- Support: {{ .SiteURL }}/support
- Datenschutz: {{ .SiteURL }}/privacy
© 2024 NinjaCross. Alle Rechte vorbehalten.

View File

@@ -0,0 +1,249 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Passwort zurücksetzen - NinjaCross</title>
<style>
/* E-Mail-Client-kompatible Styles */
body {
font-family: Arial, sans-serif;
background-color: #0a0a0f;
color: #ffffff;
margin: 0;
padding: 20px;
line-height: 1.6;
}
.email-container {
max-width: 600px;
margin: 0 auto;
background-color: #1e293b;
border: 2px solid #334155;
border-radius: 15px;
overflow: hidden;
}
.email-header {
background-color: #00d4ff;
padding: 30px 20px;
text-align: center;
}
.logo {
font-size: 28px;
font-weight: bold;
color: #ffffff;
margin-bottom: 5px;
}
.tagline {
color: #e2e8f0;
font-size: 14px;
}
.email-content {
padding: 30px 20px;
background-color: #1e293b;
}
.reset-title {
font-size: 24px;
font-weight: bold;
color: #e2e8f0;
text-align: center;
margin-bottom: 20px;
}
.reset-message {
color: #cbd5e1;
font-size: 16px;
text-align: center;
margin-bottom: 30px;
}
.reset-info {
background-color: #334155;
border: 1px solid #475569;
border-radius: 10px;
padding: 20px;
margin-bottom: 30px;
text-align: center;
}
.reset-icon {
font-size: 40px;
margin-bottom: 10px;
display: block;
}
.reset-description {
color: #94a3b8;
font-size: 14px;
}
.cta-button {
display: block;
width: 100%;
max-width: 300px;
margin: 0 auto 30px;
padding: 15px 30px;
background-color: #00d4ff;
color: #ffffff;
text-decoration: none;
border-radius: 10px;
font-weight: bold;
font-size: 16px;
text-align: center;
text-transform: uppercase;
}
.security-tips {
background-color: #22c55e;
border: 1px solid #16a34a;
border-radius: 10px;
padding: 20px;
margin-top: 30px;
}
.security-title {
color: #ffffff;
font-weight: bold;
font-size: 16px;
margin-bottom: 15px;
text-align: center;
}
.security-list {
color: #ffffff;
font-size: 14px;
line-height: 1.8;
}
.security-list li {
margin-bottom: 8px;
}
.warning-info {
background-color: #ef4444;
border: 1px solid #dc2626;
border-radius: 10px;
padding: 15px;
margin-top: 20px;
}
.warning-title {
color: #ffffff;
font-weight: bold;
font-size: 14px;
margin-bottom: 8px;
text-align: center;
}
.warning-text {
color: #ffffff;
font-size: 13px;
text-align: center;
}
.email-footer {
background-color: #0f172a;
padding: 20px;
text-align: center;
color: #64748b;
font-size: 12px;
}
.footer a {
color: #00d4ff;
text-decoration: none;
margin: 0 10px;
}
/* Mobile Responsive */
@media (max-width: 600px) {
.email-container {
margin: 0 10px;
}
.email-content {
padding: 20px 15px;
}
.logo {
font-size: 24px;
}
.reset-title {
font-size: 20px;
}
}
</style>
</head>
<body>
<div class="email-container">
<!-- Header -->
<div class="email-header">
<div class="logo">🥷 NINJACROSS</div>
<div class="tagline">Die ultimative Timer-Rangliste</div>
</div>
<!-- Content -->
<div class="email-content">
<h1 class="reset-title">Passwort zurücksetzen 🔐</h1>
<p class="reset-message">
Du hast eine Anfrage zum Zurücksetzen deines Passworts gestellt.
Klicke auf den Button unten, um ein neues Passwort zu erstellen.
</p>
<div class="reset-info">
<span class="reset-icon">🔑</span>
<div class="reset-description">
Dieser Link ist sicher und führt dich zur Passwort-Reset-Seite
</div>
</div>
<a href="{{ .ConfirmationURL }}" class="cta-button">
🔄 Passwort zurücksetzen
</a>
<div class="security-tips">
<div class="security-title">🛡️ Tipps für ein sicheres Passwort:</div>
<ul class="security-list">
<li>• Verwende mindestens 8 Zeichen</li>
<li>• Kombiniere Groß- und Kleinbuchstaben</li>
<li>• Füge Zahlen und Sonderzeichen hinzu</li>
<li>• Verwende keine persönlichen Informationen</li>
<li>• Nutze ein einzigartiges Passwort nur für NinjaCross</li>
</ul>
</div>
<div class="warning-info">
<div class="warning-title">⚠️ Sicherheitshinweis</div>
<div class="warning-text">
Dieser Link verfällt nach 24 Stunden. Falls du diese Anfrage nicht gestellt hast,
kannst du diese E-Mail ignorieren.
</div>
</div>
</div>
<!-- Footer -->
<div class="email-footer">
<p>
Falls du diese Anfrage nicht gestellt hast, kannst du diese E-Mail ignorieren.
</p>
<p style="margin-top: 15px;">
<a href="{{ .SiteURL }}">Zur Website</a>
<a href="{{ .SiteURL }}/support">Support</a>
<a href="{{ .SiteURL }}/privacy">Datenschutz</a>
</p>
<p style="margin-top: 15px;">
© 2024 NinjaCross. Alle Rechte vorbehalten.
</p>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,301 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Passwort zurücksetzen - NinjaCross</title>
<!--[if mso]>
<noscript>
<xml>
<o:OfficeDocumentSettings>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
</noscript>
<![endif]-->
<style type="text/css">
/* Reset styles for email clients */
body, table, td, p, a, li, blockquote {
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
table, td {
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
-ms-interpolation-mode: bicubic;
border: 0;
height: auto;
line-height: 100%;
outline: none;
text-decoration: none;
}
/* Main styles */
body {
font-family: Arial, sans-serif;
background-color: #0a0a0f;
color: #ffffff;
margin: 0;
padding: 0;
line-height: 1.6;
}
.email-wrapper {
width: 100%;
background-color: #0a0a0f;
padding: 20px 0;
}
.email-container {
max-width: 600px;
margin: 0 auto;
background-color: #1e293b;
border: 2px solid #334155;
}
.email-header {
background-color: #00d4ff;
padding: 30px 20px;
text-align: center;
}
.logo {
font-size: 28px;
font-weight: bold;
color: #ffffff;
margin: 0 0 5px 0;
}
.tagline {
color: #e2e8f0;
font-size: 14px;
margin: 0;
}
.email-content {
padding: 30px 20px;
background-color: #1e293b;
}
.reset-title {
font-size: 24px;
font-weight: bold;
color: #ffffff;
text-align: center;
margin: 0 0 20px 0;
}
.reset-message {
color: #cbd5e1;
font-size: 16px;
text-align: center;
margin: 0 0 30px 0;
}
.reset-info {
background-color: #334155;
border: 1px solid #475569;
padding: 20px;
margin: 0 0 30px 0;
text-align: center;
}
.reset-icon {
font-size: 40px;
margin: 0 0 10px 0;
display: block;
}
.reset-description {
color: #94a3b8;
font-size: 14px;
margin: 0;
}
.cta-button {
display: block;
width: 100%;
max-width: 300px;
margin: 0 auto 30px;
padding: 15px 30px;
background-color: #00d4ff;
color: #ffffff;
text-decoration: none;
font-weight: bold;
font-size: 16px;
text-align: center;
text-transform: uppercase;
}
.cta-button:hover {
background-color: #0891b2;
}
.security-tips {
background-color: #22c55e;
border: 1px solid #16a34a;
padding: 20px;
margin: 30px 0 0 0;
}
.security-title {
color: #ffffff;
font-weight: bold;
font-size: 16px;
margin: 0 0 15px 0;
text-align: center;
}
.security-list {
color: #ffffff;
font-size: 14px;
line-height: 1.8;
margin: 0;
padding: 0;
}
.security-list li {
margin: 0 0 8px 0;
list-style: none;
}
.warning-info {
background-color: #ef4444;
border: 1px solid #dc2626;
padding: 15px;
margin: 20px 0 0 0;
}
.warning-title {
color: #ffffff;
font-weight: bold;
font-size: 14px;
margin: 0 0 8px 0;
text-align: center;
}
.warning-text {
color: #ffffff;
font-size: 13px;
text-align: center;
margin: 0;
}
.email-footer {
background-color: #0f172a;
padding: 20px;
text-align: center;
color: #64748b;
font-size: 12px;
}
.footer a {
color: #00d4ff;
text-decoration: none;
margin: 0 10px;
}
.footer a:hover {
color: #0891b2;
}
/* Mobile styles */
@media only screen and (max-width: 600px) {
.email-wrapper {
padding: 10px 0;
}
.email-container {
margin: 0 10px;
}
.email-content {
padding: 20px 15px;
}
.logo {
font-size: 24px;
}
.reset-title {
font-size: 20px;
}
.cta-button {
padding: 12px 20px;
font-size: 14px;
}
}
</style>
</head>
<body>
<div class="email-wrapper">
<div class="email-container">
<!-- Header -->
<div class="email-header">
<div class="logo">🥷 NINJACROSS</div>
<div class="tagline">Die ultimative Timer-Rangliste</div>
</div>
<!-- Content -->
<div class="email-content">
<h1 class="reset-title">Passwort zurücksetzen 🔐</h1>
<p class="reset-message">
Du hast eine Anfrage zum Zurücksetzen deines Passworts gestellt.
Klicke auf den Button unten, um ein neues Passwort zu erstellen.
</p>
<div class="reset-info">
<span class="reset-icon">🔑</span>
<div class="reset-description">
Dieser Link ist sicher und führt dich zur Passwort-Reset-Seite
</div>
</div>
<a href="{{ .ConfirmationURL }}" class="cta-button">
🔄 Passwort zurücksetzen
</a>
<div class="security-tips">
<div class="security-title">🛡️ Tipps für ein sicheres Passwort:</div>
<ul class="security-list">
<li>• Verwende mindestens 8 Zeichen</li>
<li>• Kombiniere Groß- und Kleinbuchstaben</li>
<li>• Füge Zahlen und Sonderzeichen hinzu</li>
<li>• Verwende keine persönlichen Informationen</li>
<li>• Nutze ein einzigartiges Passwort nur für NinjaCross</li>
</ul>
</div>
<div class="warning-info">
<div class="warning-title">⚠️ Sicherheitshinweis</div>
<div class="warning-text">
Dieser Link verfällt nach 24 Stunden. Falls du diese Anfrage nicht gestellt hast,
kannst du diese E-Mail ignorieren.
</div>
</div>
</div>
<!-- Footer -->
<div class="email-footer">
<p>
Falls du diese Anfrage nicht gestellt hast, kannst du diese E-Mail ignorieren.
</p>
<p style="margin-top: 15px;">
<a href="https://ninja.reptilfpv.de:3000">Zur Website</a>
<a href="https://ninja.reptilfpv.de:3000/support">Support</a>
<a href="https://ninja.reptilfpv.de:3000/privacy">Datenschutz</a>
</p>
<p style="margin-top: 15px;">
© 2024 NinjaCross. Alle Rechte vorbehalten.
</p>
</div>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,323 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Passwort zurücksetzen - NinjaCross</title>
<!--[if mso]>
<noscript>
<xml>
<o:OfficeDocumentSettings>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
</noscript>
<![endif]-->
<style type="text/css">
/* Reset styles */
body, table, td, p, a, li, blockquote {
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
table, td {
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
-ms-interpolation-mode: bicubic;
border: 0;
height: auto;
line-height: 100%;
outline: none;
text-decoration: none;
}
/* Main styles */
body {
font-family: Arial, sans-serif;
background-color: #0a0a0f;
color: #ffffff;
margin: 0;
padding: 0;
line-height: 1.6;
}
.email-wrapper {
width: 100%;
background-color: #0a0a0f;
padding: 20px 0;
}
.email-container {
max-width: 600px;
margin: 0 auto;
background-color: #1e293b;
border: 2px solid #334155;
}
.email-header {
background-color: #00d4ff;
padding: 30px 20px;
text-align: center;
}
.logo {
font-size: 28px;
font-weight: bold;
color: #ffffff;
margin: 0 0 5px 0;
}
.tagline {
color: #e2e8f0;
font-size: 14px;
margin: 0;
}
.email-content {
padding: 30px 20px;
background-color: #1e293b;
}
.reset-title {
font-size: 24px;
font-weight: bold;
color: #ffffff;
text-align: center;
margin: 0 0 20px 0;
}
.reset-message {
color: #cbd5e1;
font-size: 16px;
text-align: center;
margin: 0 0 30px 0;
}
.reset-info {
background-color: #334155;
border: 1px solid #475569;
padding: 20px;
margin: 0 0 30px 0;
text-align: center;
}
.reset-icon {
font-size: 40px;
margin: 0 0 10px 0;
display: block;
}
.reset-description {
color: #94a3b8;
font-size: 14px;
margin: 0;
}
.cta-button {
display: block;
width: 100%;
max-width: 300px;
margin: 0 auto 30px;
padding: 15px 30px;
background-color: #00d4ff;
color: #ffffff;
text-decoration: none;
font-weight: bold;
font-size: 16px;
text-align: center;
text-transform: uppercase;
}
.security-tips {
background-color: #22c55e;
border: 1px solid #16a34a;
padding: 20px;
margin: 30px 0 0 0;
}
.security-title {
color: #ffffff;
font-weight: bold;
font-size: 16px;
margin: 0 0 15px 0;
text-align: center;
}
.security-list {
color: #ffffff;
font-size: 14px;
line-height: 1.8;
margin: 0;
padding: 0;
}
.security-list li {
margin: 0 0 8px 0;
list-style: none;
}
.warning-info {
background-color: #ef4444;
border: 1px solid #dc2626;
padding: 15px;
margin: 20px 0 0 0;
}
.warning-title {
color: #ffffff;
font-weight: bold;
font-size: 14px;
margin: 0 0 8px 0;
text-align: center;
}
.warning-text {
color: #ffffff;
font-size: 13px;
text-align: center;
margin: 0;
}
.email-footer {
background-color: #0f172a;
padding: 20px;
text-align: center;
color: #64748b;
font-size: 12px;
}
.footer a {
color: #00d4ff;
text-decoration: none;
margin: 0 10px;
}
/* Mobile styles */
@media only screen and (max-width: 600px) {
.email-wrapper {
padding: 10px 0;
}
.email-container {
margin: 0 10px;
}
.email-content {
padding: 20px 15px;
}
.logo {
font-size: 24px;
}
.reset-title {
font-size: 20px;
}
.cta-button {
padding: 12px 20px;
font-size: 14px;
}
}
</style>
</head>
<body>
<div class="email-wrapper">
<table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color: #0a0a0f;">
<tr>
<td align="center" style="padding: 20px 0;">
<table width="600" cellpadding="0" cellspacing="0" border="0" style="background-color: #1e293b; border: 2px solid #334155; max-width: 600px;">
<!-- Header -->
<tr>
<td style="background-color: #00d4ff; padding: 30px 20px; text-align: center;">
<div class="logo">🥷 NINJACROSS</div>
<div class="tagline">Die ultimative Timer-Rangliste</div>
</td>
</tr>
<!-- Content -->
<tr>
<td style="padding: 30px 20px; background-color: #1e293b;">
<h1 class="reset-title">Passwort zurücksetzen 🔐</h1>
<p class="reset-message">
Du hast eine Anfrage zum Zurücksetzen deines Passworts gestellt.
Klicke auf den Button unten, um ein neues Passwort zu erstellen.
</p>
<table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color: #334155; border: 1px solid #475569; margin: 0 0 30px 0;">
<tr>
<td style="padding: 20px; text-align: center;">
<span class="reset-icon">🔑</span>
<div class="reset-description">
Dieser Link ist sicher und führt dich zur Passwort-Reset-Seite
</div>
</td>
</tr>
</table>
<table width="100%" cellpadding="0" cellspacing="0" border="0" style="margin: 0 0 30px 0;">
<tr>
<td align="center">
<a href="{{ .ConfirmationURL }}" class="cta-button">
🔄 Passwort zurücksetzen
</a>
</td>
</tr>
</table>
<table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color: #22c55e; border: 1px solid #16a34a; margin: 30px 0 0 0;">
<tr>
<td style="padding: 20px;">
<div class="security-title">🛡️ Tipps für ein sicheres Passwort:</div>
<ul class="security-list">
<li>• Verwende mindestens 8 Zeichen</li>
<li>• Kombiniere Groß- und Kleinbuchstaben</li>
<li>• Füge Zahlen und Sonderzeichen hinzu</li>
<li>• Verwende keine persönlichen Informationen</li>
<li>• Nutze ein einzigartiges Passwort nur für NinjaCross</li>
</ul>
</td>
</tr>
</table>
<table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color: #ef4444; border: 1px solid #dc2626; margin: 20px 0 0 0;">
<tr>
<td style="padding: 15px;">
<div class="warning-title">⚠️ Sicherheitshinweis</div>
<div class="warning-text">
Dieser Link verfällt nach 24 Stunden. Falls du diese Anfrage nicht gestellt hast,
kannst du diese E-Mail ignorieren.
</div>
</td>
</tr>
</table>
</td>
</tr>
<!-- Footer -->
<tr>
<td style="background-color: #0f172a; padding: 20px; text-align: center; color: #64748b; font-size: 12px;">
<p>
Falls du diese Anfrage nicht gestellt hast, kannst du diese E-Mail ignorieren.
</p>
<p style="margin-top: 15px;">
<a href="https://ninja.reptilfpv.de:3000" style="color: #00d4ff; text-decoration: none; margin: 0 10px;">Zur Website</a>
<a href="https://ninja.reptilfpv.de:3000/support" style="color: #00d4ff; text-decoration: none; margin: 0 10px;">Support</a>
<a href="https://ninja.reptilfpv.de:3000/privacy" style="color: #00d4ff; text-decoration: none; margin: 0 10px;">Datenschutz</a>
</p>
<p style="margin-top: 15px;">
© 2024 NinjaCross. Alle Rechte vorbehalten.
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</div>
</body>
</html>

View File

@@ -0,0 +1,294 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Passwort zurücksetzen - NinjaCross</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', sans-serif;
background: #0a0a0f;
color: #ffffff;
line-height: 1.6;
margin: 0;
padding: 0;
}
.email-container {
max-width: 600px;
margin: 0 auto;
background: #0a0a0f;
background-image:
radial-gradient(circle at 20% 80%, #1a1a2e 0%, transparent 50%),
radial-gradient(circle at 80% 20%, #16213e 0%, transparent 50%),
radial-gradient(circle at 40% 40%, #0f3460 0%, transparent 50%);
}
.email-header {
text-align: center;
padding: 3rem 2rem 2rem;
}
.logo {
font-size: 2.5rem;
font-weight: 700;
background: linear-gradient(135deg, #00d4ff, #ff6b35, #ffd700);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 0.5rem;
letter-spacing: -0.02em;
}
.tagline {
color: #94a3b8;
font-size: 1rem;
font-weight: 400;
}
.email-content {
background: rgba(30, 41, 59, 0.95);
backdrop-filter: blur(20px);
border: 1px solid rgba(51, 65, 85, 0.3);
margin: 0 2rem;
padding: 2.5rem;
border-radius: 1.5rem;
box-shadow:
0 25px 50px rgba(0, 0, 0, 0.3),
0 0 0 1px rgba(0, 212, 255, 0.1);
}
.reset-title {
font-size: 1.75rem;
font-weight: 600;
color: #e2e8f0;
text-align: center;
margin-bottom: 1.5rem;
}
.reset-message {
color: #cbd5e1;
font-size: 1rem;
margin-bottom: 2rem;
text-align: center;
}
.reset-info {
background: rgba(51, 65, 85, 0.3);
border: 1px solid rgba(0, 212, 255, 0.1);
border-radius: 0.75rem;
padding: 1.5rem;
margin-bottom: 2rem;
text-align: center;
}
.reset-icon {
font-size: 3rem;
margin-bottom: 1rem;
display: block;
}
.reset-description {
color: #94a3b8;
font-size: 0.9rem;
margin-bottom: 1rem;
}
.cta-button {
display: inline-block;
width: 100%;
padding: 1rem 2rem;
background: linear-gradient(135deg, #00d4ff, #0891b2);
color: white;
text-decoration: none;
border-radius: 0.75rem;
font-weight: 600;
font-size: 1rem;
text-align: center;
text-transform: uppercase;
letter-spacing: 0.05em;
box-shadow: 0 4px 12px rgba(0, 212, 255, 0.3);
transition: all 0.2s ease;
margin-bottom: 2rem;
}
.cta-button:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0, 212, 255, 0.4);
}
.security-tips {
background: rgba(34, 197, 94, 0.1);
border: 1px solid rgba(34, 197, 94, 0.3);
border-radius: 0.75rem;
padding: 1.5rem;
margin-top: 2rem;
}
.security-title {
color: #22c55e;
font-weight: 600;
font-size: 1rem;
margin-bottom: 1rem;
text-align: center;
}
.security-list {
color: #86efac;
font-size: 0.9rem;
line-height: 1.8;
}
.security-list li {
margin-bottom: 0.5rem;
}
.warning-info {
background: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.3);
border-radius: 0.75rem;
padding: 1rem;
margin-top: 1.5rem;
}
.warning-title {
color: #ef4444;
font-weight: 600;
font-size: 0.9rem;
margin-bottom: 0.5rem;
text-align: center;
}
.warning-text {
color: #fca5a5;
font-size: 0.85rem;
text-align: center;
}
.email-footer {
text-align: center;
padding: 2rem;
color: #64748b;
font-size: 0.875rem;
}
.footer-links {
margin-top: 1rem;
}
.footer-links a {
color: #00d4ff;
text-decoration: none;
margin: 0 1rem;
}
.footer-links a:hover {
color: #0891b2;
}
.divider {
height: 1px;
background: linear-gradient(90deg, transparent, #334155, transparent);
margin: 2rem 0;
}
/* Mobile Responsive */
@media (max-width: 600px) {
.email-content {
margin: 0 1rem;
padding: 2rem;
}
.email-header {
padding: 2rem 1rem 1rem;
}
.logo {
font-size: 2rem;
}
.reset-title {
font-size: 1.5rem;
}
.reset-icon {
font-size: 2.5rem;
}
}
</style>
</head>
<body>
<div class="email-container">
<!-- Header -->
<div class="email-header">
<div class="logo">🥷 NINJACROSS</div>
<div class="tagline">Die ultimative Timer-Rangliste</div>
</div>
<!-- Main Content -->
<div class="email-content">
<h1 class="reset-title">Passwort zurücksetzen 🔐</h1>
<p class="reset-message">
Du hast eine Anfrage zum Zurücksetzen deines Passworts gestellt.
Klicke auf den Button unten, um ein neues Passwort zu erstellen.
</p>
<div class="reset-info">
<span class="reset-icon">🔑</span>
<div class="reset-description">
Dieser Link ist sicher und führt dich zur Passwort-Reset-Seite
</div>
</div>
<a href="{{ .ConfirmationURL }}" class="cta-button">
🔄 Passwort zurücksetzen
</a>
<div class="security-tips">
<div class="security-title">🛡️ Tipps für ein sicheres Passwort:</div>
<ul class="security-list">
<li>• Verwende mindestens 8 Zeichen</li>
<li>• Kombiniere Groß- und Kleinbuchstaben</li>
<li>• Füge Zahlen und Sonderzeichen hinzu</li>
<li>• Verwende keine persönlichen Informationen</li>
<li>• Nutze ein einzigartiges Passwort nur für NinjaCross</li>
</ul>
</div>
<div class="warning-info">
<div class="warning-title">⚠️ Sicherheitshinweis</div>
<div class="warning-text">
Dieser Link verfällt nach 24 Stunden. Falls du diese Anfrage nicht gestellt hast,
kannst du diese E-Mail ignorieren.
</div>
</div>
</div>
<!-- Footer -->
<div class="email-footer">
<p>
Falls du diese Anfrage nicht gestellt hast, kannst du diese E-Mail ignorieren.
</p>
<div class="footer-links">
<a href="{{ .SiteURL }}">Zur Website</a>
<a href="{{ .SiteURL }}/support">Support</a>
<a href="{{ .SiteURL }}/privacy">Datenschutz</a>
</div>
<p style="margin-top: 1.5rem; font-size: 0.75rem; color: #64748b;">
© 2024 NinjaCross. Alle Rechte vorbehalten.
</p>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,32 @@
🥷 NINJACROSS - Die ultimative Timer-Rangliste
================================================
Passwort zurücksetzen 🔐
Du hast eine Anfrage zum Zurücksetzen deines Passworts gestellt.
Klicke auf den Link unten, um ein neues Passwort zu erstellen.
🔑 Passwort zurücksetzen:
{{ .ConfirmationURL }}
🛡️ Tipps für ein sicheres Passwort:
• Verwende mindestens 8 Zeichen
• Kombiniere Groß- und Kleinbuchstaben
• Füge Zahlen und Sonderzeichen hinzu
• Verwende keine persönlichen Informationen
• Nutze ein einzigartiges Passwort nur für NinjaCross
⚠️ Sicherheitshinweis:
Dieser Link verfällt nach 24 Stunden. Falls du diese Anfrage nicht gestellt hast,
kannst du diese E-Mail ignorieren.
================================================
Falls du diese Anfrage nicht gestellt hast, kannst du diese E-Mail ignorieren.
Links:
- Zur Website: {{ .SiteURL }}
- Support: {{ .SiteURL }}/support
- Datenschutz: {{ .SiteURL }}/privacy
© 2024 NinjaCross. Alle Rechte vorbehalten.

View File

@@ -0,0 +1,208 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Willkommen bei NinjaCross</title>
<style>
/* E-Mail-Client-kompatible Styles */
body {
font-family: Arial, sans-serif;
background-color: #0a0a0f;
color: #ffffff;
margin: 0;
padding: 20px;
line-height: 1.6;
}
.email-container {
max-width: 600px;
margin: 0 auto;
background-color: #1e293b;
border: 2px solid #334155;
border-radius: 15px;
overflow: hidden;
}
.email-header {
background-color: #00d4ff;
padding: 30px 20px;
text-align: center;
}
.logo {
font-size: 28px;
font-weight: bold;
color: #ffffff;
margin-bottom: 5px;
}
.tagline {
color: #e2e8f0;
font-size: 14px;
}
.email-content {
padding: 30px 20px;
background-color: #1e293b;
}
.welcome-title {
font-size: 24px;
font-weight: bold;
color: #e2e8f0;
text-align: center;
margin-bottom: 20px;
}
.welcome-message {
color: #cbd5e1;
font-size: 16px;
text-align: center;
margin-bottom: 30px;
}
.cta-button {
display: block;
width: 100%;
max-width: 300px;
margin: 0 auto 30px;
padding: 15px 30px;
background-color: #00d4ff;
color: #ffffff;
text-decoration: none;
border-radius: 10px;
font-weight: bold;
font-size: 16px;
text-align: center;
text-transform: uppercase;
}
.features {
margin-top: 30px;
}
.feature {
background-color: #334155;
border: 1px solid #475569;
border-radius: 10px;
padding: 15px;
margin-bottom: 15px;
}
.feature-icon {
font-size: 20px;
margin-right: 10px;
}
.feature-text {
color: #cbd5e1;
font-size: 14px;
}
.email-footer {
background-color: #0f172a;
padding: 20px;
text-align: center;
color: #64748b;
font-size: 12px;
}
.footer a {
color: #00d4ff;
text-decoration: none;
margin: 0 10px;
}
/* Mobile Responsive */
@media (max-width: 600px) {
.email-container {
margin: 0 10px;
}
.email-content {
padding: 20px 15px;
}
.logo {
font-size: 24px;
}
.welcome-title {
font-size: 20px;
}
}
</style>
</head>
<body>
<div class="email-container">
<!-- Header -->
<div class="email-header">
<div class="logo">🥷 NINJACROSS</div>
<div class="tagline">Die ultimative Timer-Rangliste</div>
</div>
<!-- Content -->
<div class="email-content">
<h1 class="welcome-title">Willkommen bei NinjaCross! 🎉</h1>
<p class="welcome-message">
Vielen Dank für deine Registrierung! Du bist jetzt Teil der NinjaCross-Community.
Bestätige deine E-Mail-Adresse, um dein Konto zu aktivieren und sofort loszulegen.
</p>
<a href="{{ .ConfirmationURL }}" class="cta-button">
✉️ E-Mail bestätigen
</a>
<!-- Features -->
<div class="features">
<div class="feature">
<span class="feature-icon">🏃‍♂️</span>
<span class="feature-text">
<strong>Timer-Tracking:</strong> Erfasse deine Zeiten und verfolge deinen Fortschritt
</span>
</div>
<div class="feature">
<span class="feature-icon">🏆</span>
<span class="feature-text">
<strong>Leaderboards:</strong> Vergleiche dich mit anderen Spielern und erreiche die Spitze
</span>
</div>
<div class="feature">
<span class="feature-icon">📊</span>
<span class="feature-text">
<strong>Statistiken:</strong> Detaillierte Analysen deiner Performance und Verbesserungen
</span>
</div>
<div class="feature">
<span class="feature-icon">🌍</span>
<span class="feature-text">
<strong>Multi-Location:</strong> Spiele an verschiedenen Standorten und sammle Erfahrungen
</span>
</div>
</div>
</div>
<!-- Footer -->
<div class="email-footer">
<p>
Falls du dich nicht registriert hast, kannst du diese E-Mail ignorieren.
</p>
<p style="margin-top: 15px;">
<a href="{{ .SiteURL }}">Zur Website</a>
<a href="{{ .SiteURL }}/support">Support</a>
<a href="{{ .SiteURL }}/privacy">Datenschutz</a>
</p>
<p style="margin-top: 15px;">
© 2024 NinjaCross. Alle Rechte vorbehalten.
</p>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,187 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Willkommen bei NinjaCross</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #0a0a0f;
color: #ffffff;
margin: 0;
padding: 20px;
line-height: 1.6;
}
.container {
max-width: 600px;
margin: 0 auto;
background-color: #1e293b;
border: 2px solid #334155;
border-radius: 15px;
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #00d4ff, #0891b2);
padding: 30px 20px;
text-align: center;
}
.logo {
font-size: 28px;
font-weight: bold;
color: #ffffff;
margin-bottom: 5px;
}
.tagline {
color: #e2e8f0;
font-size: 14px;
}
.content {
padding: 30px 20px;
}
.title {
font-size: 24px;
font-weight: bold;
color: #e2e8f0;
text-align: center;
margin-bottom: 20px;
}
.message {
color: #cbd5e1;
font-size: 16px;
text-align: center;
margin-bottom: 30px;
}
.button {
display: block;
width: 100%;
max-width: 300px;
margin: 0 auto 30px;
padding: 15px 30px;
background: linear-gradient(135deg, #00d4ff, #0891b2);
color: #ffffff;
text-decoration: none;
border-radius: 10px;
font-weight: bold;
font-size: 16px;
text-align: center;
text-transform: uppercase;
}
.features {
margin-top: 30px;
}
.feature {
background-color: #334155;
border: 1px solid #475569;
border-radius: 10px;
padding: 15px;
margin-bottom: 15px;
}
.feature-icon {
font-size: 20px;
margin-right: 10px;
}
.feature-text {
color: #cbd5e1;
font-size: 14px;
}
.footer {
background-color: #0f172a;
padding: 20px;
text-align: center;
color: #64748b;
font-size: 12px;
}
.footer a {
color: #00d4ff;
text-decoration: none;
margin: 0 10px;
}
</style>
</head>
<body>
<div class="container">
<!-- Header -->
<div class="header">
<div class="logo">🥷 NINJACROSS</div>
<div class="tagline">Die ultimative Timer-Rangliste</div>
</div>
<!-- Content -->
<div class="content">
<h1 class="title">Willkommen bei NinjaCross! 🎉</h1>
<p class="message">
Vielen Dank für deine Registrierung! Du bist jetzt Teil der NinjaCross-Community.
Bestätige deine E-Mail-Adresse, um dein Konto zu aktivieren und sofort loszulegen.
</p>
<a href="{{ .ConfirmationURL }}" class="button">
✉️ E-Mail bestätigen
</a>
<!-- Features -->
<div class="features">
<div class="feature">
<span class="feature-icon">🏃‍♂️</span>
<span class="feature-text">
<strong>Timer-Tracking:</strong> Erfasse deine Zeiten und verfolge deinen Fortschritt
</span>
</div>
<div class="feature">
<span class="feature-icon">🏆</span>
<span class="feature-text">
<strong>Leaderboards:</strong> Vergleiche dich mit anderen Spielern und erreiche die Spitze
</span>
</div>
<div class="feature">
<span class="feature-icon">📊</span>
<span class="feature-text">
<strong>Statistiken:</strong> Detaillierte Analysen deiner Performance und Verbesserungen
</span>
</div>
<div class="feature">
<span class="feature-icon">🌍</span>
<span class="feature-text">
<strong>Multi-Location:</strong> Spiele an verschiedenen Standorten und sammle Erfahrungen
</span>
</div>
</div>
</div>
<!-- Footer -->
<div class="footer">
<p>
Falls du dich nicht registriert hast, kannst du diese E-Mail ignorieren.
</p>
<p style="margin-top: 15px;">
<a href="{{ .SiteURL }}">Zur Website</a>
<a href="{{ .SiteURL }}/support">Support</a>
<a href="{{ .SiteURL }}/privacy">Datenschutz</a>
</p>
<p style="margin-top: 15px;">
© 2024 NinjaCross. Alle Rechte vorbehalten.
</p>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,274 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Willkommen bei NinjaCross</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', sans-serif;
background: #0a0a0f;
color: #ffffff;
line-height: 1.6;
margin: 0;
padding: 0;
}
.email-container {
max-width: 600px;
margin: 0 auto;
background: #0a0a0f;
background-image:
radial-gradient(circle at 20% 80%, #1a1a2e 0%, transparent 50%),
radial-gradient(circle at 80% 20%, #16213e 0%, transparent 50%),
radial-gradient(circle at 40% 40%, #0f3460 0%, transparent 50%);
}
.email-header {
text-align: center;
padding: 3rem 2rem 2rem;
}
.logo {
font-size: 2.5rem;
font-weight: 700;
background: linear-gradient(135deg, #00d4ff, #ff6b35, #ffd700);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 0.5rem;
letter-spacing: -0.02em;
}
.tagline {
color: #94a3b8;
font-size: 1rem;
font-weight: 400;
}
.email-content {
background: rgba(30, 41, 59, 0.95);
backdrop-filter: blur(20px);
border: 1px solid rgba(51, 65, 85, 0.3);
margin: 0 2rem;
padding: 2.5rem;
border-radius: 1.5rem;
box-shadow:
0 25px 50px rgba(0, 0, 0, 0.3),
0 0 0 1px rgba(0, 212, 255, 0.1);
}
.welcome-title {
font-size: 1.75rem;
font-weight: 600;
color: #e2e8f0;
text-align: center;
margin-bottom: 1.5rem;
}
.welcome-message {
color: #cbd5e1;
font-size: 1rem;
margin-bottom: 2rem;
text-align: center;
}
.cta-button {
display: inline-block;
width: 100%;
padding: 1rem 2rem;
background: linear-gradient(135deg, #00d4ff, #0891b2);
color: white;
text-decoration: none;
border-radius: 0.75rem;
font-weight: 600;
font-size: 1rem;
text-align: center;
text-transform: uppercase;
letter-spacing: 0.05em;
box-shadow: 0 4px 12px rgba(0, 212, 255, 0.3);
transition: all 0.2s ease;
margin-bottom: 2rem;
}
.cta-button:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0, 212, 255, 0.4);
}
.features-section {
margin-top: 2rem;
}
.features-title {
font-size: 1.25rem;
font-weight: 600;
color: #e2e8f0;
margin-bottom: 1rem;
text-align: center;
}
.feature-item {
display: flex;
align-items: center;
margin-bottom: 1rem;
padding: 1rem;
background: rgba(51, 65, 85, 0.3);
border-radius: 0.75rem;
border: 1px solid rgba(0, 212, 255, 0.1);
}
.feature-icon {
font-size: 1.5rem;
margin-right: 1rem;
width: 2rem;
text-align: center;
}
.feature-text {
color: #cbd5e1;
font-size: 0.95rem;
}
.email-footer {
text-align: center;
padding: 2rem;
color: #64748b;
font-size: 0.875rem;
}
.footer-links {
margin-top: 1rem;
}
.footer-links a {
color: #00d4ff;
text-decoration: none;
margin: 0 1rem;
}
.footer-links a:hover {
color: #0891b2;
}
.divider {
height: 1px;
background: linear-gradient(90deg, transparent, #334155, transparent);
margin: 2rem 0;
}
/* Mobile Responsive */
@media (max-width: 600px) {
.email-content {
margin: 0 1rem;
padding: 2rem;
}
.email-header {
padding: 2rem 1rem 1rem;
}
.logo {
font-size: 2rem;
}
.welcome-title {
font-size: 1.5rem;
}
.feature-item {
flex-direction: column;
text-align: center;
}
.feature-icon {
margin-right: 0;
margin-bottom: 0.5rem;
}
}
</style>
</head>
<body>
<div class="email-container">
<!-- Header -->
<div class="email-header">
<div class="logo">🥷 NINJACROSS</div>
<div class="tagline">Die ultimative Timer-Rangliste</div>
</div>
<!-- Main Content -->
<div class="email-content">
<h1 class="welcome-title">Willkommen bei NinjaCross! 🎉</h1>
<p class="welcome-message">
Vielen Dank für deine Registrierung! Du bist jetzt Teil der NinjaCross-Community.
Bestätige deine E-Mail-Adresse, um dein Konto zu aktivieren und sofort loszulegen.
</p>
<a href="{{ .ConfirmationURL }}" class="cta-button">
✉️ E-Mail bestätigen
</a>
<div class="divider"></div>
<!-- Features Section -->
<div class="features-section">
<h2 class="features-title">Was dich erwartet:</h2>
<div class="feature-item">
<div class="feature-icon">🏃‍♂️</div>
<div class="feature-text">
<strong>Timer-Tracking:</strong> Erfasse deine Zeiten und verfolge deinen Fortschritt
</div>
</div>
<div class="feature-item">
<div class="feature-icon">🏆</div>
<div class="feature-text">
<strong>Leaderboards:</strong> Vergleiche dich mit anderen Spielern und erreiche die Spitze
</div>
</div>
<div class="feature-item">
<div class="feature-icon">📊</div>
<div class="feature-text">
<strong>Statistiken:</strong> Detaillierte Analysen deiner Performance und Verbesserungen
</div>
</div>
<div class="feature-item">
<div class="feature-icon">🌍</div>
<div class="feature-text">
<strong>Multi-Location:</strong> Spiele an verschiedenen Standorten und sammle Erfahrungen
</div>
</div>
</div>
</div>
<!-- Footer -->
<div class="email-footer">
<p>
Falls du dich nicht registriert hast, kannst du diese E-Mail ignorieren.
</p>
<div class="footer-links">
<a href="{{ .SiteURL }}">Zur Website</a>
<a href="{{ .SiteURL }}/support">Support</a>
<a href="{{ .SiteURL }}/privacy">Datenschutz</a>
</div>
<p style="margin-top: 1.5rem; font-size: 0.75rem; color: #64748b;">
© 2024 NinjaCross. Alle Rechte vorbehalten.
</p>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,32 @@
🥷 NINJACROSS - Die ultimative Timer-Rangliste
================================================
Willkommen bei NinjaCross! 🎉
Vielen Dank für deine Registrierung! Du bist jetzt Teil der NinjaCross-Community.
Bestätige deine E-Mail-Adresse, um dein Konto zu aktivieren und sofort loszulegen.
📧 E-Mail bestätigen:
{{ .ConfirmationURL }}
Was dich erwartet:
==================
🏃‍♂️ Timer-Tracking: Erfasse deine Zeiten und verfolge deinen Fortschritt
🏆 Leaderboards: Vergleiche dich mit anderen Spielern und erreiche die Spitze
📊 Statistiken: Detaillierte Analysen deiner Performance und Verbesserungen
🌍 Multi-Location: Spiele an verschiedenen Standorten und sammle Erfahrungen
================================================
Falls du dich nicht registriert hast, kannst du diese E-Mail ignorieren.
Links:
- Zur Website: {{ .SiteURL }}
- Support: {{ .SiteURL }}/support
- Datenschutz: {{ .SiteURL }}/privacy
© 2024 NinjaCross. Alle Rechte vorbehalten.

85
public/generator.html Normal file
View File

@@ -0,0 +1,85 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Lizenzgenerator</title>
<link rel="icon" type="image/x-icon" href="/pictures/favicon.ico">
<link rel="stylesheet" href="/css/generator.css">
</head>
<body>
<div class="container">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<h1 style="margin: 0;">🔐 Lizenzgenerator</h1>
<div style="display: flex; gap: 10px;">
<button onclick="goBackToDashboard()" style="padding: 10px 20px; background: #2196f3; color: white; border: none; border-radius: 8px; cursor: pointer; font-weight: 500; transition: all 0.3s ease;">⬅️ Zurück zum Dashboard</button>
<button onclick="logout()" style="padding: 10px 20px; background: #f44336; color: white; border: none; border-radius: 8px; cursor: pointer; font-weight: 500; transition: all 0.3s ease;">🚪 Abmelden</button>
</div>
</div>
<form id="licenseForm" onsubmit="generateLicense(); return false;">
<div class="form-group">
<label for="mac">MAC-Adresse</label>
<input type="text" id="mac" name="mac" placeholder="00:1A:2B:3C:4D:5E" required>
</div>
<div class="form-group">
<label for="tier">Lizenzstufe</label>
<input type="number" id="tier" name="tier" min="1" max="4" value="1" required>
</div>
<div id="dbConfig" class="db-config">
<!-- Token-Felder werden hier dynamisch eingefügt -->
</div>
<button type="submit" class="generate-btn" id="generateBtn">
<span id="btn-text">Lizenz generieren</span>
</button>
</form>
<div id="result" class="result-section">
<div class="license-label">Generierter Lizenzschlüssel:</div>
<div class="license-output" id="license-output"></div>
<button class="copy-btn" id="copyButton" onclick="copyToClipboard()">📋 In Zwischenablage kopieren</button>
</div>
<div id="success" class="success"></div>
<div id="error" class="error"></div>
<div class="info-text">
Der Lizenzgenerator erstellt sichere API-Token für verschiedene Zugriffsstufen.
</div>
</div>
<!-- Footer -->
<footer class="footer">
<div class="footer-content">
<div class="footer-links">
<a href="/impressum.html" class="footer-link">Impressum</a>
<a href="/datenschutz.html" class="footer-link">Datenschutz</a>
<button id="cookie-settings-footer" class="footer-link cookie-settings-btn">Cookie-Einstellungen</button>
</div>
<div class="footer-text">
<p>&copy; 2024 NinjaCross. Alle Rechte vorbehalten.</p>
</div>
</div>
</footer>
<script src="/js/cookie-consent.js"></script>
<script src="/js/generator.js"></script>
<script>
// Verhindert Enter-Taste in Eingabefeldern
document.addEventListener('DOMContentLoaded', function() {
const inputs = document.querySelectorAll('input, textarea');
inputs.forEach(input => {
input.addEventListener('keydown', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
return false;
}
});
});
});
</script>
</body>
</html>

241
public/impressum.html Normal file
View File

@@ -0,0 +1,241 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Impressum - NinjaCross</title>
<link rel="icon" type="image/x-icon" href="/pictures/favicon.ico">
<link rel="stylesheet" href="/css/leaderboard.css">
<style>
.legal-container {
max-width: 1000px;
margin: 0 auto;
padding: 2rem;
min-height: 100vh;
}
.legal-header {
text-align: center;
margin-bottom: 3rem;
}
.legal-title {
font-size: 3.5rem;
font-weight: 700;
background: linear-gradient(135deg, #00d4ff, #ff6b35, #ffd700);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 0.5rem;
letter-spacing: -0.02em;
}
.legal-subtitle {
font-size: 1.2rem;
color: #8892b0;
font-weight: 300;
}
.legal-content {
background: rgba(30, 41, 59, 0.8);
backdrop-filter: blur(20px);
border: 1px solid rgba(51, 65, 85, 0.3);
border-radius: 20px;
padding: 3rem;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
}
.section {
margin-bottom: 2.5rem;
}
.section h2 {
color: #00d4ff;
font-size: 1.8rem;
margin-bottom: 1rem;
border-bottom: 2px solid #334155;
padding-bottom: 0.5rem;
font-weight: 600;
}
.section h3 {
color: #ff6b35;
font-size: 1.3rem;
margin: 1.5rem 0 0.8rem 0;
font-weight: 500;
}
.section p, .section li {
margin-bottom: 0.8rem;
color: #e2e8f0;
line-height: 1.6;
}
.section ul {
margin-left: 1.5rem;
}
.contact-info {
background: rgba(0, 212, 255, 0.1);
border: 1px solid rgba(0, 212, 255, 0.3);
padding: 1.5rem;
border-radius: 12px;
margin: 1rem 0;
}
.contact-info p {
margin-bottom: 0.5rem;
}
.contact-info strong {
color: #00d4ff;
}
.back-button {
display: inline-flex;
align-items: center;
gap: 0.5rem;
background: linear-gradient(135deg, #00d4ff, #0099cc);
color: white;
padding: 12px 24px;
text-decoration: none;
border-radius: 12px;
margin-top: 2rem;
font-weight: 600;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(0, 212, 255, 0.3);
}
.back-button:hover {
background: linear-gradient(135deg, #0099cc, #007aa3);
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 212, 255, 0.4);
}
.highlight-box {
background: rgba(255, 107, 53, 0.1);
border: 1px solid rgba(255, 107, 53, 0.3);
padding: 1.5rem;
border-radius: 12px;
margin: 1.5rem 0;
}
.highlight-box h3 {
color: #ff6b35;
margin-top: 0;
}
.highlight-box p {
color: #e2e8f0;
margin: 0;
}
@media (max-width: 768px) {
.legal-container {
padding: 1rem;
}
.legal-content {
padding: 1.5rem;
}
.legal-title {
font-size: 2.5rem;
}
}
</style>
</head>
<body>
<div class="legal-container">
<div class="legal-header">
<h1 class="legal-title">🏃‍♂️ Impressum</h1>
<p class="legal-subtitle">NinjaCross - Speedrun Arena</p>
</div>
<div class="legal-content">
<div class="section">
<h2>Angaben gemäß § 5 TMG</h2>
<div class="contact-info">
<p><strong>Betreiber der Website:</strong></p>
<p>Carsten Graf<br>
Erfurter Str. 20<br>
75365 Calw<br>
Deutschland</p>
<p><strong>Kontakt:</strong></p>
<p>Telefon: +49 (0) 123 456789<br>
E-Mail: reptil1990(at)me.com</p>
</div>
</div>
<div class="section">
<h2>Verantwortlich für den Inhalt nach § 55 Abs. 2 RStV</h2>
<p>Carsten Graf<br>
Erfurter Str. 20<br>
75365 Calw<br>
Deutschland</p>
</div>
<div class="section">
<h2>Haftung für Inhalte</h2>
<p>Als Diensteanbieter sind wir gemäß § 7 Abs.1 TMG für eigene Inhalte auf diesen Seiten nach den allgemeinen Gesetzen verantwortlich. Nach §§ 8 bis 10 TMG sind wir als Diensteanbieter jedoch nicht unter der Verpflichtung, übermittelte oder gespeicherte fremde Informationen zu überwachen oder nach Umständen zu forschen, die auf eine rechtswidrige Tätigkeit hinweisen.</p>
<p>Verpflichtungen zur Entfernung oder Sperrung der Nutzung von Informationen nach den allgemeinen Gesetzen bleiben hiervon unberührt. Eine diesbezügliche Haftung ist jedoch erst ab dem Zeitpunkt der Kenntnis einer konkreten Rechtsverletzung möglich. Bei Bekanntwerden von entsprechenden Rechtsverletzungen werden wir diese Inhalte umgehend entfernen.</p>
</div>
<div class="section">
<h2>Haftung für Links</h2>
<p>Unser Angebot enthält Links zu externen Websites Dritter, auf deren Inhalte wir keinen Einfluss haben. Deshalb können wir für diese fremden Inhalte auch keine Gewähr übernehmen. Für die Inhalte der verlinkten Seiten ist stets der jeweilige Anbieter oder Betreiber der Seiten verantwortlich. Die verlinkten Seiten wurden zum Zeitpunkt der Verlinkung auf mögliche Rechtsverstöße überprüft. Rechtswidrige Inhalte waren zum Zeitpunkt der Verlinkung nicht erkennbar.</p>
<p>Eine permanente inhaltliche Kontrolle der verlinkten Seiten ist jedoch ohne konkrete Anhaltspunkte einer Rechtsverletzung nicht zumutbar. Bei Bekanntwerden von Rechtsverletzungen werden wir derartige Links umgehend entfernen.</p>
</div>
<div class="section">
<h2>Urheberrecht</h2>
<p>Die durch die Seitenbetreiber erstellten Inhalte und Werke auf diesen Seiten unterliegen dem deutschen Urheberrecht. Die Vervielfältigung, Bearbeitung, Verbreitung und jede Art der Verwertung außerhalb der Grenzen des Urheberrechtes bedürfen der schriftlichen Zustimmung des jeweiligen Autors bzw. Erstellers. Downloads und Kopien dieser Seite sind nur für den privaten, nicht kommerziellen Gebrauch gestattet.</p>
<p>Soweit die Inhalte auf dieser Seite nicht vom Betreiber erstellt wurden, werden die Urheberrechte Dritter beachtet. Insbesondere werden Inhalte Dritter als solche gekennzeichnet. Sollten Sie trotzdem auf eine Urheberrechtsverletzung aufmerksam werden, bitten wir um einen entsprechenden Hinweis. Bei Bekanntwerden von Rechtsverletzungen werden wir derartige Inhalte umgehend entfernen.</p>
</div>
<div class="section">
<h2>Streitschlichtung</h2>
<p>Die Europäische Kommission stellt eine Plattform zur Online-Streitbeilegung (OS) bereit: <a href="https://ec.europa.eu/consumers/odr/" target="_blank" rel="noopener">https://ec.europa.eu/consumers/odr/</a></p>
<p>Unsere E-Mail-Adresse finden Sie oben im Impressum.</p>
<p>Wir sind nicht bereit oder verpflichtet, an Streitbeilegungsverfahren vor einer Verbraucherschlichtungsstelle teilzunehmen.</p>
</div>
<a href="/" class="back-button">← Zurück zur Startseite</a>
</div>
</div>
<!-- Footer -->
<footer class="footer">
<div class="footer-content">
<div class="footer-links">
<a href="/impressum.html" class="footer-link">Impressum</a>
<a href="/datenschutz.html" class="footer-link">Datenschutz</a>
<button id="cookie-settings-footer" class="footer-link cookie-settings-btn">Cookie-Einstellungen</button>
</div>
<div class="footer-text">
<p>&copy; 2024 NinjaCross. Alle Rechte vorbehalten.</p>
</div>
</div>
</footer>
<script src="/js/cookie-consent.js"></script>
<script>
// Add cookie settings button functionality
document.addEventListener('DOMContentLoaded', function() {
const cookieSettingsBtn = document.getElementById('cookie-settings-footer');
if (cookieSettingsBtn) {
cookieSettingsBtn.addEventListener('click', function() {
if (window.cookieConsent) {
window.cookieConsent.resetConsent();
}
});
}
});
</script>
</body>
</html>

262
public/index.html Normal file
View File

@@ -0,0 +1,262 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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/index.css">
</head>
<body>
<!-- Notification Bubble -->
<div class="notification-bubble" id="notificationBubble">
<div class="notification-content">
<div class="notification-icon">🏁</div>
<div class="notification-text">
<div class="notification-title" id="notificationTitle" data-de="Neue Zeit!" data-en="New Time!">New Time!</div>
<div class="notification-subtitle" id="notificationSubtitle" data-de="Ein neuer Rekord wurde erstellt" data-en="A new record has been created">A new record has been created</div>
</div>
</div>
</div>
<div class="main-container">
<!-- Sticky Header Container -->
<div class="sticky-header">
<!-- Language Selector -->
<div class="language-selector">
<select id="languageSelect" onchange="changeLanguage()">
<option value="de" data-flag="🇩🇪">Deutsch</option>
<option value="en" data-flag="🇺🇸">English</option>
</select>
</div>
<!-- Admin Login Button -->
<div class="mobile-nav-buttons">
<a href="/login" class="admin-login-btn" id="adminLoginBtn" onclick="handleLoginClick(event)">🔐 Login</a>
<a href="/dashboard" class="dashboard-btn" id="dashboardBtn" style="display: none;">📊 Dashboard</a>
<button class="logout-btn" id="logoutBtn" onclick="logout()" style="display: none;">🚪 Logout</button>
</div>
</div>
<div class="header-section">
<h1 class="main-title">NINJACROSS LEADERBOARD</h1>
<p class="tagline" data-de="Die ultimative NinjaCross-Rangliste" data-en="The ultimate NinjaCross leaderboard">The ultimate NinjaCross leaderboard</p>
</div>
<div class="dashboard-grid">
<div class="control-panel">
<div class="control-group">
<label class="control-label" data-de="Standort" data-en="Location">Location</label>
<div class="location-control">
<select class="custom-select location-select" id="locationSelect">
<option value="">📍 Please select location</option>
<!-- Locations are loaded dynamically -->
</select>
<button class="location-btn" id="findLocationBtn" onclick="findNearestLocation()" title="Find nearest location" data-de="📍 Mein Standort" data-en="📍 My Location">
📍 My Location
</button>
</div>
</div>
<div class="control-group">
<label class="control-label" data-de="Zeitraum" data-en="Time Period">Time Period</label>
<div class="time-tabs">
<button class="time-tab" data-period="today">
<span class="tab-icon">📅</span>
<span class="tab-text" data-de="Heute" data-en="Today">Today</span>
</button>
<button class="time-tab" data-period="week">
<span class="tab-icon">📊</span>
<span class="tab-text" data-de="Diese Woche" data-en="This Week">This Week</span>
</button>
<button class="time-tab" data-period="month">
<span class="tab-icon">📈</span>
<span class="tab-text" data-de="Dieser Monat" data-en="This Month">This Month</span>
</button>
<button class="time-tab active" data-period="all">
<span class="tab-icon">♾️</span>
<span class="tab-text" data-de="Alle Zeiten" data-en="All Times">All Times</span>
</button>
</div>
</div>
<button class="refresh-btn pulse-animation" onclick="loadData()" data-de="⚡ Live Update" data-en="⚡ Live Update">
⚡ Live Update
</button>
</div>
<div class="stats-panel">
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value" id="totalPlayers">0</div>
<div class="stat-label" data-de="Teilnehmer" data-en="Participants">Participants</div>
</div>
<div class="stat-card">
<div class="stat-value" id="bestTime">--:--</div>
<div class="stat-label" data-de="Rekordzeit" data-en="Record Time">Record Time</div>
</div>
<div class="stat-card">
<div class="stat-value" id="totalRecords">0</div>
<div class="stat-label" data-de="Läufe" data-en="Runs">Runs</div>
</div>
</div>
</div>
</div>
<div class="leaderboard-container">
<div class="leaderboard-header">
<div class="active-filters" id="currentSelection">
📍 Please select location • ♾️ All Times
</div>
<div class="last-sync" id="lastUpdated">
Last Sync: Loading...
</div>
</div>
<div class="rankings-list" id="rankingList">
<!-- Rankings will be inserted here -->
</div>
</div>
</div>
<!-- Footer -->
<footer class="footer">
<div class="footer-content">
<div class="footer-links">
<a href="/impressum.html" class="footer-link" data-de="Impressum" data-en="Imprint">Imprint</a>
<a href="/datenschutz.html" class="footer-link" data-de="Datenschutz" data-en="Privacy">Privacy</a>
<button id="cookie-settings-footer" class="footer-link cookie-settings-btn" data-de="Cookie-Einstellungen" data-en="Cookie Settings">Cookie Settings</button>
</div>
<div class="footer-text">
<p data-de="&copy; 2024 NinjaCross. Alle Rechte vorbehalten." data-en="&copy; 2024 NinjaCross. All rights reserved.">&copy; 2024 NinjaCross. All rights reserved.</p>
</div>
</div>
</footer>
<!-- External Libraries -->
<script src="https://unpkg.com/@supabase/supabase-js@2"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.8.1/socket.io.min.js"></script>
<!-- Application JavaScript -->
<script src="/js/cookie-consent.js"></script>
<script src="/js/index.js"></script>
<script>
// iOS Detection
function isIOS() {
return /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
}
// Mobile Sticky Header Fix
function initMobileStickyHeader() {
const stickyHeader = document.querySelector('.sticky-header');
if (!stickyHeader) return;
// Check if we're on mobile
const isMobile = window.innerWidth <= 768;
if (isMobile || isIOS()) {
// Force fixed positioning for mobile
stickyHeader.style.position = 'fixed';
stickyHeader.style.top = '0';
stickyHeader.style.left = '0';
stickyHeader.style.right = '0';
stickyHeader.style.width = '100%';
stickyHeader.style.zIndex = '99999';
stickyHeader.style.marginBottom = '0';
stickyHeader.style.borderRadius = '0';
// Add padding to main container
const mainContainer = document.querySelector('.main-container');
if (mainContainer) {
mainContainer.style.paddingTop = '80px';
}
console.log('Mobile sticky header fix applied');
}
}
// iOS Touch Event Fix
function handleLoginClick(event) {
console.log('Login button clicked');
// Prevent default behavior temporarily
event.preventDefault();
// Add visual feedback
const button = event.target;
button.style.transform = 'scale(0.95)';
button.style.opacity = '0.8';
// Reset after short delay
setTimeout(() => {
button.style.transform = '';
button.style.opacity = '';
// Navigate to login page
window.location.href = '/login';
}, 150);
}
// Add touch event listeners for better mobile support
document.addEventListener('DOMContentLoaded', function() {
// Initialize mobile sticky header
initMobileStickyHeader();
const loginBtn = document.getElementById('adminLoginBtn');
if (loginBtn) {
// Make sure button is clickable
loginBtn.style.pointerEvents = 'auto';
loginBtn.style.position = 'relative';
loginBtn.style.zIndex = '100000';
// Add multiple event listeners for maximum compatibility
loginBtn.addEventListener('touchstart', function(e) {
console.log('Touch start on login button');
e.preventDefault();
this.style.transform = 'scale(0.95)';
this.style.opacity = '0.8';
}, { passive: false });
loginBtn.addEventListener('touchend', function(e) {
console.log('Touch end on login button');
e.preventDefault();
e.stopPropagation();
setTimeout(() => {
this.style.transform = '';
this.style.opacity = '';
window.location.href = '/login';
}, 100);
}, { passive: false });
loginBtn.addEventListener('click', function(e) {
console.log('Click on login button');
e.preventDefault();
e.stopPropagation();
window.location.href = '/login';
});
// Add mousedown for desktop
loginBtn.addEventListener('mousedown', function(e) {
console.log('Mouse down on login button');
this.style.transform = 'scale(0.95)';
this.style.opacity = '0.8';
});
loginBtn.addEventListener('mouseup', function(e) {
console.log('Mouse up on login button');
this.style.transform = '';
this.style.opacity = '';
});
}
});
// Re-apply mobile fixes on window resize
window.addEventListener('resize', function() {
setTimeout(initMobileStickyHeader, 100);
});
</script>
</body>
</html>

1768
public/js/admin-dashboard.js Normal file

File diff suppressed because it is too large Load Diff

91
public/js/adminlogin.js Normal file
View File

@@ -0,0 +1,91 @@
function showMessage(elementId, message, isError = false) {
const messageDiv = document.getElementById(elementId);
messageDiv.textContent = message;
messageDiv.classList.add("show");
setTimeout(() => {
messageDiv.classList.remove("show");
}, 4000);
}
function showError(message) {
showMessage("error", message, true);
}
function showSuccess(message) {
showMessage("success", message, false);
}
function setLoading(isLoading) {
const btnText = document.getElementById("btn-text");
const btn = document.getElementById("loginBtn");
if (isLoading) {
btnText.innerHTML = '<span class="loading"></span>Anmelde...';
btn.disabled = true;
} else {
btnText.textContent = 'Anmelden';
btn.disabled = false;
}
}
async function handleLogin(event) {
event.preventDefault();
const username = document.getElementById('username').value.trim();
const password = document.getElementById('password').value;
if (!username || !password) {
showError('Bitte füllen Sie alle Felder aus.');
return;
}
setLoading(true);
try {
const response = await fetch('/api/v1/public/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ username, password })
});
const result = await response.json();
if (result.success) {
showSuccess('✅ Anmeldung erfolgreich! Weiterleitung...');
setTimeout(() => {
window.location.href = '/admin-dashboard';
}, 1000);
} else {
showError(result.message || 'Anmeldung fehlgeschlagen');
}
} catch (error) {
console.error('Fehler bei der Anmeldung:', error);
showError('Verbindungsfehler. Bitte versuchen Sie es erneut.');
} finally {
setLoading(false);
}
}
// Enter-Taste für Login
document.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
handleLogin(e);
}
});
// Fokus auf erstes Eingabefeld
document.addEventListener('DOMContentLoaded', function() {
document.getElementById('username').focus();
// Add cookie settings button functionality
const cookieSettingsBtn = document.getElementById('cookie-settings-footer');
if (cookieSettingsBtn) {
cookieSettingsBtn.addEventListener('click', function() {
if (window.cookieConsent) {
window.cookieConsent.resetConsent();
}
});
}
});

601
public/js/cookie-consent.js Normal file
View File

@@ -0,0 +1,601 @@
// Cookie Consent Management
class CookieConsent {
constructor() {
this.cookieName = 'ninjacross_cookie_consent';
this.cookieSettingsName = 'ninjacross_cookie_settings';
this.consentGiven = false;
this.settings = {
necessary: true, // Always true, can't be disabled
functional: false,
analytics: false
};
this.init();
}
init() {
// Check if consent was already given
const savedConsent = this.getCookie(this.cookieName);
const savedSettings = this.getCookie(this.cookieSettingsName);
if (savedConsent === 'true') {
this.consentGiven = true;
if (savedSettings) {
this.settings = { ...this.settings, ...JSON.parse(savedSettings) };
}
this.applySettings();
} else {
this.showConsentBanner();
}
}
showConsentBanner() {
// Don't show banner if already shown
if (document.getElementById('cookie-consent-banner')) {
return;
}
const banner = document.createElement('div');
banner.id = 'cookie-consent-banner';
banner.innerHTML = `
<div class="cookie-banner">
<div class="cookie-content">
<div class="cookie-icon">🍪</div>
<div class="cookie-text">
<h3>Cookie-Einstellungen</h3>
<p>Wir verwenden Cookies, um Ihnen die beste Erfahrung auf unserer Website zu bieten. Einige sind notwendig, andere helfen uns, die Website zu verbessern.</p>
</div>
</div>
<div class="cookie-actions">
<button id="cookie-settings-btn" class="btn-cookie-settings">Einstellungen</button>
<button id="cookie-accept-all" class="btn-cookie-accept">Alle akzeptieren</button>
<button id="cookie-accept-necessary" class="btn-cookie-necessary">Nur notwendige</button>
</div>
</div>
`;
// Add styles
const style = document.createElement('style');
style.textContent = `
#cookie-consent-banner {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
color: white;
z-index: 10000;
box-shadow: 0 -4px 20px rgba(0,0,0,0.3);
animation: slideUp 0.3s ease-out;
}
@keyframes slideUp {
from { transform: translateY(100%); }
to { transform: translateY(0); }
}
.cookie-banner {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px;
max-width: 1200px;
margin: 0 auto;
gap: 20px;
}
.cookie-content {
display: flex;
align-items: center;
gap: 15px;
flex: 1;
}
.cookie-icon {
font-size: 2rem;
flex-shrink: 0;
}
.cookie-text h3 {
margin: 0 0 5px 0;
font-size: 1.1rem;
color: white;
}
.cookie-text p {
margin: 0;
font-size: 0.9rem;
opacity: 0.9;
line-height: 1.4;
}
.cookie-actions {
display: flex;
gap: 10px;
flex-shrink: 0;
}
.btn-cookie-settings,
.btn-cookie-accept,
.btn-cookie-necessary {
padding: 10px 16px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 0.9rem;
font-weight: 500;
transition: all 0.2s ease;
}
.btn-cookie-settings {
background: rgba(255,255,255,0.2);
color: white;
border: 1px solid rgba(255,255,255,0.3);
}
.btn-cookie-settings:hover {
background: rgba(255,255,255,0.3);
}
.btn-cookie-accept {
background: #10b981;
color: white;
}
.btn-cookie-accept:hover {
background: #059669;
}
.btn-cookie-necessary {
background: #6b7280;
color: white;
}
.btn-cookie-necessary:hover {
background: #4b5563;
}
@media (max-width: 768px) {
.cookie-banner {
flex-direction: column;
text-align: center;
gap: 15px;
}
.cookie-content {
flex-direction: column;
text-align: center;
}
.cookie-actions {
flex-wrap: wrap;
justify-content: center;
}
}
`;
document.head.appendChild(style);
document.body.appendChild(banner);
// Add event listeners
document.getElementById('cookie-accept-all').addEventListener('click', () => {
this.acceptAll();
});
document.getElementById('cookie-accept-necessary').addEventListener('click', () => {
this.acceptNecessary();
});
document.getElementById('cookie-settings-btn').addEventListener('click', () => {
this.showSettingsModal();
});
}
showSettingsModal() {
// Remove banner
const banner = document.getElementById('cookie-consent-banner');
if (banner) {
banner.remove();
}
// Create modal
const modal = document.createElement('div');
modal.id = 'cookie-settings-modal';
modal.innerHTML = `
<div class="cookie-modal-overlay">
<div class="cookie-modal">
<div class="cookie-modal-header">
<h2>🍪 Cookie-Einstellungen</h2>
<button id="cookie-modal-close" class="btn-close">&times;</button>
</div>
<div class="cookie-modal-content">
<p>Wählen Sie aus, welche Cookies Sie zulassen möchten:</p>
<div class="cookie-category">
<div class="cookie-category-header">
<h3>Notwendige Cookies</h3>
<label class="cookie-toggle">
<input type="checkbox" id="necessary-cookies" checked disabled>
<span class="toggle-slider"></span>
</label>
</div>
<p>Diese Cookies sind für die Grundfunktionen der Website erforderlich und können nicht deaktiviert werden.</p>
</div>
<div class="cookie-category">
<div class="cookie-category-header">
<h3>Funktionale Cookies</h3>
<label class="cookie-toggle">
<input type="checkbox" id="functional-cookies" ${this.settings.functional ? 'checked' : ''}>
<span class="toggle-slider"></span>
</label>
</div>
<p>Diese Cookies ermöglichen erweiterte Funktionen wie Benutzeranmeldung und Einstellungen.</p>
</div>
<div class="cookie-category">
<div class="cookie-category-header">
<h3>Analyse-Cookies</h3>
<label class="cookie-toggle">
<input type="checkbox" id="analytics-cookies" ${this.settings.analytics ? 'checked' : ''}>
<span class="toggle-slider"></span>
</label>
</div>
<p>Diese Cookies helfen uns zu verstehen, wie Besucher mit der Website interagieren.</p>
</div>
</div>
<div class="cookie-modal-footer">
<button id="cookie-save-settings" class="btn-save">Einstellungen speichern</button>
<button id="cookie-accept-all-modal" class="btn-accept-all">Alle akzeptieren</button>
</div>
</div>
</div>
`;
// Add modal styles
const modalStyle = document.createElement('style');
modalStyle.textContent = `
.cookie-modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
z-index: 10001;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.cookie-modal {
background: white;
border-radius: 12px;
max-width: 600px;
width: 100%;
max-height: 80vh;
overflow-y: auto;
box-shadow: 0 20px 40px rgba(0,0,0,0.3);
animation: modalSlideIn 0.3s ease-out;
}
@keyframes modalSlideIn {
from { transform: scale(0.9); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}
.cookie-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
border-bottom: 1px solid #e2e8f0;
}
.cookie-modal-header h2 {
margin: 0;
color: #1e3c72;
}
.btn-close {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: #6b7280;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
}
.btn-close:hover {
color: #374151;
}
.cookie-modal-content {
padding: 20px;
}
.cookie-category {
margin-bottom: 20px;
padding: 15px;
border: 1px solid #e2e8f0;
border-radius: 8px;
}
.cookie-category-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.cookie-category-header h3 {
margin: 0;
color: #1e3c72;
font-size: 1.1rem;
}
.cookie-category p {
margin: 0;
color: #6b7280;
font-size: 0.9rem;
line-height: 1.4;
}
.cookie-toggle {
position: relative;
display: inline-block;
width: 50px;
height: 24px;
}
.cookie-toggle input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: .4s;
border-radius: 24px;
}
.toggle-slider:before {
position: absolute;
content: "";
height: 18px;
width: 18px;
left: 3px;
bottom: 3px;
background-color: white;
transition: .4s;
border-radius: 50%;
}
.cookie-toggle input:checked + .toggle-slider {
background-color: #1e3c72;
}
.cookie-toggle input:checked + .toggle-slider:before {
transform: translateX(26px);
}
.cookie-toggle input:disabled + .toggle-slider {
background-color: #10b981;
cursor: not-allowed;
}
.cookie-modal-footer {
padding: 20px;
border-top: 1px solid #e2e8f0;
display: flex;
gap: 10px;
justify-content: flex-end;
}
.btn-save, .btn-accept-all {
padding: 10px 20px;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: 500;
transition: all 0.2s ease;
}
.btn-save {
background: #1e3c72;
color: white;
}
.btn-save:hover {
background: #2a5298;
}
.btn-accept-all {
background: #10b981;
color: white;
}
.btn-accept-all:hover {
background: #059669;
}
@media (max-width: 768px) {
.cookie-modal {
margin: 10px;
max-height: 90vh;
}
.cookie-modal-footer {
flex-direction: column;
}
.btn-save, .btn-accept-all {
width: 100%;
}
}
`;
document.head.appendChild(modalStyle);
document.body.appendChild(modal);
// Add event listeners
document.getElementById('cookie-modal-close').addEventListener('click', () => {
modal.remove();
this.showConsentBanner();
});
document.getElementById('cookie-save-settings').addEventListener('click', () => {
this.saveSettings();
modal.remove();
});
document.getElementById('cookie-accept-all-modal').addEventListener('click', () => {
this.acceptAll();
modal.remove();
});
// Close on overlay click
modal.querySelector('.cookie-modal-overlay').addEventListener('click', (e) => {
if (e.target === e.currentTarget) {
modal.remove();
this.showConsentBanner();
}
});
}
saveSettings() {
this.settings.functional = document.getElementById('functional-cookies').checked;
this.settings.analytics = document.getElementById('analytics-cookies').checked;
this.setCookie(this.cookieName, 'true', 365);
this.setCookie(this.cookieSettingsName, JSON.stringify(this.settings), 365);
this.consentGiven = true;
this.applySettings();
}
acceptAll() {
this.settings.functional = true;
this.settings.analytics = true;
this.setCookie(this.cookieName, 'true', 365);
this.setCookie(this.cookieSettingsName, JSON.stringify(this.settings), 365);
this.consentGiven = true;
this.applySettings();
}
acceptNecessary() {
this.settings.functional = false;
this.settings.analytics = false;
this.setCookie(this.cookieName, 'true', 365);
this.setCookie(this.cookieSettingsName, JSON.stringify(this.settings), 365);
this.consentGiven = true;
this.applySettings();
}
applySettings() {
// Remove banner if exists
const banner = document.getElementById('cookie-consent-banner');
if (banner) {
banner.remove();
}
// Apply functional cookies
if (this.settings.functional) {
// Enable functional features
console.log('Functional cookies enabled');
} else {
// Disable functional features
console.log('Functional cookies disabled');
}
// Apply analytics cookies
if (this.settings.analytics) {
// Enable analytics
console.log('Analytics cookies enabled');
this.enableAnalytics();
} else {
// Disable analytics
console.log('Analytics cookies disabled');
this.disableAnalytics();
}
}
enableAnalytics() {
// Enable page tracking
if (typeof trackPageView === 'function') {
trackPageView('main_page_visit');
}
}
disableAnalytics() {
// Disable page tracking
console.log('Analytics disabled by user choice');
}
setCookie(name, value, days) {
const expires = new Date();
expires.setTime(expires.getTime() + (days * 24 * 60 * 60 * 1000));
document.cookie = `${name}=${value};expires=${expires.toUTCString()};path=/;SameSite=Lax`;
}
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;
}
// Public method to check if consent was given
hasConsent() {
return this.consentGiven;
}
// Public method to get current settings
getSettings() {
return { ...this.settings };
}
// Public method to reset consent
resetConsent() {
this.setCookie(this.cookieName, '', -1);
this.setCookie(this.cookieSettingsName, '', -1);
this.consentGiven = false;
this.settings = {
necessary: true,
functional: false,
analytics: false
};
this.showConsentBanner();
}
}
// Initialize cookie consent when DOM is loaded
document.addEventListener('DOMContentLoaded', function() {
window.cookieConsent = new CookieConsent();
});
// Export for use in other scripts
if (typeof module !== 'undefined' && module.exports) {
module.exports = CookieConsent;
}

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;

2163
public/js/dashboard.js Normal file

File diff suppressed because it is too large Load Diff

573
public/js/generator.js Normal file
View File

@@ -0,0 +1,573 @@
// Toggle Token-Felder basierend auf Lizenzstufe
function toggleTokenFields() {
const tierInput = document.getElementById("tier");
const dbConfig = document.getElementById("dbConfig");
const tier = parseInt(tierInput.value);
if (tier >= 3 && !isNaN(tier)) {
dbConfig.innerHTML = `
<h3>🗄️ Token-Informationen (für Stufe 3+)</h3>
<div class="form-group">
<label for="description">Token Beschreibung</label>
<textarea id="description" placeholder="z.B. API-Zugang für Standort München"></textarea>
</div>
<div class="form-group">
<label for="standorte">Standorte</label>
<input type="text" id="standorte" placeholder="z.B. München, Berlin">
</div>
<!-- Neue Standortsuche-Sektion -->
<div class="form-group">
<label for="locationSearch">Standort suchen & auf Karte anzeigen</label>
<div class="location-search-container">
<input type="text" id="locationSearch" placeholder="z.B. München, Marienplatz">
<button onclick="searchLocation(this)" style="padding: 15px 20px; background: #4caf50; color: white; border: none; border-radius: 12px; cursor: pointer; font-weight: 500; transition: all 0.3s ease;">🔍 Suchen</button>
</div>
</div>
<!-- Koordinaten-Anzeige -->
<div id="coordinates" class="coordinates-display" style="display: none;">
<div style="background: #f0f8ff; border: 2px solid #4caf50; border-radius: 8px; padding: 15px; margin-top: 15px;">
<h4 style="margin: 0 0 10px 0; color: #2e7d32;">📍 Gefundene Koordinaten:</h4>
<div style="display: flex; gap: 20px; flex-wrap: wrap;">
<div>
<strong>Breitengrad (LAT):</strong>
<span id="latitude" style="font-family: monospace; color: #1565c0;"></span>
</div>
<div>
<strong>Längengrad (LON):</strong>
<span id="longitude" style="font-family: monospace; color: #1565c0;"></span>
</div>
</div>
<div style="margin-top: 15px; text-align: center;">
<div style="font-size: 0.85em; color: #666; margin-bottom: 10px;">
💡 Der Standort wird automatisch beim Generieren der Lizenz gespeichert
</div>
<button id="saveLocationBtn" onclick="saveLocationToDatabase()" style="padding: 12px 24px; background: #2196f3; color: white; border: none; border-radius: 8px; cursor: pointer; font-weight: 500; transition: all 0.3s ease;">
💾 Standort manuell speichern
</button>
</div>
</div>
</div>
<!-- Karten-Container -->
<div id="mapContainer" class="map-container" style="display: none; margin-top: 20px;">
<h4 style="margin: 0 0 15px 0; color: #333;">🗺️ Standort auf der Karte:</h4>
<div id="mapFrame" style="width: 100%; height: 300px; border: 2px solid #ddd; border-radius: 12px; overflow: hidden;"></div>
</div>
<div class="info-text" style="margin-top: 10px; font-size: 0.8em;">
📝 Standorte werden in der lokalen PostgreSQL-Datenbank gespeichert
</div>
`;
dbConfig.classList.add("show");
} else {
dbConfig.classList.remove("show");
setTimeout(() => {
if (!dbConfig.classList.contains("show")) {
dbConfig.innerHTML = "";
}
}, 400);
}
}
const secret = "542ff224606c61fb3024e22f76ef9ac8";
function isValidMac(mac) {
const pattern = /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$|^[0-9A-Fa-f]{12}$/;
return pattern.test(mac);
}
function showMessage(elementId, message, isError = false) {
const messageDiv = document.getElementById(elementId);
messageDiv.textContent = message;
messageDiv.classList.add("show");
setTimeout(() => {
messageDiv.classList.remove("show");
}, 4000);
}
function showError(message) {
showMessage("error", message, true);
}
function showSuccess(message) {
showMessage("success", message, false);
}
function setLoading(isLoading) {
const btnText = document.getElementById("btn-text");
const btn = document.querySelector(".generate-btn");
if (isLoading) {
btnText.innerHTML = '<span class="loading"></span>Generiere...';
btn.disabled = true;
btn.style.opacity = '0.7';
} else {
btnText.textContent = 'Lizenz generieren';
btn.disabled = false;
btn.style.opacity = '1';
}
}
async function saveToDatabase(token, tier) {
const description = document.getElementById("description").value.trim();
const standorte = document.getElementById("standorte").value.trim();
try {
const response = await fetch('/api/v1/web/save-token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
token: token,
description: description || `API-Token Stufe ${tier}`,
standorte: standorte
})
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || 'Fehler beim Speichern in der Datenbank');
}
const result = await response.json();
return result;
} catch (error) {
// Fallback: Zeige dem Benutzer den SQL-Befehl an, den er manuell ausführen kann
const sql = `INSERT INTO api_tokens (token, description, standorte) VALUES ('${token}', '${description || `API-Token Stufe ${tier}`}', '${standorte}');`;
throw new Error(`Automatisches Speichern fehlgeschlagen. Server nicht erreichbar.\n\nFühren Sie folgenden SQL-Befehl manuell aus:\n${sql}`);
}
}
async function generateLicense() {
const macInput = document.getElementById("mac").value.trim();
const tierInput = document.getElementById("tier").value.trim();
const resultDiv = document.getElementById("result");
const licenseOutput = document.getElementById("license-output");
const errorDiv = document.getElementById("error");
const successDiv = document.getElementById("success");
// Reset states
resultDiv.classList.remove("show");
errorDiv.classList.remove("show");
successDiv.classList.remove("show");
setLoading(true);
// Simulate slight delay for better UX
await new Promise(resolve => setTimeout(resolve, 500));
try {
if (!isValidMac(macInput)) {
throw new Error("Ungültige MAC-Adresse. Bitte verwenden Sie das Format 00:1A:2B:3C:4D:5E");
}
const mac = macInput.replace(/[:-]/g, "").toUpperCase();
const tier = parseInt(tierInput);
if (isNaN(tier) || tier < 1 || tier > 4) {
throw new Error("Lizenzstufe muss eine Zahl zwischen 1 und 4 sein.");
}
// Standort automatisch speichern, falls vorhanden
let locationSaved = false;
const locationName = document.getElementById('locationSearch')?.value?.trim();
const latitude = document.getElementById('latitude')?.textContent;
const longitude = document.getElementById('longitude')?.textContent;
if (locationName && latitude && longitude && tier >= 3) {
try {
await saveLocationToDatabase();
locationSaved = true;
} catch (locationError) {
console.warn('Standort konnte nicht gespeichert werden:', locationError);
// Fahre trotzdem mit der Lizenzgenerierung fort
}
}
const data = `${mac}:${tier}`;
const enc = new TextEncoder();
const key = await crypto.subtle.importKey(
"raw",
enc.encode(secret),
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"]
);
const signature = await crypto.subtle.sign("HMAC", key, enc.encode(data));
const hex = Array.from(new Uint8Array(signature))
.map(b => b.toString(16).padStart(2, "0"))
.join("")
.toUpperCase();
licenseOutput.textContent = hex;
resultDiv.classList.add("show");
// Reset copy button
const copyBtn = document.getElementById("copyButton");
copyBtn.textContent = "📋 In Zwischenablage kopieren";
copyBtn.classList.remove("copied");
// Bei Stufe 3+ in Datenbank speichern
if (tier >= 3) {
try {
await saveToDatabase(hex, tier);
let successMessage = `✅ Lizenzschlüssel generiert und als API-Token gespeichert!`;
if (locationSaved) {
successMessage += ` Standort wurde ebenfalls gespeichert.`;
}
showSuccess(successMessage);
} catch (dbError) {
showError(`⚠️ Lizenz generiert, aber Datenbank-Fehler: ${dbError.message}`);
}
} else {
let successMessage = `✅ Lizenzschlüssel erfolgreich generiert!`;
if (locationSaved) {
successMessage += ` Standort wurde in der Datenbank gespeichert.`;
}
showSuccess(successMessage);
}
} catch (error) {
showError(error.message);
} finally {
setLoading(false);
}
}
async function copyToClipboard() {
const licenseOutput = document.getElementById("license-output");
const copyBtn = document.getElementById("copyButton");
try {
await navigator.clipboard.writeText(licenseOutput.textContent);
copyBtn.textContent = "✅ Kopiert!";
copyBtn.classList.add("copied");
setTimeout(() => {
copyBtn.textContent = "📋 In Zwischenablage kopieren";
copyBtn.classList.remove("copied");
}, 2000);
} catch (err) {
// Fallback for older browsers
const textArea = document.createElement("textarea");
textArea.value = licenseOutput.textContent;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
copyBtn.textContent = "✅ Kopiert!";
copyBtn.classList.add("copied");
setTimeout(() => {
copyBtn.textContent = "📋 In Zwischenablage kopieren";
copyBtn.classList.remove("copied");
}, 2000);
}
}
// Enter key support
document.addEventListener('keypress', function (e) {
if (e.key === 'Enter') {
generateLicense();
}
});
// Input formatting for MAC address
document.getElementById('mac').addEventListener('input', function (e) {
let value = e.target.value.replace(/[^0-9A-Fa-f]/g, '');
if (value.length > 12) value = value.substr(0, 12);
// Add colons every 2 characters
value = value.replace(/(.{2})/g, '$1:').replace(/:$/, '');
e.target.value = value;
});
// Input event listener für Lizenzstufe
document.getElementById('tier').addEventListener('input', toggleTokenFields);
// Standortsuche-Funktionalität
async function searchLocation(buttonElement) {
const locationInput = document.getElementById('locationSearch').value.trim();
const coordinatesDiv = document.getElementById('coordinates');
const mapContainer = document.getElementById('mapContainer');
const latitudeSpan = document.getElementById('latitude');
const longitudeSpan = document.getElementById('longitude');
const mapFrame = document.getElementById('mapFrame');
if (!locationInput) {
showError('Bitte geben Sie einen Standort ein.');
return;
}
let originalText = '';
let searchBtn = null;
try {
// Zeige Ladeanimation
searchBtn = buttonElement || document.querySelector('button[onclick*="searchLocation"]');
if (searchBtn) {
originalText = searchBtn.innerHTML;
searchBtn.innerHTML = '<span class="loading"></span>Suche...';
searchBtn.disabled = true;
}
// API-Abfrage an Nominatim (OpenStreetMap)
const response = await fetch(`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(locationInput)}&limit=1`);
if (!response.ok) {
throw new Error('Fehler bei der API-Abfrage');
}
const data = await response.json();
if (data.length === 0) {
throw new Error('Standort nicht gefunden. Bitte versuchen Sie eine andere Beschreibung.');
}
const location = data[0];
const lat = parseFloat(location.lat);
const lon = parseFloat(location.lon);
// Der Name wird vom User bestimmt - nur Koordinaten aus der API verwenden
// Kein verstecktes Feld nötig, da der User den Namen selbst eingibt
// Koordinaten anzeigen
updateCoordinates(lat, lon);
coordinatesDiv.style.display = 'block';
// Interaktive Karte erstellen
createInteractiveMap(lat, lon);
mapContainer.style.display = 'block';
// Erfolgsmeldung
showSuccess(`✅ Koordinaten für "${locationInput}" erfolgreich gefunden! Klicken Sie auf die Karte, um den Pin zu verschieben.`);
} catch (error) {
showError(`Fehler bei der Standortsuche: ${error.message}`);
coordinatesDiv.style.display = 'none';
mapContainer.style.display = 'none';
} finally {
// Button zurücksetzen
if (searchBtn && originalText) {
searchBtn.innerHTML = originalText;
searchBtn.disabled = false;
}
}
}
// Koordinaten aktualisieren
function updateCoordinates(lat, lon) {
const latitudeSpan = document.getElementById('latitude');
const longitudeSpan = document.getElementById('longitude');
if (latitudeSpan && longitudeSpan) {
latitudeSpan.textContent = lat.toFixed(6);
longitudeSpan.textContent = lon.toFixed(6);
}
}
// Interaktive Karte erstellen
function createInteractiveMap(initialLat, initialLon) {
const mapFrame = document.getElementById('mapFrame');
// Verwende Leaflet.js für interaktive Karte
const mapHtml = `
<div id="interactiveMap" style="width: 100%; height: 100%; position: relative;">
<div id="map" style="width: 100%; height: 100%; border-radius: 10px;"></div>
<div style="position: absolute; top: 10px; right: 10px; background: white; padding: 8px; border-radius: 6px; box-shadow: 0 2px 8px rgba(0,0,0,0.2); font-size: 12px; color: #666;">
📍 Klicken Sie auf die Karte, um den Pin zu verschieben
</div>
</div>
`;
mapFrame.innerHTML = mapHtml;
// Leaflet.js laden und Karte initialisieren
loadLeafletAndCreateMap(initialLat, initialLon);
}
// Leaflet.js laden und Karte erstellen
function loadLeafletAndCreateMap(initialLat, initialLon) {
// Prüfe ob Leaflet bereits geladen ist
if (typeof L !== 'undefined') {
createMap(initialLat, initialLon);
return;
}
// Leaflet CSS laden
const leafletCSS = document.createElement('link');
leafletCSS.rel = 'stylesheet';
leafletCSS.href = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css';
document.head.appendChild(leafletCSS);
// Leaflet JavaScript laden
const leafletScript = document.createElement('script');
leafletScript.src = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js';
leafletScript.onload = () => createMap(initialLat, initialLon);
document.head.appendChild(leafletScript);
}
// Karte mit Leaflet erstellen
function createMap(initialLat, initialLon) {
try {
const map = L.map('map').setView([initialLat, initialLon], 15);
// OpenStreetMap Tile Layer
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);
// Marker erstellen
const marker = L.marker([initialLat, initialLon], {
draggable: true,
title: 'Standort'
}).addTo(map);
// Marker-Drag Event
marker.on('dragend', function (event) {
const newLat = event.target.getLatLng().lat;
const newLon = event.target.getLatLng().lng;
updateCoordinates(newLat, newLon);
showSuccess(`📍 Pin auf neue Position verschoben: ${newLat.toFixed(6)}, ${newLon.toFixed(6)}`);
});
// Klick-Event auf die Karte
map.on('click', function (event) {
const newLat = event.latlng.lat;
const newLon = event.latlng.lng;
// Marker auf neue Position setzen
marker.setLatLng([newLat, newLon]);
// Koordinaten aktualisieren
updateCoordinates(newLat, newLon);
// Erfolgsmeldung
showSuccess(`📍 Pin auf neue Position gesetzt: ${newLat.toFixed(6)}, ${newLon.toFixed(6)}`);
});
// Zoom-Controls hinzufügen
map.zoomControl.setPosition('bottomright');
} catch (error) {
console.error('Fehler beim Erstellen der Karte:', error);
// Fallback zu iframe
const mapFrame = document.getElementById('mapFrame');
const mapUrl = `https://www.openstreetmap.org/export/embed.html?bbox=${initialLon - 0.01},${initialLat - 0.01},${initialLon + 0.01},${initialLat + 0.01}&layer=mapnik&marker=${initialLat},${initialLon}`;
mapFrame.innerHTML = `<iframe src="${mapUrl}" width="100%" height="100%" frameborder="0" scrolling="no" marginheight="0" marginwidth="0" title="Standort auf der Karte"></iframe>`;
}
}
// Standort in Datenbank speichern
async function saveLocationToDatabase() {
const locationName = document.getElementById('standorte').value.trim();
const latitude = document.getElementById('latitude').textContent;
const longitude = document.getElementById('longitude').textContent;
const saveBtn = document.getElementById('saveLocationBtn');
if (!locationName || !latitude || !longitude) {
showError('Bitte suchen Sie zuerst einen Standort.');
return;
}
try {
// Button-Status ändern
const originalText = saveBtn.innerHTML;
saveBtn.innerHTML = '<span class="loading"></span>Speichere...';
saveBtn.disabled = true;
// Web-authenticated API für Standortverwaltung aufrufen
const response = await fetch('/api/v1/web/create-location', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: locationName,
lat: parseFloat(latitude),
lon: parseFloat(longitude)
})
});
const result = await response.json();
if (result.success) {
showSuccess(`✅ Standort "${locationName}" erfolgreich in der Datenbank gespeichert!`);
saveBtn.innerHTML = '✅ Gespeichert!';
saveBtn.style.background = '#4caf50';
// Button nach 3 Sekunden zurücksetzen
setTimeout(() => {
saveBtn.innerHTML = originalText;
saveBtn.disabled = false;
saveBtn.style.background = '#2196f3';
}, 3000);
} else {
throw new Error(result.message || 'Unbekannter Fehler beim Speichern');
}
} catch (error) {
console.error('Fehler beim Speichern:', error);
showError(`Fehler beim Speichern: ${error.message}`);
// Button zurücksetzen
saveBtn.innerHTML = '💾 Standort in Datenbank speichern';
saveBtn.disabled = false;
}
}
// Zurück zum Dashboard
function goBackToDashboard() {
window.location.href = '/admin-dashboard';
}
// Logout-Funktion
async function logout() {
try {
const response = await fetch('/api/v1/public/logout', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
});
const result = await response.json();
if (result.success) {
window.location.href = '/login';
} else {
console.error('Fehler beim Abmelden:', result.message);
// Trotzdem zur Login-Seite weiterleiten
window.location.href = '/login';
}
} catch (error) {
console.error('Fehler beim Abmelden:', error);
// Bei Fehler trotzdem zur Login-Seite weiterleiten
window.location.href = '/login';
}
}
// Enter-Taste für Standortsuche
document.addEventListener('DOMContentLoaded', function () {
const locationSearch = document.getElementById('locationSearch');
if (locationSearch) {
locationSearch.addEventListener('keypress', function (e) {
if (e.key === 'Enter') {
searchLocation();
}
});
}
// Add cookie settings button functionality
const cookieSettingsBtn = document.getElementById('cookie-settings-footer');
if (cookieSettingsBtn) {
cookieSettingsBtn.addEventListener('click', function () {
if (window.cookieConsent) {
window.cookieConsent.resetConsent();
}
});
}
});

808
public/js/index.js Normal file
View File

@@ -0,0 +1,808 @@
// 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);
// Initialize Socket.IO connection
let socket;
function setupSocketListeners() {
if (!socket) return;
socket.on('connect', () => {
console.log('🔌 WebSocket connected');
});
socket.on('disconnect', () => {
console.log('🔌 WebSocket disconnected');
});
socket.on('newTime', (data) => {
console.log('🏁 New time received:', data);
showNotification(data);
// Reload data to show the new time
loadData();
});
}
function initializeSocket() {
if (typeof io !== 'undefined') {
socket = io();
setupSocketListeners();
} else {
console.error('Socket.IO library not loaded');
}
}
// Try to initialize immediately, fallback to DOMContentLoaded
if (typeof io !== 'undefined') {
initializeSocket();
} else {
document.addEventListener('DOMContentLoaded', initializeSocket);
}
// 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 are now in setupSocketListeners() function
// Notification Functions
function showNotification(timeData) {
const notificationBubble = document.getElementById('notificationBubble');
const notificationTitle = document.getElementById('notificationTitle');
const notificationSubtitle = document.getElementById('notificationSubtitle');
// Format the time data
const playerName = timeData.player_name || (currentLanguage === 'de' ? 'Unbekannter Spieler' : 'Unknown Player');
const locationName = timeData.location_name || (currentLanguage === 'de' ? 'Unbekannter Standort' : 'Unknown Location');
const timeString = timeData.recorded_time || '--:--';
// Update notification content
const newTimeText = currentLanguage === 'de' ? 'Neue Zeit von' : 'New time from';
notificationTitle.textContent = `🏁 ${newTimeText} ${playerName}!`;
notificationSubtitle.textContent = `${timeString}${locationName}`;
// Ensure notification is above sticky header
notificationBubble.style.zIndex = '100000';
// Check if we're on mobile and adjust position
if (window.innerWidth <= 768) {
notificationBubble.style.top = '5rem'; // Below sticky header on mobile
} else {
notificationBubble.style.top = '2rem'; // Normal position on desktop
}
// Show notification
notificationBubble.classList.remove('hide');
notificationBubble.classList.add('show');
// Auto-hide after 5 seconds
setTimeout(() => {
hideNotification();
}, 5000);
}
function hideNotification() {
const notificationBubble = document.getElementById('notificationBubble');
notificationBubble.classList.remove('show');
notificationBubble.classList.add('hide');
// Remove hide class after animation
setTimeout(() => {
notificationBubble.classList.remove('hide');
}, 300);
}
// Check authentication status
async function checkAuth() {
try {
const { data: { session } } = await supabase.auth.getSession();
if (session) {
// User is logged in, show dashboard button
document.getElementById('adminLoginBtn').style.display = 'none';
document.getElementById('dashboardBtn').style.display = 'inline-block';
document.getElementById('logoutBtn').style.display = 'inline-block';
} else {
// User is not logged in, show admin login button
document.getElementById('adminLoginBtn').style.display = 'inline-block';
document.getElementById('dashboardBtn').style.display = 'none';
document.getElementById('logoutBtn').style.display = 'none';
}
} catch (error) {
console.error('Error checking auth:', error);
// Fallback: show login button if auth check fails
document.getElementById('adminLoginBtn').style.display = 'inline-block';
document.getElementById('dashboardBtn').style.display = 'none';
document.getElementById('logoutBtn').style.display = 'none';
}
}
// Logout function
async function logout() {
try {
const { error } = await supabase.auth.signOut();
if (error) {
console.error('Error logging out:', error);
} else {
window.location.reload();
}
} catch (error) {
console.error('Error during logout:', error);
window.location.reload();
}
}
// Load locations from database
async function loadLocations() {
try {
const response = await fetch('/api/v1/public/locations');
if (!response.ok) {
throw new Error('Failed to fetch locations');
}
const responseData = await response.json();
const locations = responseData.data || responseData; // Handle both formats
const locationSelect = document.getElementById('locationSelect');
// Store locations globally for distance calculations
locationsData = locations;
// Clear existing options and set default placeholder
const placeholderText = currentLanguage === 'de' ? '📍 Bitte Standort auswählen' : '📍 Please select location';
locationSelect.innerHTML = `<option value="">${placeholderText}</option>`;
// Add locations from database
locations.forEach(location => {
const option = document.createElement('option');
option.value = location.name;
option.textContent = `📍 ${location.name}`;
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);
}
}
// Calculate distance between two points using Haversine formula
function calculateDistance(lat1, lon1, lat2, lon2) {
const R = 6371; // Earth's radius in kilometers
const dLat = toRadians(lat2 - lat1);
const dLon = toRadians(lon2 - lon1);
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(toRadians(lat1)) * Math.cos(toRadians(lat2)) *
Math.sin(dLon / 2) * Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
const distance = R * c; // Distance in kilometers
return distance;
}
function toRadians(degrees) {
return degrees * (Math.PI / 180);
}
// Find nearest location based on user's current position
async function findNearestLocation() {
const btn = document.getElementById('findLocationBtn');
const locationSelect = document.getElementById('locationSelect');
// Check if geolocation is supported
if (!navigator.geolocation) {
const errorMsg = currentLanguage === 'de' ?
'Geolocation wird von diesem Browser nicht unterstützt.' :
'Geolocation is not supported by this browser.';
showLocationError(errorMsg);
return;
}
// Update button state to loading
btn.disabled = true;
btn.classList.add('loading');
btn.textContent = currentLanguage === 'de' ? '🔍 Suche...' : '🔍 Searching...';
try {
// Get user's current position
const position = await new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition(
resolve,
reject,
{
enableHighAccuracy: true,
timeout: 10000,
maximumAge: 300000 // 5 minutes
}
);
});
const userLat = position.coords.latitude;
const userLon = position.coords.longitude;
// Calculate distances to all locations
const locationsWithDistance = locationsData.map(location => ({
...location,
distance: calculateDistance(
userLat,
userLon,
parseFloat(location.latitude),
parseFloat(location.longitude)
)
}));
// Find the nearest location
const nearestLocation = locationsWithDistance.reduce((nearest, current) => {
return current.distance < nearest.distance ? current : nearest;
});
// Select the nearest location in the dropdown
locationSelect.value = nearestLocation.name;
// Trigger change event to update the leaderboard
locationSelect.dispatchEvent(new Event('change'));
// Show success notification
showLocationSuccess(nearestLocation.name, nearestLocation.distance);
} catch (error) {
console.error('Error getting location:', error);
let errorMessage = currentLanguage === 'de' ? 'Standort konnte nicht ermittelt werden.' : 'Location could not be determined.';
if (error.code) {
switch (error.code) {
case error.PERMISSION_DENIED:
errorMessage = currentLanguage === 'de' ?
'Standortzugriff wurde verweigert. Bitte erlaube den Standortzugriff in den Browser-Einstellungen.' :
'Location access was denied. Please allow location access in browser settings.';
break;
case error.POSITION_UNAVAILABLE:
errorMessage = currentLanguage === 'de' ?
'Standortinformationen sind nicht verfügbar.' :
'Location information is not available.';
break;
case error.TIMEOUT:
errorMessage = currentLanguage === 'de' ?
'Zeitüberschreitung beim Abrufen des Standorts.' :
'Timeout while retrieving location.';
break;
}
}
showLocationError(errorMessage);
} finally {
// Reset button state
btn.disabled = false;
btn.classList.remove('loading');
btn.textContent = currentLanguage === 'de' ? '📍 Mein Standort' : '📍 My Location';
}
}
// Show success notification for location finding
function showLocationSuccess(locationName, distance) {
const notificationBubble = document.getElementById('notificationBubble');
const notificationTitle = document.getElementById('notificationTitle');
const notificationSubtitle = document.getElementById('notificationSubtitle');
// Update notification content
const locationFoundText = currentLanguage === 'de' ? 'Standort gefunden!' : 'Location found!';
const distanceText = currentLanguage === 'de' ? 'km entfernt' : 'km away';
notificationTitle.textContent = `📍 ${locationFoundText}`;
notificationSubtitle.textContent = `${locationName} (${distance.toFixed(1)} ${distanceText})`;
// Ensure notification is above sticky header
notificationBubble.style.zIndex = '100000';
// Check if we're on mobile and adjust position
if (window.innerWidth <= 768) {
notificationBubble.style.top = '5rem'; // Below sticky header on mobile
} else {
notificationBubble.style.top = '2rem'; // Normal position on desktop
}
// Show notification
notificationBubble.classList.remove('hide');
notificationBubble.classList.add('show');
// Auto-hide after 4 seconds
setTimeout(() => {
hideNotification();
}, 4000);
}
// Show error notification for location finding
function showLocationError(message) {
const notificationBubble = document.getElementById('notificationBubble');
const notificationTitle = document.getElementById('notificationTitle');
const notificationSubtitle = document.getElementById('notificationSubtitle');
// Change notification style to error
notificationBubble.style.background = 'linear-gradient(135deg, #dc3545, #c82333)';
// Update notification content
const errorText = currentLanguage === 'de' ? 'Fehler' : 'Error';
notificationTitle.textContent = `${errorText}`;
notificationSubtitle.textContent = message;
// Ensure notification is above sticky header
notificationBubble.style.zIndex = '100000';
// Check if we're on mobile and adjust position
if (window.innerWidth <= 768) {
notificationBubble.style.top = '5rem'; // Below sticky header on mobile
} else {
notificationBubble.style.top = '2rem'; // Normal position on desktop
}
// Show notification
notificationBubble.classList.remove('hide');
notificationBubble.classList.add('show');
// Auto-hide after 6 seconds
setTimeout(() => {
hideNotification();
// Reset notification style
notificationBubble.style.background = 'linear-gradient(135deg, #00d4ff, #0891b2)';
}, 6000);
}
// Show prompt when no location is selected
function showLocationSelectionPrompt() {
const rankingList = document.getElementById('rankingList');
const emptyTitle = currentLanguage === 'de' ? 'Standort auswählen' : 'Select Location';
const emptyDescription = currentLanguage === 'de' ?
'Bitte wähle einen Standort aus dem Dropdown-Menü aus<br>oder nutze den "📍 Mein Standort" Button, um automatisch<br>den nächstgelegenen Standort zu finden.' :
'Please select a location from the dropdown menu<br>or use the "📍 My Location" button to automatically<br>find the nearest location.';
rankingList.innerHTML = `
<div class="empty-state">
<div class="empty-icon">📍</div>
<div class="empty-title">${emptyTitle}</div>
<div class="empty-description">${emptyDescription}</div>
</div>
`;
// Reset stats to show no data
document.getElementById('totalPlayers').textContent = '0';
document.getElementById('bestTime').textContent = '--:--';
document.getElementById('totalRecords').textContent = '0';
// Update current selection display
updateCurrentSelection();
}
// Load data from local database via MCP
async function loadData() {
try {
const location = document.getElementById('locationSelect').value;
const period = document.querySelector('.time-tab.active').dataset.period;
// Don't load data if no location is selected
if (!location || location === '') {
showLocationSelectionPrompt();
return;
}
// Build query parameters
const params = new URLSearchParams();
if (location && location !== 'all') {
params.append('location', location);
}
if (period && period !== 'all') {
params.append('period', period);
}
// Fetch times with player and location data from local database
const response = await fetch(`/api/v1/public/times-with-details?${params.toString()}`);
if (!response.ok) {
throw new Error('Failed to fetch times');
}
const times = await response.json();
// Convert to the format expected by the leaderboard
const leaderboardData = times.map(time => {
const { minutes, seconds, milliseconds } = time.recorded_time;
const timeString = `${minutes}:${seconds.toString().padStart(2, '0')}.${milliseconds}`;
const playerName = time.player ?
`${time.player.firstname} ${time.player.lastname}` :
(currentLanguage === 'de' ? 'Unbekannter Spieler' : 'Unknown Player');
const locationName = time.location ? time.location.name :
(currentLanguage === 'de' ? 'Unbekannter Standort' : 'Unknown Location');
const date = new Date(time.created_at).toISOString().split('T')[0];
return {
name: playerName,
time: timeString,
date: date,
location: locationName
};
});
// Sort by time (fastest first)
leaderboardData.sort((a, b) => {
const timeA = timeToSeconds(a.time);
const timeB = timeToSeconds(b.time);
return timeA - timeB;
});
updateLeaderboard(leaderboardData);
updateStats(leaderboardData);
updateCurrentSelection();
} catch (error) {
console.error('Error loading data:', error);
// Fallback to sample data if API fails
loadSampleData();
}
}
// Fallback sample data based on real database data
function loadSampleData() {
const sampleData = [
{ name: "Carsten Graf", time: "01:28.945", date: "2025-08-30", location: "Ulm Donaubad" },
{ name: "Carsten Graf", time: "01:30.945", date: "2025-08-30", location: "Ulm Donaubad" },
{ name: "Max Mustermann", time: "01:50.945", date: "2025-08-30", location: "Ulm Donaubad" },
{ name: "Carsten Graf", time: "02:50.945", date: "2025-08-31", location: "Test" },
{ name: "Max Mustermann", time: "02:50.945", date: "2025-08-31", location: "Test" },
{ name: "Carsten Graf", time: "01:10.945", date: "2025-09-02", location: "Test" },
{ name: "Carsten Graf", time: "01:11.945", date: "2025-09-02", location: "Test" },
{ name: "Carsten Graf", time: "01:11.945", date: "2025-09-02", location: "Ulm Donaubad" }
];
updateLeaderboard(sampleData);
updateStats(sampleData);
updateCurrentSelection();
}
function timeToSeconds(timeStr) {
const [minutes, seconds] = timeStr.split(':');
return parseFloat(minutes) * 60 + parseFloat(seconds);
}
function updateStats(data) {
const totalPlayers = new Set(data.map(item => item.name)).size;
const bestTime = data.length > 0 ? data[0].time : '--:--';
const totalRecords = data.length;
document.getElementById('totalPlayers').textContent = totalPlayers;
document.getElementById('bestTime').textContent = bestTime;
document.getElementById('totalRecords').textContent = totalRecords;
}
function updateCurrentSelection() {
const location = document.getElementById('locationSelect').value;
const period = document.querySelector('.time-tab.active').dataset.period;
// Get the display text from the selected option
const locationSelect = document.getElementById('locationSelect');
const selectedLocationOption = locationSelect.options[locationSelect.selectedIndex];
const locationDisplay = selectedLocationOption ? selectedLocationOption.textContent :
(currentLanguage === 'de' ? '📍 Bitte Standort auswählen' : '📍 Please select location');
const periodIcons = currentLanguage === 'de' ? {
'today': '📅 Heute',
'week': '📊 Diese Woche',
'month': '📈 Dieser Monat',
'all': '♾️ Alle Zeiten'
} : {
'today': '📅 Today',
'week': '📊 This Week',
'month': '📈 This Month',
'all': '♾️ All Times'
};
document.getElementById('currentSelection').textContent =
`${locationDisplay}${periodIcons[period]}`;
const lastSyncText = currentLanguage === 'de' ? 'Letzter Sync' : 'Last Sync';
document.getElementById('lastUpdated').textContent =
`${lastSyncText}: ${new Date().toLocaleTimeString(currentLanguage === 'de' ? 'de-DE' : 'en-US')}`;
}
function updateLeaderboard(data) {
const rankingList = document.getElementById('rankingList');
if (data.length === 0) {
const emptyTitle = currentLanguage === 'de' ? 'Keine Rekorde gefunden' : 'No records found';
const emptyDescription = currentLanguage === 'de' ?
'Für diese Filtereinstellungen liegen noch keine Zeiten vor.<br>Versuche es mit einem anderen Zeitraum oder Standort.' :
'No times available for these filter settings.<br>Try a different time period or location.';
rankingList.innerHTML = `
<div class="empty-state">
<div class="empty-icon">🏁</div>
<div class="empty-title">${emptyTitle}</div>
<div class="empty-description">${emptyDescription}</div>
</div>
`;
return;
}
rankingList.innerHTML = data.map((player, index) => {
const rank = index + 1;
let positionClass = '';
let trophy = '';
if (rank === 1) {
positionClass = 'gold';
trophy = '👑';
} else if (rank === 2) {
positionClass = 'silver';
trophy = '🥈';
} else if (rank === 3) {
positionClass = 'bronze';
trophy = '🥉';
} else if (rank <= 10) {
trophy = '⭐';
}
const formatDate = new Date(player.date).toLocaleDateString(currentLanguage === 'de' ? 'de-DE' : 'en-US', {
day: '2-digit',
month: 'short'
});
return `
<div class="rank-entry">
<div class="position ${positionClass}">#${rank}</div>
<div class="player-data">
<div class="player-name">${player.name}</div>
<div class="player-meta">
<span class="location-tag">${player.location}</span>
<span>🗓️ ${formatDate}</span>
</div>
</div>
<div class="time-result">${player.time}</div>
${trophy ? `<div class="trophy-icon">${trophy}</div>` : '<div class="trophy-icon"></div>'}
</div>
`;
}).join('');
}
// Event Listeners Setup
function setupEventListeners() {
// Location select event listener
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 => {
tab.addEventListener('click', function () {
// Remove active class from all tabs
document.querySelectorAll('.time-tab').forEach(t => t.classList.remove('active'));
// Add active class to clicked tab
this.classList.add('active');
// Load data with new period
loadData();
});
});
// Smooth scroll for better UX
const rankingsList = document.querySelector('.rankings-list');
if (rankingsList) {
rankingsList.style.scrollBehavior = 'smooth';
}
}
// Initialize page
async function init() {
await checkAuth();
await loadLocations();
showLocationSelectionPrompt(); // Show prompt instead of loading data initially
setupEventListeners();
}
// Auto-refresh function
function startAutoRefresh() {
setInterval(loadData, 45000);
}
// Language Management
let currentLanguage = 'en'; // Default to English
// Translation function
function translateElement(element, language) {
if (element.dataset[language]) {
// Check if the content contains HTML tags
if (element.dataset[language].includes('<')) {
element.innerHTML = element.dataset[language];
} else {
element.textContent = element.dataset[language];
}
}
}
// Change language function
function changeLanguage() {
const languageSelect = document.getElementById('languageSelect');
currentLanguage = languageSelect.value;
// Save language preference
localStorage.setItem('ninjacross_language', currentLanguage);
// Update flag in select
updateLanguageFlag();
// Translate all elements with data attributes
const elementsToTranslate = document.querySelectorAll('[data-de][data-en]');
elementsToTranslate.forEach(element => {
translateElement(element, currentLanguage);
});
// Update dynamic content
updateDynamicContent();
console.log(`🌐 Language changed to: ${currentLanguage}`);
}
// Update flag in language selector
function updateLanguageFlag() {
const languageSelect = document.getElementById('languageSelect');
if (languageSelect) {
if (currentLanguage === 'de') {
// German flag (black-red-gold)
languageSelect.style.backgroundImage = `url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="20" height="15" viewBox="0 0 20 15"><rect width="20" height="5" fill="%23000000"/><rect y="5" width="20" height="5" fill="%23DD0000"/><rect y="10" width="20" height="5" fill="%23FFCE00"/></svg>')`;
} else {
// USA flag
languageSelect.style.backgroundImage = `url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="20" height="15" viewBox="0 0 20 15"><rect width="20" height="15" fill="%23B22234"/><rect width="20" height="1.15" fill="%23FFFFFF"/><rect y="2.3" width="20" height="1.15" fill="%23FFFFFF"/><rect y="4.6" width="20" height="1.15" fill="%23FFFFFF"/><rect y="6.9" width="20" height="1.15" fill="%23FFFFFF"/><rect y="9.2" width="20" height="1.15" fill="%23FFFFFF"/><rect y="11.5" width="20" height="1.15" fill="%23FFFFFF"/><rect y="13.8" width="20" height="1.15" fill="%23FFFFFF"/><rect width="7.7" height="8.05" fill="%230033A0"/></svg>')`;
}
}
}
// Update dynamic content that's not in HTML
function updateDynamicContent() {
// Update location select placeholder
const locationSelect = document.getElementById('locationSelect');
if (locationSelect && locationSelect.options[0]) {
locationSelect.options[0].textContent = currentLanguage === 'de' ?
'📍 Bitte Standort auswählen' : '📍 Please select location';
}
// Update find location button
const findLocationBtn = document.getElementById('findLocationBtn');
if (findLocationBtn) {
findLocationBtn.textContent = currentLanguage === 'de' ?
'📍 Mein Standort' : '📍 My Location';
findLocationBtn.title = currentLanguage === 'de' ?
'Nächstgelegenen Standort finden' : 'Find nearest location';
}
// Update refresh button
const refreshBtn = document.querySelector('.refresh-btn');
if (refreshBtn) {
refreshBtn.textContent = currentLanguage === 'de' ?
'⚡ Live Update' : '⚡ Live Update';
}
// Update notification elements
const notificationTitle = document.getElementById('notificationTitle');
const notificationSubtitle = document.getElementById('notificationSubtitle');
if (notificationTitle) {
notificationTitle.textContent = currentLanguage === 'de' ? 'Neue Zeit!' : 'New Time!';
}
if (notificationSubtitle) {
notificationSubtitle.textContent = currentLanguage === 'de' ?
'Ein neuer Rekord wurde erstellt' : 'A new record has been created';
}
// Update current selection display
updateCurrentSelection();
// Reload data to update any dynamic content
if (document.getElementById('locationSelect').value) {
loadData();
} else {
showLocationSelectionPrompt();
}
}
// Load saved language preference
function loadLanguagePreference() {
const savedLanguage = localStorage.getItem('ninjacross_language');
if (savedLanguage && (savedLanguage === 'de' || savedLanguage === 'en')) {
currentLanguage = savedLanguage;
const languageSelect = document.getElementById('languageSelect');
if (languageSelect) {
languageSelect.value = currentLanguage;
// Update flag when loading
updateLanguageFlag();
}
}
}
// Start the application when DOM is loaded
document.addEventListener('DOMContentLoaded', function () {
loadLanguagePreference();
changeLanguage(); // Apply saved language
init();
startAutoRefresh();
// Add cookie settings button functionality
const cookieSettingsBtn = document.getElementById('cookie-settings-footer');
if (cookieSettingsBtn) {
cookieSettingsBtn.addEventListener('click', function () {
if (window.cookieConsent) {
window.cookieConsent.resetConsent();
}
});
}
});

291
public/js/login.js Normal file
View File

@@ -0,0 +1,291 @@
// 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);
// Check if user is already logged in
async function checkAuth() {
const { data: { session } } = await supabase.auth.getSession();
if (session) {
// Show a message that user is already logged in
showMessage('Sie sind bereits eingeloggt! Weiterleitung zum Dashboard...', 'success');
setTimeout(() => {
window.location.href = '/';
}, 2000);
}
}
// Check if device is iOS
function isIOS() {
return /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
}
// Google OAuth Sign In
async function signInWithGoogle() {
try {
setLoading(true);
clearMessage();
// iOS-specific handling
if (isIOS()) {
// For iOS, use a different approach with popup
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: `${window.location.origin}/auth/callback`,
queryParams: {
access_type: 'offline',
prompt: 'consent',
},
skipBrowserRedirect: true // Important for iOS
}
});
if (error) {
console.error('Google OAuth error:', error);
showMessage('Fehler bei der Google-Anmeldung: ' + error.message, 'error');
setLoading(false);
return;
}
if (data.url) {
// Open in same window for iOS
window.location.href = data.url;
}
} else {
// Standard handling for other devices
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: `${window.location.origin}/auth/callback`
}
});
if (error) {
console.error('Google OAuth error:', error);
showMessage('Fehler bei der Google-Anmeldung: ' + error.message, 'error');
}
}
} catch (error) {
console.error('Google OAuth error:', error);
showMessage('Fehler bei der Google-Anmeldung: ' + error.message, 'error');
setLoading(false);
}
}
// Discord OAuth Sign In
async function signInWithDiscord() {
try {
setLoading(true);
clearMessage();
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'discord',
options: {
redirectTo: `${window.location.origin}/auth/callback`
}
});
if (error) {
console.error('Discord OAuth error:', error);
showMessage('Fehler bei der Discord-Anmeldung: ' + error.message, 'error');
}
// Note: OAuth redirects the page, so we don't need to handle success here
} catch (error) {
console.error('Discord OAuth error:', error);
showMessage('Fehler bei der Discord-Anmeldung: ' + error.message, 'error');
} finally {
setLoading(false);
}
}
// Toggle between login and register forms
function toggleForm() {
const loginForm = document.getElementById('loginForm');
const registerForm = document.getElementById('registerForm');
if (loginForm.classList.contains('active')) {
loginForm.classList.remove('active');
registerForm.classList.add('active');
} else {
registerForm.classList.remove('active');
loginForm.classList.add('active');
}
clearMessage();
showPasswordReset(false); // Hide password reset when switching forms
}
// Show message
function showMessage(message, type = 'success') {
const messageDiv = document.getElementById('message');
messageDiv.innerHTML = `<div class="message ${type}">${message}</div>`;
setTimeout(() => {
messageDiv.innerHTML = '';
}, 5000);
}
// Clear message
function clearMessage() {
document.getElementById('message').innerHTML = '';
}
// Show/hide password reset container
function showPasswordReset(show) {
const resetContainer = document.getElementById('passwordResetContainer');
if (show) {
resetContainer.classList.add('active');
} else {
resetContainer.classList.remove('active');
}
}
// Show/hide loading
function setLoading(show) {
const loading = document.getElementById('loading');
if (show) {
loading.classList.add('active');
} else {
loading.classList.remove('active');
}
}
// Event Listeners Setup
function setupEventListeners() {
// Handle Google OAuth
document.getElementById('googleSignInBtn').addEventListener('click', signInWithGoogle);
// Handle Discord OAuth
document.getElementById('discordSignInBtn').addEventListener('click', signInWithDiscord);
// Cookie settings button
const cookieSettingsBtn = document.getElementById('cookie-settings-footer');
if (cookieSettingsBtn) {
cookieSettingsBtn.addEventListener('click', function() {
if (window.cookieConsent) {
window.cookieConsent.resetConsent();
}
});
}
// Handle login
document.getElementById('loginFormElement').addEventListener('submit', async (e) => {
e.preventDefault();
setLoading(true);
clearMessage();
showPasswordReset(false); // Hide reset button initially
const email = document.getElementById('loginEmail').value;
const password = document.getElementById('loginPassword').value;
try {
const { data, error } = await supabase.auth.signInWithPassword({
email: email,
password: password
});
if (error) {
showMessage(error.message, 'error');
// Show password reset button on login failure
showPasswordReset(true);
} else {
showMessage('Login successful! Redirecting...', 'success');
setTimeout(() => {
window.location.href = '/';
}, 1000);
}
} catch (error) {
showMessage('An unexpected error occurred', 'error');
showPasswordReset(true);
} finally {
setLoading(false);
}
});
// Handle registration
document.getElementById('registerFormElement').addEventListener('submit', async (e) => {
e.preventDefault();
setLoading(true);
clearMessage();
const email = document.getElementById('registerEmail').value;
const password = document.getElementById('registerPassword').value;
const confirmPassword = document.getElementById('confirmPassword').value;
if (password !== confirmPassword) {
showMessage('Passwords do not match', 'error');
setLoading(false);
return;
}
if (password.length < 6) {
showMessage('Password must be at least 6 characters', 'error');
setLoading(false);
return;
}
try {
const { data, error } = await supabase.auth.signUp({
email: email,
password: password
});
if (error) {
showMessage(error.message, 'error');
} else {
if (data.user && !data.user.email_confirmed_at) {
showMessage('Registration successful! Please check your email to confirm your account.', 'success');
} else {
showMessage('Registration successful! Redirecting...', 'success');
setTimeout(() => {
window.location.href = '/';
}, 1000);
}
}
} catch (error) {
showMessage('An unexpected error occurred', 'error');
} finally {
setLoading(false);
}
});
// Handle password reset
document.getElementById('resetPasswordBtn').addEventListener('click', async () => {
const email = document.getElementById('loginEmail').value;
if (!email) {
showMessage('Bitte geben Sie zuerst Ihre E-Mail-Adresse ein', 'error');
return;
}
setLoading(true);
clearMessage();
try {
const { error } = await supabase.auth.resetPasswordForEmail(email, {
redirectTo: `${window.location.origin}/reset-password.html`
});
if (error) {
showMessage('Fehler beim Senden der E-Mail: ' + error.message, 'error');
} else {
showMessage('Passwort-Reset-E-Mail wurde gesendet! Bitte überprüfen Sie Ihr E-Mail-Postfach.', 'success');
showPasswordReset(false);
}
} catch (error) {
showMessage('Ein unerwarteter Fehler ist aufgetreten', 'error');
} finally {
setLoading(false);
}
});
}
// Initialize page
async function init() {
await checkAuth();
setupEventListeners();
}
// Start the application when DOM is loaded
document.addEventListener('DOMContentLoaded', init);

View File

@@ -0,0 +1,33 @@
// Page tracking functionality
function trackPageView(pageName) {
// Get user information
const userAgent = navigator.userAgent;
const referer = document.referrer || '';
// Send tracking data to server
fetch('/api/v1/public/track-page-view', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
page: pageName,
userAgent: userAgent,
ipAddress: null, // Will be determined by server
referer: referer
})
}).catch(error => {
console.log('Page tracking failed:', error);
// Silently fail - don't interrupt user experience
});
}
// Auto-track page on load - only track main page visits
document.addEventListener('DOMContentLoaded', function() {
// Only track the main page (index.html or root path)
const path = window.location.pathname;
if (path === '/' || path === '/index.html') {
trackPageView('main_page_visit');
}
});

202
public/js/reset-password.js Normal file
View File

@@ -0,0 +1,202 @@
// Supabase Konfiguration
const supabaseUrl = 'https://lfxlplnypzvjrhftaoog.supabase.co';
const supabaseKey = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImxmeGxwbG55cHp2anJoZnRhb29nIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDkyMTQ3NzIsImV4cCI6MjA2NDc5MDc3Mn0.XR4preBqWAQ1rT4PFbpkmRdz57BTwIusBI89fIxDHM8';
const supabase = window.supabase.createClient(supabaseUrl, supabaseKey);
// DOM Elemente
const resetForm = document.getElementById('resetForm');
const newPasswordInput = document.getElementById('newPassword');
const confirmPasswordInput = document.getElementById('confirmPassword');
const resetBtn = document.getElementById('resetBtn');
const loading = document.getElementById('loading');
const messageContainer = document.getElementById('messageContainer');
// Nachricht anzeigen (muss vor anderen Funktionen definiert werden)
function showMessage(type, message) {
messageContainer.innerHTML = `
<div class="message ${type}">
${message}
</div>
`;
}
// URL-Parameter extrahieren
console.log('URL Hash:', window.location.hash);
const urlParams = new URLSearchParams(window.location.hash.substring(1));
const accessToken = urlParams.get('access_token');
const refreshToken = urlParams.get('refresh_token');
const tokenType = urlParams.get('token_type');
console.log('Access Token gefunden:', !!accessToken);
console.log('Refresh Token gefunden:', !!refreshToken);
// Prüfen ob Reset-Token vorhanden ist
if (!accessToken) {
showMessage('error', 'Ungültiger oder fehlender Reset-Link. Bitte fordere einen neuen Reset-Link an.');
resetForm.style.display = 'none';
}
// Session mit Token setzen
async function setSession() {
if (!accessToken || !refreshToken) {
showMessage('error', 'Ungültiger Reset-Link. Tokens fehlen.');
resetForm.style.display = 'none';
return false;
}
try {
console.log('Setze Session mit Tokens...');
const { data, error } = await supabase.auth.setSession({
access_token: accessToken,
refresh_token: refreshToken
});
if (error) {
console.error('Session Error:', error);
throw error;
}
console.log('Session erfolgreich gesetzt:', data.user?.email);
showMessage('success', `Session aktiv für: ${data.user?.email}`);
return true;
} catch (error) {
console.error('Fehler beim Setzen der Session:', error);
showMessage('error', `Fehler beim Laden des Reset-Links: ${error.message}`);
resetForm.style.display = 'none';
return false;
}
}
// Passwort zurücksetzen
async function resetPassword(newPassword) {
try {
console.log('Starte Passwort-Update...');
// Erstmal Session prüfen
const { data: session } = await supabase.auth.getSession();
console.log('Aktuelle Session:', session);
if (!session.session) {
throw new Error('Keine aktive Session gefunden');
}
const { data, error } = await supabase.auth.updateUser({
password: newPassword
});
if (error) {
console.error('Update User Error:', error);
throw error;
}
console.log('Passwort erfolgreich aktualisiert:', data);
return { success: true, data };
} catch (error) {
console.error('Fehler beim Zurücksetzen des Passworts:', error);
return { success: false, error: error.message };
}
}
// Formular-Validierung
function validateForm() {
const newPassword = newPasswordInput.value;
const confirmPassword = confirmPasswordInput.value;
if (newPassword.length < 8) {
showMessage('error', 'Das Passwort muss mindestens 8 Zeichen lang sein.');
return false;
}
if (newPassword !== confirmPassword) {
showMessage('error', 'Die Passwörter stimmen nicht überein.');
return false;
}
return true;
}
// Formular-Submit Handler
resetForm.addEventListener('submit', async (e) => {
e.preventDefault();
if (!validateForm()) {
return;
}
// UI-Status ändern
resetBtn.disabled = true;
loading.style.display = 'block';
resetForm.style.display = 'none';
try {
// Warten bis Session gesetzt ist
const sessionSet = await setSession();
if (!sessionSet) {
throw new Error('Session konnte nicht gesetzt werden');
}
const result = await resetPassword(newPasswordInput.value);
if (result.success) {
showMessage('success', '✅ Passwort erfolgreich zurückgesetzt! Du wirst zur Hauptseite weitergeleitet...');
// Nach 3 Sekunden zur Hauptseite weiterleiten
setTimeout(() => {
window.location.href = '/';
}, 3000);
} else {
showMessage('error', `❌ Fehler beim Zurücksetzen: ${result.error}`);
resetForm.style.display = 'block';
}
} catch (error) {
console.error('Submit Error:', error);
showMessage('error', `❌ Fehler: ${error.message}`);
resetForm.style.display = 'block';
} finally {
resetBtn.disabled = false;
loading.style.display = 'none';
}
});
// Session beim Laden der Seite setzen (nur wenn Token vorhanden)
if (accessToken && refreshToken) {
setSession();
}
// Add cookie settings button functionality
document.addEventListener('DOMContentLoaded', function() {
const cookieSettingsBtn = document.getElementById('cookie-settings-footer');
if (cookieSettingsBtn) {
cookieSettingsBtn.addEventListener('click', function() {
if (window.cookieConsent) {
window.cookieConsent.resetConsent();
}
});
}
});
// Passwort-Sicherheitshinweise
newPasswordInput.addEventListener('input', function() {
const password = this.value;
const hasLength = password.length >= 8;
const hasUpper = /[A-Z]/.test(password);
const hasLower = /[a-z]/.test(password);
const hasNumber = /\d/.test(password);
const hasSpecial = /[!@#$%^&*(),.?":{}|<>]/.test(password);
if (password.length > 0) {
let hints = [];
if (!hasLength) hints.push('Mindestens 8 Zeichen');
if (!hasUpper) hints.push('Großbuchstaben');
if (!hasLower) hints.push('Kleinbuchstaben');
if (!hasNumber) hints.push('Zahlen');
if (!hasSpecial) hints.push('Sonderzeichen');
if (hints.length > 0) {
showMessage('info', `💡 Tipp: Verwende auch ${hints.join(', ')} für ein sicheres Passwort.`);
} else {
showMessage('success', '✅ Starkes Passwort!');
}
}
});

121
public/login.html Normal file
View File

@@ -0,0 +1,121 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ninja Server - Admin Login</title>
<link rel="icon" type="image/x-icon" href="/pictures/favicon.ico">
<script src="https://unpkg.com/@supabase/supabase-js@2"></script>
<link rel="stylesheet" href="/css/login.css">
</head>
<body>
<!-- Back to Home Button -->
<a href="/" class="back-button">Zur Hauptseite</a>
<div class="main-content">
<div class="container">
<div class="logo">
<h1>🥷 NINJACROSS</h1>
<p>Dein Dashboard</p>
</div>
<div id="message"></div>
<div id="loading" class="loading">
<div class="spinner"></div>
<p>Processing...</p>
</div>
<!-- Login Form -->
<div id="loginForm" class="form-container active">
<h2 style="text-align: center; margin-bottom: 1.5rem; color: #e2e8f0; font-weight: 600;">Welcome Back</h2>
<!-- OAuth Buttons -->
<div class="oauth-container">
<button type="button" id="googleSignInBtn" class="btn btn-google">
<svg width="20" height="20" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
<path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
<path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
<path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
</svg>
Continue with Google
</button>
<button type="button" id="discordSignInBtn" class="btn btn-discord">
<svg width="20" height="20" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill="#5865F2" d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/>
</svg>
Continue with Discord
</button>
</div>
<div class="divider">
<span>or</span>
</div>
<form id="loginFormElement">
<div class="form-group">
<label for="loginEmail">Email</label>
<input type="email" id="loginEmail" required>
</div>
<div class="form-group">
<label for="loginPassword">Password</label>
<input type="password" id="loginPassword" required>
</div>
<button type="submit" class="btn btn-primary">Sign In</button>
</form>
<!-- Password Reset Container -->
<div id="passwordResetContainer" class="password-reset-container">
<p>Passwort vergessen? Kein Problem!</p>
<button type="button" id="resetPasswordBtn" class="btn btn-reset">Passwort zurücksetzen</button>
</div>
<div class="toggle-form">
<p>Don't have an account? <button type="button" onclick="toggleForm()">Sign Up</button></p>
</div>
</div>
<!-- Register Form -->
<div id="registerForm" class="form-container">
<h2 style="text-align: center; margin-bottom: 1.5rem; color: #e2e8f0; font-weight: 600;">Create Account</h2>
<form id="registerFormElement">
<div class="form-group">
<label for="registerEmail">Email</label>
<input type="email" id="registerEmail" required>
</div>
<div class="form-group">
<label for="registerPassword">Password</label>
<input type="password" id="registerPassword" required minlength="6">
</div>
<div class="form-group">
<label for="confirmPassword">Confirm Password</label>
<input type="password" id="confirmPassword" required>
</div>
<button type="submit" class="btn btn-primary">Create Account</button>
</form>
<div class="toggle-form">
<p>Already have an account? <button type="button" onclick="toggleForm()">Sign In</button></p>
</div>
</div>
</div>
</div>
<!-- Footer -->
<footer class="footer">
<div class="footer-content">
<div class="footer-links">
<a href="/impressum.html" class="footer-link">Impressum</a>
<a href="/datenschutz.html" class="footer-link">Datenschutz</a>
<button id="cookie-settings-footer" class="footer-link cookie-settings-btn">Cookie-Einstellungen</button>
</div>
<div class="footer-text">
<p>&copy; 2024 NinjaCross. Alle Rechte vorbehalten.</p>
</div>
</div>
</footer>
<!-- Application JavaScript -->
<script src="/js/cookie-consent.js"></script>
<script src="/js/login.js"></script>
</body>
</html>

55
public/manifest.json Normal file
View File

@@ -0,0 +1,55 @@
{
"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/icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/pictures/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"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": "/dashboard.html",
"icons": [
{
"src": "/pictures/icon-192.png",
"sizes": "192x192"
}
]
}
],
"screenshots": [
{
"src": "/pictures/screenshot-mobile.png",
"sizes": "390x844",
"type": "image/png",
"form_factor": "narrow"
}
],
"related_applications": [],
"edge_side_panel": {
"preferred_width": 400
}
}

BIN
public/pictures/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@@ -0,0 +1,81 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Passwort zurücksetzen - NinjaCross</title>
<link rel="icon" type="image/x-icon" href="/pictures/favicon.ico">
<!-- Supabase -->
<script src="https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2"></script>
<link rel="stylesheet" href="/css/reset-password.css">
</head>
<body>
<div class="container">
<div class="logo">🥷 NINJACROSS</div>
<div class="tagline">Die ultimative Timer-Rangliste</div>
<h1 class="title">Passwort zurücksetzen 🔐</h1>
<p class="subtitle">Gib dein neues Passwort ein, um dein Konto zu sichern</p>
<div id="messageContainer"></div>
<form id="resetForm">
<div class="form-group">
<label for="newPassword" class="form-label">Neues Passwort</label>
<input
type="password"
id="newPassword"
name="newPassword"
class="form-input"
placeholder="Mindestens 8 Zeichen"
required
minlength="8"
>
</div>
<div class="form-group">
<label for="confirmPassword" class="form-label">Passwort bestätigen</label>
<input
type="password"
id="confirmPassword"
name="confirmPassword"
class="form-input"
placeholder="Passwort wiederholen"
required
minlength="8"
>
</div>
<button type="submit" class="btn btn-primary" id="resetBtn">
🔄 Passwort zurücksetzen
</button>
</form>
<div class="loading" id="loading">
<div class="spinner"></div>
Passwort wird zurückgesetzt...
</div>
<a href="/" class="back-link">← Zurück zur Hauptseite</a>
</div>
<!-- Footer -->
<footer class="footer">
<div class="footer-content">
<div class="footer-links">
<a href="/impressum.html" class="footer-link">Impressum</a>
<a href="/datenschutz.html" class="footer-link">Datenschutz</a>
<button id="cookie-settings-footer" class="footer-link cookie-settings-btn">Cookie-Einstellungen</button>
</div>
<div class="footer-text">
<p>&copy; 2024 NinjaCross. Alle Rechte vorbehalten.</p>
</div>
</div>
</footer>
<script src="/js/cookie-consent.js"></script>
<script src="/js/reset-password.js"></script>
</body>
</html>

137
public/sw.js Normal file
View File

@@ -0,0 +1,137 @@
// Service Worker für iPhone Notifications
const CACHE_NAME = 'ninjacross-v1';
const urlsToCache = [
'/',
'/test-push.html',
'/sw.js'
];
// Install event
self.addEventListener('install', function(event) {
console.log('Service Worker installing...');
event.waitUntil(
caches.open(CACHE_NAME)
.then(function(cache) {
// Add files one by one to handle failures gracefully
return Promise.allSettled(
urlsToCache.map(url =>
cache.add(url).catch(err => {
console.warn(`Failed to cache ${url}:`, err);
return null; // Continue with other files
})
)
);
})
.then(() => {
console.log('Service Worker installation completed');
// Skip waiting to activate immediately
return self.skipWaiting();
})
.catch(err => {
console.error('Service Worker installation failed:', err);
// Still try to skip waiting
return self.skipWaiting();
})
);
});
// Activate event
self.addEventListener('activate', function(event) {
console.log('Service Worker activating...');
event.waitUntil(
caches.keys().then(function(cacheNames) {
return Promise.all(
cacheNames.map(function(cacheName) {
if (cacheName !== CACHE_NAME) {
return caches.delete(cacheName);
}
})
);
}).then(() => {
// Take control of all clients immediately
return self.clients.claim();
})
);
});
// Listen for skip waiting message
self.addEventListener('message', function(event) {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
});
// 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/icon-192.png',
badge: '/pictures/icon-192.png',
vibrate: [100, 50, 100],
data: {
dateOfArrival: Date.now(),
primaryKey: 1
},
actions: [
{
action: 'explore',
title: 'Dashboard öffnen',
icon: '/pictures/icon-192.png'
},
{
action: 'close',
title: 'Schließen',
icon: '/pictures/icon-192.png'
}
]
};
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');
}

560
public/test-push.html Normal file
View File

@@ -0,0 +1,560 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Push Notification Test - Ninja Cross</title>
<link rel="icon" type="image/x-icon" href="/pictures/favicon.ico">
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
min-height: 100vh;
}
.container {
background: rgba(255, 255, 255, 0.1);
padding: 30px;
border-radius: 15px;
backdrop-filter: blur(10px);
}
h1 {
text-align: center;
margin-bottom: 30px;
}
.status {
background: rgba(0, 0, 0, 0.2);
padding: 15px;
border-radius: 10px;
margin: 20px 0;
}
.status.success { background: rgba(34, 197, 94, 0.3); }
.status.error { background: rgba(239, 68, 68, 0.3); }
.status.warning { background: rgba(245, 158, 11, 0.3); }
button {
background: linear-gradient(135deg, #00d4ff, #0891b2);
color: white;
border: none;
padding: 12px 24px;
border-radius: 8px;
cursor: pointer;
font-size: 16px;
margin: 10px 5px;
transition: transform 0.2s;
}
button:hover {
transform: translateY(-2px);
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
input, textarea {
width: 100%;
padding: 10px;
border: none;
border-radius: 5px;
margin: 10px 0;
font-size: 16px;
}
.log {
background: rgba(0, 0, 0, 0.3);
padding: 15px;
border-radius: 10px;
margin-top: 20px;
max-height: 300px;
overflow-y: auto;
font-family: monospace;
font-size: 14px;
}
</style>
</head>
<body>
<div class="container">
<h1>🧪 Push Notification Test</h1>
<div id="status" class="status">
<strong>Status:</strong> <span id="statusText">Lädt...</span>
</div>
<div>
<h3>1. Service Worker Status</h3>
<button onclick="checkServiceWorker()">Service Worker prüfen</button>
<button onclick="registerServiceWorker()">Service Worker registrieren</button>
</div>
<div>
<h3>2. Notification Permission</h3>
<button onclick="requestPermission()">Berechtigung anfordern</button>
<button onclick="checkPermission()">Berechtigung prüfen</button>
</div>
<div>
<h3>3. Push Subscription</h3>
<button onclick="subscribeToPush()">Push abonnieren</button>
<button onclick="unsubscribeFromPush()">Push abbestellen</button>
<button onclick="checkSubscription()">Subscription prüfen</button>
</div>
<div>
<h3>4. Test Notifications</h3>
<input type="text" id="testMessage" placeholder="Test-Nachricht" value="Das ist eine Test-Push-Notification!">
<button onclick="sendTestPush()">Test-Push senden</button>
<button onclick="sendTestWebNotification()">Web-Notification senden</button>
<button onclick="sendWindowsNotification()">Windows-Notification senden</button>
</div>
<div>
<h3>5. Push Status</h3>
<button onclick="getPushStatus()">Push-Status abrufen</button>
</div>
<div class="log" id="log">
<div>Log wird hier angezeigt...</div>
</div>
</div>
<script>
let currentSubscription = null;
// Convert VAPID key from base64url to Uint8Array
function urlBase64ToUint8Array(base64String) {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding)
.replace(/\-/g, '+')
.replace(/_/g, '/');
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
function log(message, type = 'info') {
const logDiv = document.getElementById('log');
const timestamp = new Date().toLocaleTimeString();
const logEntry = document.createElement('div');
logEntry.style.color = type === 'error' ? '#ff6b6b' : type === 'success' ? '#51cf66' : '#ffffff';
logEntry.textContent = `[${timestamp}] ${message}`;
logDiv.appendChild(logEntry);
logDiv.scrollTop = logDiv.scrollHeight;
}
function updateStatus(message, type = 'info') {
const statusDiv = document.getElementById('status');
const statusText = document.getElementById('statusText');
statusText.textContent = message;
statusDiv.className = `status ${type}`;
}
// Service Worker Functions
async function checkServiceWorker() {
if ('serviceWorker' in navigator) {
const registrations = await navigator.serviceWorker.getRegistrations();
log(`Service Worker registriert: ${registrations.length > 0 ? 'Ja' : 'Nein'}`);
if (registrations.length > 0) {
const reg = registrations[0];
log(`SW Scope: ${reg.scope}`);
log(`SW State: ${reg.active ? reg.active.state : 'kein aktiver Worker'}`);
}
} else {
log('Service Worker nicht unterstützt', 'error');
}
}
async function registerServiceWorker() {
if ('serviceWorker' in navigator) {
try {
log('Service Worker wird registriert...', 'info');
const registration = await navigator.serviceWorker.register('/sw.js');
log('Service Worker erfolgreich registriert', 'success');
log(`SW Scope: ${registration.scope}`);
updateStatus('Service Worker registriert', 'success');
} catch (error) {
log(`Service Worker Registrierung fehlgeschlagen: ${error.message}`, 'error');
log(`Error Details: ${JSON.stringify(error)}`, 'error');
updateStatus(`Service Worker Registrierung fehlgeschlagen: ${error.message}`, 'error');
}
} else {
log('Service Worker nicht unterstützt', 'error');
updateStatus('Service Worker nicht unterstützt', 'error');
}
}
// Notification Permission Functions
async function requestPermission() {
if ('Notification' in window) {
const permission = await Notification.requestPermission();
log(`Notification Permission: ${permission}`, permission === 'granted' ? 'success' : 'warning');
updateStatus(`Notification Permission: ${permission}`, permission === 'granted' ? 'success' : 'warning');
} else {
log('Notifications nicht unterstützt', 'error');
}
}
function checkPermission() {
if ('Notification' in window) {
log(`Notification Permission: ${Notification.permission}`);
updateStatus(`Notification Permission: ${Notification.permission}`,
Notification.permission === 'granted' ? 'success' : 'warning');
} else {
log('Notifications nicht unterstützt', 'error');
}
}
// Push Subscription Functions
async function subscribeToPush() {
log('Push Subscription gestartet...', 'info');
// Check basic requirements
if (!('serviceWorker' in navigator)) {
log('Service Worker nicht unterstützt', 'error');
updateStatus('Service Worker nicht unterstützt', 'error');
return;
}
if (!('PushManager' in window)) {
log('Push Manager nicht unterstützt', 'error');
updateStatus('Push Manager nicht unterstützt', 'error');
return;
}
// Check notification permission first
if (Notification.permission !== 'granted') {
log('Notification Permission nicht erteilt. Bitte zuerst "Berechtigung anfordern" klicken!', 'error');
updateStatus('Notification Permission erforderlich', 'error');
return;
}
try {
log('Service Worker wird geladen...', 'info');
// First check if service worker is already registered
let registration;
const existingRegistrations = await navigator.serviceWorker.getRegistrations();
if (existingRegistrations.length > 0) {
log('Service Worker bereits registriert, verwende bestehende...', 'info');
registration = existingRegistrations[0];
} else {
log('Service Worker nicht registriert, registriere jetzt...', 'info');
registration = await navigator.serviceWorker.register('/sw.js');
log('Service Worker registriert', 'success');
}
// Wait for service worker to be ready with timeout
log('Warte auf Service Worker ready...', 'info');
// Check if service worker is active
if (registration.active) {
log('Service Worker ist bereits aktiv', 'success');
} else if (registration.installing && registration.installing.state) {
log('Service Worker wird installiert, warte...', 'info');
await new Promise((resolve) => {
const installingWorker = registration.installing;
if (installingWorker) {
installingWorker.addEventListener('statechange', () => {
if (installingWorker.state === 'installed') {
resolve();
}
});
} else {
resolve();
}
});
log('Service Worker Installation abgeschlossen', 'success');
} else if (registration.waiting && registration.waiting.state) {
log('Service Worker wartet, aktiviere...', 'info');
registration.waiting.postMessage({ type: 'SKIP_WAITING' });
await new Promise((resolve) => {
const waitingWorker = registration.waiting;
if (waitingWorker) {
waitingWorker.addEventListener('statechange', () => {
if (waitingWorker.state === 'activated') {
resolve();
}
});
} else {
resolve();
}
});
log('Service Worker aktiviert', 'success');
} else {
log('Service Worker Status unbekannt, warte auf ready...', 'info');
try {
await navigator.serviceWorker.ready;
log('Service Worker bereit', 'success');
} catch (error) {
log(`Service Worker ready fehlgeschlagen: ${error.message}`, 'error');
throw error;
}
}
// Convert VAPID key from base64url to ArrayBuffer
const vapidPublicKey = 'BJmNVx0C3XeVxeKGTP9c-Z4HcuZNmdk6QdiLocZgCmb-miCS0ESFO3W2TvJlRhhNAShV63pWA5p36BTVSetyTds';
log('VAPID Key wird konvertiert...', 'info');
let applicationServerKey;
try {
applicationServerKey = urlBase64ToUint8Array(vapidPublicKey);
log('VAPID Key konvertiert', 'success');
} catch (error) {
log(`VAPID Key Konvertierung fehlgeschlagen: ${error.message}`, 'error');
throw error;
}
log('Push Subscription wird erstellt...', 'info');
// Check if push manager is available
if (!registration.pushManager) {
throw new Error('Push Manager nicht verfügbar in diesem Service Worker');
}
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: applicationServerKey
});
currentSubscription = subscription;
log('Push Subscription erfolgreich erstellt', 'success');
log(`Endpoint: ${subscription.endpoint.substring(0, 50)}...`);
// Send to server
log('Subscription wird an Server gesendet...', 'info');
const response = await fetch('/api/v1/public/subscribe', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(subscription)
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const result = await response.json();
if (result.success) {
log('Subscription erfolgreich an Server gesendet', 'success');
log(`Player ID: ${result.playerId || 'anonymous'}`, 'success');
// Store the player ID for later use
if (result.playerId) {
localStorage.setItem('pushPlayerId', result.playerId);
}
updateStatus('Push Subscription erfolgreich!', 'success');
// Store the subscription endpoint for later use
localStorage.setItem('pushSubscriptionEndpoint', subscription.endpoint);
} else {
log(`Server-Fehler: ${result.message}`, 'error');
updateStatus(`Server-Fehler: ${result.message}`, 'error');
}
} catch (error) {
log(`Push Subscription fehlgeschlagen: ${error.message}`, 'error');
log(`Error Details: ${JSON.stringify(error)}`, 'error');
updateStatus(`Push Subscription fehlgeschlagen: ${error.message}`, 'error');
}
}
async function unsubscribeFromPush() {
if (currentSubscription) {
try {
await currentSubscription.unsubscribe();
currentSubscription = null;
log('Push Subscription erfolgreich abbestellt', 'success');
} catch (error) {
log(`Push Unsubscribe fehlgeschlagen: ${error.message}`, 'error');
}
} else {
log('Keine aktive Subscription gefunden', 'warning');
}
}
async function checkSubscription() {
log('Überprüfe Push Subscription...', 'info');
if (!('serviceWorker' in navigator)) {
log('Service Worker nicht unterstützt', 'error');
return;
}
try {
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.getSubscription();
if (subscription) {
currentSubscription = subscription;
log('Aktive Push Subscription gefunden', 'success');
log(`Endpoint: ${subscription.endpoint.substring(0, 50)}...`);
updateStatus('Push Subscription aktiv', 'success');
} else {
log('Keine Push Subscription gefunden', 'warning');
updateStatus('Keine Push Subscription gefunden', 'warning');
}
} catch (error) {
log(`Subscription Check fehlgeschlagen: ${error.message}`, 'error');
updateStatus(`Subscription Check fehlgeschlagen: ${error.message}`, 'error');
}
}
// Test Functions
async function sendTestPush() {
const message = document.getElementById('testMessage').value;
log('Test-Push wird gesendet...', 'info');
// First check if we have a subscription
if (!currentSubscription) {
log('Keine Push Subscription gefunden. Bitte zuerst "Push abonnieren" klicken!', 'error');
updateStatus('Keine Push Subscription gefunden', 'error');
return;
}
// Use the stored player ID from subscription
const storedPlayerId = localStorage.getItem('pushPlayerId');
let userId = 'test-user';
if (storedPlayerId) {
userId = storedPlayerId;
}
log(`Sende Test-Push an Player ID: ${userId}`, 'info');
try {
const response = await fetch('/api/v1/public/test-push', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
userId: userId,
message: message
})
});
const result = await response.json();
if (result.success) {
log('Test-Push erfolgreich gesendet', 'success');
log(`An User ID: ${userId}`, 'success');
updateStatus('Test-Push erfolgreich gesendet!', 'success');
} else {
log(`Test-Push fehlgeschlagen: ${result.message}`, 'error');
updateStatus(`Test-Push fehlgeschlagen: ${result.message}`, 'error');
}
} catch (error) {
log(`Test-Push Fehler: ${error.message}`, 'error');
updateStatus(`Test-Push Fehler: ${error.message}`, 'error');
}
}
function sendTestWebNotification() {
if ('Notification' in window && Notification.permission === 'granted') {
const message = document.getElementById('testMessage').value;
const notification = new Notification('🧪 Test Web Notification', {
body: message,
icon: '/pictures/icon-192.png',
badge: '/pictures/icon-192.png',
tag: 'test-notification',
requireInteraction: true,
silent: false
});
notification.onclick = function() {
window.focus();
notification.close();
};
// Auto-close after 10 seconds
setTimeout(() => {
notification.close();
}, 10000);
log('Web-Notification gesendet', 'success');
} else {
log('Web-Notifications nicht verfügbar oder nicht erlaubt', 'error');
}
}
// Windows Desktop Notification (falls verfügbar)
function sendWindowsNotification() {
if ('Notification' in window && Notification.permission === 'granted') {
const message = document.getElementById('testMessage').value;
// Erstelle eine Windows-ähnliche Notification
const notification = new Notification('🏆 Ninja Cross - Achievement!', {
body: message,
icon: '/pictures/icon-192.png',
badge: '/pictures/icon-192.png',
tag: 'ninja-cross-achievement',
requireInteraction: true,
silent: false,
data: {
type: 'achievement',
timestamp: Date.now()
}
});
notification.onclick = function() {
window.focus();
notification.close();
};
// Auto-close after 15 seconds
setTimeout(() => {
notification.close();
}, 15000);
log('Windows-ähnliche Notification gesendet', 'success');
} else {
log('Web-Notifications nicht verfügbar oder nicht erlaubt', 'error');
}
}
async function getPushStatus() {
try {
const response = await fetch('/api/v1/public/push-status');
const result = await response.json();
if (result.success) {
log(`Push Status: ${JSON.stringify(result.data, null, 2)}`, 'success');
} else {
log(`Push Status Fehler: ${result.message}`, 'error');
}
} catch (error) {
log(`Push Status Fehler: ${error.message}`, 'error');
}
}
// Initialize
window.addEventListener('load', function() {
console.log('Push Notification Test Seite geladen');
log('Push Notification Test Seite geladen');
// Check if we're on HTTPS
if (location.protocol !== 'https:' && location.hostname !== 'localhost') {
log('WARNUNG: Push Notifications funktionieren nur über HTTPS!', 'error');
updateStatus('HTTPS erforderlich für Push Notifications', 'error');
} else {
log('HTTPS-Verbindung erkannt - Push Notifications möglich', 'success');
}
checkServiceWorker();
checkPermission();
checkSubscription();
});
// Also initialize on DOMContentLoaded as backup
document.addEventListener('DOMContentLoaded', function() {
console.log('DOM Content Loaded');
log('DOM Content Loaded - Initialisierung gestartet');
});
</script>
</body>
</html>