284 lines
12 KiB
TypeScript
284 lines
12 KiB
TypeScript
import React, { useState, useEffect, useRef } from 'react';
|
|
import { useSearchParams, Link } from 'react-router-dom';
|
|
import { getRooms } from '../../services/api/roomService';
|
|
import type { Room } from '../../services/api/roomService';
|
|
import RoomFilter from '../../components/rooms/RoomFilter';
|
|
import RoomCard from '../../components/rooms/RoomCard';
|
|
import RoomCardSkeleton from '../../components/rooms/RoomCardSkeleton';
|
|
import Pagination from '../../components/rooms/Pagination';
|
|
import { ArrowLeft, Hotel, Filter, ChevronDown, ChevronUp } from 'lucide-react';
|
|
|
|
const RoomListPage: React.FC = () => {
|
|
const [searchParams] = useSearchParams();
|
|
const [rooms, setRooms] = useState<Room[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [isFilterOpen, setIsFilterOpen] = useState(false);
|
|
const filterRef = useRef<HTMLDivElement>(null);
|
|
const [pagination, setPagination] = useState({
|
|
total: 0,
|
|
page: 1,
|
|
limit: 10,
|
|
totalPages: 1,
|
|
});
|
|
|
|
// Scroll to filter when opened on mobile
|
|
useEffect(() => {
|
|
if (isFilterOpen && filterRef.current && window.innerWidth < 1280) {
|
|
setTimeout(() => {
|
|
filterRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
}, 100);
|
|
}
|
|
}, [isFilterOpen]);
|
|
|
|
// Fetch rooms based on URL params
|
|
useEffect(() => {
|
|
const fetchRooms = async () => {
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const params = {
|
|
type: searchParams.get('type') || undefined,
|
|
minPrice: searchParams.get('minPrice')
|
|
? Number(searchParams.get('minPrice'))
|
|
: undefined,
|
|
maxPrice: searchParams.get('maxPrice')
|
|
? Number(searchParams.get('maxPrice'))
|
|
: undefined,
|
|
capacity: searchParams.get('capacity')
|
|
? Number(searchParams.get('capacity'))
|
|
: undefined,
|
|
page: searchParams.get('page')
|
|
? Number(searchParams.get('page'))
|
|
: 1,
|
|
limit: 12,
|
|
};
|
|
|
|
const response = await getRooms(params);
|
|
|
|
if (response.status === 'success' && response.data) {
|
|
setRooms(response.data.rooms || []);
|
|
if (response.data.pagination) {
|
|
setPagination(response.data.pagination);
|
|
}
|
|
} else {
|
|
throw new Error('Failed to fetch rooms');
|
|
}
|
|
} catch (err) {
|
|
console.error('Error fetching rooms:', err);
|
|
setError('Unable to load room list. Please try again.');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
fetchRooms();
|
|
}, [searchParams]);
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-full" style={{ width: '100vw', position: 'relative', left: '50%', right: '50%', marginLeft: '-50vw', marginRight: '-50vw', marginTop: '-1.5rem', marginBottom: '-1.5rem' }}>
|
|
{/* Full-width hero section */}
|
|
<div className="w-full bg-gradient-to-br from-[#1a1a1a] via-[#0f0f0f] to-[#1a1a1a] border-b border-[#d4af37]/10 pt-6 sm:pt-7 md:pt-8">
|
|
<div className="w-full max-w-[1920px] mx-auto px-3 sm:px-4 md:px-6 lg:px-8 xl:px-12 2xl:px-16 3xl:px-20 py-6 sm:py-7 md:py-8 lg:py-10">
|
|
{/* Back Button */}
|
|
<Link
|
|
to="/"
|
|
className="inline-flex items-center gap-2
|
|
bg-gradient-to-r from-[#d4af37] to-[#c9a227]
|
|
text-[#0f0f0f] hover:from-[#f5d76e] hover:to-[#d4af37]
|
|
active:scale-95
|
|
mb-4 sm:mb-5 md:mb-6 transition-all duration-300
|
|
group font-medium tracking-wide text-sm
|
|
px-4 py-2 rounded-sm
|
|
shadow-lg shadow-[#d4af37]/30 hover:shadow-xl hover:shadow-[#d4af37]/40
|
|
touch-manipulation"
|
|
>
|
|
<ArrowLeft className="w-4 h-4 sm:w-4 sm:h-4 group-hover:-translate-x-1 transition-transform" />
|
|
<span>Back to home</span>
|
|
</Link>
|
|
|
|
{/* Page Header */}
|
|
<div className="text-center max-w-3xl mx-auto px-2">
|
|
<div className="inline-flex items-center justify-center gap-2 mb-3 sm:mb-4">
|
|
<div className="p-2 sm:p-2.5 bg-[#d4af37]/10 rounded-lg border border-[#d4af37]/30 backdrop-blur-sm">
|
|
<Hotel className="w-5 h-5 sm:w-5 sm:h-5 text-[#d4af37]" />
|
|
</div>
|
|
</div>
|
|
<h1 className="text-2xl xs:text-3xl sm:text-4xl md:text-5xl font-serif font-semibold
|
|
text-white mb-2 sm:mb-3 tracking-tight leading-tight px-2
|
|
bg-gradient-to-r from-white via-[#d4af37] to-white
|
|
bg-clip-text text-transparent"
|
|
>
|
|
Our Rooms & Suites
|
|
</h1>
|
|
<p className="text-gray-400 font-light tracking-wide text-xs sm:text-sm md:text-base max-w-xl mx-auto px-2 sm:px-4 leading-relaxed">
|
|
Discover our collection of luxurious accommodations,
|
|
each designed to provide an exceptional stay
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Full-width content area */}
|
|
<div className="w-full py-4 sm:py-5 md:py-6 lg:py-8">
|
|
<div className="w-full max-w-[1920px] mx-auto px-3 sm:px-4 md:px-6 lg:px-8 xl:px-12 2xl:px-16 3xl:px-20">
|
|
<div className="grid grid-cols-1 xl:grid-cols-12 gap-3 sm:gap-4 md:gap-5 lg:gap-6 xl:gap-7">
|
|
{/* Mobile Filter Toggle Button */}
|
|
<div className="xl:hidden order-1 mb-4">
|
|
<button
|
|
onClick={() => setIsFilterOpen(!isFilterOpen)}
|
|
className="w-full bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a]
|
|
border border-[#d4af37]/30 rounded-xl p-4
|
|
backdrop-blur-xl shadow-lg shadow-[#d4af37]/10
|
|
flex items-center justify-between gap-3
|
|
hover:border-[#d4af37]/50 hover:shadow-xl hover:shadow-[#d4af37]/20
|
|
transition-all duration-300 touch-manipulation"
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
<div className="p-2 bg-[#d4af37]/10 rounded-lg border border-[#d4af37]/30">
|
|
<Filter className="w-5 h-5 text-[#d4af37]" />
|
|
</div>
|
|
<span className="text-white font-medium tracking-wide text-base">
|
|
Filters
|
|
</span>
|
|
</div>
|
|
{isFilterOpen ? (
|
|
<ChevronUp className="w-5 h-5 text-[#d4af37]" />
|
|
) : (
|
|
<ChevronDown className="w-5 h-5 text-[#d4af37]" />
|
|
)}
|
|
</button>
|
|
</div>
|
|
|
|
{/* Filter Sidebar - Collapsible on mobile, sidebar on desktop */}
|
|
<aside
|
|
ref={filterRef}
|
|
className={`xl:col-span-3 order-2 xl:order-1 mb-4 sm:mb-5 md:mb-6 xl:mb-0 transition-all duration-300 ${
|
|
isFilterOpen ? 'block' : 'hidden xl:block'
|
|
}`}
|
|
>
|
|
<div className="xl:sticky xl:top-4 xl:max-h-[calc(100vh-2rem)] xl:overflow-y-auto">
|
|
<RoomFilter />
|
|
</div>
|
|
</aside>
|
|
|
|
{/* Main Content - Full width on mobile, 9 columns on desktop */}
|
|
<main className="xl:col-span-9 order-3 xl:order-2">
|
|
{loading && (
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-3
|
|
gap-3 sm:gap-4 md:gap-5 lg:gap-6"
|
|
>
|
|
{Array.from({ length: 6 }).map((_, index) => (
|
|
<RoomCardSkeleton key={index} />
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{error && !loading && (
|
|
<div className="bg-gradient-to-br from-red-900/20 to-red-800/10
|
|
border border-red-500/30 rounded-xl p-6 sm:p-8 md:p-12 text-center
|
|
backdrop-blur-xl shadow-2xl shadow-red-500/10"
|
|
>
|
|
<div className="inline-flex items-center justify-center w-14 h-14 sm:w-16 sm:h-16 md:w-20 md:h-20
|
|
bg-red-500/20 rounded-full mb-4 sm:mb-5 md:mb-6 border border-red-500/30"
|
|
>
|
|
<svg
|
|
className="w-7 h-7 sm:w-8 sm:h-8 md:w-10 md:h-10 text-red-400"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0
|
|
9 9 0 0118 0z"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
<p className="text-red-300 font-light text-sm sm:text-base md:text-lg mb-4 sm:mb-5 md:mb-6 tracking-wide px-2">{error}</p>
|
|
<button
|
|
onClick={() => window.location.reload()}
|
|
className="px-5 sm:px-6 md:px-8 py-2.5 sm:py-3 bg-gradient-to-r from-[#d4af37] to-[#c9a227]
|
|
text-[#0f0f0f] rounded-sm font-medium tracking-wide text-sm sm:text-base
|
|
hover:from-[#f5d76e] hover:to-[#d4af37] active:scale-95
|
|
transition-all duration-300 shadow-lg shadow-[#d4af37]/30
|
|
touch-manipulation min-h-[44px]"
|
|
>
|
|
Try Again
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{!loading && !error && rooms.length === 0 && (
|
|
<div className="bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a]
|
|
border border-[#d4af37]/20 rounded-xl p-8 sm:p-10 md:p-12 lg:p-16 text-center
|
|
backdrop-blur-xl shadow-2xl shadow-[#d4af37]/5"
|
|
>
|
|
<div className="inline-flex items-center justify-center w-16 h-16 sm:w-20 sm:h-20 md:w-24 md:h-24
|
|
bg-[#d4af37]/10 rounded-2xl mb-4 sm:mb-5 md:mb-6 lg:mb-8 border border-[#d4af37]/30"
|
|
>
|
|
<Hotel className="w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 text-[#d4af37]" />
|
|
</div>
|
|
<h3 className="text-lg sm:text-xl md:text-2xl font-serif font-semibold
|
|
text-white mb-3 sm:mb-4 tracking-wide px-2"
|
|
>
|
|
No matching rooms found
|
|
</h3>
|
|
<p className="text-gray-400 font-light tracking-wide mb-5 sm:mb-6 md:mb-8 text-sm sm:text-base md:text-lg px-2">
|
|
Please try adjusting the filters or search differently
|
|
</p>
|
|
<button
|
|
onClick={() => window.location.href = '/rooms'}
|
|
className="px-5 sm:px-6 md:px-8 py-2.5 sm:py-3 bg-gradient-to-r from-[#d4af37] to-[#c9a227]
|
|
text-[#0f0f0f] rounded-sm font-medium tracking-wide text-sm sm:text-base
|
|
hover:from-[#f5d76e] hover:to-[#d4af37] active:scale-95
|
|
transition-all duration-300 shadow-lg shadow-[#d4af37]/30
|
|
touch-manipulation min-h-[44px]"
|
|
>
|
|
Clear Filters
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{!loading && !error && rooms.length > 0 && (
|
|
<>
|
|
{/* Results Count */}
|
|
<div className="mb-3 sm:mb-4 md:mb-5 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-2 sm:gap-3">
|
|
<p className="text-gray-400 font-light tracking-wide text-xs sm:text-sm md:text-base">
|
|
Showing <span className="text-[#d4af37] font-medium">{rooms.length}</span> of{' '}
|
|
<span className="text-[#d4af37] font-medium">{pagination.total}</span> rooms
|
|
</p>
|
|
</div>
|
|
|
|
{/* Responsive grid: 1 col mobile, 2 cols tablet, 3 cols desktop */}
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-3
|
|
gap-3 sm:gap-4 md:gap-5 lg:gap-6 mb-4 sm:mb-5 md:mb-6"
|
|
>
|
|
{rooms.map((room) => (
|
|
<RoomCard key={room.id} room={room} />
|
|
))}
|
|
</div>
|
|
|
|
{pagination.totalPages > 1 && (
|
|
<div className="mt-4 sm:mt-5 md:mt-6 pt-3 sm:pt-4 border-t border-[#d4af37]/20">
|
|
<Pagination
|
|
currentPage={pagination.page}
|
|
totalPages={pagination.totalPages}
|
|
/>
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</main>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default RoomListPage;
|