Spaces:
Running
Running
| <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="theme-color" content="#0f172a"> | |
| <meta name="apple-mobile-web-app-capable" content="yes"> | |
| <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"> | |
| <title>SpeedGuard Rewards - Smart Speed Tracking</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://unpkg.com/lucide@latest"></script> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet"> | |
| <style> | |
| * { | |
| font-family: 'Inter', sans-serif; | |
| -webkit-tap-highlight-color: transparent; | |
| } | |
| body { | |
| background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%); | |
| min-height: 100vh; | |
| } | |
| .glass-panel { | |
| background: rgba(255, 255, 255, 0.05); | |
| backdrop-filter: blur(10px); | |
| -webkit-backdrop-filter: blur(10px); | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| } | |
| .speed-gauge { | |
| background: conic-gradient(from 180deg, #10b981 0deg, #f59e0b 120deg, #ef4444 240deg, #dc2626 360deg); | |
| border-radius: 50%; | |
| padding: 8px; | |
| } | |
| .speed-gauge-inner { | |
| background: #0f172a; | |
| border-radius: 50%; | |
| } | |
| .pulse-ring { | |
| animation: pulse-ring 2s cubic-bezier(0.215, 0.61, 0.355, 1) infinite; | |
| } | |
| @keyframes pulse-ring { | |
| 0% { transform: scale(0.8); opacity: 1; } | |
| 100% { transform: scale(1.4); opacity: 0; } | |
| } | |
| .slide-up { | |
| animation: slideUp 0.3s ease-out; | |
| } | |
| @keyframes slideUp { | |
| from { transform: translateY(100%); opacity: 0; } | |
| to { transform: translateY(0); opacity: 1; } | |
| } | |
| .shake { | |
| animation: shake 0.5s ease-in-out; | |
| } | |
| @keyframes shake { | |
| 0%, 100% { transform: translateX(0); } | |
| 25% { transform: translateX(-10px); } | |
| 75% { transform: translateX(10px); } | |
| } | |
| .coupon-card { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| } | |
| .fine-alert { | |
| background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); | |
| } | |
| .reward-badge { | |
| background: linear-gradient(135deg, #fa709a 0%, #fee140 100%); | |
| } | |
| /* Hide scrollbar */ | |
| .no-scrollbar::-webkit-scrollbar { | |
| display: none; | |
| } | |
| .no-scrollbar { | |
| -ms-overflow-style: none; | |
| scrollbar-width: none; | |
| } | |
| .zone-indicator { | |
| transition: all 0.3s ease; | |
| } | |
| .tracking-active { | |
| box-shadow: 0 0 20px rgba(16, 185, 129, 0.5); | |
| } | |
| </style> | |
| </head> | |
| <body class="text-white overflow-x-hidden"> | |
| <!-- App Container --> | |
| <div id="app" class="max-w-md mx-auto min-h-screen relative pb-20"> | |
| <!-- Header --> | |
| <header class="sticky top-0 z-50 glass-panel px-4 py-3 flex items-center justify-between"> | |
| <div class="flex items-center gap-2"> | |
| <div class="w-10 h-10 bg-gradient-to-br from-emerald-400 to-blue-500 rounded-xl flex items-center justify-center"> | |
| <i data-lucide="gauge" class="w-5 h-5 text-white"></i> | |
| </div> | |
| <div> | |
| <h1 class="font-bold text-lg leading-tight">SpeedGuard</h1> | |
| <p class="text-xs text-gray-400">Smart Highway Tracker</p> | |
| </div> | |
| </div> | |
| <div class="flex items-center gap-3"> | |
| <div class="text-right"> | |
| <p class="text-xs text-gray-400">Points</p> | |
| <p class="font-bold text-emerald-400" id="totalPoints">0</p> | |
| </div> | |
| <button onclick="showProfile()" class="w-10 h-10 rounded-full bg-gray-700 flex items-center justify-center"> | |
| <i data-lucide="user" class="w-5 h-5"></i> | |
| </button> | |
| </div> | |
| </header> | |
| <!-- Main Dashboard --> | |
| <main class="p-4 space-y-4"> | |
| <!-- Speedometer Section --> | |
| <div class="glass-panel rounded-2xl p-6 relative overflow-hidden"> | |
| <div class="absolute top-0 right-0 p-4"> | |
| <span id="zoneBadge" class="px-3 py-1 rounded-full text-xs font-semibold bg-gray-700 text-gray-300 zone-indicator"> | |
| Highway Zone A | |
| </span> | |
| </div> | |
| <!-- Speed Gauge --> | |
| <div class="flex flex-col items-center justify-center py-4"> | |
| <div class="relative w-48 h-48"> | |
| <div class="absolute inset-0 speed-gauge"></div> | |
| <div class="absolute inset-2 speed-gauge-inner flex flex-col items-center justify-center"> | |
| <span id="currentSpeed" class="text-5xl font-bold tabular-nums">0</span> | |
| <span class="text-gray-400 text-sm mt-1">km/h</span> | |
| </div> | |
| <!-- Speed Indicator Needle (visual) --> | |
| <div id="speedNeedle" class="absolute top-1/2 left-1/2 w-1 h-20 bg-white origin-bottom transform -translate-x-1/2 -translate-y-full rotate-0 transition-transform duration-300" style="transform: translate(-50%, -100%) rotate(-90deg);"></div> | |
| </div> | |
| </div> | |
| <!-- Speed Limit Display --> | |
| <div class="flex justify-center items-center gap-4 mt-2"> | |
| <div class="text-center"> | |
| <p class="text-xs text-gray-400 mb-1">Speed Limit</p> | |
| <div class="flex items-center gap-1 text-2xl font-bold text-white"> | |
| <i data-lucide="alert-circle" class="w-5 h-5 text-amber-400"></i> | |
| <span id="speedLimit">120</span> | |
| <span class="text-sm text-gray-400">km/h</span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Camera Status --> | |
| <div class="mt-4 flex items-center justify-center gap-2 text-sm"> | |
| <div id="cameraStatus" class="flex items-center gap-2 px-3 py-1 rounded-full bg-emerald-500/20 text-emerald-400"> | |
| <div class="w-2 h-2 bg-emerald-400 rounded-full animate-pulse"></div> | |
| <span>Camera Connected</span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Control Buttons --> | |
| <div class="grid grid-cols-2 gap-3"> | |
| <button id="startTracking" onclick="toggleTracking()" class="glass-panel rounded-xl p-4 flex flex-col items-center gap-2 hover:bg-white/10 transition-all active:scale-95"> | |
| <div id="trackingIcon" class="w-12 h-12 rounded-full bg-emerald-500/20 flex items-center justify-center text-emerald-400"> | |
| <i data-lucide="play" class="w-6 h-6"></i> | |
| </div> | |
| <span id="trackingText" class="font-medium">Start Tracking</span> | |
| </button> | |
| <button onclick="showHistory()" class="glass-panel rounded-xl p-4 flex flex-col items-center gap-2 hover:bg-white/10 transition-all active:scale-95"> | |
| <div class="w-12 h-12 rounded-full bg-blue-500/20 flex items-center justify-center text-blue-400"> | |
| <i data-lucide="history" class="w-6 h-6"></i> | |
| </div> | |
| <span class="font-medium">History</span> | |
| </button> | |
| </div> | |
| <!-- Stats Cards --> | |
| <div class="grid grid-cols-3 gap-3"> | |
| <div class="glass-panel rounded-xl p-3 text-center"> | |
| <i data-lucide="navigation" class="w-5 h-5 mx-auto mb-1 text-blue-400"></i> | |
| <p class="text-xs text-gray-400">Distance</p> | |
| <p id="totalDistance" class="font-bold text-sm">0 km</p> | |
| </div> | |
| <div class="glass-panel rounded-xl p-3 text-center"> | |
| <i data-lucide="clock" class="w-5 h-6 mx-auto mb-1 text-purple-400"></i> | |
| <p class="text-xs text-gray-400">Drive Time</p> | |
| <p id="driveTime" class="font-bold text-sm">00:00</p> | |
| </div> | |
| <div class="glass-panel rounded-xl p-3 text-center"> | |
| <i data-lucide="shield-check" class="w-5 h-5 mx-auto mb-1 text-emerald-400"></i> | |
| <p class="text-xs text-gray-400">Safe Drives</p> | |
| <p id="safeDrives" class="font-bold text-sm">0</p> | |
| </div> | |
| </div> | |
| <!-- Rewards Section --> | |
| <div> | |
| <div class="flex items-center justify-between mb-3"> | |
| <h2 class="font-semibold text-lg">Your Rewards 🎁</h2> | |
| <button onclick="showAllCoupons()" class="text-sm text-emerald-400">View All</button> | |
| </div> | |
| <div id="couponsList" class="space-y-3"> | |
| <!-- Coupons will be dynamically inserted here --> | |
| </div> | |
| </div> | |
| <!-- Recent Alerts --> | |
| <div> | |
| <h2 class="font-semibold text-lg mb-3">Recent Alerts</h2> | |
| <div id="alertsList" class="space-y-2 max-h-40 overflow-y-auto no-scrollbar"> | |
| <div class="glass-panel rounded-lg p-3 text-center text-gray-400 text-sm"> | |
| No alerts yet. Start driving to receive alerts. | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| <!-- Bottom Navigation --> | |
| <nav class="fixed bottom-0 left-0 right-0 glass-panel border-t border-white/10 px-6 py-3 max-w-md mx-auto"> | |
| <div class="flex justify-around items-center"> | |
| <button onclick="showDashboard()" class="flex flex-col items-center gap-1 text-emerald-400"> | |
| <i data-lucide="home" class="w-6 h-6"></i> | |
| <span class="text-xs">Home</span> | |
| </button> | |
| <button onclick="showRewards()" class="flex flex-col items-center gap-1 text-gray-400 hover:text-white transition-colors"> | |
| <i data-lucide="gift" class="w-6 h-6"></i> | |
| <span class="text-xs">Rewards</span> | |
| </button> | |
| <button onclick="showFines()" class="flex flex-col items-center gap-1 text-gray-400 hover:text-white transition-colors"> | |
| <i data-lucide="receipt" class="w-6 h-6"></i> | |
| <span class="text-xs">Fines</span> | |
| </button> | |
| <button onclick="showSettings()" class="flex flex-col items-center gap-1 text-gray-400 hover:text-white transition-colors"> | |
| <i data-lucide="settings" class="w-6 h-6"></i> | |
| <span class="text-xs">Settings</span> | |
| </button> | |
| </div> | |
| </nav> | |
| <!-- Overspeeding Alert Modal --> | |
| <div id="overspeedModal" class="hidden fixed inset-0 z-50 bg-black/80 flex items-center justify-center p-4"> | |
| <div class="glass-panel rounded-2xl p-6 w-full max-w-sm slide-up"> | |
| <div class="flex items-center gap-3 mb-4"> | |
| <div class="w-12 h-12 rounded-full bg-red-500/20 flex items-center justify-center animate-pulse"> | |
| <i data-lucide="alert-triangle" class="w-6 h-6 text-red-500"></i> | |
| </div> | |
| <div> | |
| <h3 class="font-bold text-lg text-red-400">SPEEDING ALERT!</h3> | |
| <p class="text-sm text-gray-400">Camera detected violation</p> | |
| </div> | |
| </div> | |
| <div class="bg-red-500/10 rounded-xl p-4 mb-4"> | |
| <div class="flex justify-between items-center mb-2"> | |
| <span class="text-gray-400">Your Speed</span> | |
| <span id="violationSpeed" class="text-2xl font-bold text-red-400">135 km/h</span> | |
| </div> | |
| <div class="flex justify-between items-center mb-2"> | |
| <span class="text-gray-400">Speed Limit</span> | |
| <span id="violationLimit" class="font-semibold">120 km/h</span> | |
| </div> | |
| <div class="flex justify-between items-center pt-2 border-t border-white/10"> | |
| <span class="text-gray-400">Excess</span> | |
| <span id="excessSpeed" class="font-bold text-red-400">+15 km/h</span> | |
| </div> | |
| </div> | |
| <div class="fine-alert rounded-xl p-4 mb-4 text-center"> | |
| <p class="text-sm mb-1">Fine Amount</p> | |
| <p id="fineAmount" class="text-3xl font-bold">$50.00</p> | |
| </div> | |
| <button onclick="acknowledgeFine()" class="w-full bg-red-500 hover:bg-red-600 text-white font-semibold py-3 rounded-xl transition-colors"> | |
| Acknowledge & Pay | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Reward Earned Modal --> | |
| <div id="rewardModal" class="hidden fixed inset-0 z-50 bg-black/80 flex items-center justify-center p-4"> | |
| <div class="glass-panel rounded-2xl p-6 w-full max-w-sm slide-up text-center"> | |
| <div class="w-20 h-20 mx-auto mb-4 reward-badge rounded-full flex items-center justify-center animate-bounce"> | |
| <i data-lucide="trophy" class="w-10 h-10 text-white"></i> | |
| </div> | |
| <h3 class="font-bold text-2xl mb-2">Reward Earned! 🎉</h3> | |
| <p class="text-gray-400 mb-4">Great job maintaining safe speed!</p> | |
| <div class="coupon-card rounded-xl p-4 mb-4"> | |
| <div class="flex items-center justify-between mb-2"> | |
| <span class="text-xs bg-white/20 px-2 py-1 rounded">COUPON</span> | |
| <span id="rewardPoints" class="text-2xl font-bold">+50 pts</span> | |
| </div> | |
| <p id="rewardTitle" class="font-semibold text-lg">Fuel Discount</p> | |
| <p id="rewardDesc" class="text-sm text-white/80">10% off at Shell Gas Stations</p> | |
| </div> | |
| <button onclick="closeReward()" class="w-full bg-gradient-to-r from-emerald-500 to-blue-500 hover:opacity-90 text-white font-semibold py-3 rounded-xl transition-opacity"> | |
| Awesome! | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Pages (Hidden by default) --> | |
| <!-- History Page --> | |
| <div id="historyPage" class="hidden fixed inset-0 z-40 bg-gray-900 pt-16 pb-20 overflow-y-auto"> | |
| <div class="p-4"> | |
| <div class="flex items-center gap-3 mb-6"> | |
| <button onclick="showDashboard()" class="w-10 h-10 rounded-full glass-panel flex items-center justify-center"> | |
| <i data-lucide="arrow-left" class="w-5 h-5"></i> | |
| </button> | |
| <h2 class="font-bold text-xl">Trip History</h2> | |
| </div> | |
| <div id="historyList" class="space-y-3"> | |
| <!-- History items --> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- All Coupons Page --> | |
| <div id="couponsPage" class="hidden fixed inset-0 z-40 bg-gray-900 pt-16 pb-20 overflow-y-auto"> | |
| <div class="p-4"> | |
| <div class="flex items-center gap-3 mb-6"> | |
| <button onclick="showDashboard()" class="w-10 h-10 rounded-full glass-panel flex items-center justify-center"> | |
| <i data-lucide="arrow-left" class="w-5 h-5"></i> | |
| </button> | |
| <h2 class="font-bold text-xl">All Rewards</h2> | |
| </div> | |
| <div id="allCouponsList" class="grid grid-cols-2 gap-3"> | |
| <!-- All coupons --> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // Initialize Lucide icons | |
| lucide.createIcons(); | |
| // App State | |
| const state = { | |
| isTracking: false, | |
| currentSpeed: 0, | |
| speedLimit: 120, | |
| totalPoints: parseInt(localStorage.getItem('totalPoints')) || 0, | |
| totalDistance: parseFloat(localStorage.getItem('totalDistance')) || 0, | |
| safeDrives: parseInt(localStorage.getItem('safeDrives')) || 0, | |
| driveTime: parseInt(localStorage.getItem('driveTime')) || 0, | |
| fines: JSON.parse(localStorage.getItem('fines')) || [], | |
| trips: JSON.parse(localStorage.getItem('trips')) || [], | |
| coupons: JSON.parse(localStorage.getItem('coupons')) || [ | |
| { id: 1, title: 'Fuel Discount', desc: '10% off at Shell', points: 50, claimed: false, icon: 'fuel' }, | |
| { id: 2, title: 'Coffee Free', desc: 'Free coffee at Starbucks', points: 100, claimed: false, icon: 'coffee' }, | |
| { id: 3, title: 'Car Wash', desc: '50% off premium wash', points: 75, claimed: false, icon: 'droplets' }, | |
| { id: 4, title: 'Parking', desc: '2 hours free parking', points: 30, claimed: false, icon: 'parking-circle' } | |
| ], | |
| currentZone: 'Highway Zone A', | |
| watchId: null, | |
| startTime: null, | |
| lastPosition: null, | |
| consecutiveSafeChecks: 0 | |
| }; | |
| // Speed Zones (Simulated camera zones) | |
| const speedZones = [ | |
| { name: 'Highway Zone A', limit: 120, lat: 0, lng: 0, radius: 5000 }, | |
| { name: 'Urban Zone B', limit: 80, lat: 0, lng: 0, radius: 3000 }, | |
| { name: 'School Zone C', limit: 40, lat: 0, lng: 0, radius: 1000 }, | |
| { name: 'Highway Zone D', limit: 100, lat: 0, lng: 0, radius: 4000 } | |
| ]; | |
| // Initialize | |
| function init() { | |
| updateUI(); | |
| renderCoupons(); | |
| renderHistory(); | |
| } | |
| // Update UI | |
| function updateUI() { | |
| document.getElementById('totalPoints').textContent = state.totalPoints; | |
| document.getElementById('totalDistance').textContent = state.totalDistance.toFixed(1) + ' km'; | |
| document.getElementById('safeDrives').textContent = state.safeDrives; | |
| const minutes = Math.floor(state.driveTime / 60); | |
| const hours = Math.floor(minutes / 60); | |
| const mins = minutes % 60; | |
| document.getElementById('driveTime').textContent = | |
| hours > 0 ? `${hours}:${mins.toString().padStart(2, '0')}` : `00:${mins.toString().padStart(2, '0')}`; | |
| } | |
| // Toggle Tracking | |
| function toggleTracking() { | |
| if (!state.isTracking) { | |
| startTracking(); | |
| } else { | |
| stopTracking(); | |
| } | |
| } | |
| function startTracking() { | |
| if (!navigator.geolocation) { | |
| alert('Geolocation is not supported by your device'); | |
| return; | |
| } | |
| state.isTracking = true; | |
| state.startTime = Date.now(); | |
| // Update UI | |
| document.getElementById('trackingIcon').className = 'w-12 h-12 rounded-full bg-red-500/20 flex items-center justify-center text-red-400 tracking-active'; | |
| document.getElementById('trackingText').textContent = 'Stop Tracking'; | |
| document.getElementById('trackingIcon').innerHTML = '<i data-lucide="square" class="w-6 h-6"></i>'; | |
| lucide.createIcons(); | |
| // Start GPS tracking | |
| state.watchId = navigator.geolocation.watchPosition( | |
| handlePosition, | |
| handleError, | |
| { | |
| enableHighAccuracy: true, | |
| maximumAge: 1000, | |
| timeout: 5000 | |
| } | |
| ); | |
| // Simulate zone changes and speed variations for demo | |
| simulateDriving(); | |
| addAlert('Tracking started - Camera connected', 'success'); | |
| } | |
| function stopTracking() { | |
| state.isTracking = false; | |
| if (state.watchId) { | |
| navigator.geolocation.clearWatch(state.watchId); | |
| } | |
| // Calculate trip stats | |
| if (state.startTime) { | |
| const tripDuration = Math.floor((Date.now() - state.startTime) / 60000); | |
| state.driveTime += tripDuration; | |
| const trip = { | |
| date: new Date().toISOString(), | |
| duration: tripDuration, | |
| distance: (state.currentSpeed * tripDuration / 60).toFixed(1), | |
| maxSpeed: state.currentSpeed, | |
| status: 'completed' | |
| }; | |
| state.trips.push(trip); | |
| localStorage.setItem('trips', JSON.stringify(state.trips)); | |
| } | |
| // Update UI | |
| document.getElementById('trackingIcon').className = 'w-12 h-12 rounded-full bg-emerald-500/20 flex items-center justify-center text-emerald-400'; | |
| document.getElementById('trackingText').textContent = 'Start Tracking'; | |
| document.getElementById('trackingIcon').innerHTML = '<i data-lucide="play" class="w-6 h-6"></i>'; | |
| document.getElementById('currentSpeed').textContent = '0'; | |
| document.getElementById('speedNeedle').style.transform = 'translate(-50%, -100%) rotate(-90deg)'; | |
| lucide.createIcons(); | |
| clearInterval(state.simulationInterval); | |
| localStorage.setItem('driveTime', state.driveTime); | |
| updateUI(); | |
| renderHistory(); | |
| addAlert('Tracking stopped - Trip saved', 'info'); | |
| } | |
| function handlePosition(position) { | |
| const speed = position.coords.speed || 0; | |
| const speedKmh = Math.round(speed * 3.6); | |
| updateSpeed(speedKmh); | |
| // Calculate distance | |
| if (state.lastPosition) { | |
| const distance = calculateDistance( | |
| state.lastPosition.coords.latitude, | |
| state.lastPosition.coords.longitude, | |
| position.coords.latitude, | |
| position.coords.longitude | |
| ); | |
| state.totalDistance += distance; | |
| localStorage.setItem('totalDistance', state.totalDistance); | |
| } | |
| state.lastPosition = position; | |
| updateUI(); | |
| } | |
| function handleError(error) { | |
| console.error('GPS Error:', error); | |
| addAlert('GPS signal weak - Using simulated data', 'warning'); | |
| } | |
| // Calculate distance between coordinates | |
| function calculateDistance(lat1, lon1, lat2, lon2) { | |
| const R = 6371; // Earth's radius in km | |
| const dLat = (lat2 - lat1) * Math.PI / 180; | |
| const dLon = (lon2 - lon1) * Math.PI / 180; | |
| const a = Math.sin(dLat/2) * Math.sin(dLat/2) + | |
| Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * | |
| Math.sin(dLon/2) * Math.sin(dLon/2); | |
| const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); | |
| return R * c; | |
| } | |
| // Simulate driving for demo purposes | |
| function simulateDriving() { | |
| let baseSpeed = 90; | |
| let zoneIndex = 0; | |
| state.simulationInterval = setInterval(() => { | |
| // Vary speed | |
| const variation = Math.random() * 20 - 10; | |
| let newSpeed = Math.max(0, baseSpeed + variation); | |
| // Occasionally speed up for demo | |
| if (Math.random() > 0.8) { | |
| newSpeed += 30; | |
| } | |
| newSpeed = Math.round(newSpeed); | |
| updateSpeed(newSpeed); | |
| // Change zones periodically | |
| if (Math.random() > 0.95) { | |
| zoneIndex = (zoneIndex + 1) % speedZones.length; | |
| changeZone(speedZones[zoneIndex]); | |
| } | |
| // Update distance | |
| state.totalDistance += (newSpeed / 3600); // km per second | |
| localStorage.setItem('totalDistance', state.totalDistance); | |
| updateUI(); | |
| }, 1000); | |
| } | |
| function updateSpeed(speed) { | |
| state.currentSpeed = speed; | |
| document.getElementById('currentSpeed').textContent = speed; | |
| // Update needle rotation (-90 to 270 degrees) | |
| const maxSpeed = 200; | |
| const rotation = -90 + (speed / maxSpeed) * 360; | |
| document.getElementById('speedNeedle').style.transform = `translate(-50%, -100%) rotate(${rotation}deg)`; | |
| // Check speed limit | |
| checkSpeedLimit(speed); | |
| } | |
| function changeZone(zone) { | |
| state.currentZone = zone.name; | |
| state.speedLimit = zone.limit; | |
| document.getElementById('zoneBadge').textContent = zone.name; | |
| document.getElementById('speedLimit').textContent = zone.limit; | |
| // Visual feedback | |
| document.getElementById('zoneBadge').classList.add('scale-110'); | |
| setTimeout(() => { | |
| document.getElementById('zoneBadge').classList.remove('scale-110'); | |
| }, 300); | |
| addAlert(`Entered ${zone.name} - Limit: ${zone.limit} km/h`, 'info'); | |
| } | |
| function checkSpeedLimit(speed) { | |
| const excess = speed - state.speedLimit; | |
| if (excess > 5) { | |
| // Overspeeding - Issue fine | |
| issueFine(speed, excess); | |
| state.consecutiveSafeChecks = 0; | |
| } else if (speed < state.speedLimit && speed > 20) { | |
| // Safe driving - Accumulate points | |
| state.consecutiveSafeChecks++; | |
| if (state.consecutiveSafeChecks >= 30) { // 30 seconds of safe driving | |
| issueReward(); | |
| state.consecutiveSafeChecks = 0; | |
| state.safeDrives++; | |
| localStorage.setItem('safeDrives', state.safeDrives); | |
| } | |
| } | |
| } | |
| function issueFine(speed, excess) { | |
| const fineAmount = excess > 30 ? 200 : excess > 20 ? 100 : 50; | |
| const fine = { | |
| id: Date.now(), | |
| date: new Date().toISOString(), | |
| speed: speed, | |
| limit: state.speedLimit, | |
| excess: excess, | |
| amount: fineAmount, | |
| zone: state.currentZone, | |
| paid: false | |
| }; | |
| state.fines.push(fine); | |
| localStorage.setItem('fines', JSON.stringify(state.fines)); | |
| // Show modal | |
| document.getElementById('violationSpeed').textContent = speed + ' km/h'; | |
| document.getElementById('violationLimit').textContent = state.speedLimit + ' km/h'; | |
| document.getElementById('excessSpeed').textContent = '+' + excess + ' km/h'; | |
| document.getElementById('fineAmount').textContent = '$' + fineAmount + '.00'; | |
| document.getElementById('overspeedModal').classList.remove('hidden'); | |
| // Add alert | |
| addAlert(`FINE ISSUED: $${fineAmount} for speeding in ${state.currentZone}`, 'error'); | |
| // Haptic feedback if available | |
| if (navigator.vibrate) { | |
| navigator.vibrate([200, 100, 200]); | |
| } | |
| } | |
| function acknowledgeFine() { | |
| document.getElementById('overspeedModal').classList.add('hidden'); | |
| addAlert('Fine acknowledged. Drive safely!', 'warning'); | |
| } | |
| function issueReward() { | |
| const points = Math.floor(Math.random() * 30) + 20; | |
| state.totalPoints += points; | |
| localStorage.setItem('totalPoints', state.totalPoints); | |
| // Pick random reward | |
| const rewards = [ | |
| { title: 'Fuel Discount', desc: '10% off at Shell Gas Stations', points: points }, | |
| { title: 'Free Coffee', desc: 'Complimentary coffee at Starbucks', points: points }, | |
| { title: 'Car Wash', desc: 'Premium wash 50% off', points: points }, | |
| { title: 'Parking Credit', desc: '2 hours free parking', points: points } | |
| ]; | |
| const reward = rewards[Math.floor(Math.random() * rewards.length)]; | |
| // Show modal | |
| document.getElementById('rewardPoints').textContent = '+' + points + ' pts'; | |
| document.getElementById('rewardTitle').textContent = reward.title; | |
| document.getElementById('rewardDesc').textContent = reward.desc; | |
| document.getElementById('rewardModal').classList.remove('hidden'); | |
| addAlert(`REWARD EARNED: ${points} points for safe driving!`, 'success'); | |
| updateUI(); | |
| renderCoupons(); | |
| } | |
| function closeReward() { | |
| document.getElementById('rewardModal').classList.add('hidden'); | |
| } | |
| function addAlert(message, type) { | |
| const alertsList = document.getElementById('alertsList'); | |
| const alertDiv = document.createElement('div'); | |
| let bgClass = 'glass-panel'; | |
| let icon = 'info'; | |
| if (type === 'error') { | |
| bgClass = 'bg-red-500/20 border border-red-500/30'; | |
| icon = 'alert-triangle'; | |
| } else if (type === 'success') { | |
| bgClass = 'bg-emerald-500/20 border border-emerald-500/30'; | |
| icon = 'check-circle'; | |
| } else if (type === 'warning') { | |
| bgClass = 'bg-amber-500/20 border border-amber-500/30'; | |
| icon = 'alert-circle'; | |
| } | |
| alertDiv.className = `${bgClass} rounded-lg p-3 flex items-center gap-3 slide-up`; | |
| alertDiv.innerHTML = ` | |
| <i data-lucide="${icon}" class="w-5 h-5 ${type === 'error' ? 'text-red-400' : type === 'success' ? 'text-emerald-400' : 'text-amber-400'}"></i> | |
| <p class="text-sm flex-1">${message}</p> | |
| <span class="text-xs text-gray-400">${new Date().toLocaleTimeString()}</span> | |
| `; | |
| // Remove "no alerts" message | |
| if (alertsList.children.length === 1 && alertsList.children[0].textContent.includes('No alerts')) { | |
| alertsList.innerHTML = ''; | |
| } | |
| alertsList.insertBefore(alertDiv, alertsList.firstChild); | |
| lucide.createIcons(); | |
| // Keep only last 10 alerts | |
| while (alertsList.children.length > 10) { | |
| alertsList.removeChild(alertsList.lastChild); | |
| } | |
| } | |
| function renderCoupons() { | |
| const container = document.getElementById('couponsList'); | |
| const availableCoupons = state.coupons.filter(c => !c.claimed && c.points <= state.totalPoints); | |
| if (availableCoupons.length === 0) { | |
| container.innerHTML = ` | |
| <div class="glass-panel rounded-xl p-4 text-center"> | |
| <p class="text-gray-400 text-sm">Keep driving safely to earn more rewards!</p> | |
| <p class="text-emerald-400 text-xs mt-1">Next reward at ${Math.ceil(state.totalPoints / 50) * 50} points</p> | |
| </div> | |
| `; | |
| return; | |
| } | |
| container.innerHTML = availableCoupons.slice(0, 2).map(coupon => ` | |
| <div class="coupon-card rounded-xl p-4 relative overflow-hidden"> | |
| <div class="absolute top-0 right-0 -mt-2 -mr-2 w-16 h-16 bg-white/20 rounded-full blur-xl"></div> | |
| <div class="flex items-start justify-between relative z-10"> | |
| <div class="flex items-center gap-3"> | |
| <div class="w-10 h-10 rounded-lg bg-white/20 flex items-center justify-center"> | |
| <i data-lucide="${coupon.icon}" class="w-5 h-5"></i> | |
| </div> | |
| <div> | |
| <h4 class="font-semibold">${coupon.title}</h4> | |
| <p class="text-xs text-white/80">${coupon.desc}</p> | |
| </div> | |
| </div> | |
| <div class="text-right"> | |
| <span class="text-xs bg-white/20 px-2 py-1 rounded">${coupon.points} pts</span> | |
| </div> | |
| </div> | |
| <button onclick="claimCoupon(${coupon.id})" class="mt-3 w-full bg-white text-purple-600 font-semibold py-2 rounded-lg text-sm hover:bg-gray-100 transition-colors"> | |
| Claim Reward | |
| </button> | |
| </div> | |
| `).join(''); | |
| lucide.createIcons(); | |
| } | |
| function claimCoupon(id) { | |
| const coupon = state.coupons.find(c => c.id === id); | |
| if (coupon && state.totalPoints >= coupon.points) { | |
| state.totalPoints -= coupon.points; | |
| coupon.claimed = true; | |
| localStorage.setItem('totalPoints', state.totalPoints); | |
| localStorage.setItem('coupons', JSON.stringify(state.coupons)); | |
| updateUI(); | |
| renderCoupons(); | |
| addAlert(`Coupon claimed: ${coupon.title}!`, 'success'); | |
| } | |
| } | |
| function renderHistory() { | |
| const container = document.getElementById('historyList'); | |
| if (state.trips.length === 0) { | |
| container.innerHTML = '<p class="text-center text-gray-400 py-8">No trips recorded yet</p>'; | |
| return; | |
| } | |
| container.innerHTML = state.trips.slice().reverse().map(trip => ` | |
| <div class="glass-panel rounded-xl p-4"> | |
| <div class="flex items-center justify-between mb-2"> | |
| <span class="text-sm font-medium">${new Date(trip.date).toLocaleDateString()}</span> | |
| <span class="text-xs px-2 py-1 rounded-full ${trip.status === 'completed' ? 'bg-emerald-500/20 text-emerald-400' : 'bg-amber-500/20 text-amber-400'}"> | |
| ${trip.status} | |
| </span> | |
| </div> | |
| <div class="flex items-center justify-between text-sm text-gray-400"> | |
| <span><i data-lucide="clock" class="w-4 h-4 inline mr-1"></i> ${trip.duration} min</span> | |
| <span><i data-lucide="navigation" class="w-4 h-4 inline mr-1"></i> ${trip.distance} km</span> | |
| <span><i data-lucide="gauge" class="w-4 h-4 inline mr-1"></i> ${trip.maxSpeed} km/h</span> | |
| </div> | |
| </div> | |
| `).join(''); | |
| lucide.createIcons(); | |
| } | |
| // Navigation functions | |
| function showDashboard() { | |
| document.getElementById('historyPage').classList.add('hidden'); | |
| document.getElementById('couponsPage').classList.add('hidden'); | |
| } | |
| function showHistory() { | |
| document.getElementById('historyPage').classList.remove('hidden'); | |
| } | |
| function showAllCoupons() { | |
| const container = document.getElementById('allCouponsList'); | |
| container.innerHTML = state.coupons.map(coupon => ` | |
| <div class="glass-panel rounded-xl p-3 ${coupon.claimed ? 'opacity-50' : ''}"> | |
| <div class="w-10 h-10 rounded-lg bg-gradient-to-br from-purple-500 to-pink-500 flex items-center justify-center mb-2"> | |
| <i data-lucide="${coupon.icon}" class="w-5 h-5"></i> | |
| </div> | |
| <h4 class="font-semibold text-sm">${coupon.title}</h4> | |
| <p class="text-xs text-gray-400 mb-2">${coupon.desc}</p> | |
| <div class="flex items-center justify-between"> | |
| <span class="text-xs text-emerald-400">${coupon.points} pts</span> | |
| ${coupon.claimed ? | |
| '<span class="text-xs bg-gray-700 px-2 py-1 rounded">Claimed</span>' : | |
| `<button onclick="claimCoupon(${coupon.id})" class="text-xs bg-emerald-500 px-2 py-1 rounded text-white" ${state.totalPoints < coupon.points ? 'disabled class="opacity-50"' : ''}>Claim</button>` | |
| } | |
| </div> | |
| </div> | |
| `).join(''); | |
| lucide.createIcons(); | |
| document.getElementById('couponsPage').classList.remove('hidden'); | |
| } | |
| function showRewards() { | |
| showAllCoupons(); | |
| } | |
| function showFines() { | |
| alert(`You have ${state.fines.length} fines totaling $${state.fines.reduce((a, b) => a + b.amount, 0)}. Feature coming soon!`); | |
| } | |
| function showSettings() { | |
| alert('Settings: GPS Accuracy, Alert Sounds, Notifications - Coming soon!'); | |
| } | |
| function showProfile() { | |
| alert(`Profile Stats:\nTotal Points: ${state.totalPoints}\nSafe Drives: ${state.safeDrives}\nTotal Distance: ${state.totalDistance.toFixed(1)} km\nFines: ${state.fines.length}`); | |
| } | |
| // Initialize app | |
| init(); | |
| </script> | |
| <script src="https://deepsite.hf.co/deepsite-badge.js"></script> | |
| </body> | |
| </html> |