#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
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
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
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
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
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
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
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
Hope this helps!
Ilia