411 lines
12 KiB
TypeScript
411 lines
12 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { Link } from 'react-router-dom';
|
|
import {
|
|
ArrowRight,
|
|
AlertCircle,
|
|
} from 'lucide-react';
|
|
import {
|
|
BannerCarousel,
|
|
BannerSkeleton,
|
|
RoomCard,
|
|
RoomCardSkeleton,
|
|
SearchRoomForm,
|
|
} from '../components/rooms';
|
|
import {
|
|
bannerService,
|
|
roomService
|
|
} from '../services/api';
|
|
import type { Banner } from '../services/api/bannerService';
|
|
import type { Room } from '../services/api/roomService';
|
|
|
|
const HomePage: React.FC = () => {
|
|
const [banners, setBanners] = useState<Banner[]>([]);
|
|
const [featuredRooms, setFeaturedRooms] = useState<Room[]>([]);
|
|
const [newestRooms, setNewestRooms] = useState<Room[]>([]);
|
|
const [isLoadingBanners, setIsLoadingBanners] =
|
|
useState(true);
|
|
const [isLoadingRooms, setIsLoadingRooms] = useState(true);
|
|
const [isLoadingNewest, setIsLoadingNewest] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
// Fetch banners
|
|
useEffect(() => {
|
|
const fetchBanners = async () => {
|
|
try {
|
|
setIsLoadingBanners(true);
|
|
const response = await bannerService
|
|
.getBannersByPosition('home');
|
|
|
|
// Handle both response formats
|
|
if (
|
|
response.success ||
|
|
response.status === 'success'
|
|
) {
|
|
setBanners(response.data?.banners || []);
|
|
}
|
|
} catch (err: any) {
|
|
console.error('Error fetching banners:', err);
|
|
// Don't show error for banners, just use fallback
|
|
// Silently fail - banners are not critical for page functionality
|
|
} finally {
|
|
setIsLoadingBanners(false);
|
|
}
|
|
};
|
|
|
|
fetchBanners();
|
|
}, []);
|
|
|
|
// Fetch featured rooms
|
|
useEffect(() => {
|
|
const fetchFeaturedRooms = async () => {
|
|
try {
|
|
setIsLoadingRooms(true);
|
|
setError(null);
|
|
const response = await roomService.getFeaturedRooms({
|
|
featured: true,
|
|
limit: 6,
|
|
});
|
|
|
|
// Handle both response formats
|
|
if (
|
|
response.success ||
|
|
response.status === 'success'
|
|
) {
|
|
const rooms = response.data?.rooms || [];
|
|
setFeaturedRooms(rooms);
|
|
// If no rooms found but request succeeded, don't show error
|
|
if (rooms.length === 0) {
|
|
setError(null);
|
|
}
|
|
} else {
|
|
// Response didn't indicate success
|
|
setError(
|
|
response.message ||
|
|
'Unable to load room list'
|
|
);
|
|
}
|
|
} catch (err: any) {
|
|
console.error('Error fetching rooms:', err);
|
|
|
|
// Check if it's a rate limit error
|
|
if (err.response?.status === 429) {
|
|
setError(
|
|
'Too many requests. Please wait a moment and refresh the page.'
|
|
);
|
|
} else {
|
|
setError(
|
|
err.response?.data?.message ||
|
|
err.message ||
|
|
'Unable to load room list'
|
|
);
|
|
}
|
|
} finally {
|
|
setIsLoadingRooms(false);
|
|
}
|
|
};
|
|
|
|
fetchFeaturedRooms();
|
|
}, []);
|
|
|
|
// Fetch newest rooms
|
|
useEffect(() => {
|
|
const fetchNewestRooms = async () => {
|
|
try {
|
|
setIsLoadingNewest(true);
|
|
const response = await roomService.getRooms({
|
|
page: 1,
|
|
limit: 6,
|
|
sort: 'newest',
|
|
});
|
|
|
|
// Handle both response formats
|
|
if (
|
|
response.success ||
|
|
response.status === 'success'
|
|
) {
|
|
setNewestRooms(response.data?.rooms || []);
|
|
}
|
|
} catch (err: any) {
|
|
console.error('Error fetching newest rooms:', err);
|
|
// Silently fail for newest rooms section - not critical
|
|
} finally {
|
|
setIsLoadingNewest(false);
|
|
}
|
|
};
|
|
|
|
fetchNewestRooms();
|
|
}, []);
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gray-50">
|
|
{/* Banner Section */}
|
|
<section className="container mx-auto px-4 pb-8">
|
|
{isLoadingBanners ? (
|
|
<BannerSkeleton />
|
|
) : (
|
|
<BannerCarousel banners={banners} />
|
|
)}
|
|
</section>
|
|
|
|
{/* Search Section */}
|
|
<section className="container mx-auto px-4 py-8">
|
|
<SearchRoomForm />
|
|
</section>
|
|
|
|
{/* Featured Rooms Section */}
|
|
<section className="container mx-auto px-4 py-12">
|
|
{/* Section Header */}
|
|
<div className="flex items-center justify-between mb-8">
|
|
<div className="flex items-center gap-3">
|
|
<div>
|
|
<h2
|
|
className="text-3xl font-bold
|
|
text-gray-900"
|
|
>
|
|
Featured Rooms
|
|
</h2>
|
|
</div>
|
|
</div>
|
|
|
|
<Link
|
|
to="/rooms"
|
|
className="hidden md:flex items-center gap-2
|
|
text-indigo-600 hover:text-indigo-700
|
|
font-semibold transition-colors"
|
|
>
|
|
View All Rooms
|
|
<ArrowRight className="w-5 h-5" />
|
|
</Link>
|
|
</div>
|
|
|
|
{/* Loading State */}
|
|
{isLoadingRooms && (
|
|
<div
|
|
className="grid grid-cols-1 md:grid-cols-2
|
|
lg:grid-cols-3 gap-6"
|
|
>
|
|
{[...Array(6)].map((_, index) => (
|
|
<RoomCardSkeleton key={index} />
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Error State */}
|
|
{error && !isLoadingRooms && (
|
|
<div
|
|
className="bg-red-50 border border-red-200
|
|
rounded-lg p-6 text-center"
|
|
>
|
|
<AlertCircle
|
|
className="w-12 h-12 text-red-500
|
|
mx-auto mb-3"
|
|
/>
|
|
<p className="text-red-700 font-medium">
|
|
{error}
|
|
</p>
|
|
<button
|
|
onClick={() => window.location.reload()}
|
|
className="mt-4 px-4 py-2 bg-red-600
|
|
text-white rounded-lg
|
|
hover:bg-red-700 transition-colors"
|
|
>
|
|
Try Again
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{/* Rooms Grid */}
|
|
{!isLoadingRooms && !error && (
|
|
<>
|
|
{featuredRooms.length > 0 ? (
|
|
<div
|
|
className="grid grid-cols-1 md:grid-cols-2
|
|
lg:grid-cols-3 gap-6"
|
|
>
|
|
{featuredRooms.map((room) => (
|
|
<RoomCard key={room.id} room={room} />
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div
|
|
className="bg-gray-100 rounded-lg
|
|
p-12 text-center"
|
|
>
|
|
<p className="text-gray-600 text-lg">
|
|
No featured rooms available
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* View All Button (Mobile) */}
|
|
{featuredRooms.length > 0 && (
|
|
<div className="mt-8 text-center md:hidden">
|
|
<Link
|
|
to="/rooms"
|
|
className="inline-flex items-center gap-2
|
|
bg-indigo-600 text-white px-6 py-3
|
|
rounded-lg hover:bg-indigo-700
|
|
transition-colors font-semibold"
|
|
>
|
|
View All Rooms
|
|
<ArrowRight className="w-5 h-5" />
|
|
</Link>
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</section>
|
|
|
|
{/* Newest Rooms Section */}
|
|
<section className="container mx-auto px-4 py-12">
|
|
{/* Section Header */}
|
|
<div className="flex items-center justify-between mb-8">
|
|
<div className="flex items-center gap-3">
|
|
<div>
|
|
<h2
|
|
className="text-3xl font-bold
|
|
text-gray-900"
|
|
>
|
|
Newest Rooms
|
|
</h2>
|
|
</div>
|
|
</div>
|
|
|
|
<Link
|
|
to="/rooms"
|
|
className="hidden md:flex items-center gap-2
|
|
text-indigo-600 hover:text-indigo-700
|
|
font-semibold transition-colors"
|
|
>
|
|
View All Rooms
|
|
<ArrowRight className="w-5 h-5" />
|
|
</Link>
|
|
</div>
|
|
|
|
{/* Loading State */}
|
|
{isLoadingNewest && (
|
|
<div
|
|
className="grid grid-cols-1 md:grid-cols-2
|
|
lg:grid-cols-3 gap-6"
|
|
>
|
|
{[...Array(6)].map((_, index) => (
|
|
<RoomCardSkeleton key={index} />
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Rooms Grid */}
|
|
{!isLoadingNewest && (
|
|
<>
|
|
{newestRooms.length > 0 ? (
|
|
<div
|
|
className="grid grid-cols-1 md:grid-cols-2
|
|
lg:grid-cols-3 gap-6"
|
|
>
|
|
{newestRooms.map((room) => (
|
|
<RoomCard key={room.id} room={room} />
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div
|
|
className="bg-gray-100 rounded-lg
|
|
p-12 text-center"
|
|
>
|
|
<p className="text-gray-600 text-lg">
|
|
No new rooms available
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* View All Button (Mobile) */}
|
|
{newestRooms.length > 0 && (
|
|
<div className="mt-8 text-center md:hidden">
|
|
<Link
|
|
to="/rooms"
|
|
className="inline-flex items-center gap-2
|
|
bg-indigo-600 text-white px-6 py-3
|
|
rounded-lg hover:bg-indigo-700
|
|
transition-colors font-semibold"
|
|
>
|
|
View All Rooms
|
|
<ArrowRight className="w-5 h-5" />
|
|
</Link>
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</section>
|
|
|
|
{/* Features Section */}
|
|
<section
|
|
className="container mx-auto px-4 py-12
|
|
bg-white rounded-xl shadow-sm mx-4"
|
|
>
|
|
<div
|
|
className="grid grid-cols-1 md:grid-cols-3
|
|
gap-8"
|
|
>
|
|
<div className="text-center">
|
|
<div
|
|
className="w-16 h-16 bg-indigo-100
|
|
rounded-full flex items-center
|
|
justify-center mx-auto mb-4"
|
|
>
|
|
<span className="text-3xl">🏨</span>
|
|
</div>
|
|
<h3
|
|
className="text-xl font-semibold mb-2
|
|
text-gray-900"
|
|
>
|
|
Easy Booking
|
|
</h3>
|
|
<p className="text-gray-600">
|
|
Search and book rooms with just a few clicks
|
|
</p>
|
|
</div>
|
|
|
|
<div className="text-center">
|
|
<div
|
|
className="w-16 h-16 bg-green-100
|
|
rounded-full flex items-center
|
|
justify-center mx-auto mb-4"
|
|
>
|
|
<span className="text-3xl">💰</span>
|
|
</div>
|
|
<h3
|
|
className="text-xl font-semibold mb-2
|
|
text-gray-900"
|
|
>
|
|
Best Prices
|
|
</h3>
|
|
<p className="text-gray-600">
|
|
Best price guarantee in the market
|
|
</p>
|
|
</div>
|
|
|
|
<div className="text-center">
|
|
<div
|
|
className="w-16 h-16 bg-blue-100
|
|
rounded-full flex items-center
|
|
justify-center mx-auto mb-4"
|
|
>
|
|
<span className="text-3xl">🎧</span>
|
|
</div>
|
|
<h3
|
|
className="text-xl font-semibold mb-2
|
|
text-gray-900"
|
|
>
|
|
24/7 Support
|
|
</h3>
|
|
<p className="text-gray-600">
|
|
Support team always ready to serve
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default HomePage;
|