Skip to content
Snippets Groups Projects
Commit 69edc8e0 authored by Sepide Jamshididana's avatar Sepide Jamshididana
Browse files

Initial commit

parents
No related branches found
No related tags found
No related merge requests found
Showing with 596 additions and 0 deletions
node_modules
\ No newline at end of file
# User Stories for techNotes
1. [ ] Replace current sticky note system
2. [ ] Add a public facing page with basic contact info
3. [ ] Add an employee login to the notes app
4. [ ] Provide a welcome page after login
5. [ ] Provide easy navigation
6. [ ] Display current user and assigned role
7. [ ] Provide a logout option
8. [ ] Require users to login at least once per week
9. [ ] Provide a way to remove employee access asap if needed
10. [ ] Notes are assigned to specific employees
11. [ ] Notes have a ticket #, title, note body, created & updated dates
12. [ ] Notes are either OPEN or COMPLETED
13. [ ] Users can be Employees, Managers, or Admins
14. [ ] Notes can only be deleted by Managers or Admins
15. [ ] Anyone can create a note (when customer checks-in)
16. [ ] Employees can only view and edit their assigned notes
17. [ ] Managers and Admins can view, edit, and delete all notes
18. [ ] Only Managers and Admins can access User Settings
19. [ ] Only Managers and Admins can create new users
20. [ ] Desktop mode is most important but should be available in mobile
\ No newline at end of file
This diff is collapsed.
{
"name": "todo-app-client",
"version": "0.1.0",
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^6.1.2",
"@fortawesome/free-solid-svg-icons": "^6.1.2",
"@fortawesome/react-fontawesome": "^0.2.0",
"@reduxjs/toolkit": "^1.9.5",
"@types/jwt-decode": "^3.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-redux": "^8.1.2",
"react-router-dom": "^6.3.0",
"react-scripts": "^5.0.1",
"validator": "^13.11.0"
},
"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"
]
}
}
public/favicon.ico

3.78 KiB

public/img/background.jpg

3.36 MiB

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Todo-app Api</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
\ No newline at end of file
public/logo192.png

5.22 KiB

public/logo512.png

9.44 KiB

