Merge pull request #32 from ricardo-campos-org/feat/31-define-functions-and-screens

feat: define functions and screens
This commit is contained in:
Ricardo Campos
2024-08-26 10:29:15 -03:00
committed by GitHub
37 changed files with 1205 additions and 150 deletions
+2
View File
@@ -22,3 +22,5 @@
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.env
+703 -34
View File
File diff suppressed because it is too large Load Diff
+13 -1
View File
@@ -3,19 +3,30 @@
"version": "1.0.0",
"license": "GPL-3.0-only",
"description": "TaskNote App - Your daily-basis friend to help you with TODOs and Notes",
"keywords": ["react", "typescript", "node", "nestjs"],
"keywords": [
"react",
"typescript",
"node",
"nestjs"
],
"repository": "https://github.com/ricardo-campos-org/react-typescript-todolist",
"private": true,
"dependencies": {
"@popperjs/core": "^2.11.8",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^16.0.0",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^29.5.12",
"@types/node": "^22.5.0",
"@types/react-dom": "^18.3.0",
"@types/react-router-bootstrap": "^0.26.6",
"@vitejs/plugin-react": "^4.3.1",
"bootstrap": "^5.3.3",
"react": "^18.3.1",
"react-bootstrap": "^2.10.4",
"react-dom": "^18.3.1",
"react-router-bootstrap": "^0.26.3",
"react-router-dom": "^6.26.1",
"typescript": "^4.8.4",
"vite": "^5.4.2"
},
@@ -58,6 +69,7 @@
"eslint-plugin-react": "^7.35.0",
"eslint-plugin-react-hooks": "^4.6.2",
"jsdom": "^24.1.1",
"sass": "^1.77.8",
"source-map-support": "^0.5.21",
"vitest": "^2.0.5"
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 341 KiB

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 651 B

After

Width:  |  Height:  |  Size: 831 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

