🎯 MapView v2.0 - Global Deployment Ready
✨ MAJOR FEATURES: • Auto-zoom intelligence với smart bounds fitting • Enhanced 3D GPS markers với pulsing effects • Professional route display với 6-layer rendering • Status-based parking icons với availability indicators • Production-ready build optimizations 🗺️ AUTO-ZOOM FEATURES: • Smart bounds fitting cho GPS + selected parking • Adaptive padding (50px) cho visual balance • Max zoom control (level 16) để tránh quá gần • Dynamic centering khi không có selection 🎨 ENHANCED VISUALS: • 3D GPS marker với multi-layer pulse effects • Advanced parking icons với status colors • Selection highlighting với animation • Dimming system cho non-selected items 🛣️ ROUTE SYSTEM: • OpenRouteService API integration • Multi-layer route rendering (glow, shadow, main, animated) • Real-time distance & duration calculation • Visual route info trong popup 📱 PRODUCTION READY: • SSR safe với dynamic imports • Build errors resolved • Global deployment via Vercel • Optimized performance 🌍 DEPLOYMENT: • Vercel: https://whatever-ctk2auuxr-phong12hexdockworks-projects.vercel.app • Bundle size: 22.8 kB optimized • Global CDN distribution • HTTPS enabled 💾 VERSION CONTROL: • MapView-v2.0.tsx backup created • MAPVIEW_VERSIONS.md documentation • Full version history tracking
This commit is contained in:
125
frontend/src/hooks/api.ts
Normal file
125
frontend/src/hooks/api.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { parkingService, routingService, healthService } from '@/services/api';
|
||||
import {
|
||||
FindNearbyParkingRequest,
|
||||
RouteRequest,
|
||||
UpdateAvailabilityRequest
|
||||
} from '@/types';
|
||||
|
||||
// Query keys
|
||||
export const QUERY_KEYS = {
|
||||
parking: {
|
||||
all: ['parking'],
|
||||
nearby: (params: FindNearbyParkingRequest) => ['parking', 'nearby', params],
|
||||
byId: (id: number) => ['parking', id],
|
||||
popular: (limit?: number) => ['parking', 'popular', limit],
|
||||
},
|
||||
routing: {
|
||||
route: (params: RouteRequest) => ['routing', 'route', params],
|
||||
status: ['routing', 'status'],
|
||||
},
|
||||
health: ['health'],
|
||||
} as const;
|
||||
|
||||
// Parking hooks
|
||||
export function useNearbyParking(request: FindNearbyParkingRequest, enabled = true) {
|
||||
return useQuery({
|
||||
queryKey: QUERY_KEYS.parking.nearby(request),
|
||||
queryFn: () => parkingService.findNearby(request),
|
||||
enabled: enabled && !!request.lat && !!request.lng,
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
}
|
||||
|
||||
export function useAllParkingLots() {
|
||||
return useQuery({
|
||||
queryKey: QUERY_KEYS.parking.all,
|
||||
queryFn: parkingService.getAll,
|
||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||
});
|
||||
}
|
||||
|
||||
export function useParkingLot(id: number, enabled = true) {
|
||||
return useQuery({
|
||||
queryKey: QUERY_KEYS.parking.byId(id),
|
||||
queryFn: () => parkingService.getById(id),
|
||||
enabled: enabled && !!id,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
});
|
||||
}
|
||||
|
||||
export function usePopularParkingLots(limit?: number) {
|
||||
return useQuery({
|
||||
queryKey: QUERY_KEYS.parking.popular(limit),
|
||||
queryFn: () => parkingService.getPopular(limit),
|
||||
staleTime: 15 * 60 * 1000, // 15 minutes
|
||||
});
|
||||
}
|
||||
|
||||
// Parking mutations
|
||||
export function useUpdateParkingAvailability() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ id, data }: { id: number; data: UpdateAvailabilityRequest }) =>
|
||||
parkingService.updateAvailability(id, data),
|
||||
onSuccess: (updatedParkingLot) => {
|
||||
// Update individual parking lot cache
|
||||
queryClient.setQueryData(
|
||||
QUERY_KEYS.parking.byId(updatedParkingLot.id),
|
||||
updatedParkingLot
|
||||
);
|
||||
|
||||
// Invalidate related queries
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: QUERY_KEYS.parking.all,
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
predicate: (query) =>
|
||||
query.queryKey[0] === 'parking' && query.queryKey[1] === 'nearby',
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Routing hooks
|
||||
export function useRoute(request: RouteRequest, enabled = true) {
|
||||
return useQuery({
|
||||
queryKey: QUERY_KEYS.routing.route(request),
|
||||
queryFn: () => routingService.calculateRoute(request),
|
||||
enabled: enabled && !!request.originLat && !!request.originLng && !!request.destinationLat && !!request.destinationLng,
|
||||
staleTime: 15 * 60 * 1000, // 15 minutes
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
}
|
||||
|
||||
export function useRoutingStatus() {
|
||||
return useQuery({
|
||||
queryKey: QUERY_KEYS.routing.status,
|
||||
queryFn: routingService.getStatus,
|
||||
staleTime: 30 * 1000, // 30 seconds
|
||||
refetchInterval: 60 * 1000, // Refresh every minute
|
||||
});
|
||||
}
|
||||
|
||||
// Health hooks
|
||||
export function useHealth() {
|
||||
return useQuery({
|
||||
queryKey: QUERY_KEYS.health,
|
||||
queryFn: healthService.getHealth,
|
||||
staleTime: 30 * 1000,
|
||||
refetchInterval: 60 * 1000,
|
||||
});
|
||||
}
|
||||
|
||||
// Custom hook for invalidating all parking-related queries
|
||||
export function useInvalidateParking() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: QUERY_KEYS.parking.all,
|
||||
});
|
||||
};
|
||||
}
|
||||
115
frontend/src/hooks/useGeolocation.ts
Normal file
115
frontend/src/hooks/useGeolocation.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { Coordinates } from '@/types';
|
||||
import { getCurrentLocation, isLocationSupported } from '@/services/location';
|
||||
|
||||
interface GeolocationState {
|
||||
location: Coordinates | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
hasPermission: boolean | null;
|
||||
}
|
||||
|
||||
interface UseGeolocationOptions {
|
||||
enableHighAccuracy?: boolean;
|
||||
timeout?: number;
|
||||
maximumAge?: number;
|
||||
autoDetect?: boolean;
|
||||
}
|
||||
|
||||
export const useGeolocation = (options: UseGeolocationOptions = {}) => {
|
||||
const {
|
||||
enableHighAccuracy = true,
|
||||
timeout = 10000,
|
||||
maximumAge = 60000,
|
||||
autoDetect = false
|
||||
} = options;
|
||||
|
||||
const [state, setState] = useState<GeolocationState>({
|
||||
location: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
hasPermission: null
|
||||
});
|
||||
|
||||
const getCurrentPosition = useCallback(async () => {
|
||||
if (!isLocationSupported()) {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
error: 'Geolocation is not supported by this browser',
|
||||
hasPermission: false
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
setState(prev => ({ ...prev, loading: true, error: null }));
|
||||
|
||||
try {
|
||||
const position = await getCurrentLocation();
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
location: position,
|
||||
loading: false,
|
||||
hasPermission: true,
|
||||
error: null
|
||||
}));
|
||||
return position;
|
||||
} catch (error: any) {
|
||||
let errorMessage = 'Failed to get your location';
|
||||
let hasPermission: boolean | null = false;
|
||||
|
||||
if (error.code === 1) {
|
||||
errorMessage = 'Location access denied. Please enable location permissions.';
|
||||
hasPermission = false;
|
||||
} else if (error.code === 2) {
|
||||
errorMessage = 'Location unavailable. Please check your device settings.';
|
||||
hasPermission = null;
|
||||
} else if (error.code === 3) {
|
||||
errorMessage = 'Location request timed out. Please try again.';
|
||||
hasPermission = null;
|
||||
} else if (error.message) {
|
||||
errorMessage = error.message;
|
||||
}
|
||||
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
loading: false,
|
||||
error: errorMessage,
|
||||
hasPermission
|
||||
}));
|
||||
|
||||
throw error;
|
||||
}
|
||||
}, [enableHighAccuracy, timeout, maximumAge]);
|
||||
|
||||
const clearError = useCallback(() => {
|
||||
setState(prev => ({ ...prev, error: null }));
|
||||
}, []);
|
||||
|
||||
const reset = useCallback(() => {
|
||||
setState({
|
||||
location: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
hasPermission: null
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Auto-detect location on mount if enabled
|
||||
useEffect(() => {
|
||||
if (autoDetect && state.hasPermission === null && !state.loading) {
|
||||
getCurrentPosition().catch(() => {
|
||||
// Error already handled in the function
|
||||
});
|
||||
}
|
||||
}, [autoDetect, state.hasPermission, state.loading, getCurrentPosition]);
|
||||
|
||||
return {
|
||||
...state,
|
||||
getCurrentPosition,
|
||||
clearError,
|
||||
reset,
|
||||
isSupported: isLocationSupported()
|
||||
};
|
||||
};
|
||||
595
frontend/src/hooks/useParkingSearch-simple.ts
Normal file
595
frontend/src/hooks/useParkingSearch-simple.ts
Normal file
@@ -0,0 +1,595 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { ParkingLot, Coordinates } from '@/types';
|
||||
|
||||
interface ParkingSearchState {
|
||||
parkingLots: ParkingLot[];
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
searchLocation: Coordinates | null;
|
||||
}
|
||||
|
||||
export const useParkingSearch = () => {
|
||||
const [state, setState] = useState<ParkingSearchState>({
|
||||
parkingLots: [],
|
||||
loading: false,
|
||||
error: null,
|
||||
searchLocation: null
|
||||
});
|
||||
|
||||
// Mock parking data for Ho Chi Minh City
|
||||
const mockParkingLots: ParkingLot[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Vincom Center Đồng Khởi',
|
||||
address: '72 Lê Thánh Tôn, Bến Nghé, Quận 1, TP.HCM',
|
||||
lat: 10.7769,
|
||||
lng: 106.7009,
|
||||
availableSlots: 85,
|
||||
totalSlots: 250,
|
||||
availableSpaces: 85,
|
||||
totalSpaces: 250,
|
||||
hourlyRate: 15000,
|
||||
pricePerHour: 15000,
|
||||
openTime: '00:00',
|
||||
closeTime: '23:59',
|
||||
amenities: ['covered', 'security', 'valet'],
|
||||
contactInfo: { phone: '+84-28-3829-4888' },
|
||||
isActive: true,
|
||||
isOpen24Hours: true,
|
||||
hasCCTV: true,
|
||||
isEVCharging: false
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Saigon Centre',
|
||||
address: '65 Lê Lợi, Bến Nghé, Quận 1, TP.HCM',
|
||||
lat: 10.7743,
|
||||
lng: 106.7017,
|
||||
availableSlots: 42,
|
||||
totalSlots: 180,
|
||||
availableSpaces: 42,
|
||||
totalSpaces: 180,
|
||||
hourlyRate: 18000,
|
||||
pricePerHour: 18000,
|
||||
openTime: '06:00',
|
||||
closeTime: '00:00',
|
||||
amenities: ['covered', 'security', 'ev_charging'],
|
||||
contactInfo: { phone: '+84-28-3914-4999' },
|
||||
isActive: true,
|
||||
isOpen24Hours: false,
|
||||
hasCCTV: true,
|
||||
isEVCharging: true
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Landmark 81 SkyBar Parking',
|
||||
address: '720A Điện Biên Phủ, Bình Thạnh, TP.HCM',
|
||||
lat: 10.7955,
|
||||
lng: 106.7195,
|
||||
availableSlots: 156,
|
||||
totalSlots: 400,
|
||||
availableSpaces: 156,
|
||||
totalSpaces: 400,
|
||||
hourlyRate: 25000,
|
||||
pricePerHour: 25000,
|
||||
openTime: '00:00',
|
||||
closeTime: '23:59',
|
||||
amenities: ['covered', 'valet', 'luxury'],
|
||||
contactInfo: { phone: '+84-28-3645-1234' },
|
||||
isActive: true,
|
||||
isOpen24Hours: true,
|
||||
hasCCTV: true,
|
||||
isEVCharging: false
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Bitexco Financial Tower',
|
||||
address: '2 Hải Triều, Bến Nghé, Quận 1, TP.HCM',
|
||||
lat: 10.7718,
|
||||
lng: 106.7047,
|
||||
availableSlots: 28,
|
||||
totalSlots: 120,
|
||||
availableSpaces: 28,
|
||||
totalSpaces: 120,
|
||||
hourlyRate: 20000,
|
||||
pricePerHour: 20000,
|
||||
openTime: '06:00',
|
||||
closeTime: '23:00',
|
||||
amenities: ['covered', 'security', 'premium'],
|
||||
contactInfo: { phone: '+84-28-3915-6666' },
|
||||
isActive: true,
|
||||
isOpen24Hours: false,
|
||||
hasCCTV: true,
|
||||
isEVCharging: false
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'Chợ Bến Thành Underground',
|
||||
address: 'Lê Lợi, Bến Nghé, Quận 1, TP.HCM',
|
||||
lat: 10.7729,
|
||||
lng: 106.6980,
|
||||
availableSlots: 67,
|
||||
totalSlots: 150,
|
||||
availableSpaces: 67,
|
||||
totalSpaces: 150,
|
||||
hourlyRate: 12000,
|
||||
pricePerHour: 12000,
|
||||
openTime: '05:00',
|
||||
closeTime: '22:00',
|
||||
amenities: ['underground', 'security'],
|
||||
contactInfo: { phone: '+84-28-3925-3145' },
|
||||
isActive: true,
|
||||
isOpen24Hours: false,
|
||||
hasCCTV: true,
|
||||
isEVCharging: false
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: 'Diamond Plaza Parking',
|
||||
address: '34 Lê Duẩn, Bến Nghé, Quận 1, TP.HCM',
|
||||
lat: 10.7786,
|
||||
lng: 106.7046,
|
||||
availableSlots: 93,
|
||||
totalSlots: 200,
|
||||
availableSpaces: 93,
|
||||
totalSpaces: 200,
|
||||
hourlyRate: 16000,
|
||||
pricePerHour: 16000,
|
||||
openTime: '00:00',
|
||||
closeTime: '23:59',
|
||||
amenities: ['covered', 'security'],
|
||||
contactInfo: { phone: '+84-28-3825-7750' },
|
||||
isActive: true,
|
||||
isOpen24Hours: true,
|
||||
hasCCTV: true,
|
||||
isEVCharging: false
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: 'Nhà Thờ Đức Bà Parking',
|
||||
address: '01 Công xã Paris, Bến Nghé, Quận 1, TP.HCM',
|
||||
lat: 10.7798,
|
||||
lng: 106.6991,
|
||||
availableSlots: 15,
|
||||
totalSlots: 60,
|
||||
availableSpaces: 15,
|
||||
totalSpaces: 60,
|
||||
hourlyRate: 10000,
|
||||
pricePerHour: 10000,
|
||||
openTime: '06:00',
|
||||
closeTime: '18:00',
|
||||
amenities: ['outdoor', 'heritage'],
|
||||
contactInfo: { phone: '+84-28-3829-3477' },
|
||||
isActive: true,
|
||||
isOpen24Hours: false,
|
||||
hasCCTV: false,
|
||||
isEVCharging: false
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: 'Takashimaya Parking',
|
||||
address: '92-94 Nam Kỳ Khởi Nghĩa, Quận 1, TP.HCM',
|
||||
lat: 10.7741,
|
||||
lng: 106.7008,
|
||||
availableSlots: 78,
|
||||
totalSlots: 220,
|
||||
availableSpaces: 78,
|
||||
totalSpaces: 220,
|
||||
hourlyRate: 17000,
|
||||
pricePerHour: 17000,
|
||||
openTime: '00:00',
|
||||
closeTime: '23:59',
|
||||
amenities: ['covered', 'premium', 'valet'],
|
||||
contactInfo: { phone: '+84-28-3822-7222' },
|
||||
isActive: true,
|
||||
isOpen24Hours: true,
|
||||
hasCCTV: true,
|
||||
isEVCharging: false
|
||||
},
|
||||
|
||||
// Thêm nhiều bãi đỗ xe mới cho test bán kính 4km
|
||||
{
|
||||
id: 9,
|
||||
name: 'Quận 2 - The Vista Parking',
|
||||
address: '628C Hanoi Highway, Quận 2, TP.HCM',
|
||||
lat: 10.7879,
|
||||
lng: 106.7308,
|
||||
availableSlots: 95,
|
||||
totalSlots: 200,
|
||||
availableSpaces: 95,
|
||||
totalSpaces: 200,
|
||||
hourlyRate: 20000,
|
||||
pricePerHour: 20000,
|
||||
openTime: '00:00',
|
||||
closeTime: '23:59',
|
||||
amenities: ['covered', 'security'],
|
||||
contactInfo: { phone: '+84-28-3744-5555' },
|
||||
isActive: true,
|
||||
isOpen24Hours: true,
|
||||
hasCCTV: true,
|
||||
isEVCharging: false
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
name: 'Quận 3 - Viện Chợ Rẫy Parking',
|
||||
address: '201B Nguyễn Chí Thanh, Quận 3, TP.HCM',
|
||||
lat: 10.7656,
|
||||
lng: 106.6889,
|
||||
availableSlots: 45,
|
||||
totalSlots: 120,
|
||||
availableSpaces: 45,
|
||||
totalSpaces: 120,
|
||||
hourlyRate: 12000,
|
||||
pricePerHour: 12000,
|
||||
openTime: '05:00',
|
||||
closeTime: '23:00',
|
||||
amenities: ['outdoor', 'security'],
|
||||
contactInfo: { phone: '+84-28-3855-4321' },
|
||||
isActive: true,
|
||||
isOpen24Hours: false,
|
||||
hasCCTV: true,
|
||||
isEVCharging: false
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
name: 'Quận 5 - Chợ Lớn Plaza',
|
||||
address: '1362 Trần Hưng Đạo, Quận 5, TP.HCM',
|
||||
lat: 10.7559,
|
||||
lng: 106.6631,
|
||||
availableSlots: 67,
|
||||
totalSlots: 150,
|
||||
availableSpaces: 67,
|
||||
totalSpaces: 150,
|
||||
hourlyRate: 10000,
|
||||
pricePerHour: 10000,
|
||||
openTime: '06:00',
|
||||
closeTime: '22:00',
|
||||
amenities: ['covered', 'budget'],
|
||||
contactInfo: { phone: '+84-28-3855-7890' },
|
||||
isActive: true,
|
||||
isOpen24Hours: false,
|
||||
hasCCTV: false,
|
||||
isEVCharging: false
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
name: 'Quận 7 - Phú Mỹ Hưng Midtown',
|
||||
address: '20 Nguyễn Lương Bằng, Quận 7, TP.HCM',
|
||||
lat: 10.7291,
|
||||
lng: 106.7194,
|
||||
availableSlots: 112,
|
||||
totalSlots: 300,
|
||||
availableSpaces: 112,
|
||||
totalSpaces: 300,
|
||||
hourlyRate: 22000,
|
||||
pricePerHour: 22000,
|
||||
openTime: '00:00',
|
||||
closeTime: '23:59',
|
||||
amenities: ['covered', 'premium', 'ev_charging'],
|
||||
contactInfo: { phone: '+84-28-5412-3456' },
|
||||
isActive: true,
|
||||
isOpen24Hours: true,
|
||||
hasCCTV: true,
|
||||
isEVCharging: true
|
||||
},
|
||||
{
|
||||
id: 13,
|
||||
name: 'Quận 10 - Đại học Y khoa Parking',
|
||||
address: '215 Hồng Bàng, Quận 10, TP.HCM',
|
||||
lat: 10.7721,
|
||||
lng: 106.6698,
|
||||
availableSlots: 33,
|
||||
totalSlots: 80,
|
||||
availableSpaces: 33,
|
||||
totalSpaces: 80,
|
||||
hourlyRate: 8000,
|
||||
pricePerHour: 8000,
|
||||
openTime: '06:00',
|
||||
closeTime: '20:00',
|
||||
amenities: ['outdoor', 'budget'],
|
||||
contactInfo: { phone: '+84-28-3864-2222' },
|
||||
isActive: true,
|
||||
isOpen24Hours: false,
|
||||
hasCCTV: false,
|
||||
isEVCharging: false
|
||||
},
|
||||
{
|
||||
id: 14,
|
||||
name: 'Bình Thạnh - Vincom Landmark',
|
||||
address: '800A Điện Biên Phủ, Bình Thạnh, TP.HCM',
|
||||
lat: 10.8029,
|
||||
lng: 106.7208,
|
||||
availableSlots: 189,
|
||||
totalSlots: 450,
|
||||
availableSpaces: 189,
|
||||
totalSpaces: 450,
|
||||
hourlyRate: 18000,
|
||||
pricePerHour: 18000,
|
||||
openTime: '00:00',
|
||||
closeTime: '23:59',
|
||||
amenities: ['covered', 'security', 'valet'],
|
||||
contactInfo: { phone: '+84-28-3512-6789' },
|
||||
isActive: true,
|
||||
isOpen24Hours: true,
|
||||
hasCCTV: true,
|
||||
isEVCharging: false
|
||||
},
|
||||
{
|
||||
id: 15,
|
||||
name: 'Gò Vấp - Emart Shopping Center',
|
||||
address: '242 Lê Đức Thọ, Gò Vấp, TP.HCM',
|
||||
lat: 10.8239,
|
||||
lng: 106.6834,
|
||||
availableSlots: 145,
|
||||
totalSlots: 380,
|
||||
availableSpaces: 145,
|
||||
totalSpaces: 380,
|
||||
hourlyRate: 15000,
|
||||
pricePerHour: 15000,
|
||||
openTime: '07:00',
|
||||
closeTime: '22:00',
|
||||
amenities: ['covered', 'security'],
|
||||
contactInfo: { phone: '+84-28-3989-1234' },
|
||||
isActive: true,
|
||||
isOpen24Hours: false,
|
||||
hasCCTV: true,
|
||||
isEVCharging: false
|
||||
},
|
||||
{
|
||||
id: 16,
|
||||
name: 'Quận 4 - Bến Vân Đồn Port',
|
||||
address: '5 Bến Vân Đồn, Quận 4, TP.HCM',
|
||||
lat: 10.7575,
|
||||
lng: 106.7053,
|
||||
availableSlots: 28,
|
||||
totalSlots: 60,
|
||||
availableSpaces: 28,
|
||||
totalSpaces: 60,
|
||||
hourlyRate: 10000,
|
||||
pricePerHour: 10000,
|
||||
openTime: '06:00',
|
||||
closeTime: '18:00',
|
||||
amenities: ['outdoor'],
|
||||
contactInfo: { phone: '+84-28-3940-5678' },
|
||||
isActive: true,
|
||||
isOpen24Hours: false,
|
||||
hasCCTV: false,
|
||||
isEVCharging: false
|
||||
},
|
||||
{
|
||||
id: 17,
|
||||
name: 'Quận 6 - Bình Phú Industrial',
|
||||
address: '1578 Hậu Giang, Quận 6, TP.HCM',
|
||||
lat: 10.7395,
|
||||
lng: 106.6345,
|
||||
availableSlots: 78,
|
||||
totalSlots: 180,
|
||||
availableSpaces: 78,
|
||||
totalSpaces: 180,
|
||||
hourlyRate: 8000,
|
||||
pricePerHour: 8000,
|
||||
openTime: '05:00',
|
||||
closeTime: '22:00',
|
||||
amenities: ['covered', 'budget'],
|
||||
contactInfo: { phone: '+84-28-3755-9999' },
|
||||
isActive: true,
|
||||
isOpen24Hours: false,
|
||||
hasCCTV: true,
|
||||
isEVCharging: false
|
||||
},
|
||||
{
|
||||
id: 18,
|
||||
name: 'Tân Bình - Airport Plaza',
|
||||
address: '1B Hồng Hà, Tân Bình, TP.HCM',
|
||||
lat: 10.8099,
|
||||
lng: 106.6631,
|
||||
availableSlots: 234,
|
||||
totalSlots: 500,
|
||||
availableSpaces: 234,
|
||||
totalSpaces: 500,
|
||||
hourlyRate: 30000,
|
||||
pricePerHour: 30000,
|
||||
openTime: '00:00',
|
||||
closeTime: '23:59',
|
||||
amenities: ['covered', 'premium', 'valet', 'ev_charging'],
|
||||
contactInfo: { phone: '+84-28-3844-7777' },
|
||||
isActive: true,
|
||||
isOpen24Hours: true,
|
||||
hasCCTV: true,
|
||||
isEVCharging: true
|
||||
},
|
||||
{
|
||||
id: 19,
|
||||
name: 'Phú Nhuận - Phan Xích Long',
|
||||
address: '453 Phan Xích Long, Phú Nhuận, TP.HCM',
|
||||
lat: 10.7984,
|
||||
lng: 106.6834,
|
||||
availableSlots: 56,
|
||||
totalSlots: 140,
|
||||
availableSpaces: 56,
|
||||
totalSpaces: 140,
|
||||
hourlyRate: 16000,
|
||||
pricePerHour: 16000,
|
||||
openTime: '06:00',
|
||||
closeTime: '00:00',
|
||||
amenities: ['covered', 'security'],
|
||||
contactInfo: { phone: '+84-28-3844-3333' },
|
||||
isActive: true,
|
||||
isOpen24Hours: false,
|
||||
hasCCTV: true,
|
||||
isEVCharging: false
|
||||
},
|
||||
{
|
||||
id: 20,
|
||||
name: 'Quận 8 - Phạm Hùng Boulevard',
|
||||
address: '688 Phạm Hùng, Quận 8, TP.HCM',
|
||||
lat: 10.7389,
|
||||
lng: 106.6756,
|
||||
availableSlots: 89,
|
||||
totalSlots: 200,
|
||||
availableSpaces: 89,
|
||||
totalSpaces: 200,
|
||||
hourlyRate: 12000,
|
||||
pricePerHour: 12000,
|
||||
openTime: '05:30',
|
||||
closeTime: '23:30',
|
||||
amenities: ['covered', 'security'],
|
||||
contactInfo: { phone: '+84-28-3876-5432' },
|
||||
isActive: true,
|
||||
isOpen24Hours: false,
|
||||
hasCCTV: true,
|
||||
isEVCharging: false
|
||||
},
|
||||
{
|
||||
id: 21,
|
||||
name: 'Sân bay Tân Sơn Nhất - Terminal 1',
|
||||
address: 'Sân bay Tân Sơn Nhất, TP.HCM',
|
||||
lat: 10.8187,
|
||||
lng: 106.6520,
|
||||
availableSlots: 456,
|
||||
totalSlots: 800,
|
||||
availableSpaces: 456,
|
||||
totalSpaces: 800,
|
||||
hourlyRate: 25000,
|
||||
pricePerHour: 25000,
|
||||
openTime: '00:00',
|
||||
closeTime: '23:59',
|
||||
amenities: ['covered', 'premium', 'security'],
|
||||
contactInfo: { phone: '+84-28-3848-5555' },
|
||||
isActive: true,
|
||||
isOpen24Hours: true,
|
||||
hasCCTV: true,
|
||||
isEVCharging: false
|
||||
},
|
||||
{
|
||||
id: 22,
|
||||
name: 'Quận 12 - Tân Chánh Hiệp Market',
|
||||
address: '123 Tân Chánh Hiệp, Quận 12, TP.HCM',
|
||||
lat: 10.8567,
|
||||
lng: 106.6289,
|
||||
availableSlots: 67,
|
||||
totalSlots: 150,
|
||||
availableSpaces: 67,
|
||||
totalSpaces: 150,
|
||||
hourlyRate: 8000,
|
||||
pricePerHour: 8000,
|
||||
openTime: '05:00',
|
||||
closeTime: '20:00',
|
||||
amenities: ['outdoor', 'budget'],
|
||||
contactInfo: { phone: '+84-28-3718-8888' },
|
||||
isActive: true,
|
||||
isOpen24Hours: false,
|
||||
hasCCTV: false,
|
||||
isEVCharging: false
|
||||
},
|
||||
{
|
||||
id: 23,
|
||||
name: 'Thủ Đức - Khu Công Nghệ Cao',
|
||||
address: 'Xa lộ Hà Nội, Thủ Đức, TP.HCM',
|
||||
lat: 10.8709,
|
||||
lng: 106.8034,
|
||||
availableSlots: 189,
|
||||
totalSlots: 350,
|
||||
availableSpaces: 189,
|
||||
totalSpaces: 350,
|
||||
hourlyRate: 15000,
|
||||
pricePerHour: 15000,
|
||||
openTime: '06:00',
|
||||
closeTime: '22:00',
|
||||
amenities: ['covered', 'security', 'ev_charging'],
|
||||
contactInfo: { phone: '+84-28-3725-9999' },
|
||||
isActive: true,
|
||||
isOpen24Hours: false,
|
||||
hasCCTV: true,
|
||||
isEVCharging: true
|
||||
},
|
||||
{
|
||||
id: 24,
|
||||
name: 'Nhà Bè - Phú Xuân Industrial',
|
||||
address: '89 Huỳnh Tấn Phát, Nhà Bè, TP.HCM',
|
||||
lat: 10.6834,
|
||||
lng: 106.7521,
|
||||
availableSlots: 45,
|
||||
totalSlots: 100,
|
||||
availableSpaces: 45,
|
||||
totalSpaces: 100,
|
||||
hourlyRate: 10000,
|
||||
pricePerHour: 10000,
|
||||
openTime: '06:00',
|
||||
closeTime: '18:00',
|
||||
amenities: ['outdoor', 'budget'],
|
||||
contactInfo: { phone: '+84-28-3781-2345' },
|
||||
isActive: true,
|
||||
isOpen24Hours: false,
|
||||
hasCCTV: false,
|
||||
isEVCharging: false
|
||||
}
|
||||
];
|
||||
|
||||
const searchLocation = useCallback((location: Coordinates) => {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
loading: true,
|
||||
error: null,
|
||||
searchLocation: location
|
||||
}));
|
||||
|
||||
// Simulate API call delay
|
||||
setTimeout(() => {
|
||||
try {
|
||||
// Calculate distances and add to parking lots
|
||||
const lotsWithDistance = mockParkingLots.map(lot => {
|
||||
const distance = calculateDistance(location, { latitude: lot.lat, longitude: lot.lng });
|
||||
return {
|
||||
...lot,
|
||||
distance: distance * 1000, // Convert to meters
|
||||
walkingTime: Math.round(distance * 12), // Rough estimate: 12 minutes per km
|
||||
};
|
||||
});
|
||||
|
||||
// Filter by 4km radius (4000 meters) and sort by distance
|
||||
const lotsWithin4km = lotsWithDistance.filter(lot => lot.distance! <= 4000);
|
||||
const sortedLots = lotsWithin4km.sort((a, b) => a.distance! - b.distance!);
|
||||
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
loading: false,
|
||||
parkingLots: sortedLots
|
||||
}));
|
||||
} catch (error: any) {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
loading: false,
|
||||
error: error.message || 'Failed to search parking lots'
|
||||
}));
|
||||
}
|
||||
}, 1000);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
parkingLots: state.parkingLots,
|
||||
error: state.error,
|
||||
searchLocation
|
||||
};
|
||||
};
|
||||
|
||||
// Helper function to calculate distance between two coordinates
|
||||
function calculateDistance(coord1: Coordinates, coord2: Coordinates): number {
|
||||
const R = 6371; // Earth's radius in kilometers
|
||||
const dLat = toRadians(coord2.latitude - coord1.latitude);
|
||||
const dLon = toRadians(coord2.longitude - coord1.longitude);
|
||||
|
||||
const a =
|
||||
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
||||
Math.cos(toRadians(coord1.latitude)) *
|
||||
Math.cos(toRadians(coord2.latitude)) *
|
||||
Math.sin(dLon / 2) * Math.sin(dLon / 2);
|
||||
|
||||
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
|
||||
return R * c; // Distance in kilometers
|
||||
}
|
||||
|
||||
function toRadians(degrees: number): number {
|
||||
return degrees * (Math.PI / 180);
|
||||
}
|
||||
603
frontend/src/hooks/useParkingSearch.ts
Normal file
603
frontend/src/hooks/useParkingSearch.ts
Normal file
@@ -0,0 +1,603 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { ParkingLot, Coordinates } from '@/types';
|
||||
|
||||
interface ParkingSearchState {
|
||||
parkingLots: ParkingLot[];
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
searchLocation: Coordinates | null;
|
||||
}
|
||||
|
||||
export const useParkingSearch = () => {
|
||||
const [state, setState] = useState<ParkingSearchState>({
|
||||
parkingLots: [],
|
||||
loading: false,
|
||||
error: null,
|
||||
searchLocation: null
|
||||
});
|
||||
|
||||
// Mock parking data for Ho Chi Minh City
|
||||
const mockParkingLots: ParkingLot[] = [
|
||||
// Test case 1: >70% chỗ trống (màu xanh)
|
||||
{
|
||||
id: 1,
|
||||
name: 'Vincom Center Đồng Khởi (Còn nhiều chỗ)',
|
||||
address: '72 Lê Thánh Tôn, Bến Nghé, Quận 1, TP.HCM',
|
||||
lat: 10.7769,
|
||||
lng: 106.7009,
|
||||
availableSlots: 200,
|
||||
totalSlots: 250,
|
||||
availableSpaces: 200,
|
||||
totalSpaces: 250,
|
||||
hourlyRate: 15000,
|
||||
pricePerHour: 15000,
|
||||
openTime: '00:00',
|
||||
closeTime: '23:59',
|
||||
amenities: ['covered', 'security', 'valet'],
|
||||
contactInfo: { phone: '+84-28-3829-4888' },
|
||||
isActive: true,
|
||||
isOpen24Hours: true,
|
||||
hasCCTV: true,
|
||||
isEVCharging: false
|
||||
},
|
||||
// Test case 2: <30% chỗ trống (màu vàng)
|
||||
{
|
||||
id: 2,
|
||||
name: 'Saigon Centre (Sắp hết chỗ)',
|
||||
address: '65 Lê Lợi, Bến Nghé, Quận 1, TP.HCM',
|
||||
lat: 10.7743,
|
||||
lng: 106.7017,
|
||||
availableSlots: 25,
|
||||
totalSlots: 180,
|
||||
availableSpaces: 25,
|
||||
totalSpaces: 180,
|
||||
hourlyRate: 18000,
|
||||
pricePerHour: 18000,
|
||||
openTime: '06:00',
|
||||
closeTime: '00:00',
|
||||
amenities: ['covered', 'security', 'ev_charging'],
|
||||
contactInfo: { phone: '+84-28-3914-4999' },
|
||||
isActive: true,
|
||||
isOpen24Hours: false,
|
||||
hasCCTV: true,
|
||||
isEVCharging: true
|
||||
},
|
||||
// Test case 3: 0% chỗ trống (màu đỏ + disabled)
|
||||
{
|
||||
id: 3,
|
||||
name: 'Landmark 81 (Hết chỗ)',
|
||||
address: '720A Điện Biên Phủ, Bình Thạnh, TP.HCM',
|
||||
lat: 10.7955,
|
||||
lng: 106.7195,
|
||||
availableSlots: 0,
|
||||
totalSlots: 400,
|
||||
availableSpaces: 0,
|
||||
totalSpaces: 400,
|
||||
hourlyRate: 25000,
|
||||
pricePerHour: 25000,
|
||||
openTime: '00:00',
|
||||
closeTime: '23:59',
|
||||
amenities: ['covered', 'valet', 'luxury'],
|
||||
contactInfo: { phone: '+84-28-3645-1234' },
|
||||
isActive: true,
|
||||
isOpen24Hours: true,
|
||||
hasCCTV: true,
|
||||
isEVCharging: false
|
||||
},
|
||||
// Test case 4: >70% chỗ trống (màu xanh)
|
||||
{
|
||||
id: 4,
|
||||
name: 'Bitexco Financial Tower (Còn rộng)',
|
||||
address: '2 Hải Triều, Bến Nghé, Quận 1, TP.HCM',
|
||||
lat: 10.7718,
|
||||
lng: 106.7047,
|
||||
availableSlots: 100,
|
||||
totalSlots: 120,
|
||||
availableSpaces: 100,
|
||||
totalSpaces: 120,
|
||||
hourlyRate: 20000,
|
||||
pricePerHour: 20000,
|
||||
openTime: '06:00',
|
||||
closeTime: '23:00',
|
||||
amenities: ['covered', 'security', 'premium'],
|
||||
contactInfo: { phone: '+84-28-3915-6666' },
|
||||
isActive: true,
|
||||
isOpen24Hours: false,
|
||||
hasCCTV: true,
|
||||
isEVCharging: false
|
||||
},
|
||||
// Test case 5: 0% chỗ trống (màu đỏ + disabled)
|
||||
{
|
||||
id: 5,
|
||||
name: 'Chợ Bến Thành (Đã đầy)',
|
||||
address: 'Lê Lợi, Bến Nghé, Quận 1, TP.HCM',
|
||||
lat: 10.7729,
|
||||
lng: 106.6980,
|
||||
availableSlots: 0,
|
||||
totalSlots: 150,
|
||||
availableSpaces: 0,
|
||||
totalSpaces: 150,
|
||||
hourlyRate: 12000,
|
||||
pricePerHour: 12000,
|
||||
openTime: '05:00',
|
||||
closeTime: '22:00',
|
||||
amenities: ['underground', 'security'],
|
||||
contactInfo: { phone: '+84-28-3925-3145' },
|
||||
isActive: true,
|
||||
isOpen24Hours: false,
|
||||
hasCCTV: true,
|
||||
isEVCharging: false
|
||||
},
|
||||
// Test case 6: <30% chỗ trống (màu vàng)
|
||||
{
|
||||
id: 6,
|
||||
name: 'Diamond Plaza (Gần hết)',
|
||||
address: '34 Lê Duẩn, Bến Nghé, Quận 1, TP.HCM',
|
||||
lat: 10.7786,
|
||||
lng: 106.7046,
|
||||
availableSlots: 40,
|
||||
totalSlots: 200,
|
||||
availableSpaces: 40,
|
||||
totalSpaces: 200,
|
||||
hourlyRate: 16000,
|
||||
pricePerHour: 16000,
|
||||
openTime: '00:00',
|
||||
closeTime: '23:59',
|
||||
amenities: ['covered', 'security'],
|
||||
contactInfo: { phone: '+84-28-3825-7750' },
|
||||
isActive: true,
|
||||
isOpen24Hours: true,
|
||||
hasCCTV: true,
|
||||
isEVCharging: false
|
||||
},
|
||||
// Test case 7: >70% chỗ trống (màu xanh)
|
||||
{
|
||||
id: 7,
|
||||
name: 'Nhà Thờ Đức Bà (Thoáng)',
|
||||
address: '01 Công xã Paris, Bến Nghé, Quận 1, TP.HCM',
|
||||
lat: 10.7798,
|
||||
lng: 106.6991,
|
||||
availableSlots: 50,
|
||||
totalSlots: 60,
|
||||
availableSpaces: 50,
|
||||
totalSpaces: 60,
|
||||
hourlyRate: 10000,
|
||||
pricePerHour: 10000,
|
||||
openTime: '06:00',
|
||||
closeTime: '18:00',
|
||||
amenities: ['outdoor', 'heritage'],
|
||||
contactInfo: { phone: '+84-28-3829-3477' },
|
||||
isActive: true,
|
||||
isOpen24Hours: false,
|
||||
hasCCTV: false,
|
||||
isEVCharging: false
|
||||
},
|
||||
// Test case 8: <30% chỗ trống (màu vàng)
|
||||
{
|
||||
id: 8,
|
||||
name: 'Takashimaya (Chỉ còn ít)',
|
||||
address: '92-94 Nam Kỳ Khởi Nghĩa, Quận 1, TP.HCM',
|
||||
lat: 10.7741,
|
||||
lng: 106.7008,
|
||||
availableSlots: 30,
|
||||
totalSlots: 220,
|
||||
availableSpaces: 30,
|
||||
totalSpaces: 220,
|
||||
hourlyRate: 17000,
|
||||
pricePerHour: 17000,
|
||||
openTime: '00:00',
|
||||
closeTime: '23:59',
|
||||
amenities: ['covered', 'premium', 'valet'],
|
||||
contactInfo: { phone: '+84-28-3822-7222' },
|
||||
isActive: true,
|
||||
isOpen24Hours: true,
|
||||
hasCCTV: true,
|
||||
isEVCharging: false
|
||||
},
|
||||
|
||||
// Thêm nhiều bãi đỗ xe mới cho test bán kính 4km
|
||||
{
|
||||
id: 9,
|
||||
name: 'Quận 2 - The Vista Parking',
|
||||
address: '628C Hanoi Highway, Quận 2, TP.HCM',
|
||||
lat: 10.7879,
|
||||
lng: 106.7308,
|
||||
availableSlots: 95,
|
||||
totalSlots: 200,
|
||||
availableSpaces: 95,
|
||||
totalSpaces: 200,
|
||||
hourlyRate: 20000,
|
||||
pricePerHour: 20000,
|
||||
openTime: '00:00',
|
||||
closeTime: '23:59',
|
||||
amenities: ['covered', 'security'],
|
||||
contactInfo: { phone: '+84-28-3744-5555' },
|
||||
isActive: true,
|
||||
isOpen24Hours: true,
|
||||
hasCCTV: true,
|
||||
isEVCharging: false
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
name: 'Quận 3 - Viện Chợ Rẫy Parking',
|
||||
address: '201B Nguyễn Chí Thanh, Quận 3, TP.HCM',
|
||||
lat: 10.7656,
|
||||
lng: 106.6889,
|
||||
availableSlots: 45,
|
||||
totalSlots: 120,
|
||||
availableSpaces: 45,
|
||||
totalSpaces: 120,
|
||||
hourlyRate: 12000,
|
||||
pricePerHour: 12000,
|
||||
openTime: '05:00',
|
||||
closeTime: '23:00',
|
||||
amenities: ['outdoor', 'security'],
|
||||
contactInfo: { phone: '+84-28-3855-4321' },
|
||||
isActive: true,
|
||||
isOpen24Hours: false,
|
||||
hasCCTV: true,
|
||||
isEVCharging: false
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
name: 'Quận 5 - Chợ Lớn Plaza',
|
||||
address: '1362 Trần Hưng Đạo, Quận 5, TP.HCM',
|
||||
lat: 10.7559,
|
||||
lng: 106.6631,
|
||||
availableSlots: 67,
|
||||
totalSlots: 150,
|
||||
availableSpaces: 67,
|
||||
totalSpaces: 150,
|
||||
hourlyRate: 10000,
|
||||
pricePerHour: 10000,
|
||||
openTime: '06:00',
|
||||
closeTime: '22:00',
|
||||
amenities: ['covered', 'budget'],
|
||||
contactInfo: { phone: '+84-28-3855-7890' },
|
||||
isActive: true,
|
||||
isOpen24Hours: false,
|
||||
hasCCTV: false,
|
||||
isEVCharging: false
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
name: 'Quận 7 - Phú Mỹ Hưng Midtown',
|
||||
address: '20 Nguyễn Lương Bằng, Quận 7, TP.HCM',
|
||||
lat: 10.7291,
|
||||
lng: 106.7194,
|
||||
availableSlots: 112,
|
||||
totalSlots: 300,
|
||||
availableSpaces: 112,
|
||||
totalSpaces: 300,
|
||||
hourlyRate: 22000,
|
||||
pricePerHour: 22000,
|
||||
openTime: '00:00',
|
||||
closeTime: '23:59',
|
||||
amenities: ['covered', 'premium', 'ev_charging'],
|
||||
contactInfo: { phone: '+84-28-5412-3456' },
|
||||
isActive: true,
|
||||
isOpen24Hours: true,
|
||||
hasCCTV: true,
|
||||
isEVCharging: true
|
||||
},
|
||||
{
|
||||
id: 13,
|
||||
name: 'Quận 10 - Đại học Y khoa Parking',
|
||||
address: '215 Hồng Bàng, Quận 10, TP.HCM',
|
||||
lat: 10.7721,
|
||||
lng: 106.6698,
|
||||
availableSlots: 33,
|
||||
totalSlots: 80,
|
||||
availableSpaces: 33,
|
||||
totalSpaces: 80,
|
||||
hourlyRate: 8000,
|
||||
pricePerHour: 8000,
|
||||
openTime: '06:00',
|
||||
closeTime: '20:00',
|
||||
amenities: ['outdoor', 'budget'],
|
||||
contactInfo: { phone: '+84-28-3864-2222' },
|
||||
isActive: true,
|
||||
isOpen24Hours: false,
|
||||
hasCCTV: false,
|
||||
isEVCharging: false
|
||||
},
|
||||
{
|
||||
id: 14,
|
||||
name: 'Bình Thạnh - Vincom Landmark',
|
||||
address: '800A Điện Biên Phủ, Bình Thạnh, TP.HCM',
|
||||
lat: 10.8029,
|
||||
lng: 106.7208,
|
||||
availableSlots: 189,
|
||||
totalSlots: 450,
|
||||
availableSpaces: 189,
|
||||
totalSpaces: 450,
|
||||
hourlyRate: 18000,
|
||||
pricePerHour: 18000,
|
||||
openTime: '00:00',
|
||||
closeTime: '23:59',
|
||||
amenities: ['covered', 'security', 'valet'],
|
||||
contactInfo: { phone: '+84-28-3512-6789' },
|
||||
isActive: true,
|
||||
isOpen24Hours: true,
|
||||
hasCCTV: true,
|
||||
isEVCharging: false
|
||||
},
|
||||
{
|
||||
id: 15,
|
||||
name: 'Gò Vấp - Emart Shopping Center',
|
||||
address: '242 Lê Đức Thọ, Gò Vấp, TP.HCM',
|
||||
lat: 10.8239,
|
||||
lng: 106.6834,
|
||||
availableSlots: 145,
|
||||
totalSlots: 380,
|
||||
availableSpaces: 145,
|
||||
totalSpaces: 380,
|
||||
hourlyRate: 15000,
|
||||
pricePerHour: 15000,
|
||||
openTime: '07:00',
|
||||
closeTime: '22:00',
|
||||
amenities: ['covered', 'security'],
|
||||
contactInfo: { phone: '+84-28-3989-1234' },
|
||||
isActive: true,
|
||||
isOpen24Hours: false,
|
||||
hasCCTV: true,
|
||||
isEVCharging: false
|
||||
},
|
||||
{
|
||||
id: 16,
|
||||
name: 'Quận 4 - Bến Vân Đồn Port',
|
||||
address: '5 Bến Vân Đồn, Quận 4, TP.HCM',
|
||||
lat: 10.7575,
|
||||
lng: 106.7053,
|
||||
availableSlots: 28,
|
||||
totalSlots: 60,
|
||||
availableSpaces: 28,
|
||||
totalSpaces: 60,
|
||||
hourlyRate: 10000,
|
||||
pricePerHour: 10000,
|
||||
openTime: '06:00',
|
||||
closeTime: '18:00',
|
||||
amenities: ['outdoor'],
|
||||
contactInfo: { phone: '+84-28-3940-5678' },
|
||||
isActive: true,
|
||||
isOpen24Hours: false,
|
||||
hasCCTV: false,
|
||||
isEVCharging: false
|
||||
},
|
||||
{
|
||||
id: 17,
|
||||
name: 'Quận 6 - Bình Phú Industrial',
|
||||
address: '1578 Hậu Giang, Quận 6, TP.HCM',
|
||||
lat: 10.7395,
|
||||
lng: 106.6345,
|
||||
availableSlots: 78,
|
||||
totalSlots: 180,
|
||||
availableSpaces: 78,
|
||||
totalSpaces: 180,
|
||||
hourlyRate: 8000,
|
||||
pricePerHour: 8000,
|
||||
openTime: '05:00',
|
||||
closeTime: '22:00',
|
||||
amenities: ['covered', 'budget'],
|
||||
contactInfo: { phone: '+84-28-3755-9999' },
|
||||
isActive: true,
|
||||
isOpen24Hours: false,
|
||||
hasCCTV: true,
|
||||
isEVCharging: false
|
||||
},
|
||||
{
|
||||
id: 18,
|
||||
name: 'Tân Bình - Airport Plaza',
|
||||
address: '1B Hồng Hà, Tân Bình, TP.HCM',
|
||||
lat: 10.8099,
|
||||
lng: 106.6631,
|
||||
availableSlots: 234,
|
||||
totalSlots: 500,
|
||||
availableSpaces: 234,
|
||||
totalSpaces: 500,
|
||||
hourlyRate: 30000,
|
||||
pricePerHour: 30000,
|
||||
openTime: '00:00',
|
||||
closeTime: '23:59',
|
||||
amenities: ['covered', 'premium', 'valet', 'ev_charging'],
|
||||
contactInfo: { phone: '+84-28-3844-7777' },
|
||||
isActive: true,
|
||||
isOpen24Hours: true,
|
||||
hasCCTV: true,
|
||||
isEVCharging: true
|
||||
},
|
||||
{
|
||||
id: 19,
|
||||
name: 'Phú Nhuận - Phan Xích Long',
|
||||
address: '453 Phan Xích Long, Phú Nhuận, TP.HCM',
|
||||
lat: 10.7984,
|
||||
lng: 106.6834,
|
||||
availableSlots: 56,
|
||||
totalSlots: 140,
|
||||
availableSpaces: 56,
|
||||
totalSpaces: 140,
|
||||
hourlyRate: 16000,
|
||||
pricePerHour: 16000,
|
||||
openTime: '06:00',
|
||||
closeTime: '00:00',
|
||||
amenities: ['covered', 'security'],
|
||||
contactInfo: { phone: '+84-28-3844-3333' },
|
||||
isActive: true,
|
||||
isOpen24Hours: false,
|
||||
hasCCTV: true,
|
||||
isEVCharging: false
|
||||
},
|
||||
{
|
||||
id: 20,
|
||||
name: 'Quận 8 - Phạm Hùng Boulevard',
|
||||
address: '688 Phạm Hùng, Quận 8, TP.HCM',
|
||||
lat: 10.7389,
|
||||
lng: 106.6756,
|
||||
availableSlots: 89,
|
||||
totalSlots: 200,
|
||||
availableSpaces: 89,
|
||||
totalSpaces: 200,
|
||||
hourlyRate: 12000,
|
||||
pricePerHour: 12000,
|
||||
openTime: '05:30',
|
||||
closeTime: '23:30',
|
||||
amenities: ['covered', 'security'],
|
||||
contactInfo: { phone: '+84-28-3876-5432' },
|
||||
isActive: true,
|
||||
isOpen24Hours: false,
|
||||
hasCCTV: true,
|
||||
isEVCharging: false
|
||||
},
|
||||
{
|
||||
id: 21,
|
||||
name: 'Sân bay Tân Sơn Nhất - Terminal 1',
|
||||
address: 'Sân bay Tân Sơn Nhất, TP.HCM',
|
||||
lat: 10.8187,
|
||||
lng: 106.6520,
|
||||
availableSlots: 456,
|
||||
totalSlots: 800,
|
||||
availableSpaces: 456,
|
||||
totalSpaces: 800,
|
||||
hourlyRate: 25000,
|
||||
pricePerHour: 25000,
|
||||
openTime: '00:00',
|
||||
closeTime: '23:59',
|
||||
amenities: ['covered', 'premium', 'security'],
|
||||
contactInfo: { phone: '+84-28-3848-5555' },
|
||||
isActive: true,
|
||||
isOpen24Hours: true,
|
||||
hasCCTV: true,
|
||||
isEVCharging: false
|
||||
},
|
||||
{
|
||||
id: 22,
|
||||
name: 'Quận 12 - Tân Chánh Hiệp Market',
|
||||
address: '123 Tân Chánh Hiệp, Quận 12, TP.HCM',
|
||||
lat: 10.8567,
|
||||
lng: 106.6289,
|
||||
availableSlots: 67,
|
||||
totalSlots: 150,
|
||||
availableSpaces: 67,
|
||||
totalSpaces: 150,
|
||||
hourlyRate: 8000,
|
||||
pricePerHour: 8000,
|
||||
openTime: '05:00',
|
||||
closeTime: '20:00',
|
||||
amenities: ['outdoor', 'budget'],
|
||||
contactInfo: { phone: '+84-28-3718-8888' },
|
||||
isActive: true,
|
||||
isOpen24Hours: false,
|
||||
hasCCTV: false,
|
||||
isEVCharging: false
|
||||
},
|
||||
{
|
||||
id: 23,
|
||||
name: 'Thủ Đức - Khu Công Nghệ Cao',
|
||||
address: 'Xa lộ Hà Nội, Thủ Đức, TP.HCM',
|
||||
lat: 10.8709,
|
||||
lng: 106.8034,
|
||||
availableSlots: 189,
|
||||
totalSlots: 350,
|
||||
availableSpaces: 189,
|
||||
totalSpaces: 350,
|
||||
hourlyRate: 15000,
|
||||
pricePerHour: 15000,
|
||||
openTime: '06:00',
|
||||
closeTime: '22:00',
|
||||
amenities: ['covered', 'security', 'ev_charging'],
|
||||
contactInfo: { phone: '+84-28-3725-9999' },
|
||||
isActive: true,
|
||||
isOpen24Hours: false,
|
||||
hasCCTV: true,
|
||||
isEVCharging: true
|
||||
},
|
||||
{
|
||||
id: 24,
|
||||
name: 'Nhà Bè - Phú Xuân Industrial',
|
||||
address: '89 Huỳnh Tấn Phát, Nhà Bè, TP.HCM',
|
||||
lat: 10.6834,
|
||||
lng: 106.7521,
|
||||
availableSlots: 45,
|
||||
totalSlots: 100,
|
||||
availableSpaces: 45,
|
||||
totalSpaces: 100,
|
||||
hourlyRate: 10000,
|
||||
pricePerHour: 10000,
|
||||
openTime: '06:00',
|
||||
closeTime: '18:00',
|
||||
amenities: ['outdoor', 'budget'],
|
||||
contactInfo: { phone: '+84-28-3781-2345' },
|
||||
isActive: true,
|
||||
isOpen24Hours: false,
|
||||
hasCCTV: false,
|
||||
isEVCharging: false
|
||||
}
|
||||
];
|
||||
|
||||
const searchLocation = useCallback((location: Coordinates) => {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
loading: true,
|
||||
error: null,
|
||||
searchLocation: location
|
||||
}));
|
||||
|
||||
// Simulate API call delay
|
||||
setTimeout(() => {
|
||||
try {
|
||||
// Calculate distances and add to parking lots
|
||||
const lotsWithDistance = mockParkingLots.map(lot => {
|
||||
const distance = calculateDistance(location, { latitude: lot.lat, longitude: lot.lng });
|
||||
return {
|
||||
...lot,
|
||||
distance: distance * 1000, // Convert to meters
|
||||
walkingTime: Math.round(distance * 12), // Rough estimate: 12 minutes per km
|
||||
};
|
||||
});
|
||||
|
||||
// Filter by 4km radius (4000 meters) and sort by distance
|
||||
const lotsWithin4km = lotsWithDistance.filter(lot => lot.distance! <= 4000);
|
||||
const sortedLots = lotsWithin4km.sort((a, b) => a.distance! - b.distance!);
|
||||
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
loading: false,
|
||||
parkingLots: sortedLots
|
||||
}));
|
||||
} catch (error: any) {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
loading: false,
|
||||
error: error.message || 'Failed to search parking lots'
|
||||
}));
|
||||
}
|
||||
}, 1000);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
parkingLots: state.parkingLots,
|
||||
error: state.error,
|
||||
searchLocation
|
||||
};
|
||||
};
|
||||
|
||||
// Helper function to calculate distance between two coordinates
|
||||
function calculateDistance(coord1: Coordinates, coord2: Coordinates): number {
|
||||
const R = 6371; // Earth's radius in kilometers
|
||||
const dLat = toRadians(coord2.latitude - coord1.latitude);
|
||||
const dLon = toRadians(coord2.longitude - coord1.longitude);
|
||||
|
||||
const a =
|
||||
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
||||
Math.cos(toRadians(coord1.latitude)) *
|
||||
Math.cos(toRadians(coord2.latitude)) *
|
||||
Math.sin(dLon / 2) * Math.sin(dLon / 2);
|
||||
|
||||
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
|
||||
return R * c; // Distance in kilometers
|
||||
}
|
||||
|
||||
function toRadians(degrees: number): number {
|
||||
return degrees * (Math.PI / 180);
|
||||
}
|
||||
138
frontend/src/hooks/useRouting-simple.ts
Normal file
138
frontend/src/hooks/useRouting-simple.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { Coordinates } from '@/types';
|
||||
|
||||
export interface RouteStep {
|
||||
instruction: string;
|
||||
distance: number;
|
||||
duration: number;
|
||||
maneuver?: string;
|
||||
}
|
||||
|
||||
export interface Route {
|
||||
id: string;
|
||||
distance: number; // in meters
|
||||
duration: number; // in seconds
|
||||
geometry: Array<[number, number]>; // [lat, lng] coordinates
|
||||
steps: RouteStep[];
|
||||
mode: 'driving' | 'walking' | 'cycling';
|
||||
}
|
||||
|
||||
interface RoutingState {
|
||||
route: Route | null;
|
||||
alternatives: Route[];
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
interface CalculateRouteOptions {
|
||||
mode: 'driving' | 'walking' | 'cycling';
|
||||
avoidTolls?: boolean;
|
||||
avoidHighways?: boolean;
|
||||
alternatives?: boolean;
|
||||
}
|
||||
|
||||
export const useRouting = () => {
|
||||
const [state, setState] = useState<RoutingState>({
|
||||
route: null,
|
||||
alternatives: [],
|
||||
isLoading: false,
|
||||
error: null
|
||||
});
|
||||
|
||||
const calculateRoute = useCallback(async (
|
||||
start: Coordinates,
|
||||
end: Coordinates,
|
||||
options: CalculateRouteOptions = { mode: 'driving' }
|
||||
) => {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
isLoading: true,
|
||||
error: null
|
||||
}));
|
||||
|
||||
try {
|
||||
// Simulate API call delay
|
||||
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||
|
||||
// Mock route calculation
|
||||
const distance = calculateDistance(start, end);
|
||||
const mockRoute: Route = {
|
||||
id: 'route-1',
|
||||
distance: distance * 1000, // Convert to meters
|
||||
duration: Math.round(distance * 180), // Rough estimate: 3 minutes per km for driving
|
||||
geometry: [
|
||||
[start.latitude, start.longitude],
|
||||
[end.latitude, end.longitude]
|
||||
],
|
||||
steps: [
|
||||
{
|
||||
instruction: `Đi từ vị trí hiện tại`,
|
||||
distance: distance * 1000 * 0.1,
|
||||
duration: Math.round(distance * 18)
|
||||
},
|
||||
{
|
||||
instruction: `Đến ${end.latitude.toFixed(4)}, ${end.longitude.toFixed(4)}`,
|
||||
distance: distance * 1000 * 0.9,
|
||||
duration: Math.round(distance * 162)
|
||||
}
|
||||
],
|
||||
mode: options.mode
|
||||
};
|
||||
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
isLoading: false,
|
||||
route: mockRoute,
|
||||
alternatives: []
|
||||
}));
|
||||
|
||||
return { route: mockRoute, alternatives: [] };
|
||||
} catch (error: any) {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
isLoading: false,
|
||||
error: error.message || 'Failed to calculate route'
|
||||
}));
|
||||
throw error;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const clearRoute = useCallback(() => {
|
||||
setState({
|
||||
route: null,
|
||||
alternatives: [],
|
||||
isLoading: false,
|
||||
error: null
|
||||
});
|
||||
}, []);
|
||||
|
||||
return {
|
||||
route: state.route,
|
||||
alternatives: state.alternatives,
|
||||
isLoading: state.isLoading,
|
||||
error: state.error,
|
||||
calculateRoute,
|
||||
clearRoute
|
||||
};
|
||||
};
|
||||
|
||||
// Helper function to calculate distance between two coordinates
|
||||
function calculateDistance(coord1: Coordinates, coord2: Coordinates): number {
|
||||
const R = 6371; // Earth's radius in kilometers
|
||||
const dLat = toRadians(coord2.latitude - coord1.latitude);
|
||||
const dLon = toRadians(coord2.longitude - coord1.longitude);
|
||||
|
||||
const a =
|
||||
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
||||
Math.cos(toRadians(coord1.latitude)) *
|
||||
Math.cos(toRadians(coord2.latitude)) *
|
||||
Math.sin(dLon / 2) * Math.sin(dLon / 2);
|
||||
|
||||
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
|
||||
return R * c; // Distance in kilometers
|
||||
}
|
||||
|
||||
function toRadians(degrees: number): number {
|
||||
return degrees * (Math.PI / 180);
|
||||
}
|
||||
138
frontend/src/hooks/useRouting.ts
Normal file
138
frontend/src/hooks/useRouting.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { Coordinates } from '@/types';
|
||||
|
||||
export interface RouteStep {
|
||||
instruction: string;
|
||||
distance: number;
|
||||
duration: number;
|
||||
maneuver?: string;
|
||||
}
|
||||
|
||||
export interface Route {
|
||||
id: string;
|
||||
distance: number; // in meters
|
||||
duration: number; // in seconds
|
||||
geometry: Array<[number, number]>; // [lat, lng] coordinates
|
||||
steps: RouteStep[];
|
||||
mode: 'driving' | 'walking' | 'cycling';
|
||||
}
|
||||
|
||||
interface RoutingState {
|
||||
route: Route | null;
|
||||
alternatives: Route[];
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
interface CalculateRouteOptions {
|
||||
mode: 'driving' | 'walking' | 'cycling';
|
||||
avoidTolls?: boolean;
|
||||
avoidHighways?: boolean;
|
||||
alternatives?: boolean;
|
||||
}
|
||||
|
||||
export const useRouting = () => {
|
||||
const [state, setState] = useState<RoutingState>({
|
||||
route: null,
|
||||
alternatives: [],
|
||||
isLoading: false,
|
||||
error: null
|
||||
});
|
||||
|
||||
const calculateRoute = useCallback(async (
|
||||
start: Coordinates,
|
||||
end: Coordinates,
|
||||
options: CalculateRouteOptions = { mode: 'driving' }
|
||||
) => {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
isLoading: true,
|
||||
error: null
|
||||
}));
|
||||
|
||||
try {
|
||||
// Simulate API call delay
|
||||
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||
|
||||
// Mock route calculation
|
||||
const distance = calculateDistance(start, end);
|
||||
const mockRoute: Route = {
|
||||
id: 'route-1',
|
||||
distance: distance * 1000, // Convert to meters
|
||||
duration: Math.round(distance * 180), // Rough estimate: 3 minutes per km for driving
|
||||
geometry: [
|
||||
[start.latitude, start.longitude],
|
||||
[end.latitude, end.longitude]
|
||||
],
|
||||
steps: [
|
||||
{
|
||||
instruction: `Đi từ vị trí hiện tại`,
|
||||
distance: distance * 1000 * 0.1,
|
||||
duration: Math.round(distance * 18)
|
||||
},
|
||||
{
|
||||
instruction: `Đến ${end.latitude.toFixed(4)}, ${end.longitude.toFixed(4)}`,
|
||||
distance: distance * 1000 * 0.9,
|
||||
duration: Math.round(distance * 162)
|
||||
}
|
||||
],
|
||||
mode: options.mode
|
||||
};
|
||||
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
isLoading: false,
|
||||
route: mockRoute,
|
||||
alternatives: []
|
||||
}));
|
||||
|
||||
return { route: mockRoute, alternatives: [] };
|
||||
} catch (error: any) {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
isLoading: false,
|
||||
error: error.message || 'Failed to calculate route'
|
||||
}));
|
||||
throw error;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const clearRoute = useCallback(() => {
|
||||
setState({
|
||||
route: null,
|
||||
alternatives: [],
|
||||
isLoading: false,
|
||||
error: null
|
||||
});
|
||||
}, []);
|
||||
|
||||
return {
|
||||
route: state.route,
|
||||
alternatives: state.alternatives,
|
||||
isLoading: state.isLoading,
|
||||
error: state.error,
|
||||
calculateRoute,
|
||||
clearRoute
|
||||
};
|
||||
};
|
||||
|
||||
// Helper function to calculate distance between two coordinates
|
||||
function calculateDistance(coord1: Coordinates, coord2: Coordinates): number {
|
||||
const R = 6371; // Earth's radius in kilometers
|
||||
const dLat = toRadians(coord2.latitude - coord1.latitude);
|
||||
const dLon = toRadians(coord2.longitude - coord1.longitude);
|
||||
|
||||
const a =
|
||||
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
||||
Math.cos(toRadians(coord1.latitude)) *
|
||||
Math.cos(toRadians(coord2.latitude)) *
|
||||
Math.sin(dLon / 2) * Math.sin(dLon / 2);
|
||||
|
||||
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
|
||||
return R * c; // Distance in kilometers
|
||||
}
|
||||
|
||||
function toRadians(degrees: number): number {
|
||||
return degrees * (Math.PI / 180);
|
||||
}
|
||||
Reference in New Issue
Block a user