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:
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.