Update
This commit is contained in:
336
public/404.html
Normal file
336
public/404.html
Normal 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
254
public/admin-dashboard.html
Normal 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">×</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">×</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')">×</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>© 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
55
public/adminlogin.html
Normal 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>© 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
244
public/agb.html
Normal 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>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
753
public/css/admin-dashboard.css
Normal file
753
public/css/admin-dashboard.css
Normal 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
294
public/css/adminlogin.css
Normal 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
1981
public/css/dashboard.css
Normal file
File diff suppressed because it is too large
Load Diff
500
public/css/generator.css
Normal file
500
public/css/generator.css
Normal 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
1289
public/css/index.css
Normal file
File diff suppressed because it is too large
Load Diff
486
public/css/login.css
Normal file
486
public/css/login.css
Normal 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;
|
||||
}
|
||||
}
|
||||
305
public/css/reset-password.css
Normal file
305
public/css/reset-password.css
Normal 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
902
public/dashboard.html
Normal 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')">×</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')">×</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="© 2024 NinjaCross. Alle Rechte vorbehalten." data-en="© 2024 NinjaCross. All rights reserved.">© 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
362
public/datenschutz.html
Normal 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>© 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>
|
||||
181
public/email-templates/EMAIL-COMPATIBILITY-GUIDE.md
Normal file
181
public/email-templates/EMAIL-COMPATIBILITY-GUIDE.md
Normal 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! 📧✨
|
||||
157
public/email-templates/README.md
Normal file
157
public/email-templates/README.md
Normal 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** 🥷
|
||||
119
public/email-templates/TEMPLATES-OVERVIEW.md
Normal file
119
public/email-templates/TEMPLATES-OVERVIEW.md
Normal 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!** ✨
|
||||
174
public/email-templates/URL-CONFIGURATION-GUIDE.md
Normal file
174
public/email-templates/URL-CONFIGURATION-GUIDE.md
Normal 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! 🚀
|
||||
274
public/email-templates/change-email.html
Normal file
274
public/email-templates/change-email.html
Normal 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>
|
||||
29
public/email-templates/change-email.txt
Normal file
29
public/email-templates/change-email.txt
Normal 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.
|
||||
300
public/email-templates/invite-user.html
Normal file
300
public/email-templates/invite-user.html
Normal 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>
|
||||
34
public/email-templates/invite-user.txt
Normal file
34
public/email-templates/invite-user.txt
Normal 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.
|
||||
257
public/email-templates/magic-link.html
Normal file
257
public/email-templates/magic-link.html
Normal 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>
|
||||
25
public/email-templates/magic-link.txt
Normal file
25
public/email-templates/magic-link.txt
Normal 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.
|
||||
327
public/email-templates/reauthentication.html
Normal file
327
public/email-templates/reauthentication.html
Normal 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>
|
||||
30
public/email-templates/reauthentication.txt
Normal file
30
public/email-templates/reauthentication.txt
Normal 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.
|
||||
249
public/email-templates/reset-password-compatible.html
Normal file
249
public/email-templates/reset-password-compatible.html
Normal 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>
|
||||
301
public/email-templates/reset-password-optimized.html
Normal file
301
public/email-templates/reset-password-optimized.html
Normal 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>
|
||||
323
public/email-templates/reset-password-table.html
Normal file
323
public/email-templates/reset-password-table.html
Normal 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>
|
||||
294
public/email-templates/reset-password.html
Normal file
294
public/email-templates/reset-password.html
Normal 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>
|
||||
32
public/email-templates/reset-password.txt
Normal file
32
public/email-templates/reset-password.txt
Normal 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.
|
||||
208
public/email-templates/welcome-email-compatible.html
Normal file
208
public/email-templates/welcome-email-compatible.html
Normal 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>
|
||||
187
public/email-templates/welcome-email-simple.html
Normal file
187
public/email-templates/welcome-email-simple.html
Normal 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>
|
||||
274
public/email-templates/welcome-email.html
Normal file
274
public/email-templates/welcome-email.html
Normal 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>
|
||||
32
public/email-templates/welcome-email.txt
Normal file
32
public/email-templates/welcome-email.txt
Normal 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
85
public/generator.html
Normal 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>© 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
241
public/impressum.html
Normal 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>© 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
262
public/index.html
Normal 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="© 2024 NinjaCross. Alle Rechte vorbehalten." data-en="© 2024 NinjaCross. All rights reserved.">© 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
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
91
public/js/adminlogin.js
Normal 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
601
public/js/cookie-consent.js
Normal 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">×</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
105
public/js/cookie-utils.js
Normal 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
2163
public/js/dashboard.js
Normal file
File diff suppressed because it is too large
Load Diff
573
public/js/generator.js
Normal file
573
public/js/generator.js
Normal 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
808
public/js/index.js
Normal 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
291
public/js/login.js
Normal 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);
|
||||
33
public/js/page-tracking.js
Normal file
33
public/js/page-tracking.js
Normal 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
202
public/js/reset-password.js
Normal 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
121
public/login.html
Normal 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>© 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
55
public/manifest.json
Normal 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
BIN
public/pictures/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
public/pictures/icon-192.png
Normal file
BIN
public/pictures/icon-192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.4 KiB |
81
public/reset-password.html
Normal file
81
public/reset-password.html
Normal 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>© 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
137
public/sw.js
Normal 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
560
public/test-push.html
Normal 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>
|
||||
Reference in New Issue
Block a user