Files
Hotel-Booking/client/src/pages/customer/RoomDetailPage.tsx
Iliyan Angelov 93d4c1df80 update
2025-11-16 15:12:43 +02:00

282 lines
9.1 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { useParams, useNavigate, Link } from 'react-router-dom';
import {
Users,
MapPin,
DollarSign,
ArrowLeft,
} from 'lucide-react';
import { getRoomById, type Room } from
'../../services/api/roomService';
import RoomGallery from '../../components/rooms/RoomGallery';
import RoomAmenities from '../../components/rooms/RoomAmenities';
import ReviewSection from '../../components/rooms/ReviewSection';
import RatingStars from '../../components/rooms/RatingStars';
const RoomDetailPage: React.FC = () => {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const [room, setRoom] = useState<Room | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (id) {
fetchRoomDetail(Number(id));
}
}, [id]);
const fetchRoomDetail = async (roomId: number) => {
try {
setLoading(true);
setError(null);
const response = await getRoomById(roomId);
// backend uses `status: 'success'` (not `success`), accept both
if ((response as any).success || (response as any).status === 'success') {
if (response.data && response.data.room) {
setRoom(response.data.room);
} else {
throw new Error('Failed to fetch room details');
}
} else {
throw new Error('Failed to fetch room details');
}
} catch (err: any) {
console.error('Error fetching room:', err);
const message =
err.response?.data?.message ||
'Unable to load room information';
setError(message);
} finally {
setLoading(false);
}
};
if (loading) {
return (
<div className="min-h-screen bg-gray-50">
<div className="max-w-7xl mx-auto px-4 py-8">
<div className="animate-pulse space-y-6">
<div className="h-96 bg-gray-300 rounded-lg" />
<div className="h-8 bg-gray-300 rounded w-1/3" />
<div className="h-4 bg-gray-300 rounded w-2/3" />
<div className="h-32 bg-gray-300 rounded" />
</div>
</div>
</div>
);
}
if (error || !room) {
return (
<div className="min-h-screen bg-gray-50">
<div className="max-w-7xl mx-auto px-4 py-8">
<div className="bg-red-50 border border-red-200
rounded-lg p-8 text-center"
>
<p className="text-red-800 font-medium mb-4">
{error || 'Room not found'}
</p>
<button
onClick={() => navigate('/rooms')}
className="inline-flex items-center gap-2 bg-indigo-600
text-white px-3 py-2 rounded-md hover:bg-indigo-700
disabled:bg-gray-400 mb-6 transition-colors"
>
Back to Room List
</button>
</div>
</div>
</div>
);
}
const roomType = room.room_type;
const formattedPrice = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(roomType?.base_price || 0);
return (
<div className="min-h-screen bg-gray-50">
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Back Button */}
<Link
to="/rooms"
className="inline-flex items-center gap-2 bg-indigo-600
text-white px-3 py-2 rounded-md hover:bg-indigo-700
disabled:bg-gray-400 mb-6 transition-colors"
>
<ArrowLeft className="w-5 h-5" />
<span>Back to room list</span>
</Link>
{/* Image Gallery */}
<div className="mb-8">
<RoomGallery
images={roomType?.images || []}
roomName={roomType?.name || 'Room'}
/>
</div>
{/* Room Information */}
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 mb-12">
{/* Main Info */}
<div className="lg:col-span-8 space-y-6">
{/* Title & Basic Info */}
<div>
<h1 className="text-4xl font-bold
text-gray-900 mb-4"
>
{roomType?.name}
</h1>
<div className="flex flex-wrap items-center
gap-6 text-gray-600 mb-4"
>
<div className="flex items-center gap-2">
<MapPin className="w-5 h-5" />
<span>
Room {room.room_number} - Floor {room.floor}
</span>
</div>
<div className="flex items-center gap-2">
<Users className="w-5 h-5" />
<span>
{roomType?.capacity || 0} guests
</span>
</div>
{room.average_rating != null && (
<div className="flex items-center gap-2">
<RatingStars
rating={Number(room.average_rating)}
size="sm"
showNumber
/>
<span className="text-sm text-gray-500">
({room.total_reviews || 0} đánh giá)
</span>
</div>
)}
</div>
{/* Status Badge */}
<div
className={`inline-block px-4 py-2
rounded-full text-sm font-semibold
${
room.status === 'available'
? 'bg-green-100 text-green-800'
: room.status === 'occupied'
? 'bg-red-100 text-red-800'
: 'bg-gray-100 text-gray-800'
}`}
>
{room.status === 'available'
? 'Available'
: room.status === 'occupied'
? 'Booked'
: 'Maintenance'}
</div>
</div>
{/* Description */}
{roomType?.description && (
<div>
<h2 className="text-2xl font-bold
text-gray-900 mb-4"
>
Room Description
</h2>
<p className="text-gray-700 leading-relaxed">
{roomType.description}
</p>
</div>
)}
{/* Amenities */}
<div>
<h2 className="text-2xl font-bold
text-gray-900 mb-4"
>
Amenities
</h2>
<RoomAmenities
amenities={
(room.amenities && room.amenities.length > 0)
? room.amenities
: (roomType?.amenities || [])
}
/>
</div>
</div>
{/* Booking Card */}
<aside className="lg:col-span-4">
<div className="bg-white rounded-xl shadow-md p-6 sticky top-6">
<div className="flex items-baseline gap-3 mb-4">
<DollarSign className="w-5 h-5 text-gray-600" />
<div>
<div className="text-3xl font-extrabold text-indigo-600">
{formattedPrice}
</div>
<div className="text-sm text-gray-500">/ night</div>
</div>
</div>
<div className="mt-4">
<Link
to={`/booking/${room.id}`}
className={`block w-full py-3 text-center font-semibold rounded-md transition-colors ${
room.status === 'available'
? 'bg-indigo-600 text-white hover:bg-indigo-700'
: 'bg-gray-200 text-gray-500 cursor-not-allowed'
}`}
onClick={(e) => {
if (room.status !== 'available') e.preventDefault();
}}
>
{room.status === 'available' ? 'Book Now' : 'Not Available'}
</Link>
</div>
{room.status === 'available' && (
<p className="text-sm text-gray-500 text-center mt-3">
No immediate charge pay at the hotel
</p>
)}
<hr className="my-4" />
<div className="text-sm text-gray-700 space-y-2">
<div className="flex items-center justify-between">
<span>Room Type</span>
<strong>{roomType?.name}</strong>
</div>
<div className="flex items-center justify-between">
<span>Guests</span>
<span>{roomType?.capacity} guests</span>
</div>
<div className="flex items-center justify-between">
<span>Rooms</span>
<span>1</span>
</div>
</div>
</div>
</aside>
</div>
{/* Reviews Section */}
<div className="mb-12">
<ReviewSection roomId={room.id} />
</div>
</div>
</div>
);
};
export default RoomDetailPage;