├── setupTests.js
├── src
├── pages
│ ├── Home
│ │ ├── index.js
│ │ ├── components
│ │ │ ├── HeroSection
│ │ │ │ ├── index.js
│ │ │ │ ├── HeroSection.jsx
│ │ │ │ └── style.module.css
│ │ │ ├── FeaturedDeals
│ │ │ │ ├── index.js
│ │ │ │ ├── SkeletonDealCard.jsx
│ │ │ │ ├── DealCard.jsx
│ │ │ │ ├── style.module.css
│ │ │ │ └── FeaturedDeals.jsx
│ │ │ ├── TrendingDestinations
│ │ │ │ ├── index.js
│ │ │ │ ├── TrendingDestinationsWithLoading.jsx
│ │ │ │ ├── TrendingDestinationsSection.jsx
│ │ │ │ ├── TrendingDestinations.jsx
│ │ │ │ └── TrendingDestinationsWithoutLoading.jsx
│ │ │ └── RecentlyVisitedHotels
│ │ │ │ ├── index.js
│ │ │ │ ├── RecentlyVisitedHotelsWithLoading.jsx
│ │ │ │ ├── RecentlyVisitedHotelsSection.jsx
│ │ │ │ ├── RecentlyVisitedHotels.jsx
│ │ │ │ └── RecentlyVisitedHotelsWithoutLoading.jsx
│ │ ├── style.module.css
│ │ └── Home.jsx
│ ├── Hotel
│ │ ├── index.js
│ │ ├── components
│ │ │ ├── AvailableRooms
│ │ │ │ ├── index.js
│ │ │ │ ├── style.module.css
│ │ │ │ └── AvailableRooms.jsx
│ │ │ ├── HotelAmenities
│ │ │ │ ├── index.js
│ │ │ │ ├── style.module.css
│ │ │ │ └── HotelAmenities.jsx
│ │ │ ├── HotelDetails
│ │ │ │ ├── index.js
│ │ │ │ └── HotelDetails.jsx
│ │ │ ├── HotelMapLocation
│ │ │ │ ├── index.js
│ │ │ │ ├── style.module.css
│ │ │ │ └── HotelMapLocation.jsx
│ │ │ ├── HotelReviews
│ │ │ │ ├── index.js
│ │ │ │ ├── style.module.css
│ │ │ │ └── HotelReviews.jsx
│ │ │ ├── VisualGallery
│ │ │ │ ├── index.js
│ │ │ │ ├── VisualGallery.jsx
│ │ │ │ └── style.module.css
│ │ │ └── HotelHeaderAndDescription
│ │ │ │ ├── index.js
│ │ │ │ ├── style.module.css
│ │ │ │ └── HotelHeaderAndDescription.jsx
│ │ ├── HotelWithLoading.jsx
│ │ ├── style.module.css
│ │ ├── Hotel.jsx
│ │ ├── HotelWithoutLoading.jsx
│ │ └── hooks
│ │ │ └── useHotelData.js
│ ├── Login
│ │ ├── index.js
│ │ ├── style.module.css
│ │ └── Login.jsx
│ ├── Checkout
│ │ ├── index.js
│ │ ├── components
│ │ │ ├── CartItems
│ │ │ │ ├── index.js
│ │ │ │ └── CartItems.jsx
│ │ │ ├── CustomTextField
│ │ │ │ ├── index.js
│ │ │ │ ├── style.module.css
│ │ │ │ └── CustomTextField.jsx
│ │ │ └── FormInformation
│ │ │ │ ├── index.js
│ │ │ │ ├── style.module.css
│ │ │ │ ├── paymentSchema.js
│ │ │ │ └── FormInformation.jsx
│ │ └── Checkout.jsx
│ ├── SearchPage
│ │ ├── index.js
│ │ ├── components
│ │ │ ├── DateCheck
│ │ │ │ ├── index.js
│ │ │ │ ├── style.module.css
│ │ │ │ └── DateCheck.jsx
│ │ │ ├── SearchBar
│ │ │ │ ├── index.js
│ │ │ │ ├── styles.jsx
│ │ │ │ └── style.module.css
│ │ │ ├── OptionItem
│ │ │ │ ├── index.js
│ │ │ │ ├── style.module.css
│ │ │ │ └── OptionItem.jsx
│ │ │ ├── SearchFilters
│ │ │ │ ├── index.js
│ │ │ │ └── style.module.css
│ │ │ ├── SearchResult
│ │ │ │ ├── index.js
│ │ │ │ ├── style.module.css
│ │ │ │ └── SearchResult.jsx
│ │ │ ├── SearchResultItem
│ │ │ │ ├── index.js
│ │ │ │ └── SearchResultItem.jsx
│ │ │ └── SearchItemContainer
│ │ │ │ ├── index.js
│ │ │ │ ├── style.module.css
│ │ │ │ └── SearchItemContainer.jsx
│ │ ├── style.module.css
│ │ └── SearchPage.jsx
│ ├── Admin
│ │ ├── pages
│ │ │ ├── Cities
│ │ │ │ ├── index.js
│ │ │ │ ├── components
│ │ │ │ │ ├── UpdateCityForm
│ │ │ │ │ │ ├── index.js
│ │ │ │ │ │ └── UpdateCityForm.jsx
│ │ │ │ │ └── CreateCityDialog
│ │ │ │ │ │ ├── index.js
│ │ │ │ │ │ └── CreateCityDialog.jsx
│ │ │ │ ├── cityConfig.js
│ │ │ │ └── Cities.jsx
│ │ │ ├── Hotels
│ │ │ │ ├── index.js
│ │ │ │ ├── components
│ │ │ │ │ ├── UpdateHotelForm
│ │ │ │ │ │ ├── index.js
│ │ │ │ │ │ └── UpdateHotelForm.jsx
│ │ │ │ │ └── CreateHotelDialog
│ │ │ │ │ │ ├── index.js
│ │ │ │ │ │ └── CreateHotelDialog.jsx
│ │ │ │ ├── hotelConfig.js
│ │ │ │ └── Hotels.jsx
│ │ │ └── Rooms
│ │ │ │ ├── index.js
│ │ │ │ ├── components
│ │ │ │ ├── SearchBar
│ │ │ │ │ ├── index.js
│ │ │ │ │ └── SearchBar.jsx
│ │ │ │ ├── UpdateRoomForm
│ │ │ │ │ ├── index.js
│ │ │ │ │ └── UpdateRoomForm.jsx
│ │ │ │ ├── CreateRoomDialog
│ │ │ │ │ ├── index.js
│ │ │ │ │ └── CreateRoomDialog.jsx
│ │ │ │ └── RoomsDetailedGrid
│ │ │ │ │ ├── index.js
│ │ │ │ │ └── RoomsDetailedGrid.jsx
│ │ │ │ ├── roomConfig.js
│ │ │ │ └── Rooms.jsx
│ │ ├── components
│ │ │ ├── SearchBar
│ │ │ │ ├── index.js
│ │ │ │ ├── style.module.css
│ │ │ │ └── SearchBar.jsx
│ │ │ ├── CreateButton
│ │ │ │ ├── index.js
│ │ │ │ └── CreateButton.jsx
│ │ │ ├── DetailedGrid
│ │ │ │ ├── index.js
│ │ │ │ ├── DetailedGridWithLoading.jsx
│ │ │ │ └── DetailedGrid.jsx
│ │ │ ├── LeftNavigation
│ │ │ │ ├── index.js
│ │ │ │ └── LeftNavigation.jsx
│ │ │ ├── UpdateButton
│ │ │ │ ├── index.js
│ │ │ │ └── UpdateButton.jsx
│ │ │ ├── UpdateEntityForm
│ │ │ │ ├── index.js
│ │ │ │ └── UpdateEntityForm.jsx
│ │ │ └── CreateEntityDialog
│ │ │ │ ├── index.js
│ │ │ │ └── CreateEntityDialog.jsx
│ │ └── hooks
│ │ │ └── useDialogState.js
│ ├── Confirmation
│ │ ├── index.js
│ │ ├── components
│ │ │ └── ConfirmationTable
│ │ │ │ ├── index.js
│ │ │ │ ├── style.module.css
│ │ │ │ └── ConfirmationTable.jsx
│ │ ├── style.module.css
│ │ └── Confirmation.jsx
│ └── PageNotFound
│ │ ├── index.js
│ │ ├── style.module.css
│ │ └── PageNotFound.jsx
├── components
│ ├── Footer
│ │ ├── index.js
│ │ └── Footer.jsx
│ ├── NavBar
│ │ ├── index.js
│ │ ├── ButtonLink.jsx
│ │ ├── NavBar.jsx
│ │ ├── DrawerComponent.jsx
│ │ └── AppBarComponent.jsx
│ ├── StarRating
│ │ ├── index.js
│ │ └── StarRating.jsx
│ ├── CustomButton
│ │ ├── index.js
│ │ └── CustomButton.jsx
│ ├── WithLoading
│ │ ├── index.js
│ │ └── WithLoading.jsx
│ ├── GenericSnackbar
│ │ ├── index.js
│ │ └── GenericSnackbar.jsx
│ └── CircularProgressIndicator
│ │ ├── index.js
│ │ ├── style.module.css
│ │ └── CircularProgressIndicator.jsx
├── assets
│ └── images
│ │ ├── pageError.jpg
│ │ ├── HomeHeroBackground.jpg
│ │ └── travel-booking-icon.png
├── Axios
│ └── axiosInstance.js
├── constants
│ └── colorConstants.js
├── hooks
│ ├── useAuthToken.js
│ ├── useComponentLoader.js
│ ├── useCartContext.js
│ ├── useValueFromToken.js
│ └── useSnackbar.js
├── services
│ ├── authService.js
│ ├── bookingServices.js
│ ├── searchService.js
│ ├── homePageServices.js
│ ├── manageRooms.js
│ ├── hotelPageServices.js
│ ├── manageCities.js
│ └── manageHotels.js
├── helpers
│ └── helpers.jsx
├── routes
│ └── ProtectedRoutes.jsx
├── context
│ ├── CheckoutFormContext .jsx
│ ├── searchContext.jsx
│ ├── authContext.jsx
│ └── CartContext.jsx
├── index.css
├── main.jsx
├── __tests__
│ ├── SearchService.test.jsx
│ ├── Home.test.jsx
│ └── Login.test.jsx
└── App.jsx
├── vercel.json
├── .babelrc
├── vite.config.js
├── .gitignore
├── index.html
├── .eslintrc.cjs
├── public
└── vite.svg
└── package.json
/setupTests.js:
--------------------------------------------------------------------------------
1 | import "@testing-library/jest-dom";
2 |
--------------------------------------------------------------------------------
/src/pages/Home/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./Home";
2 |
--------------------------------------------------------------------------------
/src/pages/Hotel/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./Hotel";
2 |
--------------------------------------------------------------------------------
/src/pages/Login/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./Login";
2 |
--------------------------------------------------------------------------------
/src/components/Footer/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./Footer";
2 |
--------------------------------------------------------------------------------
/src/components/NavBar/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./NavBar";
2 |
--------------------------------------------------------------------------------
/src/pages/Checkout/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./Checkout";
2 |
--------------------------------------------------------------------------------
/src/pages/SearchPage/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./SearchPage";
2 |
--------------------------------------------------------------------------------
/src/components/StarRating/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./StarRating";
2 |
--------------------------------------------------------------------------------
/src/pages/Admin/pages/Cities/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./Cities";
2 |
--------------------------------------------------------------------------------
/src/pages/Admin/pages/Hotels/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./Hotels";
2 |
--------------------------------------------------------------------------------
/src/pages/Admin/pages/Rooms/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./Rooms";
2 |
--------------------------------------------------------------------------------
/src/pages/Confirmation/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./Confirmation";
2 |
--------------------------------------------------------------------------------
/src/pages/PageNotFound/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./PageNotFound";
2 |
--------------------------------------------------------------------------------
/src/components/CustomButton/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./CustomButton";
2 |
--------------------------------------------------------------------------------
/src/components/WithLoading/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./WithLoading";
2 |
--------------------------------------------------------------------------------
/src/components/GenericSnackbar/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./GenericSnackbar";
2 |
--------------------------------------------------------------------------------
/src/pages/Admin/components/SearchBar/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./SearchBar";
2 |
--------------------------------------------------------------------------------
/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "rewrites": [{ "source": "/(.*)", "destination": "/" }]
3 | }
4 |
--------------------------------------------------------------------------------
/src/pages/Checkout/components/CartItems/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./CartItems";
2 |
--------------------------------------------------------------------------------
/src/pages/Home/components/HeroSection/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./HeroSection";
2 |
--------------------------------------------------------------------------------
/src/pages/SearchPage/components/DateCheck/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./DateCheck";
2 |
--------------------------------------------------------------------------------
/src/pages/SearchPage/components/SearchBar/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./SearchBar";
2 |
--------------------------------------------------------------------------------
/src/pages/Admin/components/CreateButton/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./CreateButton";
2 |
--------------------------------------------------------------------------------
/src/pages/Admin/components/DetailedGrid/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./DetailedGrid";
2 |
--------------------------------------------------------------------------------
/src/pages/Admin/components/LeftNavigation/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./LeftNavigation";
2 |
--------------------------------------------------------------------------------
/src/pages/Admin/components/UpdateButton/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./UpdateButton";
2 |
--------------------------------------------------------------------------------
/src/pages/Home/components/FeaturedDeals/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./FeaturedDeals";
2 |
--------------------------------------------------------------------------------
/src/pages/Hotel/components/AvailableRooms/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./AvailableRooms";
2 |
--------------------------------------------------------------------------------
/src/pages/Hotel/components/HotelAmenities/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./HotelAmenities";
2 |
--------------------------------------------------------------------------------
/src/pages/Hotel/components/HotelDetails/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./HotelDetails";
2 |
--------------------------------------------------------------------------------
/src/pages/Hotel/components/HotelMapLocation/index.js:
--------------------------------------------------------------------------------
1 | export {default} from "./HotelMapLocation";
--------------------------------------------------------------------------------
/src/pages/Hotel/components/HotelReviews/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./HotelReviews";
2 |
--------------------------------------------------------------------------------
/src/pages/Hotel/components/VisualGallery/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./VisualGallery";
2 |
--------------------------------------------------------------------------------
/src/pages/SearchPage/components/OptionItem/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./OptionItem";
2 |
--------------------------------------------------------------------------------
/src/pages/Admin/components/UpdateEntityForm/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./UpdateEntityForm";
2 |
--------------------------------------------------------------------------------
/src/pages/Admin/pages/Rooms/components/SearchBar/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./SearchBar";
2 |
--------------------------------------------------------------------------------
/src/pages/Checkout/components/CustomTextField/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./CustomTextField";
2 |
--------------------------------------------------------------------------------
/src/pages/Checkout/components/FormInformation/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./FormInformation";
2 |
--------------------------------------------------------------------------------
/src/pages/Home/components/TrendingDestinations/index.js:
--------------------------------------------------------------------------------
1 | export {default} from "./TrendingDestinations";
--------------------------------------------------------------------------------
/src/pages/SearchPage/components/SearchFilters/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./SearchFilters";
2 |
--------------------------------------------------------------------------------
/src/pages/SearchPage/components/SearchResult/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./SearchResult";
2 |
--------------------------------------------------------------------------------
/src/components/CircularProgressIndicator/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./CircularProgressIndicator";
2 |
--------------------------------------------------------------------------------
/src/pages/Admin/components/CreateEntityDialog/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./CreateEntityDialog";
2 |
--------------------------------------------------------------------------------
/src/pages/Home/components/RecentlyVisitedHotels/index.js:
--------------------------------------------------------------------------------
1 | export {default} from "./RecentlyVisitedHotels";
--------------------------------------------------------------------------------
/src/pages/SearchPage/components/SearchResultItem/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./SearchResultItem";
2 |
--------------------------------------------------------------------------------
/src/pages/Admin/pages/Cities/components/UpdateCityForm/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./UpdateCityForm";
2 |
--------------------------------------------------------------------------------
/src/pages/Admin/pages/Hotels/components/UpdateHotelForm/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./UpdateHotelForm";
2 |
--------------------------------------------------------------------------------
/src/pages/Admin/pages/Rooms/components/UpdateRoomForm/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./UpdateRoomForm";
2 |
--------------------------------------------------------------------------------
/src/pages/Confirmation/components/ConfirmationTable/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./ConfirmationTable";
2 |
--------------------------------------------------------------------------------
/src/pages/SearchPage/components/SearchItemContainer/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./SearchItemContainer";
2 |
--------------------------------------------------------------------------------
/src/pages/Admin/pages/Cities/components/CreateCityDialog/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./CreateCityDialog";
2 |
--------------------------------------------------------------------------------
/src/pages/Admin/pages/Hotels/components/CreateHotelDialog/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./CreateHotelDialog";
2 |
--------------------------------------------------------------------------------
/src/pages/Admin/pages/Rooms/components/CreateRoomDialog/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./CreateRoomDialog";
2 |
--------------------------------------------------------------------------------
/src/pages/Admin/pages/Rooms/components/RoomsDetailedGrid/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./RoomsDetailedGrid";
2 |
--------------------------------------------------------------------------------
/src/pages/Hotel/components/HotelHeaderAndDescription/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./HotelHeaderAndDescription";
2 |
--------------------------------------------------------------------------------
/src/assets/images/pageError.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rahaf-Mansour/travel-booking-platform/HEAD/src/assets/images/pageError.jpg
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "@babel/preset-env",
4 | ["@babel/preset-react", { "runtime": "automatic" }]
5 | ]
6 | }
--------------------------------------------------------------------------------
/src/assets/images/HomeHeroBackground.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rahaf-Mansour/travel-booking-platform/HEAD/src/assets/images/HomeHeroBackground.jpg
--------------------------------------------------------------------------------
/src/assets/images/travel-booking-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rahaf-Mansour/travel-booking-platform/HEAD/src/assets/images/travel-booking-icon.png
--------------------------------------------------------------------------------
/src/pages/SearchPage/components/SearchItemContainer/style.module.css:
--------------------------------------------------------------------------------
1 | .heroSearchItem {
2 | display: flex;
3 | align-items: center;
4 | gap: 10px;
5 | }
--------------------------------------------------------------------------------
/src/pages/Checkout/components/CustomTextField/style.module.css:
--------------------------------------------------------------------------------
1 | .field {
2 | margin-bottom: 20px;
3 | }
4 |
5 | .error {
6 | color: red;
7 | font-size: 14px;
8 | }
--------------------------------------------------------------------------------
/src/pages/SearchPage/components/SearchResult/style.module.css:
--------------------------------------------------------------------------------
1 | .resultList {
2 | flex: 3;
3 | }
4 |
5 | .noHotelsMessage {
6 | color: red;
7 | font-style: italic;
8 | }
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 | import react from "@vitejs/plugin-react";
3 |
4 | export default defineConfig({
5 | plugins: [react()],
6 | envDir: '.',
7 | });
8 |
--------------------------------------------------------------------------------
/src/Axios/axiosInstance.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 |
3 | const axiosInstance = axios.create({
4 | baseURL: import.meta.env.VITE_API_BASE_URL,
5 | });
6 |
7 | export default axiosInstance;
8 |
--------------------------------------------------------------------------------
/src/constants/colorConstants.js:
--------------------------------------------------------------------------------
1 | const colors = {
2 | primaryColor: "#395591",
3 | secondaryColor: "#4b6cb7",
4 | tertiaryColor: "#b9c0d2",
5 | mainColor: "#6f6c6c",
6 | };
7 |
8 | export default colors;
9 |
--------------------------------------------------------------------------------
/src/pages/Confirmation/components/ConfirmationTable/style.module.css:
--------------------------------------------------------------------------------
1 | .TableHeadRow {
2 | font-weight: bold;
3 | background-color: #d7d3d3a2;
4 | }
5 |
6 | .TableBodyRow:nth-child(odd) {
7 | background-color: #f3f3f3;
8 | }
--------------------------------------------------------------------------------
/src/pages/Home/style.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | margin: 0 auto;
3 | padding: 2rem;
4 | max-width: 1200px;
5 | }
6 |
7 | @media (max-width: 768px) {
8 | .container {
9 | padding: 2rem;
10 | }
11 | }
--------------------------------------------------------------------------------
/src/pages/Hotel/HotelWithLoading.jsx:
--------------------------------------------------------------------------------
1 | import WithLoading from "../../components/WithLoading";
2 | import HotelWithoutLoading from "./HotelWithoutLoading";
3 |
4 | const HotelWithLoading = WithLoading(HotelWithoutLoading);
5 | export default HotelWithLoading;
6 |
--------------------------------------------------------------------------------
/src/components/CircularProgressIndicator/style.module.css:
--------------------------------------------------------------------------------
1 | .centered {
2 | display: flex;
3 | justify-content: center;
4 | align-items: center;
5 | height: 80vh;
6 | }
7 |
8 | @media (max-width: 768px) {
9 | .centered {
10 | height: 90svh;
11 | }
12 | }
--------------------------------------------------------------------------------
/src/pages/Admin/components/DetailedGrid/DetailedGridWithLoading.jsx:
--------------------------------------------------------------------------------
1 | import WithLoading from "../../../../components/WithLoading";
2 | import DetailedGrid from "./DetailedGrid";
3 |
4 | const DetailedGridWithLoading = WithLoading(DetailedGrid);
5 | export default DetailedGridWithLoading;
6 |
--------------------------------------------------------------------------------
/src/pages/Hotel/components/HotelMapLocation/style.module.css:
--------------------------------------------------------------------------------
1 | .mapContainer {
2 | width: 100%;
3 | height: 300px;
4 | margin-top: 2rem;
5 | margin-bottom: 3rem;
6 | }
7 |
8 | .mapContainer h3 {
9 | font-size: 1.2rem;
10 | margin-bottom: 1rem;
11 | color: #333;
12 | }
--------------------------------------------------------------------------------
/src/hooks/useAuthToken.js:
--------------------------------------------------------------------------------
1 | import { useContext } from "react";
2 | import { AuthContext } from "../context/authContext";
3 |
4 | const useAuthToken = () => {
5 | const { user } = useContext(AuthContext);
6 | const token = user ? user.authentication : null;
7 | return token;
8 | };
9 |
10 | export default useAuthToken;
11 |
--------------------------------------------------------------------------------
/src/hooks/useComponentLoader.js:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 |
3 | const useComponentLoader = () => {
4 | const [isLoading, setIsLoading] = useState(true);
5 |
6 | const stopLoading = () => {
7 | setIsLoading(false);
8 | };
9 |
10 | return { isLoading, stopLoading };
11 | };
12 |
13 | export default useComponentLoader;
14 |
--------------------------------------------------------------------------------
/src/pages/Admin/components/SearchBar/style.module.css:
--------------------------------------------------------------------------------
1 | .searchBar {
2 | display: flex;
3 | justify-content: space-between;
4 | align-items: center;
5 | box-shadow: 0px 0px 5px 0px rgba(0, 0, 0, 0.2);
6 | gap: 2px;
7 | padding: 1px 4px;
8 | border: 1px solid #ccc;
9 | border-radius: 5px;
10 | margin: 0 8px 20px 0;
11 | }
--------------------------------------------------------------------------------
/src/pages/SearchPage/components/SearchFilters/style.module.css:
--------------------------------------------------------------------------------
1 | .filterSide {
2 | border-radius: 10px;
3 | background-color: #fff;
4 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
5 | padding: 1rem;
6 | margin-bottom: 1rem;
7 | }
8 |
9 | @media screen and (max-width: 768px) {
10 | .filterSide {
11 | padding: 1rem;
12 | }
13 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 | .env
--------------------------------------------------------------------------------
/src/services/authService.js:
--------------------------------------------------------------------------------
1 | import axiosInstance from "../Axios/axiosInstance";
2 |
3 | export const loginAPI = async (values) => {
4 | try {
5 | const response = await axiosInstance.post(`/auth/authenticate`, values);
6 | return response.data;
7 | } catch (error) {
8 | throw new Error(error.response?.data.message || "Error: Unauthorized User");
9 | }
10 | };
11 |
--------------------------------------------------------------------------------
/src/components/CircularProgressIndicator/CircularProgressIndicator.jsx:
--------------------------------------------------------------------------------
1 | import { CircularProgress } from "@mui/material";
2 | import styles from "./style.module.css";
3 |
4 | const CircularProgressIndicator = () => {
5 | return (
6 |
7 |
8 |
9 | );
10 | };
11 |
12 | export default CircularProgressIndicator;
13 |
--------------------------------------------------------------------------------
/src/pages/Home/components/TrendingDestinations/TrendingDestinationsWithLoading.jsx:
--------------------------------------------------------------------------------
1 | import WithLoading from "../../../../components/WithLoading";
2 | import TrendingDestinationsWithoutLoading from "./TrendingDestinationsWithoutLoading";
3 |
4 | const TrendingDestinationsWithLoading = WithLoading(
5 | TrendingDestinationsWithoutLoading
6 | );
7 | export default TrendingDestinationsWithLoading;
8 |
--------------------------------------------------------------------------------
/src/pages/Home/components/RecentlyVisitedHotels/RecentlyVisitedHotelsWithLoading.jsx:
--------------------------------------------------------------------------------
1 | import WithLoading from "../../../../components/WithLoading";
2 | import RecentlyVisitedHotelsWithoutLoading from "./RecentlyVisitedHotelsWithoutLoading";
3 |
4 | const RecentlyVisitedHotelsWithLoading = WithLoading(
5 | RecentlyVisitedHotelsWithoutLoading
6 | );
7 | export default RecentlyVisitedHotelsWithLoading;
8 |
--------------------------------------------------------------------------------
/src/hooks/useCartContext.js:
--------------------------------------------------------------------------------
1 | import { useContext } from "react";
2 | import { CartContext } from "../context/CartContext";
3 |
4 | const useCartContext = () => {
5 | const context = useContext(CartContext);
6 | if (context === undefined) {
7 | throw new Error("useCartContext must be used within a CartContextProvider");
8 | }
9 | return context;
10 | };
11 |
12 | export default useCartContext;
13 |
--------------------------------------------------------------------------------
/src/services/bookingServices.js:
--------------------------------------------------------------------------------
1 | import axiosInstance from "../Axios/axiosInstance";
2 |
3 | export const postNewBooking = async (values) => {
4 | try {
5 | const response = await axiosInstance.post(`/bookings`, values);
6 | return response.data;
7 | } catch (error) {
8 | throw new Error(
9 | error.response?.data.message || "Error: Can't post the new booking"
10 | );
11 | }
12 | };
13 |
--------------------------------------------------------------------------------
/src/pages/Admin/hooks/useDialogState.js:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 |
3 | const useDialogState = () => {
4 | const [isDialogOpen, setIsDialogOpen] = useState(false);
5 |
6 | const handleDialogOpen = () => setIsDialogOpen(true);
7 | const handleDialogClose = () => setIsDialogOpen(false);
8 |
9 | return { isDialogOpen, handleDialogOpen, handleDialogClose };
10 | };
11 |
12 | export default useDialogState;
13 |
--------------------------------------------------------------------------------
/src/pages/SearchPage/components/SearchItemContainer/SearchItemContainer.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from "prop-types";
2 | import styles from "./style.module.css";
3 |
4 | const SearchItemContainer = ({ children }) => {
5 | return {children}
;
6 | };
7 |
8 | SearchItemContainer.propTypes = {
9 | children: PropTypes.node.isRequired,
10 | };
11 |
12 | export default SearchItemContainer;
13 |
--------------------------------------------------------------------------------
/src/services/searchService.js:
--------------------------------------------------------------------------------
1 | import axiosInstance from "../Axios/axiosInstance";
2 |
3 | export const searchAPI = async (values) => {
4 | try {
5 | const response = await axiosInstance.get(`/home/search`, {
6 | params: {
7 | ...values,
8 | },
9 | });
10 | return response.data;
11 | } catch (error) {
12 | throw new Error(error.response?.data.message || "Error: Can't search");
13 | }
14 | };
15 |
--------------------------------------------------------------------------------
/src/pages/Home/components/TrendingDestinations/TrendingDestinationsSection.jsx:
--------------------------------------------------------------------------------
1 | import TrendingDestinations from "./TrendingDestinations";
2 |
3 | const TrendingDestinationsSection = () => {
4 | return (
5 | <>
6 |
7 | Trending Destinations
8 |
9 |
10 | >
11 | );
12 | };
13 |
14 | export default TrendingDestinationsSection;
15 |
--------------------------------------------------------------------------------
/src/pages/Home/components/RecentlyVisitedHotels/RecentlyVisitedHotelsSection.jsx:
--------------------------------------------------------------------------------
1 | import RecentlyVisitedHotels from "./RecentlyVisitedHotels";
2 |
3 | const RecentlyVisitedHotelsSection = () => {
4 | return (
5 | <>
6 |
7 | Recently Visited Hotels
8 |
9 |
10 | >
11 | );
12 | };
13 |
14 | export default RecentlyVisitedHotelsSection;
15 |
--------------------------------------------------------------------------------
/src/pages/Hotel/components/HotelAmenities/style.module.css:
--------------------------------------------------------------------------------
1 | .hotelAmenities {
2 | margin-bottom: 2rem;
3 | }
4 |
5 | .hotelAmenities h3 {
6 | font-size: 1.2rem;
7 | margin-bottom: 1rem;
8 | color: #333;
9 | }
10 |
11 | .hotelAmenities ul {
12 | list-style: none;
13 | padding: 0;
14 | }
15 |
16 | .hotelAmenities li {
17 | margin-bottom: 0.5rem;
18 | font-size: 1rem;
19 | line-height: 1.5;
20 | display: flex;
21 | align-items: center;
22 | gap: 0.5rem;
23 | }
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
10 |
11 | Travel Booking Platform
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/pages/Hotel/components/HotelHeaderAndDescription/style.module.css:
--------------------------------------------------------------------------------
1 | .hotelHeaderAndDescriptionContainer {
2 | margin-bottom: 2rem;
3 | }
4 |
5 | .hotelHeaderAndDescriptionContainer p {
6 | font-size: 16px;
7 | margin-bottom: 4px;
8 | }
9 |
10 | .hotelHeader {
11 | display: flex;
12 | justify-content: space-between;
13 | align-items: center;
14 | vertical-align: middle;
15 | margin-bottom: 0.8rem;
16 | }
17 |
18 | .hotelName {
19 | font-size: 1.5rem;
20 | font-weight: 600;
21 | }
--------------------------------------------------------------------------------
/src/components/WithLoading/WithLoading.jsx:
--------------------------------------------------------------------------------
1 | import CircularProgressIndicator from "../CircularProgressIndicator";
2 | import PropTypes from "prop-types";
3 |
4 | function WithLoading(Component) {
5 | function WithLoadingComponent({ isLoading, ...props }) {
6 | if (!isLoading) return ;
7 | return ;
8 | }
9 |
10 | WithLoadingComponent.propTypes = {
11 | isLoading: PropTypes.bool.isRequired,
12 | };
13 | return WithLoadingComponent;
14 | }
15 | export default WithLoading;
16 |
--------------------------------------------------------------------------------
/src/helpers/helpers.jsx:
--------------------------------------------------------------------------------
1 | import WifiIcon from "@mui/icons-material/Wifi";
2 | import TvIcon from "@mui/icons-material/Tv";
3 | import AcUnitIcon from "@mui/icons-material/AcUnit";
4 |
5 | export const renderAmenityIcon = (amenityName) => {
6 | switch (amenityName.toLowerCase()) {
7 | case "free wi-fi":
8 | return ;
9 | case "tv":
10 | return ;
11 | case "air conditioning":
12 | return ;
13 | default:
14 | return null;
15 | }
16 | };
17 |
--------------------------------------------------------------------------------
/src/pages/Admin/pages/Cities/cityConfig.js:
--------------------------------------------------------------------------------
1 | import * as Yup from "yup";
2 |
3 | export const fields = [
4 | { name: "name", label: "Name", type: "text" },
5 | { name: "description", label: "Description", type: "text" },
6 | ];
7 |
8 | export const initialValues = fields.reduce((values, field) => {
9 | values[field.name] = "";
10 | return values;
11 | }, {});
12 |
13 | export const validationSchema = Yup.object().shape({
14 | name: Yup.string().required("Name is required"),
15 | description: Yup.string().required("Description is required"),
16 | });
17 |
--------------------------------------------------------------------------------
/src/pages/Home/components/FeaturedDeals/SkeletonDealCard.jsx:
--------------------------------------------------------------------------------
1 | import { Skeleton } from "@mui/material";
2 | import styles from "./style.module.css";
3 |
4 | const SkeletonDealCard = () => {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | );
15 | };
16 |
17 | export default SkeletonDealCard;
18 |
--------------------------------------------------------------------------------
/src/pages/Admin/components/UpdateButton/UpdateButton.jsx:
--------------------------------------------------------------------------------
1 | import { Button, Box } from "@mui/material";
2 | import PropTypes from "prop-types";
3 |
4 | const UpdateButton = ({ isSubmitting }) => {
5 | return (
6 |
7 |
13 | Update
14 |
15 |
16 | );
17 | };
18 |
19 | export default UpdateButton;
20 |
21 | UpdateButton.propTypes = {
22 | isSubmitting: PropTypes.bool,
23 | };
24 |
--------------------------------------------------------------------------------
/src/hooks/useValueFromToken.js:
--------------------------------------------------------------------------------
1 | import { jwtDecode } from "jwt-decode";
2 | import useAuthToken from "./useAuthToken";
3 |
4 | const getValueFromToken = (token, thing) => {
5 | try {
6 | if (token) {
7 | const decodedToken = jwtDecode(token);
8 | return decodedToken[thing];
9 | }
10 | } catch (error) {
11 | console.error("Error decoding token:", error);
12 | }
13 | return null;
14 | };
15 |
16 | const useValueFromToken = (thing) => {
17 | const token = useAuthToken();
18 | return getValueFromToken(token, thing);
19 | };
20 |
21 | export default useValueFromToken;
22 |
--------------------------------------------------------------------------------
/src/components/StarRating/StarRating.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from "prop-types";
2 | import { Star } from "@mui/icons-material";
3 |
4 | const StarRating = ({ starsNumber, className }) => {
5 | return (
6 |
7 | {Array(starsNumber)
8 | .fill()
9 | .map((_, i) => (
10 |
11 | ))}
12 |
13 | );
14 | };
15 |
16 | export default StarRating;
17 |
18 | StarRating.propTypes = {
19 | starsNumber: PropTypes.number.isRequired,
20 | className: PropTypes.string,
21 | };
22 |
--------------------------------------------------------------------------------
/src/routes/ProtectedRoutes.jsx:
--------------------------------------------------------------------------------
1 | import { useContext } from "react";
2 | import { Navigate, Outlet } from "react-router-dom";
3 | import { AuthContext } from "../context/authContext";
4 | import PropTypes from "prop-types";
5 |
6 | const ProtectedRoutes = ({ allowedRoles }) => {
7 | const { user } = useContext(AuthContext);
8 |
9 | if (!user || !allowedRoles.includes(user.userType)) {
10 | return ;
11 | }
12 |
13 | return ;
14 | };
15 |
16 | export default ProtectedRoutes;
17 |
18 | ProtectedRoutes.propTypes = {
19 | allowedRoles: PropTypes.arrayOf(PropTypes.string).isRequired,
20 | };
21 |
--------------------------------------------------------------------------------
/src/pages/Admin/pages/Rooms/roomConfig.js:
--------------------------------------------------------------------------------
1 | import * as Yup from "yup";
2 |
3 | export const fields = [
4 | { name: "roomNumber", label: "Room Number", type: "number" },
5 | { name: "cost", label: "Cost", type: "number" },
6 | { name: "hotelId", label: "Hotel Id", type: "number" },
7 | ];
8 |
9 | export const initialValues = fields.reduce((values, field) => {
10 | values[field.name] = 0;
11 | return values;
12 | }, {});
13 |
14 | export const validationSchema = Yup.object(
15 | fields.reduce((schema, field) => {
16 | schema[field.name] = Yup.string().required(`${field.label} is required`);
17 | return schema;
18 | }, {})
19 | );
20 |
--------------------------------------------------------------------------------
/src/pages/Home/components/HeroSection/HeroSection.jsx:
--------------------------------------------------------------------------------
1 | import HomeHeroBackground from "../../../../assets/images/HomeHeroBackground.jpg";
2 | import styles from "./style.module.css";
3 |
4 | const HeroSection = () => {
5 | return (
6 |
7 |
8 | Book with us for a happy,
9 | comfortable accommodation!
10 |
11 |
16 |
17 | );
18 | };
19 |
20 | export default HeroSection;
21 |
--------------------------------------------------------------------------------
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: { browser: true, es2020: true, jest: true },
4 | extends: [
5 | "eslint:recommended",
6 | "plugin:react/recommended",
7 | "plugin:react/jsx-runtime",
8 | "plugin:react-hooks/recommended",
9 | ],
10 | ignorePatterns: ["dist", ".eslintrc.cjs"],
11 | parserOptions: { ecmaVersion: "latest", sourceType: "module" },
12 | settings: { react: { version: "18.2" } },
13 | plugins: ["react-refresh"],
14 | rules: {
15 | "react/display-name": "off",
16 | "react-refresh/only-export-components": [
17 | "warn",
18 | { allowConstantExport: true },
19 | ],
20 | },
21 | };
22 |
--------------------------------------------------------------------------------
/src/context/CheckoutFormContext .jsx:
--------------------------------------------------------------------------------
1 | import { createContext, useState } from "react";
2 | import PropTypes from "prop-types";
3 |
4 | export const FormContext = createContext();
5 |
6 | const FormContextProvider = ({ children }) => {
7 | const [formValues, setFormValues] = useState(null);
8 |
9 | const setValues = (values) => {
10 | setFormValues(values);
11 | };
12 |
13 | return (
14 |
15 | {children}
16 |
17 | );
18 | };
19 |
20 | FormContextProvider.propTypes = {
21 | children: PropTypes.node.isRequired,
22 | };
23 |
24 | export default FormContextProvider;
25 |
--------------------------------------------------------------------------------
/src/pages/Admin/components/CreateButton/CreateButton.jsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@mui/material";
2 | import AddBoxIcon from "@mui/icons-material/AddBox";
3 | import PropTypes from "prop-types";
4 |
5 | const CreateButton = ({ handleDialogOpen }) => {
6 | return (
7 |
12 | Create
13 |
14 | );
15 | };
16 |
17 | export default CreateButton;
18 |
19 | CreateButton.propTypes = {
20 | handleDialogOpen: PropTypes.func.isRequired,
21 | };
22 |
--------------------------------------------------------------------------------
/src/components/NavBar/ButtonLink.jsx:
--------------------------------------------------------------------------------
1 | import { Button, Typography } from "@mui/material";
2 | import { Link } from "react-router-dom";
3 | import PropTypes from "prop-types";
4 |
5 | const ButtonLink = ({ to, icon, text }) => (
6 |
7 | {icon}
8 |
12 | {text}
13 |
14 |
15 | );
16 |
17 | export default ButtonLink;
18 |
19 | ButtonLink.propTypes = {
20 | to: PropTypes.string.isRequired,
21 | icon: PropTypes.element.isRequired,
22 | text: PropTypes.string,
23 | };
24 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@400;600&display=swap');
2 | @import url("https://fonts.googleapis.com/css2?family=Monoton&family=Quicksand:wght@500;700&display=swap");
3 | @import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');
4 |
5 | * {
6 | margin: 0;
7 | padding: 0;
8 | box-sizing: border-box;
9 | font-family: 'Poppins', sans-serif;
10 |
11 | }
12 |
13 | :root {
14 | --primary-color: #395591;
15 | --secondary-color: #4b6cb7;
16 | --tertiary-color: #b9c0d2;
17 | --main-color: #6f6c6c;
18 | }
--------------------------------------------------------------------------------
/src/pages/PageNotFound/style.module.css:
--------------------------------------------------------------------------------
1 | .errorContainer {
2 | display: flex;
3 | flex-direction: column;
4 | justify-content: center;
5 | align-items: center;
6 | height: 100vh;
7 | }
8 |
9 | .pageNotFoundImage {
10 | max-width: 100%;
11 | height: auto;
12 | vertical-align: middle;
13 | margin-top: 1rem;
14 | }
15 |
16 | .errorMessage {
17 | color: #000;
18 | font-size: 1.4rem;
19 | text-align: center;
20 | margin: 0 0 3rem;
21 | }
22 |
23 | @media screen and (max-width: 625px) {
24 | .errorMessage {
25 | font-size: 1.2rem;
26 | margin: 0 4rem;
27 | }
28 | }
29 |
30 | @media screen and (max-width: 400px) {
31 | .errorMessage {
32 | margin: 1rem;
33 | }
34 | }
--------------------------------------------------------------------------------
/src/components/CustomButton/CustomButton.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from "prop-types";
2 |
3 | const CustomButton = ({
4 | type = "submit",
5 | className,
6 | onClick,
7 | children,
8 | style,
9 | disabled = false,
10 | }) => {
11 | return (
12 |
19 | {children}
20 |
21 | );
22 | };
23 |
24 | CustomButton.propTypes = {
25 | type: PropTypes.string,
26 | className: PropTypes.string,
27 | onClick: PropTypes.func,
28 | children: PropTypes.node.isRequired,
29 | style: PropTypes.object,
30 | disabled: PropTypes.bool,
31 | };
32 |
33 | export default CustomButton;
34 |
--------------------------------------------------------------------------------
/src/pages/PageNotFound/PageNotFound.jsx:
--------------------------------------------------------------------------------
1 | import pageError from "../../assets/images/pageError.jpg";
2 | import NavBar from "../../components/NavBar";
3 | import styles from "./style.module.css";
4 | import Footer from "../../components/Footer";
5 |
6 | const PageNotFound = () => {
7 | return (
8 | <>
9 |
10 |
11 |
17 |
18 | We cannot find the page you are looking for!
19 |
20 |
21 |
22 | >
23 | );
24 | };
25 |
26 | export default PageNotFound;
27 |
--------------------------------------------------------------------------------
/src/pages/SearchPage/style.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | margin-top: 8rem;
3 | display: flex;
4 | justify-content: center;
5 | max-width: 1200px;
6 | margin-left: auto;
7 | margin-right: auto;
8 | }
9 |
10 | .wrapper {
11 | width: 100%;
12 | max-width: 1150px;
13 | display: flex;
14 | gap: 30px;
15 | flex-wrap: wrap;
16 | }
17 |
18 | @media screen and (max-width: 768px) {
19 | .container {
20 | margin-top: 15rem;
21 | margin-left: 2rem;
22 | margin-right: 2rem;
23 | }
24 |
25 | .wrapper {
26 | flex-direction: column;
27 | gap: 0;
28 | }
29 | }
30 |
31 | @media screen and (min-width: 768px) and (max-width: 1200px) {
32 | .container {
33 | margin-top: 15rem;
34 | max-width: 90%;
35 | }
36 | }
--------------------------------------------------------------------------------
/src/components/GenericSnackbar/GenericSnackbar.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from "prop-types";
2 | import Snackbar from "@mui/material/Snackbar";
3 | import Alert from "@mui/material/Alert";
4 |
5 | const GenericSnackbar = ({ open, message, onClose, severity = "info" }) => {
6 | return (
7 |
8 |
14 | {message}
15 |
16 |
17 | );
18 | };
19 |
20 | GenericSnackbar.propTypes = {
21 | open: PropTypes.bool,
22 | message: PropTypes.string.isRequired,
23 | onClose: PropTypes.func,
24 | severity: PropTypes.oneOf(["error", "warning", "info", "success"]),
25 | };
26 |
27 | export default GenericSnackbar;
28 |
--------------------------------------------------------------------------------
/src/main.jsx:
--------------------------------------------------------------------------------
1 | import ReactDOM from "react-dom/client";
2 | import App from "./App.jsx";
3 | import "./index.css";
4 | import AuthContextProvider from "./context/authContext.jsx";
5 | import { BrowserRouter } from "react-router-dom";
6 | import CartContextProvider from "./context/CartContext.jsx";
7 | import FormContextProvider from "./context/CheckoutFormContext .jsx";
8 | import SearchContextProvider from "./context/searchContext.jsx";
9 |
10 | ReactDOM.createRoot(document.getElementById("root")).render(
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | );
23 |
--------------------------------------------------------------------------------
/src/pages/Checkout/components/CustomTextField/CustomTextField.jsx:
--------------------------------------------------------------------------------
1 | import { Field, ErrorMessage } from "formik";
2 | import { TextField } from "@mui/material";
3 | import styles from "./style.module.css";
4 | import PropTypes from "prop-types";
5 |
6 | const CustomTextField = ({ name, label, type = "text", ...otherProps }) => {
7 | return (
8 |
9 |
17 |
18 |
19 | );
20 | };
21 |
22 | export default CustomTextField;
23 |
24 | CustomTextField.propTypes = {
25 | name: PropTypes.string.isRequired,
26 | label: PropTypes.string,
27 | type: PropTypes.string,
28 | };
29 |
--------------------------------------------------------------------------------
/src/pages/Checkout/components/FormInformation/style.module.css:
--------------------------------------------------------------------------------
1 | .formContainer {
2 | margin: auto;
3 | padding: 20px;
4 | border: 1px solid #ccc;
5 | border-radius: 5px;
6 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
7 | }
8 |
9 | .formHeader {
10 | color: #333;
11 | font-size: 1.5rem;
12 | text-align: center;
13 | }
14 |
15 | .formContainer p {
16 | color: #666;
17 | margin: 1rem 0;
18 | }
19 |
20 | @media screen and (max-width: 600px) {
21 | .formContainer {
22 | padding: 16px;
23 | }
24 |
25 | .formHeader {
26 | font-size: 1.3rem;
27 | }
28 |
29 | .field {
30 | margin-bottom: 15px;
31 | }
32 | }
33 |
34 | @media screen and (max-width: 450px) {
35 | .formHeader {
36 | font-size: 1.2rem;
37 | }
38 |
39 | .field {
40 | margin-bottom: 10px;
41 | }
42 | }
--------------------------------------------------------------------------------
/src/pages/Hotel/components/HotelHeaderAndDescription/HotelHeaderAndDescription.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from "prop-types";
2 | import styles from "./style.module.css";
3 | import StarRating from "../../../../components/StarRating";
4 |
5 | const HotelHeaderAndDescription = ({ hotelName, starRating, description }) => {
6 | return (
7 |
8 |
9 |
{hotelName}
10 |
11 |
12 |
{description}
13 |
14 | );
15 | };
16 |
17 | export default HotelHeaderAndDescription;
18 |
19 | HotelHeaderAndDescription.propTypes = {
20 | hotelName: PropTypes.string.isRequired,
21 | starRating: PropTypes.number.isRequired,
22 | description: PropTypes.string.isRequired,
23 | };
24 |
--------------------------------------------------------------------------------
/src/pages/Admin/pages/Rooms/components/SearchBar/SearchBar.jsx:
--------------------------------------------------------------------------------
1 | import { Paper, InputBase, IconButton } from "@mui/material";
2 | import SearchIcon from "@mui/icons-material/Search";
3 |
4 | const SearchBar = () => {
5 | return (
6 |
18 |
23 |
24 |
25 |
26 |
27 | );
28 | };
29 |
30 | export default SearchBar;
31 |
--------------------------------------------------------------------------------
/src/pages/SearchPage/components/SearchBar/styles.jsx:
--------------------------------------------------------------------------------
1 | import { Box, styled } from "@mui/material";
2 |
3 | export const SearchContainer = styled(Box, {
4 | shouldForwardProp: (prop) =>
5 | prop !== "position" && prop !== "topXs" && prop !== "topLg",
6 | })(({ theme, position = "absolute", topXs, topLg }) => ({
7 | height: "fit-content",
8 | backgroundColor: "white",
9 | border: "3px solid #e5a905",
10 | display: "flex",
11 | alignItems: "center",
12 | justifyContent: "space-between",
13 | padding: "10px",
14 | borderRadius: "10px",
15 | position: position,
16 | zIndex: "9",
17 | width: "90%",
18 | maxWidth: "1160px",
19 | [theme.breakpoints.up("xs")]: {
20 | top: topXs,
21 | flexDirection: "column",
22 | gap: "10px",
23 | },
24 | [theme.breakpoints.up("lg")]: {
25 | top: topLg,
26 | flexDirection: "row",
27 | gap: "none",
28 | },
29 | }));
30 |
--------------------------------------------------------------------------------
/src/pages/Hotel/components/HotelAmenities/HotelAmenities.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from "prop-types";
2 | import styles from "./style.module.css";
3 | import CheckCircleIcon from "@mui/icons-material/CheckCircle";
4 |
5 | const HotelAmenities = ({ amenities }) => {
6 | return (
7 |
8 |
Amenities
9 |
10 | {amenities.map((amenity, index) => (
11 |
12 |
13 | {amenity.name}
14 |
15 | ))}
16 |
17 |
18 | );
19 | };
20 |
21 | HotelAmenities.propTypes = {
22 | amenities: PropTypes.arrayOf(
23 | PropTypes.shape({
24 | name: PropTypes.string.isRequired,
25 | description: PropTypes.string.isRequired,
26 | })
27 | ),
28 | };
29 |
30 | export default HotelAmenities;
31 |
--------------------------------------------------------------------------------
/src/pages/SearchPage/components/DateCheck/style.module.css:
--------------------------------------------------------------------------------
1 | .heroSearchText {
2 | font-size: 1rem;
3 | color: black;
4 | cursor: pointer;
5 | display: flex;
6 | align-items: center;
7 | outline: none;
8 | border: none;
9 | background-color: transparent;
10 | }
11 |
12 | .heroIcon {
13 | color: var(--main-color);
14 | margin-right: 5px;
15 | }
16 |
17 | .datePicker {
18 | position: absolute;
19 | top: 60px;
20 | box-shadow: 0px 0px 10px -3px rgba(19, 16, 16, 0.808);
21 | }
22 |
23 | @media screen and (max-width: 480px) {
24 | .datePicker {
25 | top: 80px;
26 | left: 0;
27 | right: 0;
28 | }
29 | }
30 |
31 | @media screen and (max-width: 780px) {
32 | .datePicker {
33 | top: 80px;
34 | }
35 | }
36 |
37 | @media screen and (max-width: 1200px) {
38 | .datePicker {
39 | top: 80px;
40 | z-index: 99;
41 | }
42 | }
--------------------------------------------------------------------------------
/src/pages/Home/Home.jsx:
--------------------------------------------------------------------------------
1 | import NavBar from "../../components/NavBar";
2 | import HeroSection from "./components/HeroSection";
3 | import SearchBar from "../SearchPage/components/SearchBar";
4 | import FeaturedDeals from "./components/FeaturedDeals";
5 | import styles from "./style.module.css";
6 | import Footer from "../../components/Footer";
7 | import RecentlyVisitedHotelsSection from "./components/RecentlyVisitedHotels/RecentlyVisitedHotelsSection";
8 | import TrendingDestinationsSection from "./components/TrendingDestinations/TrendingDestinationsSection";
9 |
10 | const Home = () => {
11 | return (
12 | <>
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | >
23 | );
24 | };
25 |
26 | export default Home;
27 |
--------------------------------------------------------------------------------
/src/pages/SearchPage/SearchPage.jsx:
--------------------------------------------------------------------------------
1 | import SearchBar from "./components/SearchBar";
2 | import NavBar from "../../components/NavBar";
3 | import SearchResult from "./components/SearchResult";
4 | import styles from "./style.module.css";
5 | import SearchFilters from "./components/SearchFilters";
6 | import Footer from "../../components/Footer";
7 | import { useState } from "react";
8 |
9 | const SearchPage = () => {
10 | const [filters, setFilters] = useState({});
11 |
12 | const handleFilter = (newFilters) => {
13 | setFilters(newFilters);
14 | };
15 |
16 | return (
17 |
28 | );
29 | };
30 |
31 | export default SearchPage;
32 |
--------------------------------------------------------------------------------
/src/pages/Hotel/style.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | margin: 0 auto;
3 | padding: 20px 0 20px 15px;
4 | margin-top: 1rem;
5 | display: flex;
6 | flex-wrap: wrap;
7 | justify-content: center;
8 | }
9 |
10 | .hotelDetailsAndMapContainer {
11 | background-color: #fff;
12 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
13 | padding: 20px;
14 | border-radius: 8px;
15 | flex: 1.6;
16 | }
17 |
18 | .galleryAndRoomsContainer {
19 | flex: 3.8;
20 | margin-left: 1rem;
21 | }
22 |
23 | .errorMessage {
24 | color: red;
25 | font-size: 1.1rem;
26 | text-align: center;
27 | margin-top: 1rem;
28 | }
29 |
30 | @media screen and (max-width: 450px) {
31 | .container {
32 | margin-left: 1rem;
33 | margin-right: 1rem;
34 | padding: 1rem;
35 | }
36 |
37 | .galleryAndRoomsContainer {
38 | margin-left: 0;
39 | }
40 | }
41 |
42 | @media screen and (min-width: 768px) {
43 | .container {
44 | max-width: 1200px;
45 | }
46 | }
--------------------------------------------------------------------------------
/src/pages/SearchPage/components/OptionItem/style.module.css:
--------------------------------------------------------------------------------
1 | .optionItem {
2 | display: flex;
3 | align-items: center;
4 | justify-content: start;
5 | margin: 10px 15px;
6 | }
7 |
8 | .optionText {
9 | color: black;
10 | width: 120px;
11 | text-align: left;
12 | margin-right: 10px;
13 | }
14 |
15 | .optionCounter {
16 | display: flex;
17 | align-items: center;
18 | justify-content: space-between;
19 | color: black;
20 | }
21 |
22 | .optionCounterButton,
23 | .optionCounterNumber {
24 | width: 30px;
25 | text-align: center;
26 | display: flex;
27 | align-items: center;
28 | justify-content: center;
29 | font-size: 1rem;
30 | }
31 |
32 | .optionCounterButton {
33 | border: 1px solid var(--secondary-color);
34 | color: var(--secondary-color);
35 | cursor: pointer;
36 | }
37 |
38 | .optionCounterButton:disabled {
39 | cursor: not-allowed;
40 | background-color: #f5e7e7;
41 | }
42 |
43 | .sign {
44 | font-size: 1.1rem;
45 | font-weight: 500;
46 | }
--------------------------------------------------------------------------------
/src/pages/Home/components/HeroSection/style.module.css:
--------------------------------------------------------------------------------
1 | .imageContainer {
2 | position: relative;
3 | max-height: 400px;
4 | width: 100%;
5 | display: flex;
6 | justify-content: center;
7 | align-items: center;
8 | overflow: hidden;
9 | }
10 |
11 | .homeBackground {
12 | object-fit: cover;
13 | background-position: center;
14 | background-repeat: no-repeat;
15 | width: 100%;
16 | }
17 |
18 | .overlayText {
19 | position: absolute;
20 | left: 0;
21 | top: 40%;
22 | transform: translateY(-50%);
23 | background-color: rgba(11, 25, 102, 0.5);
24 | margin-left: 6rem;
25 | color: #fff;
26 | font-weight: bold;
27 | padding: 10px 20px;
28 | border-radius: 20px;
29 | font-family: "Quicksand";
30 | }
31 |
32 | @media (max-width: 768px) {
33 | .overlayText {
34 | margin-left: 1.8rem;
35 | font-size: 1.4rem;
36 | }
37 | }
38 |
39 | @media (max-width: 480px) {
40 | .overlayText {
41 | font-size: 1.2rem;
42 | margin-top: 0.5rem;
43 | }
44 | }
--------------------------------------------------------------------------------
/src/pages/Checkout/components/FormInformation/paymentSchema.js:
--------------------------------------------------------------------------------
1 | import * as Yup from "yup";
2 |
3 | const paymentSchema = Yup.object().shape({
4 | fullName: Yup.string().required("Full Name is required"),
5 |
6 | email: Yup.string()
7 | .email("Invalid email address")
8 | .required("Email is required"),
9 |
10 | paymentMethod: Yup.string().required("Payment Method is required"),
11 | cardNumber: Yup.string()
12 | .required("Card Number is required", "Invalid card number")
13 | .matches(/^\d{4} \d{4} \d{4} \d{4}$/, "Invalid card number"),
14 |
15 | expirationDate: Yup.string()
16 | .required("Expiration Date is required")
17 | .matches(
18 | /^(0?[1-9]|1[0-2])\/?(2[4-9]|3[0-9]|4[0-9]|50)$/,
19 | "Invalid expiration date format"
20 | ),
21 |
22 | cvv: Yup.string().required("CVV is required"),
23 |
24 | billingAddress: Yup.object().shape({
25 | state: Yup.string().required("State is required"),
26 | city: Yup.string().required("City is required"),
27 | }),
28 | });
29 |
30 | export default paymentSchema;
31 |
--------------------------------------------------------------------------------
/src/pages/Admin/pages/Hotels/hotelConfig.js:
--------------------------------------------------------------------------------
1 | import * as Yup from "yup";
2 |
3 | export const fields = [
4 | { name: "name", label: "Name", type: "text" },
5 | { name: "description", label: "Description", type: "text" },
6 | { name: "hotelType", label: "Hotel Type", type: "number" },
7 | { name: "starRating", label: "Star Rating", type: "number" },
8 | { name: "latitude", label: "Latitude", type: "number" },
9 | { name: "longitude", label: "Longitude", type: "number" },
10 | { name: "cityId", label: "City ID", type: "number" },
11 | ];
12 |
13 | export const initialValues = fields.reduce((values, field) => {
14 | values[field.name] = field.type === "number" ? 0 : "";
15 | return values;
16 | }, {});
17 |
18 | export const validationSchema = Yup.object(
19 | fields.reduce((schema, field) => {
20 | if (field.type === "text") {
21 | schema[field.name] = Yup.string().required(`${field.label} is required`);
22 | } else if (field.type === "number") {
23 | schema[field.name] = Yup.number().required(`${field.label} is required`);
24 | }
25 | return schema;
26 | }, {})
27 | );
28 |
--------------------------------------------------------------------------------
/src/services/homePageServices.js:
--------------------------------------------------------------------------------
1 | import axiosInstance from "../Axios/axiosInstance";
2 |
3 | export const featuredDealsAPI = async () => {
4 | try {
5 | const response = await axiosInstance.get(`/home/featured-deals`);
6 | return response.data;
7 | } catch (error) {
8 | throw new Error(
9 | error.response?.data.message || "Error: Can't fetch the featured deals"
10 | );
11 | }
12 | };
13 |
14 | export const trendingDestinationsAPI = async () => {
15 | try {
16 | const response = await axiosInstance.get(`/home/destinations/trending`);
17 | return response.data;
18 | } catch (error) {
19 | throw new Error(
20 | error.response?.data.message ||
21 | "Error: Can't fetch the trending destinations"
22 | );
23 | }
24 | };
25 |
26 | export const recentlyVisitedHotelsAPI = async (userId) => {
27 | try {
28 | const response = await axiosInstance.get(
29 | `/home/users/${userId}/recent-hotels`
30 | );
31 | return response.data;
32 | } catch (error) {
33 | throw new Error(
34 | error.response?.data.message ||
35 | "Error: Can't fetch the recently visited hotels"
36 | );
37 | }
38 | };
39 |
--------------------------------------------------------------------------------
/src/services/manageRooms.js:
--------------------------------------------------------------------------------
1 | import axiosInstance from "../Axios/axiosInstance";
2 |
3 | export const postNewRoom = async (hotelId, roomNumber, cost) => {
4 | try {
5 | const response = await axiosInstance.post(`/hotels/${hotelId}/rooms`, {
6 | roomNumber,
7 | cost,
8 | });
9 | return response.data;
10 | } catch (error) {
11 | throw new Error(
12 | error.response?.data.message || "Error: Can't create a new room"
13 | );
14 | }
15 | };
16 |
17 | export const updateRoom = async (roomId, roomNumber, cost) => {
18 | try {
19 | const response = await axiosInstance.put(`/rooms/${roomId}`, {
20 | roomNumber,
21 | cost,
22 | });
23 | return response.data;
24 | } catch (error) {
25 | throw new Error(
26 | error.response?.data.message || "Error: Can't update the room"
27 | );
28 | }
29 | };
30 |
31 | export const deleteRoom = async (hotelId, roomId) => {
32 | try {
33 | const response = await axiosInstance.delete(
34 | `/hotels/${hotelId}/rooms/${roomId}`
35 | );
36 | return response;
37 | } catch (error) {
38 | throw new Error(
39 | error.response?.data.message || "Error: Can't delete the room"
40 | );
41 | }
42 | };
43 |
--------------------------------------------------------------------------------
/src/pages/Confirmation/style.module.css:
--------------------------------------------------------------------------------
1 | .confirmationContainer {
2 | display: flex;
3 | flex-direction: column;
4 | align-items: center;
5 | margin: 1rem 0 0;
6 | }
7 |
8 | .confirmationHeader {
9 | display: flex;
10 | align-items: center;
11 | margin: 1rem 0 2rem;
12 | gap: 0.5rem;
13 | background-color: #d7f0e2;
14 | padding: 0.5rem 1rem;
15 | border-radius: 10px;
16 | border: 1px solid #2E8B57;
17 | }
18 |
19 | .confirmationContainer h1 {
20 | font-size: 1.3rem;
21 | text-align: center;
22 | }
23 |
24 | .printButton {
25 | background-color: #2E8B57;
26 | border: none;
27 | border-radius: 0.5rem;
28 | color: #fff;
29 | font-size: 1rem;
30 | padding: 0.5rem 1rem;
31 | font-weight: bold;
32 | cursor: pointer;
33 | margin: 2rem 0;
34 | }
35 |
36 | .printButton:hover {
37 | background-color: #3CB371;
38 | }
39 |
40 | @media screen and (max-width: 768px) {
41 | .confirmationHeader {
42 | margin: 1rem 0 2rem;
43 | }
44 |
45 | .confirmationContainer h1 {
46 | font-size: 1rem;
47 | }
48 | }
49 |
50 | @media screen and (max-width: 625px) {
51 | .confirmationHeader {
52 | margin: 1rem 1rem 2rem 1rem;
53 | }
54 | }
--------------------------------------------------------------------------------
/src/pages/Admin/components/UpdateEntityForm/UpdateEntityForm.jsx:
--------------------------------------------------------------------------------
1 | import { Field } from "formik";
2 | import { TextField } from "@mui/material";
3 | import PropTypes from "prop-types";
4 |
5 | const UpdateEntityForm = ({ fields, touched, errors }) => {
6 | return (
7 | <>
8 | {fields.map((field) => (
9 |
22 | ))}
23 | >
24 | );
25 | };
26 |
27 | export default UpdateEntityForm;
28 |
29 | UpdateEntityForm.propTypes = {
30 | fields: PropTypes.arrayOf(
31 | PropTypes.shape({
32 | name: PropTypes.string.isRequired,
33 | label: PropTypes.string.isRequired,
34 | type: PropTypes.string,
35 | })
36 | ),
37 | touched: PropTypes.object,
38 | errors: PropTypes.object,
39 | handleChange: PropTypes.func,
40 | values: PropTypes.object,
41 | };
42 |
--------------------------------------------------------------------------------
/src/hooks/useSnackbar.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 |
3 | const useSnackbar = () => {
4 | const [snackbarQueue, setSnackbarQueue] = useState([]);
5 | const [snackbar, setSnackbar] = useState({
6 | open: false,
7 | message: "",
8 | severity: "info",
9 | });
10 |
11 | const handleCloseSnackbar = () => {
12 | if (snackbar.open) {
13 | setSnackbar({ ...snackbar, open: false });
14 | setSnackbarQueue((prevQueue) => prevQueue.slice(1));
15 | }
16 | };
17 |
18 | const showSnackbar = (message, severity) => {
19 | setSnackbarQueue((prevQueue) => [...prevQueue, { message, severity }]);
20 | };
21 |
22 | const showSuccessSnackbar = (message) => {
23 | showSnackbar(message, "success");
24 | };
25 |
26 | const showErrorSnackbar = (message) => {
27 | showSnackbar(message, "error");
28 | };
29 |
30 | useEffect(() => {
31 | if (snackbarQueue.length > 0) {
32 | const nextSnackbar = snackbarQueue[0];
33 | setSnackbar({ open: true, ...nextSnackbar });
34 | }
35 | }, [snackbarQueue]);
36 |
37 | return {
38 | snackbar,
39 | handleCloseSnackbar,
40 | showSuccessSnackbar,
41 | showErrorSnackbar,
42 | showSnackbar,
43 | };
44 | };
45 |
46 | export default useSnackbar;
47 |
--------------------------------------------------------------------------------
/src/pages/Checkout/Checkout.jsx:
--------------------------------------------------------------------------------
1 | import { useContext } from "react";
2 | import NavBar from "../../components/NavBar";
3 | import { Box, Grid, Card, Container } from "@mui/material";
4 | import FormInformation from "./components/FormInformation/FormInformation";
5 | import CartItems from "./components/CartItems";
6 | import { CartContext } from "../../context/CartContext";
7 | import Footer from "../../components/Footer";
8 |
9 | const Checkout = () => {
10 | const { cart } = useContext(CartContext);
11 |
12 | return (
13 | <>
14 |
15 |
16 |
17 |
18 | 0 ? 6 : 12}>
19 |
22 |
23 |
24 |
25 | {cart.length > 0 && (
26 |
27 |
28 |
29 |
30 |
31 | )}
32 |
33 |
34 |
35 | {cart.length > 0 && }
36 | >
37 | );
38 | };
39 |
40 | export default Checkout;
41 |
--------------------------------------------------------------------------------
/src/services/hotelPageServices.js:
--------------------------------------------------------------------------------
1 | import axiosInstance from "../Axios/axiosInstance";
2 |
3 | export const getHotelDetails = async (hotelId) => {
4 | try {
5 | const response = await axiosInstance.get(`/hotels/${hotelId}`);
6 | return response.data;
7 | } catch (error) {
8 | return Promise.reject(error.response);
9 | }
10 | };
11 |
12 | export const getHotelGuestReviews = async (hotelId) => {
13 | try {
14 | const response = await axiosInstance.get(`/hotels/${hotelId}/reviews`);
15 | return response.data;
16 | } catch (error) {
17 | return Promise.reject(error.response);
18 | }
19 | };
20 |
21 | export const getHotelPicturesGallery = async (hotelId) => {
22 | try {
23 | const response = await axiosInstance.get(`/hotels/${hotelId}/gallery`);
24 | return response.data;
25 | } catch (error) {
26 | return Promise.reject(error.response);
27 | }
28 | };
29 |
30 | export const getHotelAvailableRooms = async (
31 | hotelId,
32 | checkInDate,
33 | checkOutDate
34 | ) => {
35 | try {
36 | const response = await axiosInstance.get(
37 | `/hotels/${hotelId}/available-rooms`,
38 | {
39 | params: {
40 | checkInDate,
41 | checkOutDate,
42 | },
43 | }
44 | );
45 | return response.data;
46 | } catch (error) {
47 | return Promise.reject(error.response);
48 | }
49 | };
50 |
--------------------------------------------------------------------------------
/src/pages/Hotel/components/HotelReviews/style.module.css:
--------------------------------------------------------------------------------
1 | .hotelReviews {
2 | margin-top: 20px;
3 | }
4 |
5 | .hotelReviews h3 {
6 | font-size: 1.2rem;
7 | margin-bottom: 16px;
8 | color: #333;
9 | }
10 |
11 | .hotelReview {
12 | border: 1px solid #ddd;
13 | border-radius: 8px;
14 | padding: 16px;
15 | margin-bottom: 16px;
16 | background-color: #fff;
17 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
18 | }
19 |
20 | .hotelReview p {
21 | margin: 4px 0;
22 | }
23 |
24 | .reviewHeader {
25 | display: flex;
26 | justify-content: space-between;
27 | align-items: center;
28 | }
29 |
30 | .customerName {
31 | font-size: 1rem;
32 | font-weight: bold;
33 | margin-bottom: 8px;
34 | }
35 |
36 | .rating {
37 | font-size: 1rem;
38 | color: #f39c12;
39 | font-weight: 500;
40 | }
41 |
42 | .description {
43 | font-size: 0.9rem;
44 | line-height: 1.4;
45 | }
46 |
47 | .centerButton {
48 | display: flex;
49 | justify-content: center;
50 | align-items: center;
51 | }
52 |
53 | .loadButton {
54 | background-color: var(--secondary-color);
55 | color: #fff;
56 | padding: 10px 20px;
57 | border: none;
58 | border-radius: 4px;
59 | cursor: pointer;
60 | font-size: 16px;
61 | transition: background-color 0.2s ease-in-out;
62 | }
63 |
64 | .loadButton:hover {
65 | background-color: var(--primary-color);
66 | }
--------------------------------------------------------------------------------
/src/pages/Hotel/components/HotelMapLocation/HotelMapLocation.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from "prop-types";
2 | import styles from "./style.module.css";
3 | import "leaflet/dist/leaflet.css";
4 | import { MapContainer, TileLayer, Marker, Popup } from "react-leaflet";
5 |
6 | const HotelMapLocation = ({ latitude, longitude, hotelName, location }) => {
7 | const position = [latitude, longitude];
8 |
9 | return (
10 |
11 |
See on map:
12 |
18 |
22 |
23 |
24 | {hotelName}
25 |
26 | {location}
27 |
28 |
29 |
30 |
31 | );
32 | };
33 |
34 | HotelMapLocation.propTypes = {
35 | latitude: PropTypes.number.isRequired,
36 | longitude: PropTypes.number.isRequired,
37 | hotelName: PropTypes.string.isRequired,
38 | location: PropTypes.string.isRequired,
39 | };
40 |
41 | export default HotelMapLocation;
42 |
--------------------------------------------------------------------------------
/src/context/searchContext.jsx:
--------------------------------------------------------------------------------
1 | import { createContext, useState } from "react";
2 | import PropTypes from "prop-types";
3 | import dayjs from "dayjs";
4 |
5 | export const SearchContext = createContext();
6 |
7 | const SearchContextProvider = ({ children }) => {
8 | const [searchParams, setSearchParams] = useState({
9 | checkInDate: dayjs().format("YYYY-MM-DD"),
10 | checkOutDate: dayjs().add(1, "day").format("YYYY-MM-DD"),
11 | city: "",
12 | adults: 1,
13 | children: 0,
14 | numberOfRooms: 1,
15 | });
16 |
17 | const updateSearchParams = (newParams) => {
18 | setSearchParams((prevParams) => ({ ...prevParams, ...newParams }));
19 | };
20 |
21 | const getNumberOfNights = () => {
22 | const startDate = new Date(searchParams.checkInDate);
23 | const endDate = new Date(searchParams.checkOutDate);
24 | const differenceInTime = endDate.getTime() - startDate.getTime();
25 | const differenceInDays = differenceInTime / (1000 * 3600 * 24);
26 | const nightsLength = Math.round(differenceInDays);
27 | return nightsLength;
28 | };
29 |
30 | return (
31 |
34 | {children}
35 |
36 | );
37 | };
38 |
39 | export default SearchContextProvider;
40 |
41 | SearchContextProvider.propTypes = {
42 | children: PropTypes.node.isRequired,
43 | };
44 |
--------------------------------------------------------------------------------
/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/pages/Home/components/TrendingDestinations/TrendingDestinations.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 | import { trendingDestinationsAPI } from "../../../../services/homePageServices";
3 | import useComponentLoader from "../../../../hooks/useComponentLoader";
4 | import useSnackbar from "../../../../hooks/useSnackbar";
5 | import TrendingDestinationsWithLoading from "./TrendingDestinationsWithLoading";
6 |
7 | export function TrendingDestinations() {
8 | const [trendingDestinations, setTrendingDestinations] = useState([]);
9 | const { isLoading, stopLoading } = useComponentLoader();
10 | const { snackbar, showErrorSnackbar, handleCloseSnackbar } = useSnackbar();
11 |
12 | useEffect(() => {
13 | const handleFetchTrendingDestinations = async () => {
14 | try {
15 | const trendingDestinationsData = await trendingDestinationsAPI();
16 | setTrendingDestinations(trendingDestinationsData);
17 | } catch (error) {
18 | showErrorSnackbar(
19 | "Failed to fetch trending destinations. Please try again later."
20 | );
21 | } finally {
22 | stopLoading();
23 | }
24 | };
25 |
26 | handleFetchTrendingDestinations();
27 | }, []);
28 |
29 | return (
30 |
36 | );
37 | }
38 |
39 | export default TrendingDestinations;
40 |
--------------------------------------------------------------------------------
/src/pages/Hotel/Hotel.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useContext } from "react";
2 | import { useHotelData } from "./hooks/useHotelData";
3 | import useSnackbar from "../../hooks/useSnackbar";
4 | import { useParams } from "react-router-dom";
5 | import { SearchContext } from "../../context/searchContext";
6 | import HotelWithLoading from "./HotelWithLoading";
7 | import NavBar from "../../components/NavBar";
8 |
9 | const Hotel = () => {
10 | const { hotelId } = useParams();
11 | const { searchParams } = useContext(SearchContext);
12 | const { checkInDate, checkOutDate } = searchParams;
13 | const isThereDates = checkInDate !== null && checkOutDate !== null;
14 |
15 | const {
16 | hotelDetails,
17 | hotelGuestReviews,
18 | hotelGallery,
19 | hotelAvailableRooms,
20 | isLoading,
21 | error,
22 | } = useHotelData(hotelId, isThereDates, checkInDate, checkOutDate);
23 |
24 | const { snackbar, showErrorSnackbar, handleCloseSnackbar } = useSnackbar();
25 |
26 | useEffect(() => {
27 | if (error) {
28 | showErrorSnackbar(error);
29 | }
30 | }, [error]);
31 |
32 | return (
33 | <>
34 |
35 |
36 |
45 | >
46 | );
47 | };
48 |
49 | export default Hotel;
50 |
--------------------------------------------------------------------------------
/src/pages/Confirmation/Confirmation.jsx:
--------------------------------------------------------------------------------
1 | import { useRef, useEffect } from "react";
2 | import NavBar from "../../components/NavBar";
3 | import { useReactToPrint } from "react-to-print";
4 | import CustomButton from "../../components/CustomButton";
5 | import styles from "./style.module.css";
6 | import ConfirmationTable from "./components/ConfirmationTable";
7 | import CelebrationIcon from "@mui/icons-material/Celebration";
8 | import useCartContext from "../../hooks/useCartContext";
9 | import Footer from "../../components/Footer";
10 |
11 | const Confirmation = () => {
12 | const { clearCart } = useCartContext();
13 | const componentRef = useRef();
14 |
15 | const handlePrint = useReactToPrint({
16 | content: () => componentRef.current,
17 | });
18 |
19 | useEffect(() => {
20 | return () => {
21 | clearCart();
22 | };
23 | }, [clearCart]);
24 |
25 | return (
26 | <>
27 |
28 |
29 |
30 |
31 |
Here is your confirmation details. Hope you enjoy your stay!
32 |
33 |
34 |
35 |
36 |
37 | Print Confirmation
38 |
39 |
40 |
41 | >
42 | );
43 | };
44 |
45 | export default Confirmation;
46 |
--------------------------------------------------------------------------------
/src/pages/SearchPage/components/OptionItem/OptionItem.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from "prop-types";
2 | import styles from "./style.module.css";
3 | import CustomButton from "../../../../components/CustomButton";
4 |
5 | const OptionItem = ({ count, label, onDecrement, onIncrement, min }) => {
6 | const handleDecrement = () => {
7 | if (count > min) {
8 | onDecrement();
9 | }
10 | };
11 |
12 | return (
13 |
14 |
{label}
15 |
16 |
23 | −
24 |
25 | {count}
26 |
32 | +
33 |
34 |
35 |
36 | );
37 | };
38 |
39 | export default OptionItem;
40 |
41 | OptionItem.propTypes = {
42 | count: PropTypes.number.isRequired,
43 | label: PropTypes.string.isRequired,
44 | onDecrement: PropTypes.func.isRequired,
45 | onIncrement: PropTypes.func.isRequired,
46 | min: PropTypes.number,
47 | };
48 |
--------------------------------------------------------------------------------
/src/pages/Hotel/components/HotelDetails/HotelDetails.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from "prop-types";
2 | import HotelHeaderAndDescription from "../HotelHeaderAndDescription";
3 | import HotelReviews from "../HotelReviews";
4 | import HotelAmenities from "../HotelAmenities";
5 |
6 | const HotelDetails = ({ hotelDetails, hotelGuestReviews }) => {
7 | if ({ hotelDetails }.length === 0 || hotelGuestReviews.length === 0) {
8 | return No Data available.
;
9 | }
10 |
11 | return (
12 |
13 |
18 |
19 |
20 |
21 | );
22 | };
23 |
24 | export default HotelDetails;
25 |
26 | HotelDetails.propTypes = {
27 | hotelDetails: PropTypes.shape({
28 | hotelName: PropTypes.string.isRequired,
29 | starRating: PropTypes.number.isRequired,
30 | description: PropTypes.string.isRequired,
31 | amenities: PropTypes.arrayOf(
32 | PropTypes.shape({
33 | name: PropTypes.string.isRequired,
34 | description: PropTypes.string.isRequired,
35 | })
36 | ),
37 | }),
38 | hotelGuestReviews: PropTypes.arrayOf(
39 | PropTypes.shape({
40 | reviewId: PropTypes.number.isRequired,
41 | customerName: PropTypes.string.isRequired,
42 | rating: PropTypes.number.isRequired,
43 | description: PropTypes.string.isRequired,
44 | })
45 | ).isRequired,
46 | };
47 |
--------------------------------------------------------------------------------
/src/pages/Hotel/components/VisualGallery/VisualGallery.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import PropTypes from "prop-types";
3 | import styles from "./style.module.css";
4 | import CloseIcon from "@mui/icons-material/Close";
5 |
6 | const VisualGallery = ({ hotelGallery }) => {
7 | const [selectedImage, setSelectedImage] = useState(null);
8 |
9 | const openFullscreen = (image) => {
10 | setSelectedImage(image);
11 | };
12 |
13 | const closeFullscreen = () => {
14 | setSelectedImage(null);
15 | };
16 |
17 | return (
18 |
19 |
20 | {hotelGallery.map((image) => (
21 |
openFullscreen(image)}
26 | className={styles.thumbnail}
27 | />
28 | ))}
29 |
30 |
31 | {selectedImage && (
32 |
33 |
34 |
35 |
36 |
41 |
42 | )}
43 |
44 | );
45 | };
46 |
47 | export default VisualGallery;
48 |
49 | VisualGallery.propTypes = {
50 | hotelGallery: PropTypes.arrayOf(
51 | PropTypes.shape({
52 | id: PropTypes.number.isRequired,
53 | url: PropTypes.string.isRequired,
54 | })
55 | ).isRequired,
56 | };
57 |
--------------------------------------------------------------------------------
/src/__tests__/SearchService.test.jsx:
--------------------------------------------------------------------------------
1 | import { searchAPI } from "../services/searchService";
2 | import axiosInstance from "../Axios/axiosInstance";
3 |
4 | jest.mock("../Axios/axiosInstance", () => ({
5 | get: jest.fn(),
6 | }));
7 |
8 | describe("searchAPI", () => {
9 | it("fetches search results based on parameters", async () => {
10 | const responseData = [
11 | {
12 | hotelId: 1,
13 | hotelName: "Plaza Hotel",
14 | starRating: 5,
15 | latitude: 12.32342342,
16 | longitude: 32.23245675,
17 | roomPrice: 100.0,
18 | roomType: "Double",
19 | cityName: "Ramallah",
20 | roomPhotoUrl:
21 | "https://images.pexels.com/photos/164595/pexels-photo-164595.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1",
22 | discount: 0.2,
23 | amenities: [
24 | {
25 | id: 0,
26 | name: "wifi",
27 | description: "Very fast wifi in the room.",
28 | },
29 | {
30 | id: 1,
31 | name: "Room Service",
32 | description: "Very Fast room service available.",
33 | },
34 | ],
35 | },
36 | ];
37 |
38 | axiosInstance.get.mockResolvedValueOnce({ data: responseData });
39 |
40 | const searchParams = {
41 | city: "Ramallah",
42 | checkInDate: "2024-01-31",
43 | checkOutDate: "2024-02-01",
44 | adults: 2,
45 | children: 0,
46 | numberOfRooms: 1,
47 | };
48 |
49 | const data = await searchAPI(searchParams);
50 |
51 | expect(axiosInstance.get).toHaveBeenCalledWith(`/home/search`, {
52 | params: searchParams,
53 | });
54 |
55 | expect(data).toEqual(responseData);
56 | });
57 | });
58 |
--------------------------------------------------------------------------------
/src/components/NavBar/NavBar.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { useTheme } from "@mui/material/styles";
3 | import useMediaQuery from "@mui/material/useMediaQuery";
4 | import { AppBar, IconButton } from "@mui/material";
5 | import { Menu as MenuIcon } from "@mui/icons-material";
6 | import colors from "../../constants/colorConstants";
7 | import AppBarComponent from "./AppBarComponent";
8 | import DrawerComponent from "./DrawerComponent";
9 |
10 | const NavBar = () => {
11 | const theme = useTheme();
12 | const isSmallScreen = useMediaQuery(theme?.breakpoints?.down("sm"));
13 | const [isMobileOpened, setIsMobileOpened] = useState(false);
14 |
15 | const handleDrawerToggle = () => {
16 | setIsMobileOpened(!isMobileOpened);
17 | };
18 |
19 | return (
20 | <>
21 | {isSmallScreen ? (
22 | <>
23 |
27 |
28 |
35 |
44 |
45 |
46 |
47 | >
48 | ) : (
49 |
50 | )}
51 | >
52 | );
53 | };
54 |
55 | export default NavBar;
56 |
--------------------------------------------------------------------------------
/src/App.jsx:
--------------------------------------------------------------------------------
1 | import Login from "./pages/Login";
2 | import { Routes, Route, Navigate } from "react-router-dom";
3 | import PageNotFound from "./pages/PageNotFound";
4 | import Home from "./pages/Home";
5 | import SearchPage from "./pages/SearchPage";
6 | import Hotel from "./pages/Hotel";
7 | import Checkout from "./pages/Checkout";
8 | import Confirmation from "./pages/Confirmation";
9 | import Cities from "./pages/Admin/pages/Cities";
10 | import Hotels from "./pages/Admin/pages/Hotels";
11 | import Rooms from "./pages/Admin/pages/Rooms/Rooms";
12 | import ProtectedRoutes from "./routes/ProtectedRoutes";
13 |
14 | function App() {
15 | return (
16 |
17 | } />
18 | } />
19 |
20 | }>
21 | } />
22 | } />
23 | } />
24 | } />
25 | } />
26 |
27 |
28 | }>
29 | }
32 | />
33 | } />
34 | } />
35 | } />
36 |
37 |
38 | } />
39 |
40 | );
41 | }
42 |
43 | export default App;
44 |
--------------------------------------------------------------------------------
/src/pages/Admin/components/CreateEntityDialog/CreateEntityDialog.jsx:
--------------------------------------------------------------------------------
1 | import { DialogContent, Button, TextField, DialogActions } from "@mui/material";
2 | import { Form, Field } from "formik";
3 | import PropTypes from "prop-types";
4 |
5 | const CreateEntityDialog = ({
6 | fields,
7 | touched,
8 | errors,
9 | handleDialogClose,
10 | isSubmitting,
11 | }) => {
12 | return (
13 |
40 | );
41 | };
42 |
43 | export default CreateEntityDialog;
44 |
45 | CreateEntityDialog.propTypes = {
46 | fields: PropTypes.arrayOf(
47 | PropTypes.shape({
48 | name: PropTypes.string.isRequired,
49 | label: PropTypes.string.isRequired,
50 | type: PropTypes.string,
51 | })
52 | ),
53 | touched: PropTypes.object,
54 | errors: PropTypes.object,
55 | handleDialogClose: PropTypes.func,
56 | isSubmitting: PropTypes.bool,
57 | };
58 |
--------------------------------------------------------------------------------
/src/pages/Hotel/components/VisualGallery/style.module.css:
--------------------------------------------------------------------------------
1 | .thumbnailContainer {
2 | display: flex;
3 | flex-wrap: wrap;
4 | gap: 10px;
5 | justify-content: center;
6 | }
7 |
8 | .thumbnail {
9 | max-width: 180px;
10 | height: auto;
11 | cursor: pointer;
12 | object-fit: cover;
13 | transition: filter 0.2s ease;
14 | }
15 |
16 | .thumbnail:hover {
17 | filter: brightness(90%);
18 | }
19 |
20 | .fullscreenOverlay {
21 | position: fixed;
22 | top: 0;
23 | left: 0;
24 | width: 100%;
25 | height: 100%;
26 | background-color: rgba(0, 0, 0, 0.8);
27 | display: flex;
28 | justify-content: center;
29 | align-items: center;
30 | cursor: pointer;
31 | z-index: 1000;
32 | overflow: hidden;
33 | }
34 |
35 | .fullscreenImage {
36 | max-width: 95%;
37 | max-height: 95%;
38 | margin-top: 0.3rem;
39 | object-fit: contain;
40 | }
41 |
42 | .closeButton {
43 | position: absolute;
44 | top: 20px;
45 | right: 20px;
46 | background: none;
47 | border: none;
48 | font-size: 1.5rem;
49 | color: #fff;
50 | cursor: pointer;
51 | outline: none;
52 | }
53 |
54 | @media screen and (max-width: 450px) {
55 | .thumbnailContainer {
56 | margin-top: 2rem;
57 | }
58 |
59 | .thumbnail {
60 | max-width: 165px;
61 | }
62 | }
63 |
64 | @media screen and (min-width: 320px) and (max-width: 389px) {
65 | .thumbnailContainer {
66 | margin-top: 2rem;
67 | }
68 |
69 | .thumbnail {
70 | max-width: 150px;
71 | }
72 | }
73 |
74 | @media screen and (max-width: 400px) {
75 | .thumbnailContainer {
76 | margin-top: 2rem;
77 | }
78 |
79 | .thumbnail {
80 | max-width: 120px;
81 | }
82 | }
--------------------------------------------------------------------------------
/src/pages/Admin/components/SearchBar/SearchBar.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import {
3 | Paper,
4 | InputBase,
5 | IconButton,
6 | FormControl,
7 | Select,
8 | MenuItem,
9 | } from "@mui/material";
10 | import SearchIcon from "@mui/icons-material/Search";
11 | import PropTypes from "prop-types";
12 | import styles from "./style.module.css";
13 |
14 | const SearchBar = ({ onSearch }) => {
15 | const [searchTerm, setSearchTerm] = useState("");
16 | const [searchType, setSearchType] = useState("name");
17 |
18 | const handleSearch = (event) => {
19 | event.preventDefault();
20 | onSearch(searchTerm, searchType);
21 | };
22 |
23 | return (
24 |
29 |
30 | setSearchType(e.target.value)}
33 | inputProps={{ "aria-label": "Without label" }}
34 | >
35 | Name
36 | Description
37 |
38 |
39 |
40 | setSearchTerm(e.target.value)}
46 | />
47 |
48 |
49 |
50 |
51 |
52 | );
53 | };
54 |
55 | export default SearchBar;
56 |
57 | SearchBar.propTypes = {
58 | onSearch: PropTypes.func,
59 | };
60 |
--------------------------------------------------------------------------------
/src/context/authContext.jsx:
--------------------------------------------------------------------------------
1 | import { createContext, useState, useEffect, useCallback } from "react";
2 | import PropTypes from "prop-types";
3 | import { useCookies } from "react-cookie";
4 | import axiosInstance from "../Axios/axiosInstance";
5 |
6 | export const AuthContext = createContext();
7 |
8 | const AuthContextProvider = ({ children }) => {
9 | const [cookies, setCookie, removeCookie] = useCookies(["auth"]);
10 | const [user, setUser] = useState(cookies["auth"] || null);
11 |
12 | const loginUser = (authData) => {
13 | setCookie("auth", authData);
14 | setUser(authData);
15 | };
16 |
17 | const logoutUser = useCallback(() => {
18 | setUser(null);
19 | removeCookie("auth");
20 | }, [removeCookie]);
21 |
22 | if (user) {
23 | axiosInstance.defaults.headers.common[
24 | "Authorization"
25 | ] = `Bearer ${user.authentication}`;
26 | } else {
27 | delete axiosInstance.defaults.headers.common["Authorization"];
28 | }
29 |
30 | console.log("user", user);
31 |
32 | useEffect(() => {
33 | const interceptor = axiosInstance.interceptors.response.use(
34 | (response) => response,
35 | (error) => {
36 | // unauthorized error
37 | if (error.response && error.response.status === 401) {
38 | logoutUser();
39 | }
40 | return Promise.reject(error);
41 | }
42 | );
43 |
44 | return () => {
45 | axiosInstance.interceptors.response.eject(interceptor);
46 | };
47 | }, [logoutUser]);
48 |
49 | return (
50 |
51 | {children}
52 |
53 | );
54 | };
55 |
56 | export default AuthContextProvider;
57 |
58 | AuthContextProvider.propTypes = {
59 | children: PropTypes.node.isRequired,
60 | };
61 |
--------------------------------------------------------------------------------
/src/pages/Home/components/RecentlyVisitedHotels/RecentlyVisitedHotels.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 | import { recentlyVisitedHotelsAPI } from "../../../../services/homePageServices";
3 | import useValueFromToken from "../../../../hooks/useValueFromToken";
4 | import useSnackbar from "../../../../hooks/useSnackbar";
5 | import { useNavigate } from "react-router-dom";
6 | import RecentlyVisitedHotelsWithLoading from "./RecentlyVisitedHotelsWithLoading";
7 | import useComponentLoader from "../../../../hooks/useComponentLoader";
8 |
9 | export const RecentlyVisitedHotels = () => {
10 | const { isLoading, stopLoading } = useComponentLoader();
11 | const { snackbar, showErrorSnackbar, handleCloseSnackbar } = useSnackbar();
12 | const [recentHotels, setRecentHotels] = useState([]);
13 |
14 | const userId = useValueFromToken("user_id");
15 | const navigate = useNavigate();
16 |
17 | const handleFetchRecentlyVisitedHotels = async () => {
18 | try {
19 | const recentHotelsData = await recentlyVisitedHotelsAPI(userId);
20 | setRecentHotels(recentHotelsData);
21 | } catch (error) {
22 | showErrorSnackbar(
23 | "Failed to fetch recently visited hotels. Please try again later."
24 | );
25 | } finally {
26 | stopLoading();
27 | }
28 | };
29 |
30 | useEffect(() => {
31 | handleFetchRecentlyVisitedHotels();
32 | }, []);
33 |
34 | const handleNavigation = (hotelId) => {
35 | navigate(`/hotel/${hotelId}`);
36 | };
37 |
38 | const lastVisitedHotels = recentHotels.slice(1, 5);
39 |
40 | return (
41 |
48 | );
49 | };
50 |
51 | export default RecentlyVisitedHotels;
52 |
--------------------------------------------------------------------------------
/src/services/manageCities.js:
--------------------------------------------------------------------------------
1 | import axiosInstance from "../Axios/axiosInstance";
2 |
3 | export const getAllCities = async () => {
4 | try {
5 | const response = await axiosInstance.get(`/cities`);
6 | return response.data;
7 | } catch (error) {
8 | throw new Error(
9 | error.response?.data.message || "Error: Can't get the cities"
10 | );
11 | }
12 | };
13 |
14 | export const searchCities = async (values) => {
15 | try {
16 | const response = await axiosInstance.get(`/cities`, {
17 | params: {
18 | ...values,
19 | },
20 | });
21 | return response.data;
22 | } catch (error) {
23 | throw new Error(
24 | error.response?.data.message || "Error: Can't get the cities"
25 | );
26 | }
27 | };
28 |
29 | export const postNewCity = async (cityName, cityDescription) => {
30 | try {
31 | const response = await axiosInstance.post(`/cities`, {
32 | name: cityName,
33 | description: cityDescription,
34 | });
35 | return response.data;
36 | } catch (error) {
37 | throw new Error(
38 | error.response?.data.message || "Error: Can't create a new city"
39 | );
40 | }
41 | };
42 |
43 | export const updateCity = async (cityId, cityName, cityDescription) => {
44 | try {
45 | const response = await axiosInstance.put(`/cities/${cityId}`, {
46 | name: cityName,
47 | description: cityDescription,
48 | });
49 | return response;
50 | } catch (error) {
51 | throw new Error(
52 | error.response?.data.message || "Error: Can't update the city"
53 | );
54 | }
55 | };
56 |
57 | export const deleteCity = async (cityId) => {
58 | try {
59 | const response = await axiosInstance.delete(`/cities/${cityId}`);
60 | return response;
61 | } catch (error) {
62 | throw new Error(
63 | error.response?.data.message || "Error: Can't delete the city"
64 | );
65 | }
66 | };
67 |
--------------------------------------------------------------------------------
/src/components/NavBar/DrawerComponent.jsx:
--------------------------------------------------------------------------------
1 | import {
2 | Box,
3 | Divider,
4 | Drawer,
5 | List,
6 | ListItem,
7 | ListItemButton,
8 | ListItemText,
9 | Typography,
10 | } from "@mui/material";
11 | import { useContext } from "react";
12 | import { useNavigate } from "react-router-dom";
13 | import { AuthContext } from "../../context/authContext";
14 | import PropTypes from "prop-types";
15 |
16 | const DrawerComponent = ({ isMobileOpened, handleDrawerToggle }) => {
17 | const { logoutUser } = useContext(AuthContext);
18 | const navItems = ["Home", "Search", "Cart"];
19 | const navigate = useNavigate();
20 |
21 | const handleNavigate = (item) => {
22 | navigate(`/${item.toLowerCase()}`);
23 | };
24 |
25 | return (
26 |
32 |
33 |
34 | Booking
35 |
36 |
37 |
38 |
39 |
40 | {navItems.map((item) => (
41 |
42 | handleNavigate(item)}
45 | >
46 |
47 |
48 |
49 | ))}
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 | );
58 | };
59 |
60 | DrawerComponent.propTypes = {
61 | isMobileOpened: PropTypes.bool.isRequired,
62 | handleDrawerToggle: PropTypes.func.isRequired,
63 | };
64 |
65 | export default DrawerComponent;
66 |
--------------------------------------------------------------------------------
/src/__tests__/Home.test.jsx:
--------------------------------------------------------------------------------
1 | import { render, waitFor, screen } from "@testing-library/react";
2 | import Home from "../pages/Home";
3 | import { AuthContext } from "../context/authContext";
4 | import { BrowserRouter } from "react-router-dom";
5 |
6 | jest.mock("../components/NavBar", () => () => NavBar
);
7 | jest.mock("../pages/Home/components/HeroSection", () => () => (
8 | HeroSection
9 | ));
10 | jest.mock("../pages/SearchPage/components/SearchBar", () => () => (
11 | SearchBar
12 | ));
13 | jest.mock("../pages/Home/components/FeaturedDeals", () => () => (
14 | FeaturedDeals
15 | ));
16 | jest.mock("../pages/Home/components/TrendingDestinations", () => () => (
17 | TrendingDestinations
18 | ));
19 | jest.mock("../pages/Home/components/RecentlyVisitedHotels", () => () => (
20 | RecentlyVisitedHotels
21 | ));
22 | jest.mock("../components/Footer", () => () => Footer
);
23 |
24 | describe("Home Component", () => {
25 | it("renders the home page after successful login", async () => {
26 | const authContextValue = {
27 | user: {},
28 | loginUser: jest.fn(),
29 | logoutUser: jest.fn(),
30 | };
31 |
32 | render(
33 |
34 |
35 |
36 |
37 |
38 | );
39 |
40 | await waitFor(() => {
41 | expect(screen.getByText("NavBar")).toBeInTheDocument();
42 | expect(screen.getByText("HeroSection")).toBeInTheDocument();
43 | expect(screen.getByText("SearchBar")).toBeInTheDocument();
44 | expect(screen.getByText("FeaturedDeals")).toBeInTheDocument();
45 | expect(screen.getByText("TrendingDestinations")).toBeInTheDocument();
46 | expect(screen.getByText("RecentlyVisitedHotels")).toBeInTheDocument();
47 | expect(screen.getByText("Footer")).toBeInTheDocument();
48 | });
49 | });
50 | });
51 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "travel-booking-platform",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "vite build",
9 | "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
10 | "preview": "vite preview",
11 | "test": "jest"
12 | },
13 | "dependencies": {
14 | "@emotion/react": "^11.11.1",
15 | "@emotion/styled": "^11.11.0",
16 | "@mui/icons-material": "^5.15.0",
17 | "@mui/material": "^5.15.0",
18 | "axios": "^1.6.2",
19 | "dayjs": "^1.11.10",
20 | "formik": "^2.4.5",
21 | "jwt-decode": "^4.0.0",
22 | "leaflet": "^1.9.4",
23 | "react": "^18.2.0",
24 | "react-cookie": "^6.1.1",
25 | "react-date-range": "^1.4.0",
26 | "react-dom": "^18.2.0",
27 | "react-leaflet": "^4.2.1",
28 | "react-router-dom": "^6.21.0",
29 | "react-slick": "^0.29.0",
30 | "react-to-print": "^2.14.15",
31 | "slick-carousel": "^1.8.1",
32 | "yup": "^1.3.3"
33 | },
34 | "devDependencies": {
35 | "@babel/preset-env": "^7.23.9",
36 | "@babel/preset-react": "^7.23.3",
37 | "@testing-library/jest-dom": "^6.4.0",
38 | "@testing-library/react": "^14.1.2",
39 | "@testing-library/user-event": "^14.5.2",
40 | "@vitejs/plugin-react": "^4.2.1",
41 | "eslint": "^8.55.0",
42 | "eslint-plugin-react": "^7.33.2",
43 | "eslint-plugin-react-hooks": "^4.6.0",
44 | "eslint-plugin-react-refresh": "^0.4.5",
45 | "identity-obj-proxy": "^3.0.0",
46 | "jest": "^29.7.0",
47 | "jest-environment-jsdom": "^29.7.0",
48 | "jest-svg-transformer": "^1.0.0",
49 | "vite": "^5.0.8"
50 | },
51 | "jest": {
52 | "testEnvironment": "jsdom",
53 | "moduleNameMapper": {
54 | "^.+\\.svg$": "jest-svg-transformer",
55 | "^.+\\.(css|less|scss)$": "identity-obj-proxy"
56 | },
57 | "setupFilesAfterEnv": [
58 | "/setupTests.js"
59 | ]
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/context/CartContext.jsx:
--------------------------------------------------------------------------------
1 | import { createContext, useState, useEffect } from "react";
2 | import PropTypes from "prop-types";
3 |
4 | export const CartContext = createContext();
5 |
6 | const CartContextProvider = ({ children }) => {
7 | const [cart, setCart] = useState(() => {
8 | const localCart = localStorage.getItem("cart");
9 | return localCart ? JSON.parse(localCart) : [];
10 | });
11 |
12 | const clearCart = () => {
13 | setCart([]);
14 | localStorage.removeItem("cart");
15 | };
16 |
17 | useEffect(() => {
18 | localStorage.setItem("cart", JSON.stringify(cart));
19 | }, [cart]);
20 |
21 | const isRoomAlreadyInCart = (roomId) => {
22 | return cart.some((item) => item.roomId === roomId);
23 | };
24 |
25 | const addToCart = (room) => {
26 | (cart.length === 0 || !isRoomAlreadyInCart(room.roomId)) &&
27 | setCart((prevCart) => {
28 | const updatedCart = [...prevCart, room];
29 | localStorage.setItem("cart", JSON.stringify(updatedCart));
30 | return updatedCart;
31 | });
32 | };
33 |
34 | const removeFromCart = (roomId) => {
35 | setCart((prevCart) => {
36 | const updatedCart = prevCart.filter((room) => room.roomId !== roomId);
37 | localStorage.setItem("cart", JSON.stringify(updatedCart));
38 | return updatedCart;
39 | });
40 | };
41 |
42 | const getCartTotalPrice = () => {
43 | const totalCost = cart.reduce((total, room) => total + room.price, 0);
44 | return {
45 | totalCost,
46 | };
47 | };
48 |
49 | return (
50 |
60 | {children}
61 |
62 | );
63 | };
64 |
65 | export default CartContextProvider;
66 |
67 | CartContextProvider.propTypes = {
68 | children: PropTypes.node.isRequired,
69 | };
70 |
--------------------------------------------------------------------------------
/src/components/Footer/Footer.jsx:
--------------------------------------------------------------------------------
1 | import { Box, Typography, IconButton, Link } from "@mui/material";
2 | import GitHubIcon from "@mui/icons-material/GitHub";
3 | import LinkedInIcon from "@mui/icons-material/LinkedIn";
4 | import EmailIcon from "@mui/icons-material/Email";
5 | import colors from "../../constants/colorConstants";
6 |
7 | const Footer = () => {
8 | const iconLinks = [
9 | {
10 | icon: ,
11 | href: "https://github.com/Rahaf-Mansour",
12 | label: "GitHub",
13 | },
14 | {
15 | icon: ,
16 | href: "https://www.linkedin.com/in/rahafmansour/",
17 | label: "LinkedIn",
18 | },
19 | {
20 | icon: ,
21 | href: "mailto:rahafmansour2018@gmail.com",
22 | label: "Email",
23 | },
24 | ];
25 |
26 | return (
27 |
39 |
40 | Developed by Rahaf Mansour
41 |
42 |
43 | All rights reserved © {new Date().getFullYear()}
44 |
45 |
52 | {iconLinks.map((link, index) => (
53 |
61 | {link.icon}
62 |
63 | ))}
64 |
65 |
66 | );
67 | };
68 |
69 | export default Footer;
70 |
--------------------------------------------------------------------------------
/src/pages/SearchPage/components/DateCheck/DateCheck.jsx:
--------------------------------------------------------------------------------
1 | import { DateRange } from "react-date-range";
2 | import { useState } from "react";
3 | import "react-date-range/dist/styles.css";
4 | import "react-date-range/dist/theme/default.css";
5 | import DateRangeIcon from "@mui/icons-material/DateRange";
6 | import styles from "./style.module.css";
7 | import dayjs from "dayjs";
8 | import PropTypes from "prop-types";
9 | import CustomButton from "../../../../components/CustomButton";
10 |
11 | const DateCheck = ({ handleSetDate, isDateOpened, toggleDate, dateValues }) => {
12 | const [date, setDate] = useState([
13 | {
14 | startDate: dayjs(dateValues.checkInDate).toDate(),
15 | endDate: dayjs(dateValues.checkOutDate).toDate(),
16 | key: "selection",
17 | },
18 | ]);
19 |
20 | const handleChangeDate = (newDate) => {
21 | try {
22 | setDate([newDate.selection]);
23 | handleSetDate(newDate.selection);
24 | } catch (error) {
25 | console.error("Error updating date:", error);
26 | }
27 | };
28 |
29 | return (
30 | <>
31 |
36 |
37 |
38 | {`${dayjs(date[0].startDate).format("YYYY-MM-DD")} - ${dayjs(
39 | date[0].endDate
40 | ).format("YYYY-MM-DD")}`}
41 |
42 |
43 |
44 | {isDateOpened && (
45 | handleChangeDate(newDate)}
48 | moveRangeOnFirstSelection={false}
49 | ranges={date}
50 | className={styles.datePicker}
51 | />
52 | )}
53 | >
54 | );
55 | };
56 |
57 | export default DateCheck;
58 |
59 | DateCheck.propTypes = {
60 | handleSetDate: PropTypes.func,
61 | isDateOpened: PropTypes.bool,
62 | toggleDate: PropTypes.func,
63 | dateValues: PropTypes.object,
64 | };
65 |
--------------------------------------------------------------------------------
/src/pages/Hotel/components/HotelReviews/HotelReviews.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import PropTypes from "prop-types";
3 | import styles from "./style.module.css";
4 | import CustomButton from "../../../../components/CustomButton";
5 |
6 | const HotelReviews = ({ hotelGuestReviews }) => {
7 | const [visibleReviews, setVisibleReviews] = useState(3);
8 |
9 | const loadMoreReviews = () => {
10 | setVisibleReviews((prevVisibleReviews) => prevVisibleReviews + 3);
11 | };
12 |
13 | const loadLessReviews = () => {
14 | setVisibleReviews(3);
15 | };
16 |
17 | const sortedReviews = hotelGuestReviews
18 | .slice()
19 | .sort((a, b) => b.rating - a.rating);
20 |
21 | return (
22 |
23 |
What guests loved the most:
24 |
25 | {sortedReviews.slice(0, visibleReviews).map((review) => (
26 |
27 |
28 |
{review.customerName}
29 |
Rating: {review.rating}
30 |
31 |
{review.description}
32 |
33 | ))}
34 |
35 |
36 |
44 | {visibleReviews < hotelGuestReviews.length
45 | ? "Load More"
46 | : "Load Less"}
47 |
48 |
49 |
50 | );
51 | };
52 |
53 | export default HotelReviews;
54 |
55 | HotelReviews.propTypes = {
56 | hotelGuestReviews: PropTypes.arrayOf(
57 | PropTypes.shape({
58 | reviewId: PropTypes.number.isRequired,
59 | customerName: PropTypes.string.isRequired,
60 | rating: PropTypes.number.isRequired,
61 | description: PropTypes.string.isRequired,
62 | })
63 | ).isRequired,
64 | };
65 |
--------------------------------------------------------------------------------
/src/pages/SearchPage/components/SearchBar/style.module.css:
--------------------------------------------------------------------------------
1 | .parent {
2 | display: flex;
3 | justify-content: center;
4 | position: relative;
5 | }
6 |
7 | .searchIcon {
8 | color: var(--main-color);
9 | margin-right: 5px;
10 | }
11 |
12 | .searchInput {
13 | border: none;
14 | outline: none;
15 | color: black;
16 | font-size: 1rem;
17 | width: 100%;
18 | margin-left: -10px;
19 | }
20 |
21 | .searchText {
22 | font-size: 1rem;
23 | color: black;
24 | cursor: pointer;
25 | display: flex;
26 | align-items: center;
27 | outline: none;
28 | border: none;
29 | background-color: transparent;
30 | }
31 |
32 | .searchButton {
33 | background-color: var(--secondary-color);
34 | border: none;
35 | outline: none;
36 | padding: 10px 20px;
37 | border-radius: 10px;
38 | color: white;
39 | font-size: 1rem;
40 | cursor: pointer;
41 | }
42 |
43 | .searchButton a {
44 | color: white;
45 | text-decoration: none;
46 | }
47 |
48 | .searchButton:hover {
49 | background-color: var(--primary-color);
50 | }
51 |
52 | .options {
53 | z-index: 9;
54 | position: absolute;
55 | top: 60px;
56 | background-color: white;
57 | color: var(--main-color);
58 | border-radius: 5px;
59 | box-shadow: 0px 0px 10px -3px rgba(19, 16, 16, 0.808);
60 | }
61 |
62 | @media (max-width: 480px) {
63 | .heroSearch {
64 | bottom: -200px;
65 | }
66 |
67 | .heroSearchInput,
68 | .heroSearchText,
69 | .heroSearchButton {
70 | font-size: 0.8rem;
71 | }
72 | }
73 |
74 | @media (max-width: 768px) {
75 | .heroSearch {
76 | flex-direction: column;
77 | padding: 10px;
78 | bottom: -200px;
79 | }
80 |
81 | .heroSearchItem {
82 | width: 100%;
83 | justify-content: space-evenly;
84 | margin-bottom: 10px;
85 | }
86 |
87 | .heroSearchButton {
88 | width: 100%;
89 | }
90 |
91 | .options {
92 | top: 110px;
93 | }
94 | }
95 |
96 | @media (max-width: 1200px) {
97 | .options {
98 | top: 110px;
99 | }
100 | }
--------------------------------------------------------------------------------
/src/services/manageHotels.js:
--------------------------------------------------------------------------------
1 | import axiosInstance from "../Axios/axiosInstance";
2 |
3 | export const getAllHotels = async () => {
4 | try {
5 | const response = await axiosInstance.get(`/hotels`);
6 | return response.data;
7 | } catch (error) {
8 | throw new Error(
9 | error.response?.data.message || "Error: Can't get the hotels"
10 | );
11 | }
12 | };
13 |
14 | export const getHotelInfoByItsId = async (hotelId) => {
15 | try {
16 | const response = await axiosInstance.get(`/hotels/${hotelId}`);
17 | return response.data;
18 | } catch (error) {
19 | throw new Error(
20 | error.response?.data.message ||
21 | "Error: Can't get the specific hotel's data"
22 | );
23 | }
24 | };
25 |
26 | export const postNewHotel = async (
27 | cityId,
28 | name,
29 | description,
30 | hotelType,
31 | starRating,
32 | latitude,
33 | longitude
34 | ) => {
35 | try {
36 | const response = await axiosInstance.post(`/cities/${cityId}/hotels`, {
37 | name,
38 | description,
39 | hotelType,
40 | starRating,
41 | latitude,
42 | longitude,
43 | });
44 | return response.data;
45 | } catch (error) {
46 | throw new Error(
47 | error.response?.data.message || "Error: Can't create a new hotel"
48 | );
49 | }
50 | };
51 |
52 | export const updateHotel = async (
53 | hotelId,
54 | name,
55 | description,
56 | hotelType,
57 | starRating,
58 | latitude,
59 | longitude
60 | ) => {
61 | try {
62 | const response = await axiosInstance.put(`/hotels/${hotelId}`, {
63 | name,
64 | description,
65 | hotelType,
66 | starRating,
67 | latitude,
68 | longitude,
69 | });
70 | return response;
71 | } catch (error) {
72 | throw new Error(
73 | error.response?.data.message || "Error: Can't update the hotel"
74 | );
75 | }
76 | };
77 |
78 | export const deleteHotel = async (cityId, hotelId) => {
79 | try {
80 | const response = await axiosInstance.delete(
81 | `/cities/${cityId}/hotels/${hotelId}`
82 | );
83 | return response;
84 | } catch (error) {
85 | throw new Error(
86 | error.response?.data.message || "Error: Can't delete the hotel"
87 | );
88 | }
89 | };
90 |
--------------------------------------------------------------------------------
/src/pages/Login/style.module.css:
--------------------------------------------------------------------------------
1 | .loginPage {
2 | background: linear-gradient(90deg, #b9c0d2 0%, #243b69 100%);
3 | font-family: "Comfortaa", cursive;
4 | height: 100vh;
5 | display: flex;
6 | justify-content: center;
7 | align-items: center;
8 | }
9 |
10 | .loginForm {
11 | background: #fff;
12 | padding: 40px;
13 | border-radius: 10px;
14 | max-width: 360px;
15 | text-align: center;
16 | width: 90%;
17 | }
18 |
19 | .loginForm h1 {
20 | font-size: 40px;
21 | color: var(--primary-color);
22 | margin-bottom: 25px;
23 | }
24 |
25 | .loginForm input {
26 | background: #f2f2f2;
27 | width: 100%;
28 | border: none;
29 | border-radius: 5px;
30 | margin: 0 0 15px;
31 | padding: 15px;
32 | font-size: 14px;
33 | }
34 |
35 | .loginForm input:focus {
36 | background: #dbdbdb;
37 | }
38 |
39 | .errorMessage {
40 | margin: -10px 0px 15px 2px;
41 | text-align: left;
42 | font-size: 14px;
43 | color: red;
44 | font-weight: 600;
45 | font-family: Source Sans Pro, sans-serif;
46 | }
47 |
48 | .loginForm button {
49 | font-size: 14px;
50 | font-weight: bold;
51 | text-transform: uppercase;
52 | width: 100%;
53 | border: none;
54 | border-radius: 5px;
55 | padding: 15px;
56 | color: #fff;
57 | transition: all 0.3s ease;
58 | }
59 |
60 | :global(.defaultBtn) {
61 | background-color: var(--tertiary-color);
62 | }
63 |
64 | :global(.activeBtn) {
65 | background-color: var(--secondary-color);
66 | cursor: pointer;
67 | }
68 |
69 | :global(.activeBtn:hover) {
70 | background-color: var(--primary-color);
71 | }
72 |
73 | @media (max-width: 600px) {
74 |
75 | .loginForm {
76 | width: 85%;
77 | padding: 20px;
78 | margin: 30% auto;
79 | }
80 |
81 | .loginForm h1 {
82 | font-size: 32px;
83 | }
84 |
85 | .loginForm input,
86 | .loginForm button {
87 | padding: 10px;
88 | }
89 | }
90 |
91 | @media (max-width: 400px) {
92 | .loginForm {
93 | width: 75%;
94 | padding: 20px;
95 | }
96 |
97 | .loginForm h1 {
98 | font-size: 28px;
99 | }
100 | }
--------------------------------------------------------------------------------
/src/components/NavBar/AppBarComponent.jsx:
--------------------------------------------------------------------------------
1 | import { useContext } from "react";
2 | import { Link } from "react-router-dom";
3 | import { AppBar, Badge, Button, Toolbar, Typography } from "@mui/material";
4 | import {
5 | ShoppingCart as ShoppingCartIcon,
6 | Home as HomeIcon,
7 | Search as SearchIcon,
8 | Logout as LogoutIcon,
9 | } from "@mui/icons-material";
10 | import { CartContext } from "../../context/CartContext";
11 | import { AuthContext } from "../../context/authContext";
12 | import colors from "../../constants/colorConstants";
13 | import ButtonLink from "./ButtonLink";
14 |
15 | const AppBarComponent = () => {
16 | const { cart } = useContext(CartContext);
17 | const { logoutUser } = useContext(AuthContext);
18 |
19 | return (
20 |
25 |
32 |
42 | Booking
43 |
44 |
45 |
46 | } text="Home" />
47 | } text="Search" />
48 |
52 |
53 |
54 | }
55 | />
56 |
57 |
58 |
62 | Logout
63 |
64 |
65 |
66 |
67 |
68 | );
69 | };
70 |
71 | export default AppBarComponent;
72 |
--------------------------------------------------------------------------------
/src/pages/Home/components/FeaturedDeals/DealCard.jsx:
--------------------------------------------------------------------------------
1 | import styles from "./style.module.css";
2 | import StarRating from "../../../../components/StarRating";
3 | import { useNavigate } from "react-router-dom";
4 | import { getHotelDetails } from "../../../../services/hotelPageServices";
5 | import LocationOnIcon from "@mui/icons-material/LocationOn";
6 | import PropTypes from "prop-types";
7 |
8 | const DealCard = ({ deal }) => {
9 | const {
10 | hotelId,
11 | roomPhotoUrl,
12 | hotelName,
13 | cityName,
14 | hotelStarRating,
15 | originalRoomPrice,
16 | finalPrice,
17 | } = deal;
18 |
19 | const navigate = useNavigate();
20 |
21 | const handleDealClick = async () => {
22 | try {
23 | await getHotelDetails(hotelId);
24 | navigate(`/hotel/${hotelId}`);
25 | } catch (error) {
26 | console.error("Error fetching hotel details:", error);
27 | }
28 | };
29 |
30 | return (
31 |
32 |
33 |
34 |
35 |
{hotelName}
36 |
37 |
38 |
39 |
40 |
43 | {cityName}
44 |
45 |
46 |
${originalRoomPrice}
47 |
${finalPrice}
48 |
49 |
50 |
51 |
52 | );
53 | };
54 |
55 | export default DealCard;
56 |
57 | DealCard.propTypes = {
58 | deal: PropTypes.shape({
59 | hotelId: PropTypes.number.isRequired,
60 | roomPhotoUrl: PropTypes.string.isRequired,
61 | hotelName: PropTypes.string.isRequired,
62 | cityName: PropTypes.string.isRequired,
63 | hotelStarRating: PropTypes.number.isRequired,
64 | originalRoomPrice: PropTypes.number.isRequired,
65 | finalPrice: PropTypes.number.isRequired,
66 | }).isRequired,
67 | };
68 |
--------------------------------------------------------------------------------
/src/pages/Hotel/HotelWithoutLoading.jsx:
--------------------------------------------------------------------------------
1 | import Footer from "../../components/Footer";
2 | import HotelDetails from "./components/HotelDetails";
3 | import HotelMapLocation from "./components/HotelMapLocation";
4 | import AvailableRooms from "./components/AvailableRooms";
5 | import VisualGallery from "./components/VisualGallery";
6 | import GenericSnackbar from "../../components/GenericSnackbar";
7 | import styles from "./style.module.css";
8 | import PropTypes from "prop-types";
9 |
10 | const HotelWithoutLoading = ({
11 | hotelDetails,
12 | hotelGallery,
13 | hotelAvailableRooms,
14 | hotelGuestReviews,
15 | error,
16 | snackbar,
17 | handleCloseSnackbar,
18 | }) => {
19 | return (
20 | <>
21 | {hotelDetails && (
22 |
23 | {error && (
24 |
25 | Failed to fetch. please try again later.
26 |
27 | )}
28 |
29 |
30 |
31 |
35 | {hotelDetails && (
36 |
42 | )}
43 |
44 |
45 |
49 |
50 |
51 |
52 | )}
53 |
54 |
55 | >
56 | );
57 | };
58 |
59 | export default HotelWithoutLoading;
60 |
61 | HotelWithoutLoading.propTypes = {
62 | hotelDetails: PropTypes.object,
63 | hotelGallery: PropTypes.array,
64 | hotelAvailableRooms: PropTypes.array,
65 | hotelGuestReviews: PropTypes.array,
66 | error: PropTypes.string,
67 | snackbar: PropTypes.object.isRequired,
68 | handleCloseSnackbar: PropTypes.func.isRequired,
69 | };
70 |
--------------------------------------------------------------------------------
/src/pages/Hotel/components/AvailableRooms/style.module.css:
--------------------------------------------------------------------------------
1 | .header {
2 | margin-top: 3rem;
3 | margin-bottom: 2rem;
4 | font-size: 1.2rem;
5 | text-align: center;
6 | }
7 |
8 | .availableRoomsContainer {
9 | display: flex;
10 | flex-wrap: wrap;
11 | justify-content: space-evenly;
12 | }
13 |
14 | .warningInfoParagraph {
15 | text-align: center;
16 | margin-top: 1rem;
17 | color: red;
18 | }
19 |
20 | .roomCard {
21 | width: 47%;
22 | margin-bottom: 1rem;
23 | border: 1px solid #c2c2c2;
24 | padding: 20px;
25 | box-sizing: border-box;
26 | }
27 |
28 | .roomImage {
29 | width: 100%;
30 | height: 200px;
31 | object-fit: cover;
32 | margin-bottom: 10px;
33 | }
34 |
35 | .roomHeader {
36 | display: flex;
37 | justify-content: space-between;
38 | align-items: center;
39 | margin-bottom: 1rem;
40 | }
41 |
42 | .roomType {
43 | font-size: 1.1rem;
44 | font-weight: bold;
45 | }
46 |
47 | .price {
48 | font-size: 1rem;
49 | font-weight: bold;
50 | color: #f39c12;
51 | }
52 |
53 | .capacityInfo {
54 | display: flex;
55 | align-items: center;
56 | margin-top: 8px;
57 | }
58 |
59 | .iconContainer {
60 | margin-right: 8px;
61 | }
62 |
63 | .amenities {
64 | margin-top: 8px;
65 | }
66 |
67 | .amenities ul {
68 | list-style: none;
69 | padding: 0;
70 | }
71 |
72 | .amenities li {
73 | display: flex;
74 | align-items: center;
75 | margin-bottom: 10px;
76 | }
77 |
78 | .amenities li svg {
79 | margin-right: 1rem;
80 | }
81 |
82 | .addButtonCommon {
83 | padding: 8px 12px;
84 | border: none;
85 | cursor: pointer;
86 | margin-top: 1rem;
87 | display: flex;
88 | align-items: center;
89 | justify-content: center;
90 | width: 100%;
91 | border-radius: 5px;
92 | gap: 8px;
93 | font-size: 1rem;
94 | color: #fff;
95 | }
96 |
97 | .addCartButton {
98 | background-color: #3f89bb;
99 | }
100 |
101 | .addCartButton:hover {
102 | background-color: #3a7ca0;
103 | }
104 |
105 | .addedButton {
106 | background-color: #acaaad;
107 | cursor: not-allowed;
108 | }
109 |
110 | @media screen and (max-width: 768px) {
111 | .availableRoomsContainer {
112 | flex-direction: column;
113 | }
114 |
115 | .roomCard {
116 | width: 100%;
117 | }
118 | }
--------------------------------------------------------------------------------
/src/pages/Home/components/FeaturedDeals/style.module.css:
--------------------------------------------------------------------------------
1 | .featuredDealsContainer {
2 | margin-top: 4rem;
3 | margin-left: auto;
4 | margin-right: auto;
5 | max-width: 1200px;
6 | }
7 |
8 | .dealCard {
9 | border: none;
10 | outline: none;
11 | margin-right: 5px;
12 | transition: box-shadow 0.3s, border-radius 0.3s;
13 | }
14 |
15 | .skeletonDealCard {
16 | background-color: #f0f0f0;
17 | border-radius: 10px;
18 | overflow: hidden;
19 | margin-top: 2rem;
20 | margin-right: 10px;
21 | }
22 |
23 | .dealCard:hover {
24 | box-shadow: 2px 2px 10px rgba(77, 75, 75, 0.3);
25 | border-radius: 10px;
26 | }
27 |
28 | .bottomContainer {
29 | border: 2px solid #e0e0e0;
30 | border-top: none;
31 | border-radius: 0 0 10px 10px;
32 | padding: 1rem;
33 | }
34 |
35 | .roomPhoto {
36 | width: 100%;
37 | height: 300px;
38 | object-fit: cover;
39 | border-radius: 10px 10px 0 0;
40 | border: 1px solid #e0e0e0;
41 | }
42 |
43 | .topSpaceBetweenContainer {
44 | display: flex;
45 | justify-content: space-between;
46 | align-items: center;
47 | }
48 |
49 | .hotelName {
50 | font-size: 1.2rem;
51 | font-weight: 600;
52 | margin-top: 0.5rem;
53 | }
54 |
55 | .bottomSpaceBetweenContainer {
56 | display: flex;
57 | justify-content: space-between;
58 | align-items: center;
59 | margin-top: 0.5rem;
60 | }
61 |
62 | .cityName {
63 | font-size: 1rem;
64 | font-weight: 400;
65 | margin: 0.2rem 0;
66 | display: flex;
67 | align-items: center;
68 | }
69 |
70 | .priceInfoContainer {
71 | display: flex;
72 | justify-content: start;
73 | align-items: center;
74 | }
75 |
76 | .originalPrice {
77 | font-size: 0.9rem;
78 | font-weight: 400;
79 | text-decoration: line-through;
80 | color: #757575;
81 | }
82 |
83 | .finalPrice {
84 | font-size: 1.2rem;
85 | font-weight: 600;
86 | color: #ff5a5f;
87 | margin-left: 1.2rem;
88 | }
89 |
90 | @media screen and (max-width: 768px) {
91 | .featuredDealsContainer {
92 | margin-top: 14rem;
93 | }
94 |
95 | .dealCard {
96 | margin-right: 0;
97 | }
98 | }
99 |
100 | @media screen and (min-width: 769px) and (max-width: 1200px) {
101 | .featuredDealsContainer {
102 | max-width: 992px;
103 | margin-top: 14rem;
104 | }
105 | }
--------------------------------------------------------------------------------
/src/pages/Admin/pages/Cities/components/CreateCityDialog/CreateCityDialog.jsx:
--------------------------------------------------------------------------------
1 | import { Dialog, DialogTitle } from "@mui/material";
2 | import { Formik } from "formik";
3 | import PropTypes from "prop-types";
4 | import { postNewCity } from "../../../../../../services/manageCities";
5 | import CreateEntityDialog from "../../../../components/CreateEntityDialog";
6 | import CreateButton from "../../../../components/CreateButton/CreateButton";
7 | import { fields, initialValues, validationSchema } from "../../cityConfig";
8 | import useDialogState from "../../../../hooks/useDialogState";
9 |
10 | const CreateCityDialog = ({ addCity, snackbarProps }) => {
11 | const { isDialogOpen, handleDialogOpen, handleDialogClose } =
12 | useDialogState();
13 |
14 | const handleCreateCity = async (values, actions) => {
15 | try {
16 | const newCity = await postNewCity(values.name, values.description);
17 | console.log("City created:", newCity);
18 | snackbarProps.showSuccessSnackbar("New city created successfully!");
19 | addCity(newCity);
20 | } catch (error) {
21 | snackbarProps.showErrorSnackbar(`Whoops! ${error.message}`);
22 | } finally {
23 | handleDialogClose();
24 | actions.setSubmitting(false);
25 | }
26 | };
27 |
28 | return (
29 |
30 |
31 |
32 |
33 | Create New City
34 |
39 | {({ errors, touched, isSubmitting }) => (
40 |
47 | )}
48 |
49 |
50 |
51 | );
52 | };
53 |
54 | export default CreateCityDialog;
55 |
56 | CreateCityDialog.propTypes = {
57 | addCity: PropTypes.func.isRequired,
58 | snackbarProps: PropTypes.shape({
59 | open: PropTypes.bool.isRequired,
60 | message: PropTypes.string,
61 | severity: PropTypes.string,
62 | handleCloseSnackbar: PropTypes.func,
63 | showSuccessSnackbar: PropTypes.func,
64 | showErrorSnackbar: PropTypes.func,
65 | }),
66 | };
67 |
--------------------------------------------------------------------------------
/src/pages/Hotel/hooks/useHotelData.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 | import {
3 | getHotelDetails,
4 | getHotelGuestReviews,
5 | getHotelPicturesGallery,
6 | getHotelAvailableRooms,
7 | } from "../../../services/hotelPageServices";
8 | import useComponentLoader from "../../../hooks/useComponentLoader";
9 |
10 | export const useHotelData = (
11 | hotelId,
12 | checkInDate,
13 | checkOutDate,
14 | isThereDates
15 | ) => {
16 | const [hotelDetails, setHotelDetails] = useState(null);
17 | const [hotelGuestReviews, setHotelGuestReviews] = useState([]);
18 | const [hotelGallery, setHotelGallery] = useState([]);
19 | const [hotelAvailableRooms, setHotelAvailableRooms] = useState([]);
20 | const { isLoading, stopLoading } = useComponentLoader();
21 | const [error, setError] = useState("");
22 |
23 | const fetchHotelData = async () => {
24 | setError("");
25 |
26 | Promise.allSettled([
27 | getHotelDetails(hotelId),
28 | getHotelGuestReviews(hotelId),
29 | getHotelPicturesGallery(hotelId),
30 | isThereDates
31 | ? getHotelAvailableRooms(hotelId, checkInDate, checkOutDate)
32 | : Promise.resolve([]),
33 | ])
34 | .then((results) => {
35 | results.forEach((result, index) => {
36 | if (result.status === "fulfilled") {
37 | switch (index) {
38 | case 0:
39 | setHotelDetails(result.value);
40 | break;
41 | case 1:
42 | setHotelGuestReviews(result.value);
43 | break;
44 | case 2:
45 | setHotelGallery(result.value);
46 | break;
47 | case 3:
48 | setHotelAvailableRooms(result.value);
49 | break;
50 | }
51 | } else {
52 | console.error(result.reason);
53 | setError(
54 | (prev) =>
55 | `${prev} ${
56 | result.reason?.data?.message ||
57 | result.reason?.statusText ||
58 | "Error fetching data"
59 | }. `
60 | );
61 | }
62 | });
63 | })
64 | .finally(() => stopLoading());
65 | };
66 |
67 | useEffect(() => {
68 | fetchHotelData();
69 | }, [hotelId, checkInDate, checkOutDate]);
70 |
71 | return {
72 | hotelDetails,
73 | hotelGuestReviews,
74 | hotelGallery,
75 | hotelAvailableRooms,
76 | isLoading,
77 | error,
78 | };
79 | };
80 |
--------------------------------------------------------------------------------
/src/pages/Admin/pages/Rooms/components/CreateRoomDialog/CreateRoomDialog.jsx:
--------------------------------------------------------------------------------
1 | import { Dialog, DialogTitle } from "@mui/material";
2 | import { Formik } from "formik";
3 | import PropTypes from "prop-types";
4 | import { postNewRoom } from "../../../../../../services/manageRooms";
5 | import CreateEntityDialog from "../../../../components/CreateEntityDialog";
6 | import CreateButton from "../../../../components/CreateButton/CreateButton";
7 | import { fields, initialValues, validationSchema } from "../../roomConfig";
8 | import useDialogState from "../../../../hooks/useDialogState";
9 |
10 | const CreateRoomDialog = ({ addRoom, snackbarProps }) => {
11 | const { isDialogOpen, handleDialogOpen, handleDialogClose } =
12 | useDialogState();
13 |
14 | const handleCreateEntity = async (values, actions) => {
15 | try {
16 | const newEntity = await postNewRoom(
17 | parseInt(values.hotelId),
18 | values.roomNumber,
19 | parseInt(values.cost)
20 | );
21 | console.log("Room created:", newEntity);
22 | snackbarProps.showSuccessSnackbar("New Room created successfully!");
23 | addRoom(newEntity);
24 | } catch (error) {
25 | snackbarProps.showErrorSnackbar(`Whoops! ${error.message}`);
26 | } finally {
27 | handleDialogClose();
28 | actions.setSubmitting(false);
29 | }
30 | };
31 |
32 | return (
33 |
34 |
35 |
36 | Create New Room
37 |
38 |
43 | {({ errors, touched, isSubmitting }) => (
44 |
51 | )}
52 |
53 |
54 |
55 | );
56 | };
57 |
58 | export default CreateRoomDialog;
59 |
60 | CreateRoomDialog.propTypes = {
61 | addRoom: PropTypes.func.isRequired,
62 | snackbarProps: PropTypes.shape({
63 | open: PropTypes.bool,
64 | message: PropTypes.string,
65 | severity: PropTypes.string,
66 | handleCloseSnackbar: PropTypes.func,
67 | showSuccessSnackbar: PropTypes.func,
68 | showErrorSnackbar: PropTypes.func,
69 | }),
70 | };
71 |
--------------------------------------------------------------------------------
/src/pages/Home/components/TrendingDestinations/TrendingDestinationsWithoutLoading.jsx:
--------------------------------------------------------------------------------
1 | import { Grid, Card, CardContent, Typography, CardMedia } from "@mui/material";
2 | import GenericSnackbar from "../../../../components/GenericSnackbar";
3 | import LocationOnIcon from "@mui/icons-material/LocationOn";
4 | import PropTypes from "prop-types";
5 |
6 | const TrendingDestinationsWithoutLoading = ({
7 | trendingDestinations,
8 | snackbar,
9 | handleCloseSnackbar,
10 | }) => {
11 | return (
12 | <>
13 |
14 | {trendingDestinations.map((item) => (
15 |
16 |
24 |
31 |
32 |
33 |
43 |
46 | {item.cityName} - {item.countryName}
47 |
48 |
49 |
50 |
51 | ))}
52 |
53 |
54 |
55 | >
56 | );
57 | };
58 |
59 | export default TrendingDestinationsWithoutLoading;
60 |
61 | TrendingDestinationsWithoutLoading.propTypes = {
62 | trendingDestinations: PropTypes.arrayOf(
63 | PropTypes.shape({
64 | cityId: PropTypes.number.isRequired,
65 | cityName: PropTypes.string.isRequired,
66 | countryName: PropTypes.string.isRequired,
67 | thumbnailUrl: PropTypes.string.isRequired,
68 | })
69 | ).isRequired,
70 | snackbar: PropTypes.object.isRequired,
71 | handleCloseSnackbar: PropTypes.func.isRequired,
72 | };
73 |
--------------------------------------------------------------------------------
/src/pages/Confirmation/components/ConfirmationTable/ConfirmationTable.jsx:
--------------------------------------------------------------------------------
1 | import { useContext } from "react";
2 | import Table from "@mui/material/Table";
3 | import TableBody from "@mui/material/TableBody";
4 | import TableCell from "@mui/material/TableCell";
5 | import TableContainer from "@mui/material/TableContainer";
6 | import TableHead from "@mui/material/TableHead";
7 | import TableRow from "@mui/material/TableRow";
8 | import Paper from "@mui/material/Paper";
9 | import { FormContext } from "../../../../context/CheckoutFormContext ";
10 | import { SearchContext } from "../../../../context/searchContext";
11 | import Container from "@mui/material/Container";
12 | import styles from "./style.module.css";
13 | import useCartContext from "../../../../hooks/useCartContext";
14 |
15 | const ConfirmationTable = () => {
16 | const { searchParams, getNumberOfNights } = useContext(SearchContext);
17 | const { checkInDate, checkOutDate } = searchParams;
18 | const { formValues } = useContext(FormContext);
19 | const { cart } = useCartContext();
20 |
21 | const fields = [
22 | { label: "Confirmation Number", value: "20240109-5460" },
23 | { label: "Full Name", value: formValues.fullName },
24 | { label: "Email", value: formValues.email },
25 | { label: "State", value: formValues.billingAddress.state },
26 | { label: "Payment Method", value: formValues.paymentMethod },
27 | {
28 | label: "Check-in date",
29 | value: checkInDate,
30 | },
31 | {
32 | label: "Check-out date",
33 | value: checkOutDate,
34 | },
35 | { label: "Room Type", value: cart[0].roomType },
36 | { label: "Room Number", value: cart[0].roomNumber },
37 | { label: "Total cost", value: `$${cart[0].price * getNumberOfNights()}` },
38 | ];
39 |
40 | return (
41 |
42 |
43 |
44 |
45 |
46 | Field
47 | Value
48 |
49 |
50 |
51 | {fields.map((field, index) => (
52 |
56 | {field.label}
57 | {field.value}
58 |
59 | ))}
60 |
61 |
62 |
63 |
64 | );
65 | };
66 |
67 | export default ConfirmationTable;
68 |
--------------------------------------------------------------------------------
/src/pages/Admin/pages/Hotels/components/CreateHotelDialog/CreateHotelDialog.jsx:
--------------------------------------------------------------------------------
1 | import { Dialog, DialogTitle } from "@mui/material";
2 | import { Formik } from "formik";
3 | import PropTypes from "prop-types";
4 | import { postNewHotel } from "../../../../../../services/manageHotels";
5 | import CreateButton from "../../../../components/CreateButton/CreateButton";
6 | import { fields, initialValues, validationSchema } from "../../hotelConfig";
7 | import CreateEntityDialog from "../../../../components/CreateEntityDialog";
8 | import useDialogState from "../../../../hooks/useDialogState";
9 |
10 | const CreateHotelDialog = ({ addHotel, snackbarProps }) => {
11 | const { isDialogOpen, handleDialogOpen, handleDialogClose } =
12 | useDialogState();
13 |
14 | const handleCreateHotel = async (values, actions) => {
15 | try {
16 | const newHotel = await postNewHotel(
17 | parseInt(values.cityId),
18 | values.name,
19 | values.description,
20 | parseInt(values.hotelType),
21 | parseFloat(values.starRating),
22 | parseFloat(values.latitude),
23 | parseFloat(values.longitude)
24 | );
25 | console.log("Hotel created:", newHotel);
26 | snackbarProps.showSuccessSnackbar("New Hotel created successfully!");
27 | addHotel(newHotel);
28 | } catch (error) {
29 | snackbarProps.showErrorSnackbar(`Whoops! ${error.message}`);
30 | } finally {
31 | handleDialogClose();
32 | actions.setSubmitting(false);
33 | }
34 | };
35 |
36 | return (
37 |
38 |
39 |
40 | Create New Hotel
41 |
46 | {({ errors, touched, isSubmitting }) => (
47 |
54 | )}
55 |
56 |
57 |
58 | );
59 | };
60 |
61 | export default CreateHotelDialog;
62 |
63 | CreateHotelDialog.propTypes = {
64 | addHotel: PropTypes.func.isRequired,
65 | snackbarProps: PropTypes.shape({
66 | open: PropTypes.bool.isRequired,
67 | message: PropTypes.string,
68 | severity: PropTypes.string,
69 | handleCloseSnackbar: PropTypes.func,
70 | showSuccessSnackbar: PropTypes.func,
71 | showErrorSnackbar: PropTypes.func,
72 | }),
73 | };
74 |
--------------------------------------------------------------------------------
/src/pages/Home/components/RecentlyVisitedHotels/RecentlyVisitedHotelsWithoutLoading.jsx:
--------------------------------------------------------------------------------
1 | import { Grid, Card, CardContent, CardMedia, Typography } from "@mui/material";
2 | import GenericSnackbar from "../../../../components/GenericSnackbar";
3 | import PropTypes from "prop-types";
4 |
5 | function RecentlyVisitedHotelsWithoutLoading({
6 | lastVisitedHotels,
7 | handleNavigation,
8 | snackbar,
9 | handleCloseSnackbar,
10 | }) {
11 | return (
12 | <>
13 |
14 | {lastVisitedHotels.map((hotel) => (
15 |
16 | handleNavigation(hotel.hotelId)}
18 | style={{
19 | cursor: "pointer",
20 | display: "flex",
21 | flexDirection: "column",
22 | boxShadow:
23 | "2px 2px 4px rgba(8, 12, 2, 0.2), -2px -2px 4px rgba(8, 12, 2, 0.2)",
24 | }}
25 | >
26 |
33 |
34 |
35 |
36 | {hotel.hotelName}
37 |
38 |
43 | {`${hotel.cityName} | ${hotel.starRating} Stars | Price Range: $${hotel.priceLowerBound} - $${hotel.priceUpperBound}`}
44 |
45 |
46 |
47 |
48 | ))}
49 |
50 |
51 |
52 | >
53 | );
54 | }
55 |
56 | export default RecentlyVisitedHotelsWithoutLoading;
57 |
58 | RecentlyVisitedHotelsWithoutLoading.propTypes = {
59 | lastVisitedHotels: PropTypes.arrayOf(
60 | PropTypes.shape({
61 | hotelId: PropTypes.number.isRequired,
62 | hotelName: PropTypes.string.isRequired,
63 | cityName: PropTypes.string.isRequired,
64 | starRating: PropTypes.number.isRequired,
65 | priceLowerBound: PropTypes.number.isRequired,
66 | priceUpperBound: PropTypes.number.isRequired,
67 | thumbnailUrl: PropTypes.string.isRequired,
68 | })
69 | ).isRequired,
70 | handleNavigation: PropTypes.func.isRequired,
71 | snackbar: PropTypes.shape({
72 | open: PropTypes.bool.isRequired,
73 | message: PropTypes.string.isRequired,
74 | severity: PropTypes.string.isRequired,
75 | }).isRequired,
76 | handleCloseSnackbar: PropTypes.func.isRequired,
77 | };
78 |
--------------------------------------------------------------------------------
/src/pages/Home/components/FeaturedDeals/FeaturedDeals.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import Slider from "react-slick";
3 | import "slick-carousel/slick/slick.css";
4 | import "slick-carousel/slick/slick-theme.css";
5 | import styles from "./style.module.css";
6 | import { featuredDealsAPI } from "../../../../services/homePageServices";
7 | import useSnackbar from "../../../../hooks/useSnackbar";
8 | import GenericSnackbar from "../../../../components/GenericSnackbar";
9 | import DealCard from "./DealCard";
10 | import SkeletonDealCard from "./SkeletonDealCard";
11 | import useComponentLoader from "../../../../hooks/useComponentLoader";
12 |
13 | const FeaturedDeals = () => {
14 | const [deals, setDeals] = useState([]);
15 | const { isLoading, stopLoading } = useComponentLoader();
16 | const [error, setError] = useState(null);
17 | const [slidesToShow, setSlidesToShow] = useState(1);
18 | const { snackbar, showErrorSnackbar, handleCloseSnackbar } = useSnackbar();
19 |
20 | useEffect(() => {
21 | const fetchDeals = async () => {
22 | try {
23 | const dealsData = await featuredDealsAPI();
24 | setDeals(dealsData);
25 | } catch (error) {
26 | setError(error);
27 | showErrorSnackbar("Failed to fetch deals. Please try again later.");
28 | } finally {
29 | stopLoading();
30 | }
31 | };
32 |
33 | fetchDeals();
34 | }, []);
35 |
36 | useEffect(() => {
37 | const handleResize = () => {
38 | const windowWidth = window.innerWidth;
39 | if (windowWidth < 768) {
40 | setSlidesToShow(1);
41 | } else if (windowWidth >= 768 && windowWidth <= 1000) {
42 | setSlidesToShow(2);
43 | } else {
44 | setSlidesToShow(3);
45 | }
46 | };
47 |
48 | handleResize();
49 | window.addEventListener("resize", handleResize);
50 | return () => window.removeEventListener("resize", handleResize);
51 | }, []);
52 |
53 | const settings = {
54 | dots: true,
55 | infinite: true,
56 | speed: 500,
57 | slidesToScroll: 1,
58 | adaptiveHeight: true,
59 | slidesToShow: slidesToShow,
60 | autoplay: true,
61 | };
62 |
63 | return (
64 |
65 |
Featured Deals
66 | {error &&
Something went wrong. Please try again later.
}
67 |
68 | {isLoading ? (
69 |
70 | {[1, 2, 3].map((_, index) => (
71 |
72 | ))}
73 |
74 | ) : (
75 |
76 |
77 | {deals.map((deal) => (
78 |
79 | ))}
80 |
81 |
82 | )}
83 |
84 |
85 |
86 | );
87 | };
88 |
89 | export default FeaturedDeals;
90 |
--------------------------------------------------------------------------------
/src/pages/Admin/pages/Cities/components/UpdateCityForm/UpdateCityForm.jsx:
--------------------------------------------------------------------------------
1 | import { Drawer, IconButton, Typography, Box } from "@mui/material";
2 | import { ChevronRight as ChevronRightIcon } from "@mui/icons-material";
3 | import { Formik, Form } from "formik";
4 | import PropTypes from "prop-types";
5 | import { updateCity } from "../../../../../../services/manageCities";
6 | import { fields, validationSchema } from "../../cityConfig";
7 | import UpdateButton from "../../../../components/UpdateButton";
8 | import UpdateEntityForm from "../../../../components/UpdateEntityForm";
9 |
10 | const UpdateCityForm = ({ open, onClose, entityData, onUpdate }) => {
11 | if (!entityData) {
12 | return null;
13 | }
14 |
15 | const handleUpdateClick = async (values, actions) => {
16 | try {
17 | await updateCity(entityData.id, values.name, values.description);
18 | const updatedCity = {
19 | ...entityData,
20 | name: values.name,
21 | description: values.description,
22 | };
23 | if (onUpdate) {
24 | onUpdate(updatedCity);
25 | }
26 | console.log("City updated:", updatedCity);
27 | } catch (error) {
28 | console.log(error.message);
29 | } finally {
30 | onClose();
31 | actions.setSubmitting(false);
32 | }
33 | };
34 |
35 | const initialValues = fields.reduce((values, field) => {
36 | values[field.name] = entityData[field.name] || "";
37 | return values;
38 | }, {});
39 |
40 | return (
41 |
42 |
48 |
49 |
50 |
51 |
52 |
53 | Update City
54 |
55 |
56 |
62 | {({ errors, touched, isSubmitting }) => (
63 |
73 | )}
74 |
75 |
76 |
77 | );
78 | };
79 |
80 | export default UpdateCityForm;
81 |
82 | UpdateCityForm.propTypes = {
83 | open: PropTypes.bool,
84 | onClose: PropTypes.func,
85 | entityData: PropTypes.shape({
86 | id: PropTypes.number.isRequired,
87 | name: PropTypes.string,
88 | description: PropTypes.string,
89 | }),
90 | onUpdate: PropTypes.func,
91 | };
92 |
--------------------------------------------------------------------------------
/src/pages/Admin/pages/Rooms/Rooms.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import LeftNavigation from "../../components/LeftNavigation";
3 | import SearchBar from "./components/SearchBar";
4 | import { CssBaseline, Box, Container } from "@mui/material";
5 | import UpdateRoomForm from "./components/UpdateRoomForm";
6 | import GenericSnackbar from "../../../../components/GenericSnackbar";
7 | import useSnackbar from "../../../../hooks/useSnackbar";
8 | import { deleteRoom } from "../../../../services/manageRooms";
9 | import CreateRoomDialog from "./components/CreateRoomDialog";
10 | import RoomsDetailedGrid from "./components/RoomsDetailedGrid";
11 |
12 | const Rooms = () => {
13 | const [rooms, setRooms] = useState([]);
14 | const {
15 | snackbar,
16 | handleCloseSnackbar,
17 | showErrorSnackbar,
18 | showSuccessSnackbar,
19 | } = useSnackbar();
20 |
21 | const handleAddRoom = (newRoom) => {
22 | setRooms((prevRooms) => [...prevRooms, newRoom]);
23 | };
24 |
25 | const handleUpdateRooms = (updatedRoom) => {
26 | setRooms((prevRooms) =>
27 | prevRooms.map((room) => (room.id === updatedRoom.id ? updatedRoom : room))
28 | );
29 | };
30 |
31 | const handleDeleteRoom = async (selectedRoom) => {
32 | try {
33 | await deleteRoom(2, selectedRoom.id);
34 | console.log("deleted room " + selectedRoom.id + " successfully");
35 | showSuccessSnackbar("Room deleted successfully!");
36 | setRooms((prevRooms) =>
37 | prevRooms.filter((room) => room.id !== selectedRoom.id)
38 | );
39 | } catch (error) {
40 | showErrorSnackbar(`Whoops! ${error.message}`);
41 | }
42 | };
43 |
44 | return (
45 |
46 |
47 |
48 |
49 |
56 |
57 |
58 |
67 |
68 |
69 |
70 |
80 |
81 |
82 |
83 |
89 |
90 | );
91 | };
92 |
93 | export default Rooms;
94 |
--------------------------------------------------------------------------------
/src/pages/Admin/pages/Rooms/components/UpdateRoomForm/UpdateRoomForm.jsx:
--------------------------------------------------------------------------------
1 | import { Drawer, IconButton, Typography, Box } from "@mui/material";
2 | import { ChevronRight as ChevronRightIcon } from "@mui/icons-material";
3 | import { Formik, Form } from "formik";
4 | import PropTypes from "prop-types";
5 | import { updateRoom } from "../../../../../../services/manageRooms";
6 | import { fields, validationSchema } from "../../roomConfig";
7 | import UpdateButton from "../../../../components/UpdateButton";
8 | import UpdateEntityForm from "../../../../components/UpdateEntityForm";
9 |
10 | const UpdateRoomForm = ({ open, onClose, entityData, onUpdate }) => {
11 | if (!entityData) {
12 | return null;
13 | }
14 |
15 | const handleUpdateClick = async (values, actions) => {
16 | try {
17 | await updateRoom(entityData.id, values.roomNumber, values.cost);
18 | const updatedRoom = {
19 | ...entityData,
20 | roomNumber: values.roomNumber,
21 | cost: values.cost,
22 | };
23 | if (onUpdate) {
24 | onUpdate(updatedRoom);
25 | }
26 | console.log("Room updated:", updatedRoom);
27 | } catch (error) {
28 | console.log(error.message);
29 | } finally {
30 | onClose();
31 | actions.setSubmitting(false);
32 | }
33 | };
34 |
35 | const updatedFields = fields.filter((field) => field.name !== "hotelId");
36 |
37 | const initialValues = fields.reduce((values, field) => {
38 | values[field.name] =
39 | entityData && entityData[field.name] !== undefined
40 | ? entityData[field.name]
41 | : field.type === "number"
42 | ? 0
43 | : "";
44 | return values;
45 | }, {});
46 |
47 | return (
48 |
49 |
55 |
56 |
57 |
58 |
59 | Update Room
60 |
61 |
67 | {({ errors, touched, isSubmitting }) => (
68 |
78 | )}
79 |
80 |
81 |
82 | );
83 | };
84 |
85 | export default UpdateRoomForm;
86 |
87 | UpdateRoomForm.propTypes = {
88 | open: PropTypes.bool,
89 | onClose: PropTypes.func,
90 | entityData: PropTypes.shape({
91 | id: PropTypes.number.isRequired,
92 | name: PropTypes.string,
93 | description: PropTypes.string,
94 | }),
95 | onUpdate: PropTypes.func,
96 | };
97 |
--------------------------------------------------------------------------------
/src/pages/Admin/pages/Rooms/components/RoomsDetailedGrid/RoomsDetailedGrid.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import {
3 | Paper,
4 | Table,
5 | TableBody,
6 | TableCell,
7 | TableContainer,
8 | TableHead,
9 | TableRow,
10 | Container,
11 | IconButton,
12 | } from "@mui/material";
13 | import DeleteIcon from "@mui/icons-material/Delete";
14 | import PropTypes from "prop-types";
15 |
16 | const RoomsDetailedGrid = ({
17 | data,
18 | columns,
19 | onUpdate,
20 | onDelete,
21 | EntityFormComponent,
22 | }) => {
23 | const [isDrawerOpen, setIsDrawerOpen] = useState(false);
24 | const [selectedEntity, setSelectedEntity] = useState(null);
25 |
26 | const handleRowClick = (entity) => {
27 | setSelectedEntity(entity);
28 | setIsDrawerOpen(true);
29 | };
30 |
31 | const closeDrawer = () => {
32 | setIsDrawerOpen(false);
33 | };
34 |
35 | const handleUpdate = (updatedEntity) => {
36 | onUpdate(updatedEntity);
37 | closeDrawer();
38 | };
39 |
40 | const handleDelete = (entity, event) => {
41 | event.stopPropagation();
42 | onDelete(entity);
43 | };
44 |
45 | return (
46 | <>
47 |
48 |
49 |
50 |
51 |
52 | {columns.map((column) => (
53 | {column.headerName}
54 | ))}
55 | Action
56 |
57 |
58 |
59 |
60 | {data.map((row) => (
61 | handleRowClick(row)}
65 | >
66 | {columns.map((column) => (
67 |
68 | {row[column.field]}
69 |
70 | ))}
71 |
72 | handleDelete(row, e)}
75 | >
76 |
77 |
78 |
79 |
80 | ))}
81 |
82 |
83 |
84 |
85 |
86 | {EntityFormComponent && (
87 |
93 | )}
94 | >
95 | );
96 | };
97 |
98 | export default RoomsDetailedGrid;
99 |
100 | RoomsDetailedGrid.propTypes = {
101 | data: PropTypes.arrayOf(PropTypes.object).isRequired,
102 | columns: PropTypes.arrayOf(
103 | PropTypes.shape({
104 | field: PropTypes.string.isRequired,
105 | headerName: PropTypes.string.isRequired,
106 | renderAction: PropTypes.func,
107 | })
108 | ).isRequired,
109 | onUpdate: PropTypes.func.isRequired,
110 | onDelete: PropTypes.func.isRequired,
111 | EntityFormComponent: PropTypes.elementType,
112 | };
113 |
--------------------------------------------------------------------------------
/src/pages/Admin/components/LeftNavigation/LeftNavigation.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useContext } from "react";
2 | import AppBar from "@mui/material/AppBar";
3 | import Drawer from "@mui/material/Drawer";
4 | import Typography from "@mui/material/Typography";
5 | import IconButton from "@mui/material/IconButton";
6 | import MenuIcon from "@mui/icons-material/Menu";
7 | import Box from "@mui/material/Box";
8 | import Divider from "@mui/material/Divider";
9 | import List from "@mui/material/List";
10 | import ListItemIcon from "@mui/material/ListItemIcon";
11 | import ListItemText from "@mui/material/ListItemText";
12 | import { NavLink } from "react-router-dom";
13 | import CityIcon from "@mui/icons-material/LocationCity";
14 | import HotelIcon from "@mui/icons-material/Hotel";
15 | import RoomIcon from "@mui/icons-material/MeetingRoom";
16 | import { Logout as LogoutIcon } from "@mui/icons-material";
17 | import { AuthContext } from "../../../../context/authContext";
18 | import { ListItemButton } from "@mui/material";
19 |
20 | const LeftNavigation = () => {
21 | const [isMobileOpened, setIsMobileOpened] = useState(false);
22 | const { logoutUser } = useContext(AuthContext);
23 |
24 | const handleDrawerToggle = () => {
25 | setIsMobileOpened(!isMobileOpened);
26 | };
27 |
28 | const drawerWidth = 240;
29 |
30 | const adminLinks = [
31 | { name: "Cities", icon: , path: "/adminDashboard/cities" },
32 | { name: "Hotels", icon: , path: "/adminDashboard/hotels" },
33 | { name: "Rooms", icon: , path: "/adminDashboard/rooms" },
34 | ];
35 |
36 | const renderDrawer = () => (
37 |
50 |
51 |
52 | Admin Page
53 |
54 |
55 |
56 |
57 |
58 | {adminLinks.map((item) => (
59 |
64 |
65 | {item.icon}
66 |
67 |
68 |
69 | ))}
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 | );
81 |
82 | return (
83 | <>
84 | {renderDrawer()}
85 |
93 |
102 |
103 |
104 |
105 | >
106 | );
107 | };
108 |
109 | export default LeftNavigation;
110 |
--------------------------------------------------------------------------------
/src/pages/Admin/pages/Hotels/components/UpdateHotelForm/UpdateHotelForm.jsx:
--------------------------------------------------------------------------------
1 | import { Drawer, IconButton, Typography, Box } from "@mui/material";
2 | import { ChevronRight as ChevronRightIcon } from "@mui/icons-material";
3 | import { Formik, Form } from "formik";
4 | import PropTypes from "prop-types";
5 | import { updateHotel } from "../../../../../../services/manageHotels";
6 | import { fields, validationSchema } from "../../hotelConfig";
7 | import UpdateEntityForm from "../../../../components/UpdateEntityForm";
8 | import UpdateButton from "../../../../components/UpdateButton";
9 |
10 | const UpdateHotelForm = ({ open, onClose, entityData, onUpdate }) => {
11 | if (!entityData) {
12 | return null;
13 | }
14 |
15 | const handleUpdateClick = async (values, actions) => {
16 | try {
17 | await updateHotel(
18 | entityData.id,
19 | values.name,
20 | values.description,
21 | values.hotelType,
22 | values.starRating,
23 | values.latitude,
24 | values.longitude
25 | );
26 |
27 | const updatedHotel = {
28 | ...entityData,
29 | name: values.name,
30 | description: values.description,
31 | hotelType: values.hotelType,
32 | starRating: values.starRating,
33 | latitude: values.latitude,
34 | longitude: values.longitude,
35 | };
36 |
37 | if (onUpdate) {
38 | onUpdate(updatedHotel);
39 | }
40 | console.log("Hotel updated:", updatedHotel);
41 | } catch (error) {
42 | console.log(error.message);
43 | } finally {
44 | onClose();
45 | actions.setSubmitting(false);
46 | }
47 | };
48 |
49 | const updatedFields = fields.filter((field) => field.name !== "cityId");
50 |
51 | const initialValues = fields.reduce((values, field) => {
52 | values[field.name] =
53 | entityData && entityData[field.name] !== undefined
54 | ? entityData[field.name]
55 | : field.type === "number"
56 | ? 0
57 | : "";
58 | return values;
59 | }, {});
60 |
61 | return (
62 |
63 |
69 |
70 |
71 |
72 |
73 |
74 | Update Hotel
75 |
76 |
77 |
83 | {({ errors, touched, isSubmitting }) => (
84 |
94 | )}
95 |
96 |
97 |
98 | );
99 | };
100 |
101 | export default UpdateHotelForm;
102 |
103 | UpdateHotelForm.propTypes = {
104 | open: PropTypes.bool,
105 | onClose: PropTypes.func,
106 | entityData: PropTypes.shape({
107 | id: PropTypes.number.isRequired,
108 | name: PropTypes.string,
109 | description: PropTypes.string,
110 | }),
111 | onUpdate: PropTypes.func,
112 | };
113 |
--------------------------------------------------------------------------------
/src/pages/SearchPage/components/SearchResultItem/SearchResultItem.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from "prop-types";
2 | import {
3 | Card,
4 | CardContent,
5 | CardMedia,
6 | Typography,
7 | Button,
8 | Chip,
9 | Box,
10 | } from "@mui/material";
11 | import StarRating from "../../../../components/StarRating";
12 |
13 | const SearchResultItem = ({ hotel }) => {
14 | const {
15 | hotelName,
16 | starRating,
17 | roomType,
18 | roomPrice,
19 | roomPhotoUrl,
20 | cityName,
21 | amenities,
22 | } = hotel;
23 |
24 | return (
25 |
26 |
37 |
48 |
54 |
64 | {hotelName}
65 |
66 |
67 |
68 |
69 | {hotelName} in {cityName} offers {roomType} rooms.
70 |
71 |
72 |
73 | {amenities.map((amenity, index) => (
74 |
86 | ))}
87 |
88 |
89 |
90 |
91 | US${roomPrice}/night
92 |
93 |
94 |
95 |
96 |
104 | See Availability
105 |
106 |
107 |
108 |
109 |
110 | );
111 | };
112 |
113 | export default SearchResultItem;
114 |
115 | SearchResultItem.propTypes = {
116 | hotel: PropTypes.shape({
117 | hotelName: PropTypes.string.isRequired,
118 | starRating: PropTypes.number.isRequired,
119 | roomType: PropTypes.string.isRequired,
120 | roomPrice: PropTypes.number.isRequired,
121 | roomPhotoUrl: PropTypes.string.isRequired,
122 | cityName: PropTypes.string.isRequired,
123 | amenities: PropTypes.arrayOf(PropTypes.object).isRequired,
124 | }),
125 | };
126 |
--------------------------------------------------------------------------------
/src/pages/Login/Login.jsx:
--------------------------------------------------------------------------------
1 | import { useContext } from "react";
2 | import styles from "./style.module.css";
3 | import { Formik, Form, Field, ErrorMessage } from "formik";
4 | import * as Yup from "yup";
5 | import { useNavigate } from "react-router-dom";
6 | import { loginAPI } from "../../services/authService";
7 | import GenericSnackbar from "../../components/GenericSnackbar";
8 | import useSnackbar from "../../hooks/useSnackbar";
9 | import { AuthContext } from "../../context/authContext";
10 | import CustomButton from "../../components/CustomButton";
11 |
12 | const loginSchema = Yup.object().shape({
13 | username: Yup.string().required("⚠️ Username is a required field"),
14 | password: Yup.string().required("⚠️ Password is a required field"),
15 | });
16 |
17 | const Login = () => {
18 | const navigate = useNavigate();
19 | const { loginUser } = useContext(AuthContext);
20 | const {
21 | snackbar,
22 | handleCloseSnackbar,
23 | showErrorSnackbar,
24 | showSuccessSnackbar,
25 | } = useSnackbar();
26 |
27 | const handleLogin = async (values, { setSubmitting }) => {
28 | setSubmitting(true);
29 | try {
30 | const response = await loginAPI(values);
31 | loginUser(response); // loginUser is a function from AuthContext => returns {userType, authentication}
32 | if (response.userType === "User" || response.userType === "Admin") {
33 | showSuccessSnackbar("Welcome, you're successfully logged in!");
34 | setTimeout(() => {
35 | navigate(response.userType === "User" ? "/home" : "/adminDashboard");
36 | }, 600);
37 | }
38 | } catch (error) {
39 | showErrorSnackbar(
40 | "Login failed! Unable to login with provided credentials."
41 | );
42 | }
43 | setSubmitting(false);
44 | };
45 |
46 | return (
47 |
48 |
53 | {({ values, errors, isSubmitting }) => {
54 | const areFieldsFilled = values.username && values.password;
55 | const areThereErrors = errors.username || errors.password;
56 | const buttonClassName =
57 | areFieldsFilled && !areThereErrors ? "activeBtn" : "defaultBtn";
58 |
59 | return (
60 |
61 |
86 |
87 | );
88 | }}
89 |
90 |
91 |
97 |
98 | );
99 | };
100 |
101 | export default Login;
102 |
--------------------------------------------------------------------------------
/src/pages/SearchPage/components/SearchResult/SearchResult.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import styles from "./style.module.css";
3 | import SearchResultItem from "../SearchResultItem";
4 | import { Link } from "react-router-dom";
5 | import { useSearchParams } from "react-router-dom";
6 | import { searchAPI } from "../../../../services/searchService";
7 | import GenericSnackbar from "../../../../components/GenericSnackbar";
8 | import useSnackbar from "../../../../hooks/useSnackbar";
9 | import PropTypes from "prop-types";
10 | import useComponentLoader from "../../../../hooks/useComponentLoader";
11 | import CircularProgressIndicator from "../../../../components/CircularProgressIndicator";
12 |
13 | const SearchResult = ({ filters }) => {
14 | const [searchParams] = useSearchParams();
15 | const [initialResults, setInitialResults] = useState([]);
16 | const [finalResults, setFinalResults] = useState([]);
17 | const { snackbar, showErrorSnackbar, handleCloseSnackbar } = useSnackbar();
18 | const { isLoading, stopLoading } = useComponentLoader();
19 |
20 | useEffect(() => {
21 | const fetchResults = async () => {
22 | const params = Object.fromEntries([...searchParams]);
23 | try {
24 | const fetchResultsData = await searchAPI(params);
25 | setInitialResults(fetchResultsData);
26 | } catch (error) {
27 | showErrorSnackbar(
28 | "Failed to fetch the results. Please try again later."
29 | );
30 | } finally {
31 | stopLoading();
32 | }
33 | };
34 |
35 | if (searchParams.toString()) {
36 | fetchResults();
37 | }
38 | }, [searchParams]);
39 |
40 | useEffect(() => {
41 | const applyFiltersAndSort = () => {
42 | const filteredResults = initialResults.filter((item) => {
43 | if (
44 | filters.starRating &&
45 | item.starRating !== parseInt(filters.starRating)
46 | ) {
47 | return false;
48 | }
49 |
50 | if (filters.roomType && item.roomType !== filters.roomType) {
51 | return false;
52 | }
53 |
54 | if (
55 | filters.priceRange &&
56 | (item.roomPrice < filters.priceRange[0] ||
57 | item.roomPrice > filters.priceRange[1])
58 | ) {
59 | return false;
60 | }
61 |
62 | if (
63 | filters.amenities &&
64 | filters.amenities.length > 0 &&
65 | !filters.amenities.some((filterAmenity) =>
66 | item.amenities.some((amenity) => amenity.name === filterAmenity)
67 | )
68 | ) {
69 | return false;
70 | }
71 |
72 | return true;
73 | });
74 |
75 | if (filters.sort) {
76 | filteredResults.sort((a, b) =>
77 | filters.sort === "Price"
78 | ? a.roomPrice - b.roomPrice
79 | : b.starRating - a.starRating
80 | );
81 | }
82 |
83 | setFinalResults(filteredResults);
84 | };
85 |
86 | applyFiltersAndSort();
87 | }, [filters, initialResults]);
88 |
89 | return (
90 |
91 | {isLoading ? (
92 |
93 | ) : finalResults.length > 0 ? (
94 | finalResults.map((hotel) => (
95 |
100 |
101 |
102 | ))
103 | ) : (
104 | !isLoading && (
105 |
106 | No hotels available for the selected criteria. Please refine your
107 | search parameters and try again.
108 |
109 | )
110 | )}
111 |
112 |
113 |
114 | );
115 | };
116 |
117 | export default SearchResult;
118 |
119 | SearchResult.propTypes = {
120 | filters: PropTypes.shape({
121 | starRating: PropTypes.number,
122 | roomType: PropTypes.string,
123 | roomPrice: PropTypes.number,
124 | priceRange: PropTypes.arrayOf(PropTypes.number),
125 | sort: PropTypes.string,
126 | amenities: PropTypes.arrayOf(PropTypes.string),
127 | }),
128 | };
129 |
--------------------------------------------------------------------------------
/src/pages/Admin/components/DetailedGrid/DetailedGrid.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import {
3 | Paper,
4 | Table,
5 | TableBody,
6 | TableCell,
7 | TableContainer,
8 | TableHead,
9 | TableRow,
10 | Container,
11 | IconButton,
12 | } from "@mui/material";
13 | import TablePagination from "@mui/material/TablePagination";
14 | import DeleteIcon from "@mui/icons-material/Delete";
15 | import PropTypes from "prop-types";
16 |
17 | const DetailedGrid = ({
18 | data,
19 | columns,
20 | onUpdate,
21 | onDelete,
22 | EntityFormComponent,
23 | page,
24 | setPage,
25 | rowsPerPage,
26 | setRowsPerPage,
27 | }) => {
28 | const [isDrawerOpen, setIsDrawerOpen] = useState(false);
29 | const [selectedEntity, setSelectedEntity] = useState(null);
30 |
31 | const handleRowClick = (entity) => {
32 | setSelectedEntity(entity);
33 | setIsDrawerOpen(true);
34 | };
35 |
36 | const closeDrawer = () => {
37 | setIsDrawerOpen(false);
38 | };
39 |
40 | const handleUpdate = (updatedEntity) => {
41 | onUpdate(updatedEntity);
42 | closeDrawer();
43 | };
44 |
45 | const handleDelete = (entity, event) => {
46 | event.stopPropagation();
47 | onDelete(entity);
48 | };
49 |
50 | const handleChangePage = (event, newPage) => {
51 | setPage(newPage);
52 | };
53 |
54 | const handleChangeRowsPerPage = (event) => {
55 | setRowsPerPage(parseInt(event.target.value, 10));
56 | setPage(0);
57 | };
58 |
59 | return (
60 | <>
61 |
62 |
63 |
64 |
65 |
66 | {columns.map((column) => (
67 | {column.headerName}
68 | ))}
69 | Action
70 |
71 |
72 |
73 |
74 | {data.length > 0 ? (
75 | data
76 | .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
77 | .map((row) => (
78 | handleRowClick(row)}
82 | >
83 | {columns.map((column) => (
84 |
85 | {row[column.field]}
86 |
87 | ))}
88 |
89 |
90 | handleDelete(row, e)}
93 | >
94 |
95 |
96 |
97 |
98 | ))
99 | ) : (
100 |
101 |
102 | No data were found
103 |
104 |
105 | )}
106 |
107 |
108 |
109 |
110 |
119 |
120 |
121 | {EntityFormComponent && (
122 |
128 | )}
129 | >
130 | );
131 | };
132 |
133 | export default DetailedGrid;
134 |
135 | DetailedGrid.propTypes = {
136 | data: PropTypes.arrayOf(PropTypes.object).isRequired,
137 | columns: PropTypes.arrayOf(
138 | PropTypes.shape({
139 | field: PropTypes.string.isRequired,
140 | headerName: PropTypes.string.isRequired,
141 | renderAction: PropTypes.func,
142 | })
143 | ).isRequired,
144 | onUpdate: PropTypes.func.isRequired,
145 | onDelete: PropTypes.func.isRequired,
146 | EntityFormComponent: PropTypes.elementType,
147 | page: PropTypes.number.isRequired,
148 | setPage: PropTypes.func.isRequired,
149 | rowsPerPage: PropTypes.number.isRequired,
150 | setRowsPerPage: PropTypes.func.isRequired,
151 | };
152 |
--------------------------------------------------------------------------------
/src/pages/Admin/pages/Cities/Cities.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 | import LeftNavigation from "../../components/LeftNavigation";
3 | import SearchBar from "../../components/SearchBar";
4 | import { CssBaseline, Box, Container } from "@mui/material";
5 | import UpdateCityForm from "./components/UpdateCityForm";
6 | import GenericSnackbar from "../../../../components/GenericSnackbar";
7 | import useSnackbar from "../../../../hooks/useSnackbar";
8 | import { deleteCity } from "../../../../services/manageCities";
9 | import CreateCityDialog from "./components/CreateCityDialog";
10 | import useComponentLoader from "../../../../hooks/useComponentLoader";
11 | import axiosInstance from "../../../../Axios/axiosInstance";
12 | import DetailedGridWithLoading from "../../components/DetailedGrid/DetailedGridWithLoading";
13 |
14 | const Cities = () => {
15 | const [cities, setCities] = useState([]);
16 | const { isLoading, stopLoading } = useComponentLoader();
17 | const [page, setPage] = useState(0);
18 | const [rowsPerPage, setRowsPerPage] = useState(10);
19 | const {
20 | snackbar,
21 | handleCloseSnackbar,
22 | showErrorSnackbar,
23 | showSuccessSnackbar,
24 | } = useSnackbar();
25 |
26 | const handleAddCity = (newCity) => {
27 | setCities((prevCities) => [newCity, ...prevCities]);
28 | };
29 |
30 | const handleUpdateCities = (updatedCity) => {
31 | setCities((prevCities) =>
32 | prevCities.map((city) =>
33 | city.id === updatedCity.id ? updatedCity : city
34 | )
35 | );
36 | };
37 |
38 | const handleDeleteCity = async (selectedCity) => {
39 | try {
40 | await deleteCity(selectedCity.id);
41 | console.log("deleted city " + selectedCity.id + " successfully");
42 | showSuccessSnackbar("City deleted successfully!");
43 | setCities((prevCities) =>
44 | prevCities.filter((city) => city.id !== selectedCity.id)
45 | );
46 | } catch (error) {
47 | showErrorSnackbar(`Whoops! ${error.message}`);
48 | }
49 | };
50 |
51 | const fetchCities = async (
52 | searchTerm = "",
53 | searchType = "name",
54 | page = 0,
55 | pageSize = rowsPerPage
56 | ) => {
57 | const queryParam = searchTerm
58 | ? `${searchType === "name" ? "name" : "searchQuery"}=${encodeURIComponent(
59 | searchTerm
60 | )}&`
61 | : "";
62 | try {
63 | const response = await axiosInstance.get(
64 | `/cities?${queryParam}pageSize=${pageSize}&pageNumber=${page + 1}`
65 | );
66 | setCities(response.data);
67 | if (response.data.length > 0) {
68 | showSuccessSnackbar("Cities fetched successfully!");
69 | } else {
70 | showErrorSnackbar(`No data were found.`);
71 | }
72 | } catch (error) {
73 | console.error("Fetching cities failed: ", error);
74 | showErrorSnackbar(`Error fetching cities: ${error.message}`);
75 | } finally {
76 | stopLoading();
77 | }
78 | };
79 |
80 | useEffect(() => {
81 | fetchCities();
82 | }, [page, rowsPerPage]);
83 |
84 | return (
85 |
86 |
87 |
88 |
89 |
90 |
99 |
100 |
101 |
110 |
111 |
112 |
113 |
128 |
129 |
130 |
131 |
137 |
138 | );
139 | };
140 |
141 | export default Cities;
142 |
--------------------------------------------------------------------------------
/src/pages/Hotel/components/AvailableRooms/AvailableRooms.jsx:
--------------------------------------------------------------------------------
1 | import { useContext, useEffect } from "react";
2 | import PropTypes from "prop-types";
3 | import styles from "./style.module.css";
4 | import CustomButton from "../../../../components/CustomButton";
5 | import PersonIcon from "@mui/icons-material/Person";
6 | import ChildFriendlyIcon from "@mui/icons-material/ChildFriendly";
7 | import AddShoppingCartIcon from "@mui/icons-material/AddShoppingCart";
8 | import { renderAmenityIcon } from "../../../../helpers/helpers";
9 | import useCartContext from "../../../../hooks/useCartContext";
10 | import useSnackbar from "../../../../hooks/useSnackbar";
11 | import GenericSnackbar from "../../../../components/GenericSnackbar";
12 | import { SearchContext } from "../../../../context/searchContext";
13 |
14 | const AvailableRooms = ({ hotelAvailableRooms }) => {
15 | const { addToCart, isRoomAlreadyInCart } = useCartContext();
16 | const { snackbar, showSuccessSnackbar, handleCloseSnackbar, showSnackbar } =
17 | useSnackbar();
18 | const { checkInDate, checkOutDate } = useContext(SearchContext);
19 |
20 | useEffect(() => {
21 | hotelAvailableRooms &&
22 | hotelAvailableRooms.length === 0 &&
23 | showSnackbar(
24 | `There are no available rooms between ${checkInDate} and ${checkOutDate}.`,
25 | "info"
26 | );
27 | }, [hotelAvailableRooms, checkInDate, checkOutDate]);
28 |
29 | const handleAddToCart = (room) => {
30 | if (!isRoomAlreadyInCart(room.roomId)) {
31 | addToCart(room);
32 | showSuccessSnackbar(`${room.roomType} Room Added to cart!`);
33 | }
34 | };
35 |
36 | const availableRooms = hotelAvailableRooms.filter(
37 | (room) => room.availability
38 | );
39 |
40 | return (
41 | <>
42 | Available Rooms
43 | {hotelAvailableRooms && hotelAvailableRooms.length === 0 && (
44 | <>
45 |
46 | {`There are no available rooms between ${checkInDate} and ${checkOutDate}.`}
47 |
48 | >
49 | )}
50 |
51 |
52 | {availableRooms.map((room, index) => (
53 |
54 |
59 |
60 |
{room.roomType} Room
61 |
${room.price}/night
62 |
63 |
64 |
65 |
68 |
Adults: {room.capacityOfAdults}
69 |
70 |
71 |
72 |
73 |
74 |
Children: {room.capacityOfChildren}
75 |
76 |
77 |
78 | {room.roomAmenities.map((amenity, amenityIndex) => (
79 |
80 | {renderAmenityIcon(amenity.name)} {amenity.name}:{" "}
81 | {amenity.description}
82 |
83 | ))}
84 |
85 |
86 |
87 |
handleAddToCart(room)}
94 | >
95 | {isRoomAlreadyInCart(room.roomId) ? "Added!" : "Add to Cart"}
96 | {!isRoomAlreadyInCart(room.roomId) && (
97 |
98 | )}
99 |
100 |
101 | ))}
102 |
103 |
104 |
105 | >
106 | );
107 | };
108 |
109 | export default AvailableRooms;
110 |
111 | AvailableRooms.propTypes = {
112 | hotelAvailableRooms: PropTypes.arrayOf(
113 | PropTypes.shape({
114 | roomType: PropTypes.string.isRequired,
115 | roomPhotoUrl: PropTypes.string.isRequired,
116 | price: PropTypes.number.isRequired,
117 | availability: PropTypes.bool.isRequired,
118 | capacityOfAdults: PropTypes.number.isRequired,
119 | capacityOfChildren: PropTypes.number.isRequired,
120 | roomAmenities: PropTypes.arrayOf(
121 | PropTypes.shape({
122 | name: PropTypes.string.isRequired,
123 | description: PropTypes.string.isRequired,
124 | })
125 | ).isRequired,
126 | })
127 | ),
128 | };
129 |
--------------------------------------------------------------------------------
/src/pages/Checkout/components/FormInformation/FormInformation.jsx:
--------------------------------------------------------------------------------
1 | import { Formik, Form } from "formik";
2 | import Button from "@mui/material/Button";
3 | import Select from "@mui/material/Select";
4 | import MenuItem from "@mui/material/MenuItem";
5 | import styles from "./style.module.css";
6 | import GenericSnackbar from "../../../../components/GenericSnackbar";
7 | import { useNavigate } from "react-router-dom";
8 | import { FormContext } from "../../../../context/CheckoutFormContext ";
9 | import useCartContext from "../../../../hooks/useCartContext";
10 | import { postNewBooking } from "../../../../services/bookingServices";
11 | import CustomTextField from "../CustomTextField";
12 | import paymentSchema from "./paymentSchema";
13 | import useSnackbar from "../../../../hooks/useSnackbar";
14 | import { useContext } from "react";
15 |
16 | const initialValues = {
17 | fullName: "",
18 | email: "",
19 | paymentMethod: "Visa",
20 | cardNumber: "",
21 | expirationDate: "",
22 | cvv: "",
23 | billingAddress: {
24 | state: "",
25 | city: "",
26 | },
27 | specialRequests: "",
28 | };
29 |
30 | const paymentMethods = ["Visa", "MasterCard", "American Express", "Discover"];
31 |
32 | const handleFormatCardNumber = (value) => {
33 | const noSpacesValue = value.replace(/\s/g, "");
34 | return noSpacesValue && noSpacesValue.match(/.{1,4}/g).join(" ");
35 | };
36 |
37 | const FormInformation = () => {
38 | const {
39 | snackbar,
40 | handleCloseSnackbar,
41 | showErrorSnackbar,
42 | showSuccessSnackbar,
43 | } = useSnackbar();
44 | const navigateToConfirmationPage = useNavigate();
45 | const { setValues } = useContext(FormContext);
46 | const { cart } = useCartContext();
47 |
48 | const handlePayment = async (values) => {
49 | try {
50 | const bookingRequest = {
51 | customerName: values.fullName,
52 | paymentMethod: values.paymentMethod,
53 | roomNumber: cart[0].roomNumber,
54 | roomType: cart[0].roomType,
55 | totalCost: cart[0].price,
56 | };
57 | const response = await postNewBooking(bookingRequest);
58 | console.log("Booking response:", response);
59 | setValues(values);
60 | showSuccessSnackbar("Completed! Thanks for your order!");
61 | setTimeout(() => {
62 | navigateToConfirmationPage("/confirmation");
63 | }, 600);
64 | } catch (error) {
65 | showErrorSnackbar("Sorry, your booking is failed.");
66 | }
67 | };
68 |
69 | return (
70 |
71 |
Payment Information
72 |
73 | To complete your booking, please provide your personal details and
74 | payment information. Additionally, feel free to include any special
75 | requests or remarks.
76 |
77 |
82 |
136 |
137 |
138 |
144 |
145 | );
146 | };
147 |
148 | export default FormInformation;
149 |
--------------------------------------------------------------------------------
/src/pages/Admin/pages/Hotels/Hotels.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 | import LeftNavigation from "../../components/LeftNavigation";
3 | import SearchBar from "../../components/SearchBar";
4 | import { CssBaseline, Box, Container } from "@mui/material";
5 | import UpdateHotelForm from "./components/UpdateHotelForm";
6 | import GenericSnackbar from "../../../../components/GenericSnackbar";
7 | import useSnackbar from "../../../../hooks/useSnackbar";
8 | import {
9 | getHotelInfoByItsId,
10 | deleteHotel,
11 | } from "../../../../services/manageHotels";
12 | import CreateHotelDialog from "./components/CreateHotelDialog";
13 | import useComponentLoader from "../../../../hooks/useComponentLoader";
14 | import axiosInstance from "../../../../Axios/axiosInstance";
15 | import DetailedGridWithLoading from "../../components/DetailedGrid/DetailedGridWithLoading";
16 |
17 | const Hotels = () => {
18 | const [hotels, setHotels] = useState([]);
19 | const [page, setPage] = useState(0);
20 | const { isLoading, stopLoading } = useComponentLoader();
21 | const [rowsPerPage, setRowsPerPage] = useState(10);
22 | const {
23 | snackbar,
24 | handleCloseSnackbar,
25 | showErrorSnackbar,
26 | showSuccessSnackbar,
27 | } = useSnackbar();
28 |
29 | const handleAddHotel = (newHotel) => {
30 | setHotels((prevHotels) => [newHotel, ...prevHotels]);
31 | };
32 |
33 | const handleUpdateHotels = (updatedHotel) => {
34 | setHotels((prevHotels) =>
35 | prevHotels.map((hotel) =>
36 | hotel.id === updatedHotel.id ? updatedHotel : hotel
37 | )
38 | );
39 | };
40 |
41 | const handleDeleteHotel = async (selectedHotel) => {
42 | try {
43 | const hotelInfo = await getHotelInfoByItsId(selectedHotel.id);
44 | await deleteHotel(hotelInfo.cityId, selectedHotel.id);
45 | console.log("deleted hotel " + selectedHotel.id + " successfully");
46 | showSuccessSnackbar("Hotel deleted successfully!");
47 | setHotels((prevHotels) =>
48 | prevHotels.filter((hotel) => hotel.id !== selectedHotel.id)
49 | );
50 | } catch (error) {
51 | showErrorSnackbar(`Whoops! ${error.message}`);
52 | }
53 | };
54 |
55 | const fetchHotels = async (
56 | searchTerm = "",
57 | searchType = "name",
58 | page = 0,
59 | pageSize = rowsPerPage
60 | ) => {
61 | const queryParam = searchTerm
62 | ? `${searchType === "name" ? "name" : "searchQuery"}=${encodeURIComponent(
63 | searchTerm
64 | )}&`
65 | : "";
66 | try {
67 | const response = await axiosInstance.get(
68 | `/hotels?${queryParam}pageSize=${pageSize}&pageNumber=${page + 1}`
69 | );
70 | setHotels(response.data);
71 | if (response.data.length > 0) {
72 | showSuccessSnackbar("Hotels fetched successfully!");
73 | } else {
74 | showErrorSnackbar(`No data were found.`);
75 | }
76 | } catch (error) {
77 | console.error("Fetching hotels failed: ", error);
78 | showErrorSnackbar(`Error fetching hotels: ${error.message}`);
79 | } finally {
80 | stopLoading();
81 | }
82 | };
83 |
84 | useEffect(() => {
85 | fetchHotels();
86 | }, [page, rowsPerPage]);
87 |
88 | return (
89 |
90 |
91 |
92 |
93 |
94 |
103 |
104 |
105 |
114 |
115 |
116 |
117 |
137 |
138 |
139 |
140 |
146 |
147 | );
148 | };
149 |
150 | export default Hotels;
151 |
--------------------------------------------------------------------------------
/src/pages/Checkout/components/CartItems/CartItems.jsx:
--------------------------------------------------------------------------------
1 | import {
2 | Typography,
3 | Card,
4 | CardContent,
5 | CardActions,
6 | Avatar,
7 | Grid,
8 | Box,
9 | Button,
10 | Chip,
11 | } from "@mui/material";
12 | import DeleteIcon from "@mui/icons-material/Delete";
13 | import ShopCartIcon from "@mui/icons-material/ShoppingCart";
14 | import SentimentVeryDissatisfiedIcon from "@mui/icons-material/SentimentVeryDissatisfied";
15 | import useCartContext from "../../../../hooks/useCartContext";
16 | import { SearchContext } from "../../../../context/searchContext";
17 | import { useContext } from "react";
18 |
19 | const CartItems = () => {
20 | const { cart, removeFromCart, getCartTotalPrice } = useCartContext();
21 | const { totalCost } = getCartTotalPrice();
22 | const { getNumberOfNights } = useContext(SearchContext);
23 |
24 | return (
25 |
26 |
42 | Your Cart
43 |
44 | {cart.length === 0 ? (
45 |
49 | Your cart is empty.
50 |
51 | ) : (
52 |
53 | {cart.map((item, index) => (
54 |
55 |
61 |
62 |
70 |
76 |
85 |
86 | {item.roomType}
87 |
88 |
89 |
90 |
95 | ${item.price} (x {getNumberOfNights()}{" "}
96 | {getNumberOfNights() > 1 ? "nights" : "night"})
97 |
98 |
99 |
100 |
101 |
102 | Room Number: {item.roomNumber}
103 |
104 |
105 |
106 | Capacity: {item.capacityOfAdults} Adults and{" "}
107 | {item.capacityOfChildren} Children
108 |
109 |
110 |
111 | {item.roomAmenities.map((amenity, amenityIndex) => (
112 |
119 | ))}
120 |
121 |
122 |
123 |
124 |
125 | }
129 | onClick={() => removeFromCart(item.roomId)}
130 | >
131 | Delete
132 |
133 |
134 |
135 |
136 | ))}
137 |
138 | )}
139 |
140 |
141 | Total cost: ${totalCost * getNumberOfNights()}
142 |
143 |
144 |
145 | );
146 | };
147 |
148 | export default CartItems;
149 |
--------------------------------------------------------------------------------
/src/__tests__/Login.test.jsx:
--------------------------------------------------------------------------------
1 | import { render, screen, fireEvent, waitFor } from "@testing-library/react";
2 | import { MemoryRouter } from "react-router-dom";
3 | import Login from "../pages/Login";
4 | import AuthContextProvider from "../context/authContext";
5 | import { loginAPI } from "../services/authService";
6 |
7 | jest.mock("../services/authService", () => ({
8 | loginAPI: jest.fn(),
9 | }));
10 |
11 | describe("Login Component", () => {
12 | it("renders Login component", () => {
13 | render(
14 |
15 |
16 |
17 |
18 |
19 | );
20 | const headerElement = screen.getByText(/Login 👋/i);
21 | expect(headerElement).toBeInTheDocument();
22 | });
23 |
24 | it("submits the form with valid credentials when trying to login as user", async () => {
25 | loginAPI.mockResolvedValue({ userType: "User" });
26 |
27 | render(
28 |
29 |
30 |
31 |
32 |
33 | );
34 |
35 | const usernameInput = screen.getByPlaceholderText(/Username/i);
36 | const passwordInput = screen.getByPlaceholderText(/Password/i);
37 | const loginButton = screen.getByText(/Log in/i);
38 |
39 | fireEvent.change(usernameInput, { target: { value: "user" } });
40 | fireEvent.change(passwordInput, { target: { value: "user" } });
41 |
42 | await waitFor(() => {
43 | expect(usernameInput).toHaveValue("user");
44 | expect(passwordInput).toHaveValue("user");
45 | });
46 |
47 | fireEvent.click(loginButton);
48 |
49 | await waitFor(() => {
50 | expect(loginAPI).toHaveBeenCalledWith({
51 | username: "user",
52 | password: "user",
53 | });
54 | });
55 |
56 | await waitFor(() => {
57 | const successMessage = screen.getByText(
58 | /Welcome, you're successfully logged in!/i
59 | );
60 | expect(successMessage).toBeInTheDocument();
61 | });
62 | });
63 |
64 | it("displays error message for invalid credentials when trying to login as user", async () => {
65 | loginAPI.mockRejectedValue(new Error("Invalid credentials"));
66 |
67 | render(
68 |
69 |
70 |
71 |
72 |
73 | );
74 |
75 | const usernameInput = screen.getByPlaceholderText(/Username/i);
76 | const passwordInput = screen.getByPlaceholderText(/Password/i);
77 | const loginButton = screen.getByText(/Log in/i);
78 |
79 | fireEvent.change(usernameInput, { target: { value: "user" } });
80 | fireEvent.change(passwordInput, { target: { value: "wrongPassword" } });
81 |
82 | fireEvent.click(loginButton);
83 |
84 | await waitFor(() => {
85 | expect(loginAPI).toHaveBeenCalledWith({
86 | username: "user",
87 | password: "wrongPassword",
88 | });
89 | });
90 |
91 | await waitFor(() => {
92 | const errorMessage = screen.getByText(
93 | /Login failed! Unable to login with provided credentials./i
94 | );
95 | expect(errorMessage).toBeInTheDocument();
96 | });
97 | });
98 | });
99 |
100 | it("submits the form with valid credentials when trying to login as admin", async () => {
101 | loginAPI.mockResolvedValue({ userType: "Admin" });
102 |
103 | render(
104 |
105 |
106 |
107 |
108 |
109 | );
110 |
111 | const usernameInput = screen.getByPlaceholderText(/Username/i);
112 | const passwordInput = screen.getByPlaceholderText(/Password/i);
113 | const loginButton = screen.getByText(/Log in/i);
114 |
115 | fireEvent.change(usernameInput, { target: { value: "admin" } });
116 | fireEvent.change(passwordInput, { target: { value: "admin" } });
117 |
118 | await waitFor(() => {
119 | expect(usernameInput).toHaveValue("admin");
120 | expect(passwordInput).toHaveValue("admin");
121 | });
122 |
123 | fireEvent.click(loginButton);
124 |
125 | await waitFor(() => {
126 | expect(loginAPI).toHaveBeenCalledWith({
127 | username: "admin",
128 | password: "admin",
129 | });
130 | });
131 |
132 | await waitFor(() => {
133 | const successMessage = screen.getByText(
134 | /Welcome, you're successfully logged in!/i
135 | );
136 | expect(successMessage).toBeInTheDocument();
137 | });
138 | });
139 |
140 | it("displays error message for invalid credentials when trying to login as admin", async () => {
141 | loginAPI.mockRejectedValue(new Error("Invalid credentials"));
142 |
143 | render(
144 |
145 |
146 |
147 |
148 |
149 | );
150 |
151 | const usernameInput = screen.getByPlaceholderText(/Username/i);
152 | const passwordInput = screen.getByPlaceholderText(/Password/i);
153 | const loginButton = screen.getByText(/Log in/i);
154 |
155 | fireEvent.change(usernameInput, { target: { value: "admin" } });
156 | fireEvent.change(passwordInput, { target: { value: "wrongPassword" } });
157 |
158 | fireEvent.click(loginButton);
159 |
160 | await waitFor(() => {
161 | expect(loginAPI).toHaveBeenCalledWith({
162 | username: "admin",
163 | password: "wrongPassword",
164 | });
165 | });
166 |
167 | await waitFor(() => {
168 | const errorMessage = screen.getByText(
169 | /Login failed! Unable to login with provided credentials./i
170 | );
171 | expect(errorMessage).toBeInTheDocument();
172 | });
173 | });
174 |
--------------------------------------------------------------------------------