Create a Rest API in PHP and Consume it in React Part 4
In the fourth part of this tutorial, we are going to create the frontend using react and consume the rest API we have done in the previous lessons, so I assume that you have already a fresh new react app.
Packages we need
all the packages you need are in the JSON file below.
{
"name": "react_php_api",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"axios": "^1.4.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.14.2",
"react-scripts": "5.0.1",
"react-toastify": "^9.1.3",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
Header component
The structure of our application will be like this:
>src
>components
>layouts
>pages
>user
>helpers
Inside components/layouts, we add a new file Header.js here we have the navigation menu.
import axios from 'axios';
import React, { useContext } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { toast } from 'react-toastify';
import { AuthContext } from '../../App';
import { BASE_URL } from '../../helpers/URL';
export default function Header() {
const { loggedInUser, setLoggedInUser } = useContext(AuthContext);
const navigate = useNavigate();
const logout = async () => {
const config = {
headers: {
"X-API-KEY": `${loggedInUser.api_key}`
}
}
try {
const response = await axios.post(`${BASE_URL}/logout`,
{user_id: loggedInUser.id}, config);
toast.success(response.data.message, {
position: toast.POSITION.TOP_RIGHT
});
localStorage.removeItem('user');
setLoggedInUser(null);
navigate('/login');
} catch (error) {
toast.error('Something went wrong try again later', {
position: toast.POSITION.TOP_RIGHT
});
console.log(error);
}
}
return (
<nav className="navbar navbar-expand-lg navbar-light bg-light">
<div className="container-fluid">
<Link className="navbar-brand" to="/">Events App</Link>
<button className="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span className="navbar-toggler-icon"></span>
</button>
<div className="collapse navbar-collapse" id="navbarSupportedContent">
<ul className="navbar-nav me-auto mb-2 mb-lg-0">
<li className="nav-item">
<Link className="nav-link active" aria-current="page"
to="/">Home</Link>
</li>
{
loggedInUser ?
<>
<li className="nav-item">
<Link className="nav-link" to="#">
{ loggedInUser.name }
</Link>
</li>
<li className="nav-item">
<Link className="nav-link" to="#"
onClick={() => logout()}>
Logout
</Link>
</li>
</>
:
<>
<li className="nav-item">
<Link className="nav-link" to="/register">Register</Link>
</li>
<li className="nav-item">
<Link className="nav-link" to="/login">Login</Link>
</li>
</>
}
</ul>
</div>
</div>
</nav>
)
}
Home Component
Inside components/pages, we add a new file Home.js here we fetch all events, events by category, and all categories.
import axios from "axios";
import { useEffect, useState } from "react";
import { BASE_URL } from "../../helpers/URL";
function Home() {
const [events, setEvents] = useState([]);
const [categories, setCategories] = useState([]);
const [selectedCategory, setSelectedCategory] = useState('');
useEffect(() => {
fetchCategories();
fetchEvents();
}, [selectedCategory])
const fetchEvents = async () => {
try {
if(selectedCategory) {
const response = await axios.get(`${BASE_URL}/events/category/${selectedCategory}`);
setEvents(response.data.events);
}else {
const response = await axios.get(`${BASE_URL}`);
setEvents(response.data.events);
}
} catch (error) {
console.log(error);
}
}
const fetchCategories = async () => {
try {
const response = await axios.get(`${BASE_URL}/categories`);
setCategories(response.data.categories);
} catch (error) {
console.log(error);
}
}
return (
<div className="container">
<div className="row my-5">
<ul className="nav justify-content-center my-4">
<li className="nav-item">
<a className={`nav-link fw-bold ${selectedCategory === '' ? 'active' : 'text-dark'}`}
onClick={() => setSelectedCategory('')}
href="#">
All
</a>
</li>
{
categories?.map(category => (
<li className="nav-item" key={category.id}>
<a className={`nav-link fw-bold ${selectedCategory !== '' && selectedCategory === category.id ? 'active' : 'text-dark'}`}
onClick={() => setSelectedCategory(category.id)}
href="#">
{category.name}
</a>
</li>
))
}
</ul>
{
events?.map(event => (
<div className="col-md-4" key={event.id}>
<div className="card" style={{border: 'dashed'}}>
<div className="card-body">
<h5 className="card-title">{event.title}</h5>
<p className="card-text">{event.description}</p>
<span className="badge bg-dark">tickets: {event.tickets_availlable}</span>
</div>
<div className="card-footer bg-white d-flex justify-content-between">
<span className="badge bg-primary">{event.event_address}</span>
<span className="badge bg-danger">{event.event_date}</span>
</div>
</div>
</div>
))
}
</div>
</div>
);
}
export default Home;
Register Component
Inside components/pages/user, we add a new file Register.js here we have the registration form.
import axios from 'axios';
import React, { useContext, useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { toast } from 'react-toastify';
import { AuthContext } from '../../../App';
import { BASE_URL } from '../../../helpers/URL';
export default function Register() {
const [user, setUser] = useState({
name: '',
email: '',
password: ''
});
const [loading, setLoading] = useState(false);
const navigate = useNavigate();
const { loggedInUser } = useContext(AuthContext);
useEffect(() => {
if(loggedInUser) {
navigate('/');
}
}, [loggedInUser]);
const registerUser = async (e) => {
e.preventDefault();
setLoading(true);
try {
const response = await axios.post(`${BASE_URL}/register`,
user);
setLoading(false);
if(!response.data.error) {
toast.success(response.data.message, {
position: toast.POSITION.TOP_RIGHT
});
navigate('/login');
}else {
toast.error(response.data.message, {
position: toast.POSITION.TOP_RIGHT
});
}
} catch (error) {
setLoading(false);
console.log(error);
}
}
return (
<div className='container'>
<div className="row my-5 mb-5">
<div className="col-md-6 mx-auto">
<div className="card">
<div className="card-header">
<h5 className="text-cenret mt-2">
Register
</h5>
</div>
<div className="card-body">
<form className='mt-5' onSubmit={(e) => registerUser(e)}>
<div className="mb-3">
<label htmlFor="name"
className='form-label'>Name*</label>
<input type="text" id="name"
onChange={(e) => setUser({
...user, name: e.target.value
})}
required
className="form-control" />
</div>
<div className="mb-3">
<label htmlFor="email"
className='form-label'>Email*</label>
<input type="email" id="email"
onChange={(e) => setUser({
...user, email: e.target.value
})}
required
className="form-control" />
</div>
<div className="mb-3">
<label htmlFor="password"
className='form-label'>Password*</label>
<input type="password" id="password"
onChange={(e) => setUser({
...user, password: e.target.value
})}
required
minLength={8}
className="form-control" />
</div>
<div className="mb-3">
{
loading ?
<div className="spinner-border" role="status">
<span className="visually-hidden">Loading...</span>
</div>
:
<button type="submit"
className='btn btn-primary'>
Submit
</button>
}
</div>
</form>
</div>
</div>
</div>
</div>
</div>
)
}
Login Component
Inside components/pages/user, we add a new file Login.js here we have the login form.
import axios from 'axios';
import React, { useContext, useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { toast } from 'react-toastify';
import { AuthContext } from '../../../App';
import { BASE_URL } from '../../../helpers/URL';
export default function Login() {
const [user, setUser] = useState({
email: '',
password: ''
});
const { loggedInUser, setLoggedInUser } = useContext(AuthContext);
const [loading, setLoading] = useState(false);
const navigate = useNavigate();
useEffect(() => {
if(loggedInUser) {
navigate('/');
}
}, [loggedInUser]);
const loginUser = async (e) => {
e.preventDefault();
setLoading(true);
try {
const response = await axios.post(`${BASE_URL}/login`,
user);
setLoading(false);
if(!response.data.error) {
toast.success(response.data.message, {
position: toast.POSITION.TOP_RIGHT
});
localStorage.setItem('user', JSON.stringify(response.data.user));
setLoggedInUser(response.data.user);
navigate('/');
}else {
toast.error(response.data.message, {
position: toast.POSITION.TOP_RIGHT
});
}
} catch (error) {
setLoading(false);
console.log(error);
}
}
return (
<div className='container'>
<div className="row my-5 mb-5">
<div className="col-md-6 mx-auto">
<div className="card">
<div className="card-header">
<h5 className="text-cenret mt-2">
Login
</h5>
</div>
<div className="card-body">
<form className='mt-5' onSubmit={(e) => loginUser(e)}>
<div className="mb-3">
<label htmlFor="email"
className='form-label'>Email*</label>
<input type="email" id="email"
onChange={(e) => setUser({
...user, email: e.target.value
})}
required
className="form-control" />
</div>
<div className="mb-3">
<label htmlFor="password"
className='form-label'>Password*</label>
<input type="password" id="password"
onChange={(e) => setUser({
...user, password: e.target.value
})}
required
minLength={8}
className="form-control" />
</div>
<div className="mb-3">
{
loading ?
<div className="spinner-border" role="status">
<span className="visually-hidden">Loading...</span>
</div>
:
<button type="submit"
className='btn btn-primary'>
Submit
</button>
}
</div>
</form>
</div>
</div>
</div>
</div>
</div>
)
}