├── .gitignore ├── README.md ├── backend ├── config │ └── db.ts ├── controllers │ ├── bookingController.ts │ ├── roomController.ts │ └── userController.ts ├── middlewares │ ├── authMiddleware.ts │ └── errorMiddleware.ts ├── models │ ├── Booking.ts │ ├── Room.ts │ └── User.ts ├── routes │ ├── bookingRoutes.ts │ ├── roomRoutes.ts │ ├── uploadRoutes.ts │ └── userRoutes.ts ├── server.ts └── utils │ ├── generateToken.ts │ └── getStripe.ts ├── frontend ├── README.md ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt ├── src │ ├── @types │ │ └── allTypes.d.ts │ ├── App.css │ ├── App.tsx │ ├── bootstrap.min.css │ ├── components │ │ ├── FormReview.tsx │ │ ├── Header.tsx │ │ ├── ListReviews.tsx │ │ ├── Loader.tsx │ │ ├── Message.tsx │ │ ├── OnlyAdmin.tsx │ │ ├── Paginate.tsx │ │ ├── ProtectedRoute.tsx │ │ ├── Rating.tsx │ │ ├── RoomCard.tsx │ │ ├── RoomFeatures.tsx │ │ └── SearchRooms.tsx │ ├── hooks │ │ └── useAuthStatus.tsx │ ├── index.tsx │ ├── interfaces │ │ ├── IBooking.ts │ │ ├── IRoom.ts │ │ └── IUser.ts │ ├── react-app-env.d.ts │ ├── redux │ │ ├── actions │ │ │ ├── BookingActions.tsx │ │ │ ├── RoomActions.tsx │ │ │ └── UserActions.tsx │ │ ├── constants │ │ │ ├── BookingConstants.tsx │ │ │ ├── RoomConstants.tsx │ │ │ └── UserConstants.tsx │ │ ├── reducers │ │ │ ├── BookingReducers.tsx │ │ │ ├── RoomReducers.tsx │ │ │ └── UserReducers.tsx │ │ └── store.tsx │ └── screens │ │ ├── AdminBookingsScreen.tsx │ │ ├── AdminCreateRoomScreen.tsx │ │ ├── AdminEditRoomScreen.tsx │ │ ├── AdminEditUserScreen.tsx │ │ ├── AdminRoomsScreen.tsx │ │ ├── AdminUsersScreen.tsx │ │ ├── HomeScreen.tsx │ │ ├── LoginScreen.tsx │ │ ├── MyBookingsScreen.tsx │ │ ├── PasswordScreen.tsx │ │ ├── ProfileScreen.tsx │ │ ├── RegisterScreen.tsx │ │ └── RoomDetailsScreen.tsx ├── tsconfig.json └── yarn.lock ├── package-lock.json ├── package.json ├── tsconfig.json └── uploads ├── default-room.jpeg ├── image-1643568802545-854384589.png ├── image-1643569249831-505829359.jpeg ├── image-1644677085702-997801630.jpg ├── image-1644677120247-123558054.jpg ├── image-1644677259587-703863587.jpg ├── image-1644677336866-354344121.jpg ├── image-1644959297658-856579790.jpg ├── image-1644959297667-735126546.jpg ├── image-1644959682091-418979889.jpg ├── image-1644959682097-256588529.jpg ├── image-1645292083270-442621328.jpg ├── image-1645292083276-634322936.jpg ├── image-1645292974624-924191412.jpg ├── image-1645292974630-35970355.jpg ├── image-1645293318427-623712477.jpg ├── image-1645293318433-327794372.jpg └── user-default.jpg /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | /node_modules 3 | /frontend/node_modules -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HotelBooking 2 | 3 | Hotel Booking App Using The MERN Stack With TypeScript & Redux 🤩 4 | 5 | ![screenshot](https://i.ibb.co/5Fprvgc/roomhotel.png) 6 | 7 | ## Features: 8 | 9 | - Room reviews and ratings 10 | - Room pagination 11 | - Room search feature 12 | - User profile with bookings 13 | - Admin Room management 14 | - Admin User management 15 | - Admin Booking management 16 | 17 | ## Technology Stack: 18 | 19 | - TypeScript 20 | - Node js 21 | - Express Js 22 | - MongoDB 23 | - JWT 24 | - React 25 | - React Bootstrap 26 | - Redux 27 | - React Paypal Button V2 28 | 29 | ## Usage 30 | 31 | ### Env Variables 32 | 33 | Create a .env file in then root and add the following 34 | 35 | ``` 36 | NODE_ENV = development 37 | PORT = 5000 38 | MONGO_URI = your mongodb uri 39 | JWT_SECRET = 'abc123' 40 | PAYPAL_CLIENT_ID = your paypal client id 41 | ``` 42 | 43 | ## Install Dependencies 44 | 45 | ``` 46 | npm install 47 | cd frontend 48 | npm install 49 | ``` 50 | 51 | ### Run 52 | 53 | ``` 54 | # Run frontend 55 | npm run client 56 | 57 | # Run backend 58 | npm run server 59 | ``` 60 | 61 | - Version: 1.0.0 62 | - License: MIT 63 | - Author: Said Mounaim 64 | -------------------------------------------------------------------------------- /backend/config/db.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import { ConnectionOptions } from 'tls'; 3 | 4 | const connectDB = async () => { 5 | try { 6 | const connect = await mongoose.connect(process.env.MONGO_URI as string, { 7 | useNewUrlParser: true, 8 | useUnifiedTopology: true, 9 | } as ConnectionOptions); 10 | console.log("Database is connected"); 11 | } catch (error: any) { 12 | console.log(error.message); 13 | } 14 | } 15 | 16 | export default connectDB; -------------------------------------------------------------------------------- /backend/controllers/bookingController.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import asyncHandler from 'express-async-handler'; 3 | import Booking, { IBooking } from '../models/Booking'; 4 | import { IUserRequest } from '../models/User'; 5 | import Moment from 'moment'; 6 | import { extendMoment } from 'moment-range'; 7 | 8 | const moment = extendMoment(Moment as any); 9 | 10 | // @Desc new booking 11 | // @Route /api/bookings 12 | // @Method POST 13 | export const newBooking = asyncHandler(async (req: IUserRequest, res: Response) => { 14 | 15 | const { room, checkInDate, checkOutDate, amountPaid, daysOfStay, paymentInfo } = req.body; 16 | 17 | const booking = await Booking.create({ 18 | room, 19 | user: req.user._id, 20 | checkInDate, 21 | checkOutDate, 22 | amountPaid, 23 | daysOfStay, 24 | paymentInfo, 25 | paidAt: Date.now(), 26 | }); 27 | 28 | res.status(201).json(booking); 29 | 30 | }) 31 | 32 | // @Desc Check room is available for booking 33 | // @Route /api/bookings/check 34 | // @Method POST 35 | export const checkRoomIsAvailble = asyncHandler(async (req: Request, res: Response) => { 36 | 37 | const { roomId, checkInDate, checkOutDate } = req.body; 38 | 39 | const room = await Booking.find({ room: roomId, $and: [{ 40 | checkInDate: { 41 | $lte: checkOutDate 42 | } 43 | }, { 44 | checkOutDate: { 45 | $gte: checkInDate 46 | } 47 | }]}); 48 | 49 | let roomAvailable; 50 | 51 | if(room && room.length === 0) { 52 | roomAvailable = true; 53 | } else { 54 | roomAvailable = false; 55 | } 56 | 57 | res.status(201).json({ roomAvailable }); 58 | 59 | }) 60 | 61 | // @Desc Get all bookings current user 62 | // @Route /api/bookings/me 63 | // @Method GET 64 | export const myBookings = asyncHandler(async (req: IUserRequest, res: Response) => { 65 | 66 | const bookings = await Booking.find({ user: req.user._id }).populate("user", "name email").populate("room", "name images"); 67 | 68 | if(!bookings) { 69 | res.status(401); 70 | throw new Error("Bookings not found"); 71 | } 72 | 73 | res.status(201).json(bookings); 74 | 75 | }) 76 | 77 | // @Desc Get booked dates 78 | // Route /api/bookings/dates/:roomId 79 | // @Route GET 80 | export const getBookedDates = asyncHandler(async (req: Request, res: Response) => { 81 | 82 | const bookings = await Booking.find({ room: req.params.roomId }); 83 | 84 | let bookedDates: {}[] = []; 85 | 86 | const timeDiffernece = moment().utcOffset() / 60; 87 | 88 | bookings.forEach((booking: IBooking) => { 89 | 90 | const checkInDate = moment(booking.checkInDate).add(timeDiffernece, 'hours') 91 | const checkOutDate = moment(booking.checkOutDate).add(timeDiffernece, 'hours') 92 | 93 | const range = moment.range(moment(checkInDate), moment(checkOutDate)); 94 | 95 | const dates = Array.from(range.by('day')); 96 | bookedDates = bookedDates.concat(dates); 97 | }) 98 | 99 | res.status(200).json(bookedDates); 100 | 101 | }) 102 | 103 | // @Desc Get all bookings 104 | // @Route /api/bookings 105 | // @Method GET 106 | export const getAll = asyncHandler(async (req: Request, res: Response) => { 107 | const pageSize = 4; 108 | const page = Number(req.query.pageNumber) || 1; 109 | const count = await Booking.countDocuments(); 110 | const bookings = await Booking.find({}).populate("room", "name").populate("user", "name email").limit(pageSize).skip(pageSize * (page - 1)); 111 | res.status(201).json({ 112 | bookings, 113 | page, 114 | pages: Math.ceil(count / pageSize), 115 | count 116 | }); 117 | }) 118 | 119 | // @Desc Delete booking 120 | // @Route /api/bookings/:id 121 | // @Method DELETE 122 | export const deleteBooking = asyncHandler(async (req: Request, res: Response) => { 123 | 124 | const booking = await Booking.findById(req.params.id); 125 | 126 | if(!booking) { 127 | res.status(401); 128 | throw new Error("Booking not found"); 129 | } 130 | 131 | await Booking.findByIdAndDelete(req.params.id); 132 | 133 | res.status(201).json({}); 134 | 135 | }) -------------------------------------------------------------------------------- /backend/controllers/roomController.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import asyncHandler from 'express-async-handler'; 3 | import Room from '../models/Room'; 4 | import { IUserRequest } from '../models/User'; 5 | 6 | // @Desc Get All Rooms 7 | // @Route /api/rooms 8 | // @Method GET 9 | export const getAll = asyncHandler(async(req: Request, res: Response) => { 10 | 11 | const pageSize = 4; 12 | const page = Number(req.query.pageNumber) || 1; 13 | 14 | const keyword = req.query.keyword ? { 15 | $or: [ 16 | {name: { $regex: req.query.keyword, $options: "i" }}, 17 | {description: { $regex: req.query.keyword, $options: "i" }}, 18 | ] 19 | } 20 | : {}; 21 | 22 | const numOfBeds = req.query.numOfBeds ? {numOfBeds: req.query.numOfBeds} : {}; 23 | 24 | const category = req.query.roomType ? {category: req.query.roomType} : {}; 25 | 26 | const count = await Room.countDocuments({ ...keyword, ...numOfBeds, ...category }) 27 | 28 | const rooms = await Room.find({ ...keyword, ...numOfBeds, ...category }).limit(pageSize) 29 | .skip(pageSize * (page - 1)); 30 | res.status(201).json({ 31 | rooms, 32 | page, 33 | pages: Math.ceil(count / pageSize), 34 | count 35 | }); 36 | }) 37 | 38 | // @Desc Search rooms 39 | // @Route /api/rooms/search/ 40 | // @Method GET 41 | export const searchRooms = asyncHandler(async(req: Request, res: Response) => { 42 | const filterd = await Room.find({ $and: [ 43 | { $or: [{name: req.query.keyword },{description: req.query.keyword}] }, 44 | {numOfBeds: req.query.numOfBeds}, 45 | {category: req.query.roomType} 46 | ] }); 47 | res.status(201).json(filterd); 48 | }) 49 | 50 | // @Desc Get Single Room 51 | // @Route /api/rooms/:id 52 | // @Method GET 53 | export const getSingle = asyncHandler(async (req: Request, res: Response) => { 54 | 55 | const room = await Room.findById(req.params.id); 56 | 57 | if(!room) { 58 | res.status(401); 59 | throw new Error("Room not found"); 60 | } 61 | 62 | res.status(201).json(room); 63 | 64 | }) 65 | 66 | // @Desc Create new room 67 | // @Route /api/rooms 68 | // @Method POST 69 | export const addRoom = asyncHandler(async (req: IUserRequest, res: Response) => { 70 | 71 | req.body.user = req.user._id; 72 | 73 | const room = await Room.create(req.body); 74 | 75 | res.status(201).json(room); 76 | 77 | }) 78 | 79 | // @Desc Update room 80 | // @Route /api/rooms/:id 81 | // @Method PUT 82 | export const updateRoom = asyncHandler(async (req: IUserRequest, res: Response) => { 83 | 84 | let room = await Room.findById(req.params.id); 85 | 86 | if(!room) { 87 | res.status(401); 88 | throw new Error("Room not found"); 89 | } 90 | 91 | room = await Room.findByIdAndUpdate(req.params.id, req.body, { new: true }); 92 | 93 | res.status(201).json(room); 94 | 95 | }) 96 | 97 | // @Desc Delete room 98 | // @Route /api/rooms/:id 99 | // @Method DELETE 100 | export const deleteRoom = asyncHandler(async (req: IUserRequest, res: Response) => { 101 | 102 | let room = await Room.findById(req.params.id); 103 | 104 | if(!room) { 105 | res.status(401); 106 | throw new Error("Room not found"); 107 | } 108 | 109 | room = await Room.findByIdAndDelete(req.params.id); 110 | 111 | res.status(201).json({}); 112 | 113 | }) 114 | 115 | // @Desc Create Room Review 116 | // @Route /api/rooms/:id/reviews 117 | // @Method POST 118 | export const createRoomReview = asyncHandler(async (req: IUserRequest, res: Response) => { 119 | 120 | const room = await Room.findById(req.params.id); 121 | 122 | if(room) { 123 | 124 | const alreadyReviewd = room.reviews?.find(review => review.user.toString() === req.user._id.toString()); 125 | 126 | if(alreadyReviewd) { 127 | res.status(401); 128 | throw new Error("Already reviewed"); 129 | } 130 | 131 | const { rating, comment } = req.body; 132 | 133 | const review = { 134 | user: req.user._id, 135 | name: req.user.name, 136 | rating: Number(rating), 137 | comment, 138 | } 139 | 140 | room.reviews?.push(review); 141 | 142 | room.numOfReviews = room.reviews?.length; 143 | 144 | room.ratings = room.reviews?.reduce((acc: any, item: any) => item?.rating + acc, 0) / Number(room.reviews?.length); 145 | 146 | await room.save(); 147 | 148 | res.status(201).json({ message: "Review added" }); 149 | 150 | } else { 151 | res.status(401); 152 | throw new Error("Room not found"); 153 | } 154 | 155 | }) -------------------------------------------------------------------------------- /backend/controllers/userController.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | import asyncHandler from "express-async-handler"; 3 | import bcrypt from 'bcrypt'; 4 | import User, { IUserRequest } from "../models/User"; 5 | import generateToken from "../utils/generateToken"; 6 | 7 | // @Desc Register user 8 | // @Route /api/users/register 9 | // @Method POST 10 | export const register = asyncHandler(async (req: Request, res: Response) => { 11 | const { name, email, password, avatar } = req.body; 12 | 13 | const user = new User({ 14 | name, 15 | email, 16 | password, 17 | avatar 18 | }); 19 | 20 | await user.save(); 21 | 22 | res.status(201).json({ 23 | id: user._id, 24 | name: user.name, 25 | email: user.email, 26 | avatar: user.avatar, 27 | isAdmin: user.isAdmin, 28 | token: generateToken(user._id) 29 | }); 30 | }); 31 | 32 | 33 | // @Desc Login user 34 | // @Route /api/users/login 35 | // @Method POST 36 | export const login = asyncHandler(async (req: Request, res: Response) => { 37 | 38 | const { email, password } = req.body; 39 | 40 | const user = await User.findOne({ email }) 41 | 42 | if(!user) { 43 | res.status(401); 44 | throw new Error("User not found"); 45 | } 46 | 47 | if(await user.comparePassword(password)) { 48 | 49 | res.status(201).json({ 50 | id: user._id, 51 | name: user.name, 52 | email: user.email, 53 | avatar: user.avatar, 54 | isAdmin: user.isAdmin, 55 | token: generateToken(user._id) 56 | }); 57 | 58 | } else { 59 | res.status(401); 60 | throw new Error("Email or password incorrect"); 61 | } 62 | 63 | }) 64 | 65 | // @Desc Update profile 66 | // @Route /api/users/update 67 | // @Method PUT 68 | export const updateProfile = asyncHandler(async (req: IUserRequest, res: Response) => { 69 | 70 | let user = await User.findById(req.user.id); 71 | 72 | if(!user) { 73 | res.status(401); 74 | throw new Error("User not found"); 75 | } 76 | 77 | const { name, email, avatar } = req.body; 78 | 79 | user = await User.findByIdAndUpdate(req.user.id, { 80 | name, email, avatar 81 | }, { new: true }).select("-password"); 82 | 83 | res.status(201).json({ 84 | id: user?._id, 85 | name: user?.name, 86 | email: user?.email, 87 | avatar: user?.avatar, 88 | isAdmin: user?.isAdmin, 89 | token: generateToken(user?._id) 90 | }); 91 | 92 | }) 93 | 94 | // @Desc Update password 95 | // @Route /api/users/update/password 96 | // @Method PUT 97 | export const updatePassword = asyncHandler(async(req: IUserRequest, res: Response) => { 98 | 99 | let user = await User.findById(req.user.id); 100 | 101 | if(!user) { 102 | res.status(401); 103 | throw new Error("User not found"); 104 | } 105 | 106 | const { oldPassword, newPassword } = req.body; 107 | 108 | if((await user.comparePassword(oldPassword))) { 109 | 110 | const salt = bcrypt.genSaltSync(10); 111 | const hash = bcrypt.hashSync(newPassword, salt); 112 | 113 | user = await User.findByIdAndUpdate(req.user.id, { 114 | password: hash 115 | }, { new: true }); 116 | 117 | res.status(201).json({ 118 | id: user?._id, 119 | name: user?.name, 120 | email: user?.email, 121 | avatar: user?.avatar, 122 | isAdmin: user?.isAdmin, 123 | token: generateToken(user?._id) 124 | }); 125 | 126 | } else { 127 | res.status(401); 128 | throw new Error("Old password incorrect"); 129 | } 130 | 131 | }) 132 | 133 | // @Desc Get all users 134 | // @Route /api/users 135 | // @Method GET 136 | export const getAll = asyncHandler(async (req: Request, res: Response) => { 137 | 138 | const pageSize = 4; 139 | const page = Number(req.query.pageNumber) || 1; 140 | const count = await User.countDocuments(); 141 | const users = await User.find({}).select("-password").limit(pageSize).skip(pageSize * (page - 1)); 142 | res.status(201).json({ 143 | users, 144 | page, 145 | pages: Math.ceil(count / pageSize), 146 | count 147 | }); 148 | 149 | }) 150 | 151 | // @Desc Get single user by ID 152 | // @Route /api/users/:id 153 | // @Method GET 154 | export const getSingleUser = asyncHandler(async (req: Request, res: Response) => { 155 | 156 | const user = await User.findById(req.params.id).select("-password"); 157 | 158 | if(!user) { 159 | res.status(401); 160 | throw new Error("User not found"); 161 | } 162 | 163 | res.status(201).json(user); 164 | 165 | }) 166 | 167 | // @Desc Update user by ID 168 | // @Route /api/users/:id 169 | // @Method PUT 170 | export const updateUser = asyncHandler(async (req: Request, res: Response) => { 171 | 172 | let user = await User.findById(req.params.id); 173 | 174 | if(!user) { 175 | res.status(401); 176 | throw new Error("User not found"); 177 | } 178 | 179 | user = await User.findByIdAndUpdate(req.params.id, req.body, { new: true }).select("-password"); 180 | 181 | res.status(201).json(user); 182 | 183 | }) 184 | 185 | // @Desc Delete user by ID 186 | // @Route /api/users/:id 187 | // @Method DELETE 188 | export const deleteUser = asyncHandler(async (req: Request, res: Response) => { 189 | 190 | let user = await User.findById(req.params.id); 191 | 192 | if(!user) { 193 | res.status(401); 194 | throw new Error("User not found"); 195 | } 196 | 197 | await User.findByIdAndDelete(req.params.id); 198 | 199 | res.status(201).json({}); 200 | 201 | }) -------------------------------------------------------------------------------- /backend/middlewares/authMiddleware.ts: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken'; 2 | import { NextFunction, Request, Response } from 'express'; 3 | import asyncHandler from 'express-async-handler'; 4 | import User, { IUserRequest } from '../models/User'; 5 | 6 | export const protect = asyncHandler (async(req: IUserRequest, res: Response, next: NextFunction) => { 7 | 8 | let token; 9 | 10 | if(req.headers.authorization && req.headers.authorization.startsWith("Bearer")) { 11 | 12 | try { 13 | token = req.headers.authorization.split(" ")[1]; 14 | const decoded: any = jwt.verify(token, process.env.JWT_SECRET as string); 15 | 16 | req.user = await User.findById(decoded.id).select("-password"); 17 | 18 | next(); 19 | } catch (error: any) { 20 | console.log(error.message); 21 | res.status(401); 22 | throw new Error("no token, no auth"); 23 | } 24 | 25 | } 26 | 27 | if(!token) { 28 | res.status(401); 29 | throw new Error("no token, no auth"); 30 | } 31 | 32 | }) 33 | 34 | export const admin = (req: IUserRequest, res: Response, next: NextFunction) => { 35 | 36 | if(req.user && req.user.isAdmin) { 37 | next(); 38 | } else { 39 | res.status(401); 40 | throw new Error("no token, no auth"); 41 | } 42 | 43 | } -------------------------------------------------------------------------------- /backend/middlewares/errorMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from "express"; 2 | 3 | const notFound = async (req: Request, res: Response, next: NextFunction) => { 4 | const error = new Error(`Not Found ${req.originalUrl}`); 5 | res.status(404); 6 | next(error); 7 | }; 8 | 9 | const errorHandler = ( 10 | err: any, 11 | req: Request, 12 | res: Response, 13 | next: NextFunction 14 | ) => { 15 | const statusCode = res.statusCode === 200 ? 500 : res.statusCode; 16 | res.status(statusCode); 17 | res.json({ success: false, message: err.message }); 18 | }; 19 | 20 | export { notFound, errorHandler }; 21 | -------------------------------------------------------------------------------- /backend/models/Booking.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | type TPaymentInfo = { 4 | id: string, 5 | status: string, 6 | update_time: Date, 7 | email_address: string, 8 | } 9 | 10 | export interface IBooking { 11 | room: string, 12 | user: string, 13 | checkInDate: Date, 14 | checkOutDate: Date, 15 | amountPaid: Number, 16 | daysOfStay: Number, 17 | paymentInfo: TPaymentInfo, 18 | paidAt: Date, 19 | createdAt: Date, 20 | updatedAt: Date 21 | } 22 | 23 | const BookingSchema = new mongoose.Schema({ 24 | 25 | room: { 26 | type: mongoose.Types.ObjectId, 27 | required: true, 28 | ref: "Room" 29 | }, 30 | 31 | user: { 32 | type: mongoose.Types.ObjectId, 33 | required: true, 34 | ref: "User" 35 | }, 36 | 37 | checkInDate: { 38 | type: Date, 39 | required: true, 40 | }, 41 | 42 | checkOutDate: { 43 | type: Date, 44 | required: true, 45 | }, 46 | 47 | amountPaid: { 48 | type: Number, 49 | required: true, 50 | }, 51 | 52 | daysOfStay: { 53 | type: Number, 54 | required: true, 55 | }, 56 | 57 | paymentInfo: { 58 | id: { type: String }, 59 | status: { type: String }, 60 | update_time: { type: Date }, 61 | email_address: { type: String }, 62 | }, 63 | 64 | paidAt: { 65 | type: Date, 66 | required: true 67 | } 68 | 69 | }, { 70 | timestamps: true 71 | }) 72 | 73 | const Booking = mongoose.model("Booking", BookingSchema); 74 | 75 | export default Booking; -------------------------------------------------------------------------------- /backend/models/Room.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | interface IReviews { 4 | user: string, 5 | name: string, 6 | rating: number, 7 | comment: string 8 | } 9 | 10 | interface IImage { 11 | image: string 12 | } 13 | 14 | export interface IRoom extends mongoose.Document { 15 | name: string 16 | description: string, 17 | images: IImage[], 18 | pricePerNight: Number, 19 | address: string, 20 | guestCapacity: Number, 21 | numOfBeds: Number, 22 | internet: Boolean, 23 | airConditioned: Boolean, 24 | petsAllowed: Boolean, 25 | roomCleaning: Boolean, 26 | ratings?: Number, 27 | numOfReviews?: Number, 28 | category: 'King' | 'Single' | 'Twins', 29 | reviews?: IReviews[], 30 | user: mongoose.Types.ObjectId, 31 | createdAt: Date, 32 | updatedAt: Date, 33 | } 34 | 35 | const RoomSchema = new mongoose.Schema({ 36 | 37 | name: { 38 | type: String, 39 | required: true 40 | }, 41 | 42 | description: { 43 | type: String, 44 | required: true 45 | }, 46 | 47 | images: [ 48 | { 49 | image: String 50 | } 51 | ], 52 | 53 | pricePerNight: { 54 | type: Number, 55 | required: true, 56 | }, 57 | 58 | address: { 59 | type: String, 60 | required: true, 61 | }, 62 | 63 | guestCapacity: { 64 | type: Number, 65 | required: true, 66 | }, 67 | 68 | numOfBeds: { 69 | type: Number, 70 | required: true, 71 | }, 72 | 73 | internet: { 74 | type: Boolean, 75 | default: false, 76 | }, 77 | 78 | breakfast: { 79 | type: Boolean, 80 | default: false, 81 | }, 82 | 83 | airConditioned: { 84 | type: Boolean, 85 | default: false, 86 | }, 87 | 88 | petsAllowed: { 89 | type: Boolean, 90 | default: false 91 | }, 92 | 93 | roomCleaning: { 94 | type: Boolean, 95 | default: false 96 | }, 97 | 98 | ratings: { 99 | type: Number, 100 | default: 0 101 | }, 102 | 103 | numOfReviews: { 104 | type: Number, 105 | default: 0 106 | }, 107 | 108 | category: { 109 | type: String, 110 | required: true, 111 | enum: ['King', 'Single', 'Twins'] 112 | }, 113 | 114 | reviews: [ 115 | { 116 | user: { 117 | type: mongoose.Types.ObjectId, 118 | ref: 'User', 119 | required: true 120 | }, 121 | name: { 122 | type: String, 123 | required: true, 124 | }, 125 | rating: { 126 | type: Number, 127 | required: true 128 | }, 129 | comment: { 130 | type: String, 131 | required: true 132 | } 133 | } 134 | ], 135 | 136 | user: { 137 | type: mongoose.Types.ObjectId, 138 | ref: 'User', 139 | required: true 140 | }, 141 | 142 | }, { 143 | timestamps: true 144 | }); 145 | 146 | const Room = mongoose.model("Room", RoomSchema); 147 | 148 | export default Room; -------------------------------------------------------------------------------- /backend/models/User.ts: -------------------------------------------------------------------------------- 1 | import { Request } from 'express'; 2 | import mongoose from 'mongoose'; 3 | import bcrypt from 'bcrypt'; 4 | 5 | export interface IUserRequest extends Request { 6 | user?: any 7 | } 8 | 9 | export interface IUser extends mongoose.Document { 10 | 11 | name: string, 12 | email: string, 13 | password: string, 14 | avatar?: string, 15 | isAdmin: boolean, 16 | token?: string, 17 | createdAt: Date, 18 | updatedAt: Date, 19 | comparePassword(entredPassword: string): Promise 20 | 21 | } 22 | 23 | const UserSchema = new mongoose.Schema({ 24 | 25 | name: { 26 | type: String, 27 | required: true, 28 | }, 29 | 30 | email: { 31 | type: String, 32 | required: true, 33 | unique: true, 34 | match: [ 35 | /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/, 36 | "Please fill a valid email address", 37 | ], 38 | }, 39 | 40 | password: { 41 | type: String, 42 | required: true, 43 | }, 44 | 45 | avatar: { 46 | type: String, 47 | }, 48 | 49 | isAdmin: { 50 | type: Boolean, 51 | default: false, 52 | } 53 | 54 | }, { 55 | timestamps: true 56 | }); 57 | 58 | UserSchema.pre("save", async function(next) { 59 | 60 | const user = this as IUser; 61 | 62 | if(!user.isModified("password")) return next(); 63 | 64 | const salt = bcrypt.genSaltSync(10); 65 | const hash = bcrypt.hashSync(user.password, salt); 66 | 67 | user.password = hash; 68 | 69 | next(); 70 | 71 | }) 72 | 73 | UserSchema.methods.comparePassword = function(entredPassword: string) { 74 | const user = this as IUser; 75 | return bcrypt.compareSync(entredPassword, user.password); 76 | } 77 | 78 | const User = mongoose.model("User", UserSchema); 79 | 80 | export default User; -------------------------------------------------------------------------------- /backend/routes/bookingRoutes.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { getAll, newBooking, checkRoomIsAvailble, getBookedDates, myBookings, deleteBooking } from '../controllers/bookingController'; 3 | import { protect, admin } from '../middlewares/authMiddleware'; 4 | 5 | const router = express.Router(); 6 | 7 | router.route("/").post(protect, newBooking).get(protect, admin, getAll); 8 | router.route("/me").get(protect, myBookings); 9 | router.route("/check").post(checkRoomIsAvailble); 10 | router.route("/dates/:roomId").get(getBookedDates); 11 | router.route("/:id").delete(protect, admin, deleteBooking); 12 | 13 | export default router; -------------------------------------------------------------------------------- /backend/routes/roomRoutes.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { getAll, getSingle, addRoom, updateRoom, deleteRoom, createRoomReview } from '../controllers/roomController'; 3 | import { protect, admin } from '../middlewares/authMiddleware'; 4 | 5 | const router = express.Router(); 6 | 7 | router.route("/").get(getAll).post(protect, admin, addRoom); 8 | router.route("/:id/reviews").post(protect, createRoomReview); 9 | router.route("/:id").get(getSingle).put(protect, updateRoom).delete(protect, admin, deleteRoom); 10 | 11 | export default router; -------------------------------------------------------------------------------- /backend/routes/uploadRoutes.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import express from "express"; 3 | import { Request, Response } from 'express' 4 | import multer from "multer"; 5 | 6 | const router = express.Router(); 7 | 8 | const storage = multer.diskStorage({ 9 | destination: function (req, file: any, cb) { 10 | cb(null, "uploads/"); 11 | }, 12 | filename: function (req, file: any, cb) { 13 | const uniqueSuffix = Date.now() + "-" + Math.round(Math.random() * 1e9); 14 | cb( 15 | null, 16 | `${file.fieldname}-${uniqueSuffix}${path.extname(file.originalname)}` 17 | ); 18 | }, 19 | }); 20 | 21 | function checkFileType(file: any, cb: any) { 22 | const fileTypes = /jpg|jpeg|png/; 23 | const extname = fileTypes.test( 24 | path.extname(file.originalname).toLocaleLowerCase() 25 | ); 26 | const mimetype = fileTypes.test(file.mimetype); 27 | 28 | if (extname && mimetype) { 29 | cb(null, true); 30 | } else { 31 | cb("Images only"); 32 | } 33 | } 34 | 35 | const upload = multer({ 36 | storage, 37 | fileFilter: function (req, file: any, cb: any) { 38 | checkFileType(file, cb); 39 | }, 40 | }); 41 | 42 | router.post("/", upload.array("image", 10), (req: Request, res: Response) => { 43 | console.log(req.files); 44 | res.send(JSON.stringify(req.files)); 45 | }); 46 | 47 | export default router; -------------------------------------------------------------------------------- /backend/routes/userRoutes.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { getAll, register, login, updateUser, getSingleUser, deleteUser, updateProfile, updatePassword } from '../controllers/userController'; 3 | import { protect, admin } from '../middlewares/authMiddleware'; 4 | 5 | const router = express.Router(); 6 | 7 | router.route("/").get(protect, admin, getAll); 8 | router.route("/:id").put(protect, admin, updateUser).get(protect, admin, getSingleUser).delete(protect, admin, deleteUser); 9 | router.route("/register").post(register); 10 | router.route("/login").post(login); 11 | router.route("/update").put(protect, updateProfile); 12 | router.route("/update/password").put(protect, updatePassword); 13 | 14 | export default router; -------------------------------------------------------------------------------- /backend/server.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { Application, Request, Response } from 'express'; 3 | import cors from 'cors'; 4 | import dotenv from 'dotenv'; 5 | import path from 'path'; 6 | import connectDB from './config/db'; 7 | import { errorHandler, notFound } from './middlewares/errorMiddleware'; 8 | 9 | // Routes 10 | import roomRoutes from './routes/roomRoutes'; 11 | import userRoutes from './routes/userRoutes'; 12 | import bookingRoutes from './routes/bookingRoutes'; 13 | import uploadRoutes from './routes/uploadRoutes'; 14 | 15 | const app: Application = express(); 16 | 17 | dotenv.config(); 18 | 19 | connectDB(); 20 | 21 | app.use(cors()); 22 | app.use(express.json()); 23 | 24 | app.use("/uploads", express.static(path.join(__dirname, "../uploads"))); 25 | 26 | // Default 27 | app.get("/api", (req: Request, res: Response) => { 28 | res.status(201).json({ message: "Welcome to Hotel Booking App" }); 29 | }) 30 | 31 | // Room Route 32 | app.use("/api/rooms", roomRoutes); 33 | 34 | // User Route 35 | app.use("/api/users", userRoutes); 36 | 37 | // Booking Route 38 | app.use("/api/bookings", bookingRoutes); 39 | 40 | // Upload Route 41 | app.use("/api/uploads", uploadRoutes); 42 | 43 | app.get("/api/config/paypal", (req, res) => { 44 | res.status(201).send(process.env.PAYPAL_CLIENT_ID); 45 | }); 46 | 47 | app.use(errorHandler); 48 | app.use(notFound); 49 | 50 | const PORT = process.env.PORT || 5000; 51 | 52 | app.listen(PORT, (): void => console.log(`Server is running on PORT ${PORT}`)); 53 | -------------------------------------------------------------------------------- /backend/utils/generateToken.ts: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken'; 2 | 3 | const generateToken = (id: string) => { 4 | const token = jwt.sign({ id }, process.env.JWT_SECRET as string); 5 | return token; 6 | } 7 | 8 | export default generateToken; -------------------------------------------------------------------------------- /backend/utils/getStripe.ts: -------------------------------------------------------------------------------- 1 | import { loadStripe } from '@stripe/stripe-js' 2 | 3 | let stripePromise: any; 4 | 5 | const getStripe = () => { 6 | if (!stripePromise) { 7 | stripePromise = loadStripe(process.env.STRIPE_API_KEY as string); 8 | } 9 | 10 | return stripePromise; 11 | } 12 | 13 | export default getStripe; -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `yarn start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `yarn test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `yarn build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `yarn eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "proxy": "http://127.0.0.1:5000/", 5 | "private": true, 6 | "dependencies": { 7 | "@testing-library/jest-dom": "^5.14.1", 8 | "@testing-library/react": "^12.0.0", 9 | "@testing-library/user-event": "^13.2.1", 10 | "@types/jest": "^27.0.1", 11 | "@types/node": "^16.7.13", 12 | "@types/react": "^17.0.20", 13 | "@types/react-bootstrap": "^0.32.29", 14 | "@types/react-dom": "^17.0.9", 15 | "axios": "^0.25.0", 16 | "bootstrap-icons": "^1.8.0", 17 | "react": "^17.0.2", 18 | "react-bootstrap": "^2.1.1", 19 | "react-datepicker": "^4.6.0", 20 | "react-dom": "^17.0.2", 21 | "react-paypal-button-v2": "^2.6.3", 22 | "react-redux": "^7.2.6", 23 | "react-router-bootstrap": "^0.26.0", 24 | "react-router-dom": "^6.2.1", 25 | "react-scripts": "5.0.0", 26 | "redux": "^4.1.2", 27 | "redux-thunk": "^2.4.1", 28 | "typescript": "^4.4.2", 29 | "web-vitals": "^2.1.0" 30 | }, 31 | "scripts": { 32 | "start": "react-scripts start", 33 | "build": "react-scripts build", 34 | "test": "react-scripts test", 35 | "eject": "react-scripts eject" 36 | }, 37 | "eslintConfig": { 38 | "extends": [ 39 | "react-app", 40 | "react-app/jest" 41 | ] 42 | }, 43 | "browserslist": { 44 | "production": [ 45 | ">0.2%", 46 | "not dead", 47 | "not op_mini all" 48 | ], 49 | "development": [ 50 | "last 1 chrome version", 51 | "last 1 firefox version", 52 | "last 1 safari version" 53 | ] 54 | }, 55 | "devDependencies": { 56 | "@types/react-datepicker": "^4.3.4", 57 | "@types/react-router-bootstrap": "^0.24.5" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saidMounaim/hotel-booking/b1c1f1cd3aa6364044360c27644b317704ecd6ed/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /frontend/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saidMounaim/hotel-booking/b1c1f1cd3aa6364044360c27644b317704ecd6ed/frontend/public/logo192.png -------------------------------------------------------------------------------- /frontend/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saidMounaim/hotel-booking/b1c1f1cd3aa6364044360c27644b317704ecd6ed/frontend/public/logo512.png -------------------------------------------------------------------------------- /frontend/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 | -------------------------------------------------------------------------------- /frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /frontend/src/@types/allTypes.d.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saidMounaim/hotel-booking/b1c1f1cd3aa6364044360c27644b317704ecd6ed/frontend/src/@types/allTypes.d.ts -------------------------------------------------------------------------------- /frontend/src/App.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | } 6 | 7 | .avatar { 8 | width: 40px; 9 | height: 40px; 10 | border-radius: 50%; 11 | margin-right: 5px; 12 | } 13 | 14 | .dropdown-avatar .dropdown-toggle::after { 15 | position: absolute; 16 | right: -11px; 17 | top: 50%; 18 | } 19 | 20 | .card-room { 21 | border: none; 22 | } 23 | 24 | .card-room img { 25 | height: 200px; 26 | border-radius: 20px; 27 | } 28 | 29 | .rating { 30 | display: flex; 31 | align-items: center; 32 | } 33 | 34 | .rating i { 35 | margin-right: 3px; 36 | } 37 | 38 | .carousel-room img { 39 | height: 60vh; 40 | object-fit: cover; 41 | } 42 | 43 | .list-group-item { 44 | background-color: transparent; 45 | color: black; 46 | font-size: 20px; 47 | } 48 | 49 | .list-group-item:hover { 50 | color: inherit; 51 | } 52 | 53 | .field-rating button { 54 | background-color: transparent !important; 55 | border: none; 56 | outline: none; 57 | cursor: pointer; 58 | margin-right: 5px; 59 | } 60 | 61 | .field-rating .on { 62 | color: #000; 63 | } 64 | 65 | .field-rating .off { 66 | color: #ccc; 67 | } 68 | -------------------------------------------------------------------------------- /frontend/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BrowserRouter, Routes, Route } from 'react-router-dom'; 3 | import './App.css'; 4 | import Header from './components/Header'; 5 | import OnlyAdmin from './components/OnlyAdmin'; 6 | import ProtectedRoute from './components/ProtectedRoute'; 7 | import AdminBookingsScreen from './screens/AdminBookingsScreen'; 8 | import AdminCreateRoomScreen from './screens/AdminCreateRoomScreen'; 9 | import AdminEditRoomScreen from './screens/AdminEditRoomScreen'; 10 | import AdminEditUserScreen from './screens/AdminEditUserScreen'; 11 | import AdminRoomsScreen from './screens/AdminRoomsScreen'; 12 | import AdminUsersScreen from './screens/AdminUsersScreen'; 13 | import HomeScreen from './screens/HomeScreen'; 14 | import LoginScreen from './screens/LoginScreen'; 15 | import MyBookingsScreen from './screens/MyBookingsScreen'; 16 | import PasswordScreen from './screens/PasswordScreen'; 17 | import ProfileScreen from './screens/ProfileScreen'; 18 | import RegisterScreen from './screens/RegisterScreen'; 19 | import RoomDetailsScreen from './screens/RoomDetailsScreen'; 20 | 21 | function App() { 22 | return ( 23 | 24 |
25 |
26 |
27 | 28 | } /> 29 | } /> 30 | } /> 31 | } > 32 | } /> 33 | 34 | } > 35 | } /> 36 | 37 | } > 38 | } /> 39 | 40 | } > 41 | } /> 42 | 43 | } > 44 | } /> 45 | 46 | } > 47 | } /> 48 | 49 | } > 50 | } /> 51 | 52 | } > 53 | } /> 54 | 55 | } > 56 | } /> 57 | 58 | } /> 59 | 60 |
61 |
62 |
63 | ); 64 | } 65 | 66 | export default App; 67 | -------------------------------------------------------------------------------- /frontend/src/components/FormReview.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Form, FormGroup, Button, FloatingLabel } from 'react-bootstrap' 3 | import { createRoomReview } from '../redux/actions/RoomActions'; 4 | import { Link } from 'react-router-dom'; 5 | import { useDispatch } from 'react-redux'; 6 | import Message from './Message'; 7 | import { useAuthStatus } from '../hooks/useAuthStatus'; 8 | import { IRoom } from '../interfaces/IRoom'; 9 | 10 | type TFormReview = { 11 | idRoom: IRoom['_id'] 12 | } 13 | 14 | const FormReview: React.FC = ({ idRoom }) => { 15 | 16 | const { loggedIn } = useAuthStatus(); 17 | 18 | const dispatch = useDispatch(); 19 | const [comment, setComment] = useState(""); 20 | const [rating, setRating] = useState(0); 21 | const [hover, setHover] = useState(0); 22 | 23 | const handleReview = (e: React.FormEvent) => { 24 | e.preventDefault(); 25 | 26 | if(comment !== "") { 27 | dispatch(createRoomReview(idRoom, { rating, comment })); 28 | setComment(""); 29 | setRating(0); 30 | } 31 | 32 | } 33 | 34 | 35 | return ( 36 | <> 37 | {loggedIn ? ( 38 |
39 | 40 | {[...Array(5)].map((star, index) => { 41 | index += 1; 42 | return ( 43 | 52 | ); 53 | })} 54 | 55 | 56 | 57 | setComment(e.target.value)} 63 | style={{ height: '100px' }} 64 | /> 65 | 66 | 67 | 68 | 71 | 72 |
73 | ) : ( 74 | 75 | Sign in to write a review 76 | 77 | )} 78 | 79 | ); 80 | }; 81 | 82 | export default FormReview; 83 | -------------------------------------------------------------------------------- /frontend/src/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Navbar, Container, Nav, NavDropdown, Image } from "react-bootstrap"; 3 | import { LinkContainer } from "react-router-bootstrap"; 4 | import { useDispatch, useSelector, RootStateOrAny } from 'react-redux'; 5 | import { logout } from '../redux/actions/UserActions'; 6 | 7 | 8 | const Header: React.FC = () => { 9 | 10 | 11 | const dispatch = useDispatch(); 12 | const { userInfo } = useSelector((state: RootStateOrAny) => state.userLogin); 13 | 14 | const handleLogout = () => { 15 | dispatch(logout()); 16 | } 17 | 18 | return ( 19 | 20 | 21 | 22 | 23 | Hotel Book 24 | 25 | 26 | 27 | 28 | 33 | 77 | 78 | 79 | 80 | ); 81 | }; 82 | 83 | export default Header; 84 | -------------------------------------------------------------------------------- /frontend/src/components/ListReviews.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ListGroup } from 'react-bootstrap'; 3 | import Rating from './Rating'; 4 | 5 | type TListReviews = { 6 | roomReviews: [] 7 | } 8 | 9 | const ListReviews: React.FC = ({ roomReviews }) => { 10 | return ( 11 | 12 | {roomReviews?.map((r: any) => 13 | 14 |

{r.name}

15 | 16 |

17 | {r.comment} 18 |

19 |
20 | )} 21 |
22 | ); 23 | }; 24 | 25 | export default ListReviews; 26 | -------------------------------------------------------------------------------- /frontend/src/components/Loader.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Spinner } from 'react-bootstrap'; 3 | 4 | const Loader = () => { 5 | return ( 6 | 7 | Loading... 8 | 9 | ); 10 | }; 11 | 12 | export default Loader; 13 | -------------------------------------------------------------------------------- /frontend/src/components/Message.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Alert } from "react-bootstrap"; 3 | 4 | interface IProps { 5 | variant: string, 6 | children: any 7 | }; 8 | 9 | const Message: React.FC = ({ variant, children }) => { 10 | return {children}; 11 | }; 12 | 13 | Message.defaultProps = { 14 | variant: "info" 15 | } 16 | 17 | export default Message; -------------------------------------------------------------------------------- /frontend/src/components/OnlyAdmin.tsx: -------------------------------------------------------------------------------- 1 | import { RootStateOrAny, useSelector } from 'react-redux'; 2 | import { Navigate, Outlet } from 'react-router-dom'; 3 | import Loader from './Loader'; 4 | 5 | const OnlyAdmin = () => { 6 | 7 | const { userInfo, loading } = useSelector((state: RootStateOrAny) => state.userLogin); 8 | 9 | if(loading) { 10 | return 11 | } 12 | 13 | return userInfo?.isAdmin ? : 14 | 15 | } 16 | 17 | export default OnlyAdmin; -------------------------------------------------------------------------------- /frontend/src/components/Paginate.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | type PaginateParams = { 4 | currentPage: number, 5 | setCurrentPage: React.Dispatch>, 6 | totalPosts: number, 7 | postPerPage: number 8 | } 9 | 10 | const Paginate: React.FC = ({ currentPage, setCurrentPage, totalPosts, postPerPage }) => { 11 | const totalPages = Math.ceil(totalPosts / postPerPage); 12 | 13 | let pages = []; 14 | 15 | for (let p = 1; p <= totalPages; p++) { 16 | pages.push(p); 17 | } 18 | 19 | return ( 20 |
    21 |
  • 22 | 25 |
  • 26 | {pages.map((page) => ( 27 |
  • setCurrentPage(page)} 31 | > 32 | 33 |
  • 34 | ))} 35 |
  • 36 | 39 |
  • 40 |
41 | ); 42 | }; 43 | 44 | export default Paginate; -------------------------------------------------------------------------------- /frontend/src/components/ProtectedRoute.tsx: -------------------------------------------------------------------------------- 1 | import { Navigate, Outlet } from 'react-router-dom'; 2 | import { useAuthStatus } from '../hooks/useAuthStatus'; 3 | import Loader from './Loader'; 4 | 5 | const ProtectedRoute = () => { 6 | 7 | const { loggedIn, checkingStatus } = useAuthStatus(); 8 | 9 | if(checkingStatus) { 10 | return 11 | } 12 | 13 | return loggedIn ? : 14 | 15 | } 16 | 17 | export default ProtectedRoute; -------------------------------------------------------------------------------- /frontend/src/components/Rating.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | type TReview = { 4 | reviews?: Number 5 | } 6 | 7 | const Rating: React.FC = ({ reviews }) => { 8 | return ( 9 |
10 | = 1 13 | ? `bi bi-star-fill` 14 | : Number(reviews) >= 0.5 15 | ? `bi bi-star-half` 16 | : `bi bi-star` 17 | } 18 | > 19 | = 2 22 | ? `bi bi-star-fill` 23 | : Number(reviews) >= 1.5 24 | ? `bi bi-star-half` 25 | : `bi bi-star` 26 | } 27 | > 28 | = 3 31 | ? `bi bi-star-fill` 32 | : Number(reviews) >= 2.5 33 | ? `bi bi-star-half` 34 | : `bi bi-star` 35 | } 36 | > 37 | = 4 40 | ? `bi bi-star-fill` 41 | : Number(reviews) >= 3.5 42 | ? `bi bi-star-half` 43 | : `bi bi-star` 44 | } 45 | > 46 | = 5 49 | ? `bi bi-star-fill` 50 | : Number(reviews) >= 4.5 51 | ? `bi bi-star-half` 52 | : `bi bi-star` 53 | } 54 | > 55 | ({reviews} Reviews) 56 |
57 | ); 58 | }; 59 | 60 | export default Rating; 61 | -------------------------------------------------------------------------------- /frontend/src/components/RoomCard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Card, Button } from 'react-bootstrap'; 3 | import { LinkContainer } from 'react-router-bootstrap'; 4 | import { Link } from 'react-router-dom'; 5 | import { IRoom } from '../interfaces/IRoom'; 6 | import Rating from './Rating'; 7 | 8 | type IRoomCard = Pick; 9 | 10 | const RoomCard: React.FC = (props: IRoomCard) => { 11 | 12 | const { _id, images, name, pricePerNight, ratings } = props; 13 | 14 | return ( 15 | 16 | 17 | 18 | 19 | {name} 20 | 21 | ${pricePerNight} / Per Night 22 | 23 | 24 | 25 | 26 | 27 | 28 | ); 29 | }; 30 | 31 | export default RoomCard; 32 | -------------------------------------------------------------------------------- /frontend/src/components/RoomFeatures.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ListGroup } from 'react-bootstrap'; 3 | 4 | type TRoomFeatures = { 5 | guestCapacity: Number, 6 | numOfBeds: Number, 7 | internet: boolean, 8 | airConditioned: boolean, 9 | petsAllowed: boolean, 10 | roomCleaning: boolean 11 | } 12 | 13 | type TRoomFeaturesProps = { 14 | room: TRoomFeatures 15 | } 16 | 17 | const RoomFeatures: React.FC = ({ room }) => { 18 | return ( 19 | <> 20 |

Features:

21 | 22 | 23 | 24 | {room.guestCapacity} Guests 25 | 26 | {room.numOfBeds} Beds 27 | 28 | {room.internet ? 29 | : 30 | 31 | } 32 | Internet 33 | 34 | 35 | {room.airConditioned ? 36 | : 37 | 38 | } 39 | Air Conditioned 40 | 41 | 42 | {room.petsAllowed ? 43 | : 44 | 45 | } 46 | Pets Allowed 47 | 48 | 49 | {room.roomCleaning ? 50 | : 51 | 52 | } 53 | Room Cleaning 54 | 55 | 56 | 57 | ); 58 | }; 59 | 60 | export default RoomFeatures; 61 | -------------------------------------------------------------------------------- /frontend/src/components/SearchRooms.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Col, Form, FormGroup, Row } from 'react-bootstrap'; 3 | 4 | type SearchRoomsParams = { 5 | keyword: string, 6 | setKeyword: React.Dispatch>, 7 | numOfBeds: number | string, 8 | setNumOfBeds: React.Dispatch>, 9 | roomType: string, 10 | setRoomType: React.Dispatch> 11 | } 12 | 13 | const SearchRooms: React.FC = 14 | ({ keyword, setKeyword, numOfBeds, setNumOfBeds, roomType, setRoomType }) => { 15 | 16 | return ( 17 |
18 | 19 | 20 | 21 | Search 22 | setKeyword(e.target.value)} 28 | /> 29 | 30 | 31 | 32 | 33 | Num of Beds 34 | setNumOfBeds(e.target.value)} 38 | aria-label="Default select example" 39 | > 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | Room Type 51 | setRoomType(e.target.value)} 55 | aria-label="Default select example" 56 | > 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 |
65 | ); 66 | }; 67 | 68 | export default SearchRooms; 69 | -------------------------------------------------------------------------------- /frontend/src/hooks/useAuthStatus.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { useSelector, RootStateOrAny } from 'react-redux'; 3 | 4 | export const useAuthStatus = () => { 5 | 6 | const [loggedIn, setLoggedIn] = useState(false) 7 | const [checkingStatus, setCheckingStatus] = useState(true) 8 | 9 | const { userInfo } = useSelector((state: RootStateOrAny) => state.userLogin); 10 | 11 | useEffect(() => { 12 | if (userInfo) { 13 | setLoggedIn(true) 14 | } else { 15 | setLoggedIn(false) 16 | } 17 | setCheckingStatus(false) 18 | }, [userInfo]) 19 | 20 | return { loggedIn, checkingStatus } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /frontend/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './bootstrap.min.css'; 4 | import 'bootstrap-icons/font/bootstrap-icons.css' 5 | import App from './App'; 6 | import { Provider } from 'react-redux'; 7 | import store from './redux/store'; 8 | 9 | ReactDOM.render( 10 | 11 | 12 | 13 | 14 | , 15 | document.getElementById('root') 16 | ); -------------------------------------------------------------------------------- /frontend/src/interfaces/IBooking.ts: -------------------------------------------------------------------------------- 1 | import { IRoom } from "./IRoom" 2 | 3 | export interface ICreateBooking { 4 | room: IRoom["_id"] | undefined, 5 | checkInDate: Date | undefined, 6 | checkOutDate: Date | undefined, 7 | amountPaid: number, 8 | paymentInfo: {} | null, 9 | daysOfStay: number, 10 | } 11 | 12 | export interface IBooking extends ICreateBooking { 13 | _id: string 14 | } 15 | 16 | export type TMyBookings = Pick -------------------------------------------------------------------------------- /frontend/src/interfaces/IRoom.ts: -------------------------------------------------------------------------------- 1 | export interface ICreateReview { 2 | rating: number, 3 | comment: string 4 | } 5 | 6 | interface IReviews extends ICreateReview { 7 | user: {}, 8 | } 9 | 10 | export type TImage = { 11 | _id?: string, 12 | image: string 13 | } 14 | 15 | export interface IRoom { 16 | _id: string, 17 | name: string 18 | description: string, 19 | images: TImage[], 20 | pricePerNight: Number, 21 | address: string, 22 | guestCapacity: Number, 23 | numOfBeds: Number, 24 | breakfast: Boolean, 25 | internet: Boolean, 26 | airConditioned: Boolean, 27 | petsAllowed: Boolean, 28 | roomCleaning: Boolean, 29 | ratings?: Number, 30 | numOfReviews?: Number, 31 | category: 'King' | 'Single' | 'Twins' | string, 32 | reviews?: IReviews[], 33 | createdAt: Date, 34 | } 35 | 36 | export type TCreateRoom = Pick 37 | -------------------------------------------------------------------------------- /frontend/src/interfaces/IUser.ts: -------------------------------------------------------------------------------- 1 | export interface IUserLogin { 2 | email: string, 3 | password: string 4 | } 5 | 6 | export interface IUserRegister extends IUserLogin { 7 | name: string, 8 | avatar?: string 9 | } 10 | 11 | export interface IUser extends IUserRegister { 12 | _id: string, 13 | isAdmin?: boolean 14 | } 15 | 16 | export interface IUpdatePassword { 17 | oldPassword: string, 18 | newPassword: string, 19 | confirmPassword?: string, 20 | errConfirmPassword?: string 21 | } -------------------------------------------------------------------------------- /frontend/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /frontend/src/redux/actions/BookingActions.tsx: -------------------------------------------------------------------------------- 1 | import { Dispatch } from "redux"; 2 | import axios from 'axios'; 3 | import * as actions from '../constants/BookingConstants'; 4 | import { IRoom } from "../../interfaces/IRoom"; 5 | import { IBooking, ICreateBooking } from "../../interfaces/IBooking"; 6 | 7 | export const checkRoomBooking = (id: IRoom['_id'], checkInDate: Date, checkOutDate: Date) => async (dispatch: Dispatch) => { 8 | 9 | try { 10 | dispatch({ type: actions.CHECK_ROOM_BOOKING_REQUEST }); 11 | 12 | const config = { 13 | headers: { 14 | "Content-Type": "application/JSON", 15 | } 16 | } 17 | 18 | await axios.post(`/api/bookings/check`, {id, checkInDate, checkOutDate}, config); 19 | 20 | dispatch({ type: actions.CHECK_ROOM_BOOKING_SUCCESS }); 21 | 22 | } catch (error: any) { 23 | dispatch({ 24 | type: actions.CHECK_ROOM_BOOKING_FAIL, 25 | payload: error.response && error.response.data.message ? 26 | error.response.data.message : error.message }); 27 | } 28 | 29 | } 30 | 31 | export const createBooking = (bookingData: ICreateBooking) => async (dispatch: Dispatch, getState: any) => { 32 | 33 | try { 34 | dispatch({ type: actions.CREATE_BOOKING_REQUEST }); 35 | 36 | const { userLogin: { userInfo } } = getState(); 37 | 38 | const config = { 39 | headers: { 40 | "Content-Type": "application/JSON", 41 | "Authorization": `Bearer ${userInfo.token}` 42 | } 43 | } 44 | 45 | await axios.post(`/api/bookings`, bookingData, config); 46 | 47 | dispatch({ type: actions.CREATE_BOOKING_SUCCESS }); 48 | 49 | } catch (error: any) { 50 | dispatch({ 51 | type: actions.CREATE_BOOKING_FAIL, 52 | payload: error.response && error.response.data.message ? 53 | error.response.data.message : error.message }); 54 | } 55 | 56 | } 57 | 58 | export const getBookedDates = (roomId: IRoom['_id']) => async (dispatch: Dispatch) => { 59 | 60 | try { 61 | dispatch({ type: actions.GET_BOOKED_DATES_REQUEST }); 62 | 63 | const { data } = await axios.get(`/api/bookings/dates/${roomId}`); 64 | 65 | dispatch({ type: actions.GET_BOOKED_DATES_SUCCESS, payload: data }); 66 | 67 | } catch (error: any) { 68 | dispatch({ 69 | type: actions.GET_BOOKED_DATES_FAIL, 70 | payload: error.response && error.response.data.message ? 71 | error.response.data.message : error.message }); 72 | } 73 | 74 | } 75 | 76 | export const getMyBookings = () => async (dispatch: Dispatch, getState: any) => { 77 | 78 | try { 79 | dispatch({ type: actions.GET_MY_BOOKINGS_REQUEST }); 80 | 81 | const { userLogin: { userInfo } } = getState(); 82 | 83 | const config = { 84 | headers: { 85 | "Content-Type": "application/JSON", 86 | "Authorization": `Bearer ${userInfo.token}` 87 | } 88 | }; 89 | 90 | const { data } = await axios.get(`/api/bookings/me`, config); 91 | 92 | dispatch({ type: actions.GET_MY_BOOKINGS_SUCCESS, payload: data }); 93 | 94 | } catch (error: any) { 95 | dispatch({ 96 | type: actions.GET_MY_BOOKINGS_FAIL, 97 | payload: error.response && error.response.data.message ? 98 | error.response.data.message : error.message }); 99 | } 100 | 101 | } 102 | 103 | export const getAllBookings = (currentPage: number) => async (dispatch: Dispatch, getState: any) => { 104 | 105 | try { 106 | dispatch({ type: actions.FETCH_BOOKINGS_REQUEST }); 107 | 108 | const { userLogin: { userInfo } } = getState(); 109 | 110 | const config = { 111 | headers: { 112 | "Content-Type": "application/JSON", 113 | "Authorization": `Bearer ${userInfo.token}` 114 | } 115 | }; 116 | 117 | const { data } = await axios.get(`/api/bookings/?pageNumber=${currentPage}`, config); 118 | 119 | dispatch({ type: actions.FETCH_BOOKINGS_SUCCESS, payload: data }); 120 | 121 | } catch (error: any) { 122 | dispatch({ 123 | type: actions.FETCH_BOOKINGS_FAIL, 124 | payload: error.response && error.response.data.message ? 125 | error.response.data.message : error.message }); 126 | } 127 | 128 | } 129 | 130 | export const deleteBooking = (bookingId: IBooking['_id']) => async (dispatch: Dispatch, getState: any) => { 131 | 132 | try { 133 | dispatch({ type: actions.DELETE_BOOKING_REQUEST }); 134 | 135 | const { userLogin: { userInfo } } = getState(); 136 | 137 | const config = { 138 | headers: { 139 | "Authorization": `Bearer ${userInfo.token}` 140 | } 141 | }; 142 | 143 | await axios.delete(`/api/bookings/${bookingId}`, config); 144 | 145 | dispatch({ type: actions.DELETE_BOOKING_SUCCESS }); 146 | 147 | } catch (error: any) { 148 | dispatch({ 149 | type: actions.DELETE_BOOKING_FAIL, 150 | payload: error.response && error.response.data.message ? 151 | error.response.data.message : error.message }); 152 | } 153 | 154 | } -------------------------------------------------------------------------------- /frontend/src/redux/actions/RoomActions.tsx: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { Dispatch } from 'redux'; 3 | import * as actions from '../constants/RoomConstants'; 4 | import { IRoom, ICreateReview, TCreateRoom } from '../../interfaces/IRoom'; 5 | 6 | export const fetchRooms = (keyword: string, numOfBeds: number | string, roomType: string, currentPage: number) => 7 | async (dispatch: Dispatch) => { 8 | try { 9 | dispatch({ type: actions.FETCH_ROOMS_REQUEST }); 10 | 11 | const { data } = 12 | await axios.get(`/api/rooms/?keyword=${keyword}&numOfBeds=${numOfBeds}&roomType=${roomType}&pageNumber=${currentPage}`); 13 | 14 | dispatch({ type: actions.FETCH_ROOMS_SUCCESS, payload: data }); 15 | 16 | } catch (error: any) { 17 | dispatch({ 18 | type: actions.FETCH_ROOMS_FAIL, 19 | payload: error.response && error.response.data.message ? 20 | error.response.data.message : error.message }); 21 | } 22 | } 23 | 24 | export const getRoomDetails = (id: IRoom['_id']) => async (dispatch: Dispatch) => { 25 | 26 | try { 27 | dispatch({ type: actions.ROOM_DETAILS_REQUEST }); 28 | 29 | const { data } = await axios.get(`/api/rooms/${id}`); 30 | dispatch({ type: actions.ROOM_DETAILS_SUCCESS, payload: data }); 31 | 32 | } catch (error: any) { 33 | dispatch({ 34 | type: actions.ROOM_DETAILS_FAIL, 35 | payload: error.response && error.response.data.message ? 36 | error.response.data.message : error.message }); 37 | } 38 | 39 | } 40 | 41 | export const createRoomReview = (id: IRoom['_id'], review: ICreateReview) => async (dispatch: Dispatch, getState: any) => { 42 | 43 | try { 44 | dispatch({ type: actions.ROOM_CREATE_REVIEW_REQUEST }); 45 | 46 | const { userLogin: { userInfo } } = getState(); 47 | 48 | const config = { 49 | headers: { 50 | "Content-Type": "application/JSON", 51 | "Authorization": `Bearer ${userInfo.token}` 52 | } 53 | } 54 | 55 | await axios.post(`/api/rooms/${id}/reviews`, review, config); 56 | dispatch({ type: actions.ROOM_CREATE_REVIEW_SUCCESS }); 57 | 58 | } catch (error: any) { 59 | dispatch({ 60 | type: actions.ROOM_CREATE_REVIEW_FAIL, 61 | payload: error.response && error.response.data.message ? 62 | error.response.data.message : error.message }); 63 | } 64 | 65 | } 66 | 67 | export const createRoom = (roomData: TCreateRoom) => async (dispatch: Dispatch, getState: any) => { 68 | 69 | try { 70 | dispatch({ type: actions.CREATE_ROOM_REQUEST }); 71 | 72 | const { userLogin: { userInfo } } = getState(); 73 | 74 | const config = { 75 | headers: { 76 | "Content-Type": "application/JSON", 77 | "Authorization": `Bearer ${userInfo.token}` 78 | } 79 | } 80 | 81 | await axios.post(`/api/rooms`, roomData, config); 82 | dispatch({ type: actions.CREATE_ROOM_SUCCESS }); 83 | 84 | } catch (error: any) { 85 | dispatch({ 86 | type: actions.CREATE_ROOM_FAIL, 87 | payload: error.response && error.response.data.message ? 88 | error.response.data.message : error.message }); 89 | } 90 | 91 | } 92 | 93 | export const updateRoom = (roomId: IRoom['_id'], roomData: TCreateRoom) => async (dispatch: Dispatch, getState: any) => { 94 | 95 | try { 96 | 97 | dispatch({ type: actions.UPDATE_ROOM_REQUEST }); 98 | 99 | const { userLogin: { userInfo } } = getState(); 100 | 101 | const config = { 102 | headers: { 103 | 'Content-Type': 'application/JSON', 104 | 'Authorization': `Bearer ${userInfo.token}` 105 | } 106 | }; 107 | 108 | await axios.put(`/api/rooms/${roomId}`, roomData, config); 109 | dispatch({ type: actions.UPDATE_ROOM_SUCCESS }); 110 | 111 | } catch (error: any) { 112 | dispatch({ 113 | type: actions.UPDATE_ROOM_FAIL, 114 | payload: error.response && error.response.data.message ? 115 | error.response.data.message : error.message }); 116 | } 117 | 118 | } 119 | 120 | export const deleteRoom = (roomId: IRoom['_id']) => async (dispatch: Dispatch, getState: any) => { 121 | 122 | try { 123 | dispatch({ type: actions.DELETE_ROOM_REQUEST }); 124 | 125 | const { userLogin: { userInfo } } = getState(); 126 | 127 | const config = { 128 | headers: { 129 | "Authorization": `Bearer ${userInfo.token}` 130 | } 131 | } 132 | 133 | await axios.delete(`/api/rooms/${roomId}`, config); 134 | dispatch({ type: actions.DELETE_ROOM_SUCCESS }); 135 | 136 | } catch (error: any) { 137 | dispatch({ 138 | type: actions.DELETE_ROOM_FAIL, 139 | payload: error.response && error.response.data.message ? 140 | error.response.data.message : error.message }); 141 | } 142 | 143 | } -------------------------------------------------------------------------------- /frontend/src/redux/actions/UserActions.tsx: -------------------------------------------------------------------------------- 1 | import { Dispatch } from 'redux'; 2 | import * as actions from '../constants/UserConstants'; 3 | import axios from 'axios'; 4 | import { IUpdatePassword, IUser, IUserLogin, IUserRegister } from '../../interfaces/IUser'; 5 | 6 | export const login = (user: IUserLogin) => async (dispatch: Dispatch) => { 7 | 8 | try { 9 | 10 | dispatch({ type: actions.USER_LOGIN_REQUEST }); 11 | 12 | const config = { 13 | headers: { 14 | "Content-Type": "application/json" 15 | } 16 | }; 17 | 18 | const { data } = await axios.post("/api/users/login", user, config); 19 | 20 | dispatch({ type: actions.USER_LOGIN_SUCCESS, payload: data }); 21 | 22 | localStorage.setItem("userInfo", JSON.stringify(data)); 23 | 24 | } catch (error: any) { 25 | dispatch({ 26 | type: actions.USER_LOGIN_FAIL, 27 | payload: 28 | error.response && error.response.data.message 29 | ? error.response.data.message 30 | : error.message, 31 | }); 32 | } 33 | 34 | } 35 | 36 | export const logout = () => (dispatch: Dispatch) => { 37 | 38 | dispatch({ type: actions.USER_LOGOUT }); 39 | localStorage.removeItem("userInfo"); 40 | 41 | } 42 | 43 | export const register = (user: IUserRegister) => async (dispatch: Dispatch) => { 44 | 45 | try { 46 | 47 | dispatch({ type: actions.USER_REGISTER_REQUEST }); 48 | 49 | const config = { 50 | headers: { 51 | "Content-Type": "application/json" 52 | } 53 | }; 54 | 55 | const { data } = await axios.post("/api/users/register", user, config); 56 | 57 | dispatch({ type: actions.USER_REGISTER_SUCCESS }); 58 | dispatch({ type: actions.USER_LOGIN_SUCCESS, payload: data }); 59 | 60 | localStorage.setItem("userInfo", JSON.stringify(data)); 61 | 62 | } catch (error: any) { 63 | dispatch({ 64 | type: actions.USER_REGISTER_FAIL, 65 | payload: 66 | error.response && error.response.data.message 67 | ? error.response.data.message 68 | : error.message, 69 | }); 70 | } 71 | 72 | } 73 | 74 | export const updateProfile = (user: {}) => async (dispatch: Dispatch, getState: any) => { 75 | 76 | try { 77 | 78 | dispatch({ type: actions.UPDATE_PROFILE_REQUEST }); 79 | 80 | const { userLogin: { userInfo } } = getState(); 81 | 82 | const config = { 83 | headers: { 84 | "Content-Type": "application/json", 85 | "Authorization": `Bearer ${userInfo.token}` 86 | } 87 | }; 88 | 89 | const { data } = await axios.put("/api/users/update", user, config); 90 | 91 | dispatch({ type: actions.UPDATE_PROFILE_SUCCESS }); 92 | dispatch({ type: actions.USER_LOGIN_SUCCESS, payload: data }); 93 | localStorage.setItem("userInfo", JSON.stringify(data)); 94 | 95 | } catch (error: any) { 96 | const message = 97 | error.response && error.response.data.message 98 | ? error.response.data.message 99 | : error.message; 100 | if (message === "no token, no auth") { 101 | dispatch(logout()); 102 | } 103 | dispatch({ 104 | type: actions.UPDATE_PROFILE_FAIL, 105 | payload: message 106 | }); 107 | } 108 | 109 | } 110 | 111 | export const updatePassword = (user: IUpdatePassword) => async (dispatch: Dispatch, getState: any) => { 112 | 113 | try { 114 | 115 | dispatch({ type: actions.UPDATE_PASSWORD_REQUEST }); 116 | 117 | const { userLogin: { userInfo } } = getState(); 118 | 119 | const config = { 120 | headers: { 121 | "Content-Type": "application/json", 122 | "Authorization": `Bearer ${userInfo.token}` 123 | } 124 | }; 125 | 126 | const { data } = await axios.put("/api/users/update/password", user, config); 127 | 128 | dispatch({ type: actions.UPDATE_PASSWORD_SUCCESS }); 129 | dispatch({ type: actions.USER_LOGIN_SUCCESS, payload: data }); 130 | localStorage.setItem("userInfo", JSON.stringify(data)); 131 | 132 | } catch (error: any) { 133 | const message = 134 | error.response && error.response.data.message 135 | ? error.response.data.message 136 | : error.message; 137 | if (message === "no token, no auth") { 138 | dispatch(logout()); 139 | } 140 | dispatch({ 141 | type: actions.UPDATE_PASSWORD_FAIL, 142 | payload: message 143 | }); 144 | } 145 | 146 | } 147 | 148 | 149 | export const fetchUsers = (currentPage: number) => async (dispatch: Dispatch, getState: any) => { 150 | 151 | try { 152 | 153 | dispatch({ type: actions.FETCH_USERS_REQUEST }); 154 | 155 | const { userLogin: { userInfo } } = getState(); 156 | 157 | const config = { 158 | headers: { 159 | "Authorization": `Bearer ${userInfo.token}` 160 | } 161 | }; 162 | 163 | const { data } = await axios.get(`/api/users/?pageNumber=${currentPage}`, config); 164 | 165 | dispatch({ type: actions.FETCH_USERS_SUCCESS, payload: data }); 166 | 167 | } catch (error: any) { 168 | const message = 169 | error.response && error.response.data.message 170 | ? error.response.data.message 171 | : error.message; 172 | if (message === "no token, no auth") { 173 | dispatch(logout()); 174 | } 175 | dispatch({ 176 | type: actions.FETCH_USERS_FAIL, 177 | payload: message 178 | }); 179 | } 180 | 181 | } 182 | 183 | export const deleteUser = (userId: IUser['_id']) => async (dispatch: Dispatch, getState: any) => { 184 | 185 | try { 186 | 187 | dispatch({ type: actions.DELETE_USER_REQUEST }); 188 | 189 | const { userLogin: { userInfo } } = getState(); 190 | 191 | const config = { 192 | headers: { 193 | "Authorization": `Bearer ${userInfo.token}` 194 | } 195 | }; 196 | 197 | await axios.delete(`/api/users/${userId}`, config); 198 | 199 | dispatch({ type: actions.DELETE_USER_SUCCESS }); 200 | 201 | } catch (error: any) { 202 | const message = 203 | error.response && error.response.data.message 204 | ? error.response.data.message 205 | : error.message; 206 | if (message === "no token, no auth") { 207 | dispatch(logout()); 208 | } 209 | dispatch({ 210 | type: actions.DELETE_USER_FAIL, 211 | payload: message 212 | }); 213 | } 214 | 215 | } 216 | 217 | export const detailsUser = (userId: IUser['_id']) => async (dispatch: Dispatch, getState: any) => { 218 | 219 | try { 220 | 221 | dispatch({ type: actions.GET_USER_REQUEST }); 222 | 223 | const { userLogin: { userInfo } } = getState(); 224 | 225 | const config = { 226 | headers: { 227 | "Authorization": `Bearer ${userInfo.token}` 228 | } 229 | }; 230 | 231 | const { data } = await axios.get(`/api/users/${userId}`, config); 232 | 233 | dispatch({ type: actions.GET_USER_SUCCESS, payload: data }); 234 | 235 | } catch (error: any) { 236 | const message = 237 | error.response && error.response.data.message 238 | ? error.response.data.message 239 | : error.message; 240 | if (message === "no token, no auth") { 241 | dispatch(logout()); 242 | } 243 | dispatch({ 244 | type: actions.GET_USER_FAIL, 245 | payload: message 246 | }); 247 | } 248 | 249 | } 250 | 251 | export const updateUser = (userId: IUser['_id'], userData: {}) => async (dispatch: Dispatch, getState: any) => { 252 | 253 | try { 254 | 255 | dispatch({ type: actions.UPDATE_USER_REQUEST }); 256 | 257 | const { userLogin: { userInfo } } = getState(); 258 | 259 | const config = { 260 | headers: { 261 | "Authorization": `Bearer ${userInfo.token}` 262 | } 263 | }; 264 | 265 | await axios.put(`/api/users/${userId}`, userData, config); 266 | 267 | dispatch({ type: actions.UPDATE_USER_SUCCESS }); 268 | 269 | } catch (error: any) { 270 | const message = 271 | error.response && error.response.data.message 272 | ? error.response.data.message 273 | : error.message; 274 | if (message === "no token, no auth") { 275 | dispatch(logout()); 276 | } 277 | dispatch({ 278 | type: actions.UPDATE_USER_FAIL, 279 | payload: message 280 | }); 281 | } 282 | 283 | } -------------------------------------------------------------------------------- /frontend/src/redux/constants/BookingConstants.tsx: -------------------------------------------------------------------------------- 1 | export const CHECK_ROOM_BOOKING_REQUEST = "CHECK_ROOM_BOOKING_REQUEST"; 2 | export const CHECK_ROOM_BOOKING_SUCCESS = "CHECK_ROOM_BOOKING_SUCCESS"; 3 | export const CHECK_ROOM_BOOKING_FAIL = "CHECK_ROOM_BOOKING_FAIL"; 4 | export const CHECK_ROOM_BOOKING_RESET = "CHECK_ROOM_BOOKING_RESET"; 5 | 6 | export const CREATE_BOOKING_REQUEST = "CREATE_BOOKING_REQUEST"; 7 | export const CREATE_BOOKING_SUCCESS = "CREATE_BOOKING_SUCCESS"; 8 | export const CREATE_BOOKING_FAIL = "CREATE_BOOKING_FAIL"; 9 | export const CREATE_BOOKING_RESET = "CREATE_BOOKING_RESET"; 10 | 11 | export const GET_BOOKED_DATES_REQUEST = "GET_BOOKED_DATES_REQUEST"; 12 | export const GET_BOOKED_DATES_SUCCESS = "GET_BOOKED_DATES_SUCCESS"; 13 | export const GET_BOOKED_DATES_FAIL = "GET_BOOKED_DATES_FAIL"; 14 | 15 | export const GET_MY_BOOKINGS_REQUEST = "GET_MY_BOOKINGS_REQUEST"; 16 | export const GET_MY_BOOKINGS_SUCCESS = "GET_MY_BOOKINGS_SUCCESS"; 17 | export const GET_MY_BOOKINGS_FAIL = "GET_MY_BOOKINGS_FAIL"; 18 | 19 | export const FETCH_BOOKINGS_REQUEST = "FETCH_BOOKINGS_REQUEST"; 20 | export const FETCH_BOOKINGS_SUCCESS = "FETCH_BOOKINGS_SUCCESS"; 21 | export const FETCH_BOOKINGS_FAIL = "FETCH_BOOKINGS_FAIL"; 22 | 23 | export const DELETE_BOOKING_REQUEST = "DELETE_BOOKING_REQUEST"; 24 | export const DELETE_BOOKING_SUCCESS = "DELETE_BOOKING_SUCCESS"; 25 | export const DELETE_BOOKING_FAIL = "DELETE_BOOKING_FAIL"; -------------------------------------------------------------------------------- /frontend/src/redux/constants/RoomConstants.tsx: -------------------------------------------------------------------------------- 1 | export const FETCH_ROOMS_REQUEST = "FETCH_ROOMS_REQUEST"; 2 | export const FETCH_ROOMS_SUCCESS = "FETCH_ROOMS_SUCCESS"; 3 | export const FETCH_ROOMS_FAIL = "FETCH_ROOMS_FAIL"; 4 | 5 | export const ROOM_DETAILS_REQUEST = "ROOM_DETAILS_REQUEST"; 6 | export const ROOM_DETAILS_SUCCESS = "ROOM_DETAILS_SUCCESS"; 7 | export const ROOM_DETAILS_FAIL = "ROOM_DETAILS_FAIL"; 8 | 9 | export const ROOM_CREATE_REVIEW_REQUEST = "ROOM_CREATE_REVIEW_REQUEST"; 10 | export const ROOM_CREATE_REVIEW_SUCCESS = "ROOM_CREATE_REVIEW_SUCCESS"; 11 | export const ROOM_CREATE_REVIEW_FAIL = "ROOM_CREATE_REVIEW_FAIL"; 12 | 13 | export const CREATE_ROOM_REQUEST = "CREATE_ROOM_REQUEST"; 14 | export const CREATE_ROOM_SUCCESS = "CREATE_ROOM_SUCCESS"; 15 | export const CREATE_ROOM_FAIL = "CREATE_ROOM_FAIL"; 16 | export const CREATE_ROOM_RESET = "CREATE_ROOM_RESET"; 17 | 18 | export const UPDATE_ROOM_REQUEST = "UPDATE_ROOM_REQUEST"; 19 | export const UPDATE_ROOM_SUCCESS = "UPDATE_ROOM_SUCCESS"; 20 | export const UPDATE_ROOM_FAIL = "UPDATE_ROOM_FAIL"; 21 | export const UPDATE_ROOM_RESET = "UPDATE_ROOM_RESET"; 22 | 23 | export const DELETE_ROOM_REQUEST = "DELETE_ROOM_REQUEST"; 24 | export const DELETE_ROOM_SUCCESS = "DELETE_ROOM_SUCCESS"; 25 | export const DELETE_ROOM_FAIL = "DELETE_ROOM_FAIL"; 26 | export const DELETE_ROOM_RESET = "DELETE_ROOM_RESET"; 27 | -------------------------------------------------------------------------------- /frontend/src/redux/constants/UserConstants.tsx: -------------------------------------------------------------------------------- 1 | export const USER_LOGIN_REQUEST = "USER_LOGIN_REQUEST"; 2 | export const USER_LOGIN_SUCCESS = "USER_LOGIN_SUCCESS"; 3 | export const USER_LOGIN_FAIL = "USER_LOGIN_FAIL"; 4 | export const USER_LOGOUT = "USER_LOGOUT"; 5 | 6 | export const USER_REGISTER_REQUEST = "USER_REGISTER_REQUEST"; 7 | export const USER_REGISTER_SUCCESS = "USER_REGISTER_SUCCESS"; 8 | export const USER_REGISTER_FAIL = "USER_REGISTER_FAIL"; 9 | 10 | export const UPDATE_PROFILE_REQUEST = "UPDATE_PROFILE_REQUEST" 11 | export const UPDATE_PROFILE_SUCCESS = "UPDATE_PROFILE_SUCCESS" 12 | export const UPDATE_PROFILE_FAIL = "UPDATE_PROFILE_FAIL" 13 | 14 | export const UPDATE_PASSWORD_REQUEST = "UPDATE_PASSWORD_REQUEST" 15 | export const UPDATE_PASSWORD_SUCCESS = "UPDATE_PASSWORD_SUCCESS" 16 | export const UPDATE_PASSWORD_FAIL = "UPDATE_PASSWORD_FAIL" 17 | 18 | export const FETCH_USERS_REQUEST = "FETCH_USERS_REQUEST"; 19 | export const FETCH_USERS_SUCCESS = "FETCH_USERS_SUCCESS"; 20 | export const FETCH_USERS_FAIL = "FETCH_USERS_FAIL"; 21 | 22 | export const GET_USER_REQUEST = "GET_USER_REQUEST"; 23 | export const GET_USER_SUCCESS = "GET_USER_SUCCESS"; 24 | export const GET_USER_FAIL = "GET_USER_FAIL"; 25 | 26 | export const UPDATE_USER_REQUEST = "UPDATE_USER_REQUEST"; 27 | export const UPDATE_USER_SUCCESS = "UPDATE_USER_SUCCESS"; 28 | export const UPDATE_USER_FAIL = "UPDATE_USER_FAIL"; 29 | export const UPDATE_USER_RESET = "UPDATE_USER_RESET"; 30 | 31 | export const DELETE_USER_REQUEST = "DELETE_USER_REQUEST"; 32 | export const DELETE_USER_SUCCESS = "DELETE_USER_SUCCESS"; 33 | export const DELETE_USER_FAIL = "DELETE_USER_FAIL"; -------------------------------------------------------------------------------- /frontend/src/redux/reducers/BookingReducers.tsx: -------------------------------------------------------------------------------- 1 | import * as actions from '../constants/BookingConstants'; 2 | import { AnyAction } from 'redux' 3 | 4 | export const roomBookingCheckReducer = (state = {}, action: AnyAction) => { 5 | 6 | switch (action.type) { 7 | case actions.CHECK_ROOM_BOOKING_REQUEST: 8 | return { 9 | loading: true, 10 | }; 11 | case actions.CHECK_ROOM_BOOKING_SUCCESS: 12 | return { 13 | loading: false, 14 | success: true 15 | }; 16 | case actions.CHECK_ROOM_BOOKING_FAIL: 17 | return { 18 | loading: false, 19 | error: action.payload 20 | }; 21 | case actions.CHECK_ROOM_BOOKING_RESET: 22 | return {} 23 | default: 24 | return state; 25 | } 26 | 27 | } 28 | 29 | export const bookingCreateReducer = (state = {}, action: AnyAction) => { 30 | 31 | switch (action.type) { 32 | case actions.CREATE_BOOKING_REQUEST: 33 | return { 34 | loading: true, 35 | }; 36 | case actions.CREATE_BOOKING_SUCCESS: 37 | return { 38 | loading: false, 39 | success: true, 40 | }; 41 | case actions.CREATE_BOOKING_FAIL: 42 | return { 43 | loading: false, 44 | error: action.payload 45 | }; 46 | case actions.CREATE_BOOKING_RESET: 47 | return {} 48 | default: 49 | return state; 50 | } 51 | 52 | } 53 | 54 | export const bookedDatesReducer = (state = {}, action: AnyAction) => { 55 | 56 | switch (action.type) { 57 | case actions.GET_BOOKED_DATES_REQUEST: 58 | return { 59 | loading: true, 60 | }; 61 | case actions.GET_BOOKED_DATES_SUCCESS: 62 | return { 63 | loading: false, 64 | bookedDates: action.payload 65 | }; 66 | case actions.GET_BOOKED_DATES_FAIL: 67 | return { 68 | loading: false, 69 | error: action.payload 70 | }; 71 | default: 72 | return state; 73 | } 74 | 75 | } 76 | 77 | export const BookingsMyReducer = (state = {}, action: AnyAction) => { 78 | 79 | switch (action.type) { 80 | case actions.GET_MY_BOOKINGS_REQUEST: 81 | return { 82 | loading: true, 83 | }; 84 | case actions.GET_MY_BOOKINGS_SUCCESS: 85 | return { 86 | loading: false, 87 | myBookings: action.payload 88 | }; 89 | case actions.GET_MY_BOOKINGS_FAIL: 90 | return { 91 | loading: false, 92 | error: action.payload 93 | }; 94 | default: 95 | return state; 96 | } 97 | 98 | } 99 | 100 | export const bookingsFetchReducer = (state = {}, action: AnyAction) => { 101 | 102 | switch (action.type) { 103 | case actions.FETCH_BOOKINGS_REQUEST: 104 | return { 105 | loading: true, 106 | }; 107 | case actions.FETCH_BOOKINGS_SUCCESS: 108 | return { 109 | loading: false, 110 | bookings: action.payload.bookings, 111 | page: action.payload.page, 112 | pages: action.payload.pages, 113 | count: action.payload.count 114 | }; 115 | case actions.FETCH_BOOKINGS_FAIL: 116 | return { 117 | loading: false, 118 | error: action.payload 119 | }; 120 | default: 121 | return state; 122 | } 123 | 124 | } 125 | 126 | export const bookingDeleteReducer = (state = {}, action: AnyAction) => { 127 | 128 | switch (action.type) { 129 | case actions.DELETE_BOOKING_REQUEST: 130 | return { 131 | loading: true, 132 | }; 133 | case actions.DELETE_BOOKING_SUCCESS: 134 | return { 135 | loading: false, 136 | success: true 137 | }; 138 | case actions.DELETE_BOOKING_FAIL: 139 | return { 140 | loading: false, 141 | error: action.payload 142 | }; 143 | default: 144 | return state; 145 | } 146 | 147 | } -------------------------------------------------------------------------------- /frontend/src/redux/reducers/RoomReducers.tsx: -------------------------------------------------------------------------------- 1 | import * as actions from '../constants/RoomConstants'; 2 | import { AnyAction } from 'redux' 3 | import { IRoom } from '../../interfaces/IRoom'; 4 | 5 | const initialState: { rooms: IRoom[] } = { 6 | rooms: [], 7 | }; 8 | 9 | export const roomsFetchReducer = (state = initialState, action: AnyAction) => { 10 | switch (action.type) { 11 | case actions.FETCH_ROOMS_REQUEST: 12 | return { 13 | loading: true 14 | }; 15 | case actions.FETCH_ROOMS_SUCCESS: 16 | return { 17 | loading: false, 18 | rooms: action.payload.rooms, 19 | count: action.payload.count 20 | }; 21 | case actions.FETCH_ROOMS_FAIL: 22 | return { 23 | loading: false, 24 | error: action.payload 25 | } 26 | default: 27 | return state; 28 | } 29 | } 30 | 31 | export const roomDetailsReducer = (state = { room: {} }, action: AnyAction) => { 32 | switch (action.type) { 33 | case actions.ROOM_DETAILS_REQUEST: 34 | return { 35 | loading: true 36 | }; 37 | case actions.ROOM_DETAILS_SUCCESS: 38 | return { 39 | loading: false, 40 | room: action.payload 41 | }; 42 | case actions.ROOM_DETAILS_FAIL: 43 | return { 44 | loading: false, 45 | error: action.payload 46 | } 47 | default: 48 | return state; 49 | } 50 | } 51 | 52 | export const roomCreateReviewReducer = (state = {}, action: AnyAction) => { 53 | switch (action.type) { 54 | case actions.ROOM_CREATE_REVIEW_REQUEST: 55 | return { 56 | loading: true 57 | }; 58 | case actions.ROOM_CREATE_REVIEW_SUCCESS: 59 | return { 60 | loading: false, 61 | success: true 62 | }; 63 | case actions.ROOM_CREATE_REVIEW_FAIL: 64 | return { 65 | loading: false, 66 | error: action.payload 67 | } 68 | default: 69 | return state; 70 | } 71 | } 72 | 73 | export const roomCreateReducer = (state = {}, action: AnyAction) => { 74 | switch (action.type) { 75 | case actions.CREATE_ROOM_REQUEST: 76 | return { 77 | loading: true 78 | }; 79 | case actions.CREATE_ROOM_SUCCESS: 80 | return { 81 | loading: false, 82 | success: true 83 | }; 84 | case actions.CREATE_ROOM_FAIL: 85 | return { 86 | loading: false, 87 | error: action.payload 88 | } 89 | case actions.CREATE_ROOM_RESET: 90 | return {} 91 | default: 92 | return state; 93 | } 94 | } 95 | 96 | export const roomUpdateReducer = (state = {}, action: AnyAction) => { 97 | switch (action.type) { 98 | case actions.UPDATE_ROOM_REQUEST: 99 | return { 100 | loading: true 101 | }; 102 | case actions.UPDATE_ROOM_SUCCESS: 103 | return { 104 | loading: false, 105 | success: true 106 | }; 107 | case actions.UPDATE_ROOM_FAIL: 108 | return { 109 | loading: false, 110 | error: action.payload 111 | } 112 | case actions.UPDATE_ROOM_RESET: 113 | return {} 114 | default: 115 | return state; 116 | } 117 | } 118 | 119 | export const roomDeleteReducer = (state = {}, action: AnyAction) => { 120 | switch (action.type) { 121 | case actions.DELETE_ROOM_REQUEST: 122 | return { 123 | loading: true 124 | }; 125 | case actions.DELETE_ROOM_SUCCESS: 126 | return { 127 | loading: false, 128 | success: true 129 | }; 130 | case actions.DELETE_ROOM_FAIL: 131 | return { 132 | loading: false, 133 | error: action.payload 134 | } 135 | case actions.DELETE_ROOM_RESET: 136 | return {} 137 | default: 138 | return state; 139 | } 140 | } -------------------------------------------------------------------------------- /frontend/src/redux/reducers/UserReducers.tsx: -------------------------------------------------------------------------------- 1 | import * as actions from '../constants/UserConstants'; 2 | import { AnyAction } from 'redux' 3 | 4 | export const userLoginReducer = (state = {}, action: AnyAction) => { 5 | 6 | switch (action.type) { 7 | case actions.USER_LOGIN_REQUEST: 8 | return { 9 | loading: true, 10 | }; 11 | case actions.USER_LOGIN_SUCCESS: 12 | return { 13 | loading: false, 14 | success: true, 15 | userInfo: action.payload 16 | }; 17 | case actions.USER_LOGIN_FAIL: 18 | return { 19 | loading: false, 20 | error: action.payload 21 | }; 22 | case actions.USER_LOGOUT: 23 | return { 24 | loading: false, 25 | userInfo: null 26 | }; 27 | default: 28 | return state; 29 | } 30 | 31 | } 32 | 33 | export const userRegisterReducer = (state = {}, action: any) => { 34 | 35 | switch (action.type) { 36 | case actions.USER_REGISTER_REQUEST: 37 | return { 38 | loading: true 39 | }; 40 | case actions.USER_LOGIN_SUCCESS: 41 | return { 42 | loading: false, 43 | success: true, 44 | }; 45 | case actions.USER_REGISTER_FAIL: 46 | return { 47 | loading: false, 48 | error: action.payload 49 | }; 50 | default: 51 | return state; 52 | } 53 | 54 | } 55 | 56 | export const profileUpdateReducer = (state = {}, action: any) => { 57 | 58 | switch (action.type) { 59 | case actions.UPDATE_PROFILE_REQUEST: 60 | return { 61 | loading: true 62 | }; 63 | case actions.UPDATE_PROFILE_SUCCESS: 64 | return { 65 | loading: false, 66 | success: true, 67 | }; 68 | case actions.UPDATE_PROFILE_FAIL: 69 | return { 70 | loading: false, 71 | error: action.payload 72 | }; 73 | default: 74 | return state; 75 | } 76 | 77 | } 78 | 79 | export const passwordUpdateReducer = (state = {}, action: any) => { 80 | 81 | switch (action.type) { 82 | case actions.UPDATE_PASSWORD_REQUEST: 83 | return { 84 | loading: true 85 | }; 86 | case actions.UPDATE_PASSWORD_SUCCESS: 87 | return { 88 | loading: false, 89 | success: true, 90 | }; 91 | case actions.UPDATE_PASSWORD_FAIL: 92 | return { 93 | loading: false, 94 | error: action.payload 95 | }; 96 | default: 97 | return state; 98 | } 99 | 100 | } 101 | 102 | export const usersFetchReducer = (state = {}, action: any) => { 103 | 104 | switch (action.type) { 105 | case actions.FETCH_USERS_REQUEST: 106 | return { 107 | loading: true 108 | }; 109 | case actions.FETCH_USERS_SUCCESS: 110 | return { 111 | loading: false, 112 | users: action.payload.users, 113 | page: action.payload.page, 114 | pages: action.payload.pages, 115 | count: action.payload.count, 116 | }; 117 | case actions.FETCH_USERS_FAIL: 118 | return { 119 | loading: false, 120 | error: action.payload 121 | }; 122 | default: 123 | return state; 124 | } 125 | 126 | } 127 | 128 | export const userDetailsReducer = (state = {}, action: any) => { 129 | 130 | switch (action.type) { 131 | case actions.GET_USER_REQUEST: 132 | return { 133 | loading: true 134 | }; 135 | case actions.GET_USER_SUCCESS: 136 | return { 137 | loading: false, 138 | user: action.payload 139 | }; 140 | case actions.GET_USER_FAIL: 141 | return { 142 | loading: false, 143 | error: action.payload 144 | }; 145 | default: 146 | return state; 147 | } 148 | 149 | } 150 | 151 | export const userUpdateReducer = (state = {}, action: any) => { 152 | 153 | switch (action.type) { 154 | case actions.UPDATE_USER_REQUEST: 155 | return { 156 | loading: true 157 | }; 158 | case actions.UPDATE_USER_SUCCESS: 159 | return { 160 | loading: false, 161 | success: true 162 | }; 163 | case actions.UPDATE_USER_FAIL: 164 | return { 165 | loading: false, 166 | error: action.payload 167 | }; 168 | case actions.UPDATE_USER_RESET: 169 | return {} 170 | default: 171 | return state; 172 | } 173 | 174 | } 175 | 176 | export const userDeleteReducer = (state = {}, action: any) => { 177 | 178 | switch (action.type) { 179 | case actions.DELETE_USER_REQUEST: 180 | return { 181 | loading: true 182 | }; 183 | case actions.DELETE_USER_SUCCESS: 184 | return { 185 | loading: false, 186 | success: true, 187 | }; 188 | case actions.DELETE_USER_FAIL: 189 | return { 190 | loading: false, 191 | error: action.payload 192 | }; 193 | default: 194 | return state; 195 | } 196 | 197 | } 198 | -------------------------------------------------------------------------------- /frontend/src/redux/store.tsx: -------------------------------------------------------------------------------- 1 | import { createStore, compose, applyMiddleware, combineReducers } from "redux"; 2 | import thunk from "redux-thunk"; 3 | import { usersFetchReducer, userDetailsReducer, userUpdateReducer, userDeleteReducer, userLoginReducer, userRegisterReducer, profileUpdateReducer, passwordUpdateReducer } from './reducers/UserReducers'; 4 | import {roomsFetchReducer, roomDetailsReducer, roomCreateReviewReducer, roomCreateReducer, roomUpdateReducer, roomDeleteReducer } from './reducers/RoomReducers'; 5 | import { bookingsFetchReducer, bookingDeleteReducer, roomBookingCheckReducer, bookingCreateReducer, bookedDatesReducer, BookingsMyReducer } from './reducers/BookingReducers'; 6 | 7 | const composeEnhancer = (window && (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) || compose; 8 | 9 | const rootReducers = combineReducers({ 10 | userLogin: userLoginReducer, 11 | userRegister: userRegisterReducer, 12 | profileUpdate: profileUpdateReducer, 13 | passwordUpdate: passwordUpdateReducer, 14 | usersFetch: usersFetchReducer, 15 | userDetails: userDetailsReducer, 16 | userUpdate: userUpdateReducer, 17 | userDelete: userDeleteReducer, 18 | roomsFetch: roomsFetchReducer, 19 | roomDetails: roomDetailsReducer, 20 | roomCreateReview: roomCreateReviewReducer, 21 | roomCreate: roomCreateReducer, 22 | roomUpdate: roomUpdateReducer, 23 | roomDelete: roomDeleteReducer, 24 | roomBookingCheck: roomBookingCheckReducer, 25 | bookingsFetch: bookingsFetchReducer, 26 | bookingDelete: bookingDeleteReducer, 27 | bookedDates: bookedDatesReducer, 28 | bookingCreate: bookingCreateReducer, 29 | BookingsMy: BookingsMyReducer 30 | }); 31 | 32 | const userInfoFromStorage = JSON.parse(localStorage.getItem("userInfo")!); 33 | 34 | const initialState = { 35 | userLogin: { 36 | userInfo: userInfoFromStorage 37 | } 38 | }; 39 | 40 | const store = createStore( 41 | rootReducers, 42 | initialState, 43 | composeEnhancer(applyMiddleware(thunk)) 44 | ); 45 | 46 | export default store; -------------------------------------------------------------------------------- /frontend/src/screens/AdminBookingsScreen.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Container, Row, Col, Table, Button } from 'react-bootstrap'; 3 | import { useSelector, useDispatch, RootStateOrAny } from 'react-redux'; 4 | import Loader from "../components/Loader"; 5 | import Message from "../components/Message"; 6 | import Paginate from '../components/Paginate'; 7 | import { IBooking } from '../interfaces/IBooking'; 8 | import moment from 'moment'; 9 | import { getAllBookings, deleteBooking } from '../redux/actions/BookingActions'; 10 | 11 | const AdminBookingsScreen = () => { 12 | 13 | const dispatch = useDispatch(); 14 | 15 | const [currentPage, setCurrentPage] = useState(1); 16 | const { loading, bookings, count, error } = useSelector((state: RootStateOrAny) => state.bookingsFetch); 17 | 18 | const { loading: loadingDelete, success: successDelete, error: errorDelete } = useSelector((state: RootStateOrAny) => state.bookingDelete); 19 | 20 | const handleDelete = (id: IBooking['_id']) => { 21 | dispatch(deleteBooking(id)); 22 | } 23 | 24 | useEffect(() => { 25 | dispatch(getAllBookings(currentPage)); 26 | }, [dispatch, currentPage, successDelete]); 27 | 28 | return ( 29 | 30 | 31 | 32 |

Bookings

33 | 34 |
35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | {loading ? : error ? {error} : ( 51 | bookings?.map((booking: any) => 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 64 | 65 | ) 66 | )} 67 | 68 |
Booking IDRoomCheck In Check OutAmountByActions
{booking._id}{booking.room.name}{moment(booking.checkInDate as Date).format("LL")}{moment(booking.checkOutDate as Date).format("LL")}${booking.amountPaid}{booking.user?.name} 60 | 63 |
69 | 70 |
71 | 72 | 73 | {count !== 0 && ( 74 | 80 | )} 81 | 82 | 83 |
84 | ); 85 | }; 86 | 87 | export default AdminBookingsScreen; 88 | -------------------------------------------------------------------------------- /frontend/src/screens/AdminCreateRoomScreen.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Container, Form, Row, Col, FormGroup, FloatingLabel, Button } from 'react-bootstrap'; 3 | import { RootStateOrAny, useDispatch, useSelector } from 'react-redux'; 4 | import axios from 'axios'; 5 | import { useNavigate } from 'react-router-dom'; 6 | import { CREATE_ROOM_RESET } from '../redux/constants/RoomConstants'; 7 | import { createRoom } from '../redux/actions/RoomActions'; 8 | import { IRoom, TImage } from '../interfaces/IRoom'; 9 | import Message from '../components/Message'; 10 | import Loader from '../components/Loader'; 11 | 12 | const AdminCreateRoomScreen = () => { 13 | 14 | let navigate = useNavigate(); 15 | const dispatch = useDispatch(); 16 | 17 | const [name, setName] = useState(""); 18 | const [description, setDescription] = useState(""); 19 | const [address, setAddress] = useState(""); 20 | const [guestCapacity, setGuestCapacity] = useState(0); 21 | const [numOfBeds, setNumOfBeds] = useState(0); 22 | const [roomType, setRoomType] = useState("King"); 23 | const [internet, setInternet] = useState(false); 24 | const [airConditioned, setAirConditioned] = useState(false); 25 | const [breakfast, setBreakfast] = useState(false); 26 | const [petsAllowed, setPetsAllowed] = useState(false); 27 | const [roomCleaning, setRoomCleaning] = useState(false); 28 | const [price, setPrice] = useState(0); 29 | const [images, setImages] = useState(null); 30 | 31 | const [uploadRoomLoading, setUploadRoomLoading] = useState(false); 32 | 33 | const { success, error } = useSelector((state: RootStateOrAny) => state.roomCreate); 34 | 35 | const uploadImagesHandler = (e: React.FormEvent) => { 36 | 37 | const target = e.target as HTMLInputElement; 38 | 39 | if (!target.files?.length) { 40 | return; 41 | } 42 | 43 | const files = target.files; 44 | 45 | setImages(files); 46 | 47 | } 48 | 49 | const handlerSubmit = async (e: React.FormEvent) => { 50 | e.preventDefault(); 51 | 52 | const formData = new FormData(); 53 | 54 | for(let i = 0; i < images.length; i++) { 55 | formData.append("image", images[i]); 56 | } 57 | 58 | try { 59 | 60 | setUploadRoomLoading(true); 61 | 62 | const config = { 63 | headers: { 64 | "Content-Type": "multipart/form-data" 65 | } 66 | } 67 | 68 | const { data } = await axios.post("/api/uploads", formData, config); 69 | 70 | const allImages: TImage[] = []; 71 | for(let i = 0; i < data.length; i++) { 72 | allImages.push({ image: `/${data[i].path.toString().replace("\\", "/")}` }); 73 | } 74 | 75 | setTimeout(() => { 76 | dispatch(createRoom({ name, description, address, guestCapacity, numOfBeds, category: roomType, internet, airConditioned, breakfast, petsAllowed, roomCleaning, pricePerNight: price, images: allImages })); 77 | setUploadRoomLoading(false); 78 | dispatch({ type: CREATE_ROOM_RESET }); 79 | }, 1000); 80 | 81 | } catch (error: any) { 82 | console.log(error.message); 83 | } 84 | 85 | } 86 | 87 | useEffect(() => { 88 | if(success) { 89 | navigate("/"); 90 | } 91 | }, [dispatch, success]); 92 | 93 | return ( 94 | 95 | 96 | 97 |

Create Room

98 | 99 |
100 | 101 | 102 | {error && ( 103 | 104 | {error} 105 | 106 | )} 107 |
108 | 109 | 110 | Name 111 | 112 | setName(e.target.value)} 118 | required 119 | /> 120 | 121 | 122 | 123 | setDescription(e.target.value)} 129 | style={{ height: '100px' }} 130 | required 131 | /> 132 | 133 | 134 | 135 | 136 | Address 137 | 138 | setAddress(e.target.value)} 144 | required 145 | /> 146 | 147 | 148 | 149 | 150 | 151 | Guest Capacity 152 | 153 | setGuestCapacity(Number(e.target.value))} 157 | aria-label="Default select example" 158 | > 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | Num Of Beds 171 | 172 | setNumOfBeds(Number(e.target.value))} 176 | aria-label="Default select example" 177 | > 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | Room Type 189 | setRoomType(e.target.value)} 193 | aria-label="Default select example" 194 | > 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | setInternet(!internet)} 210 | /> 211 | 212 | 213 | 214 | 215 | setBreakfast(!breakfast)} 220 | /> 221 | 222 | 223 | 224 | 225 | setAirConditioned(!airConditioned)} 230 | /> 231 | 232 | 233 | 234 | 235 | setPetsAllowed(!petsAllowed)} 240 | /> 241 | 242 | 243 | 244 | 245 | setRoomCleaning(!roomCleaning)} 250 | /> 251 | 252 | 253 | 254 | 255 | 256 | Price 257 | 258 | setPrice(Number(e.target.value))} 262 | placeholder="Price Per Night" 263 | min="10" 264 | max="100" 265 | /> 266 | 267 | 268 | 269 | Images 270 | 271 | 278 | 279 | 280 | 283 | 284 |
285 | 286 |
287 |
288 | ); 289 | }; 290 | 291 | export default AdminCreateRoomScreen; 292 | -------------------------------------------------------------------------------- /frontend/src/screens/AdminEditRoomScreen.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react' 2 | import { RootStateOrAny, useDispatch, useSelector } from 'react-redux'; 3 | import { useNavigate, useParams } from 'react-router-dom'; 4 | import Message from '../components/Message'; 5 | import Loader from '../components/Loader'; 6 | import { getRoomDetails } from '../redux/actions/RoomActions'; 7 | import { IRoom, TImage } from '../interfaces/IRoom'; 8 | import { Container, Row, Col, Form, FormGroup, Button, FloatingLabel, Image } from 'react-bootstrap'; 9 | import { updateRoom } from '../redux/actions/RoomActions'; 10 | import axios from 'axios'; 11 | import { UPDATE_ROOM_RESET } from '../redux/constants/RoomConstants'; 12 | 13 | 14 | type TId = { 15 | id: IRoom['_id'] 16 | } 17 | 18 | const AdminEditRoomScreen = () => { 19 | 20 | const dispatch = useDispatch(); 21 | 22 | let navigate = useNavigate(); 23 | let { id } = useParams(); 24 | 25 | const [name, setName] = useState(""); 26 | const [description, setDescription] = useState(""); 27 | const [address, setAddress] = useState(""); 28 | const [guestCapacity, setGuestCapacity] = useState(0); 29 | const [numOfBeds, setNumOfBeds] = useState(0); 30 | const [roomType, setRoomType] = useState("King"); 31 | const [internet, setInternet] = useState(false); 32 | const [airConditioned, setAirConditioned] = useState(false); 33 | const [breakfast, setBreakfast] = useState(false); 34 | const [petsAllowed, setPetsAllowed] = useState(false); 35 | const [roomCleaning, setRoomCleaning] = useState(false); 36 | const [price, setPrice] = useState(0); 37 | const [oldImages, setOldImages] = useState([]); 38 | const [newImages, setNewImages] = useState(null); 39 | 40 | const { loading: loadingUpdate, success: successUpdate, error: errorUpdate } = useSelector((state: RootStateOrAny) => state.roomUpdate); 41 | 42 | const { room, loading, error } = useSelector((state: RootStateOrAny) => state.roomDetails); 43 | 44 | useEffect(() => { 45 | if(successUpdate) { 46 | dispatch(getRoomDetails(id as string)); 47 | navigate("/admin/rooms"); 48 | dispatch({ type: UPDATE_ROOM_RESET }); 49 | } 50 | if(!room?.name || room._id !== id) { 51 | dispatch(getRoomDetails(id as string)); 52 | } else { 53 | setName(room.name); 54 | setDescription(room.description); 55 | setAddress(room.address); 56 | setGuestCapacity(room.guestCapacity); 57 | setNumOfBeds(room.numOfBeds); 58 | setRoomType(room.roomType); 59 | setInternet(room.internet); 60 | setAirConditioned(room.petsAllowed); 61 | setBreakfast(room.breakfast); 62 | setPetsAllowed(room.petsAllowed); 63 | setRoomCleaning(room.roomCleaning); 64 | setPrice(room.pricePerNight); 65 | setOldImages(room.images); 66 | } 67 | }, [dispatch, room, successUpdate, id]); 68 | 69 | const removeImage = (imageId: string) => { 70 | const removedImage: any = oldImages.filter((e: TImage) => e._id !== imageId); 71 | setOldImages(removedImage); 72 | } 73 | 74 | const uploadImagesHandler = (e: React.FormEvent) => { 75 | 76 | const target = e.target as HTMLInputElement; 77 | 78 | if (!target.files?.length) { 79 | return; 80 | } 81 | 82 | const files = target.files; 83 | 84 | setNewImages(files); 85 | 86 | } 87 | 88 | const handlerSubmit = async (e: React.FormEvent) => { 89 | e.preventDefault(); 90 | 91 | const formData = new FormData(); 92 | 93 | for(let i = 0; i < newImages?.length; i++) { 94 | formData.append("image", newImages[i]); 95 | } 96 | 97 | try { 98 | 99 | const config = { 100 | headers: { 101 | "Content-Type": "multipart/form-data" 102 | } 103 | } 104 | 105 | const { data } = await axios.post("/api/uploads", formData, config); 106 | 107 | const allImages: TImage[] = oldImages; 108 | for(let i = 0; i < data?.length; i++) { 109 | allImages.push({ image: `/${data[i].path.toString().replace("\\", "/")}` }); 110 | } 111 | 112 | dispatch(updateRoom(id as string, { name, description, address, guestCapacity, numOfBeds, category: roomType, internet, airConditioned, breakfast, petsAllowed, roomCleaning, pricePerNight: price, images: allImages })); 113 | 114 | } catch (error: any) { 115 | console.log(error.message); 116 | } 117 | 118 | } 119 | 120 | return ( 121 | 122 | 123 | 124 |

