From 2288d042fe5fb228b3c61a0a612b5dbb56546682 Mon Sep 17 00:00:00 2001 From: Nils Knappmeier Date: Sun, 11 Feb 2024 01:01:07 +0100 Subject: [PATCH] sync choices to localStorage --- package.json | 1 + src/components/DojoChooser.tsx | 6 +-- .../ExamTableChooser/ExamTableChooser.tsx | 18 +++----- .../ExamTableChooser/TechniqueChooser.tsx | 18 ++++---- src/components/ExamTableChooser/store.ts | 14 ++++++ src/exam-tables/index.ts | 20 +-------- src/index.tsx | 3 -- src/model/Technique.ts | 12 +++++ src/model/TechniqueList.ts | 9 ++++ src/pages/chooser/index.tsx | 4 +- src/store/selectedDojo.ts | 45 +++++++++++++++++++ src/store/techniqueList.ts | 16 +++++++ src/utils/playAudioFile.ts | 7 ++- yarn.lock | 5 +++ 14 files changed, 128 insertions(+), 50 deletions(-) create mode 100644 src/components/ExamTableChooser/store.ts create mode 100644 src/store/selectedDojo.ts create mode 100644 src/store/techniqueList.ts diff --git a/package.json b/package.json index d8fea28..962b916 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "0.1.0", "private": true, "dependencies": { + "@nanostores/persistent": "^0.9.1", "@nanostores/react": "^0.7.1", "@testing-library/jest-dom": "^6.3.0", "@testing-library/react": "^14.1.2", diff --git a/src/components/DojoChooser.tsx b/src/components/DojoChooser.tsx index 6dcc90d..7b344d8 100644 --- a/src/components/DojoChooser.tsx +++ b/src/components/DojoChooser.tsx @@ -1,11 +1,11 @@ import React from "react"; import { Dropdown } from "react-bootstrap"; -import { currentDojo, dojos, selectDojo } from "../exam-tables"; +import { dojos } from "../exam-tables"; import css from "./DojoChooser.module.scss"; -import { useStore } from "@nanostores/react"; +import { useSelectedDojo } from "../store/selectedDojo"; export const DojoChooser: React.FC = () => { - const selectedDojo = useStore(currentDojo); + const { selectedDojo, selectDojo } = useSelectedDojo(); return ( diff --git a/src/components/ExamTableChooser/ExamTableChooser.tsx b/src/components/ExamTableChooser/ExamTableChooser.tsx index cc7ab60..6fcc167 100644 --- a/src/components/ExamTableChooser/ExamTableChooser.tsx +++ b/src/components/ExamTableChooser/ExamTableChooser.tsx @@ -1,5 +1,5 @@ import { resolveExamTables } from "../../utils/resolve-exam-tables"; -import React, { useEffect, useState } from "react"; +import React, { useEffect } from "react"; import { useTranslation } from "react-i18next"; import { Col, Row } from "react-bootstrap"; @@ -7,15 +7,16 @@ import { ExamTable } from "../../exam-tables/baseTypes"; import { CheckButton } from "../CheckButton"; import { TechniqueList } from "../../model/TechniqueList"; import { useStore } from "@nanostores/react"; -import { currentDojoLazyData } from "../../exam-tables"; +import { chooseExam, chosenExams } from "./store"; +import { useSelectedDojo } from "../../store/selectedDojo"; export interface ExamTableChooserProps { onChoice(techniques: TechniqueList): void; } export const ExamTableChooser: React.FC = ({ onChoice }) => { - const [buttonState, setButtonState] = useState>({}); - const dojo = useStore(currentDojoLazyData); + const buttonState = useStore(chosenExams); + const { selectedDojoLazyData: dojo } = useSelectedDojo(); useEffect(() => { const selectedButtons = Object.keys(dojo.exams).filter((buttonName) => buttonState[buttonName] === true); @@ -24,13 +25,6 @@ export const ExamTableChooser: React.FC = ({ onChoice }) onChoice(selectedTechniques); }, [buttonState, onChoice, dojo]); - function updateState(key: string, checked: boolean): void { - setButtonState((buttonState) => ({ - ...buttonState, - [key]: checked, - })); - } - const { t } = useTranslation(); return ( @@ -41,7 +35,7 @@ export const ExamTableChooser: React.FC = ({ onChoice }) className={"w-100"} id={"toggle-" + buttonName} onChange={(event) => { - updateState(buttonName, event.currentTarget.checked); + chooseExam(buttonName, event.currentTarget.checked); }} type="checkbox" value={buttonName} diff --git a/src/components/ExamTableChooser/TechniqueChooser.tsx b/src/components/ExamTableChooser/TechniqueChooser.tsx index 1345b65..c7d2951 100644 --- a/src/components/ExamTableChooser/TechniqueChooser.tsx +++ b/src/components/ExamTableChooser/TechniqueChooser.tsx @@ -7,10 +7,9 @@ import { TechniqueFilters, techniquePredicate } from "../../utils/technique-filt import { ShowTechniqueFilters } from "./ShowTechniqueFilters"; import { ShowShuffleControls, ShuffleControls } from "./ShuffleControls"; import { TechniqueList } from "../../model/TechniqueList"; -import { useStore } from "@nanostores/react"; -import { currentDojo, currentDojoLazyData } from "../../exam-tables"; import { Alert } from "react-bootstrap"; import { DojoChooser } from "../DojoChooser"; +import { useSelectedDojo } from "../../store/selectedDojo"; export interface ExamTableChooserProps { onChoice(techniques: TechniqueList): void; @@ -19,10 +18,9 @@ export interface ExamTableChooserProps { export const TechniqueChooser: React.FC = ({ onChoice }) => { const [chosenTechniques, setChosenTechniques] = useState(new TechniqueList()); const [result, setResult] = useState(new TechniqueList()); - const dojoLazy = useStore(currentDojoLazyData); - const dojo = useStore(currentDojo); + const { selectedDojo, selectedDojoLazyData } = useSelectedDojo(); - useEffect(() => setResult(new TechniqueList()), [dojo]); + useEffect(() => setResult(new TechniqueList()), [selectedDojo]); const [shuffleControls, setShuffleControls] = useState({ shouldShuffle: true, @@ -50,17 +48,17 @@ export const TechniqueChooser: React.FC = ({ onChoice }) <>
- + - {dojoLazy.additionalText && ( + {selectedDojoLazyData.additionalText && ( Hinweis - {dojoLazy.additionalText} + {selectedDojoLazyData.additionalText} )} - {dojoLazy.sourceLink && ( + {selectedDojoLazyData.sourceLink && (

- Quelle: {dojoLazy.sourceLink} + Quelle: {selectedDojoLazyData.sourceLink}

)}
diff --git a/src/components/ExamTableChooser/store.ts b/src/components/ExamTableChooser/store.ts new file mode 100644 index 0000000..772b185 --- /dev/null +++ b/src/components/ExamTableChooser/store.ts @@ -0,0 +1,14 @@ +import { persistentAtom } from "@nanostores/persistent"; + +export const chosenExams = persistentAtom>( + "aikido-exam-chosen", + {}, + { + encode: JSON.stringify, + decode: JSON.parse, + }, +); + +export function chooseExam(name: string, choose: boolean) { + chosenExams.set({ ...chosenExams.get(), [name]: choose }); +} diff --git a/src/exam-tables/index.ts b/src/exam-tables/index.ts index e4c719f..51d4c34 100644 --- a/src/exam-tables/index.ts +++ b/src/exam-tables/index.ts @@ -1,26 +1,8 @@ -import { atom } from "nanostores"; -import { DojoLazyData, Dojo } from "./baseTypes"; import aifd from "./aikido-foederation"; import darmstadt from "./aikido-dojo-darmstadt"; - -export const currentDojoLazyData = atom({ exams: {} }); - -export const currentDojo = atom>({ name: "loading dojos" }); +import { Dojo } from "./baseTypes"; export const dojos: Record = { "aikido-foederation-deutschland": aifd, "aikido-dojo-darmstadt": darmstadt, }; - -export async function initCurrentDojo(): Promise { - const searchParams = new URLSearchParams(window.location.search); - const dojoId = searchParams.get("dojo") ?? "aikido-foederation-deutschland"; - currentDojo.set(dojos[dojoId]); - const dojoRules = await dojos[dojoId].lazyData(); - currentDojoLazyData.set(dojoRules.default); -} - -export async function selectDojo(dojo: string): Promise { - window.history.pushState(null, "", "?dojo=" + encodeURIComponent(dojo)); - await initCurrentDojo(); -} diff --git a/src/index.tsx b/src/index.tsx index 247bc93..c00bbbe 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -5,13 +5,10 @@ import "./index.css"; import App from "./App"; import { initI18Next } from "./i18n/i18n"; import i18n from "i18next"; -import { initCurrentDojo } from "./exam-tables"; initI18Next().catch(console.error); document.title = i18n.t("app.title"); -initCurrentDojo().catch(console.error); - const root = createRoot(document.getElementById("root")!); root.render( diff --git a/src/model/Technique.ts b/src/model/Technique.ts index dba306b..4073900 100644 --- a/src/model/Technique.ts +++ b/src/model/Technique.ts @@ -41,4 +41,16 @@ export class Technique { get [Symbol.toStringTag](): string { return this.definition.join(" "); } + + toJson(): unknown { + return { + definition: this.definition, + metadata: this.metadata, + }; + } + + static fromJson(json: unknown): Technique { + const j = json as Technique; + return new Technique(j.definition, j.metadata); + } } diff --git a/src/model/TechniqueList.ts b/src/model/TechniqueList.ts index 99c3d65..c437519 100644 --- a/src/model/TechniqueList.ts +++ b/src/model/TechniqueList.ts @@ -58,6 +58,15 @@ export class TechniqueList { get length(): number { return this.techniques.length; } + + toJson(): unknown { + return this.techniques.map((technique) => technique.toJson()); + } + + static fromJson(json: unknown): TechniqueList { + const array = json as Array; + return new TechniqueList(array.map((item) => Technique.fromJson(item))); + } } class Grouper { diff --git a/src/pages/chooser/index.tsx b/src/pages/chooser/index.tsx index d83b4f5..f3c9d1e 100644 --- a/src/pages/chooser/index.tsx +++ b/src/pages/chooser/index.tsx @@ -6,11 +6,11 @@ import { NoTechniquesChosen } from "src/components/Reader/NoTechniquesChosen"; import { Reader } from "src/components/Reader/Reader"; import { ShowExamTable } from "src/components/ShowExamTable/ShowExamTable"; import { DefaultLayout } from "src/layout/DefaultLayout"; -import { TechniqueList } from "src/model/TechniqueList"; import css from "src/components/Reader/Reader.module.scss"; +import { useTechniqueList } from "src/store/techniqueList"; export const Component: React.FC = () => { - const [techniques, setTechniques] = useState(new TechniqueList()); + const [techniques, setTechniques] = useTechniqueList(); const [currentTechniqueIndex, setCurrentTechniqueIndex] = useState(-1); return ( diff --git a/src/store/selectedDojo.ts b/src/store/selectedDojo.ts new file mode 100644 index 0000000..e27d2f7 --- /dev/null +++ b/src/store/selectedDojo.ts @@ -0,0 +1,45 @@ +import { useStore } from "@nanostores/react"; +import { DojoLazyData } from "../exam-tables/baseTypes"; +import { atom } from "nanostores"; +import { dojos } from "../exam-tables"; +import { useMemo } from "react"; + +type ShortDojo = { + id: string; + name: string; + logo?: string; +}; + +const searchParams = new URLSearchParams(window.location.search); +const currentDojo = atom(searchParams.get("dojo") ?? "aikido-foederation-deutschland"); +const currentDojoLazyData = atom({ exams: {} }); + +export function useSelectedDojo(): { + selectedDojo: ShortDojo; + selectedDojoLazyData: DojoLazyData; + selectDojo: (id: string) => void; +} { + const currentId = useStore(currentDojo); + const lazyData = useStore(currentDojoLazyData); + const selectedDojo = useMemo(() => { + return { + id: currentId, + logo: dojos[currentId].logo, + name: dojos[currentId].name, + }; + }, [currentId]); + return { + selectedDojo, + selectedDojoLazyData: lazyData, + selectDojo: currentDojo.set.bind(currentDojo), + }; +} + +currentDojo.subscribe((value) => + window.history.pushState(null, "", "?dojo=" + encodeURIComponent(value) + document.location.hash), +); + +currentDojo.subscribe(async (value) => { + const dojoRules = await dojos[value].lazyData(); + currentDojoLazyData.set(dojoRules.default); +}); diff --git a/src/store/techniqueList.ts b/src/store/techniqueList.ts new file mode 100644 index 0000000..2c56c97 --- /dev/null +++ b/src/store/techniqueList.ts @@ -0,0 +1,16 @@ +import { persistentAtom } from "@nanostores/persistent"; +import { TechniqueList } from "../model/TechniqueList"; +import { useStore } from "@nanostores/react"; + +const techniqueList = persistentAtom("aikido-exam-techniques", new TechniqueList(), { + encode: (techniqueList) => { + return JSON.stringify(techniqueList.toJson()); + }, + decode: (string) => { + return TechniqueList.fromJson(JSON.parse(string)); + }, +}); + +export function useTechniqueList(): [techniques: TechniqueList, setTechniques: (techniques: TechniqueList) => void] { + return [useStore(techniqueList), techniqueList.set.bind(techniqueList)]; +} diff --git a/src/utils/playAudioFile.ts b/src/utils/playAudioFile.ts index fa97ab6..b227701 100644 --- a/src/utils/playAudioFile.ts +++ b/src/utils/playAudioFile.ts @@ -1,7 +1,12 @@ import { AudioFile, audioFiles } from "../exam-tables/audio-files"; -export async function playAudioFile(audioElement: HTMLAudioElement, audioFile: AudioFile): Promise { +export async function playAudioFile( + audioElement: HTMLAudioElement, + audioFile: AudioFile, + { playbackRate = 1 } = {}, +): Promise { audioElement.src = audioFiles[audioFile]; + audioElement.playbackRate = playbackRate; await audioElement.play(); return new Promise((resolve, reject) => { const onEnded = () => { diff --git a/yarn.lock b/yarn.lock index 78c3402..9d6f1a0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -453,6 +453,11 @@ jsbi "^4.3.0" tslib "^2.4.1" +"@nanostores/persistent@^0.9.1": + version "0.9.1" + resolved "https://registry.yarnpkg.com/@nanostores/persistent/-/persistent-0.9.1.tgz#e668c64431dee2c9d7a1b8f7ffc1e9c0806ac227" + integrity sha512-ow57Hxm5VMaI5GHET/cVk8hX/iKMmbhcGrB9owfN8p8OHiiJgUlYxe1giacwlAALJXAh2t8bxXh42hHb64BCEA== + "@nanostores/react@^0.7.1": version "0.7.1" resolved "https://registry.npmjs.org/@nanostores/react/-/react-0.7.1.tgz"