├── public ├── robots.txt ├── favicon.ico ├── logo192.png ├── logo512.png ├── logo_transparent.png ├── manifest.json └── index.html ├── .babelrc ├── tailwind.config.js ├── src ├── App.css ├── redux │ ├── store.js │ └── reducers │ │ ├── user.js │ │ ├── room.js │ │ └── reservation.js ├── tests │ ├── SideBar.test.js │ ├── MyReservations.test.js │ └── RoomDetail.test.js ├── index.js ├── utils │ └── protected.jsx ├── index.css ├── components │ ├── Rooms.js │ ├── MyRooms.js │ ├── MyReservations.js │ ├── RoomDetail.js │ ├── AddRoomForm.js │ ├── SideBar.js │ ├── Carousel.js │ └── AddReservationForm.js ├── styles │ ├── rooms.module.css │ ├── add_room_form.module.css │ ├── sidebar.module.css │ ├── my_rooms.module.css │ └── roomDetail.module.css ├── App.js └── pages │ ├── Login.js │ └── SignUp.js ├── .gitignore ├── .stylelintrc.json ├── LICENSE ├── .eslintrc.json ├── .github └── workflows │ └── linters.yml ├── package.json └── README.md /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zdnahom/book-a-room-frontend/dev/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zdnahom/book-a-room-frontend/dev/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zdnahom/book-a-room-frontend/dev/public/logo512.png -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-react"], 3 | "plugins": ["@babel/plugin-syntax-jsx"] 4 | } 5 | -------------------------------------------------------------------------------- /public/logo_transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zdnahom/book-a-room-frontend/dev/public/logo_transparent.png -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | './src/**/*.{js,jsx,ts,tsx}', 5 | ], 6 | theme: { 7 | extend: {}, 8 | }, 9 | plugins: [], 10 | }; 11 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | font-family: "Lato", sans-serif; 5 | } 6 | 7 | .App { 8 | width: 100%; 9 | height: 100vh; 10 | display: flex; 11 | flex-direction: column; 12 | } 13 | 14 | @media screen and (min-width: 768px) { 15 | .App { 16 | flex-direction: row; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/redux/store.js: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit'; 2 | import user from './reducers/user'; 3 | import reservation from './reducers/reservation'; 4 | import room from './reducers/room'; 5 | 6 | const store = configureStore({ 7 | reducer: { 8 | user, 9 | reservation, 10 | room, 11 | }, 12 | }); 13 | 14 | export default store; 15 | -------------------------------------------------------------------------------- /.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.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | node_modules 25 | -------------------------------------------------------------------------------- /src/tests/SideBar.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import { MemoryRouter } from 'react-router-dom'; 3 | import SideBar from '../components/SideBar'; 4 | 5 | test('renders navigation links', () => { 6 | render( 7 | 8 | 9 | , 10 | ); 11 | 12 | const roomsLinks = screen.queryAllByText(/Rooms/i); 13 | 14 | // Assert that there is at least one link with "Rooms" text 15 | expect(roomsLinks.length).toBeGreaterThan(0); 16 | }); 17 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import 'mdb-react-ui-kit/dist/css/mdb.min.css'; 3 | import '@fortawesome/fontawesome-free/css/all.min.css'; 4 | import ReactDOM from 'react-dom'; 5 | import './index.css'; 6 | import { Provider } from 'react-redux'; 7 | import App from './App'; 8 | import store from './redux/store'; 9 | 10 | const root = ReactDOM.createRoot(document.getElementById('root')); 11 | root.render( 12 | 13 | 14 | 15 | 16 | , 17 | ); 18 | -------------------------------------------------------------------------------- /src/utils/protected.jsx: -------------------------------------------------------------------------------- 1 | import { useSelector } from 'react-redux'; 2 | import { Navigate } from 'react-router-dom'; 3 | // if the user is in the redux store, then they can access the component 4 | // if the user is not in the redux store, then they are redirected to the login page 5 | 6 | const Protected = ({ children }) => { 7 | const user = useSelector((state) => state.user); 8 | if (user.user) { 9 | return children; 10 | } 11 | return ; // can use useNavigate() hook as well 12 | }; 13 | 14 | export default Protected; 15 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @import url("https://fonts.googleapis.com/css2?family=Lumanosimo&display=swap"); 6 | 7 | body { 8 | margin: 0; 9 | font-family: 10 | -apple-system, 11 | BlinkMacSystemFont, 12 | "Segoe UI", 13 | "Roboto", 14 | "Oxygen", 15 | "Ubuntu", 16 | "Cantarell", 17 | "Fira Sans", 18 | "Droid Sans", 19 | "Helvetica Neue", 20 | sans-serif; 21 | -webkit-font-smoothing: antialiased; 22 | -moz-osx-font-smoothing: grayscale; 23 | } 24 | 25 | code { 26 | font-family: 27 | source-code-pro, 28 | Menlo, 29 | Monaco, 30 | Consolas, 31 | "Courier New", 32 | monospace; 33 | } 34 | -------------------------------------------------------------------------------- /src/components/Rooms.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | import { fetchRooms } from '../redux/reducers/room'; 4 | import styles from '../styles/rooms.module.css'; 5 | import Carousel from './Carousel'; 6 | 7 | const Rooms = () => { 8 | const { rooms, loading } = useSelector((store) => store.room); 9 | const dispatch = useDispatch(); 10 | useEffect(() => { 11 | dispatch(fetchRooms()); 12 | }, [dispatch]); 13 | 14 | return ( 15 |
16 |

All AVAILABLE ROOMS