Edit Room

125 | 126 |
127 | 128 | 129 | {loading ? : error ? {error} : 130 | <> 131 | {errorUpdate && {errorUpdate}} 132 |
133 | 134 | 135 | Name 136 | 137 | setName(e.target.value)} 143 | required 144 | /> 145 | 146 | 147 | 148 | setDescription(e.target.value)} 154 | style={{ height: '100px' }} 155 | required 156 | /> 157 | 158 | 159 | 160 | 161 | Address 162 | 163 | setAddress(e.target.value)} 169 | required 170 | /> 171 | 172 | 173 | 174 | 175 | 176 | Guest Capacity 177 | 178 | setGuestCapacity(Number(e.target.value))} 182 | aria-label="Default select example" 183 | > 184 | {[1,2,3,4,5].map((guest: number) => 185 | 186 | )} 187 | 188 | 189 | 190 | 191 | 192 | 193 | Num Of Beds 194 | 195 | setNumOfBeds(Number(e.target.value))} 199 | aria-label="Default select example" 200 | > 201 | {[1,2,3,4,5].map((numOfBeds: number) => 202 | 203 | )} 204 | 205 | 206 | 207 | 208 | 209 | Room Type 210 | setRoomType(e.target.value)} 214 | aria-label="Default select example" 215 | > 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | setInternet(!internet)} 231 | /> 232 | 233 | 234 | 235 | 236 | setBreakfast(!breakfast)} 241 | /> 242 | 243 | 244 | 245 | 246 | setAirConditioned(!airConditioned)} 251 | /> 252 | 253 | 254 | 255 | 256 | setPetsAllowed(!petsAllowed)} 261 | /> 262 | 263 | 264 | 265 | 266 | setRoomCleaning(!roomCleaning)} 271 | /> 272 | 273 | 274 | 275 | 276 | 277 | Price 278 | 279 | setPrice(Number(e.target.value))} 283 | placeholder="Price Per Night" 284 | min="10" 285 | max="100" 286 | /> 287 | 288 | 289 | 290 | Images 291 | 292 | 299 | 300 |
301 | 302 | {oldImages.map((image: TImage) => 303 | 304 | Image Room 305 | 311 | 312 | )} 313 | 314 |
315 | 316 | 319 | 320 |
321 | 322 | } 323 | 324 |
325 |
326 | ) 327 | } 328 | 329 | export default AdminEditRoomScreen -------------------------------------------------------------------------------- /frontend/src/screens/AdminEditUserScreen.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react' 2 | import { RootStateOrAny, useDispatch, useSelector } from 'react-redux'; 3 | import { Container, Col, Row, Form, Button } from 'react-bootstrap' 4 | import { useParams, useNavigate } from 'react-router-dom'; 5 | import { IUser } from '../interfaces/IUser'; 6 | import { detailsUser, updateUser } from '../redux/actions/UserActions'; 7 | import Loader from '../components/Loader'; 8 | import Message from '../components/Message'; 9 | import { UPDATE_USER_RESET } from '../redux/constants/UserConstants'; 10 | 11 | type TId = { 12 | id: IUser['_id'] 13 | } 14 | 15 | const AdminEditUserScreen = () => { 16 | 17 | const dispatch = useDispatch(); 18 | let navigate = useNavigate(); 19 | 20 | const { id } = useParams(); 21 | const [name, setName] = useState(""); 22 | const [email, setEmail] = useState(""); 23 | const [isAdmin, setIsAdmin] = useState(false); 24 | 25 | const { user, loading, error } = useSelector((state: RootStateOrAny) => state.userDetails); 26 | 27 | const { loading: loadingUpdate, success: successUpdate, error: errorUpdate } = useSelector((state: RootStateOrAny) => state.userUpdate); 28 | 29 | useEffect(() => { 30 | if(!user || user._id !== id || successUpdate) { 31 | dispatch({ type: UPDATE_USER_RESET }); 32 | dispatch(detailsUser(id as string)); 33 | } else { 34 | setName(user.name); 35 | setEmail(user.email); 36 | setIsAdmin(user.isAdmin); 37 | } 38 | 39 | if(error) { 40 | navigate("/"); 41 | } 42 | }, [dispatch, user, id, successUpdate]) 43 | 44 | const handlerSubmit = (e: React.FormEvent) => { 45 | e.preventDefault(); 46 | dispatch(updateUser(user._id as string, { name, email, isAdmin })); 47 | } 48 | 49 | return ( 50 | 51 | 52 | 53 |

