Skip to content

Commit

Permalink
Separate session data per config
Browse files Browse the repository at this point in the history
Pass the file name to the session data key, so that we get separate
entries per config.

This has the effect of auto-switching the grid when choosing a new
config - that's not actually an expected use case but it feels correct
that the app should only be able to show a consistent set of config and
words.

Jump through some hoops to avoid not found resulting in junk
localstorage entries:

Since we fall back to a default list if the name is not recognised
(maybe a bad idea?), the param doesn't always match the list filename,
and we don't want eg http://localhost:5173/bingo-frontend/foo to result
in storing bingoSession-foo in localstorage.

Maybe we can skip all that nonsense and do some kind of redirect or not
found instead?
  • Loading branch information
dgmstuart committed Apr 22, 2024
1 parent 0327e7e commit 85b9f3a
Show file tree
Hide file tree
Showing 13 changed files with 93 additions and 55 deletions.
2 changes: 1 addition & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const App: React.FC = () => {
{
path: "qr_code",
element: <QRCode />,
loader: configLoader,
loader: defaultConfigLoader,
},
],
},
Expand Down
8 changes: 4 additions & 4 deletions src/components/Card.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ describe("'New card' button", () => {
test("changes the words on the card", () => {
render(
<BrowserRouter>
<Card wordList={wordList} name={""} url={""} />
<Card wordList={wordList} name={""} url={""} id={""} />
</BrowserRouter>,
);
const initialCells = screen.queryAllByRole("gridcell");
Expand All @@ -30,7 +30,7 @@ describe("'New card' button", () => {
test("clears any stamped cells", () => {
render(
<BrowserRouter>
<Card wordList={wordList} name={""} url={""} />
<Card wordList={wordList} name={""} url={""} id={""} />
</BrowserRouter>,
);
const cells = screen.queryAllByRole("gridcell");
Expand All @@ -49,7 +49,7 @@ describe("'Clear' button", () => {
test("clears any stamped cells", () => {
render(
<BrowserRouter>
<Card wordList={wordList} name={""} url={""} />
<Card wordList={wordList} name={""} url={""} id={""} />
</BrowserRouter>,
);
const cells = screen.queryAllByRole("gridcell");
Expand All @@ -74,7 +74,7 @@ describe("'Share' button", () => {
const user = userEvent.setup();
render(
<BrowserRouter>
<Card wordList={wordList} name={""} url={""} />
<Card wordList={wordList} name={""} url={""} id={""} />
<textarea rows={5} />
</BrowserRouter>,
);
Expand Down
17 changes: 10 additions & 7 deletions src/components/Card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,16 @@ import share from "../lib/share";
import type { CellProps } from "./Cell";
import type { ButtonClickHandler } from "../clickHandler";

const Card: React.FC<{ name: string; url: string; wordList: string[] }> = ({
name,
url,
wordList,
}) => {
const [cellDataList, toggleStamped, setNewWords, clearAllCells] =
useCard(wordList);
const Card: React.FC<{
id: string;
name: string;
url: string;
wordList: string[];
}> = ({ id, name, url, wordList }) => {
const [cellDataList, toggleStamped, setNewWords, clearAllCells] = useCard(
id,
wordList,
);

const cellPropsList: CellProps[] = cellDataList.map((cellData, index) => ({
...cellData,
Expand Down
6 changes: 3 additions & 3 deletions src/components/DynamicCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@ import React from "react";
import { useLoaderData } from "react-router-dom";
import Card from "./Card";
import flattenWordList from "../lib/flattenWordList";
import type { Config } from "../data/config";
import type { KeyedConfig } from "../loaders/configLoaders";

const DynamicCard: React.FC = () => {
const { name = "", url, wordList } = useLoaderData() as Config;
const { id, name = "", url, wordList } = useLoaderData() as KeyedConfig;

if (wordList.length > 0) {
// Card saves the word list to the session, so don't render a card if the
// word list hasn't loaded yet
const words = flattenWordList(wordList);
return <Card wordList={words} name={name} url={url} />;
return <Card wordList={words} name={name} url={url} id={id} />;
}
};

Expand Down
8 changes: 5 additions & 3 deletions src/hooks/useCard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ type GetterSetters = [
ButtonClickHandler,
];

const useCard = (wordList: string[]): GetterSetters => {
const useCard = (id: string, wordList: string[]): GetterSetters => {
const newWords = (): string[] => shuffle(wordList).slice(0, 25);

const newCellDataList = function (): CellData[] {
Expand All @@ -19,8 +19,10 @@ const useCard = (wordList: string[]): GetterSetters => {
});
};

const [cellDataList, setCellDataList] =
useSession<CellData[]>(newCellDataList);
const [cellDataList, setCellDataList] = useSession<CellData[]>({
keyName: `bingoSession-${id}`,
initFunction: newCellDataList,
});

const setStamped = (index: number, stamped: boolean): void => {
setCellDataList(
Expand Down
12 changes: 9 additions & 3 deletions src/hooks/useSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,16 @@ import GuaranteedJsonSession from "../lib/GuaranteedJsonSession";

type GetterSetter<T> = [T, (data: T) => void];

const useSession = function <T>(initialData: () => T): GetterSetter<T> {
const useSession = function <T>({
keyName,
initFunction,
}: {
keyName: string;
initFunction: () => T;
}): GetterSetter<T> {
const session = useMemo(
() => new GuaranteedJsonSession<T>(initialData),
[initialData],
() => new GuaranteedJsonSession<T>({ keyName, initFunction }),
[initFunction],
);

const [data, setData] = useState<T>(session.sessionData);
Expand Down
10 changes: 8 additions & 2 deletions src/lib/GuaranteedJsonSession.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ beforeEach(() => {

test("can store an array of cell data in the session", () => {
const initFunction = () => [];
const session = new GuaranteedJsonSession<CellData[]>(initFunction);
const session = new GuaranteedJsonSession<CellData[]>({
keyName: "key",
initFunction,
});

session.sessionData = [
{ word: "Aardvark", stamped: false },
Expand All @@ -22,7 +25,10 @@ test("can store an array of cell data in the session", () => {

test("returns a new array if nothing is stored", () => {
const initFunction = () => [{ word: "Camel", stamped: false }];
const session = new GuaranteedJsonSession<CellData[]>(initFunction);
const session = new GuaranteedJsonSession<CellData[]>({
keyName: "key",
initFunction,
});

expect(session.sessionData).toEqual([{ word: "Camel", stamped: false }]);
});
10 changes: 8 additions & 2 deletions src/lib/GuaranteedJsonSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,14 @@ class GuaranteedJsonSession<T> {
#session: JsonSession<T>;
#initFunction: () => T;

constructor(initFunction: () => T) {
this.#session = new JsonSession<T>();
constructor({
keyName,
initFunction,
}: {
keyName: string;
initFunction: () => T;
}) {
this.#session = new JsonSession<T>(keyName);
this.#initFunction = initFunction;
}

Expand Down
19 changes: 0 additions & 19 deletions src/lib/JsonDataImporter.ts

This file was deleted.

4 changes: 2 additions & 2 deletions src/lib/JsonSession.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ beforeEach(() => {
});

test("can store an array of cell data in the session", () => {
const session = new JsonSession<CellData[]>();
const session = new JsonSession<CellData[]>("key");

session.sessionData = [
{ word: "Aardvark", stamped: false },
Expand All @@ -20,7 +20,7 @@ test("can store an array of cell data in the session", () => {
});

test("returns null if nothing is stored", () => {
const session = new JsonSession();
const session = new JsonSession("Key");

expect(session.sessionData).toEqual(null);
});
6 changes: 5 additions & 1 deletion src/lib/JsonSession.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
class JsonSession<T> {
#store = window.localStorage;
#keyName = "sessionData";
#keyName: string;

constructor(keyName: string) {
this.#keyName = keyName;
}

get sessionData(): T | null {
const sessionDataString: string | null = this.#store.getItem(this.#keyName);
Expand Down
21 changes: 21 additions & 0 deletions src/lib/importJsonData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
type EmptyObject = Record<string, never>;
type Result<T> =
| { success: true; data: T }
| { success: false; data: EmptyObject };

const importJsonData = async <T>(listName: string): Promise<Result<T>> => {
try {
return {
success: true,
data: (await import(`../data/${listName}.json`)).default,
};
} catch (error) {
return {
success: false,
data: {},
};
console.error("Failed to load the data", error);
}
};

export default importJsonData;
25 changes: 17 additions & 8 deletions src/loaders/configLoaders.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,34 @@
import defaultConfig from "../data/teamLindy.json";
import nullConfig from "../data/teamLindy.json";
import JsonDataImporter from "../lib/JsonDataImporter";
import importJsonData from "../lib/importJsonData";
import type { Params } from "react-router-dom";
import type { Config } from "../data/config";

export const defaultConfigLoader = (): Config => {
return defaultConfig;
export type KeyedConfig = { id: string } & Config;

const defaultKeyedConfig: KeyedConfig = { id: "teamLindy", ...defaultConfig };

export const defaultConfigLoader = (): KeyedConfig => {
return defaultKeyedConfig;
};

export const configLoader = async ({
params,
}: {
params: Params<string>;
}): Promise<Config> => {
if (params.gameName) {
const importer = new JsonDataImporter({ defaultData: defaultConfig });
return await importer.import(params.gameName);
}): Promise<KeyedConfig> => {
const id = params.gameName;
if (id) {
const { success, data } = await importJsonData<Config>(id);
if (success) {
return { id, ...data };
} else {
return defaultKeyedConfig;
}
} else {
console.error(
"expected a param of 'gameName' but didn't find one or it had a falsey value",
);
return nullConfig;
return { id: "null", ...nullConfig };
}
};

0 comments on commit 85b9f3a

Please sign in to comment.