Files
laca-website/frontend/src/app/page-hcmc.tsx
PhongPham 51f2505839 🚀 Complete Laca City Website with VPS Deployment
- 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
2025-08-12 07:06:15 +07:00

165 lines
6.6 KiB
TypeScript
Raw Blame History

'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 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>
);
}