} props.popularDestinationsData.errors - Any errors that occurred while fetching the data.
15 | */
16 | const PopularLocations = (props) => {
17 | const { popularDestinationsData } = props;
18 | const navigate = useNavigate();
19 |
20 | const onPopularDestincationCardClick = (city) => {
21 | navigate('/hotels', {
22 | state: {
23 | city: city.toString().toLowerCase(),
24 | },
25 | });
26 | };
27 |
28 | return (
29 |
30 |
31 | Book Hotels at Popular Destinations
32 |
33 |
34 | {popularDestinationsData.isLoading
35 | ? Array.from({ length: 5 }, (_, index) => (
36 |
37 | ))
38 | : popularDestinationsData.data.map((city) => (
39 |
45 | ))}
46 |
47 |
48 | );
49 | };
50 | export default PopularLocations;
51 |
--------------------------------------------------------------------------------
/src/components/ux/loader/loader.jsx:
--------------------------------------------------------------------------------
1 | const Loader = ({ height, isFullScreen, loaderText }) => {
2 | const heightClass = height ? `min-h-[${height}]` : `h-[120px]`;
3 | return (
4 |
5 |
12 |
13 |
14 |
30 |
31 |
32 |
Loading...
33 | {loaderText && (
34 |
35 | {loaderText}
36 |
37 | )}
38 |
39 |
40 |
41 | );
42 | };
43 | export default Loader;
44 |
--------------------------------------------------------------------------------
/src/routes/checkout/components/final-booking-summary/FinalBookingSummary.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { differenceInCalendarDays } from 'date-fns';
3 |
4 | /**
5 | * Component for displaying the final booking summary.
6 | * @param {Object} props The component props.
7 | * @param {string} props.hotelName The name of the hotel.
8 | * @param {string} props.checkIn The check-in date.
9 | * @param {string} props.checkOut The check-out date.
10 | * @param {boolean} props.isAuthenticated The user authentication status.
11 | * @param {string} props.phone The user's phone number.
12 | * @param {string} props.email The user's email.
13 | * @param {string} props.fullName The user's full name.
14 | *
15 | * @returns {JSX.Element} The rendered FinalBookingSummary component.
16 | */
17 | const FinalBookingSummary = ({
18 | hotelName,
19 | checkIn,
20 | checkOut,
21 | isAuthenticated,
22 | phone,
23 | email,
24 | fullName,
25 | }) => {
26 | const numNights = differenceInCalendarDays(
27 | new Date(checkOut),
28 | new Date(checkIn)
29 | );
30 | return (
31 |
32 |
33 |
{hotelName}
34 |
35 |
36 |
Check-in
37 |
{checkIn}
38 |
39 |
40 |
41 | {numNights} Nights
42 |
43 |
44 |
45 |
Check-out
46 |
{checkOut}
47 |
48 |
49 |
50 | {isAuthenticated && (
51 |
52 |
53 | Booking details will be sent to:
54 |
55 |
{fullName} (Primary)
56 |
{email}
57 |
{phone}
58 |
59 | )}
60 |
61 | );
62 | };
63 |
64 | export default FinalBookingSummary;
65 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import './index.scss';
4 | import HotelsSearch from './routes/listings/HotelsSearch';
5 | import UserProfile from './routes/user-profile/UserProfile';
6 | import { createBrowserRouter, RouterProvider } from 'react-router-dom';
7 | import reportWebVitals from './reportWebVitals';
8 | import Home from './routes/home/Home';
9 | import { AuthProvider } from './contexts/AuthContext';
10 | import { makeServer } from './mirage/mirageServer';
11 | import HotelDetails from './routes/hotel-details/HotelDetails';
12 | import Login from './routes/login/Login';
13 | import Register from './routes/register/Register';
14 | import AboutUs from './routes/about-us/AboutUs';
15 | import BaseLayout from './routes/layouts/base-layout/BaseLayout';
16 | import ForgotPassword from './routes/forgot-password/ForgotPassword';
17 | import Checkout from 'routes/checkout/Checkout';
18 | import BookingConfirmation from 'routes/booking-confimation/BookingConifrmation';
19 |
20 | // if (process.env.NODE_ENV === 'development') {
21 | // makeServer();
22 | // }
23 |
24 | makeServer();
25 |
26 | const router = createBrowserRouter([
27 | {
28 | path: '/',
29 | element: ,
30 | children: [
31 | {
32 | path: '/',
33 | element: ,
34 | },
35 | {
36 | path: '/hotels',
37 | element: ,
38 | },
39 | {
40 | path: '/about-us',
41 | element: ,
42 | },
43 | {
44 | path: '/user-profile',
45 | element: ,
46 | },
47 | {
48 | path: '/login',
49 | element: ,
50 | },
51 | {
52 | path: '/register',
53 | element: ,
54 | },
55 | {
56 | path: '/hotel/:hotelId',
57 | element: ,
58 | },
59 | {
60 | path: '/forgot-password',
61 | element: ,
62 | },
63 | {
64 | path: '/checkout',
65 | element: ,
66 | },
67 | {
68 | path: '/booking-confirmation',
69 | element: ,
70 | },
71 | ],
72 | },
73 | ]);
74 |
75 | ReactDOM.createRoot(document.getElementById('root')).render(
76 |
77 |
78 |
79 | );
80 |
81 | // If you want to start measuring performance in your app, pass a function
82 | // to log results (for example: reportWebVitals(console.log))
83 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
84 | reportWebVitals();
85 |
--------------------------------------------------------------------------------
/src/components/ux/data-range-picker/DateRangePicker.jsx:
--------------------------------------------------------------------------------
1 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
2 | import React, { useRef } from 'react';
3 | import { faCalendar } from '@fortawesome/free-solid-svg-icons';
4 | import { DateRange } from 'react-date-range';
5 | import { formatDate } from 'utils/date-helpers';
6 | import useOutsideClickHandler from 'hooks/useOutsideClickHandler';
7 |
8 | const inputSyleMap = {
9 | SECONDARY: 'stay-booker__input--secondary',
10 | DARK: 'stay-booker__input--dark',
11 | };
12 |
13 | const DateRangePicker = (props) => {
14 | const {
15 | isDatePickerVisible,
16 | onDatePickerIconClick,
17 | onDateChangeHandler,
18 | dateRange,
19 | setisDatePickerVisible,
20 | inputStyle,
21 | } = props;
22 |
23 | const wrapperRef = useRef();
24 | useOutsideClickHandler(wrapperRef, () => setisDatePickerVisible(false));
25 |
26 | // Format dates for display
27 | const formattedStartDate = dateRange[0].startDate
28 | ? formatDate(dateRange[0].startDate)
29 | : 'Check-in';
30 | const formattedEndDate = dateRange[0].endDate
31 | ? formatDate(dateRange[0].endDate)
32 | : 'Check-out';
33 |
34 | return (
35 |
36 |
47 |
53 |
64 |
65 | {isDatePickerVisible && (
66 |
75 | )}
76 |
77 |
78 | );
79 | };
80 |
81 | export default DateRangePicker;
82 |
--------------------------------------------------------------------------------
/src/routes/hotel-details/components/user-reviews/components/UserRatingsSelector.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
3 | import { faStar as fasStar } from '@fortawesome/free-solid-svg-icons';
4 | import { faStar as farStar } from '@fortawesome/free-regular-svg-icons';
5 | import { AuthContext } from 'contexts/AuthContext';
6 | /**
7 | * Renders the user ratings selector component.
8 | *
9 | * @component
10 | * @param {Object} props - The component props.
11 | * @param {number} props.userRating - The user's rating.
12 | * @param {Function} props.handleRating - The function to handle rating changes made by user.
13 | * @param {boolean} props.isEmpty - The flag to determine if the user review is empty.
14 | * @param {string} props.userReview - The user's review.
15 | * @param {Function} props.handleReviewSubmit - The function to handle user review submission.
16 | * @param {Function} props.handleUserReviewChange - The function to handle user review changes.
17 | * @returns {JSX.Element} The rendered component.
18 | */
19 | const UserRatingsSelector = ({
20 | userRating,
21 | handleRating,
22 | isEmpty,
23 | userReview,
24 | handleReviewSubmit,
25 | handleUserReviewChange,
26 | }) => {
27 | const { isAuthenticated } = React.useContext(AuthContext);
28 |
29 | return isAuthenticated ? (
30 |
35 |
Your Rating
36 |
37 | {[1, 2, 3, 4, 5].map((star) => (
38 | handleRating(star)}
45 | />
46 | ))}
47 |
48 |
61 | ) : (
62 |
63 | Please login to submit your review
64 |
65 | );
66 | };
67 |
68 | export default UserRatingsSelector;
69 |
--------------------------------------------------------------------------------
/src/components/ux/dropdown-button/DropdownButton.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useRef } from 'react';
2 | import useOutsideClickHandler from 'hooks/useOutsideClickHandler';
3 |
4 | const DropdownButton = (props) => {
5 | const triggerType = props.triggerType || 'click';
6 | const color = props.color || 'bg-brand';
7 | const wrapperRef = useRef();
8 | const buttonRef = useRef();
9 | const [isDropdownContainerVisible, setIsDropdownContainerVisible] =
10 | useState(false);
11 |
12 | const onDropdownClickTrigger = () => {
13 | triggerType === 'click' &&
14 | setIsDropdownContainerVisible(!isDropdownContainerVisible);
15 | };
16 |
17 | const onDropdownItemClick = (onClikCallback) => {
18 | setIsDropdownContainerVisible(false);
19 | onClikCallback();
20 | };
21 |
22 | useOutsideClickHandler(wrapperRef, (event) => {
23 | if (!buttonRef.current.contains(event.target)) {
24 | setIsDropdownContainerVisible(false);
25 | }
26 | });
27 |
28 | return (
29 |
30 |
54 |
55 |
62 |
66 | {props.options &&
67 | props.options.map((option, index) => (
68 | -
69 |
77 |
78 | ))}
79 |
80 |
81 |
82 | );
83 | };
84 |
85 | export default DropdownButton;
86 |
--------------------------------------------------------------------------------
/src/components/vertical-filters/VerticalFilters.jsx:
--------------------------------------------------------------------------------
1 | import Checkbox from 'components/ux/checkbox/Checkbox';
2 |
3 | /**
4 | * VerticalFilters Component
5 | * Renders a vertical filter UI for filtering hotel results.
6 | *
7 | * @param {Object} props - Props for the component.
8 | * @param {Array} props.filtersData - An array of filters data objects to display.
9 | * @param {Function} props.onFiltersUpdate - Callback function to handle filter updates.
10 | * @param {Function} props.onClearFiltersAction - Callback function to handle clearing of filters.
11 | * @param {boolean} props.isVerticalFiltersOpen - Flag to control the visibility of the vertical filters.
12 | */
13 | const VerticalFilters = (props) => {
14 | const {
15 | filtersData,
16 | onFiltersUpdate,
17 | onClearFiltersAction,
18 | isVerticalFiltersOpen,
19 | } = props;
20 |
21 | const isActiveFilterSelected = () => {
22 | for (const filterGroup of filtersData) {
23 | for (const subfilter of filterGroup.filters) {
24 | if (subfilter.isSelected) {
25 | return true;
26 | }
27 | }
28 | }
29 | return false;
30 | };
31 |
32 | return (
33 |
39 |
40 |
41 | Filters
42 |
43 |
53 |
54 | {filtersData.map((filter) => (
55 |
56 |
57 | {filter.title}
58 |
59 | {filter.filters.map((subfilter) => (
60 |
68 | ))}
69 |
70 | ))}
71 |
72 | );
73 | };
74 |
75 | export default VerticalFilters;
76 |
--------------------------------------------------------------------------------
/src/routes/about-us/AboutUs.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | /**
4 | * AboutUs component
5 | * @returns {jsx}
6 | */
7 | const AboutUs = () => {
8 | return (
9 |
10 |
About Us
11 |
12 | Welcome to STAY BOOKER, where we are
13 | dedicated to providing you with the best experience for booking hotels
14 | around the world. Our mission is to make your travel comfortable,
15 | convenient, and memorable.
16 |
17 |
18 |
Our Vision
19 |
20 | At STAY BOOKER, we envision a world
21 | where every traveler finds the perfect accommodation that suits their
22 | needs and preferences. We aim to simplify the hotel booking process,
23 | offering a wide range of options for every budget.
24 |
25 |
26 |
27 | Why Choose Us?
28 |
29 |
30 | -
31 | We offer a diverse range of hotels, from luxury resorts to cozy
32 | boutique stays, ensuring that you find the perfect match for your
33 | travel style.
34 |
35 | -
36 | Our user-friendly interface makes it simple and quick to book your
37 | ideal stay. With just a few clicks, you can secure your reservation
38 | hassle-free.
39 |
40 | -
41 | Our dedicated customer support team is available 24/7 to assist you
42 | with any inquiries or issues you may encounter during your booking
43 | process or stay.
44 |
45 | -
46 | We prioritize the security of your personal information and
47 | transactions. Book with confidence, knowing that your data is safe
48 | with us.
49 |
50 |
51 |
52 |
Contact Us
53 |
54 | Have questions or need assistance? Feel free to reach out to our
55 | customer support team at{' '}
56 |
60 | info@staybooker.com
61 |
62 | . We're here to help!
63 |
64 |
65 | Thank you for choosing STAY BOOKER.
66 | We look forward to being your go-to platform for all your hotel booking
67 | needs.
68 |
69 |
70 | );
71 | };
72 |
73 | export default AboutUs;
74 |
--------------------------------------------------------------------------------
/src/routes/home/components/hero-cover/HeroCover.jsx:
--------------------------------------------------------------------------------
1 | import GlobalSearchBox from 'components/global-search-box/GlobalSearchbox';
2 |
3 | /**
4 | * HeroCover Component
5 | * Renders the hero cover section of the home page.
6 | * @param {Object} props - The component props.
7 | * @param {String} props.locationInputValue - The location input value.
8 | * @param {String} props.numGuestsInputValue - The number of guests input value.
9 | * @param {Boolean} props.isDatePickerVisible - The date picker visibility state.
10 | * @param {Function} props.onLocationChangeInput - The location input change handler.
11 | * @param {Function} props.onNumGuestsInputChange - The number of guests input change handler.
12 | * @param {Object} props.dateRange - The date range object.
13 | * @param {Function} props.onDateChangeHandler - The date change handler.
14 | * @param {Function} props.onDatePickerIconClick - The date picker icon click handler.
15 | * @param {Function} props.onSearchButtonAction - The search button click handler.
16 | * @param {Array} props.locationTypeheadResults - The location typehead results.
17 | * @param {Function} props.setisDatePickerVisible - The date picker visibility state setter.
18 | * @returns {JSX.Element} - The HeroCover component.
19 | */
20 | const HeroCover = (props) => {
21 | const {
22 | locationInputValue,
23 | numGuestsInputValue,
24 | isDatePickerVisible,
25 | onLocationChangeInput,
26 | onNumGuestsInputChange,
27 | dateRange,
28 | onDateChangeHandler,
29 | onDatePickerIconClick,
30 | onSearchButtonAction,
31 | locationTypeheadResults,
32 | setisDatePickerVisible,
33 | } = props;
34 | return (
35 |
36 |
37 | <>>
38 |
39 |
40 | Discover your perfect stay around the globe
41 |
42 |
43 | Enter your dates to see the latest prices and begin your journey of
44 | relaxation and adventure today.
45 |
46 |
47 |
60 |
61 |
62 | );
63 | };
64 |
65 | export default HeroCover;
66 |
--------------------------------------------------------------------------------
/src/components/global-search-box/GlobalSearchbox.jsx:
--------------------------------------------------------------------------------
1 | import { faLocationDot, faPerson } from '@fortawesome/free-solid-svg-icons';
2 | import DateRangePicker from 'components/ux/data-range-picker/DateRangePicker';
3 | import Input from 'components/ux/input/Input';
4 |
5 | /**
6 | * GlobalSearchBox Component
7 | * Renders a search box with input fields for location, number of guests, and a date range picker.
8 | * It includes a search button to trigger the search based on the entered criteria.
9 | *
10 | * @param {Object} props - Props for the component.
11 | * @param {string} props.locationInputValue - The current value of the location input.
12 | * @param {string} props.numGuestsInputValue - The current value of the number of guests input.
13 | * @param {boolean} props.isDatePickerVisible - Flag to control the visibility of the date picker.
14 | * @param {Function} props.onLocationChangeInput - Callback for location input changes.
15 | * @param {Function} props.onNumGuestsInputChange - Callback for number of guests input changes.
16 | * @param {Function} props.onDatePickerIconClick - Callback for the date picker icon click event.
17 | * @param {Array} props.locationTypeheadResults - Results for the location input typeahead.
18 | * @param {Function} props.onSearchButtonAction - Callback for the search button click event.
19 | * @param {Function} props.onDateChangeHandler - Callback for handling date range changes.
20 | * @param {Function} props.setisDatePickerVisible - Callback to set the visibility state of the date picker.
21 | * @param {Object} props.dateRange - The selected date range.
22 | */
23 | const GlobalSearchBox = (props) => {
24 | const {
25 | locationInputValue,
26 | numGuestsInputValue,
27 | isDatePickerVisible,
28 | onLocationChangeInput,
29 | onNumGuestsInputChange,
30 | onDatePickerIconClick,
31 | locationTypeheadResults,
32 | onSearchButtonAction,
33 | onDateChangeHandler,
34 | setisDatePickerVisible,
35 | dateRange,
36 | } = props;
37 | return (
38 |
39 |
46 |
53 |
61 |
67 |
68 | );
69 | };
70 |
71 | export default GlobalSearchBox;
72 |
--------------------------------------------------------------------------------
/src/components/navbar-items/NavbarItems.jsx:
--------------------------------------------------------------------------------
1 | import { Link, useNavigate, useLocation } from 'react-router-dom';
2 | import DropdownButton from 'components/ux/dropdown-button/DropdownButton';
3 | import { networkAdapter } from 'services/NetworkAdapter';
4 | import { useContext } from 'react';
5 | import { AuthContext } from 'contexts/AuthContext';
6 |
7 | /**
8 | * A component that renders the navigation items for the navbar for both mobile/desktop view.
9 | *
10 | * @param {Object} props - The component's props.
11 | * @param {boolean} props.isAuthenticated - A flag indicating whether the user is authenticated.
12 | * @param {Function} props.onHamburgerMenuToggle
13 | */
14 | const NavbarItems = ({ isAuthenticated, onHamburgerMenuToggle }) => {
15 | const navigate = useNavigate();
16 | const location = useLocation();
17 | const context = useContext(AuthContext);
18 |
19 | /**
20 | * Handles the logout action by calling the logout API and updating the authentication state.
21 | */
22 | const handleLogout = async () => {
23 | await networkAdapter.post('api/users/logout');
24 | context.triggerAuthCheck();
25 | navigate('/login');
26 | };
27 |
28 | const dropdownOptions = [
29 | { name: 'Profile', onClick: () => navigate('/user-profile') },
30 | { name: 'Logout', onClick: handleLogout },
31 | ];
32 |
33 | /**
34 | * Determines if a given path is the current active path.
35 | *
36 | * @param {string} path - The path to check.
37 | * @returns {boolean} - True if the path is active, false otherwise.
38 | */
39 | const isActive = (path) => {
40 | return location.pathname === path;
41 | };
42 |
43 | return (
44 | <>
45 |
46 |
53 | Home
54 |
55 |
56 |
57 |
64 | Hotels
65 |
66 |
67 |
68 |
75 | About us
76 |
77 |
78 |
81 | {isAuthenticated ? (
82 |
83 | ) : (
84 |
91 | Login/Register
92 |
93 | )}
94 |
95 | >
96 | );
97 | };
98 |
99 | export default NavbarItems;
100 |
--------------------------------------------------------------------------------
/src/components/hotel-view-card/HotelViewCard.jsx:
--------------------------------------------------------------------------------
1 | import { faStar, faCheck } from '@fortawesome/free-solid-svg-icons';
2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
3 | import { Link, useNavigate } from 'react-router-dom';
4 | import { formatPrice } from 'utils/price-helpers';
5 |
6 | /**
7 | * HotelViewCard Component
8 | * Renders a card view for a hotel, displaying its image, title, subtitle, benefits, price, and ratings.
9 | * Provides a 'Book now' button to navigate to the hotel's detailed view.
10 | *
11 | * @param {Object} props - Props for the component.
12 | * @param {string} props.id - The unique code of the hotel.
13 | * @param {Object} props.image - The image object for the hotel, containing the URL and alt text.
14 | * @param {string} props.title - The title of the hotel.
15 | * @param {string} props.subtitle - The subtitle or a short description of the hotel.
16 | * @param {Array} props.benefits - A list of benefits or features offered by the hotel.
17 | * @param {string} props.price - The price information for the hotel.
18 | * @param {number} props.ratings - The ratings of the hotel.
19 | */
20 | const HotelViewCard = (props) => {
21 | const {
22 | id: hotelCode,
23 | image,
24 | title,
25 | subtitle,
26 | benefits,
27 | price,
28 | ratings,
29 | } = props;
30 | const navigate = useNavigate();
31 | const onBookNowClick = () => {
32 | navigate(`/hotel/${hotelCode}`);
33 | };
34 |
35 | return (
36 |
40 |
41 |
45 |

50 |
51 |
52 |
53 |
54 |
58 |
{title}
59 |
60 |
{subtitle}
61 |
62 |
63 | {benefits.length > 0 &&
64 | benefits.map((benefit, index) => (
65 | -
66 | {benefit}
67 |
68 | ))}
69 |
70 |
71 |
72 |
73 |
74 | {ratings}
75 |
76 |
77 | ₹ {formatPrice(price)}
78 |
79 |
80 |
86 |
87 |
88 | );
89 | };
90 |
91 | export default HotelViewCard;
92 |
--------------------------------------------------------------------------------
/src/services/NetworkAdapter.js:
--------------------------------------------------------------------------------
1 | // Usage: import { networkAdapter } from 'path/to/NetworkAdapter.js';
2 | // Usage: const response = await networkAdapter.get('/api/hotel/123');
3 | // Usage: const response = await networkAdapter.post('/api/hotel', { name: 'Hotel Name' });
4 | class NetworkAdapter {
5 | static #API_CONFIG = {
6 | MIRAGE: window.location.origin,
7 | EXPRESS: 'http://localhost:4000',
8 | };
9 | static #API_URL = NetworkAdapter.#API_CONFIG.MIRAGE;
10 | async get(endpoint, params = {}) {
11 | const endpointURL = new URL(endpoint, NetworkAdapter.#API_URL);
12 | try {
13 | const url = new URL(endpointURL, window.location.origin);
14 |
15 | Object.entries(params).forEach(([key, value]) => {
16 | url.searchParams.append(key, value);
17 | });
18 |
19 | const response = await fetch(url.toString(), { credentials: 'include' });
20 | return await response.json();
21 | } catch (error) {
22 | return {
23 | data: {},
24 | errors: [error.message],
25 | };
26 | }
27 | }
28 |
29 | async post(endpoint, data = {}) {
30 | try {
31 | const endpointURL = new URL(endpoint, NetworkAdapter.#API_URL);
32 | const url = new URL(endpointURL, window.location.origin);
33 | const response = await fetch(url.toString(), {
34 | method: 'POST',
35 | headers: {
36 | 'Content-Type': 'application/json',
37 | },
38 | body: JSON.stringify(data),
39 | credentials: 'include',
40 | });
41 |
42 | return await response.json();
43 | } catch (error) {
44 | return {
45 | data: {},
46 | errors: [error.message],
47 | };
48 | }
49 | }
50 |
51 | async put(endpoint, data = {}) {
52 | try {
53 | const endpointURL = new URL(endpoint, NetworkAdapter.#API_URL);
54 | const url = new URL(endpointURL, window.location.origin);
55 | const response = await fetch(url.toString(), {
56 | method: 'PUT',
57 | headers: {
58 | 'Content-Type': 'application/json',
59 | },
60 | body: JSON.stringify(data),
61 | credentials: 'include',
62 | });
63 |
64 | return await response.json();
65 | } catch (error) {
66 | return {
67 | data: {},
68 | errors: [error.message],
69 | };
70 | }
71 | }
72 |
73 | async delete(endpoint) {
74 | try {
75 | const endpointURL = new URL(endpoint, NetworkAdapter.#API_URL);
76 | const url = new URL(endpointURL, window.location.origin);
77 | const response = await fetch(url.toString(), {
78 | method: 'DELETE',
79 | credentials: 'include',
80 | });
81 |
82 | return await response.json();
83 | } catch (error) {
84 | return {
85 | data: {},
86 | errors: [error.message],
87 | };
88 | }
89 | }
90 |
91 | async patch(endpoint, data = {}) {
92 | try {
93 | const endpointURL = new URL(endpoint, NetworkAdapter.#API_URL);
94 | const url = new URL(endpointURL, window.location.origin);
95 | const response = await fetch(url.toString(), {
96 | method: 'PATCH',
97 | headers: {
98 | 'Content-Type': 'application/json',
99 | },
100 | body: JSON.stringify(data),
101 | credentials: 'include',
102 | });
103 |
104 | return await response.json();
105 | } catch (error) {
106 | return {
107 | data: {},
108 | errors: [error.message],
109 | };
110 | }
111 | }
112 | }
113 |
114 | export const networkAdapter = new NetworkAdapter();
115 |
--------------------------------------------------------------------------------
/src/routes/booking-confimation/BookingConifrmation.jsx:
--------------------------------------------------------------------------------
1 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
2 | import { faStar } from '@fortawesome/free-solid-svg-icons';
3 | import { useEffect, useState } from 'react';
4 | import { useLocation, useNavigate } from 'react-router-dom';
5 | import { Link } from 'react-router-dom';
6 | import { useReactToPrint } from 'react-to-print';
7 | import { useRef } from 'react';
8 |
9 | /**
10 | * Represents the booking confirmation component.
11 | * @component
12 | * @returns {JSX.Element} The booking confirmation component.
13 | */
14 | const BookingConfirmation = () => {
15 | const contentToPrint = useRef(null);
16 | const location = useLocation();
17 | const navigate = useNavigate();
18 |
19 | const [bookingDetails, setBookingDetails] = useState(null);
20 |
21 | /**
22 | * Handles the print event.
23 | * @function
24 | * @returns {void}
25 | */
26 | const handlePrint = useReactToPrint({
27 | documentTitle: 'Booking Confirmation',
28 | removeAfterPrint: true,
29 | });
30 |
31 | // Set booking details from location state passed from the previous page(checkout page)
32 | useEffect(() => {
33 | if (location.state) {
34 | const { bookingDetails } = location.state.confirmationData;
35 | setBookingDetails(bookingDetails);
36 | } else {
37 | navigate('/');
38 | }
39 | }, [bookingDetails, location.state, navigate]);
40 |
41 | return (
42 |
43 |
44 |
48 | Back to home
49 |
50 |
58 |
59 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
Booking Confirmed
71 |
72 | Thank you for your booking! Your reservation has been confirmed.
73 |
74 |
75 | Please check your email for the booking details and instructions for
76 | your stay.
77 |
78 |
79 | {bookingDetails &&
80 | bookingDetails.map((detail, index) => (
81 |
82 |
{detail.label}
83 |
84 | {detail.value}
85 |
86 |
87 | ))}
88 |
89 |
90 |
91 | );
92 | };
93 | export default BookingConfirmation;
94 |
--------------------------------------------------------------------------------
/src/routes/user-profile/components/BookingPanel.jsx:
--------------------------------------------------------------------------------
1 | const BookingPanel = ({ bookings }) => {
2 | return (
3 |
4 |
5 | {bookings.map((booking, index) => (
6 | -
7 |
8 |
9 |
10 | {booking.hotelName}
11 |
12 |
13 |
14 | Booking ID: {booking.bookingId}
15 |
16 |
17 |
18 |
19 |
20 |
21 |
34 | Booking Date: {booking.bookingDate}
35 |
36 |
37 |
50 | Check-in: {booking.checkInDate}
51 |
52 |
53 |
66 | Check-out: {booking.checkOutDate}
67 |
68 |
69 |
70 |
71 | Total Fare: {' '}
72 | {booking.totalFare}
73 |
74 |
75 |
76 |
77 |
78 | ))}
79 |
80 |
81 | );
82 | };
83 |
84 | export default BookingPanel;
85 |
--------------------------------------------------------------------------------
/src/routes/hotel-details/components/hotel-details-view-card/HotelDetailsViewCard.jsx:
--------------------------------------------------------------------------------
1 | import HotelBookingDetailsCard from '../hotel-booking-details-card/HotelBookingDetailsCard';
2 | import UserReviews from '../user-reviews/UserReviews';
3 | import { networkAdapter } from 'services/NetworkAdapter';
4 | import React, { useEffect, useState } from 'react';
5 | import ReactImageGallery from 'react-image-gallery';
6 |
7 | const HotelDetailsViewCard = ({ hotelDetails }) => {
8 | const images = hotelDetails.images.map((image) => ({
9 | original: image.imageUrl,
10 | thumbnail: image.imageUrl,
11 | thumbnailClass: 'h-[80px]',
12 | thumbnailLoading: 'lazy',
13 | }));
14 |
15 | const [reviewData, setReviewData] = useState({
16 | isLoading: true,
17 | data: [],
18 | });
19 | const [currentReviewsPage, setCurrentReviewPage] = useState(1);
20 |
21 | const handlePageChange = (page) => {
22 | setCurrentReviewPage(page);
23 | };
24 |
25 | const handlePreviousPageChange = () => {
26 | setCurrentReviewPage((prev) => {
27 | if (prev <= 1) return prev;
28 | return prev - 1;
29 | });
30 | };
31 |
32 | const handleNextPageChange = () => {
33 | setCurrentReviewPage((prev) => {
34 | if (prev >= reviewData.pagination.totalPages) return prev;
35 | return prev + 1;
36 | });
37 | };
38 |
39 | useEffect(() => {
40 | setReviewData({
41 | isLoading: true,
42 | data: [],
43 | });
44 | const fetchHotelReviews = async () => {
45 | const response = await networkAdapter.get(
46 | `/api/hotel/${hotelDetails.hotelCode}/reviews`,
47 | {
48 | currentPage: currentReviewsPage,
49 | }
50 | );
51 | if (response && response.data) {
52 | setReviewData({
53 | isLoading: false,
54 | data: response.data.elements,
55 | metadata: response.metadata,
56 | pagination: response.paging,
57 | });
58 | }
59 | };
60 | fetchHotelReviews();
61 | }, [hotelDetails.hotelCode, currentReviewsPage]);
62 |
63 | return (
64 |
65 |
66 |
67 |
68 |
73 | {hotelDetails.discount && (
74 |
75 | {hotelDetails.discount} OFF
76 |
77 | )}
78 |
79 |
80 |
81 | {hotelDetails.title}
82 |
83 |
84 | {hotelDetails.subtitle}
85 |
86 |
87 | {hotelDetails.description.map((line, index) => (
88 |
89 | {line}
90 |
91 | ))}
92 |
93 |
94 |
95 |
96 | {hotelDetails.benefits.join(' | ')}
97 |
98 |
99 |
100 |
101 |
102 |
108 |
109 |
110 |
111 | );
112 | };
113 |
114 | export default HotelDetailsViewCard;
115 |
--------------------------------------------------------------------------------
/src/components/ux/pagination-controller/PaginationController.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | /**
4 | * PaginationController component for paginating search results.
5 | * @param {Object} props - The component props.
6 | * @param {number} props.currentPage - The current page number.
7 | * @param {number} props.totalPages - The total number of pages.
8 | * @param {Function} props.handlePageChange - The function to handle page change.
9 | * @param {Function} props.handlePreviousPageChange - The function to handle previous page change.
10 | * @param {Function} props.handleNextPageChange - The function to handle next page change.
11 | *
12 | * @returns {JSX.Element} The rendered component.
13 | *
14 | * @example
15 | * return (
16 | *
23 | * );
24 | *
25 | */
26 | const PaginationController = (props) => {
27 | const {
28 | currentPage,
29 | totalPages,
30 | handlePageChange,
31 | handlePreviousPageChange,
32 | handleNextPageChange,
33 | } = props;
34 |
35 | const isNextDisabled = currentPage >= totalPages ? true : false;
36 |
37 | const isPreviousDisabled = currentPage <= 1 ? true : false;
38 |
39 | return (
40 |
41 |
111 |
112 | );
113 | };
114 |
115 | export default PaginationController;
116 |
--------------------------------------------------------------------------------
/src/routes/user-profile/UserProfile.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from 'react';
2 | import Tabs from 'components/ux/tabs/Tabs';
3 | import TabPanel from 'components/ux/tab-panel/TabPanel';
4 | import {
5 | faAddressCard,
6 | faHotel,
7 | faCreditCard,
8 | } from '@fortawesome/free-solid-svg-icons';
9 | import { AuthContext } from 'contexts/AuthContext';
10 | import { networkAdapter } from 'services/NetworkAdapter';
11 | import { useContext } from 'react';
12 | import PaymentMethodsPanel from './components/PaymentsMethodsPanel';
13 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
14 | import { faBars } from '@fortawesome/free-solid-svg-icons';
15 | import { faXmark } from '@fortawesome/free-solid-svg-icons';
16 | import useOutsideClickHandler from 'hooks/useOutsideClickHandler';
17 | import { useNavigate } from 'react-router-dom';
18 | import BookingPanel from './components/BookingPanel';
19 | import ProfileDetailsPanel from './components/ProfileDetailsPanel';
20 |
21 | /**
22 | * UserProfile
23 | * Renders the user profile page with tabs for personal details, bookings, and payment methods.
24 | * @returns {JSX.Element} - The UserProfile component
25 | * */
26 | const UserProfile = () => {
27 | const { userDetails } = useContext(AuthContext);
28 | const navigate = useNavigate();
29 |
30 | const wrapperRef = useRef();
31 | const buttonRef = useRef();
32 |
33 | const [isTabsVisible, setIsTabsVisible] = useState(false);
34 |
35 | // Fetch user bookings data
36 | const [userBookingsData, setUserBookingsData] = useState({
37 | isLoading: true,
38 | data: [],
39 | errors: [],
40 | });
41 |
42 | // Fetch user payment methods data
43 | const [userPaymentMethodsData, setUserPaymentMethodsData] = useState({
44 | isLoading: true,
45 | data: [],
46 | errors: [],
47 | });
48 |
49 | useOutsideClickHandler(wrapperRef, (event) => {
50 | if (!buttonRef.current.contains(event.target)) {
51 | setIsTabsVisible(false);
52 | }
53 | });
54 |
55 | const onTabsMenuButtonAction = () => {
56 | setIsTabsVisible(!isTabsVisible);
57 | };
58 |
59 | // effect to set initial state of user details
60 | useEffect(() => {
61 | if (!userDetails) {
62 | navigate('/login');
63 | }
64 | }, [navigate, userDetails]);
65 |
66 | // effect to set initial state of user bookings data
67 | useEffect(() => {
68 | const getInitialData = async () => {
69 | const userBookingsDataResponse = await networkAdapter.get(
70 | '/api/users/bookings'
71 | );
72 | const userPaymentMethodsResponse = await networkAdapter.get(
73 | 'api/users/payment-methods'
74 | );
75 | if (userBookingsDataResponse && userBookingsDataResponse.data) {
76 | setUserBookingsData({
77 | isLoading: false,
78 | data: userBookingsDataResponse.data.elements,
79 | errors: userBookingsDataResponse.errors,
80 | });
81 | }
82 | if (userPaymentMethodsResponse && userPaymentMethodsResponse.data) {
83 | setUserPaymentMethodsData({
84 | isLoading: false,
85 | data: userPaymentMethodsResponse.data.elements,
86 | errors: userPaymentMethodsResponse.errors,
87 | });
88 | }
89 | };
90 | getInitialData();
91 | }, []);
92 |
93 | return (
94 | <>
95 |
96 |
97 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
120 |
121 |
122 |
123 | >
124 | );
125 | };
126 |
127 | export default UserProfile;
128 |
--------------------------------------------------------------------------------
/src/routes/hotel-details/components/user-reviews/UserReviews.jsx:
--------------------------------------------------------------------------------
1 | import Review from './components/Review';
2 | import React, { useState } from 'react';
3 | import RatingsOverview from './components/RatingsOverview';
4 | import UserRatingsSelector from './components/UserRatingsSelector';
5 | import { networkAdapter } from 'services/NetworkAdapter';
6 | import Toast from 'components/ux/toast/Toast';
7 | import PaginationController from 'components/ux/pagination-controller/PaginationController';
8 | import Loader from 'components/ux/loader/loader';
9 |
10 | /**
11 | * Renders the user reviews component.
12 | *
13 | * @component
14 | * @param {Object} reviewData - The review data object.
15 | * @returns {JSX.Element} The user reviews component.
16 | */
17 | const UserReviews = ({
18 | reviewData,
19 | handlePageChange,
20 | handlePreviousPageChange,
21 | handleNextPageChange,
22 | }) => {
23 | const [userRating, setUserRating] = useState(0);
24 |
25 | const [userReview, setUserReview] = useState('');
26 |
27 | const [shouldHideUserRatingsSelector, setShouldHideUserRatingsSelector] =
28 | useState(false);
29 |
30 | const [toastMessage, setToastMessage] = useState('');
31 |
32 | /**
33 | * Handles the selected user rating.
34 | * @param {number} rate - The rating value.
35 | */
36 | const handleRating = (rate) => {
37 | setUserRating(rate);
38 | };
39 |
40 | const clearToastMessage = () => {
41 | setToastMessage('');
42 | };
43 |
44 | const handleReviewSubmit = async () => {
45 | if (userRating === 0) {
46 | setToastMessage({
47 | type: 'error',
48 | message: 'Please select a rating before submitting.',
49 | });
50 | return;
51 | }
52 | // TODO: Add validation for userRating and userReview
53 | const response = await networkAdapter.put('/api/hotel/add-review', {
54 | rating: userRating,
55 | review: userReview,
56 | });
57 | if (response && response.errors.length === 0 && response.data.status) {
58 | setToastMessage({
59 | type: 'success',
60 | message: response.data.status,
61 | });
62 | } else {
63 | setToastMessage({
64 | type: 'error',
65 | message: 'Review submission failed',
66 | });
67 | }
68 | setShouldHideUserRatingsSelector(true);
69 | };
70 |
71 | const handleUserReviewChange = (review) => {
72 | setUserReview(review);
73 | };
74 |
75 | const isEmpty = reviewData.data.length === 0;
76 |
77 | return (
78 |
79 |
User Reviews
80 |
81 | {reviewData.data.length === 0 ? (
82 |
83 |
84 | Be the first to leave a review!
85 |
86 |
87 | ) : (
88 |
93 | )}
94 | {shouldHideUserRatingsSelector ? null : (
95 |
103 | )}
104 |
105 | {toastMessage && (
106 |
111 | )}
112 |
113 | {reviewData.isLoading ? (
114 |
115 | ) : (
116 |
117 | {reviewData.data.map((review, index) => (
118 |
126 | ))}
127 |
128 | )}
129 |
130 | {reviewData.data.length > 0 && (
131 |
138 | )}
139 |
140 | );
141 | };
142 |
143 | export default UserReviews;
144 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Stay Booker Pro
2 |
3 | Stay Booker Pro is a production-ready hotel booking website built with modern web technologies. It is designed to be a fully functional and responsive web application for hotel booking services. For the backend api checkout: [staybooker-express-api](https://github.com/iZooGooD/stay-booker-hotel-booking-express-api)
4 |
5 | ## Key Features
6 |
7 | - **Production-Ready**: Crafted with production requirements in mind.
8 | - **Modern Tech Stack**: Built using React, Tailwind CSS, MirageJS for mocking APIs, and Cypress for end-to-end testing.
9 | - **Skeleton Loading**: Implements skeleton screens for an enhanced user experience during data loading.
10 | - **Responsive Design**: Fully responsive interface built purely with Tailwind CSS.
11 | - **Comprehensive Test Coverage**: Extensive test cases using Cypress to cover every functionality, ensuring robust and reliable code.
12 | - **Future Backend Integration**: Planned integration with a backend built using Express.js.
13 |
14 | ## Getting Started
15 |
16 | These instructions will get you a copy of the project up and running on your local machine for development and testing purposes.
17 |
18 | ### Prerequisites
19 |
20 | - Node.js
21 | - npm or yarn
22 |
23 | ### Installing
24 |
25 | 1. Clone the repository:
26 |
27 | ```bash
28 | git clone https://github.com/iZooGooD/stay-booker-pro.git
29 | ```
30 |
31 | 2. Navigate to the project directory:
32 |
33 | ```bash
34 | cd stay-booker-pro
35 | ```
36 |
37 | 3. Install dependencies:
38 |
39 | ```bash
40 | npm install
41 | # or
42 | yarn install
43 | ```
44 |
45 | 4. Start the development server:
46 |
47 | ```bash
48 | npm start
49 | # or
50 | yarn start
51 | ```
52 |
53 | The application should now be running on [http://localhost:3000](http://localhost:3000).
54 |
55 | ## Running the Tests
56 |
57 | To ensure the reliability and stability of the application, comprehensive test suites have been written using Cypress.
58 |
59 | To run the tests:
60 |
61 | ```bash
62 | npm test
63 | # or
64 | npx cypress open
65 | ```
66 |
67 | This command will open the Cypress test runner, where you can execute specific tests or the entire test suite.
68 |
69 | ## Code Quality and Workflow
70 |
71 | ### Husky for Pre-Commit Hooks
72 |
73 | Stay Booker Pro uses Husky to manage pre-commit hooks, ensuring that code quality and formatting standards are maintained. Before each commit, Husky runs various checks to make sure that the committed code adheres to defined standards.
74 |
75 | ### GitHub Workflow
76 |
77 | The project is equipped with a GitHub Actions workflow to automate the testing, building, and code quality checks. The workflow consists of three primary jobs:
78 |
79 | 1. **Build**: Ensures that the application builds correctly on each push and pull request to the `master` branch.
80 |
81 | 2. **Code Quality - Prettier**: Checks code formatting using Prettier. This step helps maintain a consistent coding style and format across the project.
82 |
83 | 3. **Run Tests**: Executes the test suites to ensure all tests pass. This step is crucial for identifying issues early and maintaining the reliability of the application.
84 |
85 | ### Continuous Integration and Code Quality
86 |
87 | This automated workflow ensures that each change to the codebase is built, tested, and checked for code quality, thereby maintaining the overall health and reliability of the application. It encourages a culture of continuous integration and frequent, reliable delivery of high-quality software.
88 |
89 | It would be most appropriate to include the linting instructions in the "Contributing" section of your documentation. This approach helps to ensure that contributors are aware of the coding standards and practices expected for your project right from the start. By integrating linting guidelines with contribution instructions, you emphasize the importance of code quality as an integral part of the contribution process.
90 |
91 | Here's how you can seamlessly incorporate it into the "Contributing" section:
92 |
93 | ---
94 |
95 | ## Contributing
96 |
97 | We welcome contributions to Stay Booker Pro! If you have suggestions or would like to contribute code, please feel free to create issues or submit pull requests.
98 |
99 | ### Code Quality and Linting
100 |
101 | As part of our commitment to maintain high code quality, we use ESLint for linting. Before submitting a Pull Request or committing any changes, please ensure you run the following command:
102 |
103 | ```bash
104 | npm run lint-fix
105 | ```
106 |
107 | This will automatically fix many common linting errors. If there are errors that can't be auto-fixed, ESLint will report them, and you should manually address these issues. Maintaining a consistent coding standard is crucial for the project.
108 |
109 | If you need to bypass the linting check in a special case, you can use the `-n` parameter with `git commit`. However, we strongly advise against skipping lint checks as it can compromise code quality:
110 |
111 | ```bash
112 | git commit -m "Your commit message" -n
113 | ```
114 |
115 | ## Future Scope
116 |
117 | - Backend integration with Express.js for a complete full-stack experience.
118 | - Additional features and improvements to the booking process.
119 |
120 |
--------------------------------------------------------------------------------
/src/routes/user-profile/components/PaymentsMethodsPanel.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 |
3 | /* PaymentMethodsPanel
4 | * Renders a list of payment methods with the ability to edit and save changes.
5 | * @param {Array} paymentMethods - An array of payment methods.
6 | * @param {Function} setPaymentMethods - A function to update the payment methods.
7 | * @returns {JSX.Element} - The PaymentMethodsPanel component.
8 | */
9 | const PaymentMethodsPanel = ({
10 | userPaymentMethodsData,
11 | setUserPaymentMethodsData,
12 | }) => {
13 | const [editIndex, setEditIndex] = useState(-1); // -1 means no edit is active
14 | const [currentEdit, setCurrentEdit] = useState({});
15 |
16 | const handleEdit = (index) => {
17 | setEditIndex(index);
18 | setCurrentEdit({ ...userPaymentMethodsData.data[index] });
19 | };
20 |
21 | const handleCancel = () => {
22 | setEditIndex(-1);
23 | };
24 |
25 | const handleSave = () => {
26 | const updatedPaymentMethods = [...userPaymentMethodsData.data];
27 | updatedPaymentMethods[editIndex] = currentEdit;
28 | setUserPaymentMethodsData({ data: updatedPaymentMethods });
29 | setEditIndex(-1);
30 | };
31 |
32 | const handleChange = (e, field) => {
33 | setCurrentEdit({ ...currentEdit, [field]: e.target.value });
34 | };
35 |
36 | return (
37 |
38 | {userPaymentMethodsData.data?.length === 0 ? (
39 |
40 | You have no saved payment methods.
41 |
42 | ) : (
43 |
114 | )}
115 |
116 | );
117 | };
118 |
119 | export default PaymentMethodsPanel;
120 |
--------------------------------------------------------------------------------
/src/routes/forgot-password/ForgotPassword.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { Link } from 'react-router-dom';
3 | import { networkAdapter } from 'services/NetworkAdapter';
4 | import validations from 'utils/validations';
5 | import Toast from 'components/ux/toast/Toast';
6 |
7 | /**
8 | * ForgotPassword component responsible for handling the forgot password form.
9 | * @returns {jsx}
10 | */
11 | const ForgotPassword = () => {
12 | const [success, setsuccess] = useState(false);
13 | const [loginData, setLoginData] = useState({
14 | email: '',
15 | });
16 | const [errorMessage, setErrorMessage] = useState(false);
17 | /**
18 | * Handles input changes for the login form fields.
19 | * Updates the loginData state with the field values.
20 | * @param {Object} e - The event object from the input field.
21 | */
22 | const handleInputChange = (e) => {
23 | setLoginData({ ...loginData, [e.target.name]: e.target.value });
24 | };
25 | const dismissError = () => {
26 | setErrorMessage('');
27 | };
28 | /**
29 | * Handles the submission of the login form.
30 | * Attempts to authenticate the user with the provided credentials.
31 | * Navigates to the user profile on successful login or sets an error message on failure.
32 | * @param {Object} e - The event object from the form submission.
33 | */
34 | const handleforgotsubmit = async (e) => {
35 | e.preventDefault();
36 |
37 | if (validations.validate('email', loginData.email)) {
38 | const response = await networkAdapter.post('/api/forgot', loginData);
39 | if (response) {
40 | setsuccess(true);
41 | } else {
42 | setErrorMessage('Invalid email.');
43 | }
44 | } else {
45 | setErrorMessage('Invalid email.');
46 | }
47 | };
48 | return (
49 | <>
50 |
51 |
52 | {success ? (
53 |
54 |
63 |
64 |
65 | Recovery Email has been sent!
66 |
67 |
68 | {' '}
69 | Don't forgot to check you spam{' '}
70 |
71 |
72 |
76 | GO BACK
77 |
78 |
79 |
80 |
81 | ) : (
82 |
130 | )}
131 |
132 |
133 | >
134 | );
135 | };
136 |
137 | export default ForgotPassword;
138 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our
6 | community a harassment-free experience for everyone, regardless of age, body
7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
8 | identity and expression, level of experience, education, socio-economic status,
9 | nationality, personal appearance, race, religion, or sexual identity
10 | and orientation.
11 |
12 | We pledge to act and interact in ways that contribute to an open, welcoming,
13 | diverse, inclusive, and healthy community.
14 |
15 | ## Our Standards
16 |
17 | Examples of behavior that contributes to a positive environment for our
18 | community include:
19 |
20 | * Demonstrating empathy and kindness toward other people
21 | * Being respectful of differing opinions, viewpoints, and experiences
22 | * Giving and gracefully accepting constructive feedback
23 | * Accepting responsibility and apologizing to those affected by our mistakes,
24 | and learning from the experience
25 | * Focusing on what is best not just for us as individuals, but for the
26 | overall community
27 |
28 | Examples of unacceptable behavior include:
29 |
30 | * The use of sexualized language or imagery, and sexual attention or
31 | advances of any kind
32 | * Trolling, insulting or derogatory comments, and personal or political attacks
33 | * Public or private harassment
34 | * Publishing others' private information, such as a physical or email
35 | address, without their explicit permission
36 | * Other conduct which could reasonably be considered inappropriate in a
37 | professional setting
38 |
39 | ## Enforcement Responsibilities
40 |
41 | Community leaders are responsible for clarifying and enforcing our standards of
42 | acceptable behavior and will take appropriate and fair corrective action in
43 | response to any behavior that they deem inappropriate, threatening, offensive,
44 | or harmful.
45 |
46 | Community leaders have the right and responsibility to remove, edit, or reject
47 | comments, commits, code, wiki edits, issues, and other contributions that are
48 | not aligned to this Code of Conduct, and will communicate reasons for moderation
49 | decisions when appropriate.
50 |
51 | ## Scope
52 |
53 | This Code of Conduct applies within all community spaces, and also applies when
54 | an individual is officially representing the community in public spaces.
55 | Examples of representing our community include using an official e-mail address,
56 | posting via an official social media account, or acting as an appointed
57 | representative at an online or offline event.
58 |
59 | ## Enforcement
60 |
61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
62 | reported to the community leaders responsible for enforcement at
63 | .
64 | All complaints will be reviewed and investigated promptly and fairly.
65 |
66 | All community leaders are obligated to respect the privacy and security of the
67 | reporter of any incident.
68 |
69 | ## Enforcement Guidelines
70 |
71 | Community leaders will follow these Community Impact Guidelines in determining
72 | the consequences for any action they deem in violation of this Code of Conduct:
73 |
74 | ### 1. Correction
75 |
76 | **Community Impact**: Use of inappropriate language or other behavior deemed
77 | unprofessional or unwelcome in the community.
78 |
79 | **Consequence**: A private, written warning from community leaders, providing
80 | clarity around the nature of the violation and an explanation of why the
81 | behavior was inappropriate. A public apology may be requested.
82 |
83 | ### 2. Warning
84 |
85 | **Community Impact**: A violation through a single incident or series
86 | of actions.
87 |
88 | **Consequence**: A warning with consequences for continued behavior. No
89 | interaction with the people involved, including unsolicited interaction with
90 | those enforcing the Code of Conduct, for a specified period of time. This
91 | includes avoiding interactions in community spaces as well as external channels
92 | like social media. Violating these terms may lead to a temporary or
93 | permanent ban.
94 |
95 | ### 3. Temporary Ban
96 |
97 | **Community Impact**: A serious violation of community standards, including
98 | sustained inappropriate behavior.
99 |
100 | **Consequence**: A temporary ban from any sort of interaction or public
101 | communication with the community for a specified period of time. No public or
102 | private interaction with the people involved, including unsolicited interaction
103 | with those enforcing the Code of Conduct, is allowed during this period.
104 | Violating these terms may lead to a permanent ban.
105 |
106 | ### 4. Permanent Ban
107 |
108 | **Community Impact**: Demonstrating a pattern of violation of community
109 | standards, including sustained inappropriate behavior, harassment of an
110 | individual, or aggression toward or disparagement of classes of individuals.
111 |
112 | **Consequence**: A permanent ban from any sort of public interaction within
113 | the community.
114 |
115 | ## Attribution
116 |
117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118 | version 2.0, available at
119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
120 |
121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct
122 | enforcement ladder](https://github.com/mozilla/diversity).
123 |
124 | [homepage]: https://www.contributor-covenant.org
125 |
126 | For answers to common questions about this code of conduct, see the FAQ at
127 | https://www.contributor-covenant.org/faq. Translations are available at
128 | https://www.contributor-covenant.org/translations.
129 |
--------------------------------------------------------------------------------
/src/components/results-container/ResultsContainer.jsx:
--------------------------------------------------------------------------------
1 | import HotelViewCard from 'components/hotel-view-card/HotelViewCard';
2 | import VerticalFilters from 'components/vertical-filters/VerticalFilters';
3 | import HotelViewCardSkeleton from 'components/hotel-view-card-skeleton/HotelViewCardSkeleton';
4 | import VerticalFiltersSkeleton from 'components/vertical-filters-skeleton/VerticalFiltersSkeleton';
5 | import EmptyHotelsState from 'components/empty-hotels-state/EmptyHotelsState';
6 | import { useRef, useState } from 'react';
7 | import useOutsideClickHandler from 'hooks/useOutsideClickHandler';
8 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
9 | import { faFilter } from '@fortawesome/free-solid-svg-icons';
10 | import Select from 'react-select';
11 |
12 | /**
13 | * ResultsContainer Component
14 | * Renders a container that displays hotel results, including hotel cards and filters.
15 | * It supports toggling of vertical filters and displays skeletons or empty states based on loading or data availability.
16 | *
17 | * @param {Object} props - Props for the component.
18 | * @param {Object} props.hotelsResults - Object containing hotel results data and loading state.
19 | * @param {boolean} props.enableFilters - Flag to enable or disable the filter feature.
20 | * @param {Array} props.filtersData - Array of filter data objects for the vertical filters.
21 | * @param {Array} props.selectedFiltersState - Array of selected filter states.
22 | * @param {Function} props.onFiltersUpdate - Callback function to handle filter updates.
23 | * @param {Function} props.onClearFiltersAction - Callback function to handle the action of clearing filters.
24 | * @param {Array} props.sortingFilterOptions - Array of sorting filter options.
25 | * @param {Object} props.sortByFilterValue - Object containing the selected sorting filter value.
26 | * @param {Function} props.onSortingFilterChange - Callback function to handle sorting filter changes.
27 | */
28 | const ResultsContainer = (props) => {
29 | const {
30 | hotelsResults,
31 | enableFilters,
32 | filtersData,
33 | selectedFiltersState,
34 | onFiltersUpdate,
35 | onClearFiltersAction,
36 | sortingFilterOptions,
37 | sortByFilterValue,
38 | onSortingFilterChange,
39 | } = props;
40 |
41 | // Check if sorting filter is visible
42 | const isSortingFilterVisible =
43 | sortingFilterOptions && sortingFilterOptions.length > 0;
44 |
45 | const [isVerticalFiltersOpen, setIsVerticalFiltersOpen] = useState(false);
46 |
47 | const wrapperRef = useRef();
48 | const buttonRef = useRef();
49 |
50 | useOutsideClickHandler(wrapperRef, (event) => {
51 | if (!buttonRef.current.contains(event.target)) {
52 | setIsVerticalFiltersOpen(false);
53 | }
54 | });
55 |
56 | const toggleVerticalFiltersAction = () => {
57 | // Toggle based on the current state
58 | setIsVerticalFiltersOpen((prevState) => !prevState);
59 | };
60 |
61 | return (
62 |
63 |
64 | {enableFilters && selectedFiltersState.length > 0 && (
65 |
66 |
72 |
73 | )}
74 | {enableFilters && filtersData.isLoading &&
}
75 |
76 |
77 | {enableFilters && (
78 |
79 |
88 |
89 | )}
90 | {isSortingFilterVisible && (
91 |
97 | )}
98 |
99 |
100 | {hotelsResults.isLoading ? (
101 | Array.from({ length: 5 }, (_, index) => (
102 |
103 | ))
104 | ) : hotelsResults.data.length > 0 ? (
105 | hotelsResults.data.map((hotel) => (
106 |
116 | ))
117 | ) : (
118 |
119 | )}
120 |
121 |
122 |
123 |
124 | );
125 | };
126 |
127 | export default ResultsContainer;
128 |
--------------------------------------------------------------------------------
/src/routes/home/Home.jsx:
--------------------------------------------------------------------------------
1 | import HeroCover from './components/hero-cover/HeroCover';
2 | import PopularLocations from './components/popular-locations/popular-locations';
3 | import { networkAdapter } from 'services/NetworkAdapter';
4 | import { useState, useEffect, useCallback } from 'react';
5 | import { MAX_GUESTS_INPUT_VALUE } from 'utils/constants';
6 | import ResultsContainer from 'components/results-container/ResultsContainer';
7 | import { formatDate } from 'utils/date-helpers';
8 | import { useNavigate } from 'react-router-dom';
9 | import _debounce from 'lodash/debounce';
10 |
11 | /**
12 | * Home component that renders the main page of the application.
13 | * It includes a navigation bar, hero cover, popular locations, results container, and footer.
14 | */
15 | const Home = () => {
16 | const navigate = useNavigate();
17 |
18 | // State variables
19 | const [isDatePickerVisible, setisDatePickerVisible] = useState(false);
20 | const [locationInputValue, setLocationInputValue] = useState('pune');
21 | const [numGuestsInputValue, setNumGuestsInputValue] = useState('');
22 | const [popularDestinationsData, setPopularDestinationsData] = useState({
23 | isLoading: true,
24 | data: [],
25 | errors: [],
26 | });
27 | const [hotelsResults, setHotelsResults] = useState({
28 | isLoading: true,
29 | data: [],
30 | errors: [],
31 | });
32 |
33 | // State for storing available cities
34 | const [availableCities, setAvailableCities] = useState([]);
35 |
36 | const [filteredTypeheadResults, setFilteredTypeheadResults] = useState([]);
37 |
38 | // eslint-disable-next-line react-hooks/exhaustive-deps
39 | const debounceFn = useCallback(_debounce(queryResults, 1000), []);
40 |
41 | const [dateRange, setDateRange] = useState([
42 | {
43 | startDate: null,
44 | endDate: null,
45 | key: 'selection',
46 | },
47 | ]);
48 |
49 | const onDatePickerIconClick = () => {
50 | setisDatePickerVisible(!isDatePickerVisible);
51 | };
52 |
53 | const onLocationChangeInput = async (newValue) => {
54 | setLocationInputValue(newValue);
55 | // Debounce the queryResults function to avoid making too many requests
56 | debounceFn(newValue, availableCities);
57 | };
58 |
59 | /**
60 | * Queries the available cities based on the user's input.
61 | * @param {string} query - The user's input.
62 | * @returns {void}
63 | *
64 | */
65 | function queryResults(query, availableCities) {
66 | const filteredResults = availableCities.filter((city) =>
67 | city.toLowerCase().includes(query.toLowerCase())
68 | );
69 | setFilteredTypeheadResults(filteredResults);
70 | }
71 |
72 | const onNumGuestsInputChange = (numGuests) => {
73 | if (
74 | (numGuests < MAX_GUESTS_INPUT_VALUE && numGuests > 0) ||
75 | numGuests === ''
76 | ) {
77 | setNumGuestsInputValue(numGuests);
78 | }
79 | };
80 |
81 | const onDateChangeHandler = (ranges) => {
82 | setDateRange([ranges.selection]);
83 | };
84 |
85 | /**
86 | * Handles the click event of the search button.
87 | * It gathers the number of guests, check-in and check-out dates, and selected city
88 | * from the component's state, and then navigates to the '/hotels' route with this data.
89 | */
90 | const onSearchButtonAction = () => {
91 | const numGuest = Number(numGuestsInputValue);
92 | const checkInDate = formatDate(dateRange[0].startDate) ?? '';
93 | const checkOutDate = formatDate(dateRange[0].endDate) ?? '';
94 | const city = locationInputValue;
95 | navigate('/hotels', {
96 | state: {
97 | numGuest,
98 | checkInDate,
99 | checkOutDate,
100 | city,
101 | },
102 | });
103 | };
104 |
105 | useEffect(() => {
106 | /**
107 | * Fetches initial data for the Home route.
108 | * @returns {Promise} A promise that resolves when the data is fetched.
109 | */
110 | const getInitialData = async () => {
111 | const popularDestinationsResponse = await networkAdapter.get(
112 | '/api/popularDestinations'
113 | );
114 | const hotelsResultsResponse =
115 | await networkAdapter.get('/api/nearbyHotels');
116 |
117 | const availableCitiesResponse = await networkAdapter.get(
118 | '/api/availableCities'
119 | );
120 | if (availableCitiesResponse) {
121 | setAvailableCities(availableCitiesResponse.data.elements);
122 | }
123 |
124 | if (popularDestinationsResponse) {
125 | setPopularDestinationsData({
126 | isLoading: false,
127 | data: popularDestinationsResponse.data.elements,
128 | errors: popularDestinationsResponse.errors,
129 | });
130 | }
131 | if (hotelsResultsResponse) {
132 | setHotelsResults({
133 | isLoading: false,
134 | data: hotelsResultsResponse.data.elements,
135 | errors: hotelsResultsResponse.errors,
136 | });
137 | }
138 | };
139 | getInitialData();
140 | }, []);
141 |
142 | return (
143 | <>
144 |
157 |
158 |
159 |
160 |
161 | Handpicked nearby hotels for you
162 |
163 |
167 |
168 |
169 | >
170 | );
171 | };
172 |
173 | export default Home;
174 |
--------------------------------------------------------------------------------
/src/routes/login/Login.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { Link } from 'react-router-dom';
3 | import { networkAdapter } from 'services/NetworkAdapter';
4 | import React, { useContext } from 'react';
5 | import { AuthContext } from 'contexts/AuthContext';
6 | import { useNavigate } from 'react-router-dom';
7 | import validations from 'utils/validations';
8 | import Toast from 'components/ux/toast/Toast';
9 | import { LOGIN_MESSAGES } from 'utils/constants';
10 |
11 | /**
12 | * Login Component
13 | * Renders a login form allowing users to sign in to their account.
14 | * It handles user input for email and password, submits login credentials to the server,
15 | * and navigates the user to their profile upon successful authentication.
16 | * Displays an error message for invalid login attempts.
17 | */
18 | const Login = () => {
19 | const navigate = useNavigate();
20 | const context = useContext(AuthContext);
21 | const [loginData, setLoginData] = useState({
22 | email: '',
23 | password: '',
24 | });
25 |
26 | const [errorMessage, setErrorMessage] = useState(false);
27 |
28 | /**
29 | * Handles input changes for the login form fields.
30 | * Updates the loginData state with the field values.
31 | * @param {Object} e - The event object from the input field.
32 | */
33 | const handleInputChange = (e) => {
34 | setLoginData({ ...loginData, [e.target.name]: e.target.value });
35 | };
36 |
37 | /**
38 | * Handles the submission of the login form.
39 | * Attempts to authenticate the user with the provided credentials.
40 | * Navigates to the user profile on successful login or sets an error message on failure.
41 | * @param {Object} e - The event object from the form submission.
42 | */
43 | const handleLoginSubmit = async (e) => {
44 | e.preventDefault();
45 |
46 | if (validations.validate('email', loginData.email)) {
47 | const response = await networkAdapter.post('api/users/login', loginData);
48 | if (response && response.data.token) {
49 | context.triggerAuthCheck();
50 | navigate('/user-profile');
51 | } else if (response && response.errors.length > 0) {
52 | setErrorMessage(response.errors[0]);
53 | }
54 | } else {
55 | setErrorMessage(LOGIN_MESSAGES.FAILED);
56 | }
57 | };
58 |
59 | /**
60 | * Clears the current error message displayed to the user.
61 | */
62 | const dismissError = () => {
63 | setErrorMessage('');
64 | };
65 |
66 | return (
67 | <>
68 |
148 |
149 | test user details
150 | Email: user1@example.com
151 | Password: password1
152 |
153 | >
154 | );
155 | };
156 |
157 | export default Login;
158 |
--------------------------------------------------------------------------------
/src/routes/register/Register.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { Link } from 'react-router-dom';
3 | import { networkAdapter } from 'services/NetworkAdapter';
4 | import { useNavigate } from 'react-router-dom';
5 | import Toast from 'components/ux/toast/Toast';
6 | import { REGISTRATION_MESSAGES } from 'utils/constants';
7 | import { Formik, Form, Field } from 'formik';
8 | import Schemas from 'utils/validation-schemas';
9 |
10 | /**
11 | * Register Component
12 | * Renders a registration form that allows new users to create an account.
13 | * It captures user input for personal information and credentials, submitting these to a registration endpoint.
14 | * Upon successful registration, the user is notified and redirected to the login page.
15 | */
16 | const Register = () => {
17 | const navigate = useNavigate();
18 | const [toastMessage, setToastMessage] = useState('');
19 | const [toastType, setToastType] = useState('success');
20 | const [showToast, setShowToast] = useState(false);
21 |
22 | /**
23 | * Submits the registration form data to the server.
24 | * It performs an asynchronous operation to post the form data to a registration endpoint.
25 | * If registration is successful, a success message is displayed, and the user is redirected to the login page after a brief delay.
26 | * Otherwise, the user is informed of the failure.
27 | *
28 | * @param {Object} e - The event object generated by the form submission.
29 | */
30 | const handleSubmit = async (values) => {
31 | const response = await networkAdapter.put('/api/users/register', values);
32 | console.log('response', response);
33 | if (response && response.errors && response.errors.length < 1) {
34 | setToastMessage(REGISTRATION_MESSAGES.SUCCESS);
35 | setShowToast(true);
36 | setTimeout(() => navigate('/login'), 2000);
37 | } else {
38 | setToastType('error');
39 | setToastMessage(response.errors[0]);
40 | setShowToast(true);
41 | }
42 | };
43 |
44 | return (
45 | <>
46 |
47 |
48 |
handleSubmit(values)}
59 | >
60 | {({ errors, touched }) => (
61 |
144 | )}
145 |
146 |
147 |
148 | >
149 | );
150 | };
151 |
152 | export default Register;
153 |
--------------------------------------------------------------------------------
/src/routes/hotel-details/components/hotel-booking-details-card/HotelBookingDetailsCard.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import Select from 'react-select';
3 | import { differenceInCalendarDays } from 'date-fns';
4 | import DateRangePicker from 'components/ux/data-range-picker/DateRangePicker';
5 | import { networkAdapter } from 'services/NetworkAdapter';
6 | import { DEFAULT_TAX_DETAILS } from 'utils/constants';
7 | import { useNavigate } from 'react-router-dom';
8 | import queryString from 'query-string';
9 | import { formatPrice } from 'utils/price-helpers';
10 | import Toast from 'components/ux/toast/Toast';
11 | import format from 'date-fns/format';
12 |
13 | /**
14 | * A component that displays the booking details for a hotel, including date range, room type, and pricing.
15 | *
16 | * @param {Object} props - The component's props.
17 | * @param {string} props.hotelCode - The unique code for the hotel.
18 | */
19 | const HotelBookingDetailsCard = ({ hotelCode }) => {
20 | // State for date picker visibility
21 | const [isDatePickerVisible, setisDatePickerVisible] = useState(false);
22 |
23 | const navigate = useNavigate();
24 |
25 | // State for error message
26 | const [errorMessage, setErrorMessage] = useState('');
27 |
28 | // State for date range
29 | const [dateRange, setDateRange] = useState([
30 | {
31 | startDate: new Date(),
32 | endDate: null,
33 | key: 'selection',
34 | },
35 | ]);
36 |
37 | // State for selected room, guests, and rooms
38 | const [selectedRoom, setSelectedRoom] = useState({
39 | value: '1 King Bed Standard Non Smoking',
40 | label: '1 King Bed Standard Non Smoking',
41 | });
42 | const [selectedGuests, setSelectedGuests] = useState({
43 | value: 2,
44 | label: '2 guests',
45 | });
46 | const [selectedRooms, setSelectedRooms] = useState({
47 | value: 1,
48 | label: '1 room',
49 | });
50 |
51 | // State for pricing and booking details
52 | const [total, setTotal] = useState(0);
53 | const [taxes, setTaxes] = useState(0);
54 | const [bookingPeriodDays, setBookingPeriodDays] = useState(1);
55 | const [bookingDetails, setBookingDetails] = useState({});
56 |
57 | // Options for guests and rooms
58 | const guestOptions = Array.from(
59 | { length: bookingDetails.maxGuestsAllowed },
60 | (_, i) => ({ value: i + 1, label: `${i + 1} guest` })
61 | );
62 | const roomNumberOptions = Array.from(
63 | { length: bookingDetails.maxRoomsAllowedPerGuest },
64 | (_, i) => ({ value: i + 1, label: `${i + 1} room` })
65 | );
66 | const roomOptions = [
67 | {
68 | value: '1 King Bed Standard Non Smoking',
69 | label: '1 King Bed Standard Non Smoking',
70 | },
71 | ];
72 |
73 | // Handlers for select changes
74 | const handleRoomTypeChange = (selectedOption) => {
75 | setSelectedRoom(selectedOption);
76 | calculatePrices();
77 | };
78 | const handleGuestsNumberChange = (selectedOption) => {
79 | setSelectedGuests(selectedOption);
80 | };
81 | const handleRoomsNumberChange = (selectedOption) => {
82 | setSelectedRooms(selectedOption);
83 | calculatePrices();
84 | };
85 |
86 | // Handler for date picker visibility toggle
87 | const onDatePickerIconClick = () => {
88 | setisDatePickerVisible(!isDatePickerVisible);
89 | };
90 |
91 | /**
92 | * Handler for date range changes. Updates the booking period days and recalculates prices.
93 | *
94 | * @param {Object} ranges - The selected date ranges.
95 | */
96 | const onDateChangeHandler = (ranges) => {
97 | const { startDate, endDate } = ranges.selection;
98 | setDateRange([ranges.selection]);
99 | const days =
100 | startDate && endDate
101 | ? differenceInCalendarDays(endDate, startDate) + 1
102 | : 1;
103 | setBookingPeriodDays(days);
104 | calculatePrices();
105 | };
106 |
107 | /**
108 | * Calculates the total price and taxes based on the selected room and booking period.
109 | */
110 | const calculatePrices = () => {
111 | const pricePerNight = bookingDetails.currentNightRate * selectedRooms.value;
112 | const gstRate =
113 | pricePerNight <= 2500 ? 0.12 : pricePerNight > 7500 ? 0.18 : 0.12;
114 | const totalGst = (pricePerNight * bookingPeriodDays * gstRate).toFixed(2);
115 | const totalPrice = (
116 | pricePerNight * bookingPeriodDays +
117 | parseFloat(totalGst)
118 | ).toFixed(2);
119 | if (!isNaN(totalPrice)) {
120 | setTotal(`${formatPrice(totalPrice)} INR`);
121 | }
122 | setTaxes(`${formatPrice(totalGst)} INR`);
123 | };
124 |
125 | const onBookingConfirm = () => {
126 | if (!dateRange[0].startDate || !dateRange[0].endDate) {
127 | setErrorMessage('Please select check-in and check-out dates.');
128 | return;
129 | }
130 | const checkIn = format(dateRange[0].startDate, 'dd-MM-yyyy');
131 | const checkOut = format(dateRange[0].endDate, 'dd-MM-yyyy');
132 | const queryParams = {
133 | hotelCode,
134 | checkIn,
135 | checkOut,
136 | guests: selectedGuests.value,
137 | rooms: selectedRooms.value,
138 | hotelName: bookingDetails.name.replaceAll(' ', '-'), // url friendly hotel name
139 | };
140 |
141 | const url = `/checkout?${queryString.stringify(queryParams)}`;
142 | navigate(url, {
143 | state: {
144 | total,
145 | checkInTime: bookingDetails.checkInTime,
146 | checkOutTime: bookingDetails.checkOutTime,
147 | },
148 | });
149 | };
150 |
151 | // Handler for dismissing error message
152 | const dismissError = () => {
153 | setErrorMessage('');
154 | };
155 |
156 | // Effect for initial price calculation
157 | useEffect(() => {
158 | calculatePrices();
159 | // eslint-disable-next-line react-hooks/exhaustive-deps
160 | }, [bookingPeriodDays, selectedRooms, selectedRoom, bookingDetails]);
161 |
162 | // Effect for fetching booking details
163 | useEffect(() => {
164 | const getBookingDetails = async () => {
165 | const response = await networkAdapter.get(
166 | `api/hotel/${hotelCode}/booking/enquiry`
167 | );
168 | if (response && response.data) {
169 | setBookingDetails(response.data);
170 | }
171 | };
172 | getBookingDetails();
173 | }, [hotelCode]);
174 |
175 | return (
176 |
177 |
178 |
Booking Details
179 |
180 |
181 | {/* Total Price */}
182 |
183 |
184 | Total Price
185 |
186 |
{total}
187 |
188 | {bookingDetails.cancellationPolicy}
189 |
190 |
191 |
192 | {/* Dates & Time */}
193 |
194 |
Dates & Time
195 |
196 |
204 |
205 |
206 |
207 | {/* Reservation */}
208 |
209 |
Reservation
210 |
216 |
221 |
222 |
223 | {/* Room Type */}
224 |
225 |
Room Type
226 |
231 |
232 |
233 | {/* Per day rate */}
234 |
235 |
Per day rate
236 |
237 | {formatPrice(bookingDetails.currentNightRate)} INR
238 |
239 |
240 |
241 | {/* Taxes */}
242 |
243 |
Taxes
244 |
{taxes}
245 |
{DEFAULT_TAX_DETAILS}
246 |
247 |
248 | {errorMessage && (
249 |
254 | )}
255 |
256 |
257 |
263 |
264 |
265 | );
266 | };
267 |
268 | export default HotelBookingDetailsCard;
269 |
--------------------------------------------------------------------------------
/src/routes/user-profile/components/ProfileDetailsPanel.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import Toast from 'components/ux/toast/Toast';
3 | import { networkAdapter } from 'services/NetworkAdapter';
4 | import Select from 'react-select';
5 |
6 | /**
7 | * Renders the user profile details panel.
8 | * @component
9 | * @param {Object} props - The component props.
10 | * @param {Object} props.userDetails - The user's details.
11 | * @returns {JSX.Element} The rendered component.
12 | * */
13 | const ProfileDetailsPanel = ({ userDetails }) => {
14 | // states to manage the edit mode and user details
15 | const [isEditMode, setIsEditMode] = useState(false);
16 | const [firstName, setFirstName] = useState('');
17 | const [lastName, setLastName] = useState('');
18 | const [email, setEmail] = useState('');
19 | const [phoneNumber, setPhoneNumber] = useState('');
20 | const [dateOfBirth, setDateOfBirth] = useState('');
21 | const [isEmailVerified, setIsEmailVerified] = useState(false);
22 | const [isPhoneVerified, setIsPhoneVerified] = useState(false);
23 | const [nationality, setNationality] = useState('');
24 | const [countries, setCountries] = useState([]);
25 |
26 | const [toastMessage, setToastMessage] = useState('');
27 |
28 | const clearToastMessage = () => {
29 | setToastMessage('');
30 | };
31 |
32 | const handleEditClick = () => {
33 | setIsEditMode(!isEditMode);
34 | };
35 |
36 | const handleCancelClick = () => {
37 | setIsEditMode(!isEditMode);
38 | };
39 |
40 | /**
41 | * Handles the save button click event.
42 | * Updates the user details and sets the edit mode to false.
43 | * */
44 | const handleSaveClick = async () => {
45 | // check if newstate is different from old state
46 | if (
47 | firstName === userDetails.firstName &&
48 | lastName === userDetails.lastName &&
49 | phoneNumber === userDetails.phone &&
50 | nationality === userDetails.country
51 | ) {
52 | setIsEditMode(false);
53 | return;
54 | }
55 |
56 | const updatedUserDetails = {
57 | firstName,
58 | lastName,
59 | phoneNumber,
60 | country: nationality,
61 | };
62 | // Call the API to update the user details
63 | const response = await networkAdapter.patch(
64 | '/api/users/update-profile',
65 | updatedUserDetails
66 | );
67 | if (response && response.data.status) {
68 | setToastMessage({
69 | type: 'success',
70 | message: response.data.status,
71 | });
72 | } else {
73 | // revert to original state
74 | setFirstName(userDetails.firstName);
75 | setLastName(userDetails.lastName);
76 | setPhoneNumber(userDetails.phone);
77 | setNationality(userDetails.country);
78 | setToastMessage({
79 | type: 'error',
80 | message: 'Oops, something went wrong. Please try again later.',
81 | });
82 | }
83 |
84 | setIsEditMode(false);
85 | };
86 |
87 | // effect to set initial state of user details
88 | useEffect(() => {
89 | if (userDetails) {
90 | setFirstName(userDetails.firstName || '');
91 | setLastName(userDetails.lastName || '');
92 | setEmail(userDetails.email || '');
93 | setPhoneNumber(userDetails.phone || '');
94 | setNationality(userDetails.country || '');
95 | setIsEmailVerified(userDetails.isEmailVerified || '');
96 | setIsPhoneVerified(userDetails.isPhoneVerified || '');
97 | setDateOfBirth(userDetails.dateOfBirth || '');
98 | }
99 | }, [userDetails]);
100 |
101 | useEffect(() => {
102 | const fetchCountries = async () => {
103 | const countriesData = await networkAdapter.get('/api/misc/countries');
104 | if (countriesData && countriesData.data) {
105 | console.log('countriesData', countriesData.data);
106 | const mappedValues = countriesData.data.elements.map((country) => ({
107 | label: country.name,
108 | value: country.name,
109 | }));
110 | setCountries(mappedValues);
111 | }
112 | };
113 | fetchCountries();
114 | }, []);
115 |
116 | return (
117 |
118 |
119 |
120 | Personal details
121 |
122 |
123 | Keep your details current to ensure seamless communication and
124 | services
125 |
126 |
127 |
128 |
129 | {isEditMode ? (
130 | // Editable fields
131 | <>
132 |
137 |
142 |
148 |
154 |
155 |
162 |
163 | >
164 | ) : (
165 | // Display fields
166 | <>
167 |
168 |
169 |
174 |
179 |
183 |
184 | >
185 | )}
186 |
187 |
188 |
189 | {isEditMode ? (
190 | <>
191 |
197 |
203 | >
204 | ) : (
205 |
211 | )}
212 |
213 | {toastMessage && (
214 |
215 |
220 |
221 | )}
222 |
223 | );
224 | };
225 |
226 | const DisplayField = ({ label, value, verified }) => (
227 |
232 |
{label}
233 |
234 | {value}{' '}
235 | {verified && Verified}
236 |
237 |
238 | );
239 |
240 | const TextField = ({
241 | label,
242 | value,
243 | onChange,
244 | type = 'text',
245 | isSelectable,
246 | selectableData,
247 | }) => (
248 |
249 |
{label}
250 |
251 | {isSelectable ? (
252 |
266 |
267 | );
268 |
269 | export default ProfileDetailsPanel;
270 |
--------------------------------------------------------------------------------
/src/mirage/data/hotels.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "hotelCode": 71222,
4 | "images": [
5 | {
6 | "imageUrl": "/images/hotels/481481762/481481762.jpg",
7 | "accessibleText": "hyatt pune hotel"
8 | },
9 | {
10 | "imageUrl": "/images/hotels/481481762/525626081.jpg",
11 | "accessibleText": "hyatt pune hotel"
12 | },
13 | {
14 | "imageUrl": "/images/hotels/481481762/525626095.jpg",
15 | "accessibleText": "hyatt pune hotel"
16 | },
17 | {
18 | "imageUrl": "/images/hotels/481481762/525626104.jpg",
19 | "accessibleText": "hyatt pune hotel"
20 | },
21 | {
22 | "imageUrl": "/images/hotels/481481762/525626212.jpg",
23 | "accessibleText": "hyatt pune hotel"
24 | }
25 | ],
26 | "title": "Hyatt Pune",
27 | "subtitle": "Kalyani Nagar, Pune | 3.3 kms from city center",
28 | "benefits": [
29 | "Free cancellation",
30 | "No prepayment needed – pay at the property"
31 | ],
32 | "price": "18900",
33 | "ratings": "5",
34 | "city": "pune",
35 | "reviews": {
36 | "data": [
37 | {
38 | "reviewerName": "Rahul Patel",
39 | "rating": 5,
40 | "review": "The hotel is very good and the staff is very friendly. The food is also very good.",
41 | "date": "Date of stay: 2021-01-01",
42 | "verified": true
43 | },
44 | {
45 | "reviewerName": "Sara Johnson",
46 | "rating": 4,
47 | "review": "Great hotel with excellent service. The rooms are spacious and clean. The staff went above and beyond to ensure a comfortable stay. Highly recommended!",
48 | "date": "Date of stay: 2021-02-15",
49 | "verified": false
50 | },
51 | {
52 | "reviewerName": "John Smith",
53 | "rating": 3,
54 | "review": "Average hotel. The staff could be more attentive.",
55 | "date": "Date of stay: 2021-03-10",
56 | "verified": true
57 | },
58 | {
59 | "reviewerName": "Emily Davis",
60 | "rating": 5,
61 | "review": "Amazing experience! The hotel exceeded my expectations.",
62 | "date": "Date of stay: 2021-04-20",
63 | "verified": false
64 | },
65 | {
66 | "reviewerName": "David Wilson",
67 | "rating": 1,
68 | "review": "Terrible experience. The hotel was dirty and the staff was rude.",
69 | "date": "Date of stay: 2021-05-05",
70 | "verified": true
71 | },
72 | {
73 | "reviewerName": "Jessica Thompson",
74 | "rating": 4,
75 | "review": "Lovely hotel with a great location. The staff was friendly and helpful.",
76 | "date": "Date of stay: 2021-06-12",
77 | "verified": false
78 | },
79 | {
80 | "reviewerName": "Michael Brown",
81 | "rating": 2,
82 | "review": "Disappointing stay. The room was not clean and the service was slow.",
83 | "date": "Date of stay: 2021-07-20",
84 | "verified": true
85 | },
86 | {
87 | "reviewerName": "Sophia Lee",
88 | "rating": 5,
89 | "review": "Exceptional service and beautiful rooms. The staff was incredibly friendly and attentive. The amenities provided were top-notch. Overall, a truly memorable experience!",
90 | "date": "Date of stay: 2021-08-05",
91 | "verified": false
92 | },
93 | {
94 | "reviewerName": "Daniel Johnson",
95 | "rating": 3,
96 | "review": "Decent hotel with average facilities. The staff was polite and helpful. However, the room could have been cleaner. It was an okay stay overall.",
97 | "date": "Date of stay: 2021-09-10",
98 | "verified": true
99 | },
100 | {
101 | "reviewerName": "Olivia Wilson",
102 | "rating": 4,
103 | "review": "Enjoyed my stay at the hotel. The room was comfortable and the staff was friendly.",
104 | "date": "Date of stay: 2021-10-15",
105 | "verified": false
106 | },
107 | {
108 | "reviewerName": "Ethan Davis",
109 | "rating": 4,
110 | "review": "Fantastic hotel with great amenities. The staff was attentive and helpful.",
111 | "date": "Date of stay: 2021-11-20",
112 | "verified": true
113 | },
114 | {
115 | "reviewerName": "Ava Smith",
116 | "rating": 2,
117 | "review": "Not satisfied with the hotel. The room was small and the service was poor.",
118 | "date": "Date of stay: 2021-12-05",
119 | "verified": false
120 | },
121 | {
122 | "reviewerName": "Mia Johnson",
123 | "rating": 4,
124 | "review": "Had a pleasant stay at the hotel. The location was convenient and the staff was friendly.",
125 | "date": "Date of stay: 2022-01-10",
126 | "verified": true
127 | },
128 | {
129 | "reviewerName": "Noah Wilson",
130 | "rating": 3,
131 | "review": "Average hotel with decent facilities. The staff was helpful.",
132 | "date": "Date of stay: 2022-02-15",
133 | "verified": false
134 | },
135 | {
136 | "reviewerName": "Liam Davis",
137 | "rating": 4,
138 | "review": "Outstanding hotel with top-notch service. The rooms were luxurious and comfortable.",
139 | "date": "Date of stay: 2022-03-20",
140 | "verified": true
141 | }
142 | ]
143 | }
144 | },
145 | {
146 | "hotelCode": 71223,
147 | "images": [
148 | {
149 | "imageUrl": "/images/hotels/465660377/465660377.jpg",
150 | "accessibleText": "Courtyard by Marriott Pune"
151 | }
152 | ],
153 | "title": "Courtyard by Marriott Pune Hinjewadi",
154 | "subtitle": "500 meters from the Rajiv Gandhi Infotech Park",
155 | "benefits": [
156 | "Free cancellation",
157 | "No prepayment needed – pay at the property",
158 | "Free wifi",
159 | "Free lunch"
160 | ],
161 | "price": "25300",
162 | "ratings": "4",
163 | "city": "pune"
164 | },
165 | {
166 | "hotelCode": 71224,
167 | "images": [
168 | {
169 | "imageUrl": "/images/hotels/469186143/469186143.jpg",
170 | "accessibleText": "The Westin Pune Koregaon Park"
171 | }
172 | ],
173 | "title": "The Westin Pune Koregaon Park",
174 | "subtitle": "5.4 km from centre",
175 | "benefits": [
176 | "Free cancellation",
177 | "No prepayment needed – pay at the property",
178 | "Free wifi"
179 | ],
180 | "price": "11300",
181 | "ratings": "5",
182 | "city": "pune"
183 | },
184 | {
185 | "hotelCode": 71225,
186 | "images": [
187 | {
188 | "imageUrl": "/images/hotels/252004905/252004905.jpg",
189 | "accessibleText": "Novotel Pune Viman Nagar Road"
190 | }
191 | ],
192 | "title": "Novotel Pune Viman Nagar Road",
193 | "subtitle": "Weikfield IT City Infopark | 7.1 km from centre",
194 | "benefits": [
195 | "Pets allowed",
196 | "Dinner + Lunch included",
197 | "Free wifi",
198 | "Free taxi from airport"
199 | ],
200 | "price": "14599",
201 | "ratings": "3",
202 | "city": "pune"
203 | },
204 | {
205 | "hotelCode": 71226,
206 | "images": [
207 | {
208 | "imageUrl": "/images/hotels/54360345/54360345.jpg",
209 | "accessibleText": "Vivanta Pune"
210 | }
211 | ],
212 | "title": "Vivanta Pune",
213 | "subtitle": "Xion Complex, | 14.2 km from centre",
214 | "benefits": [
215 | "Pets allowed",
216 | "Free wifi",
217 | "Free cancellation",
218 | "No prepayment needed – pay at the property"
219 | ],
220 | "price": "9799",
221 | "ratings": "4.3",
222 | "city": "pune"
223 | },
224 | {
225 | "hotelCode": 81223,
226 | "images": [
227 | {
228 | "imageUrl": "/images/hotels/13800549/13800549.jpg",
229 | "accessibleText": "Taj Lands End"
230 | }
231 | ],
232 | "title": "Taj Lands End",
233 | "subtitle": "200 metres from the seafrontBandstand",
234 | "benefits": [
235 | "Daily housekeeping",
236 | "Safety deposit box",
237 | "Free wifi",
238 | "Outdoor swimming pool"
239 | ],
240 | "price": "34100",
241 | "ratings": "4.2",
242 | "city": "mumbai"
243 | },
244 | {
245 | "hotelCode": 81224,
246 | "images": [
247 | {
248 | "imageUrl": "/images/hotels/32810889/32810889.jpg",
249 | "accessibleText": "Trident Nariman Point"
250 | }
251 | ],
252 | "title": "Trident Nariman Point",
253 | "subtitle": "24 km from Chhatrapati Shivaji International Airport",
254 | "benefits": [
255 | "Airport shuttle",
256 | "Spa and wellness centre",
257 | "Free wifi",
258 | "Tea/coffee maker in all rooms",
259 | "Fitness centre"
260 | ],
261 | "price": "38460",
262 | "ratings": "3.9",
263 | "city": "mumbai"
264 | },
265 | {
266 | "hotelCode": 81225,
267 | "images": [
268 | {
269 | "imageUrl": "/images/hotels/503567645/503567645.jpg",
270 | "accessibleText": "Aurika - Luxury by Lemon Tree Hotels"
271 | }
272 | ],
273 | "title": "Aurika - Luxury by Lemon Tree Hotels",
274 | "subtitle": "5.1 km from Phoenix Market City Mall, Aurika",
275 | "benefits": [
276 | "Outdoor swimming pool",
277 | "Spa and wellness centre",
278 | "Free wifi",
279 | "Free parking",
280 | "Bar"
281 | ],
282 | "price": "17460",
283 | "ratings": "4.9",
284 | "city": "mumbai"
285 | },
286 | {
287 | "hotelCode": 81226,
288 | "images": [
289 | {
290 | "imageUrl": "/images/hotels/472036509/472036509.jpg",
291 | "accessibleText": "Four Seasons Hotel Mumbai"
292 | }
293 | ],
294 | "title": "Four Seasons Hotel Mumbai",
295 | "subtitle": "In the heart of Worli, the business hub of India’s largest city",
296 | "benefits": [
297 | "Kitchen",
298 | "Pets allowed",
299 | "Free wifi",
300 | "Free parking",
301 | "24-hour front desk"
302 | ],
303 | "price": "12460",
304 | "ratings": "3.5",
305 | "city": "mumbai"
306 | },
307 | {
308 | "hotelCode": 81227,
309 | "images": [
310 | {
311 | "imageUrl": "/images/hotels/516847915/516847915.jpg",
312 | "accessibleText": "Treebo Trend New Light Suites"
313 | }
314 | ],
315 | "title": "Treebo Trend New Light Suites",
316 | "subtitle": "6.2 km from The Heritage Centre & Aerospace Museum",
317 | "benefits": [
318 | "Non-smoking rooms",
319 | "Laundry",
320 | "Free wifi",
321 | "Tea/coffee maker in all rooms"
322 | ],
323 | "price": "5460",
324 | "ratings": "3.5",
325 | "city": "bangalore"
326 | },
327 | {
328 | "hotelCode": 81228,
329 | "images": [
330 | {
331 | "imageUrl": "/images/hotels/513923904/513923904.jpg",
332 | "accessibleText": "Renaissance Bengaluru Race Course Hotel"
333 | }
334 | ],
335 | "title": "Renaissance Bengaluru Race Course Hotel",
336 | "subtitle": "Located in the heart of Bengaluru Renaissance Bengaluru Race Course Hotel",
337 | "benefits": [
338 | "Outdoor swimming poo",
339 | "Spa and wellness centre",
340 | "Airport shuttle",
341 | "Bar"
342 | ],
343 | "price": "21460",
344 | "ratings": "5",
345 | "city": "bangalore"
346 | }
347 | ]
348 |
--------------------------------------------------------------------------------