{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
\ No newline at end of file
User-agent: *
Disallow:
\ No newline at end of file
import { Routes, Route } from "react-router-dom";
import Layout from "./components/Layout";
import Public from "./components/Public";
import Login from "./features/auth/Login";
import DashLayout from "./components/DashLayout";
import Welcome from "./features/auth/Welcome";
import TodosList from "./features/todos/TodosList";
import UsersList from "./features/users/UsersList";
import EditUser from "./features/users/EditUser";
import NewUserForm from "./features/users/NewUserForm";
import EditTodo from "./features/todos/EditTodo";
import NewTodo from "./features/todos/NewTodo";
import Prefetch from "./features/auth/Prefetch";
import PersistLogin from "./features/auth/PersistLogin";
function App() {
return (
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Public />} />
<Route path="login" element={<Login />} />
<Route element={<PersistLogin />}>
<Route element={<Prefetch />}>
<Route path="dash" element={<DashLayout />}>
<Route index element={<Welcome />} />
<Route path="users">
<Route index element={<UsersList />} />
<Route path=":id" element={<EditUser />} />
<Route path="new" element={<NewUserForm />} />
</Route>
<Route path="todos">
<Route index element={<TodosList />} />
<Route path=":id" element={<EditTodo />} />
<Route path="new" element={<NewTodo />} />
</Route>
</Route>
{/* End Dash */}
</Route>
</Route>
</Route>
</Routes>
);
}
export default App;
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import { setCredentials } from '../../features/auth/authSlice'
const baseQuery = fetchBaseQuery({
baseUrl: 'http://localhost:3500', //https://www.todo-app.com
credentials: 'include',
prepareHeaders: (headers, { getState }) => {
const token = getState().auth.token
if (token) {
headers.set("authorization", `Bearer ${token}`)
}
return headers
}
})
const baseQueryWithReauth = async (args, api, extraOptions) => {
// console.log(args) // request url, method, body
// console.log(api) // signal, dispatch, getState()
// console.log(extraOptions) //custom like {shout: true}
let result = await baseQuery(args, api, extraOptions)
// If you want, handle other status codes, too
if (result?.error?.status === 403) {
console.log('sending refresh token')
// send refresh token to get new access token
const refreshResult = await baseQuery('/auth/refresh', api, extraOptions)
if (refreshResult?.data) {
// store the new token
api.dispatch(setCredentials({ ...refreshResult.data }))
// retry original query with new access token
result = await baseQuery(args, api, extraOptions)
} else {
if (refreshResult?.error?.status === 403) {
refreshResult.error.data.message = "Your login has expired. "
}
return refreshResult
}
}
return result
}
export const apiSlice = createApi({
baseQuery: baseQueryWithReauth,
tagTypes: ['Todo', 'User'],
endpoints: builder => ({})
})
\ No newline at end of file
import { configureStore } from "@reduxjs/toolkit";
import { apiSlice } from "./api/apiSlice";
import { setupListeners } from "@reduxjs/toolkit/query";
import authReducer from "../features/auth/authSlice";
export const store = configureStore({
reducer: {
[apiSlice.reducerPath]: apiSlice.reducer,
auth: authReducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(apiSlice.middleware),
devTools: true,
});
setupListeners(store.dispatch);
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faHouse } from "@fortawesome/free-solid-svg-icons";
import { useNavigate, useLocation } from "react-router-dom";
import useAuth from "../hooks/useAuth";
const DashFooter = () => {
const { email } = useAuth();
const navigate = useNavigate();
const { pathname } = useLocation();
const onGoHomeClicked = () => navigate("/dash");
let goHomeButton = null;
if (pathname !== "/dash") {
goHomeButton = (
<button
className="dash-footer__button icon-button"
title="Home"
onClick={onGoHomeClicked}
>
<FontAwesomeIcon icon={faHouse} />
</button>
);
}
const content = (
<footer className="dash-footer">
{goHomeButton}
<p>Current User: {email}</p>
</footer>
);
return content;
};
export default DashFooter;
import { useEffect } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faFileCirclePlus,
faFilePen,
faUserGear,
faUserPlus,
faRightFromBracket,
} from "@fortawesome/free-solid-svg-icons";
import { useNavigate, Link, useLocation } from "react-router-dom";
import { useSendLogoutMutation } from "../features/auth/authApiSlice";
const DASH_REGEX = /^\/dash(\/)?$/;
const NOTES_REGEX = /^\/dash\/todos(\/)?$/;
const USERS_REGEX = /^\/dash\/users(\/)?$/;
const DashHeader = () => {
const navigate = useNavigate();
const { pathname } = useLocation();
const [sendLogout, { isLoading, isSuccess, isError, error }] =
useSendLogoutMutation();
useEffect(() => {
if (isSuccess) navigate("/");
}, [isSuccess, navigate]);
const onNewTodoClicked = () => navigate("/dash/todos/new");
const onNewUserClicked = () => navigate("/dash/users/new");
const onTodosClicked = () => navigate("/dash/todos");
const onUsersClicked = () => navigate("/dash/users");
if (isLoading) return <p>Logging Out...</p>;
if (isError) return <p>Error: {error.data?.message}</p>;
let dashClass = null;
if (
!DASH_REGEX.test(pathname) &&
!NOTES_REGEX.test(pathname) &&
!USERS_REGEX.test(pathname)
) {
dashClass = "dash-header__container--small";
}
let newTodoButton = null;
if (NOTES_REGEX.test(pathname)) {
newTodoButton = (
<button
className="icon-button"
title="New Todo"
onClick={onNewTodoClicked}
>
<FontAwesomeIcon icon={faFileCirclePlus} />
</button>
);
}
let newUserButton = null;
if (USERS_REGEX.test(pathname)) {
newUserButton = (
<button
className="icon-button"
title="New User"
onClick={onNewUserClicked}
>
<FontAwesomeIcon icon={faUserPlus} />
</button>
);
}
let userButton = null;
if (!USERS_REGEX.test(pathname) && pathname.includes("/dash")) {
userButton = (
<button className="icon-button" title="Users" onClick={onUsersClicked}>
<FontAwesomeIcon icon={faUserGear} />
</button>
);
}
let todosButton = null;
if (!NOTES_REGEX.test(pathname) && pathname.includes("/dash")) {
todosButton = (
<button className="icon-button" title="Todos" onClick={onTodosClicked}>
<FontAwesomeIcon icon={faFilePen} />
</button>
);
}
const logoutButton = (
<button className="icon-button" title="Logout" onClick={sendLogout}>
<FontAwesomeIcon icon={faRightFromBracket} />
</button>
);
const errClass = isError ? "errmsg" : "offscreen";
let buttonContent;
if (isLoading) {
buttonContent = <p>Logging Out...</p>;
} else {
buttonContent = (
<>
{newTodoButton}
{newUserButton}
{todosButton}
{userButton}
{logoutButton}
</>
);
}
const content = (
<>
<p className={errClass}>{error?.data?.message}</p>
<header className="dash-header">
<div className={`dash-header__container ${dashClass}`}>
<Link to="/dash">
<h1 className="dash-header__title">TODO-APP API</h1>
</Link>
<nav className="dash-header__nav">{buttonContent}</nav>
</div>
</header>
</>
);
return content;
};
export default DashHeader;
import { Outlet } from "react-router-dom";
import DashHeader from "./DashHeader";
import DashFooter from "./DashFooter";
const DashLayout = () => {
return (
<>
<DashHeader />
<div className="dash-container">
<Outlet />
</div>
<DashFooter />
</>
);
};
export default DashLayout;
import { Outlet } from 'react-router-dom'
const Layout = () => {
return <Outlet />
}
export default Layout
\ No newline at end of file
import { Link } from "react-router-dom";
const Public = () => {
const content = (
<section className="public">
<header>
<h1>
Welcome to <span className="nowrap">DevOps Project</span>
</h1>
</header>
<main className="public__main">
<p>
Located in Beautiful Downtown Foo City, Dan D. Repairs provides a
trained staff ready to meet your tech repair needs.
</p>
<address className="public__addr">
Dan D. Repairs
<br />
555 Foo Drive
<br />
Foo City, CA 12345
<br />
<a href="tel:+15555555555">(555) 555-5555</a>
</address>
<br />
<p>Owner: Sepi Dana</p>
</main>
<footer>
<Link to="/login">User Login</Link>
</footer>
</section>
);
return content;
};
export default Public;
import { useRef, useState, useEffect } from "react";
import { useNavigate, Link } from "react-router-dom";
import { useDispatch } from "react-redux";
import { setCredentials } from "./authSlice";
import { useLoginMutation } from "./authApiSlice";
import usePersist from "../../hooks/usePersist";
const Login = () => {
const userRef = useRef();
const errRef = useRef();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [errMsg, setErrMsg] = useState("");
const [persist, setPersist] = usePersist();
const navigate = useNavigate();
const dispatch = useDispatch();
const [login, { isLoading }] = useLoginMutation();
useEffect(() => {
userRef.current.focus();
}, []);
useEffect(() => {
setErrMsg("");
}, [email, password]);
const handleSubmit = async (e) => {
e.preventDefault();
try {
const { accessToken } = await login({ email, password }).unwrap();
dispatch(setCredentials({ accessToken }));
setEmail("");
setPassword("");
navigate("/dash");
} catch (err) {
if (!err.status) {
setErrMsg("No Server Response");
} else if (err.status === 400) {
setErrMsg("Missing Email or Password");
} else if (err.status === 401) {
setErrMsg("Unauthorized");
} else {
setErrMsg(err.data?.message);
}
errRef.current.focus();
}
};
const handleUserInput = (e) => setEmail(e.target.value);
const handlePwdInput = (e) => setPassword(e.target.value);
const handleToggle = () => setPersist((prev) => !prev);
const errClass = errMsg ? "errmsg" : "offscreen";
if (isLoading) return <p>Loading...</p>;
const content = (
<section className="public">
<header>
<h1>User Login</h1>
</header>
<main className="login">
<p ref={errRef} className={errClass} aria-live="assertive">
{errMsg}
</p>
<form className="form" onSubmit={handleSubmit}>
<label htmlFor="username">Email:</label>
<input
className="form__input"
type="text"
id="username"
ref={userRef}
value={email}
onChange={handleUserInput}
autoComplete="off"
required
/>
<label htmlFor="password">Password:</label>
<input
className="form__input"
type="password"
id="password"
onChange={handlePwdInput}
value={password}
required
/>
<button className="form__submit-button">Sign In</button>
<label htmlFor="persist" className="form__persist">
<input
type="checkbox"
className="form__checkbox"
id="persist"
onChange={handleToggle}
checked={persist}
/>
Trust This Device
</label>
</form>
</main>
<footer>
<Link to="/">Back to Home</Link>
</footer>
</section>
);
return content;
};
export default Login;
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment