feat(phase1): complete base structure - landing page, auth, dashboard, airports, API, DB schema, 60 airports CSV
This commit is contained in:
865
www/public/assets/js/main.js
Normal file
865
www/public/assets/js/main.js
Normal file
@@ -0,0 +1,865 @@
|
||||
/**
|
||||
* Airline Tycoon - Main JavaScript
|
||||
* Vanilla JS, Mobile-responsive, Touch-friendly
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// ============================================
|
||||
// GLOBAL STATE
|
||||
// ============================================
|
||||
const App = {
|
||||
config: {
|
||||
basePath: window.BASE_PATH || '/',
|
||||
apiTimeout: 10000
|
||||
},
|
||||
state: {
|
||||
theme: localStorage.getItem('theme') || 'light',
|
||||
map: null,
|
||||
markers: [],
|
||||
selectedAirport: null
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// INITIALIZATION
|
||||
// ============================================
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
initTheme();
|
||||
initNavigation();
|
||||
initModals();
|
||||
initToasts();
|
||||
initForms();
|
||||
initWorldMap();
|
||||
initTouchHandlers();
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// THEME MANAGEMENT
|
||||
// ============================================
|
||||
function initTheme() {
|
||||
const themeToggle = document.getElementById('themeToggle');
|
||||
|
||||
// System-Präferenz prüfen
|
||||
if (!localStorage.getItem('theme')) {
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
App.state.theme = prefersDark ? 'dark' : 'light';
|
||||
}
|
||||
|
||||
applyTheme(App.state.theme);
|
||||
|
||||
if (themeToggle) {
|
||||
themeToggle.addEventListener('click', toggleTheme);
|
||||
}
|
||||
}
|
||||
|
||||
function toggleTheme() {
|
||||
App.state.theme = App.state.theme === 'light' ? 'dark' : 'light';
|
||||
localStorage.setItem('theme', App.state.theme);
|
||||
applyTheme(App.state.theme);
|
||||
}
|
||||
|
||||
function applyTheme(theme) {
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
|
||||
const themeIcon = document.querySelector('#themeToggle, .theme-toggle');
|
||||
if (themeIcon) {
|
||||
themeIcon.textContent = theme === 'dark' ? '☀️' : '🌙';
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// NAVIGATION
|
||||
// ============================================
|
||||
function initNavigation() {
|
||||
const navbarToggle = document.querySelector('.navbar-toggle');
|
||||
const navbarMenu = document.querySelector('.navbar-menu');
|
||||
const navbarOverlay = document.querySelector('.navbar-overlay');
|
||||
|
||||
if (navbarToggle && navbarMenu) {
|
||||
navbarToggle.addEventListener('click', function() {
|
||||
navbarMenu.classList.toggle('active');
|
||||
navbarOverlay?.classList.toggle('active');
|
||||
});
|
||||
|
||||
navbarOverlay?.addEventListener('click', function() {
|
||||
navbarMenu.classList.remove('active');
|
||||
navbarOverlay?.classList.remove('active');
|
||||
});
|
||||
}
|
||||
|
||||
// Aktive Navigation highlight
|
||||
const currentPath = window.location.pathname;
|
||||
document.querySelectorAll('.navbar-item').forEach(item => {
|
||||
if (item.getAttribute('href') === currentPath) {
|
||||
item.classList.add('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// MODALS
|
||||
// ============================================
|
||||
function initModals() {
|
||||
document.querySelectorAll('[data-modal]').forEach(trigger => {
|
||||
trigger.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
const modalId = this.getAttribute('data-modal');
|
||||
openModal(modalId);
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.modal-overlay').forEach(overlay => {
|
||||
overlay.addEventListener('click', function(e) {
|
||||
if (e.target === this) {
|
||||
closeModal(this.id);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.modal-close').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
const modal = this.closest('.modal-overlay');
|
||||
if (modal) {
|
||||
closeModal(modal.id);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ESC Key zum Schließen
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') {
|
||||
document.querySelectorAll('.modal-overlay.active').forEach(modal => {
|
||||
closeModal(modal.id);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function openModal(id) {
|
||||
const modal = document.getElementById(id);
|
||||
if (modal) {
|
||||
modal.classList.add('active');
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
}
|
||||
|
||||
function closeModal(id) {
|
||||
const modal = document.getElementById(id);
|
||||
if (modal) {
|
||||
modal.classList.remove('active');
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// TOAST NOTIFICATIONS
|
||||
// ============================================
|
||||
function initToasts() {
|
||||
// Toasts werden dynamisch erstellt
|
||||
}
|
||||
|
||||
window.showToast = function(message, type = 'info', duration = 4000) {
|
||||
const container = document.getElementById('toastContainer') || createToastContainer();
|
||||
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast toast-${type}`;
|
||||
toast.innerHTML = `
|
||||
<span>${getToastIcon(type)}</span>
|
||||
<span>${escapeHtml(message)}</span>
|
||||
`;
|
||||
|
||||
container.appendChild(toast);
|
||||
|
||||
setTimeout(() => {
|
||||
toast.style.animation = 'slideIn 0.3s ease reverse';
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
}, duration);
|
||||
};
|
||||
|
||||
function createToastContainer() {
|
||||
const container = document.createElement('div');
|
||||
container.id = 'toastContainer';
|
||||
container.className = 'toast-container';
|
||||
document.body.appendChild(container);
|
||||
return container;
|
||||
}
|
||||
|
||||
function getToastIcon(type) {
|
||||
const icons = {
|
||||
success: '✅',
|
||||
error: '❌',
|
||||
warning: '⚠️',
|
||||
info: 'ℹ️'
|
||||
};
|
||||
return icons[type] || icons.info;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// FORM HANDLING
|
||||
// ============================================
|
||||
function initForms() {
|
||||
document.querySelectorAll('form[data-ajax]').forEach(form => {
|
||||
form.addEventListener('submit', handleAjaxForm);
|
||||
});
|
||||
|
||||
document.querySelectorAll('form').forEach(form => {
|
||||
// CSRF Token hinzufügen
|
||||
const csrfInput = form.querySelector('input[name="csrf_token"]');
|
||||
if (!csrfInput && form.dataset.csrf !== 'false') {
|
||||
const token = document.querySelector('meta[name="csrf-token"]');
|
||||
if (token) {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'hidden';
|
||||
input.name = 'csrf_token';
|
||||
input.value = token.content;
|
||||
form.appendChild(input);
|
||||
}
|
||||
}
|
||||
|
||||
// Form Validation
|
||||
form.addEventListener('submit', function(e) {
|
||||
if (!validateForm(this)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function handleAjaxForm(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const form = e.target;
|
||||
const submitBtn = form.querySelector('[type="submit"]');
|
||||
const originalText = submitBtn?.textContent;
|
||||
|
||||
try {
|
||||
if (submitBtn) {
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerHTML = '<span class="loading"></span>';
|
||||
}
|
||||
|
||||
const formData = new FormData(form);
|
||||
const response = await fetch(form.action || window.location.href, {
|
||||
method: form.method || 'POST',
|
||||
body: formData,
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
showToast(data.message || 'Erfolgreich!', 'success');
|
||||
|
||||
if (data.redirect) {
|
||||
setTimeout(() => {
|
||||
window.location.href = data.redirect;
|
||||
}, 1000);
|
||||
} else if (data.reset && form.dataset.reset !== 'false') {
|
||||
form.reset();
|
||||
}
|
||||
|
||||
// Callback
|
||||
if (typeof window[data.onSuccess] === 'function') {
|
||||
window[data.onSuccess](data);
|
||||
}
|
||||
} else {
|
||||
showToast(data.error || 'Ein Fehler ist aufgetreten', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Form submission error:', error);
|
||||
showToast('Netzwerkfehler. Bitte versuche es erneut.', 'error');
|
||||
} finally {
|
||||
if (submitBtn) {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = originalText;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function validateForm(form) {
|
||||
let isValid = true;
|
||||
|
||||
form.querySelectorAll('[required]').forEach(field => {
|
||||
if (!field.value.trim()) {
|
||||
isValid = false;
|
||||
showFieldError(field, 'Dieses Feld ist erforderlich');
|
||||
} else {
|
||||
clearFieldError(field);
|
||||
}
|
||||
});
|
||||
|
||||
// Email Validation
|
||||
form.querySelectorAll('[type="email"]').forEach(field => {
|
||||
if (field.value && !isValidEmail(field.value)) {
|
||||
isValid = false;
|
||||
showFieldError(field, 'Bitte gib eine gültige E-Mail ein');
|
||||
}
|
||||
});
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
function showFieldError(field, message) {
|
||||
clearFieldError(field);
|
||||
|
||||
field.classList.add('error');
|
||||
|
||||
const error = document.createElement('div');
|
||||
error.className = 'form-error';
|
||||
error.textContent = message;
|
||||
|
||||
field.parentNode.appendChild(error);
|
||||
}
|
||||
|
||||
function clearFieldError(field) {
|
||||
field.classList.remove('error');
|
||||
|
||||
const error = field.parentNode.querySelector('.form-error');
|
||||
if (error) {
|
||||
error.remove();
|
||||
}
|
||||
}
|
||||
|
||||
function isValidEmail(email) {
|
||||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// WORLD MAP
|
||||
// ============================================
|
||||
function initWorldMap() {
|
||||
const mapContainer = document.getElementById('mapContainer');
|
||||
if (!mapContainer) return;
|
||||
|
||||
// Map Canvas erstellen
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.id = 'mapCanvas';
|
||||
canvas.className = 'map-canvas';
|
||||
mapContainer.appendChild(canvas);
|
||||
|
||||
// Controls erstellen
|
||||
const controls = document.createElement('div');
|
||||
controls.className = 'map-controls';
|
||||
controls.innerHTML = `
|
||||
<button class="map-btn" id="mapZoomIn" title="Zoomen">+</button>
|
||||
<button class="map-btn" id="mapZoomOut" title="Verkleinern">−</button>
|
||||
<button class="map-btn" id="mapReset" title="Zurücksetzen">⟲</button>
|
||||
`;
|
||||
mapContainer.appendChild(controls);
|
||||
|
||||
// Tooltip erstellen
|
||||
const tooltip = document.createElement('div');
|
||||
tooltip.className = 'map-tooltip';
|
||||
tooltip.id = 'mapTooltip';
|
||||
mapContainer.appendChild(tooltip);
|
||||
|
||||
// Map State
|
||||
App.state.map = {
|
||||
canvas: canvas,
|
||||
ctx: canvas.getContext('2d'),
|
||||
zoom: 1,
|
||||
offsetX: 0,
|
||||
offsetY: 0,
|
||||
isDragging: false,
|
||||
lastX: 0,
|
||||
lastY: 0
|
||||
};
|
||||
|
||||
// Canvas Größe setzen
|
||||
resizeMapCanvas();
|
||||
window.addEventListener('resize', resizeMapCanvas);
|
||||
|
||||
// Map Events
|
||||
canvas.addEventListener('mousedown', mapMouseDown);
|
||||
canvas.addEventListener('mousemove', mapMouseMove);
|
||||
canvas.addEventListener('mouseup', mapMouseUp);
|
||||
canvas.addEventListener('mouseleave', mapMouseUp);
|
||||
canvas.addEventListener('wheel', mapWheel);
|
||||
|
||||
// Touch Events
|
||||
canvas.addEventListener('touchstart', mapTouchStart, { passive: false });
|
||||
canvas.addEventListener('touchmove', mapTouchMove, { passive: false });
|
||||
canvas.addEventListener('touchend', mapTouchEnd);
|
||||
|
||||
// Control Events
|
||||
document.getElementById('mapZoomIn')?.addEventListener('click', () => mapZoom(1.2));
|
||||
document.getElementById('mapZoomOut')?.addEventListener('click', () => mapZoom(0.8));
|
||||
document.getElementById('mapReset')?.addEventListener('click', mapReset);
|
||||
|
||||
// Map laden
|
||||
loadMapData();
|
||||
}
|
||||
|
||||
function resizeMapCanvas() {
|
||||
if (!App.state.map) return;
|
||||
|
||||
const rect = App.state.map.canvas.parentElement.getBoundingClientRect();
|
||||
App.state.map.canvas.width = rect.width;
|
||||
App.state.map.canvas.height = rect.height;
|
||||
renderMap();
|
||||
}
|
||||
|
||||
async function loadMapData() {
|
||||
try {
|
||||
const response = await fetch(App.config.basePath + 'api/airports.php?action=list');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.airports) {
|
||||
App.state.airports = data.airports;
|
||||
renderMap();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Map data loading error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function renderMap() {
|
||||
if (!App.state.map || !App.state.map.ctx) return;
|
||||
|
||||
const { canvas, ctx, zoom, offsetX, offsetY } = App.state.map;
|
||||
|
||||
// Canvas löschen
|
||||
ctx.fillStyle = getComputedStyle(document.documentElement)
|
||||
.getPropertyValue('--bg-tertiary').trim() || '#e8e0d4';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// Weltkarte zeichnen (vereinfacht)
|
||||
drawWorldMap(ctx, canvas.width, canvas.height, zoom, offsetX, offsetY);
|
||||
|
||||
// Flughäfen zeichnen
|
||||
if (App.state.airports) {
|
||||
App.state.airports.forEach(airport => {
|
||||
drawAirport(ctx, airport, zoom, offsetX, offsetY);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function drawWorldMap(ctx, width, height, zoom, offsetX, offsetY) {
|
||||
// Vereinfachte Weltkarte mit Kontinenten
|
||||
ctx.save();
|
||||
ctx.translate(width / 2 + offsetX, height / 2 + offsetY);
|
||||
ctx.scale(zoom, zoom);
|
||||
|
||||
// Ozean Hintergrund
|
||||
ctx.fillStyle = 'rgba(78, 205, 196, 0.1)';
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(0, 0, 800, 400, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// Vereinfachte Kontinente
|
||||
const continents = [
|
||||
// Nordamerika
|
||||
{ x: -300, y: -100, points: [[0,0], [100,20], [150,80], [100,150], [0,120], [-50,50]], color: '#6BCB77' },
|
||||
// Südamerika
|
||||
{ x: -100, y: 100, points: [[0,0], [50,20], [60,80], [30,150], [-20,100], [-30,40]], color: '#6BCB77' },
|
||||
// Europa
|
||||
{ x: 100, y: -80, points: [[0,0], [80,10], [100,50], [60,80], [10,60], [-20,20]], color: '#FFD93D' },
|
||||
// Afrika
|
||||
{ x: 120, y: 60, points: [[0,0], [70,10], [90,60], [50,120], [0,80], [-30,30]], color: '#FFD93D' },
|
||||
// Asien
|
||||
{ x: 350, y: -50, points: [[0,0], [150,20], [200,80], [180,150], [100,100], [30,30]], color: '#FF9F43' },
|
||||
// Australien
|
||||
{ x: 450, y: 150, points: [[0,0], [80,20], [90,70], [50,100], [-10,60], [-30,20]], color: '#FF6B6B' }
|
||||
];
|
||||
|
||||
continents.forEach(continent => {
|
||||
ctx.fillStyle = continent.color + '40';
|
||||
ctx.strokeStyle = continent.color;
|
||||
ctx.lineWidth = 2 / zoom;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(continent.x + continent.points[0][0], continent.y + continent.points[0][1]);
|
||||
continent.points.forEach((point, i) => {
|
||||
if (i > 0) ctx.lineTo(continent.x + point[0], continent.y + point[1]);
|
||||
});
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
});
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function drawAirport(ctx, airport, zoom, offsetX, offsetY) {
|
||||
const { canvas } = App.state.map;
|
||||
|
||||
// Koordinaten zu Pixeln konvertieren
|
||||
const x = canvas.width / 2 + offsetX + (airport.longitude * 8 * zoom);
|
||||
const y = canvas.height / 2 + offsetY + (airport.latitude * -5 * zoom);
|
||||
|
||||
// Sichtbarkeit prüfen
|
||||
if (x < -20 || x > canvas.width + 20 || y < -20 || y > canvas.height + 20) return;
|
||||
|
||||
ctx.save();
|
||||
|
||||
// Airport Marker
|
||||
const isSelected = App.state.selectedAirport?.id === airport.id;
|
||||
const radius = (isSelected ? 12 : 8) / zoom;
|
||||
|
||||
// Glow für ausgewählten Flughafen
|
||||
if (isSelected) {
|
||||
ctx.fillStyle = 'rgba(255, 107, 107, 0.3)';
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, radius * 2, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
// Hauptmarker
|
||||
ctx.fillStyle = isSelected ? '#FF6B6B' : '#4ECDC4';
|
||||
ctx.strokeStyle = '#2c2416';
|
||||
ctx.lineWidth = 2 / zoom;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, radius, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
|
||||
// IATA Code
|
||||
ctx.fillStyle = '#2c2416';
|
||||
ctx.font = `${10 / zoom}px Nunito, sans-serif`;
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(airport.iata_code, x, y + radius + 14 / zoom);
|
||||
|
||||
ctx.restore();
|
||||
|
||||
// Airport in Marker-Array speichern
|
||||
airport.pixelX = x;
|
||||
airport.pixelY = y;
|
||||
}
|
||||
|
||||
function getAirportFromPixel(x, y) {
|
||||
if (!App.state.airports) return null;
|
||||
|
||||
return App.state.airports.find(airport => {
|
||||
const dx = Math.abs(airport.pixelX - x);
|
||||
const dy = Math.abs(airport.pixelY - y);
|
||||
return dx < 20 && dy < 20;
|
||||
});
|
||||
}
|
||||
|
||||
function mapMouseDown(e) {
|
||||
const rect = App.state.map.canvas.getBoundingClientRect();
|
||||
App.state.map.isDragging = true;
|
||||
App.state.map.lastX = e.clientX - rect.left;
|
||||
App.state.map.lastY = e.clientY - rect.top;
|
||||
App.state.map.dragStartTime = Date.now();
|
||||
App.state.map.dragStartX = App.state.map.offsetX;
|
||||
App.state.map.dragStartY = App.state.map.offsetY;
|
||||
}
|
||||
|
||||
function mapMouseMove(e) {
|
||||
const rect = App.state.map.canvas.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
|
||||
if (App.state.map.isDragging) {
|
||||
const dx = x - App.state.map.lastX;
|
||||
const dy = y - App.state.map.lastY;
|
||||
|
||||
App.state.map.offsetX += dx;
|
||||
App.state.map.offsetY += dy;
|
||||
|
||||
App.state.map.lastX = x;
|
||||
App.state.map.lastY = y;
|
||||
|
||||
renderMap();
|
||||
} else {
|
||||
// Tooltip bei Hover
|
||||
const airport = getAirportFromPixel(x, y);
|
||||
const tooltip = document.getElementById('mapTooltip');
|
||||
|
||||
if (airport) {
|
||||
tooltip.innerHTML = `
|
||||
<strong>${airport.iata_code}</strong> - ${airport.name}<br>
|
||||
<small>${airport.city}, ${airport.country}</small><br>
|
||||
<small>Passagiere: ${formatNumber(airport.passenger_volume)}/Jahr</small>
|
||||
`;
|
||||
tooltip.style.left = (x + 15) + 'px';
|
||||
tooltip.style.top = (y - 10) + 'px';
|
||||
tooltip.classList.add('visible');
|
||||
App.state.map.canvas.style.cursor = 'pointer';
|
||||
} else {
|
||||
tooltip.classList.remove('visible');
|
||||
App.state.map.canvas.style.cursor = 'grab';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function mapMouseUp(e) {
|
||||
if (App.state.map.isDragging) {
|
||||
const dragDuration = Date.now() - App.state.map.dragStartTime;
|
||||
const dragDistance = Math.abs(App.state.map.offsetX - App.state.map.dragStartX) +
|
||||
Math.abs(App.state.map.offsetY - App.state.map.dragStartY);
|
||||
|
||||
// Klick erkennen (kurze Dauer, little movement)
|
||||
if (dragDuration < 200 && dragDistance < 5) {
|
||||
const rect = App.state.map.canvas.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
const airport = getAirportFromPixel(x, y);
|
||||
|
||||
if (airport) {
|
||||
selectAirport(airport);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
App.state.map.isDragging = false;
|
||||
}
|
||||
|
||||
function mapWheel(e) {
|
||||
e.preventDefault();
|
||||
const delta = e.deltaY > 0 ? 0.9 : 1.1;
|
||||
mapZoom(delta);
|
||||
}
|
||||
|
||||
function mapTouchStart(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (e.touches.length === 1) {
|
||||
const touch = e.touches[0];
|
||||
const rect = App.state.map.canvas.getBoundingClientRect();
|
||||
|
||||
App.state.map.isDragging = true;
|
||||
App.state.map.lastX = touch.clientX - rect.left;
|
||||
App.state.map.lastY = touch.clientY - rect.top;
|
||||
App.state.map.dragStartTime = Date.now();
|
||||
App.state.map.dragStartX = App.state.map.offsetX;
|
||||
App.state.map.dragStartY = App.state.map.offsetY;
|
||||
App.state.map.touchStartX = touch.clientX;
|
||||
App.state.map.touchStartY = touch.clientY;
|
||||
}
|
||||
}
|
||||
|
||||
function mapTouchMove(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (e.touches.length === 1 && App.state.map.isDragging) {
|
||||
const touch = e.touches[0];
|
||||
const rect = App.state.map.canvas.getBoundingClientRect();
|
||||
const x = touch.clientX - rect.left;
|
||||
const y = touch.clientY - rect.top;
|
||||
|
||||
const dx = x - App.state.map.lastX;
|
||||
const dy = y - App.state.map.lastY;
|
||||
|
||||
App.state.map.offsetX += dx;
|
||||
App.state.map.offsetY += dy;
|
||||
|
||||
App.state.map.lastX = x;
|
||||
App.state.map.lastY = y;
|
||||
|
||||
renderMap();
|
||||
}
|
||||
}
|
||||
|
||||
function mapTouchEnd(e) {
|
||||
if (App.state.map.isDragging) {
|
||||
const touch = e.changedTouches[0];
|
||||
const dragDuration = Date.now() - App.state.map.dragStartTime;
|
||||
const dragDistance = Math.abs(touch.clientX - App.state.map.touchStartX) +
|
||||
Math.abs(touch.clientY - App.state.map.touchStartY);
|
||||
|
||||
// Tap erkennen
|
||||
if (dragDuration < 200 && dragDistance < 10) {
|
||||
const rect = App.state.map.canvas.getBoundingClientRect();
|
||||
const x = touch.clientX - rect.left;
|
||||
const y = touch.clientY - rect.top;
|
||||
const airport = getAirportFromPixel(x, y);
|
||||
|
||||
if (airport) {
|
||||
selectAirport(airport);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
App.state.map.isDragging = false;
|
||||
}
|
||||
|
||||
function mapZoom(factor) {
|
||||
const newZoom = Math.max(0.5, Math.min(4, App.state.map.zoom * factor));
|
||||
|
||||
// Zoom um Mausposition
|
||||
const rect = App.state.map.canvas.getBoundingClientRect();
|
||||
const mouseX = App.state.map.lastX - rect.width / 2;
|
||||
const mouseY = App.state.map.lastY - rect.height / 2;
|
||||
|
||||
App.state.map.offsetX = mouseX - (mouseX - App.state.map.offsetX) * factor;
|
||||
App.state.map.offsetY = mouseY - (mouseY - App.state.map.offsetY) * factor;
|
||||
App.state.map.zoom = newZoom;
|
||||
|
||||
renderMap();
|
||||
}
|
||||
|
||||
function mapReset() {
|
||||
App.state.map.zoom = 1;
|
||||
App.state.map.offsetX = 0;
|
||||
App.state.map.offsetY = 0;
|
||||
renderMap();
|
||||
}
|
||||
|
||||
function selectAirport(airport) {
|
||||
App.state.selectedAirport = airport;
|
||||
renderMap();
|
||||
|
||||
// Airport Info Panel aktualisieren
|
||||
const infoPanel = document.getElementById('airportInfo');
|
||||
if (infoPanel && airport) {
|
||||
infoPanel.innerHTML = `
|
||||
<h3>${airport.iata_code} - ${airport.name}</h3>
|
||||
<p><strong>Stadt:</strong> ${airport.city}, ${airport.country}</p>
|
||||
<p><strong>Kontinent:</strong> ${airport.continent}</p>
|
||||
<p><strong>Passagiere:</strong> ${formatNumber(airport.passenger_volume)}/Jahr</p>
|
||||
<p><strong>Nachfrage:</strong> ${(airport.demand_multiplier * 100).toFixed(0)}%</p>
|
||||
<button class="btn btn-primary mt-md" onclick="createRouteFromAirport(${airport.id})">
|
||||
Route erstellen
|
||||
</button>
|
||||
`;
|
||||
infoPanel.classList.add('active');
|
||||
}
|
||||
|
||||
// Event emit
|
||||
document.dispatchEvent(new CustomEvent('airportSelected', { detail: airport }));
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// TOUCH HANDLERS
|
||||
// ============================================
|
||||
function initTouchHandlers() {
|
||||
// Touch-freundliche Button-Press-Effekte
|
||||
document.querySelectorAll('.btn').forEach(btn => {
|
||||
btn.addEventListener('touchstart', function() {
|
||||
this.classList.add('pressed');
|
||||
}, { passive: true });
|
||||
|
||||
btn.addEventListener('touchend', function() {
|
||||
this.classList.remove('pressed');
|
||||
}, { passive: true });
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// API HELPERS
|
||||
// ============================================
|
||||
window.api = {
|
||||
async get(endpoint, params = {}) {
|
||||
const queryString = new URLSearchParams(params).toString();
|
||||
const url = App.config.basePath + 'api/' + endpoint + (queryString ? '?' + queryString : '');
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
});
|
||||
|
||||
return response.json();
|
||||
},
|
||||
|
||||
async post(endpoint, data = {}) {
|
||||
const formData = new FormData();
|
||||
Object.keys(data).forEach(key => formData.append(key, data[key]));
|
||||
|
||||
const response = await fetch(App.config.basePath + 'api/' + endpoint, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
});
|
||||
|
||||
return response.json();
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// GAME ACTIONS
|
||||
// ============================================
|
||||
window.createRouteFromAirport = async function(fromAirportId) {
|
||||
const modal = document.getElementById('createRouteModal');
|
||||
if (modal) {
|
||||
document.getElementById('routeFromAirport').value = fromAirportId;
|
||||
openModal('createRouteModal');
|
||||
}
|
||||
};
|
||||
|
||||
window.executeFlight = async function(routeId, aircraftId) {
|
||||
try {
|
||||
const result = await api.post('game.php?action=flight', {
|
||||
route_id: routeId,
|
||||
aircraft_id: aircraftId
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
showToast(`Flug erfolgreich! +${formatMoney(result.profit)}`, 'success');
|
||||
|
||||
// Dashboard aktualisieren
|
||||
if (typeof updateDashboard === 'function') {
|
||||
updateDashboard();
|
||||
}
|
||||
} else {
|
||||
showToast(result.error || 'Flug fehlgeschlagen', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showToast('Netzwerkfehler', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
window.purchaseAircraft = async function(type) {
|
||||
try {
|
||||
const result = await api.post('game.php?action=purchase', {
|
||||
aircraft_type: type
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
showToast(`${type} gekauft für ${formatMoney(result.price)}`, 'success');
|
||||
|
||||
// Modal schließen
|
||||
const modal = document.getElementById('purchaseAircraftModal');
|
||||
if (modal) closeModal('purchaseAircraftModal');
|
||||
|
||||
// Dashboard aktualisieren
|
||||
if (typeof updateDashboard === 'function') {
|
||||
updateDashboard();
|
||||
}
|
||||
} else {
|
||||
showToast(result.error || 'Kauf fehlgeschlagen', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showToast('Netzwerkfehler', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// UTILITIES
|
||||
// ============================================
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function formatNumber(num) {
|
||||
return new Intl.NumberFormat('de-DE').format(num);
|
||||
}
|
||||
|
||||
function formatMoney(amount) {
|
||||
return new Intl.NumberFormat('de-DE', {
|
||||
style: 'currency',
|
||||
currency: 'EUR'
|
||||
}).format(amount);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// EXPORTS
|
||||
// ============================================
|
||||
window.App = App;
|
||||
window.showToast = showToast;
|
||||
window.openModal = openModal;
|
||||
window.closeModal = closeModal;
|
||||
|
||||
})();
|
||||
Reference in New Issue
Block a user