├── .eslintrc.json ├── .gitignore ├── README.md ├── components ├── Home.jsx ├── Search.jsx ├── admin │ ├── AllBookings.jsx │ ├── AllRooms.jsx │ ├── AllUsers.jsx │ ├── NewRoom.jsx │ ├── RoomReviews.jsx │ ├── UpdateRoom.jsx │ └── UpdateUser.jsx ├── auth │ ├── Login.jsx │ └── Register.jsx ├── bookings │ ├── BookingDetails.jsx │ └── MyBookings.jsx ├── layouts │ ├── ButtonLoader.jsx │ ├── Footer.jsx │ ├── Header.jsx │ ├── Layout.jsx │ ├── Loader.jsx │ └── NotFound.jsx ├── review │ ├── ListReviews.jsx │ └── NewReview.jsx ├── room │ ├── RoomDetails.jsx │ ├── RoomFeatures.jsx │ ├── RoomItem.jsx │ └── RoomOption.jsx └── user │ ├── ForgotPassword.jsx │ ├── NewPassword.jsx │ └── Profile.jsx ├── config └── dbConnect.js ├── controllers ├── authControllers.js ├── bookingControllers.js ├── paymentControllers.js └── roomControllers.js ├── middlewares ├── auth.js ├── catchAsyncError.js └── error.js ├── models ├── booking.js ├── room.js └── user.js ├── next.config.js ├── package.json ├── pages ├── 404.jsx ├── _app.jsx ├── _document.js ├── admin │ ├── bookings │ │ ├── [id].jsx │ │ └── index.jsx │ ├── reviews.jsx │ ├── rooms │ │ ├── index.jsx │ │ └── new.jsx │ └── users │ │ ├── [id].jsx │ │ └── index.jsx ├── api │ ├── admin │ │ ├── bookings │ │ │ ├── [id].js │ │ │ └── index.js │ │ ├── rooms │ │ │ └── index.js │ │ └── users │ │ │ └── [id].js │ ├── auth │ │ ├── [...nextauth].js │ │ └── register.js │ ├── bookings │ │ ├── [id].js │ │ ├── check.js │ │ ├── check_booked_dates.js │ │ ├── index.js │ │ └── me.js │ ├── checkout_session │ │ └── [roomId].js │ ├── me.js │ ├── me │ │ └── update.js │ ├── password │ │ ├── forgot.js │ │ └── reset │ │ │ └── [token].js │ ├── reviews │ │ ├── check_review_availability.js │ │ └── index.js │ ├── rooms │ │ ├── [id].js │ │ └── index.js │ ├── users │ │ └── index.js │ └── webhook.js ├── bookings │ ├── [id].jsx │ └── me.jsx ├── index.jsx ├── login.jsx ├── me │ └── update.jsx ├── password │ ├── forgot.jsx │ └── reset │ │ └── [token].jsx ├── register.jsx ├── room │ └── [id].jsx └── search.jsx ├── public ├── favicon.ico ├── images │ ├── bookit_logo.png │ └── default_avatar.png └── vercel.svg ├── redux ├── actions │ ├── bookingActions.js │ ├── roomActions.js │ └── userActions.js ├── constants │ ├── bookingConstants.js │ ├── roomConstants.js │ └── userConstants.js ├── reducers │ ├── bookingReducers.js │ ├── reducers.js │ ├── roomReducers.js │ └── userReducers.js └── store.js ├── styles ├── Home.module.css └── globals.css ├── utils ├── apiFeatures.js ├── errorHandler.js ├── getStripe.js ├── seeder.js └── sendEmail.js └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | 36 | # local data 37 | data/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 概要 2 | 3 | Next.jsの練習ついでに作成した宿泊予約アプリ 4 | 5 | ![スクリーンショット 2021-11-23 13 35 42](https://user-images.githubusercontent.com/66961071/142971983-769db1a3-7ff6-47d4-9270-34128db5e5d8.png) 6 | 7 | ## 機能 8 | 9 | ### ユーザー側 10 | 11 | - 登録&ログイン機能(メールアドレス&パスワード) 12 | - プロフィール作成・更新(ユーザー名・メールアドレス・アバター画像) 13 | - パスワードを忘れた際の再設定(メール配信) 14 | - 宿泊の予約 15 | - 宿泊レビュー 16 | - 予約一覧確認(予約内容をPDFでダウンロード可能) 17 | - Stripe決済 18 | 19 | ### 管理者(Admin)側 20 | 21 | - ユーザー一覧確認 22 | - 宿泊施設一覧確認&編集機能 23 | - レビュー一覧確認&編集機能 24 | 25 | ## 使用技術 26 | 27 | - Next.js 28 | - Redux 29 | - Vercel 30 | - MongoDB 31 | - StripeAPI(決済用) 32 | - Cloudinary(アバター画像・宿泊施設画像の保存用) 33 | -------------------------------------------------------------------------------- /components/Home.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { useRouter } from "next/dist/client/router"; 3 | import Link from "next/dist/client/link"; 4 | import { useSelector, useDispatch } from "react-redux"; 5 | import { toast } from "react-toastify"; 6 | import Pagination from "react-js-pagination"; 7 | 8 | import { RoomItem } from "./room/RoomItem"; 9 | import { clearErrors } from "../redux/actions/roomActions"; 10 | 11 | export const Home = () => { 12 | const dispatch = useDispatch(); 13 | const router = useRouter(); 14 | 15 | const { rooms, resPerPage, roomsCount, filteredRoomsCount, error } = 16 | useSelector((state) => state.allRooms); 17 | let { location, page = 1 } = router.query; 18 | page = Number(page); 19 | let count = roomsCount; 20 | if (location) { 21 | count = filteredRoomsCount; 22 | } 23 | useEffect(() => { 24 | if (error) { 25 | toast.error(error); 26 | dispatch(clearErrors()); 27 | } 28 | }, []); 29 | 30 | const handlePagination = (pageNumber) => { 31 | router.push(`/?page=${pageNumber}`); 32 | }; 33 | 34 | return ( 35 | <> 36 |
37 |

38 | {location ? `Rooms in ${location}` : "All Rooms"} 39 |

