Refactor ALL
This commit is contained in:
@@ -4,199 +4,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Login - Lizenzgenerator</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.login-container {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
padding: 40px;
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.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: #333;
|
||||
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;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="/css/adminlogin.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-container">
|
||||
@@ -226,90 +34,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
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/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();
|
||||
});
|
||||
</script>
|
||||
<script src="/js/adminlogin.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
</html>
|
||||
191
public/css/adminlogin.css
Normal file
191
public/css/adminlogin.css
Normal file
@@ -0,0 +1,191 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.login-container {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
padding: 40px;
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.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: #333;
|
||||
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;
|
||||
}
|
||||
}
|
||||
684
public/css/dashboard.css
Normal file
684
public/css/dashboard.css
Normal file
@@ -0,0 +1,684 @@
|
||||
@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%);
|
||||
}
|
||||
|
||||
.main-container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.header-section {
|
||||
text-align: center;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.main-title {
|
||||
font-size: 4rem;
|
||||
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 {
|
||||
font-size: 1.2rem;
|
||||
color: #8892b0;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.nav-buttons {
|
||||
position: fixed;
|
||||
top: 2rem;
|
||||
right: 2rem;
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 0.75rem 1.5rem;
|
||||
border: none;
|
||||
border-radius: 0.75rem;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #00d4ff, #0891b2);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 25px rgba(0, 212, 255, 0.3);
|
||||
}
|
||||
|
||||
.btn-logout {
|
||||
background: linear-gradient(135deg, #dc3545, #c82333);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-logout:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 25px rgba(220, 53, 69, 0.3);
|
||||
}
|
||||
|
||||
.dashboard-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.welcome-card {
|
||||
background: rgba(15, 23, 42, 0.8);
|
||||
border: 1px solid #1e293b;
|
||||
border-radius: 1rem;
|
||||
padding: 2rem;
|
||||
backdrop-filter: blur(20px);
|
||||
margin-bottom: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: rgba(15, 23, 42, 0.8);
|
||||
border: 1px solid #1e293b;
|
||||
border-radius: 1rem;
|
||||
padding: 2rem;
|
||||
backdrop-filter: blur(20px);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-2px);
|
||||
border-color: #00d4ff;
|
||||
box-shadow: 0 10px 25px rgba(0, 212, 255, 0.1);
|
||||
}
|
||||
|
||||
.card h3 {
|
||||
color: #ffffff;
|
||||
font-size: 1.3rem;
|
||||
margin-bottom: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.card p {
|
||||
color: #8892b0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: #8892b0;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
border: 3px solid #1e293b;
|
||||
border-top: 3px solid #00d4ff;
|
||||
border-radius: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 1rem;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: #8892b0;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #00d4ff, #0891b2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
/* Modal Styles */
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
z-index: 2000;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: rgba(15, 23, 42, 0.95);
|
||||
margin: 5% auto;
|
||||
padding: 2rem;
|
||||
border: 1px solid #1e293b;
|
||||
border-radius: 1rem;
|
||||
width: 90%;
|
||||
max-width: 500px;
|
||||
backdrop-filter: blur(20px);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
color: #ffffff;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.close {
|
||||
color: #8892b0;
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.close:hover {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
color: #e2e8f0;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
background: #1e293b;
|
||||
border: 2px solid #334155;
|
||||
border-radius: 0.5rem;
|
||||
color: #ffffff;
|
||||
font-size: 1rem;
|
||||
transition: border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
outline: none;
|
||||
border-color: #00d4ff;
|
||||
}
|
||||
|
||||
.form-input::placeholder {
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.message {
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.message.success {
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
color: #22c55e;
|
||||
border: 1px solid rgba(34, 197, 94, 0.3);
|
||||
}
|
||||
|
||||
.message.error {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
color: #ef4444;
|
||||
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
.message.info {
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
color: #3b82f6;
|
||||
border: 1px solid rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
.player-selection {
|
||||
display: grid;
|
||||
gap: 0.5rem;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
background: #0f172a;
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.player-option {
|
||||
padding: 0.75rem;
|
||||
background: #1e293b;
|
||||
border: 2px solid #334155;
|
||||
border-radius: 0.5rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.player-option:hover {
|
||||
border-color: #00d4ff;
|
||||
background: #334155;
|
||||
}
|
||||
|
||||
.player-option.selected {
|
||||
border-color: #00d4ff;
|
||||
background: rgba(0, 212, 255, 0.1);
|
||||
}
|
||||
|
||||
.player-info {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.player-rfid {
|
||||
color: #8892b0;
|
||||
font-size: 0.9rem;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.times-grid {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.time-card {
|
||||
background: #1e293b;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.time-location {
|
||||
color: #00d4ff;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.time-value {
|
||||
color: #ffffff;
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.time-date {
|
||||
color: #8892b0;
|
||||
font-size: 0.9rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
/* Times Section Styles */
|
||||
.times-section {
|
||||
background: rgba(15, 23, 42, 0.8);
|
||||
border: 1px solid #1e293b;
|
||||
border-radius: 1rem;
|
||||
padding: 2rem;
|
||||
backdrop-filter: blur(20px);
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.times-header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.times-header h2 {
|
||||
color: #ffffff;
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.times-header p {
|
||||
color: #8892b0;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.times-loading {
|
||||
text-align: center;
|
||||
padding: 3rem;
|
||||
color: #8892b0;
|
||||
}
|
||||
|
||||
.times-not-linked {
|
||||
text-align: center;
|
||||
padding: 3rem 2rem;
|
||||
}
|
||||
|
||||
.not-linked-content {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.not-linked-icon {
|
||||
font-size: 4rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.times-not-linked h3 {
|
||||
color: #ffffff;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.times-not-linked p {
|
||||
color: #8892b0;
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 2rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.link-info {
|
||||
background: rgba(0, 212, 255, 0.1);
|
||||
border: 1px solid rgba(0, 212, 255, 0.3);
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.5rem;
|
||||
margin-top: 2rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.link-info h4 {
|
||||
color: #00d4ff;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.link-info ol {
|
||||
color: #cbd5e1;
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
.link-info li {
|
||||
margin-bottom: 0.5rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.times-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: rgba(0, 212, 255, 0.1);
|
||||
border: 1px solid rgba(0, 212, 255, 0.3);
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.5rem;
|
||||
text-align: center;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-2px);
|
||||
border-color: #00d4ff;
|
||||
box-shadow: 0 10px 25px rgba(0, 212, 255, 0.1);
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
color: #00d4ff;
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
font-family: monospace;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: #8892b0;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.times-content {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.times-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.user-time-card {
|
||||
background: #1e293b;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.5rem;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.user-time-card:hover {
|
||||
transform: translateY(-2px);
|
||||
border-color: #00d4ff;
|
||||
box-shadow: 0 10px 25px rgba(0, 212, 255, 0.1);
|
||||
}
|
||||
|
||||
.user-time-card.expanded {
|
||||
transform: none;
|
||||
border-color: #00d4ff;
|
||||
box-shadow: 0 10px 25px rgba(0, 212, 255, 0.2);
|
||||
}
|
||||
|
||||
.user-time-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
background: linear-gradient(90deg, #00d4ff, #0891b2);
|
||||
}
|
||||
|
||||
.time-location-name {
|
||||
color: #00d4ff;
|
||||
font-weight: 600;
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.time-value-large {
|
||||
color: #ffffff;
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
font-family: monospace;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.time-date-info {
|
||||
color: #8892b0;
|
||||
font-size: 0.9rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.time-rank {
|
||||
background: rgba(255, 107, 53, 0.1);
|
||||
color: #ff6b35;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.expand-indicator {
|
||||
color: #8892b0;
|
||||
font-size: 1.2rem;
|
||||
transition: transform 0.3s ease;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.user-time-card.expanded .expand-indicator {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.card-main-content {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.expanded-content {
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.3s ease, opacity 0.3s ease;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.user-time-card.expanded .expanded-content {
|
||||
max-height: 1000px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.all-runs-title {
|
||||
color: #00d4ff;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid #334155;
|
||||
}
|
||||
|
||||
.run-item {
|
||||
background: rgba(0, 212, 255, 0.05);
|
||||
border: 1px solid rgba(0, 212, 255, 0.1);
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
margin-bottom: 0.75rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.run-item:hover {
|
||||
background: rgba(0, 212, 255, 0.1);
|
||||
border-color: rgba(0, 212, 255, 0.3);
|
||||
}
|
||||
|
||||
.run-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.run-time {
|
||||
color: #ffffff;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.run-details {
|
||||
text-align: right;
|
||||
color: #8892b0;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.run-rank-badge {
|
||||
background: #00d4ff;
|
||||
color: #0a0a0f;
|
||||
padding: 0.2rem 0.5rem;
|
||||
border-radius: 0.3rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.run-rank-badge.best {
|
||||
background: #22c55e;
|
||||
}
|
||||
|
||||
.run-rank-badge.second {
|
||||
background: #f59e0b;
|
||||
}
|
||||
|
||||
.run-rank-badge.third {
|
||||
background: #ef4444;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.header {
|
||||
padding: 1rem;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.nav-buttons {
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.dashboard-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
margin: 2% auto;
|
||||
width: 95%;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
}
|
||||
386
public/css/generator.css
Normal file
386
public/css/generator.css
Normal file
@@ -0,0 +1,386 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
padding: 40px;
|
||||
max-width: 700px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.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: #333;
|
||||
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;
|
||||
}
|
||||
235
public/css/reset-password.css
Normal file
235
public/css/reset-password.css
Normal file
@@ -0,0 +1,235 @@
|
||||
/* 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;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
81
public/js/adminlogin.js
Normal file
81
public/js/adminlogin.js
Normal file
@@ -0,0 +1,81 @@
|
||||
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/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();
|
||||
});
|
||||
577
public/js/dashboard.js
Normal file
577
public/js/dashboard.js
Normal file
@@ -0,0 +1,577 @@
|
||||
// Supabase configuration
|
||||
const SUPABASE_URL = 'https://lfxlplnypzvjrhftaoog.supabase.co';
|
||||
const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImxmeGxwbG55cHp2anJoZnRhb29nIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDkyMTQ3NzIsImV4cCI6MjA2NDc5MDc3Mn0.XR4preBqWAQ1rT4PFbpkmRdz57BTwIusBI89fIxDHM8';
|
||||
|
||||
// Initialize Supabase client
|
||||
const supabase = window.supabase.createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
|
||||
|
||||
// Global variables
|
||||
let currentUser = null;
|
||||
|
||||
// Check authentication and load dashboard
|
||||
async function initDashboard() {
|
||||
try {
|
||||
// Get current session
|
||||
const { data: { session }, error } = await supabase.auth.getSession();
|
||||
|
||||
if (error) {
|
||||
console.error('Error checking authentication:', error);
|
||||
// Temporarily show dashboard for testing
|
||||
currentUser = { id: 'test-user', email: 'admin@speedrun-arena.com' };
|
||||
displayUserInfo({ email: 'admin@speedrun-arena.com' });
|
||||
showDashboard();
|
||||
// Check times section
|
||||
checkLinkStatusAndLoadTimes();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
// No session, redirect to login
|
||||
window.location.href = '/login';
|
||||
return;
|
||||
}
|
||||
|
||||
// User is authenticated, show dashboard
|
||||
if (session.user) {
|
||||
console.log('User data:', session.user);
|
||||
currentUser = session.user;
|
||||
displayUserInfo(session.user);
|
||||
} else {
|
||||
// Fallback if no user data
|
||||
currentUser = { id: 'test-user', email: 'admin@speedrun-arena.com' };
|
||||
displayUserInfo({ email: 'admin@speedrun-arena.com' });
|
||||
}
|
||||
showDashboard();
|
||||
|
||||
// Load times section
|
||||
checkLinkStatusAndLoadTimes();
|
||||
|
||||
} catch (error) {
|
||||
console.error('An unexpected error occurred:', error);
|
||||
// window.location.href = '/login';
|
||||
}
|
||||
}
|
||||
|
||||
// Display user information
|
||||
function displayUserInfo(user) {
|
||||
const userEmail = document.getElementById('userEmail');
|
||||
const userAvatar = document.getElementById('userAvatar');
|
||||
|
||||
userEmail.textContent = user.email;
|
||||
userAvatar.textContent = user.email.charAt(0).toUpperCase();
|
||||
}
|
||||
|
||||
// Show dashboard content
|
||||
function showDashboard() {
|
||||
document.getElementById('loading').style.display = 'none';
|
||||
document.getElementById('dashboardContent').style.display = 'block';
|
||||
}
|
||||
|
||||
// Logout function
|
||||
async function logout() {
|
||||
try {
|
||||
const { error } = await supabase.auth.signOut();
|
||||
if (error) {
|
||||
console.error('Error logging out:', error);
|
||||
} else {
|
||||
window.location.href = '/';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error during logout:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Listen for auth state changes
|
||||
supabase.auth.onAuthStateChange((event, session) => {
|
||||
if (event === 'SIGNED_OUT' || !session) {
|
||||
window.location.href = '/login';
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize dashboard when page loads
|
||||
initDashboard();
|
||||
|
||||
// Modal functions
|
||||
function openModal(modalId) {
|
||||
document.getElementById(modalId).style.display = 'block';
|
||||
}
|
||||
|
||||
function closeModal(modalId) {
|
||||
document.getElementById(modalId).style.display = 'none';
|
||||
// Reset modal state
|
||||
if (modalId === 'rfidModal') {
|
||||
stopQRScanner();
|
||||
document.getElementById('manualRfidInput').value = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Close modal when clicking outside
|
||||
window.onclick = function(event) {
|
||||
if (event.target.classList.contains('modal')) {
|
||||
closeModal(event.target.id);
|
||||
}
|
||||
}
|
||||
|
||||
// QR Scanner variables
|
||||
let qrStream = null;
|
||||
let qrScanning = false;
|
||||
|
||||
// Show RFID Settings
|
||||
async function showRFIDSettings() {
|
||||
openModal('rfidModal');
|
||||
// Reset scanner state
|
||||
stopQRScanner();
|
||||
}
|
||||
|
||||
// Check link status and load times
|
||||
async function checkLinkStatusAndLoadTimes() {
|
||||
if (!currentUser) {
|
||||
showTimesNotLinked();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Check if user has a linked player
|
||||
const response = await fetch(`/api/user-player/${currentUser.id}`);
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
// User is linked, load times
|
||||
await loadUserTimesSection(result.data);
|
||||
} else {
|
||||
// User is not linked
|
||||
showTimesNotLinked();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking link status:', error);
|
||||
showTimesNotLinked();
|
||||
}
|
||||
}
|
||||
|
||||
// Start QR Scanner
|
||||
async function startQRScanner() {
|
||||
try {
|
||||
// Request camera access
|
||||
qrStream = await navigator.mediaDevices.getUserMedia({
|
||||
video: {
|
||||
facingMode: 'environment', // Use back camera if available
|
||||
width: { ideal: 1280 },
|
||||
height: { ideal: 720 }
|
||||
}
|
||||
});
|
||||
|
||||
const video = document.getElementById('qrVideo');
|
||||
const canvas = document.getElementById('qrCanvas');
|
||||
const context = canvas.getContext('2d');
|
||||
|
||||
video.srcObject = qrStream;
|
||||
video.play();
|
||||
|
||||
// Show camera container and update buttons
|
||||
document.getElementById('cameraContainer').style.display = 'block';
|
||||
document.getElementById('startScanBtn').style.display = 'none';
|
||||
document.getElementById('stopScanBtn').style.display = 'inline-block';
|
||||
document.getElementById('scanningStatus').style.display = 'block';
|
||||
|
||||
qrScanning = true;
|
||||
|
||||
// Start scanning loop
|
||||
video.addEventListener('loadedmetadata', () => {
|
||||
canvas.width = video.videoWidth;
|
||||
canvas.height = video.videoHeight;
|
||||
scanQRCode();
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error accessing camera:', error);
|
||||
showMessage('rfidMessage', 'Kamera-Zugriff fehlgeschlagen. Bitte verwende die manuelle Eingabe.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Stop QR Scanner
|
||||
function stopQRScanner() {
|
||||
qrScanning = false;
|
||||
|
||||
if (qrStream) {
|
||||
qrStream.getTracks().forEach(track => track.stop());
|
||||
qrStream = null;
|
||||
}
|
||||
|
||||
// Reset UI
|
||||
document.getElementById('cameraContainer').style.display = 'none';
|
||||
document.getElementById('startScanBtn').style.display = 'inline-block';
|
||||
document.getElementById('stopScanBtn').style.display = 'none';
|
||||
document.getElementById('scanningStatus').style.display = 'none';
|
||||
}
|
||||
|
||||
// Scan QR Code from video stream
|
||||
function scanQRCode() {
|
||||
if (!qrScanning) return;
|
||||
|
||||
const video = document.getElementById('qrVideo');
|
||||
const canvas = document.getElementById('qrCanvas');
|
||||
const context = canvas.getContext('2d');
|
||||
|
||||
if (video.readyState === video.HAVE_ENOUGH_DATA) {
|
||||
canvas.width = video.videoWidth;
|
||||
canvas.height = video.videoHeight;
|
||||
context.drawImage(video, 0, 0, canvas.width, canvas.height);
|
||||
|
||||
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
|
||||
const code = jsQR(imageData.data, imageData.width, imageData.height);
|
||||
|
||||
if (code) {
|
||||
console.log('QR Code detected:', code.data);
|
||||
handleQRCodeDetected(code.data);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Continue scanning
|
||||
if (qrScanning) {
|
||||
requestAnimationFrame(scanQRCode);
|
||||
}
|
||||
}
|
||||
|
||||
// Format RFID UID to match database format
|
||||
function formatRfidUid(rawUid) {
|
||||
// Remove any existing formatting (spaces, colons, etc.)
|
||||
let cleanUid = rawUid.replace(/[^a-fA-F0-9]/g, '').toUpperCase();
|
||||
|
||||
// Handle different UID lengths
|
||||
if (cleanUid.length === 6) {
|
||||
// Pad 6-digit UID to 8 digits by adding leading zeros
|
||||
cleanUid = '00' + cleanUid;
|
||||
} else if (cleanUid.length === 8) {
|
||||
// Already correct length
|
||||
} else if (cleanUid.length < 6) {
|
||||
// Pad shorter UIDs to 8 digits
|
||||
cleanUid = cleanUid.padStart(8, '0');
|
||||
} else {
|
||||
throw new Error(`Ungültige RFID UID Länge: ${cleanUid.length} Zeichen (unterstützt: 6-8)`);
|
||||
}
|
||||
|
||||
// Format as XX:XX:XX:XX
|
||||
return cleanUid.match(/.{2}/g).join(':');
|
||||
}
|
||||
|
||||
// Handle detected QR code
|
||||
async function handleQRCodeDetected(qrData) {
|
||||
stopQRScanner();
|
||||
|
||||
try {
|
||||
// Extract and format RFID UID from QR code
|
||||
const rawUid = qrData.trim();
|
||||
|
||||
if (!rawUid) {
|
||||
showMessage('rfidMessage', 'QR-Code enthält keine gültige RFID UID', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Format the UID to match database format (XX:XX:XX:XX)
|
||||
const formattedUid = formatRfidUid(rawUid);
|
||||
|
||||
showMessage('rfidMessage', `QR-Code erkannt: ${rawUid} → ${formattedUid}`, 'info');
|
||||
|
||||
// Link the user using the formatted RFID UID
|
||||
await linkUserByRfidUid(formattedUid);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error formatting RFID UID:', error);
|
||||
showMessage('rfidMessage', `Fehler beim Formatieren der RFID UID: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Manual RFID linking
|
||||
async function linkManualRfid() {
|
||||
const rawUid = document.getElementById('manualRfidInput').value.trim();
|
||||
|
||||
if (!rawUid) {
|
||||
showMessage('rfidMessage', 'Bitte gib eine RFID UID ein', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Format the UID to match database format
|
||||
const formattedUid = formatRfidUid(rawUid);
|
||||
|
||||
showMessage('rfidMessage', `Formatiert: ${rawUid} → ${formattedUid}`, 'info');
|
||||
|
||||
await linkUserByRfidUid(formattedUid);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error formatting manual RFID UID:', error);
|
||||
showMessage('rfidMessage', `Fehler beim Formatieren: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Link user by RFID UID (core function)
|
||||
async function linkUserByRfidUid(rfidUid) {
|
||||
if (!currentUser) {
|
||||
showMessage('rfidMessage', 'Benutzer nicht authentifiziert', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// First, find the player with this RFID UID
|
||||
const response = await fetch('/api/link-by-rfid', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
rfiduid: rfidUid,
|
||||
supabase_user_id: currentUser.id
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
showMessage('rfidMessage', `✅ RFID erfolgreich verknüpft!\nSpieler: ${result.data.firstname} ${result.data.lastname}`, 'success');
|
||||
setTimeout(() => {
|
||||
closeModal('rfidModal');
|
||||
// Reload times section after successful linking
|
||||
checkLinkStatusAndLoadTimes();
|
||||
}, 2000);
|
||||
} else {
|
||||
showMessage('rfidMessage', result.message || 'Fehler beim Verknüpfen', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error linking RFID:', error);
|
||||
showMessage('rfidMessage', 'Fehler beim Verknüpfen der RFID', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Show not linked state
|
||||
function showTimesNotLinked() {
|
||||
document.getElementById('timesLoading').style.display = 'none';
|
||||
document.getElementById('timesNotLinked').style.display = 'block';
|
||||
document.getElementById('timesDisplay').style.display = 'none';
|
||||
}
|
||||
|
||||
// Show loading state
|
||||
function showTimesLoading() {
|
||||
document.getElementById('timesLoading').style.display = 'block';
|
||||
document.getElementById('timesNotLinked').style.display = 'none';
|
||||
document.getElementById('timesDisplay').style.display = 'none';
|
||||
}
|
||||
|
||||
// Load user times for the section
|
||||
async function loadUserTimesSection(playerData) {
|
||||
showTimesLoading();
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/user-times/${currentUser.id}`);
|
||||
const times = await response.json();
|
||||
|
||||
// Update stats
|
||||
updateTimesStats(times, playerData);
|
||||
|
||||
// Display times
|
||||
displayUserTimes(times);
|
||||
|
||||
// Show the times display
|
||||
document.getElementById('timesLoading').style.display = 'none';
|
||||
document.getElementById('timesNotLinked').style.display = 'none';
|
||||
document.getElementById('timesDisplay').style.display = 'block';
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading user times:', error);
|
||||
showTimesNotLinked();
|
||||
}
|
||||
}
|
||||
|
||||
// Update stats cards
|
||||
function updateTimesStats(times, playerData) {
|
||||
// Total runs
|
||||
document.getElementById('totalRuns').textContent = times.length;
|
||||
|
||||
// Best time
|
||||
if (times.length > 0) {
|
||||
const bestTimeValue = times.reduce((best, current) => {
|
||||
const currentSeconds = convertTimeToSeconds(current.recorded_time);
|
||||
const bestSeconds = convertTimeToSeconds(best.recorded_time);
|
||||
return currentSeconds < bestSeconds ? current : best;
|
||||
});
|
||||
document.getElementById('bestTime').textContent = formatTime(bestTimeValue.recorded_time);
|
||||
} else {
|
||||
document.getElementById('bestTime').textContent = '--:--';
|
||||
}
|
||||
|
||||
// Unique locations count
|
||||
const uniqueLocations = [...new Set(times.map(time => time.location_name))];
|
||||
document.getElementById('locationsCount').textContent = uniqueLocations.length;
|
||||
|
||||
// Linked player name
|
||||
document.getElementById('linkedPlayer').textContent = `${playerData.firstname} ${playerData.lastname}`;
|
||||
}
|
||||
|
||||
// Display user times in grid
|
||||
function displayUserTimes(times) {
|
||||
const timesGrid = document.getElementById('userTimesGrid');
|
||||
|
||||
if (times.length === 0) {
|
||||
timesGrid.innerHTML = `
|
||||
<div style="grid-column: 1 / -1; text-align: center; padding: 3rem; color: #8892b0;">
|
||||
<h3>Noch keine Zeiten aufgezeichnet</h3>
|
||||
<p>Deine ersten Läufe werden hier angezeigt, sobald du sie abgeschlossen hast!</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Group times by location
|
||||
const timesByLocation = times.reduce((acc, time) => {
|
||||
if (!acc[time.location_name]) {
|
||||
acc[time.location_name] = [];
|
||||
}
|
||||
acc[time.location_name].push(time);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// Generate cards for each location
|
||||
const cards = Object.entries(timesByLocation).map(([locationName, locationTimes], index) => {
|
||||
// Sort times by performance (best first)
|
||||
const sortedTimes = locationTimes.sort((a, b) => {
|
||||
return convertTimeToSeconds(a.recorded_time) - convertTimeToSeconds(b.recorded_time);
|
||||
});
|
||||
|
||||
// Get best time for this location
|
||||
const bestTime = sortedTimes[0];
|
||||
|
||||
// Generate all runs for expanded view
|
||||
const allRunsHtml = sortedTimes.map((run, runIndex) => {
|
||||
let rankBadge = '';
|
||||
let rankClass = '';
|
||||
|
||||
if (runIndex === 0) {
|
||||
rankBadge = '🥇 Beste';
|
||||
rankClass = 'best';
|
||||
} else if (runIndex === 1) {
|
||||
rankBadge = '🥈 2.';
|
||||
rankClass = 'second';
|
||||
} else if (runIndex === 2) {
|
||||
rankBadge = '🥉 3.';
|
||||
rankClass = 'third';
|
||||
} else {
|
||||
rankBadge = `${runIndex + 1}.`;
|
||||
rankClass = '';
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="run-item">
|
||||
<div>
|
||||
<div class="run-time">${formatTime(run.recorded_time)}</div>
|
||||
</div>
|
||||
<div class="run-details">
|
||||
<div>${new Date(run.created_at).toLocaleDateString('de-DE')}</div>
|
||||
<div>${new Date(run.created_at).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })}</div>
|
||||
<span class="run-rank-badge ${rankClass}">${rankBadge}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
return `
|
||||
<div class="user-time-card" onclick="toggleTimeCard(this)" data-location="${locationName}">
|
||||
<div class="card-header">
|
||||
<div class="time-location-name">${locationName}</div>
|
||||
<div class="expand-indicator">▼</div>
|
||||
</div>
|
||||
|
||||
<div class="card-main-content">
|
||||
<div class="time-value-large">${formatTime(bestTime.recorded_time)}</div>
|
||||
<div class="time-date-info">
|
||||
<span>${new Date(bestTime.created_at).toLocaleDateString('de-DE')}</span>
|
||||
<span class="time-rank">${locationTimes.length} Läufe</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="expanded-content">
|
||||
<div class="all-runs-title">Alle Läufe an diesem Standort:</div>
|
||||
${allRunsHtml}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
timesGrid.innerHTML = cards;
|
||||
}
|
||||
|
||||
// Toggle time card expansion
|
||||
function toggleTimeCard(cardElement) {
|
||||
const isExpanded = cardElement.classList.contains('expanded');
|
||||
|
||||
// Close all other cards first
|
||||
document.querySelectorAll('.user-time-card.expanded').forEach(card => {
|
||||
if (card !== cardElement) {
|
||||
card.classList.remove('expanded');
|
||||
}
|
||||
});
|
||||
|
||||
// Toggle current card
|
||||
if (isExpanded) {
|
||||
cardElement.classList.remove('expanded');
|
||||
} else {
|
||||
cardElement.classList.add('expanded');
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to convert time to seconds for comparison
|
||||
function convertTimeToSeconds(timeValue) {
|
||||
if (typeof timeValue === 'string') {
|
||||
// Handle HH:MM:SS format
|
||||
const parts = timeValue.split(':');
|
||||
if (parts.length === 3) {
|
||||
return parseInt(parts[0]) * 3600 + parseInt(parts[1]) * 60 + parseFloat(parts[2]);
|
||||
}
|
||||
// Handle MM:SS format
|
||||
if (parts.length === 2) {
|
||||
return parseInt(parts[0]) * 60 + parseFloat(parts[1]);
|
||||
}
|
||||
}
|
||||
return parseFloat(timeValue) || 0;
|
||||
}
|
||||
|
||||
// Format time interval to readable format
|
||||
function formatTime(interval) {
|
||||
// Postgres interval format: {"hours":0,"minutes":1,"seconds":23.45}
|
||||
if (typeof interval === 'object') {
|
||||
const { hours = 0, minutes = 0, seconds = 0 } = interval;
|
||||
const totalSeconds = hours * 3600 + minutes * 60 + seconds;
|
||||
return formatSeconds(totalSeconds);
|
||||
}
|
||||
|
||||
// Fallback for string format
|
||||
if (typeof interval === 'string') {
|
||||
// Parse format like "00:01:23.45"
|
||||
const parts = interval.split(':');
|
||||
if (parts.length === 3) {
|
||||
const hours = parseInt(parts[0]);
|
||||
const minutes = parseInt(parts[1]);
|
||||
const seconds = parseFloat(parts[2]);
|
||||
const totalSeconds = hours * 3600 + minutes * 60 + seconds;
|
||||
return formatSeconds(totalSeconds);
|
||||
}
|
||||
}
|
||||
|
||||
return interval;
|
||||
}
|
||||
|
||||
function formatSeconds(totalSeconds) {
|
||||
const minutes = Math.floor(totalSeconds / 60);
|
||||
const seconds = (totalSeconds % 60).toFixed(2);
|
||||
|
||||
if (minutes > 0) {
|
||||
return `${minutes}:${seconds.padStart(5, '0')}`;
|
||||
} else {
|
||||
return `${seconds}s`;
|
||||
}
|
||||
}
|
||||
|
||||
// Show message in modal
|
||||
function showMessage(containerId, message, type) {
|
||||
const container = document.getElementById(containerId);
|
||||
container.innerHTML = `<div class="message ${type}">${message}</div>`;
|
||||
}
|
||||
560
public/js/generator.js
Normal file
560
public/js/generator.js
Normal file
@@ -0,0 +1,560 @@
|
||||
// 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/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);
|
||||
|
||||
// Koordinaten anzeigen
|
||||
updateCoordinates(lat, lon);
|
||||
coordinatesDiv.style.display = 'block';
|
||||
|
||||
// Interaktive Karte erstellen
|
||||
createInteractiveMap(lat, lon);
|
||||
mapContainer.style.display = 'block';
|
||||
|
||||
// Erfolgsmeldung
|
||||
showSuccess(`✅ Standort "${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('locationSearch').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/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/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();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
190
public/js/reset-password.js
Normal file
190
public/js/reset-password.js
Normal file
@@ -0,0 +1,190 @@
|
||||
// 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();
|
||||
}
|
||||
|
||||
// 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!');
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -8,243 +8,7 @@
|
||||
<!-- Supabase -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2"></script>
|
||||
|
||||
<style>
|
||||
/* 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;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="/css/reset-password.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
@@ -296,197 +60,6 @@
|
||||
<a href="/" class="back-link">← Zurück zur Hauptseite</a>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 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();
|
||||
}
|
||||
|
||||
// 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!');
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<script src="/js/reset-password.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user