SafeRoute / src /App.tsx
ayushsahu45's picture
Upload 4 files
af43b60 verified
/**
* @license
* SPDX-License-Identifier: Apache-2.0
*/
import { useState, useEffect, lazy, Suspense } from 'react';
import { BrowserRouter as Router, Routes, Route, Link, useLocation } from 'react-router-dom';
import { motion } from 'framer-motion';
import LeafletMapView from './components/LeafletMapView';
import Sidebar from './components/Sidebar';
import { ThinSidebar } from './components/ThinSidebar';
import { Map, Settings, Info, User, ChevronDown, ChevronUp } from 'lucide-react';
const AIAssistant = lazy(() => import('./components/AIAssistant'));
import { OnboardingModal } from './components/OnboardingModal';
import { TripHistory } from './components/TripHistory';
import { IncidentReportModal } from './components/IncidentReportModal';
import { RouteLoadingSkeleton } from './components/SkeletonLoader';
import { requestNotificationPermission, showRouteAlert } from './lib/notifications';
import { useKeyboardShortcuts } from './hooks';
import { getCached, setCache, generateRouteCacheKey } from './lib/routeCache';
import { geocode, getRoute } from './lib/api';
function MapTab({
isSidebarOpen,
routeData, setRouteData,
alternativeRoutes, setAlternativeRoutes,
riskData, setRiskData,
previousRouteData, setPreviousRouteData,
previousRiskData, setPreviousRiskData,
incidentsData, setIncidentsData,
weather, setWeather,
startLoc, setStartLoc,
endLoc, setEndLoc,
startQuery, setStartQuery,
endQuery, setEndQuery,
theme, setTheme,
vehicleType, setVehicleType,
showIncidents, setShowIncidents,
showRoutePlayback, setShowRoutePlayback,
playbackSpeed, setPlaybackSpeed,
mapStyleType, setMapStyleType,
activeLayers, setActiveLayers,
waypoints,
voiceNavEnabled,
onOpenIncidentReport,
onOpenTripHistory,
onToggleVoiceNav
}: any) {
return (
<div className="flex w-full h-full overflow-hidden">
{isSidebarOpen && (
<Sidebar
setRouteData={setRouteData}
setAlternativeRoutes={setAlternativeRoutes}
alternativeRoutes={alternativeRoutes}
setRiskData={setRiskData}
setPreviousRouteData={setPreviousRouteData}
setPreviousRiskData={setPreviousRiskData}
setIncidentsData={setIncidentsData}
setWeather={setWeather}
setStartLoc={setStartLoc}
setEndLoc={setEndLoc}
startLoc={startLoc}
endLoc={endLoc}
startQuery={startQuery}
setStartQuery={setStartQuery}
endQuery={endQuery}
setEndQuery={setEndQuery}
routeData={routeData}
riskData={riskData}
previousRouteData={previousRouteData}
previousRiskData={previousRiskData}
incidentsData={incidentsData}
weather={weather}
theme={theme}
setTheme={setTheme}
vehicleType={vehicleType}
setVehicleType={setVehicleType}
showIncidents={showIncidents}
setShowIncidents={setShowIncidents}
showRoutePlayback={showRoutePlayback}
setShowRoutePlayback={setShowRoutePlayback}
playbackSpeed={playbackSpeed}
setPlaybackSpeed={setPlaybackSpeed}
onOpenIncidentReport={onOpenIncidentReport}
onOpenTripHistory={onOpenTripHistory}
onToggleVoiceNav={onToggleVoiceNav}
voiceNavEnabled={voiceNavEnabled}
/>
)}
<main className="flex-1 relative">
<LeafletMapView
routeData={routeData}
alternativeRoutes={alternativeRoutes}
riskData={riskData}
previousRouteData={previousRouteData}
previousRiskData={previousRiskData}
incidentsData={incidentsData}
startLoc={startLoc}
endLoc={endLoc}
setStartLoc={setStartLoc}
setEndLoc={setEndLoc}
setStartQuery={setStartQuery}
setEndQuery={setEndQuery}
theme={theme}
showIncidents={showIncidents}
showRoutePlayback={showRoutePlayback}
playbackSpeed={playbackSpeed}
mapStyleType={mapStyleType}
setMapStyleType={setMapStyleType}
activeLayers={activeLayers}
setActiveLayers={setActiveLayers}
vehicleType={vehicleType}
waypoints={waypoints}
/>
<Suspense fallback={null}>
<AIAssistant
routeData={routeData}
riskData={riskData}
incidentsData={incidentsData}
weather={weather}
/>
</Suspense>
</main>
</div>
);
}
function SettingsTab({ theme, setTheme, vehicleType, setVehicleType, mapStyleType, setMapStyleType, userProfile, onLogout, onUpdateProfile, highContrast, setHighContrast }: any) {
const [isEditingProfile, setIsEditingProfile] = useState(false);
const [profileName, setProfileName] = useState(userProfile?.name || '');
const [profileEmail, setProfileEmail] = useState(userProfile?.email || '');
const [selectedAvatar, setSelectedAvatar] = useState(userProfile?.photo || '');
const avatarOptions = [
{ id: 'avataaars', label: 'Avatar', url: (name: string) => `https://api.dicebear.com/7.x/avataaars/svg?seed=${encodeURIComponent(name)}` },
{ id: 'male', label: 'Male', url: (name: string) => `https://api.dicebear.com/7.x/personas/svg?seed=${encodeURIComponent(name)}&backgroundColor=b6e3f4` },
{ id: 'female', label: 'Female', url: (name: string) => `https://api.dicebear.com/7.x/personas/svg?seed=${encodeURIComponent(name)}&backgroundColor=ffd5dc` },
{ id: 'children', label: 'Children', url: (name: string) => `https://api.dicebear.com/7.x/croodles/svg?seed=${encodeURIComponent(name)}` },
{ id: 'identicon', label: 'Abstract', url: (name: string) => `https://api.dicebear.com/7.x/identicon/svg?seed=${encodeURIComponent(name)}` },
{ id: 'micah', label: 'Micah', url: (name: string) => `https://api.dicebear.com/7.x/micah/svg?seed=${encodeURIComponent(name)}` },
{ id: 'miniavs', label: 'Mini', url: (name: string) => `https://api.dicebear.com/7.x/miniavs/svg?seed=${encodeURIComponent(name)}` },
{ id: 'pixel-art', label: 'Pixel', url: (name: string) => `https://api.dicebear.com/7.x/pixel-art/svg?seed=${encodeURIComponent(name)}` }
];
useEffect(() => {
setProfileName(userProfile?.name || '');
setProfileEmail(userProfile?.email || '');
setSelectedAvatar(userProfile?.photo || '');
}, [userProfile]);
const handleSaveProfile = () => {
if (profileName.trim()) {
const photo = selectedAvatar || avatarOptions[0].url(profileName);
onUpdateProfile(profileName, profileEmail, photo);
setIsEditingProfile(false);
}
};
const handleDeleteProfile = () => {
if (confirm('Are you sure you want to delete your profile?')) {
onLogout();
}
};
const handleClearAllData = () => {
if (confirm('This will clear all saved locations, recent searches, and your profile. Continue?')) {
localStorage.removeItem('savedLocations');
localStorage.removeItem('recentSearches');
localStorage.removeItem('userProfile');
onLogout();
}
};
const getAvatarUrl = (avatarId: string) => {
const avatar = avatarOptions.find(a => a.id === avatarId);
return avatar ? avatar.url(profileName || 'user') : avatarOptions[0].url(profileName || 'user');
};
return (
<div className="w-full h-full overflow-y-auto bg-zinc-50 dark:bg-zinc-950 p-6">
<div className="max-w-3xl mx-auto space-y-6">
{/* Header */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="mb-8"
>
<h1 className="text-3xl font-bold text-zinc-900 dark:text-zinc-50 mb-2">Settings</h1>
<p className="text-zinc-600 dark:text-zinc-400">Manage your profile and preferences</p>
</motion.div>
{/* User Profile Section */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
className="bg-white dark:bg-zinc-900 rounded-2xl border border-zinc-200 dark:border-zinc-800 shadow-lg overflow-hidden"
>
<div className="p-6 border-b border-zinc-200 dark:border-zinc-800">
<div className="flex items-center gap-3 mb-1">
<svg xmlns="http://www.w3.org/2000/svg" className="w-5 h-5 text-emerald-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
<h2 className="text-lg font-bold text-zinc-900 dark:text-zinc-50">User Profile</h2>
</div>
<p className="text-sm text-zinc-500 dark:text-zinc-400 ml-8">Manage your personal information</p>
</div>
<div className="p-6">
{userProfile ? (
<>
{isEditingProfile ? (
<div className="space-y-6">
{/* Avatar Selection */}
<div>
<label className="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-3">Choose Avatar Style</label>
<div className="grid grid-cols-4 gap-3">
{avatarOptions.map((avatar) => (
<button
key={avatar.id}
onClick={() => setSelectedAvatar(avatar.url(profileName || 'user'))}
className={`p-2 rounded-xl border-2 transition-all ${
selectedAvatar === avatar.url(profileName || 'user') ||
(selectedAvatar === '' && avatar.id === 'avataaars')
? 'border-emerald-500 bg-emerald-50 dark:bg-emerald-500/20'
: 'border-zinc-200 dark:border-zinc-700 hover:border-emerald-300'
}`}
>
<img
src={avatar.url(profileName || 'user')}
alt={avatar.label}
className="w-full aspect-square rounded-lg bg-zinc-100 dark:bg-zinc-800"
/>
<span className="block text-xs text-center mt-1 text-zinc-600 dark:text-zinc-400">{avatar.label}</span>
</button>
))}
</div>
</div>
{/* Name Input */}
<div>
<label className="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-2">Name</label>
<input
type="text"
value={profileName}
onChange={(e) => {
setProfileName(e.target.value);
// Update avatar preview with new name
if (!selectedAvatar || avatarOptions.some(a => selectedAvatar.includes(a.id))) {
const currentAvatar = avatarOptions.find(a => selectedAvatar?.includes(a.id));
if (currentAvatar) {
setSelectedAvatar(currentAvatar.url(e.target.value));
}
}
}}
className="w-full px-4 py-3 bg-zinc-50 dark:bg-zinc-950 border border-zinc-200 dark:border-zinc-800 rounded-lg text-zinc-900 dark:text-zinc-100 focus:outline-none focus:ring-2 focus:ring-emerald-500/50"
placeholder="Enter your name"
/>
</div>
{/* Email Input */}
<div>
<label className="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-2">Email</label>
<input
type="email"
value={profileEmail}
onChange={(e) => setProfileEmail(e.target.value)}
className="w-full px-4 py-3 bg-zinc-50 dark:bg-zinc-950 border border-zinc-200 dark:border-zinc-800 rounded-lg text-zinc-900 dark:text-zinc-100 focus:outline-none focus:ring-2 focus:ring-emerald-500/50"
placeholder="Enter your email"
/>
</div>
{/* Preview */}
<div className="flex items-center gap-4 p-4 bg-zinc-50 dark:bg-zinc-950 rounded-xl">
<img
src={selectedAvatar || avatarOptions[0].url(profileName || 'user')}
alt="Preview"
className="w-20 h-20 rounded-full border-2 border-emerald-500"
/>
<div>
<p className="font-medium text-zinc-900 dark:text-zinc-100">{profileName || 'Your Name'}</p>
<p className="text-sm text-zinc-500 dark:text-zinc-400">{profileEmail || 'your@email.com'}</p>
<p className="text-xs text-emerald-600 dark:text-emerald-400 mt-1">Preview</p>
</div>
</div>
<div className="flex gap-3">
<button
onClick={handleSaveProfile}
className="flex-1 px-4 py-3 bg-emerald-500 hover:bg-emerald-600 text-white rounded-lg font-medium transition-colors"
>
Save Changes
</button>
<button
onClick={() => {
setIsEditingProfile(false);
setProfileName(userProfile?.name || '');
setProfileEmail(userProfile?.email || '');
setSelectedAvatar(userProfile?.photo || '');
}}
className="px-6 py-3 bg-zinc-100 dark:bg-zinc-800 text-zinc-700 dark:text-zinc-300 rounded-lg font-medium hover:bg-zinc-200 dark:hover:bg-zinc-700 transition-colors"
>
Cancel
</button>
</div>
</div>
) : (
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="w-20 h-20 rounded-full overflow-hidden border-2 border-emerald-500 bg-zinc-100 dark:bg-zinc-800">
{userProfile.photo ? (
<img src={userProfile.photo} alt="Profile" className="w-full h-full object-cover" />
) : (
<div className="w-full h-full bg-emerald-500 flex items-center justify-center text-white text-3xl font-bold">
{userProfile.name?.charAt(0).toUpperCase()}
</div>
)}
</div>
<div>
<h3 className="font-bold text-zinc-900 dark:text-zinc-50 text-xl">{userProfile.name}</h3>
<p className="text-zinc-500 dark:text-zinc-400">{userProfile.email}</p>
<p className="text-xs text-emerald-600 dark:text-emerald-400 mt-1">Active Profile</p>
</div>
</div>
<div className="flex gap-2">
<button
onClick={() => setIsEditingProfile(true)}
className="px-4 py-2 bg-emerald-500 hover:bg-emerald-600 text-white rounded-lg font-medium transition-colors"
>
Edit Profile
</button>
<button
onClick={handleDeleteProfile}
className="px-4 py-2 bg-rose-500/10 text-rose-600 dark:text-rose-400 rounded-lg font-medium hover:bg-rose-500/20 transition-colors"
>
Delete
</button>
</div>
</div>
)}
</>
) : (
<div className="text-center py-8">
<div className="w-20 h-20 bg-zinc-100 dark:bg-zinc-800 rounded-full mx-auto mb-4 flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" className="w-10 h-10 text-zinc-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
</div>
<h3 className="font-bold text-zinc-900 dark:text-zinc-50 mb-2">Create Your Profile</h3>
<p className="text-zinc-500 dark:text-zinc-400 mb-4 max-w-md mx-auto">Set up a profile to save your preferences, locations, and travel history.</p>
<div className="flex gap-2 justify-center">
<input
type="text"
value={profileName}
onChange={(e) => setProfileName(e.target.value)}
className="px-4 py-2 bg-zinc-50 dark:bg-zinc-950 border border-zinc-200 dark:border-zinc-800 rounded-lg text-zinc-900 dark:text-zinc-100 focus:outline-none focus:ring-2 focus:ring-emerald-500/50 w-64"
placeholder="Enter your name"
/>
<button
onClick={handleSaveProfile}
disabled={!profileName.trim()}
className="px-6 py-2 bg-emerald-500 hover:bg-emerald-600 disabled:bg-zinc-300 disabled:cursor-not-allowed text-white rounded-lg font-medium transition-colors"
>
Create Profile
</button>
</div>
</div>
)}
</div>
</motion.div>
{/* Appearance Settings */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="bg-white dark:bg-zinc-900 rounded-2xl border border-zinc-200 dark:border-zinc-800 shadow-lg overflow-hidden"
>
<div className="p-6 border-b border-zinc-200 dark:border-zinc-800">
<div className="flex items-center gap-3 mb-1">
<svg xmlns="http://www.w3.org/2000/svg" className="w-5 h-5 text-emerald-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01" />
</svg>
<h2 className="text-lg font-bold text-zinc-900 dark:text-zinc-50">Appearance</h2>
</div>
<p className="text-sm text-zinc-500 dark:text-zinc-400 ml-8">Customize how SafeRoute looks</p>
</div>
<div className="p-6 space-y-6">
<div className="flex items-center justify-between">
<div>
<h3 className="font-medium text-zinc-900 dark:text-zinc-100">Theme</h3>
<p className="text-sm text-zinc-500 dark:text-zinc-400">Switch between light and dark mode</p>
</div>
<button
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
className={`relative w-16 h-8 rounded-full transition-colors ${theme === 'dark' ? 'bg-emerald-500' : 'bg-zinc-300'}`}
>
<div className={`absolute top-1 w-6 h-6 bg-white rounded-full shadow-md transition-transform ${theme === 'dark' ? 'translate-x-9' : 'translate-x-1'}`} />
</button>
</div>
<div className="flex items-center justify-between pt-4 border-t border-zinc-100 dark:border-zinc-800">
<div>
<h3 className="font-medium text-zinc-900 dark:text-zinc-100">High Contrast Mode</h3>
<p className="text-sm text-zinc-500 dark:text-zinc-400">Increase contrast for better visibility</p>
</div>
<button
onClick={() => setHighContrast(!highContrast)}
className={`relative w-16 h-8 rounded-full transition-colors ${highContrast ? 'bg-emerald-500' : 'bg-zinc-300 dark:bg-zinc-600'}`}
aria-label="Toggle high contrast mode"
>
<div className={`absolute top-1 w-6 h-6 bg-white rounded-full shadow-md transition-transform ${highContrast ? 'translate-x-9' : 'translate-x-1'}`} />
</button>
</div>
<div className="flex items-center justify-between pt-4 border-t border-zinc-100 dark:border-zinc-800">
<div>
<h3 className="font-medium text-zinc-900 dark:text-zinc-100">Voice Navigation</h3>
<p className="text-sm text-zinc-500 dark:text-zinc-400">Enable spoken turn-by-turn directions</p>
</div>
<button
onClick={() => {
const newState = !highContrast;
if (typeof window !== 'undefined' && 'Notification' in window) {
requestNotificationPermission();
}
}}
className={`relative w-16 h-8 rounded-full transition-colors bg-emerald-500`}
aria-label="Voice navigation is enabled"
>
<div className="absolute top-1 w-6 h-6 bg-white rounded-full shadow-md translate-x-9" />
</button>
</div>
</div>
</motion.div>
{/* Preferences */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
className="bg-white dark:bg-zinc-900 rounded-2xl border border-zinc-200 dark:border-zinc-800 shadow-lg overflow-hidden"
>
<div className="p-6 border-b border-zinc-200 dark:border-zinc-800">
<div className="flex items-center gap-3 mb-1">
<svg xmlns="http://www.w3.org/2000/svg" className="w-5 h-5 text-emerald-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4" />
</svg>
<h2 className="text-lg font-bold text-zinc-900 dark:text-zinc-50">Preferences</h2>
</div>
<p className="text-sm text-zinc-500 dark:text-zinc-400 ml-8">Set your default travel preferences</p>
</div>
<div className="p-6 space-y-6">
<div>
<h3 className="font-medium text-zinc-900 dark:text-zinc-100 mb-3">Default Vehicle</h3>
<div className="flex gap-3">
<button
onClick={() => setVehicleType('driving')}
className={`flex-1 p-4 rounded-xl border-2 transition-all ${
vehicleType === 'driving'
? 'border-emerald-500 bg-emerald-500/10'
: 'border-zinc-200 dark:border-zinc-800 hover:border-emerald-300'
}`}
>
<svg xmlns="http://www.w3.org/2000/svg" className={`w-8 h-8 mx-auto mb-2 ${vehicleType === 'driving' ? 'text-emerald-500' : 'text-zinc-400'}`} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M8 7h8m-8 4h8m-4-8v12m-4 0h8a2 2 0 002-2V7a2 2 0 00-2-2H8a2 2 0 00-2 2v6a2 2 0 002 2z" />
</svg>
<p className={`font-medium ${vehicleType === 'driving' ? 'text-emerald-600 dark:text-emerald-400' : 'text-zinc-700 dark:text-zinc-300'}`}>Car</p>
</button>
<button
onClick={() => setVehicleType('cycling')}
className={`flex-1 p-4 rounded-xl border-2 transition-all ${
vehicleType === 'cycling'
? 'border-emerald-500 bg-emerald-500/10'
: 'border-zinc-200 dark:border-zinc-800 hover:border-emerald-300'
}`}
>
<svg xmlns="http://www.w3.org/2000/svg" className={`w-8 h-8 mx-auto mb-2 ${vehicleType === 'cycling' ? 'text-emerald-500' : 'text-zinc-400'}`} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
<p className={`font-medium ${vehicleType === 'cycling' ? 'text-emerald-600 dark:text-emerald-400' : 'text-zinc-700 dark:text-zinc-300'}`}>Bicycle</p>
</button>
</div>
</div>
<div>
<h3 className="font-medium text-zinc-900 dark:text-zinc-100 mb-3">Map Style</h3>
<div className="flex gap-3">
{[
{ value: 'default', label: 'Default', icon: <path strokeLinecap="round" strokeLinejoin="round" d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7" /> },
{ value: 'satellite', label: 'Satellite', icon: <path strokeLinecap="round" strokeLinejoin="round" d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> },
{ value: 'navigation', label: 'Navigation', icon: <path strokeLinecap="round" strokeLinejoin="round" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" /> }
].map((style) => (
<button
key={style.value}
onClick={() => setMapStyleType(style.value)}
className={`flex-1 p-4 rounded-xl border-2 transition-all ${
mapStyleType === style.value
? 'border-emerald-500 bg-emerald-500/10'
: 'border-zinc-200 dark:border-zinc-800 hover:border-emerald-300'
}`}
>
<svg xmlns="http://www.w3.org/2000/svg" className={`w-8 h-8 mx-auto mb-2 ${mapStyleType === style.value ? 'text-emerald-500' : 'text-zinc-400'}`} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
{style.icon}
</svg>
<p className={`font-medium text-sm ${mapStyleType === style.value ? 'text-emerald-600 dark:text-emerald-400' : 'text-zinc-700 dark:text-zinc-300'}`}>{style.label}</p>
</button>
))}
</div>
</div>
</div>
</motion.div>
{/* Data Management */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4 }}
className="bg-white dark:bg-zinc-900 rounded-2xl border border-zinc-200 dark:border-zinc-800 shadow-lg overflow-hidden"
>
<div className="p-6 border-b border-zinc-200 dark:border-zinc-800">
<div className="flex items-center gap-3 mb-1">
<svg xmlns="http://www.w3.org/2000/svg" className="w-5 h-5 text-rose-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
<h2 className="text-lg font-bold text-zinc-900 dark:text-zinc-50">Data Management</h2>
</div>
<p className="text-sm text-zinc-500 dark:text-zinc-400 ml-8">Manage your saved data and history</p>
</div>
<div className="p-6 space-y-4">
<div className="flex items-center justify-between p-4 bg-zinc-50 dark:bg-zinc-950 rounded-xl">
<div>
<h3 className="font-medium text-zinc-900 dark:text-zinc-100">Saved Locations</h3>
<p className="text-sm text-zinc-500 dark:text-zinc-400">Your bookmarked places</p>
</div>
<span className="px-3 py-1 bg-emerald-100 dark:bg-emerald-500/20 text-emerald-600 dark:text-emerald-400 rounded-full text-sm font-medium">
{((): number => {
try {
return JSON.parse(localStorage.getItem('savedLocations') || '[]').length;
} catch {
return 0;
}
})()} saved
</span>
</div>
<div className="flex items-center justify-between p-4 bg-zinc-50 dark:bg-zinc-950 rounded-xl">
<div>
<h3 className="font-medium text-zinc-900 dark:text-zinc-100">Recent Searches</h3>
<p className="text-sm text-zinc-500 dark:text-zinc-400">Your search history</p>
</div>
<span className="px-3 py-1 bg-emerald-100 dark:bg-emerald-500/20 text-emerald-600 dark:text-emerald-400 rounded-full text-sm font-medium">
{((): number => {
try {
return JSON.parse(localStorage.getItem('recentSearches') || '[]').length;
} catch {
return 0;
}
})()} searches
</span>
</div>
<button
onClick={handleClearAllData}
className="w-full mt-4 px-4 py-3 bg-rose-500/10 text-rose-600 dark:text-rose-400 rounded-xl font-medium hover:bg-rose-500/20 transition-colors flex items-center justify-center gap-2"
>
<svg xmlns="http://www.w3.org/2000/svg" className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
Clear All Data
</button>
</div>
</motion.div>
{/* Version Info */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.5 }}
className="text-center py-6"
>
<p className="text-sm text-zinc-400">SafeRoute v1.0.0</p>
<p className="text-xs text-emerald-600 dark:text-emerald-400 mt-1 font-medium">Made-with-Ayush</p>
</motion.div>
</div>
</div>
);
}
function AboutTab() {
return (
<div className="w-full h-full overflow-y-auto bg-zinc-50 dark:bg-zinc-950">
<div className="max-w-5xl mx-auto px-6 py-12">
{/* Hero Section */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
className="text-center mb-16"
>
<div className="inline-flex items-center gap-3 mb-6">
<div className="w-14 h-14 bg-emerald-500/10 rounded-2xl flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" className="w-8 h-8 text-emerald-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7" />
</svg>
</div>
</div>
<h1 className="text-4xl md:text-5xl font-bold text-zinc-900 dark:text-zinc-50 mb-4">
About SafeRoute
</h1>
<p className="text-lg text-zinc-600 dark:text-zinc-400 max-w-2xl mx-auto leading-relaxed">
An intelligent traffic safety platform that combines real-time routing, weather analysis, and AI-powered risk prediction to help you travel safer.
</p>
</motion.div>
{/* Problem & Solution */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.1 }}
className="grid md:grid-cols-2 gap-6 mb-16"
>
<div className="bg-white dark:bg-zinc-900 rounded-2xl p-8 border border-zinc-200 dark:border-zinc-800 shadow-lg hover:shadow-xl transition-shadow duration-300">
<div className="w-12 h-12 bg-rose-500/10 rounded-xl flex items-center justify-center mb-4">
<svg xmlns="http://www.w3.org/2000/svg" className="w-6 h-6 text-rose-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
</div>
<h3 className="text-xl font-bold text-zinc-900 dark:text-zinc-50 mb-3">The Problem</h3>
<p className="text-zinc-600 dark:text-zinc-400 leading-relaxed">
Road accidents claim over 1.3 million lives annually. Traditional navigation apps lack real-time safety insights, leaving drivers vulnerable to changing road conditions.
</p>
</div>
<div className="bg-white dark:bg-zinc-900 rounded-2xl p-8 border border-zinc-200 dark:border-zinc-800 shadow-lg hover:shadow-xl transition-shadow duration-300">
<div className="w-12 h-12 bg-emerald-500/10 rounded-xl flex items-center justify-center mb-4">
<svg xmlns="http://www.w3.org/2000/svg" className="w-6 h-6 text-emerald-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
</div>
<h3 className="text-xl font-bold text-zinc-900 dark:text-zinc-50 mb-3">Our Solution</h3>
<p className="text-zinc-600 dark:text-zinc-400 leading-relaxed">
SafeRoute analyzes multiple risk factors including weather, traffic, time of day, and historical data to provide intelligent route recommendations.
</p>
</div>
</motion.div>
{/* Key Features */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
className="mb-16"
>
<h2 className="text-2xl font-bold text-zinc-900 dark:text-zinc-50 mb-8 text-center">Key Features</h2>
<div className="grid md:grid-cols-3 gap-6">
{[
{
icon: (
<svg xmlns="http://www.w3.org/2000/svg" className="w-7 h-7" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7" />
</svg>
),
title: 'Route Risk Analysis',
desc: 'Dynamic color-coded routes showing real-time risk levels for each segment'
},
{
icon: (
<svg xmlns="http://www.w3.org/2000/svg" className="w-7 h-7" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z" />
</svg>
),
title: 'Weather Integration',
desc: 'Real-time weather data affecting route risk calculations automatically'
},
{
icon: (
<svg xmlns="http://www.w3.org/2000/svg" className="w-7 h-7" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
</svg>
),
title: 'AI Assistant',
desc: 'Intelligent chatbot providing contextual safety advice and recommendations'
},
{
icon: (
<svg xmlns="http://www.w3.org/2000/svg" className="w-7 h-7" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
),
title: 'Route Playback',
desc: 'Visualize your entire route with animated playback feature'
},
{
icon: (
<svg xmlns="http://www.w3.org/2000/svg" className="w-7 h-7" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
<path strokeLinecap="round" strokeLinejoin="round" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
),
title: 'Multiple Profiles',
desc: 'Save locations, view history, and customize preferences'
},
{
icon: (
<svg xmlns="http://www.w3.org/2000/svg" className="w-7 h-7" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
),
title: 'Safety Score',
desc: 'Overall route safety rating based on multiple risk factors'
}
].map((feature, idx) => (
<motion.div
key={idx}
whileHover={{ y: -5 }}
className="bg-white dark:bg-zinc-900 rounded-2xl p-6 border border-zinc-200 dark:border-zinc-800 shadow-lg hover:shadow-xl transition-all duration-300 group"
>
<div className="w-12 h-12 bg-emerald-500/10 rounded-xl flex items-center justify-center mb-4 text-emerald-500 group-hover:bg-emerald-500 group-hover:text-white transition-colors duration-300">
{feature.icon}
</div>
<h3 className="text-lg font-bold text-zinc-900 dark:text-zinc-50 mb-2">{feature.title}</h3>
<p className="text-sm text-zinc-600 dark:text-zinc-400 leading-relaxed">{feature.desc}</p>
</motion.div>
))}
</div>
</motion.div>
{/* How It Works */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.3 }}
className="mb-16"
>
<h2 className="text-2xl font-bold text-zinc-900 dark:text-zinc-50 mb-8 text-center">How It Works</h2>
<div className="grid md:grid-cols-4 gap-4">
{[
{ step: '01', title: 'Enter Route', desc: 'Search for your origin and destination' },
{ step: '02', title: 'Set Vehicle', desc: 'Choose driving or cycling mode' },
{ step: '03', title: 'Analyze Risk', desc: 'Our AI calculates safety scores' },
{ step: '04', title: 'Safe Travel', desc: 'Follow the safest route with live updates' }
].map((item, idx) => (
<motion.div
key={idx}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.5, delay: 0.4 + idx * 0.1 }}
className="relative"
>
<div className="bg-white dark:bg-zinc-900 rounded-2xl p-6 border border-zinc-200 dark:border-zinc-800 shadow-lg text-center">
<div className="w-10 h-10 bg-emerald-500 rounded-full flex items-center justify-center text-white font-bold text-sm mx-auto mb-4">
{item.step}
</div>
<h4 className="font-bold text-zinc-900 dark:text-zinc-50 mb-2">{item.title}</h4>
<p className="text-sm text-zinc-600 dark:text-zinc-400">{item.desc}</p>
</div>
{idx < 3 && (
<div className="hidden md:block absolute top-1/2 -right-2 transform -translate-y-1/2">
<svg className="w-4 h-4 text-emerald-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</div>
)}
</motion.div>
))}
</div>
</motion.div>
{/* Technology Stack */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.4 }}
className="mb-16"
>
<h2 className="text-2xl font-bold text-zinc-900 dark:text-zinc-50 mb-8 text-center">Built With</h2>
<div className="flex flex-wrap justify-center gap-4">
{[
{ name: 'React', icon: <svg xmlns="http://www.w3.org/2000/svg" className="w-4 h-4" viewBox="0 0 24 24" fill="currentColor"><path d="M12 10.11c1.03 0 1.87.84 1.87 1.89 0 1-.84 1.85-1.87 1.85S10.13 13 10.13 12c0-1.05.84-1.89 1.87-1.89M7.37 20c.63.38 2.01-.2 3.6-1.7-.52-.59-1.03-1.23-1.51-1.9a22.7 22.7 0 01-2.4-.36c-.51 2.14-.32 3.61.31 3.96m.71-5.74l-.29-.51c-.11.29-.22.58-.29.86.27.06.57.11.88.16-.1-.18-.19-.36-.3-.51m6.92-.76l.65-1.12a16.2 16.2 0 00-3.18-2.21l-.98 1.67a14.2 14.2 0 013.51 1.66M12 5.73c.91.1 1.92.17 3.02.2l.44-1.82c-1.05-.05-2.08-.15-3.05-.29l-.41 1.91m5.79 2.56l1.12.73c.34.22.71.44 1.1.66-.11-.34-.23-.68-.37-1.02l-1.85-.37m-11.62-.37l-1.85.37c-.14.34-.26.68-.37 1.02.39-.22.76-.44 1.1-.66l1.12-.73M9.93 8.21c.94.13 1.97.23 3.07.29l.44-1.82a46.2 46.2 0 00-3.51-.2v1.73m8.97 3.77c.1.18.19.36.3.51.31-.05.61-.1.88-.16-.07-.28-.18-.57-.29-.86l-.89 1.5v-.99m0 5.74l.88.16c.11-.29.22-.58.29-.86l-.29.51c-.11.15-.2.33-.3.51.27-.05.57-.1.88-.16-.11-.34-.23-.68-.37-1.02l-1.09-.14v1m-4.78 1.78l-.71.36c.3.16.61.3.92.43.27.11.54.22.82.31-.1-.26-.18-.54-.26-.82l-.77-.28m7.73-4.3c-.14.34-.26.68-.37 1.02.39-.22.76-.44 1.1-.66l1.12-.73-.81-1.38-.98 1.67-.06.08M12 3.07v-.99c-1.16.02-2.3.1-3.43.24l.44 1.82c1.1-.07 2.18-.17 3.21-.29l-.22-1.78M7.64 6.31c.27.05.57.1.88.16.1-.18.19-.36.3-.51l-.89-1.5-.29.86v.99m.29 6.18l-.29-.51c-.11.29-.22.58-.29.86.27.06.57.11.88.16-.1-.18-.19-.36-.3-.51m6.92-.76l.65-1.12a16.2 16.2 0 00-3.18-2.21l-.98 1.67a14.2 14.2 0 013.51 1.66M12 5.73c.91.1 1.92.17 3.02.2l.44-1.82c-1.05-.05-2.08-.15-3.05-.29l-.41 1.91m5.79 2.56l1.12.73c.34.22.71.44 1.1.66-.11-.34-.23-.68-.37-1.02l-1.85-.37m-11.62-.37l-1.85.37c-.14.34-.26.68-.37 1.02.39-.22.76-.44 1.1-.66l1.12-.73M9.93 8.21c.94.13 1.97.23 3.07.29l.44-1.82a46.2 46.2 0 00-3.51-.2v1.73m8.97 3.77c.1.18.19.36.3.51.31-.05.61-.1.88-.16-.07-.28-.18-.57-.29-.86l-.89 1.5v-.99m0 5.74l.88.16c.11-.29.22-.58.29-.86l-.29.51c-.11.15-.2.33-.3.51.27-.05.57-.1.88-.16-.11-.34-.23-.68-.37-1.02l-1.09-.14v1m-4.78 1.78l-.71.36c.3.16.61.3.92.43.27.11.54.22.82.31-.1-.26-.18-.54-.26-.82l-.77-.28m7.73-4.3c-.14.34-.26.68-.37 1.02.39-.22.76-.44 1.1-.66l1.12-.73-.81-1.38-.98 1.67-.06.08M12 3.07v-.99c-1.16.02-2.3.1-3.43.24l.44 1.82c1.1-.07 2.18-.17 3.21-.29l-.22-1.78M7.64 6.31c.27.05.57.1.88.16.1-.18.19-.36.3-.51l-.89-1.5-.29.86v.99Z"/></svg> },
{ name: 'Leaflet', icon: <svg xmlns="http://www.w3.org/2000/svg" className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/></svg> },
{ name: 'OpenStreetMap', icon: <svg xmlns="http://www.w3.org/2000/svg" className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="12" cy="12" r="10"/><path d="M12 2a14.5 14.5 0 000 20 14.5 14.5 0 000-20"/><path d="M2 12h20"/></svg> },
{ name: 'Express', icon: <svg xmlns="http://www.w3.org/2000/svg" className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/></svg> },
{ name: 'Machine Learning', icon: <svg xmlns="http://www.w3.org/2000/svg" className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M12 2a2 2 0 012 2v1a2 2 0 01-2 2 2 2 0 01-2-2V4a2 2 0 012-2zm7 3a1 1 0 011 1v1a1 1 0 01-1 1 1 1 0 01-1-1V6a1 1 0 011-1zM5 6a1 1 0 011 1v1a1 1 0 01-1 1 1 1 0 01-1-1V7a1 1 0 011-1zM12 18a2 2 0 012 2v1a2 2 0 01-2 2 2 2 0 01-2-2v-1a2 2 0 012-2zm7 3a1 1 0 011 1v1a1 1 0 01-1 1 1 1 0 01-1-1v-1a1 1 0 011-1zM5 19a1 1 0 011 1v1a1 1 0 01-1 1 1 1 0 01-1-1v-1a1 1 0 011-1z"/><path d="M7 12h10M7 8h10M7 16h10"/></svg> },
{ name: 'Generative AI', icon: <svg xmlns="http://www.w3.org/2000/svg" className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg> },
{ name: 'Data Analytics', icon: <svg xmlns="http://www.w3.org/2000/svg" className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M18 20V10M12 20V4M6 20v-6"/></svg> },
{ name: 'Predictive Modeling', icon: <svg xmlns="http://www.w3.org/2000/svg" className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M3 3v18h18"/><path d="M18 17l-5-5-4 4-3-3"/></svg> },
{ name: 'Real-Time Data', icon: <svg xmlns="http://www.w3.org/2000/svg" className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="12" cy="12" r="2"/><path d="M16.24 7.76a6 6 0 010 8.49m-8.48-.01a6 6 0 010-8.49m11.31-2.82a10 10 0 010 14.14m-14.14 0a10 10 0 010-14.14"/></svg> }
].map((tech) => (
<span key={tech.name} className="px-5 py-2.5 bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-full text-sm font-medium text-zinc-700 dark:text-zinc-300 shadow-sm hover:border-emerald-400 dark:hover:border-emerald-600 hover:text-emerald-600 dark:hover:text-emerald-400 transition-colors inline-flex items-center gap-2">
{tech.icon}
{tech.name}
</span>
))}
</div>
</motion.div>
{/* Closing */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.5 }}
className="text-center"
>
<div className="inline-flex items-center gap-2 px-6 py-3 bg-emerald-500/10 rounded-full mb-4">
<svg xmlns="http://www.w3.org/2000/svg" className="w-5 h-5 text-emerald-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
<span className="text-emerald-600 dark:text-emerald-400 font-medium">Made-with-Ayush</span>
</div>
<p className="text-zinc-600 dark:text-zinc-400 max-w-lg mx-auto">
SafeRoute is designed to help you make informed decisions about your travel routes. Always drive safely and follow local traffic regulations.
</p>
</motion.div>
</div>
</div>
);
}
export default function App() {
const [routeData, setRouteData] = useState(null);
const [alternativeRoutes, setAlternativeRoutes] = useState<any[]>([]);
const [riskData, setRiskData] = useState(null);
const [previousRouteData, setPreviousRouteData] = useState(null);
const [previousRiskData, setPreviousRiskData] = useState(null);
const [incidentsData, setIncidentsData] = useState(null);
const [weather, setWeather] = useState(null);
const [startLoc, setStartLoc] = useState<[number, number] | null>(null);
const [endLoc, setEndLoc] = useState<[number, number] | null>(null);
const [startQuery, setStartQuery] = useState('');
const [endQuery, setEndQuery] = useState('');
const [showRoutePlayback, setShowRoutePlayback] = useState(false);
const [playbackSpeed, setPlaybackSpeed] = useState(1);
const [theme, setTheme] = useState<'dark' | 'light'>(() => {
try {
const saved = localStorage.getItem('theme');
return (saved === 'dark' || saved === 'light') ? saved : 'dark';
} catch {
return 'dark';
}
});
const [vehicleType, setVehicleType] = useState<'driving' | 'cycling'>(() => {
try {
const saved = localStorage.getItem('vehicleType');
return (saved === 'driving' || saved === 'cycling') ? saved : 'driving';
} catch {
return 'driving';
}
});
const [showIncidents, setShowIncidents] = useState(false);
const [activeLayers, setActiveLayers] = useState<string[]>([]);
const [mapStyleType, setMapStyleType] = useState(() => {
try {
return localStorage.getItem('mapStyleType') || 'default';
} catch {
return 'default';
}
});
const [showHeader, setShowHeader] = useState(true);
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
const [userProfile, setUserProfile] = useState<any>(() => {
try {
const saved = localStorage.getItem('userProfile');
return saved ? JSON.parse(saved) : null;
} catch {
return null;
}
});
const [showOnboarding, setShowOnboarding] = useState(() => {
try {
return !localStorage.getItem('onboardingComplete');
} catch {
return true;
}
});
const [showTripHistory, setShowTripHistory] = useState(false);
const [incidentReportLoc, setIncidentReportLoc] = useState<[number, number] | null>(null);
const [voiceNavEnabled, setVoiceNavEnabled] = useState(false);
const [highContrast, setHighContrast] = useState(() => {
try {
return localStorage.getItem('highContrast') === 'true';
} catch {
return false;
}
});
const handleLogin = (profile?: { name: string; email: string; photo?: string }) => {
if (profile) {
const user = {
name: profile.name,
email: profile.email,
photo: profile.photo || `https://api.dicebear.com/7.x/avataaars/svg?seed=${encodeURIComponent(profile.name)}`
};
setUserProfile(user);
localStorage.setItem('userProfile', JSON.stringify(user));
} else {
const mockUser = {
name: 'Guest User',
email: 'guest@example.com',
photo: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Felix'
};
setUserProfile(mockUser);
localStorage.setItem('userProfile', JSON.stringify(mockUser));
}
};
const handleLogout = () => {
setUserProfile(null);
localStorage.removeItem('userProfile');
};
const handleUpdateProfile = (name: string, email: string, photo: string) => {
const user = {
name,
email,
photo
};
setUserProfile(user);
localStorage.setItem('userProfile', JSON.stringify(user));
};
useEffect(() => {
localStorage.setItem('theme', theme);
if (theme === 'dark') {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
}, [theme]);
useEffect(() => {
localStorage.setItem('vehicleType', vehicleType);
}, [vehicleType]);
useEffect(() => {
localStorage.setItem('mapStyleType', mapStyleType);
}, [mapStyleType]);
useEffect(() => {
localStorage.setItem('highContrast', String(highContrast));
if (highContrast) {
document.documentElement.classList.add('high-contrast');
} else {
document.documentElement.classList.remove('high-contrast');
}
}, [highContrast]);
const handleOnboardingComplete = () => {
setShowOnboarding(false);
localStorage.setItem('onboardingComplete', 'true');
requestNotificationPermission();
};
useKeyboardShortcuts([
{ key: 'h', ctrl: true, handler: () => setHighContrast(prev => !prev), description: 'Toggle high contrast' },
{ key: 'o', ctrl: true, handler: () => setShowOnboarding(true), description: 'Show onboarding' },
{ key: 't', ctrl: true, handler: () => setShowTripHistory(true), description: 'Trip history' },
]);
return (
<Router>
<div className="relative w-full h-screen overflow-hidden bg-zinc-50 dark:bg-zinc-950 text-zinc-900 dark:text-zinc-50 flex transition-colors duration-300">
<ThinSidebar
userProfile={userProfile}
onLogin={handleLogin}
onLogout={handleLogout}
isSidebarOpen={isSidebarOpen}
setIsSidebarOpen={setIsSidebarOpen}
onSelectLocation={(loc, name) => {
setEndLoc(loc);
setEndQuery(name);
}}
currentEndLoc={endLoc}
currentEndQuery={endQuery}
/>
<div className="flex-1 relative flex flex-col h-full">
<Routes>
<Route path="/" element={
<MapTab
isSidebarOpen={isSidebarOpen}
routeData={routeData} setRouteData={setRouteData}
alternativeRoutes={alternativeRoutes} setAlternativeRoutes={setAlternativeRoutes}
riskData={riskData} setRiskData={setRiskData}
previousRouteData={previousRouteData} setPreviousRouteData={setPreviousRouteData}
previousRiskData={previousRiskData} setPreviousRiskData={setPreviousRiskData}
incidentsData={incidentsData} setIncidentsData={setIncidentsData}
weather={weather} setWeather={setWeather}
startLoc={startLoc} setStartLoc={setStartLoc}
endLoc={endLoc} setEndLoc={setEndLoc}
startQuery={startQuery} setStartQuery={setStartQuery}
endQuery={endQuery} setEndQuery={setEndQuery}
theme={theme} setTheme={setTheme}
vehicleType={vehicleType} setVehicleType={setVehicleType}
showIncidents={showIncidents} setShowIncidents={setShowIncidents}
showRoutePlayback={showRoutePlayback} setShowRoutePlayback={setShowRoutePlayback}
playbackSpeed={playbackSpeed} setPlaybackSpeed={setPlaybackSpeed}
mapStyleType={mapStyleType} setMapStyleType={setMapStyleType}
activeLayers={activeLayers} setActiveLayers={setActiveLayers}
waypoints={[]}
voiceNavEnabled={voiceNavEnabled}
onOpenIncidentReport={(loc: [number, number]) => setIncidentReportLoc(loc)}
onOpenTripHistory={() => setShowTripHistory(true)}
onToggleVoiceNav={(enabled: boolean) => setVoiceNavEnabled(enabled)}
/>
} />
<Route path="/settings" element={
<SettingsTab
theme={theme} setTheme={setTheme}
vehicleType={vehicleType} setVehicleType={setVehicleType}
mapStyleType={mapStyleType} setMapStyleType={setMapStyleType}
userProfile={userProfile} onLogout={handleLogout} onUpdateProfile={handleUpdateProfile}
highContrast={highContrast} setHighContrast={setHighContrast}
/>
} />
<Route path="/about" element={<AboutTab />} />
</Routes>
</div>
{showOnboarding && <OnboardingModal onComplete={handleOnboardingComplete} />}
{showTripHistory && <TripHistory onClose={() => setShowTripHistory(false)} />}
{incidentReportLoc && (
<IncidentReportModal
location={incidentReportLoc}
onClose={() => setIncidentReportLoc(null)}
onSubmit={(report) => {
console.log('Incident reported:', report);
setIncidentReportLoc(null);
}}
/>
)}
</div>
</Router>
);
}