diff --git a/src/App.css b/src/App.css index 77780c5..38dd52d 100644 --- a/src/App.css +++ b/src/App.css @@ -13,6 +13,20 @@ form textarea { word-break: break-word; } +.appcss-pulse { + animation: loading 1.4s infinite; +} + +@keyframes loading { + 50% { + opacity: 0.5; + } + + 100% { + opacity: 1; + } +} + form > div { width: 90%; margin-left: auto; diff --git a/src/components/BooksBody.tsx b/src/components/BooksBody.tsx index d81860a..ba95602 100644 --- a/src/components/BooksBody.tsx +++ b/src/components/BooksBody.tsx @@ -2,12 +2,12 @@ import { ReactElement } from 'react' import { AuthContainer } from '../hooks/useAuth'; const BooksBody = ({ hook }: { hook: Record }): ReactElement | null => { - const auth = AuthContainer.useContainer(); - if (hook.booksLoading) return null; + const { isLoading, getRequest } = AuthContainer.useContainer(); + if (isLoading && getRequest) return null; return ( -
+
{ - hook.books.length ? hook.books.map(((book: any) => { + hook.books.map(((book: any) => { return (
@@ -33,8 +33,7 @@ const BooksBody = ({ hook }: { hook: Record }): ReactElement | null
) - })) : -
You have not added any book yet. Click on the add button to begin.
+ })) }
) diff --git a/src/components/Loader.tsx b/src/components/Loader.tsx index e51c578..a75d003 100644 --- a/src/components/Loader.tsx +++ b/src/components/Loader.tsx @@ -1,11 +1,17 @@ import { ReactElement } from "react"; import { AuthContainer } from "../hooks/useAuth"; +import { useLoader } from "../hooks/useLoader"; +import SvgAnimatedLoader from "./ui/SvgAnimatedLoader"; -const Loader = (): ReactElement | null => { - const auth = AuthContainer.useContainer(); - if (!auth.isLoading) return null; +const Loader = ({ fixed }: { fixed: boolean }): ReactElement | null => { + const { isLoading } = AuthContainer.useContainer(); + const { loader } = useLoader({ fixed }); + if (!isLoading) return null; return ( -
Loading...
+
+
{loader.text}
+ +
); } diff --git a/src/components/forms/AuthForm.tsx b/src/components/forms/AuthForm.tsx index a524702..6c4c6f8 100644 --- a/src/components/forms/AuthForm.tsx +++ b/src/components/forms/AuthForm.tsx @@ -2,6 +2,7 @@ import { Fragment, ReactElement, useEffect } from "react"; import { AuthContainer } from "../../hooks/useAuth"; import AppLink from "../AppLink"; import Error from "../Error"; +import Loader from "../Loader"; const AuthForm = (): ReactElement => { const auth = AuthContainer.useContainer(); @@ -64,6 +65,7 @@ const AuthForm = (): ReactElement => { text={auth.authFormContent.LinkText} />
+ diff --git a/src/components/forms/BookForm.tsx b/src/components/forms/BookForm.tsx index d9c0b31..95b9a78 100644 --- a/src/components/forms/BookForm.tsx +++ b/src/components/forms/BookForm.tsx @@ -81,7 +81,7 @@ const BookForm = ({ hook }: { hook: Record }): ReactElement => { )} - + diff --git a/src/components/ui/Skeleton.tsx b/src/components/ui/Skeleton.tsx new file mode 100644 index 0000000..4cd8557 --- /dev/null +++ b/src/components/ui/Skeleton.tsx @@ -0,0 +1,36 @@ +import { ReactElement } from "react"; +import { AuthContainer } from "../../hooks/useAuth"; + +const Skeleton = (): ReactElement | null => { + const { isLoading, getRequest } = AuthContainer.useContainer(); + if (isLoading && getRequest) { + return ( +
+ { + [...Array(6)].map(((book: any, index: number) => { + return ( +
+
+

Skeleton title

+

Skeleton description text

+

Preview or download PDF

+
+
+
+ Skeleton +
+
+ Skeleton +
+
+
+ ) + })) + } +
+ ) + } + return null; +} + +export default Skeleton; \ No newline at end of file diff --git a/src/components/ui/SvgAnimatedLoader.tsx b/src/components/ui/SvgAnimatedLoader.tsx new file mode 100644 index 0000000..f3f4d2c --- /dev/null +++ b/src/components/ui/SvgAnimatedLoader.tsx @@ -0,0 +1,62 @@ +import { ReactElement } from "react"; + +const SvgAnimatedLoader = (): ReactElement => { + return ( +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {/* */} + +
+ ); +} + +export default SvgAnimatedLoader; \ No newline at end of file diff --git a/src/hooks/useAuth.tsx b/src/hooks/useAuth.tsx index 14f199d..08c8fa8 100644 --- a/src/hooks/useAuth.tsx +++ b/src/hooks/useAuth.tsx @@ -1,4 +1,4 @@ -import { ChangeEvent, useState } from "react"; +import { ChangeEvent, useEffect, useState } from "react"; import { Location, NavigateFunction, useLocation, useNavigate } from "react-router-dom"; import { createContainer } from "unstated-next"; import { AuthForm, AuthFormContent, User } from "../interfaces/auth"; @@ -10,6 +10,8 @@ export const useAuth = () => { const pageRoute: string = location.pathname; const [isAuthenticated, setIsAuthenticated] = useState(false); const [isLoading, setIsLoading] = useState(false); + const [isFetching, setIsFetching] = useState(false); + const [getRequest, setGetRequest] = useState(false); const [error, setError] = useState(false); const [errorMessage, setErrorMessage] = useState(''); const [unAuthorizedError, setUnAuthorizedError] = useState(false); @@ -36,8 +38,16 @@ export const useAuth = () => { const [user, setUser] = useState(initialUser); const [form, setForm] = useState(initialForm); + useEffect(() => { + const token = localStorage.getItem('buuks_accessToken'); + token ? handleLogIn() : handleLogout(); + isFetching ? setIsLoading(true) : setIsLoading(false); + if (getRequest) setIsLoading(true); + }, [isFetching, getRequest]); + const authenticateUser = async (e: any, apiEndpoint: string, destinationPage: string) => { e.preventDefault(); + setIsFetching(true); const response = await fetch(`${process.env.REACT_APP_BASE_URL}/${apiEndpoint}`, { method: 'POST', headers: { @@ -48,10 +58,9 @@ export const useAuth = () => { const data = await response.json(); if (data.user) { if (pageRoute === '/login') { - localStorage.setItem('accessToken', data.accessToken); - localStorage.setItem('_user', JSON.stringify(data.user)); - setUser(JSON.parse(localStorage.getItem('_user') as string)); - setIsAuthenticated(true); + localStorage.setItem('buuks_accessToken', data.accessToken); + localStorage.setItem('buuks_user', JSON.stringify(data.user)); + handleLogIn(); } navigate(destinationPage); resetForm(initialForm); @@ -66,6 +75,7 @@ export const useAuth = () => { if (response.status === 401) { handleError(true, data.error.message); } + setIsFetching(false); // console.log(data) } @@ -76,16 +86,17 @@ export const useAuth = () => { } const handleLogIn = () => { + const userLocal = localStorage.getItem('buuks_user'); + setUser(JSON.parse(userLocal as string)); setIsAuthenticated(true); - localStorage.getItem('accessToken'); } const handleLogout = () => { setIsAuthenticated(false); setUnAuthorizedError(false); navigate('/login'); - localStorage.removeItem('accessToken'); - localStorage.removeItem('_user'); + localStorage.removeItem('buuks_accessToken'); + localStorage.removeItem('buuks_user'); setUser(initialUser); } @@ -143,9 +154,12 @@ export const useAuth = () => { user, setUser, isLoading, + getRequest, error, errorMessage, setIsLoading, + setIsFetching, + setGetRequest, handleError, unAuthorizedError, setUnAuthorizedError, diff --git a/src/hooks/useBooks.tsx b/src/hooks/useBooks.tsx index cd00b8e..9b6345a 100644 --- a/src/hooks/useBooks.tsx +++ b/src/hooks/useBooks.tsx @@ -1,15 +1,21 @@ -import { ChangeEvent, useState } from "react"; +import { ChangeEvent, Dispatch, SetStateAction, useEffect, useState } from "react"; import { fetchOptions } from "../lib/books"; import { BooksObject, ModalForm, PostForm } from "../interfaces/books"; -import { AuthContainer } from "./useAuth"; +import { User } from "../interfaces/auth"; -export const useBooks = (): Record => { - const auth = AuthContainer.useContainer(); +export const useBooks = ( + { user, isAuthenticated, handleLogout, setIsFetching, setGetRequest }: + { + user: User, + isAuthenticated: boolean, + handleLogout: () => void, + setIsFetching: Dispatch>, + setGetRequest: Dispatch> + }) => { let [books, setBooks] = useState([]); - const [modal, setModal] = useState(false); + const [modal, setModal] = useState(false); const [success, setSuccess] = useState(false); const [successMessage, setSuccessMessage] = useState(''); - const [booksLoading, setBooksLoading] = useState(null); const initialForm = { title: '', description: '', @@ -34,16 +40,24 @@ export const useBooks = (): Record => { const [form, setForm] = useState(initialForm); const [modalForm, setModalForm] = useState(initialModalform); + useEffect(() => { + if (isAuthenticated) getBooks(); + }, [isAuthenticated]); + + const getBooks = () => fetchBookData(null, 'GET', `books/user/${user._id}`, null); + const fetchBookData = async (e: any, method: string, apiEndpoint: string, bookId: string | null) => { if (method === 'POST' || method === 'PUT') e.preventDefault(); - if (method === 'GET' && !auth.isLoading) setBooksLoading(false); - if (method !== 'GET' && !auth.isLoading) setBooksLoading(null); - trackProgress(true, false, ''); + setIsFetching(true); + if (method === 'GET') setGetRequest(true); + // trackProgress(true, false, ''); let response: Response; response = await fetch(`${process.env.REACT_APP_BASE_URL}/${apiEndpoint}`, fetchOptions(method, form)); const data = await response.json(); + if (isAuthenticated) console.log(apiEndpoint); + if (response.ok) { - trackProgress(false, false, ''); + // trackProgress(false, false, ''); if (method === 'GET') setBooks(data.books); if (method === 'DELETE') setBooks(books.filter(book => book._id !== bookId)); if (method === 'POST') { @@ -69,22 +83,25 @@ export const useBooks = (): Record => { resetForm(); } if (response.status === 400) { - if (method === 'POST') trackProgress(false, true, 'All fields are required. Also, only PDF files are allowed.'); - if (method === 'PUT') trackProgress(false, true, 'Only PDF files are allowed.'); - if (method === 'POST' && data[0].code === 'too_small') trackProgress(false, true, `${data[0].message}. All fields are required. Also, only PDF files are allowed.`); + // if (method === 'POST') trackProgress(false, true, 'All fields are required. Also, only PDF files are allowed.'); + // if (method === 'PUT') trackProgress(false, true, 'Only PDF files are allowed.'); + // if (method === 'POST' && data[0].code === 'too_small') trackProgress(false, true, `${data[0].message}. All fields are required. Also, only PDF files are allowed.`); } if (response.status === 401) { - trackProgress(false, true, 'Your session has expired, please login again'); - auth.setUnAuthorizedError(true); - const removeError = () => auth.setUnAuthorizedError(false); - const interval = setInterval(removeError, 200); - const removeInterval = () => { - auth.handleLogout(); - clearInterval(interval); - } - setTimeout(removeInterval, 200); + handleLogout(); + // trackProgress(false, true, 'Your session has expired, please login again'); + // auth.setUnAuthorizedError(true); + // const removeError = () => auth.setUnAuthorizedError(false); + // const interval = setInterval(removeError, 200); + // const removeInterval = () => { + // auth.handleLogout(); + // clearInterval(interval); + // } + // setTimeout(removeInterval, 200); } - // console.log('data: ', data); + setIsFetching(false); + setGetRequest(false); + if (isAuthenticated) console.log('data: ', data); } const handleInputChange = (e: ChangeEvent, checkboxString: string) => { // checkboxString: string | null @@ -98,19 +115,19 @@ export const useBooks = (): Record => { setModalForm({ ...modalForm, [checkboxString]: false }); } } - trackProgress(false, false, ''); + // trackProgress(false, false, ''); } - const trackProgress = (loading: boolean, errBool: boolean, errString: string) => { - auth.setIsLoading(loading); - auth.handleError(errBool, errString); - } + // const trackProgress = (loading: boolean, errBool: boolean, errString: string) => { + // auth.setIsLoading(loading); + // auth.handleError(errBool, errString); + // } const handleModal = (boolean: boolean) => { setModal(boolean); if (!boolean) { setModalForm(initialModalform); - auth.handleError(false, ''); + // auth.handleError(false, ''); resetForm(); } } @@ -171,11 +188,11 @@ export const useBooks = (): Record => { form, handleModal, fetchBookData, + getBooks, handleInputChange, handlePostRequestForm, modalForm, - booksLoading, success, successMessage } -} \ No newline at end of file +} diff --git a/src/hooks/useLoader.tsx b/src/hooks/useLoader.tsx new file mode 100644 index 0000000..d114299 --- /dev/null +++ b/src/hooks/useLoader.tsx @@ -0,0 +1,25 @@ +import { useEffect, useState } from 'react'; + +export const useLoader = ({ fixed }: { fixed: boolean }) => { + const [loader, setLoader] = useState<{ text: string; style: string;}>({ + text: '', + style: '' + }); + useEffect(() => { + if (fixed) { + setLoader({ + text: 'Loading...', + style: 'fixed bottom-6 right-6 py-2 px-3 md:text-lg md:px-4 rounded border border-pink-800' + }); + } else { + setLoader({ + text: 'Updating books count...', + style: 'mt-4' + }); + } + }, [fixed]); + + return { + loader + } +} \ No newline at end of file diff --git a/src/lib/books.tsx b/src/lib/books.tsx index 7333c55..950bc63 100644 --- a/src/lib/books.tsx +++ b/src/lib/books.tsx @@ -5,8 +5,8 @@ export const fetchOptions = (method: string, form: PostForm) => { options = { method: method, headers: { - 'Authorization': `Bearer ${localStorage.accessToken}`, - 'x-access-token': `${localStorage.accessToken}` + 'Authorization': `Bearer ${localStorage.buuks_accessToken}`, + 'x-access-token': `${localStorage.buuks_accessToken}` } } if (method === 'POST') { diff --git a/src/pages/Books.tsx b/src/pages/Books.tsx index cd102ae..6823432 100644 --- a/src/pages/Books.tsx +++ b/src/pages/Books.tsx @@ -1,4 +1,4 @@ -import { ReactElement, useEffect } from "react"; +import { ReactElement } from "react"; import Modal from "../components/Modal"; import { useBooks } from "../hooks/useBooks"; import { AuthContainer } from "../hooks/useAuth"; @@ -6,28 +6,19 @@ import Loader from "../components/Loader"; import Toastr from "../components/Toastr"; import BooksBody from "../components/BooksBody"; import BookForm from "../components/forms/BookForm"; +import Skeleton from "../components/ui/Skeleton"; const Books = (): ReactElement => { - const auth = AuthContainer.useContainer(); - const hook = useBooks(); - useEffect(() => { - let abortController = new AbortController(); - const user = JSON.parse(localStorage.getItem('_user') as string); - auth.setUser(user); - hook.fetchBookData(null, 'GET', `books/user/${user._id}`, null); - auth.handleLogIn(); - return () => { - abortController.abort(); - } - }, []); + const { user, isAuthenticated, isLoading, handleLogout, setIsFetching, setGetRequest } = AuthContainer.useContainer(); + const hook = useBooks({ user, isAuthenticated, handleLogout, setIsFetching, setGetRequest }); return (
-

{auth.user.name}

+

{user.name}