From 76626db1fccc4a3a49ca358b0e27125e78d5a662 Mon Sep 17 00:00:00 2001 From: Satyajeet Chavan Date: Sun, 3 Aug 2025 07:58:30 +0000 Subject: [PATCH] Feat: Connected frontend pages to live API with full loading state handling --- client/src/api/api.js | 17 ++ client/src/components/ErrorMessage.jsx | 5 + client/src/components/Loader.jsx | 2 + client/src/pages/AllRooms.jsx | 242 +++++++++++--------- client/src/pages/MyBookings.jsx | 154 +++++++++---- client/src/pages/RoomDetails.jsx | 280 +++++++++++++++--------- server/controllers/bookingController.js | 10 + server/controllers/clerkWebhooks.js | 99 +++++---- server/controllers/roomController.js | 21 ++ server/server.js | 54 +++-- 10 files changed, 566 insertions(+), 318 deletions(-) create mode 100644 client/src/api/api.js create mode 100644 client/src/components/ErrorMessage.jsx create mode 100644 client/src/components/Loader.jsx create mode 100644 server/controllers/bookingController.js create mode 100644 server/controllers/roomController.js diff --git a/client/src/api/api.js b/client/src/api/api.js new file mode 100644 index 0000000..514767e --- /dev/null +++ b/client/src/api/api.js @@ -0,0 +1,17 @@ +export async function fetchRooms() { + const res = await fetch('/api/rooms'); + if (!res.ok) throw new Error('Failed to fetch rooms'); + return res.json(); +} + +export async function fetchRoomById(id) { + const res = await fetch(`/api/rooms/${id}`); + if (!res.ok) throw new Error('Failed to fetch room details'); + return res.json(); +} + +export async function fetchBookings() { + const res = await fetch('/api/bookings'); + if (!res.ok) throw new Error('Failed to fetch bookings'); + return res.json(); +} diff --git a/client/src/components/ErrorMessage.jsx b/client/src/components/ErrorMessage.jsx new file mode 100644 index 0000000..4a1a435 --- /dev/null +++ b/client/src/components/ErrorMessage.jsx @@ -0,0 +1,5 @@ +// ErrorMessage.jsx +const ErrorMessage = ({ message }) => ( +
{message}
+); +export default ErrorMessage; \ No newline at end of file diff --git a/client/src/components/Loader.jsx b/client/src/components/Loader.jsx new file mode 100644 index 0000000..1b99e65 --- /dev/null +++ b/client/src/components/Loader.jsx @@ -0,0 +1,2 @@ +const Loader = () =>
Loading...
; +export default Loader; \ No newline at end of file diff --git a/client/src/pages/AllRooms.jsx b/client/src/pages/AllRooms.jsx index 7afcc5d..18e1e55 100644 --- a/client/src/pages/AllRooms.jsx +++ b/client/src/pages/AllRooms.jsx @@ -1,116 +1,152 @@ -import React, { useState } from 'react' -import { assets, roomsDummyData, facilityIcons } from '../assets/assets' -import { useNavigate } from 'react-router-dom' +import React, { useState, useEffect } from 'react'; +import { assets, facilityIcons } from '../assets/assets'; +import { useNavigate } from 'react-router-dom'; import StarRating from '../components/StarRating'; -const CheckBox = ({label, selected = false, onChange = () => { }})=>{ - return ( - - ) -} +const CheckBox = ({ label, selected = false, onChange = () => {} }) => { + return ( + + ); +}; -const RadioButton = ({label, selected = false, onChange = () => { }})=>{ - return ( - - ) -} +const RadioButton = ({ label, selected = false, onChange = () => {} }) => { + return ( + + ); +}; const AllRooms = () => { - const navigate = useNavigate(); - const [openFilters, setOpenFilters] = useState(false); - const roomTypes = [ - "Single Bed", - "Double Bed", - "Luxury Room", - "Family Suite", - ]; - const priceRange = [ - '0 to 500', - '500 to 1000', - '1000 to 1500', - '1500 to 2000', - '2000 to 2500', - '2500 to 3000', - ]; - const sortOptions = [ - "Price Low to High", - "Price High to Low", - "Newest First" - ]; + const navigate = useNavigate(); + const [openFilters, setOpenFilters] = useState(false); + const [rooms, setRooms] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const roomTypes = ["Single Bed", "Double Bed", "Luxury Room", "Family Suite"]; + const priceRange = ['0 to 500', '500 to 1000', '1000 to 1500', '1500 to 2000', '2000 to 2500', '2500 to 3000']; + const sortOptions = ["Price Low to High", "Price High to Low", "Newest First"]; + + // Fetch Rooms Data + useEffect(() => { + const fetchRooms = async () => { + try { + const response = await fetch('/api/rooms'); + if (!response.ok) throw new Error('Failed to fetch rooms'); + const data = await response.json(); + setRooms(data); + } catch (err) { + setError(err.message); + } finally { + setLoading(false); + } + }; + fetchRooms(); + }, []); + return ( -
-
-
-

Hotel Rooms

-

Take advantage of our limited-time offers and special packages to enhance your stay and create unforgettable memories.

-
- {roomsDummyData.map((room)=>( -
- {navigate(`/rooms/${room._id}`); scrollTo(0,0)}} src={room.images[0]} alt="hotel-img" title='View Room Details' className='max-h-65 md:w-1/2 rounded-xl shadow-lg object-cover cursor-pointer'/> -
-

{room.hotel.city}

-

{navigate(`/rooms/${room._id}`); scrollTo(0,0)}}>{room.hotel.name}

-
- -

200+ reviews

-
-
- location-icon - {room.hotel.address} -
-
- {room.amenities.map((item,index)=>( -
- {item} -

{item}

-
- ))} -
- {/* {Room Price per Night} */} -

${room.pricePerNight}/night

-
-
- ))} +
+
+
+

Hotel Rooms

+

+ Take advantage of our limited-time offers and special packages to enhance your stay and create unforgettable memories. +

- {/* {filters} */} -
-
-

FILTERS

-
- setOpenFilters(!openFilters)}> {openFilters ? "HIDE" : "SHOW"} - CLEAR -
+ + {/* Loading State */} + {loading &&

Loading available rooms...

} + + {/* Error State */} + {error &&

Error: {error}

} + + {/* Empty State */} + {!loading && !error && rooms.length === 0 &&

No rooms available at the moment.

} + + {/* Rooms List */} + {!loading && !error && rooms.length > 0 && + rooms.map((room) => ( +
+ { navigate(`/rooms/${room._id}`); scrollTo(0, 0); }} + src={room.images[0]} + alt="hotel-img" + title="View Room Details" + className="max-h-65 md:w-1/2 rounded-xl shadow-lg object-cover cursor-pointer" + /> +
+

{room.hotel.city}

+

{ navigate(`/rooms/${room._id}`); scrollTo(0, 0); }} + > + {room.hotel.name} +

+
+ +

200+ reviews

-
-
-

Popular Filters

- {roomTypes.map((room,index)=>( - - ))} -
-
-

Price Range

- {priceRange.map((range,index)=>( - - ))} -
-
-

Sort By

- {sortOptions.map((option,index)=>( - - ))} +
+ location-icon + {room.hotel.address} +
+
+ {room.amenities.map((item, index) => ( +
+ {item} +

{item}

- + ))}
+

${room.pricePerNight}/night

+
+ )) + } +
+ + {/* Filters */} +
+
+

FILTERS

+
+ setOpenFilters(!openFilters)}> + {openFilters ? "HIDE" : "SHOW"} + + CLEAR +
+
+
+
+

Popular Filters

+ {roomTypes.map((room, index) => ( + + ))} +
+
+

Price Range

+ {priceRange.map((range, index) => ( + + ))} +
+
+

Sort By