-9
View File
@@ -1,9 +0,0 @@
.main {
min-height: 60vh;
text-align: center;
padding: 2em;
}
.main h2 {
margin-bottom: 0.8em;
}
+42 -69
View File
@@ -1,84 +1,57 @@
import React, { useState } from 'react';
// Components
import Footer from './components/Footer';
import Header from './components/Header';
import TaskForm from './components/TaskForm';
import TaskList from './components/TaskList';
import Modal from './components/Modal';
import React, { useContext, useEffect } from 'react';
// Styles
import styles from './App.module.css';
import './styles/custom.scss';
// Interfaces
import { ITask } from './interfaces/Task';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import Landing from './views/Landing';
import ProtectedRoute from './routes/ProtectedRoute';
import Layout from './layout/PrivateLayout';
import browserRoutes from './routes';
import NotFound from './views/NotFound';
import AuthContext from './context/AuthContext';
const App: React.FC = () => {
const [taskList, setTaskList] = useState<ITask[]>([]);
const [taskToUpdate, setTaskToUpdate] = useState<ITask | null>(null);
const { signed, checkCurrentAuthUser } = useContext(AuthContext);
const deleteTask = (id: number) => {
setTaskList(
taskList.filter((task) => task.id !== id)
);
};
const hideOrShowModal = (display: boolean) => {
const modal = document.querySelector('#modal');
if (display) {
modal!.classList.remove('hide');
} else {
modal!.classList.add('hide');
const notSignedRouter = createBrowserRouter([
{
path: '*',
element: <Landing />
}
]);
const signedRouter = createBrowserRouter([
{
path: '/',
element: <ProtectedRoute />,
children: [
{
element: <Layout />,
children: browserRoutes
}
]
},
{
path: '*',
element: <NotFound />
}
]);
const getBrowserRoutes = () => {
if (signed) {
return signedRouter;
}
return notSignedRouter;
};
const editTask = (task: ITask): void => {
hideOrShowModal(true);
setTaskToUpdate(task);
};
const updateTask = (id: number, title: string, difficulty: number) => {
const updatedTask: ITask = { id, title, difficulty };
const updatedItems = taskList.map((task) => (task.id === updatedTask.id ? updatedTask : task));
setTaskList(updatedItems);
hideOrShowModal(false);
};
useEffect(() => {
checkCurrentAuthUser(window.location.pathname);
}, []);
return (
<>
<Modal>
<TaskForm
btnText="Update task"
taskList={taskList}
task={taskToUpdate}
handleUpdate={updateTask}
/>
</Modal>
<Header />
<main className={styles.main}>
<div>
<h2>What are you going to do?</h2>
<TaskForm
btnText="New task"
taskList={taskList}
setTaskList={setTaskList}
/>
</div>
<div>
<h2>Your tasks</h2>
<TaskList
taskList={taskList}
handleDelete={deleteTask}
handleEdit={editTask}
/>
</div>
</main>
<Footer />
</>
<RouterProvider router={getBrowserRoutes()} />
);
};
+37
View File
@@ -0,0 +1,37 @@
const getHeaders = (): Headers => {
const headers = new Headers();
return headers;
};
const api = {
get: (url: string, params?: object) => fetch(url, {
method: 'GET',
headers: getHeaders(),
...params
}),
post: (url: string, data: BodyInit) => fetch(url, {
method: 'POST',
headers: getHeaders(),
body: data
}),
put: (url: string, data: BodyInit) => fetch(url, {
method: 'PUT',
headers: getHeaders(),
body: data
}),
patch: (url: string, data: any) => fetch(url, {
method: 'PATCH',
headers: getHeaders(),
body: data
}),
delete: (url: string) => fetch(url, {
method: 'DELETE',
headers: getHeaders()
})
};
export default api;
+36
View File
@@ -0,0 +1,36 @@
import { env } from '../env';
import { User } from '../types/User';
const server = env.VITE_BACKEND_SERVER;
/**
*
*/
function privateSessionFake(signed: boolean): Promise<User | undefined> {
return new Promise((resolve, reject) => {
if (signed) {
resolve({
name: 'User',
email: 'email@domain.com'
});
} else {
reject();
}
});
}
const ApiConfig = {
login: `${server}/login`,
loginFake: async (): Promise<void> => new Promise((resolve) => {
resolve();
}),
logoutFake: async (): Promise<void> => new Promise((resolve) => {
resolve();
}),
currentSessionFake: privateSessionFake
};
export default ApiConfig;
+1
View File
@@ -0,0 +1 @@
export const REDIRECT_PATH = 'TASKNOTE-REDIRECT';
Binary file not shown.

After

Width:  |  Height:  |  Size: 235 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

+17 -8
View File
@@ -1,18 +1,27 @@
import React from 'react';
import React, { useContext } from 'react';
import { Button } from 'react-bootstrap';
import style from './Footer.module.css';
import { env } from '../env';
import AuthContext from '../context/AuthContext';
const Footer = () => {
const build = env.BUILD;
const { signOut } = useContext(AuthContext);
const build = env.VITE_BUILD;
return (
<footer className={style.footer}>
<p>
<span>React + TS Todo</span>
{' '}
@ 2022
{` Build: ${build}`}
</p>
<span>React + TS Todoo</span>
{' '}
@ 2024
{` Build: ${build}`}
<Button
type="button"
onClick={() => signOut()}
>
Sair
</Button>
</footer>
);
};
-6
View File
@@ -1,6 +0,0 @@
.header {
background-color: #282c34;
color: #61dafb;
text-align: center;
padding: 1em;
}
-10
View File
@@ -1,10 +0,0 @@
import React from 'react';
import styles from './Header.module.css';
const Header = () => (
<header className={styles.header}>
<h1>React + TS Todo</h1>
</header>
);
export default Header;
+38
View File
@@ -0,0 +1,38 @@
import React from 'react';
import Container from 'react-bootstrap/Container';
import Nav from 'react-bootstrap/Nav';
import Navbar from 'react-bootstrap/Navbar';
import { LinkContainer } from 'react-router-bootstrap';
import Logo from '../../assets/logo2-450-450.png';
/**
*
*/
function Header() {
return (
<header>
<Navbar expand="lg" className="bg-body-secondary">
<Container>
<LinkContainer to="/home">
<Navbar.Brand>
<img width="30" src={Logo} alt="TaskNote logo" />
</Navbar.Brand>
</LinkContainer>
<Navbar.Toggle aria-controls="basic-navbar-nav" />
<Navbar.Collapse id="basic-navbar-nav">
<Nav className="me-auto">
<LinkContainer to="/home">
<Nav.Link>Home</Nav.Link>
</LinkContainer>
<LinkContainer to="/about">
<Nav.Link>About</Nav.Link>
</LinkContainer>
</Nav>
</Navbar.Collapse>
</Container>
</Navbar>
</header>
);
}
export default Header;
+15
View File
@@ -0,0 +1,15 @@
import { createContext } from 'react';
import { User } from '../types/User';
export interface AuthContextData {
signed: boolean;
user: User | undefined;
checkCurrentAuthUser: (pathname: string) => Promise<void>;
signIn: () => void;
signOut: () => void;
isAdmin: boolean;
}
const AuthContext = createContext<AuthContextData>({} as AuthContextData);
export default AuthContext;
+117
View File
@@ -0,0 +1,117 @@
/* eslint-disable no-console */
import React, { useMemo, useState } from 'react';
import { User } from '../types/User';
import { env } from '../env';
import ApiConfig from '../api-service/apiConfig';
import AuthContext, { AuthContextData } from './AuthContext';
import { REDIRECT_PATH } from '../app-constants/app-constants';
interface Props {
children: React.ReactNode;
}
const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }: Props) => {
const [signed, setSigned] = useState<boolean>(false);
const [user, setUser] = useState<User | undefined>();
const [isAdmin, setIsAdmin] = useState<boolean>(false);
const [intervalInstance, setIntervalInstance] = useState<NodeJS.Timeout | null>(null);
const fetchCurrentSession = async (pathname: string): Promise<User | undefined> => {
try {
const currentUser = await ApiConfig.currentSessionFake(signed);
if (currentUser) {
setSigned(true);
}
return currentUser;
} catch (e) {
if (e instanceof Error) {
console.warn(e.message);
} else if (e) {
console.warn(e);
}
// Clear stored client id and name
localStorage.clear();
localStorage.setItem(REDIRECT_PATH, pathname);
setUser(undefined);
setSigned(false);
}
return undefined;
};
const updateUserSession = (userPriv: User) => {
localStorage.setItem('TaskNote-token', userPriv.email);
};
const checkCurrentAuthUser = async (pathname: string): Promise<void> => {
const currentUser = await fetchCurrentSession(pathname);
if (currentUser) {
updateUserSession(currentUser);
setUser(currentUser);
}
};
const signIn = async (): Promise<void> => {
const appEnv = env.VITE_ENV || 'dev';
await ApiConfig.loginFake();
setSigned(true);
const currentUser: User = {
name: `Ricardo ${appEnv}`,
email: 'ricardompcampos@gmail.com'
};
setUser(currentUser);
};
const signOut = async (): Promise<void> => {
await ApiConfig.logoutFake();
setSigned(false);
setUser(undefined);
setIsAdmin(false);
if (intervalInstance) {
clearInterval(intervalInstance);
setIntervalInstance(null);
}
localStorage.clear();
};
const refreshTokenPvt = async () => {
const currentUser = await fetchCurrentSession('/');
if (currentUser) {
updateUserSession(currentUser);
}
};
// 2 minutes
const second = 1000;
const minute = second * 60;
const REFRESH_TIMER = minute * 2;
if (intervalInstance == null && signed) {
const instance = setInterval(() => {
refreshTokenPvt()
.then(() => {
console.log('User session successfully refreshed!');
})
.catch((e) => console.error(e));
}, REFRESH_TIMER);
setIntervalInstance(instance);
}
const contextValue: AuthContextData = useMemo(() => ({
signed,
user,
checkCurrentAuthUser,
signIn,
signOut,
isAdmin
}), [signed, user, checkCurrentAuthUser, signIn, signOut, isAdmin]);
return (
<AuthContext.Provider value={contextValue}>
{ children }
</AuthContext.Provider>
);
};
export default AuthProvider;
-9
View File
@@ -1,9 +0,0 @@
* {
padding: 0;
margin: 0;
font-family: Helvetica;
}
.hide {
display: none !important;
}
+4 -3
View File
@@ -1,8 +1,7 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './index.css';
import AuthProvider from './context/AuthProvider';
window.global ||= window;
@@ -12,6 +11,8 @@ const root = ReactDOM.createRoot(
root.render(
<React.StrictMode>
<App />
<AuthProvider>
<App />
</AuthProvider>
</React.StrictMode>
);
+24
View File
@@ -0,0 +1,24 @@
import React from 'react';
import { Outlet } from 'react-router-dom';
import { Container } from 'react-bootstrap';
import Header from '../../components/Header';
import Footer from '../../components/Footer';
/**
*
*/
function Layout() {
return (
<>
<Header />
<Container>
<Outlet />
</Container>
<Footer />
</>
);
}
export default Layout;
+15
View File
@@ -0,0 +1,15 @@
import React, { useContext } from 'react';
import { Navigate, Outlet } from 'react-router-dom';
import AuthContext from '../../context/AuthContext';
const ProtectedRoute = (): React.JSX.Element => {
const { signed, signOut } = useContext(AuthContext);
if (!signed) {
signOut();
return <Navigate to="/" replace />;
}
return <Outlet />;
};
export default ProtectedRoute;
+41
View File
@@ -0,0 +1,41 @@
import React from 'react';
import { Navigate, RouteObject } from 'react-router-dom';
import getStoredPath from '../utils/PathUtils';
import Home from '../views/Home';
import About from '../views/About';
import NotFound from '../views/NotFound';
const browserRoutes: RouteObject[] = [
{
path: '/',
element: (
<Navigate to={getStoredPath()} replace />
)
},
{
path: '/login',
element: (
<Navigate to="/home" replace />
)
},
{
path: '/home',
element: (
<Home />
)
},
{
path: '/about',
element: (
<About />
)
},
{
path: '/404',
element: (
<NotFound />
)
}
];
export default browserRoutes;
+10
View File
@@ -0,0 +1,10 @@
$theme-colors: (
'info': tomato,
'danger': teal
);
@import '~bootstrap/scss/bootstrap';
.bg-body-secondary {
background-color: red;
}
+4
View File
@@ -0,0 +1,4 @@
export type User = {
name: string;
email: string;
}
+18
View File
@@ -0,0 +1,18 @@
import { REDIRECT_PATH } from '../app-constants/app-constants';
/**
*
*/
function getStoredPath() {
const root: string = '/';
const home: string = '/home';
const storedPath: string | null = localStorage.getItem(REDIRECT_PATH);
if (storedPath) {
localStorage.removeItem(REDIRECT_PATH);
return storedPath === root ? home : storedPath;
}
return home;
}
export default getStoredPath;
+12
View File
@@ -0,0 +1,12 @@
import React from 'react';
/**
*
*/
function About() {
return (
<h1>This is about!</h1>
);
}
export default About;
+12
View File
@@ -0,0 +1,12 @@
import React from 'react';
/**
*
*/
function Home() {
return (
<h1>This is home!</h1>
);
}
export default Home;
+25
View File
@@ -0,0 +1,25 @@
import React, { useContext } from 'react';
import { Button } from 'react-bootstrap';
import './styles.scss';
import AuthContext from '../../context/AuthContext';
/**
*
*/
function Landing() {
const { signIn } = useContext(AuthContext);
return (
<>
<h1>This is landing page</h1>
<Button
type="button"
onClick={() => signIn()}
>
Login
</Button>
</>
);
}
export default Landing;
+3
View File
@@ -0,0 +1,3 @@
body {
background-color: #ccc;
}
+12
View File
@@ -0,0 +1,12 @@
import React from 'react';
/**
*
*/
function NotFound() {
return (
<h2>Not found!</h2>
);
}
export default NotFound;
+1
View File
@@ -1 +1,2 @@
/// <reference types="vite/client" />
declare module '*.png';
+3 -1
View File
@@ -1,6 +1,7 @@
import { ConfigEnv, defineConfig, UserConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { fileURLToPath } from 'url';
import path from 'path';
export default defineConfig(({ mode }: ConfigEnv) => {
const config: UserConfig = {
@@ -38,7 +39,8 @@ export default defineConfig(({ mode }: ConfigEnv) => {
},
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
'@': fileURLToPath(new URL('./src', import.meta.url)),
'~bootstrap': path.resolve(__dirname, 'node_modules/bootstrap')
}
}
};