diff --git a/public/admin-dashboard.html b/public/admin-dashboard.html index e8b4219..7278c5a 100644 --- a/public/admin-dashboard.html +++ b/public/admin-dashboard.html @@ -4,307 +4,7 @@ Admin Dashboard - NinjaCross - +
@@ -425,480 +125,7 @@
- + + diff --git a/public/css/admin-dashboard.css b/public/css/admin-dashboard.css new file mode 100644 index 0000000..5903be4 --- /dev/null +++ b/public/css/admin-dashboard.css @@ -0,0 +1,343 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%); + min-height: 100vh; + color: #333; +} + +.header { + background: rgba(255, 255, 255, 0.95); + padding: 20px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + display: flex; + justify-content: space-between; + align-items: center; +} + +.header h1 { + color: #1e3c72; + font-size: 2em; +} + +.user-info { + display: flex; + align-items: center; + gap: 15px; +} + +.access-badge { + background: #4CAF50; + color: white; + padding: 5px 10px; + border-radius: 15px; + font-size: 0.8em; + font-weight: bold; +} + +.access-badge.level-1 { + background: #ff9800; +} + +.access-badge.level-2 { + background: #4CAF50; +} + +.btn { + background: #2196F3; + color: white; + border: none; + padding: 10px 20px; + border-radius: 5px; + cursor: pointer; + text-decoration: none; + display: inline-block; + font-size: 0.9em; + transition: all 0.3s ease; +} + +.btn:hover { + background: #1976D2; + transform: translateY(-2px); +} + +.btn-danger { + background: #f44336; +} + +.btn-danger:hover { + background: #d32f2f; +} + +.btn-warning { + background: #ff9800; +} + +.btn-warning:hover { + background: #f57c00; +} + +.btn-success { + background: #4CAF50; +} + +.btn-success:hover { + background: #388E3C; +} + +.container { + max-width: 1200px; + margin: 30px auto; + padding: 0 20px; +} + +.dashboard-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 20px; + margin-bottom: 30px; +} + +.card { + background: white; + border-radius: 10px; + padding: 20px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); + transition: transform 0.3s ease; +} + +.card:hover { + transform: translateY(-5px); +} + +.card h3 { + color: #1e3c72; + margin-bottom: 15px; + display: flex; + align-items: center; + gap: 10px; +} + +.icon { + font-size: 1.5em; +} + +.stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 15px; + margin-bottom: 30px; +} + +.stat-card { + background: white; + padding: 20px; + border-radius: 10px; + text-align: center; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); +} + +.stat-number { + font-size: 2.5em; + font-weight: bold; + color: #1e3c72; + margin-bottom: 5px; +} + +.stat-label { + color: #666; + font-size: 0.9em; +} + +.data-table { + width: 100%; + border-collapse: collapse; + background: white; + border-radius: 10px; + overflow: hidden; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); +} + +.data-table th, +.data-table td { + padding: 12px; + text-align: left; + border-bottom: 1px solid #eee; +} + +.data-table th { + background: #f5f5f5; + font-weight: bold; + color: #333; +} + +.data-table tr:hover { + background: #f9f9f9; +} + +.modal { + display: none; + position: fixed; + z-index: 1000; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); +} + +.modal-content { + background-color: white; + margin: 10% auto; + padding: 20px; + border-radius: 10px; + width: 90%; + max-width: 500px; + position: relative; +} + +.close { + color: #aaa; + float: right; + font-size: 28px; + font-weight: bold; + cursor: pointer; + position: absolute; + right: 15px; + top: 10px; +} + +.close:hover { + color: #000; +} + +.form-group { + margin-bottom: 15px; +} + +.form-group label { + display: block; + margin-bottom: 5px; + font-weight: bold; + color: #333; +} + +.form-group input, +.form-group select { + width: 100%; + padding: 10px; + border: 1px solid #ddd; + border-radius: 5px; + font-size: 1em; +} + +.message { + padding: 10px; + border-radius: 5px; + margin-bottom: 15px; + display: none; +} + +.message.success { + background: #d4edda; + color: #155724; + border: 1px solid #c3e6cb; +} + +.message.error { + background: #f8d7da; + color: #721c24; + border: 1px solid #f5c6cb; +} + +.loading { + text-align: center; + padding: 20px; + color: #666; +} + +.no-data { + text-align: center; + padding: 40px; + color: #999; + font-style: italic; +} + +.action-buttons { + display: flex; + gap: 5px; +} + +.btn-small { + padding: 5px 10px; + font-size: 0.8em; +} + +.table-container { + max-height: 400px; + overflow-y: auto; + margin-top: 15px; +} + +.search-container { + margin-bottom: 20px; + display: flex; + gap: 10px; + align-items: center; +} + +.search-input { + flex: 1; + padding: 10px; + border: 1px solid #ddd; + border-radius: 5px; +} + +@media (max-width: 768px) { + .header { + flex-direction: column; + gap: 15px; + text-align: center; + } + + .dashboard-grid { + grid-template-columns: 1fr; + } + + .stats-grid { + grid-template-columns: repeat(2, 1fr); + } + + .search-container { + flex-direction: column; + align-items: stretch; + } + + .action-buttons { + flex-direction: column; + } + + .modal-content { + margin: 5% auto; + width: 95%; + } +} + +@media (max-width: 480px) { + .stats-grid { + grid-template-columns: 1fr; + } + + .container { + padding: 0 10px; + } + + .card { + padding: 15px; + } +} diff --git a/public/css/leaderboard.css b/public/css/leaderboard.css new file mode 100644 index 0000000..5805579 --- /dev/null +++ b/public/css/leaderboard.css @@ -0,0 +1,693 @@ +@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; +} + +.dashboard-grid { + display: grid; + grid-template-columns: 1fr 2fr; + gap: 2rem; + margin-bottom: 2rem; +} + +.control-panel { + background: rgba(15, 23, 42, 0.8); + border: 1px solid #1e293b; + border-radius: 1rem; + padding: 2rem; + backdrop-filter: blur(20px); +} + +.control-group { + margin-bottom: 1.5rem; +} + +.control-label { + display: block; + margin-bottom: 0.5rem; + font-size: 0.9rem; + font-weight: 500; + color: #cbd5e1; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.custom-select { + width: 100%; + padding: 1rem; + background: #1e293b; + border: 2px solid #334155; + border-radius: 0.75rem; + color: #ffffff; + font-size: 1rem; + font-family: inherit; + transition: all 0.2s ease; +} + +.custom-select:focus { + outline: none; + border-color: #00d4ff; + box-shadow: 0 0 0 3px rgba(0, 212, 255, 0.1); +} + +/* Location Control Layout */ +.location-control { + display: flex; + gap: 0.75rem; + align-items: center; +} + +.location-select { + flex: 1; +} + +.location-btn { + padding: 1rem 1.5rem; + background: linear-gradient(135deg, #10b981, #059669); + border: none; + border-radius: 0.75rem; + color: white; + font-weight: 600; + font-size: 0.9rem; + cursor: pointer; + transition: all 0.2s ease; + white-space: nowrap; + min-width: 140px; +} + +.location-btn:hover { + background: linear-gradient(135deg, #059669, #047857); + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3); +} + +.location-btn:disabled { + background: #374151; + color: #9ca3af; + cursor: not-allowed; + transform: none; + box-shadow: none; +} + +.location-btn.loading { + background: linear-gradient(135deg, #6366f1, #4f46e5); + position: relative; + overflow: hidden; +} + +.location-btn.loading::after { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent); + animation: loading-sweep 1.5s infinite; +} + +@keyframes loading-sweep { + 0% { left: -100%; } + 100% { left: 100%; } +} + +/* Horizontal Time Tabs */ +.time-tabs { + display: flex; + background: #1e293b; + border: 2px solid #334155; + border-radius: 0.75rem; + padding: 0.25rem; + gap: 0.25rem; + overflow-x: auto; +} + +.time-tab { + flex: 1; + min-width: 0; + padding: 0.75rem 1rem; + background: transparent; + border: none; + border-radius: 0.5rem; + color: #94a3b8; + font-size: 0.875rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + white-space: nowrap; +} + +.time-tab:hover { + background: #334155; + color: #e2e8f0; + transform: translateY(-1px); +} + +.time-tab.active { + background: linear-gradient(135deg, #00d4ff, #0891b2); + color: white; + box-shadow: 0 4px 12px rgba(0, 212, 255, 0.3); +} + +.time-tab.active:hover { + transform: translateY(-1px); + box-shadow: 0 6px 16px rgba(0, 212, 255, 0.4); +} + +.tab-icon { + font-size: 1rem; + flex-shrink: 0; +} + +.tab-text { + font-size: 0.875rem; + font-weight: 500; +} + +.refresh-btn { + width: 100%; + padding: 1rem; + background: linear-gradient(135deg, #00d4ff, #0891b2); + border: none; + border-radius: 0.75rem; + color: white; + font-weight: 600; + font-size: 1rem; + cursor: pointer; + transition: all 0.2s ease; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.refresh-btn:hover { + transform: translateY(-2px); + box-shadow: 0 10px 25px rgba(0, 212, 255, 0.3); +} + +.stats-panel { + background: rgba(15, 23, 42, 0.8); + border: 1px solid #1e293b; + border-radius: 1rem; + padding: 2rem; + backdrop-filter: blur(20px); +} + +.stats-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1.5rem; + height: 100%; +} + +.stat-card { + text-align: center; + padding: 1rem; + background: linear-gradient(135deg, rgba(0, 212, 255, 0.1), rgba(255, 107, 53, 0.1)); + border-radius: 0.75rem; + border: 1px solid rgba(255, 255, 255, 0.1); +} + +.stat-value { + font-size: 2.5rem; + font-weight: 700; + color: #00d4ff; + margin-bottom: 0.25rem; +} + +.stat-label { + font-size: 0.8rem; + color: #8892b0; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.leaderboard-container { + background: rgba(15, 23, 42, 0.8); + border: 1px solid #1e293b; + border-radius: 1rem; + backdrop-filter: blur(20px); + overflow: hidden; +} + +.leaderboard-header { + padding: 2rem; + background: linear-gradient(135deg, #1e293b, #0f172a); + border-bottom: 1px solid #334155; +} + +.active-filters { + font-size: 1.5rem; + font-weight: 600; + color: #ffffff; + margin-bottom: 0.5rem; +} + +.last-sync { + font-size: 0.9rem; + color: #64748b; +} + +.rankings-list { + max-height: 600px; + overflow-y: auto; + scroll-behavior: smooth; +} + +.rankings-list::-webkit-scrollbar { + width: 6px; +} + +.rankings-list::-webkit-scrollbar-track { + background: #1e293b; +} + +.rankings-list::-webkit-scrollbar-thumb { + background: #475569; + border-radius: 3px; +} + +.rank-entry { + display: grid; + grid-template-columns: 80px 1fr auto auto; + align-items: center; + padding: 1.5rem 2rem; + border-bottom: 1px solid rgba(51, 65, 85, 0.5); + transition: all 0.2s ease; + position: relative; +} + +.rank-entry:hover { + background: rgba(0, 212, 255, 0.05); + border-left: 4px solid #00d4ff; +} + +.rank-entry:last-child { + border-bottom: none; +} + +.position { + font-size: 2rem; + font-weight: 700; + text-align: center; +} + +.position.gold { + color: #ffd700; + text-shadow: 0 0 20px rgba(255, 215, 0, 0.5); +} + +.position.silver { + color: #c0c0c0; + text-shadow: 0 0 20px rgba(192, 192, 192, 0.3); +} + +.position.bronze { + color: #cd7f32; + text-shadow: 0 0 20px rgba(205, 127, 50, 0.3); +} + +.player-data { + padding-left: 1rem; +} + +.player-name { + font-size: 1.3rem; + font-weight: 600; + color: #ffffff; + margin-bottom: 0.25rem; +} + +.player-meta { + font-size: 0.9rem; + color: #8892b0; + display: flex; + gap: 1rem; + align-items: center; +} + +.location-tag { + background: rgba(0, 212, 255, 0.2); + color: #00d4ff; + padding: 0.25rem 0.5rem; + border-radius: 0.25rem; + font-size: 0.8rem; + font-weight: 500; +} + +.time-result { + font-family: 'Courier New', monospace; + font-size: 2.5rem; + font-weight: 700; + color: #ffffff; + text-align: right; +} + +.trophy-icon { + font-size: 2rem; + margin-left: 1rem; +} + +.empty-state { + text-align: center; + padding: 4rem 2rem; + color: #64748b; +} + +.empty-icon { + font-size: 4rem; + margin-bottom: 1rem; +} + +.empty-title { + font-size: 1.5rem; + font-weight: 600; + margin-bottom: 0.5rem; + color: #8892b0; +} + +.empty-description { + font-size: 1rem; + line-height: 1.6; +} + +/* Admin Login Button */ +.admin-login-btn { + position: fixed; + top: 2rem; + right: 2rem; + padding: 0.75rem 1.5rem; + background: linear-gradient(135deg, #ff6b35, #f7931e); + border: none; + border-radius: 0.75rem; + color: white; + font-weight: 600; + font-size: 0.9rem; + cursor: pointer; + transition: all 0.2s ease; + text-decoration: none; + display: inline-block; + z-index: 1000; +} + +.admin-login-btn:hover { + transform: translateY(-2px); + box-shadow: 0 10px 25px rgba(255, 107, 53, 0.3); +} + +.dashboard-btn { + position: fixed; + top: 2rem; + right: 2rem; + padding: 0.75rem 1.5rem; + background: linear-gradient(135deg, #00d4ff, #0891b2); + border: none; + border-radius: 0.75rem; + color: white; + font-weight: 600; + font-size: 0.9rem; + cursor: pointer; + transition: all 0.2s ease; + text-decoration: none; + display: inline-block; + z-index: 1000; +} + +.dashboard-btn:hover { + transform: translateY(-2px); + box-shadow: 0 10px 25px rgba(0, 212, 255, 0.3); +} + +.logout-btn { + position: fixed; + top: 2rem; + right: 12rem; + padding: 0.75rem 1.5rem; + background: linear-gradient(135deg, #dc3545, #c82333); + border: none; + border-radius: 0.75rem; + color: white; + font-weight: 600; + font-size: 0.9rem; + cursor: pointer; + transition: all 0.2s ease; + text-decoration: none; + display: inline-block; + z-index: 1000; +} + +.logout-btn:hover { + transform: translateY(-2px); + box-shadow: 0 10px 25px rgba(220, 53, 69, 0.3); +} + +.pulse-animation { + animation: pulse 2s infinite; +} + +@keyframes pulse { + 0% { opacity: 1; } + 50% { opacity: 0.7; } + 100% { opacity: 1; } +} + +/* Notification Bubble Styles */ +.notification-bubble { + position: fixed; + top: 2rem; + left: 50%; + transform: translateX(-50%); + background: linear-gradient(135deg, #00d4ff, #0891b2); + color: white; + padding: 1rem 2rem; + border-radius: 1rem; + box-shadow: 0 10px 25px rgba(0, 212, 255, 0.3); + z-index: 2000; + opacity: 0; + transform: translateX(-50%) translateY(-20px); + transition: all 0.3s ease; + backdrop-filter: blur(20px); + border: 1px solid rgba(255, 255, 255, 0.2); + max-width: 400px; + text-align: center; +} + +.notification-bubble.show { + opacity: 1; + transform: translateX(-50%) translateY(0); +} + +.notification-bubble.hide { + opacity: 0; + transform: translateX(-50%) translateY(-20px); +} + +.notification-content { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.notification-icon { + font-size: 1.5rem; + animation: bounce 0.6s ease-in-out; +} + +.notification-text { + flex: 1; +} + +.notification-title { + font-weight: 600; + font-size: 1rem; + margin-bottom: 0.25rem; +} + +.notification-subtitle { + font-size: 0.85rem; + opacity: 0.9; +} + +@keyframes bounce { + 0%, 20%, 50%, 80%, 100% { + transform: translateY(0); + } + 40% { + transform: translateY(-10px); + } + 60% { + transform: translateY(-5px); + } +} + +@keyframes slideIn { + from { + opacity: 0; + transform: translateX(-50%) translateY(-20px); + } + to { + opacity: 1; + transform: translateX(-50%) translateY(0); + } +} + +@keyframes slideOut { + from { + opacity: 1; + transform: translateX(-50%) translateY(0); + } + to { + opacity: 0; + transform: translateX(-50%) translateY(-20px); + } +} + +/* Mobile responsive styles */ +@media (max-width: 1024px) { + .dashboard-grid { + grid-template-columns: 1fr; + } + + .stats-grid { + grid-template-columns: repeat(3, 1fr); + } +} + +@media (max-width: 768px) { + .main-title { + font-size: 2.5rem; + } + + .rank-entry { + grid-template-columns: 60px 1fr auto; + padding: 1rem; + gap: 0.5rem; + } + + .trophy-icon { + display: none; + } + + .time-result { + font-size: 1.5rem; + } + + .stats-grid { + grid-template-columns: 1fr; + gap: 1rem; + } + + .player-meta { + flex-direction: column; + align-items: flex-start; + gap: 0.5rem; + } + + .admin-login-btn, .dashboard-btn, .logout-btn { + top: 1rem; + right: 1rem; + padding: 0.5rem 1rem; + font-size: 0.8rem; + } + + .logout-btn { + right: 8rem; + } + + .location-control { + flex-direction: column; + gap: 0.5rem; + } + + .location-btn { + width: 100%; + min-width: auto; + } + + /* Mobile responsive tabs */ + .time-tabs { + flex-direction: column; + gap: 0.5rem; + padding: 0.5rem; + } + + .time-tab { + padding: 1rem; + justify-content: flex-start; + border-radius: 0.75rem; + } + + .tab-text { + font-size: 1rem; + } +} + +@media (max-width: 480px) { + .time-tabs { + flex-direction: row; + overflow-x: auto; + padding: 0.25rem; + gap: 0.25rem; + } + + .time-tab { + min-width: 80px; + padding: 0.5rem 0.75rem; + flex-direction: column; + gap: 0.25rem; + } + + .tab-icon { + font-size: 1.2rem; + } + + .tab-text { + font-size: 0.75rem; + } +} diff --git a/public/css/login.css b/public/css/login.css new file mode 100644 index 0000000..c7da188 --- /dev/null +++ b/public/css/login.css @@ -0,0 +1,309 @@ +@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; + display: flex; + align-items: center; + justify-content: center; + background-image: + radial-gradient(circle at 20% 80%, #1a1a2e 0%, transparent 50%), + radial-gradient(circle at 80% 20%, #16213e 0%, transparent 50%), + radial-gradient(circle at 40% 40%, #0f3460 0%, transparent 50%); + position: relative; +} + +.back-button { + position: fixed; + top: 20px; + right: 20px; + background: rgba(30, 41, 59, 0.95); + backdrop-filter: blur(20px); + border: 1px solid rgba(51, 65, 85, 0.3); + border-radius: 12px; + padding: 12px 20px; + color: #00d4ff; + text-decoration: none; + font-weight: 600; + font-size: 0.9rem; + text-transform: uppercase; + letter-spacing: 0.5px; + transition: all 0.3s ease; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); + z-index: 1000; +} + +.back-button:hover { + background: #00d4ff; + color: #ffffff; + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(0, 212, 255, 0.3); +} + +.back-button::before { + content: "← "; + margin-right: 5px; +} + +.container { + background: rgba(30, 41, 59, 0.95); + backdrop-filter: blur(20px); + border: 1px solid rgba(51, 65, 85, 0.3); + padding: 2.5rem; + border-radius: 1.5rem; + box-shadow: + 0 25px 50px rgba(0, 0, 0, 0.3), + 0 0 0 1px rgba(0, 212, 255, 0.1); + width: 100%; + max-width: 420px; +} + +.logo { + text-align: center; + margin-bottom: 2rem; +} + +.logo h1 { + font-size: 2.5rem; + font-weight: 700; + background: linear-gradient(135deg, #00d4ff, #ff6b35, #ffd700); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + margin-bottom: 0.5rem; + letter-spacing: -0.02em; +} + +.logo p { + color: #94a3b8; + margin-top: 0.5rem; + font-size: 1rem; + font-weight: 400; +} + +.form-container { + display: none; +} + +.form-container.active { + display: block; +} + +.form-group { + margin-bottom: 1.5rem; +} + +.form-group label { + display: block; + margin-bottom: 0.5rem; + color: #e2e8f0; + font-weight: 500; + font-size: 0.875rem; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.form-group input { + width: 100%; + padding: 1rem; + background: #1e293b; + border: 2px solid #334155; + border-radius: 0.75rem; + color: #ffffff; + font-size: 1rem; + font-family: inherit; + transition: all 0.2s ease; +} + +.form-group input:focus { + outline: none; + border-color: #00d4ff; + box-shadow: 0 0 0 3px rgba(0, 212, 255, 0.1); +} + +.form-group input::placeholder { + color: #64748b; +} + +.btn { + width: 100%; + padding: 1rem; + border: none; + border-radius: 0.75rem; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; + margin-bottom: 1rem; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.btn-primary { + background: linear-gradient(135deg, #00d4ff, #0891b2); + color: white; + box-shadow: 0 4px 12px rgba(0, 212, 255, 0.3); +} + +.btn-primary:hover { + transform: translateY(-2px); + box-shadow: 0 6px 16px rgba(0, 212, 255, 0.4); +} + +.btn-secondary { + background: transparent; + color: #00d4ff; + border: 2px solid #00d4ff; +} + +.btn-secondary:hover { + background: #00d4ff; + color: #0a0a0f; +} + +.toggle-form { + text-align: center; + margin-top: 1rem; +} + +.toggle-form button { + background: none; + border: none; + color: #00d4ff; + cursor: pointer; + text-decoration: underline; + font-size: 0.9rem; + transition: color 0.2s ease; +} + +.toggle-form button:hover { + color: #0891b2; +} + +.message { + padding: 1rem; + border-radius: 0.75rem; + margin-bottom: 1rem; + text-align: center; + font-weight: 500; + border: 1px solid; +} + +.message.success { + background: rgba(34, 197, 94, 0.1); + color: #22c55e; + border-color: rgba(34, 197, 94, 0.3); +} + +.message.error { + background: rgba(239, 68, 68, 0.1); + color: #ef4444; + border-color: rgba(239, 68, 68, 0.3); +} + +.password-reset-container { + display: none; + margin-top: 1rem; + padding: 1rem; + background: rgba(239, 68, 68, 0.05); + border: 1px solid rgba(239, 68, 68, 0.2); + border-radius: 0.75rem; + text-align: center; +} + +.password-reset-container.active { + display: block; +} + +.password-reset-container p { + color: #ef4444; + margin-bottom: 1rem; + font-size: 0.9rem; +} + +.btn-reset { + background: linear-gradient(135deg, #ef4444, #dc2626); + color: white; + box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3); +} + +.btn-reset:hover { + transform: translateY(-2px); + box-shadow: 0 6px 16px rgba(239, 68, 68, 0.4); +} + +.loading { + display: none; + text-align: center; + color: #94a3b8; +} + +.loading.active { + display: block; +} + +.spinner { + border: 3px solid #334155; + border-top: 3px solid #00d4ff; + border-radius: 50%; + width: 30px; + height: 30px; + animation: spin 1s linear infinite; + margin: 0 auto 1rem; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +/* Responsive Design */ +@media (max-width: 768px) { + .container { + margin: 1rem; + padding: 2rem; + max-width: none; + } + + .logo h1 { + font-size: 2rem; + } + + .form-group input { + padding: 0.875rem; + } + + .btn { + padding: 0.875rem; + } +} + +@media (max-width: 480px) { + .container { + margin: 0.5rem; + padding: 1.5rem; + } + + .logo h1 { + font-size: 1.75rem; + } + + .logo p { + font-size: 0.875rem; + } + + .back-button { + top: 15px; + right: 15px; + padding: 10px 15px; + font-size: 0.8rem; + } +} diff --git a/public/generator.html b/public/generator.html index f0aaea4..6810286 100644 --- a/public/generator.html +++ b/public/generator.html @@ -397,7 +397,10 @@

