Skip to content

Commit

Permalink
🐛Fixed infinite loading of public folder module when offline (#149)
Browse files Browse the repository at this point in the history
  • Loading branch information
Rllyyy committed Jul 13, 2022
1 parent bc2accd commit b3f8131
Show file tree
Hide file tree
Showing 6 changed files with 159 additions and 114 deletions.
123 changes: 37 additions & 86 deletions src/Components/Main/Home/Components/GridCards.js
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -78,27 +18,38 @@ const GridCards = () => {

//Return grid of modules and "add module" card when the component has loaded
return (
<div className='grid-cards'>
{modules.map((module) => {
const { id, name, questions, disabled } = module;
return (
<Card
key={id}
disabled={disabled}
type='module'
title={`${name} (${id})`}
description={`${questions.length} Questions`}
icon={<ProgressPie progress={55} />}
leftBottom={{
type: "link",
linkTo: `/module/${id}`,
linkAriaLabel: `View ${name}`,
linkText: "View",
}}
/>
);
})}
</div>
<>
<div className='grid-cards'>
{modules.map((module) => {
const { id, name, questions, disabled } = module;
return (
<Card
key={id}
disabled={disabled}
type='module'
title={`${name} (${id})`}
description={`${questions?.length} Questions`}
icon={<ProgressPie progress={55} />}
leftBottom={{
type: "link",
linkTo: `/module/${id}`,
linkAriaLabel: `View ${name}`,
linkText: "View",
}}
/>
);
})}
</div>
{/*
//TODO display errors with react-toastify
//Need to check length of array?
<div className='errors'>
{errors &&
errors.map((error, index) => {
return <p key={index}>{error}</p>;
})}
</div> */}
</>
);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
83 changes: 83 additions & 0 deletions src/Components/Main/Home/hooks/useAllModules.js
Original file line number Diff line number Diff line change
@@ -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;
44 changes: 18 additions & 26 deletions src/Context/ModuleContext.js
Original file line number Diff line number Diff line change
@@ -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([]);
Expand All @@ -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
Expand Down Expand Up @@ -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;
}

Expand Down
16 changes: 16 additions & 0 deletions src/functions/fetchModuleFromPublicFolder.js
Original file line number Diff line number Diff line change
@@ -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;
}
}

0 comments on commit b3f8131

Please sign in to comment.