added frontent authentication,backend ticket service, frontend ticket creation
This commit is contained in:
@@ -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;
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -0,0 +1,8 @@
|
||||
function Spinner() {
|
||||
return (
|
||||
<div className="=loadingSpinnerContainer">
|
||||
<div className="loadingSpinner"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default Spinner;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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}
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user