🔐 Lizenzgenerator

- +
+ + +
@@ -1010,6 +1013,11 @@ } } + // Zurück zum Dashboard + function goBackToDashboard() { + window.location.href = '/admin-dashboard'; + } + // Logout-Funktion async function logout() { try { diff --git a/public/index.html b/public/index.html index b59c9bb..37a522b 100644 --- a/public/index.html +++ b/public/index.html @@ -4,703 +4,7 @@ Timer Leaderboard - + @@ -801,500 +105,11 @@
- + - - + + - \ No newline at end of file + diff --git a/public/js/admin-dashboard.js b/public/js/admin-dashboard.js new file mode 100644 index 0000000..0cea831 --- /dev/null +++ b/public/js/admin-dashboard.js @@ -0,0 +1,737 @@ +let currentUser = null; +let currentDataType = null; +let currentData = []; + +// Beim Laden der Seite +document.addEventListener('DOMContentLoaded', function() { + checkAuth(); + loadStatistics(); + setupEventListeners(); +}); + +function setupEventListeners() { + // Logout Button + document.getElementById('logoutBtn').addEventListener('click', logout); + + // Generator Button + document.getElementById('generatorBtn').addEventListener('click', function() { + window.location.href = '/generator'; + }); + + // Modal Close Buttons + document.querySelectorAll('.close').forEach(closeBtn => { + closeBtn.addEventListener('click', closeModal); + }); + + // Confirm Modal Buttons + document.getElementById('confirmNo').addEventListener('click', closeModal); + + // Search Input + document.getElementById('searchInput').addEventListener('input', filterData); + + // Add Form + document.getElementById('addForm').addEventListener('submit', handleAddSubmit); +} + +async function checkAuth() { + try { + const response = await fetch('/api/check-session'); + const result = await response.json(); + + if (!result.success) { + window.location.href = '/adminlogin.html'; + return; + } + + currentUser = result.user; + document.getElementById('username').textContent = currentUser.username; + + const accessBadge = document.getElementById('accessBadge'); + accessBadge.textContent = `Level ${currentUser.access_level}`; + accessBadge.className = `access-badge level-${currentUser.access_level}`; + + // Generator-Button nur für Level 2 + if (currentUser.access_level >= 2) { + document.getElementById('generatorBtn').style.display = 'inline-block'; + } + + } catch (error) { + console.error('Auth check failed:', error); + window.location.href = '/adminlogin.html'; + } +} + +async function logout() { + try { + await fetch('/api/logout', { method: 'POST' }); + window.location.href = '/adminlogin.html'; + } catch (error) { + console.error('Logout failed:', error); + window.location.href = '/adminlogin.html'; + } +} + +async function loadStatistics() { + try { + const response = await fetch('/api/admin-stats'); + const stats = await response.json(); + + if (stats.success) { + document.getElementById('totalPlayers').textContent = stats.data.players || 0; + document.getElementById('totalRuns').textContent = stats.data.runs || 0; + document.getElementById('totalLocations').textContent = stats.data.locations || 0; + document.getElementById('totalAdminUsers').textContent = stats.data.adminUsers || 0; + } + } catch (error) { + console.error('Failed to load statistics:', error); + } +} + +function showUserManagement() { + showDataSection('users', 'Benutzer-Verwaltung'); + loadUsers(); +} + +function showPlayerManagement() { + showDataSection('players', 'Spieler-Verwaltung'); + loadPlayers(); +} + +function showRunManagement() { + showDataSection('runs', 'Läufe-Verwaltung'); + loadRuns(); +} + +function showLocationManagement() { + showDataSection('locations', 'Standort-Verwaltung'); + loadLocations(); +} + +function showAdminUserManagement() { + showDataSection('adminusers', 'Admin-Benutzer'); + loadAdminUsers(); +} + +function showSystemInfo() { + showDataSection('system', 'System-Informationen'); + loadSystemInfo(); +} + +function showDataSection(type, title) { + currentDataType = type; + document.getElementById('dataTitle').textContent = title; + document.getElementById('dataSection').style.display = 'block'; + document.getElementById('searchInput').value = ''; +} + +async function loadUsers() { + // Implementation für Supabase-Benutzer + document.getElementById('dataContent').innerHTML = '
Supabase-Benutzer werden über die Supabase-Console verwaltet
'; +} + +async function loadPlayers() { + try { + const response = await fetch('/api/admin-players'); + const result = await response.json(); + + if (result.success) { + currentData = result.data; + displayPlayersTable(result.data); + } else { + showError('Fehler beim Laden der Spieler'); + } + } catch (error) { + console.error('Failed to load players:', error); + showError('Fehler beim Laden der Spieler'); + } +} + +async function loadRuns() { + try { + const response = await fetch('/api/admin-runs'); + const result = await response.json(); + + if (result.success) { + currentData = result.data; + displayRunsTable(result.data); + } else { + showError('Fehler beim Laden der Läufe'); + } + } catch (error) { + console.error('Failed to load runs:', error); + showError('Fehler beim Laden der Läufe'); + } +} + +async function loadLocations() { + try { + const response = await fetch('/api/admin-locations'); + const result = await response.json(); + + if (result.success) { + currentData = result.data; + displayLocationsTable(result.data); + } else { + showError('Fehler beim Laden der Standorte'); + } + } catch (error) { + console.error('Failed to load locations:', error); + showError('Fehler beim Laden der Standorte'); + } +} + +async function loadAdminUsers() { + try { + const response = await fetch('/api/admin-adminusers'); + const result = await response.json(); + + if (result.success) { + currentData = result.data; + displayAdminUsersTable(result.data); + } else { + showError('Fehler beim Laden der Admin-Benutzer'); + } + } catch (error) { + console.error('Failed to load admin users:', error); + showError('Fehler beim Laden der Admin-Benutzer'); + } +} + +function displayPlayersTable(players) { + let html = '
'; + html += ''; + + players.forEach(player => { + html += ` + + + + + + + `; + }); + + html += '
IDNameRFID UIDSupabase UserRegistriertAktionen
${player.id}${player.full_name || '-'}${player.rfiduid || '-'}${player.supabase_user_id ? '✅' : '❌'}${new Date(player.created_at).toLocaleDateString('de-DE')} + + +
'; + document.getElementById('dataContent').innerHTML = html; +} + +function displayRunsTable(runs) { + let html = '
'; + html += ''; + + runs.forEach(run => { + // Use the time_seconds value from the backend + const timeInSeconds = parseFloat(run.time_seconds) || 0; + + html += ` + + + + + + + `; + }); + + html += '
IDSpielerStandortZeitDatumAktionen
${run.id}${run.player_name || `Player ${run.player_id}`}${run.location_name || `Location ${run.location_id}`}${timeInSeconds.toFixed(3)}s${new Date(run.created_at).toLocaleDateString('de-DE')} ${new Date(run.created_at).toLocaleTimeString('de-DE')} + + +
'; + document.getElementById('dataContent').innerHTML = html; +} + +function displayLocationsTable(locations) { + let html = '
'; + html += ''; + + locations.forEach(location => { + html += ` + + + + + + + `; + }); + + html += '
IDNameLatitudeLongitudeErstelltAktionen
${location.id}${location.name}${location.latitude}${location.longitude}${new Date(location.created_at).toLocaleDateString('de-DE')} + + +
'; + document.getElementById('dataContent').innerHTML = html; +} + +function displayAdminUsersTable(users) { + let html = '
'; + html += ''; + + users.forEach(user => { + const isCurrentUser = user.id === currentUser.id; + html += ` + + + + + + + `; + }); + + html += '
IDBenutzernameAccess LevelAktivLetzter LoginAktionen
${user.id}${user.username} ${isCurrentUser ? '(Du)' : ''}Level ${user.access_level}${user.is_active ? '✅' : '❌'}${user.last_login ? new Date(user.last_login).toLocaleDateString('de-DE') : 'Nie'} + ${!isCurrentUser ? `` : ''} +
'; + document.getElementById('dataContent').innerHTML = html; +} + +function loadSystemInfo() { + document.getElementById('dataContent').innerHTML = ` +
+

