I use Google Analytics to monitor how many visitors my site receives for security purposes.

#05 - Building a MERN stack app

Part 2 - UI and CRUD

2023-06-10

Hello again reader,

In part 2 of building a MERN app, we will be working on the frontend UI and CRUD features.

If you’re simply interested at looking at the codebase as a whole, you can visit my repo here. Otherwise, follow along below.

Now we are going to create the user interface which will include

  • Navbar that changes after login
  • Login, Signup
  • Home Page
  • Book Form to create new books
  • List of Books, able to delete or update
  • Protected Routes
Navigate into your client folder

cd client
lang-bash
We will need to install some dependancies

npm i react-router-dom cloudinary-react cloudinary universal-cookie fontawesome react-quill @fortawesome/fontawesome-svg-core@^6.3.0 @fortawesome/free-solid-svg-icons@^6.3.0 @fortawesome/react-fontawesome@^0.2.0
lang-bash
After that is complete, you can run npm start

npm start
lang-bash
A new tab should appear with localhost:3000
Let’s go to our App.js first and establish our routes as well as some authentication which will be used for user login.

App.js

import './App.css';
import Navbar from './components/navbar';
import Home from './pages/home';
import Books from './pages/books';
import NewBook from './pages/addBook';
import UpdateBook from './pages/updateBook';
import ShowBook from './pages/ShowBook';
import AddUser from './pages/signup';
import LoginUser from './pages/login';
import ShowUser from './pages/showUser';
import UpdateUser from './pages/updateUser';
import {BrowserRouter as Router, Route, Routes} from 'react-router-dom';
import ProtectedRoutes from './ProtectedRoutes';
import { UserContext } from "./UserContext";
import { useMemo, useState } from 'react';
import { useEffect } from 'react';
import { config } from './config/config';

const URL = config.url;
console.log("URL shown in App.js",URL)
console.log("What environment has been detected?", process.env.NODE_ENV)

function App() {
  const [user, setUser] = useState(null);

  useEffect(() => {
    const id = localStorage.getItem('id');

    if (id !== null) {
      console.log("condition true")
    fetch(`${URL}/user/show/${id}`, {
        method: 'GET',
        })
        .then((response) => response.json())
        .then((data) => {
            setUser(data);
        })
        .catch((err) => {
            console.log(err.message);
        });
    }},
    []);

  const value = useMemo(() => ({ user, setUser }), [user, setUser]);

  return (
    <Router>
      <UserContext.Provider value={value}>
        <div className="App">
          <Navbar />
          <Routes>
            <Route 
                path="/" 
                element={<Home />} 
                />
              <Route 
                path="/books" 
                element={<Books />} 
              />
              <Route 
                path="/book/show/:id" 
                element={<ShowBook />} 
                />
              <Route 
                path="/signup" 
                element={<AddUser />} 
                />
              <Route 
                path="/login" 
                element={<LoginUser />} 
                />
              <Route element={<ProtectedRoutes/>}>
                <Route
                  path="/new-book" 
                  element={
                    <NewBook />   
                  }
                  />
                <Route
                  path="/book/update/:id" 
                  element={
                    <UpdateBook />
                  }
                  />
                <Route
                  path="/user/show/:id" 
                  element={
                    <ShowUser />
                  }
                />
                <Route
                  path="/user/update/:id" 
                  element={
                    <UpdateUser />
                  }
                  />
              </Route>
          </Routes>
        </div>
      </UserContext.Provider>
    </Router>
  );
};

export default App;
lang-javascript
Now we need to create the config.js file as well as the protected routes file.
In your src directory, create a new folder called config that contains config.js.

config.js

const production = {
  url: 'https://your-production-backend-url.app'
};

const development = {
  url: 'http://localhost:4000'
};

export const config = process.env.NODE_ENV === 'development' ? development : production;
lang-javascript
and next go back to your src file and create a ProtectedRoutes.js file

ProtectedRoutes.js

import React from "react";
import { Outlet, Navigate } from "react-router-dom";
import Cookies from "universal-cookie";
const cookies = new Cookies();

const ProtectedRoutes = () => {
  const auth = cookies.get("TOKEN");
  console.log("auth:", auth)

return (
    auth ? <Outlet/> : <Navigate to='/login'/>
  )
}

export default ProtectedRoutes
lang-javascript
and lastly, we will create another file in src called UserContext.js

UserContext.js

import { createContext } from "react";

export const UserContext = createContext(null);
lang-javascript
Now we will need to create these components and pages that are listed at the beginning of our app.js

import Navbar from './components/navbar';
import Home from './pages/home';
import Books from './pages/books';
import NewBook from './pages/addBook';
import UpdateBook from './pages/updateBook';
import ShowBook from './pages/ShowBook';
import AddUser from './pages/signup';
import LoginUser from './pages/login';
import ShowUser from './pages/showUser';
import UpdateUser from './pages/updateUser';
lang-javascript
So let’s create this structure in our src directory

Create a page folder in src as well as a components folder in src

In the components folder, create these files

navbar.js

