In the fast world where there is no time for people to cook food or go to restaurants to eat, Food Delivery applications are one of the best options for them. In this tutorial, you will learn how to create a simple food delivery app using MERN stack. Our application will allow users to browse through a list of restaurants, view their menus, and add items to a shopping cart.
Preview of final output: Let us have a look at how the final output will look like
.jpg)
Prerequisites:
Approach to create Restaurant App using MERN:
1. Import Statements:
- Import necessary dependencies and components.
- React is imported for defining React components.
- RestaurantList, RestaurantCard, DishesMenu, DishCard and Cart are custom components, assumed to be present in the ./components directory.
- RestaurantContext is imported, presumably a custom context provider.
2.Functional Component:
- Define a functional component named App.
3.Context Provider:
- Wrap the App component inside the RestaurantContext provider. This suggests that the components within this provider have access to the context provided by RestaurantContext.
4.Component Rendering:
- Render the following components:
- RestaurantContext: Presumably, this is a context provider that wraps its child components (App). The purpose of this context is not clear from the provided code snippet.
- All other components such as RestaurantList and DishesMenu is wrapped inside App component so it also has the access of RestaurantContext.
- RestaurantList wraps RestaurantCard
Steps to create Application:
Step 1: creating the folder for the project
mkdir food-delivery-app
cd food-delivery-app
Step 2: Create a backend using the following commands
mkdir backend
cd backend
npm init -y
Step 3: Install the required dependencies.
npm i cors express mongoose nodemonProject Structure(Backend):
Backend Dependencies:
"dependencies": {
"cors": "^2.8.5",
"express": "^4.18.2",
"mongoose": "^8.0.3",
"nodemon": "^3.0.2"
}
Example: Create server.js file and add the following code.
//server.js
const express = require("express");
const mongoose = require("mongoose");
const cors = require("cors");
const app = express();
const PORT = process.env.PORT || 5000;
app.use(cors());
app.use(express.json());
mongoose
.connect("mongodb://localhost:27017/food-delivery-app", {
useNewUrlParser: true,
useUnifiedTopology: true,
})
.then(() => console.log("Connected to db"))
.catch((err) => console.log("Error connecting to db", err));
const restaurantSchema = new mongoose.Schema({
name: String,
image: String,
menu: [
{
name: String,
price: Number,
image: String,
},
],
rating: Number,
});
const Restaurant = mongoose.model("Restaurant", restaurantSchema);
// Define the PreviousOrder schema
const previousOrderSchema = new mongoose.Schema({
orderId: { type: String, required: true },
dateOfOrder: { type: Date, required: true },
amount: { type: Number, required: true },
});
const PreviousOrder = mongoose.model("PreviousOrder", previousOrderSchema);
// Seed initial data
const seedData = [
{
name: "Italian Delight",
image: "https://media.geeksforgeeks.org/wp-content/uploads/20240110004602/pexels-chan-walrus-958545-(1).jpg",
menu: [
{
name: "Pasta Alfredo",
price: 10,
image:
"https://media.geeksforgeeks.org/wp-content/uploads/20240110004646/file.jpg",
},
{
name: "Margherita Pizza",
price: 15,
image:
"https://media.geeksforgeeks.org/wp-content/uploads/20240110004646/file.jpg",
},
{
name: "Chicken Parmesan",
price: 20,
image:
"https://media.geeksforgeeks.org/wp-content/uploads/20240110004646/file.jpg",
},
],
rating: 4.5,
},
{
name: "Seafood Paradise",
image:
"https://media.geeksforgeeks.org/wp-content/uploads/20240110004602/pexels-chan-walrus-958545-(1).jpg",
menu: [
{
name: "Grilled Salmon",
price: 12,
image:
"https://media.geeksforgeeks.org/wp-content/uploads/20240110004602/pexels-chan-walrus-958545-(1).jpg",
},
{
name: "Lobster Bisque",
price: 18,
image:
"https://media.geeksforgeeks.org/wp-content/uploads/20240110004602/pexels-chan-walrus-958545-(1).jpg",
},
{
name: "Shrimp Scampi",
price: 25,
image:
"https://media.geeksforgeeks.org/wp-content/uploads/20240110004602/pexels-chan-walrus-958545-(1).jpg",
},
],
rating: 3.8,
},
{
name: "Vegetarian Haven",
image: "https://media.geeksforgeeks.org/wp-content/uploads/20240110004602/pexels-chan-walrus-958545-(1).jpg",
menu: [
{
name: "Quinoa Salad",
price: 8,
image:
"https://media.geeksforgeeks.org/wp-content/uploads/20240110004602/pexels-chan-walrus-958545-(1).jpg",
},
{
name: "Eggplant Parmesan",
price: 12,
image:
"https://media.geeksforgeeks.org/wp-content/uploads/20240110004602/pexels-chan-walrus-958545-(1).jpg",
},
{
name: "Mushroom Risotto",
price: 16,
image:
"https://media.geeksforgeeks.org/wp-content/uploads/20240110004602/pexels-chan-walrus-958545-(1).jpg",
},
],
rating: 4.2,
},
{
name: "Sizzling Steakhouse",
image: "https://media.geeksforgeeks.org/wp-content/uploads/20240110004602/pexels-chan-walrus-958545-(1).jpg",
menu: [
{
name: "Filet Mignon",
price: 22,
image: "https://media.geeksforgeeks.org/wp-content/uploads/20240110004602/pexels-chan-walrus-958545-(1).jpg",
},
{
name: "New York Strip",
price: 18,
image: "https://media.geeksforgeeks.org/wp-content/uploads/20240110004602/pexels-chan-walrus-958545-(1).jpg",
},
{
name: "Ribeye Steak",
price: 25,
image: "https://media.geeksforgeeks.org/wp-content/uploads/20240110004602/pexels-chan-walrus-958545-(1).jpg",
},
],
rating: 4.7,
},
{
name: "Asian Fusion",
image: "https://media.geeksforgeeks.org/wp-content/uploads/20240110004602/pexels-chan-walrus-958545-(1).jpg",
menu: [
{
name: "Sushi Platter",
price: 20,
image: "https://media.geeksforgeeks.org/wp-content/uploads/20240110004602/pexels-chan-walrus-958545-(1).jpg",
},
{
name: "Pad Thai",
price: 15,
image: "https://media.geeksforgeeks.org/wp-content/uploads/20240110004602/pexels-chan-walrus-958545-(1).jpg",
},
{
name: "Mongolian Beef",
price: 18,
image: "https://media.geeksforgeeks.org/wp-content/uploads/20240110004602/pexels-chan-walrus-958545-(1).jpg",
},
],
rating: 4.0,
},
];
const seedDatabase = async () => {
try {
await Restaurant.deleteMany(); // Clear existing data
await Restaurant.insertMany(seedData);
console.log("Database seeded successfully.");
} catch (error) {
console.error("Error seeding the database:", error.message);
}
};
// Seed data when the server starts
seedDatabase();
// Insert dummy data when the server starts
const insertDummyData = async () => {
try {
const existingOrders = await PreviousOrder.find();
// Insert dummy data only if the database is empty
if (existingOrders.length === 0) {
const dummyOrders = [
{ orderId: "001", dateOfOrder: new Date(), amount: 30 },
{ orderId: "002", dateOfOrder: new Date(), amount: 45 },
// Add more dummy orders as needed
];
await PreviousOrder.insertMany(dummyOrders);
console.log("Dummy data inserted successfully!");
}
} catch (error) {
console.error("Error inserting dummy data:", error);
}
};
insertDummyData();
app.get("/restaurants", async (req, res) => {
try {
// Use the 'find' method of the 'Restaurant' model to retrieve all restaurants
const restaurants = await Restaurant.find({});
// Send the retrieved restaurants as a JSON response
res.json(restaurants);
} catch (error) {
// Handle any errors that may occur during the process and send a 500 Internal Server Error response
res.status(500).json({ error: error.message });
}
});
// Endpoint to retrieve previous orders
app.get("/previousOrders", async (req, res) => {
try {
const orders = await PreviousOrder.find();
res.status(200).json(orders);
} catch (error) {
res.status(500).json({ error: "Internal server error" });
}
});
// Endpoint to save order data
app.post("/previousOrders", async (req, res) => {
try {
const { orderId, dateOfOrder, amount } = req.body;
console.log(orderId, dateOfOrder, amount);
const newOrder = new PreviousOrder({
orderId,
dateOfOrder: new Date(dateOfOrder),
amount,
});
await newOrder.save();
res.status(201).json({ message: "Dummy order saved successfully!" });
} catch (error) {
res.status(500).json({ error: "Internal server error" });
}
});
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
Step 4: To start the backend run the following command.
nodemon server.jsStep 5: Go to the root directory of the application and create the frontend application.
npx create-react-app client
cd client
Step 6: Install important dependencies: axios
npm i axiosProject Structure(Frontend):
Frontend dependencies:
"dependencies": {
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"axios": "^1.6.5",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.21.1",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4"
}
Example: Create the required files and add the following code.
/* App.css */
.container {
font-family: 'Arial, sans-serif';
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
}
.header {
font-size: 24px;
margin-bottom: 10px;
border-radius: 15px;
}
/* Styles to Resturant List */
/* RestaurantList.css */
.container {
font-family: 'Arial, sans-serif';
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
}
.header {
font-size: 32px;
margin-bottom: 20px;
background-color: #fc0671;
color: white;
padding: 10px;
}
.filter-container {
display: flex;
justify-content: center;
align-items: center;
gap: 20px;
margin-bottom: 20px;
}
.filter-container label {
font-size: 18px;
color: #555;
}
.filter-input {
padding: 8px;
font-size: 16px;
}
.restaurant-card-container {
display: flex;
flex-wrap: wrap;
gap: 20px;
justify-content: center;
}
/* RestaurantCard.css */
.card {
border: 1px solid #ccc;
border-radius: 8px;
padding: 12px;
margin: 10px;
width: 200px;
cursor: pointer;
transition: transform 0.3s ease-in-out;
}
.card:hover {
transform: scale(1.05);
}
.image-container {
overflow: hidden;
border-radius: 8px;
margin-bottom: 10px;
height: 150px;
/* Set a fixed height for the image container */
}
.restaurant-image {
width: 100%;
height: 100%;
object-fit: cover;
/* Maintain the aspect ratio and cover the container */
border-radius: 8px;
}
/* Responsive Styles */
@media screen and (max-width: 600px) {
.card {
width: 100%;
}
}
/* Dish Card */
.dish-card {
border: 1px solid #ccc;
border-radius: 8px;
padding: 12px;
margin: 10px;
width: 200px;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
transition: transform 0.3s ease-in-out;
}
.dish-card:hover {
transform: scale(1.05);
}
img {
width: 100%;
/* Set the width to fill the container */
height: 100px;
/* Set the fixed height for the image */
object-fit: cover;
/* Maintain the aspect ratio and cover the container */
border-radius: 8px;
margin-bottom: 10px;
}
h3 {
margin-bottom: 8px;
}
p {
margin-bottom: 8px;
}
button {
margin-top: 8px;
padding: 8px;
cursor: pointer;
background-color: #4caf50;
color: white;
border: none;
border-radius: 4px;
}
/* Responsive Styles */
@media screen and (max-width: 600px) {
.dish-card {
width: 100%;
}
}
/* CART */
.cart-container {
position: fixed;
top: 10px;
right: 10px;
width: 200px;
border: 2px solid #fc0671;
border-radius: 10px;
height: fit-content;
padding: 5px 10px;
background-color: #fff;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
overflow-y: auto;
z-index: 1000;
}
.cart-container button {
background-color: #fc0671;
color: white;
border: none;
border-radius: 10px;
}
h2 {
margin-bottom: 10px;
margin-left: 20px;
}
.cart-content {
padding: 16px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
}
#pre-orders {
background-color: #fc0671;
color: aliceblue;
font-size: 20px;
padding: 5px 10px;
border-radius: 10px;
cursor: pointer;
}
/* Previous orders */
/* PreviousOrders.css */
.previous-orders-container {
max-width: 600px;
margin: 20px auto;
padding: 20px;
position: absolute;
background-color: #fc0671;
color: white;
border-radius: 10px;
}
.orders-list {
list-style: none;
padding: 0;
}
.order-card {
background-color: #fff;
border: 1px solid #ddd;
border-radius: 8px;
padding: 15px;
margin-bottom: 15px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
transition: transform 0.3s ease-in-out;
}
.order-card:hover {
transform: scale(1.02);
}
.order-card h3 {
color: #333;
margin-bottom: 10px;
}
.order-details {
display: flex;
justify-content: space-between;
font-size: 14px;
color: #666;
}
//index.js (update previous index.js)
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { RestaurantProvider } from './contexts/RestaurantContext';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<RestaurantProvider>
<App />
</RestaurantProvider>
);
reportWebVitals();
// App.js
import React, { useContext } from "react";
import RestaurantList from "./components/RestaurantList";
import DishesMenu from "./components/DishesMenu";
import Cart from "./components/Cart";
import { RestaurantContext } from "./contexts/RestaurantContext";
import "./App.css"; // Import the CSS file
const App = () => {
const { selectedRestaurant } = useContext(RestaurantContext);
return (
<>
<div className="container">
<h1 className="header">GFG Food Delivery App</h1>
<Cart
style={{ position: "absolute", right: "20px", top: "20px" }}
/>
<RestaurantList />
{selectedRestaurant && <DishesMenu />}
</div>
</>
);
};
export default App;
//RestaurantContext.js
import React, { createContext, useState, useEffect } from "react";
import axios from "axios";
const RestaurantContext = createContext();
const RestaurantProvider = ({ children }) => {
const [restaurants, setRestaurants] = useState([]);
const [selectedRestaurant, setSelectedRestaurant] = useState(null);
const [cartItems, setCartItems] = useState([]);
const [totalPrice, setTotalPrice] = useState(0);
useEffect(() => {
const fetchRestaurants = async () => {
try {
const response = await axios.get(
"http://localhost:5000/restaurants"
);
setRestaurants(response.data);
} catch (error) {
console.error("Error fetching restaurants:", error.message);
}
};
fetchRestaurants();
}, []);
const handleAddItems = (dish) => {
console.log("Dish:", dish);
// Check if the dish already exists in the cart
const existingItemIndex = cartItems.findIndex(
(item) => item._id === dish._id
);
if (existingItemIndex !== -1) {
// If the dish already exists, update
// the quantity or any other logic
console.log(
"Dish already exists in the cart.
You may want to update the quantity."
);
// Example: Increment the quantity
const updatedCartItems = [...cartItems];
updatedCartItems[existingItemIndex] = {
...updatedCartItems[existingItemIndex],
quantity: updatedCartItems[existingItemIndex].quantity + 1,
};
// console.log('cart',cartItems.length);
// setTotalPrice(prev=>prev-dish.price)
setCartItems(updatedCartItems);
} else {
// If the dish is not in the cart, add it
console.log("Dish does not exist in the cart. Adding to the cart.");
console.log("cart", cartItems.length);
// setTotalPrice(prev=>prev-dish.price)
setCartItems([...cartItems, { ...dish, quantity: 1 }]);
}
setTotalPrice((prev) => prev + dish.price);
};
const handleRemoveItems = (dish) => {
console.log("Dish ID to remove:", dish);
// Check if the dish exists in the cart
const existingItemIndex = cartItems.findIndex(
(item) => item._id === dish._id
);
if (existingItemIndex !== -1) {
// If the dish exists, decrement the
// quantity or remove it from the cart
console.log(
"Dish exists in the cart. You may
want to decrease the quantity or remove it."
);
const updatedCartItems = [...cartItems];
if (updatedCartItems[existingItemIndex].quantity > 1) {
// If the quantity is greater than 1, decrement the quantity
updatedCartItems[existingItemIndex] = {
...updatedCartItems[existingItemIndex],
quantity: updatedCartItems[existingItemIndex].quantity - 1,
};
setTotalPrice(totalPrice - cartItems[existingItemIndex].price);
} else {
// If the quantity is 1, remove the dish from the cart
updatedCartItems.splice(existingItemIndex, 1);
setTotalPrice(totalPrice - cartItems[existingItemIndex].price);
}
setCartItems(updatedCartItems);
} else {
// If the dish is not in the cart,
// log a message or handle accordingly
console.log("Dish does not exist in the cart.");
}
};
const emptyCart = () => {
setCartItems([]);
setTotalPrice(0);
};
const value = {
restaurants,
selectedRestaurant,
setSelectedRestaurant,
handleAddItems,
handleRemoveItems,
totalPrice,
emptyCart,
};
return (
<RestaurantContext.Provider value={value}>
{children}
</RestaurantContext.Provider>
);
};
export { RestaurantContext, RestaurantProvider };
//Cart.js
import React, { useContext, useState } from "react";
import axios from "axios";
import { RestaurantContext } from "../contexts/RestaurantContext";
const Cart = () => {
const { totalPrice, emptyCart } = useContext(RestaurantContext);
const [isCheckingOut, setIsCheckingOut] = useState(false);
const generateOrderId = () => {
// Generate a unique order ID
// (you can use a library like uuid for a more robust solution)
return `${Math.floor(Math.random() * 1000)}`;
};
const handleCheckout = async () => {
try {
setIsCheckingOut(true);
const orderId = generateOrderId();
// Assuming you have a backend endpoint to handle the checkout
const response = await axios.post(
"http://localhost:5000/previousOrders",
{
orderId,
dateOfOrder: new Date(),
amount: totalPrice,
}
);
console.log(response.data);
emptyCart();
} catch (error) {
console.error("Error during checkout:", error.message);
} finally {
setIsCheckingOut(false);
}
};
return (
<div className="cart-container">
<h2>Cart</h2>
<div className="cart-content">
<span style={{ color: "brown" }}>Total Price: </span> $
{totalPrice}
<button onClick={handleCheckout} disabled={isCheckingOut}>
{isCheckingOut ? "Checking out..." : "Checkout"}
</button>
</div>
</div>
);
};
export default Cart;
//DishCard.js
import React, { useContext } from "react";
import { RestaurantContext } from "../contexts/RestaurantContext";
const DishCard = ({ dish }) => {
const { handleAddItems, handleRemoveItems } = useContext(RestaurantContext);
const handleAdd = () => {
handleAddItems(dish);
};
const handleRemove = () => {
handleRemoveItems(dish);
};
return (
<div className="dish-card">
<h3>{dish.name}</h3>
<img src={dish.image} alt="" />
<p>Price: ${dish.price}</p>
<div
style={{
width: "40%",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<button onClick={handleAdd}>+</button>
<button onClick={handleRemove}>-</button>
</div>
</div>
);
};
export default DishCard;
//DishesMenu.js
import React, { useContext } from 'react';
import DishCard from './DishCard';
import { RestaurantContext } from '../contexts/RestaurantContext';
const DishesMenu = () => {
const { selectedRestaurant } = useContext(RestaurantContext);
return (
<div>
<h2>Menu</h2>
{selectedRestaurant && (
<div style={{ display: 'flex', flexWrap: 'wrap' }}>
{selectedRestaurant.menu.map((dish) => (
<DishCard key={dish.name} dish={dish} />
))}
</div>
)}
</div>
);
};
export default DishesMenu;
//PreviousOrders.js
import React, { useState, useEffect } from 'react';
import axios from 'axios';
const PreviousOrders = ({ handleShow }) => {
const [orders, setOrders] = useState([]);
useEffect(() => {
const fetchOrders = async () => {
try {
const response = await axios.get('http://localhost:5000/previousOrders');
setOrders(response.data);
} catch (error) {
console.error('Error fetching orders:', error.message);
}
};
fetchOrders();
}, []);
return (
<div className="previous-orders-container">
<h2>Your Previous Orders</h2>
<button style={{ backgroundColor: "white", color: "red" }} onClick={handleShow}>Close</button>
<ul className="orders-list">
{orders.map(order => (
<li key={order.orderId} className="order-card">
<h3>Order #{order.orderId}</h3>
<div className="order-details">
<div>Items: 1</div>
<div>Total Amount: ${order.amount.toFixed(2)}</div>
</div>
<div>Ordered on: {new Date(order.dateOfOrder).toLocaleDateString()}</div>
</li>
))}
</ul>
</div>
);
};
export default PreviousOrders;
//RestaurantCard.js
import React from 'react';
const RestaurantCard = ({ restaurant, onClick }) => {
return (
<div className="card" onClick={onClick}>
<h3>{restaurant.name}</h3>
<div className="image-container">
<img className="restaurant-image" src={restaurant.image} alt={restaurant.name} />
</div>
<p>Rating: {restaurant.rating}</p>
</div>
);
};
export default RestaurantCard;
//RestaurantList.js
import React, { useContext, useState, useEffect } from 'react';
import RestaurantCard from './RestaurantCard';
import { RestaurantContext } from '../contexts/RestaurantContext';
import PreviousOrders from './PreviousOders';
const RestaurantList = () => {
const { restaurants, setSelectedRestaurant } = useContext(RestaurantContext);
const [filteredRestaurants, setFilteredRestaurants] = useState([...restaurants]);
const [ratingFilter, setRatingFilter] = useState('');
const [searchTerm, setSearchTerm] = useState('');
const [showOrder, setShowOrder] = useState(false)
useEffect(() => {
filterRestaurants();
}, [ratingFilter, searchTerm, restaurants]);
const handleRestaurantClick = (restaurantId) => {
setSelectedRestaurant(restaurants.find((restaurant) => restaurant._id === restaurantId));
};
const handleRatingChange = (e) => {
setRatingFilter(e.target.value);
};
const handleSearchChange = (e) => {
setSearchTerm(e.target.value);
};
const filterRestaurants = () => {
let filtered = restaurants;
if (ratingFilter) {
filtered = filtered.filter((restaurant) => restaurant.rating >= parseFloat(ratingFilter));
}
if (searchTerm) {
const searchLower = searchTerm.toLowerCase();
filtered = filtered.filter((restaurant) =>
restaurant.name.toLowerCase().includes(searchLower)
);
}
setFilteredRestaurants(filtered);
};
const handleShow = () => {
setShowOrder(!showOrder)
}
return (
<div className="container">
<h2 className="header">Restaurant List</h2>
<div className="filter-container">
<label htmlFor="rating" className="filter-label">
Filter by Rating:
</label>
<input
type="number"
id="rating"
value={ratingFilter}
onChange={handleRatingChange}
className="filter-input"
/>
<label htmlFor="search" className="filter-label">
Search by Name:
</label>
<input
type="text"
id="search"
value={searchTerm}
onChange={handleSearchChange}
className="filter-input"
/>
<p id='pre-orders' onClick={handleShow}>
Previous Orders
</p>
</div>
<div className="restaurant-card-container">
{filteredRestaurants.map((restaurant) => (
<RestaurantCard
key={restaurant._id}
restaurant={restaurant}
onClick={() => handleRestaurantClick(restaurant._id)}
/>
))}
</div>
{showOrder && <PreviousOrders handleShow={handleShow} />}
</div>
);
};
export default RestaurantList;
Step 7: To start the frontend run the following command.
npm startOutput: