In the earlier tutorials, you learnt to create API endpoints in Spring Boot & MYQL to perform CRUD operationsCRUD operations, upload form data with files, and to setup authentication & role-based access control using JWT.
In this tutorial, you learn to create React front-end app to access the API endpoints. After completing this tutorial, you build a simple product admin panel that allows an authorized user to add new products with image files, update and delete existing products from MYSQL database.
In working directory of your choice, execute the following command to setup a React app.
npm init react-app spr-client
After the app initialization completes, the spr-client folder is created. To run the app, change to spr-client folder and execute the command below:
spr-client>npm run start
We use bootstrap to style the app. Let install bootstrap by executing the command below:
spr-client>npm install bootstrap
We use icons and components from react-icons and reactstrap. To install these packages, execute the commands:
spr-client>npm install react-icons reactstrap
In src, create components folder. Add NavBar.js file to the components folder. The NavBar component display a top navigation bar that has two items: Home and Login/Logout items.
components/NavBar.js
Add register.js, login.js, logout.js, and product.js files to the components folder.
The register.js contains Register component to display a register form to create a new user account.
components/register.js
import React, { useState } from "react" import axios from "axios"; import { useNavigate } from "react-router-dom"; import { Button, FormGroup,Card, CardHeader,CardBody,CardFooter, Input,Label, Form,Col, } 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!"; } } //password 2 if (!inputs.password2) { formIsValid = false; es['password2']="can not empty!"; } if (inputs.password!==inputs.password2) { formIsValid = false; es['password2']="Passwords not match!"; } setErrors(es); return formIsValid; } const ROLE_CHOICES = [ {"id":1,"text":"admin"}, {"id":2,"text":"mod"}, {"id":3,"text":"user"}, ]; function handleSubmit(e){ e.preventDefault(); if(handleValidation()){ axios({ method: 'post', url: "/api/auth/register", data: {username: inputs.username,password:inputs.password,email:inputs.email,role:[inputs.role?inputs.role:'admin']}, headers: { "Content-type":"application/json",} }).then(response=>{ try { console.log(response); let dt=JSON.parse(JSON.stringify(response)); if(dt.status===200){ 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> <FormGroup> <Input type="password" name="password2" placeholder="Enter confirm password" value={inputs.password2 || ""} onChange={handleChange} /> <span style={{color: '#ff2222'}}>{errors.password2}</span> </FormGroup> <FormGroup row> <Label for="emp_status" sm={10}>Role</Label> <Col sm={10}> <Input onChange={(e) =>handleChange(e)} type="select" name="role" id="role" value={inputs.role}> { ROLE_CHOICES.map((emp)=>{ return ( <option key={emp.id} value={emp.text}>{emp.text}</option> ); }) } </Input> </Col> </FormGroup> </CardBody> <CardFooter> <FormGroup> <Button color="primary" onClick={handleSubmit}>Register</Button> </FormGroup> </CardFooter> </Form> </Card> </div> ) } export default Register
The API endpoint to create a new user in MYSQL database can be access via http://localhost:8080/api/auth/register. It is better to use only /api/auth/register. This can be achieved using proxy that will forward API request from port 3000 to 8080.
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 endpoint axios({ method: 'post', url: "/api/auth/login", data: {username: username,password:password}, headers: { "Content-type":"application/json"} }).then(response=>{ console.log("res=",response); try { let dt=JSON.parse(JSON.stringify(response)); if(dt.status===200){ localStorage.setItem("token", dt.data.accessToken); handleTokenChange(dt.data.accessToken); history("/"); } else{ setLogError("Error in login"); } } catch (e) { setLogError('Invalid user!'); } }) .catch(error => { 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
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); } useEffect(() => { localStorage.removeItem('token'); handleTokenChange(null); history("/"); }, []); return ( <><div>Logout work</div></> ) } export default Logout
import React, { useState, useEffect } from "react" import axios from "axios"; import { FaTrash } from "react-icons/fa" import { FaEdit } from "react-icons/fa"import { Button, FormGroup,Col, Card, CardHeader,CardBody, Input, Form, Label, Table } from "reactstrap"; const Product= (props) => { const token= localStorage.getItem("token");import { useNavigate } from "react-router-dom";
const history=useNavigate();const [id, setId]=useState(0); const [inputs, setInputs] = useState( { 'id':0, 'name':'', } ); const [fileatt, setDocument] = useState(null); const [isEditMode, setIsEditmode]= useState(false); const [message,setMessage]= useState(''); const [products, setProducts]=useState(null); const [refresh, setRefresh]=useState(false); useEffect(() => { if(token){ axios.get(`/products/all`,{ headers:{ "Content-type":"application/json", "authorization": `Bearer ${token}`, }, }) .then((res) => { const resObj=JSON.parse(JSON.stringify(res)); if(resObj.status===200){ setProducts(resObj.data); } }) .catch((err) => console.log(err)); }else{ console.log('Unauthorized user');
history("/login");} }, [refresh]); const handleChange = (event) =>{ const name = event.target.name; const value = event.target.value; setInputs(values => ({...values, [name]: value})) } const handleFileChange = (e) => { setDocument(e.target.files[0]); }; const editProduct = (item) =>{ setIsEditmode(true); setId(parseInt(item.id)); setInputs(values => ({...values, ['name']: item.name})); setInputs(values => ({...values, ['price']: item.price}));
setInputs(values => ({...values, ['thumbnail']: item.thumbnail}));} const deleteProduct = (id) => { axios({ method: 'delete',
url: "/products/delete/"+id+"/", headers: { "Content-type":"application/json",
"authorization": `Bearer ${token}`} }).then(response=>{ setRefresh(!refresh); });
}; const handleSubmit = (e) => { e.preventDefault(); let form_data = new FormData(); form_data.append('id', id); form_data.append('name', inputs.name); form_data.append('price', inputs.price); if(inputs.thumbnail) form_data.append('thumbnail', inputs.thumbnail);if(fileatt!=null) { form_data.append('file', fileatt, fileatt.name); } for (var key of form_data.entries()) { console.log(key[0] + ', ' + key[1]); } // Make the POST call by passing a config object to the instance axios({ method: isEditMode?'put':'post', url: "/products/"+(!isEditMode?"add":"update/"+id), data: form_data, headers: { "authorization": `Bearer ${token}` } }).then(res=>{ const resObj=JSON.parse(JSON.stringify(res)); if(resObj.status===201 || resObj.status===200){ setMessage(" saved successfully!"); } else{ setMessage(" Failed to save!"); } setRefresh(!refresh); }); }; const reset = () =>{ setIsEditmode(false); setId(parseInt(0)); setInputs(values => ({...values, ['name']: ''})); setInputs(values => ({...values, ['price']: 0.0}));
setInputs(values => ({...values, ['thumbnail']: ''}));} return ( <div className="container" style={{width: '30rem',marginTop:"5px"}}> <Card className="my-2" style={{width: '30rem'}} > <CardHeader>Add/Update Product</CardHeader> <Form> <CardBody> <FormGroup row> <Label for="name" sm={4} size="lg">Name</Label> <Col sm={8}> <Input type="text" value={inputs.name} name="name" id="name" placeholder="Name" bsSize="lg" onChange={(e) =>handleChange(e)} required /> </Col> </FormGroup> <FormGroup row> <Label for="price" sm={4}>Price</Label> <Col sm={8}> <Input type="text" value={inputs.price} name="price" id="price" placeholder="Price" bsSize="lg" onChange={(e) =>handleChange(e)} required /> </Col> </FormGroup> <FormGroup> <Label for="file" sm={10}>Image</Label> <Col sm={10}> <Input type="file" id="fileatt" name="fileatt" accept="image/*" onChange={handleFileChange} /> </Col> </FormGroup> <FormGroup row> <Col sm={3}> <Button color="primary" onClick={handleSubmit}>Submit</Button> </Col> <Col sm={2}> <Button color="primary" onClick={reset}>New</Button> </Col> <Col sm={4}><span>{message}</span></Col> </FormGroup> </CardBody> </Form> </Card> <h1>Products</h1> { <Table striped bordered hover> <thead> <tr><th>Id</th><th>Name</th><th>Price</th><th>Image</th><th>Actions</th></tr> </thead> <tbody> { products && products.map((item, i) => { return ( <tr key={item.id}> <td >{item.id}</td> <td >{item.name}</td> <td >{item.price}</td> <td ><img style={{width: '50px',height:"50px"}} src={"image/"+item.thumbnail}></img></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> } </div> ) } export default Product
Save the project. Then try create a new user with admin role and login to add new products, update and delete products from the list.
Video Demo
Comments
Post a Comment