feat: Enhanced CSS animations, improved UI components, and project reorganization

- Enhanced globals.css with comprehensive animation system
- Added advanced map marker animations (GPS, parking)
- Improved button and filter animations with hover effects
- Added new UI components: BookingModal, ParkingDetails, WheelPicker
- Reorganized project structure with better documentation
- Added optimization scripts and improved development workflow
- Updated deployment guides and technical documentation
- Enhanced mobile responsiveness and accessibility support
This commit is contained in:
2025-08-03 07:00:22 +07:00
parent 4f8d8c40c2
commit bc87a88719
34 changed files with 5851 additions and 2353 deletions

View File

@@ -1,595 +0,0 @@
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);
}

View File

@@ -1,138 +0,0 @@
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);
}