+ {sortOptions.map((option, index) => ( + + ))} +
+
+
- ) -} + ); +}; -export default AllRooms \ No newline at end of file +export default AllRooms; diff --git a/client/src/pages/MyBookings.jsx b/client/src/pages/MyBookings.jsx index 995f08a..d3b0fb7 100644 --- a/client/src/pages/MyBookings.jsx +++ b/client/src/pages/MyBookings.jsx @@ -1,67 +1,135 @@ -import React, { useState } from 'react' -import Title from '../components/Title' -import { assets, userBookingsDummyData } from '../assets/assets' +import React, { useState, useEffect } from 'react'; +import Title from '../components/Title'; +import { assets } from '../assets/assets'; + const MyBookings = () => { - const [bookings, setBookings] = useState(userBookingsDummyData); + const [bookings, setBookings] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchBookings = async () => { + try { + const response = await fetch('/api/bookings'); + if (!response.ok) { + throw new Error('Failed to fetch bookings'); + } + const data = await response.json(); + setBookings(data); + } catch (err) { + setError(err.message); + } finally { + setLoading(false); + } + }; + + fetchBookings(); + }, []); + return (
-
- - <div className='hidden md:grid md:grid-cols-[3fr_2fr_1fr] w-full border-b border-gray-300 font-medium text-base py-3 mt-6'> - <div className='w-1/3'>Hotels</div> - <div className='w-1/3'>Date & Timings</div> - <div className='w-1/3'>Payment</div> - </div> - {bookings.map((booking)=>( - <div key={booking._id} className='grid grid-cols-1 md:grid-cols-[3fr_2fr_1fr] w-full border-b border-gray-300 py-6 first:border-t'> + <div className="max-w-6xl mx-auto w-full text-gray-800"> + <Title + title="My Bookings" + subTitle="Easily manage your past, current, and upcoming hotel reservations in one place. Plan your trips seamlessly with just a few clicks." + align="left" + /> + + {/* Loading State */} + {loading && <p className="mt-6 text-center text-gray-500">Loading your bookings...</p>} + + {/* Error State */} + {error && <p className="mt-6 text-center text-red-500">Error: {error}</p>} + + {/* Empty State */} + {!loading && !error && bookings.length === 0 && ( + <p className="mt-6 text-center text-gray-500">You have no bookings yet.</p> + )} + + {/* Table Header */} + {!loading && !error && bookings.length > 0 && ( + <> + <div className="hidden md:grid md:grid-cols-[3fr_2fr_1fr] w-full border-b border-gray-300 font-medium text-base py-3 mt-6"> + <div className="w-1/3">Hotels</div> + <div className="w-1/3">Date & Timings</div> + <div className="w-1/3">Payment</div> + </div> + + {/* Booking List */} + {bookings.map((booking) => ( + <div + key={booking._id} + className="grid grid-cols-1 md:grid-cols-[3fr_2fr_1fr] w-full border-b border-gray-300 py-6 first:border-t" + > {/* Hotel Details */} - <div className='flex flex-col md:flex-row'> - <img src={booking.room.images[0]} alt="hotel-img" className='min-md:w-44 rounded shadow object-cover'/> - <div className='flex flex-col gap-1.5 max-md:mt-3 min-md:ml-4'> - <p className='font-playfair text-2xl'>{booking.hotel.name} - <span className='font-inter text-sm'> ({booking.room.roomType})</span> - </p> - <div className='flex items-center gap-1 text-gray-500 text-sm'> - <img src={assets.locationIcon} alt="location-icon"/> - <span>{booking.hotel.address}</span> - </div> - <div className='flex items-center gap-1 text-gray-500 text-sm'> - <img src={assets.guestsIcon} alt="guest-icon"/> - <span>Guests: {booking.guests}</span> - </div> - <p className='text-base'>Total: ${booking.totalPrice}</p> + <div className="flex flex-col md:flex-row"> + <img + src={booking.room.images[0]} + alt="hotel-img" + className="min-md:w-44 rounded shadow object-cover" + /> + <div className="flex flex-col gap-1.5 max-md:mt-3 min-md:ml-4"> + <p className="font-playfair text-2xl"> + {booking.hotel.name} + <span className="font-inter text-sm"> ({booking.room.roomType})</span> + </p> + <div className="flex items-center gap-1 text-gray-500 text-sm"> + <img src={assets.locationIcon} alt="location-icon" /> + <span>{booking.hotel.address}</span> </div> + <div className="flex items-center gap-1 text-gray-500 text-sm"> + <img src={assets.guestsIcon} alt="guest-icon" /> + <span>Guests: {booking.guests}</span> + </div> + <p className="text-base">Total: ${booking.totalPrice}</p> + </div> </div> + {/* Date & Timings */} - <div className='flex flex-row md:items-center md:gap-12 mt-3 gap-8'> + <div className="flex flex-row md:items-center md:gap-12 mt-3 gap-8"> <div> <p>Check-In:</p> - <p className='text-gray-500 text-sm'>{new Date(booking.checkInDate).toDateString()}</p> + <p className="text-gray-500 text-sm"> + {new Date(booking.checkInDate).toDateString()} + </p> </div> <div> <p>Check-Out:</p> - <p className='text-gray-500 text-sm'>{new Date(booking.checkOutDate).toDateString()}</p> + <p className="text-gray-500 text-sm"> + {new Date(booking.checkOutDate).toDateString()} + </p> </div> - </div> - {/* Payment Status */} - <div className='flex flex-col items-start justify-center pt-3'> - <div className='flex items-center gap-2'> - <div className={`h-3 w-3 rounded-full ${booking.isPaid ? "bg-green-500" : "bg-red-500"}`}> - - </div> - <p className={`text-sm ${booking.isPaid ? "text-green-500" : "text-red-500"}`}>{booking.isPaid ? "Paid" : "Unpaid"}</p> + {/* Payment Status */} + <div className="flex flex-col items-start justify-center pt-3"> + <div className="flex items-center gap-2"> + <div + className={`h-3 w-3 rounded-full ${ + booking.isPaid ? 'bg-green-500' : 'bg-red-500' + }`} + ></div> + <p + className={`text-sm ${ + booking.isPaid ? 'text-green-500' : 'text-red-500' + }`} + > + {booking.isPaid ? 'Paid' : 'Unpaid'} + </p> </div> {!booking.isPaid && ( - <button className='px-4 py-1.5 mt-4 text-xs border border-gray-400 rounded-full hover:bg-gray-50 transition-all cursor-pointer'>Pay Now</button> + <button className="px-4 py-1.5 mt-4 text-xs border border-gray-400 rounded-full hover:bg-gray-50 transition-all cursor-pointer"> + Pay Now + </button> )} </div> - </div> - ))} + </div> + ))} + </> + )} </div> </div> ); }; -export default MyBookings \ No newline at end of file +export default MyBookings; diff --git a/client/src/pages/RoomDetails.jsx b/client/src/pages/RoomDetails.jsx index ba1f7bf..178807b 100644 --- a/client/src/pages/RoomDetails.jsx +++ b/client/src/pages/RoomDetails.jsx @@ -1,122 +1,188 @@ import React, { useState, useEffect } from 'react'; import { useParams } from 'react-router-dom'; -import { roomsDummyData, assets, facilityIcons, roomCommonData } from '../assets/assets'; +import { assets, facilityIcons, roomCommonData } from '../assets/assets'; import StarRating from '../components/StarRating'; const RoomDetails = () => { - const {id} = useParams(); - const [room, setRoom] = useState(null); - const [mainImage, setMainImage] = useState(null); - useEffect(()=>{ - const room = roomsDummyData.find(room => room._id === id); - room && setRoom(room); - room && setMainImage(room.images[0]); - },[]) - return room &&( - <div className='py-28 md:py-35 px-4 md:px-16 lg:px-24 xl:px-32'> - {/* {Room Details} */} - <div className='flex flex-col md:flex-row items-start md:items-center gap-2'> - <h1 className='text-3xl md:text-4xl font-playfair'>{room.hotel.name} <span className='font-inner text-sm'>({room.roomType})</span></h1> - <p className='text-xs font-inter py-1.5 px-3 text-white bg-orange-500 rounded-full'>20% OFF</p> - </div> - {/* {Room Rating} */} - <div className='flex items-center gap-1 mt-2'> - <StarRating /> - <p className='ml-2'>200+ reviews</p> - </div> - {/* Room Address */} - <div className='flex items-center gap-1 text-gray-500 mt-2'> - <img src={assets.locationIcon} alt="location-icon" /> - <span>{room.hotel.address}</span> - </div> - {/* Room Images */} - <div className='flex flex-col lg:flex-row mt-6 gap-6'> - <div className='w-full lg:w-1/2'> - <img src={mainImage} alt="room-img" className='w-full rounded-xl shadow-lg object-cover' /> - </div> - <div className='grid grid-cols-2 gap-4 lg:w-1/2 w-full'> - {room?.images.length > 1 && room.images.map((image,index)=>( - <img onClick={()=>setMainImage(image)} key={index} src={image} alt="room-img" className={`w-full rounded-xl shadow-md object-cover cursor-pointer ${mainImage === image && 'outline-3 outline-orange-500'}`}/> - ))} - </div> + const { id } = useParams(); + const [room, setRoom] = useState(null); + const [mainImage, setMainImage] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchRoomDetails = async () => { + try { + const response = await fetch(`/api/rooms/${id}`); + if (!response.ok) { + throw new Error('Failed to fetch room details'); + } + const data = await response.json(); + setRoom(data); + setMainImage(data.images?.[0]); + } catch (err) { + setError(err.message); + } finally { + setLoading(false); + } + }; + + fetchRoomDetails(); + }, [id]); + + // Loading State + if (loading) { + return <p className="text-center mt-20 text-gray-500">Loading room details...</p>; + } + + // Error State + if (error) { + return <p className="text-center mt-20 text-red-500">Error: {error}</p>; + } + + // Empty State (if room not found) + if (!room) { + return <p className="text-center mt-20 text-gray-500">Room details not available.</p>; + } + + return ( + <div className="py-28 md:py-35 px-4 md:px-16 lg:px-24 xl:px-32"> + {/* Room Details Header */} + <div className="flex flex-col md:flex-row items-start md:items-center gap-2"> + <h1 className="text-3xl md:text-4xl font-playfair"> + {room.hotel.name}{' '} + <span className="font-inner text-sm">({room.roomType})</span> + </h1> + <p className="text-xs font-inter py-1.5 px-3 text-white bg-orange-500 rounded-full"> + 20% OFF + </p> + </div> + + {/* Rating */} + <div className="flex items-center gap-1 mt-2"> + <StarRating /> + <p className="ml-2">200+ reviews</p> + </div> + + {/* Address */} + <div className="flex items-center gap-1 text-gray-500 mt-2"> + <img src={assets.locationIcon} alt="location-icon" /> + <span>{room.hotel.address}</span> + </div> + + {/* Room Images */} + <div className="flex flex-col lg:flex-row mt-6 gap-6"> + <div className="w-full lg:w-1/2"> + <img + src={mainImage} + alt="room-img" + className="w-full rounded-xl shadow-lg object-cover" + /> </div> - {/* Room Highlights */} - <div className='flex flex-col md:flex-row md:justify-between mt-10'> - <div className='flex flex-col'> - <h1 className='text-3xl md:text-4xl font-playfair'>Experience Luxury Like Never Before</h1> - <div className='flex flex-wrap items-center mt-3 mb-6 gap-4'> - {room.amenities.map((item,index)=>( - <div key={index} className='flex items-center gap-2 px-3 py-2 rounded-lg bg-gray-100'> - <img src={facilityIcons[item]} alt={item} className='w-5 h-5'/> - <p className='text-xs'>{item}</p> - </div> - ))} - </div> - </div> - {/* Room Price */} - <p className='text-2xl font-medium font-playfair'>${room.pricePerNight}/night</p> + <div className="grid grid-cols-2 gap-4 lg:w-1/2 w-full"> + {room?.images?.length > 1 && + room.images.map((image, index) => ( + <img + key={index} + src={image} + alt="room-img" + onClick={() => setMainImage(image)} + className={`w-full rounded-xl shadow-md object-cover cursor-pointer ${ + mainImage === image && 'outline-3 outline-orange-500' + }`} + /> + ))} </div> - {/* Checkin Checkout form */} - <form className='flex flex-col md:flex-row justify-between mt-16 items-start md:items-center bg-white shadow-[0px_0px_20px_rgba(0,0,0,0.15)] p-6 rounded-xl mx-auto max-w-6xl'> - <div className='flex flex-col flex-wrap md:gap-10 text-gray-500 md:flex-row items-start md:items-center gap-4'> - - <div className='flex flex-col'> - <label htmlFor="checkInDate" className='font-medium'>Check-In</label> - <input type="date" id='checkInDate' placeholder='Check-In' className='rounded border border-gray-300 px-3 py-2 mt-1.5 outline-none' required/> - </div> - <div className='w-px h-15 bg-gray-300/70 max-md:hidden'></div> - <div className='flex flex-col'> - <label htmlFor="checkOutDate" className='font-medium'>Check-Out</label> - <input type="date" id='checkOutDate' placeholder='Check-Out' className='rounded border border-gray-300 px-3 py-2 mt-1.5 outline-none' required/> - </div> - <div className='w-px h-15 bg-gray-300/70 max-md:hidden'></div> - <div className='flex flex-col'> - <label htmlFor="guests" className='font-medium'>Guests</label> - <input type="number" id='guests' placeholder='0' className='max-w-20 rounded border border-gray-300 px-3 py-2 mt-1.5 outline-none' required/> - </div> + </div> - </div> - <button type="submit" className='bg-primary hover:bg-primary-dull active:scale-95 transition-all text-white rounded-md max-md:w-full max-md:mt-6 md:px-25 md:py-4 py-3 text-base cursor-pointer'>Check Availability</button> - - </form> - {/* Common Specifiications */} - <div className='mt-25 space-y-4'> - {roomCommonData.map((spec,index)=>( - <div key={index} className='flex items-start gap-2'> - <img src={spec.icon} alt={`${spec.title}-icon`} className='w-6.5'/> - <div> - <p className='font-base'>{spec.title}</p> - <p className='text-gray-500'>{spec.description}</p> - </div> - - </div> + {/* Highlights */} + <div className="flex flex-col md:flex-row md:justify-between mt-10"> + <div className="flex flex-col"> + <h1 className="text-3xl md:text-4xl font-playfair"> + Experience Luxury Like Never Before + </h1> + <div className="flex flex-wrap items-center mt-3 mb-6 gap-4"> + {room.amenities.map((item, index) => ( + <div + key={index} + className="flex items-center gap-2 px-3 py-2 rounded-lg bg-gray-100" + > + <img src={facilityIcons[item]} alt={item} className="w-5 h-5" /> + <p className="text-xs">{item}</p> + </div> ))} + </div> </div> - <div className='max-w-3xl border-y border-gray-300 my-15 py-10 text-gray-500'> - <p>Guests will be allocated on the ground floor according to availability. You get a comfortable Two bedroom apartment has a true city feeling. - The price quoted is for two guest, at the guest slot please mark the number of guests to get the exact price for groups. The Guests will be allocated - ground floor according to availability. You get the comfortable two-bedroom apartment that has a true city feeling. </p> + {/* Price */} + <p className="text-2xl font-medium font-playfair"> + ${room.pricePerNight}/night + </p> + </div> + + {/* Checkin Checkout Form */} + <form className="flex flex-col md:flex-row justify-between mt-16 items-start md:items-center bg-white shadow-[0px_0px_20px_rgba(0,0,0,0.15)] p-6 rounded-xl mx-auto max-w-6xl"> + <div className="flex flex-col flex-wrap md:gap-10 text-gray-500 md:flex-row items-start md:items-center gap-4"> + <div className="flex flex-col"> + <label htmlFor="checkInDate" className="font-medium">Check-In</label> + <input type="date" id="checkInDate" className="rounded border border-gray-300 px-3 py-2 mt-1.5 outline-none" required /> + </div> + <div className="w-px h-15 bg-gray-300/70 max-md:hidden"></div> + <div className="flex flex-col"> + <label htmlFor="checkOutDate" className="font-medium">Check-Out</label> + <input type="date" id="checkOutDate" className="rounded border border-gray-300 px-3 py-2 mt-1.5 outline-none" required /> + </div> + <div className="w-px h-15 bg-gray-300/70 max-md:hidden"></div> + <div className="flex flex-col"> + <label htmlFor="guests" className="font-medium">Guests</label> + <input type="number" id="guests" placeholder="0" className="max-w-20 rounded border border-gray-300 px-3 py-2 mt-1.5 outline-none" required /> + </div> </div> - {/* Hosted By */} - <div className='flex flex-col items-start gap-4'> - <div className='flex gap-4'> - <img src={room.hotel.owner.image} alt="Host" className='w-14 h-14 md:h-18 md:w-18 rounded-full' /> - <div> - <p className='text-lg md:text-xl'>Hosted By {room.hotel.name}</p> - <div className='flex items-center mt-1'> - <StarRating/> - <p className='ml-2'>200+ Reviews</p> - </div> - - </div> + <button type="submit" className="bg-primary hover:bg-primary-dull active:scale-95 transition-all text-white rounded-md max-md:w-full max-md:mt-6 md:px-25 md:py-4 py-3 text-base cursor-pointer"> + Check Availability + </button> + </form> + + {/* Common Specifications */} + <div className="mt-25 space-y-4"> + {roomCommonData.map((spec, index) => ( + <div key={index} className="flex items-start gap-2"> + <img src={spec.icon} alt={`${spec.title}-icon`} className="w-6.5" /> + <div> + <p className="font-base">{spec.title}</p> + <p className="text-gray-500">{spec.description}</p> </div> - <button className='px-6 py-2 bg-primary hover:bg-primary-dull transition-all text-white cursor-pointer'> - Contact Now - </button> - </div> + </div> + ))} + </div> + + {/* Description */} + <div className="max-w-3xl border-y border-gray-300 my-15 py-10 text-gray-500"> + <p> + Guests will be allocated on the ground floor according to availability. + You get a comfortable Two bedroom apartment with a true city feeling. + The price quoted is for two guests, adjust the guest slot to get an exact price for groups. + Comfortable, modern, and well-suited for your stay. + </p> + </div> + {/* Hosted By */} + <div className="flex flex-col items-start gap-4"> + <div className="flex gap-4"> + <img src={room.hotel.owner.image} alt="Host" className="w-14 h-14 md:h-18 md:w-18 rounded-full" /> + <div> + <p className="text-lg md:text-xl">Hosted By {room.hotel.name}</p> + <div className="flex items-center mt-1"> + <StarRating /> + <p className="ml-2">200+ Reviews</p> + </div> + </div> + </div> + <button className="px-6 py-2 bg-primary hover:bg-primary-dull transition-all text-white cursor-pointer"> + Contact Now + </button> + </div> </div> - ) -} + ); +}; -export default RoomDetails \ No newline at end of file +export default RoomDetails; diff --git a/server/controllers/bookingController.js b/server/controllers/bookingController.js new file mode 100644 index 0000000..31146c6 --- /dev/null +++ b/server/controllers/bookingController.js @@ -0,0 +1,10 @@ +// /workspaces/Hotel-Booking/server/controllers/bookingController.js +export const getBookings = async (req, res) => { + try { + // TODO: Replace with real DB query + const bookings = []; // dummy placeholder + res.json(bookings); + } catch (error) { + res.status(500).json({ message: "Error fetching bookings" }); + } +}; diff --git a/server/controllers/clerkWebhooks.js b/server/controllers/clerkWebhooks.js index b8fe030..cc17b48 100644 --- a/server/controllers/clerkWebhooks.js +++ b/server/controllers/clerkWebhooks.js @@ -2,49 +2,62 @@ import User from "../models/User.js"; import { Webhook } from "svix"; const clerkWebhooks = async (req, res) => { - try { - const whook = new Webhook(process.env.CLERK_WEBHOOK_SECRET); - //Getting Headers - const headers = { - 'svix-id': req.headers['svix-id'], - 'svix-timestamp': req.headers['svix-timestamp'], - 'svix-signature': req.headers['svix-signature'] - }; - await whook.verify(JSON.stringify(req.body), headers); - - //Getting data from request body - const {data, type} = req.body; - const userData = { - _id: data.id, - email: data.email_addresses[0].email_address, - username: data.first_name + " " + data.last_name, - image: data.image_url, - } - - //Switch case for different types of events - switch (type) { - case "user.created":{ - await User.create(userData); - break; - } - case "user.updated":{ - await User.findByIdAndUpdate(data.id, userData); - break; - } - case "user.deleted":{ - await User.findByIdAndDelete(data.id); - break; - } - - default: - break; - } - res.json({success: true, message: "Webhook Received"}); - - } catch (error) { - console.log(error.message); - res.status(500).json({success: false, error: error.message }); + try { + // ✅ Verify Clerk Webhook Signature + const whook = new Webhook(process.env.CLERK_WEBHOOK_SECRET); + const headers = { + "svix-id": req.headers["svix-id"], + "svix-timestamp": req.headers["svix-timestamp"], + "svix-signature": req.headers["svix-signature"], + }; + + if (!headers["svix-id"] || !headers["svix-timestamp"] || !headers["svix-signature"]) { + return res.status(400).json({ success: false, message: "Missing Svix headers" }); + } + + await whook.verify(JSON.stringify(req.body), headers); + + // ✅ Extract data from the webhook + const { data, type } = req.body; + if (!data?.id) { + return res.status(400).json({ success: false, message: "Invalid webhook payload" }); + } + + const userData = { + _id: data.id, + email: data.email_addresses?.[0]?.email_address || "", + username: `${data.first_name || ""} ${data.last_name || ""}`.trim(), + image: data.image_url || "", + }; + + // ✅ Handle different webhook events + switch (type) { + case "user.created": + await User.create(userData); + console.log(`✅ User created: ${userData.email}`); + break; + + case "user.updated": + await User.findByIdAndUpdate(data.id, userData, { new: true }); + console.log(`✅ User updated: ${userData.email}`); + break; + + case "user.deleted": + await User.findByIdAndDelete(data.id); + console.log(`✅ User deleted: ${data.id}`); + break; + + default: + console.log(`ℹ️ Unknown Clerk event type received: ${type}`); + break; } -} + + return res.json({ success: true, message: "Webhook processed successfully" }); + + } catch (error) { + console.error("❌ Clerk Webhook Error:", error.message); + return res.status(500).json({ success: false, error: error.message }); + } +}; export default clerkWebhooks; diff --git a/server/controllers/roomController.js b/server/controllers/roomController.js new file mode 100644 index 0000000..3cef1fe --- /dev/null +++ b/server/controllers/roomController.js @@ -0,0 +1,21 @@ +// /workspaces/Hotel-Booking/server/controllers/roomController.js +export const getRooms = async (req, res) => { + try { + // TODO: Replace with real DB query + const rooms = []; // dummy placeholder + res.json(rooms); + } catch (error) { + res.status(500).json({ message: "Error fetching rooms" }); + } +}; + +export const getRoomById = async (req, res) => { + try { + // TODO: Replace with real DB query + const room = null; // dummy placeholder + if (!room) return res.status(404).json({ message: "Room not found" }); + res.json(room); + } catch (error) { + res.status(500).json({ message: "Error fetching room details" }); + } +}; diff --git a/server/server.js b/server/server.js index 6a10a2b..58f2823 100644 --- a/server/server.js +++ b/server/server.js @@ -2,39 +2,49 @@ import express from "express"; import "dotenv/config"; import cors from "cors"; import connectDB from "./configs/db.js"; -import { clerkMiddleware } from '@clerk/express' +import { clerkMiddleware } from "@clerk/express"; import clerkWebhooks from "./controllers/clerkWebhooks.js"; + +// ✅ Import controllers +import { getRooms, getRoomById } from "./controllers/roomController.js"; +import { getBookings } from "./controllers/bookingController.js"; + +// ✅ Connect to MongoDB connectDB(); const app = express(); -app.use(cors()); // Enable Cross-Origin Resource Sharing -//MiddleWare -app.use(express.json()); -app.use(clerkMiddleware()); - +// ✅ Middleware +app.use(cors({ origin: process.env.CLIENT_URL || "*", credentials: true })); // Allow frontend URL +app.use(express.json({ limit: "10mb" })); // Handle larger JSON payloads +app.use(clerkMiddleware()); // Clerk authentication middleware -//API to listen to clerk Webhooks +// ✅ Health Check Route +app.get("/", (req, res) => { + res.status(200).json({ success: true, message: "API is Up and Running 🚀" }); +}); -app.use('/api/clerk', clerkWebhooks); +// ✅ Clerk Webhook (must be BEFORE other routes if it uses raw body) +app.post("/api/clerk", clerkWebhooks); -app.get('/', (req, res) => { - res.send("API is Up and running"); -}); +// ✅ API Routes +app.get("/api/rooms", getRooms); +app.get("/api/rooms/:id", getRoomById); +app.get("/api/bookings", getBookings); -//404 error handling -app.use((req,res,next)=>{ - res.status(404).json({message:"Route not found"}); - +// ✅ 404 Error Handling +app.use((req, res) => { + res.status(404).json({ success: false, message: "Route not found" }); }); -//global error handler -app.use((err,req,res,next)=>{ - console.error(err.stack); - res.status(500).json({message:"Internal Server Error"}); +// ✅ Global Error Handler +app.use((err, req, res, next) => { + console.error("🔥 Server Error:", err.stack); + res.status(500).json({ success: false, message: "Internal Server Error" }); }); -const PORT = process.env.PORT || 3000; +// ✅ Start Server +const PORT = process.env.PORT || 3000; app.listen(PORT, () => { - console.log(`Server is running on port ${PORT}`) -}); \ No newline at end of file + console.log(`✅ Server running on http://localhost:${PORT}`); +});