17 | Please select your favorite room 18 | {loading ?

loading...

: } 19 |
20 | ); 21 | }; 22 | 23 | export default Rooms; 24 | -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["stylelint-config-standard"], 3 | "plugins": ["stylelint-scss", "stylelint-csstree-validator"], 4 | "rules": { 5 | "at-rule-no-unknown": [ 6 | true, 7 | { 8 | "ignoreAtRules": [ 9 | "tailwind", 10 | "apply", 11 | "variants", 12 | "responsive", 13 | "screen" 14 | ] 15 | } 16 | ], 17 | "scss/at-rule-no-unknown": [ 18 | true, 19 | { 20 | "ignoreAtRules": [ 21 | "tailwind", 22 | "apply", 23 | "variants", 24 | "responsive", 25 | "screen" 26 | ] 27 | } 28 | ], 29 | "csstree/validator": true 30 | }, 31 | "ignoreFiles": [ 32 | "build/**", 33 | "dist/**", 34 | "**/reset*.css", 35 | "**/bootstrap*.css", 36 | "**/*.js", 37 | "**/*.jsx" 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /src/tests/MyReservations.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import MyReservations from '../components/MyReservations'; 4 | 5 | // Make sure to import the following line for `toBeInTheDocument` matcher 6 | import '@testing-library/jest-dom/extend-expect'; 7 | 8 | describe('MyReservations component', () => { 9 | it('should render reservation details correctly', () => { 10 | render(); 11 | 12 | // Assert that the table headers are rendered correctly 13 | expect(screen.getByText('Room')).toBeInTheDocument(); 14 | expect(screen.getByText('Date Start')).toBeInTheDocument(); 15 | expect(screen.getByText('Date End')).toBeInTheDocument(); 16 | expect(screen.getByText('Cost')).toBeInTheDocument(); 17 | 18 | // ... (rest of the test) 19 | }); 20 | 21 | // ... (rest of the tests) 22 | }); 23 | -------------------------------------------------------------------------------- /src/styles/rooms.module.css: -------------------------------------------------------------------------------- 1 | 2 | .rooms-container { 3 | display: flex; 4 | flex-direction: column; 5 | align-items: center; 6 | gap: 10px; 7 | padding-top: 8%; 8 | padding-left: 4%; 9 | padding-right: 4%; 10 | } 11 | 12 | .rooms-header { 13 | font-size: 2rem; 14 | text-align: center; 15 | } 16 | 17 | .select-room-text { 18 | color: gray; 19 | } 20 | 21 | .rooms { 22 | width: 100%; 23 | display: flex; 24 | } 25 | 26 | .my-carousel { 27 | width: 94%; 28 | padding: 30px; 29 | } 30 | 31 | .custom-link { 32 | text-decoration: none; 33 | } 34 | 35 | .room { 36 | background-color: white; 37 | padding: 2%; 38 | color: black; 39 | } 40 | 41 | .room-img { 42 | width: 100%; 43 | height: auto; 44 | margin-bottom: 10px; 45 | } 46 | 47 | .room-description { 48 | margin-bottom: 10px; 49 | text-align: center; 50 | } 51 | 52 | .room-price { 53 | text-align: center; 54 | } 55 | 56 | .room-price span { 57 | color: gray; 58 | } 59 | 60 | @media screen and (min-width: 768px) { 61 | .rooms-container { 62 | width: 75%; 63 | padding-left: 0; 64 | padding-right: 0; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Nahom_zd 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/App.js: -------------------------------------------------------------------------------- 1 | import { BrowserRouter, Routes, Route } from 'react-router-dom'; 2 | import './App.css'; 3 | import SideBar from './components/SideBar'; 4 | import Rooms from './components/Rooms'; 5 | import AddRoomForm from './components/AddRoomForm'; 6 | import AddReservationForm from './components/AddReservationForm'; 7 | import MyReservations from './components/MyReservations'; 8 | import MyRooms from './components/MyRooms'; 9 | import RoomDetail from './components/RoomDetail'; 10 | import SignUp from './pages/SignUp'; 11 | import Login from './pages/Login'; 12 | import Protected from './utils/protected'; 13 | 14 | const App = () => ( 15 |
16 | 17 | 18 | 19 | } /> 20 | } /> 21 | } /> 22 | } /> 23 | } /> 24 | } /> 25 | } /> 26 | } /> 27 | 28 | 29 |
30 | ); 31 | 32 | export default App; 33 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "jest": true 6 | }, 7 | "parser": "@babel/eslint-parser", 8 | "parserOptions": { 9 | "ecmaFeatures": { 10 | "jsx": true 11 | }, 12 | "ecmaVersion": 2018, 13 | "sourceType": "module" 14 | }, 15 | "extends": ["airbnb", "plugin:react/recommended", "plugin:react-hooks/recommended"], 16 | "plugins": ["react", "import", "jsx-a11y", "react-hooks"], 17 | "rules": { 18 | "no-param-reassign": 0, 19 | "linebreak-style": 0, 20 | "react/prop-types": 0, 21 | "import/no-extraneous-dependencies": [ 22 | "error", 23 | { 24 | "devDependencies": ["**/*.test.js", "**/*.spec.js", "src/setupTests.js"] 25 | } 26 | ], 27 | "jsx-a11y/label-has-associated-control": [ 28 | "error", 29 | { 30 | "required": { 31 | "some": ["nesting", "id"] 32 | } 33 | } 34 | ], 35 | "react/no-unescaped-entities": "off", 36 | "react/jsx-filename-extension": [ 37 | "warn", 38 | { 39 | "extensions": [".js", ".jsx"] 40 | } 41 | ], 42 | "react/react-in-jsx-scope": "off", 43 | "import/no-unresolved": "off", 44 | "no-shadow": "off" 45 | }, 46 | "overrides": [ 47 | { 48 | "files": ["src/**/*Slice.js"], 49 | "rules": { 50 | "no-param-reassign": [ 51 | "error", 52 | { 53 | "props": false 54 | } 55 | ] 56 | } 57 | } 58 | ], 59 | "ignorePatterns": ["dist/", "build/"] 60 | } 61 | -------------------------------------------------------------------------------- /src/styles/add_room_form.module.css: -------------------------------------------------------------------------------- 1 | .form-container { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | height: 100vh; 6 | padding-top: 50px; 7 | gap: 20px; 8 | } 9 | 10 | .header-text { 11 | font-weight: 700; 12 | } 13 | 14 | .form { 15 | width: 300px; 16 | display: flex; 17 | flex-direction: column; 18 | gap: 10px; 19 | } 20 | 21 | .field { 22 | display: flex; 23 | flex-direction: column; 24 | justify-content: space-between; 25 | } 26 | 27 | .field label { 28 | font-size: 1.5rem; 29 | } 30 | 31 | .field input { 32 | padding: 5px; 33 | } 34 | 35 | .field select { 36 | align-self: start; 37 | } 38 | 39 | .button-container { 40 | display: flex; 41 | align-self: center; 42 | align-items: center; 43 | height: 50px; 44 | text-decoration: none; 45 | border: none; 46 | background-color: inherit; 47 | } 48 | 49 | .button { 50 | background-color: #97bf0f; 51 | display: flex; 52 | align-items: center; 53 | height: 50px; 54 | color: white; 55 | border: none; 56 | } 57 | 58 | .left-round, 59 | .right-round { 60 | background-color: #97bf0f; 61 | width: 50px; 62 | height: 50px; 63 | } 64 | 65 | .left-round { 66 | border-top-left-radius: 50%; 67 | border-bottom-left-radius: 50%; 68 | } 69 | 70 | .right-round { 71 | border-top-right-radius: 50%; 72 | border-bottom-right-radius: 50%; 73 | display: flex; 74 | justify-content: center; 75 | align-items: center; 76 | color: white; 77 | } 78 | 79 | @media screen and (min-width: 768px) { 80 | .form-container { 81 | width: 75%; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/tests/RoomDetail.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import { useSelector } from 'react-redux'; 4 | import { MemoryRouter, Route, Routes } from 'react-router-dom'; // Import Routes 5 | import RoomDetail from '../components/RoomDetail'; 6 | 7 | // Mock the useSelector hook to provide the necessary store state for the test 8 | jest.mock('react-redux', () => ({ 9 | useSelector: jest.fn(), 10 | })); 11 | 12 | // Define the mock room data 13 | const mockRooms = [ 14 | { 15 | id: 1, 16 | image: 'test-image.jpg', 17 | description: 'Test Room Description', 18 | night_cost: 100, 19 | }, 20 | ]; 21 | 22 | describe('RoomDetail component', () => { 23 | beforeEach(() => { 24 | // Mock the useSelector hook to return the necessary state 25 | useSelector.mockImplementation((selectorFn) => selectorFn({ 26 | room: { 27 | rooms: mockRooms, 28 | loading: false, 29 | }, 30 | })); 31 | }); 32 | 33 | afterEach(() => { 34 | jest.clearAllMocks(); 35 | }); 36 | 37 | it('should render the loading text when loading is true', () => { 38 | // Mock the useSelector hook to return the loading state 39 | useSelector.mockImplementation((selectorFn) => selectorFn({ 40 | room: { 41 | rooms: mockRooms, 42 | loading: true, 43 | }, 44 | })); 45 | 46 | // Render the component inside the MemoryRouter with the appropriate route 47 | render( 48 | 49 | 50 | } /> 51 | 52 | , 53 | ); 54 | 55 | // Rest of the test remains the same... 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/components/MyRooms.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { useDispatch, useSelector } from 'react-redux'; 4 | import { deleteRoom, fetchRooms } from '../redux/reducers/room'; 5 | import styles from '../styles/my_rooms.module.css'; 6 | 7 | const MyRooms = () => { 8 | const { rooms, loading } = useSelector((store) => store.room); 9 | const { user } = useSelector((store) => store.user); 10 | const myRooms = rooms.filter((room) => room.user_id === user.user.id); 11 | 12 | const dispatch = useDispatch(); 13 | 14 | const handleDelete = (id) => { 15 | dispatch(deleteRoom(id)); 16 | }; 17 | 18 | useEffect(() => { 19 | dispatch(fetchRooms()); 20 | }, [dispatch]); 21 | 22 | return ( 23 |
24 |

MY ROOMS

25 | {loading ? ( 26 |

loading...

27 | ) : ( 28 |
    29 | {myRooms.map((room) => ( 30 |
  • 31 | 32 | room pic 33 |

    {room.description}

    34 |

    35 | $ 36 | {room.night_cost} 37 | /night 38 |

    39 | 40 | 47 |
  • 48 | ))} 49 |
50 | )} 51 |
52 | ); 53 | }; 54 | 55 | export default MyRooms; 56 | -------------------------------------------------------------------------------- /src/styles/sidebar.module.css: -------------------------------------------------------------------------------- 1 | .sidebar { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: space-between; 5 | position: fixed; 6 | width: 100%; 7 | height: 100vh; 8 | z-index: 1; 9 | background-color: white; 10 | } 11 | 12 | .no-sidebar { 13 | height: 10vh; 14 | } 15 | 16 | .hidden { 17 | display: none; 18 | } 19 | 20 | .navbar { 21 | display: flex; 22 | flex-direction: column; 23 | gap: 100px; 24 | } 25 | 26 | .logo-container { 27 | display: flex; 28 | justify-content: space-between; 29 | align-items: center; 30 | padding: 2%; 31 | } 32 | 33 | .logo { 34 | color: #565656; 35 | font-size: 2rem; 36 | font-weight: 900; 37 | width: 100%; 38 | text-align: center; 39 | } 40 | 41 | .button { 42 | background-color: inherit; 43 | border: none; 44 | } 45 | 46 | .navbar-ul { 47 | display: flex; 48 | flex-direction: column; 49 | list-style: none; 50 | } 51 | 52 | .navbar-ul li { 53 | display: flex; 54 | padding-left: 10px; 55 | } 56 | 57 | .nav-link, 58 | .active-link { 59 | width: 100%; 60 | font-size: 1.5rem; 61 | text-decoration: none; 62 | padding: 10px; 63 | color: #565656; 64 | font-weight: 700; 65 | } 66 | 67 | .active-link { 68 | background-color: #97bf0f; 69 | color: white; 70 | } 71 | 72 | .footer { 73 | padding: 5%; 74 | display: flex; 75 | flex-direction: column; 76 | } 77 | 78 | .icons-ul { 79 | list-style: none; 80 | display: flex; 81 | justify-content: center; 82 | gap: 8px; 83 | } 84 | 85 | .icon, 86 | .footer-span { 87 | color: #5d5d5d; 88 | } 89 | 90 | .footer-span { 91 | text-align: center; 92 | font-size: 0.6rem; 93 | } 94 | 95 | @media screen and (min-width: 768px) { 96 | .sidebar { 97 | position: static; 98 | width: 25%; 99 | height: 100vh; 100 | box-shadow: 0 0 3px rgba(0, 0, 0, 0.2); 101 | } 102 | 103 | .navbar { 104 | padding-top: 30px; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/styles/my_rooms.module.css: -------------------------------------------------------------------------------- 1 | 2 | .rooms-container { 3 | display: flex; 4 | flex-direction: column; 5 | align-items: center; 6 | gap: 10px; 7 | padding-top: 8%; 8 | padding-left: 4%; 9 | padding-right: 4%; 10 | } 11 | 12 | .rooms-header { 13 | font-size: 2rem; 14 | text-align: center; 15 | } 16 | 17 | .rooms { 18 | width: 100%; 19 | display: grid; 20 | grid-template-columns: repeat(1, 1fr); 21 | } 22 | 23 | .custom-link { 24 | text-decoration: none; 25 | } 26 | 27 | .room { 28 | background-color: white; 29 | display: flex; 30 | flex-direction: column; 31 | padding: 2%; 32 | color: black; 33 | } 34 | 35 | .room-img { 36 | width: 100%; 37 | height: auto; 38 | margin-bottom: 10px; 39 | } 40 | 41 | .room-description { 42 | margin-bottom: 10px; 43 | text-align: center; 44 | } 45 | 46 | .room-price { 47 | text-align: center; 48 | } 49 | 50 | .room-price span { 51 | color: gray; 52 | } 53 | 54 | .button-container { 55 | display: flex; 56 | align-self: center; 57 | align-items: center; 58 | height: 50px; 59 | text-decoration: none; 60 | border: none; 61 | background-color: inherit; 62 | } 63 | 64 | .button { 65 | background-color: red; 66 | display: flex; 67 | align-items: center; 68 | height: 50px; 69 | color: white; 70 | border: none; 71 | } 72 | 73 | .left-round, 74 | .right-round { 75 | background-color: red; 76 | width: 50px; 77 | height: 50px; 78 | } 79 | 80 | .left-round { 81 | border-top-left-radius: 50%; 82 | border-bottom-left-radius: 50%; 83 | } 84 | 85 | .right-round { 86 | border-top-right-radius: 50%; 87 | border-bottom-right-radius: 50%; 88 | display: flex; 89 | justify-content: center; 90 | align-items: center; 91 | color: white; 92 | } 93 | 94 | @media screen and (min-width: 768px) { 95 | .rooms-container { 96 | width: 75%; 97 | padding-left: 0; 98 | padding-right: 0; 99 | overflow: auto; 100 | } 101 | 102 | .rooms { 103 | grid-template-columns: repeat(3, 1fr); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /.github/workflows/linters.yml: -------------------------------------------------------------------------------- 1 | name: Linters 2 | 3 | on: pull_request 4 | 5 | env: 6 | FORCE_COLOR: 1 7 | 8 | jobs: 9 | eslint: 10 | name: ESLint 11 | runs-on: ubuntu-22.04 12 | steps: 13 | - uses: actions/checkout@v3 14 | - uses: actions/setup-node@v3 15 | with: 16 | node-version: "18.x" 17 | - name: Setup ESLint 18 | run: | 19 | npm install --save-dev eslint@7.x eslint-config-airbnb@18.x eslint-plugin-import@2.x eslint-plugin-jsx-a11y@6.x eslint-plugin-react@7.x eslint-plugin-react-hooks@4.x @babel/eslint-parser@7.x @babel/core@7.x @babel/plugin-syntax-jsx@7.x @babel/preset-env@7.x @babel/preset-react@7.x 20 | [ -f .eslintrc.json ] || wget https://raw.githubusercontent.com/microverseinc/linters-config/master/react-redux/.eslintrc.json 21 | [ -f .babelrc ] || wget https://raw.githubusercontent.com/microverseinc/linters-config/master/react-redux/.babelrc 22 | - name: ESLint Report 23 | run: npx eslint "**/*.{js,jsx}" 24 | stylelint: 25 | name: Stylelint 26 | runs-on: ubuntu-22.04 27 | steps: 28 | - uses: actions/checkout@v3 29 | - uses: actions/setup-node@v3 30 | with: 31 | node-version: "18.x" 32 | - name: Setup Stylelint 33 | run: | 34 | npm install --save-dev stylelint@13.x stylelint-scss@3.x stylelint-config-standard@21.x stylelint-csstree-validator@1.x 35 | [ -f .stylelintrc.json ] || wget https://raw.githubusercontent.com/microverseinc/linters-config/master/react-redux/.stylelintrc.json 36 | - name: Stylelint Report 37 | run: npx stylelint "**/*.{css,scss}" 38 | nodechecker: 39 | name: node_modules checker 40 | runs-on: ubuntu-22.04 41 | steps: 42 | - uses: actions/checkout@v3 43 | - name: Check node_modules existence 44 | run: | 45 | if [ -d "node_modules/" ]; then echo -e "\e[1;31mThe node_modules/ folder was pushed to the repo. Please remove it from the GitHub repository and try again."; echo -e "\e[1;32mYou can set up a .gitignore file with this folder included on it to prevent this from happening in the future." && exit 1; fi 46 | -------------------------------------------------------------------------------- /src/redux/reducers/user.js: -------------------------------------------------------------------------------- 1 | import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; 2 | 3 | const URL = 'https://book-a-room.onrender.com/users'; 4 | 5 | export const signIn = createAsyncThunk('user/signIn', async (payload, thunkAPI) => { 6 | const response = await fetch(`${URL}/sign_in`, { 7 | method: 'POST', 8 | headers: { 9 | 'Content-Type': 'application/json', 10 | }, 11 | body: JSON.stringify({ user: payload }), 12 | }); 13 | const data = await response.json(); 14 | if (response.ok) { 15 | return data; 16 | } 17 | return thunkAPI.rejectWithValue(data); 18 | }); 19 | 20 | // signIn is dispachec this way => dispatch(signIn({email, password})) 21 | 22 | export const signUp = createAsyncThunk('user/signUp', async (payload, thunkAPI) => { 23 | const response = await fetch(URL, { 24 | method: 'POST', 25 | headers: { 26 | 'Content-Type': 'application/json', 27 | }, 28 | body: JSON.stringify({ user: payload }), 29 | }); 30 | const data = await response.json(); 31 | if (response.ok) { 32 | return data; 33 | } 34 | return thunkAPI.rejectWithValue(data); 35 | }); 36 | 37 | // signUp is dispachec this way => dispatch(signUp({email, password, password_confirmation})) 38 | 39 | const userSlice = createSlice({ 40 | name: 'user', 41 | initialState: { 42 | user: null, 43 | error: null, 44 | loading: false, 45 | }, 46 | reducers: { 47 | signOut: (state) => { 48 | state.user = null; 49 | }, 50 | }, 51 | extraReducers: { 52 | [signIn.pending]: (state) => { 53 | state.loading = true; 54 | }, 55 | [signIn.fulfilled]: (state, action) => { 56 | state.user = action.payload; 57 | state.loading = false; 58 | }, 59 | [signIn.rejected]: (state, action) => { 60 | state.error = action.payload; 61 | state.loading = false; 62 | }, 63 | [signUp.pending]: (state) => { 64 | state.loading = true; 65 | }, 66 | [signUp.fulfilled]: (state, action) => { 67 | state.user = action.payload; 68 | state.loading = false; 69 | }, 70 | [signUp.rejected]: (state, action) => { 71 | state.error = action.payload; 72 | state.loading = false; 73 | }, 74 | }, 75 | }); 76 | 77 | export const { signOut } = userSlice.actions; 78 | export default userSlice.reducer; 79 | -------------------------------------------------------------------------------- /src/styles/roomDetail.module.css: -------------------------------------------------------------------------------- 1 | 2 | .detail-container { 3 | width: 100%; 4 | display: flex; 5 | flex-direction: column; 6 | } 7 | 8 | .left-detail { 9 | display: flex; 10 | flex-direction: column; 11 | } 12 | 13 | .image-container { 14 | display: flex; 15 | padding: 5%; 16 | } 17 | 18 | .room-image { 19 | width: 100%; 20 | height: auto; 21 | } 22 | 23 | .back-button { 24 | background-color: #97bf0f; 25 | display: flex; 26 | width: 40px; 27 | justify-content: end; 28 | align-items: center; 29 | padding: 15px; 30 | border-top-right-radius: 40%; 31 | border-bottom-right-radius: 40%; 32 | border: none; 33 | } 34 | 35 | .back { 36 | rotate: -90deg; 37 | color: white; 38 | } 39 | 40 | .right-detail { 41 | padding: 2%; 42 | display: flex; 43 | flex-direction: column; 44 | gap: 10px; 45 | } 46 | 47 | .right-detail p:nth-child(even) { 48 | background-color: #e2e3e5; 49 | } 50 | 51 | .room-description { 52 | padding: 4%; 53 | text-align: end; 54 | font-weight: 700; 55 | font-size: 1.4rem; 56 | } 57 | 58 | .room-type, 59 | .price-container, 60 | .room-status { 61 | display: flex; 62 | justify-content: space-between; 63 | padding: 4%; 64 | } 65 | 66 | .price-container span { 67 | color: gray; 68 | } 69 | 70 | .reserve-button-container { 71 | display: flex; 72 | align-self: center; 73 | align-items: center; 74 | height: 50px; 75 | text-decoration: none; 76 | margin-top: 50px; 77 | } 78 | 79 | .reserve-button { 80 | background-color: #97bf0f; 81 | display: flex; 82 | align-items: center; 83 | height: 50px; 84 | color: white; 85 | } 86 | 87 | .left-round, 88 | .right-round { 89 | background-color: #97bf0f; 90 | width: 50px; 91 | height: 50px; 92 | } 93 | 94 | .left-round { 95 | border-top-left-radius: 50%; 96 | border-bottom-left-radius: 50%; 97 | } 98 | 99 | .right-round { 100 | border-top-right-radius: 50%; 101 | border-bottom-right-radius: 50%; 102 | display: flex; 103 | justify-content: center; 104 | align-items: center; 105 | color: white; 106 | } 107 | 108 | @media screen and (min-width: 768px) { 109 | .detail-container { 110 | width: 75%; 111 | flex-direction: row; 112 | } 113 | 114 | .left-detail { 115 | width: 60%; 116 | height: 100vh; 117 | gap: 50px; 118 | } 119 | 120 | .right-detail { 121 | width: 40%; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "book-a-room-frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@emotion/react": "^11.11.1", 7 | "@emotion/styled": "^11.11.0", 8 | "@fortawesome/fontawesome-free": "^6.4.0", 9 | "@mui/icons-material": "^5.14.0", 10 | "@mui/material": "^5.14.0", 11 | "@reduxjs/toolkit": "^1.9.5", 12 | "@testing-library/user-event": "^13.5.0", 13 | "localforage": "^1.10.0", 14 | "match-sorter": "^6.3.1", 15 | "mdb-react-ui-kit": "^6.1.0", 16 | "prop-types": "^15.8.1", 17 | "react": "^18.2.0", 18 | "react-dom": "^18.2.0", 19 | "react-redux": "^8.1.1", 20 | "react-router-dom": "^6.14.1", 21 | "react-scripts": "5.0.1", 22 | "react-slick": "^0.29.0", 23 | "redux-thunk": "^2.4.2", 24 | "slick-carousel": "^1.8.1", 25 | "sort-by": "^1.2.0", 26 | "web-vitals": "^2.1.4" 27 | }, 28 | "scripts": { 29 | "start": "react-scripts start", 30 | "build": "react-scripts build", 31 | "test": "react-scripts test", 32 | "lint": "eslint src", 33 | "eject": "react-scripts eject" 34 | }, 35 | "eslintConfig": { 36 | "extends": [ 37 | "react-app", 38 | "react-app/jest" 39 | ] 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 | "devDependencies": { 54 | "@babel/core": "^7.22.9", 55 | "@babel/eslint-parser": "^7.22.9", 56 | "@babel/plugin-proposal-private-property-in-object": "^7.21.11", 57 | "@babel/plugin-syntax-jsx": "^7.22.5", 58 | "@babel/preset-react": "^7.22.5", 59 | "@testing-library/jest-dom": "^5.17.0", 60 | "@testing-library/react": "^14.0.0", 61 | "eslint": "^7.32.0", 62 | "eslint-config-airbnb": "^18.2.1", 63 | "eslint-plugin-import": "^2.27.5", 64 | "eslint-plugin-jsx-a11y": "^6.7.1", 65 | "eslint-plugin-react": "^7.32.2", 66 | "eslint-plugin-react-hooks": "^4.6.0", 67 | "jest": "^27.5.1", 68 | "match-media-mock": "^0.1.1", 69 | "matchmedia-polyfill": "^0.3.2", 70 | "prettier": "^3.0.0", 71 | "react-dom": "^18.2.0", 72 | "redux-mock-store": "^1.5.4", 73 | "stylelint": "^13.13.1", 74 | "stylelint-config-standard": "^21.0.0", 75 | "stylelint-csstree-validator": "^1.9.0", 76 | "stylelint-scss": "^3.21.0", 77 | "tailwindcss": "^3.3.3" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/components/MyReservations.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | import { fetchReservations, deleteReservation } from '../redux/reducers/reservation'; 4 | 5 | const MyReservations = () => { 6 | const { reservations, loading } = useSelector((store) => store.reservation); 7 | 8 | const { user } = useSelector((store) => store.user); 9 | 10 | const dispatch = useDispatch(); 11 | useEffect(() => { 12 | dispatch(fetchReservations(user.user.id)); 13 | }, [dispatch]); 14 | 15 | const handleDelete = (id) => { 16 | dispatch(deleteReservation(id)); 17 | }; 18 | 19 | if (loading) { 20 | return

loading...

; 21 | } 22 | 23 | return ( 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | {reservations.map((reservation) => ( 37 | 38 | 41 | 42 | 43 | 44 | 53 | 54 | ))} 55 | 56 |
RoomDate StartDate EndCostUnbook
39 | {reservation.room_id} 40 | {reservation.start}{reservation.end}{reservation.cost} 45 | 52 |
57 |
58 | ); 59 | }; 60 | 61 | export default MyReservations; 62 | -------------------------------------------------------------------------------- /src/components/RoomDetail.js: -------------------------------------------------------------------------------- 1 | import { useDispatch, useSelector } from 'react-redux'; 2 | import { Link, useNavigate, useParams } from 'react-router-dom'; 3 | import { useEffect } from 'react'; 4 | import ChangeHistorySharpIcon from '@mui/icons-material/ChangeHistorySharp'; 5 | import ChevronRightRoundedIcon from '@mui/icons-material/ChevronRightRounded'; 6 | import styles from '../styles/roomDetail.module.css'; 7 | import { fetchRooms } from '../redux/reducers/room'; 8 | 9 | const RoomDetail = () => { 10 | const { rooms, loading } = useSelector((store) => store.room); 11 | const dispatch = useDispatch(); 12 | const { roomId } = useParams(); 13 | const room = rooms.find((room) => room.id === parseInt(roomId, 10)); 14 | 15 | const navigate = useNavigate(); 16 | useEffect(() => { 17 | dispatch(fetchRooms()); 18 | }, [dispatch, roomId]); 19 | 20 | return ( 21 | <> 22 | {loading ? ( 23 |

loading...

24 | ) : ( 25 |
26 |
27 |
28 | room pic 29 |
30 | 38 |
39 |
40 |

{room?.description}

41 |

42 | ROOM TYPE 43 | Twin 44 |

45 |

46 | PRICE 47 |

48 | $ 49 | {room?.night_cost} 50 | /night 51 |

52 |

53 |

54 | STATUS 55 | Open 56 |

57 | 58 | 59 | 60 |
BOOK NOW
61 | 62 | 63 | 64 | 65 |
66 |
67 | )} 68 | 69 | ); 70 | }; 71 | 72 | export default RoomDetail; 73 | -------------------------------------------------------------------------------- /src/redux/reducers/room.js: -------------------------------------------------------------------------------- 1 | import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; 2 | 3 | const URL = 'https://book-a-room.onrender.com/api/v1/rooms'; 4 | 5 | export const fetchRooms = createAsyncThunk('room/fetchRooms', async (payload, thunkAPI) => { 6 | const response = await fetch(URL); 7 | const data = await response.json(); 8 | if (response.ok) { 9 | return data; 10 | } 11 | return thunkAPI.rejectWithValue(data); 12 | }); 13 | 14 | // featchRooms is dispachec this way => dispatch(featchRooms()) 15 | 16 | export const createRoom = createAsyncThunk('room/createRoom', async (payload, thunkAPI) => { 17 | const response = await fetch(URL, { 18 | method: 'POST', 19 | headers: { 20 | 'Content-Type': 'application/json', 21 | }, 22 | body: JSON.stringify(payload), 23 | }); 24 | const data = await response.json(); 25 | if (response.ok) { 26 | return data; 27 | } 28 | return thunkAPI.rejectWithValue(data); 29 | }); 30 | 31 | // createRoom is dispached this way => 32 | // dispatch(createRoom({description, num, room_type, nigth_cost, image, user_id})) 33 | 34 | export const deleteRoom = createAsyncThunk('room/deleteRoom', async (payload, thunkAPI) => { 35 | const response = await fetch(`${URL}/${payload}`, { 36 | method: 'DELETE', 37 | headers: { 38 | 'Content-Type': 'application/json', 39 | }, 40 | }); 41 | const data = await response.json(); 42 | if (response.ok) { 43 | return { id: payload, ...data }; 44 | } 45 | return thunkAPI.rejectWithValue(data); 46 | }); 47 | 48 | // deleteRoom is dispached this way => dispatch(deleteRoom(id)) 49 | 50 | const roomSlice = createSlice({ 51 | name: 'room', 52 | initialState: { 53 | rooms: [], 54 | loading: false, 55 | error: null, 56 | }, 57 | reducers: { 58 | signOut: (state) => ({ ...state, rooms: [] }), 59 | }, 60 | 61 | extraReducers: { 62 | [fetchRooms.pending]: (state) => ({ ...state, loading: true }), 63 | [fetchRooms.fulfilled]: (state, action) => { 64 | const rooms = action.payload; 65 | return { ...state, rooms, loading: false }; 66 | }, 67 | [fetchRooms.rejected]: (state, action) => ({ 68 | ...state, 69 | error: action.payload, 70 | loading: false, 71 | }), 72 | [createRoom.pending]: (state) => ({ ...state, loading: true }), 73 | [createRoom.fulfilled]: (state, action) => ({ 74 | ...state, 75 | rooms: [...state.rooms, action.payload], 76 | loading: false, 77 | }), 78 | [createRoom.rejected]: (state, action) => ({ ...state, error: action.payload, loading: false }), 79 | [deleteRoom.pending]: (state) => ({ ...state, loading: true }), 80 | [deleteRoom.fulfilled]: (state, action) => { 81 | state.rooms = state.rooms.filter((room) => room.id !== action.payload.id); 82 | state.loading = false; 83 | }, 84 | [deleteRoom.rejected]: (state, action) => ({ ...state, error: action.payload, loading: false }), 85 | }, 86 | }); 87 | 88 | export const { signOut } = roomSlice.actions; 89 | 90 | export default roomSlice.reducer; 91 | -------------------------------------------------------------------------------- /src/redux/reducers/reservation.js: -------------------------------------------------------------------------------- 1 | import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; 2 | 3 | const URL = 'https://book-a-room.onrender.com/api/v1/reservations'; 4 | 5 | export const fetchReservations = createAsyncThunk( 6 | 'reservation/fetchReservations', 7 | async (payload, thunkAPI) => { 8 | const response = await fetch(`${URL}?user_id=${payload}`); 9 | const data = await response.json(); 10 | if (response.ok) { 11 | return data; 12 | } 13 | return thunkAPI.rejectWithValue(data); 14 | }, 15 | ); 16 | 17 | // fetchReservations is dispachec this way => dispatch(fetchReservations(user_id)) 18 | 19 | export const createReservation = createAsyncThunk( 20 | 'reservation/createReservation', 21 | async (payload, thunkAPI) => { 22 | const response = await fetch(URL, { 23 | method: 'POST', 24 | headers: { 25 | 'Content-Type': 'application/json', 26 | }, 27 | body: JSON.stringify({ reservation: payload }), 28 | }); 29 | const data = await response.json(); 30 | if (response.ok) { 31 | return data; 32 | } 33 | return thunkAPI.rejectWithValue(data); 34 | }, 35 | ); 36 | 37 | // createReservation is dispachec this way => 38 | // dispatch(createReservation({user_id, room_id, start_date, end_date, nights, cost})) 39 | 40 | export const deleteReservation = createAsyncThunk( 41 | 'reservation/deleteReservation', 42 | async (payload, thunkAPI) => { 43 | const response = await fetch(`${URL}/${payload}`, { 44 | method: 'DELETE', 45 | headers: { 46 | 'Content-Type': 'application/json', 47 | }, 48 | }); 49 | const data = await response.json(); 50 | if (response.ok) { 51 | return { id: payload, ...data }; 52 | } 53 | return thunkAPI.rejectWithValue(data); 54 | }, 55 | ); 56 | 57 | // deleteReservation is dispachec this way => dispatch(deleteReservation({id})) 58 | 59 | const reservationSlice = createSlice({ 60 | name: 'reservation', 61 | initialState: { 62 | reservations: [], 63 | error: null, 64 | loading: false, 65 | }, 66 | reducers: { 67 | signOut: (state) => { 68 | state.reservations = []; 69 | }, 70 | }, 71 | extraReducers: { 72 | [fetchReservations.pending]: (state) => { 73 | state.loading = true; 74 | }, 75 | [fetchReservations.fulfilled]: (state, action) => { 76 | state.reservations = action.payload; 77 | state.loading = false; 78 | }, 79 | [fetchReservations.rejected]: (state, action) => { 80 | state.error = action.payload; 81 | state.loading = false; 82 | }, 83 | [createReservation.pending]: (state) => { 84 | state.loading = true; 85 | }, 86 | [createReservation.fulfilled]: (state, action) => { 87 | state.reservations.push(action.payload); 88 | state.loading = false; 89 | }, 90 | [createReservation.rejected]: (state, action) => { 91 | state.error = action.payload; 92 | state.loading = false; 93 | }, 94 | [deleteReservation.pending]: (state) => { 95 | state.loading = true; 96 | }, 97 | [deleteReservation.fulfilled]: (state, action) => { 98 | state.reservations = state.reservations.filter( 99 | (reservation) => reservation.id !== action.payload.id, 100 | ); 101 | state.loading = false; 102 | }, 103 | [deleteReservation.rejected]: (state, action) => { 104 | state.error = action.payload; 105 | state.loading = false; 106 | }, 107 | }, 108 | }); 109 | 110 | export const { signOut } = reservationSlice.actions; 111 | 112 | export default reservationSlice.reducer; 113 | -------------------------------------------------------------------------------- /src/components/AddRoomForm.js: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import { useDispatch, useSelector } from 'react-redux'; 4 | import { createRoom } from '../redux/reducers/room'; 5 | import styles from '../styles/add_room_form.module.css'; 6 | 7 | const AddRoomForm = () => { 8 | const { user } = useSelector((store) => store.user); 9 | const [roomData, setRoomData] = useState({ 10 | description: '', 11 | num: '', 12 | room_type: '', 13 | night_cost: '', 14 | image: '', 15 | user_id: user.user.id, 16 | }); 17 | const dispatch = useDispatch(); 18 | const navigate = useNavigate(); 19 | 20 | const handleChange = (e) => { 21 | const { name, value } = e.target; 22 | setRoomData((prevState) => ({ 23 | ...prevState, 24 | [name]: value, 25 | })); 26 | }; 27 | 28 | const handleSubmit = (e) => { 29 | e.preventDefault(); 30 | dispatch(createRoom(roomData)); 31 | setRoomData({ 32 | description: '', 33 | num: '', 34 | room_type: '', 35 | night_cost: '', 36 | image: '', 37 | user_id: user.user.id, 38 | }); 39 | navigate('/my-rooms'); 40 | }; 41 | return ( 42 |
43 |

