Skip to content

Commit

Permalink
sync choices to localStorage
Browse files Browse the repository at this point in the history
  • Loading branch information
nknapp committed Feb 11, 2024
1 parent bdad287 commit 2288d04
Show file tree
Hide file tree
Showing 14 changed files with 128 additions and 50 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
6 changes: 3 additions & 3 deletions src/components/DojoChooser.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Dropdown>
<Dropdown.Toggle variant={"success"} className={css.dropdownItem}>
Expand Down
18 changes: 6 additions & 12 deletions src/components/ExamTableChooser/ExamTableChooser.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
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";

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<ExamTableChooserProps> = ({ onChoice }) => {
const [buttonState, setButtonState] = useState<Record<string, boolean>>({});
const dojo = useStore(currentDojoLazyData);
const buttonState = useStore(chosenExams);
const { selectedDojoLazyData: dojo } = useSelectedDojo();

useEffect(() => {
const selectedButtons = Object.keys(dojo.exams).filter((buttonName) => buttonState[buttonName] === true);
Expand All @@ -24,13 +25,6 @@ export const ExamTableChooser: React.FC<ExamTableChooserProps> = ({ onChoice })
onChoice(selectedTechniques);
}, [buttonState, onChoice, dojo]);

function updateState(key: string, checked: boolean): void {
setButtonState((buttonState) => ({
...buttonState,
[key]: checked,
}));
}

const { t } = useTranslation();
return (
<Row xs={3} sm={4} md={4} lg={8}>
Expand All @@ -41,7 +35,7 @@ export const ExamTableChooser: React.FC<ExamTableChooserProps> = ({ onChoice })
className={"w-100"}
id={"toggle-" + buttonName}
onChange={(event) => {
updateState(buttonName, event.currentTarget.checked);
chooseExam(buttonName, event.currentTarget.checked);
}}
type="checkbox"
value={buttonName}
Expand Down
18 changes: 8 additions & 10 deletions src/components/ExamTableChooser/TechniqueChooser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -19,10 +18,9 @@ export interface ExamTableChooserProps {
export const TechniqueChooser: React.FC<ExamTableChooserProps> = ({ onChoice }) => {
const [chosenTechniques, setChosenTechniques] = useState<TechniqueList>(new TechniqueList());
const [result, setResult] = useState<TechniqueList>(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<ShuffleControls>({
shouldShuffle: true,
Expand Down Expand Up @@ -50,17 +48,17 @@ export const TechniqueChooser: React.FC<ExamTableChooserProps> = ({ onChoice })
<>
<DojoChooser />
<hr />
<ExamTableChooser key={dojo.name} onChoice={setChosenTechniques} />
<ExamTableChooser key={selectedDojo.name} onChoice={setChosenTechniques} />

{dojoLazy.additionalText && (
{selectedDojoLazyData.additionalText && (
<Alert variant={"warning"}>
<Alert.Heading>Hinweis</Alert.Heading>
{dojoLazy.additionalText}
{selectedDojoLazyData.additionalText}
</Alert>
)}
{dojoLazy.sourceLink && (
{selectedDojoLazyData.sourceLink && (
<p>
Quelle: <a href={dojoLazy.sourceLink}>{dojoLazy.sourceLink}</a>
Quelle: <a href={selectedDojoLazyData.sourceLink}>{selectedDojoLazyData.sourceLink}</a>
</p>
)}
<hr />
Expand Down
14 changes: 14 additions & 0 deletions src/components/ExamTableChooser/store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { persistentAtom } from "@nanostores/persistent";

export const chosenExams = persistentAtom<Record<string, boolean>>(
"aikido-exam-chosen",
{},
{
encode: JSON.stringify,
decode: JSON.parse,
},
);

export function chooseExam(name: string, choose: boolean) {
chosenExams.set({ ...chosenExams.get(), [name]: choose });
}
20 changes: 1 addition & 19 deletions src/exam-tables/index.ts
Original file line number Diff line number Diff line change
@@ -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<DojoLazyData>({ exams: {} });

export const currentDojo = atom<Pick<Dojo, "name" | "logo">>({ name: "loading dojos" });
import { Dojo } from "./baseTypes";

export const dojos: Record<string, Dojo> = {
"aikido-foederation-deutschland": aifd,
"aikido-dojo-darmstadt": darmstadt,
};

export async function initCurrentDojo(): Promise<void> {
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<void> {
window.history.pushState(null, "", "?dojo=" + encodeURIComponent(dojo));
await initCurrentDojo();
}
3 changes: 0 additions & 3 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<React.StrictMode>
Expand Down
12 changes: 12 additions & 0 deletions src/model/Technique.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
9 changes: 9 additions & 0 deletions src/model/TechniqueList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<unknown>;
return new TechniqueList(array.map((item) => Technique.fromJson(item)));
}
}

class Grouper<K extends string, V> {
Expand Down
4 changes: 2 additions & 2 deletions src/pages/chooser/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<TechniqueList>(new TechniqueList());
const [techniques, setTechniques] = useTechniqueList();
const [currentTechniqueIndex, setCurrentTechniqueIndex] = useState(-1);

return (
Expand Down
45 changes: 45 additions & 0 deletions src/store/selectedDojo.ts
Original file line number Diff line number Diff line change
@@ -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<string>(searchParams.get("dojo") ?? "aikido-foederation-deutschland");
const currentDojoLazyData = atom<DojoLazyData>({ 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);
});
16 changes: 16 additions & 0 deletions src/store/techniqueList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { persistentAtom } from "@nanostores/persistent";
import { TechniqueList } from "../model/TechniqueList";
import { useStore } from "@nanostores/react";

const techniqueList = persistentAtom<TechniqueList>("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)];
}
7 changes: 6 additions & 1 deletion src/utils/playAudioFile.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { AudioFile, audioFiles } from "../exam-tables/audio-files";

export async function playAudioFile(audioElement: HTMLAudioElement, audioFile: AudioFile): Promise<void> {
export async function playAudioFile(
audioElement: HTMLAudioElement,
audioFile: AudioFile,
{ playbackRate = 1 } = {},
): Promise<void> {
audioElement.src = audioFiles[audioFile];
audioElement.playbackRate = playbackRate;
await audioElement.play();
return new Promise<void>((resolve, reject) => {
const onEnded = () => {
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

0 comments on commit 2288d04

Please sign in to comment.