import {Link} from 'react-router-dom';
import './navbar.css';
import Cookies from "universal-cookie";
import { useContext } from 'react';
import { UserContext } from '../UserContext';

const Navbar = () => {
    const { user, setUser } = useContext(UserContext);

    const logout = () => {
        const cookies = new Cookies();
        cookies.remove("TOKEN", { path: "/" });
        localStorage.clear();
        // isLoggedIn(false);
        setUser(null);
        };
    
    const id = localStorage.getItem('id');

    return (
        <div className="navbar">
            {user  ? (
            <div className="navbar logged-in">    
                <div className="navitems">
                    <div className="nav-item">
                        <Link to="/books" className="item">
                            Books
                        </Link>
                    </div>
                    <div className="nav-item">
                        <Link to="/new-book" className="item">
                            Add a book
                        </Link>
                    </div>
                    <div className="user-navitem">
                        User
                        <div className='nav-dropdown'>
                            <div className="dropdown-item">
                                <Link to={`/user/show/${id}`} className="item-in-dropdown">
                                    Profile
                                </Link>
                            </div>
                            <div className="dropdown-item"> 
                                <a href='/' onClick={() => logout()} className="item-in-dropdown">
                                    Logout
                                </a>    
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        ) : (
            <div className="navitems">
                <div className="nav-item">
                    <Link to="/" className="item">
                        Home
                    </Link>
                </div>
                <div className="nav-item">
                    <Link to="/login" className="item">
                        Login
                    </Link>
                </div>
                <div className="nav-item nav-cta">
                    <Link to="/signup" className="item">
                        Signup
                    </Link>
                </div>
            </div>
        )}
        </div>
    );
};

export default Navbar;
lang-javascript
and navbar.css

.navbar {
    /* background-color: red; */
}

.logged-in {
    background-color: #ED3D1E;
    /* position: fixed;
    width: 100%; */
}

.navitems {
    color: #272624;
    text-decoration: none;
    display: flex;
    justify-content: right;
}

.nav-item {
    padding: 30px;
}

.item {
    color: #272624;
    text-decoration: none;
    
}

.nav-cta a {
    background-color: #ED3D1E;
    color:white;
    padding: 10px 15px;
}

Link {
    background-color: green;
}

.user-navitem {
    color:white;
    padding: 30px;
}

.nav-dropdown {
    display: none;
    position: absolute;
    top: 80px;
    right: 0px ;
    background-color: beige;
    z-index: 1;
}

.dropdown-item {
    padding: 20px;
}

.user-navitem:hover .nav-dropdown {
    display: block;
}

.item-in-dropdown {
    color: black;
    text-decoration: none;
}
lang-css
Thats our navbar done

now lets create the home page in the pages folder

home.js

import {PrimaryButton} from "../components/buttons";
import "./home.css"

const Home = () => {
    return (
        <div className="container fade-page">
            <div className="hero-banner">
                    <div className="hero-txt-area">
                        <h1>MERN Template</h1>
                        <p>Create, show, update and delete books,</p>
                        <p>but you should change or add on to continue your own mern stack project.</p>
                        <p>Hope this helps!</p>
                        <p>Ilia</p>
                    </div>
                    <PrimaryButton />
                </div>
            </div>
    );
}

export default Home;
lang-javascript
home.css

.container {
    height: 90vh;
    display: flex;
    align-items: center;
}

.hero-banner {
    padding: 210px 0px 0px 0px;
    height: 500px;
    width: 1400px;
    margin: auto ;
}

.hero-img {
    margin-top: -100px;
}

.hero-txt {
    width: 500px;
    margin-top: -100px;
}

.hero-txt-area {
    padding-bottom: 20px;;
}
lang-css

Lets also create our buttons component

in components folder, create buttons.js and buttons.css files

buttons.js

import {Link} from 'react-router-dom';
import "./button.css"

const PrimaryButton = () => {
    return (
        <div className="">
                <Link to="/books" className="primary-button">
                    See Books
                </Link>
        </div>
    );
};


const SecondaryButton = () => {
    return (
        <div className="">
            <div className="">
                <Link to="/add-book">
                    <p>secondary button</p>
                </Link>
            </div>
        </div>
    );
};



const TertiaryButton = () => {
    return (
        <div className="">
            <div className="">
                <Link to="/add-book">
                    <p>tertiery button</p>
                </Link>
            </div>
        </div>
    );
};

export  {PrimaryButton, SecondaryButton, TertiaryButton};
lang-javascript
buttons.css

.primary-button {
    color: white;
    text-decoration: none;
    background-color: #ED3D1E;
    padding: 15px 20px;
    cursor: pointer;
}
lang-css
Great, now let’s create our page where we can see a list of books

in our pages folder, create a books.js file

import BookCard from '../components/bookCard';
import './books.css'


const Books = () => {
    return (
        <div className="ramen-list-body fade-page">
            <div className="">
                <div className="page-title">
                    <h1>All Books</h1>
                </div>
                <BookCard />
            </div>
        </div>
    );
}

export default Books;
lang-javascript
Lets create the BookCard component in our component folder

bookCard.js