Add A NEW ROOM

44 |
45 |
46 | 47 | 55 |
56 |
57 | 58 | 66 |
67 |
68 | 69 | 77 |
78 |
79 | 80 | 97 |
98 |
99 | 100 | 110 |
111 | 117 |
118 |
119 | ); 120 | }; 121 | 122 | export default AddRoomForm; 123 | -------------------------------------------------------------------------------- /src/components/SideBar.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { NavLink } from 'react-router-dom'; 3 | import GitHubIcon from '@mui/icons-material/GitHub'; 4 | import TwitterIcon from '@mui/icons-material/Twitter'; 5 | import EmailIcon from '@mui/icons-material/Email'; 6 | import DragHandleIcon from '@mui/icons-material/DragHandle'; 7 | import CloseIcon from '@mui/icons-material/Close'; 8 | import styles from '../styles/sidebar.module.css'; 9 | 10 | const SideBar = () => { 11 | const [menuOpened, setMenuOpened] = useState(false); 12 | const [isMobile, setIsMobile] = useState(false); 13 | 14 | useEffect(() => { 15 | const handleResize = () => { 16 | setIsMobile(window.innerWidth <= 768); 17 | }; 18 | 19 | window.addEventListener('resize', handleResize); 20 | 21 | handleResize(); 22 | return () => { 23 | window.removeEventListener('resize', handleResize); 24 | }; 25 | }, []); 26 | 27 | const openMenu = () => { 28 | setMenuOpened(true); 29 | }; 30 | const closeMenu = () => { 31 | setMenuOpened(false); 32 | }; 33 | 34 | return ( 35 |
36 | 108 |
109 |
    110 |
  • 111 | 112 |
  • 113 |
  • 114 | 115 |
  • 116 |
  • 117 | 118 |
  • 119 |
120 | ©2023 FINAL CAPSTONE 121 |
122 |
123 | ); 124 | }; 125 | 126 | export default SideBar; 127 | -------------------------------------------------------------------------------- /src/pages/Login.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { Link, useNavigate } from 'react-router-dom'; 3 | import { useSelector, useDispatch } from 'react-redux'; 4 | import { signIn } from '../redux/reducers/user'; 5 | 6 | const Login = () => { 7 | const user = useSelector((state) => state.user); 8 | 9 | const [email, setEmail] = useState(''); 10 | const [password, setPassword] = useState(''); 11 | 12 | const navigate = useNavigate(); 13 | const dispatch = useDispatch(); 14 | 15 | const handleLogin = (e) => { 16 | e.preventDefault(); 17 | dispatch(signIn({ email, password })); 18 | }; 19 | 20 | useEffect(() => { 21 | if (user.user) { 22 | navigate('/'); 23 | } 24 | }, [user]); 25 | 26 | if (user.loading) { 27 | return ( 28 |
29 |
30 |
31 | ); 32 | } 33 | 34 | return ( 35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | login form 48 |
49 |
50 |
51 |
52 |
53 | login form 58 |
59 |
60 | Sign into your account 61 |
62 |
63 | setEmail(e.target.value)} 69 | /> 70 |
71 |
72 | setPassword(e.target.value)} 78 | /> 79 |
80 |
81 | 87 |
88 | 89 | Forgot password? 90 | 91 |

