Files
Hotel-Booking/Frontend/src/pages/HomePage.tsx
Iliyan Angelov 48353cde9c update
2025-11-16 20:05:08 +02:00

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;