import { useState, useEffect } from "react";
import { useNavigate } from 'react-router-dom';
import { useContext } from 'react';
import { UserContext } from '../UserContext';
import './book-card.css'
import Cookies from "universal-cookie";
import { config } from '../config/config';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faPenToSquare, faEye, faTrash } from '@fortawesome/free-solid-svg-icons'

const cookies = new Cookies();

const URL = config.url;
console.log("prod or dev?", URL)

const BookCard = () => {
    const [books, setBook] = useState([]);
    const { user } = useContext(UserContext);
    const navigate = useNavigate();

    const token = cookies.get("TOKEN");

    
    useEffect(() => {
        fetch(`${URL}/books`)
            .then((response) => response.json())
            .then((data) => {
                console.log(data);
                setBook(data);
            })
            .catch((err) => {
                console.log(err.message);
            });  
        }, []);

    const deleteBook = async (id, public_id, user_id, user) => {
        console.log("delete:",id)
        console.log("delete:",public_id)
        console.log("user who created book",user)
        const theLoggedInUser = localStorage.getItem('id')
        console.log("logged in user who is trying to delete book",theLoggedInUser)

        if (user !== theLoggedInUser){
            console.log("you cannot delete another persons book")
        }

        await fetch(`${URL}/book/delete/${id}/${public_id}/user/${user_id}`, {
        method: 'DELETE',
        headers: {
            'Authorization': `${token}` 
        },
        }).then((response) => {            
            if (response.status === 200) {
                setBook((prevBooks) => prevBooks.filter((book) => book._id !== id));
                console.log("Book deleted");
                } else {
                    console.log("Book not deleted");
                }
            });
        };

    const viewBook = async (id) => {
        console.log("this is id", id);
        navigate(`/book/show/${id}`);
    };

    const updateBook = (id) => {
        navigate(`/book/update/${id}`);
    };

    return (
        <div className="">
            <div className="card-area">
                {books.map((book) => {
                return (
                    <div id={book._id} className="book-card" >
                        <div class="card-image-container">
                            <img src={book.imageUrl} alt="" style={{width: 400}} />
                        </div>
                        <div className="card-text-area">
                            <h4>{book.title}</h4>
                            <p dangerouslySetInnerHTML={{ __html: book.description}}></p>
                        {user ? (
                            <div className="card-button-area">
                                <div className="show-button button" onClick={() => viewBook(book._id)} >
                                    <FontAwesomeIcon icon={faEye} className="eye"/>
                                </div>
                                <div className="update-button button" onClick={() => updateBook(book._id)} >
                                    <FontAwesomeIcon icon={faPenToSquare} className="update"/>
                                </div>
                                <div className="delete-button button" onClick={() => deleteBook(book._id, book.public_id, localStorage.getItem('id'), book.user)} id={book.id}>
                                    <FontAwesomeIcon icon={faTrash} className="delete"/>
                                </div>
                            </div>
                                ) : (
                                    <div className="card-button-area">
                                        <div className="show-button button" onClick={() => viewBook(book._id)} >
                                        <FontAwesomeIcon icon={faEye} className="eye"/>
                                        </div>
                                    </div>
                                )}
                            </div>
                    </div>
                    );
                })}
            </div>
        </div>
    );
};

export default BookCard;
lang-javascript
and the book-card.css file

.card-area {
    padding: 20px;
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
    justify-content: center;
}

.book-card {
    background-color: #f3f0eb;
    border-radius: 5px;
    width: 400px;
    height: 400px;
    box-shadow: 0 0 15px rgba(0,0,0,0.2);
    margin: 20px;
    padding: 20px;
}

.card-text-area {
    position: relative;
    height: 120px;
}

.card-button-area {
    display: flex;
    /* justify-content: flex-end; */
    position: absolute;
    bottom: 0px;
    right: 0px;
}

.button {
    padding: 10px 15px;
    border-radius: 5px;
    color: white;

}

.show-button, .delete-button, .update-button  {
    color: #818181;
    cursor: pointer;
}

.show-button:hover, .delete-button:hover, .update-button:hover {
    color: #ED3D1E;
    transition: all 0.3s ease-in;
}


.card-image-container {
    /* background-color: grey; */
    width: 400px;
    height: 250px;
    overflow: hidden;
    
}
lang-css
Now let’s create our page and form that will enable us to create book entries to our mongodb

in pages, create a AddBook.js file and its corresponding css file

AddBook.js

import AddBookForm from "../components/addBookForm";

const NewBook = () => {
    return (
        <div className="">
            <div className="fade-page">
                <div className="page-title">
                    <h1>Add Book</h1>
                </div>
                <AddBookForm />
            </div>
        </div>
    );
}

export default NewBook;
lang-javascript
Now in our components lets create the form that we will use to create our book entry

addBookForm.js

import { useState } from "react";
import { useNavigate } from 'react-router-dom';
import {Image} from 'cloudinary-react';
import { config } from '../config/config';
import "./book-form.css"
import ReactQuill from 'react-quill';
import 'react-quill/dist/quill.snow.css';


const URL = config.url;

