/** * CarCache Editor for StreetRacer Web Dashboard * Allows VIP users to create CarCaches with checkpoints and quizzes via Mapbox * Uses modal dialogs for checkpoint/quiz editing */ class CarCacheEditor { constructor() { this.map = null; this.isInitialized = false; this.currentTool = 'select'; this.checkpoints = []; // { name, lat, lng, quiz: { question, answers, correctIndex } } this.markers = []; this.currentCacheId = null; this.mapboxToken = 'pk.eyJ1IjoiZnJhbmNvaXNyZWluZXJ0IiwiYSI6ImNtazBja2liYjBvbnozZnM4bWE2OGF0b3UifQ.3k-VptMsIpvD0ys4igFWeg'; console.log('CarCacheEditor constructed'); } reset() { if (this.map) { this.map.remove(); this.map = null; } this.isInitialized = false; this.checkpoints = []; this.markers = []; this.currentCacheId = null; } initialize() { if (this.isInitialized && this.map) { this.map.resize(); return; } // Check VIP access (permanent_vip, subscriber, or unlocked via credits) const authHandler = window.authHandler; const hasAccess = authHandler && ( authHandler.accessLevel === 'permanent_vip' || authHandler.accessLevel === 'subscriber' || authHandler.unlockedFeatures?.['carcache-editor'] ); const gate = document.getElementById('carcache-vip-gate'); const content = document.getElementById('carcache-editor-content'); if (!hasAccess) { gate.style.display = 'block'; content.style.display = 'none'; return; } gate.style.display = 'none'; content.style.display = 'grid'; let attempts = 0; const checkAndInit = () => { attempts++; const container = document.getElementById('carcache-editor-map'); const tabContent = document.getElementById('tab-carcache-editor'); const isTabVisible = tabContent && !tabContent.classList.contains('hidden'); const width = container?.getBoundingClientRect().width || 0; const height = container?.getBoundingClientRect().height || 0; if (container && width > 100 && height > 100 && isTabVisible) { this.initializeMap(); this.setupToolbar(); this.loadMyCaches(); } else if (attempts < 30) { setTimeout(checkAndInit, 150); } else if (container) { container.style.width = '100%'; container.style.height = '500px'; setTimeout(() => { this.initializeMap(); this.setupToolbar(); this.loadMyCaches(); }, 100); } }; setTimeout(checkAndInit, 50); } initializeMap() { const container = document.getElementById('carcache-editor-map'); if (!container) return; try { mapboxgl.accessToken = this.mapboxToken; this.map = new mapboxgl.Map({ container: 'carcache-editor-map', style: 'mapbox://styles/mapbox/dark-v11', center: [10.45, 51.16], zoom: 6, }); this.map.addControl(new mapboxgl.NavigationControl(), 'top-right'); this.map.on('load', () => { this.isInitialized = true; console.log('CarCache editor map loaded'); }); this.map.on('click', (e) => { if (this.currentTool === 'checkpoint') { this.addCheckpoint(e.lngLat.lat, e.lngLat.lng); } }); // Try to center on user's location if (navigator.geolocation) { navigator.geolocation.getCurrentPosition(pos => { this.map.flyTo({ center: [pos.coords.longitude, pos.coords.latitude], zoom: 13 }); }, () => {}); } } catch (err) { console.error('Error initializing CarCache map:', err); } } setupToolbar() { document.querySelectorAll('[data-cctool]').forEach(btn => { btn.addEventListener('click', (e) => { const tool = e.currentTarget.dataset.cctool; this.setTool(tool); }); }); document.getElementById('cc-btn-undo')?.addEventListener('click', () => { if (this.checkpoints.length > 0) { this.removeCheckpoint(this.checkpoints.length - 1); } }); document.getElementById('cc-btn-clear')?.addEventListener('click', () => { if (this.checkpoints.length > 0 && confirm('Alle Checkpoints entfernen?')) { this.clearAll(); } }); document.getElementById('cc-btn-save')?.addEventListener('click', () => { this.saveCache(); }); document.getElementById('cc-btn-new')?.addEventListener('click', () => { this.newCache(); }); } setTool(tool) { this.currentTool = tool; document.querySelectorAll('[data-cctool]').forEach(btn => { btn.classList.toggle('active', btn.dataset.cctool === tool); }); if (this.map) { this.map.getCanvas().style.cursor = tool === 'checkpoint' ? 'crosshair' : ''; } } addCheckpoint(lat, lng) { const idx = this.checkpoints.length; const cp = { name: `Checkpoint ${idx + 1}`, lat, lng, quiz: idx > 0 ? { question: '', answers: ['', '', '', ''], correctIndex: 0 } : null, }; this.checkpoints.push(cp); const el = document.createElement('div'); const isStart = idx === 0; el.style.cssText = `width:30px;height:30px;border-radius:50%;background:${isStart ? '#2ecc71' : '#E040FB'};color:#fff;font-weight:bold;font-size:13px;display:flex;align-items:center;justify-content:center;border:2px solid #fff;cursor:grab;`; el.textContent = idx + 1; const marker = new mapboxgl.Marker({ element: el, draggable: true }) .setLngLat([lng, lat]) .addTo(this.map); marker.on('dragend', () => { const pos = marker.getLngLat(); cp.lat = pos.lat; cp.lng = pos.lng; }); // Click marker to open edit modal el.addEventListener('click', (e) => { e.stopPropagation(); this.openCheckpointModal(idx); }); this.markers.push(marker); this.updateUI(); } removeCheckpoint(idx) { if (this.markers[idx]) { this.markers[idx].remove(); } this.checkpoints.splice(idx, 1); this.markers.splice(idx, 1); // Recreate markers with correct numbers this.markers.forEach(m => m.remove()); this.markers = []; this.checkpoints.forEach((cp, i) => { const el = document.createElement('div'); const isStart = i === 0; el.style.cssText = `width:30px;height:30px;border-radius:50%;background:${isStart ? '#2ecc71' : '#E040FB'};color:#fff;font-weight:bold;font-size:13px;display:flex;align-items:center;justify-content:center;border:2px solid #fff;cursor:grab;`; el.textContent = i + 1; const marker = new mapboxgl.Marker({ element: el, draggable: true }) .setLngLat([cp.lng, cp.lat]) .addTo(this.map); marker.on('dragend', () => { const pos = marker.getLngLat(); cp.lat = pos.lat; cp.lng = pos.lng; }); el.addEventListener('click', (ev) => { ev.stopPropagation(); this.openCheckpointModal(i); }); this.markers.push(marker); if (i === 0) cp.quiz = null; else if (!cp.quiz) cp.quiz = { question: '', answers: ['', '', '', ''], correctIndex: 0 }; }); this.updateUI(); } clearAll() { this.markers.forEach(m => m.remove()); this.markers = []; this.checkpoints = []; this.currentCacheId = null; this.updateUI(); } newCache() { this.clearAll(); document.getElementById('cc-name').value = ''; document.getElementById('cc-description').value = ''; document.getElementById('cc-status').textContent = 'Nicht gespeichert'; this.setTool('checkpoint'); } updateUI() { document.getElementById('cc-checkpoint-count').textContent = this.checkpoints.length; this.renderCheckpointList(); } // Compact sidebar list - click to open modal renderCheckpointList() { const container = document.getElementById('cc-checkpoints-list'); if (!container) return; if (this.checkpoints.length === 0) { container.innerHTML = '
Klicke auf die Karte um Checkpoints zu setzen
'; return; } container.innerHTML = this.checkpoints.map((cp, i) => { const isStart = i === 0; const color = isStart ? '#2ecc71' : '#E040FB'; const hasQuiz = cp.quiz && cp.quiz.question; const quizIcon = isStart ? 'Start' : (hasQuiz ? 'Quiz OK' : 'Quiz fehlt'); return `
${i + 1}
${this.escHtml(cp.name)}
${cp.lat.toFixed(4)}, ${cp.lng.toFixed(4)}
${quizIcon}
`; }).join(''); } // Modal for editing a single checkpoint + quiz openCheckpointModal(idx) { const cp = this.checkpoints[idx]; if (!cp) return; // Fly to checkpoint if (this.map) this.map.flyTo({ center: [cp.lng, cp.lat], zoom: 15, duration: 800 }); // Remove existing modal document.getElementById('cc-edit-modal')?.remove(); const isStart = idx === 0; const color = isStart ? '#2ecc71' : '#E040FB'; let quizSection = ''; if (!isStart && cp.quiz) { quizSection = `
quiz Quiz-Frage
${cp.quiz.answers.map((a, ai) => `
${String.fromCharCode(65 + ai)}
`).join('')}
Korrekte Antwort markieren
`; } const modal = document.createElement('div'); modal.id = 'cc-edit-modal'; modal.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.75);display:flex;align-items:center;justify-content:center;z-index:10000;padding:20px;'; modal.innerHTML = `
${idx + 1}
Checkpoint ${idx + 1}${isStart ? ' (Start)' : ''} place ${cp.lat.toFixed(5)}, ${cp.lng.toFixed(5)}
${quizSection}
`; document.body.appendChild(modal); // Close handlers document.getElementById('cc-modal-close').onclick = () => modal.remove(); modal.addEventListener('click', (e) => { if (e.target === modal) modal.remove(); }); document.addEventListener('keydown', function escHandler(e) { if (e.key === 'Escape') { modal.remove(); document.removeEventListener('keydown', escHandler); } }); // Delete document.getElementById('cc-modal-delete').onclick = () => { modal.remove(); this.removeCheckpoint(idx); }; // Save document.getElementById('cc-modal-save').onclick = () => { cp.name = document.getElementById('cc-modal-name').value.trim() || `Checkpoint ${idx + 1}`; if (!isStart && cp.quiz) { cp.quiz.question = document.getElementById('cc-modal-quiz-q').value.trim(); const checked = modal.querySelector('input[name="cc-modal-correct"]:checked'); if (checked) cp.quiz.correctIndex = parseInt(checked.value); modal.querySelectorAll('.cc-modal-answer').forEach(inp => { const ai = parseInt(inp.dataset.ai); cp.quiz.answers[ai] = inp.value.trim(); }); } modal.remove(); this.renderCheckpointList(); }; } escHtml(str) { if (!str) return ''; return str.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } async saveCache() { const name = document.getElementById('cc-name').value.trim(); const description = document.getElementById('cc-description').value.trim(); if (!name) { this.showToast('Bitte Cache-Name eingeben'); return; } if (this.checkpoints.length < 3) { this.showToast('Mindestens 3 Checkpoints erforderlich'); return; } for (let i = 1; i < this.checkpoints.length; i++) { const cp = this.checkpoints[i]; if (!cp.quiz?.question) { this.showToast(`Checkpoint ${i + 1}: Quiz-Frage fehlt`); return; } const filledAnswers = cp.quiz.answers.filter(a => a.trim()); if (filledAnswers.length < 2) { this.showToast(`Checkpoint ${i + 1}: Mindestens 2 Antworten erforderlich`); return; } } const user = auth.currentUser; if (!user) return; const cacheData = { name, description: description || name, checkpoints: this.checkpoints.map((cp, i) => ({ name: cp.name, lat: cp.lat, lng: cp.lng, quiz: cp.quiz || null, })), totalCheckpoints: this.checkpoints.length, centerLat: this.checkpoints[0].lat, centerLng: this.checkpoints[0].lng, radiusKm: 50, creatorUid: user.uid, status: 'pending', createdAt: Date.now(), }; let nickname = 'Unbekannt'; try { const nickSnap = await database.ref(`cars/${user.uid}/nickname`).once('value'); nickname = nickSnap.val() || 'Unbekannt'; } catch (e) {} const cacheId = this.currentCacheId || `cc-${user.uid}-${Date.now()}`; try { await database.ref(`carcaches/${cacheId}`).set(cacheData); await database.ref(`carcacheReviews/${cacheId}`).set({ cacheData, creatorUid: user.uid, creatorNickname: nickname, status: 'pending', submittedAt: Date.now(), }); this.currentCacheId = cacheId; document.getElementById('cc-status').textContent = 'Eingereicht (Pending)'; this.showToast('CarCache gespeichert und zur Moderation eingereicht!'); this.loadMyCaches(); } catch (e) { console.error('Error saving cache:', e); this.showToast('Fehler beim Speichern: ' + e.message); } } async loadMyCaches() { const container = document.getElementById('cc-my-caches'); if (!container) return; const user = auth.currentUser; if (!user) { container.innerHTML = '
Nicht angemeldet
'; return; } try { const snap = await database.ref('carcaches').orderByChild('creatorUid').equalTo(user.uid).once('value'); const caches = snap.val() || {}; const entries = Object.entries(caches); if (entries.length === 0) { container.innerHTML = '
Noch keine Caches erstellt
'; return; } container.innerHTML = entries.map(([id, cache]) => { const isActive = this.currentCacheId === id; const statusColors = { pending: '#f39c12', active: '#2ecc71', rejected: '#e74c3c' }; const statusColor = statusColors[cache.status] || '#888'; return `
${this.escHtml(cache.name || 'Unbenannt')} ${cache.status || 'pending'}
${cache.totalCheckpoints || 0} Checkpoints
`; }).join(''); } catch (e) { console.error('Error loading caches:', e); container.innerHTML = '
Fehler beim Laden
'; } } async loadCache(cacheId) { try { const snap = await database.ref(`carcaches/${cacheId}`).once('value'); const cache = snap.val(); if (!cache) return; this.clearAll(); this.currentCacheId = cacheId; document.getElementById('cc-name').value = cache.name || ''; document.getElementById('cc-description').value = cache.description || ''; document.getElementById('cc-status').textContent = cache.status || 'pending'; const cps = cache.checkpoints || []; const bounds = new mapboxgl.LngLatBounds(); cps.forEach((cp, i) => { this.addCheckpoint(cp.lat, cp.lng); this.checkpoints[i].name = cp.name || `Checkpoint ${i + 1}`; if (i > 0 && cp.quiz) { this.checkpoints[i].quiz = { question: cp.quiz.question || '', answers: cp.quiz.answers || ['', '', '', ''], correctIndex: cp.quiz.correctIndex || 0, }; } bounds.extend([cp.lng, cp.lat]); }); this.updateUI(); if (this.map && cps.length > 0) { this.map.fitBounds(bounds, { padding: 60 }); } this.loadMyCaches(); this.setTool('select'); } catch (e) { console.error('Error loading cache:', e); } } showToast(message) { if (window.authHandler?.showToast) { window.authHandler.showToast(message); } else { alert(message); } } } // Initialize window.carcacheEditor = new CarCacheEditor();