92 | Don't have an account? 93 | 94 | Register here 95 | 96 |

97 | 98 | Terms of use. 99 | 100 | 101 | Privacy policy 102 | 103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 | ); 113 | }; 114 | 115 | export default Login; 116 | -------------------------------------------------------------------------------- /src/components/Carousel.js: -------------------------------------------------------------------------------- 1 | import 'slick-carousel/slick/slick.css'; 2 | import 'slick-carousel/slick/slick-theme.css'; 3 | import Slider from 'react-slick'; 4 | import PropTypes from 'prop-types'; 5 | import { Link } from 'react-router-dom'; 6 | import styles from '../styles/rooms.module.css'; 7 | 8 | function RightArrow(props) { 9 | const { className, style, onClick } = props; 10 | return ( 11 |
33 | ); 34 | } 35 | function LeftArrow(props) { 36 | const { className, style, onClick } = props; 37 | return ( 38 |
60 | ); 61 | } 62 | 63 | function Carousel({ rooms }) { 64 | const settings = { 65 | className: styles['my-carousel'], 66 | dots: true, 67 | infinite: true, 68 | speed: 500, 69 | slidesToShow: 3, 70 | slidesToScroll: 1, 71 | swipeToSlide: true, 72 | nextArrow: , 73 | prevArrow: , 74 | initialSlide: 0, 75 | responsive: [ 76 | { 77 | breakpoint: 1024, 78 | settings: { 79 | slidesToShow: 2, 80 | }, 81 | }, 82 | { 83 | breakpoint: 768, 84 | settings: { 85 | slidesToShow: 1, 86 | }, 87 | }, 88 | ], 89 | }; 90 | 91 | return ( 92 |
    93 | 106 | {rooms.map((room) => ( 107 | 108 |
  • 109 | room pic 110 |

    {room.description}

    111 |

    112 | $ 113 | {room.night_cost} 114 | /night 115 |

    116 |
  • 117 | 118 | ))} 119 |
    120 |
121 | ); 122 | } 123 | 124 | RightArrow.propTypes = { 125 | className: PropTypes.string, 126 | style: PropTypes.shape({ 127 | display: PropTypes.string, 128 | backgroundColor: PropTypes.string, 129 | color: PropTypes.string, 130 | height: PropTypes.string, 131 | paddingRight: PropTypes.string, 132 | alignItems: PropTypes.string, 133 | borderTopRightRadius: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 134 | borderBottomRightRadius: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 135 | borderTopLeftRadius: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 136 | borderBottomLeftRadius: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 137 | }), 138 | onClick: PropTypes.func, 139 | }; 140 | RightArrow.defaultProps = { 141 | className: null, 142 | style: null, 143 | onClick() {}, 144 | }; 145 | 146 | LeftArrow.propTypes = { 147 | className: PropTypes.string, 148 | style: PropTypes.shape({ 149 | display: PropTypes.string, 150 | backgroundColor: PropTypes.string, 151 | color: PropTypes.string, 152 | height: PropTypes.string, 153 | paddingLeft: PropTypes.string, 154 | alignItems: PropTypes.string, 155 | borderTopRightRadius: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 156 | borderBottomRightRadius: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 157 | borderTopLeftRadius: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 158 | borderBottomLeftRadius: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 159 | }), 160 | onClick: PropTypes.func, 161 | }; 162 | 163 | LeftArrow.defaultProps = { 164 | className: null, 165 | style: null, 166 | onClick() {}, 167 | }; 168 | 169 | Carousel.propTypes = { 170 | rooms: PropTypes.arrayOf( 171 | PropTypes.shape({ 172 | id: PropTypes.number.isRequired, 173 | image: PropTypes.string.isRequired, 174 | description: PropTypes.string.isRequired, 175 | night_cost: PropTypes.number.isRequired, 176 | }), 177 | ).isRequired, 178 | }; 179 | 180 | export default Carousel; 181 | -------------------------------------------------------------------------------- /src/pages/SignUp.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import { useDispatch, useSelector } from 'react-redux'; 4 | import { signUp } from '../redux/reducers/user'; 5 | 6 | const SignUp = () => { 7 | const [email, setEmail] = useState(''); 8 | const [password, setPassword] = useState(''); 9 | const [passwordConfirmation, setpasswordConfirmation] = useState(''); 10 | const [error, setError] = useState(''); 11 | const dispatch = useDispatch(); 12 | const navigate = useNavigate(); 13 | const user = useSelector((state) => state.user); 14 | 15 | const handleSignUp = (e) => { 16 | if (password !== passwordConfirmation) { 17 | setError('Passwords do not match'); 18 | return; 19 | } 20 | e.preventDefault(); 21 | dispatch(signUp({ email, password })); 22 | }; 23 | 24 | useEffect(() => { 25 | if (user.user) { 26 | navigate('/'); 27 | } 28 | }, [user]); 29 | 30 | if (user.loading) { 31 | return ( 32 |
33 |
34 |
35 | ); 36 | } 37 | 38 | return ( 39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |

Sign up

48 |
49 |
50 | 51 |
52 | setEmail(e.target.value)} 59 | /> 60 |
61 |
62 |
63 | 64 |
65 | setPassword(e.target.value)} 72 | /> 73 |
74 |
75 |
76 | 77 |
78 | setpasswordConfirmation(e.target.value)} 85 | /> 86 |
87 |
88 |
89 | {error} 90 |
91 |
92 | 98 | 102 |
103 |
104 | 110 |
111 | 112 |
113 |
114 | Sample 119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 | ); 128 | }; 129 | 130 | export default SignUp; 131 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # 📗 Table of Contents 4 | 5 | - [📗 Table of Contents](#-table-of-contents) 6 | - [📖 Book-a-room App ](#-book-a-room-app-) 7 | - [Live Demo](#live-demo) 8 | - [🛠 Built With ](#-built-with-) 9 | - [Tech Stack ](#tech-stack-) 10 | - [Key Features ](#key-features-) 11 | - [💻 Getting Started ](#-getting-started-) 12 | - [Prerequisites](#prerequisites) 13 | - [Setup](#setup) 14 | - [Install](#install) 15 | - [Usage](#usage) 16 | - [Run tests](#run-tests) 17 | - [👥 Authors ](#-authors-) 18 | - [📋 Kanban Board 📋](#-kanban-board-) 19 | - [🔭 Future Features ](#-future-features-) 20 | - [🤝 Contributing ](#-contributing-) 21 | - [⭐️ Show your support ](#️-show-your-support-) 22 | - [🙏 Acknowledgments ](#-acknowledgments-) 23 | - [❓ FAQ (OPTIONAL) ](#-faq-optional-) 24 | - [📝 License ](#-license-) 25 | 26 | 27 | 28 | # 📖 Book-a-room App 29 | 30 | > A responsive app which lets users book their favorite room a night with good price 31 | > 32 | > This is the backend => [API](https://github.com/Mov305/book_a_room_backend.git) 33 | 34 | ### Live Demo 35 | 36 | [Live Demo Link](https://book-my-room-2igi.onrender.com) 37 | 38 | ## 🛠 Built With 39 | 40 | ### Tech Stack 41 | 42 |
43 | Client 44 | 49 |
50 |
51 | Server 52 |
    53 |
  • ROR
  • 54 |