const AddBook = () => {
    const [title, setTitle ] = useState('');
    const [description, setDescription ] = useState('');
    const [imageUrl, setImageUrl] = useState('');
    const [publicId, setPublicId] = useState('');
    const navigate = useNavigate();

    const cloudinaryUsername = process.env.REACT_APP_CLOUDINARY_USERNAME

    const cloudinaryPreset = process.env.REACT_APP_CLOUDINARY_PRESET

    const uploadUrl = `https://api.cloudinary.com/v1_1/${cloudinaryUsername}/image/upload`

    const uploadImage = async (files) => {

        const formData = new FormData()
        formData.append("file", files.target.files[0])
        formData.append("upload_preset", `${cloudinaryPreset}`)

        await fetch(uploadUrl, {
            method: 'POST',
            body: formData
            })
            .then(async (response) => {
            const data = await response.json();
            setImageUrl(data.secure_url)
            setPublicId(data.public_id)
            })            
        };

    const AddBook = async ( title, description, imageUrl, publicId, user) => {
        const userId = localStorage.getItem('id')
        console.log(userId,": this is the logged in user id")
        await fetch(`${URL}/book/add`, {
        method: 'POST',
        body: JSON.stringify({
            title: title,
            description: description,
            imageUrl: imageUrl,
            publicId: publicId,
            user: userId
        }),
        headers: {
            'Content-type': 'application/json; charset=UTF-8'
        },
        })
        .then((response) => { 
            console.log(response.json());
        })
        .then(() => {
        setTitle();
        setDescription();
        })
        .catch((err) => {
        console.log(err.message , ":error message");
    });
    navigate('/books');
};

const handleSubmit = (e) => {
    e.preventDefault();
    AddBook( title, description, imageUrl, publicId );
};


    return (
    <div className="form-container">
        <div className="form-image-container">
            <Image className="new-book-image" cloudName={cloudinaryUsername} publicId={imageUrl} />
        </div>
        <form method="post" onSubmit={handleSubmit} enctype="multipart/form-data">
            <label className="labels">
                Ttile
                <input 
                    type="text" 
                    name="title" 
                    placeholder="Type here..."
                    onChange={e => setTitle(e.target.value)} />
            </label>    
            <label className="labels">
                Description
                <ReactQuill
                    theme="snow"
                    type="textarea" 
                    name="description" 
                    placeholder="Type here..."
                    onChange={setDescription} />
            </label>
            <label className="labels">
                Image
                <input type="file" name="book" onChange={uploadImage}/>
            </label>
            <label className="labels hidden">
                imageUrl
                <textarea 
                    type="textarea" 
                    name="imageUrl" 
                    value={imageUrl}
                    onChange={e => setImageUrl(e.target.value)} />
            </label>
            <label className="labels hidden">
                publicId
                <textarea 
                    type="textarea" 
                    name="publicId" 
                    value={publicId}
                    onChange={e => setPublicId(e.target.value)} />
            </label>
            <input type="submit" value="Submit" className="primary-submit-button" />
        </form>
    </div>
    )
};

export default AddBook;
lang-javascript
We will create one css file for all the forms

book-form.css

.container {
    height: 80vh;
    display: flex;
    align-items: center;
}

.form-container{
    width: 1300px;
    margin: auto;
    position: relative;
    padding-top: 40px;
}

form {
    display: flex;
    flex-direction: column;
    width: 500px;
    margin: auto;
    padding-left: 550px;
}

.form-container-login {
    width: 500px;
    margin: auto 0;
}

.labels {
    padding: 5px 0 5px;
    display: flex;
    flex-direction: column;
    text-align: left;
}

input {
    margin: 10px 0 10px 0;
    border-radius: 5px;
    border: 1px solid lightgrey;
    padding: 10px;
    font-size: 16px;
}


input[type="file"] {
    border: none;
  }

.primary-submit-button {
    color: white;
    font-size: 18px;;
    background-color: red;
    border: 0px;
    padding: 20px 10px;
}

.form-image-container {
    position: absolute;
    top: 50px;
    left:94px;
    background-color: lightgrey;
    height: 340px;
    width: 520px;
    overflow: hidden;
    margin: auto;
}

.new-ramen-image {
    width:520px;
}

.hidden {
    display: none;
}

.form-user-image-container {
    position: absolute;
    top: 50px;
    left:200px;
    border-radius: 50%;
    background-color: lightgrey;
    width: 250px;
    height: 250px;
    margin: auto;
    margin-top: 30px;
}

form {
    padding-bottom: 100px;
}

.quill {
    background-color: white;
    border-radius: 5px;
    margin-top: 10px;
    font-size: 16px;
}

.ql-editor {
    min-height: 100px;
}

.ql-toolbar.ql-snow {
    border-radius: 5px 5px 0 0;
}

.ql-container {
    /* font-size: 16px !important; */
}

.login-page-image{
    position: absolute;
    top: 80px;

}

.login-form {
    margin-top: 150px;
}
lang-javascript
Now that we can create a book, we need to be able to see it after its creation.

ShowBook.js