40 | 41 | 42 | Back to Search 43 | 44 | 45 |
46 | {rooms && rooms.length === 0 ? ( 47 |
48 | No Rooms 49 |
50 | ) : ( 51 | rooms && 52 | rooms.map((room) => ) 53 | )} 54 |
55 |
56 | {resPerPage < count && ( 57 |
58 | 70 |
71 | )} 72 | 73 | ); 74 | }; 75 | -------------------------------------------------------------------------------- /components/Search.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { useRouter } from "next/dist/client/router"; 3 | 4 | export const Search = () => { 5 | const [location, setLocation] = useState(""); 6 | const [guests, setGuests] = useState(""); 7 | const [category, setCategory] = useState(""); 8 | 9 | const router = useRouter(); 10 | 11 | const submitHandler = (e) => { 12 | e.preventDefault(); 13 | 14 | if (location.trim()) { 15 | router.push( 16 | `/?location=${location}&guests=${guests}&category=${category}` 17 | ); 18 | } else { 19 | router.push(`/`); 20 | } 21 | }; 22 | return ( 23 |
24 |
25 |
26 |
27 |

Search Rooms

28 |
29 | 30 | setLocation(e.target.value)} 37 | /> 38 |
39 | 40 |
41 | 42 | 54 |
55 | 56 |
57 | 58 | 70 |
71 | 72 | 75 |
76 |
77 |
78 |
79 | ); 80 | }; 81 | -------------------------------------------------------------------------------- /components/admin/AllBookings.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import Link from "next/dist/client/link"; 3 | import { useRouter } from "next/dist/client/router"; 4 | import { useSelector, useDispatch } from "react-redux"; 5 | import { MDBDataTable } from "mdbreact"; 6 | import { toast } from "react-toastify"; 7 | import { createInvoice, download } from "easyinvoice"; 8 | 9 | import { Loader } from "../layouts/Loader"; 10 | import { 11 | clearErrors, 12 | deleteBooking, 13 | getAdminBookings, 14 | } from "../../redux/actions/bookingActions"; 15 | import { DELETE_BOOKING_RESET } from "../../redux/constants/bookingConstants"; 16 | 17 | export const AllBookings = () => { 18 | const dispatch = useDispatch(); 19 | const router = useRouter(); 20 | 21 | const { bookings, error, loading } = useSelector((state) => state.bookings); 22 | const { isDeleted, error: deleteError } = useSelector( 23 | (state) => state.booking 24 | ); 25 | 26 | useEffect(() => { 27 | dispatch(getAdminBookings()); 28 | 29 | if (error) { 30 | toast.error(error); 31 | dispatch(clearErrors()); 32 | } 33 | 34 | if (deleteError) { 35 | toast.error(deleteError); 36 | dispatch(clearErrors()); 37 | } 38 | 39 | if (isDeleted) { 40 | router.push(`/admin/bookings`); 41 | dispatch({ type: DELETE_BOOKING_RESET }); 42 | } 43 | }, [dispatch, deleteError, isDeleted]); 44 | 45 | const setBookings = () => { 46 | const data = { 47 | columns: [ 48 | { 49 | label: "Booking ID", 50 | field: "id", 51 | }, 52 | { 53 | label: "Check In", 54 | field: "checkIn", 55 | sort: "asc", 56 | }, 57 | { 58 | label: "Check Out", 59 | field: "checkOut", 60 | }, 61 | { 62 | label: "Amount Paid", 63 | field: "amount", 64 | }, 65 | { 66 | label: "Actions", 67 | field: "actions", 68 | }, 69 | ], 70 | rows: [], 71 | }; 72 | 73 | bookings && 74 | bookings.forEach((booking) => { 75 | data.rows.push({ 76 | id: booking._id, 77 | checkIn: new Date(booking.checkInDate).toLocaleString("ja"), 78 | checkOut: new Date(booking.checkOutDate).toLocaleString("ja"), 79 | amount: `${booking.amountPaid}`, 80 | actions: ( 81 | <> 82 | 83 | 84 | 85 | 86 | 87 | 93 | 99 | 100 | ), 101 | }); 102 | }); 103 | return data; 104 | }; 105 | 106 | const deleteBookingHandler = (id) => { 107 | dispatch(deleteBooking(id)); 108 | }; 109 | 110 | const downloadInvoice = async (booking) => { 111 | const data = { 112 | documentTitle: "Booking INVOICE", 113 | currency: "USD", 114 | taxNotation: "vat", 115 | marginTop: 25, 116 | marginRight: 25, 117 | marginLeft: 25, 118 | marginBottom: 25, 119 | logo: "https://res.cloudinary.com/bookit/image/upload/v1617904918/bookit/bookit_logo_cbgjzv.png", 120 | sender: { 121 | company: "Book IT", 122 | address: "Oshiue 1-1-2 Sumida-ku", 123 | zip: "1310045", 124 | city: "Tokyo", 125 | country: "Japan", 126 | }, 127 | client: { 128 | company: `${booking.user.name}`, 129 | address: `${booking.user.email}`, 130 | zip: "", 131 | city: `Check In: ${new Date(booking.checkInDate).toLocaleString("ja")}`, 132 | country: `Check In: ${new Date(booking.checkOutDate).toLocaleString( 133 | "ja" 134 | )}`, 135 | }, 136 | invoiceNumber: `${booking._id}`, 137 | invoiceDate: `${new Date(Date.now()).toLocaleString("ja")}`, 138 | products: [ 139 | { 140 | quantity: `${booking.daysOfStay}`, 141 | description: `${booking.room.name}`, 142 | tax: 0, 143 | price: booking.room.pricePerNight, 144 | }, 145 | ], 146 | bottomNotice: 147 | "This is auto generated Invoice of your booking on Book IT.", 148 | }; 149 | 150 | const result = await createInvoice(data); 151 | download(`invoice_${booking._id}.pdf`, result.pdf); 152 | }; 153 | 154 | return ( 155 |
156 | {loading ? ( 157 | 158 | ) : ( 159 | <> 160 |

{`${bookings && bookings.length} Bookings`}

161 | 162 | 169 | 170 | )} 171 |
172 | ); 173 | }; 174 | -------------------------------------------------------------------------------- /components/admin/AllRooms.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import Link from "next/dist/client/link"; 3 | import { useRouter } from "next/dist/client/router"; 4 | import { useSelector, useDispatch } from "react-redux"; 5 | import { MDBDataTable } from "mdbreact"; 6 | import { toast } from "react-toastify"; 7 | 8 | import { Loader } from "../layouts/Loader"; 9 | import { 10 | clearErrors, 11 | deleteRoom, 12 | getAdminRooms, 13 | } from "../../redux/actions/roomActions"; 14 | import { DELETE_ROOM_RESET } from "../../redux/constants/roomConstants"; 15 | 16 | export const AllRooms = () => { 17 | const dispatch = useDispatch(); 18 | const router = useRouter(); 19 | 20 | const { rooms, error } = useSelector((state) => state.allRooms); 21 | const { error: deleteError, isDeleted } = useSelector((state) => state.room); 22 | 23 | useEffect(() => { 24 | dispatch(getAdminRooms()); 25 | 26 | if (error) { 27 | toast.error(error); 28 | dispatch(clearErrors()); 29 | } 30 | 31 | if (deleteError) { 32 | toast.error(deleteError); 33 | dispatch(clearErrors()); 34 | } 35 | 36 | if (isDeleted) { 37 | router.push(`/admin/rooms`); 38 | dispatch({ type: DELETE_ROOM_RESET }); 39 | } 40 | }, [dispatch, deleteError, isDeleted]); 41 | 42 | const setRooms = () => { 43 | const data = { 44 | columns: [ 45 | { 46 | label: "Room ID", 47 | field: "id", 48 | }, 49 | { 50 | label: "Name", 51 | field: "name", 52 | sort: "asc", 53 | }, 54 | { 55 | label: "Price / Night", 56 | field: "price", 57 | }, 58 | { 59 | label: "Category", 60 | field: "category", 61 | }, 62 | { 63 | label: "Actions", 64 | field: "actions", 65 | }, 66 | ], 67 | rows: [], 68 | }; 69 | 70 | rooms && 71 | rooms.forEach((room) => { 72 | data.rows.push({ 73 | id: room._id, 74 | name: room.name, 75 | price: `$${room.pricePerNight}`, 76 | category: room.category, 77 | actions: ( 78 | <> 79 | 80 | 81 | 82 | 83 | 84 | 90 | 91 | ), 92 | }); 93 | }); 94 | return data; 95 | }; 96 | 97 | const deleteRoomHandler = (id) => { 98 | dispatch(deleteRoom(id)); 99 | }; 100 | 101 | return ( 102 |
103 | {loading ? ( 104 | 105 | ) : ( 106 | <> 107 |

108 | {`${rooms && rooms.length} Rooms`} 109 | 110 | 111 | Create Room 112 | 113 | 114 |

115 | 122 | 123 | )} 124 |
125 | ); 126 | }; 127 | -------------------------------------------------------------------------------- /components/admin/AllUsers.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import Link from "next/dist/client/link"; 3 | import { useRouter } from "next/dist/client/router"; 4 | import { useSelector, useDispatch } from "react-redux"; 5 | import { MDBDataTable } from "mdbreact"; 6 | import { toast } from "react-toastify"; 7 | 8 | import { Loader } from "../layouts/Loader"; 9 | import { 10 | getAdminUsers, 11 | clearErrors, 12 | deleteUser, 13 | } from "../../redux/actions/userActions"; 14 | import { DELETE_USER_RESET } from "../../redux/constants/userConstants"; 15 | 16 | export const AllUsers = () => { 17 | const dispatch = useDispatch(); 18 | const router = useRouter(); 19 | 20 | const { loading, error, users } = useSelector((state) => state.allUsers); 21 | const { error: deleteError, isDeleted } = useSelector((state) => state.user); 22 | 23 | useEffect(() => { 24 | dispatch(getAdminUsers()); 25 | 26 | if (error) { 27 | toast.error(error); 28 | dispatch(clearErrors()); 29 | } 30 | 31 | if (deleteError) { 32 | toast.error(deleteError); 33 | dispatch(clearErrors()); 34 | } 35 | 36 | if (isDeleted) { 37 | router.push(`/admin/rooms`); 38 | dispatch({ type: DELETE_USER_RESET }); 39 | } 40 | }, [dispatch, error, isDeleted]); 41 | 42 | const setUsers = () => { 43 | const data = { 44 | columns: [ 45 | { 46 | label: "User ID", 47 | field: "id", 48 | sort: "asc", 49 | }, 50 | { 51 | label: "Name", 52 | field: "name", 53 | sort: "asc", 54 | }, 55 | { 56 | label: "Email", 57 | field: "email", 58 | sort: "asc", 59 | }, 60 | { 61 | label: "Role", 62 | field: "role", 63 | sort: "asc", 64 | }, 65 | { 66 | label: "Actions", 67 | field: "actions", 68 | sort: "asc", 69 | }, 70 | ], 71 | rows: [], 72 | }; 73 | 74 | users && 75 | users.forEach((user) => { 76 | data.rows.push({ 77 | id: user._id, 78 | name: user.name, 79 | email: user.email, 80 | role: user.role, 81 | actions: ( 82 | <> 83 | 84 | 85 | 86 | 87 | 88 | 89 | 95 | 96 | ), 97 | }); 98 | }); 99 | return data; 100 | }; 101 | 102 | const deleteUserHandler = (id) => { 103 | dispatch(deleteUser(id)); 104 | }; 105 | 106 | return ( 107 |
108 | {loading ? ( 109 | 110 | ) : ( 111 | <> 112 |

{`${users && users.length} Users`}

113 | 120 | 121 | )} 122 |
123 | ); 124 | }; 125 | -------------------------------------------------------------------------------- /components/admin/NewRoom.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { useRouter } from "next/dist/client/router"; 3 | import { useSelector, useDispatch } from "react-redux"; 4 | import { toast } from "react-toastify"; 5 | 6 | import { ButtonLoader } from "../layouts/ButtonLoader"; 7 | import { clearErrors, newRoom } from "../../redux/actions/roomActions"; 8 | import { NEW_ROOM_RESET } from "../../redux/constants/roomConstants"; 9 | 10 | export const NewRoom = () => { 11 | const [name, setName] = useState(""); 12 | const [price, setPrice] = useState(0); 13 | const [description, setDescription] = useState(""); 14 | const [address, setAddress] = useState(""); 15 | const [category, setCategory] = useState("King"); 16 | const [guestCapacity, setGuestCapacity] = useState(1); 17 | const [numOfBeds, setNumOfBeds] = useState(1); 18 | const [internet, setInternet] = useState(false); 19 | const [breakfast, setBreakfast] = useState(false); 20 | const [airConditioned, setAirConditioned] = useState(false); 21 | const [petsAllowed, setPetsAllowed] = useState(false); 22 | const [roomCleaning, setRoomCleaning] = useState(false); 23 | const [images, setImages] = useState([]); 24 | const [imagesPreview, setImagesPreview] = useState([]); 25 | 26 | const dispatch = useDispatch(); 27 | const router = useRouter(); 28 | 29 | const { loading, error, success } = useSelector((state) => state.newRoom); 30 | 31 | useEffect(() => { 32 | if (error) { 33 | toast.error(error); 34 | dispatch(clearErrors()); 35 | } 36 | 37 | if (success) { 38 | router.push("/admin/rooms"); 39 | dispatch({ type: NEW_ROOM_RESET }); 40 | } 41 | }, [dispatch, error, success]); 42 | 43 | const submitHandler = (e) => { 44 | e.preventDefault(); 45 | 46 | const roomData = { 47 | name, 48 | pricePerNight: price, 49 | description, 50 | address, 51 | category, 52 | guestCapacity: Number(guestCapacity), 53 | numOfBeds: Number(numOfBeds), 54 | internet, 55 | breakfast, 56 | airConditioned, 57 | petsAllowed, 58 | roomCleaning, 59 | images, 60 | }; 61 | 62 | if (images.length === 0) return toast.error(`Please upload images`); 63 | 64 | dispatch(newRoom(roomData)); 65 | }; 66 | 67 | const onChange = (e) => { 68 | const files = Array.from(e.target.files); 69 | setImages([]); 70 | setImagesPreview([]); 71 | 72 | files.forEach((file) => { 73 | const reader = new FileReader(); 74 | 75 | reader.onload = () => { 76 | if (reader.readyState === 2) { 77 | setImages((oldArray) => [...oldArray, reader.result]); 78 | setImagesPreview((oldArray) => [...oldArray, reader.result]); 79 | } 80 | }; 81 | 82 | reader.readAsDataURL(file); 83 | }); 84 | }; 85 | 86 | return ( 87 |
88 |
89 |
90 |
95 |

New Room

96 |
97 | 98 | setName(e.target.value)} 104 | required 105 | /> 106 |
107 |
108 | 109 | setPrice(e.target.value)} 115 | required 116 | /> 117 |
118 |
119 | 120 | 128 |
129 |
130 | 131 | setAddress(e.target.value)} 137 | required 138 | /> 139 |
140 | 141 |
142 | 143 | 155 |
156 | 157 |
158 | 159 | 171 |
172 | 173 |
174 | 175 | 187 |
188 | 189 | 190 |
191 | setInternet(e.target.checked)} 197 | /> 198 | 201 |
202 |
203 | setBreakfast(e.target.checked)} 209 | /> 210 | 213 |
214 |
215 | setAirConditioned(e.target.checked)} 221 | /> 222 | 228 |
229 |
230 | setPetsAllowed(e.target.checked)} 236 | /> 237 | 243 |
244 |
245 | setRoomCleaning(e.target.checked)} 251 | /> 252 | 258 |
259 | 260 |
261 | 262 |
263 | 271 | 274 |
275 | 276 | {imagesPreview.map((img) => ( 277 | Images Preview 285 | ))} 286 |
287 | 294 |
295 |
296 |
297 |
298 | ); 299 | }; 300 | -------------------------------------------------------------------------------- /components/admin/RoomReviews.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { useSelector, useDispatch } from "react-redux"; 3 | import { MDBDataTable } from "mdbreact"; 4 | import { toast } from "react-toastify"; 5 | 6 | import { 7 | clearErrors, 8 | deleteReview, 9 | getRoomReviews, 10 | } from "../../redux/actions/roomActions"; 11 | import { DELETE_REVIEW_RESET } from "../../redux/constants/roomConstants"; 12 | 13 | export const RoomReviews = () => { 14 | const [roomId, setRoomId] = useState(); 15 | 16 | const dispatch = useDispatch(); 17 | 18 | const { loading, error, reviews } = useSelector((state) => state.roomReviews); 19 | const { error: deleteError, isDeleted } = useSelector( 20 | (state) => state.review 21 | ); 22 | 23 | useEffect(() => { 24 | if (error) { 25 | toast.error(error); 26 | dispatch(clearErrors()); 27 | } 28 | 29 | if (roomId !== "") { 30 | dispatch(getRoomReviews(roomId)); 31 | } 32 | 33 | if (deleteError) { 34 | toast.error(deleteError); 35 | dispatch(clearErrors()); 36 | } 37 | 38 | if (isDeleted) { 39 | toast.success("Review is deleted"); 40 | dispatch({ type: DELETE_REVIEW_RESET }); 41 | } 42 | }, [dispatch, error, roomId, deleteError, isDeleted]); 43 | 44 | const setReviews = () => { 45 | const data = { 46 | columns: [ 47 | { 48 | label: "Review ID", 49 | field: "id", 50 | sort: "asc", 51 | }, 52 | { 53 | label: "Rating", 54 | field: "rating", 55 | sort: "asc", 56 | }, 57 | { 58 | label: "Comment", 59 | field: "comment", 60 | sort: "asc", 61 | }, 62 | { 63 | label: "User", 64 | field: "user", 65 | sort: "asc", 66 | }, 67 | { 68 | label: "Actions", 69 | field: "actions", 70 | sort: "asc", 71 | }, 72 | ], 73 | rows: [], 74 | }; 75 | 76 | reviews && 77 | reviews.forEach((review) => { 78 | data.rows.push({ 79 | id: review._id, 80 | rating: review.rating, 81 | comment: review.comment, 82 | user: review.name, 83 | actions: ( 84 | 90 | ), 91 | }); 92 | }); 93 | 94 | return data; 95 | }; 96 | 97 | const deleteReviewHandler = (id) => { 98 | dispatch(deleteReview(id, roomId)); 99 | }; 100 | 101 | return ( 102 |
103 |
104 |
105 |
106 |
107 | 108 | setRoomId(e.target.value)} 114 | /> 115 |
116 |
117 |
118 |
119 | 120 | {reviews && reviews.length > 0 ? ( 121 | 128 | ) : ( 129 |
No Reviews
130 | )} 131 |
132 | ); 133 | }; 134 | -------------------------------------------------------------------------------- /components/admin/UpdateRoom.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { useRouter } from "next/dist/client/router"; 3 | import { useSelector, useDispatch } from "react-redux"; 4 | import { toast } from "react-toastify"; 5 | 6 | import { 7 | clearErrors, 8 | getRoomDetails, 9 | updateRoom, 10 | } from "../../redux/actions/roomActions"; 11 | import { UPDATE_ROOM_RESET } from "../../redux/constants/roomConstants"; 12 | import { ButtonLoader } from "../layouts/ButtonLoader"; 13 | import { Loader } from "../layouts/Loader"; 14 | 15 | export const UpdateRoom = () => { 16 | const [name, setName] = useState(""); 17 | const [price, setPrice] = useState(0); 18 | const [description, setDescription] = useState(""); 19 | const [address, setAddress] = useState(""); 20 | const [category, setCategory] = useState("King"); 21 | const [guestCapacity, setGuestCapacity] = useState(1); 22 | const [numOfBeds, setNumOfBeds] = useState(1); 23 | const [internet, setInternet] = useState(false); 24 | const [breakfast, setBreakfast] = useState(false); 25 | const [airConditioned, setAirConditioned] = useState(false); 26 | const [petsAllowed, setPetsAllowed] = useState(false); 27 | const [roomCleaning, setRoomCleaning] = useState(false); 28 | const [images, setImages] = useState([]); 29 | const [oldImages, setOldImages] = useState([]); 30 | const [imagesPreview, setImagesPreview] = useState([]); 31 | 32 | const dispatch = useDispatch(); 33 | const router = useRouter(); 34 | 35 | const { loading, error, isUpdated } = useSelector((state) => state.room); 36 | const { 37 | loading: roomDetailsLoading, 38 | error: roomDetailsError, 39 | room, 40 | } = useSelector((state) => state.roomDetails); 41 | 42 | const { id } = router.query; 43 | 44 | useEffect(() => { 45 | if (room && room._id !== id) { 46 | dispatch(getRoomDetails("", id)); 47 | } else { 48 | setName(room.name); 49 | setPrice(room.pricePerNight); 50 | setDescription(room.description); 51 | setAddress(room.address); 52 | setCategory(room.category); 53 | setGuestCapacity(room.guestCapacity); 54 | setNumOfBeds(room.numOfBeds); 55 | setInternet(room.internet); 56 | setBreakfast(room.breakfast); 57 | setAirConditioned(room.airConditioned); 58 | setPetsAllowed(room.petsAllowed); 59 | setRoomCleaning(room.roomCleaning); 60 | setOldImages(room.images); 61 | } 62 | 63 | if (error) { 64 | toast.error(error); 65 | dispatch(clearErrors()); 66 | } 67 | 68 | if (roomDetailsError) { 69 | toast.roomDetailsError(error); 70 | dispatch(clearErrors()); 71 | } 72 | 73 | if (isUpdated) { 74 | dispatch(getRoomDetails("", id)); 75 | router.push("/admin/rooms"); 76 | dispatch({ type: UPDATE_ROOM_RESET }); 77 | } 78 | }, [dispatch, error, roomDetailsError, isUpdated, room, id]); 79 | 80 | const submitHandler = (e) => { 81 | e.preventDefault(); 82 | 83 | const roomData = { 84 | name, 85 | pricePerNight: price, 86 | description, 87 | address, 88 | category, 89 | guestCapacity: Number(guestCapacity), 90 | numOfBeds: Number(numOfBeds), 91 | internet, 92 | breakfast, 93 | airConditioned, 94 | petsAllowed, 95 | roomCleaning, 96 | }; 97 | 98 | if (images.length !== 0) roomData.images = images; 99 | 100 | dispatch(updateRoom(room._id, roomData)); 101 | }; 102 | 103 | const onChange = (e) => { 104 | const files = Array.from(e.target.files); 105 | 106 | setImages([]); 107 | setOldImages([]); 108 | setImagesPreview([]); 109 | 110 | files.forEach((file) => { 111 | const reader = new FileReader(); 112 | 113 | reader.onload = () => { 114 | if (reader.readyState === 2) { 115 | setImages((oldArray) => [...oldArray, reader.result]); 116 | setImagesPreview((oldArray) => [...oldArray, reader.result]); 117 | } 118 | }; 119 | 120 | reader.readAsDataURL(file); 121 | }); 122 | }; 123 | 124 | return ( 125 | <> 126 | {roomDetailsLoading ? ( 127 | 128 | ) : ( 129 |
130 |
131 |
132 |
137 |

Update Room

138 |
139 | 140 | setName(e.target.value)} 146 | required 147 | /> 148 |
149 |
150 | 151 | setPrice(e.target.value)} 157 | required 158 | /> 159 |
160 |
161 | 162 | 170 |
171 |
172 | 173 | setAddress(e.target.value)} 179 | required 180 | /> 181 |
182 | 183 |
184 | 185 | 197 |
198 | 199 |
200 | 201 | 213 |
214 | 215 |
216 | 217 | 229 |
230 | 231 | 232 |
233 | setInternet(e.target.checked)} 239 | checked={internet} 240 | /> 241 | 247 |
248 |
249 | setBreakfast(e.target.checked)} 255 | checked={breakfast} 256 | /> 257 | 263 |
264 |
265 | setAirConditioned(e.target.checked)} 271 | checked={airConditioned} 272 | /> 273 | 279 |
280 |
281 | setPetsAllowed(e.target.checked)} 287 | checked={petsAllowed} 288 | /> 289 | 295 |
296 |
297 | setRoomCleaning(e.target.checked)} 303 | checked={roomCleaning} 304 | /> 305 | 311 |
312 | 313 |
314 | 315 |
316 | 324 | 327 |
328 | 329 | {imagesPreview.map((img) => ( 330 | Images Preview 338 | ))} 339 | 340 | {oldImages && 341 | oldImages.map((img) => ( 342 | Images Preview 350 | ))} 351 |
352 | 359 |
360 |
361 |
362 |
363 | )} 364 | 365 | ); 366 | }; 367 | -------------------------------------------------------------------------------- /components/admin/UpdateUser.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { useRouter } from "next/dist/client/router"; 3 | import { useSelector, useDispatch } from "react-redux"; 4 | import { toast } from "react-toastify"; 5 | 6 | import { Loader } from "../layouts/Loader"; 7 | import { 8 | clearErrors, 9 | getUserDetails, 10 | updateUser, 11 | } from "../../redux/actions/userActions"; 12 | import { UPDATE_USER_RESET } from "../../redux/constants/userConstants"; 13 | 14 | export const UpdateUsers = () => { 15 | const [name, setName] = useState(""); 16 | const [email, setEmail] = useState(""); 17 | const [role, setRole] = useState(""); 18 | 19 | const dispatch = useDispatch(); 20 | const router = useRouter(); 21 | 22 | const { error, isUpdated } = useSelector((state) => state.user); 23 | const { user, loading } = useSelector((state) => state.userDetails); 24 | 25 | const userId = router.query.id; 26 | 27 | useEffect(() => { 28 | if (user && user._id !== userId) { 29 | dispatch(getUserDetails(userId)); 30 | } else { 31 | setName(user.name); 32 | setEmail(user.email); 33 | setRole(user.role); 34 | } 35 | 36 | if (error) { 37 | toast.error(error); 38 | dispatch(clearErrors()); 39 | } 40 | 41 | if (isUpdated) { 42 | router.push("/admin/users"); 43 | dispatch({ type: UPDATE_USER_RESET }); 44 | } 45 | }, [dispatch, isUpdated, userId, user, error]); 46 | 47 | const submitHandler = (e) => { 48 | e.preventDefault(); 49 | 50 | const userData = { 51 | name, 52 | email, 53 | role, 54 | }; 55 | 56 | dispatch(updateUser(user._id, userData)); 57 | }; 58 | 59 | return ( 60 | <> 61 | {loading ? ( 62 | 63 | ) : ( 64 |
65 |
66 |
67 |
68 |

Update User

69 | 70 |
71 | 72 | setName(e.target.value)} 79 | /> 80 |
81 | 82 |
83 | 84 | setEmail(e.target.value)} 91 | /> 92 |
93 | 94 |
95 | 96 | 97 | 107 |
108 | 109 | 115 |
116 |
117 |
118 |
119 | )} 120 | 121 | ); 122 | }; 123 | -------------------------------------------------------------------------------- /components/auth/Login.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import Link from "next/dist/client/link"; 3 | import { toast } from "react-toastify"; 4 | import { signIn } from "next-auth/client"; 5 | import { useRouter } from "next/dist/client/router"; 6 | import { ButtonLoader } from "../layouts/ButtonLoader"; 7 | 8 | export const Login = () => { 9 | const [email, setEmail] = useState(""); 10 | const [password, setPassword] = useState(""); 11 | const [loading, setLoading] = useState(false); 12 | 13 | const router = useRouter(); 14 | 15 | const submitHandler = async (e) => { 16 | e.preventDefault(); 17 | setLoading(true); 18 | 19 | const result = await signIn("credentials", { 20 | redirect: false, 21 | email, 22 | password, 23 | }); 24 | setLoading(false); 25 | 26 | if (result.error) { 27 | toast.error(result.error); 28 | } else { 29 | router.push("/"); 30 | } 31 | }; 32 | 33 | return ( 34 |
35 |
36 |
37 |
38 |

Login

39 |
40 | 41 | setEmail(e.target.value)} 47 | /> 48 |
49 | 50 |
51 | 52 | setPassword(e.target.value)} 58 | /> 59 |
60 | 61 | 62 | Forgot Password? 63 | 64 | 65 | 73 | 74 | 75 | New User? 76 | 77 |
78 |
79 |
80 |
81 | ); 82 | }; 83 | -------------------------------------------------------------------------------- /components/auth/Register.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { toast } from "react-toastify"; 3 | import { useRouter } from "next/dist/client/router"; 4 | import { ButtonLoader } from "../layouts/ButtonLoader"; 5 | import { useDispatch, useSelector } from "react-redux"; 6 | 7 | import { registerUser, clearErrors } from "../../redux/actions/userActions"; 8 | 9 | export const Register = () => { 10 | const dispatch = useDispatch(); 11 | const router = useRouter(); 12 | 13 | const [user, setUser] = useState({ 14 | name: "", 15 | email: "", 16 | password: "", 17 | }); 18 | const [avatar, setAvatar] = useState(""); 19 | const [avatarPreview, setAvatarPreview] = useState( 20 | "/images/default_avatar.png" 21 | ); 22 | 23 | const { name, email, password } = user; 24 | const { success, error, loading } = useSelector((state) => state.auth); 25 | 26 | useEffect(() => { 27 | if (success) { 28 | router.push("/login"); 29 | } 30 | if (error) { 31 | toast.error(error); 32 | dispatch(clearErrors()); 33 | } 34 | }, [dispatch, success, error]); 35 | 36 | const submitHandler = (e) => { 37 | e.preventDefault(); 38 | const userData = { name, email, password, avatar }; 39 | dispatch(registerUser(userData)); 40 | }; 41 | 42 | const onChange = (e) => { 43 | if (e.target.name === "avatar") { 44 | const reader = new FileReader(); 45 | reader.onload = () => { 46 | if (reader.readyState === 2) { 47 | setAvatar(reader.result); 48 | setAvatarPreview(reader.result); 49 | } 50 | }; 51 | reader.readAsDataURL(e.target.files[0]); 52 | } else { 53 | setUser({ ...user, [e.target.name]: e.target.value }); 54 | } 55 | }; 56 | 57 | return ( 58 |
59 |
60 |
61 |
62 |

Join Us

63 | 64 |
65 | 66 | 74 |
75 | 76 |
77 | 78 | 86 |
87 | 88 |
89 | 90 | 98 |
99 | 100 |
101 | 102 |
103 |
104 |
105 | image 110 |
111 |
112 |
113 | 121 | 124 |
125 |
126 |
127 | 128 | 136 |
137 |
138 |
139 |
140 | ); 141 | }; 142 | -------------------------------------------------------------------------------- /components/bookings/BookingDetails.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import Link from "next/dist/client/link"; 3 | import Image from "next/dist/client/image"; 4 | import { useSelector, useDispatch } from "react-redux"; 5 | import { toast } from "react-toastify"; 6 | 7 | import { clearErrors } from "../../redux/actions/bookingActions"; 8 | 9 | export const BookingDetails = () => { 10 | const dispatch = useDispatch(); 11 | const { booking, error } = useSelector((state) => state.bookingDetails); 12 | const { user } = useSelector((state) => state.loadedUser); 13 | 14 | useEffect(() => { 15 | if (error) { 16 | toast.error(error); 17 | dispatch(clearErrors()); 18 | } 19 | }, [dispatch, booking]); 20 | 21 | const isPaid = 22 | booking.paymentInfo && booking.paymentInfo.status === "paid" ? true : false; 23 | 24 | return ( 25 |
26 |
27 |
28 | {booking && booking.room && booking.user && ( 29 | <> 30 |

Booking # {booking._id}

31 | 32 |

User Info

33 |

34 | Name: {booking.user && booking.user.name} 35 |

36 |

37 | Email: {booking.user && booking.user.email} 38 |

39 |

40 | Amount: ${booking.amountPaid} 41 |

42 | 43 |
44 | 45 |

Booking Info

46 |

47 | Check In:{" "} 48 | {new Date(booking.checkInDate).toLocaleString("ja")} 49 |

50 | 51 |

52 | Check Out:{" "} 53 | {new Date(booking.checkOutDate).toLocaleString("ja")} 54 |

55 | 56 |

57 | Days of Stay: {booking.daysOfStay} 58 |

59 | 60 |
61 | 62 |

Payment Status

63 |

64 | {isPaid ? "Paid" : "Not Paid"} 65 |

66 | 67 | {user && user.role === "admin" && ( 68 | <> 69 |

Stripe Payment ID

70 |

71 | {booking.paymentInfo.id} 72 |

73 | 74 | )} 75 | 76 |

Booked Room:

77 | 78 |
79 |
80 |
81 |
82 | {booking.room.name} 88 |
89 | 90 |
91 | 92 | {booking.room.name} 93 | 94 |
95 | 96 |
97 |

${booking.room.pricePerNight}

98 |
99 | 100 |
101 |

{booking.daysOfStay} Day(s)

102 |
103 |
104 |
105 |
106 | 107 | )} 108 |
109 |
110 |
111 | ); 112 | }; 113 | -------------------------------------------------------------------------------- /components/bookings/MyBookings.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import Link from "next/dist/client/link"; 3 | import { useSelector, useDispatch } from "react-redux"; 4 | import { MDBDataTable } from "mdbreact"; 5 | import { toast } from "react-toastify"; 6 | import { createInvoice, download } from "easyinvoice"; 7 | 8 | import { clearErrors } from "../../redux/actions/bookingActions"; 9 | 10 | export const MyBookings = () => { 11 | const dispatch = useDispatch(); 12 | const { bookings, error } = useSelector((state) => state.bookings); 13 | 14 | useEffect(() => { 15 | if (error) { 16 | toast.error(error); 17 | dispatch(clearErrors()); 18 | } 19 | }, [dispatch]); 20 | 21 | const setBookings = () => { 22 | const data = { 23 | columns: [ 24 | { 25 | label: "Booking ID", 26 | field: "id", 27 | }, 28 | { 29 | label: "Check In", 30 | field: "checkIn", 31 | sort: "asc", 32 | }, 33 | { 34 | label: "Check Out", 35 | field: "checkOut", 36 | }, 37 | { 38 | label: "Amount Paid", 39 | field: "amount", 40 | }, 41 | { 42 | label: "Actions", 43 | field: "actions", 44 | }, 45 | ], 46 | rows: [], 47 | }; 48 | 49 | bookings && 50 | bookings.forEach((booking) => { 51 | data.rows.push({ 52 | id: booking._id, 53 | checkIn: new Date(booking.checkInDate).toLocaleString("ja"), 54 | checkOut: new Date(booking.checkOutDate).toLocaleString("ja"), 55 | amount: `${booking.amountPaid}`, 56 | actions: ( 57 | <> 58 | 59 | 60 | 61 | 62 | 63 | 69 | 70 | ), 71 | }); 72 | }); 73 | return data; 74 | }; 75 | 76 | const downloadInvoice = async (booking) => { 77 | const data = { 78 | documentTitle: "Booking INVOICE", 79 | currency: "USD", 80 | taxNotation: "vat", 81 | marginTop: 25, 82 | marginRight: 25, 83 | marginLeft: 25, 84 | marginBottom: 25, 85 | logo: "https://res.cloudinary.com/bookit/image/upload/v1617904918/bookit/bookit_logo_cbgjzv.png", 86 | sender: { 87 | company: "Book IT", 88 | address: "Oshiue 1-1-2 Sumida-ku", 89 | zip: "1310045", 90 | city: "Tokyo", 91 | country: "Japan", 92 | }, 93 | client: { 94 | company: `${booking.user.name}`, 95 | address: `${booking.user.email}`, 96 | zip: "", 97 | city: `Check In: ${new Date(booking.checkInDate).toLocaleString("ja")}`, 98 | country: `Check In: ${new Date(booking.checkOutDate).toLocaleString( 99 | "ja" 100 | )}`, 101 | }, 102 | invoiceNumber: `${booking._id}`, 103 | invoiceDate: `${new Date(Date.now()).toLocaleString("ja")}`, 104 | products: [ 105 | { 106 | quantity: `${booking.daysOfStay}`, 107 | description: `${booking.room.name}`, 108 | tax: 0, 109 | price: booking.room.pricePerNight, 110 | }, 111 | ], 112 | bottomNotice: 113 | "This is auto generated Invoice of your booking on Book IT.", 114 | }; 115 | 116 | const result = await createInvoice(data); 117 | download(`invoice_${booking._id}.pdf`, result.pdf); 118 | }; 119 | 120 | return ( 121 |
122 |

My Bookings

123 | 124 | 131 |
132 | ); 133 | }; 134 | -------------------------------------------------------------------------------- /components/layouts/ButtonLoader.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export const ButtonLoader = () => { 4 | return
; 5 | }; 6 | -------------------------------------------------------------------------------- /components/layouts/Footer.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export const Footer = () => { 4 | return ( 5 |
6 |

Book IT - hidetomo0102

7 |
8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /components/layouts/Header.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import Link from "next/link"; 3 | import { useDispatch, useSelector } from "react-redux"; 4 | import { signOut } from "next-auth/client"; 5 | 6 | import { loadUser } from "../../redux/actions/userActions"; 7 | 8 | export const Header = () => { 9 | const dispatch = useDispatch(); 10 | 11 | const { loading, user } = useSelector((state) => state.loadedUser); 12 | 13 | useEffect(() => { 14 | if (!user) { 15 | dispatch(loadUser()); 16 | } 17 | }, [dispatch, user]); 18 | 19 | const onClickLogout = () => { 20 | signOut(); 21 | }; 22 | 23 | return ( 24 | 115 | ); 116 | }; 117 | -------------------------------------------------------------------------------- /components/layouts/Layout.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Head from "next/head"; 3 | 4 | import { Header } from "./Header"; 5 | import { Footer } from "./Footer"; 6 | import { ToastContainer } from "react-toastify"; 7 | import "react-toastify/dist/ReactToastify.css"; 8 | 9 | export const Layout = ({ 10 | children, 11 | title = "Book Best Hotels for your Holiday", 12 | }) => { 13 | return ( 14 |
15 | 16 | {title} 17 | 18 | 19 |
20 | 21 | {children} 22 |
23 |
24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /components/layouts/Loader.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export const Loader = () => { 4 | return ( 5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /components/layouts/NotFound.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Link from "next/link"; 3 | 4 | export const NotFound = () => { 5 | return ( 6 |
7 |

404

8 |

9 | Page Not Found. Go to Home 10 |

11 |
12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /components/review/ListReviews.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export const ListReviews = ({ reviews }) => { 4 | return ( 5 | <> 6 |
7 |

Reviews:

8 |
9 | 10 | {reviews && 11 | reviews.map((review) => ( 12 |
13 |
14 |
18 |
19 |

by {review.name}

20 |

{review.comment}

21 | 22 |
23 |
24 | ))} 25 |
26 | 27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /components/review/NewReview.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { toast } from "react-toastify"; 3 | import { useRouter } from "next/dist/client/router"; 4 | import { useDispatch, useSelector } from "react-redux"; 5 | 6 | import { Loader } from "../layouts/Loader"; 7 | import { 8 | newReview, 9 | clearErrors, 10 | checkReviewAvailability, 11 | } from "../../redux/actions/roomActions"; 12 | import { NEW_REVIEW_RESET } from "../../redux/constants/roomConstants"; 13 | 14 | export const NewReview = () => { 15 | const [rating, setRating] = useState(0); 16 | const [comment, setComment] = useState(""); 17 | 18 | const dispatch = useDispatch(); 19 | const router = useRouter(); 20 | 21 | const { error, success } = useSelector((state) => state.newReview); 22 | const { reviewAvailable } = useSelector((state) => state.checkReview); 23 | const { id } = router.query; 24 | 25 | useEffect(() => { 26 | if (id !== undefined) { 27 | dispatch(checkReviewAvailability(id)); 28 | } 29 | 30 | if (error) { 31 | toast.error(error); 32 | dispatch(clearErrors()); 33 | } 34 | 35 | if (success) { 36 | toast.success("Review Posted"); 37 | dispatch({ type: NEW_REVIEW_RESET }); 38 | 39 | router.push(`/room/${id}`); 40 | } 41 | }, [dispatch, success, error]); 42 | 43 | const submitHandler = () => { 44 | const reviewData = { rating, comment, roomId: id }; 45 | dispatch(newReview(reviewData)); 46 | }; 47 | 48 | function setUserRatings() { 49 | const stars = document.querySelectorAll(".star"); 50 | 51 | stars.forEach((star, index) => { 52 | star.starValue = index + 1; 53 | 54 | ["click", "mouseover", "mouseout"].forEach(function (e) { 55 | star.addEventListener(e, showRatings); 56 | }); 57 | }); 58 | 59 | function showRatings(e) { 60 | stars.forEach((star, index) => { 61 | if (e.type === "click") { 62 | if (index < this.starValue) { 63 | star.classList.add("red"); 64 | 65 | setRating(this.starValue); 66 | } else { 67 | star.classList.remove("red"); 68 | } 69 | } 70 | 71 | if (e.type === "mouseover") { 72 | if (index < this.starValue) { 73 | star.classList.add("light-red"); 74 | } else { 75 | star.classList.remove("light-red"); 76 | } 77 | } 78 | 79 | if (e.type === "mouseout") { 80 | star.classList.remove("light-red"); 81 | } 82 | }); 83 | } 84 | } 85 | 86 | return ( 87 | <> 88 | {reviewAvailable && ( 89 | 99 | )} 100 | 163 | 164 | ); 165 | }; 166 | -------------------------------------------------------------------------------- /components/room/RoomDetails.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { useSelector, useDispatch } from "react-redux"; 3 | import { toast } from "react-toastify"; 4 | import { Carousel } from "react-bootstrap"; 5 | import { useRouter } from "next/dist/client/router"; 6 | import Head from "next/head"; 7 | import Image from "next/image"; 8 | import axios from "axios"; 9 | import moment from "moment"; 10 | import "react-dates/initialize"; 11 | import "react-dates/lib/css/_datepicker.css"; 12 | import { DateRangePicker } from "react-dates"; 13 | 14 | import { clearErrors } from "../../redux/actions/roomActions"; 15 | import { RoomFeatures } from "./RoomFeatures"; 16 | import { 17 | checkBooking, 18 | getBookedDates, 19 | } from "../../redux/actions/bookingActions"; 20 | import { CHECK_BOOKING_RESET } from "../../redux/constants/bookingConstants"; 21 | import { getStripe } from "../../utils/getStripe"; 22 | import { NewReview } from "../review/NewReview"; 23 | import { ListReviews } from "../review/ListReviews"; 24 | 25 | export const RoomDetails = () => { 26 | const [checkInDate, setCheckInDate] = useState(); 27 | const [checkOutDate, setCheckOutDate] = useState(); 28 | const [focusedInput, setFocusedInput] = useState(); 29 | const [daysOfStay, setDaysOfStay] = useState(); 30 | const [paymentLoading, setPaymentLoading] = useState(false); 31 | 32 | const dispatch = useDispatch(); 33 | const router = useRouter(); 34 | 35 | const { dates } = useSelector((state) => state.bookedDates); 36 | const { user } = useSelector((state) => state.loadedUser); 37 | const { room, error } = useSelector((state) => state.roomDetails); 38 | const { available, loading: bookingLoading } = useSelector( 39 | (state) => state.checkBooking 40 | ); 41 | 42 | const excludedDates = []; 43 | if (dates) { 44 | dates.forEach((date) => { 45 | excludedDates.push(moment(date).format("YYYY-MM-DD")); 46 | }); 47 | } 48 | 49 | const isDayBlocked = (day) => { 50 | return excludedDates.some((date) => day.isSame(date), "day"); 51 | }; 52 | 53 | const onDatesChange = ({ startDate, endDate }) => { 54 | if (focusedInput === "startDate") { 55 | setCheckInDate(startDate); 56 | } else { 57 | setCheckOutDate(endDate); 58 | } 59 | 60 | if (startDate && endDate && endDate.isAfter(startDate)) { 61 | const days = endDate.diff(startDate, "days"); 62 | setDaysOfStay(days); 63 | 64 | dispatch( 65 | checkBooking(id, startDate.toISOString(), endDate.toISOString()) 66 | ); 67 | } 68 | }; 69 | 70 | const { id } = router.query; 71 | 72 | const newBookingHandler = async () => { 73 | const bookingData = { 74 | room: router.query.id, 75 | checkInDate, 76 | checkOutDate, 77 | daysOfStay, 78 | amountPaid: room.pricePerNight * daysOfStay, 79 | paymentInfo: { 80 | id: "STRIPE_PAYMENT_ID", 81 | status: "STRIPE_PAYMENT_STATUS", 82 | }, 83 | }; 84 | 85 | try { 86 | const config = { 87 | headers: { 88 | "Content-Type": "application/json", 89 | }, 90 | }; 91 | 92 | const { data } = await axios.post("/api/bookings", bookingData, config); 93 | } catch (error) { 94 | console.log(error.response); 95 | } 96 | }; 97 | 98 | const bookRoom = async (id, pricePerNight) => { 99 | setPaymentLoading(true); 100 | 101 | const amount = pricePerNight * daysOfStay; 102 | 103 | try { 104 | const link = `/api/checkout_session/${id}?checkInDate=${checkInDate.toISOString()}&checkOutDate=${checkOutDate.toISOString()}&daysOfStay=${daysOfStay}`; 105 | 106 | const { data } = await axios.get(link, { params: { amount } }); 107 | 108 | const stripe = await getStripe(); 109 | stripe.redirectToCheckout({ sessionId: data.id }); 110 | 111 | setPaymentLoading(false); 112 | } catch (error) { 113 | setPaymentLoading(false); 114 | toast.error(error.message); 115 | } 116 | }; 117 | 118 | useEffect(() => { 119 | dispatch(getBookedDates(id)); 120 | toast.error(error); 121 | dispatch(clearErrors()); 122 | return () => { 123 | dispatch({ type: CHECK_BOOKING_RESET }); 124 | }; 125 | }, [dispatch, id]); 126 | 127 | return ( 128 | <> 129 | 130 | {room.name} - BookIT 131 | 132 |
133 |

{room.name}

134 |

{room.address}

135 |
136 |
137 |
141 |
142 | ({room.numOfReviews} Reviews) 143 |
144 | 145 | 146 | {room.images && 147 | room.images.map((image) => ( 148 | 149 |
150 | {room.name} 156 |
157 |
158 | ))} 159 |
160 | 161 |
162 |
163 |

Description

164 |

{room.description}

165 | 166 |
167 | 168 |
169 |
170 |

171 | $ {room.pricePerNight} / night 172 |

173 | 174 |
175 | 176 |

Pick Check In & Out Date

177 | 178 | { 187 | setFocusedInput(focusedInput); 188 | }} 189 | onDatesChange={onDatesChange} 190 | /> 191 | 192 | {available === true && ( 193 |
194 | Room is available. Book now 195 |
196 | )} 197 | {available === false && ( 198 |
199 | Room not available. Try different dates 200 |
201 | )} 202 | 203 | {available && !user && ( 204 |
205 | Login to book room. 206 |
207 | )} 208 | 209 | {available && user && ( 210 | 224 | )} 225 |
226 |
227 |
228 | 229 | 230 | 231 | {room.reviews && room.reviews.length > 0 ? ( 232 | 233 | ) : ( 234 |

235 | No Reviews on this room 236 |

237 | )} 238 |
239 | 240 | ); 241 | }; 242 | -------------------------------------------------------------------------------- /components/room/RoomFeatures.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { RoomOption } from "./RoomOption"; 3 | 4 | export const RoomFeatures = ({ room }) => { 5 | return ( 6 |
7 |

Features:

8 |
9 | 10 |

{room.guestCapacity} Guests

11 |
12 | 13 |
14 | 15 |

{room.numOfBeds} Beds

16 |
17 | 18 | 19 | 20 | 21 |
22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /components/room/RoomItem.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Image from "next/image"; 3 | import Link from "next/link"; 4 | 5 | export const RoomItem = ({ room }) => { 6 | return ( 7 |
8 |
9 | 15 |
16 |
17 | 18 | {room.name} 19 | 20 |
21 |
22 |

23 | ${room.pricePerNight} / night 24 |

25 |
26 |
30 |
31 | ({room.numOfReviews} Reviews) 32 |
33 | 36 |
37 |
38 |
39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /components/room/RoomOption.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export const RoomOption = ({ option, text }) => { 4 | return ( 5 |
6 | 12 |

{text}

13 |
14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /components/user/ForgotPassword.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { toast } from "react-toastify"; 3 | 4 | import { ButtonLoader } from "../layouts/ButtonLoader"; 5 | import { useDispatch, useSelector } from "react-redux"; 6 | import { forgotPassword, clearErrors } from "../../redux/actions/userActions"; 7 | 8 | export const ForgotPassword = () => { 9 | const dispatch = useDispatch(); 10 | const { error, loading, message } = useSelector( 11 | (state) => state.forgotPassword 12 | ); 13 | 14 | const [email, setEmail] = useState(""); 15 | 16 | useEffect(() => { 17 | if (error) { 18 | toast.error(error); 19 | dispatch(clearErrors()); 20 | } 21 | 22 | if (message) { 23 | toast.success(message); 24 | } 25 | }, [dispatch, message, error]); 26 | 27 | const submitHandler = (e) => { 28 | e.preventDefault(); 29 | const userData = { email }; 30 | dispatch(forgotPassword(userData)); 31 | }; 32 | 33 | return ( 34 |
35 |
36 |
37 |

Forgot Password

38 |
39 | 40 | setEmail(e.target.value)} 46 | /> 47 |
48 | 49 | 57 |
58 |
59 |
60 | ); 61 | }; 62 | -------------------------------------------------------------------------------- /components/user/NewPassword.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { toast } from "react-toastify"; 3 | import { useDispatch, useSelector } from "react-redux"; 4 | import { useRouter } from "next/dist/client/router"; 5 | 6 | import { ButtonLoader } from "../layouts/ButtonLoader"; 7 | import { resetPassword, clearErrors } from "../../redux/actions/userActions"; 8 | 9 | export const NewPassword = () => { 10 | const [password, setPassword] = useState(""); 11 | const [confirmPassword, setConfirmPassword] = useState(""); 12 | 13 | const dispatch = useDispatch(); 14 | const router = useRouter(); 15 | 16 | const { error, loading, success } = useSelector( 17 | (state) => state.forgotPassword 18 | ); 19 | 20 | useEffect(() => { 21 | if (error) { 22 | toast.error(error); 23 | dispatch(clearErrors()); 24 | } 25 | 26 | if (success) { 27 | router.push("/login"); 28 | } 29 | }, [dispatch, success, error]); 30 | 31 | const submitHandler = (e) => { 32 | e.preventDefault(); 33 | 34 | const passwords = { 35 | password, 36 | confirmPassword, 37 | }; 38 | 39 | dispatch(resetPassword(router.query.token, passwords)); 40 | }; 41 | 42 | return ( 43 |
44 |
45 |
46 |

New Password

47 | 48 |
49 | 50 | setPassword(e.target.value)} 56 | /> 57 |
58 | 59 |
60 | 61 | setConfirmPassword(e.target.value)} 67 | /> 68 |
69 | 70 | 78 |
79 |
80 |
81 | ); 82 | }; 83 | -------------------------------------------------------------------------------- /components/user/Profile.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { toast } from "react-toastify"; 3 | import { useRouter } from "next/dist/client/router"; 4 | import { ButtonLoader } from "../layouts/ButtonLoader"; 5 | import { useDispatch, useSelector } from "react-redux"; 6 | 7 | import { updateProfile, clearErrors } from "../../redux/actions/userActions"; 8 | import { UPDATE_PROFILE_RESET } from "../../redux/constants/userConstants"; 9 | import { Loader } from "../layouts/Loader"; 10 | 11 | export const Profile = () => { 12 | const dispatch = useDispatch(); 13 | const router = useRouter(); 14 | 15 | const [user, setUser] = useState({ 16 | name: "", 17 | email: "", 18 | password: "", 19 | }); 20 | const { name, email, password } = user; 21 | 22 | const [avatar, setAvatar] = useState(""); 23 | const [avatarPreview, setAvatarPreview] = useState( 24 | "/images/default_avatar.png" 25 | ); 26 | 27 | const { user: loadedUser, loading } = useSelector((state) => state.auth); 28 | const { 29 | error, 30 | isUpdated, 31 | loading: updateLoading, 32 | } = useSelector((state) => state.user); 33 | 34 | useEffect(() => { 35 | if (loadedUser) { 36 | setUser({ 37 | name: loadedUser.name, 38 | email: loadedUser.email, 39 | password: loadedUser.password, 40 | }); 41 | setAvatarPreview(loadedUser.avatar.url); 42 | } 43 | 44 | if (error) { 45 | toast.error(error); 46 | dispatch(clearErrors()); 47 | } 48 | 49 | if (isUpdated) { 50 | router.push("/"); 51 | dispatch({ type: UPDATE_PROFILE_RESET }); 52 | } 53 | }, [dispatch, isUpdated, error, loadedUser]); 54 | 55 | const submitHandler = (e) => { 56 | e.preventDefault(); 57 | const userData = { name, email, password, avatar }; 58 | dispatch(updateProfile(userData)); 59 | }; 60 | 61 | const onChange = (e) => { 62 | if (e.target.name === "avatar") { 63 | const reader = new FileReader(); 64 | reader.onload = () => { 65 | if (reader.readyState === 2) { 66 | setAvatar(reader.result); 67 | setAvatarPreview(reader.result); 68 | } 69 | }; 70 | reader.readAsDataURL(e.target.files[0]); 71 | } else { 72 | setUser({ ...user, [e.target.name]: e.target.value }); 73 | } 74 | }; 75 | 76 | return ( 77 | <> 78 | {loading ? ( 79 | 80 | ) : ( 81 |
82 |
83 |
84 |
85 |

Update Profile

86 | 87 |
88 | 89 | 97 |
98 | 99 |
100 | 101 | 109 |
110 | 111 |
112 | 113 | 121 |
122 | 123 |
124 | 125 |
126 |
127 |
128 | image 133 |
134 |
135 |
136 | 144 | 147 |
148 |
149 |
150 | 151 | 159 |
160 |
161 |
162 |
163 | )} 164 | 165 | ); 166 | }; 167 | -------------------------------------------------------------------------------- /config/dbConnect.js: -------------------------------------------------------------------------------- 1 | import { connect, connection } from "mongoose"; 2 | 3 | export const dbConnect = () => { 4 | if (connection.readyState > 1) { 5 | return; 6 | } 7 | connect(process.env.DB_URI, { 8 | useNewUrlParser: true, 9 | }); 10 | }; 11 | -------------------------------------------------------------------------------- /controllers/authControllers.js: -------------------------------------------------------------------------------- 1 | import cloudinary from "cloudinary"; 2 | import absoluteUrl from "next-absolute-url"; 3 | import crypto from "crypto"; 4 | 5 | import User from "../models/user"; 6 | import ErrorHandler from "../utils/errorHandler"; 7 | import catchAsyncError from "../middlewares/catchAsyncError"; 8 | import { sendEmail } from "../utils/sendEmail"; 9 | 10 | // SetUp cloudinary config 11 | cloudinary.config({ 12 | cloud_name: process.env.CLOUDINARY_CLOUD_NAME, 13 | api_key: process.env.CLOUDINARY_API_KEY, 14 | api_secret: process.env.CLOUDINARY_API_SECRET, 15 | }); 16 | 17 | // Register user => (POST) /api/auth/register 18 | export const registerUser = catchAsyncError(async (req, res) => { 19 | const result = await cloudinary.v2.uploader.upload(req.body.avatar, { 20 | folder: "bookit/avatars", 21 | width: "150", 22 | crop: "scale", 23 | }); 24 | 25 | const { name, email, password } = req.body; 26 | 27 | const user = await User.create({ 28 | name, 29 | email, 30 | password, 31 | avatar: { 32 | public_id: result.public_id, 33 | url: result.secure_url, 34 | }, 35 | }); 36 | 37 | res.status(200).json({ 38 | success: true, 39 | message: "Account Registered Successfully", 40 | }); 41 | }); 42 | 43 | // Current user profile => (GET) /api/me 44 | export const currentUserProfile = catchAsyncError(async (req, res) => { 45 | const user = await User.findById(req.user._id); 46 | 47 | res.status(200).json({ 48 | success: true, 49 | user, 50 | }); 51 | }); 52 | 53 | // Update user profile => (PUT) /api/me/update 54 | export const updateProfile = catchAsyncError(async (req, res) => { 55 | const user = await User.findById(req.user._id); 56 | 57 | if (user) { 58 | user.name = req.body.name; 59 | user.email = req.body.email; 60 | 61 | if (req.body.password) user.password = req.body.password; 62 | } 63 | 64 | if (req.body.avatar !== "") { 65 | const image_id = user.avatar.public_id; 66 | // Delete user previous image/avatar 67 | await cloudinary.v2.uploader.destroy(image_id); 68 | 69 | const result = await cloudinary.v2.uploader.upload(req.body.avatar, { 70 | folder: "bookit/avatars", 71 | width: "150", 72 | crop: "scale", 73 | }); 74 | 75 | user.avatar = { 76 | public_id: result.public_id, 77 | url: result.secure_url, 78 | }; 79 | } 80 | 81 | await user.save(); 82 | 83 | res.status(200).json({ 84 | success: true, 85 | }); 86 | }); 87 | 88 | // Forgot password => (POST) /api/password/forgot 89 | export const forgotPassword = catchAsyncError(async (req, res, next) => { 90 | const user = await User.findOne({ email: req.body.email }); 91 | 92 | // Get reset token 93 | const resetToken = user.getResetPasswordToken(); 94 | await user.save({ validateBeforSave: false }); 95 | 96 | const { origin } = absoluteUrl(req); 97 | 98 | // Create reset password url 99 | const resetUrl = `${origin}/password/reset/${resetToken}`; 100 | 101 | const message = `Your message reset url is as follow: \n\n ${resetUrl} \n\n If you have not requested this email, you can ignore it.`; 102 | 103 | try { 104 | await sendEmail({ 105 | email: user.email, 106 | subject: "BookIT Password Recovery", 107 | message, 108 | }); 109 | res.status(200).json({ 110 | success: true, 111 | message: `Email sent to: ${user.email}`, 112 | }); 113 | } catch (error) { 114 | user.resetPasswordToken = undefined; 115 | user.resetPasswordExpire = undefined; 116 | 117 | await user.save({ validateBeforSave: false }); 118 | return next(new ErrorHandler(error.message, 500)); 119 | } 120 | }); 121 | 122 | // Reset password => (POST) /api/password/reset/:token 123 | export const resetPassword = catchAsyncError(async (req, res, next) => { 124 | const resetPassword = crypto 125 | .createHash("sha256") 126 | .update(req.query.token) 127 | .digest("hex"); 128 | 129 | const user = await User.findOne({ 130 | resetPassword, 131 | resetPasswordExpire: { $gt: Date.now() }, 132 | }); 133 | 134 | if (!user) { 135 | return next( 136 | new ErrorHandler( 137 | "Password reset token is invalid or has been expired", 138 | 400 139 | ) 140 | ); 141 | } 142 | 143 | if (req.body.password !== req.body.confirmPassword) { 144 | return next(new ErrorHandler("Password does not match", 400)); 145 | } 146 | 147 | // Setup the new password 148 | user.password = req.body.password; 149 | 150 | user.resetPasswordToken = undefined; 151 | user.resetPasswordExpire = undefined; 152 | 153 | await user.save(); 154 | 155 | res.status(200).json({ 156 | success: true, 157 | message: "Password updated successfully", 158 | }); 159 | }); 160 | 161 | // Get all users => (GET) /api/admin/users 162 | export const allAdminUsers = catchAsyncError(async (req, res) => { 163 | const users = await User.find(); 164 | 165 | res.status(200).json({ 166 | success: true, 167 | users, 168 | }); 169 | }); 170 | 171 | // Get user details => (GET) /api/admin/users/:id 172 | export const getUserDetails = catchAsyncError(async (req, res, next) => { 173 | const user = await User.findById(req.query.id); 174 | 175 | if (!user) { 176 | return next(new ErrorHandler("User not found with this ID", 400)); 177 | } 178 | 179 | res.status(200).json({ 180 | success: true, 181 | user, 182 | }); 183 | }); 184 | 185 | // Update user details => (GET) /api/admin/users/:id 186 | export const updateUser = catchAsyncError(async (req, res) => { 187 | const newUserData = { 188 | name: req.body.name, 189 | email: req.body.email, 190 | role: req.body.role, 191 | }; 192 | 193 | const user = await User.findByIdAndUpdate(req.query.id, newUserData, { 194 | new: true, 195 | runValidators: true, 196 | }); 197 | 198 | res.status(200).json({ 199 | success: true, 200 | }); 201 | }); 202 | 203 | // Delete user => (DELETE) /api/admin/users/:id 204 | export const deleteUser = catchAsyncError(async (req, res, next) => { 205 | const user = await User.findById(req.query.id); 206 | 207 | if (!user) { 208 | return next(new ErrorHandler("User not found with this ID", 400)); 209 | } 210 | 211 | // Remove avatar 212 | const image_id = user.avatar.public_id; 213 | await cloudinary.v2.uploader.destroy(image_id); 214 | 215 | await user.remove(); 216 | 217 | res.status(200).json({ 218 | success: true, 219 | user, 220 | }); 221 | }); 222 | -------------------------------------------------------------------------------- /controllers/bookingControllers.js: -------------------------------------------------------------------------------- 1 | import Moment from "moment"; 2 | import { extendMoment } from "moment-range"; 3 | 4 | import Booking from "../models/booking"; 5 | import ErrorHandler from "../utils/errorHandler"; 6 | import catchAsyncError from "../middlewares/catchAsyncError"; 7 | 8 | const moment = extendMoment(Moment); 9 | 10 | // Create new booking => (POST) /api/bookings 11 | export const newBooking = catchAsyncError(async (req, res) => { 12 | const { 13 | room, 14 | checkInDate, 15 | checkOutDate, 16 | daysOfStay, 17 | amountPaid, 18 | paymentInfo, 19 | } = req.body; 20 | 21 | const booking = await Booking.create({ 22 | room, 23 | user: req.user._id, 24 | checkInDate, 25 | checkOutDate, 26 | daysOfStay, 27 | amountPaid, 28 | paymentInfo, 29 | paidAt: Date.now(), 30 | }); 31 | 32 | res.status(200).json({ 33 | success: true, 34 | booking, 35 | }); 36 | }); 37 | 38 | // Check room booking => (GET) /api/bookings/check 39 | export const checkRoomBookingAvailability = catchAsyncError( 40 | async (req, res) => { 41 | let { roomId, checkInDate, checkOutDate } = req.query; 42 | 43 | checkInDate = new Date(checkInDate); 44 | checkOutDate = new Date(checkOutDate); 45 | 46 | const bookings = await Booking.find({ 47 | room: roomId, 48 | $and: [ 49 | { 50 | checkInDate: { 51 | $lte: checkOutDate, 52 | }, 53 | }, 54 | { 55 | checkOutDate: { 56 | $gte: checkInDate, 57 | }, 58 | }, 59 | ], 60 | }); 61 | 62 | // check if there's any booking available 63 | let isAvailable; 64 | 65 | if (bookings && bookings.length === 0) { 66 | isAvailable = true; 67 | } else { 68 | isAvailable = false; 69 | } 70 | 71 | res.status(200).json({ 72 | success: true, 73 | isAvailable, 74 | }); 75 | } 76 | ); 77 | 78 | // Check booked dates => (GET) /api/bookings/check_booked_dates 79 | export const checkBookedDates = catchAsyncError(async (req, res) => { 80 | let { roomId } = req.query; 81 | 82 | const bookings = await Booking.find({ room: roomId }); 83 | 84 | let bookedDates = []; 85 | 86 | const timeDifference = moment().utcOffset() / 60; 87 | 88 | bookings.forEach((booking) => { 89 | const checkInDate = moment(booking.checkInDate).add( 90 | timeDifference, 91 | "hours" 92 | ); 93 | const checkOutDate = moment(booking.checkOutDate).add( 94 | timeDifference, 95 | "hours" 96 | ); 97 | 98 | const range = moment.range(moment(checkInDate), moment(checkOutDate)); 99 | 100 | const dates = Array.from(range.by("day")); 101 | bookedDates = bookedDates.concat(dates); 102 | }); 103 | 104 | res.status(200).json({ 105 | success: true, 106 | bookedDates, 107 | }); 108 | }); 109 | 110 | // Get all bookings of user => (GET) /api/bookings/me 111 | export const myBookings = catchAsyncError(async (req, res) => { 112 | const bookings = await Booking.find({ user: req.user._id }) 113 | .populate({ 114 | path: "room", 115 | select: "name pricePerNight images", 116 | }) 117 | .populate({ 118 | path: "user", 119 | select: "name email", 120 | }); 121 | 122 | res.status(200).json({ 123 | success: true, 124 | bookings, 125 | }); 126 | }); 127 | 128 | // Get booking details => (GET) /api/bookings/:id 129 | export const getBookingDetails = catchAsyncError(async (req, res) => { 130 | const booking = await Booking.findById(req.query.id) 131 | .populate({ 132 | path: "room", 133 | select: "name pricePerNight images", 134 | }) 135 | .populate({ 136 | path: "user", 137 | select: "name email", 138 | }); 139 | 140 | res.status(200).json({ 141 | success: true, 142 | booking, 143 | }); 144 | }); 145 | 146 | // Get all bookings - ADMIN => /api/admin/bookings 147 | export const allAdminBookings = catchAsyncError(async (req, res) => { 148 | const bookings = await Booking.find() 149 | .populate({ 150 | path: "room", 151 | select: "name pricePerNight images", 152 | }) 153 | .populate({ 154 | path: "user", 155 | select: "name email", 156 | }); 157 | 158 | res.status(200).json({ 159 | success: true, 160 | bookings, 161 | }); 162 | }); 163 | 164 | // Delete booking - ADMIN => /api/admin/bookings/:id 165 | export const deleteAdminBookings = catchAsyncError(async (req, res, next) => { 166 | const booking = await Booking.findById(req.query.id); 167 | 168 | if (!booking) { 169 | return next(new ErrorHandler("Booking is not found with this ID", 404)); 170 | } 171 | 172 | await booking.romove(); 173 | 174 | res.status(200).json({ 175 | success: true, 176 | }); 177 | }); 178 | -------------------------------------------------------------------------------- /controllers/paymentControllers.js: -------------------------------------------------------------------------------- 1 | import absoluteUrl from "next-absolute-url"; 2 | import Stripe from "stripe"; 3 | import getRawBody from "raw-body"; 4 | 5 | import Room from "../models/room"; 6 | import User from "../models/user"; 7 | import Booking from "../models/booking"; 8 | import catchAsyncError from "../middlewares/catchAsyncError"; 9 | 10 | const stripe_secret_key = `${process.env.STRIPE_SECRET_KEY}`; 11 | const stripe = new Stripe(stripe_secret_key, {}); 12 | 13 | // Generate stripe checkout session => (GET) /api/checkout_session/:roomId 14 | export const stripeCheckoutSession = catchAsyncError(async (req, res) => { 15 | // Get room details 16 | const room = await Room.findById(req.query.roomId); 17 | 18 | const { checkInDate, checkOutDate, daysOfStay } = req.query; 19 | 20 | // Get origin 21 | const { origin } = absoluteUrl(req); 22 | 23 | // Create stripe checkout session 24 | const session = await stripe.checkout.sessions.create({ 25 | payment_method_types: ["card"], 26 | success_url: `${origin}/bookings/me`, 27 | cancel_url: `${origin}/room/${room._id}`, 28 | customer_email: req.user.email, 29 | client_reference_id: req.query.roomId, 30 | metadata: { checkInDate, checkOutDate, daysOfStay }, 31 | line_items: [ 32 | { 33 | name: room.name, 34 | images: [`${room.images[0].url}`], 35 | amount: req.query.amount * 100, 36 | currency: "usd", 37 | quantity: 1, 38 | }, 39 | ], 40 | }); 41 | 42 | res.status(200).json(session); 43 | }); 44 | 45 | // Create new booking after payment => (POST) /api/webhook 46 | export const webhookCheckout = catchAsyncError(async (req, res) => { 47 | const rawBody = await getRawBody(req); 48 | try { 49 | const signature = req.headers["stripe-signature"]; 50 | const event = stripe.webhooks.constructEvent( 51 | rawBody, 52 | signature, 53 | process.env.STRIPE_WEBHOOK_SECRET 54 | ); 55 | 56 | if (event.type === "checkout.session.completed") { 57 | const session = event.data.object; 58 | const room = session.client_reference_id; 59 | const user = await User.findOne({ email: session.customer_email }).id; 60 | const amountPaid = session.amount_total / 100; 61 | const paymentInfo = { 62 | id: session.payment_intent, 63 | status: session.payment_status, 64 | }; 65 | const checkInDate = session.checkInDate; 66 | const checkOutDate = session.checkOutDate; 67 | const daysOfStay = session.metadata.daysOfStay; 68 | 69 | const booking = await Booking.create({ 70 | room, 71 | user, 72 | checkInDate, 73 | checkOutDate, 74 | daysOfStay, 75 | amountPaid, 76 | paymentInfo, 77 | paidAt: Date.now(), 78 | }); 79 | 80 | res.status(200).json({ success: true }); 81 | } 82 | } catch (error) { 83 | console.log(error); 84 | } 85 | }); 86 | -------------------------------------------------------------------------------- /controllers/roomControllers.js: -------------------------------------------------------------------------------- 1 | import cloudinary from "cloudinary"; 2 | 3 | import Room from "../models/room"; 4 | import Booking from "../models/booking"; 5 | import ErrorHandler from "../utils/errorHandler"; 6 | import catchAsyncError from "../middlewares/catchAsyncError"; 7 | import APIFeatures from "../utils/apiFeatures"; 8 | 9 | // Get All Rooms => (GET) /api/rooms/ 10 | export const getAllRooms = catchAsyncError(async (req, res) => { 11 | const resPerPage = 4; 12 | const roomsCount = await Room.countDocuments(); 13 | 14 | const apiFeatures = new APIFeatures(Room.find(), req.query).search().filter(); 15 | 16 | let rooms = await apiFeatures.query; 17 | let filteredRoomsCount = rooms.length; 18 | 19 | apiFeatures.pagination(resPerPage); 20 | rooms = await apiFeatures.query.clone(); 21 | 22 | res.status(200).json({ 23 | success: true, 24 | roomsCount, 25 | resPerPage, 26 | filteredRoomsCount, 27 | rooms, 28 | }); 29 | }); 30 | 31 | // Create New Room => (POST) /api/rooms 32 | export const createNewRoom = catchAsyncError(async (req, res) => { 33 | const images = req.body.images; 34 | 35 | let imagesLinks = []; 36 | 37 | for (let i = 0; i < images.length; i++) { 38 | const result = await cloudinary.v2.uploader.upload(images[i], { 39 | folder: "bookit/rooms", 40 | }); 41 | 42 | imagesLinks.push({ 43 | public_id: result.public_id, 44 | url: result.secure_url, 45 | }); 46 | } 47 | 48 | req.body.image = imagesLinks; 49 | req.body.user = req.user._id; 50 | 51 | const room = await Room.create(req.body); 52 | res.status(200).json({ 53 | success: true, 54 | room, 55 | }); 56 | }); 57 | 58 | // Get Room Details => (GET) /api/rooms/:id 59 | export const getSingleRoom = catchAsyncError(async (req, res) => { 60 | const room = await Room.findById(req.query.id); 61 | if (!room) { 62 | return next(new ErrorHandler("Room not found with this ID", 404)); 63 | } 64 | res.status(200).json({ 65 | success: true, 66 | room, 67 | }); 68 | }); 69 | 70 | // Update Room => (PUT) /api/rooms/:id 71 | export const updateRoom = catchAsyncError(async (req, res) => { 72 | let room = await Room.findById(req.query.id); 73 | if (!room) { 74 | return next(new ErrorHandler("Room not found with this ID", 400)); 75 | } 76 | 77 | if (req.body.images) { 78 | for (let i = 0; i < room.images.length; i++) { 79 | await cloudinary.v2.uploader.destroy(room.images[i].public_id); 80 | } 81 | 82 | let imagesLinks = []; 83 | const images = req.body.images; 84 | 85 | for (let i = 0; i < images.length; i++) { 86 | const result = await cloudinary.v2.uploader.upload(images[i], { 87 | folder: "bookit/rooms", 88 | }); 89 | imagesLinks.push({ 90 | public_id: result.public_id, 91 | url: result.secure_url, 92 | }); 93 | } 94 | } 95 | 96 | room = await Room.findByIdAndUpdate(req.query.id, req.body, { 97 | new: true, 98 | runValidators: true, 99 | }); 100 | 101 | res.status(200).json({ 102 | success: true, 103 | room, 104 | }); 105 | }); 106 | 107 | // Delete Room => (DELETE) /api/rooms/:id 108 | export const deleteRoom = catchAsyncError(async (req, res) => { 109 | const room = await Room.findById(req.query.id); 110 | if (!room) { 111 | return next(new ErrorHandler("Room not found with this ID", 400)); 112 | } 113 | 114 | for (let i = 0; i < room.images.length; i++) { 115 | await cloudinary.v2.uploader.destroy(room.images[i].public_id); 116 | } 117 | 118 | await room.remove(); 119 | 120 | res.status(200).json({ 121 | success: true, 122 | message: "Room is deleted", 123 | }); 124 | }); 125 | 126 | // Create new review => (POST) /api/review/ 127 | export const createNewReview = catchAsyncError(async (req, res) => { 128 | const { rating, comment, roomId } = req.body; 129 | 130 | const review = { 131 | user: req.user._id, 132 | name: req.user.name, 133 | rating: Number(rating), 134 | comment, 135 | }; 136 | 137 | const room = await Room.findById(roomId); 138 | 139 | const isReviewed = room.reviews.find( 140 | (r) => r.user.toString() === req.user._id.toString() 141 | ); 142 | 143 | if (isReviewed) { 144 | room.reviews.forEach((review) => { 145 | if (review.user.toString() === req.user._id.toString()) { 146 | review.comment = comment; 147 | review.rating = rating; 148 | } 149 | }); 150 | } else { 151 | room.reviews.push(review); 152 | room.numOfReviews = room.reviews.length; 153 | } 154 | 155 | room.ratings = 156 | room.reviews.reduce((acc, item) => item.rating + acc, 0) / 157 | room.reviews.length; 158 | 159 | await room.save({ validateBeforeSave: false }); 160 | 161 | res.status(200).json({ 162 | success: true, 163 | }); 164 | }); 165 | 166 | // Check Review Availability => (GET) /api/reviews/check_review_availability 167 | export const checkReviewAvailability = catchAsyncError(async (req, res) => { 168 | const { roomId } = req.query; 169 | 170 | const bookings = await Booking.find({ user: req.user._id, room: roomId }); 171 | 172 | let isReviewAvailable = false; 173 | if (bookings.length > 0) isReviewAvailable = true; 174 | 175 | res.status(200).json({ 176 | success: true, 177 | isReviewAvailable, 178 | }); 179 | }); 180 | 181 | // Get all rooms - Admin => (GET) /api/admin/rooms 182 | export const allAdminRooms = catchAsyncError(async (req, res) => { 183 | const rooms = await Room.find(); 184 | 185 | res.status(200).json({ 186 | success: true, 187 | rooms, 188 | }); 189 | }); 190 | 191 | // Get all Review - Admin => (GET) /api/reviews 192 | export const getAdminReviews = catchAsyncError(async (req, res) => { 193 | const room = await Room.findById(req.query.id); 194 | 195 | res.status(200).json({ 196 | success: true, 197 | reviews: room.reviews, 198 | }); 199 | }); 200 | 201 | // Delete room review - ADMIN => /api/reviews 202 | export const deleteReview = catchAsyncError(async (req, res) => { 203 | const room = await Room.findById(req.query.roomId); 204 | 205 | const reviews = room.reviews.filter( 206 | (review) => review._id.toString() !== req.query.id.toString() 207 | ); 208 | 209 | const numOfReviews = reviews.length; 210 | 211 | const ratings = 212 | room.reviews.reduce((acc, item) => item.rating + acc, 0) / reviews.length; 213 | 214 | await Room.findByIdAndUpdate( 215 | req.query.roomId, 216 | { 217 | reviews, 218 | ratings, 219 | numOfReviews, 220 | }, 221 | { 222 | new: true, 223 | runValidators: true, 224 | } 225 | ); 226 | 227 | res.status(200).json({ 228 | success: true, 229 | }); 230 | }); 231 | -------------------------------------------------------------------------------- /middlewares/auth.js: -------------------------------------------------------------------------------- 1 | import { getSession } from "next-auth/client"; 2 | 3 | import ErrorHandler from "../utils/errorHandler"; 4 | import catchAsyncError from "./catchAsyncError"; 5 | 6 | export const isAuthenticatedUser = catchAsyncError(async (req, res, next) => { 7 | const session = await getSession({ req }); 8 | 9 | if (!session) { 10 | return next(new ErrorHandler("Login first to access resource", 401)); 11 | } 12 | 13 | req.user = session.user; 14 | next(); 15 | }); 16 | 17 | // Handling user roles 18 | export const authorizeRoles = (...roles) => { 19 | return (req, res, next) => { 20 | if (!roles.includes(req.user.role)) { 21 | return next( 22 | new ErrorHandler( 23 | `Role(${req.user.role}) is not allowed to access this resource.`, 24 | 403 25 | ) 26 | ); 27 | } 28 | next(); 29 | }; 30 | }; 31 | -------------------------------------------------------------------------------- /middlewares/catchAsyncError.js: -------------------------------------------------------------------------------- 1 | export default (func) => (req, res, next) => 2 | Promise.resolve(func(req, res, next)).catch(next); 3 | -------------------------------------------------------------------------------- /middlewares/error.js: -------------------------------------------------------------------------------- 1 | import ErrorHandler from "../utils/errorHandler"; 2 | 3 | export default (err, req, res, next) => { 4 | err.statusCode = err.statusCode || 500; 5 | let error = { ...err }; 6 | error.message = err.message; 7 | 8 | // Wrong Mongoose Object ID Error 9 | if (err.name === "CastError") { 10 | const message = `Resource not found. Invalid: ${err.path}`; 11 | error = new ErrorHandler(message, 400); 12 | } 13 | 14 | // Handling mongoose Validation Error 15 | if (err.name === "ValidationError") { 16 | const message = Object.values(err.errors).map((value) => value.message); 17 | error = new ErrorHandler(message, 400); 18 | } 19 | 20 | res.status(err.statusCode).json({ 21 | success: false, 22 | error, 23 | message: error.message, 24 | stack: error.stack, 25 | }); 26 | }; 27 | -------------------------------------------------------------------------------- /models/booking.js: -------------------------------------------------------------------------------- 1 | import { models, model, Schema } from "mongoose"; 2 | import timeZone from "mongoose-timezone"; 3 | 4 | const bookingSchma = new Schema({ 5 | room: { 6 | type: Schema.Types.ObjectId, 7 | required: true, 8 | ref: "Room", 9 | }, 10 | user: { 11 | type: Schema.Types.ObjectId, 12 | required: true, 13 | ref: "User", 14 | }, 15 | checkInDate: { 16 | type: Date, 17 | required: true, 18 | }, 19 | checkOutDate: { 20 | type: Date, 21 | required: true, 22 | }, 23 | amountPaid: { 24 | type: Number, 25 | required: true, 26 | }, 27 | daysOfStay: { 28 | type: Number, 29 | required: true, 30 | }, 31 | paymentInfo: { 32 | id: { 33 | type: String, 34 | required: true, 35 | }, 36 | status: { 37 | type: String, 38 | required: true, 39 | }, 40 | }, 41 | paidAt: { 42 | type: Date, 43 | required: true, 44 | }, 45 | createdAt: { 46 | type: Date, 47 | default: Date.now, 48 | }, 49 | }); 50 | 51 | bookingSchma.plugin(timeZone); 52 | 53 | export default models.Booking || model("Booking", bookingSchma); 54 | -------------------------------------------------------------------------------- /models/room.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const model = mongoose.model; 4 | const models = mongoose.models; 5 | const Schema = mongoose.Schema; 6 | 7 | const roomSchema = new Schema({ 8 | name: { 9 | type: String, 10 | required: [true, "Please enter room name"], 11 | trim: true, 12 | maxlength: [100, "Room name cannot exceed 100 characters"], 13 | }, 14 | pricePerNight: { 15 | type: Number, 16 | required: [true, "Please enter room price"], 17 | maxlength: [4, "Room name cannot exceed 4 characters"], 18 | default: 0.0, 19 | }, 20 | description: { 21 | type: String, 22 | required: [true, "Please enter room description"], 23 | }, 24 | address: { 25 | type: String, 26 | required: [true, "Please enter room address"], 27 | }, 28 | guestCapacity: { 29 | type: Number, 30 | required: [true, "Please enter guest capacity"], 31 | }, 32 | numOfBeds: { 33 | type: Number, 34 | required: [true, "Please enter number of beds"], 35 | }, 36 | internet: { 37 | type: Boolean, 38 | default: false, 39 | }, 40 | breakfast: { 41 | type: Boolean, 42 | default: false, 43 | }, 44 | airConditioned: { 45 | type: Boolean, 46 | default: false, 47 | }, 48 | petsAllowed: { 49 | type: Boolean, 50 | default: false, 51 | }, 52 | roomCleaning: { 53 | type: Boolean, 54 | default: false, 55 | }, 56 | ratings: { 57 | type: Number, 58 | default: 0, 59 | }, 60 | numOfReviews: { 61 | type: Number, 62 | default: 0, 63 | }, 64 | images: [ 65 | { 66 | public_id: { 67 | type: String, 68 | required: true, 69 | }, 70 | url: { 71 | type: String, 72 | required: true, 73 | }, 74 | }, 75 | ], 76 | category: { 77 | type: String, 78 | required: [true, "Please enter room category"], 79 | enum: { 80 | values: ["King", "Twins", "Single"], 81 | message: "Please select category for room", 82 | }, 83 | }, 84 | reviews: [ 85 | { 86 | user: { 87 | type: Schema.ObjectId, 88 | ref: "User", 89 | required: true, 90 | }, 91 | name: { 92 | type: String, 93 | required: true, 94 | }, 95 | rating: { 96 | type: Number, 97 | required: true, 98 | }, 99 | comment: { 100 | type: String, 101 | required: true, 102 | }, 103 | }, 104 | ], 105 | user: { 106 | type: Schema.ObjectId, 107 | ref: "User", 108 | required: false, 109 | }, 110 | createdAt: { 111 | type: Date, 112 | default: Date.now, 113 | }, 114 | }); 115 | 116 | module.exports = models.Room || model("Room", roomSchema); 117 | -------------------------------------------------------------------------------- /models/user.js: -------------------------------------------------------------------------------- 1 | import { model, models, Schema } from "mongoose"; 2 | import validator from "validator"; 3 | import bcrypt from "bcryptjs"; 4 | import { createHash, randomBytes } from "crypto"; 5 | 6 | const userSchema = new Schema({ 7 | name: { 8 | type: String, 9 | required: [true, "Please enter your name"], 10 | maxlength: [50, "Your name cannot exceed 50 characters"], 11 | }, 12 | email: { 13 | type: String, 14 | required: [true, "Please enter your email"], 15 | unique: true, 16 | validate: [validator.isEmail, "Please enter valid email address"], 17 | }, 18 | password: { 19 | type: String, 20 | required: [true, "Please enter your password"], 21 | minlength: [6, "Your password must be longer than 6 characters"], 22 | select: false, 23 | }, 24 | avatar: { 25 | public_id: { 26 | type: String, 27 | required: true, 28 | }, 29 | url: { 30 | type: String, 31 | required: true, 32 | }, 33 | }, 34 | role: { 35 | type: String, 36 | default: "user", 37 | }, 38 | createdAt: { 39 | type: Date, 40 | default: Date.now, 41 | }, 42 | resetPasswordToken: String, 43 | resetPasswordExpire: Date, 44 | }); 45 | 46 | // Encrypting password before save 47 | userSchema.pre("save", async function (next) { 48 | if (!this.isModified("password")) { 49 | next(); 50 | } 51 | this.password = await bcrypt.hash(this.password, 10); 52 | }); 53 | 54 | // Compare user password 55 | userSchema.methods.comparePassword = async function (enteredPassword) { 56 | return await bcrypt.compare(enteredPassword, this.password); 57 | }; 58 | 59 | // Generate password reset token 60 | userSchema.methods.getResetPasswordToken = function () { 61 | // generate token 62 | const resetToken = randomBytes(20).toString("hex"); 63 | // hash 64 | this.resetPasswordToken = createHash("sha256") 65 | .update(resetToken) 66 | .digest("hex"); 67 | // set token expire time 68 | this.resetPasswordExpire = Date.now() + 30 * 60 * 1000; 69 | 70 | return resetToken; 71 | }; 72 | 73 | export default models.User || model("User", userSchema); 74 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | images: { 3 | domains: ["res.cloudinary.com"], 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextapp-booking", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "seed": "node utils/seeder" 11 | }, 12 | "dependencies": { 13 | "@stripe/stripe-js": "^1.21.0", 14 | "axios": "^0.23.0", 15 | "bcryptjs": "^2.4.3", 16 | "chalk": "^4.1.2", 17 | "cloudinary": "^1.27.1", 18 | "easyinvoice": "^1.0.148", 19 | "mdbreact": "^5.1.0", 20 | "moment": "^2.29.1", 21 | "moment-range": "^4.0.2", 22 | "mongoose": "^6.0.11", 23 | "mongoose-timezone": "^1.3.0", 24 | "next": "11.1.2", 25 | "next-absolute-url": "^1.2.2", 26 | "next-auth": "^3.29.0", 27 | "next-connect": "^0.10.2", 28 | "next-redux-wrapper": "^7.0.5", 29 | "nodemailer": "^6.7.0", 30 | "raw-body": "^2.4.1", 31 | "react": "17.0.2", 32 | "react-bootstrap": "^2.0.0", 33 | "react-dates": "21.7.2", 34 | "react-dom": "17.0.2", 35 | "react-js-pagination": "^3.0.3", 36 | "react-redux": "^7.2.5", 37 | "react-toastify": "^8.0.3", 38 | "redux": "^4.1.1", 39 | "redux-devtools-extension": "^2.13.9", 40 | "redux-thunk": "^2.3.0", 41 | "stripe": "^8.186.0", 42 | "validator": "^13.6.0" 43 | }, 44 | "devDependencies": { 45 | "eslint": "^8.2.0", 46 | "eslint-config-next": "11.1.2" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /pages/404.jsx: -------------------------------------------------------------------------------- 1 | import { Layout } from "../components/layouts/Layout"; 2 | import { NotFound } from "../components/layouts/NotFound"; 3 | 4 | export default function NotFoundPage() { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /pages/_app.jsx: -------------------------------------------------------------------------------- 1 | import { wrapper } from "../redux/store"; 2 | import "../styles/globals.css"; 3 | 4 | function MyApp({ Component, pageProps }) { 5 | return ; 6 | } 7 | 8 | export default wrapper.withRedux(MyApp); 9 | -------------------------------------------------------------------------------- /pages/_document.js: -------------------------------------------------------------------------------- 1 | import Document, { Html, Head, Main, NextScript } from "next/document"; 2 | 3 | class MyDocument extends Document { 4 | static async getInitialProps(ctx) { 5 | const initialProps = await Document.getInitialProps(ctx); 6 | return { ...initialProps }; 7 | } 8 | 9 | render() { 10 | return ( 11 | 12 | 13 | 18 | 23 | 24 | 25 |
26 | 27 | 32 | 37 | 42 | 43 | 44 | ); 45 | } 46 | } 47 | 48 | export default MyDocument; 49 | -------------------------------------------------------------------------------- /pages/admin/bookings/[id].jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { getSession } from "next-auth/client"; 3 | 4 | import { Layout } from "../../../components/layouts/Layout"; 5 | import { BookingDetails } from "../../../components/bookings/BookingDetails"; 6 | import { wrapper } from "../../../redux/store"; 7 | import { getBookingDetails } from "../../../redux/actions/bookingActions"; 8 | 9 | const BookingDetailsPage = () => { 10 | return ( 11 | 12 | 13 | 14 | ); 15 | }; 16 | 17 | export const getServerSideProps = wrapper.getServerSideProps( 18 | (store) => 19 | async ({ req, params }) => { 20 | const session = await getSession({ req }); 21 | 22 | if (!session || session.user.role !== "admin") { 23 | return { 24 | redirect: { 25 | destination: "/login", 26 | permanent: false, 27 | }, 28 | }; 29 | } 30 | 31 | await store.dispatch( 32 | getBookingDetails(req.headers.cookie, req, params.id) 33 | ); 34 | } 35 | ); 36 | 37 | export default BookingDetailsPage; 38 | -------------------------------------------------------------------------------- /pages/admin/bookings/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { getSession } from "next-auth/client"; 3 | 4 | import { Layout } from "../../../components/layouts/Layout"; 5 | import { AllBookings } from "../../../components/admin/AllBookings"; 6 | 7 | export default function AllBookingsPage() { 8 | return ( 9 | 10 | 11 | 12 | ); 13 | } 14 | 15 | export const getServerSideProps = async (context) => { 16 | const session = await getSession({ req: context.req }); 17 | if (!session || session.user.role !== "admin") { 18 | return { 19 | redirect: { 20 | destination: "/login", 21 | permanent: false, 22 | }, 23 | }; 24 | } 25 | return { props: {} }; 26 | }; 27 | -------------------------------------------------------------------------------- /pages/admin/reviews.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { getSession } from "next-auth/client"; 3 | 4 | import { RoomReviews } from "../../components/admin/RoomReviews"; 5 | import { Layout } from "../../components/layouts/Layout"; 6 | 7 | const RoomReviewsPage = () => { 8 | return ( 9 | 10 | 11 | 12 | ); 13 | }; 14 | 15 | export async function getServerSideProps(context) { 16 | const session = await getSession({ req: context.req }); 17 | 18 | if (!session || session.user.role !== "admin") { 19 | return { 20 | redirect: { 21 | destination: "/login", 22 | permanent: false, 23 | }, 24 | }; 25 | } 26 | 27 | return { 28 | props: {}, 29 | }; 30 | } 31 | 32 | export default RoomReviewsPage; 33 | -------------------------------------------------------------------------------- /pages/admin/rooms/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { getSession } from "next-auth/client"; 3 | 4 | import { AllRooms } from "../../../components/admin/AllRooms"; 5 | import { Layout } from "../../../components/layouts/Layout"; 6 | 7 | export default function AllRoomsPage() { 8 | return ( 9 | 10 | 11 | 12 | ); 13 | } 14 | 15 | export const getServerSideProps = async (context) => { 16 | const session = await getSession({ req: context.req }); 17 | if (!session || session.user.role !== "admin") { 18 | return { 19 | redirect: { 20 | destination: "/login", 21 | permanent: false, 22 | }, 23 | }; 24 | } 25 | return { props: {} }; 26 | }; 27 | -------------------------------------------------------------------------------- /pages/admin/rooms/new.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { getSession } from "next-auth/client"; 3 | 4 | import { Layout } from "../../../components/layouts/Layout"; 5 | import { NewRoom } from "../../../components/admin/NewRoom"; 6 | 7 | export default function NewRoomsPage() { 8 | return ( 9 | 10 | 11 | 12 | ); 13 | } 14 | 15 | export const getServerSideProps = async (context) => { 16 | const session = await getSession({ req: context.req }); 17 | if (!session || session.user.role !== "admin") { 18 | return { 19 | redirect: { 20 | destination: "/login", 21 | permanent: false, 22 | }, 23 | }; 24 | } 25 | return { props: {} }; 26 | }; 27 | -------------------------------------------------------------------------------- /pages/admin/users/[id].jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { getSession } from "next-auth/client"; 3 | 4 | import { UpdateUsers } from "../../../components/admin/UpdateUser"; 5 | import { Layout } from "../../../components/layouts/Layout"; 6 | 7 | const UpdateUser = () => { 8 | return ( 9 | 10 | 11 | 12 | ); 13 | }; 14 | 15 | export async function getServerSideProps(context) { 16 | const session = await getSession({ req: context.req }); 17 | 18 | if (!session || session.user.role !== "admin") { 19 | return { 20 | redirect: { 21 | destination: "/login", 22 | permanent: false, 23 | }, 24 | }; 25 | } 26 | 27 | return { 28 | props: {}, 29 | }; 30 | } 31 | 32 | export default UpdateUser; 33 | -------------------------------------------------------------------------------- /pages/admin/users/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { getSession } from "next-auth/client"; 3 | 4 | import { AllUsers } from "../../../components/admin/AllUsers"; 5 | import { Layout } from "../../../components/layouts/Layout"; 6 | 7 | const AllUsersPage = () => { 8 | return ( 9 | 10 | 11 | 12 | ); 13 | }; 14 | 15 | export async function getServerSideProps(context) { 16 | const session = await getSession({ req: context.req }); 17 | 18 | if (!session || session.user.role !== "admin") { 19 | return { 20 | redirect: { 21 | destination: "/login", 22 | permanent: false, 23 | }, 24 | }; 25 | } 26 | 27 | return { 28 | props: {}, 29 | }; 30 | } 31 | 32 | export default AllUsersPage; 33 | -------------------------------------------------------------------------------- /pages/api/admin/bookings/[id].js: -------------------------------------------------------------------------------- 1 | import nc from "next-connect"; 2 | 3 | import { dbConnect } from "../../../../config/dbConnect"; 4 | 5 | import onError from "../../../../middlewares/error"; 6 | import { 7 | authorizeRoles, 8 | isAuthenticatedUser, 9 | } from "../../../../middlewares/auth"; 10 | import { deleteAdminBookings } from "../../../../controllers/bookingControllers"; 11 | 12 | const handler = nc({ onError }); 13 | 14 | dbConnect(); 15 | 16 | handler 17 | .use(isAuthenticatedUser, authorizeRoles("admin")) 18 | .delete(deleteAdminBookings); 19 | 20 | export default handler; 21 | -------------------------------------------------------------------------------- /pages/api/admin/bookings/index.js: -------------------------------------------------------------------------------- 1 | import nc from "next-connect"; 2 | 3 | import { dbConnect } from "../../../../config/dbConnect"; 4 | 5 | import onError from "../../../../middlewares/error"; 6 | import { 7 | authorizeRoles, 8 | isAuthenticatedUser, 9 | } from "../../../../middlewares/auth"; 10 | import { allAdminBookings } from "../../../../controllers/bookingControllers"; 11 | 12 | const handler = nc({ onError }); 13 | 14 | dbConnect(); 15 | 16 | handler.use(isAuthenticatedUser, authorizeRoles("admin")).get(allAdminBookings); 17 | 18 | export default handler; 19 | -------------------------------------------------------------------------------- /pages/api/admin/rooms/index.js: -------------------------------------------------------------------------------- 1 | import nc from "next-connect"; 2 | 3 | import { dbConnect } from "../../../../config/dbConnect"; 4 | import { allAdminRooms } from "../../../../controllers/roomControllers"; 5 | import onError from "../../../../middlewares/error"; 6 | import { 7 | authorizeRoles, 8 | isAuthenticatedUser, 9 | } from "../../../../middlewares/auth"; 10 | 11 | const handler = nc({ onError }); 12 | 13 | dbConnect(); 14 | 15 | handler.use(isAuthenticatedUser, authorizeRoles("admin")).get(allAdminRooms); 16 | 17 | export default handler; 18 | -------------------------------------------------------------------------------- /pages/api/admin/users/[id].js: -------------------------------------------------------------------------------- 1 | import nc from "next-connect"; 2 | 3 | import { dbConnect } from "../../../../config/dbConnect"; 4 | import onError from "../../../../middlewares/error"; 5 | import { 6 | authorizeRoles, 7 | isAuthenticatedUser, 8 | } from "../../../../middlewares/auth"; 9 | import { 10 | deleteUser, 11 | getUserDetails, 12 | updateUser, 13 | } from "../../../../controllers/authControllers"; 14 | 15 | const handler = nc({ onError }); 16 | 17 | dbConnect(); 18 | 19 | handler.use(isAuthenticatedUser, authorizeRoles("admin")).get(getUserDetails); 20 | handler.use(isAuthenticatedUser, authorizeRoles("admin")).put(updateUser); 21 | handler.use(isAuthenticatedUser, authorizeRoles("admin")).delete(deleteUser); 22 | 23 | export default handler; 24 | -------------------------------------------------------------------------------- /pages/api/auth/[...nextauth].js: -------------------------------------------------------------------------------- 1 | import NextAuth from "next-auth"; 2 | import Providers from "next-auth/providers"; 3 | 4 | import { dbConnect } from "../../../config/dbConnect"; 5 | import User from "../../../models/user"; 6 | 7 | export default NextAuth({ 8 | session: { 9 | jwt: true, 10 | }, 11 | providers: [ 12 | Providers.Credentials({ 13 | async authorize(credentials) { 14 | dbConnect(); 15 | const { email, password } = credentials; 16 | // Check if email and password is entered 17 | if (!email || !password) { 18 | throw new Error("Please enter email or password"); 19 | } 20 | // Find user in the database 21 | const user = await User.findOne({ email }).select("+password"); 22 | 23 | if (!user) { 24 | throw new Error("Invalid Email or Password"); 25 | } 26 | // Check if password is correct or not 27 | const isPasswordMatched = await user.comparePassword(password); 28 | 29 | if (!isPasswordMatched) { 30 | throw new Error("Invalid Email or Password"); 31 | } 32 | 33 | return Promise.resolve(user); 34 | }, 35 | }), 36 | ], 37 | callbacks: { 38 | jwt: async (token, user) => { 39 | user && (token.user = user); 40 | return Promise.resolve(token); 41 | }, 42 | session: async (session, user) => { 43 | session.user = user.user; 44 | return Promise.resolve(session); 45 | }, 46 | }, 47 | }); 48 | -------------------------------------------------------------------------------- /pages/api/auth/register.js: -------------------------------------------------------------------------------- 1 | import nc from "next-connect"; 2 | import { dbConnect } from "../../../config/dbConnect"; 3 | import { registerUser } from "../../../controllers/authControllers"; 4 | 5 | import onError from "../../../middlewares/error"; 6 | 7 | const handler = nc({ onError }); 8 | 9 | dbConnect(); 10 | 11 | handler.post(registerUser); 12 | 13 | export default handler; 14 | -------------------------------------------------------------------------------- /pages/api/bookings/[id].js: -------------------------------------------------------------------------------- 1 | import nc from "next-connect"; 2 | 3 | import { dbConnect } from "../../../config/dbConnect"; 4 | import { getBookingDetails } from "../../../controllers/bookingControllers"; 5 | import { isAuthenticatedUser } from "../../../middlewares/auth"; 6 | import onError from "../../../middlewares/error"; 7 | 8 | const handler = nc({ onError }); 9 | 10 | dbConnect(); 11 | 12 | handler.use(isAuthenticatedUser).get(getBookingDetails); 13 | 14 | export default handler; 15 | -------------------------------------------------------------------------------- /pages/api/bookings/check.js: -------------------------------------------------------------------------------- 1 | import nc from "next-connect"; 2 | 3 | import { dbConnect } from "../../../config/dbConnect"; 4 | import { checkRoomBookingAvailability } from "../../../controllers/bookingControllers"; 5 | import onError from "../../../middlewares/error"; 6 | 7 | const handler = nc({ onError }); 8 | 9 | dbConnect(); 10 | 11 | handler.get(checkRoomBookingAvailability); 12 | 13 | export default handler; 14 | -------------------------------------------------------------------------------- /pages/api/bookings/check_booked_dates.js: -------------------------------------------------------------------------------- 1 | import nc from "next-connect"; 2 | 3 | import { dbConnect } from "../../../config/dbConnect"; 4 | import { checkBookedDates } from "../../../controllers/bookingControllers"; 5 | import onError from "../../../middlewares/error"; 6 | 7 | const handler = nc({ onError }); 8 | 9 | dbConnect(); 10 | 11 | handler.get(checkBookedDates); 12 | 13 | export default handler; 14 | -------------------------------------------------------------------------------- /pages/api/bookings/index.js: -------------------------------------------------------------------------------- 1 | import nc from "next-connect"; 2 | 3 | import { dbConnect } from "../../../config/dbConnect"; 4 | import { newBooking } from "../../../controllers/bookingControllers"; 5 | import { isAuthenticatedUser } from "../../../middlewares/auth"; 6 | import onError from "../../../middlewares/error"; 7 | 8 | const handler = nc({ onError }); 9 | 10 | dbConnect(); 11 | 12 | handler.use(isAuthenticatedUser).post(newBooking); 13 | 14 | export default handler; 15 | -------------------------------------------------------------------------------- /pages/api/bookings/me.js: -------------------------------------------------------------------------------- 1 | import nc from "next-connect"; 2 | import { dbConnect } from "../../../config/dbConnect"; 3 | 4 | import { myBookings } from "../../../controllers/bookingControllers"; 5 | import { isAuthenticatedUser } from "../../../middlewares/auth"; 6 | import onError from "../../../middlewares/error"; 7 | 8 | const handler = nc({ onError }); 9 | 10 | dbConnect(); 11 | 12 | handler.use(isAuthenticatedUser).get(myBookings); 13 | 14 | export default handler; 15 | -------------------------------------------------------------------------------- /pages/api/checkout_session/[roomId].js: -------------------------------------------------------------------------------- 1 | import nc from "next-connect"; 2 | 3 | import { dbConnect } from "../../../config/dbConnect"; 4 | import { stripeCheckoutSession } from "../../../controllers/paymentControllers"; 5 | import { isAuthenticatedUser } from "../../../middlewares/auth"; 6 | import onError from "../../../middlewares/error"; 7 | 8 | const handler = nc({ onError }); 9 | 10 | dbConnect(); 11 | 12 | handler.use(isAuthenticatedUser).get(stripeCheckoutSession); 13 | 14 | export default handler; 15 | -------------------------------------------------------------------------------- /pages/api/me.js: -------------------------------------------------------------------------------- 1 | import nc from "next-connect"; 2 | 3 | import { dbConnect } from "../../config/dbConnect"; 4 | import { currentUserProfile } from "../../controllers/authControllers"; 5 | import { isAuthenticatedUser } from "../../middlewares/auth"; 6 | import onError from "../../middlewares/error"; 7 | 8 | const handler = nc({ onError }); 9 | 10 | dbConnect(); 11 | 12 | handler.use(isAuthenticatedUser).get(currentUserProfile); 13 | 14 | export default handler; 15 | -------------------------------------------------------------------------------- /pages/api/me/update.js: -------------------------------------------------------------------------------- 1 | import nc from "next-connect"; 2 | 3 | import { dbConnect } from "../../../config/dbConnect"; 4 | import { updateProfile } from "../../../controllers/authControllers"; 5 | import { isAuthenticatedUser } from "../../../middlewares/auth"; 6 | import onError from "../../../middlewares/error"; 7 | 8 | const handler = nc({ onError }); 9 | 10 | dbConnect(); 11 | 12 | handler.use(isAuthenticatedUser).put(updateProfile); 13 | 14 | export default handler; 15 | -------------------------------------------------------------------------------- /pages/api/password/forgot.js: -------------------------------------------------------------------------------- 1 | import nc from "next-connect"; 2 | import { dbConnect } from "../../../config/dbConnect"; 3 | import { forgotPassword } from "../../../controllers/authControllers"; 4 | 5 | import onError from "../../../middlewares/error"; 6 | 7 | const handler = nc({ onError }); 8 | 9 | dbConnect(); 10 | 11 | handler.post(forgotPassword); 12 | 13 | export default handler; 14 | -------------------------------------------------------------------------------- /pages/api/password/reset/[token].js: -------------------------------------------------------------------------------- 1 | import nc from "next-connect"; 2 | 3 | import { dbConnect } from "../../../../config/dbConnect"; 4 | import { resetPassword } from "../../../../controllers/authControllers"; 5 | import onError from "../../../../middlewares/error"; 6 | 7 | const handler = nc({ onError }); 8 | 9 | dbConnect(); 10 | 11 | handler.put(resetPassword); 12 | 13 | export default handler; 14 | -------------------------------------------------------------------------------- /pages/api/reviews/check_review_availability.js: -------------------------------------------------------------------------------- 1 | import nc from "next-connect"; 2 | 3 | import { dbConnect } from "../../../config/dbConnect"; 4 | import { checkReviewAvailability } from "../../../controllers/roomControllers"; 5 | import { isAuthenticatedUser } from "../../../middlewares/auth"; 6 | import onError from "../../../middlewares/error"; 7 | 8 | const handler = nc({ onError }); 9 | 10 | dbConnect(); 11 | 12 | handler.use(isAuthenticatedUser).get(checkReviewAvailability); 13 | 14 | export default handler; 15 | -------------------------------------------------------------------------------- /pages/api/reviews/index.js: -------------------------------------------------------------------------------- 1 | import nc from "next-connect"; 2 | 3 | import { dbConnect } from "../../../config/dbConnect"; 4 | import { 5 | createNewReview, 6 | getAdminReviews, 7 | deleteReview, 8 | } from "../../../controllers/roomControllers"; 9 | import { isAuthenticatedUser } from "../../../middlewares/auth"; 10 | import onError from "../../../middlewares/error"; 11 | 12 | const handler = nc({ onError }); 13 | 14 | dbConnect(); 15 | 16 | handler.use(isAuthenticatedUser).put(createNewReview); 17 | handler.use(isAuthenticatedUser).get(getAdminReviews); 18 | handler.use(isAuthenticatedUser).delete(deleteReview); 19 | 20 | export default handler; 21 | -------------------------------------------------------------------------------- /pages/api/rooms/[id].js: -------------------------------------------------------------------------------- 1 | import nc from "next-connect"; 2 | import { dbConnect } from "../../../config/dbConnect"; 3 | 4 | import { 5 | deleteRoom, 6 | getSingleRoom, 7 | updateRoom, 8 | } from "../../../controllers/roomControllers"; 9 | import { isAuthenticatedUser, authorizeRoles } from "../../../middlewares/auth"; 10 | import onError from "../../../middlewares/error"; 11 | 12 | const handler = nc({ onError }); 13 | 14 | dbConnect(); 15 | 16 | handler.get(getSingleRoom); 17 | 18 | handler.use(isAuthenticatedUser, authorizeRoles("admin")).put(updateRoom); 19 | 20 | handler.use(isAuthenticatedUser, authorizeRoles("admin")).delete(deleteRoom); 21 | 22 | export default handler; 23 | -------------------------------------------------------------------------------- /pages/api/rooms/index.js: -------------------------------------------------------------------------------- 1 | import nc from "next-connect"; 2 | import { dbConnect } from "../../../config/dbConnect"; 3 | 4 | import { 5 | createNewRoom, 6 | getAllRooms, 7 | } from "../../../controllers/roomControllers"; 8 | import { isAuthenticatedUser, authorizeRoles } from "../../../middlewares/auth"; 9 | import onError from "../../../middlewares/error"; 10 | 11 | const handler = nc({ onError }); 12 | 13 | dbConnect(); 14 | 15 | handler.get(getAllRooms); 16 | 17 | handler.use(isAuthenticatedUser, authorizeRoles("admin")).post(createNewRoom); 18 | 19 | export default handler; 20 | -------------------------------------------------------------------------------- /pages/api/users/index.js: -------------------------------------------------------------------------------- 1 | import nc from "next-connect"; 2 | import { dbConnect } from "../../../config/dbConnect"; 3 | 4 | import { allAdminUsers } from "../../../controllers/authControllers"; 5 | import { isAuthenticatedUser, authorizeRoles } from "../../../middlewares/auth"; 6 | import onError from "../../../middlewares/error"; 7 | 8 | const handler = nc({ onError }); 9 | 10 | dbConnect(); 11 | 12 | handler.use(isAuthenticatedUser, authorizeRoles("admin")).get(allAdminUsers); 13 | 14 | export default handler; 15 | -------------------------------------------------------------------------------- /pages/api/webhook.js: -------------------------------------------------------------------------------- 1 | import nc from "next-connect"; 2 | 3 | import { dbConnect } from "../../config/dbConnect"; 4 | import { webhookCheckout } from "../../controllers/paymentControllers"; 5 | import onError from "../../middlewares/error"; 6 | 7 | const handler = nc({ onError }); 8 | 9 | dbConnect(); 10 | 11 | export const config = { 12 | api: { 13 | bodyParser: false, 14 | }, 15 | }; 16 | 17 | handler.post(webhookCheckout); 18 | 19 | export default handler; 20 | -------------------------------------------------------------------------------- /pages/bookings/[id].jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { getSession } from "next-auth/client"; 3 | 4 | import { Layout } from "../../components/layouts/Layout"; 5 | import { BookingDetails } from "../../components/bookings/BookingDetails"; 6 | import { wrapper } from "../../redux/store"; 7 | import { getBookingDetails } from "../../redux/actions/bookingActions"; 8 | 9 | const BookingDetailsPage = () => { 10 | return ( 11 | 12 | 13 | 14 | ); 15 | }; 16 | 17 | export const getServerSideProps = wrapper.getServerSideProps( 18 | (store) => 19 | async ({ req, params }) => { 20 | const session = await getSession({ req }); 21 | 22 | if (!session) { 23 | return { 24 | redirect: { 25 | destination: "/login", 26 | permanent: false, 27 | }, 28 | }; 29 | } 30 | 31 | await store.dispatch( 32 | getBookingDetails(req.headers.cookie, req, params.id) 33 | ); 34 | } 35 | ); 36 | 37 | export default BookingDetailsPage; 38 | -------------------------------------------------------------------------------- /pages/bookings/me.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { getSession } from "next-auth/client"; 3 | 4 | import { MyBookings } from "../../components/bookings/MyBookings"; 5 | import { Layout } from "../../components/layouts/Layout"; 6 | import { wrapper } from "../../redux/store"; 7 | import { myBookings } from "../../redux/actions/bookingActions"; 8 | 9 | export default function MyBookingsPage() { 10 | return ( 11 | 12 | 13 | 14 | ); 15 | } 16 | 17 | export const getServerSideProps = wrapper.getServerSideProps( 18 | (store) => 19 | async ({ req }) => { 20 | const session = await getSession({ req }); 21 | 22 | if (!session) { 23 | return { 24 | redirect: { 25 | destination: "/login", 26 | permanent: false, 27 | }, 28 | }; 29 | } 30 | 31 | await store.dispatch(myBookings(req.headers.cookie, req)); 32 | } 33 | ); 34 | -------------------------------------------------------------------------------- /pages/index.jsx: -------------------------------------------------------------------------------- 1 | import { Home } from "../components/Home"; 2 | import { Layout } from "../components/layouts/Layout"; 3 | import { getRooms } from "../redux/actions/roomActions"; 4 | import { wrapper } from "../redux/store"; 5 | 6 | export default function Index() { 7 | return ( 8 | 9 | 10 | 11 | ); 12 | } 13 | 14 | export const getServerSideProps = wrapper.getServerSideProps( 15 | (store) => 16 | async ({ req, query }) => { 17 | await store.dispatch( 18 | getRooms(req, query.page, query.location, query.guests, query.category) 19 | ); 20 | } 21 | ); 22 | -------------------------------------------------------------------------------- /pages/login.jsx: -------------------------------------------------------------------------------- 1 | import { getSession } from "next-auth/client"; 2 | 3 | import { Login } from "../components/auth/Login"; 4 | import { Layout } from "../components/layouts/Layout"; 5 | 6 | export default function LoginPage() { 7 | return ( 8 | 9 | 10 | 11 | ); 12 | } 13 | 14 | export const getServerSideProps = async (context) => { 15 | const session = await getSession({ req: context.req }); 16 | if (session) { 17 | return { 18 | redirect: { 19 | destination: "/", 20 | permanent: false, 21 | }, 22 | }; 23 | } 24 | return { props: {} }; 25 | }; 26 | -------------------------------------------------------------------------------- /pages/me/update.jsx: -------------------------------------------------------------------------------- 1 | import { getSession } from "next-auth/client"; 2 | import React from "react"; 3 | 4 | import { Layout } from "../../components/layouts/Layout"; 5 | import { Profile } from "../../components/user/Profile"; 6 | 7 | export default function UpdateProfilePage() { 8 | return ( 9 | 10 | 11 | 12 | ); 13 | } 14 | 15 | export const getServerSideProps = async (context) => { 16 | const session = await getSession({ req: context.req }); 17 | if (!session) { 18 | return { 19 | redirect: { 20 | destination: "/login", 21 | permanent: false, 22 | }, 23 | }; 24 | } 25 | return { props: { session } }; 26 | }; 27 | -------------------------------------------------------------------------------- /pages/password/forgot.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { Layout } from "../../components/layouts/Layout"; 4 | import { ForgotPassword } from "../../components/user/ForgotPassword"; 5 | 6 | export default function ForgotPasswordPage() { 7 | return ( 8 | 9 | 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /pages/password/reset/[token].jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { Layout } from "../../../components/layouts/Layout"; 4 | import { NewPassword } from "../../../components/user/NewPassword"; 5 | 6 | export default function NewPasswordPage() { 7 | return ( 8 | 9 | 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /pages/register.jsx: -------------------------------------------------------------------------------- 1 | import { getSession } from "next-auth/client"; 2 | 3 | import { Register } from "../components/auth/Register"; 4 | import { Layout } from "../components/layouts/Layout"; 5 | 6 | export default function RegisterPage() { 7 | return ( 8 | 9 | 10 | 11 | ); 12 | } 13 | 14 | export const getServerSideProps = async (context) => { 15 | const session = await getSession({ req: context.req }); 16 | if (session) { 17 | return { 18 | redirect: { 19 | destination: "/", 20 | permanent: false, 21 | }, 22 | }; 23 | } 24 | return { props: {} }; 25 | }; 26 | -------------------------------------------------------------------------------- /pages/room/[id].jsx: -------------------------------------------------------------------------------- 1 | import { Layout } from "../../components/layouts/Layout"; 2 | import { RoomDetails } from "../../components/room/RoomDetails"; 3 | import { getRoomDetails } from "../../redux/actions/roomActions"; 4 | import { wrapper } from "../../redux/store"; 5 | 6 | export default function RoomDetailsPage() { 7 | return ( 8 | 9 | 10 | 11 | ); 12 | } 13 | 14 | export const getServerSideProps = wrapper.getServerSideProps( 15 | (store) => 16 | async ({ req, params }) => { 17 | await store.dispatch(getRoomDetails(req, params.id)); 18 | } 19 | ); 20 | -------------------------------------------------------------------------------- /pages/search.jsx: -------------------------------------------------------------------------------- 1 | import { Search } from "../components/Search"; 2 | import { Layout } from "../components/layouts/Layout"; 3 | 4 | export default function SearchPage() { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mierudayo/Next.js_Booking-system/ec09de2b7978a873826eda27c451eb406bc0f408/public/favicon.ico -------------------------------------------------------------------------------- /public/images/bookit_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mierudayo/Next.js_Booking-system/ec09de2b7978a873826eda27c451eb406bc0f408/public/images/bookit_logo.png -------------------------------------------------------------------------------- /public/images/default_avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mierudayo/Next.js_Booking-system/ec09de2b7978a873826eda27c451eb406bc0f408/public/images/default_avatar.png -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /redux/actions/bookingActions.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import absoluteUrl from "next-absolute-url"; 3 | 4 | import { 5 | BOOKED_DATES_FAIL, 6 | BOOKED_DATES_SUCCESS, 7 | MY_BOOKINGS_SUCCESS, 8 | MY_BOOKINGS_FAIL, 9 | CHECK_BOOKING_FAIL, 10 | CHECK_BOOKING_REQUEST, 11 | CHECK_BOOKING_SUCCESS, 12 | BOOKING_DETAILS_SUCCESS, 13 | BOOKING_DETAILS_FAIL, 14 | ADMIN_BOOKINGS_REQUEST, 15 | ADMIN_BOOKINGS_SUCCESS, 16 | ADMIN_BOOKINGS_FAIL, 17 | DELETE_BOOKING_REQUEST, 18 | DELETE_BOOKING_SUCCESS, 19 | DELETE_BOOKING_FAIL, 20 | } from "../constants/bookingConstants"; 21 | 22 | // Check bookings 23 | export const checkBooking = 24 | (roomId, checkInDate, checkOutDate) => async (dispatch) => { 25 | try { 26 | dispatch({ type: CHECK_BOOKING_REQUEST }); 27 | 28 | let link = `/api/bookings/check?roomId=${roomId}&checkInDate=${checkInDate}&checkOutDate=${checkOutDate}`; 29 | 30 | const { data } = await axios.get(link); 31 | 32 | dispatch({ 33 | type: CHECK_BOOKING_SUCCESS, 34 | payload: data.isAvailable, 35 | }); 36 | } catch (error) { 37 | dispatch({ 38 | type: CHECK_BOOKING_FAIL, 39 | payload: error.response.data.message, 40 | }); 41 | } 42 | }; 43 | 44 | // Check booked dates 45 | export const getBookedDates = (id) => async (dispatch) => { 46 | try { 47 | const { data } = await axios.get( 48 | `/api/bookings/check_booked_dates?roomId=${id}` 49 | ); 50 | 51 | dispatch({ 52 | type: BOOKED_DATES_SUCCESS, 53 | payload: data.bookedDates, 54 | }); 55 | } catch (error) { 56 | dispatch({ 57 | type: BOOKED_DATES_FAIL, 58 | payload: error.response.data.message, 59 | }); 60 | } 61 | }; 62 | 63 | // Display all bookings of user 64 | export const myBookings = (authCookie, req) => async (dispatch) => { 65 | try { 66 | const { origin } = absoluteUrl(req); 67 | 68 | const config = { 69 | headers: { 70 | cookie: authCookie, 71 | }, 72 | }; 73 | 74 | const { data } = await axios.get(`${origin}/api/bookings/me`, config); 75 | 76 | dispatch({ 77 | type: MY_BOOKINGS_SUCCESS, 78 | payload: data.bookings, 79 | }); 80 | } catch (error) { 81 | dispatch({ 82 | type: MY_BOOKINGS_FAIL, 83 | payload: error.response.data.message, 84 | }); 85 | } 86 | }; 87 | 88 | // Display all bookings for admin 89 | export const getAdminBookings = () => async (dispatch) => { 90 | try { 91 | dispatch({ type: ADMIN_BOOKINGS_REQUEST }); 92 | 93 | const { data } = await axios.get(`${origin}/api/admin/bookings`); 94 | 95 | dispatch({ 96 | type: ADMIN_BOOKINGS_SUCCESS, 97 | payload: data.bookings, 98 | }); 99 | } catch (error) { 100 | dispatch({ 101 | type: ADMIN_BOOKINGS_FAIL, 102 | payload: error.response.data.message, 103 | }); 104 | } 105 | }; 106 | 107 | // Display booking details 108 | export const getBookingDetails = (authCookie, req, id) => async (dispatch) => { 109 | try { 110 | const { origin } = absoluteUrl(req); 111 | 112 | const config = { 113 | headers: { 114 | cookie: authCookie, 115 | }, 116 | }; 117 | 118 | const { data } = await axios.get(`${origin}/api/bookings/${id}`, config); 119 | 120 | dispatch({ 121 | type: BOOKING_DETAILS_SUCCESS, 122 | payload: data.booking, 123 | }); 124 | } catch (error) { 125 | dispatch({ 126 | type: BOOKING_DETAILS_FAIL, 127 | payload: error.response.data.message, 128 | }); 129 | } 130 | }; 131 | 132 | // Delete booking 133 | export const deleteBooking = (id) => async (dispatch) => { 134 | try { 135 | dispatch({ type: DELETE_BOOKING_REQUEST }); 136 | 137 | const { data } = await axios.delete(`/api/admin/bookings/${id}`); 138 | 139 | dispatch({ 140 | type: DELETE_BOOKING_SUCCESS, 141 | payload: data.success, 142 | }); 143 | } catch (error) { 144 | dispatch({ 145 | type: DELETE_BOOKING_FAIL, 146 | payload: error.response.data.message, 147 | }); 148 | } 149 | }; 150 | 151 | // Clear Errors 152 | export const clearErrors = () => async (dispatch) => { 153 | dispatch({ 154 | type: CLEAR_ERRORS, 155 | }); 156 | }; 157 | -------------------------------------------------------------------------------- /redux/actions/roomActions.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import absoluteUrl from "next-absolute-url"; 3 | 4 | import { 5 | ALL_ROOMS_SUCCESS, 6 | ALL_ROOMS_FAIL, 7 | ROOM_DETAILS_SUCCESS, 8 | ROOM_DETAILS_FAIL, 9 | CLEAR_ERRORS, 10 | NEW_REVIEW_REQUEST, 11 | NEW_REVIEW_SUCCESS, 12 | NEW_REVIEW_FAIL, 13 | REVIEW_AVAILABILITY_REQUEST, 14 | REVIEW_AVAILABILITY_SUCCESS, 15 | REVIEW_AVAILABILITY_FAIL, 16 | ADMIN_ROOM_REQUEST, 17 | ADMIN_ROOM_SUCCESS, 18 | ADMIN_ROOM_FAIL, 19 | NEW_ROOM_REQUEST, 20 | NEW_ROOM_SUCCESS, 21 | NEW_ROOM_FAIL, 22 | UPDATE_ROOM_REQUEST, 23 | UPDATE_ROOM_SUCCESS, 24 | UPDATE_ROOM_FAIL, 25 | DELETE_ROOM_REQUEST, 26 | DELETE_ROOM_SUCCESS, 27 | DELETE_ROOM_FAIL, 28 | GET_REVIEWS_REQUEST, 29 | GET_REVIEWS_SUCCESS, 30 | GET_REVIEWS_FAIL, 31 | DELETE_REVIEW_REQUEST, 32 | DELETE_REVIEW_SUCCESS, 33 | DELETE_REVIEW_FAIL, 34 | } from "../constants/roomConstants"; 35 | 36 | // Get all rooms 37 | export const getRooms = 38 | (req, currentPage = 1, location = "", guests, category) => 39 | async (dispatch) => { 40 | try { 41 | const { origin } = absoluteUrl(req); 42 | let link = `${origin}/api/rooms?page=${currentPage}&location=${location}`; 43 | 44 | if (guests) link = link.concat(`&guestCapacity=${guests}`); 45 | if (category) link = link.concat(`&category=${category}`); 46 | 47 | const { data } = await axios.get(link); 48 | 49 | dispatch({ 50 | type: ALL_ROOMS_SUCCESS, 51 | payload: data, 52 | }); 53 | } catch (error) { 54 | dispatch({ 55 | type: ALL_ROOMS_FAIL, 56 | payload: error.response.data.message, 57 | }); 58 | } 59 | }; 60 | 61 | // new Review 62 | export const newRoom = (roomData) => async (dispatch) => { 63 | try { 64 | dispatch({ type: NEW_ROOM_REQUEST }); 65 | 66 | const config = { 67 | header: { 68 | "Content-Type": "application/json", 69 | }, 70 | }; 71 | 72 | const { data } = await axios.post(`/api/rooms`, roomData, config); 73 | 74 | dispatch({ 75 | type: NEW_ROOM_SUCCESS, 76 | payload: data, 77 | }); 78 | } catch (error) { 79 | dispatch({ 80 | type: NEW_ROOM_FAIL, 81 | payload: error.response.data.message, 82 | }); 83 | } 84 | }; 85 | 86 | // update Review 87 | export const updateRoom = (id, roomData) => async (dispatch) => { 88 | try { 89 | dispatch({ type: UPDATE_ROOM_REQUEST }); 90 | 91 | const config = { 92 | header: { 93 | "Content-Type": "application/json", 94 | }, 95 | }; 96 | 97 | const { data } = await axios.put(`/api/rooms/${id}`, roomData, config); 98 | 99 | dispatch({ 100 | type: UPDATE_ROOM_SUCCESS, 101 | payload: data.success, 102 | }); 103 | } catch (error) { 104 | dispatch({ 105 | type: UPDATE_ROOM_FAIL, 106 | payload: error.response.data.message, 107 | }); 108 | } 109 | }; 110 | 111 | // delete Review 112 | export const deleteRoom = (id) => async (dispatch) => { 113 | try { 114 | dispatch({ type: DELETE_ROOM_REQUEST }); 115 | 116 | const { data } = await axios.delete(`/api/rooms/${id}`); 117 | 118 | dispatch({ 119 | type: DELETE_ROOM_SUCCESS, 120 | payload: data.success, 121 | }); 122 | } catch (error) { 123 | dispatch({ 124 | type: DELETE_ROOM_FAIL, 125 | payload: error.response.data.message, 126 | }); 127 | } 128 | }; 129 | 130 | // new Review 131 | export const newReview = (reviewData) => async (dispatch) => { 132 | try { 133 | dispatch({ type: NEW_REVIEW_REQUEST }); 134 | 135 | const config = { 136 | header: { 137 | "Content-Type": "application/json", 138 | }, 139 | }; 140 | 141 | const { data } = await axios.put(`/api/reviews`, reviewData, config); 142 | 143 | dispatch({ 144 | type: NEW_REVIEW_SUCCESS, 145 | payload: data.success, 146 | }); 147 | } catch (error) { 148 | dispatch({ 149 | type: NEW_REVIEW_FAIL, 150 | payload: error.response.data.message, 151 | }); 152 | } 153 | }; 154 | 155 | // check Review 156 | export const checkReviewAvailability = (roomId) => async (dispatch) => { 157 | try { 158 | dispatch({ type: REVIEW_AVAILABILITY_REQUEST }); 159 | 160 | const { data } = await axios.get( 161 | `/api/reviews/check_review_availability?roomId=${roomId}` 162 | ); 163 | 164 | dispatch({ 165 | type: REVIEW_AVAILABILITY_SUCCESS, 166 | payload: data.isReviewAvailable, 167 | }); 168 | } catch (error) { 169 | dispatch({ 170 | type: REVIEW_AVAILABILITY_FAIL, 171 | payload: error.response.data.message, 172 | }); 173 | } 174 | }; 175 | 176 | // Get room details 177 | export const getRoomDetails = (req, id) => async (dispatch) => { 178 | try { 179 | const { origin } = absoluteUrl(req); 180 | 181 | let url; 182 | 183 | if (req) { 184 | url = `${origin}/api/rooms/${id}`; 185 | } else { 186 | url = `api/rooms/${id}`; 187 | } 188 | 189 | const { data } = await axios.get(url); 190 | 191 | dispatch({ 192 | type: ROOM_DETAILS_SUCCESS, 193 | payload: data.room, 194 | }); 195 | } catch (error) { 196 | dispatch({ 197 | type: ROOM_DETAILS_FAIL, 198 | payload: error.response.data.message, 199 | }); 200 | } 201 | }; 202 | 203 | // Get all rooms - ADMIN 204 | export const getAdminRooms = () => async (dispatch) => { 205 | try { 206 | dispatch({ type: ADMIN_ROOM_REQUEST }); 207 | 208 | const { data } = await axios.get(`/api/admin/rooms`); 209 | 210 | dispatch({ 211 | type: ADMIN_ROOM_SUCCESS, 212 | payload: data.rooms, 213 | }); 214 | } catch (error) { 215 | dispatch({ 216 | type: ADMIN_ROOM_FAIL, 217 | payload: error.response.data.message, 218 | }); 219 | } 220 | }; 221 | 222 | // Get room reviews 223 | export const getRoomReviews = (id) => async (dispatch) => { 224 | try { 225 | dispatch({ type: GET_REVIEWS_REQUEST }); 226 | 227 | const { data } = await axios.get(`/api/reviews/?id=${id}`); 228 | 229 | dispatch({ 230 | type: GET_REVIEWS_SUCCESS, 231 | payload: data.reviews, 232 | }); 233 | } catch (error) { 234 | dispatch({ 235 | type: GET_REVIEWS_FAIL, 236 | payload: error.response.data.message, 237 | }); 238 | } 239 | }; 240 | 241 | // Delete review 242 | export const deleteReview = (id, roomId) => async (dispatch) => { 243 | try { 244 | dispatch({ type: DELETE_REVIEW_REQUEST }); 245 | 246 | const { data } = await axios.delete( 247 | `/api/reviews/?id=${id}&roomId=${roomId}` 248 | ); 249 | 250 | dispatch({ 251 | type: DELETE_REVIEW_SUCCESS, 252 | payload: data.success, 253 | }); 254 | } catch (error) { 255 | dispatch({ 256 | type: DELETE_REVIEW_FAIL, 257 | payload: error.response.data.message, 258 | }); 259 | } 260 | }; 261 | 262 | // Clear Errors 263 | export const clearErrors = (req) => async (dispatch) => { 264 | dispatch({ 265 | type: CLEAR_ERRORS, 266 | }); 267 | }; 268 | -------------------------------------------------------------------------------- /redux/actions/userActions.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | import { 4 | REGISTER_USER_REQUEST, 5 | REGISTER_USER_SUCCESS, 6 | REGISTER_USER_FAIL, 7 | LOAD_USER_REQUEST, 8 | LOAD_USER_SUCCESS, 9 | LOAD_USER_FAIL, 10 | CLEAR_ERRORS, 11 | UPDATE_PROFILE_REQUEST, 12 | UPDATE_PROFILE_SUCCESS, 13 | UPDATE_PROFILE_FAIL, 14 | FORGOT_PASSWORD_REQUEST, 15 | FORGOT_PASSWORD_SUCCESS, 16 | FORGOT_PASSWORD_FAIL, 17 | RESET_PASSWORD_REQUEST, 18 | RESET_PASSWORD_SUCCESS, 19 | RESET_PASSWORD_FAIL, 20 | ADMIN_USERS_REQUEST, 21 | ADMIN_USERS_SUCCESS, 22 | ADMIN_USERS_FAIL, 23 | USER_DETAILS_REQUEST, 24 | USER_DETAILS_SUCCESS, 25 | USER_DETAILS_FAIL, 26 | UPDATE_USER_SUCCESS, 27 | UPDATE_USER_FAIL, 28 | DELETE_USER_SUCCESS, 29 | DELETE_USER_FAIL, 30 | } from "../constants/userConstants"; 31 | 32 | // Register user 33 | export const registerUser = (userData) => async (dispatch) => { 34 | try { 35 | dispatch({ type: REGISTER_USER_REQUEST }); 36 | 37 | const config = { 38 | headers: { 39 | "Content-Type": "application/json", 40 | }, 41 | }; 42 | const { data } = await axios.post("/api/auth/register", userData, config); 43 | 44 | dispatch({ 45 | type: REGISTER_USER_SUCCESS, 46 | }); 47 | } catch (error) { 48 | dispatch({ 49 | type: REGISTER_USER_FAIL, 50 | payload: error.response.data.message, 51 | }); 52 | } 53 | }; 54 | 55 | // Load user 56 | export const loadUser = () => async (dispatch) => { 57 | try { 58 | dispatch({ type: LOAD_USER_REQUEST }); 59 | 60 | const { data } = await axios.get("/api/me"); 61 | 62 | dispatch({ 63 | type: LOAD_USER_SUCCESS, 64 | payload: data.user, 65 | }); 66 | } catch (error) { 67 | dispatch({ 68 | type: LOAD_USER_FAIL, 69 | payload: error.response.data.message, 70 | }); 71 | } 72 | }; 73 | 74 | // Update Profile 75 | export const updateProfile = (userData) => async (dispatch) => { 76 | try { 77 | dispatch({ type: UPDATE_PROFILE_REQUEST }); 78 | 79 | const config = { 80 | headers: { 81 | "Content-Type": "application/json", 82 | }, 83 | }; 84 | const { data } = await axios.put("/api/me/update", userData, config); 85 | 86 | dispatch({ 87 | type: UPDATE_PROFILE_SUCCESS, 88 | payload: data.success, 89 | }); 90 | } catch (error) { 91 | dispatch({ 92 | type: UPDATE_PROFILE_FAIL, 93 | payload: error.response.data.message, 94 | }); 95 | } 96 | }; 97 | 98 | // Forgot Password 99 | export const forgotPassword = (email) => async (dispatch) => { 100 | try { 101 | dispatch({ type: FORGOT_PASSWORD_REQUEST }); 102 | 103 | const config = { 104 | headers: { 105 | "Content-Type": "application/json", 106 | }, 107 | }; 108 | 109 | const { data } = await axios.post("/api/password/forgot", email, config); 110 | 111 | dispatch({ 112 | type: FORGOT_PASSWORD_SUCCESS, 113 | payload: data.message, 114 | }); 115 | } catch (error) { 116 | dispatch({ 117 | type: FORGOT_PASSWORD_FAIL, 118 | payload: error.response.data.message, 119 | }); 120 | } 121 | }; 122 | 123 | // Reset Password 124 | export const resetPassword = (token, password) => async (dispatch) => { 125 | try { 126 | dispatch({ type: RESET_PASSWORD_REQUEST }); 127 | 128 | const config = { 129 | headers: { 130 | "Content-Type": "application/json", 131 | }, 132 | }; 133 | 134 | const { data } = await axios.put( 135 | `/api/password/reset/${token}`, 136 | password, 137 | config 138 | ); 139 | 140 | dispatch({ 141 | type: RESET_PASSWORD_SUCCESS, 142 | payload: data.success, 143 | }); 144 | } catch (error) { 145 | dispatch({ 146 | type: RESET_PASSWORD_FAIL, 147 | payload: error.response.data.message, 148 | }); 149 | } 150 | }; 151 | 152 | // Display users 153 | export const getAdminUsers = () => async (dispatch) => { 154 | try { 155 | dispatch({ type: ADMIN_USERS_REQUEST }); 156 | 157 | const { data } = await axios.get(`/api/admin/users`); 158 | 159 | dispatch({ 160 | type: ADMIN_USERS_SUCCESS, 161 | payload: data.users, 162 | }); 163 | } catch (error) { 164 | dispatch({ 165 | type: ADMIN_USERS_FAIL, 166 | payload: error.response.data.message, 167 | }); 168 | } 169 | }; 170 | 171 | // Display user details 172 | export const getUserDetails = (id) => async (dispatch) => { 173 | try { 174 | dispatch({ type: USER_DETAILS_REQUEST }); 175 | 176 | const { data } = await axios.get(`/api/admin/users/${id}`); 177 | 178 | dispatch({ 179 | type: USER_DETAILS_SUCCESS, 180 | payload: data.user, 181 | }); 182 | } catch (error) { 183 | dispatch({ 184 | type: USER_DETAILS_FAIL, 185 | payload: error.response.data.message, 186 | }); 187 | } 188 | }; 189 | 190 | // Update user details 191 | export const updateUser = (id, userData) => async (dispatch) => { 192 | try { 193 | dispatch({ type: UPDATE_USER_REQUEST }); 194 | 195 | const config = { 196 | headers: { 197 | "Content-Type": "application/json", 198 | }, 199 | }; 200 | 201 | const { data } = await axios.put( 202 | `/api/admin/users/${id}`, 203 | userData, 204 | config 205 | ); 206 | 207 | dispatch({ 208 | type: UPDATE_USER_SUCCESS, 209 | payload: data.user, 210 | }); 211 | } catch (error) { 212 | dispatch({ 213 | type: UPDATE_USER_FAIL, 214 | payload: error.response.data.message, 215 | }); 216 | } 217 | }; 218 | 219 | // Delete user 220 | export const deleteUser = (id) => async (dispatch) => { 221 | try { 222 | dispatch({ type: DELETE_USER_REQUEST }); 223 | 224 | const { data } = await axios.delete(`/api/admin/users/${id}`); 225 | 226 | dispatch({ 227 | type: DELETE_USER_SUCCESS, 228 | payload: data.user, 229 | }); 230 | } catch (error) { 231 | dispatch({ 232 | type: DELETE_USER_FAIL, 233 | payload: error.response.data.message, 234 | }); 235 | } 236 | }; 237 | 238 | // Clear Errors 239 | export const clearErrors = (req) => async (dispatch) => { 240 | dispatch({ 241 | type: CLEAR_ERRORS, 242 | }); 243 | }; 244 | -------------------------------------------------------------------------------- /redux/constants/bookingConstants.js: -------------------------------------------------------------------------------- 1 | export const CHECK_BOOKING_REQUEST = "CHECK_BOOKING_REQUEST"; 2 | export const CHECK_BOOKING_SUCCESS = "CHECK_BOOKING_SUCCESS"; 3 | export const CHECK_BOOKING_RESET = "CHECK_BOOKING_RESET"; 4 | export const CHECK_BOOKING_FAIL = "CHECK_BOOKING_FAIL"; 5 | 6 | export const BOOKED_DATES_SUCCESS = "BOOKED_DATES_SUCCESS"; 7 | export const BOOKED_DATES_FAIL = "BOOKED_DATES_FAIL"; 8 | 9 | export const MY_BOOKINGS_SUCCESS = "MY_BOOKINGS_SUCCESS"; 10 | export const MY_BOOKINGS_FAIL = "MY_BOOKINGS_FAIL"; 11 | 12 | export const BOOKING_DETAILS_SUCCESS = "BOOKING_DETAILS_SUCCESS"; 13 | export const BOOKING_DETAILS_FAIL = "BOOKING_DETAILS_FAIL"; 14 | 15 | export const ADMIN_BOOKINGS_REQUEST = "ADMIN_BOOKINGS_REQUEST"; 16 | export const ADMIN_BOOKINGS_SUCCESS = "ADMIN_BOOKINGS_SUCCESS"; 17 | export const ADMIN_BOOKINGS_FAIL = "ADMIN_BOOKINGS_FAIL"; 18 | 19 | export const DELETE_BOOKING_REQUEST = "DELETE_BOOKING_REQUEST"; 20 | export const DELETE_BOOKING_SUCCESS = "DELETE_BOOKING_SUCCESS"; 21 | export const DELETE_BOOKING_RESET = "DELETE_BOOKING_RESET"; 22 | export const DELETE_BOOKING_FAIL = "DELETE_BOOKING_FAIL"; 23 | 24 | export const CLEAR_ERRORS = "CLEAR_ERRORS"; 25 | -------------------------------------------------------------------------------- /redux/constants/roomConstants.js: -------------------------------------------------------------------------------- 1 | export const ALL_ROOMS_SUCCESS = "ALL_ROOMS_SUCCESS"; 2 | export const ALL_ROOMS_FAIL = "ALL_ROOMS_FAIL"; 3 | export const CLEAR_ERRORS = "CLEAR_ERRORS"; 4 | 5 | export const NEW_REVIEW_REQUEST = "NEW_REVIEW_REQUEST"; 6 | export const NEW_REVIEW_SUCCESS = "NEW_REVIEW_SUCCESS"; 7 | export const NEW_REVIEW_RESET = "NEW_REVIEW_RESET"; 8 | export const NEW_REVIEW_FAIL = "NEW_REVIEW_FAIL"; 9 | 10 | export const NEW_ROOM_REQUEST = "NEW_ROOM_REQUEST"; 11 | export const NEW_ROOM_SUCCESS = "NEW_ROOM_SUCCESS"; 12 | export const NEW_ROOM_RESET = "NEW_ROOM_RESET"; 13 | export const NEW_ROOM_FAIL = "NEW_ROOM_FAIL"; 14 | 15 | export const UPDATE_ROOM_REQUEST = "UPDATE_ROOM_REQUEST"; 16 | export const UPDATE_ROOM_SUCCESS = "UPDATE_ROOM_SUCCESS"; 17 | export const UPDATE_ROOM_RESET = "UPDATE_ROOM_RESET"; 18 | export const UPDATE_ROOM_FAIL = "UPDATE_ROOM_FAIL"; 19 | 20 | export const DELETE_ROOM_REQUEST = "DELETE_ROOM_REQUEST"; 21 | export const DELETE_ROOM_SUCCESS = "DELETE_ROOM_SUCCESS"; 22 | export const DELETE_ROOM_RESET = "DELETE_ROOM_RESET"; 23 | export const DELETE_ROOM_FAIL = "DELETE_ROOM_FAIL"; 24 | 25 | export const REVIEW_AVAILABILITY_REQUEST = "REVIEW_AVAILABILITY_REQUEST"; 26 | export const REVIEW_AVAILABILITY_SUCCESS = "REVIEW_AVAILABILITY_SUCCESS"; 27 | export const REVIEW_AVAILABILITY_FAIL = "REVIEW_AVAILABILITY_FAIL"; 28 | 29 | export const ADMIN_ROOM_REQUEST = "ADMIN_ROOM_REQUEST"; 30 | export const ADMIN_ROOM_SUCCESS = "ADMIN_ROOM_SUCCESS"; 31 | export const ADMIN_ROOM_FAIL = "ADMIN_ROOM_FAIL"; 32 | 33 | export const GET_REVIEWS_REQUEST = "GET_REVIEWS_REQUEST"; 34 | export const GET_REVIEWS_SUCCESS = "GET_REVIEWS_SUCCESS"; 35 | export const GET_REVIEWS_FAIL = "GET_REVIEWS_FAIL"; 36 | 37 | export const DELETE_REVIEW_REQUEST = "DELETE_REVIEW_REQUEST"; 38 | export const DELETE_REVIEW_SUCCESS = "DELETE_REVIEW_SUCCESS"; 39 | export const DELETE_REVIEW_RESET = "DELETE_REVIEW_RESET"; 40 | export const DELETE_REVIEW_FAIL = "DELETE_REVIEW_FAIL"; 41 | 42 | export const ROOM_DETAILS_SUCCESS = "ROOMS_DETAILS_SUCCESS"; 43 | export const ROOM_DETAILS_FAIL = "ROOMS_DETAILS_FAIL"; 44 | -------------------------------------------------------------------------------- /redux/constants/userConstants.js: -------------------------------------------------------------------------------- 1 | export const REGISTER_USER_REQUEST = "REGISTER_USER_REQUEST"; 2 | export const REGISTER_USER_SUCCESS = "REGISTER_USER_SUCCESS"; 3 | export const REGISTER_USER_FAIL = "REGISTER_USER_FAIL"; 4 | 5 | export const LOAD_USER_REQUEST = "LOAD_USER_REQUEST"; 6 | export const LOAD_USER_SUCCESS = "LOAD_USER_SUCCESS"; 7 | export const LOAD_USER_FAIL = "LOAD_USER_FAIL"; 8 | 9 | export const UPDATE_PROFILE_REQUEST = "UPDATE_PROFILE_REQUEST"; 10 | export const UPDATE_PROFILE_SUCCESS = "UPDATE_PROFILE_SUCCESS"; 11 | export const UPDATE_PROFILE_RESET = "UPDATE_PROFILE_RESET"; 12 | export const UPDATE_PROFILE_FAIL = "UPDATE_PROFILE_FAIL"; 13 | 14 | export const FORGOT_PASSWORD_REQUEST = "FORGOT_PASSWORD_REQUEST"; 15 | export const FORGOT_PASSWORD_SUCCESS = "FORGOT_PASSWORD_SUCCESS"; 16 | export const FORGOT_PASSWORD_FAIL = "FORGOT_PASSWORD_FAIL"; 17 | 18 | export const RESET_PASSWORD_REQUEST = "RESET_PASSWORD_REQUEST"; 19 | export const RESET_PASSWORD_SUCCESS = "RESET_PASSWORD_SUCCESS"; 20 | export const RESET_PASSWORD_FAIL = "RESET_PASSWORD_FAIL"; 21 | 22 | export const ADMIN_USERS_REQUEST = "ADMIN_USERS_REQUEST"; 23 | export const ADMIN_USERS_SUCCESS = "ADMIN_USERS_SUCCESS"; 24 | export const ADMIN_USERS_FAIL = "ADMIN_USERS_FAIL"; 25 | 26 | export const USER_DETAILS_REQUEST = "USER_DETAILS_REQUEST"; 27 | export const USER_DETAILS_SUCCESS = "USER_DETAILS_SUCCESS"; 28 | export const USER_DETAILS_FAIL = "USER_DETAILS_FAIL"; 29 | 30 | export const UPDATE_USER_REQUEST = "UPDATE_USER_REQUEST"; 31 | export const UPDATE_USER_SUCCESS = "UPDATE_USER_SUCCESS"; 32 | export const UPDATE_USER_RESET = "UPDATE_USER_RESET"; 33 | export const UPDATE_USER_FAIL = "UPDATE_USER_FAIL"; 34 | 35 | export const DELETE_USER_REQUEST = "DELETE_USER_REQUEST"; 36 | export const DELETE_USER_SUCCESS = "DELETE_USER_SUCCESS"; 37 | export const DELETE_USER_RESET = "DELETE_USER_RESET"; 38 | export const DELETE_USER_FAIL = "DELETE_USER_FAIL"; 39 | 40 | export const CLEAR_ERRORS = "CLEAR_ERRORS"; 41 | -------------------------------------------------------------------------------- /redux/reducers/bookingReducers.js: -------------------------------------------------------------------------------- 1 | import { 2 | CHECK_BOOKING_REQUEST, 3 | CHECK_BOOKING_SUCCESS, 4 | CHECK_BOOKING_FAIL, 5 | CHECK_BOOKING_RESET, 6 | BOOKED_DATES_SUCCESS, 7 | BOOKED_DATES_FAIL, 8 | MY_BOOKINGS_SUCCESS, 9 | MY_BOOKINGS_FAIL, 10 | BOOKING_DETAILS_SUCCESS, 11 | BOOKING_DETAILS_FAIL, 12 | CLEAR_ERRORS, 13 | ADMIN_BOOKINGS_SUCCESS, 14 | ADMIN_BOOKINGS_FAIL, 15 | DELETE_BOOKING_SUCCESS, 16 | DELETE_BOOKING_FAIL, 17 | DELETE_BOOKING_RESET, 18 | } from "../constants/bookingConstants"; 19 | import { 20 | ADMIN_ROOM_REQUEST, 21 | DELETE_ROOM_REQUEST, 22 | } from "../constants/roomConstants"; 23 | 24 | // Check Booking Reducer 25 | export const checkBookingReducer = (state = { available: null }, action) => { 26 | switch (action.type) { 27 | case CHECK_BOOKING_REQUEST: 28 | return { 29 | loading: true, 30 | }; 31 | case CHECK_BOOKING_SUCCESS: 32 | return { 33 | loading: false, 34 | available: action.payload, 35 | }; 36 | case CHECK_BOOKING_RESET: 37 | return { 38 | loading: false, 39 | available: null, 40 | }; 41 | case CHECK_BOOKING_FAIL: 42 | return { 43 | loading: false, 44 | error: action.payload, 45 | }; 46 | case CLEAR_ERRORS: 47 | return { 48 | ...state, 49 | error: null, 50 | }; 51 | default: 52 | return state; 53 | } 54 | }; 55 | 56 | // Get all booked Reducer 57 | export const bookedDatesReducer = (state = { dates: [] }, action) => { 58 | switch (action.type) { 59 | case BOOKED_DATES_SUCCESS: 60 | return { 61 | loading: false, 62 | dates: action.payload, 63 | }; 64 | case BOOKED_DATES_FAIL: 65 | return { 66 | loading: false, 67 | error: action.payload, 68 | }; 69 | case CLEAR_ERRORS: 70 | return { 71 | ...state, 72 | error: null, 73 | }; 74 | default: 75 | return state; 76 | } 77 | }; 78 | 79 | // Get all bookings of user 80 | export const bookingsReducer = (state = { bookings: [] }, action) => { 81 | switch (action.type) { 82 | case ADMIN_ROOM_REQUEST: 83 | return { 84 | loading: true, 85 | }; 86 | case MY_BOOKINGS_SUCCESS: 87 | case ADMIN_BOOKINGS_SUCCESS: 88 | return { 89 | loading: false, 90 | bookings: action.payload, 91 | }; 92 | case MY_BOOKINGS_FAIL: 93 | case ADMIN_BOOKINGS_FAIL: 94 | return { 95 | loading: false, 96 | error: action.payload, 97 | }; 98 | case CLEAR_ERRORS: 99 | return { 100 | ...state, 101 | error: null, 102 | }; 103 | default: 104 | return state; 105 | } 106 | }; 107 | 108 | // Get booking details 109 | export const bookingDetailsReducer = (state = { booking: {} }, action) => { 110 | switch (action.type) { 111 | case BOOKING_DETAILS_SUCCESS: 112 | return { 113 | loading: false, 114 | booking: action.payload, 115 | }; 116 | case BOOKING_DETAILS_FAIL: 117 | return { 118 | loading: false, 119 | error: action.payload, 120 | }; 121 | case CLEAR_ERRORS: 122 | return { 123 | ...state, 124 | error: null, 125 | }; 126 | default: 127 | return state; 128 | } 129 | }; 130 | 131 | // Delete Booking Reducer 132 | export const bookingReducer = (state = {}, action) => { 133 | switch (action.type) { 134 | case DELETE_ROOM_REQUEST: 135 | return { 136 | loading: true, 137 | }; 138 | case DELETE_BOOKING_SUCCESS: 139 | return { 140 | loading: false, 141 | isDeleted: action.payload, 142 | }; 143 | case DELETE_BOOKING_RESET: 144 | return { 145 | loading: false, 146 | isDeleted: false, 147 | }; 148 | case DELETE_BOOKING_FAIL: 149 | return { 150 | loading: false, 151 | error: action.payload, 152 | }; 153 | case CLEAR_ERRORS: 154 | return { 155 | ...state, 156 | error: null, 157 | }; 158 | default: 159 | return state; 160 | } 161 | }; 162 | -------------------------------------------------------------------------------- /redux/reducers/reducers.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux"; 2 | 3 | import { 4 | bookedDatesReducer, 5 | bookingDetailsReducer, 6 | bookingReducer, 7 | bookingsReducer, 8 | checkBookingReducer, 9 | } from "./bookingReducers"; 10 | import { 11 | allRoomsReducer, 12 | checkReviewReducer, 13 | newReviewReducer, 14 | newRoomReducer, 15 | reviewReducer, 16 | roomDetailsReducer, 17 | roomReducer, 18 | roomReviewsReducer, 19 | } from "./roomReducers"; 20 | import { 21 | authReducer, 22 | loadedUserReducer, 23 | userReducer, 24 | forgotPasswordReducer, 25 | allUsersReducer, 26 | userDetailsReducer, 27 | } from "./userReducers"; 28 | 29 | export const reducers = combineReducers({ 30 | allRooms: allRoomsReducer, 31 | newRoom: newRoomReducer, 32 | roomDetails: roomDetailsReducer, 33 | room: roomReducer, 34 | auth: authReducer, 35 | loadedUser: loadedUserReducer, 36 | allUsers: allUsersReducer, 37 | userDetails: userDetailsReducer, 38 | user: userReducer, 39 | forgotPassword: forgotPasswordReducer, 40 | checkBooking: checkBookingReducer, 41 | bookedDates: bookedDatesReducer, 42 | bookings: bookingsReducer, 43 | booking: bookingReducer, 44 | bookingDetails: bookingDetailsReducer, 45 | newReview: newReviewReducer, 46 | checkReview: checkReviewReducer, 47 | roomReviews: roomReviewsReducer, 48 | review: reviewReducer, 49 | }); 50 | -------------------------------------------------------------------------------- /redux/reducers/roomReducers.js: -------------------------------------------------------------------------------- 1 | import { 2 | ALL_ROOMS_SUCCESS, 3 | ALL_ROOMS_FAIL, 4 | ROOM_DETAILS_SUCCESS, 5 | ROOM_DETAILS_FAIL, 6 | NEW_REVIEW_REQUEST, 7 | NEW_REVIEW_SUCCESS, 8 | NEW_REVIEW_FAIL, 9 | NEW_REVIEW_RESET, 10 | REVIEW_AVAILABILITY_REQUEST, 11 | REVIEW_AVAILABILITY_SUCCESS, 12 | REVIEW_AVAILABILITY_FAIL, 13 | ADMIN_ROOM_REQUEST, 14 | ADMIN_ROOM_SUCCESS, 15 | ADMIN_ROOM_FAIL, 16 | NEW_ROOM_REQUEST, 17 | NEW_ROOM_SUCCESS, 18 | NEW_ROOM_FAIL, 19 | NEW_ROOM_RESET, 20 | CLEAR_ERRORS, 21 | UPDATE_ROOM_REQUEST, 22 | UPDATE_ROOM_SUCCESS, 23 | UPDATE_ROOM_FAIL, 24 | DELETE_ROOM_REQUEST, 25 | DELETE_ROOM_SUCCESS, 26 | DELETE_ROOM_RESET, 27 | DELETE_ROOM_FAIL, 28 | GET_REVIEWS_REQUEST, 29 | GET_REVIEWS_SUCCESS, 30 | GET_REVIEWS_FAIL, 31 | DELETE_REVIEW_REQUEST, 32 | DELETE_REVIEW_SUCCESS, 33 | DELETE_REVIEW_RESET, 34 | DELETE_REVIEW_FAIL, 35 | } from "../constants/roomConstants"; 36 | import { UPDATE_PROFILE_RESET } from "../constants/userConstants"; 37 | 38 | // All rooms Reducer 39 | export const allRoomsReducer = (state = { rooms: [] }, action) => { 40 | switch (action.type) { 41 | case ADMIN_ROOM_REQUEST: 42 | return { 43 | loading: success, 44 | }; 45 | case ALL_ROOMS_SUCCESS: 46 | return { 47 | roomsCount: action.payload.roomsCount, 48 | resPerPage: action.payload.resPerPage, 49 | filteredRoomsCount: action.payload.filteredRoomsCount, 50 | rooms: action.payload.rooms, 51 | }; 52 | case ADMIN_ROOM_SUCCESS: 53 | return { 54 | loading: false, 55 | rooms: action.payload, 56 | }; 57 | case ALL_ROOMS_FAIL: 58 | case ADMIN_ROOM_FAIL: 59 | return { 60 | error: action.payload, 61 | }; 62 | case CLEAR_ERRORS: 63 | return { 64 | ...state, 65 | error: null, 66 | }; 67 | default: 68 | return state; 69 | } 70 | }; 71 | 72 | // Room details Reducer 73 | export const roomDetailsReducer = (state = { rooms: {} }, action) => { 74 | switch (action.type) { 75 | case ROOM_DETAILS_SUCCESS: 76 | return { 77 | room: action.payload, 78 | }; 79 | case ROOM_DETAILS_FAIL: 80 | return { 81 | error: action.payload, 82 | }; 83 | case CLEAR_ERRORS: 84 | return { 85 | ...state, 86 | error: null, 87 | }; 88 | default: 89 | return state; 90 | } 91 | }; 92 | 93 | // Room review Reducer 94 | export const newReviewReducer = (state = {}, action) => { 95 | switch (action.type) { 96 | case NEW_REVIEW_REQUEST: 97 | return { 98 | loading: true, 99 | }; 100 | case NEW_REVIEW_SUCCESS: 101 | return { 102 | loading: false, 103 | success: action.payload.success, 104 | }; 105 | case NEW_REVIEW_RESET: 106 | return { 107 | success: false, 108 | }; 109 | case NEW_REVIEW_FAIL: 110 | return { 111 | loading: false, 112 | success: false, 113 | }; 114 | case CLEAR_ERRORS: 115 | return { 116 | ...state, 117 | error: null, 118 | }; 119 | default: 120 | return state; 121 | } 122 | }; 123 | 124 | // New room Reducer 125 | export const newRoomReducer = (state = { room: {} }, action) => { 126 | switch (action.type) { 127 | case NEW_ROOM_REQUEST: 128 | return { 129 | loading: true, 130 | }; 131 | case NEW_ROOM_SUCCESS: 132 | return { 133 | loading: false, 134 | success: action.payload.success, 135 | room: action.payload.room, 136 | }; 137 | case NEW_ROOM_RESET: 138 | return { 139 | success: false, 140 | }; 141 | case NEW_ROOM_FAIL: 142 | return { 143 | loading: false, 144 | error: action.payload, 145 | }; 146 | case CLEAR_ERRORS: 147 | return { 148 | ...state, 149 | error: null, 150 | }; 151 | default: 152 | return state; 153 | } 154 | }; 155 | 156 | // Room Reducer 157 | export const roomReducer = (state = {}, action) => { 158 | switch (action.type) { 159 | case UPDATE_ROOM_REQUEST: 160 | case DELETE_ROOM_REQUEST: 161 | return { 162 | loading: true, 163 | }; 164 | case UPDATE_ROOM_SUCCESS: 165 | return { 166 | loading: false, 167 | isUpdated: action.payload, 168 | }; 169 | case DELETE_ROOM_SUCCESS: 170 | return { 171 | loading: false, 172 | isDeleted: action.payload, 173 | }; 174 | case UPDATE_PROFILE_RESET: 175 | return { 176 | loading: false, 177 | isUpdated: false, 178 | }; 179 | case DELETE_ROOM_RESET: 180 | return { 181 | loading: false, 182 | isDeleted: false, 183 | }; 184 | case UPDATE_ROOM_FAIL: 185 | case DELETE_ROOM_FAIL: 186 | return { 187 | loading: false, 188 | error: action.payload, 189 | }; 190 | case CLEAR_ERRORS: 191 | return { 192 | ...state, 193 | error: null, 194 | }; 195 | default: 196 | return state; 197 | } 198 | }; 199 | 200 | // check review availability 201 | export const checkReviewReducer = ( 202 | state = { reviewAvailable: null }, 203 | action 204 | ) => { 205 | switch (action.type) { 206 | case REVIEW_AVAILABILITY_REQUEST: 207 | return { 208 | loading: true, 209 | }; 210 | 211 | case REVIEW_AVAILABILITY_SUCCESS: 212 | return { 213 | loading: false, 214 | reviewAvailable: action.payload, 215 | }; 216 | 217 | case REVIEW_AVAILABILITY_FAIL: 218 | return { 219 | loading: false, 220 | error: action.payload, 221 | }; 222 | 223 | case CLEAR_ERRORS: 224 | return { 225 | ...state, 226 | error: null, 227 | }; 228 | 229 | default: 230 | return state; 231 | } 232 | }; 233 | 234 | // Room Review Reducer 235 | export const roomReviewsReducer = (state = { reviews: [] }, action) => { 236 | switch (action.type) { 237 | case GET_REVIEWS_REQUEST: 238 | return { 239 | loading: true, 240 | }; 241 | case GET_REVIEWS_SUCCESS: 242 | return { 243 | loading: false, 244 | reviews: action.payload.success, 245 | }; 246 | case GET_REVIEWS_FAIL: 247 | return { 248 | loading: false, 249 | error: action.payload, 250 | }; 251 | case CLEAR_ERRORS: 252 | return { 253 | ...state, 254 | error: null, 255 | }; 256 | default: 257 | return state; 258 | } 259 | }; 260 | 261 | // review Reducer 262 | export const reviewReducer = (state = {}, action) => { 263 | switch (action.type) { 264 | case DELETE_REVIEW_REQUEST: 265 | return { 266 | loading: true, 267 | }; 268 | 269 | case DELETE_REVIEW_SUCCESS: 270 | return { 271 | loading: false, 272 | isDeleted: action.payload, 273 | }; 274 | 275 | case DELETE_REVIEW_RESET: 276 | return { 277 | loading: false, 278 | isDeleted: false, 279 | }; 280 | 281 | case DELETE_REVIEW_FAIL: 282 | return { 283 | loading: false, 284 | error: action.payload, 285 | }; 286 | 287 | case CLEAR_ERRORS: 288 | return { 289 | ...state, 290 | error: null, 291 | }; 292 | 293 | default: 294 | return state; 295 | } 296 | }; 297 | -------------------------------------------------------------------------------- /redux/reducers/userReducers.js: -------------------------------------------------------------------------------- 1 | import { 2 | REGISTER_USER_REQUEST, 3 | REGISTER_USER_SUCCESS, 4 | REGISTER_USER_FAIL, 5 | LOAD_USER_REQUEST, 6 | LOAD_USER_SUCCESS, 7 | LOAD_USER_FAIL, 8 | UPDATE_PROFILE_REQUEST, 9 | UPDATE_PROFILE_SUCCESS, 10 | UPDATE_PROFILE_RESET, 11 | UPDATE_PROFILE_FAIL, 12 | FORGOT_PASSWORD_REQUEST, 13 | FORGOT_PASSWORD_SUCCESS, 14 | FORGOT_PASSWORD_FAIL, 15 | RESET_PASSWORD_REQUEST, 16 | RESET_PASSWORD_SUCCESS, 17 | RESET_PASSWORD_FAIL, 18 | CLEAR_ERRORS, 19 | ADMIN_USERS_REQUEST, 20 | ADMIN_USERS_SUCCESS, 21 | ADMIN_USERS_FAIL, 22 | USER_DETAILS_REQUEST, 23 | USER_DETAILS_SUCCESS, 24 | USER_DETAILS_FAIL, 25 | UPDATE_USER_REQUEST, 26 | UPDATE_USER_SUCCESS, 27 | UPDATE_USER_RESET, 28 | UPDATE_USER_FAIL, 29 | DELETE_USER_REQUEST, 30 | DELETE_USER_SUCCESS, 31 | DELETE_USER_RESET, 32 | DELETE_USER_FAIL, 33 | } from "../constants/userConstants"; 34 | 35 | // Auth Reducer 36 | export const authReducer = (state = { user: null }, action) => { 37 | switch (action.type) { 38 | case REGISTER_USER_REQUEST: 39 | return { 40 | loading: true, 41 | }; 42 | case REGISTER_USER_SUCCESS: 43 | return { 44 | loading: false, 45 | success: true, 46 | }; 47 | case REGISTER_USER_FAIL: 48 | return { 49 | loading: false, 50 | error: action.payload, 51 | }; 52 | case CLEAR_ERRORS: 53 | return { 54 | ...state, 55 | error: null, 56 | }; 57 | default: 58 | return state; 59 | } 60 | }; 61 | 62 | // Load User Reducer 63 | export const loadedUserReducer = ( 64 | state = { loading: true, user: null }, 65 | action 66 | ) => { 67 | switch (action.type) { 68 | case LOAD_USER_REQUEST: 69 | return { 70 | loading: true, 71 | isAuthenticated: false, 72 | }; 73 | case LOAD_USER_SUCCESS: 74 | return { 75 | loading: false, 76 | isAuthenticated: true, 77 | user: action.payload, 78 | }; 79 | case LOAD_USER_FAIL: 80 | return { 81 | loading: false, 82 | isAuthenticated: false, 83 | error: action.payload, 84 | }; 85 | case CLEAR_ERRORS: 86 | return { 87 | ...state, 88 | error: null, 89 | }; 90 | default: 91 | return state; 92 | } 93 | }; 94 | 95 | // User Reducer 96 | export const userReducer = (state = {}, action) => { 97 | switch (action.type) { 98 | case UPDATE_PROFILE_REQUEST: 99 | case UPDATE_USER_REQUEST: 100 | case DELETE_USER_REQUEST: 101 | return { 102 | loading: true, 103 | }; 104 | case UPDATE_PROFILE_SUCCESS: 105 | case UPDATE_USER_SUCCESS: 106 | return { 107 | loading: false, 108 | isUpdated: action.payload, 109 | }; 110 | case DELETE_USER_SUCCESS: 111 | return { 112 | loading: false, 113 | isDeleted: action.payload, 114 | }; 115 | case UPDATE_PROFILE_RESET: 116 | case UPDATE_USER_RESET: 117 | return { 118 | loading: false, 119 | isUpdated: false, 120 | }; 121 | case DELETE_USER_RESET: 122 | return { 123 | loading: false, 124 | isDeleted: false, 125 | }; 126 | case UPDATE_PROFILE_FAIL: 127 | case UPDATE_USER_FAIL: 128 | case DELETE_USER_FAIL: 129 | return { 130 | loading: false, 131 | error: action.payload, 132 | }; 133 | case CLEAR_ERRORS: 134 | return { 135 | ...state, 136 | error: null, 137 | }; 138 | default: 139 | return state; 140 | } 141 | }; 142 | 143 | // Forgot Password Reducer 144 | export const forgotPasswordReducer = (state = {}, action) => { 145 | switch (action.type) { 146 | case FORGOT_PASSWORD_REQUEST: 147 | case RESET_PASSWORD_REQUEST: 148 | return { 149 | loading: true, 150 | }; 151 | 152 | case FORGOT_PASSWORD_SUCCESS: 153 | return { 154 | loading: false, 155 | message: action.payload, 156 | }; 157 | case RESET_PASSWORD_SUCCESS: 158 | return { 159 | loading: false, 160 | success: action.payload, 161 | }; 162 | 163 | case FORGOT_PASSWORD_FAIL: 164 | case RESET_PASSWORD_FAIL: 165 | return { 166 | loading: false, 167 | error: action.payload, 168 | }; 169 | 170 | case CLEAR_ERRORS: 171 | return { 172 | ...state, 173 | error: null, 174 | }; 175 | 176 | default: 177 | return state; 178 | } 179 | }; 180 | 181 | // All Users Reducer 182 | export const allUsersReducer = (state = { users: [] }, action) => { 183 | switch (action.type) { 184 | case ADMIN_USERS_REQUEST: 185 | return { 186 | loading: true, 187 | }; 188 | case ADMIN_USERS_SUCCESS: 189 | return { 190 | loading: false, 191 | users: action.payload, 192 | }; 193 | case ADMIN_USERS_FAIL: 194 | return { 195 | loading: false, 196 | error: action.payload, 197 | }; 198 | case CLEAR_ERRORS: 199 | return { 200 | ...state, 201 | error: null, 202 | }; 203 | default: 204 | return state; 205 | } 206 | }; 207 | 208 | // User Details Reducer 209 | export const userDetailsReducer = (state = { users: {} }, action) => { 210 | switch (action.type) { 211 | case USER_DETAILS_REQUEST: 212 | return { 213 | ...state, 214 | loading: true, 215 | }; 216 | case USER_DETAILS_SUCCESS: 217 | return { 218 | loading: false, 219 | users: action.payload, 220 | }; 221 | case USER_DETAILS_FAIL: 222 | return { 223 | loading: false, 224 | error: action.payload, 225 | }; 226 | case CLEAR_ERRORS: 227 | return { 228 | ...state, 229 | error: null, 230 | }; 231 | default: 232 | return state; 233 | } 234 | }; 235 | -------------------------------------------------------------------------------- /redux/store.js: -------------------------------------------------------------------------------- 1 | import { createWrapper, HYDRATE } from "next-redux-wrapper"; 2 | import { applyMiddleware, createStore } from "redux"; 3 | import thunkMiddleware from "redux-thunk"; 4 | 5 | import { reducers } from "./reducers/reducers"; 6 | 7 | const bindMiddleware = (middleware) => { 8 | if (process.env.NODE_ENV === "prd") { 9 | const { composeWithDevTools } = require("redux-devtools-extension"); 10 | return composeWithDevTools(applyMiddleware(...middleware)); 11 | } 12 | 13 | return applyMiddleware(...middleware); 14 | }; 15 | 16 | const reducer = (state, action) => { 17 | if (action.type === HYDRATE) { 18 | const nextState = { 19 | ...state, 20 | ...action.payload, 21 | }; 22 | return nextState; 23 | } else { 24 | return reducers(state, action); 25 | } 26 | }; 27 | 28 | const initStore = () => { 29 | return createStore(reducer, bindMiddleware([thunkMiddleware])); 30 | }; 31 | 32 | export const wrapper = createWrapper(initStore); 33 | -------------------------------------------------------------------------------- /styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | min-height: 100vh; 3 | padding: 0 0.5rem; 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: center; 7 | align-items: center; 8 | } 9 | 10 | .main { 11 | padding: 5rem 0; 12 | flex: 1; 13 | display: flex; 14 | flex-direction: column; 15 | justify-content: center; 16 | align-items: center; 17 | } 18 | 19 | .footer { 20 | width: 100%; 21 | height: 100px; 22 | border-top: 1px solid #eaeaea; 23 | display: flex; 24 | justify-content: center; 25 | align-items: center; 26 | } 27 | 28 | .footer img { 29 | margin-left: 0.5rem; 30 | } 31 | 32 | .footer a { 33 | display: flex; 34 | justify-content: center; 35 | align-items: center; 36 | } 37 | 38 | .title a { 39 | color: #0070f3; 40 | text-decoration: none; 41 | } 42 | 43 | .title a:hover, 44 | .title a:focus, 45 | .title a:active { 46 | text-decoration: underline; 47 | } 48 | 49 | .title { 50 | margin: 0; 51 | line-height: 1.15; 52 | font-size: 4rem; 53 | } 54 | 55 | .title, 56 | .description { 57 | text-align: center; 58 | } 59 | 60 | .description { 61 | line-height: 1.5; 62 | font-size: 1.5rem; 63 | } 64 | 65 | .code { 66 | background: #fafafa; 67 | border-radius: 5px; 68 | padding: 0.75rem; 69 | font-size: 1.1rem; 70 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, 71 | Bitstream Vera Sans Mono, Courier New, monospace; 72 | } 73 | 74 | .grid { 75 | display: flex; 76 | align-items: center; 77 | justify-content: center; 78 | flex-wrap: wrap; 79 | max-width: 800px; 80 | margin-top: 3rem; 81 | } 82 | 83 | .card { 84 | margin: 1rem; 85 | flex-basis: 45%; 86 | padding: 1.5rem; 87 | text-align: left; 88 | color: inherit; 89 | text-decoration: none; 90 | border: 1px solid #eaeaea; 91 | border-radius: 10px; 92 | transition: color 0.15s ease, border-color 0.15s ease; 93 | } 94 | 95 | .card:hover, 96 | .card:focus, 97 | .card:active { 98 | color: #0070f3; 99 | border-color: #0070f3; 100 | } 101 | 102 | .card h3 { 103 | margin: 0 0 1rem 0; 104 | font-size: 1.5rem; 105 | } 106 | 107 | .card p { 108 | margin: 0; 109 | font-size: 1.25rem; 110 | line-height: 1.5; 111 | } 112 | 113 | .logo { 114 | height: 1em; 115 | } 116 | 117 | @media (max-width: 600px) { 118 | .grid { 119 | width: 100%; 120 | flex-direction: column; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | width: 100%; 4 | height: 100%; 5 | margin: 0px; 6 | padding: 0px; 7 | overflow-x: hidden; 8 | } 9 | 10 | /* Scroll Bar */ 11 | 12 | ::-webkit-scrollbar-track { 13 | background-color: #f5f5f5; 14 | } 15 | 16 | ::-webkit-scrollbar { 17 | width: 8px; 18 | background-color: #f5f5f5; 19 | } 20 | 21 | ::-webkit-scrollbar-thumb { 22 | background-color: rgba(66, 66, 66, 0.2); 23 | border: 0px; 24 | background-clip: padding-box; 25 | border-radius: 5px; 26 | } 27 | 28 | /* Loader */ 29 | 30 | .center-loader { 31 | margin-left: auto; 32 | margin-right: auto; 33 | padding: 150px 0; 34 | } 35 | 36 | .lds-ellipsis { 37 | display: inline-block; 38 | position: relative; 39 | width: 80px; 40 | height: 80px; 41 | } 42 | .lds-ellipsis div { 43 | position: absolute; 44 | top: 33px; 45 | width: 13px; 46 | height: 13px; 47 | border-radius: 50%; 48 | background: #e61e4d; 49 | animation-timing-function: cubic-bezier(0, 1, 1, 0); 50 | } 51 | .lds-ellipsis div:nth-child(1) { 52 | left: 8px; 53 | animation: lds-ellipsis1 0.6s infinite; 54 | } 55 | .lds-ellipsis div:nth-child(2) { 56 | left: 8px; 57 | animation: lds-ellipsis2 0.6s infinite; 58 | } 59 | .lds-ellipsis div:nth-child(3) { 60 | left: 32px; 61 | animation: lds-ellipsis2 0.6s infinite; 62 | } 63 | .lds-ellipsis div:nth-child(4) { 64 | left: 56px; 65 | animation: lds-ellipsis3 0.6s infinite; 66 | } 67 | @keyframes lds-ellipsis1 { 68 | 0% { 69 | transform: scale(0); 70 | } 71 | 100% { 72 | transform: scale(1); 73 | } 74 | } 75 | @keyframes lds-ellipsis3 { 76 | 0% { 77 | transform: scale(1); 78 | } 79 | 100% { 80 | transform: scale(0); 81 | } 82 | } 83 | @keyframes lds-ellipsis2 { 84 | 0% { 85 | transform: translate(0, 0); 86 | } 87 | 100% { 88 | transform: translate(24px, 0); 89 | } 90 | } 91 | 92 | /* Button Loader */ 93 | .lds-dual-ring { 94 | display: inline-block; 95 | width: 16px; 96 | height: 16px; 97 | } 98 | .lds-dual-ring:after { 99 | content: " "; 100 | display: block; 101 | width: 24px; 102 | height: 24px; 103 | border-radius: 50%; 104 | border: 3px solid #fff; 105 | border-color: #fff transparent #fff transparent; 106 | animation: lds-dual-ring 1.2s linear infinite; 107 | } 108 | @keyframes lds-dual-ring { 109 | 0% { 110 | transform: rotate(0deg); 111 | } 112 | 100% { 113 | transform: rotate(360deg); 114 | } 115 | } 116 | 117 | /* Login & Register */ 118 | 119 | .wrapper { 120 | display: flex; 121 | justify-content: center; 122 | align-items: center; 123 | margin-top: 5rem; 124 | 125 | font-weight: 700; 126 | } 127 | 128 | .wrapper form { 129 | padding: 2.5rem 3rem; 130 | } 131 | 132 | .wrapper form .btn, 133 | .booking-btn, 134 | .update-btn, 135 | #review_btn, 136 | .new-room-btn, 137 | #forgot_password_button, 138 | #new_password_button, 139 | .search-form .btn { 140 | background-color: #e61e4d; 141 | border-color: #e61e4d; 142 | color: white; 143 | margin-top: 2.5rem; 144 | font-weight: bold; 145 | } 146 | 147 | .review-btn { 148 | background-color: #e61e4d; 149 | border-color: #e61e4d; 150 | margin-top: 2rem; 151 | border-radius: 5rem; 152 | } 153 | 154 | .new-room-btn { 155 | background-color: #e61e4d; 156 | border-color: #e61e4d; 157 | } 158 | 159 | .wrapper form a { 160 | font-size: 0.9rem; 161 | color: grey; 162 | } 163 | 164 | .react-datepicker { 165 | width: 100%; 166 | } 167 | 168 | .react-datepicker__month-container { 169 | float: left; 170 | width: 100%; 171 | } 172 | 173 | /* Avatar */ 174 | .avatar { 175 | display: inline-block; 176 | margin-bottom: 0; 177 | height: 3rem; 178 | width: 3rem; 179 | -webkit-border-radius: 50%; 180 | -moz-border-radius: 50%; 181 | border-radius: 50%; 182 | } 183 | 184 | .avatar img { 185 | width: 100%; 186 | height: 100%; 187 | object-fit: cover; 188 | } 189 | 190 | .rounded-circle { 191 | border-radius: 50% !important; 192 | } 193 | 194 | /* Header User Profile */ 195 | 196 | .avatar-nav { 197 | margin-right: 0.6rem; 198 | height: 2.2rem; 199 | width: 2.2rem; 200 | } 201 | 202 | /* Navbar */ 203 | nav { 204 | background-color: white; 205 | padding: 1rem 1rem; 206 | -webkit-box-shadow: 0 4px 6px -6px #222; 207 | -moz-box-shadow: 0 4px 6px -6px #222; 208 | box-shadow: 0 4px 6px -6px #222; 209 | } 210 | 211 | .login-header-btn { 212 | background-color: #e61e4d; 213 | } 214 | 215 | .login-header-btn a { 216 | color: white; 217 | } 218 | 219 | /* Home page */ 220 | 221 | .stays-heading { 222 | font-weight: 700; 223 | } 224 | 225 | .back-to-search { 226 | font-size: 1.1rem; 227 | color: #e61e4d; 228 | font-weight: 600; 229 | } 230 | 231 | .back-to-search:hover { 232 | color: #e61e4d; 233 | } 234 | 235 | .card { 236 | height: 100%; 237 | border: 0px; 238 | } 239 | 240 | .card-title a { 241 | color: #2e2e2e; 242 | font-size: 1.2rem; 243 | } 244 | 245 | .card-title a:hover { 246 | color: #e61e4d; 247 | text-decoration: none; 248 | } 249 | 250 | .card-body { 251 | padding-left: 0; 252 | padding-right: 0; 253 | } 254 | 255 | .card-text { 256 | font-size: 1.2rem; 257 | color: black; 258 | } 259 | 260 | .view-btn { 261 | background-color: #e61e4d; 262 | } 263 | 264 | .view-btn a { 265 | color: white; 266 | } 267 | 268 | .card-img-top { 269 | border-radius: 0.8rem; 270 | } 271 | 272 | .ratings { 273 | font-size: 1.2rem; 274 | color: #e61e4d; 275 | } 276 | 277 | #no_of_reviews { 278 | font-size: 0.85rem; 279 | color: grey; 280 | margin-left: 0.5rem; 281 | } 282 | 283 | /* Ratings */ 284 | 285 | .rating-outer { 286 | display: inline-block; 287 | position: relative; 288 | font-family: FontAwesome; 289 | color: #e61e4d; 290 | } 291 | 292 | .rating-outer::before { 293 | content: "\f006 \f006 \f006 \f006 \f006"; 294 | } 295 | 296 | .rating-inner { 297 | position: absolute; 298 | top: 0; 299 | left: 0; 300 | white-space: nowrap; 301 | overflow: hidden; 302 | width: 0; 303 | } 304 | 305 | .rating-inner::before { 306 | content: "\f005 \f005 \f005 \f005 \f005"; 307 | color: #e61e4d; 308 | } 309 | 310 | /* Room Details Page */ 311 | 312 | .booking-btn:hover { 313 | color: white; 314 | } 315 | 316 | .price-per-night { 317 | font-size: 1.3rem; 318 | } 319 | 320 | .booking-card { 321 | border: 1px solid #e3e3e3; 322 | border-radius: 20px; 323 | padding: 5px 10px; 324 | } 325 | 326 | .room-feature { 327 | font-size: 1.1rem; 328 | color: #404040; 329 | } 330 | 331 | .room-feature p { 332 | margin-left: 0.4rem; 333 | display: inline-block; 334 | } 335 | 336 | /* Reviews */ 337 | .review_user { 338 | font-size: 0.8rem; 339 | color: grey; 340 | } 341 | 342 | .rating { 343 | margin-top: 10rem; 344 | } 345 | 346 | .stars { 347 | height: 100px; 348 | display: flex; 349 | align-items: center; 350 | padding-left: 0; 351 | } 352 | 353 | .star { 354 | display: inline; 355 | list-style: none; 356 | font-size: 3rem; 357 | padding-left: 0.9rem; 358 | color: #e3e3e3; 359 | } 360 | 361 | .star:first-child { 362 | padding-left: 0; 363 | } 364 | 365 | .red { 366 | color: #e61e4d; 367 | } 368 | 369 | .light-red { 370 | color: #f74f77; 371 | } 372 | 373 | /* Footer */ 374 | footer { 375 | margin-top: 6rem; 376 | color: grey; 377 | bottom: 0; 378 | width: 100%; 379 | } 380 | 381 | /* Stripe payment design */ 382 | 383 | #stripe_pay_btn { 384 | white-space: nowrap; 385 | border: 0; 386 | outline: 0; 387 | display: inline-block; 388 | height: 40px; 389 | line-height: 40px; 390 | padding: 0 14px; 391 | box-shadow: 0 4px 6px rgba(50, 50, 93, 0.11), 0 1px 3px rgba(0, 0, 0, 0.08); 392 | color: #fff; 393 | border-radius: 4px; 394 | font-size: 15px; 395 | font-weight: 600; 396 | text-transform: uppercase; 397 | letter-spacing: 0.025em; 398 | background-color: #6772e5; 399 | text-decoration: none; 400 | -webkit-transition: all 150ms ease; 401 | transition: all 150ms ease; 402 | margin-top: 10px; 403 | } 404 | 405 | #stripe_pay_btn:hover { 406 | color: #fff; 407 | cursor: pointer; 408 | background-color: #7795f8; 409 | transform: translateY(-1px); 410 | box-shadow: 0 7px 14px rgba(50, 50, 93, 0.1), 0 3px 6px rgba(0, 0, 0, 0.08); 411 | } 412 | 413 | .StripeElement { 414 | display: block; 415 | margin: 10px 0 20px 0; 416 | max-width: 500px; 417 | padding: 10px 14px; 418 | font-size: 1em; 419 | font-family: "Source Code Pro", monospace; 420 | box-shadow: rgba(50, 50, 93, 0.14902) 0px 1px 3px, 421 | rgba(0, 0, 0, 0.0196078) 0px 1px 0px; 422 | border: 0; 423 | outline: 0; 424 | border-radius: 4px; 425 | background: white; 426 | } 427 | 428 | input::placeholder { 429 | color: #aab7c4; 430 | } 431 | 432 | .StripeElement--focus { 433 | box-shadow: rgba(50, 50, 93, 0.109804) 0px 4px 6px, 434 | rgba(0, 0, 0, 0.0784314) 0px 1px 3px; 435 | -webkit-transition: all 150ms ease; 436 | transition: all 150ms ease; 437 | } 438 | 439 | .StripeElement.IdealBankElement, 440 | .StripeElement.FpxBankElement, 441 | .StripeElement.PaymentRequestButton { 442 | padding: 0; 443 | } 444 | 445 | .StripeElement.PaymentRequestButton { 446 | height: 40px; 447 | } 448 | 449 | /* Pagination */ 450 | .page-item.active .page-link { 451 | background-color: #e61e4d; 452 | border-color: #e61e4d; 453 | } 454 | 455 | .page-link { 456 | color: #e61e4d; 457 | } 458 | 459 | .page-link:hover { 460 | color: #e61e4d; 461 | } 462 | 463 | /* Booking Details */ 464 | .booking-details p { 465 | margin-left: 1.5rem; 466 | } 467 | 468 | .redColor { 469 | color: red; 470 | } 471 | 472 | .greenColor { 473 | color: green; 474 | } 475 | 476 | /* 404 Page */ 477 | 478 | .page-not-found-wrapper { 479 | display: flex; 480 | flex-direction: column; 481 | justify-content: center; 482 | align-items: center; 483 | text-align: center; 484 | min-height: 68vh; 485 | } 486 | 487 | #title_404 { 488 | color: #e61e4d; 489 | font-size: 7rem; 490 | } 491 | 492 | #description_404 { 493 | color: grey; 494 | } 495 | -------------------------------------------------------------------------------- /utils/apiFeatures.js: -------------------------------------------------------------------------------- 1 | class APIFeatures { 2 | constructor(query, queryStr) { 3 | this.query = query; 4 | this.queryStr = queryStr; 5 | } 6 | 7 | search() { 8 | const location = this.queryStr.location 9 | ? { 10 | address: { 11 | $regex: this.queryStr.location, 12 | $options: "i", 13 | }, 14 | } 15 | : {}; 16 | this.query = this.query.find({ ...location }); 17 | return this; 18 | } 19 | 20 | filter() { 21 | const queryCopy = { ...this.queryStr }; 22 | // Remove fields from query 23 | const removeFields = ["location", "page"]; 24 | removeFields.forEach((i) => delete queryCopy[i]); 25 | 26 | this.query = this.query.find(queryCopy); 27 | return this; 28 | } 29 | 30 | pagination(resPerPage) { 31 | const currentPage = Number(this.queryStr.page) || 1; 32 | const skip = resPerPage * (currentPage - 1); 33 | 34 | this.query = this.query.limit(resPerPage).skip(skip); 35 | return this; 36 | } 37 | } 38 | 39 | export default APIFeatures; 40 | -------------------------------------------------------------------------------- /utils/errorHandler.js: -------------------------------------------------------------------------------- 1 | class ErrorHandler extends Error { 2 | constructor(message, statusCode) { 3 | super(message); 4 | this.statusCode = statusCode; 5 | Error.captureStackTrace(this, this.constructor); 6 | } 7 | } 8 | 9 | export default ErrorHandler; 10 | -------------------------------------------------------------------------------- /utils/getStripe.js: -------------------------------------------------------------------------------- 1 | import { loadStripe } from "@stripe/stripe-js"; 2 | 3 | let stripePromise; 4 | 5 | const apiKey = `${process.env.NEXT_PUBLIC_STRIPE_API_KEY}`; 6 | 7 | export const getStripe = async () => { 8 | if (!stripePromise) { 9 | stripePromise = await loadStripe(apiKey); 10 | } 11 | return stripePromise; 12 | }; 13 | -------------------------------------------------------------------------------- /utils/seeder.js: -------------------------------------------------------------------------------- 1 | const Room = require("../models/room"); 2 | const mongoose = require("mongoose"); 3 | 4 | const rooms = require("../data/rooms"); 5 | 6 | mongoose.connect("", { 7 | useNewUrlParser: true, 8 | useUnifiedTopology: true, 9 | }); 10 | 11 | const seedRooms = async () => { 12 | try { 13 | await Room.deleteMany(); 14 | console.log("Rooms are deleted"); 15 | 16 | await Room.insertMany(rooms); 17 | console.log("All Rooms are added."); 18 | 19 | process.exit(); 20 | } catch (error) { 21 | console.log(error.message); 22 | process.exit(); 23 | } 24 | }; 25 | 26 | seedRooms(); 27 | -------------------------------------------------------------------------------- /utils/sendEmail.js: -------------------------------------------------------------------------------- 1 | import { createTransport } from "nodemailer"; 2 | 3 | export const sendEmail = async (options) => { 4 | const transporter = createTransport({ 5 | host: process.env.SMTP_HOST, 6 | port: process.env.SMTP_PORT, 7 | auth: { 8 | user: process.env.SMTP_USER, 9 | pass: process.env.SMTP_PASSWORD, 10 | }, 11 | }); 12 | 13 | const message = { 14 | from: `${process.env.SMTP_FROM_NAME} <${process.env.SMTP_FROM_EMAIL}>`, 15 | to: options.email, 16 | subject: options.subject, 17 | text: options.message, 18 | }; 19 | 20 | await transporter.sendMail(message); 21 | }; 22 | --------------------------------------------------------------------------------