├── 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 | You need to enable JavaScript to run this app.
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 |
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 | Room
29 | Date Start
30 | Date End
31 | Cost
32 | Unbook
33 |
34 |
35 |
36 | {reservations.map((reservation) => (
37 |
38 |
39 | {reservation.room_id}
40 |
41 | {reservation.start}
42 | {reservation.end}
43 | {reservation.cost}
44 |
45 | handleDelete(reservation.id)}
49 | >
50 | Unbook
51 |
52 |
53 |
54 | ))}
55 |
56 |
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 |
29 |
30 |
navigate(-1)}
34 | aria-label="Go back"
35 | >
36 |
37 |
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 |
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 |
37 |
38 | RE/ROOM
39 | {isMobile && (
40 | <>
41 |
47 |
48 |
49 |
55 |
56 |
57 | >
58 | )}
59 |
60 |
61 |
62 | (isActive ? styles['active-link'] : styles['nav-link'])}
65 | onClick={closeMenu}
66 | >
67 | Rooms
68 |
69 |
70 |
71 | (isActive ? styles['active-link'] : styles['nav-link'])}
74 | onClick={closeMenu}
75 | >
76 | Make a reservation
77 |
78 |
79 |
80 | (isActive ? styles['active-link'] : styles['nav-link'])}
83 | onClick={closeMenu}
84 | >
85 | My reservations
86 |
87 |
88 |
89 | (isActive ? styles['active-link'] : styles['nav-link'])}
92 | onClick={closeMenu}
93 | >
94 | My rooms
95 |
96 |
97 |
98 | (isActive ? styles['active-link'] : styles['nav-link'])}
101 | onClick={closeMenu}
102 | >
103 | Post a room
104 |
105 |
106 |
107 |
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 |
31 | );
32 | }
33 |
34 | return (
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
48 |
49 |
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 |
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 |
35 | );
36 | }
37 |
38 | return (
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
113 |
114 |
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 |
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 |
80 | ) : (
81 | 'NA'
82 | )}
83 |
84 |
85 |
86 |
87 |
88 |
178 |
179 |
180 |
181 | );
182 | };
183 |
184 | export default AddReservationForm;
185 |
--------------------------------------------------------------------------------