Update User

54 | 55 |
56 | 57 | 58 | {loading && } 59 | {!loading && user && ( 60 | <> 61 | {errorUpdate && {errorUpdate}} 62 |
63 | 64 | Full Name 65 | setName(e.target.value)} 70 | > 71 | 72 | 73 | 74 | E-Mail 75 | setEmail(e.target.value)} 80 | > 81 | 82 | 83 | 84 | setIsAdmin(!isAdmin)} 89 | /> 90 | 91 | 92 | 95 | 96 |
97 | 98 | )} 99 | 100 |
101 |
102 | ) 103 | } 104 | 105 | export default AdminEditUserScreen -------------------------------------------------------------------------------- /frontend/src/screens/AdminRoomsScreen.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Container, Row, Col, Table, Button } from 'react-bootstrap'; 3 | import { useSelector, useDispatch, RootStateOrAny } from 'react-redux'; 4 | import { LinkContainer } from 'react-router-bootstrap'; 5 | import Loader from "../components/Loader"; 6 | import Message from "../components/Message"; 7 | import Paginate from '../components/Paginate'; 8 | import { IRoom } from '../interfaces/IRoom'; 9 | import { fetchRooms, deleteRoom } from '../redux/actions/RoomActions'; 10 | 11 | type AdminRoom = Pick 12 | 13 | const AdminRoomsScreen = () => { 14 | 15 | const dispatch = useDispatch(); 16 | 17 | const [currentPage, setCurrentPage] = useState(1); 18 | const { loading, rooms, count, error } = useSelector((state: RootStateOrAny) => state.roomsFetch); 19 | 20 | const { loading: loadingDelete, success: successDelete, error: errorDelete } = useSelector((state: RootStateOrAny) => state.roomDelete); 21 | 22 | const handleDelete = (id: IRoom['_id']) => { 23 | dispatch(deleteRoom(id)); 24 | } 25 | 26 | useEffect(() => { 27 | dispatch(fetchRooms("", "", "", currentPage)); 28 | }, [dispatch, currentPage, successDelete]); 29 | 30 | return ( 31 | 32 |
33 |

Rooms

34 | 35 | 38 | 39 |
40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | {loading ? : error ? {error} : ( 55 | rooms.map((room: AdminRoom) => 56 | 57 | 58 | 59 | 60 | 61 | 62 | 72 | 73 | ) 74 | )} 75 | 76 |
IDNameAdresseCategoryPriceActions
{room._id}{room.name}{room.address}{room.category}${room.pricePerNight} 63 | 64 | 67 | 68 | 71 |
77 | 78 |
79 | 80 | 81 | {count !== 0 && ( 82 | 88 | )} 89 | 90 | 91 |
92 | ); 93 | }; 94 | 95 | export default AdminRoomsScreen; 96 | -------------------------------------------------------------------------------- /frontend/src/screens/AdminUsersScreen.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react' 2 | import { Container, Row, Col, Table, Button, Image } from 'react-bootstrap'; 3 | import { LinkContainer } from 'react-router-bootstrap'; 4 | import { RootStateOrAny, useDispatch, useSelector } from 'react-redux'; 5 | import Message from '../components/Message'; 6 | import Loader from '../components/Loader'; 7 | import Paginate from '../components/Paginate'; 8 | import { deleteUser, fetchUsers } from '../redux/actions/UserActions'; 9 | import { IUser } from '../interfaces/IUser'; 10 | 11 | const AdminUsersScreen = () => { 12 | 13 | const [currentPage, setCurrentPage] = useState(1); 14 | const dispatch = useDispatch(); 15 | 16 | const { loading, users, count, error } = useSelector((state: RootStateOrAny) => state.usersFetch); 17 | 18 | const { loading: loadingDelete, success:successDelete, error: errorSuccess } = useSelector((state: RootStateOrAny) => state.userDelete); 19 | 20 | useEffect(() => { 21 | dispatch(fetchUsers(currentPage)); 22 | }, [dispatch, currentPage, successDelete]); 23 | 24 | const handleDelete = (id: IUser['_id']) => { 25 | dispatch(deleteUser(id)); 26 | } 27 | 28 | return ( 29 | 30 | 31 | 32 |

