diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a7f1422..a49a5ba 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -19,6 +19,7 @@ "i18next": "^22.4.15", "i18next-browser-languagedetector": "^7.0.1", "i18next-http-backend": "^2.2.0", + "lodash": "^4.17.21", "moment": "^2.29.4", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -26,10 +27,12 @@ "react-lazylog": "^4.5.3", "react-router-dom": "^6.10.0", "react-toastify": "^9.1.3", - "react-world-flags": "^1.6.0" + "react-world-flags": "^1.6.0", + "swr": "^2.2.5" }, "devDependencies": { "@babel/plugin-proposal-private-property-in-object": "^7.21.11", + "@types/lodash": "^4.17.7", "@types/react": "^18.2.33", "@types/react-dom": "^18.2.14", "@types/react-world-flags": "^1.4.5", @@ -4890,6 +4893,12 @@ "dev": true, "peer": true }, + "node_modules/@types/lodash": { + "version": "4.17.7", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz", + "integrity": "sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==", + "dev": true + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -6973,6 +6982,11 @@ "node": ">=0.10.0" } }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" + }, "node_modules/cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", @@ -19483,6 +19497,18 @@ "boolbase": "~1.0.0" } }, + "node_modules/swr": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.2.5.tgz", + "integrity": "sha512-QtxqyclFeAsxEUeZIYmsaQ0UjimSq1RZ9Un7I68/0ClKK/U3LoyQunwkQfJZr2fc22DfIXLNDc2wFyTEikCUpg==", + "dependencies": { + "client-only": "^0.0.1", + "use-sync-external-store": "^1.2.0" + }, + "peerDependencies": { + "react": "^16.11.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -20209,6 +20235,14 @@ "requires-port": "^1.0.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz", + "integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -24773,6 +24807,12 @@ "dev": true, "peer": true }, + "@types/lodash": { + "version": "4.17.7", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz", + "integrity": "sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==", + "dev": true + }, "@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -26391,6 +26431,11 @@ } } }, + "client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" + }, "cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", @@ -35637,6 +35682,15 @@ } } }, + "swr": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.2.5.tgz", + "integrity": "sha512-QtxqyclFeAsxEUeZIYmsaQ0UjimSq1RZ9Un7I68/0ClKK/U3LoyQunwkQfJZr2fc22DfIXLNDc2wFyTEikCUpg==", + "requires": { + "client-only": "^0.0.1", + "use-sync-external-store": "^1.2.0" + } + }, "symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -36186,6 +36240,12 @@ "requires-port": "^1.0.0" } }, + "use-sync-external-store": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz", + "integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==", + "requires": {} + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index f3f0c63..2c581ea 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -24,6 +24,7 @@ "i18next": "^22.4.15", "i18next-browser-languagedetector": "^7.0.1", "i18next-http-backend": "^2.2.0", + "lodash": "^4.17.21", "moment": "^2.29.4", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -31,10 +32,12 @@ "react-lazylog": "^4.5.3", "react-router-dom": "^6.10.0", "react-toastify": "^9.1.3", - "react-world-flags": "^1.6.0" + "react-world-flags": "^1.6.0", + "swr": "^2.2.5" }, "devDependencies": { "@babel/plugin-proposal-private-property-in-object": "^7.21.11", + "@types/lodash": "^4.17.7", "@types/react": "^18.2.33", "@types/react-dom": "^18.2.14", "@types/react-world-flags": "^1.4.5", diff --git a/frontend/src/components/layout/TopBar.tsx b/frontend/src/components/layout/TopBar.tsx index 80705a4..4e4378d 100644 --- a/frontend/src/components/layout/TopBar.tsx +++ b/frontend/src/components/layout/TopBar.tsx @@ -7,6 +7,7 @@ import LightDarkToggle from "./LightDarkToggle"; import SensitiveInfoToggle from "./SensitiveInfoToggle"; import { styled } from "@mui/material/styles"; import { drawerWidth } from "./constants"; +import { ProgressBar } from "units/progress"; interface AppBarProps extends MuiAppBarProps { open?: boolean; @@ -64,7 +65,7 @@ const TopBar: React.FC = ({ open, onDrawerOpen }) => { - {/* */} + ); }; diff --git a/frontend/src/units/progress/ProgressBar.tsx b/frontend/src/units/progress/ProgressBar.tsx new file mode 100644 index 0000000..5682420 --- /dev/null +++ b/frontend/src/units/progress/ProgressBar.tsx @@ -0,0 +1,15 @@ +import React from "react"; +import { Box, LinearProgress } from "@mui/material"; +import { useProgressBar } from "./hooks"; + +const ProgressBar: React.FC = () => { + const { progress } = useProgressBar(); + if (!progress) return <>; + return ( + + + + ); +}; + +export default ProgressBar; diff --git a/frontend/src/units/progress/ProgressBarProvider.tsx b/frontend/src/units/progress/ProgressBarProvider.tsx new file mode 100644 index 0000000..dcf445e --- /dev/null +++ b/frontend/src/units/progress/ProgressBarProvider.tsx @@ -0,0 +1,65 @@ +import { isArray } from "lodash"; +import React, { createContext, useCallback, useMemo, useState } from "react"; + +export type ProgressBarUnit = { key: string | null; acting: boolean }; + +type ProgressBarContextPayload = { + progress: boolean; + actions: { + add: (key: string) => void; + remove: (key: string) => void; + observe: (unit?: ProgressBarUnit | ProgressBarUnit[]) => void; + }; +}; + +const initialContext: ProgressBarContextPayload = { + progress: false, + actions: { + add: () => undefined, + remove: () => undefined, + observe: () => undefined + } +}; + +const ProgressBarContext = createContext(initialContext); + +type Props = { children: React.ReactNode }; +const ProgressBarProvider: React.FC = ({ children }) => { + const [network, setNetwork] = useState([]); + const handleAdd = useCallback((key: string) => setNetwork(prev => [...prev, key]), []); + const handleRemove = useCallback((key: string) => setNetwork(prev => prev.filter(z => z !== key)), []); + + const handleObserve = useCallback( + (unit?: ProgressBarUnit | ProgressBarUnit[]) => { + const items: ProgressBarUnit[] = []; + + if (unit) { + if (isArray(unit)) { + items.push(...unit); + } else { + items.push(unit); + } + } + + for (const z of items) { + if (!z.key) continue; + if (z.acting) { + handleAdd(z.key); + } else { + handleRemove(z.key); + } + } + }, + [handleAdd, handleRemove] + ); + + const progress = useMemo(() => network.length > 0, [network.length]); + const payload = useMemo( + () => ({ progress, actions: { add: handleAdd, remove: handleRemove, observe: handleObserve } }), + [progress, handleAdd, handleRemove, handleObserve] + ); + return {children}; +}; + +export { ProgressBarContext }; +export default ProgressBarProvider; diff --git a/frontend/src/units/progress/hooks.ts b/frontend/src/units/progress/hooks.ts new file mode 100644 index 0000000..e4d45e9 --- /dev/null +++ b/frontend/src/units/progress/hooks.ts @@ -0,0 +1,14 @@ +import { useContext } from "react"; +import { ProgressBarContext, ProgressBarUnit } from "./ProgressBarProvider"; + +type UseProgressBarResult = { + progress: boolean; + observe: (unit?: ProgressBarUnit | ProgressBarUnit[]) => void; +}; + +const useProgressBar = (): UseProgressBarResult => { + const context = useContext(ProgressBarContext); + return { progress: context.progress, observe: context.actions.observe }; +}; + +export { useProgressBar }; diff --git a/frontend/src/units/progress/index.ts b/frontend/src/units/progress/index.ts new file mode 100644 index 0000000..c206efa --- /dev/null +++ b/frontend/src/units/progress/index.ts @@ -0,0 +1,4 @@ +import ProgressBarProvider from "./ProgressBarProvider"; +import ProgressBar from "./ProgressBar"; +export { ProgressBarProvider, ProgressBar }; +export * from "./hooks";