Update
This commit is contained in:
1768
public/js/admin-dashboard.js
Normal file
1768
public/js/admin-dashboard.js
Normal file
File diff suppressed because it is too large
Load Diff
91
public/js/adminlogin.js
Normal file
91
public/js/adminlogin.js
Normal file
@@ -0,0 +1,91 @@
|
||||
function showMessage(elementId, message, isError = false) {
|
||||
const messageDiv = document.getElementById(elementId);
|
||||
messageDiv.textContent = message;
|
||||
messageDiv.classList.add("show");
|
||||
setTimeout(() => {
|
||||
messageDiv.classList.remove("show");
|
||||
}, 4000);
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
showMessage("error", message, true);
|
||||
}
|
||||
|
||||
function showSuccess(message) {
|
||||
showMessage("success", message, false);
|
||||
}
|
||||
|
||||
function setLoading(isLoading) {
|
||||
const btnText = document.getElementById("btn-text");
|
||||
const btn = document.getElementById("loginBtn");
|
||||
|
||||
if (isLoading) {
|
||||
btnText.innerHTML = '<span class="loading"></span>Anmelde...';
|
||||
btn.disabled = true;
|
||||
} else {
|
||||
btnText.textContent = 'Anmelden';
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleLogin(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const username = document.getElementById('username').value.trim();
|
||||
const password = document.getElementById('password').value;
|
||||
|
||||
if (!username || !password) {
|
||||
showError('Bitte füllen Sie alle Felder aus.');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/v1/public/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ username, password })
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showSuccess('✅ Anmeldung erfolgreich! Weiterleitung...');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/admin-dashboard';
|
||||
}, 1000);
|
||||
} else {
|
||||
showError(result.message || 'Anmeldung fehlgeschlagen');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler bei der Anmeldung:', error);
|
||||
showError('Verbindungsfehler. Bitte versuchen Sie es erneut.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Enter-Taste für Login
|
||||
document.addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
handleLogin(e);
|
||||
}
|
||||
});
|
||||
|
||||
// Fokus auf erstes Eingabefeld
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.getElementById('username').focus();
|
||||
|
||||
// Add cookie settings button functionality
|
||||
const cookieSettingsBtn = document.getElementById('cookie-settings-footer');
|
||||
if (cookieSettingsBtn) {
|
||||
cookieSettingsBtn.addEventListener('click', function() {
|
||||
if (window.cookieConsent) {
|
||||
window.cookieConsent.resetConsent();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
601
public/js/cookie-consent.js
Normal file
601
public/js/cookie-consent.js
Normal file
@@ -0,0 +1,601 @@
|
||||
// Cookie Consent Management
|
||||
class CookieConsent {
|
||||
constructor() {
|
||||
this.cookieName = 'ninjacross_cookie_consent';
|
||||
this.cookieSettingsName = 'ninjacross_cookie_settings';
|
||||
this.consentGiven = false;
|
||||
this.settings = {
|
||||
necessary: true, // Always true, can't be disabled
|
||||
functional: false,
|
||||
analytics: false
|
||||
};
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// Check if consent was already given
|
||||
const savedConsent = this.getCookie(this.cookieName);
|
||||
const savedSettings = this.getCookie(this.cookieSettingsName);
|
||||
|
||||
if (savedConsent === 'true') {
|
||||
this.consentGiven = true;
|
||||
if (savedSettings) {
|
||||
this.settings = { ...this.settings, ...JSON.parse(savedSettings) };
|
||||
}
|
||||
this.applySettings();
|
||||
} else {
|
||||
this.showConsentBanner();
|
||||
}
|
||||
}
|
||||
|
||||
showConsentBanner() {
|
||||
// Don't show banner if already shown
|
||||
if (document.getElementById('cookie-consent-banner')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const banner = document.createElement('div');
|
||||
banner.id = 'cookie-consent-banner';
|
||||
banner.innerHTML = `
|
||||
<div class="cookie-banner">
|
||||
<div class="cookie-content">
|
||||
<div class="cookie-icon">🍪</div>
|
||||
<div class="cookie-text">
|
||||
<h3>Cookie-Einstellungen</h3>
|
||||
<p>Wir verwenden Cookies, um Ihnen die beste Erfahrung auf unserer Website zu bieten. Einige sind notwendig, andere helfen uns, die Website zu verbessern.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cookie-actions">
|
||||
<button id="cookie-settings-btn" class="btn-cookie-settings">Einstellungen</button>
|
||||
<button id="cookie-accept-all" class="btn-cookie-accept">Alle akzeptieren</button>
|
||||
<button id="cookie-accept-necessary" class="btn-cookie-necessary">Nur notwendige</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Add styles
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
#cookie-consent-banner {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
|
||||
color: white;
|
||||
z-index: 10000;
|
||||
box-shadow: 0 -4px 20px rgba(0,0,0,0.3);
|
||||
animation: slideUp 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from { transform: translateY(100%); }
|
||||
to { transform: translateY(0); }
|
||||
}
|
||||
|
||||
.cookie-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.cookie-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.cookie-icon {
|
||||
font-size: 2rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.cookie-text h3 {
|
||||
margin: 0 0 5px 0;
|
||||
font-size: 1.1rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.cookie-text p {
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
opacity: 0.9;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.cookie-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.btn-cookie-settings,
|
||||
.btn-cookie-accept,
|
||||
.btn-cookie-necessary {
|
||||
padding: 10px 16px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-cookie-settings {
|
||||
background: rgba(255,255,255,0.2);
|
||||
color: white;
|
||||
border: 1px solid rgba(255,255,255,0.3);
|
||||
}
|
||||
|
||||
.btn-cookie-settings:hover {
|
||||
background: rgba(255,255,255,0.3);
|
||||
}
|
||||
|
||||
.btn-cookie-accept {
|
||||
background: #10b981;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-cookie-accept:hover {
|
||||
background: #059669;
|
||||
}
|
||||
|
||||
.btn-cookie-necessary {
|
||||
background: #6b7280;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-cookie-necessary:hover {
|
||||
background: #4b5563;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.cookie-banner {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.cookie-content {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.cookie-actions {
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
document.head.appendChild(style);
|
||||
document.body.appendChild(banner);
|
||||
|
||||
// Add event listeners
|
||||
document.getElementById('cookie-accept-all').addEventListener('click', () => {
|
||||
this.acceptAll();
|
||||
});
|
||||
|
||||
document.getElementById('cookie-accept-necessary').addEventListener('click', () => {
|
||||
this.acceptNecessary();
|
||||
});
|
||||
|
||||
document.getElementById('cookie-settings-btn').addEventListener('click', () => {
|
||||
this.showSettingsModal();
|
||||
});
|
||||
}
|
||||
|
||||
showSettingsModal() {
|
||||
// Remove banner
|
||||
const banner = document.getElementById('cookie-consent-banner');
|
||||
if (banner) {
|
||||
banner.remove();
|
||||
}
|
||||
|
||||
// Create modal
|
||||
const modal = document.createElement('div');
|
||||
modal.id = 'cookie-settings-modal';
|
||||
modal.innerHTML = `
|
||||
<div class="cookie-modal-overlay">
|
||||
<div class="cookie-modal">
|
||||
<div class="cookie-modal-header">
|
||||
<h2>🍪 Cookie-Einstellungen</h2>
|
||||
<button id="cookie-modal-close" class="btn-close">×</button>
|
||||
</div>
|
||||
<div class="cookie-modal-content">
|
||||
<p>Wählen Sie aus, welche Cookies Sie zulassen möchten:</p>
|
||||
|
||||
<div class="cookie-category">
|
||||
<div class="cookie-category-header">
|
||||
<h3>Notwendige Cookies</h3>
|
||||
<label class="cookie-toggle">
|
||||
<input type="checkbox" id="necessary-cookies" checked disabled>
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<p>Diese Cookies sind für die Grundfunktionen der Website erforderlich und können nicht deaktiviert werden.</p>
|
||||
</div>
|
||||
|
||||
<div class="cookie-category">
|
||||
<div class="cookie-category-header">
|
||||
<h3>Funktionale Cookies</h3>
|
||||
<label class="cookie-toggle">
|
||||
<input type="checkbox" id="functional-cookies" ${this.settings.functional ? 'checked' : ''}>
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<p>Diese Cookies ermöglichen erweiterte Funktionen wie Benutzeranmeldung und Einstellungen.</p>
|
||||
</div>
|
||||
|
||||
<div class="cookie-category">
|
||||
<div class="cookie-category-header">
|
||||
<h3>Analyse-Cookies</h3>
|
||||
<label class="cookie-toggle">
|
||||
<input type="checkbox" id="analytics-cookies" ${this.settings.analytics ? 'checked' : ''}>
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<p>Diese Cookies helfen uns zu verstehen, wie Besucher mit der Website interagieren.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cookie-modal-footer">
|
||||
<button id="cookie-save-settings" class="btn-save">Einstellungen speichern</button>
|
||||
<button id="cookie-accept-all-modal" class="btn-accept-all">Alle akzeptieren</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Add modal styles
|
||||
const modalStyle = document.createElement('style');
|
||||
modalStyle.textContent = `
|
||||
.cookie-modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0,0,0,0.5);
|
||||
z-index: 10001;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.cookie-modal {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 20px 40px rgba(0,0,0,0.3);
|
||||
animation: modalSlideIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes modalSlideIn {
|
||||
from { transform: scale(0.9); opacity: 0; }
|
||||
to { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
|
||||
.cookie-modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.cookie-modal-header h2 {
|
||||
margin: 0;
|
||||
color: #1e3c72;
|
||||
}
|
||||
|
||||
.btn-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
color: #6b7280;
|
||||
padding: 0;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn-close:hover {
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.cookie-modal-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.cookie-category {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.cookie-category-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.cookie-category-header h3 {
|
||||
margin: 0;
|
||||
color: #1e3c72;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.cookie-category p {
|
||||
margin: 0;
|
||||
color: #6b7280;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.cookie-toggle {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 50px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.cookie-toggle input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.toggle-slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #ccc;
|
||||
transition: .4s;
|
||||
border-radius: 24px;
|
||||
}
|
||||
|
||||
.toggle-slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
left: 3px;
|
||||
bottom: 3px;
|
||||
background-color: white;
|
||||
transition: .4s;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.cookie-toggle input:checked + .toggle-slider {
|
||||
background-color: #1e3c72;
|
||||
}
|
||||
|
||||
.cookie-toggle input:checked + .toggle-slider:before {
|
||||
transform: translateX(26px);
|
||||
}
|
||||
|
||||
.cookie-toggle input:disabled + .toggle-slider {
|
||||
background-color: #10b981;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.cookie-modal-footer {
|
||||
padding: 20px;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.btn-save, .btn-accept-all {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-save {
|
||||
background: #1e3c72;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-save:hover {
|
||||
background: #2a5298;
|
||||
}
|
||||
|
||||
.btn-accept-all {
|
||||
background: #10b981;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-accept-all:hover {
|
||||
background: #059669;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.cookie-modal {
|
||||
margin: 10px;
|
||||
max-height: 90vh;
|
||||
}
|
||||
|
||||
.cookie-modal-footer {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.btn-save, .btn-accept-all {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
document.head.appendChild(modalStyle);
|
||||
document.body.appendChild(modal);
|
||||
|
||||
// Add event listeners
|
||||
document.getElementById('cookie-modal-close').addEventListener('click', () => {
|
||||
modal.remove();
|
||||
this.showConsentBanner();
|
||||
});
|
||||
|
||||
document.getElementById('cookie-save-settings').addEventListener('click', () => {
|
||||
this.saveSettings();
|
||||
modal.remove();
|
||||
});
|
||||
|
||||
document.getElementById('cookie-accept-all-modal').addEventListener('click', () => {
|
||||
this.acceptAll();
|
||||
modal.remove();
|
||||
});
|
||||
|
||||
// Close on overlay click
|
||||
modal.querySelector('.cookie-modal-overlay').addEventListener('click', (e) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
modal.remove();
|
||||
this.showConsentBanner();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
saveSettings() {
|
||||
this.settings.functional = document.getElementById('functional-cookies').checked;
|
||||
this.settings.analytics = document.getElementById('analytics-cookies').checked;
|
||||
|
||||
this.setCookie(this.cookieName, 'true', 365);
|
||||
this.setCookie(this.cookieSettingsName, JSON.stringify(this.settings), 365);
|
||||
|
||||
this.consentGiven = true;
|
||||
this.applySettings();
|
||||
}
|
||||
|
||||
acceptAll() {
|
||||
this.settings.functional = true;
|
||||
this.settings.analytics = true;
|
||||
|
||||
this.setCookie(this.cookieName, 'true', 365);
|
||||
this.setCookie(this.cookieSettingsName, JSON.stringify(this.settings), 365);
|
||||
|
||||
this.consentGiven = true;
|
||||
this.applySettings();
|
||||
}
|
||||
|
||||
acceptNecessary() {
|
||||
this.settings.functional = false;
|
||||
this.settings.analytics = false;
|
||||
|
||||
this.setCookie(this.cookieName, 'true', 365);
|
||||
this.setCookie(this.cookieSettingsName, JSON.stringify(this.settings), 365);
|
||||
|
||||
this.consentGiven = true;
|
||||
this.applySettings();
|
||||
}
|
||||
|
||||
applySettings() {
|
||||
// Remove banner if exists
|
||||
const banner = document.getElementById('cookie-consent-banner');
|
||||
if (banner) {
|
||||
banner.remove();
|
||||
}
|
||||
|
||||
// Apply functional cookies
|
||||
if (this.settings.functional) {
|
||||
// Enable functional features
|
||||
console.log('Functional cookies enabled');
|
||||
} else {
|
||||
// Disable functional features
|
||||
console.log('Functional cookies disabled');
|
||||
}
|
||||
|
||||
// Apply analytics cookies
|
||||
if (this.settings.analytics) {
|
||||
// Enable analytics
|
||||
console.log('Analytics cookies enabled');
|
||||
this.enableAnalytics();
|
||||
} else {
|
||||
// Disable analytics
|
||||
console.log('Analytics cookies disabled');
|
||||
this.disableAnalytics();
|
||||
}
|
||||
}
|
||||
|
||||
enableAnalytics() {
|
||||
// Enable page tracking
|
||||
if (typeof trackPageView === 'function') {
|
||||
trackPageView('main_page_visit');
|
||||
}
|
||||
}
|
||||
|
||||
disableAnalytics() {
|
||||
// Disable page tracking
|
||||
console.log('Analytics disabled by user choice');
|
||||
}
|
||||
|
||||
setCookie(name, value, days) {
|
||||
const expires = new Date();
|
||||
expires.setTime(expires.getTime() + (days * 24 * 60 * 60 * 1000));
|
||||
document.cookie = `${name}=${value};expires=${expires.toUTCString()};path=/;SameSite=Lax`;
|
||||
}
|
||||
|
||||
getCookie(name) {
|
||||
const nameEQ = name + "=";
|
||||
const ca = document.cookie.split(';');
|
||||
for (let i = 0; i < ca.length; i++) {
|
||||
let c = ca[i];
|
||||
while (c.charAt(0) === ' ') c = c.substring(1, c.length);
|
||||
if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Public method to check if consent was given
|
||||
hasConsent() {
|
||||
return this.consentGiven;
|
||||
}
|
||||
|
||||
// Public method to get current settings
|
||||
getSettings() {
|
||||
return { ...this.settings };
|
||||
}
|
||||
|
||||
// Public method to reset consent
|
||||
resetConsent() {
|
||||
this.setCookie(this.cookieName, '', -1);
|
||||
this.setCookie(this.cookieSettingsName, '', -1);
|
||||
this.consentGiven = false;
|
||||
this.settings = {
|
||||
necessary: true,
|
||||
functional: false,
|
||||
analytics: false
|
||||
};
|
||||
this.showConsentBanner();
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize cookie consent when DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
window.cookieConsent = new CookieConsent();
|
||||
});
|
||||
|
||||
// Export for use in other scripts
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = CookieConsent;
|
||||
}
|
||||
105
public/js/cookie-utils.js
Normal file
105
public/js/cookie-utils.js
Normal file
@@ -0,0 +1,105 @@
|
||||
// Cookie Utility Functions
|
||||
class CookieManager {
|
||||
// Set a cookie
|
||||
static setCookie(name, value, days = 30) {
|
||||
const expires = new Date();
|
||||
expires.setTime(expires.getTime() + (days * 24 * 60 * 60 * 1000));
|
||||
document.cookie = `${name}=${value};expires=${expires.toUTCString()};path=/;SameSite=Lax`;
|
||||
}
|
||||
|
||||
// Get a cookie
|
||||
static getCookie(name) {
|
||||
const nameEQ = name + "=";
|
||||
const ca = document.cookie.split(';');
|
||||
for (let i = 0; i < ca.length; i++) {
|
||||
let c = ca[i];
|
||||
while (c.charAt(0) === ' ') c = c.substring(1, c.length);
|
||||
if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Delete a cookie
|
||||
static deleteCookie(name) {
|
||||
document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 UTC;path=/;`;
|
||||
}
|
||||
|
||||
// Check if cookies are enabled
|
||||
static areCookiesEnabled() {
|
||||
try {
|
||||
this.setCookie('test', 'test');
|
||||
const enabled = this.getCookie('test') === 'test';
|
||||
this.deleteCookie('test');
|
||||
return enabled;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Location-specific cookie functions
|
||||
class LocationCookieManager {
|
||||
static COOKIE_NAME = 'ninjacross_last_location';
|
||||
static COOKIE_EXPIRY_DAYS = 90; // 3 months
|
||||
|
||||
// Save last selected location
|
||||
static saveLastLocation(locationId, locationName) {
|
||||
if (!locationId || !locationName) return;
|
||||
|
||||
const locationData = {
|
||||
id: locationId,
|
||||
name: locationName,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
try {
|
||||
CookieManager.setCookie(
|
||||
this.COOKIE_NAME,
|
||||
JSON.stringify(locationData),
|
||||
this.COOKIE_EXPIRY_DAYS
|
||||
);
|
||||
console.log('✅ Location saved to cookie:', locationName);
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to save location to cookie:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Get last selected location
|
||||
static getLastLocation() {
|
||||
try {
|
||||
const cookieValue = CookieManager.getCookie(this.COOKIE_NAME);
|
||||
if (!cookieValue) return null;
|
||||
|
||||
const locationData = JSON.parse(cookieValue);
|
||||
|
||||
// Check if cookie is not too old (optional: 30 days max)
|
||||
const cookieDate = new Date(locationData.timestamp);
|
||||
const maxAge = 30 * 24 * 60 * 60 * 1000; // 30 days in milliseconds
|
||||
if (Date.now() - cookieDate.getTime() > maxAge) {
|
||||
this.clearLastLocation();
|
||||
return null;
|
||||
}
|
||||
|
||||
return locationData;
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to parse location cookie:', error);
|
||||
this.clearLastLocation();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Clear last location
|
||||
static clearLastLocation() {
|
||||
CookieManager.deleteCookie(this.COOKIE_NAME);
|
||||
console.log('🗑️ Location cookie cleared');
|
||||
}
|
||||
|
||||
// Check if location cookie exists
|
||||
static hasLastLocation() {
|
||||
return this.getLastLocation() !== null;
|
||||
}
|
||||
}
|
||||
|
||||
// Export for use in other scripts
|
||||
window.CookieManager = CookieManager;
|
||||
window.LocationCookieManager = LocationCookieManager;
|
||||
2163
public/js/dashboard.js
Normal file
2163
public/js/dashboard.js
Normal file
File diff suppressed because it is too large
Load Diff
573
public/js/generator.js
Normal file
573
public/js/generator.js
Normal file
@@ -0,0 +1,573 @@
|
||||
// Toggle Token-Felder basierend auf Lizenzstufe
|
||||
function toggleTokenFields() {
|
||||
const tierInput = document.getElementById("tier");
|
||||
const dbConfig = document.getElementById("dbConfig");
|
||||
const tier = parseInt(tierInput.value);
|
||||
|
||||
if (tier >= 3 && !isNaN(tier)) {
|
||||
dbConfig.innerHTML = `
|
||||
<h3>🗄️ Token-Informationen (für Stufe 3+)</h3>
|
||||
<div class="form-group">
|
||||
<label for="description">Token Beschreibung</label>
|
||||
<textarea id="description" placeholder="z.B. API-Zugang für Standort München"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="standorte">Standorte</label>
|
||||
<input type="text" id="standorte" placeholder="z.B. München, Berlin">
|
||||
</div>
|
||||
|
||||
<!-- Neue Standortsuche-Sektion -->
|
||||
<div class="form-group">
|
||||
<label for="locationSearch">Standort suchen & auf Karte anzeigen</label>
|
||||
<div class="location-search-container">
|
||||
<input type="text" id="locationSearch" placeholder="z.B. München, Marienplatz">
|
||||
<button onclick="searchLocation(this)" style="padding: 15px 20px; background: #4caf50; color: white; border: none; border-radius: 12px; cursor: pointer; font-weight: 500; transition: all 0.3s ease;">🔍 Suchen</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Koordinaten-Anzeige -->
|
||||
<div id="coordinates" class="coordinates-display" style="display: none;">
|
||||
<div style="background: #f0f8ff; border: 2px solid #4caf50; border-radius: 8px; padding: 15px; margin-top: 15px;">
|
||||
<h4 style="margin: 0 0 10px 0; color: #2e7d32;">📍 Gefundene Koordinaten:</h4>
|
||||
<div style="display: flex; gap: 20px; flex-wrap: wrap;">
|
||||
<div>
|
||||
<strong>Breitengrad (LAT):</strong>
|
||||
<span id="latitude" style="font-family: monospace; color: #1565c0;"></span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Längengrad (LON):</strong>
|
||||
<span id="longitude" style="font-family: monospace; color: #1565c0;"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 15px; text-align: center;">
|
||||
<div style="font-size: 0.85em; color: #666; margin-bottom: 10px;">
|
||||
💡 Der Standort wird automatisch beim Generieren der Lizenz gespeichert
|
||||
</div>
|
||||
<button id="saveLocationBtn" onclick="saveLocationToDatabase()" style="padding: 12px 24px; background: #2196f3; color: white; border: none; border-radius: 8px; cursor: pointer; font-weight: 500; transition: all 0.3s ease;">
|
||||
💾 Standort manuell speichern
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Karten-Container -->
|
||||
<div id="mapContainer" class="map-container" style="display: none; margin-top: 20px;">
|
||||
<h4 style="margin: 0 0 15px 0; color: #333;">🗺️ Standort auf der Karte:</h4>
|
||||
<div id="mapFrame" style="width: 100%; height: 300px; border: 2px solid #ddd; border-radius: 12px; overflow: hidden;"></div>
|
||||
</div>
|
||||
<div class="info-text" style="margin-top: 10px; font-size: 0.8em;">
|
||||
📝 Standorte werden in der lokalen PostgreSQL-Datenbank gespeichert
|
||||
</div>
|
||||
`;
|
||||
dbConfig.classList.add("show");
|
||||
} else {
|
||||
dbConfig.classList.remove("show");
|
||||
setTimeout(() => {
|
||||
if (!dbConfig.classList.contains("show")) {
|
||||
dbConfig.innerHTML = "";
|
||||
}
|
||||
}, 400);
|
||||
}
|
||||
}
|
||||
|
||||
const secret = "542ff224606c61fb3024e22f76ef9ac8";
|
||||
|
||||
function isValidMac(mac) {
|
||||
const pattern = /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$|^[0-9A-Fa-f]{12}$/;
|
||||
return pattern.test(mac);
|
||||
}
|
||||
|
||||
function showMessage(elementId, message, isError = false) {
|
||||
const messageDiv = document.getElementById(elementId);
|
||||
messageDiv.textContent = message;
|
||||
messageDiv.classList.add("show");
|
||||
setTimeout(() => {
|
||||
messageDiv.classList.remove("show");
|
||||
}, 4000);
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
showMessage("error", message, true);
|
||||
}
|
||||
|
||||
function showSuccess(message) {
|
||||
showMessage("success", message, false);
|
||||
}
|
||||
|
||||
function setLoading(isLoading) {
|
||||
const btnText = document.getElementById("btn-text");
|
||||
const btn = document.querySelector(".generate-btn");
|
||||
|
||||
if (isLoading) {
|
||||
btnText.innerHTML = '<span class="loading"></span>Generiere...';
|
||||
btn.disabled = true;
|
||||
btn.style.opacity = '0.7';
|
||||
} else {
|
||||
btnText.textContent = 'Lizenz generieren';
|
||||
btn.disabled = false;
|
||||
btn.style.opacity = '1';
|
||||
}
|
||||
}
|
||||
|
||||
async function saveToDatabase(token, tier) {
|
||||
const description = document.getElementById("description").value.trim();
|
||||
const standorte = document.getElementById("standorte").value.trim();
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/v1/web/save-token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
token: token,
|
||||
description: description || `API-Token Stufe ${tier}`,
|
||||
standorte: standorte
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.message || 'Fehler beim Speichern in der Datenbank');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
return result;
|
||||
} catch (error) {
|
||||
// Fallback: Zeige dem Benutzer den SQL-Befehl an, den er manuell ausführen kann
|
||||
const sql = `INSERT INTO api_tokens (token, description, standorte) VALUES ('${token}', '${description || `API-Token Stufe ${tier}`}', '${standorte}');`;
|
||||
|
||||
throw new Error(`Automatisches Speichern fehlgeschlagen. Server nicht erreichbar.\n\nFühren Sie folgenden SQL-Befehl manuell aus:\n${sql}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function generateLicense() {
|
||||
const macInput = document.getElementById("mac").value.trim();
|
||||
const tierInput = document.getElementById("tier").value.trim();
|
||||
const resultDiv = document.getElementById("result");
|
||||
const licenseOutput = document.getElementById("license-output");
|
||||
const errorDiv = document.getElementById("error");
|
||||
const successDiv = document.getElementById("success");
|
||||
|
||||
// Reset states
|
||||
resultDiv.classList.remove("show");
|
||||
errorDiv.classList.remove("show");
|
||||
successDiv.classList.remove("show");
|
||||
setLoading(true);
|
||||
|
||||
// Simulate slight delay for better UX
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
try {
|
||||
if (!isValidMac(macInput)) {
|
||||
throw new Error("Ungültige MAC-Adresse. Bitte verwenden Sie das Format 00:1A:2B:3C:4D:5E");
|
||||
}
|
||||
|
||||
const mac = macInput.replace(/[:-]/g, "").toUpperCase();
|
||||
const tier = parseInt(tierInput);
|
||||
|
||||
if (isNaN(tier) || tier < 1 || tier > 4) {
|
||||
throw new Error("Lizenzstufe muss eine Zahl zwischen 1 und 4 sein.");
|
||||
}
|
||||
|
||||
// Standort automatisch speichern, falls vorhanden
|
||||
let locationSaved = false;
|
||||
const locationName = document.getElementById('locationSearch')?.value?.trim();
|
||||
const latitude = document.getElementById('latitude')?.textContent;
|
||||
const longitude = document.getElementById('longitude')?.textContent;
|
||||
|
||||
if (locationName && latitude && longitude && tier >= 3) {
|
||||
try {
|
||||
await saveLocationToDatabase();
|
||||
locationSaved = true;
|
||||
} catch (locationError) {
|
||||
console.warn('Standort konnte nicht gespeichert werden:', locationError);
|
||||
// Fahre trotzdem mit der Lizenzgenerierung fort
|
||||
}
|
||||
}
|
||||
|
||||
const data = `${mac}:${tier}`;
|
||||
const enc = new TextEncoder();
|
||||
const key = await crypto.subtle.importKey(
|
||||
"raw",
|
||||
enc.encode(secret),
|
||||
{ name: "HMAC", hash: "SHA-256" },
|
||||
false,
|
||||
["sign"]
|
||||
);
|
||||
const signature = await crypto.subtle.sign("HMAC", key, enc.encode(data));
|
||||
const hex = Array.from(new Uint8Array(signature))
|
||||
.map(b => b.toString(16).padStart(2, "0"))
|
||||
.join("")
|
||||
.toUpperCase();
|
||||
|
||||
licenseOutput.textContent = hex;
|
||||
resultDiv.classList.add("show");
|
||||
|
||||
// Reset copy button
|
||||
const copyBtn = document.getElementById("copyButton");
|
||||
copyBtn.textContent = "📋 In Zwischenablage kopieren";
|
||||
copyBtn.classList.remove("copied");
|
||||
|
||||
// Bei Stufe 3+ in Datenbank speichern
|
||||
if (tier >= 3) {
|
||||
try {
|
||||
await saveToDatabase(hex, tier);
|
||||
let successMessage = `✅ Lizenzschlüssel generiert und als API-Token gespeichert!`;
|
||||
if (locationSaved) {
|
||||
successMessage += ` Standort wurde ebenfalls gespeichert.`;
|
||||
}
|
||||
showSuccess(successMessage);
|
||||
} catch (dbError) {
|
||||
showError(`⚠️ Lizenz generiert, aber Datenbank-Fehler: ${dbError.message}`);
|
||||
}
|
||||
} else {
|
||||
let successMessage = `✅ Lizenzschlüssel erfolgreich generiert!`;
|
||||
if (locationSaved) {
|
||||
successMessage += ` Standort wurde in der Datenbank gespeichert.`;
|
||||
}
|
||||
showSuccess(successMessage);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
showError(error.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function copyToClipboard() {
|
||||
const licenseOutput = document.getElementById("license-output");
|
||||
const copyBtn = document.getElementById("copyButton");
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(licenseOutput.textContent);
|
||||
copyBtn.textContent = "✅ Kopiert!";
|
||||
copyBtn.classList.add("copied");
|
||||
|
||||
setTimeout(() => {
|
||||
copyBtn.textContent = "📋 In Zwischenablage kopieren";
|
||||
copyBtn.classList.remove("copied");
|
||||
}, 2000);
|
||||
} catch (err) {
|
||||
// Fallback for older browsers
|
||||
const textArea = document.createElement("textarea");
|
||||
textArea.value = licenseOutput.textContent;
|
||||
document.body.appendChild(textArea);
|
||||
textArea.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(textArea);
|
||||
|
||||
copyBtn.textContent = "✅ Kopiert!";
|
||||
copyBtn.classList.add("copied");
|
||||
|
||||
setTimeout(() => {
|
||||
copyBtn.textContent = "📋 In Zwischenablage kopieren";
|
||||
copyBtn.classList.remove("copied");
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
// Enter key support
|
||||
document.addEventListener('keypress', function (e) {
|
||||
if (e.key === 'Enter') {
|
||||
generateLicense();
|
||||
}
|
||||
});
|
||||
|
||||
// Input formatting for MAC address
|
||||
document.getElementById('mac').addEventListener('input', function (e) {
|
||||
let value = e.target.value.replace(/[^0-9A-Fa-f]/g, '');
|
||||
if (value.length > 12) value = value.substr(0, 12);
|
||||
|
||||
// Add colons every 2 characters
|
||||
value = value.replace(/(.{2})/g, '$1:').replace(/:$/, '');
|
||||
e.target.value = value;
|
||||
});
|
||||
|
||||
// Input event listener für Lizenzstufe
|
||||
document.getElementById('tier').addEventListener('input', toggleTokenFields);
|
||||
|
||||
// Standortsuche-Funktionalität
|
||||
async function searchLocation(buttonElement) {
|
||||
const locationInput = document.getElementById('locationSearch').value.trim();
|
||||
const coordinatesDiv = document.getElementById('coordinates');
|
||||
const mapContainer = document.getElementById('mapContainer');
|
||||
const latitudeSpan = document.getElementById('latitude');
|
||||
const longitudeSpan = document.getElementById('longitude');
|
||||
const mapFrame = document.getElementById('mapFrame');
|
||||
|
||||
if (!locationInput) {
|
||||
showError('Bitte geben Sie einen Standort ein.');
|
||||
return;
|
||||
}
|
||||
|
||||
let originalText = '';
|
||||
let searchBtn = null;
|
||||
|
||||
try {
|
||||
// Zeige Ladeanimation
|
||||
searchBtn = buttonElement || document.querySelector('button[onclick*="searchLocation"]');
|
||||
if (searchBtn) {
|
||||
originalText = searchBtn.innerHTML;
|
||||
searchBtn.innerHTML = '<span class="loading"></span>Suche...';
|
||||
searchBtn.disabled = true;
|
||||
}
|
||||
|
||||
// API-Abfrage an Nominatim (OpenStreetMap)
|
||||
const response = await fetch(`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(locationInput)}&limit=1`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Fehler bei der API-Abfrage');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.length === 0) {
|
||||
throw new Error('Standort nicht gefunden. Bitte versuchen Sie eine andere Beschreibung.');
|
||||
}
|
||||
|
||||
const location = data[0];
|
||||
const lat = parseFloat(location.lat);
|
||||
const lon = parseFloat(location.lon);
|
||||
|
||||
// Der Name wird vom User bestimmt - nur Koordinaten aus der API verwenden
|
||||
// Kein verstecktes Feld nötig, da der User den Namen selbst eingibt
|
||||
|
||||
// Koordinaten anzeigen
|
||||
updateCoordinates(lat, lon);
|
||||
coordinatesDiv.style.display = 'block';
|
||||
|
||||
// Interaktive Karte erstellen
|
||||
createInteractiveMap(lat, lon);
|
||||
mapContainer.style.display = 'block';
|
||||
|
||||
// Erfolgsmeldung
|
||||
showSuccess(`✅ Koordinaten für "${locationInput}" erfolgreich gefunden! Klicken Sie auf die Karte, um den Pin zu verschieben.`);
|
||||
|
||||
} catch (error) {
|
||||
showError(`Fehler bei der Standortsuche: ${error.message}`);
|
||||
coordinatesDiv.style.display = 'none';
|
||||
mapContainer.style.display = 'none';
|
||||
} finally {
|
||||
// Button zurücksetzen
|
||||
if (searchBtn && originalText) {
|
||||
searchBtn.innerHTML = originalText;
|
||||
searchBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Koordinaten aktualisieren
|
||||
function updateCoordinates(lat, lon) {
|
||||
const latitudeSpan = document.getElementById('latitude');
|
||||
const longitudeSpan = document.getElementById('longitude');
|
||||
|
||||
if (latitudeSpan && longitudeSpan) {
|
||||
latitudeSpan.textContent = lat.toFixed(6);
|
||||
longitudeSpan.textContent = lon.toFixed(6);
|
||||
}
|
||||
}
|
||||
|
||||
// Interaktive Karte erstellen
|
||||
function createInteractiveMap(initialLat, initialLon) {
|
||||
const mapFrame = document.getElementById('mapFrame');
|
||||
|
||||
// Verwende Leaflet.js für interaktive Karte
|
||||
const mapHtml = `
|
||||
<div id="interactiveMap" style="width: 100%; height: 100%; position: relative;">
|
||||
<div id="map" style="width: 100%; height: 100%; border-radius: 10px;"></div>
|
||||
<div style="position: absolute; top: 10px; right: 10px; background: white; padding: 8px; border-radius: 6px; box-shadow: 0 2px 8px rgba(0,0,0,0.2); font-size: 12px; color: #666;">
|
||||
📍 Klicken Sie auf die Karte, um den Pin zu verschieben
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
mapFrame.innerHTML = mapHtml;
|
||||
|
||||
// Leaflet.js laden und Karte initialisieren
|
||||
loadLeafletAndCreateMap(initialLat, initialLon);
|
||||
}
|
||||
|
||||
// Leaflet.js laden und Karte erstellen
|
||||
function loadLeafletAndCreateMap(initialLat, initialLon) {
|
||||
// Prüfe ob Leaflet bereits geladen ist
|
||||
if (typeof L !== 'undefined') {
|
||||
createMap(initialLat, initialLon);
|
||||
return;
|
||||
}
|
||||
|
||||
// Leaflet CSS laden
|
||||
const leafletCSS = document.createElement('link');
|
||||
leafletCSS.rel = 'stylesheet';
|
||||
leafletCSS.href = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css';
|
||||
document.head.appendChild(leafletCSS);
|
||||
|
||||
// Leaflet JavaScript laden
|
||||
const leafletScript = document.createElement('script');
|
||||
leafletScript.src = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js';
|
||||
leafletScript.onload = () => createMap(initialLat, initialLon);
|
||||
document.head.appendChild(leafletScript);
|
||||
}
|
||||
|
||||
// Karte mit Leaflet erstellen
|
||||
function createMap(initialLat, initialLon) {
|
||||
try {
|
||||
const map = L.map('map').setView([initialLat, initialLon], 15);
|
||||
|
||||
// OpenStreetMap Tile Layer
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||
}).addTo(map);
|
||||
|
||||
// Marker erstellen
|
||||
const marker = L.marker([initialLat, initialLon], {
|
||||
draggable: true,
|
||||
title: 'Standort'
|
||||
}).addTo(map);
|
||||
|
||||
// Marker-Drag Event
|
||||
marker.on('dragend', function (event) {
|
||||
const newLat = event.target.getLatLng().lat;
|
||||
const newLon = event.target.getLatLng().lng;
|
||||
updateCoordinates(newLat, newLon);
|
||||
showSuccess(`📍 Pin auf neue Position verschoben: ${newLat.toFixed(6)}, ${newLon.toFixed(6)}`);
|
||||
});
|
||||
|
||||
// Klick-Event auf die Karte
|
||||
map.on('click', function (event) {
|
||||
const newLat = event.latlng.lat;
|
||||
const newLon = event.latlng.lng;
|
||||
|
||||
// Marker auf neue Position setzen
|
||||
marker.setLatLng([newLat, newLon]);
|
||||
|
||||
// Koordinaten aktualisieren
|
||||
updateCoordinates(newLat, newLon);
|
||||
|
||||
// Erfolgsmeldung
|
||||
showSuccess(`📍 Pin auf neue Position gesetzt: ${newLat.toFixed(6)}, ${newLon.toFixed(6)}`);
|
||||
});
|
||||
|
||||
// Zoom-Controls hinzufügen
|
||||
map.zoomControl.setPosition('bottomright');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Erstellen der Karte:', error);
|
||||
// Fallback zu iframe
|
||||
const mapFrame = document.getElementById('mapFrame');
|
||||
const mapUrl = `https://www.openstreetmap.org/export/embed.html?bbox=${initialLon - 0.01},${initialLat - 0.01},${initialLon + 0.01},${initialLat + 0.01}&layer=mapnik&marker=${initialLat},${initialLon}`;
|
||||
mapFrame.innerHTML = `<iframe src="${mapUrl}" width="100%" height="100%" frameborder="0" scrolling="no" marginheight="0" marginwidth="0" title="Standort auf der Karte"></iframe>`;
|
||||
}
|
||||
}
|
||||
|
||||
// Standort in Datenbank speichern
|
||||
async function saveLocationToDatabase() {
|
||||
const locationName = document.getElementById('standorte').value.trim();
|
||||
const latitude = document.getElementById('latitude').textContent;
|
||||
const longitude = document.getElementById('longitude').textContent;
|
||||
const saveBtn = document.getElementById('saveLocationBtn');
|
||||
|
||||
if (!locationName || !latitude || !longitude) {
|
||||
showError('Bitte suchen Sie zuerst einen Standort.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Button-Status ändern
|
||||
const originalText = saveBtn.innerHTML;
|
||||
saveBtn.innerHTML = '<span class="loading"></span>Speichere...';
|
||||
saveBtn.disabled = true;
|
||||
|
||||
// Web-authenticated API für Standortverwaltung aufrufen
|
||||
const response = await fetch('/api/v1/web/create-location', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: locationName,
|
||||
lat: parseFloat(latitude),
|
||||
lon: parseFloat(longitude)
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showSuccess(`✅ Standort "${locationName}" erfolgreich in der Datenbank gespeichert!`);
|
||||
saveBtn.innerHTML = '✅ Gespeichert!';
|
||||
saveBtn.style.background = '#4caf50';
|
||||
|
||||
// Button nach 3 Sekunden zurücksetzen
|
||||
setTimeout(() => {
|
||||
saveBtn.innerHTML = originalText;
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.style.background = '#2196f3';
|
||||
}, 3000);
|
||||
} else {
|
||||
throw new Error(result.message || 'Unbekannter Fehler beim Speichern');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Speichern:', error);
|
||||
showError(`Fehler beim Speichern: ${error.message}`);
|
||||
|
||||
// Button zurücksetzen
|
||||
saveBtn.innerHTML = '💾 Standort in Datenbank speichern';
|
||||
saveBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Zurück zum Dashboard
|
||||
function goBackToDashboard() {
|
||||
window.location.href = '/admin-dashboard';
|
||||
}
|
||||
|
||||
// Logout-Funktion
|
||||
async function logout() {
|
||||
try {
|
||||
const response = await fetch('/api/v1/public/logout', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
window.location.href = '/login';
|
||||
} else {
|
||||
console.error('Fehler beim Abmelden:', result.message);
|
||||
// Trotzdem zur Login-Seite weiterleiten
|
||||
window.location.href = '/login';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Abmelden:', error);
|
||||
// Bei Fehler trotzdem zur Login-Seite weiterleiten
|
||||
window.location.href = '/login';
|
||||
}
|
||||
}
|
||||
|
||||
// Enter-Taste für Standortsuche
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const locationSearch = document.getElementById('locationSearch');
|
||||
if (locationSearch) {
|
||||
locationSearch.addEventListener('keypress', function (e) {
|
||||
if (e.key === 'Enter') {
|
||||
searchLocation();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add cookie settings button functionality
|
||||
const cookieSettingsBtn = document.getElementById('cookie-settings-footer');
|
||||
if (cookieSettingsBtn) {
|
||||
cookieSettingsBtn.addEventListener('click', function () {
|
||||
if (window.cookieConsent) {
|
||||
window.cookieConsent.resetConsent();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
808
public/js/index.js
Normal file
808
public/js/index.js
Normal file
@@ -0,0 +1,808 @@
|
||||
// Supabase configuration
|
||||
const SUPABASE_URL = 'https://lfxlplnypzvjrhftaoog.supabase.co';
|
||||
const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImxmeGxwbG55cHp2anJoZnRhb29nIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDkyMTQ3NzIsImV4cCI6MjA2NDc5MDc3Mn0.XR4preBqWAQ1rT4PFbpkmRdz57BTwIusBI89fIxDHM8';
|
||||
|
||||
// Initialize Supabase client
|
||||
const supabase = window.supabase.createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
|
||||
|
||||
// Initialize Socket.IO connection
|
||||
let socket;
|
||||
|
||||
function setupSocketListeners() {
|
||||
if (!socket) return;
|
||||
|
||||
socket.on('connect', () => {
|
||||
console.log('🔌 WebSocket connected');
|
||||
});
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
console.log('🔌 WebSocket disconnected');
|
||||
});
|
||||
|
||||
socket.on('newTime', (data) => {
|
||||
console.log('🏁 New time received:', data);
|
||||
showNotification(data);
|
||||
// Reload data to show the new time
|
||||
loadData();
|
||||
});
|
||||
}
|
||||
|
||||
function initializeSocket() {
|
||||
if (typeof io !== 'undefined') {
|
||||
socket = io();
|
||||
setupSocketListeners();
|
||||
} else {
|
||||
console.error('Socket.IO library not loaded');
|
||||
}
|
||||
}
|
||||
|
||||
// Try to initialize immediately, fallback to DOMContentLoaded
|
||||
if (typeof io !== 'undefined') {
|
||||
initializeSocket();
|
||||
} else {
|
||||
document.addEventListener('DOMContentLoaded', initializeSocket);
|
||||
}
|
||||
|
||||
// Global variable to store locations with coordinates
|
||||
let locationsData = [];
|
||||
let lastSelectedLocation = null;
|
||||
|
||||
// Cookie Functions (inline implementation)
|
||||
function setCookie(name, value, days = 30) {
|
||||
const expires = new Date();
|
||||
expires.setTime(expires.getTime() + (days * 24 * 60 * 60 * 1000));
|
||||
document.cookie = `${name}=${value};expires=${expires.toUTCString()};path=/;SameSite=Lax`;
|
||||
}
|
||||
|
||||
function getCookie(name) {
|
||||
const nameEQ = name + "=";
|
||||
const ca = document.cookie.split(';');
|
||||
for (let i = 0; i < ca.length; i++) {
|
||||
let c = ca[i];
|
||||
while (c.charAt(0) === ' ') c = c.substring(1, c.length);
|
||||
if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function loadLastSelectedLocation() {
|
||||
try {
|
||||
const cookieValue = getCookie('ninjacross_last_location');
|
||||
if (cookieValue) {
|
||||
const lastLocation = JSON.parse(cookieValue);
|
||||
lastSelectedLocation = lastLocation;
|
||||
console.log('📍 Last selected location loaded:', lastLocation.name);
|
||||
return lastLocation;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading last location:', error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function saveLocationSelection(locationId, locationName) {
|
||||
try {
|
||||
// Remove emoji from location name for storage
|
||||
const cleanName = locationName.replace(/^📍\s*/, '');
|
||||
const locationData = {
|
||||
id: locationId,
|
||||
name: cleanName,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
setCookie('ninjacross_last_location', JSON.stringify(locationData), 90);
|
||||
lastSelectedLocation = { id: locationId, name: cleanName };
|
||||
console.log('💾 Location saved to cookie:', cleanName);
|
||||
} catch (error) {
|
||||
console.error('Error saving location:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// WebSocket Event Handlers are now in setupSocketListeners() function
|
||||
|
||||
// Notification Functions
|
||||
function showNotification(timeData) {
|
||||
const notificationBubble = document.getElementById('notificationBubble');
|
||||
const notificationTitle = document.getElementById('notificationTitle');
|
||||
const notificationSubtitle = document.getElementById('notificationSubtitle');
|
||||
|
||||
// Format the time data
|
||||
const playerName = timeData.player_name || (currentLanguage === 'de' ? 'Unbekannter Spieler' : 'Unknown Player');
|
||||
const locationName = timeData.location_name || (currentLanguage === 'de' ? 'Unbekannter Standort' : 'Unknown Location');
|
||||
const timeString = timeData.recorded_time || '--:--';
|
||||
|
||||
// Update notification content
|
||||
const newTimeText = currentLanguage === 'de' ? 'Neue Zeit von' : 'New time from';
|
||||
notificationTitle.textContent = `🏁 ${newTimeText} ${playerName}!`;
|
||||
notificationSubtitle.textContent = `${timeString} • ${locationName}`;
|
||||
|
||||
// Ensure notification is above sticky header
|
||||
notificationBubble.style.zIndex = '100000';
|
||||
|
||||
// Check if we're on mobile and adjust position
|
||||
if (window.innerWidth <= 768) {
|
||||
notificationBubble.style.top = '5rem'; // Below sticky header on mobile
|
||||
} else {
|
||||
notificationBubble.style.top = '2rem'; // Normal position on desktop
|
||||
}
|
||||
|
||||
// Show notification
|
||||
notificationBubble.classList.remove('hide');
|
||||
notificationBubble.classList.add('show');
|
||||
|
||||
// Auto-hide after 5 seconds
|
||||
setTimeout(() => {
|
||||
hideNotification();
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function hideNotification() {
|
||||
const notificationBubble = document.getElementById('notificationBubble');
|
||||
notificationBubble.classList.remove('show');
|
||||
notificationBubble.classList.add('hide');
|
||||
|
||||
// Remove hide class after animation
|
||||
setTimeout(() => {
|
||||
notificationBubble.classList.remove('hide');
|
||||
}, 300);
|
||||
}
|
||||
|
||||
// Check authentication status
|
||||
async function checkAuth() {
|
||||
try {
|
||||
const { data: { session } } = await supabase.auth.getSession();
|
||||
|
||||
if (session) {
|
||||
// User is logged in, show dashboard button
|
||||
document.getElementById('adminLoginBtn').style.display = 'none';
|
||||
document.getElementById('dashboardBtn').style.display = 'inline-block';
|
||||
document.getElementById('logoutBtn').style.display = 'inline-block';
|
||||
} else {
|
||||
// User is not logged in, show admin login button
|
||||
document.getElementById('adminLoginBtn').style.display = 'inline-block';
|
||||
document.getElementById('dashboardBtn').style.display = 'none';
|
||||
document.getElementById('logoutBtn').style.display = 'none';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking auth:', error);
|
||||
// Fallback: show login button if auth check fails
|
||||
document.getElementById('adminLoginBtn').style.display = 'inline-block';
|
||||
document.getElementById('dashboardBtn').style.display = 'none';
|
||||
document.getElementById('logoutBtn').style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Logout function
|
||||
async function logout() {
|
||||
try {
|
||||
const { error } = await supabase.auth.signOut();
|
||||
if (error) {
|
||||
console.error('Error logging out:', error);
|
||||
} else {
|
||||
window.location.reload();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error during logout:', error);
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
// Load locations from database
|
||||
async function loadLocations() {
|
||||
try {
|
||||
const response = await fetch('/api/v1/public/locations');
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch locations');
|
||||
}
|
||||
|
||||
const responseData = await response.json();
|
||||
const locations = responseData.data || responseData; // Handle both formats
|
||||
const locationSelect = document.getElementById('locationSelect');
|
||||
|
||||
// Store locations globally for distance calculations
|
||||
locationsData = locations;
|
||||
|
||||
// Clear existing options and set default placeholder
|
||||
const placeholderText = currentLanguage === 'de' ? '📍 Bitte Standort auswählen' : '📍 Please select location';
|
||||
locationSelect.innerHTML = `<option value="">${placeholderText}</option>`;
|
||||
|
||||
// Add locations from database
|
||||
locations.forEach(location => {
|
||||
const option = document.createElement('option');
|
||||
option.value = location.name;
|
||||
option.textContent = `📍 ${location.name}`;
|
||||
locationSelect.appendChild(option);
|
||||
});
|
||||
|
||||
// Load and set last selected location
|
||||
const lastLocation = loadLastSelectedLocation();
|
||||
if (lastLocation) {
|
||||
// Find the option that matches the last location name
|
||||
const matchingOption = Array.from(locationSelect.options).find(option =>
|
||||
option.textContent === `📍 ${lastLocation.name}` || option.value === lastLocation.name
|
||||
);
|
||||
if (matchingOption) {
|
||||
locationSelect.value = matchingOption.value;
|
||||
console.log('📍 Last selected location restored:', lastLocation.name);
|
||||
// Update the current selection display
|
||||
updateCurrentSelection();
|
||||
// Load data for the restored location
|
||||
loadData();
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading locations:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate distance between two points using Haversine formula
|
||||
function calculateDistance(lat1, lon1, lat2, lon2) {
|
||||
const R = 6371; // Earth's radius in kilometers
|
||||
const dLat = toRadians(lat2 - lat1);
|
||||
const dLon = toRadians(lon2 - lon1);
|
||||
|
||||
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
||||
Math.cos(toRadians(lat1)) * Math.cos(toRadians(lat2)) *
|
||||
Math.sin(dLon / 2) * Math.sin(dLon / 2);
|
||||
|
||||
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
const distance = R * c; // Distance in kilometers
|
||||
|
||||
return distance;
|
||||
}
|
||||
|
||||
function toRadians(degrees) {
|
||||
return degrees * (Math.PI / 180);
|
||||
}
|
||||
|
||||
// Find nearest location based on user's current position
|
||||
async function findNearestLocation() {
|
||||
const btn = document.getElementById('findLocationBtn');
|
||||
const locationSelect = document.getElementById('locationSelect');
|
||||
|
||||
// Check if geolocation is supported
|
||||
if (!navigator.geolocation) {
|
||||
const errorMsg = currentLanguage === 'de' ?
|
||||
'Geolocation wird von diesem Browser nicht unterstützt.' :
|
||||
'Geolocation is not supported by this browser.';
|
||||
showLocationError(errorMsg);
|
||||
return;
|
||||
}
|
||||
|
||||
// Update button state to loading
|
||||
btn.disabled = true;
|
||||
btn.classList.add('loading');
|
||||
btn.textContent = currentLanguage === 'de' ? '🔍 Suche...' : '🔍 Searching...';
|
||||
|
||||
try {
|
||||
// Get user's current position
|
||||
const position = await new Promise((resolve, reject) => {
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
resolve,
|
||||
reject,
|
||||
{
|
||||
enableHighAccuracy: true,
|
||||
timeout: 10000,
|
||||
maximumAge: 300000 // 5 minutes
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
const userLat = position.coords.latitude;
|
||||
const userLon = position.coords.longitude;
|
||||
|
||||
// Calculate distances to all locations
|
||||
const locationsWithDistance = locationsData.map(location => ({
|
||||
...location,
|
||||
distance: calculateDistance(
|
||||
userLat,
|
||||
userLon,
|
||||
parseFloat(location.latitude),
|
||||
parseFloat(location.longitude)
|
||||
)
|
||||
}));
|
||||
|
||||
// Find the nearest location
|
||||
const nearestLocation = locationsWithDistance.reduce((nearest, current) => {
|
||||
return current.distance < nearest.distance ? current : nearest;
|
||||
});
|
||||
|
||||
// Select the nearest location in the dropdown
|
||||
locationSelect.value = nearestLocation.name;
|
||||
|
||||
// Trigger change event to update the leaderboard
|
||||
locationSelect.dispatchEvent(new Event('change'));
|
||||
|
||||
// Show success notification
|
||||
showLocationSuccess(nearestLocation.name, nearestLocation.distance);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error getting location:', error);
|
||||
let errorMessage = currentLanguage === 'de' ? 'Standort konnte nicht ermittelt werden.' : 'Location could not be determined.';
|
||||
|
||||
if (error.code) {
|
||||
switch (error.code) {
|
||||
case error.PERMISSION_DENIED:
|
||||
errorMessage = currentLanguage === 'de' ?
|
||||
'Standortzugriff wurde verweigert. Bitte erlaube den Standortzugriff in den Browser-Einstellungen.' :
|
||||
'Location access was denied. Please allow location access in browser settings.';
|
||||
break;
|
||||
case error.POSITION_UNAVAILABLE:
|
||||
errorMessage = currentLanguage === 'de' ?
|
||||
'Standortinformationen sind nicht verfügbar.' :
|
||||
'Location information is not available.';
|
||||
break;
|
||||
case error.TIMEOUT:
|
||||
errorMessage = currentLanguage === 'de' ?
|
||||
'Zeitüberschreitung beim Abrufen des Standorts.' :
|
||||
'Timeout while retrieving location.';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
showLocationError(errorMessage);
|
||||
} finally {
|
||||
// Reset button state
|
||||
btn.disabled = false;
|
||||
btn.classList.remove('loading');
|
||||
btn.textContent = currentLanguage === 'de' ? '📍 Mein Standort' : '📍 My Location';
|
||||
}
|
||||
}
|
||||
|
||||
// Show success notification for location finding
|
||||
function showLocationSuccess(locationName, distance) {
|
||||
const notificationBubble = document.getElementById('notificationBubble');
|
||||
const notificationTitle = document.getElementById('notificationTitle');
|
||||
const notificationSubtitle = document.getElementById('notificationSubtitle');
|
||||
|
||||
// Update notification content
|
||||
const locationFoundText = currentLanguage === 'de' ? 'Standort gefunden!' : 'Location found!';
|
||||
const distanceText = currentLanguage === 'de' ? 'km entfernt' : 'km away';
|
||||
notificationTitle.textContent = `📍 ${locationFoundText}`;
|
||||
notificationSubtitle.textContent = `${locationName} (${distance.toFixed(1)} ${distanceText})`;
|
||||
|
||||
// Ensure notification is above sticky header
|
||||
notificationBubble.style.zIndex = '100000';
|
||||
|
||||
// Check if we're on mobile and adjust position
|
||||
if (window.innerWidth <= 768) {
|
||||
notificationBubble.style.top = '5rem'; // Below sticky header on mobile
|
||||
} else {
|
||||
notificationBubble.style.top = '2rem'; // Normal position on desktop
|
||||
}
|
||||
|
||||
// Show notification
|
||||
notificationBubble.classList.remove('hide');
|
||||
notificationBubble.classList.add('show');
|
||||
|
||||
// Auto-hide after 4 seconds
|
||||
setTimeout(() => {
|
||||
hideNotification();
|
||||
}, 4000);
|
||||
}
|
||||
|
||||
// Show error notification for location finding
|
||||
function showLocationError(message) {
|
||||
const notificationBubble = document.getElementById('notificationBubble');
|
||||
const notificationTitle = document.getElementById('notificationTitle');
|
||||
const notificationSubtitle = document.getElementById('notificationSubtitle');
|
||||
|
||||
// Change notification style to error
|
||||
notificationBubble.style.background = 'linear-gradient(135deg, #dc3545, #c82333)';
|
||||
|
||||
// Update notification content
|
||||
const errorText = currentLanguage === 'de' ? 'Fehler' : 'Error';
|
||||
notificationTitle.textContent = `❌ ${errorText}`;
|
||||
notificationSubtitle.textContent = message;
|
||||
|
||||
// Ensure notification is above sticky header
|
||||
notificationBubble.style.zIndex = '100000';
|
||||
|
||||
// Check if we're on mobile and adjust position
|
||||
if (window.innerWidth <= 768) {
|
||||
notificationBubble.style.top = '5rem'; // Below sticky header on mobile
|
||||
} else {
|
||||
notificationBubble.style.top = '2rem'; // Normal position on desktop
|
||||
}
|
||||
|
||||
// Show notification
|
||||
notificationBubble.classList.remove('hide');
|
||||
notificationBubble.classList.add('show');
|
||||
|
||||
// Auto-hide after 6 seconds
|
||||
setTimeout(() => {
|
||||
hideNotification();
|
||||
// Reset notification style
|
||||
notificationBubble.style.background = 'linear-gradient(135deg, #00d4ff, #0891b2)';
|
||||
}, 6000);
|
||||
}
|
||||
|
||||
// Show prompt when no location is selected
|
||||
function showLocationSelectionPrompt() {
|
||||
const rankingList = document.getElementById('rankingList');
|
||||
const emptyTitle = currentLanguage === 'de' ? 'Standort auswählen' : 'Select Location';
|
||||
const emptyDescription = currentLanguage === 'de' ?
|
||||
'Bitte wähle einen Standort aus dem Dropdown-Menü aus<br>oder nutze den "📍 Mein Standort" Button, um automatisch<br>den nächstgelegenen Standort zu finden.' :
|
||||
'Please select a location from the dropdown menu<br>or use the "📍 My Location" button to automatically<br>find the nearest location.';
|
||||
|
||||
rankingList.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">📍</div>
|
||||
<div class="empty-title">${emptyTitle}</div>
|
||||
<div class="empty-description">${emptyDescription}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Reset stats to show no data
|
||||
document.getElementById('totalPlayers').textContent = '0';
|
||||
document.getElementById('bestTime').textContent = '--:--';
|
||||
document.getElementById('totalRecords').textContent = '0';
|
||||
|
||||
// Update current selection display
|
||||
updateCurrentSelection();
|
||||
}
|
||||
|
||||
// Load data from local database via MCP
|
||||
async function loadData() {
|
||||
try {
|
||||
const location = document.getElementById('locationSelect').value;
|
||||
const period = document.querySelector('.time-tab.active').dataset.period;
|
||||
|
||||
// Don't load data if no location is selected
|
||||
if (!location || location === '') {
|
||||
showLocationSelectionPrompt();
|
||||
return;
|
||||
}
|
||||
|
||||
// Build query parameters
|
||||
const params = new URLSearchParams();
|
||||
if (location && location !== 'all') {
|
||||
params.append('location', location);
|
||||
}
|
||||
if (period && period !== 'all') {
|
||||
params.append('period', period);
|
||||
}
|
||||
|
||||
// Fetch times with player and location data from local database
|
||||
const response = await fetch(`/api/v1/public/times-with-details?${params.toString()}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch times');
|
||||
}
|
||||
|
||||
const times = await response.json();
|
||||
|
||||
// Convert to the format expected by the leaderboard
|
||||
const leaderboardData = times.map(time => {
|
||||
const { minutes, seconds, milliseconds } = time.recorded_time;
|
||||
const timeString = `${minutes}:${seconds.toString().padStart(2, '0')}.${milliseconds}`;
|
||||
const playerName = time.player ?
|
||||
`${time.player.firstname} ${time.player.lastname}` :
|
||||
(currentLanguage === 'de' ? 'Unbekannter Spieler' : 'Unknown Player');
|
||||
const locationName = time.location ? time.location.name :
|
||||
(currentLanguage === 'de' ? 'Unbekannter Standort' : 'Unknown Location');
|
||||
const date = new Date(time.created_at).toISOString().split('T')[0];
|
||||
|
||||
return {
|
||||
name: playerName,
|
||||
time: timeString,
|
||||
date: date,
|
||||
location: locationName
|
||||
};
|
||||
});
|
||||
|
||||
// Sort by time (fastest first)
|
||||
leaderboardData.sort((a, b) => {
|
||||
const timeA = timeToSeconds(a.time);
|
||||
const timeB = timeToSeconds(b.time);
|
||||
return timeA - timeB;
|
||||
});
|
||||
|
||||
updateLeaderboard(leaderboardData);
|
||||
updateStats(leaderboardData);
|
||||
updateCurrentSelection();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading data:', error);
|
||||
// Fallback to sample data if API fails
|
||||
loadSampleData();
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback sample data based on real database data
|
||||
function loadSampleData() {
|
||||
const sampleData = [
|
||||
{ name: "Carsten Graf", time: "01:28.945", date: "2025-08-30", location: "Ulm Donaubad" },
|
||||
{ name: "Carsten Graf", time: "01:30.945", date: "2025-08-30", location: "Ulm Donaubad" },
|
||||
{ name: "Max Mustermann", time: "01:50.945", date: "2025-08-30", location: "Ulm Donaubad" },
|
||||
{ name: "Carsten Graf", time: "02:50.945", date: "2025-08-31", location: "Test" },
|
||||
{ name: "Max Mustermann", time: "02:50.945", date: "2025-08-31", location: "Test" },
|
||||
{ name: "Carsten Graf", time: "01:10.945", date: "2025-09-02", location: "Test" },
|
||||
{ name: "Carsten Graf", time: "01:11.945", date: "2025-09-02", location: "Test" },
|
||||
{ name: "Carsten Graf", time: "01:11.945", date: "2025-09-02", location: "Ulm Donaubad" }
|
||||
];
|
||||
|
||||
updateLeaderboard(sampleData);
|
||||
updateStats(sampleData);
|
||||
updateCurrentSelection();
|
||||
}
|
||||
|
||||
function timeToSeconds(timeStr) {
|
||||
const [minutes, seconds] = timeStr.split(':');
|
||||
return parseFloat(minutes) * 60 + parseFloat(seconds);
|
||||
}
|
||||
|
||||
function updateStats(data) {
|
||||
const totalPlayers = new Set(data.map(item => item.name)).size;
|
||||
const bestTime = data.length > 0 ? data[0].time : '--:--';
|
||||
const totalRecords = data.length;
|
||||
|
||||
document.getElementById('totalPlayers').textContent = totalPlayers;
|
||||
document.getElementById('bestTime').textContent = bestTime;
|
||||
document.getElementById('totalRecords').textContent = totalRecords;
|
||||
}
|
||||
|
||||
function updateCurrentSelection() {
|
||||
const location = document.getElementById('locationSelect').value;
|
||||
const period = document.querySelector('.time-tab.active').dataset.period;
|
||||
|
||||
// Get the display text from the selected option
|
||||
const locationSelect = document.getElementById('locationSelect');
|
||||
const selectedLocationOption = locationSelect.options[locationSelect.selectedIndex];
|
||||
const locationDisplay = selectedLocationOption ? selectedLocationOption.textContent :
|
||||
(currentLanguage === 'de' ? '📍 Bitte Standort auswählen' : '📍 Please select location');
|
||||
|
||||
const periodIcons = currentLanguage === 'de' ? {
|
||||
'today': '📅 Heute',
|
||||
'week': '📊 Diese Woche',
|
||||
'month': '📈 Dieser Monat',
|
||||
'all': '♾️ Alle Zeiten'
|
||||
} : {
|
||||
'today': '📅 Today',
|
||||
'week': '📊 This Week',
|
||||
'month': '📈 This Month',
|
||||
'all': '♾️ All Times'
|
||||
};
|
||||
|
||||
document.getElementById('currentSelection').textContent =
|
||||
`${locationDisplay} • ${periodIcons[period]}`;
|
||||
|
||||
const lastSyncText = currentLanguage === 'de' ? 'Letzter Sync' : 'Last Sync';
|
||||
document.getElementById('lastUpdated').textContent =
|
||||
`${lastSyncText}: ${new Date().toLocaleTimeString(currentLanguage === 'de' ? 'de-DE' : 'en-US')}`;
|
||||
}
|
||||
|
||||
function updateLeaderboard(data) {
|
||||
const rankingList = document.getElementById('rankingList');
|
||||
|
||||
if (data.length === 0) {
|
||||
const emptyTitle = currentLanguage === 'de' ? 'Keine Rekorde gefunden' : 'No records found';
|
||||
const emptyDescription = currentLanguage === 'de' ?
|
||||
'Für diese Filtereinstellungen liegen noch keine Zeiten vor.<br>Versuche es mit einem anderen Zeitraum oder Standort.' :
|
||||
'No times available for these filter settings.<br>Try a different time period or location.';
|
||||
|
||||
rankingList.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">🏁</div>
|
||||
<div class="empty-title">${emptyTitle}</div>
|
||||
<div class="empty-description">${emptyDescription}</div>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
rankingList.innerHTML = data.map((player, index) => {
|
||||
const rank = index + 1;
|
||||
let positionClass = '';
|
||||
let trophy = '';
|
||||
|
||||
if (rank === 1) {
|
||||
positionClass = 'gold';
|
||||
trophy = '👑';
|
||||
} else if (rank === 2) {
|
||||
positionClass = 'silver';
|
||||
trophy = '🥈';
|
||||
} else if (rank === 3) {
|
||||
positionClass = 'bronze';
|
||||
trophy = '🥉';
|
||||
} else if (rank <= 10) {
|
||||
trophy = '⭐';
|
||||
}
|
||||
|
||||
const formatDate = new Date(player.date).toLocaleDateString(currentLanguage === 'de' ? 'de-DE' : 'en-US', {
|
||||
day: '2-digit',
|
||||
month: 'short'
|
||||
});
|
||||
|
||||
return `
|
||||
<div class="rank-entry">
|
||||
<div class="position ${positionClass}">#${rank}</div>
|
||||
<div class="player-data">
|
||||
<div class="player-name">${player.name}</div>
|
||||
<div class="player-meta">
|
||||
<span class="location-tag">${player.location}</span>
|
||||
<span>🗓️ ${formatDate}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="time-result">${player.time}</div>
|
||||
${trophy ? `<div class="trophy-icon">${trophy}</div>` : '<div class="trophy-icon"></div>'}
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// Event Listeners Setup
|
||||
function setupEventListeners() {
|
||||
// Location select event listener
|
||||
document.getElementById('locationSelect').addEventListener('change', function () {
|
||||
// Save location selection to cookie
|
||||
const selectedOption = this.options[this.selectedIndex];
|
||||
if (selectedOption.value) {
|
||||
saveLocationSelection(selectedOption.value, selectedOption.textContent);
|
||||
}
|
||||
// Load data
|
||||
loadData();
|
||||
});
|
||||
|
||||
// Time tab event listeners
|
||||
document.querySelectorAll('.time-tab').forEach(tab => {
|
||||
tab.addEventListener('click', function () {
|
||||
// Remove active class from all tabs
|
||||
document.querySelectorAll('.time-tab').forEach(t => t.classList.remove('active'));
|
||||
// Add active class to clicked tab
|
||||
this.classList.add('active');
|
||||
// Load data with new period
|
||||
loadData();
|
||||
});
|
||||
});
|
||||
|
||||
// Smooth scroll for better UX
|
||||
const rankingsList = document.querySelector('.rankings-list');
|
||||
if (rankingsList) {
|
||||
rankingsList.style.scrollBehavior = 'smooth';
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize page
|
||||
async function init() {
|
||||
await checkAuth();
|
||||
await loadLocations();
|
||||
showLocationSelectionPrompt(); // Show prompt instead of loading data initially
|
||||
setupEventListeners();
|
||||
}
|
||||
|
||||
// Auto-refresh function
|
||||
function startAutoRefresh() {
|
||||
setInterval(loadData, 45000);
|
||||
}
|
||||
|
||||
// Language Management
|
||||
let currentLanguage = 'en'; // Default to English
|
||||
|
||||
// Translation function
|
||||
function translateElement(element, language) {
|
||||
if (element.dataset[language]) {
|
||||
// Check if the content contains HTML tags
|
||||
if (element.dataset[language].includes('<')) {
|
||||
element.innerHTML = element.dataset[language];
|
||||
} else {
|
||||
element.textContent = element.dataset[language];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Change language function
|
||||
function changeLanguage() {
|
||||
const languageSelect = document.getElementById('languageSelect');
|
||||
currentLanguage = languageSelect.value;
|
||||
|
||||
// Save language preference
|
||||
localStorage.setItem('ninjacross_language', currentLanguage);
|
||||
|
||||
// Update flag in select
|
||||
updateLanguageFlag();
|
||||
|
||||
// Translate all elements with data attributes
|
||||
const elementsToTranslate = document.querySelectorAll('[data-de][data-en]');
|
||||
elementsToTranslate.forEach(element => {
|
||||
translateElement(element, currentLanguage);
|
||||
});
|
||||
|
||||
// Update dynamic content
|
||||
updateDynamicContent();
|
||||
|
||||
console.log(`🌐 Language changed to: ${currentLanguage}`);
|
||||
}
|
||||
|
||||
// Update flag in language selector
|
||||
function updateLanguageFlag() {
|
||||
const languageSelect = document.getElementById('languageSelect');
|
||||
if (languageSelect) {
|
||||
if (currentLanguage === 'de') {
|
||||
// German flag (black-red-gold)
|
||||
languageSelect.style.backgroundImage = `url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="20" height="15" viewBox="0 0 20 15"><rect width="20" height="5" fill="%23000000"/><rect y="5" width="20" height="5" fill="%23DD0000"/><rect y="10" width="20" height="5" fill="%23FFCE00"/></svg>')`;
|
||||
} else {
|
||||
// USA flag
|
||||
languageSelect.style.backgroundImage = `url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="20" height="15" viewBox="0 0 20 15"><rect width="20" height="15" fill="%23B22234"/><rect width="20" height="1.15" fill="%23FFFFFF"/><rect y="2.3" width="20" height="1.15" fill="%23FFFFFF"/><rect y="4.6" width="20" height="1.15" fill="%23FFFFFF"/><rect y="6.9" width="20" height="1.15" fill="%23FFFFFF"/><rect y="9.2" width="20" height="1.15" fill="%23FFFFFF"/><rect y="11.5" width="20" height="1.15" fill="%23FFFFFF"/><rect y="13.8" width="20" height="1.15" fill="%23FFFFFF"/><rect width="7.7" height="8.05" fill="%230033A0"/></svg>')`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update dynamic content that's not in HTML
|
||||
function updateDynamicContent() {
|
||||
// Update location select placeholder
|
||||
const locationSelect = document.getElementById('locationSelect');
|
||||
if (locationSelect && locationSelect.options[0]) {
|
||||
locationSelect.options[0].textContent = currentLanguage === 'de' ?
|
||||
'📍 Bitte Standort auswählen' : '📍 Please select location';
|
||||
}
|
||||
|
||||
// Update find location button
|
||||
const findLocationBtn = document.getElementById('findLocationBtn');
|
||||
if (findLocationBtn) {
|
||||
findLocationBtn.textContent = currentLanguage === 'de' ?
|
||||
'📍 Mein Standort' : '📍 My Location';
|
||||
findLocationBtn.title = currentLanguage === 'de' ?
|
||||
'Nächstgelegenen Standort finden' : 'Find nearest location';
|
||||
}
|
||||
|
||||
// Update refresh button
|
||||
const refreshBtn = document.querySelector('.refresh-btn');
|
||||
if (refreshBtn) {
|
||||
refreshBtn.textContent = currentLanguage === 'de' ?
|
||||
'⚡ Live Update' : '⚡ Live Update';
|
||||
}
|
||||
|
||||
// Update notification elements
|
||||
const notificationTitle = document.getElementById('notificationTitle');
|
||||
const notificationSubtitle = document.getElementById('notificationSubtitle');
|
||||
if (notificationTitle) {
|
||||
notificationTitle.textContent = currentLanguage === 'de' ? 'Neue Zeit!' : 'New Time!';
|
||||
}
|
||||
if (notificationSubtitle) {
|
||||
notificationSubtitle.textContent = currentLanguage === 'de' ?
|
||||
'Ein neuer Rekord wurde erstellt' : 'A new record has been created';
|
||||
}
|
||||
|
||||
// Update current selection display
|
||||
updateCurrentSelection();
|
||||
|
||||
// Reload data to update any dynamic content
|
||||
if (document.getElementById('locationSelect').value) {
|
||||
loadData();
|
||||
} else {
|
||||
showLocationSelectionPrompt();
|
||||
}
|
||||
}
|
||||
|
||||
// Load saved language preference
|
||||
function loadLanguagePreference() {
|
||||
const savedLanguage = localStorage.getItem('ninjacross_language');
|
||||
if (savedLanguage && (savedLanguage === 'de' || savedLanguage === 'en')) {
|
||||
currentLanguage = savedLanguage;
|
||||
const languageSelect = document.getElementById('languageSelect');
|
||||
if (languageSelect) {
|
||||
languageSelect.value = currentLanguage;
|
||||
// Update flag when loading
|
||||
updateLanguageFlag();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Start the application when DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
loadLanguagePreference();
|
||||
changeLanguage(); // Apply saved language
|
||||
init();
|
||||
startAutoRefresh();
|
||||
|
||||
// Add cookie settings button functionality
|
||||
const cookieSettingsBtn = document.getElementById('cookie-settings-footer');
|
||||
if (cookieSettingsBtn) {
|
||||
cookieSettingsBtn.addEventListener('click', function () {
|
||||
if (window.cookieConsent) {
|
||||
window.cookieConsent.resetConsent();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
291
public/js/login.js
Normal file
291
public/js/login.js
Normal file
@@ -0,0 +1,291 @@
|
||||
// Supabase configuration
|
||||
const SUPABASE_URL = 'https://lfxlplnypzvjrhftaoog.supabase.co';
|
||||
const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImxmeGxwbG55cHp2anJoZnRhb29nIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDkyMTQ3NzIsImV4cCI6MjA2NDc5MDc3Mn0.XR4preBqWAQ1rT4PFbpkmRdz57BTwIusBI89fIxDHM8';
|
||||
|
||||
// Initialize Supabase client
|
||||
const supabase = window.supabase.createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
|
||||
|
||||
// Check if user is already logged in
|
||||
async function checkAuth() {
|
||||
const { data: { session } } = await supabase.auth.getSession();
|
||||
if (session) {
|
||||
// Show a message that user is already logged in
|
||||
showMessage('Sie sind bereits eingeloggt! Weiterleitung zum Dashboard...', 'success');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/';
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if device is iOS
|
||||
function isIOS() {
|
||||
return /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
|
||||
}
|
||||
|
||||
// Google OAuth Sign In
|
||||
async function signInWithGoogle() {
|
||||
try {
|
||||
setLoading(true);
|
||||
clearMessage();
|
||||
|
||||
// iOS-specific handling
|
||||
if (isIOS()) {
|
||||
// For iOS, use a different approach with popup
|
||||
const { data, error } = await supabase.auth.signInWithOAuth({
|
||||
provider: 'google',
|
||||
options: {
|
||||
redirectTo: `${window.location.origin}/auth/callback`,
|
||||
queryParams: {
|
||||
access_type: 'offline',
|
||||
prompt: 'consent',
|
||||
},
|
||||
skipBrowserRedirect: true // Important for iOS
|
||||
}
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error('Google OAuth error:', error);
|
||||
showMessage('Fehler bei der Google-Anmeldung: ' + error.message, 'error');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.url) {
|
||||
// Open in same window for iOS
|
||||
window.location.href = data.url;
|
||||
}
|
||||
} else {
|
||||
// Standard handling for other devices
|
||||
const { data, error } = await supabase.auth.signInWithOAuth({
|
||||
provider: 'google',
|
||||
options: {
|
||||
redirectTo: `${window.location.origin}/auth/callback`
|
||||
}
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error('Google OAuth error:', error);
|
||||
showMessage('Fehler bei der Google-Anmeldung: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Google OAuth error:', error);
|
||||
showMessage('Fehler bei der Google-Anmeldung: ' + error.message, 'error');
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Discord OAuth Sign In
|
||||
async function signInWithDiscord() {
|
||||
try {
|
||||
setLoading(true);
|
||||
clearMessage();
|
||||
|
||||
const { data, error } = await supabase.auth.signInWithOAuth({
|
||||
provider: 'discord',
|
||||
options: {
|
||||
redirectTo: `${window.location.origin}/auth/callback`
|
||||
}
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error('Discord OAuth error:', error);
|
||||
showMessage('Fehler bei der Discord-Anmeldung: ' + error.message, 'error');
|
||||
}
|
||||
// Note: OAuth redirects the page, so we don't need to handle success here
|
||||
} catch (error) {
|
||||
console.error('Discord OAuth error:', error);
|
||||
showMessage('Fehler bei der Discord-Anmeldung: ' + error.message, 'error');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle between login and register forms
|
||||
function toggleForm() {
|
||||
const loginForm = document.getElementById('loginForm');
|
||||
const registerForm = document.getElementById('registerForm');
|
||||
|
||||
if (loginForm.classList.contains('active')) {
|
||||
loginForm.classList.remove('active');
|
||||
registerForm.classList.add('active');
|
||||
} else {
|
||||
registerForm.classList.remove('active');
|
||||
loginForm.classList.add('active');
|
||||
}
|
||||
clearMessage();
|
||||
showPasswordReset(false); // Hide password reset when switching forms
|
||||
}
|
||||
|
||||
// Show message
|
||||
function showMessage(message, type = 'success') {
|
||||
const messageDiv = document.getElementById('message');
|
||||
messageDiv.innerHTML = `<div class="message ${type}">${message}</div>`;
|
||||
setTimeout(() => {
|
||||
messageDiv.innerHTML = '';
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Clear message
|
||||
function clearMessage() {
|
||||
document.getElementById('message').innerHTML = '';
|
||||
}
|
||||
|
||||
// Show/hide password reset container
|
||||
function showPasswordReset(show) {
|
||||
const resetContainer = document.getElementById('passwordResetContainer');
|
||||
if (show) {
|
||||
resetContainer.classList.add('active');
|
||||
} else {
|
||||
resetContainer.classList.remove('active');
|
||||
}
|
||||
}
|
||||
|
||||
// Show/hide loading
|
||||
function setLoading(show) {
|
||||
const loading = document.getElementById('loading');
|
||||
if (show) {
|
||||
loading.classList.add('active');
|
||||
} else {
|
||||
loading.classList.remove('active');
|
||||
}
|
||||
}
|
||||
|
||||
// Event Listeners Setup
|
||||
function setupEventListeners() {
|
||||
// Handle Google OAuth
|
||||
document.getElementById('googleSignInBtn').addEventListener('click', signInWithGoogle);
|
||||
|
||||
// Handle Discord OAuth
|
||||
document.getElementById('discordSignInBtn').addEventListener('click', signInWithDiscord);
|
||||
|
||||
// Cookie settings button
|
||||
const cookieSettingsBtn = document.getElementById('cookie-settings-footer');
|
||||
if (cookieSettingsBtn) {
|
||||
cookieSettingsBtn.addEventListener('click', function() {
|
||||
if (window.cookieConsent) {
|
||||
window.cookieConsent.resetConsent();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Handle login
|
||||
document.getElementById('loginFormElement').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
clearMessage();
|
||||
showPasswordReset(false); // Hide reset button initially
|
||||
|
||||
const email = document.getElementById('loginEmail').value;
|
||||
const password = document.getElementById('loginPassword').value;
|
||||
|
||||
try {
|
||||
const { data, error } = await supabase.auth.signInWithPassword({
|
||||
email: email,
|
||||
password: password
|
||||
});
|
||||
|
||||
if (error) {
|
||||
showMessage(error.message, 'error');
|
||||
// Show password reset button on login failure
|
||||
showPasswordReset(true);
|
||||
} else {
|
||||
showMessage('Login successful! Redirecting...', 'success');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/';
|
||||
}, 1000);
|
||||
}
|
||||
} catch (error) {
|
||||
showMessage('An unexpected error occurred', 'error');
|
||||
showPasswordReset(true);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle registration
|
||||
document.getElementById('registerFormElement').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
clearMessage();
|
||||
|
||||
const email = document.getElementById('registerEmail').value;
|
||||
const password = document.getElementById('registerPassword').value;
|
||||
const confirmPassword = document.getElementById('confirmPassword').value;
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
showMessage('Passwords do not match', 'error');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (password.length < 6) {
|
||||
showMessage('Password must be at least 6 characters', 'error');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { data, error } = await supabase.auth.signUp({
|
||||
email: email,
|
||||
password: password
|
||||
});
|
||||
|
||||
if (error) {
|
||||
showMessage(error.message, 'error');
|
||||
} else {
|
||||
if (data.user && !data.user.email_confirmed_at) {
|
||||
showMessage('Registration successful! Please check your email to confirm your account.', 'success');
|
||||
} else {
|
||||
showMessage('Registration successful! Redirecting...', 'success');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/';
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
showMessage('An unexpected error occurred', 'error');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle password reset
|
||||
document.getElementById('resetPasswordBtn').addEventListener('click', async () => {
|
||||
const email = document.getElementById('loginEmail').value;
|
||||
|
||||
if (!email) {
|
||||
showMessage('Bitte geben Sie zuerst Ihre E-Mail-Adresse ein', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
clearMessage();
|
||||
|
||||
try {
|
||||
const { error } = await supabase.auth.resetPasswordForEmail(email, {
|
||||
redirectTo: `${window.location.origin}/reset-password.html`
|
||||
});
|
||||
|
||||
if (error) {
|
||||
showMessage('Fehler beim Senden der E-Mail: ' + error.message, 'error');
|
||||
} else {
|
||||
showMessage('Passwort-Reset-E-Mail wurde gesendet! Bitte überprüfen Sie Ihr E-Mail-Postfach.', 'success');
|
||||
showPasswordReset(false);
|
||||
}
|
||||
} catch (error) {
|
||||
showMessage('Ein unerwarteter Fehler ist aufgetreten', 'error');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize page
|
||||
async function init() {
|
||||
await checkAuth();
|
||||
setupEventListeners();
|
||||
}
|
||||
|
||||
// Start the application when DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
33
public/js/page-tracking.js
Normal file
33
public/js/page-tracking.js
Normal file
@@ -0,0 +1,33 @@
|
||||
// Page tracking functionality
|
||||
function trackPageView(pageName) {
|
||||
// Get user information
|
||||
const userAgent = navigator.userAgent;
|
||||
const referer = document.referrer || '';
|
||||
|
||||
// Send tracking data to server
|
||||
fetch('/api/v1/public/track-page-view', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
page: pageName,
|
||||
userAgent: userAgent,
|
||||
ipAddress: null, // Will be determined by server
|
||||
referer: referer
|
||||
})
|
||||
}).catch(error => {
|
||||
console.log('Page tracking failed:', error);
|
||||
// Silently fail - don't interrupt user experience
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-track page on load - only track main page visits
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Only track the main page (index.html or root path)
|
||||
const path = window.location.pathname;
|
||||
|
||||
if (path === '/' || path === '/index.html') {
|
||||
trackPageView('main_page_visit');
|
||||
}
|
||||
});
|
||||
202
public/js/reset-password.js
Normal file
202
public/js/reset-password.js
Normal file
@@ -0,0 +1,202 @@
|
||||
// Supabase Konfiguration
|
||||
const supabaseUrl = 'https://lfxlplnypzvjrhftaoog.supabase.co';
|
||||
const supabaseKey = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImxmeGxwbG55cHp2anJoZnRhb29nIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDkyMTQ3NzIsImV4cCI6MjA2NDc5MDc3Mn0.XR4preBqWAQ1rT4PFbpkmRdz57BTwIusBI89fIxDHM8';
|
||||
|
||||
const supabase = window.supabase.createClient(supabaseUrl, supabaseKey);
|
||||
|
||||
// DOM Elemente
|
||||
const resetForm = document.getElementById('resetForm');
|
||||
const newPasswordInput = document.getElementById('newPassword');
|
||||
const confirmPasswordInput = document.getElementById('confirmPassword');
|
||||
const resetBtn = document.getElementById('resetBtn');
|
||||
const loading = document.getElementById('loading');
|
||||
const messageContainer = document.getElementById('messageContainer');
|
||||
|
||||
// Nachricht anzeigen (muss vor anderen Funktionen definiert werden)
|
||||
function showMessage(type, message) {
|
||||
messageContainer.innerHTML = `
|
||||
<div class="message ${type}">
|
||||
${message}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// URL-Parameter extrahieren
|
||||
console.log('URL Hash:', window.location.hash);
|
||||
const urlParams = new URLSearchParams(window.location.hash.substring(1));
|
||||
const accessToken = urlParams.get('access_token');
|
||||
const refreshToken = urlParams.get('refresh_token');
|
||||
const tokenType = urlParams.get('token_type');
|
||||
|
||||
console.log('Access Token gefunden:', !!accessToken);
|
||||
console.log('Refresh Token gefunden:', !!refreshToken);
|
||||
|
||||
// Prüfen ob Reset-Token vorhanden ist
|
||||
if (!accessToken) {
|
||||
showMessage('error', 'Ungültiger oder fehlender Reset-Link. Bitte fordere einen neuen Reset-Link an.');
|
||||
resetForm.style.display = 'none';
|
||||
}
|
||||
|
||||
// Session mit Token setzen
|
||||
async function setSession() {
|
||||
if (!accessToken || !refreshToken) {
|
||||
showMessage('error', 'Ungültiger Reset-Link. Tokens fehlen.');
|
||||
resetForm.style.display = 'none';
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('Setze Session mit Tokens...');
|
||||
const { data, error } = await supabase.auth.setSession({
|
||||
access_token: accessToken,
|
||||
refresh_token: refreshToken
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error('Session Error:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
console.log('Session erfolgreich gesetzt:', data.user?.email);
|
||||
showMessage('success', `Session aktiv für: ${data.user?.email}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Setzen der Session:', error);
|
||||
showMessage('error', `Fehler beim Laden des Reset-Links: ${error.message}`);
|
||||
resetForm.style.display = 'none';
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Passwort zurücksetzen
|
||||
async function resetPassword(newPassword) {
|
||||
try {
|
||||
console.log('Starte Passwort-Update...');
|
||||
|
||||
// Erstmal Session prüfen
|
||||
const { data: session } = await supabase.auth.getSession();
|
||||
console.log('Aktuelle Session:', session);
|
||||
|
||||
if (!session.session) {
|
||||
throw new Error('Keine aktive Session gefunden');
|
||||
}
|
||||
|
||||
const { data, error } = await supabase.auth.updateUser({
|
||||
password: newPassword
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error('Update User Error:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
console.log('Passwort erfolgreich aktualisiert:', data);
|
||||
return { success: true, data };
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Zurücksetzen des Passworts:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
// Formular-Validierung
|
||||
function validateForm() {
|
||||
const newPassword = newPasswordInput.value;
|
||||
const confirmPassword = confirmPasswordInput.value;
|
||||
|
||||
if (newPassword.length < 8) {
|
||||
showMessage('error', 'Das Passwort muss mindestens 8 Zeichen lang sein.');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (newPassword !== confirmPassword) {
|
||||
showMessage('error', 'Die Passwörter stimmen nicht überein.');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Formular-Submit Handler
|
||||
resetForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!validateForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// UI-Status ändern
|
||||
resetBtn.disabled = true;
|
||||
loading.style.display = 'block';
|
||||
resetForm.style.display = 'none';
|
||||
|
||||
try {
|
||||
// Warten bis Session gesetzt ist
|
||||
const sessionSet = await setSession();
|
||||
if (!sessionSet) {
|
||||
throw new Error('Session konnte nicht gesetzt werden');
|
||||
}
|
||||
|
||||
const result = await resetPassword(newPasswordInput.value);
|
||||
|
||||
if (result.success) {
|
||||
showMessage('success', '✅ Passwort erfolgreich zurückgesetzt! Du wirst zur Hauptseite weitergeleitet...');
|
||||
|
||||
// Nach 3 Sekunden zur Hauptseite weiterleiten
|
||||
setTimeout(() => {
|
||||
window.location.href = '/';
|
||||
}, 3000);
|
||||
} else {
|
||||
showMessage('error', `❌ Fehler beim Zurücksetzen: ${result.error}`);
|
||||
resetForm.style.display = 'block';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Submit Error:', error);
|
||||
showMessage('error', `❌ Fehler: ${error.message}`);
|
||||
resetForm.style.display = 'block';
|
||||
} finally {
|
||||
resetBtn.disabled = false;
|
||||
loading.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
// Session beim Laden der Seite setzen (nur wenn Token vorhanden)
|
||||
if (accessToken && refreshToken) {
|
||||
setSession();
|
||||
}
|
||||
|
||||
// Add cookie settings button functionality
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const cookieSettingsBtn = document.getElementById('cookie-settings-footer');
|
||||
if (cookieSettingsBtn) {
|
||||
cookieSettingsBtn.addEventListener('click', function() {
|
||||
if (window.cookieConsent) {
|
||||
window.cookieConsent.resetConsent();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Passwort-Sicherheitshinweise
|
||||
newPasswordInput.addEventListener('input', function() {
|
||||
const password = this.value;
|
||||
const hasLength = password.length >= 8;
|
||||
const hasUpper = /[A-Z]/.test(password);
|
||||
const hasLower = /[a-z]/.test(password);
|
||||
const hasNumber = /\d/.test(password);
|
||||
const hasSpecial = /[!@#$%^&*(),.?":{}|<>]/.test(password);
|
||||
|
||||
if (password.length > 0) {
|
||||
let hints = [];
|
||||
if (!hasLength) hints.push('Mindestens 8 Zeichen');
|
||||
if (!hasUpper) hints.push('Großbuchstaben');
|
||||
if (!hasLower) hints.push('Kleinbuchstaben');
|
||||
if (!hasNumber) hints.push('Zahlen');
|
||||
if (!hasSpecial) hints.push('Sonderzeichen');
|
||||
|
||||
if (hints.length > 0) {
|
||||
showMessage('info', `💡 Tipp: Verwende auch ${hints.join(', ')} für ein sicheres Passwort.`);
|
||||
} else {
|
||||
showMessage('success', '✅ Starkes Passwort!');
|
||||
}
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user