import { useState, useEffect } from "react";
import { useNavigate, useParams } from 'react-router-dom';
import { useContext } from 'react';
import { UserContext } from '../UserContext';
import { config } from '../config/config';
import "./show-book.css"
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faCircleLeft, faPenToSquare, faTrash } from '@fortawesome/free-solid-svg-icons'
import Cookies from "universal-cookie";
const cookies = new Cookies();

const URL = config.url;

const ShowBook = () => {
    const [book, setBook] = useState([]);
    const { user } = useContext(UserContext);
    const navigate = useNavigate();
    const params = useParams();

    useEffect(() => {
        const id = params.id;

        fetch(`${URL}/books/show/${id}`, {
            method: 'GET',
            }).then((response) => response.json())
            .then((data) => {
                setBook(data);
            })
            .catch((err) => {
                console.log(err.message);
            });
        },
        []);
        
        const deleteBook = async (id, public_id) => {
            console.log("delete:",id)
            console.log("delete:",public_id)
            const token = cookies.get("TOKEN");

            fetch(`${URL}/book/delete/${id}/${public_id}`, {
            method: 'DELETE',
            headers: {
            'Authorization': `${token}`,
        },
            }).then((response) => {            
                if (response.status === 200) {
                    setBook();
                    console.log("Book deleted");
                    } else {
                        return;
                    }
                });
                navigate('/books');
            };

            const allBooks = () => {
                navigate('/books');
            }

            const updateBook = (id) => {
                navigate(`/book/update/${id}`);
            };

    return (
        <div className="show-book-container fade-page">
            <div className="show-book">
                <div className="flex space-around" >
                    <div className="show-page-img-ing">   
                        <div className="show-image-container">  
                            <img src={book.imageUrl} style={{width: 400}} alt="" />
                        </div>
                    </div>
                    <div className="show-page-description">
                    <h1>{book.title}</h1>
                        <div dangerouslySetInnerHTML={{ __html: book.description}} />
                        {user ? (
                        <div className="card-button-area-show flex">
                            <div className="show-button button" onClick={() => allBooks()} ><FontAwesomeIcon icon={faCircleLeft} className="back"/> Back to list</div>
                            <div className="update-button button" onClick={() => updateBook(book._id)} ><FontAwesomeIcon icon={faPenToSquare} className="update"/> Update</div>
                            <div className="delete-button button" onClick={() => deleteBook(book._id, book.public_id)} id={book.id} ><FontAwesomeIcon icon={faTrash} className="delete"/> Delete</div>
                        </div>
                        ) : (
                        <div className="card-button-area-show">
                            <div className="show-button button" onClick={() => allBooks()} ><FontAwesomeIcon icon={faCircleLeft} className="book-bowl"/> Back to list</div>
                        </div>
                        )}
                    </div>
                </div>
            </div>
        </div>
    );
}

export default ShowBook;
lang-javascript
and its show-book.css

.show-image-container {

}

.show-book-container {
    width: 1200px;
    height: 80vh;
    margin: auto;
    padding: 20px;
}

.show-page-img-ing {
    padding-right: 40px;
    padding-top: 50px;
}

.show-page-description {

}

.card-button-area-show {

}

.show-book {
    padding-top: 80px;
}
lang-javascript
now lets create our update book page in pages folder

updateBook.js

import UpdateBookForm from "../components/updateBookForm";

const UpdateBook = () => {

    return (
        <div className="">
            <div className="fade-page">
                <div className="page-title">
                    <h1>Update Book</h1>
                </div>
                <UpdateBookForm />
            </div>
        </div>
    );
}

export default UpdateBook;
lang-javascript
Lets create the form component

updateBookForm.js

import { useNavigate, useParams } from 'react-router-dom';
import { useEffect, useState } from "react";
import {Image} from 'cloudinary-react';
import { config } from '../config/config';
import "./book-form.css";
import ReactQuill from 'react-quill';
import 'react-quill/dist/quill.snow.css';

import Cookies from "universal-cookie";
const cookies = new Cookies();

const URL = config.url;

