added frontent authentication,backend ticket service, frontend ticket creation

This commit is contained in:
QkoSad
2022-12-10 11:15:53 +02:00
parent 8fd0b86492
commit 885cdfa7fe
34 changed files with 31084 additions and 1 deletions
+36
View File
@@ -0,0 +1,36 @@
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import { ToastContainer } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
import Header from "./components/Header";
import PrivateRoute from "./components/PrivateRoute";
import Home from "./pages/Home";
import Login from "./pages/Login";
import Register from "./pages/Register";
import NewTicket from "./pages/NewTicket";
import Tickets from "./pages/Ticket";
function App() {
return (
<>
<Router>
<div className="container">
<Header />
<Routes>
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route path="/new-ticket" element={<PrivateRoute />}>
<Route path="/new-ticket" element={<NewTicket />} />
</Route>
<Route path="/tickets" element={<PrivateRoute />}>
<Route path="/tickets" element={<Tickets />} />
</Route>
</Routes>
</div>
</Router>
<ToastContainer />
</>
);
}
export default App;
+10
View File
@@ -0,0 +1,10 @@
import { configureStore } from "@reduxjs/toolkit";
import authSlice from "../features/auth/authSlice";
import ticketSlice from "../features/tickets/ticketSlice";
export const store = configureStore({
reducer: {
auth: authSlice,
tickets: ticketSlice,
},
});
+11
View File
@@ -0,0 +1,11 @@
import { FaArrowCircleLeft } from "react-icons/fa";
import { Link } from "react-router-dom";
export const BackButton = ({ url }) => {
return (
<Link to={url} className="btn btn-reverse btn-back">
<FaArrowCircleLeft />
Back
</Link>
);
};
+47
View File
@@ -0,0 +1,47 @@
import { FaSignInAlt, FaSignOutAlt, FaUser } from "react-icons/fa";
import { Link, useNavigate } from "react-router-dom";
import { useSelector, useDispatch } from "react-redux";
import { reset, logout } from "../features/auth/authSlice";
function Header() {
const navigate = useNavigate();
const dispatch = useDispatch();
const { user } = useSelector((state) => state.auth);
const onLogout = () => {
dispatch(logout());
dispatch(reset());
navigate("/");
};
return (
<header className="header">
<div className="logo">
<Link to="/">Support Desk</Link>
</div>
<ul>
{user ? (
<li>
<button onClick={onLogout} className="btn">
<FaSignOutAlt />
Logout
</button>
</li>
) : (
<>
<li>
<Link to="login">
<FaSignInAlt /> Login
</Link>
</li>
<li>
<Link to="register">
<FaUser /> Register
</Link>
</li>
</>
)}
</ul>
</header>
);
}
export default Header;
+13
View File
@@ -0,0 +1,13 @@
import { Navigate, Outlet } from "react-router-dom";
import { useAuthStatus } from "../hooks/useAuthStatus";
import Spinner from "./Spinner";
function PrivateRoute() {
const { loggedIn, checkingStatus } = useAuthStatus();
if (checkingStatus) {
return <Spinner />;
}
return loggedIn ? <Outlet /> : <Navigate to="/login" />;
}
export default PrivateRoute;
+8
View File
@@ -0,0 +1,8 @@
function Spinner() {
return (
<div className="=loadingSpinnerContainer">
<div className="loadingSpinner"></div>
</div>
);
}
export default Spinner;
+28
View File
@@ -0,0 +1,28 @@
import axios from "axios";
const API_URL = "/api/users";
const register = async (userData) => {
const response = await axios.post(API_URL, userData);
if (response.data) {
localStorage.setItem("user", JSON.stringify(response.data));
}
return response.data;
};
const login = async (userData) => {
const response = await axios.post(`${API_URL}/login`, userData);
if (response.data) {
localStorage.setItem("user", JSON.stringify(response.data));
}
return response.data;
}
const logout = ()=> localStorage.removeItem('user')
const authService = {
register,
logout,
login,
};
export default authService;
+94
View File
@@ -0,0 +1,94 @@
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import authService from "./authService";
const user = JSON.parse(localStorage.getItem("user"));
const initialState = {
user: user ? user : null,
isError: false,
isSuccess: false,
isLoading: false,
message: "",
};
export const register = createAsyncThunk(
"auth/register",
async (user, thunkAPI) => {
try {
return await authService.register(user);
} catch (error) {
const message =
(error.response &&
error.response.data &&
error.response.data.message) ||
error.message ||
error.toString();
return thunkAPI.rejectWithValue(message);
}
}
);
export const login = createAsyncThunk("auth/login", async (user, thunkAPI) => {
try {
return await authService.login(user);
} catch (error) {
const message =
(error.response && error.response.data && error.response.data.message) ||
error.message ||
error.toString();
return thunkAPI.rejectWithValue(message);
}
});
export const logout = createAsyncThunk("auth/logout", async () => {
return await authService.logout();
});
export const authSlice = createSlice({
name: "auth",
initialState,
reducers: {
reset: (state) => {
state.isLoading = false;
state.isError = false;
state.isSuccess = false;
state.message = "";
},
},
extraReducers: (builder) => {
builder
.addCase(register.pending, (state) => {
state.isLoading = true;
})
.addCase(register.fulfilled, (state, action) => {
state.isLoading = false;
state.isSuccess = true;
state.user = action.payload;
})
.addCase(register.rejected, (state, action) => {
state.isLoading = false;
state.isError = true;
state.message = action.payload;
state.user = null;
})
.addCase(logout.fulfilled, (state) => {
state.user = null;
})
.addCase(login.pending, (state) => {
state.isLoading = true;
})
.addCase(login.fulfilled, (state, action) => {
state.isLoading = false;
state.isSuccess = true;
state.user = action.payload;
})
.addCase(login.rejected, (state, action) => {
state.isLoading = false;
state.isError = true;
state.message = action.payload;
state.user = null;
})
},
});
export const { reset } = authSlice.actions;
export default authSlice.reducer;
@@ -0,0 +1,30 @@
import axios from "axios";
const API_URL = "/api/tickets/";
const createTicket = async (ticketData, token) => {
const config = {
headers: {
Authorization: `Bearer ${token}`,
},
};
const response = await axios.post(API_URL, ticketData, config);
return response.data;
};
const getTickets = async (token) => {
const config = {
headers: {
Authorization: `Bearer ${token}`,
},
};
const response = await axios.get(API_URL, config);
return response.data;
};
const ticketService = { createTicket, getTickets };
export default ticketService;
@@ -0,0 +1,87 @@
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import ticketService from "./ticketService";
const initialState = {
tickets: [],
ticket: {},
isError: false,
isSuccess: false,
isLoading: false,
message: "",
};
export const ticketSlice = createSlice({
name: "ticket",
initialState,
reducers: {
reset: (state) => initialState,
},
extraReducers: (builder) => {
builder
.addCase(createTicket.pending, (state) => {
state.isLoading = true;
})
.addCase(createTicket.fulfilled, (state) => {
state.isLoading = false;
state.isSuccess = true;
})
.addCase(createTicket.rejected, (state, action) => {
state.isLoading = false;
state.isError = true;
state.message = action.payload;
})
.addCase(getTickets.pending, (state) => {
state.isLoading = true;
})
.addCase(getTickets.fulfilled, (state, action) => {
state.isLoading = false;
state.isSuccess = true;
state.tickets = action.payload;
})
.addCase(getTickets.rejected, (state, action) => {
state.isLoading = false;
state.isError = true;
state.message = action.payload;
});
},
});
export const getTickets = createAsyncThunk(
"tickets/getAll",
async (_, thunkAPI) => {
try {
const token = thunkAPI.getState().auth.user.token;
return await ticketService.getTickets(token);
} catch (error) {
const message =
(error.response &&
error.response.data &&
error.response.data.message) ||
error.message ||
error.toString();
return thunkAPI.rejectWithValue(message);
}
}
);
export const createTicket = createAsyncThunk(
"tickets/create",
async (ticketData, thunkAPI) => {
try {
const token = thunkAPI.getState().auth.user.token;
return await ticketService(ticketData, token);
} catch (error) {
const message =
(error.response &&
error.response.data &&
error.response.data.message) ||
error.message ||
error.toString();
return thunkAPI.rejectWithValue(message);
}
}
);
export const { reset } = ticketSlice.actions;
export default ticketSlice.reducer;
+21
View File
@@ -0,0 +1,21 @@
import { useState, useEffect } from "react";
import { useSelector } from "react-redux";
export const useAuthStatus = () => {
const [loggedIn, setLoggedin] = useState(false);
const [checkingStatus, setChekcingStatus] = useState(true);
const { user } = useSelector((state) => state.auth);
useEffect(()=>{
if(user){
setLoggedin(true)
}
else{
setLoggedin(false)
}
setChekcingStatus(false)
},[user])
return {loggedIn,checkingStatus}
};
+406
View File
@@ -0,0 +1,406 @@
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600;700&display=swap');
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Poppins', sans-serif;
height: 100vh;
}
a {
text-decoration: none;
color: #000;
}
p {
line-height: 1.7;
}
ul {
list-style: none;
}
li {
line-height: 2.2;
}
h1,
h2,
h3 {
font-weight: 600;
margin-bottom: 10px;
}
.container {
width: 100%;
max-width: 960px;
margin: 0 auto;
padding: 0 20px;
text-align: center;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 0;
border-bottom: 1px solid #e6e6e6;
margin-bottom: 60px;
}
.header ul {
display: flex;
align-items: center;
justify-content: space-between;
}
.header ul li {
margin-left: 20px;
}
.header ul li a {
display: flex;
align-items: center;
}
.header ul li a:hover {
color: #777;
}
.header ul li a svg {
margin-right: 5px;
}
.heading {
font-size: 2rem;
font-weight: 700;
margin-bottom: 50px;
padding: 0 20px;
}
.heading p {
color: #828282;
}
.boxes {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
justify-content: space-between;
align-items: center;
text-align: center;
margin-bottom: 30px;
}
.boxes div {
padding: 30px;
border: 1px solid #e6e6e6;
border-radius: 10px;
}
.boxes h2 {
margin-top: 20px;
}
.boxes a:hover {
color: #777;
}
.form {
width: 70%;
margin: 0 auto;
}
.form-group {
margin-bottom: 10px;
}
.form-group input,
.form-group textarea,
.form-group select {
width: 100%;
padding: 10px;
border: 1px solid #e6e6e6;
border-radius: 5px;
margin-bottom: 10px;
font-family: inherit;
}
.form-group label {
text-align: left;
display: block;
margin: 0 0 5px 3px;
}
.btn {
padding: 10px 20px;
border: 1px solid #000;
border-radius: 5px;
background: #000;
color: #fff;
font-size: 16px;
font-weight: 700;
cursor: pointer;
text-align: center;
appearance: button;
display: flex;
align-items: center;
justify-content: center;
}
.btn svg {
margin-right: 8px;
}
.btn-reverse {
background: #fff;
color: #000;
}
.btn-block {
width: 100%;
margin-bottom: 20px;
}
.btn-sm {
padding: 5px 15px;
font-size: 13px;
}
.btn-danger {
background: darkred;
border: none;
}
.btn-back {
width: 150px;
margin-bottom: 20px;
}
.btn:hover {
transform: scale(0.98);
}
.ticket-created {
border: 1px solid #e6e6e6;
border-radius: 5px;
padding: 50px;
}
.ticket-number {
margin-bottom: 30px;
}
.ticket-number h2 {
font-size: 2.3rem;
margin-bottom: 10px;
}
.ticket-number p {
font-size: 1.3rem;
}
.ticket-info {
font-size: 1.3rem;
}
.ticket,
.ticket-headings {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
background: #f4f4f4;
padding: 10px 15px;
border-radius: 5px;
text-align: center;
}
.ticket-headings {
font-weight: 700;
}
.status {
background-color: #333;
color: #fff;
width: 100px;
padding: 0 20px;
justify-self: center;
border-radius: 10px;
font-size: 16px;
text-align: center;
}
.status-new {
background-color: green;
color: #fff;
border-radius: 10px;
}
.status-open {
background-color: steelblue;
color: #fff;
border-radius: 10px;
}
.status-closed {
background-color: darkred;
color: #fff;
border-radius: 10px;
}
.ticket-page {
position: relative;
text-align: left;
}
.ticket-page h2 {
display: flex;
align-items: center;
justify-content: space-between;
}
.ticket-page .btn {
margin-bottom: 30px;
}
.ticket-page .btn-block {
width: 100%;
margin-top: 30px;
}
.ticket-desc {
margin: 20px 0;
font-size: 17px;
background-color: #f4f4f4;
border: 1px #ccc solid;
padding: 10px 15px;
border-radius: 5px;
}
.note {
border: 1px solid #e6e6e6;
border-radius: 5px;
text-align: left;
padding: 20px;
margin-bottom: 20px;
position: relative;
}
.note-head {
background: #f4f4f4;
padding: 5px 20px;
display: lex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.note-date {
position: absolute;
top: 15px;
right: 10px;
font-size: 14px;
}
.delete-note {
color: red;
cursor: pointer;
position: absolute;
bottom: 10px;
right: 20px;
}
.btn-close {
background: none;
border: none;
color: #000;
position: absolute;
top: 5px;
right: 5px;
font-size: 16px;
cursor: pointer;
}
.btn-close:hover {
color: red;
transform: scale(0.98);
}
p.status-in-progress {
color: orangered;
}
p.status-waiting {
color: red;
}
p.status-ready {
color: steelblue;
}
p.status-complete {
color: green;
}
footer {
position: sticky;
top: 95vh;
text-align: center;
}
.loadingSpinnerContainer {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 5000;
display: flex;
justify-content: center;
align-items: center;
}
.loadingSpinner {
width: 64px;
height: 64px;
border: 8px solid;
border-color: #000 transparent #555 transparent;
border-radius: 50%;
animation: spin 1.2s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
@media (max-width: 600px) {
.boxes {
grid-template-columns: 1fr;
}
.form {
width: 90%;
}
.ticket-created h2,
.heading h1 {
font-size: 2rem;
}
.heading p {
font-size: 1.5rem;
}
}
+23
View File
@@ -0,0 +1,23 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import { Provider } from 'react-redux';
import { store } from './app/store';
import App from './App';
import reportWebVitals from './reportWebVitals';
import './index.css';
const container = document.getElementById('root');
const root = createRoot(container);
root.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
+20
View File
@@ -0,0 +1,20 @@
import { Link } from "react-router-dom";
import { FaQuestionCircle, FaTicketAlt } from "react-icons/fa";
function Home() {
return (
<>
<section className="heading">
<h1>What do you need help with?</h1>
<p>Please choose from an options below</p>
</section>
<Link to="/new-ticket" className="btn btn-reverse btn-block">
<FaQuestionCircle /> Create a new ticket
</Link>
<Link to="/tickets" className="btn btn-block">
<FaTicketAlt /> View my tickets
</Link>
</>
);
}
export default Home;
+91
View File
@@ -0,0 +1,91 @@
import { useState } from "react";
import { toast } from "react-toastify";
import { FaSignInAlt } from "react-icons/fa";
import { useDispatch, useSelector } from "react-redux";
import { useNavigate } from "react-router-dom";
import { login, reset } from "../features/auth/authSlice";
import Spinner from "../components/Spinner";
import { useEffect } from "react";
function Login() {
const [formData, setFormData] = useState({
email: "",
password: "",
});
const dispatch = useDispatch();
const navigate = useNavigate();
const { email, password } = formData;
const { user, isLoading, isError, isSuccess, message } = useSelector(
(state) => state.auth
);
useEffect(() => {
if (isError) {
toast.error(message);
}
if (isSuccess || user) {
navigate("/");
}
dispatch(reset);
}, [isError, isSuccess, user, message, navigate, dispatch]);
const onChange = (e) => {
setFormData((prevState) => ({
...prevState,
[e.target.name]: e.target.value,
}));
};
const onSubmit = (e) => {
e.preventDefault();
const userData = {
email,
password,
};
dispatch(login(userData));
navigate("/");
};
if (isLoading) {
return <Spinner />;
}
return (
<>
<section className="heading">
<h1>
<FaSignInAlt /> Login
</h1>
<p>Please login to get support</p>
</section>
<section className="form">
<form onSubmit={onSubmit}>
<div className="form-group">
<input
className="form-control"
type="email"
value={email}
id="email"
onChange={onChange}
placeholder="Enter your email"
name="email"
required
/>
</div>
<div className="form-group">
<input
className="form-control"
type="password"
value={password}
id="password"
onChange={onChange}
placeholder="Enter your password"
name="password"
required
/>
</div>
<div className="form-group">
<button className="btn btn-block">Submit</button>
</div>
</form>
</section>
</>
);
}
export default Login;
+96
View File
@@ -0,0 +1,96 @@
import { useState, useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { useNavigate } from "react-router-dom";
import { toast } from "react-toastify";
import { createTicket, reset } from "../features/tickets/ticketSlice";
import Spinner from "../components/Spinner";
import { BackButton } from "../components/BackButton";
function NewTicket() {
const { user } = useSelector((state) => state.auth);
const { isLoading, isError, isSuccess, message } = useSelector(
(state) => state.ticket
);
const [name, setName] = useState(user.name);
const [email, setEmail] = useState(user.email);
const [product, setProduct] = useState("");
const [description, setDescrtiption] = useState("");
const dispatch = useDispatch();
const navigate = useNavigate();
useEffect(()=>{
if(isError) toast.error(message)
if(isSuccess){
dispatch(reset())
navigate('/tickets')
}
dispatch(reset())
},[dispatch,isError,isSuccess,navigate,message])
const onSubmit = (e) => {
e.preventDefault();
dispatch(createTicket({product,description}))
};
if(isLoading) return <Spinner/>
return (
<>
<BackButton url='/' />
<section className="heading">
<h1>Create New Ticket</h1>
<p> Please fill out the form below</p>
<section className="form">
<div className="form-group">
<label htmlFor="name">Customer Name</label>
<input type="text" className="form-control" value={name} disabled />
</div>
<div className="form-group">
<label htmlFor="name">Customer Email</label>
<input
type="text"
className="form-control"
value={email}
disabled
/>
</div>
<form onSubmit={onSubmit}>
<div className="form-group">
<label htmlFor="product">Product</label>
<select
name="product"
id="product"
value={product}
onChange={(e) => setProduct(e.target.value)}
>
<option value="iPhone">iPhone</option>
<option value="Macbook Pro">Macbook Pro</option>
<option value="iMac">iMac</option>
<option value="iPad">iPad</option>
</select>
</div>
<div className="form-group">
<label htmlFor="description">Description of the issue</label>
<textarea
name="description"
id="description"
className="form-control"
placeholder="Description"
value={description}
onChange={(e) => setDescrtiption(e.target.value)}
/>
</div>
<div className="from-group">
<button className="btn btn-block">Submit</button>
</div>
</form>
</section>
</section>
</>
);
}
export default NewTicket;
+126
View File
@@ -0,0 +1,126 @@
import { useNavigate } from "react-router-dom";
import { toast } from "react-toastify";
import { useEffect, useState } from "react";
import { FaUser } from "react-icons/fa";
import { useSelector, useDispatch } from "react-redux";
import { reset, register } from "../features/auth/authSlice";
import Spinner from "../components/Spinner";
function Register() {
const [formData, setFormData] = useState({
name: "",
email: "",
password: "",
password2: "",
});
const { name, email, password, password2 } = formData;
const navigate = useNavigate();
const dispatch = useDispatch();
const { isError, user, isLoading, isSuccess, message } = useSelector(
(state) => state.auth
);
useEffect(() => {
if (isError) {
toast.error(message);
}
if (isSuccess || user) {
navigate("/");
}
dispatch(reset);
}, [isError, isSuccess, user, message, navigate, dispatch]);
const onChange = (e) => {
setFormData((prevState) => ({
...prevState,
[e.target.name]: e.target.value,
}));
};
const onSubmit = (e) => {
e.preventDefault();
if (password !== password2) {
toast.error("Passwords do not match");
} else {
const userData = {
name,
email,
password,
};
dispatch(register(userData));
}
};
if(isLoading){
return <Spinner />
}
return (
<>
<section className="heading">
<h1>
<FaUser />
Register
</h1>
<p>Please create an account</p>
</section>
<section className="form">
<form onSubmit={onSubmit}>
<div className="form-group">
<input
className="form-control"
type="text"
value={name}
id="name"
onChange={onChange}
placeholder="Enter your name"
name="name"
required
/>
</div>
<div className="form-group">
<input
className="form-control"
type="email"
value={email}
id="email"
onChange={onChange}
placeholder="Enter your email"
name="email"
required
/>
</div>
<div className="form-group">
<input
className="form-control"
type="password"
value={password}
id="password"
onChange={onChange}
placeholder="Enter your password"
name="password"
required
/>
</div>
<div className="form-group">
<input
className="form-control"
type="password"
value={password2}
id="password2"
onChange={onChange}
placeholder="Enter your password again"
name="password2"
required
/>
</div>
<div className="form-group">
<button className=" btn btn-block">Submit</button>
</div>
</form>
</section>
</>
);
}
export default Register;
+28
View File
@@ -0,0 +1,28 @@
import { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { getTickets, reset } from "../features/tickets/ticketSlice";
import Spinner from "../components/Spinner";
import { BackButton } from "../components/BackButton";
function Tickets() {
const { tickets, isLoading, isSuccess } = useSelector(
(state) => state.tickets
);
const dispatch = useDispatch();
useEffect(() => {
return () => {
if (isSuccess) {
dispatch(reset());
}
};
}, [dispatch, isSuccess]);
useEffect(() => {
dispatch(getTickets());
}, [dispatch]);
if (isLoading) {
return <Spinner />;
}
return <h1>Head</h1>;
}
export default Tickets;
+13
View File
@@ -0,0 +1,13 @@
const reportWebVitals = onPerfEntry => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;