418 lines
14 KiB
TypeScript
418 lines
14 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 (
|
|
<>
|
|
{/* Banner Section - Full Width, breaks out of container */}
|
|
<section
|
|
className="relative w-screen -mt-6"
|
|
style={{
|
|
marginLeft: 'calc(50% - 50vw)',
|
|
marginRight: 'calc(50% - 50vw)'
|
|
}}
|
|
>
|
|
{isLoadingBanners ? (
|
|
<BannerSkeleton />
|
|
) : (
|
|
<div className="animate-fade-in">
|
|
<BannerCarousel banners={banners}>
|
|
<SearchRoomForm className="overlay" />
|
|
</BannerCarousel>
|
|
</div>
|
|
)}
|
|
</section>
|
|
|
|
<div className="min-h-screen bg-gradient-to-br from-gray-50 via-gray-100/50 to-gray-50">
|
|
|
|
{/* Featured Rooms Section */}
|
|
<section className="container mx-auto px-4 py-16">
|
|
{/* Section Header */}
|
|
<div className="luxury-section-header flex items-center justify-between animate-fade-in">
|
|
<div className="flex items-center gap-3">
|
|
<div>
|
|
<h2 className="luxury-section-title">
|
|
Featured Rooms
|
|
</h2>
|
|
<p className="luxury-section-subtitle">
|
|
Discover our most popular accommodations
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<Link
|
|
to="/rooms"
|
|
className="hidden md:flex items-center gap-2
|
|
btn-luxury-secondary group text-white"
|
|
>
|
|
View All Rooms
|
|
<ArrowRight className="w-5 h-5 group-hover:translate-x-1 transition-transform" />
|
|
</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="luxury-card p-8 text-center animate-fade-in
|
|
border-red-200 bg-gradient-to-br from-red-50 to-red-100/50"
|
|
>
|
|
<div className="inline-flex items-center justify-center w-16 h-16
|
|
bg-red-100 rounded-full mb-4">
|
|
<AlertCircle
|
|
className="w-8 h-8 text-red-600"
|
|
/>
|
|
</div>
|
|
<p className="text-red-800 font-serif font-semibold text-lg mb-2 tracking-tight">
|
|
{error}
|
|
</p>
|
|
<button
|
|
onClick={() => window.location.reload()}
|
|
className="mt-4 px-6 py-2.5 bg-gradient-to-r from-red-600 to-red-700
|
|
text-white rounded-sm font-medium tracking-wide
|
|
hover:from-red-700 hover:to-red-800
|
|
transition-all duration-300 shadow-lg shadow-red-500/30
|
|
hover:shadow-xl hover:shadow-red-500/40 hover:-translate-y-0.5"
|
|
>
|
|
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="luxury-card p-12 text-center animate-fade-in"
|
|
>
|
|
<p className="text-gray-600 text-lg font-light tracking-wide">
|
|
No featured rooms available
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* View All Button (Mobile) */}
|
|
{featuredRooms.length > 0 && (
|
|
<div className="mt-10 text-center md:hidden animate-slide-up">
|
|
<Link
|
|
to="/rooms"
|
|
className="btn-luxury-primary inline-flex items-center gap-2"
|
|
>
|
|
<span className="relative z-10">View All Rooms</span>
|
|
<ArrowRight className="w-5 h-5 relative z-10" />
|
|
</Link>
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</section>
|
|
|
|
{/* Newest Rooms Section */}
|
|
<section className="container mx-auto px-4 py-16">
|
|
{/* Section Header */}
|
|
<div className="luxury-section-header flex items-center justify-between animate-fade-in">
|
|
<div className="flex items-center gap-3">
|
|
<div>
|
|
<h2 className="luxury-section-title">
|
|
Newest Rooms
|
|
</h2>
|
|
<p className="luxury-section-subtitle">
|
|
Explore our latest additions
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<Link
|
|
to="/rooms"
|
|
className="hidden md:flex items-center gap-2
|
|
btn-luxury-secondary group text-white"
|
|
>
|
|
View All Rooms
|
|
<ArrowRight className="w-5 h-5 group-hover:translate-x-1 transition-transform" />
|
|
</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="luxury-card p-12 text-center animate-fade-in"
|
|
>
|
|
<p className="text-gray-600 text-lg font-light tracking-wide">
|
|
No new rooms available
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* View All Button (Mobile) */}
|
|
{newestRooms.length > 0 && (
|
|
<div className="mt-10 text-center md:hidden animate-slide-up">
|
|
<Link
|
|
to="/rooms"
|
|
className="btn-luxury-primary inline-flex items-center gap-2"
|
|
>
|
|
<span className="relative z-10">View All Rooms</span>
|
|
<ArrowRight className="w-5 h-5 relative z-10" />
|
|
</Link>
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</section>
|
|
|
|
{/* Features Section */}
|
|
<section className="container mx-auto px-4 py-16">
|
|
<div className="luxury-card-gold p-12 animate-fade-in relative overflow-hidden">
|
|
{/* Decorative gold accent */}
|
|
<div className="absolute top-0 left-0 right-0 h-1 bg-gradient-to-r from-[#d4af37] to-[#f5d76e]"></div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-12">
|
|
<div className="text-center group">
|
|
<div
|
|
className="w-20 h-20 bg-gradient-to-br from-[#d4af37]/20 to-[#f5d76e]/20
|
|
rounded-sm flex items-center justify-center mx-auto mb-6
|
|
shadow-lg shadow-[#d4af37]/20 border border-[#d4af37]/30
|
|
group-hover:scale-110 group-hover:shadow-xl group-hover:shadow-[#d4af37]/30
|
|
transition-all duration-300"
|
|
>
|
|
<span className="text-4xl">🏨</span>
|
|
</div>
|
|
<h3
|
|
className="text-xl font-serif font-semibold mb-3
|
|
text-gray-900 group-hover:text-[#d4af37] transition-colors tracking-tight"
|
|
>
|
|
Easy Booking
|
|
</h3>
|
|
<p className="text-gray-600 leading-relaxed font-light tracking-wide">
|
|
Search and book rooms with just a few clicks
|
|
</p>
|
|
</div>
|
|
|
|
<div className="text-center group">
|
|
<div
|
|
className="w-20 h-20 bg-gradient-to-br from-[#d4af37]/20 to-[#f5d76e]/20
|
|
rounded-sm flex items-center justify-center mx-auto mb-6
|
|
shadow-lg shadow-[#d4af37]/20 border border-[#d4af37]/30
|
|
group-hover:scale-110 group-hover:shadow-xl group-hover:shadow-[#d4af37]/30
|
|
transition-all duration-300"
|
|
>
|
|
<span className="text-4xl">💰</span>
|
|
</div>
|
|
<h3
|
|
className="text-xl font-serif font-semibold mb-3
|
|
text-gray-900 group-hover:text-[#d4af37] transition-colors tracking-tight"
|
|
>
|
|
Best Prices
|
|
</h3>
|
|
<p className="text-gray-600 leading-relaxed font-light tracking-wide">
|
|
Best price guarantee in the market
|
|
</p>
|
|
</div>
|
|
|
|
<div className="text-center group">
|
|
<div
|
|
className="w-20 h-20 bg-gradient-to-br from-[#d4af37]/20 to-[#f5d76e]/20
|
|
rounded-sm flex items-center justify-center mx-auto mb-6
|
|
shadow-lg shadow-[#d4af37]/20 border border-[#d4af37]/30
|
|
group-hover:scale-110 group-hover:shadow-xl group-hover:shadow-[#d4af37]/30
|
|
transition-all duration-300"
|
|
>
|
|
<span className="text-4xl">🎧</span>
|
|
</div>
|
|
<h3
|
|
className="text-xl font-serif font-semibold mb-3
|
|
text-gray-900 group-hover:text-[#d4af37] transition-colors tracking-tight"
|
|
>
|
|
24/7 Support
|
|
</h3>
|
|
<p className="text-gray-600 leading-relaxed font-light tracking-wide">
|
|
Support team always ready to serve
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default HomePage;
|