const UpdateBookForm = () => {
    const [title, setTitle ] = useState('');
    const [description, setDescription ] = useState('');
    const [imageUrl, setImageUrl] = useState('');
    const [publicId, setPublicId] = useState('');

    const navigate = useNavigate();
    const params = useParams();

    useEffect(() => {
        const id = params.id;
        fetch(`${URL}/books/show/${id}`, {
            method: 'GET',
            }).then((response) => response.json())
            .then((data) => {
                setTitle(data.title);
                setDescription(data.description);
                setImageUrl(data.imageUrl);
                setPublicId(data.public_id);
            })
            .catch((err) => {
                console.log(err.message);
            });
        },
        []);
    
    const cloudinaryUsername = process.env.REACT_APP_CLOUDINARY_USERNAME

    const cloudinaryPreset = process.env.REACT_APP_CLOUDINARY_PRESET

    const uploadUrl = `https://api.cloudinary.com/v1_1/${cloudinaryUsername}/image/upload`

    const uploadImage = async (files) => {

        const formData = new FormData()
        formData.append("file", files.target.files[0])
        formData.append("upload_preset", `${cloudinaryPreset}`)

        await fetch(uploadUrl, {
            method: 'POST',
            body: formData
            })
            .then(async (response) => {
            const data = await response.json();
            setImageUrl(data.secure_url)
            setPublicId(data.public_id)
            })            
        };

    const updateBook = async (id, title, description, imageUrl, publicId) => {
        const token = cookies.get("TOKEN");

        await fetch(`${URL}/book/update/${id}`, {
            method: 'PUT',
            body: JSON.stringify({
                title: title,
                description: description,
                imageUrl: imageUrl,
                publicId: publicId
            }),
            headers: {
                'Content-type': 'application/json; charset=UTF-8',
                'Authorization': `${token}`,
            },
            })
            .then((response) => { 
                response.json();
            })
            .then(() => {
            setTitle();
            setDescription();
            setImageUrl();
            setPublicId();
            })
            .catch((err) => {
            console.log(err.message , ":error message");
        });
    }

    const handleSubmit = () => {
        const id = params.id
        updateBook(id, title, description, imageUrl, publicId );
        navigate(`/book/show/${id}`);
        
    };
    

    return (
        <div className="form-container">
            <div className="form-image-container">
                <Image className="new-book-image" cloudName={cloudinaryUsername} publicId={imageUrl} />
            </div>
            <form method="puts" onSubmit={handleSubmit} enctype="multipart/form-data">
                <label className="labels">
                    Title
                    <input 
                        type="text" 
                        name="title" 
                        placeholder={title}
                        onChange={e => setTitle(e.target.value)} />
                </label>
                <label className="labels">
                    Description
                    <ReactQuill
                        theme="snow"
                        type="textarea" 
                        name="description" 
                        placeholder={description}
                        value={description}
                        onChange={setDescription} />
                </label>
                <label className="labels">
                    Image
                    <input type="file" name="book" onChange={uploadImage}/>
                </label>
                <label className="labels hidden">
                    imageUrl
                    <textarea 
                        type="textarea" 
                        name="imageUrl" 
                        value={imageUrl}
                        placeholder={imageUrl}
                        onChange={e => setImageUrl(e.target.value)} />
                </label>
                <label className="labels hidden">
                    publicId
                    <textarea 
                        type="textarea" 
                        name="publicId" 
                        value={publicId}
                        placeholder={publicId}
                        onChange={e => setPublicId(e.target.value)} />
                </label>
                <input type="submit" value="Submit" className="primary-submit-button" />
            </form>
        </div>
    )
};

export default UpdateBookForm;
lang-javascript
Great, so now we can add books, we can also update those books, included in our code above is logic to delete our books, and we can also show our books individually or as a list

Now we need to handle our login and signup forms

navigate to pages folder and create a login.js and signup.js files

login.js

import { useContext, useState } from "react";
import { useNavigate } from 'react-router-dom';
import "../components/book-form.css"
import Cookies from "universal-cookie";
import { UserContext } from '../UserContext';
import { config } from '../config/config';

const cookies = new Cookies();

const URL = config.url;

const LoginUser = () => {
    const [email, setEmail ] = useState('');
    const [password, setPassword] = useState('');
    const [login, setLogin] = useState(false);
    const [token, setToken] = useState('');
    const { user, setUser } = useContext(UserContext);
    const navigate = useNavigate();
    
// login incorrect, its allowing any type of input to access
    const loginUser = async ( email, password ) => {
        await fetch(`${URL}/login`, {
        method: 'POST',
        body: JSON.stringify({
            email: email,
            password: password
        }),
        headers: {
            'Content-type': 'application/json; charset=UTF-8',
            'Authorization': `Bearer ${token}` 
        },
        })
        .then( async (response) => { 
            const result = await response.json();
            const userEmail = result.email;
            const userId = result.userId;
            if (userId !== undefined && userEmail !== undefined) {
                cookies.set("TOKEN", result.token, {
                    path: "/"
                    });
                localStorage.setItem('email', userEmail);
                localStorage.setItem('id', userId);
                setEmail();
                setPassword();
                setLogin(true);
                setToken(result.token);
                setUser(result);
            }
        })
        .catch((err) => {
        console.log(err.message , ":error message");
    });
    navigate('/books');
};

const handleSubmit = (e) => {
    e.preventDefault();
    loginUser(email, password);
};

    return (
    <div className="form-container-login fade-page">
        <form method="post" onSubmit={handleSubmit} enctype="multipart/form-data" className="login-form">
            <label className="labels">
                Email
                <input 
                    type="text" 
                    name="email" 
                    placeholder="email"
                    onChange={e => setEmail(e.target.value)} />
            </label>
            <label className="labels">
                Password
                <input 
                    type="text" 
                    name="password" 
                    placeholder="password"
                    onChange={e => setPassword(e.target.value)} />
            </label>
            <input type="submit" value="Submit" className="primary-submit-button" />
        </form>
    </div>
    )
};

export default LoginUser;
lang-javascript
signup.js

import { useState } from "react";
import { useNavigate } from 'react-router-dom';
import {Image} from 'cloudinary-react';
import { config } from '../config/config';
import "../components/book-form.css"
import "./signup.css"

const URL = config.url;

