Files
Hotel-Booking/Frontend/src/pages/HomePage.tsx
Iliyan Angelov a1bd576540 updates
2025-11-17 23:50:14 +02:00

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;