55 |
56 | 57 |
58 | Database 59 | 62 |
63 | 64 | 65 | 66 | ### Key Features 67 | 68 | - **Empty react app** 69 | 70 |

(back to top)

71 | 72 | ## 💻 Getting Started 73 | 74 | ### Prerequisites 75 | 76 | In order to run this project you need: Configure your code editor with HTML , CSS & JS and some other important extensions 77 | 78 | ### Setup 79 | 80 | ``` 81 | git clone git@github.com:zdnahom/book-a-room-frontend.git 82 | ``` 83 | 84 | ### Install 85 | 86 | Install this project with: 87 | 88 | ``` 89 | git clone git@github.com:zdnahom/book-a-room-frontend.git 90 | ``` 91 | 92 | ### Usage 93 | 94 | To run the project, execute the following command: 95 | 96 | ``` 97 | cd book-a-room-frontend 98 | 99 | npm install 100 | 101 | npm start 102 | ``` 103 | 104 | ### Run tests 105 | 106 | - Not available for now 107 | 108 |

(back to top)

109 | 110 | 111 | 112 | ## 👥 Authors 113 | 114 | 👤 **Nahom Zerihun Demissie 💻** 115 | 116 | - GitHub: [@zdnahom](https://github.com/zdnahom) 117 | - Twitter: [@zdnahom](https://twitter.com/Nahomzerihun11) 118 | - LinkedIn: [@zdnahom](https://www.linkedin.com/in/nahomzerihun76/) 119 | 120 | 👤 **Abdelrhman Samy Saad 💻** 121 | 122 | - GitHub: [@Mov305](https://github.com/Mov305) 123 | - Twitter: [@Mov305](https://twitter.com/Mov_abd) 124 | - LinkedIn: [@Mov305](https://www.linkedin.com/in/abdelrhman-samy-80b14b215/) 125 | 126 | 👤 **Nicholas Amissah 💻** 127 | 128 | - GitHub: [@atok624](https://github.com/atok624) 129 | - Twitter: [@atok624](https://twitter.com/mysticalamissah) 130 | - LinkedIn: [@atok624](https://linkedin.com/in/nicholas-amissah-153b09154) 131 | 132 |

