24
loading...
This website collects cookies to deliver better user experience
create-react-app
, @vue/cli
, and the rest of them, Strapi has create-strapi-app.mkdir ecommerce
npx create-strapi-app backend
create-strapi-app
globally and use directly), we set up the backend.n
and press enter.eCommerce
as the database name:cd backend
yarn develop
. The admin UI will be built with the development configuration, and your browser will open the page at http://localhost:1337/admin/auth/register-admin
:Create your first Content-Type
:Product
, then click Continue. Next, you're to enter some fields:name
, which will be the name of the Product. Then, click on Advanced settings:Required field
checkbox. With this selected, during creation (through the API), there would be a validation error if this field is not provided. You can also set a maximum or minimum length if you want.Add another field
.images
as the name. In the Advanced settings:allowed types of media
to Images only and check the Required checkbox.Add another field
for the last field. Select text, and enter description
as the name.Long text
. Go to the advanced settings again and make it required. And then click Finish
.Save
at the top to add that collection. The server would restart with the new collection saved.Content-Types Builder
from the left-side navigation, and click on Create new collection type
in the Collection Types
section.Review
, and the fields here are:reviewer_name
, required, of short textreview
, required, of long textRelation
type. The field name on the Review collection is product
, and the field name on the Product collection is reviews
with a Many to One relationship:Collection Types
in the navigation, we can select a collection and add data directly on the UI.Review
collection and add three reviews—two for one Product and one for the other.Product
you're adding a review for because Reviews have a relationship with Products:eCommerce
directory, run the following in the terminal.npx create-react-app client
cd client
npm run start
react-router
set up for this. Do the following:npm install react-router-dom
App.js
file and replace the code there with this:import React from 'react'
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom'
import Home from './components/Home'
export default function App() {
return (
<Router>
<Switch>
<Route exact path="/">
<Home />
</Route>
<Route path="/:product_id"></Route>
<Route path="/add-product"></Route>
</Switch>
</Router>
)
}
components
directory under src
and Add the Home.js
file:export default function Home() {
return <h1>Homepage</h1>
}
backend
directory and run yarn develop
, and open the admin dashboard at http://localhost:1337/admin. Then log in with your details.USERS & PERMISSIONS PLUGIN
, click on Roles:npm install axios
Home.js
to the following:import axios from 'axios'
import { useEffect, useState } from 'react'
import './Home.css'
import { Link } from 'react-router-dom'
export default function Home() {
const [products, setProducts] = useState([])
useEffect(() => {
axios({
method: 'get',
url: 'http://localhost:1337/products',
}).then((res) => {
setProducts(res.data)
})
}, [])
return (
<div className="container">
<h1>Products</h1>
<div className="products-container">
{products.map((product) => (
<Link className="product" to={`/${product.id}`}>
<img src={`http://localhost:1337${product.images[0].url}`} />
<h2 className="product-name">{product.name}</h2>
<p className="product-desc">{product.description}</p>
</Link>
))}
</div>
</div>
)
}
URL
field on each image is in the form of /uploads/…
which means we have to append that to the backend's server's URL to get the complete URL.Home.css
file in the same directory as Home.js
, with the following code:.products-container {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-gap: 20px;
}
.product {
text-decoration: none;
border: 1px solid rgb(236, 235, 235);
}
.product-image img {
height: 400px;
color: rgb(229, 224, 224);
}
.product-details {
padding: 15px;
}
.product-name {
margin: 10px 0 0;
color: rgb(103, 103, 103);
}
.product-desc {
margin: 10px 0 0;
color: rgb(187, 187, 187);
}
.container
class in the index.css
file as so:.container {
padding: 40px;
max-width: 1000px;
margin: 0 auto;
}
* {
box-sizing: border-box;
}
ProductDetail
component at the components directory:import axios from 'axios'
import { useEffect, useState } from 'react'
import { useParams } from 'react-router-dom'
import './ProductDetail.css'
export default function ProductDetail() {
const { product_id } = useParams()
const [product, setProduct] = useState(null)
useEffect(() => {
axios({
method: 'get',
url: `http://localhost:1337/products/${product_id}`,
}).then((res) => {
setProduct(res.data)
})
}, [])
return (
<div className="container">
{!product ? (
<span>Loading...</span>
) : (
<div className="product-detail-container">
<h1>{product.name}</h1>
<div className="product-img-container">
<img src={`http://localhost:1337${product.images[0].url}`} />
</div>
<p>{product.description}</p>
<div className="product-reviews">
<h2>Reviews ({product.reviews.length})</h2>
{product.reviews.map((review) => (
<div className="product-review">
<h3>{review.reviewer_name}</h3>
<p>{review.review}</p>
</div>
))}
</div>
</div>
)}
</div>
)
}
ProductDetail.css
file at the same directory:.product-img-container {
height: 500px;
overflow: hidden;
}
.product-img-container img {
object-fit: cover;
width: 100%;
height: 100%;
}
.product-reviews {
padding-top: 20px;
border-top: 1px solid #ccc;
}
.product-review {
background-color: #eeecec;
padding: 15px;
}
App.js
and update the second Route
component to this:...
<Route path="/:product_id">
<ProductDetail />
</Route>
...
npm install socket.io
module.exports = () => {
const io = require("socket.io")(strapi.server, {
cors: {
origin: "http://localhost:3000",
methods: ["GET", "POST"],
},
});
io.on("connection", function (socket) {
// send message on user connection
socket.emit("hello", JSON.stringify({ message: "Welcome to my website" }));
});
};
socket.io
library to use Strapi's server, with some cors
permissions. When any client connects to the server, the connection
event is automatically emitted, and the server can listen to it and do whatever it wants.connection
, the server emits the hello
event, and the client can listen to it and display the message.npm i socket.io-client react-toastify
App.js
file, add the following:// other imports
import { ToastContainer } from 'react-toastify'
import 'react-toastify/dist/ReactToastify.css'
...
<Router>
<ToastContainer />
<Switch>...
config
, and in it, create a socket.js
file with the following code:import { io } from 'socket.io-client'
export const socket = io('http://localhost:1337')
Home.js
file, update to the following:// other imports
import { socket } from '../config/socket'
import { toast } from 'react-toastify'
socket.on('hello', (res) => {
toast.success(res.message)
})
export default ...
AddProduct.js
:import { useState } from 'react'
import { toast } from 'react-toastify'
import { socket } from '../config/socket'
import './AddProduct.css'
export default function AddProduct() {
const [name, setName] = useState(null)
const [description, setDescription] = useState(null)
const [processing, setProcessing] = useState(false)
const formSubmit = (e) => {
e.preventDefault()
setProcessing(true)
socket.emit('addProduct', { name, description }, (product) => {
setProcessing(false)
toast.success('Product added successfully!')
setTimeout(() => {
window.location.href = `/${product.id}`
}, 1000)
})
}
return (
<div className="container">
<div className="add-product">
<h1>Add Product</h1>
<form onSubmit={formSubmit}>
<div className="input-group">
<label htmlFor="name">Name</label>
<input onChange={(e) => setName(e.target.value)} />
</div>
<div className="input-group">
<label htmlFor="description">Description</label>
<input onChange={(e) => setDescription(e.target.value)} />
</div>
<button disabled={processing} type="submit">
{processing ? 'Processing' : 'Add Product'}
</button>
</form>
</div>
</div>
)
}
addProduct
with an object of the Product's properties. Notice we aren't using Axios for the POST request? Because sockets do that already.product
, which will be passed from the backend when created. Then we navigate to that Product's detail page after 1 second.AddProduct.css file
:.add-product h1 {
text-align: center;
}
.add-product form {
max-width: 600px;
margin: 0 auto;
border: 1px solid #ccc;
padding: 15px;
}
.input-group {
width: 100%;
margin-bottom: 20px;
}
.input-group label {
display: block;
margin-bottom: 5px;
}
.input-group input {
padding: 20px;
width: 100%;
}
.add-product form button {
width: 100%;
padding: 20px;
border: none;
background: none;
background-color: rgb(193, 251, 193);
cursor: pointer;
}
App.js
, /add-product/
gives us:img
src
in the homepage and product detail page, change it to:<img
src={
product.images[0]
? `http://localhost:1337${product.images[0].url}`
: '/empty-box.svg'
}
/>
empty-box.SVG
file and paste the following:<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<path fill="none" d="M0 0h24v24H0z" />
<path
d="M3 10h18v10.004c0 .55-.445.996-.993.996H3.993A.994.994 0 0 1 3 20.004V10zm6 2v2h6v-2H9zM2 4c0-.552.455-1 .992-1h18.016c.548 0 .992.444.992 1v4H2V4z"
fill="rgba(177,177,177,1)" />
</svg>
Home.js
and add listen to the product added event:...
socket.on('hello', (res) => {
toast.success(res.message)
})
socket.on('newProductAdded', (res) => {
toast.info('A new product has been added')
setProducts((products) => [res.product, ...products])
})
...
config
directory, create a utils
directory, and in it, create a product-database.js
file. In that file, paste the following:async function createProduct({ name, description }) {
try {
const product = await strapi.services.product.create({
name,
description,
});
return product;
} catch (err) {
console.log({ err });
return "Product cannot be created. Try again";
}
}
module.exports = {
createProduct,
};
bootstrap.js
file and after the hello
emitted event, add the following:...
socket.on("addProduct", async ({ name, description }, callback) => {
try {
const product = await createProduct({
name,
description,
});
if (product) {
callback(product);
socket.broadcast.emit("newProductAdded", { product });
}
} catch (err) {
console.log({ err });
callback({ type: "error", message: err });
console.log("Error occured. Please try again");
}
});
...
addProduct
event, add the Product to the database, pass the Product as the argument to the client's callback, and emit the event newProductAdded
with the Product.socket.emit
but socket.broadcast.emit instead
since socket.emit
emits the event to the client that emitted the addProduct
event. We don't want this because the client is currently on the add product page, which means the homepage will never listen to the event.socket.broadcast.emit
, we're emitting the event to every other connected event, except the client that sent the addProduct
event.ProductReviews
component that has a form where users can enter reviews.newReviewAdded
event. And we can broadcast emit to other connected clients, letting them know of the newly added review.loadedProducts
and listen to that event on the frontend to fetch and display the products.