Файловый менеджер - Редактировать - /home/gqdcvggs/imators.systems/traffic/index.php
Назад
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> <meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"> <meta name="theme-color" content="#0ea5e9"> <title>TrafficLight Systems</title> <link rel="icon" type="image/png" href="traffic_logo.png"> <link rel="apple-touch-icon" href="traffic_logo.png"> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"> <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" /> <script src="https://cdn.tailwindcss.com"></script> <script src="./account.js"></script> <script> tailwind.config = { theme: { extend: { fontFamily: { 'sans': ['Inter', 'sans-serif'] }, colors: { primary: { 50: '#f0f9ff', 100: '#e0f2fe', 200: '#bae6fd', 300: '#7dd3fc', 400: '#38bdf8', 500: '#0ea5e9', 600: '#0284c7', 700: '#0369a1', 800: '#075985', 900: '#0c4a6e' }, traffic: { red: '#ef4444', green: '#10b981', amber: '#f59e0b' } } } } } </script> <style> body { font-family: 'Inter', sans-serif; overscroll-behavior-y: contain; } #map { height: 100vh; width: 100%; z-index: 10; } .header { z-index: 30; background: rgba(255, 255, 255, 0.9); backdrop-filter: blur(10px); box-shadow: 0 1px 10px rgba(0,0,0,0.05); position: absolute; top: 0; left: 0; right: 0; } .sidebar { transform: translateX(-100%); transition: transform 0.3s ease-in-out; z-index: 20; box-shadow: 0 4px 20px rgba(0,0,0,0.1); border-radius: 0 16px 16px 0; overflow: hidden; position: absolute; top: 64px; left: 0; height: calc(100vh - 64px); } .sidebar.active { transform: translateX(0); } .modal { display: none; backdrop-filter: blur(8px); z-index: 50; } .modal-content { max-height: 90vh; overflow-y: auto; } .location-dot { width: 20px; height: 20px; background-color: #0ea5e9; border-radius: 50%; border: 3px solid white; box-shadow: 0 0 0 2px rgba(14, 165, 233, 0.3); animation: pulse 2s infinite; } @keyframes pulse { 0% { box-shadow: 0 0 0 0 rgba(14, 165, 233, 0.7); } 70% { box-shadow: 0 0 0 15px rgba(14, 165, 233, 0); } 100% { box-shadow: 0 0 0 0 rgba(14, 165, 233, 0); } } .fade-in { animation: fadeIn 0.3s; } @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } .popup { z-index: 40; } .spin { animation: spin 1s linear infinite; } @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } .swipe-indicator { width: 40px; height: 5px; background-color: #e2e8f0; border-radius: 10px; margin: 0 auto; } .onboarding-dot { width: 8px; height: 8px; border-radius: 50%; background-color: #cbd5e0; transition: all 0.3s ease; } .onboarding-dot.active { background-color: #0ea5e9; width: 12px; height: 12px; } .dark-mode { background-color: #1a1a1a; color: #fff; } .toggle-switch { position: relative; display: inline-block; width: 46px; height: 24px; } .toggle-switch 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%; } input:checked + .toggle-slider { background-color: #0ea5e9; } input:checked + .toggle-slider:before { transform: translateX(22px); } .light-card { transition: all 0.2s ease; border-radius: 12px; border-left: 4px solid transparent; } .light-card:hover { transform: translateY(-2px); box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); } .light-card.active { border-left-color: #0ea5e9; } .draggable-modal { touch-action: none; user-select: none; } .modal-overlay { transition: opacity 0.3s ease; } .modal-body { transform: translateY(0); transition: transform 0.3s ease; } .modal-body.slide-down { transform: translateY(100%); } .floating-menu { position: fixed; bottom: 24px; right: 24px; z-index: 25; display: flex; flex-direction: column; align-items: flex-end; gap: 12px; } .menu-items { display: none; flex-direction: column; gap: 12px; } .menu-items.active { display: flex; animation: fadeIn 0.3s; } .fab-btn { width: 56px; height: 56px; border-radius: 28px; display: flex; align-items: center; justify-content: center; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); transition: all 0.2s ease; } .fab-btn:hover { transform: scale(1.05); box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2); } .fab-btn:active { transform: scale(0.95); } .menu-btn { width: 48px; height: 48px; border-radius: 24px; display: flex; align-items: center; justify-content: center; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); transition: all 0.2s ease; } .menu-btn:hover { transform: scale(1.05); box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2); } .menu-btn:active { transform: scale(0.95); } .menu-btn-label { position: absolute; right: 60px; background-color: #ffffff; padding: 6px 12px; border-radius: 6px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); font-size: 14px; font-weight: 500; white-space: nowrap; } @media (display-mode: standalone) { .header { padding-top: env(safe-area-inset-top); height: calc(64px + env(safe-area-inset-top)); } #map { height: calc(100vh - env(safe-area-inset-top)); } .sidebar { top: calc(64px + env(safe-area-inset-top)); height: calc(100vh - 64px - env(safe-area-inset-top)); } .modal { padding-top: env(safe-area-inset-top); } } .status-pill { transition: all 0.3s ease; } .pending-badge { background-color: #f3f4f6; color: #6b7280; border: 1px dashed #d1d5db; } .status-progress { height: 4px; background: #e5e7eb; border-radius: 2px; overflow: hidden; margin-top: 6px; } .status-bar { height: 100%; border-radius: 2px; transition: width 1s linear; } .dropdown-menu { position: absolute; right: 0; top: 100%; width: 180px; background-color: white; border-radius: 8px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); z-index: 100; display: none; } .dropdown-menu.show { display: block; animation: fadeIn 0.2s; } .dropdown-item { padding: 10px 16px; display: flex; align-items: center; gap: 8px; transition: all 0.15s ease; } .dropdown-item:hover { background-color: #f3f4f6; } .dropdown-divider { height: 1px; background-color: #e5e7eb; margin: 4px 0; } .user-info { padding: 12px 16px; border-bottom: 1px solid #e5e7eb; } .route-info-panel { position: fixed; bottom: 90px; left: 16px; right: 16px; z-index: 20; background-color: white; border-radius: 12px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); max-width: 500px; margin: 0 auto; transition: all 0.3s ease; transform: translateY(0); } .route-info-panel.hidden { transform: translateY(calc(100% + 90px)); } .custom-attribution { position: absolute; z-index: 15; bottom: 0; right: 0; font-size: 10px; padding: 2px 6px; background-color: rgba(255, 255, 255, 0.7); pointer-events: none; } .review-badge { position: absolute; top: 12px; right: 12px; background-color: #f59e0b; color: white; padding: 4px 8px; border-radius: 6px; font-size: 12px; font-weight: 500; z-index: 15; } .login-container { position: fixed; inset: 0; background-color: rgba(255, 255, 255, 0.9); backdrop-filter: blur(8px); z-index: 100; display: flex; flex-direction: column; justify-content: center; align-items: center; } .navigation-panel { position: fixed; inset: 0; z-index: 200; pointer-events: none; } .navigation-header { position: absolute; top: 0; left: 0; right: 0; background-color: rgba(255, 255, 255, 0.9); backdrop-filter: blur(10px); box-shadow: 0 1px 10px rgba(0,0,0,0.1); padding: 12px 16px; pointer-events: auto; } .navigation-instructions { position: absolute; bottom: 16px; left: 16px; right: 16px; background-color: white; border-radius: 12px; box-shadow: 0 4px 16px rgba(0,0,0,0.1); padding: 16px; pointer-events: auto; } .direction-icon { width: 40px; height: 40px; border-radius: 20px; background-color: #0ea5e9; display: flex; justify-content: center; align-items: center; color: white; font-size: 20px; } .maneuver-icon { transform-origin: center; } .next-maneuver { animation: pulse-highlight 2s infinite; } @keyframes pulse-highlight { 0% { background-color: #0ea5e9; } 50% { background-color: #0284c7; } 100% { background-color: #0ea5e9; } } </style> </head> <body class="bg-gray-50 overflow-hidden"> <div id="navigationPanel" class="navigation-panel hidden"> <div class="navigation-header"> <div class="flex justify-between items-center"> <button id="exitNavigation" class="w-10 h-10 flex items-center justify-center bg-gray-100 text-gray-700 rounded-full hover:bg-gray-200"> <i class="fas fa-times"></i> </button> <div class="text-center"> <h2 class="font-semibold" id="navDestination">Destination</h2> <div class="text-sm text-gray-500" id="navETA">Arrive at --:--</div> </div> <div class="w-10"></div> </div> </div> <div class="navigation-instructions"> <div class="flex gap-4"> <div class="direction-icon next-maneuver"> <i id="navDirectionIcon" class="fas fa-arrow-up maneuver-icon"></i> </div> <div class="flex-1"> <div class="font-semibold text-lg" id="navDirection">Continue straight</div> <div class="text-gray-500" id="navDistance">300 m</div> </div> </div> <div class="mt-4 pt-4 border-t border-gray-100" id="navNextStep"> <div class="flex items-center gap-3"> <div class="w-8 h-8 bg-gray-100 rounded-full flex items-center justify-center text-gray-600"> <i class="fas fa-arrow-right"></i> </div> <div class="text-sm text-gray-600">Then turn right onto Main Street</div> </div> </div> <div class="mt-4 flex justify-between items-center"> <div class="text-sm"> <span class="font-medium" id="navRemainingTime">10 min</span> <span class="text-gray-500"> (<span id="navRemainingDistance">2.5 km</span> remaining) </span> </div> <button id="recenterNavBtn" class="w-10 h-10 flex items-center justify-center bg-primary-500 text-white rounded-full hover:bg-primary-600"> <i class="fas fa-location-crosshairs"></i> </button> </div> </div> </div> <div id="loginContainer" class="login-container"> <div class="w-full max-w-sm bg-white p-6 rounded-2xl shadow-xl"> <div class="flex items-center justify-center mb-6"> <img src="traffic_logo.png" alt="Logo" class="w-12 h-12 mr-3"> <h1 class="text-2xl font-bold text-gray-800">Traffic's</h1> </div> <div id="loginForm" class="space-y-4"> <h2 class="text-lg font-semibold text-center">Sign In to Continue</h2> <div> <label class="block text-sm font-medium mb-1.5">Email</label> <input type="email" id="loginEmail" class="w-full px-3.5 py-2.5 bg-gray-100 border-0 rounded-lg focus:ring-2 focus:ring-primary-500 focus:bg-white" required> </div> <div> <label class="block text-sm font-medium mb-1.5">Password</label> <input type="password" id="loginPassword" class="w-full px-3.5 py-2.5 bg-gray-100 border-0 rounded-lg focus:ring-2 focus:ring-primary-500 focus:bg-white" required> </div> <div class="flex items-center gap-2"> <input type="checkbox" id="rememberMe" class="rounded text-primary-500"> <label for="rememberMe" class="text-sm text-gray-600">Remember me</label> </div> <button id="loginBtn" class="w-full bg-primary-500 hover:bg-primary-600 text-white font-medium px-4 py-2.5 rounded-lg transition-colors">Sign In</button> <p class="text-center text-sm text-gray-500">Don't have an TrafficLight account? <a href="#" id="showRegisterBtn" class="text-primary-500 hover:underline">Sign Up</a></p> <div class="relative flex items-center justify-center my-4"> <div class="border-t border-gray-200 flex-grow"></div> <span class="mx-3 text-sm text-gray-500">or</span> <div class="border-t border-gray-200 flex-grow"></div> </div> <button id="guestBtn" class="w-full bg-gray-200 hover:bg-gray-300 text-gray-800 font-medium px-4 py-2.5 rounded-lg transition-colors">Continue as Guest</button> </div> <div id="registerForm" class="space-y-4 hidden"> <h2 class="text-lg font-semibold text-center">Create an Account</h2> <div> <label class="block text-sm font-medium mb-1.5">Full Name</label> <input type="text" id="registerName" class="w-full px-3.5 py-2.5 bg-gray-100 border-0 rounded-lg focus:ring-2 focus:ring-primary-500 focus:bg-white" required> </div> <div> <label class="block text-sm font-medium mb-1.5">Email</label> <input type="email" id="registerEmail" class="w-full px-3.5 py-2.5 bg-gray-100 border-0 rounded-lg focus:ring-2 focus:ring-primary-500 focus:bg-white" required> </div> <div> <label class="block text-sm font-medium mb-1.5">Password</label> <input type="password" id="registerPassword" class="w-full px-3.5 py-2.5 bg-gray-100 border-0 rounded-lg focus:ring-2 focus:ring-primary-500 focus:bg-white" required> </div> <div> <label class="block text-sm font-medium mb-1.5">Confirm Password</label> <input type="password" id="registerConfirmPassword" class="w-full px-3.5 py-2.5 bg-gray-100 border-0 rounded-lg focus:ring-2 focus:ring-primary-500 focus:bg-white" required> </div> <div class="flex items-center gap-2"> <input type="checkbox" id="registerRememberMe" class="rounded text-primary-500"> <label for="registerRememberMe" class="text-sm text-gray-600">Remember me</label> </div> <button id="registerBtn" class="w-full bg-primary-500 hover:bg-primary-600 text-white font-medium px-4 py-2.5 rounded-lg transition-colors">Sign Up</button> <p class="text-center text-sm text-gray-500">Already have an account? <a href="#" id="showLoginBtn" class="text-primary-500 hover:underline">Sign In</a></p> </div> </div> </div> <header class="header h-16 flex items-center justify-between px-4"> <div class="flex items-center"> <button id="menuToggle" class="w-10 h-10 flex items-center justify-center text-gray-700 mr-3 rounded-full hover:bg-gray-100"> <i class="fas fa-bars"></i> </button> <div class="flex items-center"> <img src="traffic_logo.png" alt="Logo" class="w-8 h-8 mr-2"> <h1 class="hidden sm:block text-xl font-semibold text-gray-800">Traffic's</h1> </div> </div> <div class="flex gap-2"> <button id="searchAddressBtn" class="h-10 flex items-center justify-center bg-gray-100 text-gray-700 rounded-full hover:bg-gray-200 px-3"> <i class="fas fa-search mr-2"></i> <span class="text-sm hidden sm:inline">Search</span> </button> <button id="headerLocateBtn" class="w-10 h-10 flex items-center justify-center bg-primary-500 text-white rounded-full hover:bg-primary-600"> <i class="fas fa-location-crosshairs"></i> </button> <div class="relative"> <button id="userMenuBtn" class="w-10 h-10 flex items-center justify-center bg-gray-100 text-gray-700 rounded-full hover:bg-gray-200"> <i class="fas fa-user"></i> </button> <div id="userDropdown" class="dropdown-menu"> <div class="user-info"> <div id="userName" class="font-medium">Guest User</div> <div id="userEmail" class="text-xs text-gray-500">Limited features available</div> </div> <a href="#" class="dropdown-item" id="accountSettingsBtn"> <i class="fas fa-user-gear text-gray-500"></i> <span>Account</span> </a> <a href="#" class="dropdown-item" id="settingsBtn"> <i class="fas fa-cog text-gray-500"></i> <span>Settings</span> </a> <div class="dropdown-divider"></div> <a href="#" class="dropdown-item" id="logoutBtn"> <i class="fas fa-sign-out-alt text-gray-500"></i> <span>Sign Out</span> </a> </div> </div> </div> </header> <div id="map"></div> <div class="sidebar w-80 max-w-[85%] bg-white flex flex-col"> <div class="p-4 flex flex-col gap-3"> <div class="relative"> <input type="text" id="searchInput" placeholder="Search traffic lights..." class="w-full pl-10 pr-4 py-2.5 bg-gray-100 border-0 rounded-lg text-sm focus:ring-2 focus:ring-primary-500 focus:bg-white"> <i class="fas fa-search absolute left-3.5 top-3 text-gray-400"></i> </div> <button id="nearbyBtn" class="bg-primary-500 hover:bg-primary-600 text-white font-medium px-4 py-2.5 rounded-lg transition-colors flex items-center justify-center gap-2"> <i class="fas fa-location-dot"></i> Nearby Traffic Lights </button> </div> <div class="flex-1 overflow-y-auto p-4" id="lightsList"> <div id="loadingLights" class="text-center py-4"> <div class="w-8 h-8 border-4 border-primary-500 border-t-transparent rounded-full animate-spin mx-auto"></div> <p class="mt-2 text-sm text-gray-500">Loading traffic lights...</p> </div> <div id="noLightsMessage" class="hidden text-center py-8 text-gray-500"> <div class="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-3"> <i class="fas fa-traffic-light text-2xl text-gray-400"></i> </div> <p class="font-medium">TrafficLight</p> <p class="text-xs mt-1">Add a new one.</p> </div> </div> </div> <div class="floating-menu"> <div id="menuItems" class="menu-items"> <div class="menu-btn-container relative"> <div class="menu-btn-label">Add Light</div> <button id="addLightBtn" class="menu-btn bg-traffic-green text-white"> <i class="fas fa-traffic-light"></i> </button> </div> <div class="menu-btn-container relative"> <div class="menu-btn-label">Route</div> <button id="routeBtn" class="menu-btn bg-primary-400 text-white"> <i class="fas fa-route"></i> </button> </div> <div class="menu-btn-container relative"> <div class="menu-btn-label">Your Lights</div> <button id="myLightsBtn" class="menu-btn bg-amber-400 text-white"> <i class="fas fa-star"></i> </button> </div> </div> <button id="menuBtn" class="fab-btn bg-primary-500 text-white shadow-lg"> <i class="fas fa-plus text-xl"></i> </button> </div> <div id="routeInfoPanel" class="route-info-panel hidden"> <div class="p-4"> <div class="flex justify-between items-center mb-3"> <h3 class="font-semibold text-lg">Route Information</h3> <button id="closeRoutePanel" class="text-gray-500 hover:text-gray-700 p-1"> <i class="fas fa-times"></i> </button> </div> <div id="routeInfo" class="space-y-3"> <div class="flex items-center text-sm"> <div class="w-8 h-8 bg-primary-500 rounded-full flex items-center justify-center text-white mr-3"> <i class="fas fa-clock"></i> </div> <div> <div class="font-medium" id="routeTime">--:--</div> <div class="text-xs text-gray-500">Estimated arrival time</div> </div> </div> <div class="flex items-center text-sm"> <div class="w-8 h-8 bg-green-500 rounded-full flex items-center justify-center text-white mr-3"> <i class="fas fa-road"></i> </div> <div> <div class="font-medium" id="routeDistance">-- km</div> <div class="text-xs text-gray-500">Total distance</div> </div> </div> <div id="trafficLightsOnRoute" class="bg-gray-50 p-3 rounded-lg text-sm"> <div class="font-medium mb-2">Traffic Lights on Route</div> <div id="routeLights" class="space-y-2"> <div class="text-center text-gray-500 text-xs py-2">No traffic lights on this route</div> </div> </div> </div> <div class="mt-3 flex gap-3"> <button id="startNavigationBtn" class="bg-primary-500 hover:bg-primary-600 text-white font-medium px-4 py-2.5 rounded-lg transition-colors flex-1 flex items-center justify-center gap-2"> <i class="fas fa-play"></i> Start </button> <button id="saveRouteBtn" class="bg-gray-200 hover:bg-gray-300 text-gray-800 font-medium px-4 py-2.5 rounded-lg transition-colors flex items-center justify-center gap-2 w-12"> <i class="fas fa-bookmark"></i> </button> </div> </div> </div> <div id="lightPopup" class="popup fixed bottom-6 left-1/2 -translate-x-1/2 w-full max-w-sm bg-white rounded-2xl shadow-xl hidden fade-in"> <div class="p-5"> <div class="flex justify-between items-start mb-3"> <h3 class="font-semibold text-lg" id="popupTitle"></h3> <button id="closePopup" class="text-gray-500 hover:text-gray-700 p-1"> <i class="fas fa-times"></i> </button> </div> <div id="popupContent" class="text-sm space-y-2.5 mb-4"></div> <div id="popupStatus" class="p-4 rounded-xl text-center mb-4"></div> <div class="flex gap-3"> <button id="popupNavigate" class="bg-primary-500 hover:bg-primary-600 text-white font-medium px-4 py-2.5 rounded-lg transition-colors flex-1 flex items-center justify-center gap-2"> <i class="fas fa-directions"></i> Directions </button> <button id="popupMeasure" class="bg-traffic-green hover:bg-green-600 text-white font-medium px-4 py-2.5 rounded-lg transition-colors flex-1 flex items-center justify-center gap-2"> <i class="fas fa-stopwatch"></i> Measure </button> </div> </div> </div> <div id="addModal" class="modal fixed inset-0 bg-black bg-opacity-40 flex items-end justify-center sm:items-center"> <div class="modal-overlay absolute inset-0"></div> <div class="modal-body bg-white rounded-t-2xl sm:rounded-2xl w-full max-w-md mx-4 shadow-xl fade-in"> <div class="draggable-handle w-full flex justify-center pt-2 pb-1 sm:hidden"> <div class="swipe-indicator"></div> </div> <div class="p-5 border-b flex justify-between items-center"> <h2 class="text-lg font-semibold">Add Traffic Light</h2> <button class="close-modal text-gray-500 hover:text-gray-700 p-1"> <i class="fas fa-times"></i> </button> </div> <div class="modal-content"> <form id="addLightForm" class="p-5"> <div class="mb-4"> <label class="block text-sm font-medium mb-1.5">Name</label> <input type="text" id="lightName" class="w-full px-3.5 py-2.5 bg-gray-100 border-0 rounded-lg focus:ring-2 focus:ring-primary-500 focus:bg-white" required> </div> <div class="mb-4"> <label class="block text-sm font-medium mb-1.5">Position (tap map to set)</label> <div class="flex gap-2"> <input type="text" id="latitude" placeholder="Latitude" class="w-full px-3.5 py-2.5 bg-gray-100 border-0 rounded-lg focus:ring-2 focus:ring-primary-500 focus:bg-white" readonly required> <input type="text" id="longitude" placeholder="Longitude" class="w-full px-3.5 py-2.5 bg-gray-100 border-0 rounded-lg focus:ring-2 focus:ring-primary-500 focus:bg-white" readonly required> </div> </div> <div class="mb-4"> <label class="block text-sm font-medium mb-1.5">Direction</label> <select id="direction" class="w-full px-3.5 py-2.5 bg-gray-100 border-0 rounded-lg focus:ring-2 focus:ring-primary-500 focus:bg-white" required> <option value="north">North</option> <option value="east">East</option> <option value="south">South</option> <option value="west">West</option> </select> </div> <div class="mb-5"> <label class="block text-sm font-medium mb-1.5">Cycle Duration</label> <div class="grid grid-cols-2 gap-3"> <div> <label class="block text-xs text-traffic-red mb-1">Red (seconds)</label> <input type="number" id="redDuration" placeholder="60" class="w-full px-3.5 py-2.5 bg-gray-100 border-0 rounded-lg focus:ring-2 focus:ring-primary-500 focus:bg-white" required> </div> <div> <label class="block text-xs text-traffic-green mb-1">Green (seconds)</label> <input type="number" id="greenDuration" placeholder="30" class="w-full px-3.5 py-2.5 bg-gray-100 border-0 rounded-lg focus:ring-2 focus:ring-primary-500 focus:bg-white" required> </div> </div> </div> <div class="flex gap-3"> <button type="button" class="close-modal bg-gray-200 hover:bg-gray-300 text-gray-800 font-medium px-4 py-2.5 rounded-lg transition-colors flex-1">Cancel</button> <button type="submit" class="bg-primary-500 hover:bg-primary-600 text-white font-medium px-4 py-2.5 rounded-lg transition-colors flex-1">Add Light</button> </div> </form> </div> </div> </div> <div id="measureModal" class="modal fixed inset-0 bg-black bg-opacity-40 flex items-end justify-center sm:items-center"> <div class="modal-overlay absolute inset-0"></div> <div class="modal-body bg-white rounded-t-2xl sm:rounded-2xl w-full max-w-md mx-4 shadow-xl fade-in"> <div class="draggable-handle w-full flex justify-center pt-2 pb-1 sm:hidden"> <div class="swipe-indicator"></div> </div> <div class="p-5 border-b flex justify-between items-center"> <h2 class="text-lg font-semibold">Measure Light Timing</h2> <button class="close-modal text-gray-500 hover:text-gray-700 p-1"> <i class="fas fa-times"></i> </button> </div> <div class="modal-content"> <div class="p-5"> <div class="bg-gray-100 p-4 rounded-xl text-center mb-5"> <h3 class="font-semibold text-lg" id="measureTitle" data-id=""></h3> <div id="measureStatus" class="mt-2"></div> </div> <div class="mb-5"> <div class="flex justify-center gap-3 mb-5"> <button id="measureRedBtn" class="bg-traffic-red text-white px-4 py-2.5 rounded-lg flex-1 flex items-center justify-center gap-2 hover:bg-red-600 transition-colors"> <i class="fas fa-traffic-light"></i> Measure Red </button> <button id="measureGreenBtn" class="bg-traffic-green text-white px-4 py-2.5 rounded-lg flex-1 flex items-center justify-center gap-2 hover:bg-green-600 transition-colors"> <i class="fas fa-traffic-light"></i> Measure Green </button> </div> </div> <div id="timerContainer" class="hidden"> <div class="bg-gray-100 p-4 rounded-xl mb-5"> <p class="text-sm text-gray-600 mb-2" id="timerInstructions">Press "Start" when the light turns red, then "Stop" when it turns green.</p> <div id="timerDisplay" class="text-center text-4xl font-semibold my-3">00:00</div> </div> <div class="grid grid-cols-3 gap-3 mb-4"> <button id="startTimer" class="bg-traffic-red text-white py-2.5 rounded-lg hover:bg-red-600 transition-colors">Start</button> <button id="stopTimer" class="bg-traffic-green text-white py-2.5 rounded-lg hover:bg-green-600 transition-colors opacity-50" disabled>Stop</button> <button id="saveTimer" class="bg-primary-500 text-white py-2.5 rounded-lg hover:bg-primary-600 transition-colors opacity-50" disabled>Save</button> </div> <div id="measureResult" class="text-center text-sm text-gray-600"></div> </div> </div> </div> </div> </div> <div id="settingsModal" class="modal fixed inset-0 bg-black bg-opacity-40 flex items-end justify-center sm:items-center"> <div class="modal-overlay absolute inset-0"></div> <div class="modal-body bg-white rounded-t-2xl sm:rounded-2xl w-full max-w-md mx-4 shadow-xl fade-in"> <div class="draggable-handle w-full flex justify-center pt-2 pb-1 sm:hidden"> <div class="swipe-indicator"></div> </div> <div class="p-5 border-b flex justify-between items-center"> <h2 class="text-lg font-semibold">Settings</h2> <button class="close-modal text-gray-500 hover:text-gray-700 p-1"> <i class="fas fa-times"></i> </button> </div> <div class="modal-content"> <div class="p-5 space-y-6"> <div> <h3 class="font-semibold mb-3 text-gray-800">App Preferences</h3> <div class="space-y-4"> <div class="flex items-center justify-between"> <div> <p class="text-sm font-medium">Dark Mode</p> <p class="text-xs text-gray-500">Better for night viewing</p> </div> <label class="toggle-switch"> <input type="checkbox" id="darkModeToggle"> <span class="toggle-slider"></span> </label> </div> <div class="flex items-center justify-between"> <div> <p class="text-sm font-medium">Notifications</p> <p class="text-xs text-gray-500">Enable in-app alerts</p> </div> <label class="toggle-switch"> <input type="checkbox" id="notificationsToggle" checked> <span class="toggle-slider"></span> </label> </div> <div class="flex items-center justify-between"> <div> <p class="text-sm font-medium">Auto-Refresh</p> <p class="text-xs text-gray-500">Update light status automatically</p> </div> <label class="toggle-switch"> <input type="checkbox" id="autoRefreshToggle" checked> <span class="toggle-slider"></span> </label> </div> <div class="flex items-center justify-between"> <div> <p class="text-sm font-medium">Keep Screen On During Navigation</p> <p class="text-xs text-gray-500">Prevent screen from turning off</p> </div> <label class="toggle-switch"> <input type="checkbox" id="keepScreenOnToggle" checked> <span class="toggle-slider"></span> </label> </div> </div> </div> <div> <h3 class="font-semibold mb-3 text-gray-800">Map Settings</h3> <div class="space-y-4"> <div> <label class="block text-sm font-medium mb-1.5">Default Map Zoom</label> <select id="defaultZoom" class="w-full px-3.5 py-2.5 bg-gray-100 border-0 rounded-lg focus:ring-2 focus:ring-primary-500 focus:bg-white"> <option value="13">Low (City View)</option> <option value="15">Medium (District View)</option> <option value="17" selected>High (Street View)</option> </select> </div> <div class="flex items-center justify-between"> <div> <p class="text-sm font-medium">Show Traffic Data</p> <p class="text-xs text-gray-500">Display traffic conditions</p> </div> <label class="toggle-switch"> <input type="checkbox" id="trafficToggle" checked> <span class="toggle-slider"></span> </label> </div> <div> <label class="block text-sm font-medium mb-1.5">Traffic Light Settings</label> <div class="flex items-center gap-3 bg-gray-100 p-3 rounded-lg"> <div class="flex-1"> <p class="text-sm font-medium">Show predictions</p> <p class="text-xs text-gray-500">AI-based light prediction</p> </div> <label class="toggle-switch"> <input type="checkbox" id="predictionToggle" checked> <span class="toggle-slider"></span> </label> </div> </div> </div> </div> <div> <h3 class="font-semibold mb-3 text-gray-800">About</h3> <div class="bg-gray-100 p-4 rounded-xl"> <div class="flex items-center mb-2"> <img src="traffic_logo.png" alt="Logo" class="w-8 h-8 mr-2"> <div> <p class="font-medium">TrafficLight Systems v1.4.0</p> <p class="text-xs text-gray-500">© 2025 Imators LLC, all right reserved</p> </div> </div> <div class="mt-4 text-xs"> <h4 class="font-medium mb-1">Licences and Attributions</h4> <p>Map data © OpenStreetMap contributors</p> <p>Icons by Font Awesome</p> </div> <div class="mt-2 text-xs flex flex-wrap gap-2 justify-center"> <a href="https://imators.com/terms-of-use" class="text-primary-500 hover:underline">Terms of Service</a> <span class="text-gray-400">•</span> <a href="https://imators.com/privacy" class="text-primary-500 hover:underline">Privacy Policy</a> <span class="text-gray-400">•</span> <a href="./api-documentation" class="text-primary-500 hover:underline">API</a> <span class="text-gray-400">•</span> <a href="https://imators.com/support" class="text-primary-500 hover:underline">Need help?</a> </div> </div> </div> <div class="pt-2"> <button id="resetAppBtn" class="w-full bg-traffic-red hover:bg-red-600 text-white font-medium px-4 py-2.5 rounded-lg transition-colors flex items-center justify-center gap-2"> <i class="fas fa-trash-alt"></i> Reset App Data </button> </div> </div> </div> </div> </div> <div id="routeModal" class="modal fixed inset-0 bg-black bg-opacity-40 flex items-end justify-center sm:items-center"> <div class="modal-overlay absolute inset-0"></div> <div class="modal-body bg-white rounded-t-2xl sm:rounded-2xl w-full max-w-md mx-4 shadow-xl fade-in"> <div class="draggable-handle w-full flex justify-center pt-2 pb-1 sm:hidden"> <div class="swipe-indicator"></div> </div> <div class="p-5 border-b flex justify-between items-center"> <h2 class="text-lg font-semibold">Find Route</h2> <button class="close-modal text-gray-500 hover:text-gray-700 p-1"> <i class="fas fa-times"></i> </button> </div> <div class="modal-content"> <form id="routeForm" class="p-5"> <div class="mb-4"> <label class="block text-sm font-medium mb-1.5">Start Location</label> <div class="relative"> <input type="text" id="startLocation" placeholder="Current location" class="w-full pl-10 pr-10 py-2.5 bg-gray-100 border-0 rounded-lg focus:ring-2 focus:ring-primary-500 focus:bg-white"> <i class="fas fa-map-marker-alt absolute left-3.5 top-3 text-gray-400"></i> <button type="button" id="useCurrentLocationBtn" class="absolute right-3 top-2.5 text-primary-500"> <i class="fas fa-location-arrow"></i> </button> </div> </div> <div class="mb-4"> <label class="block text-sm font-medium mb-1.5">Destination</label> <div class="relative"> <input type="text" id="endLocation" placeholder="Enter destination" class="w-full pl-10 pr-10 py-2.5 bg-gray-100 border-0 rounded-lg focus:ring-2 focus:ring-primary-500 focus:bg-white" required> <i class="fas fa-map-marker-alt absolute left-3.5 top-3 text-gray-400"></i> <button type="button" id="searchDestinationBtn" class="absolute right-3 top-2.5 text-primary-500"> <i class="fas fa-search"></i> </button> </div> </div> <div class="mb-4"> <label class="block text-sm font-medium mb-1.5">Travel Mode</label> <div class="grid grid-cols-4 gap-2"> <button type="button" class="travel-mode-btn bg-primary-500 text-white py-2 px-1 rounded-lg text-xs flex flex-col items-center" data-mode="driving"> <i class="fas fa-car text-lg mb-1"></i> <span>Drive</span> </button> <button type="button" class="travel-mode-btn bg-gray-200 text-gray-700 py-2 px-1 rounded-lg text-xs flex flex-col items-center" data-mode="walking"> <i class="fas fa-walking text-lg mb-1"></i> <span>Walk</span> </button> <button type="button" class="travel-mode-btn bg-gray-200 text-gray-700 py-2 px-1 rounded-lg text-xs flex flex-col items-center" data-mode="bicycling"> <i class="fas fa-bicycle text-lg mb-1"></i> <span>Bike</span> </button> <button type="button" class="travel-mode-btn bg-gray-200 text-gray-700 py-2 px-1 rounded-lg text-xs flex flex-col items-center" data-mode="transit"> <i class="fas fa-bus text-lg mb-1"></i> <span>Transit</span> </button> </div> </div> <div class="mb-4"> <div class="flex items-center justify-between"> <label class="text-sm font-medium">Options</label> </div> <div class="mt-2 space-y-2"> <div class="flex items-center gap-3 bg-gray-100 p-3 rounded-lg"> <div class="flex-1"> <p class="text-sm font-medium">Avoid Tolls</p> </div> <label class="toggle-switch"> <input type="checkbox" id="avoidTollsToggle"> <span class="toggle-slider"></span> </label> </div> <div class="flex items-center gap-3 bg-gray-100 p-3 rounded-lg"> <div class="flex-1"> <p class="text-sm font-medium">Optimize for Traffic Lights</p> <p class="text-xs text-gray-500">Find route with more green lights</p> </div> <label class="toggle-switch"> <input type="checkbox" id="optimizeTrafficToggle" checked> <span class="toggle-slider"></span> </label> </div> </div> </div> <button type="submit" class="w-full bg-primary-500 hover:bg-primary-600 text-white font-medium px-4 py-2.5 rounded-lg transition-colors flex items-center justify-center gap-2"> <i class="fas fa-route"></i> Find Route </button> </form> </div> </div> </div> <div id="searchAddressModal" class="modal fixed inset-0 bg-black bg-opacity-40 flex items-end justify-center sm:items-center"> <div class="modal-overlay absolute inset-0"></div> <div class="modal-body bg-white rounded-t-2xl sm:rounded-2xl w-full max-w-md mx-4 shadow-xl fade-in"> <div class="draggable-handle w-full flex justify-center pt-2 pb-1 sm:hidden"> <div class="swipe-indicator"></div> </div> <div class="p-5 border-b flex justify-between items-center"> <h2 class="text-lg font-semibold">Search a place</h2> <button class="close-modal text-gray-500 hover:text-gray-700 p-1"> <i class="fas fa-times"></i> </button> </div> <div class="modal-content"> <div class="p-5"> <div class="relative mb-4"> <input type="text" id="addressSearchInput" placeholder="ex : street of the world, 187, Paris" class="w-full pl-10 pr-10 py-2.5 bg-gray-100 border-0 rounded-lg focus:ring-2 focus:ring-primary-500 focus:bg-white"> <i class="fas fa-search absolute left-3.5 top-3 text-gray-400"></i> <button id="clearSearchBtn" class="absolute right-3 top-2.5 text-gray-400 hover:text-gray-600"> <i class="fas fa-times"></i> </button> </div> <div id="searchResults" class="max-h-72 overflow-y-auto space-y-2 mb-4"> <div class="text-center text-sm text-gray-500 py-6"> <i class="fas fa-search mb-2 text-xl"></i> <p>Search for an address, place or landmark</p> </div> </div> <div class="space-y-2"> <h3 class="text-sm font-medium">Recent Searches</h3> <div id="recentSearches" class="space-y-2"> <div class="text-center text-xs text-gray-500 py-3"> No recent searches </div> </div> </div> </div> </div> </div> </div> <div id="welcomeModal" class="modal fixed inset-0 bg-black bg-opacity-70 flex flex-col justify-end items-center z-50"> <div class="modal-body bg-white rounded-t-3xl w-full max-w-md fade-in pb-8"> <div class="draggable-handle w-full flex justify-center pt-2 pb-1"> <div class="swipe-indicator"></div> </div> <div class="px-6 modal-content"> <div class="onboarding-slides overflow-hidden"> <div class="onboarding-slide" data-slide="1"> <div class="text-center mb-8"> <h2 class="text-2xl font-bold text-primary-600 mb-2">Welcome to Traffic's</h2> <p class="text-gray-600">Never wait at red lights again!</p> </div> <div class="flex justify-center mb-8 text-6xl text-primary-500"> <i class="fas fa-map-location-dot"></i> </div> <p class="text-center text-gray-600 mb-8">View real-time traffic light status on the map</p> </div> <div class="onboarding-slide hidden" data-slide="2"> <div class="text-center mb-8"> <h2 class="text-2xl font-bold text-traffic-green mb-2">Save Time & Fuel</h2> <p class="text-gray-600">Plan your route efficiently</p> </div> <div class="flex justify-center mb-8 text-6xl text-traffic-green"> <i class="fas fa-stopwatch"></i> </div> <p class="text-center text-gray-600 mb-8">Get predictions for upcoming light changes</p> </div> <div class="onboarding-slide hidden" data-slide="3"> <div class="text-center mb-8"> <h2 class="text-2xl font-bold text-traffic-amber mb-2">Community Powered</h2> <p class="text-gray-600">Help improve the data</p> </div> <div class="flex justify-center mb-8 text-6xl text-traffic-amber"> <i class="fas fa-users"></i> </div> <p class="text-center text-gray-600 mb-8">Contribute by measuring traffic light timings</p> </div> </div> <div class="flex justify-center items-center gap-3 mb-6"> <div class="onboarding-dot active" data-dot="1"></div> <div class="onboarding-dot" data-dot="2"></div> <div class="onboarding-dot" data-dot="3"></div> </div> <div class="flex gap-3"> <button id="skipWelcome" class="bg-gray-200 hover:bg-gray-300 text-gray-800 font-medium px-4 py-3 rounded-lg transition-colors flex-1">Skip</button> <button id="nextSlide" class="bg-primary-500 hover:bg-primary-600 text-white font-medium px-4 py-3 rounded-lg transition-colors flex-1">Next</button> </div> </div> </div> </div> <div id="reviewBanner" class="fixed top-20 left-1/2 -translate-x-1/2 bg-amber-50 border border-amber-200 p-3 rounded-lg z-40 shadow-md hidden fade-in"> <div class="flex items-center gap-3"> <i class="fas fa-clipboard-check text-amber-500 text-xl"></i> <div> <p class="font-medium text-amber-800">Your traffic light is pending certification</p> <p class="text-xs text-amber-700">It will be visible to all users after verification</p> </div> <button id="dismissReviewBanner" class="text-amber-700 hover:text-amber-900 ml-2"> <i class="fas fa-times"></i> </button> </div> </div> <div id="saveRouteModal" class="modal fixed inset-0 bg-black bg-opacity-40 flex items-end justify-center sm:items-center"> <div class="modal-overlay absolute inset-0"></div> <div class="modal-body bg-white rounded-t-2xl sm:rounded-2xl w-full max-w-md mx-4 shadow-xl fade-in"> <div class="draggable-handle w-full flex justify-center pt-2 pb-1 sm:hidden"> <div class="swipe-indicator"></div> </div> <div class="p-5 border-b flex justify-between items-center"> <h2 class="text-lg font-semibold">Save Route</h2> <button class="close-modal text-gray-500 hover:text-gray-700 p-1"> <i class="fas fa-times"></i> </button> </div> <div class="modal-content"> <form id="saveRouteForm" class="p-5"> <div class="mb-4"> <label class="block text-sm font-medium mb-1.5">Route Name</label> <input type="text" id="routeName" placeholder="e.g. Home to Work" class="w-full px-3.5 py-2.5 bg-gray-100 border-0 rounded-lg focus:ring-2 focus:ring-primary-500 focus:bg-white" required> </div> <div class="bg-gray-100 p-4 rounded-lg mb-4"> <div class="flex items-center mb-2"> <div class="w-6 h-6 bg-primary-500 rounded-full flex items-center justify-center text-white mr-2"> <i class="fas fa-play text-xs"></i> </div> <div class="text-sm" id="saveRouteStart">Current Location</div> </div> <div class="h-6 border-l-2 border-dashed border-gray-300 ml-3"></div> <div class="flex items-center"> <div class="w-6 h-6 bg-traffic-red rounded-full flex items-center justify-center text-white mr-2"> <i class="fas fa-flag-checkered text-xs"></i> </div> <div class="text-sm" id="saveRouteEnd">Destination</div> </div> </div> <button type="submit" class="w-full bg-primary-500 hover:bg-primary-600 text-white font-medium px-4 py-2.5 rounded-lg transition-colors flex items-center justify-center gap-2"> <i class="fas fa-bookmark"></i> Save Route </button> </form> </div> </div> </div> <div id="loadingScreen" class="fixed inset-0 bg-white z-50 flex items-center justify-center"> <div class="text-center"> <div class="flex items-center justify-center mb-1"> <img src="traffic_logo.png" alt="Logo" class="w-8 h-8 mr-2"> <h2 class="text-2xl font-bold">Traffic's</h2> </div> <p class="text-sm text-gray-500">Loading traffic data...</p> </div> </div> <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script> <script> document.addEventListener('DOMContentLoaded', () => { const app = { map: null, markers: {}, userMarker: null, tempMarker: null, selectedLightId: null, userWatchId: null, refreshTimerId: null, rafId: null, lastTimestamp: 0, settings: { darkMode: false, notifications: true, autoRefresh: true, predictions: true, defaultZoom: 15, mapStyle: 'streets', traffic: true, keepScreenOn: true }, measure: { timer: 0, startTime: 0, rafId: null, mode: null, lightId: null }, routing: { startMarker: null, endMarker: null, routeLayer: null, travelMode: 'driving', routeData: null, inNavigation: false, currentStep: 0, remainingSteps: [], simulationMode: false, wakeLock: null }, state: { selectingLocation: false, currentSlide: 1, tileLayer: null, pendingLights: [], userAuth: { isLoggedIn: false, isGuest: true, name: 'Guest User', email: '', userId: null }, menuOpen: false } }; const dom = { map: document.getElementById('map'), sidebar: document.querySelector('.sidebar'), lightsList: document.getElementById('lightsList'), loadingLights: document.getElementById('loadingLights'), noLightsMessage: document.getElementById('noLightsMessage'), lightPopup: document.getElementById('lightPopup'), popupTitle: document.getElementById('popupTitle'), popupContent: document.getElementById('popupContent'), popupStatus: document.getElementById('popupStatus'), closePopup: document.getElementById('closePopup'), popupNavigate: document.getElementById('popupNavigate'), popupMeasure: document.getElementById('popupMeasure'), modals: { add: document.getElementById('addModal'), measure: document.getElementById('measureModal'), settings: document.getElementById('settingsModal'), welcome: document.getElementById('welcomeModal'), route: document.getElementById('routeModal'), searchAddress: document.getElementById('searchAddressModal'), saveRoute: document.getElementById('saveRouteModal') }, overlays: document.querySelectorAll('.modal-overlay'), modalBodies: document.querySelectorAll('.modal-body'), draggableHandles: document.querySelectorAll('.draggable-handle'), closeButtons: document.querySelectorAll('.close-modal'), loadingScreen: document.getElementById('loadingScreen'), reviewBanner: document.getElementById('reviewBanner'), routeInfoPanel: document.getElementById('routeInfoPanel'), routeInfo: { time: document.getElementById('routeTime'), distance: document.getElementById('routeDistance'), lights: document.getElementById('routeLights') }, loginContainer: document.getElementById('loginContainer'), loginForm: document.getElementById('loginForm'), registerForm: document.getElementById('registerForm'), userDropdown: document.getElementById('userDropdown'), userName: document.getElementById('userName'), userEmail: document.getElementById('userEmail'), menuItems: document.getElementById('menuItems'), saveRoute: { start: document.getElementById('saveRouteStart'), end: document.getElementById('saveRouteEnd') }, navigation: { panel: document.getElementById('navigationPanel'), destination: document.getElementById('navDestination'), eta: document.getElementById('navETA'), direction: document.getElementById('navDirection'), directionIcon: document.getElementById('navDirectionIcon'), distance: document.getElementById('navDistance'), nextStep: document.getElementById('navNextStep'), remainingTime: document.getElementById('navRemainingTime'), remainingDistance: document.getElementById('navRemainingDistance') }, buttons: { menuToggle: document.getElementById('menuToggle'), headerLocate: document.getElementById('headerLocateBtn'), userMenu: document.getElementById('userMenuBtn'), addLight: document.getElementById('addLightBtn'), settings: document.getElementById('settingsBtn'), accountSettings: document.getElementById('accountSettingsBtn'), nearby: document.getElementById('nearbyBtn'), skipWelcome: document.getElementById('skipWelcome'), nextSlide: document.getElementById('nextSlide'), resetApp: document.getElementById('resetAppBtn'), measureRed: document.getElementById('measureRedBtn'), measureGreen: document.getElementById('measureGreenBtn'), startTimer: document.getElementById('startTimer'), stopTimer: document.getElementById('stopTimer'), saveTimer: document.getElementById('saveTimer'), dismissReviewBanner: document.getElementById('dismissReviewBanner'), menu: document.getElementById('menuBtn'), route: document.getElementById('routeBtn'), myLights: document.getElementById('myLightsBtn'), closeRoutePanel: document.getElementById('closeRoutePanel'), startNavigation: document.getElementById('startNavigationBtn'), saveRoute: document.getElementById('saveRouteBtn'), searchAddress: document.getElementById('searchAddressBtn'), clearSearch: document.getElementById('clearSearchBtn'), useCurrentLocation: document.getElementById('useCurrentLocationBtn'), searchDestination: document.getElementById('searchDestinationBtn'), login: document.getElementById('loginBtn'), register: document.getElementById('registerBtn'), showRegister: document.getElementById('showRegisterBtn'), showLogin: document.getElementById('showLoginBtn'), guest: document.getElementById('guestBtn'), logout: document.getElementById('logoutBtn'), exitNavigation: document.getElementById('exitNavigation'), recenterNav: document.getElementById('recenterNavBtn') }, form: { addLight: document.getElementById('addLightForm'), route: document.getElementById('routeForm'), saveRoute: document.getElementById('saveRouteForm'), searchInput: document.getElementById('searchInput'), addressSearchInput: document.getElementById('addressSearchInput') }, measure: { title: document.getElementById('measureTitle'), status: document.getElementById('measureStatus'), container: document.getElementById('timerContainer'), instructions: document.getElementById('timerInstructions'), display: document.getElementById('timerDisplay'), result: document.getElementById('measureResult') }, welcome: { slides: document.querySelectorAll('.onboarding-slide'), dots: document.querySelectorAll('.onboarding-dot') }, travelModes: document.querySelectorAll('.travel-mode-btn'), settings: { darkMode: document.getElementById('darkModeToggle'), notifications: document.getElementById('notificationsToggle'), autoRefresh: document.getElementById('autoRefreshToggle'), predictions: document.getElementById('predictionToggle'), traffic: document.getElementById('trafficToggle'), defaultZoom: document.getElementById('defaultZoom'), keepScreenOn: document.getElementById('keepScreenOnToggle') } }; init(); function init() { checkAuthState(); loadSettings(); setupTileProviders(); bindEvents(); setupDraggableModals(); } function checkAuthState() { const token = localStorage.getItem('trafficLightToken'); const userInfo = localStorage.getItem('trafficLightUser'); if (token && userInfo) { try { const user = JSON.parse(userInfo); app.state.userAuth.isLoggedIn = true; app.state.userAuth.isGuest = false; app.state.userAuth.name = user.username; app.state.userAuth.email = user.email; app.state.userAuth.userId = user.id; autoLogin(user.id, user.username); } catch (e) { console.error('Error parsing stored user', e); showLoginScreen(); } } else { showLoginScreen(); } } function autoLogin(userId, username) { const data = { user_id: userId, username: username }; fetch('db.php?action=autoLogin', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(data) }) .then(response => response.json()) .then(data => { if (data.success) { updateUserUI(); initMap(); closeLoginScreen(); } else { localStorage.removeItem('trafficLightToken'); localStorage.removeItem('trafficLightUser'); app.state.userAuth.isLoggedIn = false; app.state.userAuth.isGuest = true; showLoginScreen(); } }) .catch(error => { console.error('Auto-login error:', error); showLoginScreen(); }); } function showLoginScreen() { dom.loginContainer.style.display = 'flex'; } function loginUser(email, password, remember) { const data = { email: email, password: password, remember: remember }; dom.buttons.login.innerHTML = '<div class="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin mr-2"></div> Signing in...'; dom.buttons.login.disabled = true; fetch('db.php?action=login', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(data) }) .then(response => response.json()) .then(data => { if (data.success) { localStorage.setItem('trafficLightToken', 'auth-token-' + Date.now()); localStorage.setItem('trafficLightUser', JSON.stringify(data.user)); app.state.userAuth.isLoggedIn = true; app.state.userAuth.isGuest = false; app.state.userAuth.name = data.user.username; app.state.userAuth.email = data.user.email; app.state.userAuth.userId = data.user.id; updateUserUI(); closeLoginScreen(); initMap(); if (app.settings.notifications) { showNotification('Signed in successfully', 'success'); } } else { showNotification(data.message || 'Login failed', 'error'); } dom.buttons.login.innerHTML = 'Sign In'; dom.buttons.login.disabled = false; }) .catch(error => { console.error('Login error:', error); showNotification('Connection error. Please try again.', 'error'); dom.buttons.login.innerHTML = 'Sign In'; dom.buttons.login.disabled = false; }); } function registerUser(name, email, password, remember) { const data = { username: name, email: email, password: password, remember: remember }; dom.buttons.register.innerHTML = '<div class="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin mr-2"></div> Creating account...'; dom.buttons.register.disabled = true; fetch('db.php?action=register', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(data) }) .then(response => response.json()) .then(data => { if (data.success) { localStorage.setItem('trafficLightToken', 'auth-token-' + Date.now()); localStorage.setItem('trafficLightUser', JSON.stringify(data.user)); app.state.userAuth.isLoggedIn = true; app.state.userAuth.isGuest = false; app.state.userAuth.name = data.user.username; app.state.userAuth.email = data.user.email; app.state.userAuth.userId = data.user.id; updateUserUI(); closeLoginScreen(); initMap(); if (app.settings.notifications) { showNotification('Account created successfully', 'success'); } } else { showNotification(data.message || 'Registration failed', 'error'); } dom.buttons.register.innerHTML = 'Sign Up'; dom.buttons.register.disabled = false; }) .catch(error => { console.error('Registration error:', error); showNotification('Connection error. Please try again.', 'error'); dom.buttons.register.innerHTML = 'Sign Up'; dom.buttons.register.disabled = false; }); } function continueAsGuest() { app.state.userAuth.isLoggedIn = false; app.state.userAuth.isGuest = true; updateUserUI(); closeLoginScreen(); initMap(); if (app.settings.notifications) { showNotification('Continuing as guest. Some features will be limited.', 'info'); } } function logoutUser() { fetch('db.php?action=logout', { method: 'POST' }) .then(response => response.json()) .then(data => { if (data.success) { localStorage.removeItem('trafficLightToken'); localStorage.removeItem('trafficLightUser'); app.state.userAuth.isLoggedIn = false; app.state.userAuth.isGuest = true; app.state.userAuth.name = 'Guest User'; app.state.userAuth.email = ''; app.state.userAuth.userId = null; closeUserDropdown(); if (app.settings.notifications) { showNotification('Signed out successfully', 'info'); } setTimeout(() => { window.location.reload(); }, 1000); } else { showNotification('Logout failed', 'error'); } }) .catch(error => { console.error('Logout error:', error); showNotification('Connection error', 'error'); }); } function updateUserUI() { if (app.state.userAuth.isGuest) { dom.userName.textContent = 'Guest User'; dom.userEmail.textContent = 'Limited features available'; if (dom.buttons.myLights) { dom.buttons.myLights.classList.add('opacity-50'); dom.buttons.myLights.disabled = true; } } else { dom.userName.textContent = app.state.userAuth.name; dom.userEmail.textContent = app.state.userAuth.email; if (dom.buttons.myLights) { dom.buttons.myLights.classList.remove('opacity-50'); dom.buttons.myLights.disabled = false; } } } function closeLoginScreen() { dom.loginContainer.style.opacity = '0'; dom.loginContainer.style.transition = 'opacity 0.5s ease'; setTimeout(() => { dom.loginContainer.style.display = 'none'; }, 500); } function loadSettings() { try { const saved = localStorage.getItem('trafficLightSettings'); if (saved) { const parsed = JSON.parse(saved); app.settings = { ...app.settings, ...parsed }; applySettings(); } } catch (e) { console.error('Error loading settings', e); } } function applySettings() { if (dom.settings.darkMode) dom.settings.darkMode.checked = app.settings.darkMode; if (dom.settings.notifications) dom.settings.notifications.checked = app.settings.notifications; if (dom.settings.autoRefresh) dom.settings.autoRefresh.checked = app.settings.autoRefresh; if (dom.settings.predictions) dom.settings.predictions.checked = app.settings.predictions; if (dom.settings.traffic) dom.settings.traffic.checked = app.settings.traffic; if (dom.settings.defaultZoom) dom.settings.defaultZoom.value = app.settings.defaultZoom; if (dom.settings.keepScreenOn) dom.settings.keepScreenOn.checked = app.settings.keepScreenOn; if (app.settings.darkMode) { document.documentElement.classList.add('dark'); document.body.classList.add('dark-mode'); } } function saveSettings() { app.settings.darkMode = dom.settings.darkMode.checked; app.settings.notifications = dom.settings.notifications.checked; app.settings.autoRefresh = dom.settings.autoRefresh.checked; app.settings.predictions = dom.settings.predictions.checked; app.settings.traffic = dom.settings.traffic.checked; app.settings.defaultZoom = dom.settings.defaultZoom.value; app.settings.keepScreenOn = dom.settings.keepScreenOn.checked; try { localStorage.setItem('trafficLightSettings', JSON.stringify(app.settings)); } catch (e) { console.error('Error saving settings'); } } function checkFirstVisit() { if (!localStorage.getItem('trafficLightAppVisited')) { dom.modals.welcome.style.display = 'flex'; localStorage.setItem('trafficLightAppVisited', 'true'); } else { dom.modals.welcome.style.display = 'none'; } for (const key in dom.modals) { if (key !== 'welcome') { dom.modals[key].style.display = 'none'; } } } function setupTileProviders() { app.tileProviders = { streets: { url: 'https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png', attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> © <a href="https://carto.com/attributions">CARTO</a>' }, dark: { url: 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> © <a href="https://carto.com/attributions">CARTO</a>' } }; } function initMap() { app.map = L.map('map', { zoomControl: false, attributionControl: false }).setView([51.505, -0.09], parseInt(app.settings.defaultZoom)); updateMapStyle(); const customAttribution = document.createElement('div'); customAttribution.className = 'custom-attribution'; customAttribution.textContent = 'Map © OpenStreetMap | Imators'; dom.map.appendChild(customAttribution); L.control.zoom({ position: 'bottomright' }).addTo(app.map); app.map.on('click', handleMapClick); loadTrafficLights(); startGeolocation(); checkFirstVisit(); setTimeout(() => { dom.loadingScreen.style.opacity = '0'; setTimeout(() => { dom.loadingScreen.style.display = 'none'; }, 500); }, 800); } function updateMapStyle() { if (app.state.tileLayer) { app.map.removeLayer(app.state.tileLayer); } const provider = app.settings.darkMode ? app.tileProviders.dark : app.tileProviders.streets; app.state.tileLayer = L.tileLayer(provider.url, { maxZoom: 19, attribution: provider.attribution }).addTo(app.map); } function setupDraggableModals() { dom.draggableHandles.forEach(handle => { const modal = handle.closest('.modal-body'); let startY, currentY, initialY; let isDragging = false; handle.addEventListener('touchstart', e => { startY = e.touches[0].clientY; initialY = 0; isDragging = true; modal.style.transition = 'none'; }, { passive: true }); handle.addEventListener('touchmove', e => { if (!isDragging) return; currentY = e.touches[0].clientY; const diffY = currentY - startY; if (diffY < 0) return; modal.style.transform = `translateY(${diffY}px)`; }, { passive: true }); handle.addEventListener('touchend', e => { modal.style.transition = 'transform 0.3s ease'; if (!isDragging) return; const diffY = currentY - startY; if (diffY > 100) { modal.style.transform = 'translateY(100%)'; const modalContainer = modal.closest('.modal'); setTimeout(() => { modalContainer.style.display = 'none'; modal.style.transform = 'translateY(0)'; }, 300); } else { modal.style.transform = 'translateY(0)'; } isDragging = false; }, { passive: true }); }); dom.overlays.forEach(overlay => { overlay.addEventListener('click', () => { const modal = overlay.closest('.modal'); closeModal(modal); }); }); } function bindEvents() { dom.buttons.menuToggle.addEventListener('click', toggleSidebar); dom.buttons.headerLocate.addEventListener('click', locateUser); dom.buttons.userMenu.addEventListener('click', toggleUserDropdown); dom.buttons.nearby.addEventListener('click', findNearbyLights); dom.buttons.searchAddress.addEventListener('click', () => openModal(dom.modals.searchAddress)); dom.form.searchInput.addEventListener('input', handleSearch); dom.buttons.menu.addEventListener('click', toggleFloatingMenu); dom.buttons.addLight.addEventListener('click', () => { toggleFloatingMenu(); openModal(dom.modals.add); }); dom.buttons.route.addEventListener('click', () => { toggleFloatingMenu(); openModal(dom.modals.route); }); dom.buttons.myLights.addEventListener('click', () => { toggleFloatingMenu(); showMyLights(); }); dom.buttons.closeRoutePanel.addEventListener('click', () => { dom.routeInfoPanel.classList.add('hidden'); }); dom.buttons.startNavigation.addEventListener('click', startNavigation); dom.buttons.saveRoute.addEventListener('click', () => openModal(dom.modals.saveRoute)); dom.closePopup.addEventListener('click', () => dom.lightPopup.classList.add('hidden')); dom.popupNavigate.addEventListener('click', navigateToSelectedLight); dom.popupMeasure.addEventListener('click', () => { dom.lightPopup.classList.add('hidden'); openMeasureModal(app.selectedLightId); }); dom.closeButtons.forEach(btn => { btn.addEventListener('click', () => { const modal = btn.closest('.modal'); closeModal(modal); }); }); dom.buttons.settings.addEventListener('click', () => openModal(dom.modals.settings)); dom.settings.darkMode.addEventListener('change', toggleDarkMode); dom.settings.autoRefresh.addEventListener('change', toggleAutoRefresh); dom.settings.traffic.addEventListener('change', toggleTrafficLayer); dom.settings.notifications.addEventListener('change', saveSettings); dom.settings.predictions.addEventListener('change', () => { saveSettings(); updateAllLightStatus(); }); dom.settings.defaultZoom.addEventListener('change', saveSettings); dom.settings.keepScreenOn.addEventListener('change', saveSettings); dom.buttons.resetApp.addEventListener('click', resetAppData); dom.buttons.measureRed.addEventListener('click', () => startMeasure('red')); dom.buttons.measureGreen.addEventListener('click', () => startMeasure('green')); dom.buttons.startTimer.addEventListener('click', startMeasureTimer); dom.buttons.stopTimer.addEventListener('click', stopMeasureTimer); dom.buttons.saveTimer.addEventListener('click', saveMeasureTimer); dom.buttons.skipWelcome.addEventListener('click', () => closeModal(dom.modals.welcome)); dom.buttons.nextSlide.addEventListener('click', nextOnboardingSlide); dom.welcome.dots.forEach(dot => { dot.addEventListener('click', () => { const slide = parseInt(dot.getAttribute('data-dot')); goToSlide(slide); }); }); dom.buttons.dismissReviewBanner.addEventListener('click', () => { dom.reviewBanner.classList.add('hidden'); }); dom.form.route.addEventListener('submit', handleRouteSubmit); dom.travelModes.forEach(btn => { btn.addEventListener('click', () => { dom.travelModes.forEach(b => b.classList.remove('bg-primary-500', 'text-white')); dom.travelModes.forEach(b => b.classList.add('bg-gray-200', 'text-gray-700')); btn.classList.remove('bg-gray-200', 'text-gray-700'); btn.classList.add('bg-primary-500', 'text-white'); app.routing.travelMode = btn.getAttribute('data-mode'); }); }); dom.buttons.useCurrentLocation.addEventListener('click', useCurrentLocationAsStart); dom.buttons.searchDestination.addEventListener('click', searchDestination); dom.form.saveRoute.addEventListener('submit', handleSaveRouteSubmit); dom.form.addressSearchInput.addEventListener('input', handleAddressSearch); dom.buttons.clearSearch.addEventListener('click', clearAddressSearch); dom.buttons.login.addEventListener('click', (e) => { e.preventDefault(); const email = document.getElementById('loginEmail').value; const password = document.getElementById('loginPassword').value; const remember = document.getElementById('rememberMe').checked; if (email && password) { loginUser(email, password, remember); } else { showNotification('Please enter email and password', 'error'); } }); dom.buttons.register.addEventListener('click', (e) => { e.preventDefault(); const name = document.getElementById('registerName').value; const email = document.getElementById('registerEmail').value; const password = document.getElementById('registerPassword').value; const confirmPassword = document.getElementById('registerConfirmPassword').value; const remember = document.getElementById('registerRememberMe').checked; if (!name || !email || !password) { showNotification('Please fill all fields', 'error'); return; } if (password !== confirmPassword) { showNotification('Passwords do not match', 'error'); return; } registerUser(name, email, password, remember); }); dom.buttons.showRegister.addEventListener('click', (e) => { e.preventDefault(); dom.loginForm.classList.add('hidden'); dom.registerForm.classList.remove('hidden'); }); dom.buttons.showLogin.addEventListener('click', (e) => { e.preventDefault(); dom.registerForm.classList.add('hidden'); dom.loginForm.classList.remove('hidden'); }); dom.buttons.guest.addEventListener('click', continueAsGuest); dom.buttons.logout.addEventListener('click', logoutUser); dom.form.addLight.addEventListener('submit', handleAddLight); dom.buttons.exitNavigation.addEventListener('click', stopNavigation); dom.buttons.recenterNav.addEventListener('click', recenterNavigation); window.addEventListener('online', handleOnline); window.addEventListener('offline', handleOffline); document.addEventListener('visibilitychange', handleVisibilityChange); document.addEventListener('click', handleOutsideClick); } function handleOutsideClick(e) { if (dom.userDropdown.classList.contains('show') && !dom.userDropdown.contains(e.target) && !dom.buttons.userMenu.contains(e.target)) { closeUserDropdown(); } } function toggleUserDropdown() { dom.userDropdown.classList.toggle('show'); } function closeUserDropdown() { dom.userDropdown.classList.remove('show'); } function toggleFloatingMenu() { app.state.menuOpen = !app.state.menuOpen; if (app.state.menuOpen) { dom.menuItems.classList.add('active'); dom.buttons.menu.innerHTML = '<i class="fas fa-times text-xl"></i>'; } else { dom.menuItems.classList.remove('active'); dom.buttons.menu.innerHTML = '<i class="fas fa-plus text-xl"></i>'; } } function toggleSidebar() { dom.sidebar.classList.toggle('active'); dom.buttons.menuToggle.innerHTML = dom.sidebar.classList.contains('active') ? '<i class="fas fa-times"></i>' : '<i class="fas fa-bars"></i>'; } function locateUser() { if (app.userMarker) { app.map.setView(app.userMarker.getLatLng(), 16); } else { startGeolocation(); } } function handleSearch() { const searchTerm = dom.form.searchInput.value.toLowerCase(); const lights = document.querySelectorAll('.light-card'); let hasResults = false; lights.forEach(light => { const name = light.getAttribute('data-name').toLowerCase(); const visible = name.includes(searchTerm);light.style.display = visible ? 'block' : 'none'; if (visible) hasResults = true; }); if (lights.length > 0 && !hasResults) { dom.noLightsMessage.classList.remove('hidden'); dom.noLightsMessage.querySelector('p').textContent = 'No results found'; } else { dom.noLightsMessage.classList.add('hidden'); } } function handleAddressSearch() { const searchTerm = dom.form.addressSearchInput.value; if (searchTerm.length < 3) { return; } const resultsContainer = document.getElementById('searchResults'); resultsContainer.innerHTML = '<div class="text-center py-4"><div class="w-4 h-4 border-4 border-primary-500 border-t-transparent rounded-full animate-spin mx-auto"></div></div>'; fetch('get-apple-map-key.php') .then(response => response.json()) .then(tokenData => { if (!tokenData.success) { throw new Error(tokenData.message || 'Failed to get MapKit token'); } if (window.mapkit) { return advancedSearch(searchTerm); } else { return loadMapKitJS(tokenData.token) .then(() => advancedSearch(searchTerm)); } }) .then(results => { if (results && results.length > 0) { displaySearchResults(results); } else { resultsContainer.innerHTML = '<div class="text-center text-sm text-gray-500 py-6">No results found</div>'; } }) .catch(error => { console.error('Address search error:', error); resultsContainer.innerHTML = '<div class="text-center text-sm text-gray-500 py-6">Error searching for address</div>'; if (app.settings.notifications) { showNotification('Error searching for address', 'error'); } }); } function loadMapKitJS(token) { return new Promise((resolve, reject) => { const script = document.createElement('script'); script.src = 'https://cdn.apple-mapkit.com/mk/5.x.x/mapkit.js'; script.onload = () => { mapkit.init({ authorizationCallback: done => { done(token); } }); resolve(); }; script.onerror = () => { reject(new Error('Failed to load Apple MapKit JS')); }; document.head.appendChild(script); }); } function advancedSearch(query, limit = 5) { return new Promise((resolve, reject) => { if (!window.mapkit) { reject(new Error('MapKit not initialized')); return; } const search = new mapkit.Search(); search.search(query, (error, data) => { if (error) { reject(error); } else if (data && data.places) { const results = data.places.slice(0, limit).map(place => ({ name: place.name || '', address: formatAddress(place), lat: place.coordinate.latitude, lng: place.coordinate.longitude })); resolve(results); } else { resolve([]); } }); }); } function formatAddress(place) { const parts = []; if (place.thoroughfare) parts.push(place.thoroughfare); if (place.locality) parts.push(place.locality); if (place.postalCode) parts.push(place.postalCode); if (place.administrativeArea) parts.push(place.administrativeArea); return parts.join(', '); } function displaySearchResults(results) { const resultsContainer = document.getElementById('searchResults'); resultsContainer.innerHTML = ''; results.forEach(result => { const resultItem = document.createElement('div'); resultItem.className = 'p-3 bg-gray-100 rounded-lg cursor-pointer hover:bg-gray-200 mb-2'; resultItem.innerHTML = ` <p class="font-medium text-sm">${result.name}</p> <p class="text-xs text-gray-500">${result.address}</p> <div class="flex justify-end mt-2"> <button class="navigate-to-btn px-3 py-1 bg-primary-500 text-white text-xs rounded-lg hover:bg-primary-600"> <i class="fas fa-directions mr-1"></i> Navigate </button> </div> `; resultItem.addEventListener('click', (e) => { if (e.target.closest('.navigate-to-btn')) { e.stopPropagation(); if (app.userMarker) { const userPos = app.userMarker.getLatLng(); const destPos = L.latLng(result.lat, result.lng); app.routing.routeData = { start: { name: 'Current Location', lat: userPos.lat, lng: userPos.lng }, end: { name: result.name, lat: result.lat, lng: result.lng }, travelMode: 'driving' }; fetchAndDisplayRoute(userPos, destPos, 'Current Location', result.name); closeModal(dom.modals.searchAddress); } else { startGeolocation(); setTimeout(() => { if (app.userMarker) { const userPos = app.userMarker.getLatLng(); const destPos = L.latLng(result.lat, result.lng); app.routing.routeData = { start: { name: 'Current Location', lat: userPos.lat, lng: userPos.lng }, end: { name: result.name, lat: result.lat, lng: result.lng }, travelMode: 'driving' }; fetchAndDisplayRoute(userPos, destPos, 'Current Location', result.name); closeModal(dom.modals.searchAddress); } else { showNotification('Cannot get your location. Please try again.', 'error'); } }, 1000); } return; } app.map.setView([result.lat, result.lng], 16); if (dom.modals.route.style.display === 'flex') { document.getElementById('endLocation').value = result.name; app.tempDestination = { lat: result.lat, lng: result.lng, name: result.name }; } addToRecentSearches(result); closeModal(dom.modals.searchAddress); }); resultsContainer.appendChild(resultItem); }); } function addToRecentSearches(result) { const recentContainer = document.getElementById('recentSearches'); const emptyMessage = recentContainer.querySelector('.text-center'); if (emptyMessage) { recentContainer.innerHTML = ''; } const searchItem = document.createElement('div'); searchItem.className = 'p-2.5 bg-gray-100 rounded-lg cursor-pointer hover:bg-gray-200 flex items-center justify-between'; searchItem.innerHTML = ` <div class="flex items-center"> <i class="fas fa-history text-gray-400 mr-3"></i> <div> <p class="text-sm">${result.name}</p> </div> </div> <button class="recent-navigate-btn px-2 py-1 bg-primary-500 text-white text-xs rounded-lg hover:bg-primary-600"> <i class="fas fa-directions"></i> </button> `; const navigateBtn = searchItem.querySelector('.recent-navigate-btn'); navigateBtn.addEventListener('click', (e) => { e.stopPropagation(); if (app.userMarker) { const userPos = app.userMarker.getLatLng(); const destPos = L.latLng(result.lat, result.lng); app.routing.routeData = { start: { name: 'Current Location', lat: userPos.lat, lng: userPos.lng }, end: { name: result.name, lat: result.lat, lng: result.lng }, travelMode: 'driving' }; fetchAndDisplayRoute(userPos, destPos, 'Current Location', result.name); closeModal(dom.modals.searchAddress); } else { showNotification('Cannot get your location. Please try again.', 'error'); } }); searchItem.addEventListener('click', (e) => { if (!e.target.closest('.recent-navigate-btn')) { app.map.setView([result.lat, result.lng], 16); if (dom.modals.route.style.display === 'flex') { document.getElementById('endLocation').value = result.name; } closeModal(dom.modals.searchAddress); } }); recentContainer.insertBefore(searchItem, recentContainer.firstChild); if (recentContainer.children.length > 5) { recentContainer.removeChild(recentContainer.lastChild); } } function clearAddressSearch() { dom.form.addressSearchInput.value = ''; document.getElementById('searchResults').innerHTML = ` <div class="text-center text-sm text-gray-500 py-6"> <i class="fas fa-search mb-2 text-xl"></i> <p>Search for an address, place or landmark</p> </div> `; } function useCurrentLocationAsStart() { if (app.userMarker) { document.getElementById('startLocation').value = 'Current Location'; } else { startGeolocation(); setTimeout(() => { document.getElementById('startLocation').value = 'Current Location'; }, 1000); } } function searchDestination() { openModal(dom.modals.searchAddress); } function handleRouteSubmit(e) { e.preventDefault(); const startLocation = document.getElementById('startLocation').value; const endLocation = document.getElementById('endLocation').value; if (!endLocation) { if (app.settings.notifications) { showNotification('Please enter a destination', 'error'); } return; } const submitBtn = e.target.querySelector('[type="submit"]'); const originalText = submitBtn.innerHTML; submitBtn.innerHTML = '<div class="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin mr-2"></div> Finding route...'; submitBtn.disabled = true; let startPoint, endPoint; if (startLocation === 'Current Location' && app.userMarker) { startPoint = app.userMarker.getLatLng(); } else { startPoint = app.map.getCenter(); } if (app.tempDestination && app.tempDestination.name === endLocation) { endPoint = L.latLng(app.tempDestination.lat, app.tempDestination.lng); fetchAndDisplayRoute(startPoint, endPoint, startLocation, endLocation); submitBtn.innerHTML = originalText; submitBtn.disabled = false; closeModal(dom.modals.route); return; } fetchCoordinatesForAddress(endLocation) .then(coords => { if (coords) { endPoint = L.latLng(coords.lat, coords.lng); fetchAndDisplayRoute(startPoint, endPoint, startLocation, endLocation); closeModal(dom.modals.route); } else { throw new Error('Could not find coordinates for destination'); } }) .catch(error => { console.error('Route error:', error); showNotification('Error finding destination', 'error'); }) .finally(() => { submitBtn.innerHTML = originalText; submitBtn.disabled = false; }); } function fetchCoordinatesForAddress(address) { return new Promise((resolve, reject) => { fetch('get-apple-map-key.php') .then(response => response.json()) .then(tokenData => { if (!tokenData.success) { throw new Error(tokenData.message || 'Failed to get MapKit token'); } if (!window.mapkit) { return loadMapKitJS(tokenData.token).then(() => tokenData.token); } return tokenData.token; }) .then(() => { const search = new mapkit.Search(); return new Promise((resolveGeocode, rejectGeocode) => { search.geocode(address, (error, data) => { if (error) { rejectGeocode(error); } else if (data.results && data.results.length > 0) { const result = data.results[0]; resolveGeocode({ lat: result.coordinate.latitude, lng: result.coordinate.longitude }); } else { resolveGeocode(null); } }); }); }) .then(resolve) .catch(reject); }); } function fetchAndDisplayRoute(startPoint, endPoint, startName, endName) { dom.routeInfoPanel.classList.remove('hidden'); dom.routeInfo.lights.innerHTML = ` <div class="text-center py-4"> <div class="w-6 h-6 border-4 border-primary-500 border-t-transparent rounded-full animate-spin mx-auto"></div> <p class="mt-2 text-xs text-gray-500">Calculating route...</p> </div> `; createRouteWithLoadingState(startPoint, endPoint); const travelMode = app.routing.travelMode; const profile = travelMode === 'walking' ? 'foot' : travelMode === 'bicycling' ? 'bicycle' : 'car'; const url = `https://router.project-osrm.org/route/v1/${profile}/${startPoint.lng},${startPoint.lat};${endPoint.lng},${endPoint.lat}?overview=full&geometries=geojson&steps=true`; fetch(url) .then(response => response.json()) .then(data => { if (data.code === 'Ok' && data.routes && data.routes.length > 0) { const route = data.routes[0]; const geometry = route.geometry; const routePoints = geometry.coordinates.map(coord => L.latLng(coord[1], coord[0])); updateRouteDisplay(routePoints); const distance = (route.distance / 1000).toFixed(1); const duration = Math.round(route.duration / 60); const steps = parseRouteSteps(route.legs); app.routing.routeData = { start: { name: startName || 'Current Location', lat: startPoint.lat, lng: startPoint.lng }, end: { name: endName, lat: endPoint.lat, lng: endPoint.lng }, travelMode: app.routing.travelMode, distance: distance, duration: duration, coordinates: routePoints.map(point => [point.lat, point.lng]), steps: steps }; updateRouteInfoPanel(); dom.saveRoute.start.textContent = app.routing.routeData.start.name; dom.saveRoute.end.textContent = app.routing.routeData.end.name; } else { throw new Error('Route calculation failed'); } }) .catch(error => { console.error('Routing error:', error); const routePoints = [startPoint, endPoint]; updateRouteDisplay(routePoints); const distance = (startPoint.distanceTo(endPoint) / 1000).toFixed(1); const duration = Math.round((startPoint.distanceTo(endPoint) / 1000) * 2); const mockSteps = [ { instruction: `Head towards ${endName}`, distance: distance * 1000, duration: duration * 60, type: 'straight', coordinates: [[startPoint.lat, startPoint.lng], [endPoint.lat, endPoint.lng]] } ]; app.routing.routeData = { start: { name: startName || 'Current Location', lat: startPoint.lat, lng: startPoint.lng }, end: { name: endName, lat: endPoint.lat, lng: endPoint.lng }, travelMode: app.routing.travelMode, distance: distance, duration: duration, coordinates: [[startPoint.lat, startPoint.lng], [endPoint.lat, endPoint.lng]], steps: mockSteps }; updateRouteInfoPanel(); dom.saveRoute.start.textContent = app.routing.routeData.start.name; dom.saveRoute.end.textContent = app.routing.routeData.end.name; showNotification('Using simple route due to calculation error', 'info'); }); } function parseRouteSteps(legs) { const steps = []; if (legs && legs.length > 0) { for (const leg of legs) { if (leg.steps && leg.steps.length > 0) { for (const step of leg.steps) { const instruction = step.maneuver.instruction || 'Continue'; const type = getManeuverType(step.maneuver); const coordinates = step.geometry.coordinates.map(coord => [coord[1], coord[0]]); steps.push({ instruction: instruction, distance: step.distance, duration: step.duration, type: type, coordinates: coordinates }); } } } } return steps; } function getManeuverType(maneuver) { if (!maneuver) return 'straight'; const type = maneuver.type; const modifier = maneuver.modifier || ''; if (type === 'turn') { if (modifier.includes('right')) return 'right'; if (modifier.includes('left')) return 'left'; return 'turn'; } else if (type === 'new name' || type === 'continue') { return 'straight'; } else if (type === 'merge' || type === 'on ramp' || type === 'off ramp') { return 'merge'; } else if (type === 'roundabout' || type === 'rotary') { return 'roundabout'; } else if (type === 'arrive') { return 'arrive'; } else if (type === 'depart') { return 'depart'; } return 'straight'; } function createRouteWithLoadingState(startPoint, endPoint) { if (app.routing.routeLayer) { app.map.removeLayer(app.routing.routeLayer); } if (app.routing.startMarker) { app.map.removeLayer(app.routing.startMarker); } if (app.routing.endMarker) { app.map.removeLayer(app.routing.endMarker); } const startIcon = L.divIcon({ className: '', html: `<div class="w-8 h-8 bg-primary-500 rounded-full border-2 border-white shadow-lg flex items-center justify-center text-white"> <i class="fas fa-play"></i> </div>`, iconSize: [32, 32], iconAnchor: [16, 16] }); app.routing.startMarker = L.marker(startPoint, { icon: startIcon }).addTo(app.map); const endIcon = L.divIcon({ className: '', html: `<div class="w-8 h-8 bg-traffic-red rounded-full border-2 border-white shadow-lg flex items-center justify-center text-white"> <i class="fas fa-flag-checkered"></i> </div>`, iconSize: [32, 32], iconAnchor: [16, 16] }); app.routing.endMarker = L.marker(endPoint, { icon: endIcon }).addTo(app.map); const tempRoutePoints = [startPoint, endPoint]; app.routing.routeLayer = L.polyline(tempRoutePoints, { color: '#0ea5e9', weight: 6, opacity: 0.7, dashArray: '10, 10', lineJoin: 'round' }).addTo(app.map); app.map.fitBounds(app.routing.routeLayer.getBounds(), { padding: [50, 50] }); } function updateRouteDisplay(routePoints) { if (app.routing.routeLayer) { app.map.removeLayer(app.routing.routeLayer); } app.routing.routeLayer = L.polyline(routePoints, { color: '#0ea5e9', weight: 6, opacity: 0.9, lineJoin: 'round' }).addTo(app.map); app.map.fitBounds(app.routing.routeLayer.getBounds(), { padding: [50, 50] }); } function updateRouteInfoPanel() { if (!app.routing.routeData) return; const now = new Date(); const arrivalTime = new Date(now.getTime() + app.routing.routeData.duration * 60 * 1000); const formattedTime = arrivalTime.getHours().toString().padStart(2, '0') + ':' + arrivalTime.getMinutes().toString().padStart(2, '0'); dom.routeInfo.distance.textContent = `${app.routing.routeData.distance} km`; dom.routeInfo.time.textContent = formattedTime; findLightsNearRoute(); } function handleSaveRouteSubmit(e) { e.preventDefault(); if (!app.routing.routeData) { showNotification('No active route to save', 'error'); return; } if (app.state.userAuth.isGuest) { showNotification('Please sign in to save routes', 'error'); return; } const routeName = document.getElementById('routeName').value; if (!routeName) { showNotification('Please enter a name for this route', 'error'); return; } const submitBtn = e.target.querySelector('[type="submit"]'); const originalText = submitBtn.innerHTML; submitBtn.innerHTML = '<div class="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin mr-2"></div> Saving...'; submitBtn.disabled = true; const routeData = { name: routeName, start_point: app.routing.routeData.start, end_point: app.routing.routeData.end, coordinates: app.routing.routeData.coordinates, distance: app.routing.routeData.distance, duration: app.routing.routeData.duration, travel_mode: app.routing.routeData.travelMode, steps: app.routing.routeData.steps }; fetch('db.php?action=saveRoute', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(routeData) }) .then(response => response.json()) .then(data => { if (data.success) { showNotification('Route saved successfully', 'success'); closeModal(dom.modals.saveRoute); document.getElementById('routeName').value = ''; } else { showNotification(data.message || 'Error saving route', 'error'); } submitBtn.innerHTML = originalText; submitBtn.disabled = false; }) .catch(error => { console.error('Error saving route:', error); showNotification('Connection error', 'error'); submitBtn.innerHTML = originalText; submitBtn.disabled = false; }); } function findLightsNearRoute() { if (!app.routing.routeData || !app.routing.routeData.coordinates) { dom.routeInfo.lights.innerHTML = '<div class="text-center text-gray-500 text-xs py-2">No traffic lights on this route</div>'; return; } fetch('db.php?action=getLightsOnRoute', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ coordinates: app.routing.routeData.coordinates }) }) .then(response => response.json()) .then(data => { if (data.success && data.lights && data.lights.length > 0) { let lightsHTML = ''; data.lights.forEach(light => { const status = getLightStatus(light); lightsHTML += ` <div class="flex items-center justify-between"> <div class="flex items-center"> <div class="w-3 h-3 rounded-full bg-traffic-${status.color} mr-2"></div> <span>${light.name}</span> </div> <span class="text-xs text-gray-500">${Math.round((Math.random() * 1.5) * 10) / 10} km</span> </div> `; }); dom.routeInfo.lights.innerHTML = lightsHTML; } else { dom.routeInfo.lights.innerHTML = '<div class="text-center text-gray-500 text-xs py-2">No traffic lights on this route</div>'; } }) .catch(error => { console.error('Error finding lights on route:', error); dom.routeInfo.lights.innerHTML = '<div class="text-center text-gray-500 text-xs py-2">Could not load traffic lights</div>'; }); } function startNavigation() { if (!app.routing.routeData || !app.routing.routeData.steps || app.routing.routeData.steps.length === 0) { showNotification('No route available to navigate', 'error'); return; } app.routing.inNavigation = true; app.routing.currentStep = 0; app.routing.remainingSteps = [...app.routing.routeData.steps]; dom.routeInfoPanel.classList.add('hidden'); dom.navigation.panel.classList.remove('hidden'); dom.navigation.destination.textContent = app.routing.routeData.end.name; const now = new Date(); const arrivalTime = new Date(now.getTime() + app.routing.routeData.duration * 60 * 1000); const formattedTime = arrivalTime.getHours().toString().padStart(2, '0') + ':' + arrivalTime.getMinutes().toString().padStart(2, '0'); dom.navigation.eta.textContent = `Arrive at ${formattedTime}`; dom.navigation.remainingTime.textContent = `${app.routing.routeData.duration} min`; dom.navigation.remainingDistance.textContent = `${app.routing.routeData.distance} km`; updateNavigationInstructions(); if (app.settings.keepScreenOn) { requestWakeLock(); } if (app.userMarker) { app.map.setView(app.userMarker.getLatLng(), 18); } startNavigationTracking(); } function requestWakeLock() { if ('wakeLock' in navigator) { try { navigator.wakeLock.request('screen') .then(wakeLock => { app.routing.wakeLock = wakeLock; wakeLock.addEventListener('release', () => { console.log('Wake Lock was released'); }); }) .catch(err => { console.error('Could not obtain wake lock:', err); }); } catch (err) { console.error('Wake Lock API not fully supported:', err); } } } function releaseWakeLock() { if (app.routing.wakeLock) { app.routing.wakeLock.release() .then(() => { app.routing.wakeLock = null; }); } } function updateNavigationInstructions() { if (!app.routing.inNavigation || !app.routing.remainingSteps || app.routing.remainingSteps.length === 0) return; const currentStep = app.routing.remainingSteps[0]; dom.navigation.direction.textContent = currentStep.instruction; const distanceInMeters = currentStep.distance; let formattedDistance; if (distanceInMeters >= 1000) { formattedDistance = `${(distanceInMeters / 1000).toFixed(1)} km`; } else { formattedDistance = `${Math.round(distanceInMeters)} m`; } dom.navigation.distance.textContent = formattedDistance; updateDirectionIcon(currentStep.type); if (app.routing.remainingSteps.length > 1) { const nextStep = app.routing.remainingSteps[1]; const nextDistanceInMeters = nextStep.distance; let nextFormattedDistance; if (nextDistanceInMeters >= 1000) { nextFormattedDistance = `${(nextDistanceInMeters / 1000).toFixed(1)} km`; } else { nextFormattedDistance = `${Math.round(nextDistanceInMeters)} m`; } dom.navigation.nextStep.innerHTML = ` <div class="flex items-center gap-3"> <div class="w-8 h-8 bg-gray-100 rounded-full flex items-center justify-center text-gray-600"> ${getDirectionIconHTML(nextStep.type)} </div> <div class="text-sm text-gray-600"> Then ${nextStep.instruction} (${nextFormattedDistance}) </div> </div> `; } else { dom.navigation.nextStep.innerHTML = ` <div class="flex items-center gap-3"> <div class="w-8 h-8 bg-gray-100 rounded-full flex items-center justify-center text-gray-600"> <i class="fas fa-flag-checkered"></i> </div> <div class="text-sm text-gray-600"> Then arrive at destination </div> </div> `; } } function updateDirectionIcon(type) { let iconHTML = ''; switch (type) { case 'right': iconHTML = '<i class="fas fa-arrow-right maneuver-icon"></i>'; break; case 'left': iconHTML = '<i class="fas fa-arrow-left maneuver-icon"></i>'; break; case 'straight': iconHTML = '<i class="fas fa-arrow-up maneuver-icon"></i>'; break; case 'merge': iconHTML = '<i class="fas fa-compress-alt maneuver-icon"></i>'; break; case 'roundabout': iconHTML = '<i class="fas fa-sync maneuver-icon"></i>'; break; case 'arrive': iconHTML = '<i class="fas fa-flag-checkered maneuver-icon"></i>'; break; case 'depart': iconHTML = '<i class="fas fa-play maneuver-icon"></i>'; break; default: iconHTML = '<i class="fas fa-arrow-up maneuver-icon"></i>'; } dom.navigation.directionIcon.innerHTML = iconHTML; } function getDirectionIconHTML(type) { switch (type) { case 'right': return '<i class="fas fa-arrow-right"></i>'; case 'left': return '<i class="fas fa-arrow-left"></i>'; case 'straight': return '<i class="fas fa-arrow-up"></i>'; case 'merge': return '<i class="fas fa-compress-alt"></i>'; case 'roundabout': return '<i class="fas fa-sync"></i>'; case 'arrive': return '<i class="fas fa-flag-checkered"></i>'; case 'depart': return '<i class="fas fa-play"></i>'; default: return '<i class="fas fa-arrow-up"></i>'; } } function startNavigationTracking() { if (app.routing.simulationMode) { simulateNavigation(); } else { if (app.userMarker) { trackUserForNavigation(); } } } function simulateNavigation() { if (!app.routing.inNavigation) return; let currentStep = app.routing.remainingSteps[0]; let progress = 0; const simulationInterval = setInterval(() => { if (!app.routing.inNavigation) { clearInterval(simulationInterval); return; } progress += 0.01; if (progress >= 1) { if (app.routing.remainingSteps.length > 1) { app.routing.remainingSteps.shift(); updateNavigationInstructions(); progress = 0; } else { clearInterval(simulationInterval); finishNavigation(); } } else { const totalDistance = parseFloat(app.routing.routeData.distance); const remainingDistance = totalDistance * (1 - progress); const remainingTime = Math.round(app.routing.routeData.duration * (1 - progress)); dom.navigation.remainingDistance.textContent = `${remainingDistance.toFixed(1)} km`; dom.navigation.remainingTime.textContent = `${remainingTime} min`; if (app.routing.routeData.coordinates && app.routing.routeData.coordinates.length > 1) { const coordsLength = app.routing.routeData.coordinates.length; const coordIndex = Math.floor(progress * (coordsLength - 1)); if (coordIndex < coordsLength) { const coord = app.routing.routeData.coordinates[coordIndex]; updateUserPosition(L.latLng(coord[0], coord[1])); } } } }, 500); } function trackUserForNavigation() { if (!app.routing.inNavigation || !app.userMarker) return; const checkProgress = () => { if (!app.routing.inNavigation) return; const userPosition = app.userMarker.getLatLng(); const currentStep = app.routing.remainingSteps[0]; if (currentStep && currentStep.coordinates && currentStep.coordinates.length > 0) { const targetCoord = currentStep.coordinates[currentStep.coordinates.length - 1]; const targetPoint = L.latLng(targetCoord[0], targetCoord[1]); const distanceToTarget = userPosition.distanceTo(targetPoint); if (distanceToTarget < 30) { if (app.routing.remainingSteps.length > 1) { app.routing.remainingSteps.shift(); updateNavigationInstructions(); } else { finishNavigation(); return; } } const totalRemainingDistance = calculateRemainingDistance(); const totalRemainingTime = Math.round(totalRemainingDistance / 833); // Avg 50km/h = 833m/min dom.navigation.remainingDistance.textContent = totalRemainingDistance >= 1000 ? `${(totalRemainingDistance / 1000).toFixed(1)} km` : `${Math.round(totalRemainingDistance)} m`; dom.navigation.remainingTime.textContent = `${totalRemainingTime} min`; } requestAnimationFrame(checkProgress); }; checkProgress(); } function calculateRemainingDistance() { if (!app.routing.inNavigation || !app.routing.remainingSteps || app.routing.remainingSteps.length === 0) { return 0; } let totalDistance = 0; for (const step of app.routing.remainingSteps) { totalDistance += step.distance; } if (app.userMarker && app.routing.remainingSteps[0].coordinates && app.routing.remainingSteps[0].coordinates.length > 0) { const userPosition = app.userMarker.getLatLng(); const firstCoord = app.routing.remainingSteps[0].coordinates[0]; const firstPoint = L.latLng(firstCoord[0], firstCoord[1]); const distanceToFirstPoint = userPosition.distanceTo(firstPoint); totalDistance = Math.max(0, totalDistance - distanceToFirstPoint); } return totalDistance; } function updateUserPosition(position) { if (!app.userMarker) return; app.userMarker.setLatLng(position); if (app.routing.inNavigation) { app.map.setView(position, app.map.getZoom()); } } function finishNavigation() { showNotification('You have arrived at your destination!', 'success'); stopNavigation(); } function stopNavigation() { app.routing.inNavigation = false; dom.navigation.panel.classList.add('hidden'); if (app.settings.keepScreenOn) { releaseWakeLock(); } } function recenterNavigation() { if (app.userMarker) { app.map.setView(app.userMarker.getLatLng(), 18); } } function navigateToSelectedLight() { if (!app.selectedLightId || !app.markers[app.selectedLightId]) return; const light = app.markers[app.selectedLightId].data; const endPoint = L.latLng(light.latitude, light.longitude); if (app.userMarker) { const startPoint = app.userMarker.getLatLng(); fetchAndDisplayRoute(startPoint, endPoint, 'Current Location', light.name); dom.lightPopup.classList.add('hidden'); } else { const url = `https://www.google.com/maps/dir/?api=1&destination=${light.latitude},${light.longitude}&travelmode=driving`; window.open(url, '_blank'); } } function showMyLights() { if (app.state.userAuth.isGuest) { if (app.settings.notifications) { showNotification('Please sign in to view your traffic lights', 'info'); } return; } dom.loadingLights.style.display = 'block'; setTimeout(() => { dom.loadingLights.style.display = 'none'; const lights = document.querySelectorAll('.light-card'); if (lights.length === 0) { dom.noLightsMessage.classList.remove('hidden'); dom.noLightsMessage.querySelector('p').textContent = 'You haven\'t added any traffic lights yet'; return; } lights.forEach(light => { if (Math.random() > 0.7) { light.classList.add('active'); light.style.display = 'block'; if (!light.querySelector('.your-light-badge')) { const badge = document.createElement('div'); badge.className = 'your-light-badge text-xs text-primary-600 font-medium mt-1'; badge.innerHTML = '<i class="fas fa-user-check mr-1"></i> Added by you'; light.appendChild(badge); } } else { light.classList.remove('active'); light.style.display = 'none'; } }); if (app.settings.notifications) { showNotification('Showing your added traffic lights', 'info'); } }, 800); } function findNearbyLights() { if (app.userMarker) { app.map.setView(app.userMarker.getLatLng(), 16); highlightNearbyLights(); } else { startGeolocation(); } } function handleAddLight(e) { e.preventDefault(); if (app.state.userAuth.isGuest) { if (app.settings.notifications) { showNotification('Please sign in to add traffic lights', 'error'); } return; } const submitBtn = dom.form.addLight.querySelector('[type="submit"]'); const originalText = submitBtn.textContent; submitBtn.innerHTML = '<div class="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin mr-2"></div> Adding...'; submitBtn.disabled = true; const lightData = { name: document.getElementById('lightName').value, latitude: document.getElementById('latitude').value, longitude: document.getElementById('longitude').value, direction: document.getElementById('direction').value, red_duration: document.getElementById('redDuration').value, green_duration: document.getElementById('greenDuration').value }; fetch('db.php?action=addTrafficLight', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(lightData) }) .then(response => response.json()) .then(data => { if (data.success) { app.state.pendingLights.push(data.light.id); showReviewBanner(); if (app.settings.notifications) { showNotification('Traffic light added successfully! Under review.', 'success'); } closeModal(dom.modals.add); dom.form.addLight.reset(); if (app.tempMarker) { app.map.removeLayer(app.tempMarker); app.tempMarker = null; } app.state.selectingLocation = false; addPendingLightToMap(data.light); addPendingLightToSidebar(data.light); } else { if (app.settings.notifications) { showNotification('Error: ' + data.message, 'error'); } } submitBtn.innerHTML = originalText; submitBtn.disabled = false; }) .catch(error => { console.error('Error:', error); if (app.settings.notifications) { showNotification('Connection error', 'error'); } submitBtn.innerHTML = originalText; submitBtn.disabled = false; }); } function showReviewBanner() { dom.reviewBanner.classList.remove('hidden'); setTimeout(() => { dom.reviewBanner.classList.add('hidden'); }, 5000); } function addPendingLightToMap(light) { const pendingIcon = L.divIcon({ className: '', html: `<div class="w-8 h-8 bg-gray-400 rounded-full border-2 border-white shadow-lg flex items-center justify-center text-white relative"> <i class="fas fa-traffic-light"></i> <div class="review-badge">Review</div> </div>`, iconSize: [32, 32], iconAnchor: [16, 16] }); const marker = L.marker([light.latitude, light.longitude], { icon: pendingIcon }).addTo(app.map); marker.on('click', () => showPendingLightDetail(light)); app.markers[light.id] = { marker: marker, data: light, isPending: true }; } function addPendingLightToSidebar(light) { const card = document.createElement('div'); card.className = 'light-card bg-white shadow-md p-4 mb-3'; card.setAttribute('data-id', light.id); card.setAttribute('data-name', light.name); card.setAttribute('data-pending', 'true'); card.innerHTML = ` <div class="flex justify-between items-center"> <h3 class="font-medium">${light.name}</h3> <span class="px-2.5 py-1 text-xs font-medium rounded-full pending-badge">PENDING</span> </div> <div class="flex items-center text-sm text-gray-500 mt-2"> <i class="fas fa-location-dot mr-2"></i> <span class="capitalize">${light.direction}</span> <div class="ml-auto flex items-center"> <i class="fas fa-clock-rotate-left mr-1.5"></i> Awaiting certification </div> </div> `; card.addEventListener('click', function() { const allCards = document.querySelectorAll('.light-card'); allCards.forEach(c => c.classList.remove('active')); this.classList.add('active'); showPendingLightDetail(light); if (window.innerWidth <= 768) { dom.sidebar.classList.remove('active'); dom.buttons.menuToggle.innerHTML = '<i class="fas fa-bars"></i>'; } }); dom.lightsList.insertBefore(card, dom.loadingLights); dom.noLightsMessage.classList.add('hidden'); } function showPendingLightDetail(light) { app.selectedLightId = light.id; app.map.setView([light.latitude, light.longitude], 17); dom.popupTitle.textContent = light.name; dom.popupContent.innerHTML = ` <div class="grid grid-cols-2 gap-3"> <div class="text-gray-500">Direction:</div> <div class="font-medium capitalize">${light.direction}</div> <div class="text-gray-500">Red duration:</div> <div class="font-medium">${light.red_duration} seconds</div> <div class="text-gray-500">Green duration:</div> <div class="font-medium">${light.green_duration} seconds</div> <div class="text-gray-500">Total cycle:</div> <div class="font-medium">${parseInt(light.red_duration) + parseInt(light.green_duration)} seconds</div> <div class="text-gray-500">Location:</div> <div class="font-medium truncate">${light.latitude.substring(0, 8)}, ${light.longitude.substring(0, 8)}</div> <div class="text-gray-500">Status:</div> <div class="font-medium text-amber-500">Under Review</div> </div> `; dom.popupStatus.className = 'p-4 rounded-xl text-center mb-4 bg-amber-100 text-amber-800'; dom.popupStatus.innerHTML = ` <div class="text-xl font-semibold mb-1">Pending Certification</div> <div class="flex justify-center items-center gap-2"> <i class="fas fa-clipboard-check"></i> <span>This traffic light is being reviewed</span> </div> <p class="text-xs mt-2">Your contribution helps improve traffic data for everyone</p> `; dom.lightPopup.classList.remove('hidden'); } function nextOnboardingSlide() { if (app.state.currentSlide < 3) { goToSlide(app.state.currentSlide + 1); } else { closeModal(dom.modals.welcome); } } function goToSlide(slideNumber) { app.state.currentSlide = slideNumber; dom.welcome.slides.forEach(slide => { slide.classList.add('hidden'); }); dom.welcome.dots.forEach(dot => { dot.classList.remove('active'); }); document.querySelector(`.onboarding-slide[data-slide="${slideNumber}"]`).classList.remove('hidden'); document.querySelector(`.onboarding-dot[data-dot="${slideNumber}"]`).classList.add('active'); dom.buttons.nextSlide.textContent = slideNumber === 3 ? 'Get Started' : 'Next'; } function startMeasure(mode) { app.measure.mode = mode; dom.measure.container.classList.remove('hidden'); if (mode === 'red') { dom.measure.instructions.textContent = 'Press "Start" when the light turns red, then "Stop" when it turns green.'; dom.buttons.startTimer.className = 'bg-traffic-red text-white py-2.5 rounded-lg hover:bg-red-600 transition-colors'; dom.buttons.stopTimer.className = 'bg-traffic-green text-white py-2.5 rounded-lg hover:bg-green-600 transition-colors opacity-50'; } else { dom.measure.instructions.textContent = 'Press "Start" when the light turns green, then "Stop" when it turns red.'; dom.buttons.startTimer.className = 'bg-traffic-green text-white py-2.5 rounded-lg hover:bg-green-600 transition-colors'; dom.buttons.stopTimer.className = 'bg-traffic-red text-white py-2.5 rounded-lg hover:bg-red-600 transition-colors opacity-50'; } resetMeasureTimer(); } function startMeasureTimer() { app.measure.startTime = performance.now(); app.measure.timer = 0; if (app.measure.rafId) cancelAnimationFrame(app.measure.rafId); function updateTimer(timestamp) { const elapsed = Math.floor((timestamp - app.measure.startTime) / 1000); if (elapsed !== app.measure.timer) { app.measure.timer = elapsed; const minutes = Math.floor(elapsed / 60); const seconds = elapsed % 60; dom.measure.display.textContent = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; } app.measure.rafId = requestAnimationFrame(updateTimer); } app.measure.rafId = requestAnimationFrame(updateTimer); dom.measure.display.textContent = '00:00'; dom.measure.result.textContent = 'Measuring...'; dom.buttons.startTimer.disabled = true; dom.buttons.startTimer.classList.add('opacity-50'); dom.buttons.stopTimer.disabled = false; dom.buttons.stopTimer.classList.remove('opacity-50'); dom.buttons.saveTimer.disabled = true; dom.buttons.saveTimer.classList.add('opacity-50'); } function stopMeasureTimer() { if (app.measure.rafId) { cancelAnimationFrame(app.measure.rafId); app.measure.rafId = null; dom.measure.result.textContent = `Measured duration: ${app.measure.timer} seconds. Click Save to confirm.`; dom.buttons.stopTimer.disabled = true; dom.buttons.stopTimer.classList.add('opacity-50'); dom.buttons.saveTimer.disabled = false; dom.buttons.saveTimer.classList.remove('opacity-50'); } } function resetMeasureTimer() { if (app.measure.rafId) { cancelAnimationFrame(app.measure.rafId); app.measure.rafId = null; } app.measure.timer = 0; app.measure.startTime = 0; dom.measure.display.textContent = '00:00'; dom.measure.result.textContent = ''; dom.buttons.startTimer.disabled = false; dom.buttons.startTimer.classList.remove('opacity-50'); dom.buttons.stopTimer.disabled = true; dom.buttons.stopTimer.classList.add('opacity-50'); dom.buttons.saveTimer.disabled = true; dom.buttons.saveTimer.classList.add('opacity-50'); } function saveMeasureTimer() { const lightId = dom.measure.title.getAttribute('data-id'); if (!lightId || app.measure.timer <= 0 || !app.measure.mode) return; if (app.state.userAuth.isGuest) { if (app.settings.notifications) { showNotification('Please sign in to submit measurements', 'error'); } return; } dom.measure.result.textContent = 'Saving...'; const data = { id: lightId, duration_type: app.measure.mode, duration: app.measure.timer }; fetch('db.php?action=updateTiming', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(data) }) .then(response => response.json()) .then(data => { if (data.success) { if (app.markers[lightId]) { app.markers[lightId].data = data.light; updateLightStatus(data.light); } dom.measure.result.innerHTML = '<i class="fas fa-check text-green-500 mr-1"></i> Timing updated!'; dom.buttons.saveTimer.disabled = true; dom.buttons.saveTimer.classList.add('opacity-50'); setTimeout(() => { dom.measure.container.classList.add('hidden'); app.measure.mode = null; }, 1500); if (app.settings.notifications) { showNotification('Measurement saved successfully', 'success'); } } else { dom.measure.result.innerHTML = '<i class="fas fa-times text-red-500 mr-1"></i> Error: ' + (data.message || 'Failed to update'); } }) .catch(error => { console.error('Error saving measurement:', error); dom.measure.result.innerHTML = '<i class="fas fa-times text-red-500 mr-1"></i> Connection error'; }); } function openMeasureModal(lightId) { if (!app.markers[lightId]) return; const light = app.markers[lightId].data; app.measure.lightId = lightId; dom.measure.title.textContent = light.name; dom.measure.title.setAttribute('data-id', lightId); if (app.markers[lightId].isPending) { dom.measure.status.innerHTML = ` <span class="px-3 py-1.5 rounded-full text-sm font-medium bg-amber-500 text-white"> Under Review </span> `; } else { const status = getLightStatus(light); dom.measure.status.innerHTML = ` <span class="px-3 py-1.5 rounded-full text-sm font-medium bg-traffic-${status.color} text-white"> ${status.label} (${status.timeLeft}s) </span> `; } dom.measure.container.classList.add('hidden'); app.measure.mode = null; resetMeasureTimer(); openModal(dom.modals.measure); } function loadTrafficLights(isRefresh = false) { if (!isRefresh) { dom.loadingLights.style.display = 'block'; dom.noLightsMessage.classList.add('hidden'); } fetch('db.php?action=getTrafficLights') .then(response => response.json()) .then(data => { dom.loadingLights.style.display = 'none'; if (isRefresh) { for (let id in app.markers) { if (!app.markers[id].isPending) { app.map.removeLayer(app.markers[id].marker); delete app.markers[id]; } } const lightCards = document.querySelectorAll('.light-card:not([data-pending="true"])'); lightCards.forEach(card => card.remove()); } if (Array.isArray(data) && data.length > 0) { data.forEach(light => { addLightToMap(light); addLightToSidebar(light); }); dom.noLightsMessage.classList.add('hidden'); } else if (Object.keys(app.markers).length === 0) { dom.noLightsMessage.classList.remove('hidden'); } if (app.settings.autoRefresh) { startStatusUpdates(); } }) .catch(error => { console.error('Error loading traffic lights:', error); dom.loadingLights.style.display = 'none'; if (Object.keys(app.markers).length === 0) { dom.noLightsMessage.classList.remove('hidden'); } if (app.settings.notifications) { showNotification('Error loading traffic lights', 'error'); } }); } function addLightToMap(light) { const status = getLightStatus(light); const customIcon = L.divIcon({ className: '', html: `<div class="w-8 h-8 bg-traffic-${status.color} rounded-full border-2 border-white shadow-lg flex items-center justify-center text-white"> <i class="fas fa-traffic-light"></i> </div>`, iconSize: [32, 32], iconAnchor: [16, 16] }); const marker = L.marker([light.latitude, light.longitude], { icon: customIcon }).addTo(app.map); marker.on('click', () => showLightDetail(light)); app.markers[light.id] = { marker: marker, data: light, isPending: light.status === 'pending' }; } function addLightToSidebar(light) { const status = getLightStatus(light); const card = document.createElement('div'); card.className = 'light-card bg-white shadow-md p-4 mb-3 cursor-pointer hover:shadow-lg'; card.setAttribute('data-id', light.id); card.setAttribute('data-name', light.name); if (light.status === 'pending') { card.setAttribute('data-pending', 'true'); } const showPrediction = app.settings.predictions; let statusBadge; if (light.status === 'pending') { statusBadge = `<span id="status-${light.id}" class="px-2.5 py-1 text-xs font-medium rounded-full pending-badge">PENDING</span>`; } else { statusBadge = `<span id="status-${light.id}" class="px-2.5 py-1 text-xs font-medium rounded-full bg-traffic-${status.color} text-white">${status.label}</span>`; } const isUserLight = light.user_id && light.user_id == app.state.userAuth.userId; card.innerHTML = ` <div class="flex justify-between items-center"> <h3 class="font-medium">${light.name}</h3> ${statusBadge} </div> <div class="flex items-center text-sm text-gray-500 mt-2"> <i class="fas fa-location-dot mr-2"></i> <span class="capitalize">${light.direction}</span> <div class="ml-auto flex items-center" id="timer-${light.id}"> ${light.status === 'pending' ? '<i class="fas fa-clock-rotate-left mr-1.5"></i> Awaiting certification' : `<i class="fas fa-clock mr-1.5"></i> ${status.timeLeft}s`} </div> </div> ${(light.status !== 'pending' && showPrediction) ? ` <div class="mt-2 text-xs"> <div class="status-progress"> <div class="status-bar bg-traffic-${status.color}" style="width: ${Math.round((status.timeLeft / (parseInt(light.red_duration) + parseInt(light.green_duration))) * 100)}%"></div> </div> <div class="flex justify-between mt-1 text-gray-400"> <span>0s</span> <span>${parseInt(light.red_duration) + parseInt(light.green_duration)}s</span> </div> </div>` : ''} ${isUserLight ? '<div class="text-xs text-primary-600 font-medium mt-2"><i class="fas fa-user-check mr-1"></i> Added by you</div>' : ''} `; card.addEventListener('click', function() { const allCards = document.querySelectorAll('.light-card'); allCards.forEach(c => c.classList.remove('active')); this.classList.add('active'); if (light.status === 'pending') { showPendingLightDetail(light); } else { showLightDetail(light); } if (window.innerWidth <= 768) { dom.sidebar.classList.remove('active'); dom.buttons.menuToggle.innerHTML = '<i class="fas fa-bars"></i>'; } }); dom.lightsList.insertBefore(card, dom.loadingLights); } function getLightStatus(light) { const totalCycle = parseInt(light.red_duration) + parseInt(light.green_duration); const currentTime = Math.floor(Date.now() / 1000); const timeInCycle = currentTime % totalCycle; if (timeInCycle < light.red_duration) { return { isRed: true, color: 'red', label: 'RED', timeLeft: light.red_duration - timeInCycle }; } else { return { isRed: false, color: 'green', label: 'GREEN', timeLeft: totalCycle - timeInCycle }; } } function updateLightStatus(light) { if (app.markers[light.id] && app.markers[light.id].isPending) { return; } const status = getLightStatus(light); const showPrediction = app.settings.predictions; if (app.markers[light.id]) { const customIcon = L.divIcon({ className: '', html: `<div class="w-8 h-8 bg-traffic-${status.color} rounded-full border-2 border-white shadow-lg flex items-center justify-center text-white"> <i class="fas fa-traffic-light"></i> </div>`, iconSize: [32, 32], iconAnchor: [16, 16] }); app.markers[light.id].marker.setIcon(customIcon); } const statusElement = document.getElementById(`status-${light.id}`); const timerElement = document.getElementById(`timer-${light.id}`); if (statusElement) { statusElement.className = `px-2.5 py-1 text-xs font-medium rounded-full bg-traffic-${status.color} text-white`; statusElement.textContent = status.label; } if (timerElement) { timerElement.innerHTML = `<i class="fas fa-clock mr-1.5"></i> ${status.timeLeft}s`; } const card = document.querySelector(`.light-card[data-id="${light.id}"]`); if (card) { let predictionEl = card.querySelector('.mt-2.text-xs'); if (showPrediction) { if (predictionEl) { const progressBar = predictionEl.querySelector('.status-bar'); if (progressBar) { progressBar.className = `status-bar bg-traffic-${status.color}`; progressBar.style.width = `${Math.round((status.timeLeft / (parseInt(light.red_duration) + parseInt(light.green_duration))) * 100)}%`; } } else { predictionEl = document.createElement('div'); predictionEl.className = 'mt-2 text-xs'; predictionEl.innerHTML = ` <div class="status-progress"> <div class="status-bar bg-traffic-${status.color}" style="width: ${Math.round((status.timeLeft / (parseInt(light.red_duration) + parseInt(light.green_duration))) * 100)}%"></div> </div> <div class="flex justify-between mt-1 text-gray-400"> <span>0s</span> <span>${parseInt(light.red_duration) + parseInt(light.green_duration)}s</span> </div> `; card.appendChild(predictionEl); } } else if (!showPrediction && predictionEl) { predictionEl.remove(); } } if (app.selectedLightId === light.id && !dom.lightPopup.classList.contains('hidden')) { const popupStatus = dom.popupStatus; popupStatus.className = `p-4 rounded-xl text-center mb-4 bg-${status.color}-100 text-${status.color}-800`; popupStatus.innerHTML = ` <div class="text-xl font-semibold mb-1">${status.label}</div> <div class="flex justify-center items-center gap-2"> <i class="fas fa-clock"></i> <span>Changes in ${status.timeLeft} seconds</span> </div> ${showPrediction ?` <div class="mt-3 w-full bg-gray-200 rounded-full h-2 max-w-xs mx-auto"> <div class="bg-traffic-${status.color} h-2 rounded-full" style="width: ${Math.round((status.timeLeft / (parseInt(light.red_duration) + parseInt(light.green_duration))) * 100)}%"></div> </div> ` : ''} `; } if (app.measure.lightId === light.id && dom.modals.measure.style.display === 'flex') { dom.measure.status.innerHTML = ` <span class="px-3 py-1.5 rounded-full text-sm font-medium bg-traffic-${status.color} text-white"> ${status.label} (${status.timeLeft}s) </span> `; } } function updateAllLightStatus() { for (const id in app.markers) { if (!app.markers[id].isPending) { updateLightStatus(app.markers[id].data); } } } function startStatusUpdates() { stopStatusUpdates(); function updateStatuses(timestamp) { if (!app.lastTimestamp || timestamp - app.lastTimestamp > 1000) { app.lastTimestamp = timestamp; updateAllLightStatus(); } app.rafId = requestAnimationFrame(updateStatuses); } app.rafId = requestAnimationFrame(updateStatuses); } function stopStatusUpdates() { if (app.rafId) { cancelAnimationFrame(app.rafId); app.rafId = null; } } function showLightDetail(light) { app.selectedLightId = light.id; app.map.setView([light.latitude, light.longitude], 17); dom.popupTitle.textContent = light.name; dom.popupContent.innerHTML = ` <div class="grid grid-cols-2 gap-3"> <div class="text-gray-500">Direction:</div> <div class="font-medium capitalize">${light.direction}</div> <div class="text-gray-500">Red duration:</div> <div class="font-medium">${light.red_duration} seconds</div> <div class="text-gray-500">Green duration:</div> <div class="font-medium">${light.green_duration} seconds</div> <div class="text-gray-500">Total cycle:</div> <div class="font-medium">${parseInt(light.red_duration) + parseInt(light.green_duration)} seconds</div> <div class="text-gray-500">Location:</div> <div class="font-medium truncate">${light.latitude.substring(0, 8)}, ${light.longitude.substring(0, 8)}</div> </div> `; const status = getLightStatus(light); const showPrediction = app.settings.predictions; dom.popupStatus.className = `p-4 rounded-xl text-center mb-4 bg-${status.color}-100 text-${status.color}-800`; dom.popupStatus.innerHTML = ` <div class="text-xl font-semibold mb-1">${status.label}</div> <div class="flex justify-center items-center gap-2"> <i class="fas fa-clock"></i> <span>Changes in ${status.timeLeft} seconds</span> </div> ${showPrediction ? ` <div class="mt-3 w-full bg-gray-200 rounded-full h-2 max-w-xs mx-auto"> <div class="bg-traffic-${status.color} h-2 rounded-full" style="width: ${Math.round((status.timeLeft / (parseInt(light.red_duration) + parseInt(light.green_duration))) * 100)}%"></div> </div> ` : ''} `; dom.lightPopup.classList.remove('hidden'); } function startGeolocation() { if (navigator.geolocation) { if (app.userWatchId) navigator.geolocation.clearWatch(app.userWatchId); dom.buttons.headerLocate.innerHTML = '<div class="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"></div>'; app.userWatchId = navigator.geolocation.watchPosition( updateUserLocation, handleGeolocationError, { enableHighAccuracy: true, maximumAge: 10000, timeout: 5000 } ); navigator.geolocation.getCurrentPosition( updateUserLocation, handleGeolocationError, { enableHighAccuracy: true } ); } } function updateUserLocation(position) { const lat = position.coords.latitude; const lng = position.coords.longitude; dom.buttons.headerLocate.innerHTML = '<i class="fas fa-location-crosshairs"></i>'; if (!app.userMarker) { const locationIcon = L.divIcon({ className: '', html: '<div class="location-dot"></div>', iconSize: [20, 20], iconAnchor: [10, 10] }); app.userMarker = L.marker([lat, lng], { icon: locationIcon, zIndexOffset: 1000 }).addTo(app.map); app.map.setView([lat, lng], 16); } else { app.userMarker.setLatLng([lat, lng]); } } function handleGeolocationError(error) { console.error('Geolocation error:', error.message); dom.buttons.headerLocate.innerHTML = '<i class="fas fa-location-crosshairs"></i>'; if (app.settings.notifications) { showNotification('Location access denied', 'error'); } } function highlightNearbyLights() { if (!app.userMarker) return; const userPos = app.userMarker.getLatLng(); const nearbyLights = []; for (const id in app.markers) { const markerPos = app.markers[id].marker.getLatLng(); const distance = userPos.distanceTo(markerPos); if (distance < 1000) { nearbyLights.push({ id: id, distance: distance }); } } nearbyLights.sort((a, b) => a.distance - b.distance); document.querySelectorAll('.light-card').forEach(card => { card.classList.remove('active'); }); if (nearbyLights.length > 0) { if (app.settings.notifications) { showNotification(`${nearbyLights.length} traffic lights found within 1km`, 'info'); } nearbyLights.slice(0, 5).forEach(light => { const card = document.querySelector(`.light-card[data-id="${light.id}"]`); if (card) { card.classList.add('active'); card.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } }); } else { if (app.settings.notifications) { showNotification('No traffic lights nearby', 'info'); } } } function openModal(modal) { if (modal === dom.modals.add) { app.state.selectingLocation = true; if (app.userMarker) { const pos = app.userMarker.getLatLng(); document.getElementById('latitude').value = pos.lat.toFixed(6); document.getElementById('longitude').value = pos.lng.toFixed(6); if (app.tempMarker) app.map.removeLayer(app.tempMarker); app.tempMarker = L.marker([pos.lat, pos.lng], { draggable: true }).addTo(app.map); app.tempMarker.on('dragend', updateMarkerPosition); } } modal.style.display = 'flex'; const modalBody = modal.querySelector('.modal-body'); modalBody.style.transform = 'translateY(0)'; } function closeModal(modal) { const modalBody = modal.querySelector('.modal-body'); modalBody.style.transition = 'transform 0.3s ease'; modalBody.style.transform = 'translateY(100%)'; setTimeout(() => { modal.style.display = 'none'; modalBody.style.transform = 'translateY(0)'; if (modal === dom.modals.add) { app.state.selectingLocation = false; if (app.tempMarker) { app.map.removeLayer(app.tempMarker); app.tempMarker = null; } } }, 300); } function handleMapClick(e) { if (app.state.selectingLocation) { document.getElementById('latitude').value = e.latlng.lat.toFixed(6); document.getElementById('longitude').value = e.latlng.lng.toFixed(6); if (app.tempMarker) app.map.removeLayer(app.tempMarker); app.tempMarker = L.marker([e.latlng.lat, e.latlng.lng], { draggable: true }).addTo(app.map); app.tempMarker.on('dragend', updateMarkerPosition); } } function updateMarkerPosition() { const pos = app.tempMarker.getLatLng(); document.getElementById('latitude').value = pos.lat.toFixed(6); document.getElementById('longitude').value = pos.lng.toFixed(6); } function toggleDarkMode() { app.settings.darkMode = dom.settings.darkMode.checked; if (app.settings.darkMode) { document.documentElement.classList.add('dark'); document.body.classList.add('dark-mode'); } else { document.documentElement.classList.remove('dark'); document.body.classList.remove('dark-mode'); } updateMapStyle(); saveSettings(); } function toggleAutoRefresh() { app.settings.autoRefresh = dom.settings.autoRefresh.checked; if (app.settings.autoRefresh) { startStatusUpdates(); } else { stopStatusUpdates(); } saveSettings(); } function toggleTrafficLayer() { app.settings.traffic = dom.settings.traffic.checked; saveSettings(); if (app.settings.notifications) { showNotification(app.settings.traffic ? 'Traffic data enabled' : 'Traffic data disabled', 'info'); } } function resetAppData() { if (confirm('Are you sure you want to reset all app data? This will clear all your settings and cache.')) { localStorage.clear(); showNotification('App data reset. Refreshing...', 'info'); setTimeout(() => { window.location.reload(); }, 1500); } } function showNotification(message, type) { const colors = { success: 'bg-traffic-green', error: 'bg-traffic-red', info: 'bg-primary-500' }; const icons = { success: 'check-circle', error: 'exclamation-circle', info: 'info-circle' }; const notification = document.createElement('div'); notification.className = `fixed top-20 left-1/2 transform -translate-x-1/2 ${colors[type]} text-white px-4 py-3 rounded-lg shadow-lg z-50 fade-in flex items-center`; notification.innerHTML = `<i class="fas fa-${icons[type]} mr-2"></i>${message}`; document.body.appendChild(notification); setTimeout(() => { notification.style.opacity = '0'; notification.style.transform = 'translate(-50%, -10px)'; notification.style.transition = 'opacity 0.5s, transform 0.5s'; setTimeout(() => notification.remove(), 500); }, 3000); } function handleOnline() { if (app.settings.notifications) { showNotification('You are back online', 'success'); } loadTrafficLights(true); } function handleOffline() { if (app.settings.notifications) { showNotification('You are offline. Some features may be limited', 'error'); } } function handleVisibilityChange() { if (document.visibilityState === 'visible') { loadTrafficLights(true); } } function handleInstallPrompt() { if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/traffic/sw.js') .then(registration => { console.log('Service Worker registered with scope:', registration.scope); }) .catch(error => { console.error('Service Worker registration failed:', error); }); } let deferredPrompt; window.addEventListener('beforeinstallprompt', (e) => { e.preventDefault(); deferredPrompt = e; const installBanner = document.createElement('div'); installBanner.className = 'fixed bottom-0 left-0 right-0 bg-primary-500 text-white py-3 px-4 shadow-lg z-40 fade-in'; installBanner.innerHTML = ` <div class="flex items-center justify-between max-w-md mx-auto"> <div> <p class="font-medium">Add TrafficLight to Home Screen</p> <p class="text-sm">Get quick access to traffic lights data</p> </div> <div class="flex space-x-2"> <button id="dismissInstall" class="px-3 py-1.5 bg-primary-600 hover:bg-primary-700 rounded font-medium">Later</button> <button id="installApp" class="px-3 py-1.5 bg-white text-primary-600 hover:bg-gray-100 rounded font-medium">Install</button> </div> </div> `; document.body.appendChild(installBanner); document.getElementById('dismissInstall').addEventListener('click', () => { installBanner.style.opacity = '0'; installBanner.style.transform = 'translateY(100%)'; installBanner.style.transition = 'opacity 0.5s, transform 0.5s'; setTimeout(() => installBanner.remove(), 500); }); document.getElementById('installApp').addEventListener('click', () => { deferredPrompt.prompt(); deferredPrompt.userChoice.then((choiceResult) => { if (choiceResult.outcome === 'accepted') { console.log('User accepted the install prompt'); installBanner.style.opacity = '0'; installBanner.style.transform = 'translateY(100%)'; installBanner.style.transition = 'opacity 0.5s, transform 0.5s'; setTimeout(() => installBanner.remove(), 500); } deferredPrompt = null; }); }); }); } handleInstallPrompt(); }); </script> </body> </html>
| ver. 1.6 |
Github
|
.
| PHP 8.1.33 | Генерация страницы: 0 |
proxy
|
phpinfo
|
Настройка