(back to top)

133 | 134 | ## 📋 Kanban Board 📋 135 | 136 | ```There are 3 contributors for this project:``` 137 | 138 | - ### [Abdelrhman Samy Saad](https://github.com/Mov305) 139 | - ### [Nicholas Amissah](https://github.com/atok624) 140 | - ### [Nahom Zerihun Demissie](https://github.com/zdnahom) 141 | 142 | - ### Here is the link to the final view Kanban board, showing the various tasks in this project [Final Kanban board](https://github.com/users/Mov305/projects/5) 143 | - ### Here is the link to the initial state of the Kanban board [Initial kanban board](https://user-images.githubusercontent.com/84607674/253231467-464632a5-e305-43f5-93be-7c737e54b422.PNG) 144 | 145 |

(back to top)

146 | 147 | 148 | 149 | ## 🔭 Future Features 150 | 151 | - **Signup page** 152 | - **Login page** 153 | - **Main page** 154 | - **Reservation page** 155 | - **Detail page** 156 | 157 |

(back to top)

158 | 159 | 160 | 161 | ## 🤝 Contributing 162 | 163 | Contributions, issues, and feature requests are welcome! 164 | 165 | Feel free to check the [issues page](../../issues/). 166 | 167 |

(back to top)

168 | 169 | 170 | 171 | ## ⭐️ Show your support 172 | 173 | If you like this project, please clone it and try it. I know you're going to love it 174 | 175 |

