diff --git a/src/Components/Main/Home/Components/GridCards.js b/src/Components/Main/Home/Components/GridCards.js index 83a603b..aa1fb0c 100644 --- a/src/Components/Main/Home/Components/GridCards.js +++ b/src/Components/Main/Home/Components/GridCards.js @@ -1,74 +1,14 @@ -import React, { useState, useEffect, useCallback } from "react"; -import isElectron from "is-electron"; - //Components import Card from "../../../SharedComponents/Card/Card.js"; import Spinner from "../../../SharedComponents/Spinner/Spinner.js"; import ProgressPie from "../../../SharedComponents/Card/Components/ProgressPie.jsx"; -const GridCards = () => { - const [modules, setModules] = useState([]); - const [loading, setLoading] = useState(true); - - //Get the modules from the localStorage and set the module state - //Updates every time localeStorage changes - //TODO getting the public folder is blocking the function on no internet connection - const modulesFromBrowserStorage = useCallback(async () => { - //The question types folder from the public folder needs to be combined - //with every module from the locale storage - - try { - //Get the data from the public folder - const publicFolder = await fetch("data.json", { mode: "no-cors" }); - const resJSON = await publicFolder.json(); - - //Get modules from the localStorage - let storageModules = []; - Object.entries(localStorage).forEach((key) => { - if (key[0].startsWith("repeatio-module")) { - const module = localStorage.getItem(key[0]); - storageModules.push(JSON.parse(module)); - } - }); +//Hooks +import useAllModules from "../hooks/useAllModules.js"; - //Update the state - setModules([...storageModules, resJSON]); - setLoading(false); - } catch (error) { - console.log(error.message); - } - }, []); - - //Refetch the modules if the localeStorage changes - const onStorageChange = useCallback(() => { - setLoading(true); - modulesFromBrowserStorage(); - }, [modulesFromBrowserStorage]); - - //Fetch data for all modules by reading all repeatio files in documents folder / locale storage (in browser) - useEffect(() => { - if (isElectron()) { - // Send a message to the main process - window.api.request("toMain", ["getModules"]); - - // Called when message received from main process - window.api.response("fromMain", (data) => { - setModules(data); - setLoading(false); - }); - } else { - //Get modules from localStorage and add storage onChange handler - modulesFromBrowserStorage(); - window.addEventListener("storage", onStorageChange); - } - - //Reset the modules and remove the handler when the component unmounts - return () => { - setModules([]); - setLoading(true); - if (!isElectron()) window.removeEventListener("storage", onStorageChange); - }; - }, [modulesFromBrowserStorage, onStorageChange]); +//Component +const GridCards = () => { + const { modules, loading } = useAllModules(); //Display loading spinner while component loads //TODO switch to suspense maybe (react 18) @@ -78,27 +18,38 @@ const GridCards = () => { //Return grid of modules and "add module" card when the component has loaded return ( -
- {modules.map((module) => { - const { id, name, questions, disabled } = module; - return ( - } - leftBottom={{ - type: "link", - linkTo: `/module/${id}`, - linkAriaLabel: `View ${name}`, - linkText: "View", - }} - /> - ); - })} -
+ <> +
+ {modules.map((module) => { + const { id, name, questions, disabled } = module; + return ( + } + leftBottom={{ + type: "link", + linkTo: `/module/${id}`, + linkAriaLabel: `View ${name}`, + linkText: "View", + }} + /> + ); + })} +
+ {/* + //TODO display errors with react-toastify + //Need to check length of array? +
+ {errors && + errors.map((error, index) => { + return

{error}

; + })} +
*/} + ); }; diff --git a/src/Components/Main/Home/Components/ImportModule/CreateModule/CreateModule.jsx b/src/Components/Main/Home/Components/ImportModule/CreateModule/CreateModule.jsx index 1a5d1ac..66b3ca8 100644 --- a/src/Components/Main/Home/Components/ImportModule/CreateModule/CreateModule.jsx +++ b/src/Components/Main/Home/Components/ImportModule/CreateModule/CreateModule.jsx @@ -26,7 +26,7 @@ const CreateModule = ({ handleModalClose }) => { //Right now it just replaces the old module in the localStorage with the uploaded one. */ //Update localeStorage and tell the window that a new storage event occurred - localStorage.setItem(`repeatio-module-${module.id}`, JSON.stringify(module), { + localStorage.setItem(`repeatio-module-${module.id}`, JSON.stringify(module, null, "\t"), { sameSite: "strict", secure: true, }); diff --git a/src/Components/Main/Home/Components/ImportModule/ImportModule.jsx b/src/Components/Main/Home/Components/ImportModule/ImportModule.jsx index a316407..e08e894 100644 --- a/src/Components/Main/Home/Components/ImportModule/ImportModule.jsx +++ b/src/Components/Main/Home/Components/ImportModule/ImportModule.jsx @@ -83,7 +83,10 @@ const ImportModule = ({ handleModalClose }) => { const data = await file.text(); //Update localeStorage and tell the window that a new storage event occurred - localStorage.setItem(`repeatio-module-${JSON.parse(data).id}`, data, { sameSite: "strict", secure: true }); + localStorage.setItem(`repeatio-module-${JSON.parse(data).id}`, data, { + sameSite: "strict", + secure: true, + }); window.dispatchEvent(new Event("storage")); } catch (error) { console.error(error); diff --git a/src/Components/Main/Home/hooks/useAllModules.js b/src/Components/Main/Home/hooks/useAllModules.js new file mode 100644 index 0000000..5a59122 --- /dev/null +++ b/src/Components/Main/Home/hooks/useAllModules.js @@ -0,0 +1,83 @@ +import { useState, useEffect, useCallback } from "react"; + +//Functions +import isElectron from "is-electron"; +import fetchModuleFromPublicFolder from "../../../../functions/fetchModuleFromPublicFolder.js"; + +// Return the whole localStorage +const useAllModules = () => { + const [loading, setLoading] = useState(true); + const [modules, setModules] = useState([]); + const [errors, setErrors] = useState([]); + + //Get the modules from the localStorage and set the module state + //Updates every time localeStorage changes + const modulesFromBrowserStorage = useCallback(async () => { + //Setup variables for the module and possible errors + let localStorageModules = []; + let moduleErrors = []; + + Object.entries(localStorage).forEach((key) => { + if (key[0].startsWith("repeatio-module")) { + //Get item, transform to object, on error add to moduleErrors array + try { + const module = localStorage.getItem(key[0]); + localStorageModules.push(JSON.parse(module)); + } catch (error) { + console.warn(`${key[0]}: ${error.message}`); + moduleErrors.push(`${key[0]}: ${error.message}`); + } + } + }); + + //get the data from the public folder (types_1) + const dataFromPublicFolder = await fetchModuleFromPublicFolder(); + + //When able to fetch the data from the public folder, combine them else just show localStorage. + //This is useful for when the user is offline + if (dataFromPublicFolder !== undefined) { + setModules([...localStorageModules, dataFromPublicFolder]); + } else { + setModules(localStorageModules); + } + + //Update states + setErrors(moduleErrors); + setLoading(false); + }, []); + + //Refetch the modules if the localeStorage changes + const onStorageChange = useCallback(() => { + setLoading(true); + modulesFromBrowserStorage(); + }, [modulesFromBrowserStorage]); + + //Fetch data for all modules by reading all repeatio files in documents folder / locale storage (in browser) + useEffect(() => { + if (isElectron()) { + // Send a message to the main process + window.api.request("toMain", ["getModules"]); + + // Called when message received from main process + window.api.response("fromMain", (data) => { + setModules(data); + setLoading(false); + }); + } else { + //Get modules from localStorage and add storage onChange handler + modulesFromBrowserStorage(); + window.addEventListener("storage", onStorageChange); + } + + //Reset the modules and remove the handler when the component unmounts + return () => { + setModules([]); + setLoading(true); + if (!isElectron()) window.removeEventListener("storage", onStorageChange); + }; + }, [modulesFromBrowserStorage, onStorageChange]); + + return { modules, loading, errors }; +}; + +export default useAllModules; diff --git a/src/Context/ModuleContext.js b/src/Context/ModuleContext.js index 7f25ea0..4be8cb0 100644 --- a/src/Context/ModuleContext.js +++ b/src/Context/ModuleContext.js @@ -1,5 +1,8 @@ import { createContext, useMemo, useState, useEffect, useCallback } from "react"; + +//Functions import isElectron from "is-electron"; +import fetchModuleFromPublicFolder from "../functions/fetchModuleFromPublicFolder.js"; //Create Question Context export const ModuleContext = createContext([]); @@ -20,36 +23,25 @@ export const ModuleProvider = (props) => { //TODO move setFilteredQuestions/setInitialData into a callback - // Callback - //All questions + //Get the module data from the localStorage of the browser const getDataFromBrowser = useCallback(async () => { - //Fetch data from public folder - let dataFromPublic; - try { - const data = await fetch("data.json", { mode: "no-cors" }); - dataFromPublic = await data.json(); - } catch (error) { - console.log(error); - } + let module; - //Fetch data from the locale Storage - let storageModules = []; - Object.entries(localStorage).forEach((key) => { - if (key[0].startsWith("repeatio-module")) { - const repeatioModule = localStorage.getItem(key[0]); - storageModules.push(JSON.parse(repeatioModule)); + if (moduleContextID !== "types_1") { + //Fetch data from the locale Storage + try { + module = JSON.parse(localStorage.getItem(`repeatio-module-${moduleContextID}`)); + } catch (error) { + console.warn(error.message); } - }); - - //Combine the data from the public folder and the locale storage - const modules = [...storageModules, dataFromPublic]; - - //Find the correct module with the contextID - const correctModule = modules.find((module) => module.id === moduleContextID); + } else { + //Fetch data from public folder + module = await fetchModuleFromPublicFolder(); + } //Set the data - setInitialData(correctModule); - setFilteredQuestions(correctModule.questions); + setInitialData(module); + setFilteredQuestions(module?.questions); }, [moduleContextID]); //Get all Questions from the file system / locale storage and provide them @@ -81,7 +73,7 @@ export const ModuleProvider = (props) => { //Update the localStorage/filesystem if initialData changes useEffect(() => { //Don't update the storage if the data is undefined or from the public folder (id: types_1) - if (initialData === undefined || initialData.length < 1 || initialData.id === "types_1") { + if (initialData === undefined || initialData?.length < 1 || initialData?.id === "types_1") { return; } diff --git a/src/functions/fetchModuleFromPublicFolder.js b/src/functions/fetchModuleFromPublicFolder.js new file mode 100644 index 0000000..fcfa0a9 --- /dev/null +++ b/src/functions/fetchModuleFromPublicFolder.js @@ -0,0 +1,16 @@ +export default async function fetchModuleFromPublicFolder() { + try { + const data = await fetch("data.json", { mode: "no-cors" }); + const toJsObject = await data.json(); + + if (data.ok) { + return toJsObject; + } else { + console.warn(toJsObject.message || toJsObject.statusText); + return; + } + } catch (error) { + console.warn(error.message); + return; + } +}