Users

33 | 34 |
35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | {loading ? : error ? {error} : ( 50 | users?.map((user: IUser) => 51 | 52 | 53 | 56 | 57 | 58 | 59 | 69 | 70 | ) 71 | )} 72 | 73 |
User IDAvatarNameEmailIs AdminActions
{user._id} 54 | Avatar 55 | {user.name}{user.email}{user.isAdmin ? `Yes` : `No`} 60 | 61 | 64 | 65 | 68 |
74 | 75 |
76 | 77 | 78 | {count !== 0 && ( 79 | 85 | )} 86 | 87 | 88 |
89 | ) 90 | } 91 | 92 | export default AdminUsersScreen -------------------------------------------------------------------------------- /frontend/src/screens/HomeScreen.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { Container, Row, Col } from 'react-bootstrap'; 3 | import RoomCard from "../components/RoomCard"; 4 | import { useSelector, useDispatch, RootStateOrAny } from 'react-redux'; 5 | import Loader from "../components/Loader"; 6 | import Message from "../components/Message"; 7 | import { IRoom } from '../interfaces/IRoom'; 8 | import SearchRooms from "../components/SearchRooms"; 9 | import { fetchRooms } from '../redux/actions/RoomActions'; 10 | import Paginate from "../components/Paginate"; 11 | 12 | 13 | const HomeScreen = () => { 14 | 15 | const dispatch = useDispatch(); 16 | 17 | const [keyword, setKeyword] = useState(""); 18 | const [numOfBeds, setNumOfBeds] = useState(""); 19 | const [roomType, setRoomType] = useState(""); 20 | 21 | const [currentPage, setCurrentPage] = useState(1); 22 | const { loading, rooms, count, error } = useSelector((state: RootStateOrAny) => state.roomsFetch); 23 | 24 | useEffect(() => { 25 | dispatch(fetchRooms(keyword, numOfBeds, roomType, currentPage)); 26 | }, [dispatch, keyword, numOfBeds, roomType, currentPage]); 27 | 28 | return ( 29 | 30 | 31 | 32 |

All Rooms

33 | 34 |
35 | 43 | 44 | {loading ? : error ? {error} : rooms.length > 0 ? 45 | <> 46 | {rooms.map((room: IRoom) => 47 | 48 | 49 | 50 | )} 51 | 52 | : ( 53 | <> 54 | No Room Available 55 | 56 | )} 57 | 58 | 59 | 60 | {count !== 0 && ( 61 | 67 | )} 68 | 69 | 70 |
71 | ); 72 | }; 73 | 74 | export default HomeScreen; 75 | -------------------------------------------------------------------------------- /frontend/src/screens/LoginScreen.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import { useSelector, useDispatch, RootStateOrAny } from 'react-redux'; 4 | import { Container, Row, Col, Form, Button } from 'react-bootstrap'; 5 | import { login } from '../redux/actions/UserActions'; 6 | import Message from '../components/Message'; 7 | import Loader from '../components/Loader'; 8 | import { IUser } from '../interfaces/IUser'; 9 | 10 | const LoginScreen: React.FC = () => { 11 | 12 | let navigate = useNavigate(); 13 | const dispatch = useDispatch(); 14 | 15 | const [email, setEmail] = useState(""); 16 | const [password, setPassword] = useState(""); 17 | 18 | const { userInfo, loading, error, success } = useSelector((state: RootStateOrAny) => state.userLogin); 19 | 20 | 21 | const handleSubmit = (e: React.FormEvent) => { 22 | e.preventDefault(); 23 | dispatch(login({ email, password })); 24 | } 25 | 26 | useEffect(() => { 27 | if(success || userInfo) { 28 | navigate("/"); 29 | } 30 | }, [userInfo, success, dispatch]); 31 | 32 | 33 | return ( 34 | 35 | 36 | 37 |

Login

38 | {error && {error}} 39 |
40 | 41 | E-Mail 42 | setEmail(e.target.value)} 47 | > 48 | 49 | 50 | 51 | Password 52 | setPassword(e.target.value)} 57 | > 58 | 59 | 60 | 61 | 64 | 65 |
66 | 67 |
68 |
69 | ); 70 | }; 71 | 72 | export default LoginScreen; 73 | -------------------------------------------------------------------------------- /frontend/src/screens/MyBookingsScreen.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { Container, Row, Col, Table } from 'react-bootstrap'; 3 | import { useSelector, useDispatch, RootStateOrAny } from 'react-redux'; 4 | import Loader from '../components/Loader'; 5 | import Message from '../components/Message'; 6 | import { getMyBookings } from '../redux/actions/BookingActions'; 7 | import moment from 'moment'; 8 | import { Link } from 'react-router-dom'; 9 | 10 | const MyBookingsScreen = () => { 11 | 12 | const dispatch = useDispatch(); 13 | 14 | const { myBookings, loading, error } = useSelector((state: RootStateOrAny) => state.BookingsMy); 15 | 16 | useEffect(() => { 17 | dispatch(getMyBookings()); 18 | }, [dispatch]); 19 | 20 | 21 | return ( 22 | 23 | 24 | 25 |

My Bookings

26 | 27 |
28 | 29 | 30 | {loading ? : error ? {error} : ( 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | {myBookings?.map((book: any) => 43 | 44 | 45 | 50 | 51 | 52 | 53 | 54 | )} 55 | 56 |
Booking IDRoomCheck In Check OutAmount
{book._id} 46 | 47 | {book.room.name} 48 | 49 | {moment(book.checkInDate as Date).format("LL")}{moment(book.checkOutDate as Date).format("LL")}${book.amountPaid}
57 | )} 58 | 59 |
60 |
61 | ) 62 | }; 63 | 64 | export default MyBookingsScreen; 65 | -------------------------------------------------------------------------------- /frontend/src/screens/PasswordScreen.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import { useSelector, useDispatch, RootStateOrAny } from 'react-redux'; 4 | import { Container, Row, Col, Form, Button } from 'react-bootstrap'; 5 | import { updatePassword } from '../redux/actions/UserActions'; 6 | import Message from '../components/Message'; 7 | import Loader from '../components/Loader'; 8 | import { IUpdatePassword } from '../interfaces/IUser'; 9 | 10 | const PasswordScreen: React.FC = () => { 11 | 12 | const dispatch = useDispatch(); 13 | let navigate = useNavigate(); 14 | 15 | const [oldPassword, setOldPassword] = useState(""); 16 | const [newPassword, setNewPassword] = useState(""); 17 | const [confirmPassword, setConfirmPassword] = useState(""); 18 | const [errConfirmPassword, setErrConfirmPassowrd] = useState(""); 19 | 20 | const { success, loading, error } = useSelector((state: RootStateOrAny) => state.passwordUpdate); 21 | 22 | const handleSubmit = (e: React.FormEvent) => { 23 | e.preventDefault(); 24 | if(newPassword === confirmPassword) { 25 | dispatch(updatePassword({ oldPassword, newPassword })); 26 | setErrConfirmPassowrd(""); 27 | } else { 28 | setErrConfirmPassowrd("New Password must match confirm password") 29 | } 30 | } 31 | 32 | useEffect(() => { 33 | if(success) { 34 | navigate("/account/password"); 35 | setOldPassword(""); 36 | setNewPassword(""); 37 | setConfirmPassword(""); 38 | } 39 | }, [success, dispatch]); 40 | 41 | 42 | return ( 43 | 44 | 45 | 46 |

Update Password

47 | {error && {error}} 48 | {errConfirmPassword && {errConfirmPassword}} 49 | {success && Password Updated} 50 |
51 | 52 | Old Password 53 | setOldPassword(e.target.value)} 58 | > 59 | 60 | 61 | 62 | New Password 63 | setNewPassword(e.target.value)} 68 | > 69 | 70 | 71 | 72 | Confirm Password 73 | setConfirmPassword(e.target.value)} 78 | > 79 | 80 | 81 | 82 | 85 | 86 |
87 | 88 |
89 |
90 | ); 91 | }; 92 | 93 | export default PasswordScreen; 94 | -------------------------------------------------------------------------------- /frontend/src/screens/ProfileScreen.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import axios from 'axios'; 4 | import { useSelector, useDispatch, RootStateOrAny } from 'react-redux'; 5 | import { Container, Row, Col, Form, Button } from 'react-bootstrap'; 6 | import { updateProfile } from '../redux/actions/UserActions'; 7 | import Message from '../components/Message'; 8 | import Loader from '../components/Loader'; 9 | import { IUser } from '../interfaces/IUser'; 10 | 11 | const ProfileScreen: React.FC = () => { 12 | 13 | const dispatch = useDispatch(); 14 | let navigate = useNavigate(); 15 | 16 | const [name, setName] = useState(""); 17 | const [email, setEmail] = useState(""); 18 | const [avatar, setAvatar] = useState(""); 19 | 20 | const { userInfo } = useSelector((state: RootStateOrAny) => state.userLogin); 21 | 22 | const { success, loading, error } = useSelector((state: RootStateOrAny) => state.profileUpdate); 23 | 24 | const handleSubmit = (e: React.FormEvent) => { 25 | e.preventDefault(); 26 | dispatch(updateProfile({ name, email, avatar })); 27 | } 28 | 29 | useEffect(() => { 30 | 31 | if(userInfo) { 32 | setName(userInfo.name); 33 | setEmail(userInfo.email); 34 | setAvatar(userInfo.avatar); 35 | } 36 | 37 | }, [userInfo, dispatch]); 38 | 39 | 40 | const handleUpload = async (e: React.ChangeEvent) => { 41 | 42 | const target = e.target as HTMLInputElement; 43 | 44 | if (!target.files?.length) { 45 | return; 46 | } 47 | 48 | const file = target.files[0]; 49 | 50 | const formData = new FormData(); 51 | 52 | formData.append("image", file); 53 | 54 | try { 55 | 56 | const config = { 57 | headers: { 58 | "Content-Type": "multipart/form-data" 59 | } 60 | } 61 | 62 | const { data } = await axios.post("/api/uploads", formData, config); 63 | 64 | setAvatar(data); 65 | 66 | } catch (error: any) { 67 | console.log(error.message); 68 | } 69 | 70 | } 71 | 72 | return ( 73 | 74 | 75 | 76 |

Update Profile

77 | {error && {error}} 78 | {success && Profile Updated} 79 |
80 | 81 | Full Name 82 | setName(e.target.value)} 87 | > 88 | 89 | 90 | 91 | E-Mail 92 | setEmail(e.target.value)} 97 | > 98 | 99 | 100 | 101 | Avatar 102 | 108 | 109 | 110 | 111 | 114 | 115 |
116 | 117 |
118 |
119 | ); 120 | }; 121 | 122 | export default ProfileScreen; 123 | -------------------------------------------------------------------------------- /frontend/src/screens/RegisterScreen.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import axios from 'axios'; 4 | import { useSelector, useDispatch, RootStateOrAny } from 'react-redux'; 5 | import { Container, Row, Col, Form, Button } from 'react-bootstrap'; 6 | import { register } from '../redux/actions/UserActions'; 7 | import Message from '../components/Message'; 8 | import Loader from '../components/Loader'; 9 | import { IUser } from '../interfaces/IUser'; 10 | 11 | const RegisterScreen: React.FC = () => { 12 | 13 | const dispatch = useDispatch(); 14 | let navigate = useNavigate(); 15 | 16 | const [name, setName] = useState(""); 17 | const [email, setEmail] = useState(""); 18 | const [password, setPassword] = useState(""); 19 | const [avatar, setAvatar] = useState(""); 20 | 21 | const { loading, success, error } = useSelector((state: RootStateOrAny) => state.userRegister); 22 | const { userInfo } = useSelector((state: RootStateOrAny) => state.userLogin); 23 | 24 | const handleSubmit = (e: React.FormEvent) => { 25 | e.preventDefault(); 26 | dispatch(register({ name, email, password, avatar })); 27 | } 28 | 29 | const handleUpload = async (e: React.ChangeEvent) => { 30 | 31 | const target = e.target as HTMLInputElement; 32 | 33 | if (!target.files?.length) { 34 | return; 35 | } 36 | 37 | const file = target.files[0]; 38 | 39 | const formData = new FormData(); 40 | 41 | formData.append("image", file); 42 | 43 | try { 44 | 45 | const config = { 46 | headers: { 47 | "Content-Type": "multipart/form-data" 48 | } 49 | } 50 | 51 | const { data } = await axios.post("/api/uploads", formData, config); 52 | 53 | setAvatar(data[0].path); 54 | 55 | } catch (error: any) { 56 | console.log(error.message); 57 | } 58 | 59 | } 60 | 61 | useEffect(() => { 62 | if(userInfo) { 63 | navigate("/"); 64 | } 65 | }, [dispatch, userInfo, success]); 66 | 67 | 68 | return ( 69 | 70 | 71 | 72 |

Login

73 | {error && {error}} 74 |
75 | 76 | Full Name 77 | setName(e.target.value)} 82 | > 83 | 84 | 85 | 86 | E-Mail 87 | setEmail(e.target.value)} 92 | > 93 | 94 | 95 | 96 | Password 97 | setPassword(e.target.value)} 102 | > 103 | 104 | 105 | 106 | Avatar 107 | 113 | 114 | 115 | 116 | 119 | 120 |
121 | 122 |
123 |
124 | ); 125 | }; 126 | 127 | export default RegisterScreen; 128 | -------------------------------------------------------------------------------- /frontend/src/screens/RoomDetailsScreen.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import DatePicker from "react-datepicker"; 3 | import "react-datepicker/dist/react-datepicker.css"; 4 | import { useParams } from 'react-router-dom'; 5 | import { useSelector, useDispatch, RootStateOrAny } from 'react-redux'; 6 | import { getRoomDetails } from '../redux/actions/RoomActions'; 7 | import { IRoom } from '../interfaces/IRoom'; 8 | import Loader from '../components/Loader'; 9 | import { Container, Row, Col, Carousel, Button, Card } from 'react-bootstrap'; 10 | import FormReview from '../components/FormReview'; 11 | import Message from '../components/Message'; 12 | import Rating from '../components/Rating'; 13 | import { checkRoomBooking } from '../redux/actions/BookingActions'; 14 | import ListReviews from '../components/ListReviews'; 15 | import RoomFeatures from '../components/RoomFeatures'; 16 | import { useAuthStatus } from '../hooks/useAuthStatus'; 17 | import { Link } from 'react-router-dom'; 18 | import { CHECK_ROOM_BOOKING_RESET, CREATE_BOOKING_RESET } from '../redux/constants/BookingConstants'; 19 | import axios from 'axios'; 20 | import { PayPalButton } from "react-paypal-button-v2"; 21 | import { createBooking } from '../redux/actions/BookingActions'; 22 | import { getBookedDates } from '../redux/actions/BookingActions'; 23 | import { IBooking } from '../interfaces/IBooking'; 24 | 25 | 26 | type TId = { 27 | id: IRoom['_id'] 28 | } 29 | 30 | declare global { 31 | interface Window { 32 | paypal:any; 33 | } 34 | } 35 | 36 | const RoomDetailsScreen = () => { 37 | 38 | const { loggedIn } = useAuthStatus(); 39 | 40 | const [checkInDate, setCheckInDate] = useState(); 41 | const [checkOutDate, setCheckOutDate] = useState(); 42 | const [daysOfStay, setDaysOfStay] = useState(0); 43 | 44 | const [sdkReady, setSdkReady] = useState(false); 45 | 46 | const { id } = useParams(); 47 | 48 | const dispatch = useDispatch(); 49 | 50 | const { loading, room, error } = useSelector((state: RootStateOrAny) => state.roomDetails); 51 | 52 | const { loading: loadingCreateReview, success: successCreateReview, error: errorCreateReview } = 53 | useSelector((state: RootStateOrAny) => state.roomCreateReview); 54 | 55 | const { loading: loadingRoomIsAvailable, success: successRoomIsAvailable, error: errorRoomIsAvailable } 56 | = useSelector((state: RootStateOrAny) => state.roomBookingCheck); 57 | 58 | const { loading: loadingBookingCreate, success: successBookingCreate, error: errorBookingCreate } 59 | = useSelector((state: RootStateOrAny) => state.bookingCreate); 60 | 61 | const {bookedDates} = useSelector((state: RootStateOrAny) => state.bookedDates); 62 | 63 | useEffect(() => { 64 | 65 | const addPaypalScript = async () => { 66 | const { data: clientId } = await axios.get("/api/config/paypal"); 67 | const script = document.createElement("script"); 68 | script.type = "text/javascript"; 69 | script.src = `https://www.paypal.com/sdk/js?client-id=${clientId}`; 70 | script.async = true; 71 | script.onload = () => { 72 | setSdkReady(true); 73 | }; 74 | document.body.appendChild(script); 75 | }; 76 | 77 | if (!window.paypal && !successBookingCreate) { 78 | addPaypalScript(); 79 | } else { 80 | setSdkReady(true); 81 | } 82 | 83 | dispatch(getRoomDetails(id as string)); 84 | dispatch(getBookedDates(id as string)); 85 | dispatch({ type: CHECK_ROOM_BOOKING_RESET }); 86 | dispatch({ type: CREATE_BOOKING_RESET }); 87 | }, [dispatch, id]); 88 | 89 | const onChange = (dates: any) => { 90 | const [checkInDate, checkOutDate] = dates; 91 | setCheckInDate(checkInDate as Date); 92 | setCheckOutDate(checkOutDate as Date); 93 | 94 | if (checkInDate && checkOutDate) { 95 | 96 | // Calclate days of stay 97 | 98 | const days = Math.abs(checkInDate - checkOutDate) / (1000 * 60 * 60 * 24); 99 | 100 | setDaysOfStay(days); 101 | 102 | dispatch(checkRoomBooking(id as string, checkInDate.toISOString(), checkOutDate.toISOString())); 103 | 104 | } 105 | 106 | } 107 | 108 | const excludedDates: any[] = [] 109 | bookedDates?.forEach((date: Date) => { 110 | excludedDates.push(new Date(date)) 111 | }) 112 | 113 | const successPaymentHandler = (paymentResult: any) => { 114 | 115 | const amountPaid = Number(room.pricePerNight) * Number(daysOfStay); 116 | 117 | const paymentInfo = { 118 | id: paymentResult.id, 119 | status: paymentResult.status, 120 | update_time: paymentResult.update_time, 121 | email_address: paymentResult.payer.email_address, 122 | } 123 | 124 | const bookingData = { 125 | room: id, 126 | checkInDate, 127 | checkOutDate, 128 | amountPaid, 129 | paymentInfo, 130 | daysOfStay, 131 | } 132 | 133 | dispatch(createBooking(bookingData)); 134 | dispatch(getBookedDates(id as string)); 135 | dispatch({ type: CHECK_ROOM_BOOKING_RESET }); 136 | dispatch({ type: CREATE_BOOKING_RESET }); 137 | 138 | } 139 | 140 | return ( 141 | 142 | 143 | {loading ? : error ? {error} : ( 144 | 145 |

{room.name}

146 | {room.address} 147 | 148 |
149 | 150 | {room.images?.map((img: any) => 151 | 152 | {img._id} 157 | 158 | )} 159 | 160 |
161 | 162 | 163 |

Description

164 |

165 | {room.description} 166 |

167 | 168 | 169 | 170 |

Reviews

171 | 172 | {errorCreateReview && {errorCreateReview}} 173 | {successCreateReview && Added Review} 174 | 175 | 176 | 177 |
178 | {loadingCreateReview && } 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | ${room.pricePerNight} / Per Night 187 |
188 |

Pick Check In & Check Out Date

189 | 201 | {loadingRoomIsAvailable && } 202 | {successRoomIsAvailable && Room Is Available} 203 | {errorRoomIsAvailable && {errorRoomIsAvailable}} 204 | 205 | {loggedIn && successRoomIsAvailable && ( 206 | 209 | )} 210 | 211 | {!sdkReady && } 212 | 213 | {loggedIn && successRoomIsAvailable && sdkReady && !successBookingCreate && ( 214 | 218 | )} 219 | 220 | {!loggedIn && !successRoomIsAvailable && ( 221 | 222 | Please Sign In for booking 223 | 224 | )} 225 | 226 | {successBookingCreate && ( 227 | 228 | Your booking has been paymented 229 | 230 | )} 231 | 232 | {errorBookingCreate && ( 233 | 234 | {errorBookingCreate} 235 | 236 | )} 237 | 238 |
239 |
240 | 241 |
242 | 243 | )} 244 |
245 |
246 | ); 247 | }; 248 | 249 | export default RoomDetailsScreen; 250 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ], 26 | "typeRoots": [ 27 | "../node_modules/@types", 28 | "../@types" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hotel-book", 3 | "version": "1.0.0", 4 | "description": "Hotel Booking App Using MERN Stack With TypeScript & Redux", 5 | "main": "server.ts", 6 | "scripts": { 7 | "server": "ts-node-dev backend/server.ts", 8 | "client": "npm start --prefix frontend" 9 | }, 10 | "keywords": [], 11 | "author": "Said MOUNAIM", 12 | "license": "ISC", 13 | "devDependencies": { 14 | "@types/bcrypt": "^5.0.0", 15 | "@types/cors": "^2.8.12", 16 | "@types/express": "^4.17.13", 17 | "@types/jsonwebtoken": "^8.5.8", 18 | "@types/mongoose": "^5.11.97", 19 | "@types/multer": "^1.4.7", 20 | "@types/node": "^17.0.12", 21 | "ts-node-dev": "^1.1.8", 22 | "typescript": "^4.5.5" 23 | }, 24 | "dependencies": { 25 | "bcrypt": "^5.0.1", 26 | "cors": "^2.8.5", 27 | "dotenv": "^14.3.2", 28 | "express": "^4.17.2", 29 | "express-async-handler": "^1.2.0", 30 | "jsonwebtoken": "^8.5.1", 31 | "moment": "^2.29.1", 32 | "moment-range": "^4.0.2", 33 | "mongoose": "^6.1.8", 34 | "multer": "^1.4.4" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ 22 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | 26 | /* Modules */ 27 | "module": "commonjs", /* Specify what module code is generated. */ 28 | // "rootDir": "./", /* Specify the root folder within your source files. */ 29 | // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 30 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 31 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 32 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 33 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ 34 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 35 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 36 | // "resolveJsonModule": true, /* Enable importing .json files */ 37 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ 38 | 39 | /* JavaScript Support */ 40 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ 41 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 42 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ 43 | 44 | /* Emit */ 45 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 46 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 47 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 48 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 49 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ 50 | // "outDir": "./", /* Specify an output folder for all emitted files. */ 51 | // "removeComments": true, /* Disable emitting comments. */ 52 | // "noEmit": true, /* Disable emitting files from a compilation. */ 53 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 54 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ 55 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 56 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 58 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 59 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 60 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 61 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 62 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ 63 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ 64 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 65 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ 66 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 67 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 68 | 69 | /* Interop Constraints */ 70 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 71 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 72 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */ 73 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 74 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 75 | 76 | /* Type Checking */ 77 | "strict": true, /* Enable all strict type-checking options. */ 78 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ 79 | // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ 80 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 81 | // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ 82 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 83 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ 84 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ 85 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 86 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ 87 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ 88 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 89 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 90 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 91 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 92 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 93 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ 94 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 95 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 96 | 97 | /* Completeness */ 98 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 99 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /uploads/default-room.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saidMounaim/hotel-booking/b1c1f1cd3aa6364044360c27644b317704ecd6ed/uploads/default-room.jpeg -------------------------------------------------------------------------------- /uploads/image-1643568802545-854384589.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saidMounaim/hotel-booking/b1c1f1cd3aa6364044360c27644b317704ecd6ed/uploads/image-1643568802545-854384589.png -------------------------------------------------------------------------------- /uploads/image-1643569249831-505829359.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saidMounaim/hotel-booking/b1c1f1cd3aa6364044360c27644b317704ecd6ed/uploads/image-1643569249831-505829359.jpeg -------------------------------------------------------------------------------- /uploads/image-1644677085702-997801630.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saidMounaim/hotel-booking/b1c1f1cd3aa6364044360c27644b317704ecd6ed/uploads/image-1644677085702-997801630.jpg -------------------------------------------------------------------------------- /uploads/image-1644677120247-123558054.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saidMounaim/hotel-booking/b1c1f1cd3aa6364044360c27644b317704ecd6ed/uploads/image-1644677120247-123558054.jpg -------------------------------------------------------------------------------- /uploads/image-1644677259587-703863587.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saidMounaim/hotel-booking/b1c1f1cd3aa6364044360c27644b317704ecd6ed/uploads/image-1644677259587-703863587.jpg -------------------------------------------------------------------------------- /uploads/image-1644677336866-354344121.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saidMounaim/hotel-booking/b1c1f1cd3aa6364044360c27644b317704ecd6ed/uploads/image-1644677336866-354344121.jpg -------------------------------------------------------------------------------- /uploads/image-1644959297658-856579790.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saidMounaim/hotel-booking/b1c1f1cd3aa6364044360c27644b317704ecd6ed/uploads/image-1644959297658-856579790.jpg -------------------------------------------------------------------------------- /uploads/image-1644959297667-735126546.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saidMounaim/hotel-booking/b1c1f1cd3aa6364044360c27644b317704ecd6ed/uploads/image-1644959297667-735126546.jpg -------------------------------------------------------------------------------- /uploads/image-1644959682091-418979889.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saidMounaim/hotel-booking/b1c1f1cd3aa6364044360c27644b317704ecd6ed/uploads/image-1644959682091-418979889.jpg -------------------------------------------------------------------------------- /uploads/image-1644959682097-256588529.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saidMounaim/hotel-booking/b1c1f1cd3aa6364044360c27644b317704ecd6ed/uploads/image-1644959682097-256588529.jpg -------------------------------------------------------------------------------- /uploads/image-1645292083270-442621328.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saidMounaim/hotel-booking/b1c1f1cd3aa6364044360c27644b317704ecd6ed/uploads/image-1645292083270-442621328.jpg -------------------------------------------------------------------------------- /uploads/image-1645292083276-634322936.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saidMounaim/hotel-booking/b1c1f1cd3aa6364044360c27644b317704ecd6ed/uploads/image-1645292083276-634322936.jpg -------------------------------------------------------------------------------- /uploads/image-1645292974624-924191412.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saidMounaim/hotel-booking/b1c1f1cd3aa6364044360c27644b317704ecd6ed/uploads/image-1645292974624-924191412.jpg -------------------------------------------------------------------------------- /uploads/image-1645292974630-35970355.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saidMounaim/hotel-booking/b1c1f1cd3aa6364044360c27644b317704ecd6ed/uploads/image-1645292974630-35970355.jpg -------------------------------------------------------------------------------- /uploads/image-1645293318427-623712477.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saidMounaim/hotel-booking/b1c1f1cd3aa6364044360c27644b317704ecd6ed/uploads/image-1645293318427-623712477.jpg -------------------------------------------------------------------------------- /uploads/image-1645293318433-327794372.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saidMounaim/hotel-booking/b1c1f1cd3aa6364044360c27644b317704ecd6ed/uploads/image-1645293318433-327794372.jpg -------------------------------------------------------------------------------- /uploads/user-default.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saidMounaim/hotel-booking/b1c1f1cd3aa6364044360c27644b317704ecd6ed/uploads/user-default.jpg --------------------------------------------------------------------------------