Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Live Map - Staff Safety Dashboard</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://unpkg.com/lucide@latest"></script> | |
| <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" /> | |
| <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"> | |
| <style> | |
| body { | |
| font-family: 'Inter', sans-serif; | |
| height: 100vh; | |
| overflow: hidden; | |
| background: #0f172a; | |
| } | |
| /* Neuromorphic Dark Theme Base */ | |
| .neu-dark { | |
| background: linear-gradient(145deg, #1e293b, #0f172a); | |
| box-shadow: | |
| 8px 8px 16px rgba(0,0,0,0.5), | |
| -8px -8px 16px rgba(255,255,255,0.03), | |
| inset 1px 1px 1px rgba(255,255,255,0.05); | |
| } | |
| .neu-dark-inset { | |
| background: #0f172a; | |
| box-shadow: | |
| inset 4px 4px 8px rgba(0,0,0,0.6), | |
| inset -4px -4px 8px rgba(255,255,255,0.02); | |
| } | |
| .neu-pressed { | |
| box-shadow: | |
| inset 4px 4px 8px rgba(0,0,0,0.5), | |
| inset -4px -4px 8px rgba(255,255,255,0.05); | |
| } | |
| .neu-btn { | |
| background: linear-gradient(145deg, #1e293b, #0f172a); | |
| box-shadow: | |
| 4px 4px 8px rgba(0,0,0,0.4), | |
| -4px -4px 8px rgba(255,255,255,0.03); | |
| transition: all 0.2s ease; | |
| } | |
| .neu-btn:active { | |
| box-shadow: | |
| inset 4px 4px 8px rgba(0,0,0,0.4), | |
| inset -4px -4px 8px rgba(255,255,255,0.03); | |
| } | |
| .neu-btn.active { | |
| background: linear-gradient(145deg, #ca8a04, #eab308); | |
| color: #0f172a; | |
| box-shadow: | |
| 4px 4px 8px rgba(0,0,0,0.3), | |
| -4px -4px 8px rgba(255,255,255,0.05); | |
| } | |
| /* Yellow Accent Utilities */ | |
| .text-accent { color: #eab308; } | |
| .bg-accent { background-color: #eab308; } | |
| .border-accent { border-color: rgba(234, 179, 8, 0.3); } | |
| /* Map Container */ | |
| #map { | |
| height: 100%; | |
| width: 100%; | |
| border-radius: 1rem; | |
| z-index: 1; | |
| background: #1e293b; | |
| } | |
| /* Custom Map Tiles Dark Mode */ | |
| .leaflet-layer, | |
| .leaflet-control-zoom-in, | |
| .leaflet-control-zoom-out, | |
| .leaflet-control-attribution { | |
| filter: invert(100%) hue-rotate(180deg) brightness(95%) contrast(90%); | |
| } | |
| /* Pulse Animation */ | |
| .status-dot { | |
| animation: blink 2s infinite; | |
| } | |
| @keyframes blink { | |
| 0%, 100% { opacity: 1; } | |
| 50% { opacity: 0.4; } | |
| } | |
| /* Scrollbar Hidden */ | |
| ::-webkit-scrollbar { display: none; } | |
| body { -ms-overflow-style: none; scrollbar-width: none; } | |
| </style> | |
| </head> | |
| <body class="bg-slate-950 text-slate-200"> | |
| <!-- Sidebar --> | |
| <aside class="fixed left-0 top-0 h-full w-64 neu-dark border-r border-slate-800/50 z-40 flex flex-col"> | |
| <div class="p-6 border-b border-slate-800/50"> | |
| <div class="flex items-center space-x-3"> | |
| <div class="w-10 h-10 neu-dark-inset rounded-lg flex items-center justify-center border border-accent"> | |
| <i data-lucide="shield-check" class="text-accent w-6 h-6"></i> | |
| </div> | |
| <div> | |
| <h1 class="font-bold text-lg text-slate-100">SafeTeam</h1> | |
| <p class="text-xs text-slate-500">Safety Management</p> | |
| </div> | |
| </div> | |
| </div> | |
| <nav class="p-4 space-y-2 flex-1"> | |
| <a href="index.html" class="flex items-center space-x-3 px-4 py-3 text-slate-400 neu-btn rounded-xl hover:text-accent transition-colors"> | |
| <i data-lucide="layout-dashboard" class="w-5 h-5"></i> | |
| <span>Dashboard</span> | |
| </a> | |
| <a href="incidents.html" class="flex items-center space-x-3 px-4 py-3 text-slate-400 neu-btn rounded-xl hover:text-accent transition-colors"> | |
| <i data-lucide="alert-triangle" class="w-5 h-5"></i> | |
| <span>Incidents</span> | |
| <span class="ml-auto bg-red-500 text-white text-xs px-2 py-0.5 rounded-full">3</span> | |
| </a> | |
| <a href="training.html" class="flex items-center space-x-3 px-4 py-3 text-slate-400 neu-btn rounded-xl hover:text-accent transition-colors"> | |
| <i data-lucide="graduation-cap" class="w-5 h-5"></i> | |
| <span>Training</span> | |
| </a> | |
| <a href="live-map.html" class="flex items-center space-x-3 px-4 py-3 neu-pressed text-accent rounded-xl font-medium border border-accent/30"> | |
| <i data-lucide="map" class="w-5 h-5"></i> | |
| <span>Live Map</span> | |
| <span class="ml-auto w-2 h-2 bg-accent rounded-full status-dot"></span> | |
| </a> | |
| <a href="equipment.html" class="flex items-center space-x-3 px-4 py-3 text-slate-400 neu-btn rounded-xl hover:text-accent transition-colors"> | |
| <i data-lucide="hard-hat" class="w-5 h-5"></i> | |
| <span>Equipment</span> | |
| </a> | |
| <a href="reports.html" class="flex items-center space-x-3 px-4 py-3 text-slate-400 neu-btn rounded-xl hover:text-accent transition-colors"> | |
| <i data-lucide="file-text" class="w-5 h-5"></i> | |
| <span>Reports</span> | |
| </a> | |
| <a href="emergency.html" class="flex items-center space-x-3 px-4 py-3 text-slate-400 neu-btn rounded-xl hover:text-red-400 transition-colors"> | |
| <i data-lucide="phone-call" class="w-5 h-5"></i> | |
| <span>Emergency</span> | |
| </a> | |
| </nav> | |
| <div class="p-4 border-t border-slate-800/50"> | |
| <div class="flex items-center space-x-3 px-4 py-3 neu-dark-inset rounded-xl border border-slate-700/50"> | |
| <img src="http://static.photos/people/100x100/42" alt="User" class="w-10 h-10 rounded-full object-cover border border-slate-600"> | |
| <div> | |
| <p class="text-sm font-medium text-slate-200">Safety Manager</p> | |
| <p class="text-xs text-slate-500">Admin</p> | |
| </div> | |
| </div> | |
| </div> | |
| </aside> | |
| <!-- Main Content - Fixed Height --> | |
| <main class="md:ml-64 h-screen flex flex-col"> | |
| <!-- Header --> | |
| <header class="h-16 neu-dark border-b border-slate-800/50 flex items-center justify-between px-6 flex-shrink-0"> | |
| <div class="flex items-center space-x-4"> | |
| <button class="md:hidden p-2 neu-dark-inset rounded-lg text-slate-400 border border-slate-700"> | |
| <i data-lucide="menu" class="w-6 h-6"></i> | |
| </button> | |
| <div> | |
| <h2 class="text-xl font-bold text-slate-100">Live Transit Tracking</h2> | |
| <p class="text-xs text-slate-500" id="current-time">Real-time monitoring active</p> | |
| </div> | |
| </div> | |
| <div class="flex items-center space-x-3"> | |
| <button class="neu-btn active px-4 py-2 rounded-lg font-semibold text-sm flex items-center space-x-2"> | |
| <i data-lucide="activity" class="w-4 h-4"></i> | |
| <span>LIVE</span> | |
| </button> | |
| <div class="w-px h-8 bg-slate-700/50"></div> | |
| <button class="p-2 neu-btn rounded-lg text-slate-400 hover:text-accent"> | |
| <i data-lucide="maximize" class="w-5 h-5"></i> | |
| </button> | |
| </div> | |
| </header> | |
| <!-- Map Viewport - No overflow --> | |
| <div class="flex-1 p-4 relative"> | |
| <!-- Map Container --> | |
| <div class="h-full w-full neu-dark-inset rounded-2xl border border-slate-700/50 p-2 relative"> | |
| <div id="map" class="rounded-xl border border-slate-600/30"></div> | |
| <!-- Current Status Overlay --> | |
| <div class="absolute top-4 left-4 z-[400] flex space-x-2"> | |
| <div class="neu-dark px-3 py-1.5 rounded-lg border border-accent/20 flex items-center space-x-2"> | |
| <div class="w-2 h-2 bg-accent rounded-full status-dot"></div> | |
| <span class="text-xs font-medium text-slate-300">24 Staff Active</span> | |
| </div> | |
| <div class="neu-dark px-3 py-1.5 rounded-lg border border-green-500/20 flex items-center space-x-2"> | |
| <div class="w-2 h-2 bg-green-500 rounded-full status-dot"></div> | |
| <span class="text-xs font-medium text-slate-300">6 Security Units</span> | |
| </div> | |
| </div> | |
| <!-- Map Controls --> | |
| <div class="absolute top-4 right-4 z-[400] flex flex-col space-y-2"> | |
| <button onclick="map.zoomIn()" class="w-10 h-10 neu-dark rounded-lg flex items-center justify-center text-slate-400 hover:text-accent border border-slate-700/50"> | |
| <i data-lucide="plus" class="w-5 h-5"></i> | |
| </button> | |
| <button onclick="map.zoomOut()" class="w-10 h-10 neu-dark rounded-lg flex items-center justify-center text-slate-400 hover:text-accent border border-slate-700/50"> | |
| <i data-lucide="minus" class="w-5 h-5"></i> | |
| </button> | |
| <button onclick="centerMap()" class="w-10 h-10 neu-dark rounded-lg flex items-center justify-center text-slate-400 hover:text-accent border border-slate-700/50"> | |
| <i data-lucide="crosshair" class="w-5 h-5"></i> | |
| </button> | |
| </div> | |
| <!-- Single Active Card - Bottom Left --> | |
| <div class="absolute bottom-4 left-4 z-[400] w-80"> | |
| <div class="neu-dark rounded-xl border border-accent/20 overflow-hidden"> | |
| <div class="p-4 border-b border-slate-700/30 flex items-center justify-between bg-slate-800/30"> | |
| <div class="flex items-center space-x-2"> | |
| <i data-lucide="navigation" class="w-4 h-4 text-accent"></i> | |
| <span class="font-semibold text-slate-200">Active Transit</span> | |
| </div> | |
| <span class="text-xs font-mono text-accent">T-04</span> | |
| </div> | |
| <div class="p-4 space-y-4"> | |
| <div class="flex items-start space-x-3"> | |
| <div class="w-10 h-10 neu-dark-inset rounded-full flex items-center justify-center border border-slate-600 flex-shrink-0"> | |
| <span class="text-accent font-bold text-sm">LP</span> | |
| </div> | |
| <div class="flex-1"> | |
| <h3 class="font-bold text-slate-100">Lisa Park</h3> | |
| <p class="text-xs text-slate-500">Site Supervisor</p> | |
| <div class="mt-2 flex items-center space-x-2"> | |
| <span class="px-2 py-0.5 bg-green-500/10 text-green-400 text-[10px] rounded border border-green-500/20">On Track</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="grid grid-cols-2 gap-3"> | |
| <div class="neu-dark-inset rounded-lg p-2 border border-slate-700/30"> | |
| <p class="text-[10px] text-slate-500 uppercase tracking-wider mb-1">Destination</p> | |
| <p class="text-sm font-medium text-slate-300 truncate">Office Tower</p> | |
| </div> | |
| <div class="neu-dark-inset rounded-lg p-2 border border-slate-700/30"> | |
| <p class="text-[10px] text-slate-500 uppercase tracking-wider mb-1">ETA</p> | |
| <p class="text-sm font-medium text-accent">3 min</p> | |
| </div> | |
| </div> | |
| <div class="h-1.5 bg-slate-800 rounded-full overflow-hidden"> | |
| <div class="h-full bg-gradient-to-r from-accent to-yellow-600 rounded-full" style="width: 75%"></div> | |
| </div> | |
| <div class="flex justify-between text-[10px] text-slate-500"> | |
| <span>Distance: 0.4 mi</span> | |
| <span>75% complete</span> | |
| </div> | |
| </div> | |
| <div class="p-3 border-t border-slate-700/30 bg-slate-800/20 flex space-x-2"> | |
| <button class="flex-1 neu-btn py-2 rounded-lg text-xs font-medium text-slate-300 hover:text-accent flex items-center justify-center space-x-1"> | |
| <i data-lucide="phone" class="w-3 h-3"></i> | |
| <span>Contact</span> | |
| </button> | |
| <button class="flex-1 neu-btn py-2 rounded-lg text-xs font-medium text-slate-300 hover:text-accent flex items-center justify-center space-x-1"> | |
| <i data-lucide="shield" class="w-3 h-3"></i> | |
| <span>Escort</span> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Security Coverage Toggle - Bottom Right --> | |
| <div class="absolute bottom-4 right-4 z-[400]"> | |
| <button onclick="toggleCoverage()" id="coverage-btn" class="neu-dark px-4 py-3 rounded-xl border border-slate-700/50 text-slate-300 hover:text-accent flex items-center space-x-2"> | |
| <i data-lucide="shield" class="w-5 h-5"></i> | |
| <span class="text-sm font-medium">Coverage Zones</span> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| <script> | |
| lucide.createIcons(); | |
| // Update Time | |
| function updateTime() { | |
| const now = new Date(); | |
| document.getElementById('current-time').textContent = now.toLocaleTimeString('en-US', { | |
| hour12: false, | |
| hour: '2-digit', | |
| minute: '2-digit', | |
| second: '2-digit' | |
| }); | |
| } | |
| setInterval(updateTime, 1000); | |
| updateTime(); | |
| // Initialize Map | |
| const map = L.map('map', { | |
| zoomControl: false, | |
| attributionControl: false | |
| }).setView([40.7128, -74.0060], 15); | |
| L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', { | |
| maxZoom: 19 | |
| }).addTo(map); | |
| // Custom Yellow Icon | |
| const yellowIcon = L.divIcon({ | |
| className: 'custom-div-icon', | |
| html: `<div style="background-color: #eab308; width: 14px; height: 14px; border-radius: 50%; border: 3px solid #0f172a; box-shadow: 0 0 0 2px rgba(234, 179, 8, 0.4);"></div>`, | |
| iconSize: [14, 14], | |
| iconAnchor: [7, 7] | |
| }); | |
| const greenIcon = L.divIcon({ | |
| className: 'custom-div-icon', | |
| html: `<div style="background-color: #10b981; width: 12px; height: 12px; border-radius: 50%; border: 2px solid #0f172a;"></div>`, | |
| iconSize: [12, 12], | |
| iconAnchor: [6, 6] | |
| }); | |
| // Active Staff Marker (Lisa Park) | |
| const activeMarker = L.marker([40.7128, -74.006], { icon: yellowIcon }).addTo(map) | |
| .bindPopup(` | |
| <div style="background: #1e293b; color: #e2e8f0; padding: 8px; border-radius: 8px; min-width: 150px; border: 1px solid rgba(234, 179, 8, 0.3);"> | |
| <div style="color: #eab308; font-weight: bold; margin-bottom: 4px;">Lisa Park</div> | |
| <div style="font-size: 12px; color: #64748b;">Site Supervisor</div> | |
| <div style="font-size: 11px; color: #10b981; margin-top: 4px;">● On Track</div> | |
| </div> | |
| `, { closeButton: false }); | |
| activeMarker.openPopup(); | |
| // Other Staff Markers | |
| const otherLocations = [ | |
| [40.714, -74.008], [40.711, -74.004], [40.715, -74.010], [40.710, -74.002] | |
| ]; | |
| otherLocations.forEach((loc, i) => { | |
| L.marker(loc, { icon: greenIcon }).addTo(map) | |
| .bindPopup(`<div style="color: #94a3b8; font-size: 12px;">Unit ${i + 1}</div>`, { | |
| closeButton: false, | |
| className: 'bg-slate-800 text-slate-200' | |
| }); | |
| }); | |
| // Security Circles (Coverage) | |
| const securityZones = []; | |
| const coverageBtn = document.getElementById('coverage-btn'); | |
| let coverageEnabled = false; | |
| function toggleCoverage() { | |
| coverageEnabled = !coverageEnabled; | |
| if (coverageEnabled) { | |
| coverageBtn.classList.add('border-accent', 'text-accent'); | |
| securityZones.push(L.circle([40.714, -74.008], { | |
| color: '#eab308', | |
| fillColor: '#eab308', | |
| fillOpacity: 0.05, | |
| radius: 500, | |
| weight: 1, | |
| dashArray: '4, 4' | |
| }).addTo(map)); | |
| } else { | |
| coverageBtn.classList.remove('border-accent', 'text-accent'); | |
| securityZones.forEach(z => map.removeLayer(z)); | |
| securityZones.length = 0; | |
| } | |
| } | |
| function centerMap() { | |
| map.setView([40.7128, -74.006], 15); | |
| } | |
| // Mobile Menu | |
| document.querySelector('.md\\\\:hidden').addEventListener('click', () => { | |
| document.querySelector('aside').classList.toggle('-translate-x-full'); | |
| }); | |
| </script> | |
| </body> | |
| </html> |