Server-Informationen

+

Node.js Version: ${navigator.userAgent}

+

Aktuelle Zeit: ${new Date().toLocaleString('de-DE')}

+

Angemeldeter Benutzer: ${currentUser.username} (Level ${currentUser.access_level})

+
+ `; +} + +function filterData() { + const searchTerm = document.getElementById('searchInput').value.toLowerCase(); + if (!currentData) return; + + let filteredData = currentData.filter(item => { + return Object.values(item).some(value => + value && value.toString().toLowerCase().includes(searchTerm) + ); + }); + + switch(currentDataType) { + case 'players': + displayPlayersTable(filteredData); + break; + case 'runs': + displayRunsTable(filteredData); + break; + case 'locations': + displayLocationsTable(filteredData); + break; + case 'adminusers': + displayAdminUsersTable(filteredData); + break; + } +} + +function refreshData() { + switch(currentDataType) { + case 'players': + loadPlayers(); + break; + case 'runs': + loadRuns(); + break; + case 'locations': + loadLocations(); + break; + case 'adminusers': + loadAdminUsers(); + break; + case 'system': + loadSystemInfo(); + break; + } +} + +function showAddModal() { + const modal = document.getElementById('addModal'); + const modalTitle = document.getElementById('modalTitle'); + const formFields = document.getElementById('formFields'); + + // Modal-Titel basierend auf aktuellem Datentyp setzen + switch(currentDataType) { + case 'players': + modalTitle.textContent = 'Neuen Spieler hinzufügen'; + formFields.innerHTML = ` +
+ + +
+
+ + +
+
+ + +
+ `; + break; + + case 'locations': + modalTitle.textContent = 'Neuen Standort hinzufügen'; + formFields.innerHTML = ` +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ `; + break; + + case 'adminusers': + modalTitle.textContent = 'Neuen Admin-Benutzer hinzufügen'; + formFields.innerHTML = ` +
+ + +
+
+ + +
+
+ + +
+ `; + break; + + case 'runs': + modalTitle.textContent = 'Neuen Lauf hinzufügen'; + formFields.innerHTML = ` +
+ + +
+
+ + +
+
+ + +
+ `; + break; + + default: + modalTitle.textContent = 'Element hinzufügen'; + formFields.innerHTML = '

Keine Felder für diesen Datentyp verfügbar.

'; + } + + modal.style.display = 'block'; +} + +function closeModal() { + document.querySelectorAll('.modal').forEach(modal => { + modal.style.display = 'none'; + }); + // Formular zurücksetzen + document.getElementById('addForm').reset(); + document.getElementById('modalMessage').style.display = 'none'; +} + +async function handleAddSubmit(e) { + e.preventDefault(); + + const formData = new FormData(e.target); + const data = Object.fromEntries(formData.entries()); + + // Prüfen ob es sich um eine Edit-Operation handelt + const isEdit = data.edit_id; + const editId = data.edit_id; + delete data.edit_id; // edit_id aus den Daten entfernen + + try { + let endpoint = ''; + let successMessage = ''; + let method = 'POST'; + + switch(currentDataType) { + case 'players': + endpoint = isEdit ? `/api/admin-players/${editId}` : '/api/admin-players'; + successMessage = isEdit ? 'Spieler erfolgreich aktualisiert' : 'Spieler erfolgreich hinzugefügt'; + method = isEdit ? 'PUT' : 'POST'; + break; + case 'locations': + endpoint = isEdit ? `/api/admin-locations/${editId}` : '/api/admin-locations'; + successMessage = isEdit ? 'Standort erfolgreich aktualisiert' : 'Standort erfolgreich hinzugefügt'; + method = isEdit ? 'PUT' : 'POST'; + break; + case 'adminusers': + endpoint = isEdit ? `/api/admin-adminusers/${editId}` : '/api/admin-adminusers'; + successMessage = isEdit ? 'Admin-Benutzer erfolgreich aktualisiert' : 'Admin-Benutzer erfolgreich hinzugefügt'; + method = isEdit ? 'PUT' : 'POST'; + break; + case 'runs': + endpoint = isEdit ? `/api/admin-runs/${editId}` : '/api/admin-runs'; + successMessage = isEdit ? 'Lauf erfolgreich aktualisiert' : 'Lauf erfolgreich hinzugefügt'; + method = isEdit ? 'PUT' : 'POST'; + break; + default: + showError('Unbekannter Datentyp'); + return; + } + + const response = await fetch(endpoint, { + method: method, + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data) + }); + + const result = await response.json(); + + if (result.success) { + showSuccess(successMessage); + closeModal(); + refreshData(); // Daten neu laden + } else { + showError(result.message || `Fehler beim ${isEdit ? 'Aktualisieren' : 'Hinzufügen'}`); + } + + } catch (error) { + console.error('Submit failed:', error); + showError(`Fehler beim ${isEdit ? 'Aktualisieren' : 'Hinzufügen'}`); + } +} + +// Edit-Funktionen +async function editPlayer(id) { + const player = currentData.find(p => p.id == id); + if (!player) return; + + const modal = document.getElementById('addModal'); + const modalTitle = document.getElementById('modalTitle'); + const formFields = document.getElementById('formFields'); + + modalTitle.textContent = 'Spieler bearbeiten'; + formFields.innerHTML = ` +
+ + +
+
+ + +
+
+ + +
+ + `; + + modal.style.display = 'block'; +} + +async function editLocation(id) { + const location = currentData.find(l => l.id == id); + if (!location) return; + + const modal = document.getElementById('addModal'); + const modalTitle = document.getElementById('modalTitle'); + const formFields = document.getElementById('formFields'); + + modalTitle.textContent = 'Standort bearbeiten'; + formFields.innerHTML = ` +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + `; + + modal.style.display = 'block'; +} + +async function editRun(id) { + try { + // Lade die spezifischen Lauf-Daten direkt von der API + const response = await fetch(`/api/admin-runs/${id}`); + const result = await response.json(); + + if (!result.success) { + showError('Fehler beim Laden der Lauf-Daten: ' + (result.message || 'Unbekannter Fehler')); + return; + } + + const run = result.data; + if (!run) { + showError('Lauf nicht gefunden'); + return; + } + + console.log('Editing run:', run); // Debug log + + const modal = document.getElementById('addModal'); + const modalTitle = document.getElementById('modalTitle'); + const formFields = document.getElementById('formFields'); + + modalTitle.textContent = 'Lauf bearbeiten'; + formFields.innerHTML = ` +
+ + +
+
+ + +
+
+ + +
+ + `; + + modal.style.display = 'block'; + + } catch (error) { + console.error('Error loading run data:', error); + showError('Fehler beim Laden der Lauf-Daten'); + } +} + +async function deletePlayer(id) { + if (await confirmDelete(`Spieler mit ID ${id} löschen?`)) { + try { + const response = await fetch(`/api/admin-players/${id}`, { method: 'DELETE' }); + const result = await response.json(); + + if (result.success) { + showSuccess('Spieler erfolgreich gelöscht'); + loadPlayers(); + } else { + showError('Fehler beim Löschen des Spielers'); + } + } catch (error) { + console.error('Delete failed:', error); + showError('Fehler beim Löschen des Spielers'); + } + } +} + +async function deleteRun(id) { + if (await confirmDelete(`Lauf mit ID ${id} löschen?`)) { + try { + const response = await fetch(`/api/admin-runs/${id}`, { method: 'DELETE' }); + const result = await response.json(); + + if (result.success) { + showSuccess('Lauf erfolgreich gelöscht'); + loadRuns(); + } else { + showError('Fehler beim Löschen des Laufs'); + } + } catch (error) { + console.error('Delete failed:', error); + showError('Fehler beim Löschen des Laufs'); + } + } +} + +async function deleteLocation(id) { + if (await confirmDelete(`Standort mit ID ${id} löschen?`)) { + try { + const response = await fetch(`/api/admin-locations/${id}`, { method: 'DELETE' }); + const result = await response.json(); + + if (result.success) { + showSuccess('Standort erfolgreich gelöscht'); + loadLocations(); + } else { + showError('Fehler beim Löschen des Standorts'); + } + } catch (error) { + console.error('Delete failed:', error); + showError('Fehler beim Löschen des Standorts'); + } + } +} + +async function deleteAdminUser(id) { + if (await confirmDelete(`Admin-Benutzer mit ID ${id} löschen?`)) { + try { + const response = await fetch(`/api/admin-adminusers/${id}`, { method: 'DELETE' }); + const result = await response.json(); + + if (result.success) { + showSuccess('Admin-Benutzer erfolgreich gelöscht'); + loadAdminUsers(); + } else { + showError('Fehler beim Löschen des Admin-Benutzers'); + } + } catch (error) { + console.error('Delete failed:', error); + showError('Fehler beim Löschen des Admin-Benutzers'); + } + } +} + +function confirmDelete(message) { + return new Promise((resolve) => { + document.getElementById('confirmMessage').textContent = message; + document.getElementById('confirmModal').style.display = 'block'; + + document.getElementById('confirmYes').onclick = () => { + closeModal(); + resolve(true); + }; + + document.getElementById('confirmNo').onclick = () => { + closeModal(); + resolve(false); + }; + }); +} + +function showSuccess(message) { + const messageDiv = document.getElementById('modalMessage'); + messageDiv.textContent = message; + messageDiv.className = 'message success'; + messageDiv.style.display = 'block'; + setTimeout(() => { + messageDiv.style.display = 'none'; + }, 3000); +} + +function showError(message) { + const messageDiv = document.getElementById('modalMessage'); + messageDiv.textContent = message; + messageDiv.className = 'message error'; + messageDiv.style.display = 'block'; + setTimeout(() => { + messageDiv.style.display = 'none'; + }, 3000); +} diff --git a/public/js/leaderboard.js b/public/js/leaderboard.js new file mode 100644 index 0000000..c5228fc --- /dev/null +++ b/public/js/leaderboard.js @@ -0,0 +1,500 @@ +// Supabase configuration +const SUPABASE_URL = 'https://lfxlplnypzvjrhftaoog.supabase.co'; +const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImxmeGxwbG55cHp2anJoZnRhb29nIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDkyMTQ3NzIsImV4cCI6MjA2NDc5MDc3Mn0.XR4preBqWAQ1rT4PFbpkmRdz57BTwIusBI89fIxDHM8'; + +// Initialize Supabase client +const supabase = window.supabase.createClient(SUPABASE_URL, SUPABASE_ANON_KEY); + +// Initialize Socket.IO connection +const socket = io(); + +// Global variable to store locations with coordinates +let locationsData = []; + +// WebSocket Event Handlers +socket.on('connect', () => { + console.log('🔌 WebSocket verbunden'); +}); + +socket.on('disconnect', () => { + console.log('🔌 WebSocket getrennt'); +}); + +socket.on('newTime', (data) => { + console.log('🏁 Neue Zeit empfangen:', data); + showNotification(data); + // Reload data to show the new time + loadData(); +}); + +// Notification Functions +function showNotification(timeData) { + const notificationBubble = document.getElementById('notificationBubble'); + const notificationTitle = document.getElementById('notificationTitle'); + const notificationSubtitle = document.getElementById('notificationSubtitle'); + + // Format the time data + const playerName = timeData.player_name || 'Unbekannter Spieler'; + const locationName = timeData.location_name || 'Unbekannter Standort'; + const timeString = timeData.recorded_time || '--:--'; + + // Update notification content + notificationTitle.textContent = `🏁 Neue Zeit von ${playerName}!`; + notificationSubtitle.textContent = `${timeString} • ${locationName}`; + + // Show notification + notificationBubble.classList.remove('hide'); + notificationBubble.classList.add('show'); + + // Auto-hide after 5 seconds + setTimeout(() => { + hideNotification(); + }, 5000); +} + +function hideNotification() { + const notificationBubble = document.getElementById('notificationBubble'); + notificationBubble.classList.remove('show'); + notificationBubble.classList.add('hide'); + + // Remove hide class after animation + setTimeout(() => { + notificationBubble.classList.remove('hide'); + }, 300); +} + +// Check authentication status +async function checkAuth() { + try { + const { data: { session } } = await supabase.auth.getSession(); + + if (session) { + // User is logged in, show dashboard button + document.getElementById('adminLoginBtn').style.display = 'none'; + document.getElementById('dashboardBtn').style.display = 'inline-block'; + document.getElementById('logoutBtn').style.display = 'inline-block'; + } else { + // User is not logged in, show admin login button + document.getElementById('adminLoginBtn').style.display = 'inline-block'; + document.getElementById('dashboardBtn').style.display = 'none'; + document.getElementById('logoutBtn').style.display = 'none'; + } + } catch (error) { + console.error('Error checking auth:', error); + // Fallback: show login button if auth check fails + document.getElementById('adminLoginBtn').style.display = 'inline-block'; + document.getElementById('dashboardBtn').style.display = 'none'; + document.getElementById('logoutBtn').style.display = 'none'; + } +} + +// Logout function +async function logout() { + try { + const { error } = await supabase.auth.signOut(); + if (error) { + console.error('Error logging out:', error); + } else { + window.location.reload(); + } + } catch (error) { + console.error('Error during logout:', error); + window.location.reload(); + } +} + +// Load locations from database +async function loadLocations() { + try { + const response = await fetch('/public-api/locations'); + if (!response.ok) { + throw new Error('Failed to fetch locations'); + } + + const responseData = await response.json(); + const locations = responseData.data || responseData; // Handle both formats + const locationSelect = document.getElementById('locationSelect'); + + // Store locations globally for distance calculations + locationsData = locations; + + // Clear existing options except "Alle Standorte" + locationSelect.innerHTML = ''; + + // Add locations from database + locations.forEach(location => { + const option = document.createElement('option'); + option.value = location.name; + option.textContent = `📍 ${location.name}`; + locationSelect.appendChild(option); + }); + + } catch (error) { + console.error('Error loading locations:', error); + } +} + +// Calculate distance between two points using Haversine formula +function calculateDistance(lat1, lon1, lat2, lon2) { + const R = 6371; // Earth's radius in kilometers + const dLat = toRadians(lat2 - lat1); + const dLon = toRadians(lon2 - lon1); + + const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.cos(toRadians(lat1)) * Math.cos(toRadians(lat2)) * + Math.sin(dLon / 2) * Math.sin(dLon / 2); + + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + const distance = R * c; // Distance in kilometers + + return distance; +} + +function toRadians(degrees) { + return degrees * (Math.PI / 180); +} + +// Find nearest location based on user's current position +async function findNearestLocation() { + const btn = document.getElementById('findLocationBtn'); + const locationSelect = document.getElementById('locationSelect'); + + // Check if geolocation is supported + if (!navigator.geolocation) { + showLocationError('Geolocation wird von diesem Browser nicht unterstützt.'); + return; + } + + // Update button state to loading + btn.disabled = true; + btn.classList.add('loading'); + btn.textContent = '🔍 Suche...'; + + try { + // Get user's current position + const position = await new Promise((resolve, reject) => { + navigator.geolocation.getCurrentPosition( + resolve, + reject, + { + enableHighAccuracy: true, + timeout: 10000, + maximumAge: 300000 // 5 minutes + } + ); + }); + + const userLat = position.coords.latitude; + const userLon = position.coords.longitude; + + // Calculate distances to all locations + const locationsWithDistance = locationsData.map(location => ({ + ...location, + distance: calculateDistance( + userLat, + userLon, + parseFloat(location.latitude), + parseFloat(location.longitude) + ) + })); + + // Find the nearest location + const nearestLocation = locationsWithDistance.reduce((nearest, current) => { + return current.distance < nearest.distance ? current : nearest; + }); + + // Select the nearest location in the dropdown + locationSelect.value = nearestLocation.name; + + // Trigger change event to update the leaderboard + locationSelect.dispatchEvent(new Event('change')); + + // Show success notification + showLocationSuccess(nearestLocation.name, nearestLocation.distance); + + } catch (error) { + console.error('Error getting location:', error); + let errorMessage = 'Standort konnte nicht ermittelt werden.'; + + if (error.code) { + switch(error.code) { + case error.PERMISSION_DENIED: + errorMessage = 'Standortzugriff wurde verweigert. Bitte erlaube den Standortzugriff in den Browser-Einstellungen.'; + break; + case error.POSITION_UNAVAILABLE: + errorMessage = 'Standortinformationen sind nicht verfügbar.'; + break; + case error.TIMEOUT: + errorMessage = 'Zeitüberschreitung beim Abrufen des Standorts.'; + break; + } + } + + showLocationError(errorMessage); + } finally { + // Reset button state + btn.disabled = false; + btn.classList.remove('loading'); + btn.textContent = '📍 Mein Standort'; + } +} + +// Show success notification for location finding +function showLocationSuccess(locationName, distance) { + const notificationBubble = document.getElementById('notificationBubble'); + const notificationTitle = document.getElementById('notificationTitle'); + const notificationSubtitle = document.getElementById('notificationSubtitle'); + + // Update notification content + notificationTitle.textContent = `📍 Standort gefunden!`; + notificationSubtitle.textContent = `${locationName} (${distance.toFixed(1)} km entfernt)`; + + // Show notification + notificationBubble.classList.remove('hide'); + notificationBubble.classList.add('show'); + + // Auto-hide after 4 seconds + setTimeout(() => { + hideNotification(); + }, 4000); +} + +// Show error notification for location finding +function showLocationError(message) { + const notificationBubble = document.getElementById('notificationBubble'); + const notificationTitle = document.getElementById('notificationTitle'); + const notificationSubtitle = document.getElementById('notificationSubtitle'); + + // Change notification style to error + notificationBubble.style.background = 'linear-gradient(135deg, #dc3545, #c82333)'; + + // Update notification content + notificationTitle.textContent = '❌ Fehler'; + notificationSubtitle.textContent = message; + + // Show notification + notificationBubble.classList.remove('hide'); + notificationBubble.classList.add('show'); + + // Auto-hide after 6 seconds + setTimeout(() => { + hideNotification(); + // Reset notification style + notificationBubble.style.background = 'linear-gradient(135deg, #00d4ff, #0891b2)'; + }, 6000); +} + +// Load data from local database via MCP +async function loadData() { + try { + const location = document.getElementById('locationSelect').value; + const period = document.querySelector('.time-tab.active').dataset.period; + + // Build query parameters + const params = new URLSearchParams(); + if (location && location !== 'all') { + params.append('location', location); + } + if (period && period !== 'all') { + params.append('period', period); + } + + // Fetch times with player and location data from local database + const response = await fetch(`/public-api/times-with-details?${params.toString()}`); + if (!response.ok) { + throw new Error('Failed to fetch times'); + } + + const times = await response.json(); + + // Convert to the format expected by the leaderboard + const leaderboardData = times.map(time => { + const { minutes, seconds, milliseconds } = time.recorded_time; + const timeString = `${minutes}:${seconds.toString().padStart(2, '0')}.${milliseconds}`; + const playerName = time.player ? + `${time.player.firstname} ${time.player.lastname}` : + 'Unknown Player'; + const locationName = time.location ? time.location.name : 'Unknown Location'; + const date = new Date(time.created_at).toISOString().split('T')[0]; + + return { + name: playerName, + time: timeString, + date: date, + location: locationName + }; + }); + + // Sort by time (fastest first) + leaderboardData.sort((a, b) => { + const timeA = timeToSeconds(a.time); + const timeB = timeToSeconds(b.time); + return timeA - timeB; + }); + + updateLeaderboard(leaderboardData); + updateStats(leaderboardData); + updateCurrentSelection(); + + } catch (error) { + console.error('Error loading data:', error); + // Fallback to sample data if API fails + loadSampleData(); + } +} + +// Fallback sample data based on real database data +function loadSampleData() { + const sampleData = [ + { name: "Carsten Graf", time: "01:28.945", date: "2025-08-30", location: "Ulm Donaubad" }, + { name: "Carsten Graf", time: "01:30.945", date: "2025-08-30", location: "Ulm Donaubad" }, + { name: "Max Mustermann", time: "01:50.945", date: "2025-08-30", location: "Ulm Donaubad" }, + { name: "Carsten Graf", time: "02:50.945", date: "2025-08-31", location: "Test" }, + { name: "Max Mustermann", time: "02:50.945", date: "2025-08-31", location: "Test" }, + { name: "Carsten Graf", time: "01:10.945", date: "2025-09-02", location: "Test" }, + { name: "Carsten Graf", time: "01:11.945", date: "2025-09-02", location: "Test" }, + { name: "Carsten Graf", time: "01:11.945", date: "2025-09-02", location: "Ulm Donaubad" } + ]; + + updateLeaderboard(sampleData); + updateStats(sampleData); + updateCurrentSelection(); +} + +function timeToSeconds(timeStr) { + const [minutes, seconds] = timeStr.split(':'); + return parseFloat(minutes) * 60 + parseFloat(seconds); +} + +function updateStats(data) { + const totalPlayers = new Set(data.map(item => item.name)).size; + const bestTime = data.length > 0 ? data[0].time : '--:--'; + const totalRecords = data.length; + + document.getElementById('totalPlayers').textContent = totalPlayers; + document.getElementById('bestTime').textContent = bestTime; + document.getElementById('totalRecords').textContent = totalRecords; +} + +function updateCurrentSelection() { + const location = document.getElementById('locationSelect').value; + const period = document.querySelector('.time-tab.active').dataset.period; + + // Get the display text from the selected option + const locationSelect = document.getElementById('locationSelect'); + const selectedLocationOption = locationSelect.options[locationSelect.selectedIndex]; + const locationDisplay = selectedLocationOption ? selectedLocationOption.textContent : '🌍 Alle Standorte'; + + const periodIcons = { + 'today': '📅 Heute', + 'week': '📊 Diese Woche', + 'month': '📈 Dieser Monat', + 'all': '♾️ Alle Zeiten' + }; + + document.getElementById('currentSelection').textContent = + `${locationDisplay} • ${periodIcons[period]}`; + + document.getElementById('lastUpdated').textContent = + `Letzter Sync: ${new Date().toLocaleTimeString('de-DE')}`; +} + +function updateLeaderboard(data) { + const rankingList = document.getElementById('rankingList'); + + if (data.length === 0) { + rankingList.innerHTML = ` +
+
🏁
+
Keine Rekorde gefunden
+
+ Für diese Filtereinstellungen liegen noch keine Zeiten vor.
+ Versuche es mit einem anderen Zeitraum oder Standort. +
+
+ `; + return; + } + + rankingList.innerHTML = data.map((player, index) => { + const rank = index + 1; + let positionClass = ''; + let trophy = ''; + + if (rank === 1) { + positionClass = 'gold'; + trophy = '👑'; + } else if (rank === 2) { + positionClass = 'silver'; + trophy = '🥈'; + } else if (rank === 3) { + positionClass = 'bronze'; + trophy = '🥉'; + } else if (rank <= 10) { + trophy = '⭐'; + } + + const formatDate = new Date(player.date).toLocaleDateString('de-DE', { + day: '2-digit', + month: 'short' + }); + + return ` +
+
#${rank}
+
+
${player.name}
+
+ ${player.location} + 🗓️ ${formatDate} +
+
+
${player.time}
+ ${trophy ? `
${trophy}
` : '
'} +
+ `; + }).join(''); +} + +// Event Listeners Setup +function setupEventListeners() { + // Location select event listener + document.getElementById('locationSelect').addEventListener('change', loadData); + + // Time tab event listeners + document.querySelectorAll('.time-tab').forEach(tab => { + tab.addEventListener('click', function() { + // Remove active class from all tabs + document.querySelectorAll('.time-tab').forEach(t => t.classList.remove('active')); + // Add active class to clicked tab + this.classList.add('active'); + // Load data with new period + loadData(); + }); + }); + + // Smooth scroll for better UX + const rankingsList = document.querySelector('.rankings-list'); + if (rankingsList) { + rankingsList.style.scrollBehavior = 'smooth'; + } +} + +// Initialize page +async function init() { + await checkAuth(); + await loadLocations(); + await loadData(); + setupEventListeners(); +} + +// Auto-refresh function +function startAutoRefresh() { + setInterval(loadData, 45000); +} + +// Start the application when DOM is loaded +document.addEventListener('DOMContentLoaded', function() { + init(); + startAutoRefresh(); +}); diff --git a/public/js/login.js b/public/js/login.js new file mode 100644 index 0000000..8ae7c71 --- /dev/null +++ b/public/js/login.js @@ -0,0 +1,187 @@ +// Supabase configuration +const SUPABASE_URL = 'https://lfxlplnypzvjrhftaoog.supabase.co'; +const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImxmeGxwbG55cHp2anJoZnRhb29nIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDkyMTQ3NzIsImV4cCI6MjA2NDc5MDc3Mn0.XR4preBqWAQ1rT4PFbpkmRdz57BTwIusBI89fIxDHM8'; + +// Initialize Supabase client +const supabase = window.supabase.createClient(SUPABASE_URL, SUPABASE_ANON_KEY); + +// Check if user is already logged in +async function checkAuth() { + const { data: { session } } = await supabase.auth.getSession(); + if (session) { + window.location.href = '/'; + } +} + +// Toggle between login and register forms +function toggleForm() { + const loginForm = document.getElementById('loginForm'); + const registerForm = document.getElementById('registerForm'); + + if (loginForm.classList.contains('active')) { + loginForm.classList.remove('active'); + registerForm.classList.add('active'); + } else { + registerForm.classList.remove('active'); + loginForm.classList.add('active'); + } + clearMessage(); + showPasswordReset(false); // Hide password reset when switching forms +} + +// Show message +function showMessage(message, type = 'success') { + const messageDiv = document.getElementById('message'); + messageDiv.innerHTML = `
${message}
`; + setTimeout(() => { + messageDiv.innerHTML = ''; + }, 5000); +} + +// Clear message +function clearMessage() { + document.getElementById('message').innerHTML = ''; +} + +// Show/hide password reset container +function showPasswordReset(show) { + const resetContainer = document.getElementById('passwordResetContainer'); + if (show) { + resetContainer.classList.add('active'); + } else { + resetContainer.classList.remove('active'); + } +} + +// Show/hide loading +function setLoading(show) { + const loading = document.getElementById('loading'); + if (show) { + loading.classList.add('active'); + } else { + loading.classList.remove('active'); + } +} + +// Event Listeners Setup +function setupEventListeners() { + // Handle login + document.getElementById('loginFormElement').addEventListener('submit', async (e) => { + e.preventDefault(); + setLoading(true); + clearMessage(); + showPasswordReset(false); // Hide reset button initially + + const email = document.getElementById('loginEmail').value; + const password = document.getElementById('loginPassword').value; + + try { + const { data, error } = await supabase.auth.signInWithPassword({ + email: email, + password: password + }); + + if (error) { + showMessage(error.message, 'error'); + // Show password reset button on login failure + showPasswordReset(true); + } else { + showMessage('Login successful! Redirecting...', 'success'); + setTimeout(() => { + window.location.href = '/'; + }, 1000); + } + } catch (error) { + showMessage('An unexpected error occurred', 'error'); + showPasswordReset(true); + } finally { + setLoading(false); + } + }); + + // Handle registration + document.getElementById('registerFormElement').addEventListener('submit', async (e) => { + e.preventDefault(); + setLoading(true); + clearMessage(); + + const email = document.getElementById('registerEmail').value; + const password = document.getElementById('registerPassword').value; + const confirmPassword = document.getElementById('confirmPassword').value; + + if (password !== confirmPassword) { + showMessage('Passwords do not match', 'error'); + setLoading(false); + return; + } + + if (password.length < 6) { + showMessage('Password must be at least 6 characters', 'error'); + setLoading(false); + return; + } + + try { + const { data, error } = await supabase.auth.signUp({ + email: email, + password: password + }); + + if (error) { + showMessage(error.message, 'error'); + } else { + if (data.user && !data.user.email_confirmed_at) { + showMessage('Registration successful! Please check your email to confirm your account.', 'success'); + } else { + showMessage('Registration successful! Redirecting...', 'success'); + setTimeout(() => { + window.location.href = '/'; + }, 1000); + } + } + } catch (error) { + showMessage('An unexpected error occurred', 'error'); + } finally { + setLoading(false); + } + }); + + // Handle password reset + document.getElementById('resetPasswordBtn').addEventListener('click', async () => { + const email = document.getElementById('loginEmail').value; + + if (!email) { + showMessage('Bitte geben Sie zuerst Ihre E-Mail-Adresse ein', 'error'); + return; + } + + setLoading(true); + clearMessage(); + + try { + const { error } = await supabase.auth.resetPasswordForEmail(email, { + redirectTo: `${window.location.origin}/reset-password.html` + }); + + if (error) { + showMessage('Fehler beim Senden der E-Mail: ' + error.message, 'error'); + } else { + showMessage('Passwort-Reset-E-Mail wurde gesendet! Bitte überprüfen Sie Ihr E-Mail-Postfach.', 'success'); + showPasswordReset(false); + } + } catch (error) { + showMessage('Ein unerwarteter Fehler ist aufgetreten', 'error'); + } finally { + setLoading(false); + } + }); +} + +// Initialize page +async function init() { + await checkAuth(); + setupEventListeners(); +} + +// Start the application when DOM is loaded +document.addEventListener('DOMContentLoaded', init); diff --git a/public/login.html b/public/login.html index 1585a1a..97d50b0 100644 --- a/public/login.html +++ b/public/login.html @@ -5,316 +5,7 @@ Ninja Server - Admin Login - + @@ -382,185 +73,7 @@ - + + - + \ No newline at end of file diff --git a/routes/api.js b/routes/api.js index 1b64ec4..4cfa411 100644 --- a/routes/api.js +++ b/routes/api.js @@ -1455,9 +1455,50 @@ router.get('/admin-runs', requireAdminAuth, async (req, res) => { }); } catch (error) { console.error('Error loading runs:', error); - res.status(500).json({ - success: false, - message: 'Fehler beim Laden der Läufe' + res.status(500).json({ + success: false, + message: 'Fehler beim Laden der Läufe' + }); + } +}); + +// GET einzelner Lauf +router.get('/admin-runs/:id', requireAdminAuth, async (req, res) => { + try { + const { id } = req.params; + + const result = await pool.query(` + SELECT + t.id, + t.player_id, + t.location_id, + t.recorded_time, + EXTRACT(EPOCH FROM t.recorded_time) as time_seconds, + t.created_at, + COALESCE(CONCAT(p.firstname, ' ', p.lastname), p.firstname, p.lastname) as player_name, + l.name as location_name + FROM times t + LEFT JOIN players p ON t.player_id = p.id + LEFT JOIN locations l ON t.location_id = l.id + WHERE t.id = $1 + `, [id]); + + if (result.rows.length === 0) { + return res.status(404).json({ + success: false, + message: 'Lauf nicht gefunden' + }); + } + + res.json({ + success: true, + data: result.rows[0] + }); + } catch (error) { + console.error('Error loading run:', error); + res.status(500).json({ + success: false, + message: 'Fehler beim Laden des Laufs' }); } }); @@ -1581,4 +1622,296 @@ router.delete('/admin-adminusers/:id', requireAdminAuth, async (req, res) => { } }); +// ============================================================================ +// POST/PUT ROUTES FÜR CRUD-OPERATIONEN +// ============================================================================ + +// Admin Spieler - POST (Hinzufügen) +router.post('/admin-players', requireAdminAuth, async (req, res) => { + try { + const { full_name, rfiduid, supabase_user_id } = req.body; + + // Name in firstname und lastname aufteilen + const nameParts = full_name ? full_name.trim().split(' ') : []; + const firstname = nameParts[0] || ''; + const lastname = nameParts.slice(1).join(' ') || ''; + + const result = await pool.query( + `INSERT INTO players (firstname, lastname, rfiduid, supabase_user_id, created_at) + VALUES ($1, $2, $3, $4, NOW()) + RETURNING *`, + [firstname, lastname, rfiduid || null, supabase_user_id || null] + ); + + res.json({ + success: true, + message: 'Spieler erfolgreich hinzugefügt', + data: result.rows[0] + }); + } catch (error) { + console.error('Error creating player:', error); + res.status(500).json({ + success: false, + message: 'Fehler beim Hinzufügen des Spielers' + }); + } +}); + +// Admin Spieler - PUT (Bearbeiten) +router.put('/admin-players/:id', requireAdminAuth, async (req, res) => { + try { + const playerId = req.params.id; + const { full_name, rfiduid, supabase_user_id } = req.body; + + // Name in firstname und lastname aufteilen + const nameParts = full_name ? full_name.trim().split(' ') : []; + const firstname = nameParts[0] || ''; + const lastname = nameParts.slice(1).join(' ') || ''; + + const result = await pool.query( + `UPDATE players + SET firstname = $1, lastname = $2, rfiduid = $3, supabase_user_id = $4 + WHERE id = $5 + RETURNING *`, + [firstname, lastname, rfiduid || null, supabase_user_id || null, playerId] + ); + + if (result.rowCount > 0) { + res.json({ + success: true, + message: 'Spieler erfolgreich aktualisiert', + data: result.rows[0] + }); + } else { + res.status(404).json({ success: false, message: 'Spieler nicht gefunden' }); + } + } catch (error) { + console.error('Error updating player:', error); + res.status(500).json({ + success: false, + message: 'Fehler beim Aktualisieren des Spielers' + }); + } +}); + +// Admin Standorte - POST (Hinzufügen) +router.post('/admin-locations', requireAdminAuth, async (req, res) => { + try { + const { name, latitude, longitude, time_threshold } = req.body; + + const result = await pool.query( + `INSERT INTO locations (name, latitude, longitude, time_threshold, created_at) + VALUES ($1, $2, $3, $4, NOW()) + RETURNING *`, + [name, latitude, longitude, time_threshold || null] + ); + + res.json({ + success: true, + message: 'Standort erfolgreich hinzugefügt', + data: result.rows[0] + }); + } catch (error) { + console.error('Error creating location:', error); + res.status(500).json({ + success: false, + message: 'Fehler beim Hinzufügen des Standorts' + }); + } +}); + +// Admin Standorte - PUT (Bearbeiten) +router.put('/admin-locations/:id', requireAdminAuth, async (req, res) => { + try { + const locationId = req.params.id; + const { name, latitude, longitude, time_threshold } = req.body; + + const result = await pool.query( + `UPDATE locations + SET name = $1, latitude = $2, longitude = $3, time_threshold = $4 + WHERE id = $5 + RETURNING *`, + [name, latitude, longitude, time_threshold || null, locationId] + ); + + if (result.rowCount > 0) { + res.json({ + success: true, + message: 'Standort erfolgreich aktualisiert', + data: result.rows[0] + }); + } else { + res.status(404).json({ success: false, message: 'Standort nicht gefunden' }); + } + } catch (error) { + console.error('Error updating location:', error); + res.status(500).json({ + success: false, + message: 'Fehler beim Aktualisieren des Standorts' + }); + } +}); + +// Admin Läufe - POST (Hinzufügen) +router.post('/admin-runs', requireAdminAuth, async (req, res) => { + try { + const { player_id, location_id, time_seconds } = req.body; + + // Zeit in INTERVAL konvertieren + const timeInterval = `${time_seconds} seconds`; + + const result = await pool.query( + `INSERT INTO times (player_id, location_id, recorded_time, created_at) + VALUES ($1, $2, $3, NOW()) + RETURNING *`, + [player_id, location_id, timeInterval] + ); + + res.json({ + success: true, + message: 'Lauf erfolgreich hinzugefügt', + data: result.rows[0] + }); + } catch (error) { + console.error('Error creating run:', error); + res.status(500).json({ + success: false, + message: 'Fehler beim Hinzufügen des Laufs' + }); + } +}); + +// Admin Läufe - PUT (Bearbeiten) +router.put('/admin-runs/:id', requireAdminAuth, async (req, res) => { + try { + const runId = req.params.id; + const { player_id, location_id, time_seconds } = req.body; + + // Zeit in INTERVAL konvertieren + const timeInterval = `${time_seconds} seconds`; + + const result = await pool.query( + `UPDATE times + SET player_id = $1, location_id = $2, recorded_time = $3 + WHERE id = $4 + RETURNING *`, + [player_id, location_id, timeInterval, runId] + ); + + if (result.rowCount > 0) { + res.json({ + success: true, + message: 'Lauf erfolgreich aktualisiert', + data: result.rows[0] + }); + } else { + res.status(404).json({ success: false, message: 'Lauf nicht gefunden' }); + } + } catch (error) { + console.error('Error updating run:', error); + res.status(500).json({ + success: false, + message: 'Fehler beim Aktualisieren des Laufs' + }); + } +}); + +// Admin-Benutzer - POST (Hinzufügen) +router.post('/admin-adminusers', requireAdminAuth, async (req, res) => { + try { + const { username, password, access_level } = req.body; + + // Passwort hashen + const saltRounds = 10; + const hashedPassword = await bcrypt.hash(password, saltRounds); + + const result = await pool.query( + `INSERT INTO adminusers (username, password_hash, access_level, is_active, created_at) + VALUES ($1, $2, $3, true, NOW()) + RETURNING id, username, access_level, is_active, created_at`, + [username, hashedPassword, access_level] + ); + + res.json({ + success: true, + message: 'Admin-Benutzer erfolgreich hinzugefügt', + data: result.rows[0] + }); + } catch (error) { + console.error('Error creating admin user:', error); + if (error.code === '23505') { // Unique constraint violation + res.status(400).json({ + success: false, + message: 'Benutzername bereits vergeben' + }); + } else { + res.status(500).json({ + success: false, + message: 'Fehler beim Hinzufügen des Admin-Benutzers' + }); + } + } +}); + +// Admin-Benutzer - PUT (Bearbeiten) +router.put('/admin-adminusers/:id', requireAdminAuth, async (req, res) => { + try { + const userId = req.params.id; + const { username, password, access_level } = req.body; + + // Verhindern, dass sich selbst bearbeitet + if (parseInt(userId) === req.session.userId) { + return res.status(400).json({ + success: false, + message: 'Sie können sich nicht selbst bearbeiten' + }); + } + + let query, params; + + if (password) { + // Passwort hashen + const saltRounds = 10; + const hashedPassword = await bcrypt.hash(password, saltRounds); + + query = `UPDATE adminusers + SET username = $1, password_hash = $2, access_level = $3 + WHERE id = $4 + RETURNING id, username, access_level, is_active, created_at`; + params = [username, hashedPassword, access_level, userId]; + } else { + query = `UPDATE adminusers + SET username = $1, access_level = $2 + WHERE id = $3 + RETURNING id, username, access_level, is_active, created_at`; + params = [username, access_level, userId]; + } + + const result = await pool.query(query, params); + + if (result.rowCount > 0) { + res.json({ + success: true, + message: 'Admin-Benutzer erfolgreich aktualisiert', + data: result.rows[0] + }); + } else { + res.status(404).json({ success: false, message: 'Admin-Benutzer nicht gefunden' }); + } + } catch (error) { + console.error('Error updating admin user:', error); + if (error.code === '23505') { // Unique constraint violation + res.status(400).json({ + success: false, + message: 'Benutzername bereits vergeben' + }); + } else { + res.status(500).json({ + success: false, + message: 'Fehler beim Aktualisieren des Admin-Benutzers' + }); + } + } +}); + module.exports = { router, requireApiKey }; diff --git a/server.js b/server.js index c72f623..fbfb22c 100644 --- a/server.js +++ b/server.js @@ -169,7 +169,10 @@ app.get('/adminlogin.html', (req, res) => { // STATIC FILE SERVING // ============================================================================ -// Serve static files for public pages (CSS, JS, images) +// Serve static files directly from public directory +app.use(express.static('public')); + +// Serve static files for public pages (CSS, JS, images) - legacy route app.use('/public', express.static('public')); // Serve static files for login page