diff --git a/client/public/add-category.html b/client/public/add-category.html new file mode 100644 index 0000000..7c005c3 --- /dev/null +++ b/client/public/add-category.html @@ -0,0 +1,60 @@ + + + + + Add Room Category + + +

Add Room Category (Admin)

+

Make sure you are logged in to /admin/login first.

+ +
+

+ +
+

+ +
+

+ + + +

+
+  
+
+
diff --git a/client/public/uploads/1-1768295870059-239826802.jpg b/client/public/uploads/1-1768295870059-239826802.jpg
new file mode 100644
index 0000000..5d25430
Binary files /dev/null and b/client/public/uploads/1-1768295870059-239826802.jpg differ
diff --git a/client/public/uploads/2-1768295870063-802829438.jpg b/client/public/uploads/2-1768295870063-802829438.jpg
new file mode 100644
index 0000000..7f8a469
Binary files /dev/null and b/client/public/uploads/2-1768295870063-802829438.jpg differ
diff --git a/client/public/uploads/3-1768295870064-579147982.webp b/client/public/uploads/3-1768295870064-579147982.webp
new file mode 100644
index 0000000..ebcea41
Binary files /dev/null and b/client/public/uploads/3-1768295870064-579147982.webp differ
diff --git a/client/public/uploads/4-1768295870068-442995448.jpg b/client/public/uploads/4-1768295870068-442995448.jpg
new file mode 100644
index 0000000..1b339db
Binary files /dev/null and b/client/public/uploads/4-1768295870068-442995448.jpg differ
diff --git a/client/public/uploads/5-1768295870072-844168095.png b/client/public/uploads/5-1768295870072-844168095.png
new file mode 100644
index 0000000..782ac88
Binary files /dev/null and b/client/public/uploads/5-1768295870072-844168095.png differ
diff --git a/client/public/uploads/888-1768295870105-385689950.jpg b/client/public/uploads/888-1768295870105-385689950.jpg
new file mode 100644
index 0000000..f3fa8ce
Binary files /dev/null and b/client/public/uploads/888-1768295870105-385689950.jpg differ
diff --git a/client/public/uploads/ai-accounting-1768295870109-673067819.jpg b/client/public/uploads/ai-accounting-1768295870109-673067819.jpg
new file mode 100644
index 0000000..0eac489
Binary files /dev/null and b/client/public/uploads/ai-accounting-1768295870109-673067819.jpg differ
diff --git a/client/public/uploads/optimized-3-1768295870064-579147982.webp b/client/public/uploads/optimized-3-1768295870064-579147982.webp
new file mode 100644
index 0000000..7839da2
Binary files /dev/null and b/client/public/uploads/optimized-3-1768295870064-579147982.webp differ
diff --git a/client/src/components/admin/AdminLayout.js b/client/src/components/admin/AdminLayout.js
index c8619cb..79aa9c8 100644
--- a/client/src/components/admin/AdminLayout.js
+++ b/client/src/components/admin/AdminLayout.js
@@ -49,6 +49,10 @@ const menuItems = [
   { title: 'Settings', path: '/admin/settings', icon:  },
 ];
 