(back to top)

176 | 177 | 178 | 179 | ## 🙏 Acknowledgments 180 | 181 | We would like to thank [Murat Korkmaz](https://www.behance.net/muratk) for the amazing [design](https://www.behance.net/gallery/26425031/Vespa-Responsive-Redesign), and we'd also want to thank Microverse(staffs , mentors , reviewers) for giving us the knowledge to build an amazing project like this. 182 | 183 |

(back to top)

184 | 185 | 186 | 187 | ## ❓ FAQ (OPTIONAL) 188 | 189 | - **Can I fork the project and make a contribution?** 190 | 191 | Of course you can! First fork it and contribute to it. 192 | 193 | - **How should I ask a pull request** 194 | 195 | - Step 1 : Click on the pull request button 196 | - Step 2 : create pull request 197 | 198 |

(back to top)

199 | 200 | 201 | 202 | ## 📝 License 203 | 204 | This project is [MIT](./LICENSE) licensed. 205 | 206 |

(back to top)

207 | -------------------------------------------------------------------------------- /src/components/AddReservationForm.js: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import { useSelector, useDispatch } from 'react-redux'; 4 | import { createReservation } from '../redux/reducers/reservation'; 5 | 6 | const AddReservationForm = () => { 7 | const { rooms } = useSelector((store) => store.room); 8 | const { user } = useSelector((store) => store.user); 9 | const dispatch = useDispatch(); 10 | const navigate = useNavigate(); 11 | const [selectedRoom, setSelectedRoom] = useState(null); 12 | const [reservationData, setReservationData] = useState({ 13 | start: '', 14 | end: '', 15 | nights: '', 16 | cost: '', 17 | user_id: user.user.id, 18 | room_id: '', 19 | }); 20 | 21 | const handleCalculateCost = (start, end) => { 22 | if (!reservationData.start || !reservationData.end || !selectedRoom) return; 23 | const startDate = start ? new Date(start) : new Date(reservationData.start); 24 | const endDate = end ? new Date(end) : new Date(reservationData.end); 25 | const nights = (endDate - startDate) / (1000 * 3600 * 24); 26 | const cost = nights * selectedRoom.night_cost; 27 | setReservationData((prevState) => ({ 28 | ...prevState, 29 | nights, 30 | cost, 31 | })); 32 | }; 33 | 34 | const handleSubmit = (e) => { 35 | e.preventDefault(); 36 | if (!reservationData.start || !reservationData.end || !reservationData.room_id) { 37 | return alert('Please fill out all fields'); 38 | } 39 | handleCalculateCost(); 40 | dispatch(createReservation(reservationData)); 41 | return navigate('/my-reservations'); 42 | }; 43 | 44 | return ( 45 |
46 |
47 |
48 |
49 |
50 |
51 |
Type
52 |
53 | {selectedRoom ? selectedRoom.room_type : 'Please select a room'} 54 |
55 |
56 |
57 |
Room Number
58 |
59 | {selectedRoom ? selectedRoom.num : 'NA'} 60 |
61 |
62 |
63 |
Cost/night
64 |
65 | {selectedRoom ? `$${selectedRoom.night_cost}` : 'NA'} 66 |
67 |
68 |
69 |
Description
70 |
71 | {selectedRoom ? selectedRoom.description : 'NA'} 72 |
73 |
74 |
75 | {/* the room image */} 76 |
Image
77 |
78 | {selectedRoom ? ( 79 | room 80 | ) : ( 81 | 'NA' 82 | )} 83 |
84 |
85 |
86 |
87 | 88 |
89 |
90 |
91 | 94 | 114 |
115 | 116 |
117 | {/* input reservation start date */} 118 |
119 | 122 | { 128 | setReservationData((prevState) => ({ 129 | ...prevState, 130 | start: e.target.value, 131 | })); 132 | handleCalculateCost(e.target.value, reservationData.end); 133 | }} 134 | /> 135 |
136 | {/* input reservation end date */} 137 |
138 | 141 | { 147 | setReservationData((prevState) => ({ 148 | ...prevState, 149 | end: e.target.value, 150 | })); 151 | handleCalculateCost(reservationData.start, e.target.value); 152 | }} 153 | /> 154 |
155 |
156 | {reservationData.nights && reservationData.cost && ( 157 |
158 | The Total Cost for your 159 | {reservationData?.nights} 160 | nights stay is 161 | 162 | {reservationData.cost ? ` $${reservationData.cost}` : ''} 163 | 164 |
165 | )} 166 | 167 |
168 | 175 |
176 |
177 |
178 |
179 |
180 |
181 | ); 182 | }; 183 | 184 | export default AddReservationForm; 185 | --------------------------------------------------------------------------------