Restaurant Reservation System using MERN Stack
Restaurant reservation system that helps people book tables at restaurants they like. It’s built using React, Node.js, Express, and the MERN stack, along with Sass for styling. Users can search for restaurants based on their preferences, see details like location, description, price range, and contact info, and then pick a date, time, and number of people for their reservation. MongoDB stores all the important information about restaurants, reservations, and users. React makes the system easy to use with its dynamic and interactive features.
Project Preview: Let us have a look at how the final output will look like.
Prerequisites
Approach to Create Restaurant Reservation System
The website has the following features:
- Authenticate & Authorization: Separate admin and customer register and login. Only admins can create restaurant details and view reservations at their restaurant and only customers can make reservations and view reservations made by them.
- Admin Side: The admin serves the purpose of creating and viewing their restaurant and monitoring the reservations made at their restaurant.
- Search of Restaurants: The customers are at the front of the business and they can search their favorite restaurants with the help of names or locations.
- Restaurant Page: On clicking a restaurant, customers can view details like name, location on map, images, rating etc and make reservations.
- Reservations: Customers can make reservations based on availability of slots on particular days and also add number of people. Once a slot is booked for a particular day, it cannot be booked by anyone again, unless cancelled.
- Cancel: Customers can cancel reservations made by them.
- Interactive Maps: The website also houses a map on search page depicting the restaurants and on respective restaurant page a map showing the location. This is an additional feature made to make the website interactive, it can be skipped if not necessary.
- Security: Helmet is used in the backend to protect from database injections. Bcryptjs is used to encrypt and decrypt user password and then send it to the database.
Steps to Create the Backend of the Project
Step 1: Start a node.js app by typing the following command
npm init -y
Step 2: Install the following packages
npm i bcyptjs body-parser cors dotenv express helmet jsonwebtoken mongoose morgan nodemon
package.json file should be looking like:
"dependencies": {
"bcryptjs": "^2.4.3",
"body-parser": "^1.20.2",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.18.3",
"helmet": "^7.1.0",
"jsonwebtoken": "^9.0.2",
"mongoose": "^8.2.1",
"morgan": "^1.10.0",
"nodemon": "^3.1.0"
}
Step 3: Create the following folder structure. We are going to use the model-view-controller architecture (MVC).
Project Structure(Backend)
Example: Below is the code for Backend of Restaurant Reservation System.
// api/index.js
import express from "express";
import dotenv from "dotenv";
import helmet from "helmet";
import morgan from "morgan";
import mongoose from "mongoose";
import cookieParser from "cookie-parser";
import cors from "cors"
import adminRoute from "./routes/admins.js"
import userRoute from "./routes/users.js"
import restRoute from "./routes/rests.js"
import reservRoute from "./routes/reservations.js"
const app = express();
dotenv.config();
const PORT = process.env.PORT || 7700;
const connect = async () => {
try {
await mongoose.connect(process.env.MONGO);
console.log("Connected to mongoDB.");
} catch (error) {
throw error;
}
};
mongoose.connection.on("disconnected", () => {
console.log("mongoDB disconnected!");
});
app.get('/', (req, res) => { res.send('Hello from Express!') });
//middlewares
app.use(cookieParser())
app.use(express.json());
app.use(helmet());
app.use(cors({
origin: "http://localhost:3000",
credentials: true
}))
app.use(morgan("common"));
app.use("/api/admin", adminRoute);
app.use("/api/users", userRoute);
app.use("/api/restaurants", restRoute);
app.use("/api/reservations", reservRoute);
app.listen(PORT, () => {
console.log("Listening on port 7700");
connect();
});
// api/error.js
export const createError = (status, message) => {
const err = new Error();
err.status = status;
err.message = message;
return err;
}
// api/models/Admin.js
import mongoose from "mongoose";
const AdminSchema = new mongoose.Schema(
{
username: {
type: String,
require: true,
},
password: {
type: String,
required: true,
min: 6,
},
isAdmin: {
type: Boolean,
default: true,
},
rest: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Restaurant'
}
},
{ timestamps: true }
);
export default mongoose.model("Admin", AdminSchema);
// api/models/Reservation.js
import mongoose from "mongoose";
const ReservationSchema = new mongoose.Schema(
{
date: {
type: Date,
required: true
},
slot: {
type: String,
required: true
},
people: {
type: Number,
required: true
},
author: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
},
rest: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Restaurant'
}
},
{ timestamps: true }
);
export default mongoose.model("Reservation", ReservationSchema);
// api/models/Restaurant.js
import mongoose from "mongoose";
const RestaurantSchema = new mongoose.Schema(
{
name: {
type: 'String',
required: true
},
location: {
type: 'String',
required: true
},
admin: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Admin'
},
contact: {
type: 'String',
required: true
},
description: {
type: 'String',
required: true
},
photo: {
type: String,
required: true
},
price: {
type: 'String',
required: true
},
rating: {
type: Number
}
},
{ timestamps: true }
);
export default mongoose.model("Restaurant", RestaurantSchema);
// api/models/User.js
import mongoose from "mongoose";
const UserSchema = new mongoose.Schema(
{
username: {
type: String,
required: true,
},
email: {
type: String,
required: true,
unique: true,
},
password: {
type: String,
required: true,
},
isAdmin: {
type: Boolean,
default: false,
},
reservations: [
{
type: mongoose.Schema.Types.ObjectId,
ref: 'Reservation'
}
]
},
{ timestamps: true }
);
export default mongoose.model("User", UserSchema);
// api/controllers/admin.js
import Admin from "../models/Admin.js";
import bcrypt from "bcryptjs"
import jwt from "jsonwebtoken"
import { createError } from "../error.js"
export const register = async (req, res, next) => {
try {
const salt = bcrypt.genSaltSync(10);
const hash = bcrypt.hashSync(req.body.password, salt);
const newAdmin = new Admin({
...req.body,
password: hash
})
await newAdmin.save();
res.status(201).json("Admin has been created");
}
catch (err) {
next(err)
}
}
export const login = async (req, res, next) => {
try {
const admin = await Admin.findOne({ username: req.body.username })
if (!admin) return next(createError(404, "User not found"))
const isCorrectPassword = await bcrypt.compare(req.body.password, admin.password);
if (!isCorrectPassword) return next(createError(400, "Username or Password incorrect"));
const token = jwt.sign({ id: admin._id }, process.env.JWT)
const { password, ...otherDetails } = admin._doc;
res
.cookie("access_token", token, {
httpOnly: true,
})
.status(200)
.json({ details: { ...otherDetails }});
}
catch (err) {
next(err)
}
}
// api/controllers/reservation.js
import Reservation from "../models/Reservation.js";
import Restaurant from "../models/Restaurant.js";
import { slots } from "../data.js";
export const checkAvailableSlots = async(req, res, next) => {
const date = req.params.date;
const restId = req.params.id;
try {
const reservations = await Reservation.find({
rest: restId,
date: {$eq: new Date(date)}
});
const reservedSlots = reservations.map(reservation => reservation.slot);
const availableSlots = slots.filter(slot => !reservedSlots.includes(slot));
res.status(200).json(availableSlots);
}
catch(err) {
next(err)
}
}
export const createReservation = async (req, res, next) => {
const newRes = new Reservation(req.body);
try {
const reservation = await newRes.save();
res.status(200).json(reservation);
} catch (err) {
next(err);
}
};
// function to cancel reservation
export const deleteReservation = async (req, res, next) => {
const resId = req.params.id;
try {
await Reservation.findByIdAndDelete(resId);
res.status(200).json("Deleted Successfully");
} catch (err) {
next(err);
}
};
// Function to get reservations by user ID
export const getReservationsByUserId = async (req, res, next) => {
const userId = req.params.id;
try {
const reservations = await Reservation.find({ author: userId }).populate('rest', 'name');
res.status(200).json(reservations);
} catch (error) {
next(err);
}
};
// Function to get reservations by user ID
export const getReservationsByRestId = async (req, res, next) => {
const restId = req.params.id;
try {
const reservations = await Reservation.find({ rest: restId }).populate('rest', 'name');
res.status(200).json(reservations);
} catch (error) {
next(err);
}
};
// api/controllers/rest.js
// create, update, view, get all reservations
import Restaurant from "../models/Restaurant.js";
import Admin from "../models/Admin.js";
export const createRestaurant = async (req, res, next) => {
const adminId = req.body.admin;
const newRestaurant = new Restaurant(req.body);
try {
const savedRestaurant = await newRestaurant.save();
await Admin.findOneAndUpdate(
{ _id: adminId },
{ $set: { rest: savedRestaurant._id } },
{ new: true }
);
res.status(200).json(savedRestaurant);
} catch (err) {
next(err);
}
};
export const updateRestaurant = async (req, res, next) => {
try {
const restaurant = await Restaurant.findByIdAndUpdate(
req.params.id,
{ $set: req.body },
{ new: true }
);
res.status(200).json(restaurant);
} catch (err) {
next(err);
}
};
export const getRestaurant = async (req, res, next) => {
try {
const restaurant = await Restaurant.findById(req.params.id);
res.status(200).json(restaurant);
} catch (err) {
next(err);
}
};
export const getRestaurants = async (req, res, next) => {
try {
const rests = await Restaurant.find();
res.status(200).json(rests);
} catch (err) {
next(err)
}
}
// api/controllers/user.js
import User from "../models/User.js";
import bcrypt from "bcryptjs"
import jwt from "jsonwebtoken"
import { createError } from "../error.js"
export const register = async (req, res, next) => {
try {
const salt = bcrypt.genSaltSync(10);
const hash = bcrypt.hashSync(req.body.password, salt);
const newUser = new User({
...req.body,
password: hash
})
await newUser.save();
res.status(201).json("User has been created");
}
catch (err) {
next(err)
}
}
export const login = async (req, res, next) => {
try {
const user = await User.findOne({ username: req.body.username })
if (!user) return next(createError(404, "User not found"))
const isCorrectPassword = await bcrypt.compare(req.body.password, user.password);
if (!isCorrectPassword) return next(createError(400, "Username or Password incorrect"));
const token = jwt.sign({ id: user._id }, process.env.JWT)
const { password, ...otherDetails } = user._doc;
res
.cookie("access_token", token, {
httpOnly: true,
})
.status(200)
.json({ details: { ...otherDetails }});
}
catch (err) {
next(err)
}
}
// api/routes/admins.js
import express from "express";
import { login, register } from "../controllers/admin.js ";
const router = express.Router();
router.post("/register", register)
router.post("/login", login)
export default router;
// api/routes/reservations.js
import express from "express";
import {
createReservation,
getReservationsByUserId,
getReservationsByRestId,
deleteReservation,
checkAvailableSlots
} from "../controllers/reservation.js";
const router = express.Router();
router.get("/slots/:id/:date", checkAvailableSlots)
router.post("/", createReservation);
router.delete("/:id", deleteReservation);
router.get("/rest/:id", getReservationsByRestId);
router.get("/user/:id", getReservationsByUserId);
export default router;
import express from "express";
import {
createRestaurant,
getRestaurant,
getRestaurants,
updateRestaurant,
} from "../controllers/rest.js";
const router = express.Router();
router.post("/", createRestaurant);
router.put("/:id", updateRestaurant);
router.get("/:id", getRestaurant);
router.get("/", getRestaurants);
export default router;
// api/routes/users.js
import express from "express";
import { login, register } from "../controllers/user.js ";
const router = express.Router();
router.post("/register", register)
router.post("/login", login)
export default router;
// api/data.js
export const slots = [
"11:00 AM - 12:00 PM",
"12:00 PM - 01:00 PM",
"01:00 PM - 02:00 PM",
"02:00 PM - 03:00 PM",
"03:00 PM - 04:00 PM",
"04:00 PM - 05:00 PM",
"05:00 PM - 06:00 PM",
"06:00 PM - 07:00 PM",
"07:00 PM - 08:00 PM",
"08:00 PM - 09:00 PM",
"09:00 PM - 10:00 PM"
];
Steps to Create Frontend
Step 1: Create a react application by using the following command and navigate to the folder:
npx create-react-app client
cd client
Step 2: Install the following npm packages:
npm i @fortawesome/free-regular-svg-icons
@fortawesome/free-solid-svg-icons
@fortawesome/react-fontawesome
axios react-router-dom sass dotenv
mapbox-gl react-map-gl
This is the snippet of what package.json should look like after installing the dependencies
"dependencies": {
"@fortawesome/free-regular-svg-icons": "^6.5.1",
"@fortawesome/free-solid-svg-icons": "^6.5.1",
"@fortawesome/react-fontawesome": "^0.2.0",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"axios": "^1.6.7",
"dotenv": "^16.4.5",
"mapbox-gl": "^3.2.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-map-gl": "^7.1.7",
"react-router-dom": "^6.22.3",
"react-scripts": "5.0.1",
"sass": "^1.71.1",
"web-vitals": "^2.1.4"
}
Step 3: Create the following folder structure, separating pages, components and their styles.
Folder Structure(Frontend)
Step 4: Create .env file in the client folder and add the Mapbox access token there
REACT_APP_API_MAPBOX_KEY = <access_token>
Step 5: In the .gitignore file add .env.
// rest of the content in .gitignore
.env
Example: Below is the code of frontend Restaurant Reservation System:
/* client/src/index.css */
body {
margin: 0;
padding: 0;
font-family: "Raleway", sans-serif;
}
:root {
--magenta: #9F0D7F;
--bright-pink: #EA1179;
--black: #22092C;
--maroon: #872341;
--red: #BE3144;
--orange: #F05941;
--light-orange: #FFE4C9;
--cream: #FFF7F1;
}
/* client/src/styles/adminLanding.css */
body,
html {
overflow-x: hidden;
}
.createRestContainer {
background-color: var(--cream);
height: 100%;
}
.cpContainer {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
color: var(--maroon);
width: 100%;
padding: 50px;
}
.formContainer {
width: 70%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 100px 0;
flex-wrap: wrap;
}
.inputContainer {
width: 80%;
display: flex;
flex-direction: column;
flex-wrap: wrap;
gap: 20px;
justify-content: center;
align-items: center;
}
.inputContainer button {
width: 100px;
}
.star-rating-slider {
margin: 5px 0 15px 0;
}
.star-rating-slider .star-icon{
cursor: pointer;
margin: 0 5px;
}
.input {
display: flex;
flex-direction: column;
width: 90%;
}
.createRestContainer .column .input .formInput {
display: flex;
flex-direction: column;
width: 70%;
gap: 10px;
}
.input label {
font-size: 1rem;
font-weight: bold;
}
.inputContainer button {
width: 200px;
height: 40px;
margin-top: 20px;
border: none;
background: var(--bright-pink);
border-radius: 25px;
font-size: 18px;
color: white;
font-weight: 700;
cursor: pointer;
outline: none;
}
.input input {
font-size: 1rem;
height: 40px;
width: 100%;
font-size: 1rem;
outline: 0;
border: 0;
border-bottom: 1px solid var(--maroon);
background: transparent;
color: var(--orange);
}
.createRestContainer .column .input .type {
text-align: center;
height: 35px;
background: white;
border-radius: 5px;
color: #0c4957;
cursor: pointer;
font-weight: bold;
}
@media screen and (max-width: 800px) {
.inputContainer {
width: 80%;
}
.picsContainer {
width: 80%;
}
}
@media screen and (max-width: 600px) {
.formContainer {
width: 90%;
}
.inputContainer {
width: 100%;
}
.createRestContainer.column {
width: 100%;
margin-top: 50px;
}
}
/* client/src/styles/home.css */
.home {
background-color: var(--light-orange);
}
.search {
position: relative;
background-color: var(--orange);
height: 300px;
background-size: cover;
}
.searchBackground #surfer {
background-attachment: fixed;
background-size: cover;
height: 800px;
width: 100%;
filter: brightness(60%);
}
.searchBar {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.searchBar h2 {
color: white;
text-align: center;
font-size: 2.5em;
margin-bottom: 20px;
}
.searchInput {
background-color: white;
padding: 5px 10px;
border-radius: 50px;
width: 400px;
display: flex;
align-items: center;
justify-content: space-around;
}
.searchInput input {
border: none;
height: 40px;
width: 70%;
padding: 0 10px;
}
.searchInput input:focus {
outline: none;
}
.searchInput .icon {
cursor: pointer;
}
.searchedPosts {
position: relative;
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
padding: 50px 0;
gap: 50px;
}
#loading {
width: 20px;
height: 20px;
}
@media screen and (max-width: 700px) {
.searchInput {
width: 300px;
}
}
/* client/src/styles/landing.css */
.landing {
background-color: var(--cream);
height: 100vh;
position: relative;
.text {
position: absolute;
top: 50%;
left: 50%;
border: 7px solid var(--bright-pink);
padding: 50px;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 50px;
p {
font-size: 3rem;
span {
color: var(--magenta);
font-weight: 800;
font-style: italic;
}
}
button {
font-size: 1.5rem;
padding: 30px 50px;
border: none;
outline: none;
background-color: var(--orange);
color: white;
border-radius: 40px;
cursor: pointer;
transition: all ease-in-out 0.3s;
&:hover {
transform: translateY(-5px);
background-color: var(--bright-pink);
}
}
}
}
/* client/src/styles/register.css */
body,
html {
overflow-x: hidden;
}
.register {
background-color: var(--orange);
}
.registerCard {
background-size: cover;
background-repeat: no-repeat;
height: 1000px;
overflow: hidden;
position: relative;
}
.registerCard .center {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 400px;
background-color: white;
border-radius: 10px;
}
.center h1 {
font-size: 1.2rem;
text-align: center;
padding: 0 0 20px 0;
text-transform: capitalize;
border-bottom: 1px solid silver;
}
.center form {
padding: 0 40px;
box-sizing: border-box;
}
form .txt_field {
position: relative;
border-bottom: 2px solid #adadad;
margin: 30px 0;
}
form .txt_field_img {
position: relative;
margin-top: 10px;
}
.txt_field {
width: 100%;
padding: 0 5px;
height: 40px;
font-size: 16px;
border: none;
background: none;
outline: none;
}
.txt_field input {
width: 100%;
padding: 0 5px;
height: 40px;
font-size: 16px;
border: none;
background: none;
outline: none;
}
.registerCard form .image {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin-top: 20px;
}
.register input {
width: 100%;
height: 50px;
border: none;
background: transparent;
border-radius: 25px;
font-size: 16px;
color: black;
outline: none;
}
.login_button .button {
width: 100%;
height: 50px;
border: none;
background: transparent;
border-radius: 25px;
font-size: 18px;
color: white;
font-weight: 700;
cursor: pointer;
outline: none;
}
.login_button {
width: 100%;
height: 50px;
border: none;
background: var(--red);
border-radius: 25px;
font-size: 18px;
color: white;
font-weight: 700;
cursor: pointer;
outline: none;
}
.signup_link {
margin: 30px 0;
text-align: center;
font-size: 16px;
color: #666666;
}
.signup_link a {
color: var(--maroon);
text-decoration: none;
}
.signup_link a:hover {
text-decoration: underline;
}
@media screen and (max-width: 500px) {
.registerCard .center {
width: 300px;
margin: 0;
}
}
/* client/src/styles/reservation.css */
.reservation-container {
margin-top: 100px;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
gap: 50px;
flex-wrap: wrap;
}
/* client/src/styles/restaurant.css */
.restaurant {
.rest-container {
padding: 80px 0;
align-items: center;
justify-content: center;
display: flex;
gap: 50px;
margin-top: 30px;
width: 100%;
flex-wrap: wrap;
.leftContainer {
display: flex;
width: 40%;
gap: 20px;
flex-direction: column;
@media screen and (max-width:600px){
width: 80%;
}
.other-details {
display: flex;
flex-direction: column;
justify-content: center;
gap: 20px;
padding: 20px 0;
border-top: 2px solid var(--bright-pink);
border-bottom: 2px solid var(--bright-pink);
span {
color: var(--maroon);
font-weight: 700;
}
}
.reservation-box {
display: flex;
flex-direction: column;
justify-content: center;
background-color: var(--cream);
padding: 20px;
align-items: center;
gap: 20px;
.form-input {
display: flex;
flex-direction: column;
gap: 10px;
label {
font-size: 1.2rem;
font-weight: 700;
}
input {
width: 200px;
font-size: 1rem;
padding: 5px;
}
select {
width: 200px;
padding: 5px;
}
}
button {
width: 200px;
padding: 10px;
outline: none;
border: none;
cursor: pointer;
background-color: var(--magenta);
color: white;
border-radius: 20px;
font-size: 1rem;
font-weight: 700;
}
}
}
.rightContainer {
display: flex;
width: 40%;
align-items: center;
gap: 50px;
flex-direction: column;
.arrows {
display: flex;
gap: 10px;
font-size: 1.7rem;
margin-top: 20px;
color: var(--orange);
justify-content: center;
.arrow {
cursor: pointer;
}
}
@media screen and (max-width:600px){
width: 80%;
}
}
}
}
/* client/src/styles/card.css */
.cardContainer {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
padding: 10px;
width: 500px;
background-color: white;
.picContainer{
height: 150px;
width: fit-content;
img {
height: 150px;
width: 200px;
object-fit: cover;
}
}
.detailsContainer {
display: flex;
flex-direction: column;
padding: 0 10px;
justify-content: space-between;
gap: 10px;
h2 {
color: var(--maroon);
}
h3 {
color: var(--black);
width: inherit;
}
.star-rating-slider {
color: var(--black);
.star-icon {
color: var(--orange);
}
}
}
}
/* client/src/styles/navbar.css */
* {
margin: 0;
padding: 0;
text-decoration: none;
}
.navContainer {
overflow: hidden;
position: fixed;
top: 0;
left: 0;
right: 0;
background-color: var(--red);
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.1);
padding: 0px 7%;
display: flex;
align-items: center;
justify-content: space-between;
z-index: 100;
}
.navLogo {
color: white;
font-family: "Rubik Doodle Shadow", system-ui;
font-weight: 900;
font-size: 1.5rem;
font-style: italic;
}
.navbar ul {
list-style: none;
}
.navbar ul li {
position: relative;
float: left;
}
.profilePicture {
height: 40px;
width: 40px;
}
.profilePicture img {
margin-top: 10px;
height: 40px;
width: 40px;
border-radius: 50%;
object-fit: cover;
}
#usernamename {
display: none;
}
.navbar ul li p {
font-size: 1rem;
padding: 20px;
color: white;
display: block;
transition: all 1s;
}
.navbar ul li p:hover {
transform: translateY(-1px);
border-bottom: solid 2px white;
}
#menu-bar {
display: none;
}
.navContainer label {
font-size: 1.5rem;
color: white;
cursor: pointer;
display: none;
}
@media (max-width:800px) {
.navContainer {
height: 70px;
}
.navContainer label {
display: initial;
}
.navContainer .navbar {
position: fixed;
top: 70px;
left: -100%;
text-align: center;
background: white;
border-top: 1px solid rgba(0, 0, 0, 0.1);
display: block;
transition: all 0.3s ease;
width: 100%;
}
.profilePicture {
display: none;
}
#usernamename {
font-weight: bolder;
display: block;
}
.navbar ul li p {
color: black;
}
.navbar ul li p:hover {
transform: translateY(-1px);
border-bottom: none;
}
.navbar ul li {
width: 100%;
}
#menu-bar:checked~.navbar {
left: 0;
}
}
/* client/src/styles/reservationCard.css */
.reservation-card {
width: 40%;
height: fit-content;
background-color: var(--cream);
display: flex;
gap: 100px;
flex-wrap: wrap;
align-items: center;
justify-content: center;
padding: 50px;
.icon {
font-size: 30px;
color: var(--red);
width: 10%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease-in-out;
&:hover {
transform: translateY(-2px);
color: var(--maroon);
}
}
.details {
display: flex;
flex-direction: column;
gap: 20px;
width: 70%;
.res-name {
display: flex;
gap: 20px;
flex-wrap: wrap;
h1 {
color: var(--magenta);
}
button {
padding: 10px 20px;
text-align: center;
border: none;
background-color: var(--light-orange);
cursor: pointer;
font-size: 1.025rem;
transition: all 0.3s ease;
&:hover {
background-color: var(--orange);
color: white;
}
}
}
.res-details {
display: flex;
gap: 20px;
flex-wrap: wrap;
p {
font-size: 1.2rem;
font-weight: 800;
color: var(--maroon);
}
}
}
}
// client/src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import { AuthContextProvider } from './authContext';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<AuthContextProvider>
<React.StrictMode>
<App />
</React.StrictMode>
</AuthContextProvider>
);
// client/src/App.js
import {
BrowserRouter, Routes,
Route, Navigate
} from "react-router-dom"
import { useContext } from "react";
import { AuthContext } from "./authContext";
import AdminLanding from "./pages/AdminLanding"
import Login from "./pages/Login";
import Register from "./pages/Register";
import Landing from "./pages/Landing";
import Home from "./pages/Home";
import Restaurant from "./pages/Restaurant";
import Reservations from "./pages/Reservations";
function App() {
const { user } = useContext(AuthContext);
const ProtectedRoute = ({ children, redirectTo }) => {
if (!user || user.isAdmin) {
return <Navigate to={redirectTo} />;
} else {
return children;
}
};
const AdminProtectedRoute = ({ children, redirectTo }) => {
if (!user || !user.isAdmin) {
return <Navigate to={redirectTo} />;
} else {
return children;
}
};
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<ProtectedRoute redirectTo="/userLogin">
<Home />
</ProtectedRoute>} />
<Route path="/landing" element={<Landing />} />
<Route path="/adminLogin" element={<Login type="admin" />} />
<Route path="/restaurant/:id"
element={<ProtectedRoute redirectTo="/userLogin">
<Restaurant />
</ProtectedRoute>} />
<Route path="/reservations"
element={<ProtectedRoute redirectTo="/userLogin">
<Reservations />
</ProtectedRoute>} />
<Route path="/adminRegister" element={<Register type="admin" />} />
<Route path="/userLogin" element={<Login type="user" />} />
<Route path="/userRegister" element={<Register type="user" />} />
<Route path="/admin/dashboard" element={
<AdminProtectedRoute redirectTo="/adminLogin">
<AdminLanding />
</AdminProtectedRoute>
} />
</Routes>
</BrowserRouter>
);
}
export default App;
// client/src/useFetch.js
import { useEffect, useState } from "react";
import axios from "axios";
const useFetch = (url) => {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
useEffect(() => {
const fetchData = async () => {
setLoading(true);
try {
const res = await axios.get(url)
setData(res.data);
} catch (err) {
setError(err);
}
setLoading(false);
};
fetchData();
}, [url]);
const reFetch = async () => {
setLoading(true);
try {
const res = await axios.get(url)
setData(res.data);
} catch (err) {
setError(err);
}
setLoading(false);
};
return { data, loading, error, reFetch };
};
export default useFetch;
// client/src/authContext.js
import { createContext, useReducer, useEffect } from "react"
const INITIAL_STATE = {
user: JSON.parse(localStorage.getItem("user")) || null,
loading: false,
error: null,
};
export const AuthContext = createContext(INITIAL_STATE)
const AuthReducer = (state, action) => {
switch (action.type) {
case "LOGIN_START":
return {
user: null,
loading: true,
error: null
};
case "LOGIN_SUCCESS":
return {
user: action.payload,
loading: false,
error: null
};
case "LOGIN_FAILURE":
return {
user: null,
loading: false,
error: action.payload
};
case "LOGOUT":
return {
user: null,
loading: false,
error: null
};
default:
return state;
}
}
export const AuthContextProvider = ({ children }) => {
const [state, dispatch] = useReducer(AuthReducer, INITIAL_STATE)
useEffect(() => {
localStorage.setItem("user", JSON.stringify(state.user))
}, [state.user])
return (
<AuthContext.Provider
value={{
user: state.user,
loading: state.loading,
error: state.error,
dispatch
}}
>
{children}
</AuthContext.Provider>
)
}
// client/src/data.js
export const slots = [
"11:00 AM - 12:00 PM",
"12:00 PM - 01:00 PM",
"01:00 PM - 02:00 PM",
"02:00 PM - 03:00 PM",
"03:00 PM - 04:00 PM",
"04:00 PM - 05:00 PM",
"05:00 PM - 06:00 PM",
"06:00 PM - 07:00 PM",
"07:00 PM - 08:00 PM",
"08:00 PM - 09:00 PM",
"09:00 PM - 10:00 PM"
];
// client/src/pages/AdminLanding.jsx
import React, { useContext, useState } from "react";
import { faStar as solidStar } from "@fortawesome/free-solid-svg-icons";
import { faStar as regularStar } from "@fortawesome/free-regular-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import Navbar from '../components/Navbar'
import axios from "axios";
import { AuthContext } from "../authContext";
import { useNavigate } from "react-router-dom";
import { slots } from "../data";
import "../styles/adminLanding.scss"
const AdminLanding = () => {
const [info, setInfo] = useState({});
const [rating, setRating] = useState(0);
const { user } = useContext(AuthContext);
const navigate = useNavigate();
const handleStarClick = (selectedRating) => {
setRating(selectedRating);
};
const handleChange = (e) => {
setInfo((prev) => ({ ...prev, [e.target.id]: e.target.value }));
};
const handleClick = async (e) => {
e.preventDefault();
const newpost = {
...info,
admin: user._id,
rating: rating,
slots: slots
}
try {
const res = await axios.post("http://localhost:7700/api/restaurants", newpost)
console.log(res)
navigate(`/admin/restaurant/${res.data._id}`);
} catch (err) {
console.log(err);
}
};
return (
<div className="createRestContainer">
<Navbar />
<div className="cpContainer">
<div className="formContainer">
<div className="inputContainer">
<div className="input">
<label htmlFor="title">Name</label>
<input
onChange={handleChange}
type="text"
id="name"
placeholder="Enter Name"
/>
</div>
<div className="input">
<label htmlFor="location">Location</label>
<input
onChange={handleChange}
type="text"
id="location"
placeholder="Enter location"
/>
</div>
<div className="input">
<label htmlFor="location">Add Picture</label>
<input
onChange={handleChange}
type="text"
id="photo"
placeholder="Enter url of restaurant Picture"
/>
</div>
<div className="input">
<label htmlFor="price">Price Range</label>
<input
onChange={handleChange}
type="text"
id="price"
placeholder="Enter price range"
/>
</div>
<div className="input">
<label htmlFor="date">Contact Information</label>
<input
onChange={handleChange}
type="text"
id="contact"
placeholder="Enter the information"
/>
</div>
<div className="input">
<div className="star-rating-slider">
Rating:
{[1, 2, 3, 4, 5].map((star) => (
<FontAwesomeIcon
key={star}
icon={star <= rating ? solidStar : regularStar}
className={"star-icon"}
onClick={() => handleStarClick(star)}
/>
))}
</div>
</div>
<div className="input">
<label htmlFor="desc">Description</label>
<input
onChange={handleChange}
type="text"
id="description"
placeholder="A brief description"
/>
</div>
<button className="button" onClick={handleClick} type="submit">
Create Restaurant
</button>
</div>
</div>
</div>
</div>
)
}
export default AdminLanding
// client/src/pages/Home.jsx
import React, { useState } from 'react'
import Navbar from '../components/Navbar'
import { faMagnifyingGlass } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import useFetch from "../useFetch"
import '../styles/home.scss'
import Card from '../components/Card';
const Home = () => {
const [query, setQuery] = useState("");
const {data, loading} = useFetch(`/restaurants`)
const keys = ["name", "location"];
const search = (data) => {
return data.filter((item) =>
keys.some((key) => item[key] && item[key].toLowerCase().includes(query))
);
};
return (
<div className='home'>
<Navbar />
<div className="search">
<div className="searchBar">
<h2>Explore</h2>
<div className="searchInput">
<input
type="text"
placeholder="Search places or restaurants"
onChange={(e) => setQuery(e.target.value)}
/>
<FontAwesomeIcon className="icon" icon={faMagnifyingGlass} />
</div>
</div>
</div>
<div className="searchedPosts">
{loading ? (
<>
<div className="p" style={{color: "white", "fontFamily": "'Kaushan Script', cursive"}}>Loading...</div>
</>
) : (
<>
{search(data)?.map((item, i) => (
<Card
key={i}
_id={item._id}
photo={item.photo}
name={item.name}
location={item.location}
rating={item.rating}
/>
))}
</>
)}
</div>
</div>
)
}
export default Home
// client/src/pages/Landing.jsx
import React, { useContext, useEffect } from 'react'
import { Link, useNavigate } from 'react-router-dom'
import "../styles/landing.scss"
import { AuthContext } from '../authContext'
const Landing = () => {
const { user } = useContext(AuthContext)
const navigate = useNavigate();
useEffect(() => {
if (user) {
if (user.isAdmin) {
navigate("/admin/dashboard");
} else {
navigate('/home');
}
}
}, [user, navigate]);
return (
<div className="">
{
user ? null : (
<div className='landing'>
<div className="text">
<p>Welcome to <span>AtSeat</span> !</p>
<Link to="/adminLogin">
<button>Login as Admin</button>
</Link>
<Link to="/userLogin">
<button>Login as User</button>
</Link>
</div>
</div>
)
}
</div>
)
}
export default Landing
// client/src/pages/Login.jsx
import React from "react";
import Navbar from "../components/Navbar";
import "../styles/login.scss";
import axios from "axios";
import { useContext, useState } from "react";
import { useNavigate, Link } from "react-router-dom";
import { AuthContext } from "../authContext";
function Login({type}) {
const [credentials, setCredentials] = useState({
username: undefined,
password: undefined,
});
const urls = {
"admin": "http://localhost:7700/api/admin/login",
"user": "http://localhost:7700/api/users/login"
}
const landings = {
"admin": "/admin/dashboard",
"user": "/home"
}
const { dispatch } = useContext(AuthContext);
const navigate = useNavigate();
const handleChange = (e) => {
setCredentials((prev) => ({ ...prev, [e.target.id]: e.target.value }));
};
const handleClick = async (e) => {
e.preventDefault();
dispatch({ type: "LOGIN_START" });
try {
const res = await axios.post(urls[type], credentials);
dispatch({ type: "LOGIN_SUCCESS", payload: res.data.details });
navigate(landings[type]);
} catch (err) {
if (err.response && err.response.data) {
// If error response and data exist, dispatch LOGIN_FAILURE with error message
dispatch({ type: "LOGIN_FAILURE", payload: err.response.data });
} else {
// If no error response or data, dispatch generic error message
dispatch({ type: "LOGIN_FAILURE", payload: "An error occurred while logging in" });
}
}
};
return (
<div className="login">
<Navbar type={type}/>
<div className="loginCard">
<div className="center">
<h1>Welcome back {type}!</h1>
<form>
<div className="txt_field">
<input
type="text"
placeholder="username"
id="username"
onChange={handleChange}
className="lInput"
/>
</div>
<div className="txt_field">
<input
type="password"
placeholder="password"
id="password"
onChange={handleChange}
className="lInput"
/>
</div>
<div className="login_button">
<button className="button" onClick={handleClick}>
Login
</button>
</div>
<div className="signup_link">
<p>
Not registered? <Link to={type==="admin"? "/adminRegister" : "/userRegister"}>Register</Link>
</p>
</div>
</form>
</div>
</div>
</div>
);
}
export default Login;
// client/src/pages/Register.jsx
import React from "react";
import Navbar from "../components/Navbar";
import "../styles/register.scss";
import { Link } from "react-router-dom";
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import axios from "axios";
function Register({type}) {
const navigate = useNavigate();
const urls = {
"admin": "/admin/register",
"user": "/users/register"
}
const logins = {
"admin": "/adminLogin",
"user": "/userLogin"
}
const [info, setInfo] = useState({});
const handleChange = (e) => {
setInfo((prev) => ({ ...prev, [e.target.id]: e.target.value }));
};
const handleClick = async (e) => {
e.preventDefault();
try {
await axios.post(urls[type], info, {withcredentials: false})
navigate(logins[type]);
} catch (err) {
console.log(err)
}
};
return (
<div className="register">
<Navbar type={type}/>
<div className="registerCard">
<div className="center">
<h1>Join us dear {type}!</h1>
<form>
<div className="formInput">
<div className="txt_field">
<input
type="text"
placeholder="username"
name="username"
onChange={handleChange}
id="username"
required
/>
</div>
{type==="user" && <div className="txt_field">
<input
type="email"
placeholder="email"
name="email"
onChange={handleChange}
id="email"
required
/>
</div>}
<div className="txt_field">
<input
type="password"
placeholder="password"
name="password"
onChange={handleChange}
id="password"
required
/>
</div>
</div>
<div className="login_button">
<button className="button" onClick={handleClick}>
Register
</button>
</div>
<div className="signup_link">
<p>
Already Registered? <Link to={type==="admin"? "/adminLogin" : "/userLogin"}>Login</Link>
</p>
</div>
</form>
</div>
</div>
</div>
);
}
export default Register;
// client/src/pages/Reservations.jsx
import React, { useContext } from 'react'
import useFetch from '../useFetch'
import { AuthContext } from '../authContext'
import ReservationCard from '../components/ReservationCard'
import Navbar from '../components/Navbar'
import "../styles/reservation.scss"
const Reservations = ({type}) => {
const { user } = useContext(AuthContext)
const urls = {
"admin": `/reservations/rest/${user.rest}`,
"user": `/reservations/user/${user._id}`
}
// Call useFetch unconditionally
const {data} = useFetch(urls[type])
return (
<div>
<Navbar />
<div className="reservation-container">
{data ? (
data?.map((item, index) => (
<ReservationCard key={index} props={{...item, type}} />
))
) : (
"No Reservations Yet"
)}
</div>
</div>
)
}
export default Reservations
// client/src/pages/Restaurant.jsx
import React, { useContext, useState, useEffect } from 'react'
import Navbar from '../components/Navbar'
import useFetch from '../useFetch'
import {
faMoneyBill,
faLocationDot,
faThumbsUp,
faPhone
} from "@fortawesome/free-solid-svg-icons";
import { useLocation, useNavigate } from "react-router-dom";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import "../styles/restaurant.scss"
import Map, {Marker} from 'react-map-gl';
import 'mapbox-gl/dist/mapbox-gl.css';
import axios from 'axios'
import { AuthContext } from '../authContext';
const Restaurant = ({type}) => {
const [date, setDate] = useState("");
const location = useLocation();
let id;
if(type==="user")
id = location.pathname.split("/")[2];
else
id = location.pathname.split("/")[3];
const {data} = useFetch(`/restaurants/${id}`);
const slots = useFetch(`/reservations/slots/${id}/${date}`).data
const { user } = useContext(AuthContext);
const [info, setInfo] = useState({});
const navigate = useNavigate();
// set the usestate to the data user passed
const handleChange = (e) => {
setInfo((prev) => ({ ...prev, [e.target.id]: e.target.value }));
}
// post the usestate to database
const handleClick = async (e) => {
e.preventDefault();
const newRes = {
...info, author: user._id, rest: id, date:date
}
try {
await axios.post("http://localhost:7700/api/reservations", newRes, {
withCredentials: false
})
navigate('/reservations')
}
catch (err) {
console.log(err)
}
}
useEffect(() => {
getPlaces();
}, [data]);
const [viewState, setViewState] = React.useState({
latitude: 37.8,
longitude: -122.4,
zoom: 12
});
const getPlaces = async() => {
const promise = await fetch(`https://api.mapbox.com/geocoding/v5/mapbox.places/${data?.location}.json?access_token=${process.env.REACT_APP_API_MAPBOX_KEY}`)
const placesData = await promise.json();
if (placesData.features.length > 0) {
const firstPlace = placesData.features[0]; // Assuming you want to use the first result
const { center } = firstPlace;
setViewState((prevState) => ({
...prevState,
latitude: center[1],
longitude: center[0],
}));
}
}
return (
<div className='restaurant'>
<Navbar />
<div className="rest-container">
<div className="leftContainer">
<h1>{data.name}</h1>
<p>{data.description}</p>
<div className="other-details">
<div className="location"><span><FontAwesomeIcon icon={faLocationDot} /> Location: </span>{data.location}</div>
<div className="rating"><span><FontAwesomeIcon icon={faThumbsUp} /> Rating: </span>{data.rating}</div>
<div className="price"><span><FontAwesomeIcon icon={faMoneyBill} /> Price Range: </span>{data.price}</div>
<div className="contact"><span><FontAwesomeIcon icon={faPhone} /> Contact: </span>{data.contact}</div>
</div>
{!user.isAdmin && <div className="reservation-box">
<div className="form-input">
<label htmlFor="date">Date</label>
<input type="date" onChange={(e) => setDate(e.target.value)} id='date'/>
</div>
{date && <div className="form-input">
<label htmlFor="slot">Time</label>
<select id="slot" onChange={handleChange}>
<option key={0} value="none">-</option>
{
slots?.map((s, index) => (
<option key={index} value={s}>{s}</option>
))
}
</select>
</div>}
<div className="form-input">
<label htmlFor="people">People</label>
<input type="number" id='people' onChange={handleChange}/>
</div>
<button onClick={handleClick}>Make Reservation</button>
</div>}
</div>
<div className="rightContainer">
<div className="location-map">
<Map
{...viewState}
onMove={evt => setViewState(evt.viewState)}
style={{width: 400, height: 300}}
mapStyle="mapbox://styles/mapbox/streets-v9"
mapboxAccessToken={process.env.REACT_APP_API_MAPBOX_KEY}
>
<Marker className="marker" longitude={viewState.longitude} latitude={viewState.latitude} color="red" />
</Map>
</div>
<div className="imgSlider">
<div className="images">
<img src={data.photo} height="300px" alt="" />
</div>
</div>
</div>
</div>
</div>
)
}
export default Restaurant
// client/src/components/Card.jsx
import React from 'react'
import { faStar as solidStar } from "@fortawesome/free-solid-svg-icons";
import { faStar as regularStar } from "@fortawesome/free-regular-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import "../styles/card.scss"
import { Link } from 'react-router-dom';
const Card = (props) => {
return (
<div>
<Link to={`/restaurant/${props._id}`}>
<div className='cardContainer'>
<div className="picContainer">
<img src={props.photo} alt="" />
</div>
<div className="detailsContainer">
<h2>{props.name}</h2>
<h3>{props.location.substring(0, 50)}...</h3>
<div className="star-rating-slider">
Rating:
{[1, 2, 3, 4, 5].map((star) => (
<FontAwesomeIcon
key={star}
icon={star <= props.rating ? solidStar : regularStar}
className={"star-icon"}
/>
))}
</div>
</div>
</div>
</Link>
</div>
)
}
export default Card
// client/src/components/Navbar.jsx
import '../styles/navbar.scss'
import { useContext } from 'react';
import { faBars } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, useNavigate } from "react-router-dom"
import { AuthContext } from "../authContext"
const Navbar = ({type}) => {
const navigate = useNavigate()
const { user, dispatch } = useContext(AuthContext)
const handleClick = async (e) => {
e.preventDefault();
dispatch({ type: "LOGOUT" });
navigate("/")
}
return (
<div className='navContainer'>
<Link to="/">
<p className='navLogo'>AtSeat</p>
</Link>
<input type="checkbox" id='menu-bar' />
<label htmlFor="menu-bar"><FontAwesomeIcon icon={faBars} className="icon" /></label>
<nav className='navbar'>
<ul>
{!user && <Link to="/landing">
<li><p>Landing</p></li>
</Link>}
{user && !user.isAdmin && <Link to="/home">
<li><p>Search Page</p></li>
</Link>}
{user && user.isAdmin && <Link to={`/admin/restaurant/${user.rest}`}>
<li><p>Restaurant</p></li>
</Link>}
{user && user.isAdmin && <Link to="/admin/reservations">
<li><p>Reservations</p></li>
</Link>}
{user && !user.isAdmin && <Link to="/reservations">
<li><p>Reservations</p></li>
</Link>}
{user ? (<>
<li onClick={handleClick} style={{ cursor: "pointer" }}><p>Logout</p></li>
<li><div className="profilePicture">
<img src={user.profilePicture || "https://i.ibb.co/MBtjqXQ/no-avatar.gif"} alt="" />
</div></li>
<li id="usernamename"><p>{user.username}</p></li>
</>
)
:
(
<>
<Link to={type === "admin"? "/adminRegister":"/userRegister"}>
<li><p>Register</p></li>
</Link>
<Link to={type === "user"? "/userLogin" : "/adminLogin"}>
<li><p>Login</p></li>
</Link>
</>
)}
</ul>
</nav>
</div >
)
}
export default Navbar
// client/src/ReservationCard.jsx
import React from 'react'
import "../styles/reservationCard.scss"
import { faTrash } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link } from 'react-router-dom';
import axios from 'axios';
const ReservationCard = ({props}) => {
const handleClick = async () => {
try{
await axios.delete(`http://localhost:7700/api/reservations/${props._id}`, { withCredentials: false })
window.location.reload();
}
catch(err) {
console.log(err)
}
}
return (
<div className='reservation-card'>
<div className="details">
<div className="res-name">
<h1>{props.rest.name}</h1>
{props.type === "User" &&
<Link to={`/restaurant/${props.rest._id}`}>
<button>View</button>
</Link>
}
</div>
<div className='res-details'><p>Date: </p> <span>{props.date.substring(0, 10)}</span> <p>Time: </p> <span>{props.slot}</span> <p>People: </p> <span>{props.people}</span></div>
</div>
{props.type === "User" && <div className="icon">
<FontAwesomeIcon icon={faTrash} onClick={handleClick}/>
</div>}
</div>
)
}
export default ReservationCard
Steps to Run the Application:
Start the frontend:
cd client
npm start
Start the backend:
cd api
nodemon index.js
Output
- Browser Output
- Data stored in database:
Contact Us