const AddUser = () => {
    const [name, setName ] = useState('');
    const [surname, setSurname ] = useState('');
    const [email, setEmail ] = useState('');
    const [password, setPassword] = useState('');
    const [imageUrl, setImageUrl] = useState('');
    const [publicId, setPublicId] = useState('');
    const navigate = useNavigate();

    const cloudinaryUsername = process.env.REACT_APP_CLOUDINARY_USERNAME

    const cloudinaryPreset = process.env.REACT_APP_CLOUDINARY_PRESET

    const uploadUrl = `https://api.cloudinary.com/v1_1/${cloudinaryUsername}/image/upload`

    const uploadImage = async (files) => {

        const formData = new FormData()
        formData.append("file", files.target.files[0])
        formData.append("upload_preset", `${cloudinaryPreset}`)

        await fetch(uploadUrl, {
            method: 'POST',
            body: formData
            })
            .then(async (response) => {
            const data = await response.json();
            setImageUrl(data.secure_url)
            setPublicId(data.public_id)
            })            
        };

    const AddUser = async ( name, surname, email, password, imageUrl, publicId) => {
        await fetch(`${URL}/signup`, {
        method: 'POST',
        body: JSON.stringify({
            name: name,
            surname: surname,
            email: email,
            password: password,
            imageUrl: imageUrl,
            publicId: publicId
        }),
        headers: {
            'Content-type': 'application/json; charset=UTF-8'
        },
        })
        .then((response) => { 
            console.log(response.json());
        })
        .then(() => {
        setName();
        setSurname();
        setEmail();
        setPassword();
        })
        .catch((err) => {
        console.log(err.message , ":error message");
    });
    navigate('/books');
};

const handleSubmit = (e) => {
    e.preventDefault();
    AddUser(name, surname, email, password, imageUrl, publicId);
};

    return (
    <div className="form-container">
        <div className="form-user-image-container">
            <Image className="new-user-image" cloudName={cloudinaryUsername} publicId={imageUrl} />
        </div>
        <form method="post" onSubmit={handleSubmit} enctype="multipart/form-data">
            <label className="labels">
                Name
                <input 
                    type="text" 
                    name="name" 
                    placeholder="name"
                    onChange={e => setName(e.target.value)} />
            </label>
            <label className="labels">
                Surname
                <input 
                    type="text" 
                    name="surname" 
                    placeholder="surname"
                    onChange={e => setSurname(e.target.value)} />
            </label>
            <label className="labels">
                Email
                <input
                    type="text" 
                    name="email" 
                    placeholder="email"
                    onChange={e => setEmail(e.target.value)} />
            </label>
            <label className="labels">
                Password
                <input 
                    type="text" 
                    name="password" 
                    placeholder="password"
                    onChange={e => setPassword(e.target.value)} />
            </label>
            <label className="labels">
                Image
                <input type="file" name="book" onChange={uploadImage}/>
            </label>
            <label className="labels hidden">
                imageUrl
                <input
                    type="text" 
                    name="imageUrl" 
                    value={imageUrl}
                    onChange={e => setImageUrl(e.target.value)} />
            </label>
            <label className="labels hidden">
                publicId
                <input
                    type="text" 
                    name="publicId" 
                    value={publicId}
                    onChange={e => setPublicId(e.target.value)} />
            </label>
            <input type="submit" value="Submit" className="primary-submit-button" />
        </form>
    </div>
    )
};

export default AddUser;
lang-javascript
Now we can login and signup, but a user should be able to see his account as well as update it. So create a showUser.js file and updateUser.js file

showUser.js

import { useState, useEffect } from "react";
import { useNavigate } from 'react-router-dom';
import { config } from '../config/config';
import "./show-user.css"

const URL = config.url;

const ShowUser = () => {
    const [user, setUser] = useState('');
    const navigate = useNavigate();

    useEffect(() => {
        const id = localStorage.getItem('id');

        fetch(`${URL}/user/show/${id}`, {
            method: 'GET',
            })
            .then((response) => response.json())
            .then((data) => {
                setUser(data);
            })
            .catch((err) => {
                console.log(err.message);
            });
        },
        []);
        
        const deleteUser = async (id, public_id) => {
            console.log("delete:",id)
            console.log("delete:",public_id)

            fetch(`${URL}/user/delete/${id}/${public_id}`, {
            method: 'DELETE',
            }).then((response) => {            
                if (response.status === 200) {
                    setUser();
                    console.log("User deleted");
                    } else {
                        return;
                    }
                });
                navigate('/home');
            };

        const updateUser = (id) => {
                navigate(`/user/update/${id}`);
            };

    return (
        <div className="show-user-container">
            <div className="">
                    <div className="" >
                        <div className="show-user-image-container">  
                            <img src={user.imageUrl} style={{width: 400}} alt="" className="show-user-image"/>
                        </div>
                        <h1>{user.name} {user.surname}</h1>
                        <p>{user.email}</p>
                        <div className="user-button-area">
                            <div className="update-button button" onClick={() => updateUser(user._id)} >Update</div>
                            <div className="delete-button button" onClick={() => deleteUser(user._id, user.public_id)} id={user.id}>Delete</div>
                        </div>
                    </div>
            </div>
        </div>
    );
}

