- Added complete Next.js frontend with responsive design - Added NestJS backend with PostgreSQL and Redis - Added comprehensive VPS deployment script (vps-deploy.sh) - Added deployment guide and documentation - Added all assets and static files - Configured SSL, Nginx, PM2, and monitoring - Ready for production deployment on any VPS
165 lines
6.6 KiB
TypeScript
165 lines
6.6 KiB
TypeScript
'use client';
|
||
|
||
import React, { useState, useEffect } from 'react';
|
||
import dynamic from 'next/dynamic';
|
||
import { Header } from '@/components/Header';
|
||
import { ParkingList } from '@/components/parking/ParkingList';
|
||
import { HCMCGPSSimulator } from '@/components/HCMCGPSSimulator';
|
||
// import { ErrorMessage } from '@/components/ui/ErrorMessage';
|
||
import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
|
||
import { useParkingSearch } from '@/hooks/useParkingSearch';
|
||
import { ParkingLot, UserLocation, TransportationMode } from '@/types';
|
||
import toast from 'react-hot-toast';
|
||
|
||
export default function ParkingFinderPage() {
|
||
// State management
|
||
const [selectedParkingLot, setSelectedParkingLot] = useState<ParkingLot | null>(null);
|
||
const [userLocation, setUserLocation] = useState<UserLocation | null>(null);
|
||
const [searchRadius, setSearchRadius] = useState(4000); // meters - bán kính 4km
|
||
const [sortType, setSortType] = useState<'availability' | 'price' | 'distance'>('availability');
|
||
|
||
// Fixed to car mode only
|
||
const transportationMode: TransportationMode = 'auto';
|
||
|
||
// Custom hooks
|
||
const {
|
||
parkingLots,
|
||
error: parkingError,
|
||
searchLocation
|
||
} = useParkingSearch();
|
||
|
||
// Handle GPS location change from simulator
|
||
const handleLocationChange = (location: UserLocation) => {
|
||
setUserLocation(location);
|
||
|
||
// Search for parking near the new location
|
||
if (location) {
|
||
searchLocation({ latitude: location.lat, longitude: location.lng });
|
||
toast.success('Đã cập nhật vị trí GPS và tìm kiếm bãi đỗ xe gần đó');
|
||
}
|
||
};
|
||
|
||
const handleRefresh = () => {
|
||
if (userLocation) {
|
||
searchLocation({ latitude: userLocation.lat, longitude: userLocation.lng });
|
||
toast.success('Đã làm mới danh sách bãi đỗ xe');
|
||
} else {
|
||
toast.error('Vui lòng chọn vị trí GPS trước');
|
||
}
|
||
};
|
||
|
||
const handleParkingLotSelect = (lot: ParkingLot) => {
|
||
// If the same parking lot is selected again, deselect it
|
||
if (selectedParkingLot && selectedParkingLot.id === lot.id) {
|
||
setSelectedParkingLot(null);
|
||
toast.success('Đã bỏ chọn bãi đỗ xe');
|
||
return;
|
||
}
|
||
|
||
setSelectedParkingLot(lot);
|
||
toast.success(`Đã chọn ${lot.name}`);
|
||
};
|
||
|
||
// Show error messages
|
||
useEffect(() => {
|
||
if (parkingError) {
|
||
toast.error(parkingError);
|
||
}
|
||
}, [parkingError]);
|
||
|
||
return (
|
||
<div className="min-h-screen bg-gray-50">
|
||
<Header
|
||
title="Smart Parking Finder - TP.HCM"
|
||
subtitle="Chỉ hỗ trợ ô tô"
|
||
/>
|
||
|
||
<main className="container mx-auto px-4 py-6">
|
||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6 h-full">
|
||
{/* Left Column - Map and Parking List */}
|
||
<div className="lg:col-span-3 space-y-6">
|
||
{/* Summary Section */}
|
||
<div className="bg-white rounded-xl shadow-lg overflow-hidden">
|
||
<div className="h-96 bg-gradient-to-br from-gray-50 to-blue-50 flex items-center justify-center">
|
||
<div className="text-center p-8">
|
||
<div className="w-24 h-24 bg-blue-100 rounded-full mx-auto mb-6 flex items-center justify-center">
|
||
<svg className="w-12 h-12 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} 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" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
|
||
</svg>
|
||
</div>
|
||
<h2 className="text-2xl font-bold text-gray-800 mb-3">Parking Finder - HCMC</h2>
|
||
<p className="text-gray-600 mb-4">Find and book parking spots in Ho Chi Minh City</p>
|
||
{parkingLots.length > 0 && (
|
||
<div className="text-sm text-gray-500">
|
||
Found {parkingLots.length} parking locations nearby
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Parking List Section */}
|
||
<div className="bg-white rounded-xl shadow-lg p-6">
|
||
<div className="flex items-center justify-between mb-4">
|
||
<div>
|
||
<h2 className="text-xl font-semibold text-gray-900">
|
||
Bãi đỗ xe trong bán kính 4km
|
||
</h2>
|
||
<p className="text-sm text-gray-600 mt-1">
|
||
<EFBFBD> Chỉ hiển thị bãi xe đang mở cửa và còn chỗ trống
|
||
</p>
|
||
</div>
|
||
<button
|
||
onClick={handleRefresh}
|
||
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors flex items-center gap-2"
|
||
>
|
||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||
</svg>
|
||
Làm mới
|
||
</button>
|
||
</div>
|
||
|
||
{!userLocation ? (
|
||
<div className="text-center py-8">
|
||
<p className="text-gray-600">Vui lòng chọn vị trí GPS để tìm bãi đỗ xe</p>
|
||
</div>
|
||
) : parkingLots.length === 0 ? (
|
||
<div className="text-center py-8">
|
||
<p className="text-gray-600">Không tìm thấy bãi đỗ xe nào gần vị trí này</p>
|
||
</div>
|
||
) : (
|
||
<ParkingList
|
||
parkingLots={parkingLots}
|
||
onSelect={handleParkingLotSelect}
|
||
selectedId={selectedParkingLot?.id}
|
||
userLocation={userLocation}
|
||
sortType={sortType}
|
||
/>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Right Column - GPS Simulator */}
|
||
<div className="lg:col-span-1">
|
||
<HCMCGPSSimulator
|
||
onLocationChange={handleLocationChange}
|
||
currentLocation={userLocation}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Show errors */}
|
||
{parkingError && (
|
||
<div className="fixed bottom-4 right-4 max-w-sm">
|
||
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
|
||
{parkingError}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</main>
|
||
</div>
|
||
);
|
||
}
|