+// ✅ hide from sidebar only (keep code/routes for future)
+const hiddenMenuTitles = new Set(['Guests', 'Blog']);
+const adminMenuItems = menuItems.filter(item => !hiddenMenuTitles.has(item.title));
+
 const AdminLayout = () => {
   const theme = useTheme();
   const isMobile = useMediaQuery(theme.breakpoints.down('md'));
@@ -102,7 +106,7 @@ const AdminLayout = () => {
       
       
       
-        {menuItems.map((item) => (
+        {adminMenuItems.map((item) => (
           
              {
 };
 
 export default AdminLayout;
-
diff --git a/client/src/pages/About.js b/client/src/pages/About.js
index 28c688e..bca8458 100644
--- a/client/src/pages/About.js
+++ b/client/src/pages/About.js
@@ -20,7 +20,25 @@ import {
 import { useTranslation } from 'react-i18next';
 import { motion } from 'framer-motion';
 import { Helmet } from 'react-helmet-async';
-import staticData from '../utils/staticData';
+import api from '../utils/api'; // ✅ بدل staticData
+
+const API_BASE = process.env.REACT_APP_API_URL || 'http://localhost:5080';
+const toMediaUrl = (url) => {
+  if (!url) return url;
+  if (/^https?:\/\//i.test(url)) {
+    if (url.includes('localhost:3060/uploads/')) return url.replace('http://localhost:3060', API_BASE);
+    return url;
+  }
+  if (url.startsWith('/uploads/')) return `${API_BASE}${url}`;
+  return url; // /images/... stays on client
+};
+
+const unwrapContent = (res) => {
+  return res?.data?.data?.content || res?.data?.data || res?.data || null;
+};
+
+// ✅ remove HTML tags مثل 

..

+const stripHtml = (html = '') => String(html).replace(/<[^>]*>/g, '').trim(); const About = () => { const { t, i18n } = useTranslation(); @@ -29,20 +47,27 @@ const About = () => { const [loading, setLoading] = useState(true); const currentLanguage = i18n.language; - // Fetch about page content from static data + // ✅ Fetch about page content from API بدل static data useEffect(() => { + let mounted = true; + const fetchContent = async () => { try { - const aboutContent = await staticData.getAboutContent(); - setContent(aboutContent); + const res = await api.get('/api/content/about'); + const aboutContent = unwrapContent(res); + if (mounted) setContent(aboutContent); } catch (error) { console.error('Error loading about page content:', error); } finally { - setLoading(false); + if (mounted) setLoading(false); } }; fetchContent(); + + return () => { + mounted = false; + }; }, []); const values = [ @@ -68,7 +93,6 @@ const About = () => { } ]; - const milestones = [ { year: '1985', @@ -111,25 +135,40 @@ const About = () => { ); } - // Use translations for non-English languages, static data for English + // Use translations for non-English languages, API content for English const useTranslations = currentLanguage !== 'en'; - - // Fallback content - prioritize translations for non-English - const heroTitle = useTranslations ? t('about.heroTitle') : (content?.hero?.title || t('about.heroTitle')); - const heroSubtitle = useTranslations ? t('about.heroSubtitle') : (content?.hero?.subtitle || t('about.heroSubtitle')); - const heroDescription = useTranslations ? '' : (content?.hero?.description || ''); - const heroImage = content?.hero?.backgroundImage || '/images/about-hero.jpg'; + + // ✅ stripHtml only for content coming from dashboard (English) + const heroTitle = useTranslations ? t('about.heroTitle') : stripHtml(content?.hero?.title || t('about.heroTitle')); + const heroSubtitle = useTranslations ? t('about.heroSubtitle') : stripHtml(content?.hero?.subtitle || t('about.heroSubtitle')); + const heroDescription = useTranslations ? '' : stripHtml(content?.hero?.description || ''); + const heroImage = toMediaUrl(content?.hero?.backgroundImage) || '/images/about-hero.jpg'; // Get sections from content - use translations for non-English const heritageSectionStatic = content?.sections?.find(s => s.sectionId === 'heritage') || {}; + const heritageSection = useTranslations ? { title: t('about.heritageTitle'), content: t('about.heritageContent'), - image: heritageSectionStatic.image || '/images/about.jpg' - } : heritageSectionStatic; + image: toMediaUrl(heritageSectionStatic.image) || '/images/about.jpg' + } : { + ...heritageSectionStatic, + title: stripHtml(heritageSectionStatic.title || ''), + subtitle: stripHtml(heritageSectionStatic.subtitle || ''), + content: stripHtml(heritageSectionStatic.content || ''), + image: toMediaUrl(heritageSectionStatic.image) || '/images/about.jpg' + }; + const missionSection = content?.sections?.find(s => s.sectionId === 'mission') || {}; const visionSection = content?.sections?.find(s => s.sectionId === 'vision') || {}; - const valuesSection = content?.sections?.find(s => s.sectionId === 'values') || {}; + const valuesSectionStatic = content?.sections?.find(s => s.sectionId === 'values') || {}; + + const valuesSection = useTranslations ? valuesSectionStatic : { + ...valuesSectionStatic, + title: stripHtml(valuesSectionStatic.title || ''), + content: stripHtml(valuesSectionStatic.content || ''), + // items (إذا موجودة) نتركها كما هي بدون تغيير + }; return ( <> @@ -162,7 +201,6 @@ const About = () => { component="h1" sx={{ mb: 3, - // Use theme heading font fontSize: { xs: '2.5rem', md: '4rem' }, }} > @@ -225,7 +263,7 @@ const About = () => { sx={{ lineHeight: 1.8, fontSize: '1.1rem', - whiteSpace: 'pre-line' // Preserve line breaks + whiteSpace: 'pre-line' }} > {heritageSection.content} @@ -332,107 +370,105 @@ const About = () => { - {false && ( - - - - Our Journey - - - {/* Timeline line */} - - - {milestones.map((milestone, index) => ( + {false && ( + - - - - - {milestone.year} - - - {milestone.title} - - - {milestone.description} - - - + Our Journey + - {/* Timeline dot */} + - + {milestones.map((milestone, index) => ( + + + + + + {milestone.year} + + + {milestone.title} + + + {milestone.description} + + + + + + + + + + ))} - ))} - - - -)} - + + )} ); }; -export default About; \ No newline at end of file +export default About; diff --git a/client/src/pages/Booking.js b/client/src/pages/Booking.js index 20beaa7..4a74586 100644 --- a/client/src/pages/Booking.js +++ b/client/src/pages/Booking.js @@ -1,30 +1,184 @@ -import React from 'react'; -import { Container, Typography, Box } from '@mui/material'; +import React, { useEffect, useState } from 'react'; +import { + Container, Typography, Box, TextField, Button, + MenuItem, Alert, CircularProgress +} from '@mui/material'; +import api from '../utils/api'; const Booking = () => { + const [rooms, setRooms] = useState([]); + const [loadingRooms, setLoadingRooms] = useState(true); + const [submitting, setSubmitting] = useState(false); + const [message, setMessage] = useState(null); + + const [form, setForm] = useState({ + firstName: '', + lastName: '', + email: '', + phone: '', + roomId: '', + checkInDate: '', + checkOutDate: '', + adults: 1, + children: 0, + specialRequests: '' + }); + + useEffect(() => { + const loadRooms = async () => { + try { + const res = await api.get('/api/rooms?limit=100'); + setRooms(res?.data?.data?.rooms || []); + } catch (e) { + setMessage({ type: 'error', text: 'Failed to load rooms' }); + } finally { + setLoadingRooms(false); + } + }; + loadRooms(); + }, []); + + const handleChange = (key) => (e) => { + setForm(prev => ({ ...prev, [key]: e.target.value })); + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + setSubmitting(true); + setMessage(null); + + try { + const payload = { + guestInfo: { + firstName: form.firstName, + lastName: form.lastName, + email: form.email, + phone: form.phone + }, + roomId: form.roomId, + checkInDate: form.checkInDate, + checkOutDate: form.checkOutDate, + numberOfGuests: { + adults: Number(form.adults), + children: Number(form.children || 0) + }, + specialRequests: form.specialRequests + }; + + const res = await api.post('/api/bookings/request', payload); + const bookingNumber = res?.data?.data?.booking?.bookingNumber; + + setMessage({ + type: 'success', + text: bookingNumber + ? `Booking request submitted! Booking #: ${bookingNumber}` + : 'Booking request submitted successfully!' + }); + + // reset minimal + setForm(prev => ({ ...prev, specialRequests: '' })); + } catch (error) { + setMessage({ + type: 'error', + text: error.response?.data?.message || 'Failed to submit booking request' + }); + } finally { + setSubmitting(false); + } + }; + return ( - - + + Book Your Stay - - Reserve your room at our luxury hotel + + Submit a booking request (Pay at hotel) - - - Booking form coming soon... - - {/* Add booking form here */} - + + {message && ( + setMessage(null)}> + {message.text} + + )} + + {loadingRooms ? ( + + + + ) : ( + + + + + + + + {rooms.map(r => ( + + {r.name} (#{r.roomNumber}) - ${r.basePrice}/night + + ))} + + + + + + + + + + + + + + + )} ); }; -export default Booking; \ No newline at end of file +export default Booking; diff --git a/client/src/pages/CategoryGallery.js b/client/src/pages/CategoryGallery.js index 48cfa60..c2fbd7a 100644 --- a/client/src/pages/CategoryGallery.js +++ b/client/src/pages/CategoryGallery.js @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useMemo } from 'react'; import { Container, Typography, @@ -19,7 +19,7 @@ import { import { useParams, Link } from 'react-router-dom'; import { motion, AnimatePresence } from 'framer-motion'; import { Helmet } from 'react-helmet-async'; -import staticData from '../utils/staticData'; +import api from '../utils/api'; import CloseIcon from '@mui/icons-material/Close'; import ArrowBackIosIcon from '@mui/icons-material/ArrowBackIos'; import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos'; @@ -31,35 +31,90 @@ import 'swiper/css/navigation'; import 'swiper/css/thumbs'; import 'swiper/css/free-mode'; +const API_BASE = process.env.REACT_APP_API_URL || 'http://localhost:5080'; + +// يحوّل روابط uploads لتكون من الـ CMS بدل الـ client +const toMediaUrl = (url) => { + if (!url) return url; + + // إذا رابط كامل http/https + if (/^https?:\/\//i.test(url)) { + // Fix: لو كان بالغلط على 3060/uploads + if (url.includes('localhost:3060/uploads/')) { + return url.replace('http://localhost:3060', API_BASE); + } + return url; + } + + // إذا /uploads/... خليه من API_BASE + if (url.startsWith('/uploads/')) return `${API_BASE}${url}`; + + // باقي الروابط مثل /images/... خليها عادي + return url; +}; + const CategoryGallery = () => { const { categorySlug } = useParams(); const [category, setCategory] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [lightboxOpen, setLightboxOpen] = useState(false); const [lightboxIndex, setLightboxIndex] = useState(0); const [thumbsSwiper, setThumbsSwiper] = useState(null); + const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down('sm')); useEffect(() => { + let mounted = true; + const fetchCategory = async () => { try { - const category = await staticData.getRoomCategory(categorySlug); - setCategory(category); + setLoading(true); + setError(null); + + const res = await api.get(`/api/room-categories/${categorySlug}`); + const cat = res?.data?.data?.category || null; + + if (mounted) setCategory(cat); } catch (err) { console.error('Error loading category:', err); - setError('Failed to load room category. Please try again later.'); + if (mounted) setError('Category not found'); } finally { - setLoading(false); + if (mounted) setLoading(false); } }; - if (categorySlug) { - fetchCategory(); - } + if (categorySlug) fetchCategory(); + + return () => { + mounted = false; + }; }, [categorySlug]); + const rooms = category?.rooms || []; + + // ✅ Gallery images: + // إذا الكاتيجوري عندها صور استخدميها، إذا لا، استخدمي صور الغرف التابعة + const images = useMemo(() => { + const catImages = category?.images || []; + if (catImages.length > 0) return catImages; + + // fallback: اجمع صور الغرف + const roomImages = rooms + .flatMap((r) => r?.images || []) + .filter((img) => img?.url); + + // إزالة التكرار حسب url + const seen = new Set(); + return roomImages.filter((img) => { + if (seen.has(img.url)) return false; + seen.add(img.url); + return true; + }); + }, [category, rooms]); + const handleImageClick = (index) => { setLightboxIndex(index); setLightboxOpen(true); @@ -70,15 +125,11 @@ const CategoryGallery = () => { }; const handlePrevious = () => { - setLightboxIndex((prev) => - prev > 0 ? prev - 1 : (category.images.length - 1) - ); + setLightboxIndex((prev) => (prev > 0 ? prev - 1 : images.length - 1)); }; const handleNext = () => { - setLightboxIndex((prev) => - prev < category.images.length - 1 ? prev + 1 : 0 - ); + setLightboxIndex((prev) => (prev < images.length - 1 ? prev + 1 : 0)); }; if (loading) { @@ -102,9 +153,6 @@ const CategoryGallery = () => { ); } - const images = category.images || []; - const rooms = category.rooms || []; - return ( <> @@ -117,7 +165,7 @@ const CategoryGallery = () => { sx={{ minHeight: '40vh', background: category.primaryImage - ? `linear-gradient(rgba(0,0,0,0.5), rgba(0,0,0,0.5)), url(${category.primaryImage}) center/cover` + ? `linear-gradient(rgba(0,0,0,0.5), rgba(0,0,0,0.5)), url(${toMediaUrl(category.primaryImage)}) center/cover` : 'linear-gradient(rgba(0,0,0,0.5), rgba(0,0,0,0.5)), url("/images/hero.jpg") center/cover', display: 'flex', alignItems: 'center', @@ -135,30 +183,22 @@ const CategoryGallery = () => { {category.name} {category.description} + {category.priceRange && category.priceRange.min > 0 && ( From ${category.priceRange.min} - {category.priceRange.max > category.priceRange.min && - ` - $${category.priceRange.max}`} + {category.priceRange.max > category.priceRange.min ? ` - $${category.priceRange.max}` : ''} /night )} @@ -191,7 +231,7 @@ const CategoryGallery = () => { handleImageClick(index)} sx={{ @@ -200,9 +240,7 @@ const CategoryGallery = () => { objectFit: 'cover', cursor: 'pointer', transition: 'transform 0.3s', - '&:hover': { - transform: 'scale(1.02)', - }, + '&:hover': { transform: 'scale(1.02)' }, }} /> @@ -225,7 +263,7 @@ const CategoryGallery = () => { handleImageClick(index)} sx={{ @@ -236,9 +274,7 @@ const CategoryGallery = () => { cursor: 'pointer', border: '2px solid transparent', transition: 'border-color 0.3s', - '&:hover': { - borderColor: 'primary.main', - }, + '&:hover': { borderColor: 'primary.main' }, }} /> @@ -260,6 +296,7 @@ const CategoryGallery = () => { Available Rooms ({rooms.length}) + {rooms.map((room) => ( @@ -269,45 +306,44 @@ const CategoryGallery = () => { display: 'flex', flexDirection: 'column', transition: 'transform 0.3s, box-shadow 0.3s', - '&:hover': { - transform: 'translateY(-4px)', - boxShadow: 4, - }, + '&:hover': { transform: 'translateY(-4px)', boxShadow: 4 }, }} > 0 - ? room.images.find(img => img.isPrimary)?.url || room.images[0].url - : '/images/room-default.jpg' + toMediaUrl( + room.images?.find((img) => img.isPrimary)?.url || + room.images?.[0]?.url + ) || '/images/room-default.jpg' } alt={room.name} sx={{ objectFit: 'cover' }} + onError={(e) => { e.currentTarget.src = '/images/room-default.jpg'; }} /> + {room.name} + {room.shortDescription} + Room {room.roomNumber} | {room.size} m² | Max {room.maxOccupancy} guests + {room.amenities && room.amenities.slice(0, 3).map((amenity, idx) => ( - + ))} + @@ -317,6 +353,7 @@ const CategoryGallery = () => { per night + @@ -372,9 +403,7 @@ const CategoryGallery = () => { color: 'white', zIndex: 2, backgroundColor: 'rgba(0, 0, 0, 0.5)', - '&:hover': { - backgroundColor: 'rgba(0, 0, 0, 0.7)', - }, + '&:hover': { backgroundColor: 'rgba(0, 0, 0, 0.7)' }, }} > @@ -390,13 +419,12 @@ const CategoryGallery = () => { color: 'white', zIndex: 2, backgroundColor: 'rgba(0, 0, 0, 0.5)', - '&:hover': { - backgroundColor: 'rgba(0, 0, 0, 0.7)', - }, + '&:hover': { backgroundColor: 'rgba(0, 0, 0, 0.7)' }, }} > + { color: 'white', zIndex: 2, backgroundColor: 'rgba(0, 0, 0, 0.5)', - '&:hover': { - backgroundColor: 'rgba(0, 0, 0, 0.7)', - }, + '&:hover': { backgroundColor: 'rgba(0, 0, 0, 0.7)' }, }} > @@ -418,7 +444,7 @@ const CategoryGallery = () => { { }; export default CategoryGallery; - diff --git a/client/src/pages/Home.js b/client/src/pages/Home.js index 9cf94e1..ce2ac77 100644 --- a/client/src/pages/Home.js +++ b/client/src/pages/Home.js @@ -22,7 +22,24 @@ import { Link } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { motion } from 'framer-motion'; import { Helmet } from 'react-helmet-async'; -import staticData from '../utils/staticData'; +import api from '../utils/api'; + +const API_BASE = process.env.REACT_APP_API_URL || 'http://localhost:5080'; +const toMediaUrl = (url) => { + if (!url) return url; + if (/^https?:\/\//i.test(url)) { + if (url.includes('localhost:3060/uploads/')) return url.replace('http://localhost:3060', API_BASE); + return url; + } + if (url.startsWith('/uploads/')) return `${API_BASE}${url}`; + return url; // /images/... stays on client +}; + +const unwrapContent = (res) => { + return res?.data?.data?.content || res?.data?.data || res?.data || null; +}; + +const stripHtml = (html = '') => String(html).replace(/<[^>]*>/g, '').trim(); const Home = () => { const { t, i18n } = useTranslation(); @@ -32,22 +49,32 @@ const Home = () => { const currentLanguage = i18n.language; useEffect(() => { + let mounted = true; + const fetchContent = async () => { try { - const [homeContent, categories] = await Promise.all([ - staticData.getHomeContent(), - staticData.getRoomCategories(), + const [homeRes, catRes] = await Promise.all([ + api.get('/api/content/home'), + api.get('/api/room-categories'), ]); - setContent(homeContent); - setRoomCategories(categories.slice(0, 3)); + + const homeContent = unwrapContent(homeRes); + const categories = catRes?.data?.data?.categories || []; + + if (mounted) { + setContent(homeContent); + setRoomCategories(categories.slice(0, 3)); + } } catch (error) { console.error('Error loading homepage content:', error); } finally { - setLoading(false); + if (mounted) setLoading(false); } }; fetchContent(); + + return () => { mounted = false; }; }, []); const features = [ @@ -81,27 +108,29 @@ const Home = () => { }, ]; - const heroImage = content?.hero?.backgroundImage || '/images/hero.jpg'; + const heroImage = toMediaUrl(content?.hero?.backgroundImage) || '/images/hero.jpg'; + + const welcomeSection = + content?.sections?.find((s) => s.sectionId === 'welcome') || {}; - const welcomeSection = content?.sections?.find((s) => s.sectionId === 'welcome') || {}; const useTranslations = currentLanguage !== 'en'; const welcomeTitle = useTranslations ? t('home.welcomeTitle') - : (welcomeSection.title || t('home.welcomeTitle')); + : stripHtml(welcomeSection.title || t('home.welcomeTitle')); const welcomeSubtitle = useTranslations ? t('home.welcomeSubtitle') - : (welcomeSection.subtitle || t('home.welcomeSubtitle')); + : stripHtml(welcomeSection.subtitle || t('home.welcomeSubtitle')); const welcomeContent = useTranslations ? t('home.welcomeDescription') - : (welcomeSection.content || t('home.welcomeDescription')); + : stripHtml(welcomeSection.content || t('home.welcomeDescription')); const roomTypes = roomCategories.map((category) => ({ id: category._id || category.slug, name: category.name, - image: category.primaryImage || '/images/room-default.jpg', + image: toMediaUrl(category.primaryImage) || '/images/room-default.jpg', features: category.features?.slice(0, 4) || [], slug: category.slug, })); @@ -114,8 +143,6 @@ const Home = () => { ); } - const LOGO_BOTTOM = { xs: 40, sm: 55, md: 70 }; - return ( <> @@ -130,8 +157,7 @@ const Home = () => { /> - {/* Hero Section - Logo and Image Only */} - + {/* Hero */} { color: 'white', textAlign: 'center', position: 'relative', - pb: { xs: 25, md: 80}, + pb: { xs: 25, md: 80 }, }} > @@ -169,7 +195,7 @@ const Home = () => { - {/* Welcome Section */} + {/* Welcome */} { - {/* Features Section */} + {/* Features */} { - {/* Rooms Preview Section */} + {/* Rooms Preview */} { transition: 'transform 0.3s ease', '&:hover': { transform: 'scale(1.05)' }, }} + onError={(e) => { e.currentTarget.src = '/images/room-default.jpg'; }} /> diff --git a/client/src/pages/RoomDetails.js b/client/src/pages/RoomDetails.js index 79cd176..c0a6a8d 100644 --- a/client/src/pages/RoomDetails.js +++ b/client/src/pages/RoomDetails.js @@ -1,82 +1,103 @@ -import React, { useMemo } from 'react'; -import { Container, Typography, Box, Grid, Card, CardContent, Chip } from '@mui/material'; -import { useParams } from 'react-router-dom'; +import React, { useEffect, useMemo, useState } from 'react'; +import { Container, Typography, Box, Grid, Card, CardContent, Chip, CircularProgress, Button } from '@mui/material'; +import { useParams, Link } from 'react-router-dom'; import { Swiper, SwiperSlide } from 'swiper/react'; import SwiperCore from 'swiper'; import 'swiper/css'; import 'swiper/css/navigation'; import 'swiper/css/thumbs'; import { Navigation, Thumbs } from 'swiper/modules'; - -const ROOM_CONFIG = { - '1': { - name: 'Deluxe Room', - folder: '/images/rooms/deluxe', - features: ['King Bed', 'City View', 'Free WiFi', 'Mini Bar'], - price: 199, - images: [ - '01.jpg', - '02.jpg', - '03.jpg', - '04.jpg', - '05.jpg' - ], - }, - '2': { - name: 'Executive Suite', - folder: '/images/rooms/executive', - features: ['Separate Living Room', 'Premium View', 'Butler Service', 'Complimentary Breakfast'], - price: 349, - images: [ - '01.jpg', - '02.jpg', - '03.jpg', - '04.jpg', - '05.jpg' - ], - }, - '3': { - name: 'Presidential Suite', - folder: '/images/rooms/presidential', - features: ['2 Bedrooms', 'Private Terrace', 'Personal Chef', 'Spa Access'], - price: 599, - images: [ - '01.jpg', - '02.jpg', - '03.jpg', - '04.jpg', - '05.jpg' - ], - }, -}; +import api from '../utils/api'; SwiperCore.use([Navigation, Thumbs]); +const isObjectId = (v) => /^[0-9a-fA-F]{24}$/.test(v || ''); + const RoomDetails = () => { - const { id } = useParams(); - const room = ROOM_CONFIG[id] || ROOM_CONFIG['1']; + const { id } = useParams(); + const [room, setRoom] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchRoom = async () => { + try { + setLoading(true); + + let res; + if (isObjectId(id)) { + res = await api.get(`/api/rooms/${id}`); + } else { + res = await api.get(`/api/rooms/slug/${id}`); + } + + const data = res?.data?.data || null; + if (!data) { + setError('Room not found'); + setRoom(null); + } else { + setRoom(data); + setError(null); + } + } catch (err) { + console.error('Error loading room:', err); + setError('Failed to load room details.'); + setRoom(null); + } finally { + setLoading(false); + } + }; + + fetchRoom(); + }, [id]); const imageUrls = useMemo(() => { - return (room.images || []).map(name => `${room.folder}/${name}`); + const imgs = room?.images || []; + return (imgs || []).map((img) => img?.url).filter(Boolean); }, [room]); + if (loading) { + return ( + + + + ); + } + + if (error || !room) { + return ( + + {error || 'Room not found'} + + + ); + } + + const features = room.amenities || []; + return ( {room.name} + - From ${room.price} per night + From ${room.basePrice}{' '} + + per night + + - {room.features.map((f, i) => ( + {features.map((f, i) => ( ))} - {/* Gallery */} {imageUrls.length > 0 ? ( <> { spaceBetween={10} slidesPerView={1} style={{ borderRadius: 8, overflow: 'hidden' }} - > + > {imageUrls.map((src, idx) => ( { ))} - {/* Thumbnails */} {imageUrls.map((src, idx) => ( - + { - No images have been added yet. Place files in {room.folder} and refresh. + No images have been added yet. @@ -127,4 +147,4 @@ const RoomDetails = () => { ); }; -export default RoomDetails; \ No newline at end of file +export default RoomDetails; diff --git a/client/src/pages/Rooms.js b/client/src/pages/Rooms.js index 71143b9..07b2a78 100644 --- a/client/src/pages/Rooms.js +++ b/client/src/pages/Rooms.js @@ -16,29 +16,47 @@ import { import { Link } from 'react-router-dom'; import { motion } from 'framer-motion'; import { Helmet } from 'react-helmet-async'; -import staticData from '../utils/staticData'; +import api from '../utils/api'; import ImageIcon from '@mui/icons-material/Image'; import VisibilityIcon from '@mui/icons-material/Visibility'; +const API_BASE = process.env.REACT_APP_API_URL || 'http://localhost:5080'; +const toMediaUrl = (url) => { + if (!url) return url; + if (/^https?:\/\//i.test(url)) return url; + if (url.startsWith('/uploads/')) return `${API_BASE}${url}`; + return url; +}; + const Rooms = () => { const [categories, setCategories] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { + let mounted = true; + const fetchCategories = async () => { try { - const categories = await staticData.getRoomCategories(); - setCategories(categories); + setLoading(true); + setError(null); + + const res = await api.get('/api/room-categories'); + const cats = res?.data?.data?.categories || []; + if (mounted) setCategories(cats); } catch (err) { console.error('Error loading room categories:', err); - setError('Failed to load room categories. Please try again later.'); + if (mounted) setError('Failed to load room categories. Please try again later.'); } finally { - setLoading(false); + if (mounted) setLoading(false); } }; fetchCategories(); + + return () => { + mounted = false; + }; }, []); if (loading) { @@ -66,8 +84,14 @@ const Rooms = () => { <> Our Rooms - Old Vine Hotel - - + + {/* Hero Section */} @@ -120,6 +144,9 @@ const Rooms = () => { No room categories available at the moment. + + Please add categories from the admin dashboard. + ) : ( @@ -134,6 +161,7 @@ const Rooms = () => { { { + e.currentTarget.src = '/images/room-default.jpg'; + }} /> - {/* Image Count Badge */} + {category.imageCount > 0 && ( { )} - + { > {category.name} - + {category.shortDescription || category.description} {/* Features */} - {category.features && category.features.length > 0 && ( + {Array.isArray(category.features) && category.features.length > 0 && ( {category.features.slice(0, 4).map((feature, idx) => ( { /> ))} {category.features.length > 4 && ( - + )} )} {/* Stats */} - + {category.roomCount > 0 && ( {category.roomCount} {category.roomCount === 1 ? 'Room' : 'Rooms'} Available )} + + {category.priceRange?.min > 0 && ( + + From ${category.priceRange.min}/night + + )} - + -