export default ShowUser;
lang-javascript
and updateUser.js

import UpdateUserForm from "../components/updateUserForm";

const UpdateUser = () => {

    return (
        <div className="">
            <div className="">
                Update your account
                <UpdateUserForm />
            </div>
        </div>
    );
}

export default UpdateUser;
lang-javascript
Lets create the update user form in components

updateUserForm.js

import { useNavigate, useParams } from 'react-router-dom';
import { useEffect, useState } from "react";
import {Image} from 'cloudinary-react';
import { config } from '../config/config';
import "./book-form.css";

import Cookies from "universal-cookie";
const cookies = new Cookies();

const URL = config.url;

const UpdateUserForm = () => {
    const [name, setName ] = useState('');
    const [surname, setSurname ] = useState('');
    const [ email, setEmail ] = useState('');
    const [imageUrl, setImageUrl] = useState('');
    const [publicId, setPublicId] = useState('');
    const navigate = useNavigate();
    const params = useParams();

    useEffect(() => {
        const id = params.id;
        fetch(`${URL}/user/show/${id}`, {
            method: 'GET',
            }).then((response) => response.json())
            .then((data) => {
                setName(data.name);
                setSurname(data.surname);
                setEmail(data.email);
                setImageUrl(data.imageUrl);
                setPublicId(data.public_id);
            })
            .catch((err) => {
                console.log(err.message);
            });
        },
        []);

    const cloudinaryUsername = process.env.REACT_APP_CLOUDINARY_USERNAME

    const cloudinaryPreset = process.env.REACT_APP_CLOUDINARY_PRESET

    const uploadUrl = `https://api.cloudinary.com/v1_1/${cloudinaryUsername}/image/upload`

    const uploadImage = async (files) => {
        const formData = new FormData()
        formData.append("file", files.target.files[0])
        formData.append("upload_preset", `${cloudinaryPreset}`)

        await fetch(uploadUrl, {
            method: 'POST',
            body: formData
            })
            .then(async (response) => {
            const data = await response.json();
            setImageUrl(data.secure_url)
            setPublicId(data.public_id)
            })            
        };

    const updateUser = async (id, name, surname, email, imageUrl, publicId) => {
        const token = cookies.get("TOKEN");

        await fetch(`${URL}/user/update/${id}`, {
            method: 'PUT',
            body: JSON.stringify({
                name: name,
                surname: surname,
                email: email,
                imageUrl: imageUrl,
                publicId: publicId
            }),
            headers: {
                'Content-type': 'application/json; charset=UTF-8',
                'Authorization': `${token}`,
            },
            })
            .then((response) => { 
                response.json();
            })
            .then(() => {
            setName();
            setSurname();
            setEmail();
            setImageUrl();
            setPublicId();
            })
            .catch((err) => {
            console.log(err.message , ":error message");
        });
    }

    const handleSubmit = () => {
        const id = params.id
        updateUser(id, name, surname, email, imageUrl, publicId );
        navigate(`/user/show/${id}`);
        
    };
    

    return (
        <div>
        <div className="form-user-image-container">
            <Image className="new-user-image" cloudName={cloudinaryUsername} publicId={imageUrl} />
        </div>
        <form method="puts" onSubmit={handleSubmit} enctype="multipart/form-data">
            <label className="labels">
                Name
                <input 
                    type="text" 
                    name="name" 
                    placeholder="name"
                    value={name}
                    onChange={e => setName(e.target.value)} />
            </label>
            <label className="labels">
                Surname
                <input 
                    type="text" 
                    name="surname" 
                    placeholder="surname"
                    value={surname}
                    onChange={e => setSurname(e.target.value)} />
            </label>
            <label className="labels">
                Email
                <input
                    type="text" 
                    name="email" 
                    placeholder="email"
                    value={email}
                    onChange={e => setEmail(e.target.value)} />
            </label>
            {/* <label className="labels">
                Password
                <input 
                    type="text" 
                    name="password" 
                    placeholder="password"
                    onChange={e => setPassword(e.target.value)} />
            </label> */}
            <label className="labels">
                Image
                <input type="file" name="book" onChange={uploadImage}/>
            </label>
            <label className="labels hidden">
                imageUrl
                <input
                    type="text" 
                    name="imageUrl" 
                    value={imageUrl}
                    onChange={e => setImageUrl(e.target.value)} />
            </label>
            <label className="labels hidden">
                publicId
                <input
                    type="text" 
                    name="publicId" 
                    value={publicId}
                    onChange={e => setPublicId(e.target.value)} />
            </label>
            <input type="submit" value="Submit" className="primary-submit-button" />
        </form>
    </div>
    )
};

export default UpdateUserForm;
lang-javascript
And that is our frontend UI along with its functionality all setup.

Your app should allow you to signup, login, create a book entry, update a book entry, delete a book entry, protect your entries from other users updating or deleting books that belong to you.

You can contextualise and add onto this app to suit your own purposes.

Hope this helps!
Ilia