├── public
├── robots.txt
├── favicon.ico
├── logo192.png
├── logo512.png
├── manifest.json
└── index.html
├── src
├── assets
│ ├── house.jpg
│ └── house.png
├── constants
│ └── index.js
├── index.js
├── redux
│ ├── store.js
│ ├── users
│ │ └── usersSlice.js
│ ├── houses
│ │ ├── deleteHouseSlice.js
│ │ └── housesSlice.js
│ ├── login
│ │ └── loginSlice.js
│ ├── logout
│ │ └── logoutSlice.js
│ ├── signup
│ │ └── signupSlice.js
│ └── reservations
│ │ └── reservationsSlice.js
├── index.css
├── components
│ ├── HousesList.jsx
│ ├── MyReservations.jsx
│ ├── DeleteReservationButton.jsx
│ ├── House.jsx
│ ├── Splash.jsx
│ ├── ReservationCard.jsx
│ ├── Logout.jsx
│ ├── HouseSlider.jsx
│ ├── HouseReservations.jsx
│ ├── ShowHouse.jsx
│ ├── LoginForm.jsx
│ ├── Navbar.jsx
│ ├── Reservation.jsx
│ ├── SignupForm.jsx
│ ├── DeleteHouse.jsx
│ └── AddHouse.jsx
└── App.jsx
├── .babelrc
├── .gitignore
├── .stylelintrc.json
├── .eslintrc.json
├── tailwind.config.js
├── LICENSE
├── README.md
├── package.json
└── .github
└── workflows
└── linters.yml
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SimonGideon/Alpha-Reservation/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SimonGideon/Alpha-Reservation/HEAD/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SimonGideon/Alpha-Reservation/HEAD/public/logo512.png
--------------------------------------------------------------------------------
/src/assets/house.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SimonGideon/Alpha-Reservation/HEAD/src/assets/house.jpg
--------------------------------------------------------------------------------
/src/assets/house.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SimonGideon/Alpha-Reservation/HEAD/src/assets/house.png
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "@babel/preset-react"
4 | ],
5 | "plugins": ["@babel/plugin-syntax-jsx"]
6 | }
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/src/constants/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable quotes */
2 | const links = [
3 | { path: "/home", text: "Houses" },
4 | { path: "/reserve", text: "Reserve" },
5 | { path: "/myReservations", text: "My Reservations" },
6 | { path: "/addHouse", text: "Add House" },
7 | { path: "/deleteHouse", text: "Delete House" },
8 | { path: "/logout", text: "Logout" },
9 | ];
10 |
11 | const backendLink = "https://alpha-reservation-a6eafb83816b.herokuapp.com/";
12 |
13 | export { links, backendLink };
14 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import { BrowserRouter } from 'react-router-dom';
4 | import { Provider } from 'react-redux';
5 | import App from './App';
6 | import store from './redux/store';
7 | import './index.css';
8 |
9 | const root = ReactDOM.createRoot(document.getElementById('root'));
10 | root.render(
11 |
12 |
13 |
14 |
15 |
16 |
17 | ,
18 | );
19 |
--------------------------------------------------------------------------------
/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/redux/store.js:
--------------------------------------------------------------------------------
1 | import { configureStore } from '@reduxjs/toolkit';
2 | import housesReducer from './houses/housesSlice';
3 | import reservationsReducer from './reservations/reservationsSlice';
4 | import loginReducer from './login/loginSlice';
5 | import logoutReducer from './logout/logoutSlice';
6 | import signupReducer from './signup/signupSlice';
7 |
8 | const store = configureStore({
9 | reducer: {
10 | houses: housesReducer,
11 | reservations: reservationsReducer,
12 | login: loginReducer,
13 | logout: logoutReducer,
14 | signup: signupReducer,
15 | },
16 | });
17 |
18 | export default store;
19 |
--------------------------------------------------------------------------------
/.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": ["tailwind", "apply", "variants", "responsive", "screen"]
9 | }
10 | ],
11 | "scss/at-rule-no-unknown": [
12 | true,
13 | {
14 | "ignoreAtRules": ["tailwind", "apply", "variants", "responsive", "screen"]
15 | }
16 | ],
17 | "csstree/validator": true
18 | },
19 | "ignoreFiles": ["build/**", "dist/**", "**/reset*.css", "**/bootstrap*.css", "**/*.js", "**/*.jsx"]
20 | }
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @import url('https://fonts.googleapis.com/css2?family=Kaushan+Script&family=Noto+Sans+Display:wght@400;500;900&family=Ubuntu:wght@400;700&display=swap');
6 |
7 | * {
8 | scroll-behavior: smooth;
9 | }
10 |
11 | *::-webkit-scrollbar {
12 | width: 8px;
13 | }
14 |
15 | *::-webkit-scrollbar-track {
16 | background-color: #f1f1f1;
17 | }
18 |
19 | *::-webkit-scrollbar-thumb {
20 | background-color: #888;
21 | border-radius: 10px;
22 | }
23 |
24 | *::-webkit-scrollbar-thumb:hover {
25 | background-color: #555;
26 | }
27 |
28 | .house-background {
29 | background-image: url("https://rb.gy/paine");
30 | }
31 |
--------------------------------------------------------------------------------
/.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"],
17 | "rules": {
18 | "react/jsx-filename-extension": ["warn", { "extensions": [".js", ".jsx"] }],
19 | "react/react-in-jsx-scope": "off",
20 | "import/no-unresolved": "off",
21 | "no-shadow": "off"
22 | },
23 | "ignorePatterns": [
24 | "dist/",
25 | "build/"
26 | ]
27 | }
--------------------------------------------------------------------------------
/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 | colors: {
9 | mustard: '#ffb400',
10 | lime: '#97bf0f',
11 | limelight: '#96bf01',
12 | 'black-100': '#101010',
13 | 'white-100': '#f5f5f5',
14 | 'gray-100': '#b6b6b6',
15 | },
16 | fontFamily: {
17 | noto: ['Noto Sans Display', 'sans-serif'],
18 | kaushan: ['Kaushan Script', 'cursive'],
19 | ubuntu: ['Ubuntu', 'sans-serif'],
20 | },
21 | animation: {
22 | 'pulse-slow': 'pulse 3s linear infinite',
23 | },
24 | screens: {
25 | tablet: '1640px',
26 | xl: '1340px',
27 | },
28 | },
29 | },
30 | plugins: [],
31 | };
32 |
--------------------------------------------------------------------------------
/src/redux/users/usersSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
2 | import axios from 'axios';
3 | import { backendLink } from '../../constants';
4 |
5 | const initialState = {
6 | usersList: [],
7 | isLoading: false,
8 | };
9 |
10 | export const fetchUsers = createAsyncThunk('users/fetchUsers',
11 | async () => {
12 | const response = await axios(`${backendLink}users`);
13 | return response.data;
14 | });
15 |
16 | const usersSlice = createSlice({
17 | name: 'users',
18 | initialState,
19 | extraReducers: (builder) => {
20 | builder
21 | .addCase(fetchUsers.pending, (state) => ({
22 | ...state,
23 | isLoading: true,
24 | }))
25 | .addCase(fetchUsers.fulfilled, (state, action) => ({
26 | ...state,
27 | usersList: action.payload,
28 | isLoading: false,
29 | }))
30 | .addCase(fetchUsers.rejected, (state) => ({
31 | ...state,
32 | isLoading: false,
33 | }));
34 | },
35 | });
36 |
37 | export default usersSlice.reducer;
38 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 José Montoya, Mohamed Abd Elmohsen Saleh, Simon Gideon, Bryan Hurtado.
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/redux/houses/deleteHouseSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
2 | import axios from 'axios';
3 |
4 | import { backendLink } from '../../constants';
5 |
6 | const initialState = {
7 | house: null,
8 | isLoading: false,
9 | };
10 |
11 | export const deleteHouse = createAsyncThunk(
12 | 'houses/deleteHouse',
13 | async (houseId) => {
14 | const token = localStorage.getItem('token');
15 |
16 | await axios.delete(`${backendLink}houses/${houseId}`, {
17 | headers: {
18 | Authorization: token,
19 | },
20 | });
21 | return houseId;
22 | },
23 | );
24 |
25 | const deleteHouseSlice = createSlice({
26 | name: 'deleteHouse',
27 | initialState,
28 | extraReducers: (builder) => {
29 | builder
30 | .addCase(deleteHouse.pending, (state) => ({
31 | ...state,
32 | isLoading: true,
33 | }))
34 | .addCase(deleteHouse.fulfilled, (state, action) => ({
35 | ...state,
36 | house: action.payload,
37 | isLoading: false,
38 | }))
39 | .addCase(deleteHouse.rejected, (state) => ({
40 | ...state,
41 | isLoading: false,
42 | }));
43 | },
44 | });
45 |
46 | export default deleteHouseSlice.reducer;
47 |
--------------------------------------------------------------------------------
/src/components/HousesList.jsx:
--------------------------------------------------------------------------------
1 | import { RxDotFilled } from 'react-icons/rx';
2 | import { useNavigate } from 'react-router-dom';
3 | import { useEffect } from 'react';
4 | import PropTypes from 'prop-types';
5 | import HouseSlider from './HouseSlider';
6 |
7 | const HousesList = (props) => {
8 | const { authorized } = props;
9 | const navigate = useNavigate();
10 | useEffect(() => {
11 | if (!authorized) {
12 | navigate('/');
13 | }
14 | }, [authorized, navigate]);
15 | if (!authorized) {
16 | return (
17 | <>
18 | >
19 | );
20 | }
21 |
22 | return (
23 |
24 |
LATEST HOUSES
25 |
Please select a House
26 |
27 | {Array.from({ length: 20 }, (_, i) => (
28 |
29 | ))}
30 |
31 |
32 |
33 | );
34 | };
35 |
36 | HousesList.propTypes = {
37 | authorized: PropTypes.bool.isRequired,
38 | };
39 | export default HousesList;
40 |
--------------------------------------------------------------------------------
/src/redux/login/loginSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
2 | import axios from 'axios';
3 | import { backendLink } from '../../constants';
4 |
5 | // Define the async thunk for logging in
6 | export const login = createAsyncThunk('auth/login', async (credentials) => {
7 | try {
8 | const response = await axios.post(`${backendLink}login`, { user: { email: credentials.email, password: credentials.password } });
9 | return response.headers.authorization;
10 | } catch (error) {
11 | throw new Error('Login failed');
12 | }
13 | });
14 |
15 | // Create the login slice
16 | const loginSlice = createSlice({
17 | name: 'auth',
18 | initialState: {
19 | loginToken: null,
20 | loading: false,
21 | error: null,
22 | },
23 | reducers: {},
24 | extraReducers: (builder) => {
25 | builder
26 | .addCase(login.pending, (state) => ({
27 | ...state,
28 | isLoading: true,
29 | error: null,
30 | }))
31 | .addCase(login.fulfilled, (state, action) => ({
32 | ...state,
33 | isLoading: false,
34 | loginToken: action.payload,
35 | }))
36 | .addCase(login.rejected, (state, action) => ({
37 | ...state,
38 | isLoading: false,
39 | error: action.error.message,
40 | }));
41 | },
42 | });
43 |
44 | // Export the async thunk and the slice reducer
45 | export default loginSlice.reducer;
46 |
--------------------------------------------------------------------------------
/src/redux/logout/logoutSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
2 | import axios from 'axios';
3 | import { backendLink } from '../../constants';
4 |
5 | export const logoutUser = createAsyncThunk(
6 | 'auth/logoutUser',
7 | async () => {
8 | const token = localStorage.getItem('token');
9 |
10 | // Clear the token from local storage
11 | localStorage.removeItem('token');
12 |
13 | // Make the logout request to the backend
14 | await axios.delete(`${backendLink}logout`, {
15 | headers: {
16 | Accept: 'application/json',
17 | Authorization: token,
18 | },
19 | });
20 | },
21 | );
22 |
23 | const logoutSlice = createSlice({
24 | name: 'auth',
25 | initialState: {
26 | logoutToken: localStorage.getItem('token') || null,
27 | loading: false,
28 | error: null,
29 | },
30 | reducers: {},
31 | extraReducers: (builder) => {
32 | builder
33 | .addCase(logoutUser.pending, (state) => ({
34 | ...state,
35 | loading: true,
36 | error: null,
37 | }))
38 | .addCase(logoutUser.fulfilled, (state) => ({
39 | ...state,
40 | loading: false,
41 | logoutToken: null,
42 | }))
43 | .addCase(logoutUser.rejected, (state, action) => ({
44 | ...state,
45 | loading: false,
46 | error: action.error.message,
47 | }));
48 | },
49 | });
50 |
51 | export default logoutSlice.reducer;
52 |
--------------------------------------------------------------------------------
/src/components/MyReservations.jsx:
--------------------------------------------------------------------------------
1 | import { useSelector, useDispatch } from 'react-redux';
2 | import { useEffect } from 'react';
3 | import { useNavigate } from 'react-router-dom';
4 | import PropTypes from 'prop-types';
5 | import { fetchReservations } from '../redux/reservations/reservationsSlice';
6 | import ReservationCard from './ReservationCard';
7 |
8 | const MyReservations = (props) => {
9 | const dispatch = useDispatch();
10 | const { reservationsList } = useSelector((store) => store.reservations);
11 | const { authorized } = props;
12 | const navigate = useNavigate();
13 | useEffect(() => {
14 | if (!authorized) {
15 | navigate('/');
16 | }
17 | }, [authorized, navigate]);
18 |
19 | useEffect(() => {
20 | dispatch(fetchReservations());
21 | }, [dispatch]);
22 | return (
23 |
24 |
MY RESERVATIONS
25 | {reservationsList.map((reservation) => (
26 |
27 |
33 |
34 |
35 | ))}
36 |
37 | );
38 | };
39 | MyReservations.propTypes = {
40 | authorized: PropTypes.bool.isRequired,
41 | };
42 |
43 | export default MyReservations;
44 |
--------------------------------------------------------------------------------
/src/components/DeleteReservationButton.jsx:
--------------------------------------------------------------------------------
1 | import { useDispatch } from 'react-redux';
2 | import Swal from 'sweetalert2';
3 | import PropTypes from 'prop-types';
4 | import { deleteReservation } from '../redux/reservations/reservationsSlice';
5 |
6 | const DeleteReservationButton = (props) => {
7 | const dispatch = useDispatch();
8 | const { reservationId } = props;
9 | const handleDelete = () => {
10 | Swal.fire({
11 | title: 'Are you sure?',
12 | text: 'Once canceled, you will not be able to recover this reservation!',
13 | icon: 'warning',
14 | confirmButtonColor: '#96BF01',
15 | showCancelButton: true,
16 | confirmButtonText: 'Cancel',
17 | cancelButtonText: 'Back',
18 | reverseButtons: true,
19 | }).then((result) => {
20 | if (result.isConfirmed) {
21 | dispatch(deleteReservation(reservationId)).then(() => {
22 | Swal.fire('Canceled!', 'The Reservation has been canceled.', 'success').then(() => {
23 | window.location.reload();
24 | });
25 | });
26 | }
27 | });
28 | };
29 | return (
30 |
35 | Cancel
36 |
37 | );
38 | };
39 |
40 | DeleteReservationButton.propTypes = {
41 | reservationId: PropTypes.number.isRequired,
42 | };
43 | export default DeleteReservationButton;
44 |
--------------------------------------------------------------------------------
/src/redux/signup/signupSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
2 | import axios from 'axios';
3 | import { backendLink } from '../../constants';
4 | // Define the async thunk for logging in
5 | export const signup = createAsyncThunk('auth/signup', async (credentials) => {
6 | try {
7 | const response = await axios.post(`${backendLink}signup`, {
8 | user: {
9 | name: credentials.name,
10 | email: credentials.email,
11 | password: credentials.password,
12 | password_confirmation: credentials.password_confirmation,
13 | },
14 | });
15 | return response.headers.authorization;
16 | } catch (error) {
17 | throw new Error('Signup failed');
18 | }
19 | });
20 |
21 | // Create the login slice
22 | const signupSlice = createSlice({
23 | name: 'auth',
24 | initialState: {
25 | signupToken: null,
26 | loading: false,
27 | error: null,
28 | },
29 | reducers: {},
30 | extraReducers: (builder) => {
31 | builder
32 | .addCase(signup.pending, (state) => ({
33 | ...state,
34 | isLoading: true,
35 | error: null,
36 | }))
37 | .addCase(signup.fulfilled, (state, action) => ({
38 | ...state,
39 | isLoading: false,
40 | loginToken: action.payload,
41 | }))
42 | .addCase(signup.rejected, (state, action) => ({
43 | ...state,
44 | isLoading: false,
45 | error: action.error.message,
46 | }));
47 | },
48 | });
49 |
50 | // Export the async thunk and the slice reducer
51 | export default signupSlice.reducer;
52 |
--------------------------------------------------------------------------------
/src/components/House.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import { MdLocationOn } from 'react-icons/md';
3 | import { RxDotFilled } from 'react-icons/rx';
4 |
5 | const House = ({ house }) => (
6 |
7 |
11 |
{house.name}
12 |
13 | {Array.from({ length: 10 }, (_, i) => (
14 |
15 | ))}
16 |
17 |
{house.description}
18 |
19 |
20 |
21 |
{house.city}
22 |
23 |
-
24 |
25 |
{house.address}
26 |
27 |
28 |
29 | );
30 |
31 | House.propTypes = {
32 | house: PropTypes.shape({
33 | photo: PropTypes.string.isRequired,
34 | name: PropTypes.string.isRequired,
35 | description: PropTypes.string.isRequired,
36 | city: PropTypes.string.isRequired,
37 | address: PropTypes.string.isRequired,
38 | }).isRequired,
39 | };
40 |
41 | export default House;
42 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | Alpha Reservations
28 |
29 |
30 | You need to enable JavaScript to run this app.
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Alpha Reservations
2 |
3 | ## Table of Contents
4 |
5 | - [About the Project](#about-project)
6 | - [Built With:](#built-with)
7 | - [Tech Stack](#tech-stack)
8 | - [Key Features](#key-features)
9 | - [Live Demo](#live-demo)
10 | - [Getting Started](#getting-started)
11 | - [Setup](#setup)
12 | - [Kanbanboards](#kanbanboards)
13 | - [Prerequisites](#prerequisites)
14 | - [Install](#install)
15 | - [Usage](#usage)
16 | - [Authors](#authors)
17 | - [Future Features](#future-features)
18 | - [Contributing](#contributing)
19 | - [Show your support](#support)
20 | - [Acknowledgements](#acknowledgements)
21 | - [FAQ](#faq)
22 | - [License](#license)
23 |
24 | ## Alpha Reservations
25 |
26 | This is a full stack app built with React, Redux, Tailwind CSS, and Ruby on Rails.
27 | The application features authentication to access it, promps the user with a selection of houses, and a navigation bar, allowing the user to reserve, create, and delete houses.
28 |
29 | ## Built With
30 |
31 | ### Tech Stack :
32 | Client
33 | - React
34 |
35 | Server
36 | - Ruby on Rails
37 |
38 | Database
39 | - PostgreSQL
40 |
41 | ### Key Features :
42 | - Authentication
43 | - Manage reservations per user
44 | - API calls to reserve, create and delete entities
45 | - Responsive design
46 |
47 | (back to top )
48 |
49 | ### Kanbanboards
50 | 1. [Initial Kanbanboard](https://github.com/jmonto55/book-an-appointment-backend/issues/1)
51 | 2. [Final Kanbanboard](https://github.com/jmonto55/book-an-appointment-backend/projects/2)
52 |
53 |
54 | ## Live Demo
55 |
56 | - [Live Demo](https://alpha-reservation.vercel.app/)
57 |
58 | (back to top )
59 |
--------------------------------------------------------------------------------
/src/components/Splash.jsx:
--------------------------------------------------------------------------------
1 | import { BiChevronRightCircle } from 'react-icons/bi';
2 | import { useNavigate } from 'react-router-dom';
3 | import PropTypes from 'prop-types';
4 | import { useEffect } from 'react';
5 | import houseImage from '../assets/house.png';
6 |
7 | const Splash = (props) => {
8 | const navigate = useNavigate();
9 | const { authorized } = props;
10 | useEffect(
11 | () => {
12 | if (authorized) {
13 | navigate('/home');
14 | }
15 | },
16 | );
17 | const loginNavagtion = () => {
18 | navigate('/login');
19 | };
20 |
21 | const signupNavagtion = () => {
22 | navigate('/signup');
23 | };
24 |
25 | return (
26 |
27 |
30 |
alpha reservations
31 |
32 |
33 | Log In
34 |
35 |
36 |
37 | Sign Up
38 |
39 |
40 |
41 |
42 |
43 | );
44 | };
45 | Splash.propTypes = {
46 | authorized: PropTypes.bool.isRequired,
47 | };
48 | export default Splash;
49 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "book-an-appointment-frontend",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@reduxjs/toolkit": "^1.9.5",
7 | "@testing-library/jest-dom": "^5.16.5",
8 | "@testing-library/react": "^13.4.0",
9 | "@testing-library/user-event": "^13.5.0",
10 | "axios": "^1.4.0",
11 | "cross-env": "^7.0.3",
12 | "prop-types": "^15.8.1",
13 | "react": "^18.2.0",
14 | "react-dom": "^18.2.0",
15 | "react-icons": "^4.9.0",
16 | "react-redux": "^8.0.7",
17 | "react-router": "^6.12.1",
18 | "react-router-dom": "^6.12.1",
19 | "react-scripts": "5.0.1",
20 | "redux": "^4.2.1",
21 | "redux-devtools-extension": "^2.13.9",
22 | "redux-thunk": "^2.4.2",
23 | "sweetalert": "^2.1.2",
24 | "sweetalert2": "^11.7.12",
25 | "web-vitals": "^2.1.4"
26 | },
27 | "scripts": {
28 | "start": "cross-env PORT=3001 react-scripts start",
29 | "build": "react-scripts build",
30 | "test": "react-scripts test",
31 | "eject": "react-scripts eject"
32 | },
33 | "eslintConfig": {
34 | "extends": [
35 | "react-app",
36 | "react-app/jest"
37 | ]
38 | },
39 | "browserslist": {
40 | "production": [
41 | ">0.2%",
42 | "not dead",
43 | "not op_mini all"
44 | ],
45 | "development": [
46 | "last 1 chrome version",
47 | "last 1 firefox version",
48 | "last 1 safari version"
49 | ]
50 | },
51 | "devDependencies": {
52 | "@babel/core": "^7.22.5",
53 | "@babel/eslint-parser": "^7.22.5",
54 | "@babel/plugin-syntax-jsx": "^7.22.5",
55 | "@babel/preset-react": "^7.22.5",
56 | "eslint": "^7.32.0",
57 | "eslint-config-airbnb": "^18.2.1",
58 | "eslint-plugin-import": "^2.27.5",
59 | "eslint-plugin-jsx-a11y": "^6.7.1",
60 | "eslint-plugin-react": "^7.32.2",
61 | "eslint-plugin-react-hooks": "^4.6.0",
62 | "stylelint": "^13.13.1",
63 | "stylelint-config-standard": "^21.0.0",
64 | "stylelint-csstree-validator": "^1.9.0",
65 | "stylelint-scss": "^3.21.0",
66 | "tailwindcss": "^3.3.2"
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/App.jsx:
--------------------------------------------------------------------------------
1 | import { Routes, Route } from 'react-router-dom';
2 | import { useSelector } from 'react-redux';
3 | import Navbar from './components/Navbar';
4 | import HousesList from './components/HousesList';
5 | import Reservation from './components/Reservation';
6 | import MyReservations from './components/MyReservations';
7 | import HouseReservations from './components/HouseReservations';
8 | import AddHouse from './components/AddHouse';
9 | import DeleteHouse from './components/DeleteHouse';
10 | import ShowHouse from './components/ShowHouse';
11 | import LoginForm from './components/LoginForm';
12 | import Splash from './components/Splash';
13 | import Logout from './components/Logout';
14 | import SignupForm from './components/SignupForm';
15 |
16 | function App() {
17 | const { loginToken } = useSelector((store) => store.login);
18 | const { logoutToken } = useSelector((store) => store.logout);
19 | let isAuthorized = false;
20 | if (loginToken || logoutToken) {
21 | isAuthorized = true;
22 | }
23 | return (
24 |
25 |
26 |
27 | } />
28 | } />
29 | } />
30 | } />
31 | } />
32 | } />
33 | } />
34 | } />
35 | } />
36 | } />
37 | } />
38 |
39 |
40 | );
41 | }
42 |
43 | export default App;
44 |
--------------------------------------------------------------------------------
/.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@v2
14 | - uses: actions/setup-node@v1
15 | with:
16 | node-version: "12.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@v2
29 | - uses: actions/setup-node@v1
30 | with:
31 | node-version: "12.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@v2
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
--------------------------------------------------------------------------------
/src/components/ReservationCard.jsx:
--------------------------------------------------------------------------------
1 | import { useSelector, useDispatch } from 'react-redux';
2 | import { useEffect } from 'react';
3 | import { MdLocationOn } from 'react-icons/md';
4 | import PropTypes from 'prop-types';
5 | import { fetchHouses } from '../redux/houses/housesSlice';
6 | import DeleteReservationButton from './DeleteReservationButton';
7 |
8 | const ReservationCard = (props) => {
9 | const dispatch = useDispatch();
10 | useEffect(() => {
11 | dispatch(fetchHouses());
12 | }, [dispatch]);
13 |
14 | const { housesList } = useSelector((store) => store.houses);
15 | const {
16 | houseId, checkIn, checkOut, reservationId,
17 | } = props;
18 | const house = housesList.find((house) => house.id === houseId);
19 | return (
20 |
21 |
22 |
23 |
24 |
25 |
{house ? house.name : 'loading...'}
26 |
{house ? house.description : 'loading...'}
27 |
28 |
29 | {' '}
30 | {house ? house.city : 'loading...'}
31 | ,
32 | {' '}
33 | {house ? house.address : 'loading...'}
34 |
35 |
36 | {' '}
37 | Reservation period
38 | {' '}
39 | {checkIn}
40 | {' '}
41 | to
42 | {' '}
43 | {checkOut}
44 |
45 |
46 |
47 |
48 | );
49 | };
50 | ReservationCard.propTypes = {
51 | reservationId: PropTypes.number.isRequired,
52 | houseId: PropTypes.number.isRequired,
53 | checkIn: PropTypes.string.isRequired,
54 | checkOut: PropTypes.string.isRequired,
55 | };
56 |
57 | export default ReservationCard;
58 |
--------------------------------------------------------------------------------
/src/components/Logout.jsx:
--------------------------------------------------------------------------------
1 | import { React, useEffect } from 'react';
2 | import { useDispatch } from 'react-redux';
3 | import { useNavigate, NavLink } from 'react-router-dom';
4 | import PropTypes from 'prop-types';
5 | import { BiLeftArrow } from 'react-icons/bi';
6 | import { logoutUser } from '../redux/logout/logoutSlice';
7 | import houseImage from '../assets/house.jpg';
8 |
9 | const LogoutButton = (props) => {
10 | const dispatch = useDispatch();
11 | const navigate = useNavigate();
12 | const { authorized } = props;
13 | useEffect(
14 | () => {
15 | if (!authorized) {
16 | navigate('/');
17 | }
18 | },
19 | );
20 | const handleLogout = () => {
21 | dispatch(logoutUser());
22 | navigate('/');
23 | };
24 |
25 | return (
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
Log out?
34 |
35 |
40 | Cancel
41 |
42 |
47 | Logout
48 |
49 |
50 |
51 |
52 | );
53 | };
54 | LogoutButton.propTypes = {
55 | authorized: PropTypes.bool.isRequired,
56 | };
57 | export default LogoutButton;
58 |
--------------------------------------------------------------------------------
/src/components/HouseSlider.jsx:
--------------------------------------------------------------------------------
1 | import { useSelector, useDispatch } from 'react-redux';
2 | import { useEffect, useState } from 'react';
3 | import { NavLink } from 'react-router-dom';
4 | import { BiLeftArrow, BiRightArrow } from 'react-icons/bi';
5 | import { fetchHouses, fetchHouse } from '../redux/houses/housesSlice';
6 | import House from './House';
7 |
8 | const HouseSlider = () => {
9 | const dispatch = useDispatch();
10 | const { housesList } = useSelector((store) => store.houses);
11 |
12 | const [indexOne, setIndexOne] = useState(0);
13 | const [indexTwo, setIndexTwo] = useState(1);
14 | const [indexThree, setIndexThree] = useState(2);
15 |
16 | const nextSlide = () => {
17 | setIndexOne((indexOne - 1 + housesList.length) % housesList.length);
18 | setIndexTwo((indexTwo - 1 + housesList.length) % housesList.length);
19 | setIndexThree((indexThree - 1 + housesList.length) % housesList.length);
20 | };
21 |
22 | const prevSlide = () => {
23 | setIndexOne((indexOne + 1) % housesList.length);
24 | setIndexTwo((indexTwo + 1) % housesList.length);
25 | setIndexThree((indexThree + 1) % housesList.length);
26 | };
27 |
28 | useEffect(() => {
29 | dispatch(fetchHouses());
30 | }, [dispatch]);
31 |
32 | return (
33 |
34 |
35 |
36 |
37 |
38 |
39 | {housesList.length > 0 && (
40 | <>
41 | {
44 | dispatch(fetchHouse(housesList[indexOne].id));
45 | }}
46 | >
47 |
48 |
49 | {
53 | dispatch(fetchHouse(housesList[indexTwo].id));
54 | }}
55 | >
56 |
57 |
58 | {
62 | dispatch(fetchHouse(housesList[indexThree].id));
63 | }}
64 | >
65 |
66 |
67 | >
68 | )}
69 |
70 |
71 |
72 |
73 |
74 |
75 | );
76 | };
77 |
78 | export default HouseSlider;
79 |
--------------------------------------------------------------------------------
/src/redux/houses/housesSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
2 | import axios from 'axios';
3 | import { backendLink } from '../../constants';
4 |
5 | const initialState = {
6 | housesList: [],
7 | currentHouse: {},
8 | isLoading: false,
9 | status: null,
10 | };
11 |
12 | export const fetchHouses = createAsyncThunk(
13 | 'houses/fetchHouses',
14 | async () => {
15 | const token = localStorage.getItem('token');
16 | const response = await axios.get(`${backendLink}houses`, {
17 | headers: {
18 | authorization: token, // Include the token in the Authorization header
19 | },
20 | });
21 | return response.data;
22 | },
23 | );
24 |
25 | export const createHouse = createAsyncThunk(
26 | 'houses/createHouse',
27 | async (houseData) => {
28 | const token = localStorage.getItem('token');
29 | const response = await axios.post(`${backendLink}houses`, houseData, {
30 | headers: {
31 | authorization: token, // Include the token in the Authorization header
32 | },
33 | });
34 | return response.data;
35 | },
36 | );
37 |
38 | export const fetchHouse = createAsyncThunk('houses/fetchHouse',
39 | async (id) => {
40 | const token = localStorage.getItem('token');
41 | const response = await axios(`${backendLink}houses/${id}`, {
42 | headers: {
43 | authorization: token, // Include the token in the Authorization header
44 | },
45 | });
46 | return response.data;
47 | });
48 |
49 | const housesSlice = createSlice({
50 | name: 'houses',
51 | initialState,
52 | extraReducers: (builder) => {
53 | builder
54 | .addCase(fetchHouses.pending, (state) => ({
55 | ...state,
56 | isLoading: true,
57 | }))
58 | .addCase(fetchHouses.fulfilled, (state, action) => ({
59 | ...state,
60 | housesList: action.payload,
61 | isLoading: false,
62 | }))
63 | .addCase(fetchHouses.rejected, (state, action) => ({
64 | ...state,
65 | isLoading: false,
66 | error: action.error.message,
67 | }));
68 |
69 | builder
70 | .addCase(fetchHouse.pending, (state) => ({
71 | ...state,
72 | isLoading: true,
73 | }))
74 | .addCase(fetchHouse.fulfilled, (state, action) => ({
75 | ...state,
76 | currentHouse: action.payload,
77 | isLoading: false,
78 | }))
79 | .addCase(fetchHouse.rejected, (state, action) => ({
80 | ...state,
81 | isLoading: false,
82 | error: action.error.message,
83 | }));
84 | builder
85 | .addCase(createHouse.pending, (state) => ({
86 | ...state,
87 | isLoading: true,
88 | }))
89 | .addCase(createHouse.fulfilled, (state, action) => ({
90 | ...state,
91 | housesList: [...state.housesList, action.payload],
92 | isLoading: false,
93 | }))
94 | .addCase(createHouse.rejected, (state, action) => ({
95 | ...state,
96 | isLoading: false,
97 | error: action.error.message,
98 | }));
99 | },
100 | });
101 |
102 | export default housesSlice.reducer;
103 |
--------------------------------------------------------------------------------
/src/components/HouseReservations.jsx:
--------------------------------------------------------------------------------
1 | import { useSelector, useDispatch } from 'react-redux';
2 | import { useEffect } from 'react';
3 | import { MdLocationOn } from 'react-icons/md';
4 | import { useNavigate } from 'react-router-dom';
5 | import PropTypes from 'prop-types';
6 | import { fetchHouseReservations } from '../redux/reservations/reservationsSlice';
7 | import { fetchHouse } from '../redux/houses/housesSlice';
8 |
9 | const HouseReservations = (props) => {
10 | const dispatch = useDispatch();
11 | const { houseReservationsList } = useSelector((store) => store.reservations);
12 | const { currentHouse } = useSelector((store) => store.houses);
13 | const { authorized } = props;
14 | const houseId = window.location.href.split('/')[4];
15 | const navigate = useNavigate();
16 | useEffect(() => {
17 | if (!authorized) {
18 | navigate('/');
19 | }
20 | }, [authorized, navigate]);
21 |
22 | useEffect(() => {
23 | dispatch(fetchHouseReservations(houseId));
24 | }, [dispatch, houseId]);
25 | useEffect(() => {
26 | dispatch(fetchHouse(houseId));
27 | }, [dispatch, houseId]);
28 | return (
29 |
30 |
31 | {`${currentHouse.name} `}
32 | Reservations Details
33 |
34 |
35 |
36 | Description:
37 | {' '}
38 | {currentHouse ? currentHouse.description : 'loading...'}
39 |
40 |
41 | Location:
42 | {' '}
43 |
44 | {' '}
45 | {currentHouse ? currentHouse.city : 'loading...'}
46 | ,
47 | {' '}
48 | {currentHouse ? currentHouse.address : 'loading...'}
49 |
50 |
51 | Important Note: Here is all the reserved information for
52 | {' '}
53 | {currentHouse.name}
54 | {' '}
55 | House that will help you choose an empty time slots to reserve
56 |
57 |
58 |
59 |
60 | Check-in
61 | Check-out
62 |
63 |
64 |
65 |
66 | {houseReservationsList ? houseReservationsList.map((reservation) => (
67 |
68 | {reservation.check_in}
69 | {reservation.check_out}
70 |
71 | )) : '...loading'}
72 |
73 |
74 |
75 |
76 | );
77 | };
78 | HouseReservations.propTypes = {
79 | authorized: PropTypes.bool.isRequired,
80 | };
81 |
82 | export default HouseReservations;
83 |
--------------------------------------------------------------------------------
/src/components/ShowHouse.jsx:
--------------------------------------------------------------------------------
1 | import { useSelector } from 'react-redux';
2 | import { BiLeftArrow } from 'react-icons/bi';
3 | import { NavLink, useNavigate } from 'react-router-dom';
4 | import { MdLocationOn, MdAdsClick } from 'react-icons/md';
5 | import PropTypes from 'prop-types';
6 | import { useEffect } from 'react';
7 |
8 | const ShowHouse = (props) => {
9 | const { currentHouse } = useSelector((store) => store.houses);
10 | const { authorized } = props;
11 | const navigate = useNavigate();
12 | useEffect(() => {
13 | if (!authorized) {
14 | navigate('/');
15 | }
16 | }, [authorized, navigate]);
17 | if (!authorized) {
18 | return (<>>);
19 | }
20 |
21 | return (
22 |
23 |
HOUSE DETAILS
24 |
25 |
26 |
27 |
28 |
29 |
30 |
34 |
35 |
36 | {currentHouse.name}
37 | *$45 deposit upon any reservation.
38 |
39 |
{currentHouse.description}
40 |
41 |
42 |
43 |
{currentHouse.city}
44 |
45 |
-
46 |
47 |
{currentHouse.address}
48 |
49 |
50 |
51 | $
52 | {currentHouse.night_price}
53 | night
54 |
55 |
56 |
navigate(`/reserve?id=${currentHouse.id}`)} className="ml-auto flex bg-lime text-white-100 p-2 px-4 rounded-full">
57 |
58 | Reserve
59 |
60 |
61 |
62 |
63 |
64 |
65 | );
66 | };
67 | ShowHouse.propTypes = {
68 | authorized: PropTypes.bool.isRequired,
69 | };
70 | export default ShowHouse;
71 |
--------------------------------------------------------------------------------
/src/components/LoginForm.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { useDispatch } from 'react-redux';
3 | import { useNavigate, NavLink } from 'react-router-dom';
4 | import { BiLeftArrow } from 'react-icons/bi';
5 | import PropTypes from 'prop-types';
6 | import houseImage from '../assets/house.jpg';
7 | import { login } from '../redux/login/loginSlice';
8 |
9 | const LoginForm = (props) => {
10 | const { authorized } = props;
11 | const dispatch = useDispatch();
12 | const [email, setEmail] = useState('');
13 | const [password, setPassword] = useState('');
14 | const navigate = useNavigate();
15 |
16 | useEffect(() => {
17 | if (authorized) {
18 | navigate('/home');
19 | }
20 | }, [authorized, navigate]);
21 |
22 | useEffect(() => {
23 | setEmail('simon20163858@gmail.com');
24 | setPassword('momo123456');
25 | }, []);
26 |
27 | const handleSubmit = (e) => {
28 | e.preventDefault();
29 |
30 | dispatch(login({ email, password }))
31 | .unwrap()
32 | .then((token) => {
33 | localStorage.setItem('token', token);
34 | navigate('/home');
35 | });
36 | };
37 |
38 | return (
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
Log in
47 |
71 |
72 |
73 | );
74 | };
75 |
76 | LoginForm.propTypes = {
77 | authorized: PropTypes.bool.isRequired,
78 | };
79 |
80 | export default LoginForm;
81 |
--------------------------------------------------------------------------------
/src/components/Navbar.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, useCallback } from 'react';
2 | import { NavLink, useLocation } from 'react-router-dom';
3 | import { HiOutlineMenuAlt4 } from 'react-icons/hi';
4 | import { IoMdClose } from 'react-icons/io';
5 | import { links } from '../constants';
6 |
7 | const Navbar = () => {
8 | const [open, setOpen] = useState(true);
9 | const location = useLocation();
10 |
11 | const openMenu = () => {
12 | setOpen(!open);
13 | const nav = document.querySelector('nav');
14 | nav.style.display = 'flex';
15 | };
16 |
17 | const closeMenu = useCallback(() => {
18 | setOpen(!open);
19 | if (window.innerWidth < 1340) {
20 | const nav = document.querySelector('nav');
21 | nav.style.display = 'none';
22 | nav.style.position = 'absolute';
23 | }
24 | }, [open]);
25 |
26 | useEffect(() => {
27 | closeMenu();
28 | setOpen(true);
29 | }, [location, closeMenu]);
30 |
31 | useEffect(() => {
32 | const mediaQuery1 = window.matchMedia('(min-width: 1340px)');
33 |
34 | const handleMediaQueryChange = (event) => {
35 | if (event.matches) {
36 | const nav = document.querySelector('nav');
37 | nav.style.display = 'flex';
38 | nav.style.position = 'static';
39 | }
40 | };
41 |
42 | mediaQuery1.addEventListener('change', handleMediaQueryChange);
43 |
44 | return () => {
45 | mediaQuery1.removeEventListener('change', handleMediaQueryChange);
46 | };
47 | }, []);
48 |
49 | useEffect(() => {
50 | const mediaQuery1 = window.matchMedia('(max-width: 1340px)');
51 |
52 | const handleMediaQueryChange = (event) => {
53 | if (event.matches) {
54 | const nav = document.querySelector('nav');
55 | nav.style.display = 'none';
56 | setOpen(true);
57 | }
58 | };
59 |
60 | mediaQuery1.addEventListener('change', handleMediaQueryChange);
61 |
62 | return () => {
63 | mediaQuery1.removeEventListener('change', handleMediaQueryChange);
64 | };
65 | }, []);
66 |
67 | return (
68 | <>
69 | {open ? (
70 |
76 | ) : (
77 |
83 | )}
84 |
85 |
86 |
Alpha Reservations
87 |
88 |
89 | {links.map((link) => (
90 |
95 | {
98 | closeMenu();
99 | }}
100 | style={({ isActive }) => ({
101 | backgroundColor: isActive ? '#97bf0f' : '#fff',
102 | color: isActive ? '#fff' : '#000',
103 | })}
104 | className="text-start w-full text-xl font-black py-2 px-2 uppercase"
105 | >
106 | {link.text}
107 |
108 |
109 | ))}
110 |
111 |
112 | >
113 | );
114 | };
115 |
116 | export default Navbar;
117 |
--------------------------------------------------------------------------------
/src/redux/reservations/reservationsSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
2 | import axios from 'axios';
3 | import { backendLink } from '../../constants';
4 |
5 | const initialState = {
6 | reservationsList: [],
7 | isLoading: false,
8 | reserveError: null,
9 | houseReservationsList: [],
10 | };
11 |
12 | export const fetchReservations = createAsyncThunk('reservations/fetchReservations',
13 | async () => {
14 | const token = localStorage.getItem('token');
15 | const response = await axios(`${backendLink}reservations`, {
16 | headers: {
17 | Accept: 'application/json',
18 | authorization: token, // Include the token in the Authorization header
19 | },
20 | });
21 | return response.data;
22 | });
23 |
24 | export const fetchHouseReservations = createAsyncThunk('reservations/fetchHouseReservations',
25 | async (houseId) => {
26 | const token = localStorage.getItem('token');
27 | const response = await axios(`${backendLink}house/${houseId}/reservations`, {
28 | headers: {
29 | Accept: 'application/json',
30 | authorization: token, // Include the token in the Authorization header
31 | },
32 | });
33 | return response.data;
34 | });
35 |
36 | export const deleteReservation = createAsyncThunk(
37 | 'reservations/deleteReservation',
38 | async (reservationId) => {
39 | const token = localStorage.getItem('token');
40 |
41 | await axios.delete(`${backendLink}reservations/${reservationId}`, {
42 | headers: {
43 | Accept: 'application/json',
44 | Authorization: token,
45 | },
46 | });
47 | return reservationId;
48 | },
49 | );
50 |
51 | export const reserve = createAsyncThunk(
52 | 'reservations/reserve',
53 | async (credentials, { rejectWithValue }) => {
54 | const token = localStorage.getItem('token');
55 | try {
56 | const response = await axios.post(
57 | `${backendLink}reservations`,
58 | {
59 | reservation: {
60 | house_id: credentials.houseId,
61 | check_in: credentials.checkIn,
62 | check_out: credentials.checkOut,
63 | },
64 | },
65 | {
66 | headers: {
67 | Accept: 'application/json',
68 | Authorization: token,
69 | },
70 | },
71 | );
72 | return response.data;
73 | } catch (error) {
74 | if (error.response) {
75 | // Handle error with response from Rails
76 | return rejectWithValue(error.response.data.base);
77 | }
78 |
79 | // Handle other errors
80 | return rejectWithValue(error.message);
81 | }
82 | },
83 | );
84 |
85 | const reservationsSlice = createSlice({
86 | name: 'reservations',
87 | initialState,
88 | extraReducers: (builder) => {
89 | builder
90 | .addCase(fetchReservations.pending, (state) => ({
91 | ...state,
92 | status: 'loading',
93 | reserveError: null,
94 | }))
95 | .addCase(fetchReservations.fulfilled, (state, action) => ({
96 | ...state,
97 | reservationsList: action.payload,
98 | status: 'succeeded',
99 | reserveError: null,
100 | }))
101 | .addCase(fetchHouseReservations.fulfilled, (state, action) => ({
102 | ...state,
103 | houseReservationsList: action.payload,
104 | reserveError: null,
105 | }))
106 | .addCase(fetchReservations.rejected, (state) => ({
107 | ...state,
108 | status: 'error',
109 | reserveError: null,
110 | }))
111 | .addCase(reserve.rejected, (state, action) => ({
112 | ...state,
113 | reserveError: action.payload,
114 | }))
115 | .addCase(reserve.fulfilled, (state) => ({
116 | ...state,
117 | reserveError: 'Your reservation has been done go check my reservations page!',
118 | }));
119 | },
120 | });
121 |
122 | export default reservationsSlice.reducer;
123 |
--------------------------------------------------------------------------------
/src/components/Reservation.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { useDispatch, useSelector } from 'react-redux';
3 | import { useNavigate, useLocation } from 'react-router-dom';
4 | import PropTypes from 'prop-types';
5 | import { fetchHouses } from '../redux/houses/housesSlice';
6 | import { reserve } from '../redux/reservations/reservationsSlice';
7 |
8 | const Reservation = (props) => {
9 | const { authorized } = props;
10 | const dispatch = useDispatch();
11 | const location = useLocation();
12 | const queryParams = new URLSearchParams(location.search);
13 | const id = queryParams.get('id');
14 | const [houseId, setHouseId] = useState(id || 1);
15 | const [checkIn, setCheckIn] = useState('');
16 | const [checkOut, setCheckOut] = useState('');
17 | const { housesList } = useSelector((store) => store.houses);
18 | const { reserveError } = useSelector((store) => store.reservations);
19 | const navigate = useNavigate();
20 | useEffect(() => {
21 | if (!authorized) {
22 | navigate('/');
23 | }
24 | }, [authorized, navigate]);
25 | useEffect(() => {
26 | dispatch(fetchHouses());
27 | }, [dispatch]);
28 | const handleSubmit = async (e) => {
29 | e.preventDefault();
30 | dispatch(reserve({ houseId, checkIn, checkOut }));
31 | };
32 | useEffect(() => {
33 | if (!reserveError) {
34 | // navigate('/myReservations');
35 | }
36 | }, [reserveError, navigate]);
37 |
38 | if (!authorized) {
39 | return (<>>);
40 | }
41 |
42 | return (
43 |
44 |
45 |
ADD A NEW RESERVATION
46 |
87 | {reserveError && (
88 |
89 | {reserveError}
90 |
91 | )}
92 |
93 |
94 | );
95 | };
96 |
97 | Reservation.propTypes = {
98 | authorized: PropTypes.bool.isRequired,
99 | };
100 | export default Reservation;
101 |
--------------------------------------------------------------------------------
/src/components/SignupForm.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { useDispatch } from 'react-redux';
3 | import { useNavigate, NavLink } from 'react-router-dom';
4 | import { BiLeftArrow } from 'react-icons/bi';
5 | import PropTypes from 'prop-types';
6 | import houseImage from '../assets/house.jpg';
7 | import { signup } from '../redux/signup/signupSlice';
8 |
9 | const SignupForm = (props) => {
10 | const { authorized } = props;
11 | const dispatch = useDispatch();
12 | const [name, setName] = useState('');
13 | const [email, setEmail] = useState('');
14 | const [password, setPassword] = useState('');
15 | const [passwordConfirmation, setPasswordConfirmation] = useState('');
16 | const navigate = useNavigate();
17 |
18 | useEffect(() => {
19 | if (authorized) {
20 | navigate('/home');
21 | }
22 | }, [authorized, navigate]);
23 | const handleSubmit = (e) => {
24 | e.preventDefault();
25 |
26 | dispatch(signup({
27 | name, email, password, passwordConfirmation,
28 | }))
29 | .unwrap()
30 | .then((token) => {
31 | // Handle successful login
32 | localStorage.setItem('token', token);
33 | navigate('/login');
34 | });
35 | };
36 |
37 | return (
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
Sign up
46 |
88 |
89 |
90 | );
91 | };
92 | SignupForm.propTypes = {
93 | authorized: PropTypes.bool.isRequired,
94 | };
95 | export default SignupForm;
96 |
--------------------------------------------------------------------------------
/src/components/DeleteHouse.jsx:
--------------------------------------------------------------------------------
1 | import { FaMapMarkerAlt, FaCity } from 'react-icons/fa';
2 | import { FiInfo } from 'react-icons/fi';
3 | import { BiLeftArrow } from 'react-icons/bi';
4 | import { NavLink, useNavigate } from 'react-router-dom';
5 | import { useSelector, useDispatch } from 'react-redux';
6 | import { useEffect } from 'react';
7 | import Swal from 'sweetalert2';
8 | import PropTypes from 'prop-types';
9 | import { fetchHouses } from '../redux/houses/housesSlice';
10 | import { deleteHouse } from '../redux/houses/deleteHouseSlice';
11 |
12 | const DeleteHouse = (props) => {
13 | const dispatch = useDispatch();
14 | const { housesList } = useSelector((store) => store.houses);
15 | const { authorized } = props;
16 |
17 | const navigate = useNavigate();
18 | useEffect(() => {
19 | if (!authorized) {
20 | navigate('/');
21 | }
22 | }, [authorized, navigate]);
23 |
24 | useEffect(() => {
25 | dispatch(fetchHouses());
26 | }, [dispatch]);
27 |
28 | const handleDelete = (houseId) => {
29 | Swal.fire({
30 | title: 'Are you sure?',
31 | text: 'Once deleted, you will not be able to recover this house!',
32 | icon: 'warning',
33 | confirmButtonColor: '#96BF01',
34 | showCancelButton: true,
35 | confirmButtonText: 'Delete',
36 | cancelButtonText: 'Cancel',
37 | reverseButtons: true,
38 | }).then((result) => {
39 | if (result.isConfirmed) {
40 | dispatch(deleteHouse(houseId)).then(() => {
41 | Swal.fire('Deleted!', 'The house has been deleted.', 'success').then(() => {
42 | window.location.reload();
43 | });
44 | });
45 | }
46 | });
47 | };
48 | if (authorized) {
49 | return (
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
SELECT HOUSE TO DELETE
58 |
59 | {housesList.map((house) => (
60 |
61 |
62 |
63 |
64 |
65 | $
66 | {house.night_price}
67 |
68 |
69 |
70 |
71 |
72 |
{house.name}
73 |
74 |
75 |
76 |
77 | {house.description}
78 |
79 |
80 |
81 |
82 |
83 |
84 | {house.address}
85 |
86 |
87 |
88 |
89 |
90 | {house.city}
91 |
92 |
handleDelete(house.id)}
95 | className="bg-lime border-2 border-t-0 border-l-0 border-white/25 backdrop-filter backdrop-blur-lg bg-opacity-80 shadow-md hover:bg-mustard text-white rounded-full px-4 py-2 mt-4"
96 | >
97 | Delete
98 |
99 |
100 |
101 |
102 | ))}
103 |
104 |
105 |
106 | );
107 | }
108 |
109 | return (
110 | <>
111 | >
112 | );
113 | };
114 | DeleteHouse.propTypes = {
115 | authorized: PropTypes.bool.isRequired,
116 | };
117 | export default DeleteHouse;
118 |
--------------------------------------------------------------------------------
/src/components/AddHouse.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, useRef } from 'react';
2 | import { useDispatch } from 'react-redux';
3 | import { BiLeftArrow } from 'react-icons/bi';
4 | import { NavLink, useNavigate } from 'react-router-dom';
5 | import { MdAdsClick } from 'react-icons/md';
6 | import PropTypes from 'prop-types';
7 | import { createHouse } from '../redux/houses/housesSlice';
8 | import houseImage from '../assets/house.jpg';
9 |
10 | const AddHouse = (props) => {
11 | const { authorized } = props;
12 | const navigate = useNavigate();
13 | useEffect(() => {
14 | if (!authorized) {
15 | navigate('/');
16 | }
17 | }, [authorized, navigate]);
18 | const dispatch = useDispatch();
19 | const initialFormData = [
20 | { name: 'name', value: '' },
21 | { name: 'address', value: '' },
22 | { name: 'description', value: '' },
23 | { name: 'city', value: '' },
24 | { name: 'photo', value: '' },
25 | { name: 'night_price', value: '' },
26 | ];
27 | const [formData, setFormData] = useState(initialFormData);
28 | const [isSubmitted, setIsSubmitted] = useState(false);
29 | const successMessageRef = useRef(null);
30 |
31 | const capitalize = (string) => string.charAt(0).toUpperCase() + string.slice(1);
32 |
33 | const handleChange = (e) => {
34 | const { name, value } = e.target;
35 | setFormData((prevFormData) => prevFormData.map((field) => (
36 | field.name === name ? { ...field, value } : field)));
37 | };
38 |
39 | const handleSubmit = (e) => {
40 | e.preventDefault();
41 | const houseData = formData.reduce(
42 | (data, field) => ({ ...data, [field.name]: field.value }),
43 | {},
44 | );
45 | dispatch(createHouse(houseData));
46 | setFormData(initialFormData);
47 | setIsSubmitted(true);
48 | };
49 |
50 | useEffect(() => {
51 | const handleClickOutside = (event) => {
52 | if (successMessageRef.current && !successMessageRef.current.contains(event.target)) {
53 | setIsSubmitted(false);
54 | }
55 | };
56 |
57 | document.addEventListener('click', handleClickOutside);
58 |
59 | return () => {
60 | document.removeEventListener('click', handleClickOutside);
61 | };
62 | }, []);
63 | if (!authorized) {
64 | return (<>>);
65 | }
66 |
67 | return (
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
ADD A NEW HOUSE
76 |
111 |
112 |
113 | );
114 | };
115 |
116 | AddHouse.propTypes = {
117 | authorized: PropTypes.bool.isRequired,
118 | };
119 | export default AddHouse;
120 |
--------------------------------------------------------------------------------