├── public ├── _redirects ├── robots.txt ├── favicon.ico ├── logo192.png ├── logo512.png ├── images │ ├── cities │ │ ├── oslo.jpg │ │ ├── dubai.jpg │ │ ├── london.jpg │ │ ├── mumbai.jpg │ │ └── bangkok.jpg │ └── hotels │ │ ├── 13800549 │ │ └── 13800549.jpg │ │ ├── 32810889 │ │ └── 32810889.jpg │ │ ├── 54360345 │ │ └── 54360345.jpg │ │ ├── 252004905 │ │ └── 252004905.jpg │ │ ├── 465660377 │ │ └── 465660377.jpg │ │ ├── 469186143 │ │ └── 469186143.jpg │ │ ├── 472036509 │ │ └── 472036509.jpg │ │ ├── 481481762 │ │ ├── 481481762.jpg │ │ ├── 525626081.jpg │ │ ├── 525626095.jpg │ │ ├── 525626104.jpg │ │ └── 525626212.jpg │ │ ├── 503567645 │ │ └── 503567645.jpg │ │ ├── 513923904 │ │ └── 513923904.jpg │ │ └── 516847915 │ │ └── 516847915.jpg ├── manifest.json └── index.html ├── .eslintignore ├── src ├── components │ ├── global-navbar │ │ ├── global-navbar.scss │ │ └── GlobalNavbar.jsx │ ├── ux │ │ ├── input │ │ │ ├── input.scss │ │ │ └── Input.jsx │ │ ├── tab-panel │ │ │ └── TabPanel.jsx │ │ ├── data-range-picker │ │ │ ├── date-range-picker.scss │ │ │ └── DateRangePicker.jsx │ │ ├── checkbox │ │ │ └── Checkbox.jsx │ │ ├── toast │ │ │ ├── Toast.jsx │ │ │ └── Toast.cy.jsx │ │ ├── tabs │ │ │ └── Tabs.jsx │ │ ├── loader │ │ │ └── loader.jsx │ │ ├── dropdown-button │ │ │ └── DropdownButton.jsx │ │ └── pagination-controller │ │ │ └── PaginationController.jsx │ ├── hotel-view-card-skeleton │ │ ├── HotelViewCardSkeleton.cy.jsx │ │ └── HotelViewCardSkeleton.jsx │ ├── scroll-to-top │ │ └── ScrollToTop.js │ ├── empty-hotels-state │ │ └── EmptyHotelsState.jsx │ ├── vertical-filters-skeleton │ │ └── VerticalFiltersSkeleton.jsx │ ├── hamburger-menu │ │ └── HamburgerMenu.jsx │ ├── global-footer │ │ └── GlobalFooter.jsx │ ├── vertical-filters │ │ └── VerticalFilters.jsx │ ├── global-search-box │ │ └── GlobalSearchbox.jsx │ ├── navbar-items │ │ └── NavbarItems.jsx │ ├── hotel-view-card │ │ └── HotelViewCard.jsx │ └── results-container │ │ └── ResultsContainer.jsx ├── styles │ ├── _constants.scss │ └── _utility.scss ├── utils │ ├── helpers.js │ ├── price-helpers.js │ ├── validation-schemas.js │ ├── constants.js │ ├── validations.js │ └── date-helpers.js ├── assests │ └── logos │ │ └── stay_booker_logo.png ├── reportWebVitals.js ├── index.scss ├── routes │ ├── home │ │ ├── components │ │ │ ├── image-card-skeleton │ │ │ │ └── image-card-skeleton.jsx │ │ │ ├── image-card │ │ │ │ └── image-card.jsx │ │ │ ├── popular-locations │ │ │ │ └── popular-locations.jsx │ │ │ └── hero-cover │ │ │ │ └── HeroCover.jsx │ │ └── Home.jsx │ ├── layouts │ │ └── base-layout │ │ │ └── BaseLayout.jsx │ ├── hotel-details │ │ ├── HotelDetails.jsx │ │ └── components │ │ │ ├── hotel-details-view-card-skeleton │ │ │ └── HotelDetailsViewCardSkeleton.jsx │ │ │ ├── user-reviews │ │ │ ├── components │ │ │ │ ├── RatingsOverview.jsx │ │ │ │ ├── Review.jsx │ │ │ │ └── UserRatingsSelector.jsx │ │ │ └── UserReviews.jsx │ │ │ ├── hotel-details-view-card │ │ │ └── HotelDetailsViewCard.jsx │ │ │ └── hotel-booking-details-card │ │ │ └── HotelBookingDetailsCard.jsx │ ├── checkout │ │ └── components │ │ │ └── final-booking-summary │ │ │ └── FinalBookingSummary.jsx │ ├── about-us │ │ └── AboutUs.jsx │ ├── booking-confimation │ │ └── BookingConifrmation.jsx │ ├── user-profile │ │ ├── components │ │ │ ├── BookingPanel.jsx │ │ │ ├── PaymentsMethodsPanel.jsx │ │ │ └── ProfileDetailsPanel.jsx │ │ └── UserProfile.jsx │ ├── forgot-password │ │ └── ForgotPassword.jsx │ ├── login │ │ └── Login.jsx │ └── register │ │ └── Register.jsx ├── hooks │ └── useOutsideClickHandler.js ├── contexts │ └── AuthContext.js ├── index.js ├── services │ └── NetworkAdapter.js └── mirage │ └── data │ └── hotels.json ├── .prettierrc ├── cypress ├── fixtures │ └── profile.json ├── e2e │ ├── HomepageTest.cy.js │ └── HotelDetailsTest.cy.js └── support │ ├── component-index.html │ ├── e2e.js │ ├── commands.js │ └── component.js ├── jsconfig.json ├── .github ├── dependabot.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── tests.yml │ ├── node.js.yml │ └── check-pr-description.yml ├── .eslintrc.json ├── cypress.config.js ├── .husky └── pre-commit ├── tailwind.config.js ├── .gitignore ├── LICENSE ├── package.json ├── README.md └── CODE_OF_CONDUCT.md /public/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | hotels.json 2 | countries.json -------------------------------------------------------------------------------- /src/components/global-navbar/global-navbar.scss: -------------------------------------------------------------------------------- 1 | .site-logo__img { 2 | height: 54px; 3 | } -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "trailingComma": "es5" 5 | } 6 | -------------------------------------------------------------------------------- /src/styles/_constants.scss: -------------------------------------------------------------------------------- 1 | $primary-color: #074498; 2 | $secondary-color: #cbae37; 3 | $slate-color: #eee; -------------------------------------------------------------------------------- /cypress/fixtures/profile.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 8739, 3 | "name": "Jane", 4 | "email": "jane@example.com" 5 | } -------------------------------------------------------------------------------- /src/utils/helpers.js: -------------------------------------------------------------------------------- 1 | export default function isEmpty(obj) { 2 | return Object.keys(obj).length === 0; 3 | } 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iZooGooD/stay-booker-hotel-booking-react-frontend/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iZooGooD/stay-booker-hotel-booking-react-frontend/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iZooGooD/stay-booker-hotel-booking-react-frontend/HEAD/public/logo512.png -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "src" 4 | }, 5 | "include": [ 6 | "src" 7 | ] 8 | } -------------------------------------------------------------------------------- /public/images/cities/oslo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iZooGooD/stay-booker-hotel-booking-react-frontend/HEAD/public/images/cities/oslo.jpg -------------------------------------------------------------------------------- /public/images/cities/dubai.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iZooGooD/stay-booker-hotel-booking-react-frontend/HEAD/public/images/cities/dubai.jpg -------------------------------------------------------------------------------- /public/images/cities/london.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iZooGooD/stay-booker-hotel-booking-react-frontend/HEAD/public/images/cities/london.jpg -------------------------------------------------------------------------------- /public/images/cities/mumbai.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iZooGooD/stay-booker-hotel-booking-react-frontend/HEAD/public/images/cities/mumbai.jpg -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /public/images/cities/bangkok.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iZooGooD/stay-booker-hotel-booking-react-frontend/HEAD/public/images/cities/bangkok.jpg -------------------------------------------------------------------------------- /src/assests/logos/stay_booker_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iZooGooD/stay-booker-hotel-booking-react-frontend/HEAD/src/assests/logos/stay_booker_logo.png -------------------------------------------------------------------------------- /src/components/ux/input/input.scss: -------------------------------------------------------------------------------- 1 | .stay-booker__input { 2 | border: 2px solid $secondary-color; 3 | } 4 | 5 | .stay-booker__input { 6 | color: #868585; 7 | } -------------------------------------------------------------------------------- /public/images/hotels/13800549/13800549.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iZooGooD/stay-booker-hotel-booking-react-frontend/HEAD/public/images/hotels/13800549/13800549.jpg -------------------------------------------------------------------------------- /public/images/hotels/252004905/252004905.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iZooGooD/stay-booker-hotel-booking-react-frontend/HEAD/public/images/hotels/252004905/252004905.jpg -------------------------------------------------------------------------------- /public/images/hotels/32810889/32810889.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iZooGooD/stay-booker-hotel-booking-react-frontend/HEAD/public/images/hotels/32810889/32810889.jpg -------------------------------------------------------------------------------- /public/images/hotels/465660377/465660377.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iZooGooD/stay-booker-hotel-booking-react-frontend/HEAD/public/images/hotels/465660377/465660377.jpg -------------------------------------------------------------------------------- /public/images/hotels/469186143/469186143.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iZooGooD/stay-booker-hotel-booking-react-frontend/HEAD/public/images/hotels/469186143/469186143.jpg -------------------------------------------------------------------------------- /public/images/hotels/472036509/472036509.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iZooGooD/stay-booker-hotel-booking-react-frontend/HEAD/public/images/hotels/472036509/472036509.jpg -------------------------------------------------------------------------------- /public/images/hotels/481481762/481481762.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iZooGooD/stay-booker-hotel-booking-react-frontend/HEAD/public/images/hotels/481481762/481481762.jpg -------------------------------------------------------------------------------- /public/images/hotels/481481762/525626081.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iZooGooD/stay-booker-hotel-booking-react-frontend/HEAD/public/images/hotels/481481762/525626081.jpg -------------------------------------------------------------------------------- /public/images/hotels/481481762/525626095.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iZooGooD/stay-booker-hotel-booking-react-frontend/HEAD/public/images/hotels/481481762/525626095.jpg -------------------------------------------------------------------------------- /public/images/hotels/481481762/525626104.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iZooGooD/stay-booker-hotel-booking-react-frontend/HEAD/public/images/hotels/481481762/525626104.jpg -------------------------------------------------------------------------------- /public/images/hotels/481481762/525626212.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iZooGooD/stay-booker-hotel-booking-react-frontend/HEAD/public/images/hotels/481481762/525626212.jpg -------------------------------------------------------------------------------- /public/images/hotels/503567645/503567645.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iZooGooD/stay-booker-hotel-booking-react-frontend/HEAD/public/images/hotels/503567645/503567645.jpg -------------------------------------------------------------------------------- /public/images/hotels/513923904/513923904.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iZooGooD/stay-booker-hotel-booking-react-frontend/HEAD/public/images/hotels/513923904/513923904.jpg -------------------------------------------------------------------------------- /public/images/hotels/516847915/516847915.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iZooGooD/stay-booker-hotel-booking-react-frontend/HEAD/public/images/hotels/516847915/516847915.jpg -------------------------------------------------------------------------------- /public/images/hotels/54360345/54360345.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iZooGooD/stay-booker-hotel-booking-react-frontend/HEAD/public/images/hotels/54360345/54360345.jpg -------------------------------------------------------------------------------- /src/components/ux/tab-panel/TabPanel.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const TabPanel = ({ children }) => { 4 | return
{children}
; 5 | }; 6 | 7 | export default TabPanel; 8 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "react-app", 4 | "plugin:cypress/recommended" 5 | ], 6 | "env": { 7 | "cypress/globals": true, 8 | "browser": true, 9 | "node": true 10 | } 11 | } -------------------------------------------------------------------------------- /cypress/e2e/HomepageTest.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | describe('Homepage', () => { 4 | it('Clicking popular destinations cards navigate user to /hotels route', () => { 5 | cy.visit('http://localhost:3000/') 6 | cy.get('[data-testid=image-card]').first().click() 7 | cy.url().should('eq', 'http://localhost:3000/hotels') 8 | }) 9 | }) -------------------------------------------------------------------------------- /src/components/hotel-view-card-skeleton/HotelViewCardSkeleton.cy.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import HotelViewCardSkeleton from './HotelViewCardSkeleton'; 3 | 4 | describe('', () => { 5 | it('renders', () => { 6 | // see: https://on.cypress.io/mounting-react 7 | cy.mount(); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /cypress/support/component-index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Components App 8 | 9 | 10 |
11 | 12 | -------------------------------------------------------------------------------- /cypress.config.js: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require("cypress"); 2 | 3 | module.exports = defineConfig({ 4 | component: { 5 | devServer: { 6 | framework: "create-react-app", 7 | bundler: "webpack", 8 | }, 9 | }, 10 | 11 | e2e: { 12 | setupNodeEvents(on, config) { 13 | // implement node event listeners here 14 | }, 15 | }, 16 | }); 17 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Skip pre-commit hook if -n is provided 3 | if echo "$@" | grep -E "\-n" > /dev/null; then 4 | echo "Skipping pre-commit hook" 5 | exit 0 6 | fi 7 | 8 | # Run lint-staged and check if it fails 9 | if ! npx lint-staged; then 10 | echo "Code quality check failed, please fix the errors manually or use: npm run lint-fix" 11 | exit 1 12 | fi 13 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ['./src/**/*.{js,jsx,ts,tsx}'], 4 | theme: { 5 | extend: { 6 | colors: { 7 | 'brand': '#074498', 8 | 'brand-secondary': '#cbae37' 9 | }, 10 | fontFamily: { 11 | sans: ['Jost', 'sans-serif'], 12 | }, 13 | }, 14 | }, 15 | plugins: [], 16 | }; 17 | -------------------------------------------------------------------------------- /src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = (onPerfEntry) => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /src/components/ux/data-range-picker/date-range-picker.scss: -------------------------------------------------------------------------------- 1 | .stay-booker__input--secondary { 2 | border: 2px solid $secondary-color; 3 | } 4 | 5 | .stay-booker__input--dark { 6 | border: 1px solid rgb(204, 204, 204) !important; 7 | } 8 | 9 | .stay-booker__input { 10 | color: #868585; 11 | } 12 | 13 | .sb__date-range-picker { 14 | position: absolute; 15 | z-index: 5; 16 | left: 0; 17 | top: 100%; 18 | width: 100%; 19 | } -------------------------------------------------------------------------------- /src/components/scroll-to-top/ScrollToTop.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { useLocation } from 'react-router-dom'; 3 | 4 | /** 5 | * A component that scrolls to the top of the page when the route changes. 6 | * @component 7 | * @returns {null} 8 | */ 9 | export default function ScrollToTop() { 10 | const { pathname } = useLocation(); 11 | 12 | useEffect(() => { 13 | window.scrollTo(0, 0); 14 | }, [pathname]); 15 | 16 | return null; 17 | } 18 | -------------------------------------------------------------------------------- /src/index.scss: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @import 'react-date-range/dist/styles.css'; // main style file 6 | @import "~react-image-gallery/styles/scss/image-gallery.scss"; 7 | @import 'react-date-range/dist/theme/default.css'; // theme css file 8 | 9 | @import './styles/constants'; 10 | @import './styles/utility'; 11 | @import './components/global-navbar/global-navbar.scss'; 12 | @import './components/ux/input/input.scss'; 13 | @import './components/ux/data-range-picker/date-range-picker.scss'; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | /.vscode 22 | 23 | # scaffold test 24 | /cypress/e2e/1-getting-started 25 | /cypress/e2e/2-advanced-examples 26 | 27 | npm-debug.log* 28 | yarn-debug.log* 29 | yarn-error.log* 30 | -------------------------------------------------------------------------------- /src/utils/price-helpers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Formats the price with commas for every thousand. 3 | * @param {number} price - The price to format. 4 | * @returns {string} - The formatted price. 5 | * 6 | * @example 7 | * const formattedPrice = formatPrice(1000000); // Returns '10,00,000' 8 | * const formattedPrice = formatPrice(1000); // Returns '1,000' 9 | */ 10 | const formatPrice = (price) => { 11 | if (!price) return parseFloat(0).toLocaleString('en-IN'); 12 | return parseFloat(price).toLocaleString('en-IN'); 13 | }; 14 | 15 | export { formatPrice }; 16 | -------------------------------------------------------------------------------- /src/routes/home/components/image-card-skeleton/image-card-skeleton.jsx: -------------------------------------------------------------------------------- 1 | const ImageCardSkeleton = () => { 2 | return ( 3 |
4 | 10 | 11 | 12 |
13 |
14 | ); 15 | }; 16 | export default ImageCardSkeleton; 17 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Type of Change 2 | 3 | 4 | - [ ] Bug: [Title] 5 | - [ ] Feat: [Title] 6 | - [ ] Doc: [Title] 7 | - [ ] Test: [Title] 8 | - [ ] CI: [Title] 9 | 10 | ## Description 11 | 12 | 13 | ## Linked Issue 14 | 15 | 16 | ## Screenshots 17 | 18 | 19 | ## Tests 20 | 21 | -------------------------------------------------------------------------------- /cypress/e2e/HotelDetailsTest.cy.js: -------------------------------------------------------------------------------- 1 | describe('HotelDetails page', () => { 2 | beforeEach(() => { 3 | cy.visit('http://localhost:3000/hotel/71222'); 4 | }); 5 | 6 | it('should display the total price', () => { 7 | cy.get('.text-indigo-600').should('be.visible'); 8 | }); 9 | 10 | it('should display the date picker', () => { 11 | cy.get('[data-testid=date-range-picker]').should('be.visible'); 12 | }); 13 | 14 | it('should display an error message if check-in or check-out dates are not selected', () => { 15 | cy.get('[data-testid=toast__outlet]').should('not.exist'); 16 | cy.get('.bg-brand-secondary').click(); 17 | cy.get('[data-testid=toast__outlet]').should('be.visible'); 18 | }); 19 | }); -------------------------------------------------------------------------------- /cypress/support/e2e.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/e2e.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') -------------------------------------------------------------------------------- /src/routes/layouts/base-layout/BaseLayout.jsx: -------------------------------------------------------------------------------- 1 | import GlobalFooter from 'components/global-footer/GlobalFooter'; 2 | import GlobalNavbar from 'components/global-navbar/GlobalNavbar'; 3 | import { Outlet } from 'react-router-dom'; 4 | import ScrollToTop from 'components/scroll-to-top/ScrollToTop'; 5 | 6 | /** 7 | * BaseLayout Component 8 | * Renders the base layout for the application. 9 | * It includes the global navbar, the main content, and the global footer. 10 | * @returns {JSX.Element} - The BaseLayout component. 11 | */ 12 | const BaseLayout = () => { 13 | return ( 14 | <> 15 | 16 | 17 | 18 | 19 | 20 | ); 21 | }; 22 | 23 | export default BaseLayout; 24 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | pull_request: 6 | types: [opened, synchronize, reopened] 7 | 8 | jobs: 9 | cypress-e2e-and-component: 10 | runs-on: ubuntu-22.04 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4 14 | # Install NPM dependencies, cache them correctly 15 | # and run all Cypress tests 16 | - name: Run E2E tests 17 | uses: cypress-io/github-action@v6 18 | with: 19 | build: npm run build 20 | start: npm start 21 | 22 | - name: Run Component Testing 23 | uses: cypress-io/github-action@v6 24 | with: 25 | # we have already installed everything 26 | install: false 27 | component: true -------------------------------------------------------------------------------- /src/components/empty-hotels-state/EmptyHotelsState.jsx: -------------------------------------------------------------------------------- 1 | const EmptyHotelsState = () => ( 2 |
3 | 10 | 16 | 17 |

No Hotels Found

18 |

19 | We can't seem to find any hotels that match your search criteria. 20 |

21 |
22 | ); 23 | 24 | export default EmptyHotelsState; 25 | -------------------------------------------------------------------------------- /cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add('login', (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This will overwrite an existing command -- 25 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) -------------------------------------------------------------------------------- /src/utils/validation-schemas.js: -------------------------------------------------------------------------------- 1 | import * as Yup from 'yup'; 2 | const phoneRegExp = /^\d{10}$/; 3 | 4 | class ValidationSchema { 5 | static email = Yup.string().email('Invalid email').required('Required'); 6 | } 7 | 8 | class Schemas extends ValidationSchema { 9 | static signupSchema = Yup.object().shape({ 10 | firstName: Yup.string() 11 | .min(2, 'Too Short!') 12 | .max(50, 'Too Long!') 13 | .required('Required'), 14 | lastName: Yup.string() 15 | .min(2, 'Too Short!') 16 | .max(50, 'Too Long!') 17 | .required('Required'), 18 | email: ValidationSchema.email, 19 | phoneNumber: Yup.string() 20 | .matches(phoneRegExp, 'Phone number is not valid') 21 | .required('Required'), 22 | password: Yup.string() 23 | .min(8, 'Password is too short - should be 8 chars minimum.') 24 | .required('Required'), 25 | confirmPassword: Yup.string().required('Required'), 26 | }); 27 | } 28 | 29 | export default Schemas; 30 | -------------------------------------------------------------------------------- /src/utils/constants.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Maximum number of guests allowed in the input 3 | */ 4 | export const MAX_GUESTS_INPUT_VALUE = 10; 5 | 6 | /** 7 | * Messages related to user registration. 8 | */ 9 | export const REGISTRATION_MESSAGES = { 10 | SUCCESS: 'User created successfully. Redirecting to login...', 11 | }; 12 | 13 | /** 14 | * Messages related to user login. 15 | */ 16 | export const LOGIN_MESSAGES = { 17 | FAILED: 'Please enter valid email and password', 18 | }; 19 | 20 | /** 21 | * Represents the default tax details for hotel booking. 22 | */ 23 | export const DEFAULT_TAX_DETAILS = 24 | 'GST: 12% on INR 0 - 2,500, 12% on INR 2,500-7,500, 18% on INR 7,500 and above'; 25 | 26 | /** 27 | * Sorting filter labels 28 | */ 29 | export const SORTING_FILTER_LABELS = Object.freeze({ 30 | PRICE_LOW_TO_HIGH: 'Price: Low to High', 31 | PRICE_HIGH_TO_LOW: 'Price: High to Low', 32 | RATING_LOW_TO_HIGH: 'Rating: Low to High', 33 | RATING_HIGH_TO_LOW: 'Rating: High to Low', 34 | }); 35 | -------------------------------------------------------------------------------- /src/components/ux/checkbox/Checkbox.jsx: -------------------------------------------------------------------------------- 1 | const Checkbox = (props) => { 2 | const { id, filterId, label, subtitle, onFiltersUpdate, isSelected } = props; 3 | const onChange = () => { 4 | onFiltersUpdate({ filterId, id }); 5 | }; 6 | 7 | return ( 8 |
9 | 17 | 20 | {subtitle && ( 21 | {subtitle} 22 | )} 23 |
24 | ); 25 | }; 26 | 27 | export default Checkbox; 28 | -------------------------------------------------------------------------------- /cypress/support/component.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/component.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | 22 | import { mount } from 'cypress/react18' 23 | import '../../src/index.scss'; 24 | 25 | Cypress.Commands.add('mount', (component) => { 26 | return mount(component); 27 | }); 28 | 29 | // Example use: 30 | // cy.mount() -------------------------------------------------------------------------------- /src/routes/home/components/image-card/image-card.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | /** 4 | * ImageCard 5 | * Renders an image card with a name and image. 6 | * @param {Object} props - The component props. 7 | * @param {String} props.name - The name of the destination. 8 | * @param {String} props.imageUrl - The image url of the destination. 9 | * @param {Function} props.onPopularDestincationCardClick - The click handler for the card. 10 | * @returns {JSX.Element} - The ImageCard component. 11 | */ 12 | const ImageCard = (props) => { 13 | const { name, imageUrl, onPopularDestincationCardClick } = props; 14 | return ( 15 |
onPopularDestincationCardClick(name)} 18 | data-testid="image-card" 19 | > 20 | mumbai 21 |

{name}

22 |
23 | ); 24 | }; 25 | export default ImageCard; 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /src/hooks/useOutsideClickHandler.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | 3 | /** 4 | * A custom hook that triggers a callback function when a click event occurs outside of a specified element. 5 | * 6 | * @param {React.RefObject} ref - A React ref object attached to the element to monitor for outside clicks. 7 | * @param {Function} onOutsideClick - A callback function to execute when an outside click is detected. 8 | */ 9 | const useOutsideClickHandler = (ref, onOutsideClick) => { 10 | useEffect(() => { 11 | const handleClickOutside = (event) => { 12 | if (ref.current && !ref.current.contains(event.target)) { 13 | onOutsideClick(event); 14 | } 15 | }; 16 | 17 | // Bind the event listener 18 | document.addEventListener('mousedown', handleClickOutside); 19 | return () => { 20 | // Unbind the event listener on clean up 21 | document.removeEventListener('mousedown', handleClickOutside); 22 | }; 23 | }, [ref, onOutsideClick]); 24 | }; 25 | 26 | export default useOutsideClickHandler; 27 | -------------------------------------------------------------------------------- /src/utils/validations.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Utility object for field validations. 3 | * @namespace validations 4 | */ 5 | const validations = { 6 | /** 7 | * Field validation rules. 8 | * @memberof validations 9 | * @property {object} email - Validation rules for the email field. 10 | * @property {boolean} email.required - Indicates if the email field is required. 11 | * @property {RegExp} email.pattern - Regular expression pattern to validate the email field. 12 | */ 13 | fields: { 14 | email: { 15 | required: true, 16 | pattern: /^[^\s-]\w+@[a-zA-Z_]+?\.[a-zA-Z]{2,3}[^\s-]$/i, 17 | }, 18 | }, 19 | 20 | /** 21 | * Validates a field value based on the specified validation rules. 22 | * @memberof validations 23 | * @param {string} field - The name of the field to validate. 24 | * @param {string} value - The value of the field to validate. 25 | * @returns {boolean} - Indicates if the field value is valid. 26 | */ 27 | validate(field, value) { 28 | return this.fields[field] ? this.fields[field].pattern.test(value) : false; 29 | }, 30 | }; 31 | 32 | export default validations; 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Lakshman Chaudhary 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/styles/_utility.scss: -------------------------------------------------------------------------------- 1 | .brand-divider-bottom { 2 | border-bottom: 1px solid #053679; 3 | } 4 | 5 | .transform-center-y { 6 | position: absolute; 7 | top: 50%; 8 | transform: translate(-50%, -50%); 9 | } 10 | 11 | // animations and effects 12 | .hover-underline-animation { 13 | display: inline-block; 14 | position: relative; 15 | } 16 | 17 | .hover-underline-animation::after { 18 | content: ''; 19 | position: absolute; 20 | width: 100%; 21 | transform: scaleX(0); 22 | height: 2px; 23 | bottom: 0; 24 | left: 0; 25 | background-color: $secondary-color; 26 | transform-origin: bottom right; 27 | transition: transform .3s ease-in; 28 | } 29 | 30 | .hover-underline-animation:hover::after { 31 | transform: scaleX(1); 32 | transform-origin: bottom left; 33 | } 34 | 35 | .active-link { 36 | display: inline-block; 37 | position: relative; 38 | } 39 | 40 | .active-link::after { 41 | content: ''; 42 | position: absolute; 43 | width: 100%; 44 | height: 2px; 45 | bottom: 0; 46 | left: 0; 47 | background-color: $secondary-color; 48 | transform: scaleX(1); 49 | transform-origin: bottom left; 50 | } -------------------------------------------------------------------------------- /src/contexts/AuthContext.js: -------------------------------------------------------------------------------- 1 | import React, { createContext, useState, useEffect } from 'react'; 2 | import { networkAdapter } from 'services/NetworkAdapter'; 3 | 4 | export const AuthContext = createContext(); 5 | 6 | /** 7 | * Provides authentication state and user details to the application. 8 | * @namespace AuthProvider 9 | * @component 10 | */ 11 | export const AuthProvider = ({ children }) => { 12 | const [isAuthenticated, setIsAuthenticated] = useState(false); 13 | const [userDetails, setUserDetails] = useState(null); 14 | const [authCheckTrigger, setAuthCheckTrigger] = useState(false); 15 | 16 | useEffect(() => { 17 | const checkAuthStatus = async () => { 18 | const response = await networkAdapter.get('api/users/auth-user'); 19 | if (response && response.data) { 20 | setIsAuthenticated(response.data.isAuthenticated); 21 | setUserDetails(response.data.userDetails); 22 | } 23 | }; 24 | 25 | checkAuthStatus(); 26 | }, [authCheckTrigger]); 27 | 28 | const triggerAuthCheck = () => { 29 | setAuthCheckTrigger((prev) => !prev); 30 | }; 31 | 32 | return ( 33 | 36 | {children} 37 | 38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /src/components/vertical-filters-skeleton/VerticalFiltersSkeleton.jsx: -------------------------------------------------------------------------------- 1 | const VerticalFiltersSkeleton = () => { 2 | return ( 3 |
7 |
8 |
9 | 10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | ); 25 | }; 26 | 27 | export default VerticalFiltersSkeleton; 28 | -------------------------------------------------------------------------------- /src/routes/hotel-details/HotelDetails.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { useParams } from 'react-router-dom'; 3 | import { networkAdapter } from 'services/NetworkAdapter'; 4 | import HotelDetailsViewCard from './components/hotel-details-view-card/HotelDetailsViewCard'; 5 | import HotelDetailsViewCardSkeleton from './components/hotel-details-view-card-skeleton/HotelDetailsViewCardSkeleton'; 6 | 7 | /** 8 | * Represents the hotel details component. 9 | * @component 10 | * @returns {JSX.Element} The hotel details component. 11 | */ 12 | const HotelDetails = () => { 13 | const { hotelId } = useParams(); 14 | const [hotelDetails, setHotelDetails] = useState({ 15 | isLoading: true, 16 | data: {}, 17 | }); 18 | 19 | useEffect(() => { 20 | const fetchHotelDetails = async () => { 21 | const response = await networkAdapter.get(`/api/hotel/${hotelId}`); 22 | setHotelDetails({ 23 | isLoading: false, 24 | data: response.data, 25 | }); 26 | }; 27 | 28 | fetchHotelDetails(); 29 | }, [hotelId]); 30 | 31 | return ( 32 | <> 33 | {hotelDetails.isLoading ? ( 34 | 35 | ) : ( 36 | 37 | )} 38 | 39 | ); 40 | }; 41 | 42 | export default HotelDetails; 43 | -------------------------------------------------------------------------------- /src/components/ux/toast/Toast.jsx: -------------------------------------------------------------------------------- 1 | import { faXmark } from '@fortawesome/free-solid-svg-icons'; 2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 3 | 4 | /** 5 | * Toast Component 6 | * Displays a toast message with a dismiss button. 7 | * 8 | * @param {Object} props - Props for the component. 9 | * @param {string} props.type - The type of toast message. 10 | * @param {string} props.message - The message to display in the toast. 11 | * @param {Function} props.dismissError - The function to dismiss the toast message. 12 | */ 13 | const Toast = ({ type, message, dismissError }) => { 14 | const typeToClassMap = { 15 | error: 'bg-red-100 border-l-4 border-red-500 text-red-700', 16 | success: 'bg-green-100 border-l-4 border-green-500 text-green-700 my-2', 17 | warning: 'bg-yellow-100 border-l-4 border-yellow-500 text-yellow-700', 18 | }; 19 | return ( 20 |
24 |

{message}

25 | dismissError()} 27 | className="text-red-500 hover:text-red-700 ml-2" 28 | icon={faXmark} 29 | size="lg" 30 | data-testid="toast__dismiss" 31 | /> 32 |
33 | ); 34 | }; 35 | 36 | export default Toast; 37 | -------------------------------------------------------------------------------- /src/utils/date-helpers.js: -------------------------------------------------------------------------------- 1 | import { parse, format } from 'date-fns'; 2 | 3 | /** 4 | * Formats a JavaScript Date object into a string with the format "DD/MM/YYYY". 5 | * 6 | * @param date - The Date object to format. 7 | * @returns A string representing the formatted date. 8 | * @example formatDate(new Date('2022-01-01')) // "01/01/2022" 9 | */ 10 | function formatDate(date) { 11 | // Check if the date is undefined or not a valid Date object 12 | if (!date || !(date instanceof Date) || isNaN(date.getTime())) { 13 | return; 14 | } 15 | let day = date.getDate().toString(); 16 | let month = (date.getMonth() + 1).toString(); 17 | let year = date.getFullYear().toString(); 18 | 19 | day = day.length < 2 ? `0${day}` : day; 20 | month = month.length < 2 ? `0${month}` : month; 21 | return `${day}/${month}/${year}`; 22 | } 23 | 24 | /** 25 | * Formats a date string in the format "DD-MM-YYYY" into a more readable format. 26 | * 27 | * @param dateString - The date string to format. 28 | * @returns A string representing the formatted date. 29 | * @example getReadableMonthFormat('01-01-2022') // "1 January 2022" 30 | */ 31 | function getReadableMonthFormat(dateString) { 32 | if (!dateString) { 33 | return ''; 34 | } 35 | return format(parse(dateString, 'dd-MM-yyyy', new Date()), 'd MMMM yyyy'); 36 | } 37 | 38 | export { formatDate, getReadableMonthFormat }; 39 | -------------------------------------------------------------------------------- /src/routes/hotel-details/components/hotel-details-view-card-skeleton/HotelDetailsViewCardSkeleton.jsx: -------------------------------------------------------------------------------- 1 | const HotelDetailsViewCardSkeleton = () => { 2 | return ( 3 |
4 |
5 |
6 | 7 | 8 | 9 |
10 |
11 |
12 |
13 |
14 | {Array.from({ length: 8 }).map((_, i) => ( 15 |
19 | ))} 20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | ); 30 | }; 31 | export default HotelDetailsViewCardSkeleton; 32 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: 4 | push: 5 | branches: ['master'] 6 | pull_request: 7 | branches: ['master'] 8 | 9 | jobs: 10 | build: 11 | name: Build 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | - name: Cache node modules 17 | uses: actions/cache@v2 18 | with: 19 | path: node_modules 20 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 21 | restore-keys: | 22 | ${{ runner.os }}-node- 23 | - name: Use Node.js 18.x 24 | uses: actions/setup-node@v3 25 | with: 26 | node-version: 18.x 27 | cache: 'npm' 28 | - run: npm ci 29 | 30 | code-quality: 31 | name: Code Quality Checks 32 | runs-on: ubuntu-latest 33 | 34 | steps: 35 | - uses: actions/checkout@v3 36 | - name: Cache node modules 37 | uses: actions/cache@v2 38 | with: 39 | path: node_modules 40 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 41 | restore-keys: | 42 | ${{ runner.os }}-node- 43 | - name: Use Node.js 18.x 44 | uses: actions/setup-node@v3 45 | with: 46 | node-version: 18.x 47 | cache: 'npm' 48 | - run: npm ci 49 | - name: Check code formatting with Prettier 50 | run: npm run format:check 51 | - name: Check code quality with ESLint 52 | run: npm run lint:check 53 | -------------------------------------------------------------------------------- /.github/workflows/check-pr-description.yml: -------------------------------------------------------------------------------- 1 | name: Check PR Description 2 | 3 | on: 4 | pull_request: 5 | types: [opened, edited] 6 | 7 | jobs: 8 | check-description: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout code 12 | uses: actions/checkout@v2 13 | 14 | - name: Check PR Description for Required Sections 15 | env: 16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | run: | 18 | PR_DESCRIPTION=$(jq -r .pull_request.body "$GITHUB_EVENT_PATH") 19 | 20 | echo "Checking PR Description..." 21 | echo "$PR_DESCRIPTION" 22 | 23 | MISSING_SECTIONS="" 24 | 25 | if ! echo "$PR_DESCRIPTION" | grep -q "## Description"; then 26 | MISSING_SECTIONS="Description\n" 27 | fi 28 | if ! echo "$PR_DESCRIPTION" | grep -q "## Screenshots"; then 29 | MISSING_SECTIONS="${MISSING_SECTIONS}Screenshots\n" 30 | fi 31 | if ! echo "$PR_DESCRIPTION" | grep -q "## Tests"; then 32 | MISSING_SECTIONS="${MISSING_SECTIONS}Tests\n" 33 | fi 34 | 35 | if [ -n "$MISSING_SECTIONS" ]; then 36 | echo "🚨 The PR is missing the following sections: $MISSING_SECTIONS" 37 | exit 1 38 | else 39 | echo "✅ All required sections are present." 40 | fi 41 | -------------------------------------------------------------------------------- /src/components/global-navbar/GlobalNavbar.jsx: -------------------------------------------------------------------------------- 1 | import logo from 'assests/logos/stay_booker_logo.png'; 2 | import { useState } from 'react'; 3 | import { Link } from 'react-router-dom'; 4 | import HamburgerMenu from 'components/hamburger-menu/HamburgerMenu'; 5 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 6 | import { faBars } from '@fortawesome/free-solid-svg-icons'; 7 | import { AuthContext } from 'contexts/AuthContext'; 8 | import { useContext } from 'react'; 9 | import NavbarItems from 'components/navbar-items/NavbarItems'; 10 | 11 | const GlobalNavbar = () => { 12 | const [isVisible, setIsVisible] = useState(false); 13 | const { isAuthenticated } = useContext(AuthContext); 14 | const onHamburgerMenuToggle = () => { 15 | setIsVisible(!isVisible); 16 | }; 17 | 18 | return ( 19 |
20 |
21 | 22 | site logo 23 | 24 |
25 |
    26 | 27 |
28 | 36 | 41 |
42 | ); 43 | }; 44 | 45 | export default GlobalNavbar; 46 | -------------------------------------------------------------------------------- /src/components/ux/toast/Toast.cy.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Toast from './Toast'; 3 | 4 | describe('Toast Component', () => { 5 | it('renders error toast correctly', () => { 6 | const dismissError = cy.spy().as('dismissError'); 7 | const type = 'error'; 8 | const message = 'An error occurred'; 9 | 10 | cy.mount( 11 | 12 | ); 13 | 14 | cy.get('[data-testid="toast__outlet"]').should('exist'); 15 | cy.get('[data-testid="toast__outlet"]').should( 16 | 'have.class', 17 | 'bg-red-100 border-l-4 border-red-500 text-red-700' 18 | ); 19 | cy.get('[data-testid="toast__message"]').should('have.text', message); 20 | cy.get('[data-testid="toast__dismiss"]').should('exist'); 21 | cy.get('[data-testid="toast__dismiss"]').click(); 22 | cy.get('@dismissError').should('have.been.calledOnce'); 23 | }); 24 | 25 | it('renders success toast correctly', () => { 26 | const dismissError = cy.spy().as('dismissError'); 27 | const type = 'success'; 28 | const message = 'Action completed successfully'; 29 | 30 | cy.mount( 31 | 32 | ); 33 | 34 | cy.get('[data-testid="toast__outlet"]').should('exist'); 35 | cy.get('[data-testid="toast__outlet"]').should( 36 | 'have.class', 37 | 'bg-green-100 border-l-4 border-green-500 text-green-700 my-2' 38 | ); 39 | cy.get('[data-testid="toast__message"]').should('have.text', message); 40 | cy.get('[data-testid="toast__dismiss"]').should('exist'); 41 | cy.get('[data-testid="toast__dismiss"]').click(); 42 | cy.get('@dismissError').should('have.been.calledOnce'); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /src/components/hamburger-menu/HamburgerMenu.jsx: -------------------------------------------------------------------------------- 1 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 2 | import { faXmark } from '@fortawesome/free-solid-svg-icons'; 3 | import NavbarItems from 'components/navbar-items/NavbarItems'; 4 | 5 | /** 6 | * HamburgerMenu Component 7 | * Renders a hamburger menu with navigation links. It can be toggled visible or hidden. 8 | * The menu contains links to Home, Hotels, About Us, and depending on the authentication status, 9 | * a link to either the user profile or the login/register page. 10 | * 11 | * @param {Object} props - Props for the component. 12 | * @param {boolean} props.isVisible - Controls the visibility of the hamburger menu. 13 | * @param {Function} props.onHamburgerMenuToggle - Callback function to toggle the visibility of the menu. 14 | * @param {boolean} props.isAuthenticated - Indicates whether the user is authenticated. 15 | */ 16 | const HamburgerMenu = (props) => { 17 | const { isVisible, onHamburgerMenuToggle, isAuthenticated } = props; 18 | 19 | return ( 20 |
26 |
27 | 34 |
35 |
    36 | 40 |
41 |
42 | ); 43 | }; 44 | 45 | export default HamburgerMenu; 46 | -------------------------------------------------------------------------------- /src/components/hotel-view-card-skeleton/HotelViewCardSkeleton.jsx: -------------------------------------------------------------------------------- 1 | const HotelViewCardSkeleton = () => { 2 | return ( 3 |
7 |
8 | 9 | 10 | 11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
    21 |
    22 |
    23 |
    24 |
    25 |
    26 |
    27 |
    28 |
    29 | 32 |
    33 |
    34 |
    35 | ); 36 | }; 37 | export default HotelViewCardSkeleton; 38 | -------------------------------------------------------------------------------- /src/components/global-footer/GlobalFooter.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | const FooterLink = ({ to, label }) => ( 5 | 9 | {label} 10 | 11 | ); 12 | 13 | const GlobalFooter = () => { 14 | return ( 15 |
    16 |
    17 |
    18 |
    19 |

    Company Info

    20 | 21 | 22 | 23 |
    24 |
    25 |

    Support

    26 | 27 |
    28 |
    29 |

    Newsletter

    30 |

    Stay updated with our latest trends

    31 |
    32 | 37 | 40 |
    41 |
    42 |
    43 |
    44 |

    Designed and styled by izoogood

    45 |

    46 | © {new Date().getFullYear()} izoogood. All rights reserved. 47 |

    48 |
    49 |
    50 |
    51 | ); 52 | }; 53 | 54 | export default GlobalFooter; 55 | -------------------------------------------------------------------------------- /src/components/ux/tabs/Tabs.jsx: -------------------------------------------------------------------------------- 1 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 2 | import React, { useState } from 'react'; 3 | 4 | const Tabs = ({ children, isTabsVisible, wrapperRef }) => { 5 | const [activeTab, setActiveTab] = useState(children[0].props.label); 6 | 7 | const onClickTabItem = (tab) => { 8 | setActiveTab(tab); 9 | }; 10 | 11 | return ( 12 |
    13 |
    18 |
    19 |
      20 | {children.map((child) => { 21 | const { label, icon } = child.props; 22 | return ( 23 |
    • 29 | 33 | 41 |
    • 42 | ); 43 | })} 44 |
    45 |
    46 |
    47 |
    48 | {children.map((child) => { 49 | if (child.props.label !== activeTab) return undefined; 50 | return child.props.children; 51 | })} 52 |
    53 |
    54 | ); 55 | }; 56 | 57 | export default Tabs; 58 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 16 | 25 | 26 | 27 | 28 | StayBooker-Pro - Hotel bookings and more 29 | 30 | 31 | 32 | 33 |
    34 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/routes/hotel-details/components/user-reviews/components/RatingsOverview.jsx: -------------------------------------------------------------------------------- 1 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 2 | import { faStar as fasStar } from '@fortawesome/free-solid-svg-icons'; 3 | 4 | /** 5 | * Renders the ratings overview component. 6 | * 7 | * @component 8 | * @param {Object} props - The component props. 9 | * @param {number} props.userRating - The user's rating. 10 | * @param {number} props.averageRating - The average rating. 11 | * @param {number} props.ratingsCount - The total count of ratings. 12 | * @param {number[]} props.starCounts - The count of each star rating. 13 | * @param {Function} props.handleRating - The function to handle rating changes made by user. 14 | * @returns {JSX.Element} The rendered component. 15 | */ 16 | const RatingsOverview = ({ averageRating, ratingsCount, starCounts }) => { 17 | return ( 18 |
    19 |
    Overall Rating
    20 |
    {averageRating}/5
    21 |
    Based on {ratingsCount} reviews
    22 | {Object.keys(starCounts) 23 | .sort((a, b) => b - a) 24 | .map((starRating) => ( 25 |
    26 |
    27 | {starRating}{' '} 28 | 32 |
    33 |
    34 |
    40 |
    41 | {starCounts[starRating]} 42 |
    43 | ))} 44 |
    45 | ); 46 | }; 47 | 48 | export default RatingsOverview; 49 | -------------------------------------------------------------------------------- /src/components/ux/input/Input.jsx: -------------------------------------------------------------------------------- 1 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 2 | import { useState } from 'react'; 3 | 4 | const Input = (props) => { 5 | const { 6 | classes, 7 | value, 8 | onChangeInput, 9 | icon, 10 | type, 11 | placeholder, 12 | typeheadResults, 13 | } = props; 14 | const [isTypeheadVisible, setIsTypeheadVisible] = useState(false); 15 | 16 | const onTypeheadResultClick = (value) => { 17 | onChangeInput(value); 18 | }; 19 | 20 | const onBlur = () => { 21 | // Delay hiding the typehead results to allow time for click event on result 22 | setTimeout(() => { 23 | setIsTypeheadVisible(false); 24 | }, 200); 25 | }; 26 | 27 | return ( 28 |
    29 | onChangeInput(e.target.value)} 36 | placeholder={placeholder} 37 | onBlur={onBlur} 38 | onFocus={() => setIsTypeheadVisible(true)} 39 | > 40 | {icon && ( 41 | 46 | )} 47 |
    52 |
      53 | {typeheadResults && 54 | value.length > 0 && 55 | typeheadResults.map((result, index) => ( 56 |
    • onTypeheadResultClick(result)} 60 | > 61 | {result} 62 |
    • 63 | ))} 64 |
    65 |
    66 |
    67 | ); 68 | }; 69 | 70 | export default Input; 71 | -------------------------------------------------------------------------------- /src/routes/hotel-details/components/user-reviews/components/Review.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { faUserCircle } from '@fortawesome/free-solid-svg-icons'; 3 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 4 | 5 | /** 6 | * Represents a review component. 7 | * @component 8 | * 9 | * @param {Object} props - The props object containing reviewerName, reviewDate, review, and rating. 10 | * @param {string} props.reviewerName - The name of the reviewer. 11 | * @param {string} props.reviewDate - The date of the review. 12 | * @param {string} props.review - The content of the review. 13 | * @param {number} props.rating - The rating given by the reviewer. 14 | * @param {boolean} props.verified - Whether the review has been verified or not. 15 | * 16 | * @returns {JSX.Element} The rendered Review component. 17 | */ 18 | const Review = (props) => { 19 | const { reviewerName, reviewDate, review, rating, verified } = props; 20 | return ( 21 |
    22 |
    23 |
    24 | 28 |

    29 | {reviewerName}{' '} 30 | {verified && ( 31 | 32 | (Verified) 33 | 34 | )} 35 |

    36 |
    37 |
    38 |

    {rating}

    39 | 44 | 45 | 46 |
    47 |
    48 |

    {reviewDate}

    49 |

    {review}

    50 |
    51 | ); 52 | }; 53 | 54 | export default Review; 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stay-booker-pro", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@fortawesome/fontawesome-svg-core": "^6.5.2", 7 | "@fortawesome/free-regular-svg-icons": "^6.5.2", 8 | "@fortawesome/free-solid-svg-icons": "^6.5.2", 9 | "@fortawesome/react-fontawesome": "^0.2.0", 10 | "@testing-library/user-event": "^14.5.2", 11 | "date-fns": "^3.6.0", 12 | "formik": "^2.4.5", 13 | "husky": "^8.0.3", 14 | "lodash": "^4.17.21", 15 | "miragejs": "^0.1.48", 16 | "query-string": "^9.0.0", 17 | "react": "^18.3.1", 18 | "react-date-range": "^2.0.0-alpha.4", 19 | "react-dom": "^18.3.1", 20 | "react-image-gallery": "^1.3.0", 21 | "react-router-dom": "^6.23.1", 22 | "react-scripts": "5.0.1", 23 | "react-select": "^5.8.0", 24 | "react-to-print": "^2.15.1", 25 | "sass": "^1.75.0", 26 | "web-vitals": "^4.0.1", 27 | "yup": "^1.4.0" 28 | }, 29 | "scripts": { 30 | "start": "react-scripts start", 31 | "build": "react-scripts build", 32 | "test": "npx cypress run --e2e && npx cypress run --component", 33 | "eject": "react-scripts eject", 34 | "format:check": "prettier --check 'src/**/*.{js,jsx,ts,tsx,json,css,md}'", 35 | "lint:check": "eslint .", 36 | "lint-fix": "lint-staged", 37 | "prepare": "husky install", 38 | "cypress:open": "cypress open", 39 | "cypress:run": "npx cypress run --e2e && npx cypress run --component" 40 | }, 41 | "browserslist": { 42 | "production": [ 43 | ">0.2%", 44 | "not dead", 45 | "not op_mini all" 46 | ], 47 | "development": [ 48 | "last 1 chrome version", 49 | "last 1 firefox version", 50 | "last 1 safari version" 51 | ] 52 | }, 53 | "lint-staged": { 54 | "src/**/*.{js,jsx,ts,tsx,json,css,md}": [ 55 | "prettier --write 'src/**/*.{js,jsx,ts,tsx,json,css,md}'", 56 | "eslint --fix .", 57 | "git add" 58 | ] 59 | }, 60 | "devDependencies": { 61 | "cypress": "^13.10.0", 62 | "eslint": "^8.57.0", 63 | "eslint-plugin-cypress": "^2.15.1", 64 | "lint-staged": "^15.2.2", 65 | "prettier": "^3.2.5", 66 | "tailwindcss": "^3.4.3", 67 | "wait-on": "^7.2.0" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/routes/home/components/popular-locations/popular-locations.jsx: -------------------------------------------------------------------------------- 1 | import ImageCard from '../image-card/image-card'; 2 | import ImageCardSkeleton from '../image-card-skeleton/image-card-skeleton'; 3 | import { useNavigate } from 'react-router-dom'; 4 | 5 | /** 6 | * A component that displays a list of popular destinations with their respective image cards. 7 | * @param {Object} props - The component's props. 8 | * @param {Object} props.popularDestinationsData - The data for popular destinations. 9 | * @param {boolean} props.popularDestinationsData.isLoading - Indicates if the data is currently loading. 10 | * @param {Array} props.popularDestinationsData.data - The list of popular destination objects, each with the following properties: 11 | * @param {number} props.popularDestinationsData.data[].code - The unique code for the destination. 12 | * @param {string} props.popularDestinationsData.data[].name - The name of the destination. 13 | * @param {string} props.popularDestinationsData.data[].imageUrl - The URL of the destination's image. 14 | * @param {Array} props.popularDestinationsData.errors - Any errors that occurred while fetching the data. 15 | */ 16 | const PopularLocations = (props) => { 17 | const { popularDestinationsData } = props; 18 | const navigate = useNavigate(); 19 | 20 | const onPopularDestincationCardClick = (city) => { 21 | navigate('/hotels', { 22 | state: { 23 | city: city.toString().toLowerCase(), 24 | }, 25 | }); 26 | }; 27 | 28 | return ( 29 |
    30 |

    31 | Book Hotels at Popular Destinations 32 |

    33 |
    34 | {popularDestinationsData.isLoading 35 | ? Array.from({ length: 5 }, (_, index) => ( 36 | 37 | )) 38 | : popularDestinationsData.data.map((city) => ( 39 | 45 | ))} 46 |
    47 |
    48 | ); 49 | }; 50 | export default PopularLocations; 51 | -------------------------------------------------------------------------------- /src/components/ux/loader/loader.jsx: -------------------------------------------------------------------------------- 1 | const Loader = ({ height, isFullScreen, loaderText }) => { 2 | const heightClass = height ? `min-h-[${height}]` : `h-[120px]`; 3 | return ( 4 |
    5 |
    12 |
    13 |

    14 | 30 |

    31 | 32 | Loading... 33 | {loaderText && ( 34 | 35 | {loaderText} 36 | 37 | )} 38 |
    39 |
    40 |
    41 | ); 42 | }; 43 | export default Loader; 44 | -------------------------------------------------------------------------------- /src/routes/checkout/components/final-booking-summary/FinalBookingSummary.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { differenceInCalendarDays } from 'date-fns'; 3 | 4 | /** 5 | * Component for displaying the final booking summary. 6 | * @param {Object} props The component props. 7 | * @param {string} props.hotelName The name of the hotel. 8 | * @param {string} props.checkIn The check-in date. 9 | * @param {string} props.checkOut The check-out date. 10 | * @param {boolean} props.isAuthenticated The user authentication status. 11 | * @param {string} props.phone The user's phone number. 12 | * @param {string} props.email The user's email. 13 | * @param {string} props.fullName The user's full name. 14 | * 15 | * @returns {JSX.Element} The rendered FinalBookingSummary component. 16 | */ 17 | const FinalBookingSummary = ({ 18 | hotelName, 19 | checkIn, 20 | checkOut, 21 | isAuthenticated, 22 | phone, 23 | email, 24 | fullName, 25 | }) => { 26 | const numNights = differenceInCalendarDays( 27 | new Date(checkOut), 28 | new Date(checkIn) 29 | ); 30 | return ( 31 |
    32 |
    33 |

    {hotelName}

    34 |
    35 |
    36 |

    Check-in

    37 |

    {checkIn}

    38 |
    39 |
    40 |

    41 | {numNights} Nights 42 |

    43 |
    44 |
    45 |

    Check-out

    46 |

    {checkOut}

    47 |
    48 |
    49 |
    50 | {isAuthenticated && ( 51 |
    52 |

    53 | Booking details will be sent to: 54 |

    55 |

    {fullName} (Primary)

    56 |

    {email}

    57 |

    {phone}

    58 |
    59 | )} 60 |
    61 | ); 62 | }; 63 | 64 | export default FinalBookingSummary; 65 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './index.scss'; 4 | import HotelsSearch from './routes/listings/HotelsSearch'; 5 | import UserProfile from './routes/user-profile/UserProfile'; 6 | import { createBrowserRouter, RouterProvider } from 'react-router-dom'; 7 | import reportWebVitals from './reportWebVitals'; 8 | import Home from './routes/home/Home'; 9 | import { AuthProvider } from './contexts/AuthContext'; 10 | import { makeServer } from './mirage/mirageServer'; 11 | import HotelDetails from './routes/hotel-details/HotelDetails'; 12 | import Login from './routes/login/Login'; 13 | import Register from './routes/register/Register'; 14 | import AboutUs from './routes/about-us/AboutUs'; 15 | import BaseLayout from './routes/layouts/base-layout/BaseLayout'; 16 | import ForgotPassword from './routes/forgot-password/ForgotPassword'; 17 | import Checkout from 'routes/checkout/Checkout'; 18 | import BookingConfirmation from 'routes/booking-confimation/BookingConifrmation'; 19 | 20 | // if (process.env.NODE_ENV === 'development') { 21 | // makeServer(); 22 | // } 23 | 24 | makeServer(); 25 | 26 | const router = createBrowserRouter([ 27 | { 28 | path: '/', 29 | element: , 30 | children: [ 31 | { 32 | path: '/', 33 | element: , 34 | }, 35 | { 36 | path: '/hotels', 37 | element: , 38 | }, 39 | { 40 | path: '/about-us', 41 | element: , 42 | }, 43 | { 44 | path: '/user-profile', 45 | element: , 46 | }, 47 | { 48 | path: '/login', 49 | element: , 50 | }, 51 | { 52 | path: '/register', 53 | element: , 54 | }, 55 | { 56 | path: '/hotel/:hotelId', 57 | element: , 58 | }, 59 | { 60 | path: '/forgot-password', 61 | element: , 62 | }, 63 | { 64 | path: '/checkout', 65 | element: , 66 | }, 67 | { 68 | path: '/booking-confirmation', 69 | element: , 70 | }, 71 | ], 72 | }, 73 | ]); 74 | 75 | ReactDOM.createRoot(document.getElementById('root')).render( 76 | 77 | 78 | 79 | ); 80 | 81 | // If you want to start measuring performance in your app, pass a function 82 | // to log results (for example: reportWebVitals(console.log)) 83 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 84 | reportWebVitals(); 85 | -------------------------------------------------------------------------------- /src/components/ux/data-range-picker/DateRangePicker.jsx: -------------------------------------------------------------------------------- 1 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 2 | import React, { useRef } from 'react'; 3 | import { faCalendar } from '@fortawesome/free-solid-svg-icons'; 4 | import { DateRange } from 'react-date-range'; 5 | import { formatDate } from 'utils/date-helpers'; 6 | import useOutsideClickHandler from 'hooks/useOutsideClickHandler'; 7 | 8 | const inputSyleMap = { 9 | SECONDARY: 'stay-booker__input--secondary', 10 | DARK: 'stay-booker__input--dark', 11 | }; 12 | 13 | const DateRangePicker = (props) => { 14 | const { 15 | isDatePickerVisible, 16 | onDatePickerIconClick, 17 | onDateChangeHandler, 18 | dateRange, 19 | setisDatePickerVisible, 20 | inputStyle, 21 | } = props; 22 | 23 | const wrapperRef = useRef(); 24 | useOutsideClickHandler(wrapperRef, () => setisDatePickerVisible(false)); 25 | 26 | // Format dates for display 27 | const formattedStartDate = dateRange[0].startDate 28 | ? formatDate(dateRange[0].startDate) 29 | : 'Check-in'; 30 | const formattedEndDate = dateRange[0].endDate 31 | ? formatDate(dateRange[0].endDate) 32 | : 'Check-out'; 33 | 34 | return ( 35 |
    36 | 47 | 53 | 64 |
    65 | {isDatePickerVisible && ( 66 | 75 | )} 76 |
    77 |
    78 | ); 79 | }; 80 | 81 | export default DateRangePicker; 82 | -------------------------------------------------------------------------------- /src/routes/hotel-details/components/user-reviews/components/UserRatingsSelector.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 3 | import { faStar as fasStar } from '@fortawesome/free-solid-svg-icons'; 4 | import { faStar as farStar } from '@fortawesome/free-regular-svg-icons'; 5 | import { AuthContext } from 'contexts/AuthContext'; 6 | /** 7 | * Renders the user ratings selector component. 8 | * 9 | * @component 10 | * @param {Object} props - The component props. 11 | * @param {number} props.userRating - The user's rating. 12 | * @param {Function} props.handleRating - The function to handle rating changes made by user. 13 | * @param {boolean} props.isEmpty - The flag to determine if the user review is empty. 14 | * @param {string} props.userReview - The user's review. 15 | * @param {Function} props.handleReviewSubmit - The function to handle user review submission. 16 | * @param {Function} props.handleUserReviewChange - The function to handle user review changes. 17 | * @returns {JSX.Element} The rendered component. 18 | */ 19 | const UserRatingsSelector = ({ 20 | userRating, 21 | handleRating, 22 | isEmpty, 23 | userReview, 24 | handleReviewSubmit, 25 | handleUserReviewChange, 26 | }) => { 27 | const { isAuthenticated } = React.useContext(AuthContext); 28 | 29 | return isAuthenticated ? ( 30 |
    35 |
    Your Rating
    36 |
    37 | {[1, 2, 3, 4, 5].map((star) => ( 38 | handleRating(star)} 45 | /> 46 | ))} 47 |
    48 |