diff --git a/.editorconfig b/.editorconfig index a727df3..6ae0591 100644 --- a/.editorconfig +++ b/.editorconfig @@ -2,7 +2,8 @@ root = true [*] -indent_style = tab +indent_style = space +indent_size = 2 end_of_line = lf charset = utf-8 trim_trailing_whitespace = true diff --git a/src/components/clusters.tsx b/src/components/clusters.tsx index 1685327..8555578 100644 --- a/src/components/clusters.tsx +++ b/src/components/clusters.tsx @@ -1,7 +1,7 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useContext } from 'react'; +import { AppContext } from '../contexts/context'; import LinkCard from '../components/linkCard'; -import ClearFilter from '../utils/clearFilter'; import { fetchAndFilterData } from '../services/dataFetcher'; import '../styles/components/clusters.css'; @@ -9,24 +9,28 @@ import '../styles/popups/loading.css'; interface ClustersProps { dataKey: string; - searchTerm: string; - setSearchTerm: (value: string) => void; - isLogedIn: boolean; - token: string; - message: string | null; - setMessage: (value: string | null) => void; } -function Clusters({ dataKey, searchTerm, setSearchTerm, isLogedIn, token, message, setMessage }: ClustersProps) { +function Clusters({ dataKey }: ClustersProps) { + const { + needReload, setNeedReload, + selectedCategory, + searchTerm + } = useContext(AppContext); + const [clusters, setClusters] = useState([]); - const [selectedCategory, setSelectedCategory] = useState(null); const [isLoading, setIsLoading] = useState(true); useEffect(() => { setIsLoading(true); - fetchAndFilterData(dataKey, true, selectedCategory, searchTerm, setClusters) - .finally(() => setIsLoading(false)); - }, [dataKey, selectedCategory, searchTerm]); + fetchAndFilterData(dataKey, false, selectedCategory, searchTerm, setClusters) + .finally(() => { + setIsLoading(false); + if (needReload) { + setNeedReload(false); + } + }); + }, [dataKey, selectedCategory, searchTerm, needReload]); if (isLoading) { return ( @@ -37,18 +41,12 @@ function Clusters({ dataKey, searchTerm, setSearchTerm, isLogedIn, token, messag return ( <>

{dataKey}

-
{clusters.length > 0 ? ( clusters.map((cluster: ClusterProps) => ( )) ) : ( diff --git a/src/components/content.tsx b/src/components/content.tsx index fa76eab..3e5cd69 100644 --- a/src/components/content.tsx +++ b/src/components/content.tsx @@ -1,32 +1,25 @@ -import React from 'react'; +import React, { useContext } from 'react'; import { Route, Routes } from 'react-router-dom'; +import { AppContext } from '../contexts/context'; import Cluster from '../components/clusters'; import '../styles/components/content.css'; -interface ContentProps { - routes: { [key: string]: string }; - searchTerm: string; - setSearchTerm: (value: string) => void; - isLogedIn: boolean; - token: string; - setToken: (value: string) => void; - message: string | null; - setMessage: (value: string | null) => void; - setShowOverlay: (value: boolean) => void; -} +function Content() { + const { + routes + } = useContext(AppContext); -function Content({ routes, searchTerm, setSearchTerm, isLogedIn, token, setToken, message, setMessage, setShowOverlay }: ContentProps) { return (
} + element={} /> {Object.entries(routes).map(([path, element]) => ( } + element={} /> ))} diff --git a/src/components/header.tsx b/src/components/header.tsx index dc8902d..4b3c2f0 100644 --- a/src/components/header.tsx +++ b/src/components/header.tsx @@ -1,28 +1,25 @@ -import React, { useState, useEffect, useRef } from 'react'; +import React, { useState, useEffect, useRef, useContext } from 'react'; import { Icon } from '@iconify/react'; import { Link } from 'react-router-dom'; +import { AppContext } from '../contexts/context'; import Login from '../popups/login'; import '../styles/components/header.css'; -interface HeaderProps { - routes: { [key: string]: string }; - searchTerm: string; - setSearchTerm: (value: string) => void; - isLogedIn: boolean; - setIsLogedIn: (value: boolean) => void; - token: string; - setToken: (value: string) => void; - message: string | null; - setMessage: (value: string | null) => void; - setShowOverlay: (value: boolean) => void; -} +function Header() { + const { + isLogedIn, setIsLogedIn, + routes, + setToken, + setMessage, + searchTerm, setSearchTerm, + setShowOverlay, + setOverlayAction + } = useContext(AppContext); -function Header({ routes, searchTerm, setSearchTerm, isLogedIn, setIsLogedIn, token, setToken, message, setMessage, setShowOverlay }: HeaderProps) { const inputRef = useRef(null); useEffect(() => { - localStorage.removeItem('colorMap'); if (inputRef.current) { inputRef.current.focus(); } @@ -36,9 +33,6 @@ function Header({ routes, searchTerm, setSearchTerm, isLogedIn, setIsLogedIn, to const toggleMenu = () => setIsOpen(!isOpen); const [showLogin, setShowLogin] = useState(false); - const toggleLogin = () => { - setShowLogin(!showLogin); - }; const toggleLogout = () => { setIsLogedIn(false); setToken(''); @@ -57,7 +51,7 @@ function Header({ routes, searchTerm, setSearchTerm, isLogedIn, setIsLogedIn, to
    {Object.entries(routes).map(([path, element]) => ( -
  • {element}
  • +
  • {element as React.ReactNode}
  • ))}
@@ -72,22 +66,25 @@ function Header({ routes, searchTerm, setSearchTerm, isLogedIn, setIsLogedIn, to onChange={(e) => setSearchTerm(e.target.value)} />
- +
- + {isOpen && (
    {Object.entries(routes).map(([path, element]) => ( -
  • {element}
  • +
  • {element as React.ReactNode}
  • ))}
)} -
{ +
{ if (isOpen) toggleMenu(); - if (showLogin) toggleLogin(); }}>
- {showLogin && } + {showLogin && } ) } diff --git a/src/components/linkCard.tsx b/src/components/linkCard.tsx index 2e68125..30978fd 100644 --- a/src/components/linkCard.tsx +++ b/src/components/linkCard.tsx @@ -1,23 +1,32 @@ -import React, { useState } from 'react'; +import React, { useContext } from 'react'; import { Icon } from '@iconify/react'; import { v4 as uuidv4 } from 'uuid'; +import { AppContext } from '../contexts/context'; import { getRandomColor, getContrastColor } from '../utils/randomColor'; -import Confirm from '../popups/confirm'; import deleteCardHandler from '../services/deleteCardHandler'; import '../styles/components/linkCard.css'; interface LinkCardProps { item: ClusterProps; - setSelectedCategory: (value: string) => void; - isLogedIn: boolean; - token: string; - message: string | null; - setMessage: (value: string | null) => void; } -function LinkCard({ item, setSelectedCategory, isLogedIn, token, message, setMessage }: LinkCardProps) { +function LinkCard({ item }: LinkCardProps) { + const { + isLogedIn, + token, + setMessage, + setShowConfirm, + setConfirmMessage, + setConfirmAction, + setSelectedCategory, + setShowOverlay, + setOverlayAction + } = useContext(AppContext); + + const id = item.Id || 0; + const backgroundColor = getRandomColor(item.Category); const textColor = getContrastColor(backgroundColor); @@ -26,11 +35,7 @@ function LinkCard({ item, setSelectedCategory, isLogedIn, token, message, setMes setSelectedCategory(item.Category); }; - const [showConfirm, setShowConfirm] = useState(false); - const confirmMessage = "Are you confirmed to delete this card?"; - - const tryDeleteCard = async () => { - const id = item.Id || 0; + const tryDeleteCard = async (id: number) => { const deleteCardResult = await deleteCardHandler({ id, token }); if (deleteCardResult === true) { @@ -43,6 +48,7 @@ function LinkCard({ item, setSelectedCategory, isLogedIn, token, message, setMes return ( <>
+
@@ -59,7 +65,13 @@ function LinkCard({ item, setSelectedCategory, isLogedIn, token, message, setMes {isLogedIn ? (
- setShowConfirm(true)}> + { + setConfirmMessage('Are you sure to delete this card?'); + setConfirmAction(() => () => tryDeleteCard(id)); + setShowConfirm(true); + setShowOverlay(true); + setOverlayAction(() => () => setShowConfirm(false)); + }}>
) : null } @@ -100,7 +112,6 @@ function LinkCard({ item, setSelectedCategory, isLogedIn, token, message, setMes ) : null}
- {showConfirm ? () : null} ); } diff --git a/src/contexts/context.tsx b/src/contexts/context.tsx new file mode 100644 index 0000000..a463774 --- /dev/null +++ b/src/contexts/context.tsx @@ -0,0 +1,84 @@ +import React, { createContext, useState, useEffect } from 'react'; +import { fetchCollection } from '../services/collectionFetcher'; + +export const AppContext = createContext({ + isLoading: false, + setIsLoading: (_: boolean) => {}, + isLogedIn: false, + setIsLogedIn: (_: boolean) => {}, + needReload: false, + setNeedReload: (_: boolean) => {}, + routes: {}, + setRoutes: (_: {[_: string]: React.ReactNode}) => {}, + token: '', + setToken: (_: string) => {}, + message: null as string | null, + setMessage: (_: string | null) => {}, + showConfirm: false, + setShowConfirm: (_: boolean) => {}, + confirmMessage: '', + setConfirmMessage: (_: string) => {}, + confirmAction: () => {}, + setConfirmAction: (_: () => void) => {}, + showOverlay: false, + setShowOverlay: (_: boolean) => {}, + overlayAction: () => {}, + setOverlayAction: (_: () => void) => {}, + selectedCategory: null as string | null, + setSelectedCategory: (_: string | null) => {}, + searchTerm: '', + setSearchTerm: (_: string) => {}, +}); + +export const AppProvider = ({ children }: { children: React.ReactNode }) => { + const [isLoading, setIsLoading] = useState(true); + const [isLogedIn, setIsLogedIn] = useState(false); + const [needReload, setNeedReload] = useState(false); + const [routes, setRoutes] = useState({}); + const [token, setToken] = useState(''); + const [message, setMessage] = useState(null); + const [showConfirm, setShowConfirm] = useState(false); + const [confirmMessage, setConfirmMessage] = useState(''); + const [confirmAction, setConfirmAction] = useState<() => void>(() => () => {}); + const [showOverlay, setShowOverlay] = useState(false); + const [overlayAction, setOverlayAction] = useState<() => void>(() => () => {}); + const [selectedCategory, setSelectedCategory] = useState(null); + const [searchTerm, setSearchTerm] = useState(''); + + useEffect(() => { + setIsLoading(true); + fetchCollection(setRoutes) + .finally(() => setIsLoading(false)); + }, []); + + useEffect(() => { + if (localStorage.getItem('token')) { + setIsLogedIn(true); + setToken(localStorage.getItem('token') as string); + } + }, []); + + useEffect(() => { + localStorage.removeItem('colorMap'); + }, []); + + return ( + + {children} + + ); +}; diff --git a/src/popups/confirm.tsx b/src/popups/confirm.tsx index 45c0b7f..541af40 100644 --- a/src/popups/confirm.tsx +++ b/src/popups/confirm.tsx @@ -1,25 +1,39 @@ -import React from 'react'; +import React, { useContext } from 'react'; +import { AppContext } from '../contexts/context'; import '../styles/popups/popup.css'; import '../styles/popups/confirm.css'; -interface ConfirmProps { - message: string | null; - setShowConfirm: (value: boolean) => void; - actionHandler: () => void; -} +function Confirm() { + const { + setNeedReload, + showConfirm, setShowConfirm, + confirmMessage, + confirmAction, + setShowOverlay + } = useContext(AppContext); + + async function confirmHandler() { + await confirmAction(); + setShowConfirm(false); + setShowOverlay(false); + setNeedReload(true); + } + + if (!showConfirm) { + return null; + } -function Confirm({ message, setShowConfirm, actionHandler }: ConfirmProps) { return ( <>
-
{ message }
+
{ confirmMessage }
- - + +
diff --git a/src/popups/login.tsx b/src/popups/login.tsx index 9103790..ed8a4cb 100644 --- a/src/popups/login.tsx +++ b/src/popups/login.tsx @@ -1,6 +1,7 @@ -import React from 'react'; +import React, { useContext } from 'react'; import { Icon } from '@iconify/react'; +import { AppContext } from '../contexts/context'; import loginHandler from '../services/loginHandler'; import '../styles/popups/popup.css'; @@ -8,14 +9,16 @@ import '../styles/popups/login.css'; interface LoginProps { setShowLogin: (value: boolean) => void; - setIsLogedIn: (value: boolean) => void; - token: string; - setToken: (value: string) => void; - message: string | null; - setMessage: (value: string | null) => void; } -function Login({ setShowLogin, setIsLogedIn, token, setToken, message, setMessage }: LoginProps) { +function Login({ setShowLogin }: LoginProps) { + const { + setIsLogedIn, + token, setToken, + setMessage, + setShowOverlay + } = useContext(AppContext); + const tryLogin = async () => { const username = (document.getElementById("username") as HTMLInputElement).value; const password = (document.getElementById("password") as HTMLInputElement).value; @@ -23,6 +26,7 @@ function Login({ setShowLogin, setIsLogedIn, token, setToken, message, setMessag if (loginResult === true) { setShowLogin(false); + setShowOverlay(false); localStorage.setItem('token', token); setIsLogedIn(true); setMessage('Successfully logged in'); @@ -43,7 +47,10 @@ function Login({ setShowLogin, setIsLogedIn, token, setToken, message, setMessag
-
diff --git a/src/popups/notification.tsx b/src/popups/notification.tsx index 416d63c..a954f14 100644 --- a/src/popups/notification.tsx +++ b/src/popups/notification.tsx @@ -1,13 +1,13 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useContext } from 'react'; +import { AppContext } from '../contexts/context'; import '../styles/popups/notification.css'; -interface NotificationProps { - message: string | null; - setMessage: (value: string | null) => void; -} +function Notification() { + const { + message, setMessage + } = useContext(AppContext); -function Notification({ message, setMessage }: NotificationProps) { const [visible, setVisible] = useState(false); useEffect(() => { diff --git a/src/popups/overlay.tsx b/src/popups/overlay.tsx index 663661f..b9adf29 100644 --- a/src/popups/overlay.tsx +++ b/src/popups/overlay.tsx @@ -1,17 +1,20 @@ -import React from "react"; +import React, { useContext } from "react"; +import { AppContext } from "../contexts/context"; import "../styles/popups/overlay.css"; -interface OverlayProps { - showOverlay: boolean; - setShowOverlay: (value: boolean) => void; -} -function Overlay({ showOverlay, setShowOverlay }: OverlayProps) { +function Overlay() { + const { + showOverlay, setShowOverlay, + overlayAction + } = useContext(AppContext); + return ( <>
{ if (showOverlay) { + overlayAction(); setShowOverlay(false); } }}>
diff --git a/src/routers/router.tsx b/src/routers/router.tsx index 1c7b591..99d05a2 100644 --- a/src/routers/router.tsx +++ b/src/routers/router.tsx @@ -1,59 +1,40 @@ -import React, { useState, useEffect } from 'react'; +import React, { useContext } from 'react'; import { BrowserRouter } from 'react-router-dom'; -import { fetchCollection } from '../services/collectionFetcher'; +import { AppContext, AppProvider } from '../contexts/context'; import Header from '../components/header'; import Content from '../components/content'; import Footer from '../components/footer'; import Notification from '../popups/notification'; +import Confirm from '../popups/confirm'; import Overlay from '../popups/overlay'; +import ClearFilter from '../utils/clearFilter'; import '../styles/popups/loading.css'; -function Router() { - const [isLoading, setIsLoading] = useState(true); - const [isLogedIn, setIsLogedIn] = useState(false); - - const [routes, setRoutes] = useState({}); - - const [token, setToken] = useState(''); - const [message, setMessage] = useState(null); - const [showOverlay, setShowOverlay] = useState(false) - - const [searchTerm, setSearchTerm] = useState(''); - - useEffect(() => { - setIsLoading(true); - fetchCollection(setRoutes) - .finally(() => setIsLoading(false)); - }, []); - useEffect(() => { - if (localStorage.getItem('token')) { - setIsLogedIn(true); - setToken(localStorage.getItem('token') as string); - } - }, []); - - if (isLoading) { - return ( -
- ); - } - - return ( - <> - -
- - - -