343 lines
11 KiB
TypeScript
343 lines
11 KiB
TypeScript
import React, { useState, useEffect, useMemo } from 'react';
|
|
import { Link } from 'react-router-dom';
|
|
import {
|
|
ArrowRight,
|
|
AlertCircle,
|
|
} from 'lucide-react';
|
|
import {
|
|
BannerCarousel,
|
|
BannerSkeleton,
|
|
RoomCard,
|
|
RoomCardSkeleton,
|
|
RoomCarousel,
|
|
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);
|
|
|
|
// Combine featured and newest rooms, removing duplicates
|
|
const combinedRooms = useMemo(() => {
|
|
const roomMap = new Map<number, Room>();
|
|
|
|
// Add featured rooms first (they take priority)
|
|
featuredRooms.forEach(room => {
|
|
roomMap.set(room.id, room);
|
|
});
|
|
|
|
// Add newest rooms that aren't already in the map
|
|
newestRooms.forEach(room => {
|
|
if (!roomMap.has(room.id)) {
|
|
roomMap.set(room.id, room);
|
|
}
|
|
});
|
|
|
|
return Array.from(roomMap.values());
|
|
}, [featuredRooms, newestRooms]);
|
|
|
|
// 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 & Newest Rooms Section - Combined Carousel */}
|
|
<section className="container mx-auto px-4 py-6 md:py-8">
|
|
{/* Section Header - Centered */}
|
|
<div className="text-center animate-fade-in mb-6 md:mb-8">
|
|
<h2 className="luxury-section-title text-center">
|
|
Featured & Newest Rooms
|
|
</h2>
|
|
<p className="luxury-section-subtitle text-center max-w-2xl mx-auto mt-2">
|
|
Discover our most popular accommodations and latest additions
|
|
</p>
|
|
|
|
{/* View All Rooms Button - Golden, Centered */}
|
|
<div className="mt-6 flex justify-center">
|
|
<Link
|
|
to="/rooms"
|
|
className="btn-luxury-primary inline-flex items-center gap-2 px-6 py-3 rounded-sm font-medium tracking-wide"
|
|
>
|
|
<span className="relative z-10">View All Rooms</span>
|
|
<ArrowRight className="w-5 h-5 relative z-10 group-hover:translate-x-1 transition-transform" />
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Loading State */}
|
|
{(isLoadingRooms || isLoadingNewest) && (
|
|
<div className="flex justify-center">
|
|
<div className="max-w-md w-full">
|
|
<RoomCardSkeleton />
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Error State */}
|
|
{error && !isLoadingRooms && !isLoadingNewest && (
|
|
<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>
|
|
)}
|
|
|
|
{/* Combined Rooms Carousel */}
|
|
{!isLoadingRooms && !isLoadingNewest && (
|
|
<>
|
|
{combinedRooms.length > 0 ? (
|
|
<RoomCarousel
|
|
rooms={combinedRooms}
|
|
autoSlideInterval={4000}
|
|
showNavigation={true}
|
|
/>
|
|
) : (
|
|
<div
|
|
className="luxury-card p-12 text-center animate-fade-in"
|
|
>
|
|
<p className="text-gray-600 text-lg font-light tracking-wide">
|
|
No rooms available
|
|
</p>
|
|
</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;
|