In the earlier tutorials, you learnt to create React app, and API endpoints to do authentication with JWT, protect APIs, get and count rows of MYSQL database, and upload form data with files to the database in ExpressJs.
In this tutorial, we continue to develop the React app to build a full stack product admin panel website that accesses ExpressJs API endpoints. The product admin panel app will have a left sidebar that allows a visitor easily to navigate different parts of the app. We use bootstrap to style the app. So run the following command to add bootstrap into the React app.
my-app>npm install bootstrap
Our form components are from reactstrap and icons from react-icons packages. Let execute the commands below to install reactstrap and react-icons.
my-app>npm install reacstrap react-icons
To setup the left sidebar in the app, we start by creating the components folder in src folder. To group sidebar files, add sidebar folder to the components folder. We have three sidebar files in the sidebar folder. The SideBar displays SubMenu and the SubMenu uses data from the SideBarData.
sidebar/SideBar.jsimport React, { useState } from "react"; import { Link } from "react-router-dom"; import {FaBars} from "react-icons/fa"; import {AiOutlineClose} from "react-icons/ai"; import { SideBarData } from "./SideBarData"; import SubMenu from "./SubMenu"; import { IconContext } from "react-icons/lib"; import { FaSignInAlt,FaSignOutAlt } from "react-icons/fa" const nav_style = { background: "#0a58ca", height: "60px", display: "flex", justifyCcontent: "flex-start", alignItems: "center" }; const navitem_style = { color:"#ffffff", marginLeft: "auto", marginRight: "20px" }; const navicon_style = { marginLeft: "2rem", fontSize: "1.8rem", display: "flex", justifyContent: "flex-start", alignItems: "center", cursor: "pointer", }; const sidebarwrap_style = { width: "100%", }; const Sidebar = (props) => { const [sidebaropen, setSidebarOpen] = useState(false); const showSidebar = () => setSidebarOpen(!sidebaropen); const {token} = props; const sidebarnav_style = { background: "#0a58ca", width: "250px", height: "100vh", display: "flex", justifyContent: "center", position: "fixed", top: 0, left: (sidebaropen ? "0" : "-100%"), transition: "350ms", zIndex: 10, }; const links = [ { id: 2, path: token===null? "/login":"/logout", text: token===null?"Login":"Logout", icon: token===null?<FaSignInAlt/> : <FaSignOutAlt/> }, ]; return ( <> <IconContext.Provider value={{ color: "#fff" }}> <div style={nav_style}> <div to="#" style={navicon_style}> <FaBars onClick={showSidebar} /> </div> <h4 style={{ textAlign: "center", marginLeft: "30px", color: "white" }} > Admin </h4> <div style={navitem_style}> <ul className="navbar-nav mr-auto list-group-horizontal"> {links.map(link => { return ( <li key={link.id} className="nav-item"> <Link to={link.path} className="nav-link">{link.icon}{link.text}</Link> </li> ) })} </ul> </div> </div> <div className="nav" style={sidebarnav_style}> <div style={sidebarwrap_style}> <div to="#" style={navicon_style}> <AiOutlineClose onClick={showSidebar} /> </div> {SideBarData.map((item, index) => { return <SubMenu item={item} key={index} />; })} </div> </div> </IconContext.Provider> </> ); }; export default Sidebar;
import React from "react"; import {FaPhone,FaEnvelopeOpenText,FaProductHunt,FaServicestack} from "react-icons/fa"; import {AiFillHome} from "react-icons/ai"; import {IoIosPaper,IoMdHelpCircle} from "react-icons/io"; import {RiArrowUpSFill,RiArrowDownSFill} from "react-icons/ri"; export const SideBarData = [ { title: "Products", path: "/products", icon: <FaProductHunt />, }, { title: "Events", path: "/events", icon: <FaEnvelopeOpenText />, iconClosed: <RiArrowDownSFill />, iconOpened: <RiArrowUpSFill />, subNav: [ { title: "Event 1", path: "/events/events1", icon: <IoIosPaper />, }, { title: "Event 2", path: "/events/events2", icon: <IoIosPaper />, }, ], }, { title: "Services", path: "/services", icon: <FaServicestack />, iconClosed: <RiArrowDownSFill />, iconOpened: <RiArrowUpSFill />, subNav: [ { title: "Service 1", path: "/services/services1", icon: <IoIosPaper />, cName: "sub-nav", }, { title: "Service 2", path: "/services/services2", icon: <IoIosPaper />, cName: "sub-nav", }, { title: "Service 3", path: "/services/services3", icon: <IoIosPaper />, }, ], }, { title: "About Us", path: "/about-us", icon: <AiFillHome />, }, { title: "Contact", path: "/contact", icon: <FaPhone />, }, { title: "Support", path: "/support", icon: <IoMdHelpCircle />, }, ];
sidebar/SubMenu.js
import React, { useState } from "react"; import { Link } from "react-router-dom"; import styled from "styled-components"; const SidebarLink = styled(Link)` display: flex; color: #e1e9fc; justify-content: space-between; align-items: center; padding: 20px; list-style: none; height: 60px; text-decoration: none; font-size: 18px; &:hover { background: #252831; border-left: 4px solid green; cursor: pointer; } `; const SidebarLabel = styled.span` margin-left: 16px; `; const DropdownLink = styled(Link)` background: #252831; height: 60px; padding-left: 3rem; display: flex; align-items: center; text-decoration: none; color: #f5f5f5; font-size: 18px; &:hover { background: green; cursor: pointer; } `; const SubMenu = ({ item }) => { const [subnav, setSubnav] = useState(false); const showSubnav = () => setSubnav(!subnav); return ( <> <SidebarLink to={item.path} onClick={item.subNav && showSubnav}> <div> {item.icon} <SidebarLabel>{item.title}</SidebarLabel> </div> <div> {item.subNav && subnav ? item.iconOpened : item.subNav ? item.iconClosed : null} </div> </SidebarLink> {subnav && item.subNav.map((item, index) => { return ( <DropdownLink to={item.path} key={index}> {item.icon} <SidebarLabel>{item.title}</SidebarLabel> </DropdownLink> ); })} </> ); }; export default SubMenu;
In the components folder, create Home component to show welcome screen.
Save the project. Here is the output after the app reloads:
import React, { useState } from "react" import axios from "axios"; import { useNavigate } from "react-router-dom"; import { Button, FormGroup,Card, CardHeader,CardBody,CardFooter, Input, Form, } from "reactstrap"; const Register = (props) => { const [errors,setErrors] = useState({}); const [inputs, setInputs] = useState({}); const history=useNavigate(); function handleValidation() { let formIsValid = true; let es = {}; //Name if (!inputs.username) { formIsValid = false; es['username']="can not empty!"; } if (typeof inputs.username !== "undefined") { if (!inputs.username.match(/^[a-zA-Z0-9]+$/)) { formIsValid = false; es['username']="only letters and numbers allowed!"; } } // password if (!inputs.password) { formIsValid = false; es['password']="can not empty!"; } if (typeof inputs.password !== "undefined") { if (inputs.password.length<8) { formIsValid = false; es['password']="week password!"; } } //Email if (!inputs.email) { formIsValid = false; es['email']="can not empty!"; } if (typeof inputs.email!== "undefined") { if (!(/\S+@\S+\.\S+/.test(inputs.email))) { formIsValid = false; es['email']="invalid email!"; } } setErrors(es); return formIsValid; } function handleSubmit(e){ e.preventDefault(); if(handleValidation()){ axios({ method: 'post', url: "/api/createuser", data: {username: inputs.username,password:inputs.password,email:inputs.email}, headers: { "Content-type":"application/json",} }).then(response=>{ try { let dt=JSON.parse(JSON.stringify(response.data)); if(dt.message==='success'){ history("/login"); } else{ alert('Failed to create user!'); } } catch (e) { alert('Failed to create user!'); } }); } } const handleChange = (event) =>{ const name = event.target.name; const value = event.target.value; setInputs(values => ({...values, [name]: value})) } return ( <div className="container" style={{width: '18rem'}}> <Card className="my-2" style={{width: '18rem'}} > <CardHeader>Register</CardHeader> <Form className="form"> <CardBody> <FormGroup> <Input type="text" name="username" placeholder="Enter username" value={inputs.username || ""} onChange={handleChange} /> <span style={{color: '#ff2222'}}>{errors.username}</span> </FormGroup> <FormGroup> <Input type="email" name="email" placeholder="Enter email" value={inputs.email || ""} onChange={handleChange} /> <span style={{color: '#ff2222'}}>{errors.email}</span> </FormGroup> <FormGroup> <Input type="password" name="password" placeholder="Enter password" value={inputs.password || ""} onChange={handleChange} /> <span style={{color: '#ff2222'}}>{errors.password}</span> </FormGroup> </CardBody> <CardFooter> <FormGroup> <Button color="primary" onClick={handleSubmit}>Register</Button> </FormGroup> </CardFooter> </Form> </Card> </div> ) } export default Register
Axios is used to access ExpressJs API endpoints. Let install it.
npm install axios
import React, { useState } from "react" import axios from "axios"; import { useNavigate } from "react-router-dom"; import { Link } from "react-router-dom" import { Button,FormGroup,Card,CardHeader,
CardBody,CardFooter, Input,Form,Label, } from "reactstrap"; const Login = (props) => { const [username, setUsername]=useState(''); const [password,setPassword]=useState(''); const [logerr,setLogError] = useState(''); const {tokenchange} = props; const history=useNavigate(); function handleTokenChange(tk){ tokenchange(tk); } function handleSubmit(e){ e.preventDefault(); // Make the POST call to login API end point axios({ method: 'post', url: "/api/auth", data: {username: username,password:password}, headers: { "Content-type":"application/json",} }).then(response=>{ console.log("token",response.data); try { let dt=JSON.parse(JSON.stringify(response.data)); if(dt.message==='success'){ localStorage.setItem("token", dt.token); handleTokenChange(dt.token); history("/products"); } else{ setLogError(dt.message); } } catch (e) { setLogError('Invalid user!'); } }); } const handleUsernameInputChange = (event) => { setUsername(event.target.value); } const handlePasswordInputChange = (event) => { setPassword(event.target.value); } return ( <div className="container" style={{width: '25rem'}}> <Card className="my-2" style={{width: '25rem'}} > <CardHeader>Login</CardHeader> <Form className="form"> <CardBody> <FormGroup> <Input type="text" name="username" placeholder="Enter username" value={username} onChange={(e) =>handleUsernameInputChange(e)} required /> </FormGroup> <FormGroup> <Input type="password" name="password" placeholder="Enter password" value={password} onChange={(e) =>handlePasswordInputChange(e)} required /> </FormGroup> </CardBody> <CardFooter> <FormGroup> <Button color="primary" onClick={handleSubmit}>Login</Button> <Label style={{marginLeft: '5px'}}>Don't have an account?</Label> <Link to="/register">create user</Link> <Label className="text-danger">{logerr}</Label> </FormGroup> </CardFooter> </Form> </Card> </div> ) } export default Login
The Logout simply removes the stored token from the local storage and redirect the user to the Home page.
components/Logout.js
import React,{useEffect} from "react" import { useNavigate } from "react-router-dom"; const Logout = (props) => { const {tokenchange} = props; const history=useNavigate(); function handleTokenChange (tk) { tokenchange(tk); // inform token change to update logout status in SideBar } useEffect(() => { localStorage.removeItem('token'); handleTokenChange(null); history("/"); }, []); return ( <><div>Logout work</div></> ) } export default Logout
The ProductMain component is the product admin panel that allows an authorized user to manage products. He/she can view products list, sort the products by clicking the column headers, search for products, add new product with images, update, and delete products from the MYSQL database. To access restricted APIs, you need to pass the token via axios request headers to the sever. If the token is not valid, subsequent requests will fail.
components/ProductMain.js
import React,{ useState, useEffect } from "react"; import axios from "axios"; import SearchBar from "./SearchBar"; import { FaTrash } from "react-icons/fa" import { FaEdit } from "react-icons/fa" import { NavLink, Navigate } from "react-router-dom"; import ProgressBar from "./ProgressBar"; import { Label,Form,FormGroup,Input, Button,Col,Table, Modal, ModalHeader, ModalBody, ModalFooter, } from "reactstrap"; // global variables var start=0; var step=5; var search=''; function ProductMain(props) { //vairables/ const [products,setProducts]=useState(null); const [modal,setModal]=useState(false); const [id,setId]=useState(0); const [title,setTitle]=useState(''); const [description,setDescription]=useState(''); const [price,setPrice]=useState(0.0); const [document,setDocument]=useState(null); const [refresh,setRefresh]=useState(false); const [sorted,setSorted]=useState(false); const [isEditMode,setIsEditmode]=useState(false); const [numRows,setNumRows]=useState(0);
const hstyle={textDecoration: 'none'}; const [files,setFiles]=useState(null); const [progressing,setProgressing]= useState(false); const token= localStorage.getItem("token"); const pstyle={ marginRight: '20px', textDecoration: 'none', }; useEffect(() => { axios .get("/api/products?search="+search+"&start="+start+"&step="+step,{ headers:{ "authorization": `Bearer ${token}`, }, }) .then((res) => { parseResult(res.data); }) .catch((err) => console.log(err)); }, [refresh]); const handleSubmit = (e) => { e.preventDefault(); let form_data = new FormData(); form_data.append('id', id); form_data.append('title', title); form_data.append('description', description); form_data.append('price',price); if(document!=null){ for (let i = 0; i < document.length; i++) { form_data.append('document', document[i],document[i].name); } } for (var key of form_data.entries()) { console.log(key[0] + ', ' + key[1]); } setProgressing(true); // Make the POST call by passing a config object to the instance axios({ method: isEditMode?'put':'post', url: "/api/products", data: form_data, headers: { "Content-type":"application/json", "authorization": `Bearer ${token}`, } }).then(res=>{ console.log("res=",res); let dt=JSON.parse(JSON.stringify(res.data)); if(dt.message==='success'){ if(search!=='') searchNow(search); else setRefresh(!refresh); setProgressing(false); toggle(); } else{ if(isEditMode) alert("Failed to update record"); else alert("Failed to insert record"); } }); }; const searchNow= (q) =>{ start=0; // reset offset search=q; axios({ method: 'get', url: "/api/products?search="+search+"&start="+start+"&step="+step, headers: { "Content-type":"application/json","authorization": `Bearer ${token}`} }).then(response=>{ parseResult(response.data); }); }; const deleteProduct = (id) => { axios({ url: "/api/products/"+id+"/", headers: { "Content-type":"application/json","authorization": `Bearer ${token}`}
}).then(response=>{ console.log("deleted",response.data); setRefresh(!refresh); }); }; const editProduct = (item) => { let imgs= products .filter(product => { return ( product.id === item.id ); }).map((product,index) =>{ return(product.Images); }); setFiles(imgs[0]); setIsEditmode(true); setId(item.id); setTitle(item.title); setDescription(item.description); setPrice(item.price); toggle(!modal); }; function handleChangeTitle(e){ setTitle(e.target.value); } function handleChangeDescription(e){ setDescription(e.target.value); } function handleChangePrice(e){ setPrice(e.target.value); } function toggle() { setModal(!modal); } function openAddNewModel() { setIsEditmode(false); setFiles(null); toggle(); } const handleImageChange = (e) => { setDocument(e.target.files); }; const handlePaging = (mv) =>{ if(mv==='next') { if(start<numRows-step) start=start+step; } else if(mv==='prev') { if(start>=step) start=start-step; } axios({ method: 'get', url: "/api/products?search="+search+"&start="+start+"&step="+step, headers: { "Content-type":"application/json","authorization": `Bearer ${token}`} }).then(response=>{ parseResult(response.data); }); } const parseResult = (data) =>{ let dt=JSON.parse(JSON.stringify(data)); //console.log("dt=",data); if(dt.message==='success'){ setProducts(dt.data); setNumRows(dt.numrow); } } const handleHeaderClick = (field) =>{ // sort records if(field!=='price') products.sort((a,b) => a[field].localeCompare(b[field])); else products.sort((a,b) => b[field]-a[field]); // refresh UI setSorted(!sorted); } const MAX_PERCENTAGE = 100; const MIN_PERCENTAGE = 45; const [percentage, setPercentage] = React.useState(MAX_PERCENTAGE); useEffect(() => { const timeout = setTimeout(() => { setPercentage( percentage === MAX_PERCENTAGE ? MIN_PERCENTAGE : MAX_PERCENTAGE ); }, 2000); return () => { window.clearTimeout(timeout); }; }, [percentage]); if(token===null) return <Navigate to="/login" /> return ( <> <SearchBar searchNow={searchNow}/> <div className="container"> <h1>Products</h1> { <Table striped bordered hover> <thead> <tr><th><NavLink style={hstyle} to={"#"} onClick={() => handleHeaderClick("title")}>Title</NavLink></th><th ><NavLink style={hstyle} to={"#"} onClick={() => handleHeaderClick("description")}>Description</NavLink></th><th><NavLink style={hstyle} to={"#"} onClick={() => handleHeaderClick("price")}>Price</NavLink></th><th colSpan={2}></th></tr> </thead> <tbody> { products && products.map((item, i) => { return ( <tr key={item.id}> <td >{item.title}</td> <td >{item.description}</td> <td >{item.price}</td> <td > <Button color="primary" onClick={() =>editProduct(item)}> <FaEdit style={{ color: "white", fontSize: "12px" }} /> </Button></td> <td > <Button color="primary" onClick={() =>deleteProduct(item.id)}> <FaTrash style={{ color: "red", fontSize: "12px" }} /> </Button></td> </tr> ); }) } </tbody> </Table> } { (start-step>=0 && numRows>step) && <NavLink onClick={() =>handlePaging('prev')} style={pstyle} to={"#"}>{"<"}</NavLink> } { (numRows-start>=step && numRows>step) && <NavLink abc={numRows} onClick={() =>handlePaging('next')} style={pstyle} to={"#"}>{">"}</NavLink> } <div style={{float: 'right'}}> <Button color="primary" onClick={openAddNewModel}>Add New</Button> </div> <Modal isOpen={modal} toggle={toggle}> <ModalHeader toggle={toggle}>{isEditMode?'Edit Product':'Add Product'}</ModalHeader> <ModalBody> <Form> <FormGroup row> <Label for="title" sm={2} size="lg">Title</Label> <Col sm={10}> <Input type="text" value={title} name="title" id="title" placeholder="Title" bsSize="lg" onChange={(e) =>handleChangeTitle(e)} required /> </Col> </FormGroup> <FormGroup row> <Label for="description" sm={2}>Description</Label> <Col sm={10}> <Input type="textarea" value={description} name="description" id="description" placeholder="Description" onChange={(e) =>handleChangeDescription(e)} required /> </Col> </FormGroup> <FormGroup row> <Label for="price" sm={2}>Price</Label> <Col sm={10}> <Input type="number" value={price} name="price" id="price" placeholder="Price" onChange={(e) =>handleChangePrice(e)} required /> </Col> </FormGroup> <FormGroup> <p><Label for="file">File</Label> <Input multiple type="file" id="document" name="document" accept="image/png, image/jpeg" onChange={handleImageChange} /> { files && files.map((item, i) =>{ return( <img style={{marginRight: "5px", width: "50px",height: "50px"}} src={`http://localhost:5000/${item.fileUrl}`} /> ); }) } </p> <p> { progressing && <ProgressBar bgcolor={"#6a1b9a"} completed={percentage} /> } </p> </FormGroup> </Form> </ModalBody> <ModalFooter> <Button color="primary" onClick={handleSubmit}>Ok</Button> <Button color="secondary" onClick={toggle}>Cancel</Button> </ModalFooter> </Modal> </div> </> ); } export default ProductMain;
components/ProgressBar.js displays a progress bar when the form data is submitted. It lets the user know what is going on and wait until the form is uploaded completely.
Finally, add the Register, Login, Logout, and ProductMain components to App.js.
Save project. Congratulation! You have developed a full stack website using React Express & MYSQL.
Video Demo
Comments
Post a Comment