From fada6cca0e3ca6c1c3ef5b9b5656dd24d152bc7c Mon Sep 17 00:00:00 2001 From: Niklas Fischer Date: Tue, 20 Dec 2022 10:22:03 +0100 Subject: [PATCH] :straight_ruler:Added support for dynamic viewport units in question (#225)+:bug:Fixed overflow on mobile landscape (#226)+:hammer:Converted JavaScript files to TypeScript (#216) --- .vscode/cspell.json | 3 +- package.json | 1 + src/components/Footer/Footer.css | 5 +- src/components/GridCards/GridCards.css | 4 +- .../{GridCards.jsx => GridCards.tsx} | 2 +- .../Home/{Modules.js => Modules.tsx} | 42 +++++----- src/components/Question/Question.css | 16 +--- src/components/Question/Question.js | 2 +- .../ExtendedMatch/ExtendedMatch.js | 2 +- .../MultipleChoice/MultipleChoice.js | 2 +- .../MultipleResponse/MultipleResponse.js | 2 +- .../Question/__tests__/Question.test.js | 2 +- .../components/Actions/DeleteQuestion.tsx | 5 +- .../QuestionNavigation/QuestionNavigation.jsx | 2 +- src/components/Question/useQuestion.js | 2 +- .../AnswerOptionsEditor.tsx | 8 +- .../QuestionEditor/QuestionEditor.cy.tsx | 2 +- .../QuestionEditor/QuestionEditor.tsx | 23 +++--- src/components/QuestionList/QuestionList.js | 2 +- src/components/module/ModuleNotFound.cy.tsx | 2 +- ...{ModuleNotFound.jsx => ModuleNotFound.tsx} | 13 +++- src/components/module/module.tsx | 28 ++++--- .../{moduleContext.js => moduleContext.tsx} | 53 ++++++++----- src/index.css | 76 +++++++------------ src/index.js | 4 +- src/pages/index.js | 2 +- src/pages/module/{module.jsx => module.tsx} | 0 src/utils/{saveFile.js => saveFile.ts} | 28 ++++--- src/utils/shuffleArray.js | 12 --- src/utils/shuffleArray.ts | 15 ++++ tsconfig.json | 3 +- yarn.lock | 5 ++ 32 files changed, 195 insertions(+), 173 deletions(-) rename src/components/GridCards/{GridCards.jsx => GridCards.tsx} (52%) rename src/components/Home/{Modules.js => Modules.tsx} (84%) rename src/components/module/{ModuleNotFound.jsx => ModuleNotFound.tsx} (88%) rename src/components/module/{moduleContext.js => moduleContext.tsx} (61%) rename src/pages/module/{module.jsx => module.tsx} (100%) rename src/utils/{saveFile.js => saveFile.ts} (53%) delete mode 100644 src/utils/shuffleArray.js create mode 100644 src/utils/shuffleArray.ts diff --git a/.vscode/cspell.json b/.vscode/cspell.json index cead404..d2ea5c7 100644 --- a/.vscode/cspell.json +++ b/.vscode/cspell.json @@ -42,7 +42,8 @@ "tsmerge", "unicode", "USEEFFECTS", - "vmatrix" + "vmatrix", + "wicg" ], // flagWords - list of words to be always considered incorrect // This is useful for offensive words and common spelling errors. diff --git a/package.json b/package.json index 050b70c..34f9413 100644 --- a/package.json +++ b/package.json @@ -121,6 +121,7 @@ }, "devDependencies": { "@types/react-modal": "^3.13.1", + "@types/wicg-file-system-access": "^2020.9.5", "cypress-plugin-tab": "^1.0.5", "electron": "^17.4.7", "electron-builder": "^23.6.0" diff --git a/src/components/Footer/Footer.css b/src/components/Footer/Footer.css index 55d166c..610d48d 100644 --- a/src/components/Footer/Footer.css +++ b/src/components/Footer/Footer.css @@ -38,7 +38,6 @@ footer a:focus-visible { outline-offset: 3px; } - @media screen and (pointer: fine) and (hover: hover) { footer a:hover { color: var(--custom-border-color-lighter); @@ -54,7 +53,7 @@ footer a:focus-visible { } } -@media(min--moz-device-pixel-ratio:0) and (max-width: 650px) { +@media (min--moz-device-pixel-ratio: 0) and (max-width: 650px) { footer { padding-bottom: 50px; } @@ -66,4 +65,4 @@ footer a:focus-visible { footer ul li { font-size: 1.25rem; } -} \ No newline at end of file +} diff --git a/src/components/GridCards/GridCards.css b/src/components/GridCards/GridCards.css index 8713774..49e933a 100644 --- a/src/components/GridCards/GridCards.css +++ b/src/components/GridCards/GridCards.css @@ -1,5 +1,5 @@ .grid-cards { - margin: 10px 0px 20px 0px; + margin: 10px 0px 15px 0px; display: grid; grid-template-columns: repeat(auto-fill, minmax(370px, 1fr)); grid-auto-rows: 1fr; @@ -11,4 +11,4 @@ .grid-cards { grid-template-columns: repeat(auto-fill, 100%); } -} \ No newline at end of file +} diff --git a/src/components/GridCards/GridCards.jsx b/src/components/GridCards/GridCards.tsx similarity index 52% rename from src/components/GridCards/GridCards.jsx rename to src/components/GridCards/GridCards.tsx index 6e759fa..f7ae571 100644 --- a/src/components/GridCards/GridCards.jsx +++ b/src/components/GridCards/GridCards.tsx @@ -1,5 +1,5 @@ import "./GridCards.css"; -export const GridCards = ({ children }) => { +export const GridCards = ({ children }: { children: React.ReactNode }) => { return
{children}
; }; diff --git a/src/components/Home/Modules.js b/src/components/Home/Modules.tsx similarity index 84% rename from src/components/Home/Modules.js rename to src/components/Home/Modules.tsx index 291a098..0863412 100644 --- a/src/components/Home/Modules.js +++ b/src/components/Home/Modules.tsx @@ -4,7 +4,7 @@ import { toast } from "react-toastify"; import { fetchModuleFromPublicFolder } from "../../utils/fetchModuleFromPublicFolder.js"; //Components -import { GridCards } from "../GridCards/GridCards.jsx"; +import { GridCards } from "../GridCards/GridCards"; import { Card, LinkElement } from "../Card/Card"; import { PopoverButton, PopoverMenu, PopoverMenuItem } from "../Card/Popover"; import { Spinner } from "../Spinner/Spinner.js"; @@ -15,7 +15,11 @@ import { TbFileExport } from "react-icons/tb"; import { BiTrash } from "react-icons/bi"; //Functions -import { saveFile } from "../../utils/saveFile.js"; +import { saveFile } from "../../utils/saveFile"; + +//Interfaces and Types +import { IModule } from "./CreateModule"; +import { parseJSON } from "../../utils/parseJSON"; //Component export const Modules = () => { @@ -33,12 +37,11 @@ export const Modules = () => { return ( {modules?.map((module) => { - const { id, name, questions, disabled } = module; + const { id, name, questions } = module; return ( { // Return the whole localStorage const useAllModules = () => { const [loading, setLoading] = useState(true); - const [modules, setModules] = useState([]); - const [errors, setErrors] = useState([]); + 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 = []; + let localStorageModules: IModule[] = []; + let moduleErrors: string[] = []; 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)); + const moduleJSON = parseJSON(module); + if (moduleJSON !== undefined && moduleJSON !== null) { + localStorageModules.push(moduleJSON); + } } catch (error) { - toast.warn(`${key[0]}: ${error.message}`); - moduleErrors.push(`${key[0]}: ${error.message}`); + if (error instanceof Error) { + toast.warn(`${key[0]}: ${error.message}`); + moduleErrors.push(`${key[0]}: ${error.message}`); + } } } }); @@ -114,10 +122,10 @@ const useAllModules = () => { useEffect(() => { if (isElectron()) { // Send a message to the main process - window.api.request("toMain", ["getModules"]); + (window as any).api.request("toMain", ["getModules"]); // Called when message received from main process - window.api.response("fromMain", (data) => { + (window as any).api.response("fromMain", (data: IModule[]) => { setModules(data); setLoading(false); }); @@ -140,7 +148,7 @@ const useAllModules = () => { //Hook to use the functions inside the Popover component const useHomePopover = () => { - const [anchorEl, setAnchorEl] = useState(null); + const [anchorEl, setAnchorEl] = useState(null); //Reset anchor if component unmounts useLayoutEffect(() => { @@ -150,7 +158,7 @@ const useHomePopover = () => { }, []); //Set the anchor - const handlePopoverButtonClick = (event) => { + const handlePopoverButtonClick = (event: React.MouseEvent) => { setAnchorEl(event.currentTarget); }; @@ -162,7 +170,7 @@ const useHomePopover = () => { //Handle deletion of module const handleDelete = () => { //Get id of module by custom attribute - const moduleID = anchorEl.getAttribute("data-target"); + const moduleID = anchorEl?.getAttribute("data-target"); //Prevent deletion of example module as that is saved in the public folder if (moduleID === "types_1") { @@ -202,7 +210,7 @@ const useHomePopover = () => { } //Get id of the module from the button - const moduleID = anchorEl.getAttribute("data-target"); + const moduleID = anchorEl?.getAttribute("data-target"); let file; if (moduleID !== "types_1") { diff --git a/src/components/Question/Question.css b/src/components/Question/Question.css index 40723c4..21f4de5 100644 --- a/src/components/Question/Question.css +++ b/src/components/Question/Question.css @@ -1,7 +1,7 @@ .question-form { position: relative; /*40px represents the padding (top+bottom) of the main component defined in index.css, so the user doesn't need scroll and the navigation is always at the bottom*/ - height: calc(100vh - 40px); + height: calc(100dvh - 40px); width: 100%; background-color: white; border-radius: 5px; @@ -406,12 +406,9 @@ } /* Media query for phones */ -/* Second query is for development on desktop */ -@media only screen and (pointer: coarse), only screen and (pointer: fine) and (max-width: 650px) { +@media only screen and (max-width: 650px) { .question-form { - height: calc(100vh - 120px); - /*The viewport of mobile devices includes the address bar. Because why not. */ - margin-bottom: 0; + height: calc(100dvh - 70px); /* Height of the header + padding of main - 3px to look beetter */ } .question-data { @@ -450,13 +447,6 @@ } } -/* For desktop very small */ -@media only screen and (pointer: fine) and (max-width: 650px) { - .question-form { - height: calc(100vh - 70px); - } -} - /* On mobile firefox the scrollbar is part of the html viewport. In other browsers it is not */ @media (min--moz-device-pixel-ratio: 0) and (max-width: 650px) { .question-data { diff --git a/src/components/Question/Question.js b/src/components/Question/Question.js index 912de3b..d3a0ac3 100644 --- a/src/components/Question/Question.js +++ b/src/components/Question/Question.js @@ -23,7 +23,7 @@ import { EditQuestion } from "./components/Actions/EditQuestion"; import { BookmarkQuestion } from "./components/Actions/BookmarkQuestion"; //Context -import { ModuleContext } from "../module/moduleContext.js"; +import { ModuleContext } from "../module/moduleContext"; //Hooks import { useQuestion } from "./useQuestion.js"; diff --git a/src/components/Question/QuestionTypes/ExtendedMatch/ExtendedMatch.js b/src/components/Question/QuestionTypes/ExtendedMatch/ExtendedMatch.js index a91f951..ed4321a 100644 --- a/src/components/Question/QuestionTypes/ExtendedMatch/ExtendedMatch.js +++ b/src/components/Question/QuestionTypes/ExtendedMatch/ExtendedMatch.js @@ -17,7 +17,7 @@ import "./ExtendedMatch.css"; //Import functions import { isEqual } from "lodash"; -import { shuffleArray } from "../../../../utils/shuffleArray.js"; +import { shuffleArray } from "../../../../utils/shuffleArray"; //I am really not proud of this component :/ and refactor it for a future release //Each line in the canvas is an object in the lines array diff --git a/src/components/Question/QuestionTypes/MultipleChoice/MultipleChoice.js b/src/components/Question/QuestionTypes/MultipleChoice/MultipleChoice.js index baa093d..f559d7f 100644 --- a/src/components/Question/QuestionTypes/MultipleChoice/MultipleChoice.js +++ b/src/components/Question/QuestionTypes/MultipleChoice/MultipleChoice.js @@ -21,7 +21,7 @@ import "katex/dist/katex.min.css"; import "./MultipleChoice.css"; //Import functions -import { shuffleArray } from "../../../../utils/shuffleArray.js"; +import { shuffleArray } from "../../../../utils/shuffleArray"; /* Component */ export const MultipleChoice = forwardRef(({ options, formDisabled }, ref) => { diff --git a/src/components/Question/QuestionTypes/MultipleResponse/MultipleResponse.js b/src/components/Question/QuestionTypes/MultipleResponse/MultipleResponse.js index 5070cc4..7a2589f 100644 --- a/src/components/Question/QuestionTypes/MultipleResponse/MultipleResponse.js +++ b/src/components/Question/QuestionTypes/MultipleResponse/MultipleResponse.js @@ -21,7 +21,7 @@ import FormControl from "@mui/material/FormControl"; import "./MultipleResponse.css"; //Import functions -import { shuffleArray } from "../../../../utils/shuffleArray.js"; +import { shuffleArray } from "../../../../utils/shuffleArray"; //Component export const MultipleResponse = forwardRef(({ options, formDisabled }, ref) => { diff --git a/src/components/Question/__tests__/Question.test.js b/src/components/Question/__tests__/Question.test.js index 8f83e44..c946d9e 100644 --- a/src/components/Question/__tests__/Question.test.js +++ b/src/components/Question/__tests__/Question.test.js @@ -1,7 +1,7 @@ import { screen, render, cleanup } from "@testing-library/react"; import user from "@testing-library/user-event"; import { Question } from "../Question.js"; -import { ModuleContext } from "../../module/moduleContext.js"; +import { ModuleContext } from "../../module/moduleContext"; import { Router, Route, Switch, MemoryRouter } from "react-router-dom"; import { createMemoryHistory } from "history"; diff --git a/src/components/Question/components/Actions/DeleteQuestion.tsx b/src/components/Question/components/Actions/DeleteQuestion.tsx index c733ca5..23979b5 100644 --- a/src/components/Question/components/Actions/DeleteQuestion.tsx +++ b/src/components/Question/components/Actions/DeleteQuestion.tsx @@ -5,7 +5,7 @@ import { toast } from "react-toastify"; import { getBookmarkedLocalStorageItem } from "./BookmarkQuestion"; //Context -import { ModuleContext } from "../../../module/moduleContext.js"; +import { IModuleContext, ModuleContext } from "../../../module/moduleContext"; //TODO add moduleID as Component param not useParams @@ -33,8 +33,7 @@ export const DeleteQuestion = ({ questionID, disabled, ...props }: IDeleteQuesti let history = useHistory(); //Access Module - //TODO fix this any - const { moduleData, setModuleData, filteredQuestions } = useContext(ModuleContext); + const { moduleData, setModuleData, filteredQuestions } = useContext(ModuleContext); //Delete Question from storage const handleDelete = () => { diff --git a/src/components/Question/components/QuestionNavigation/QuestionNavigation.jsx b/src/components/Question/components/QuestionNavigation/QuestionNavigation.jsx index 39a1302..98729b3 100644 --- a/src/components/Question/components/QuestionNavigation/QuestionNavigation.jsx +++ b/src/components/Question/components/QuestionNavigation/QuestionNavigation.jsx @@ -3,7 +3,7 @@ import { useParams, useHistory, useLocation } from "react-router-dom"; import { toast } from "react-toastify"; //Context -import { ModuleContext } from "../../../module/moduleContext.js"; +import { ModuleContext } from "../../../module/moduleContext"; //Navigation svg from https://tablericons.com diff --git a/src/components/Question/useQuestion.js b/src/components/Question/useQuestion.js index 7af8871..7234c3d 100644 --- a/src/components/Question/useQuestion.js +++ b/src/components/Question/useQuestion.js @@ -3,7 +3,7 @@ import { useParams } from "react-router-dom"; import { useQuestionNavigation } from "./components/QuestionNavigation/QuestionNavigation.jsx"; //Context -import { ModuleContext } from "../module/moduleContext.js"; +import { ModuleContext } from "../module/moduleContext"; export const useQuestion = () => { //States diff --git a/src/components/QuestionEditor/AnswerOptionsEditor/AnswerOptionsEditor.tsx b/src/components/QuestionEditor/AnswerOptionsEditor/AnswerOptionsEditor.tsx index 8471a1d..ba3c576 100644 --- a/src/components/QuestionEditor/AnswerOptionsEditor/AnswerOptionsEditor.tsx +++ b/src/components/QuestionEditor/AnswerOptionsEditor/AnswerOptionsEditor.tsx @@ -126,7 +126,11 @@ export const AnswerOptionsEditor = ({ const { selectionStart, selectionEnd } = ref.current; if (selectionStart !== selectionEnd) { handleEditorChange({ - tempText: removeBracketsFromSelection((answerValues as IGapText)?.tempText, selectionStart, selectionEnd), + tempText: removeBracketsFromSelection( + (answerValues as IGapText)?.tempText || "", + selectionStart, + selectionEnd + ), }); ref.current.focus(); } else { @@ -170,7 +174,7 @@ export const AnswerOptionsEditor = ({ >; - tempText: string; + tempText?: string; } export type TAnswerOptions = IMultipleChoice[] | IMultipleResponse[] | IGapText; @@ -126,8 +126,7 @@ export const Form = ({ const { search } = useLocation(); //Context - //TODO fix this any - const { moduleData, setModuleData } = useContext(ModuleContext); + const { moduleData, setModuleData } = useContext(ModuleContext); //Fetch Data from Context useLayoutEffect(() => { @@ -140,13 +139,15 @@ export const Form = ({ //Combine the text and correctGapValues of gap-text to a variable that is used for the input //Prevent Safari because lookbehind support: https://bugs.webkit.org/show_bug.cgi?id=174931 //TODO check if safari ever supports this feature - if (questionFromContext.type === "gap-text" && !isSafari) { + if (questionFromContext?.type === "gap-text" && !isSafari) { questionFromContext = { ...questionFromContext, - answerOptions: { tempText: getGapTextTempText(questionFromContext.answerOptions) }, + answerOptions: { tempText: getGapTextTempText(questionFromContext.answerOptions as IGapText) }, }; } + if (questionFromContext === undefined) return; + //!Somehow it keeps the order of the answer options from the question //If this isn't the case anymore when using the storage, pass the question setQuestion({ ...questionFromContext }); @@ -266,7 +267,7 @@ export const Form = ({ //Adding or updating a question if (!prevQuestionID) { //If the user is adding a question (not given prevQuestion), push the new question to the end of the array - moduleData?.questions?.push(output); + moduleData?.questions?.push(output as IQuestion); setModuleData({ ...moduleData, questions: moduleData?.questions }); } else { //Handle updating a question @@ -276,7 +277,7 @@ export const Form = ({ //If the user changes the id (index <= -1), the question gets inserted at that position if (index > -1) { - moduleData.questions.splice(index, 1, output); + moduleData.questions.splice(index, 1, output as IQuestion); setModuleData({ ...moduleData, questions: moduleData.questions }); window.dispatchEvent(new Event("storage")); } else { @@ -290,7 +291,7 @@ export const Form = ({ } //Insert and update context - moduleData.questions.splice(index, 1, output); + moduleData.questions.splice(index, 1, output as IQuestion); setModuleData({ ...moduleData, questions: moduleData.questions }); //Navigate to new path with new id @@ -437,7 +438,7 @@ function getAnswerOptionsError({ if ( type === "gap-text" && "tempText" in (answerOptions as IGapText) && - (answerOptions as IGapText).tempText.startsWith("|") + (answerOptions as IGapText).tempText?.startsWith("|") ) { return "Can't start with this key! If want to render a table wrap the markdown for the table in
(line break) Markdown (line break)
."; } diff --git a/src/components/QuestionList/QuestionList.js b/src/components/QuestionList/QuestionList.js index a3ee187..732c382 100644 --- a/src/components/QuestionList/QuestionList.js +++ b/src/components/QuestionList/QuestionList.js @@ -3,7 +3,7 @@ import { useState, useEffect, useContext } from "react"; import { useParams, useHistory } from "react-router-dom"; //Context -import { ModuleContext } from "../module/moduleContext.js"; +import { ModuleContext } from "../module/moduleContext"; //Components import { Spinner } from "../Spinner/Spinner"; diff --git a/src/components/module/ModuleNotFound.cy.tsx b/src/components/module/ModuleNotFound.cy.tsx index 46e6a62..b63dec6 100644 --- a/src/components/module/ModuleNotFound.cy.tsx +++ b/src/components/module/ModuleNotFound.cy.tsx @@ -1,6 +1,6 @@ /// -import { UserModulesList } from "./ModuleNotFound.jsx"; +import { UserModulesList } from "./ModuleNotFound"; import { Router } from "react-router-dom"; import { createMemoryHistory } from "history"; diff --git a/src/components/module/ModuleNotFound.jsx b/src/components/module/ModuleNotFound.tsx similarity index 88% rename from src/components/module/ModuleNotFound.jsx rename to src/components/module/ModuleNotFound.tsx index 14e255a..921ba27 100644 --- a/src/components/module/ModuleNotFound.jsx +++ b/src/components/module/ModuleNotFound.tsx @@ -1,5 +1,7 @@ import { useState, useLayoutEffect } from "react"; import { Link, useHistory, useParams } from "react-router-dom"; +import { IParams } from "../../utils/types"; +import { IModule } from "../Home/CreateModule"; //Css import "./ModuleNotFound.css"; @@ -7,7 +9,7 @@ import "./ModuleNotFound.css"; //Get all items from localStorage and filter out everything that is not a module //inspired by: https://ui.mantine.dev/category/error-pages export const ModuleNotFound = () => { - const { moduleID } = useParams(); + const { moduleID } = useParams(); let history = useHistory(); return ( @@ -38,12 +40,15 @@ export const ModuleNotFound = () => { ); }; +type TOrder = "alphabetical"; +type ModuleItem = Pick; + //Render list of users modules export const UserModulesList = () => { - const [modules, setModules] = useState([]); + const [modules, setModules] = useState([]); //Update the order of the users modules - const updateModuleOrder = ({ order }) => { + const updateModuleOrder = (order: TOrder) => { //Take all elements from the localStorage that include module and then return a new object with the id and name let idAndNameOfModules = Object.entries(localStorage) .filter(([key, _]) => key.includes("module")) @@ -70,7 +75,7 @@ export const UserModulesList = () => { //On initial render order the modules from the localStorage alphabetically useLayoutEffect(() => { - updateModuleOrder({ order: "alphabetical" }); + updateModuleOrder("alphabetical"); return () => { setModules([]); diff --git a/src/components/module/module.tsx b/src/components/module/module.tsx index b569c5a..3bfacb3 100644 --- a/src/components/module/module.tsx +++ b/src/components/module/module.tsx @@ -1,17 +1,17 @@ import React, { useState, useContext, useCallback, useLayoutEffect, useEffect } from "react"; import { useHistory, useParams } from "react-router-dom"; -import { ModuleContext } from "./moduleContext.js"; +import { ModuleContext } from "./moduleContext"; import packageJSON from "../../../package.json"; //Components -import { GridCards } from "../GridCards/GridCards.jsx"; +import { GridCards } from "../GridCards/GridCards"; import { SiteHeading } from "../SiteHeading/SiteHeading"; import { Card, LinkElement, ButtonElement } from "../Card/Card"; import { Spinner } from "../Spinner/Spinner"; import { IQuestion, QuestionEditor } from "../QuestionEditor/QuestionEditor"; import { PopoverButton, PopoverMenu, PopoverMenuItem } from "../Card/Popover"; import { toast } from "react-toastify"; -import { ModuleNotFound } from "./ModuleNotFound.jsx"; +import { ModuleNotFound } from "./ModuleNotFound"; //Icons import { AiOutlineBook, AiOutlineEdit } from "react-icons/ai"; @@ -23,10 +23,11 @@ import { TbFileExport, TbFileImport } from "react-icons/tb"; //functions import { shuffleArray } from "../../utils/shuffleArray"; -import { saveFile } from "../../utils/saveFile.js"; +import { saveFile } from "../../utils/saveFile"; //Interfaces and types import { IParams } from "../../utils/types.js"; +import { IModuleContext } from "./moduleContext"; import { getBookmarkedLocalStorageItem, getBookmarkedQuestionsFromModule, @@ -53,8 +54,7 @@ export const Module = () => { const [showModal, setShowModal] = useState(false); //context - //TODO fix this any - const { setFilteredQuestions, moduleData, setContextModuleID } = useContext(ModuleContext); + const { setFilteredQuestions, moduleData, setContextModuleID } = useContext(ModuleContext); //History let history = useHistory(); @@ -65,13 +65,6 @@ export const Module = () => { /* USEEFFECTS */ //Update the module state by using the data from the context useLayoutEffect(() => { - //Module is loading - if (moduleData?.length === 0 || moduleData === undefined) { - setLoading(true); - setError(false); - return; - } - //Context returned nothing because module wasn't found if (moduleData === null) { setError(true); @@ -79,6 +72,12 @@ export const Module = () => { return; } + if (Object.keys(moduleData)?.length === 0 || moduleData === undefined) { + setLoading(true); + setError(false); + return; + } + //Update module if module was found setModule(moduleData); setError(false); @@ -281,8 +280,7 @@ const BookmarkedQuestionsBottom = () => { const { moduleID } = useParams(); //Context - //TODO fix this - const { setFilteredQuestions, moduleData } = useContext(ModuleContext); + const { setFilteredQuestions, moduleData } = useContext(ModuleContext); //Reset anchor if component unmounts useLayoutEffect(() => { diff --git a/src/components/module/moduleContext.js b/src/components/module/moduleContext.tsx similarity index 61% rename from src/components/module/moduleContext.js rename to src/components/module/moduleContext.tsx index 76078fc..04012aa 100644 --- a/src/components/module/moduleContext.js +++ b/src/components/module/moduleContext.tsx @@ -1,18 +1,28 @@ -import { createContext, useMemo, useState, useEffect, useCallback } from "react"; +import React, { createContext, useMemo, useState, useEffect, useCallback } from "react"; import { toast } from "react-toastify"; //Functions import isElectron from "is-electron"; import { fetchModuleFromPublicFolder } from "../../utils/fetchModuleFromPublicFolder.js"; +import { parseJSON } from "../../utils/parseJSON"; +import { IModule } from "../Home/CreateModule"; + +export interface IModuleContext { + moduleData: IModule; + setModuleData: React.Dispatch>; + setContextModuleID: React.Dispatch>; + filteredQuestions: IModule["questions"]; + setFilteredQuestions: React.Dispatch>; +} //Create Question Context -export const ModuleContext = createContext([]); +export const ModuleContext = createContext({} as IModuleContext); //Provide the data to all children -export const ModuleProvider = (props) => { - const [initialData, setInitialData] = useState([]); - const [filteredQuestions, setFilteredQuestions] = useState([]); - const [moduleContextID, setContextModuleID] = useState(""); +export const ModuleProvider = ({ children }: { children: React.ReactNode }) => { + const [initialData, setInitialData] = useState({} as IModule); + const [filteredQuestions, setFilteredQuestions] = useState([]); + const [moduleContextID, setContextModuleID] = useState(""); //Change every time module name changes const initialDataProvider = useMemo(() => ({ initialData, setInitialData }), [initialData, setInitialData]); @@ -31,9 +41,12 @@ export const ModuleProvider = (props) => { if (moduleContextID !== "types_1") { //Fetch data from the locale Storage try { - module = JSON.parse(localStorage.getItem(`repeatio-module-${moduleContextID}`)); + //module = JSON.parse(localStorage.getItem(`repeatio-module-${moduleContextID}`)); + module = parseJSON(localStorage.getItem(`repeatio-module-${moduleContextID}`)); } catch (error) { - toast.warn(error.message); + if (error instanceof Error) { + toast.warn(error.message); + } } } else { //Fetch data from public folder @@ -52,10 +65,10 @@ export const ModuleProvider = (props) => { //Get the data from the locale file system when using the electron application else (when using the website) get the data from the public folder/browser storage if (isElectron()) { // Send a message to the main process - window.api.request("toMain", ["getModule", moduleContextID]); + (window as any).api.request("toMain", ["getModule", moduleContextID]); // Called when message received from main process - window.api.response("fromMain", (data) => { + (window as any).api.response("fromMain", (data: IModule) => { setInitialData(data); setFilteredQuestions(data.questions); }); @@ -66,7 +79,7 @@ export const ModuleProvider = (props) => { //Cleanup return () => { - setInitialData([]); + setInitialData({} as IModule); setFilteredQuestions([]); }; }, [moduleContextID, getDataFromBrowser]); @@ -74,7 +87,12 @@ 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" || !initialData) { + if ( + !initialData || + initialData === undefined || + Object.keys(initialData)?.length < 1 || + initialData?.id === "types_1" + ) { return; } @@ -83,12 +101,11 @@ export const ModuleProvider = (props) => { //TODO save to filesystem } else if (!isElectron()) { try { - localStorage.setItem(`repeatio-module-${initialData.id}`, JSON.stringify(initialData, null, "\t"), { - sameSite: "strict", - secure: true, - }); + localStorage.setItem(`repeatio-module-${initialData.id}`, JSON.stringify(initialData, null, "\t")); } catch (error) { - toast.warn(error.message); + if (error instanceof Error) { + toast.warn(error.message); + } } } }, [initialData]); @@ -103,7 +120,7 @@ export const ModuleProvider = (props) => { setFilteredQuestions: filterProvider.setFilteredQuestions, }} > - {props.children} + {children} ); }; diff --git a/src/index.css b/src/index.css index 067907c..464d454 100644 --- a/src/index.css +++ b/src/index.css @@ -34,76 +34,60 @@ https://google-webfonts-helper.herokuapp.com/fonts /* roboto-300 - latin */ @font-face { - font-family: 'Roboto'; + font-family: "Roboto"; font-style: normal; font-weight: 300; - src: url('../public/fonts/roboto-v27-latin-300.eot'); + src: url("../public/fonts/roboto-v27-latin-300.eot"); /* IE9 Compat Modes */ - src: local(''), - url('../public/fonts/roboto-v27-latin-300.woff2') format('woff2'), - /* Super Modern Browsers */ - url('../public/fonts/roboto-v27-latin-300.woff') format('woff'), - /* Modern Browsers */ - url('../public/fonts/roboto-v27-latin-300.ttf') format('truetype'), - /* Safari, Android, iOS */ - url('../public/fonts/roboto-v27-latin-300.svg#Roboto') format('svg'); + src: local(""), url("../public/fonts/roboto-v27-latin-300.woff2") format("woff2"), + /* Super Modern Browsers */ url("../public/fonts/roboto-v27-latin-300.woff") format("woff"), + /* Modern Browsers */ url("../public/fonts/roboto-v27-latin-300.ttf") format("truetype"), + /* Safari, Android, iOS */ url("../public/fonts/roboto-v27-latin-300.svg#Roboto") format("svg"); /* Legacy iOS */ font-display: swap; } /* roboto-regular - latin */ @font-face { - font-family: 'Roboto'; + font-family: "Roboto"; font-style: normal; font-weight: 400; - src: url('../public/fonts/roboto-v27-latin-regular.eot'); + src: url("../public/fonts/roboto-v27-latin-regular.eot"); /* IE9 Compat Modes */ - src: local(''), - url('../public/fonts/roboto-v27-latin-regular.woff2') format('woff2'), - /* Super Modern Browsers */ - url('../public/fonts/roboto-v27-latin-regular.woff') format('woff'), - /* Modern Browsers */ - url('../public/fonts/roboto-v27-latin-regular.ttf') format('truetype'), - /* Safari, Android, iOS */ - url('../public/fonts/roboto-v27-latin-regular.svg#Roboto') format('svg'); + src: local(""), url("../public/fonts/roboto-v27-latin-regular.woff2") format("woff2"), + /* Super Modern Browsers */ url("../public/fonts/roboto-v27-latin-regular.woff") format("woff"), + /* Modern Browsers */ url("../public/fonts/roboto-v27-latin-regular.ttf") format("truetype"), + /* Safari, Android, iOS */ url("../public/fonts/roboto-v27-latin-regular.svg#Roboto") format("svg"); /* Legacy iOS */ font-display: swap; } /* roboto-500 - latin */ @font-face { - font-family: 'Roboto'; + font-family: "Roboto"; font-style: normal; font-weight: 500; - src: url('../public/fonts/roboto-v27-latin-500.eot'); + src: url("../public/fonts/roboto-v27-latin-500.eot"); /* IE9 Compat Modes */ - src: local(''), - url('../public/fonts/roboto-v27-latin-500.woff2') format('woff2'), - /* Super Modern Browsers */ - url('../public/fonts/roboto-v27-latin-500.woff') format('woff'), - /* Modern Browsers */ - url('../public/fonts/roboto-v27-latin-500.ttf') format('truetype'), - /* Safari, Android, iOS */ - url('../public/fonts/roboto-v27-latin-500.svg#Roboto') format('svg'); + src: local(""), url("../public/fonts/roboto-v27-latin-500.woff2") format("woff2"), + /* Super Modern Browsers */ url("../public/fonts/roboto-v27-latin-500.woff") format("woff"), + /* Modern Browsers */ url("../public/fonts/roboto-v27-latin-500.ttf") format("truetype"), + /* Safari, Android, iOS */ url("../public/fonts/roboto-v27-latin-500.svg#Roboto") format("svg"); /* Legacy iOS */ font-display: swap; } /* roboto-700 - latin */ @font-face { - font-family: 'Roboto'; + font-family: "Roboto"; font-style: normal; font-weight: 700; - src: url('../public/fonts/roboto-v27-latin-700.eot'); + src: url("../public/fonts/roboto-v27-latin-700.eot"); /* IE9 Compat Modes */ - src: local(''), - url('../public/fonts/roboto-v27-latin-700.woff2') format('woff2'), - /* Super Modern Browsers */ - url('../public/fonts/roboto-v27-latin-700.woff') format('woff'), - /* Modern Browsers */ - url('../public/fonts/roboto-v27-latin-700.ttf') format('truetype'), - /* Safari, Android, iOS */ - url('../public/fonts/roboto-v27-latin-700.svg#Roboto') format('svg'); + src: local(""), url("../public/fonts/roboto-v27-latin-700.woff2") format("woff2"), + /* Super Modern Browsers */ url("../public/fonts/roboto-v27-latin-700.woff") format("woff"), + /* Modern Browsers */ url("../public/fonts/roboto-v27-latin-700.ttf") format("truetype"), + /* Safari, Android, iOS */ url("../public/fonts/roboto-v27-latin-700.svg#Roboto") format("svg"); /* Legacy iOS */ font-display: swap; } @@ -132,7 +116,7 @@ body { body { margin: 0; - font-family: 'Roboto', 'Helvetica', 'Arial'; + font-family: "Roboto", "Helvetica", "Arial"; line-height: 1.5; /* background-color: rgb(227, 233, 245); */ background-color: var(--custom-tertiary-color); @@ -144,8 +128,7 @@ body { } code { - font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', - monospace; + font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; } #root { @@ -302,7 +285,7 @@ Mobile CSS main { margin-top: 46px; - padding: 15px 10px 5px 10px; + padding: 15px 10px 10px 10px; } ::-webkit-scrollbar { @@ -319,8 +302,7 @@ Mobile CSS } /* Firefox query*/ -@media(min--moz-device-pixel-ratio:0) and (max-width: 650px) { - +@media (min--moz-device-pixel-ratio: 0) and (max-width: 650px) { /* Fix for firefox on android not being able to fully zoom out again */ /* https://stackoverflow.com/questions/71561637/bug-in-firefox-for-android-small-pages-not-zooming-out-100 */ /* !Remove later when content can overflow again */ @@ -328,4 +310,4 @@ Mobile CSS #root { min-height: 101vh; } -} \ No newline at end of file +} diff --git a/src/index.js b/src/index.js index f35438b..bfa5cef 100644 --- a/src/index.js +++ b/src/index.js @@ -19,7 +19,7 @@ import { SettingsPage } from "./pages/settings.js"; import { LegalNoticePage } from "./pages/legal-notice.jsx"; import { PrivacyPage } from "./pages/privacy.jsx"; -import { ModulePage } from "./pages/module/module.jsx"; +import { ModulePage } from "./pages/module/module"; import { QuestionPage } from "./pages/module/question/question"; import { AllQuestionsPage } from "./pages/module/all-questions/index"; @@ -29,7 +29,7 @@ import { Footer } from "./components/Footer/Footer.jsx"; import { CustomToastContainer } from "./components/toast/toast.jsx"; //Context -import { ModuleProvider } from "./components/module/moduleContext.js"; +import { ModuleProvider } from "./components/module/moduleContext"; //Import functions import { ScrollToTop } from "./utils/ScrollToTop"; diff --git a/src/pages/index.js b/src/pages/index.js index 9749ec3..1053283 100644 --- a/src/pages/index.js +++ b/src/pages/index.js @@ -1,5 +1,5 @@ //Components -import { Modules } from "../components/Home/Modules.js"; +import { Modules } from "../components/Home/Modules"; import { AddModule } from "../components/Home/AddModule"; import { SiteHeading } from "../components/SiteHeading/SiteHeading"; diff --git a/src/pages/module/module.jsx b/src/pages/module/module.tsx similarity index 100% rename from src/pages/module/module.jsx rename to src/pages/module/module.tsx diff --git a/src/utils/saveFile.js b/src/utils/saveFile.ts similarity index 53% rename from src/utils/saveFile.js rename to src/utils/saveFile.ts index ce8c29b..125ed73 100644 --- a/src/utils/saveFile.js +++ b/src/utils/saveFile.ts @@ -2,10 +2,15 @@ import { saveAs } from "file-saver"; import { toast } from "react-toastify"; import isElectron from "is-electron"; +export type TSaveFile = { + file: string; + name: string; +}; + //Download a file as json to user selected location or downloads folder -export async function saveFile({ file, name }) { +export async function saveFile({ file, name }: TSaveFile) { //Cypress/Electron don't support the filePicker API - if (window.Cypress || isElectron()) { + if ((window as any).Cypress || isElectron()) { const blob = new Blob([file], { type: "application/json" }); saveAs(blob, `${name}.json`); toast.success(`Downloaded module as "${name}.json"`); @@ -14,6 +19,7 @@ export async function saveFile({ file, name }) { //Use the FilePicker api to allow the user to choose a location, if it is not supported use saveAs library // https://web.dev/file-system-access/ + //TODO currently using @types/wicg-file-system-access for showSaveFilePicker because there is currently no native Typescript support (see: https://stackoverflow.com/questions/71309058/property-showsavefilepicker-does-not-exist-on-type-window-typeof-globalthis) try { const fileHandle = await window.showSaveFilePicker({ suggestedName: `${name}.json`, @@ -27,14 +33,16 @@ export async function saveFile({ file, name }) { await writable.close(); toast.success(`Downloaded module as "${fileHandle.name}"`); } catch (e) { - //If fileHandle isn't supported (firefox/safari/mobile), use save-as library and catch aborted error - //compatibility: https://developer.mozilla.org/en-US/docs/Web/API/FileSystemHandle#browser_compatibility - if (e.name === "TypeError") { - const blob = new Blob([file], { type: "application/json" }); - saveAs(blob, `${name}.json`); - toast.success(`Downloaded module as "${name}.json"`); - } else if (e.name !== "AbortError") { - toast.warn(e.message); + if (e! instanceof Error) { + //If fileHandle isn't supported (firefox/safari/mobile), use save-as library and catch aborted error + //compatibility: https://developer.mozilla.org/en-US/docs/Web/API/FileSystemHandle#browser_compatibility + if (e.name === "TypeError") { + const blob = new Blob([file], { type: "application/json" }); + saveAs(blob, `${name}.json`); + toast.success(`Downloaded module as "${name}.json"`); + } else if (e.name !== "AbortError") { + toast.warn(e.message); + } } } } diff --git a/src/utils/shuffleArray.js b/src/utils/shuffleArray.js deleted file mode 100644 index ba0e3b6..0000000 --- a/src/utils/shuffleArray.js +++ /dev/null @@ -1,12 +0,0 @@ -//Randomize the order of an array -//https://stackoverflow.com/questions/38101522/how-to-render-random-objects-from-an-array-in-react (Fisher-Algorithm) -export function shuffleArray(array) { - let index = array.length - 1; - for (index; index > 0; index--) { - const j = Math.floor(Math.random() * (index + 1)); - const temp = array[index]; - array[index] = array[j]; - array[j] = temp; - } - return array; -} diff --git a/src/utils/shuffleArray.ts b/src/utils/shuffleArray.ts new file mode 100644 index 0000000..256040c --- /dev/null +++ b/src/utils/shuffleArray.ts @@ -0,0 +1,15 @@ +/** + * Randomize the order of a given array + * @returns A randomized array + * @source https://stackoverflow.com/questions/38101522/how-to-render-random-objects-from-an-array-in-react (Fisher-Algorithm) + */ +export function shuffleArray(array: T) { + let index = array.length - 1; + for (index; index > 0; index--) { + const j = Math.floor(Math.random() * (index + 1)); + const temp = array[index]; + array[index] = array[j]; + array[j] = temp; + } + return array; +} diff --git a/tsconfig.json b/tsconfig.json index 3a53053..a9808c4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -40,7 +40,8 @@ // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ "types": [ "cypress", - "node" + "node", + "@types/wicg-file-system-access", /* wicg-file-system is used for showSaveFilePicker (see saveFile.ts for more information) */ ], /* Specify type package names to be included without being referenced in a source file. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ diff --git a/yarn.lock b/yarn.lock index 2bdf507..4952ce7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2632,6 +2632,11 @@ resolved "https://registry.yarnpkg.com/@types/verror/-/verror-1.10.6.tgz#3e600c62d210c5826460858f84bcbb65805460bb" integrity sha512-NNm+gdePAX1VGvPcGZCDKQZKYSiAWigKhKaz5KF94hG6f2s8de9Ow5+7AbXoeKxL8gavZfk4UquSAygOF2duEQ== +"@types/wicg-file-system-access@^2020.9.5": + version "2020.9.5" + resolved "https://registry.yarnpkg.com/@types/wicg-file-system-access/-/wicg-file-system-access-2020.9.5.tgz#4a0c8f3d1ed101525f329e86c978f7735404474f" + integrity sha512-UYK244awtmcUYQfs7FR8710MJcefL2WvkyHMjA8yJzxd1mo0Gfn88sRZ1Bls7hiUhA2w7ne1gpJ9T5g3G0wOyA== + "@types/ws@^8.5.1": version "8.5.3" resolved "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz"