diff --git a/package.json b/package.json
index 8e49dbc..2b3b904 100644
--- a/package.json
+++ b/package.json
@@ -15,7 +15,7 @@
"typesafe-i18n": "typesafe-i18n",
"format": "biome format --write ./src",
"lint": "biome lint --fix && eslint --fix \"./src/**/*.{ts,tsx}\"",
- "test:browser": "vitest"
+ "test:browser": "vitest --run"
},
"dependencies": {
"@radix-ui/react-dialog": "1.0.5",
@@ -28,7 +28,6 @@
"@radix-ui/react-visually-hidden": "1.0.3",
"@tanstack/react-query": "5.38.0",
"@tanstack/react-router": "^1.121.2",
- "@tanstack/react-router-devtools": "^1.121.2",
"canvas-confetti": "^1.9.3",
"clsx": "2.1.1",
"dompurify": "3.1.4",
@@ -52,6 +51,7 @@
"@tailwindcss/container-queries": "0.1.1",
"@tanstack/eslint-plugin-query": "^5.35.6",
"@tanstack/react-query-devtools": "^5.38.0",
+ "@tanstack/react-router-devtools": "^1.121.2",
"@tanstack/router-cli": "^1.128.8",
"@tanstack/router-plugin": "^1.128.8",
"@total-typescript/ts-reset": "0.6.1",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index a227b6a..f3678b4 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -38,9 +38,6 @@ importers:
'@tanstack/react-router':
specifier: ^1.121.2
version: 1.121.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
- '@tanstack/react-router-devtools':
- specifier: ^1.121.2
- version: 1.121.2(@tanstack/react-router@1.121.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@tanstack/router-core@1.128.8)(csstype@3.1.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tiny-invariant@1.3.3)
canvas-confetti:
specifier: ^1.9.3
version: 1.9.3
@@ -105,6 +102,9 @@ importers:
'@tanstack/react-query-devtools':
specifier: ^5.38.0
version: 5.38.0(@tanstack/react-query@5.38.0(react@18.3.1))(react@18.3.1)
+ '@tanstack/react-router-devtools':
+ specifier: ^1.121.2
+ version: 1.121.2(@tanstack/react-router@1.121.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@tanstack/router-core@1.128.8)(csstype@3.1.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tiny-invariant@1.3.3)
'@tanstack/router-cli':
specifier: ^1.128.8
version: 1.128.8
diff --git a/src/achievements.tsx b/src/achievements.tsx
index f0cb8bd..348ffb4 100644
--- a/src/achievements.tsx
+++ b/src/achievements.tsx
@@ -263,7 +263,7 @@ const init = {} as Record<
(typeof ACHIEVEMENTS_LIST)[number]["id"] | (string & {}),
() => boolean
>;
-export const achivementConditionCheckByIdMap = ACHIEVEMENTS_LIST.reduce((acc, achievement) => {
+export const achievementConditionCheckByIdMap = ACHIEVEMENTS_LIST.reduce((acc, achievement) => {
acc[achievement.id] = achievement.conditionCheck.bind(achievement);
return acc;
}, init);
diff --git a/src/components/AppProviders.tsx b/src/components/AppProviders.tsx
index 277c97b..c739b39 100644
--- a/src/components/AppProviders.tsx
+++ b/src/components/AppProviders.tsx
@@ -1,8 +1,7 @@
import * as Portal from "@radix-ui/react-portal";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
-import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { createRouter, RouterProvider } from "@tanstack/react-router";
-import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
+import { lazy, Suspense } from "react";
import type React from "react";
import { Toaster } from "react-hot-toast";
@@ -13,6 +12,13 @@ import { StopwatchContextProvider } from "./StopwatchContext";
import { ThemeContextProvider } from "./ThemeContext";
import { TooltipProvider } from "./Tooltip";
+const ReactQueryDevtools = import.meta.env.DEV
+ ? lazy(() => import("@tanstack/react-query-devtools").then((m) => ({ default: m.ReactQueryDevtools })))
+ : null;
+const TanStackRouterDevtools = import.meta.env.DEV
+ ? lazy(() => import("@tanstack/react-router-devtools").then((m) => ({ default: m.TanStackRouterDevtools })))
+ : null;
+
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
@@ -50,8 +56,12 @@ export const AppProviders = () => {
}}
/>
-
-
+ {ReactQueryDevtools && TanStackRouterDevtools && (
+
+
+
+
+ )}
diff --git a/src/components/ResultDialog.tsx b/src/components/ResultDialog.tsx
index 92ee585..6db29a1 100644
--- a/src/components/ResultDialog.tsx
+++ b/src/components/ResultDialog.tsx
@@ -1,7 +1,6 @@
import * as Dialog from "@radix-ui/react-dialog";
import * as Portal from "@radix-ui/react-portal";
import { useEffect, useState } from "react";
-import { toast } from "react-hot-toast";
import { useResetGame } from "../hooks/useResetGame";
import { useI18nContext } from "../i18n/i18n-react";
import {
@@ -12,6 +11,7 @@ import {
useIsWin,
useStartingArticle,
} from "../stores/GameStore";
+import { copyNotification } from "../utils/toast";
import { ModalContent, ModalDescription, ModalRoot, ModalTitle, ModalTrigger } from "./Modal";
import { StartArrowEnd } from "./StartArrowEnd";
import { StopwatchDisplay } from "./StopwatchDisplay";
@@ -31,8 +31,6 @@ export const ResultDialog = () => {
const cheatingAttempts = useCheatingAttempts();
const missedWins = history.slice(0, -2).reduce((acc, el) => acc + el.winningLinks, 0);
- const copyNotification = () => toast.success(LL["Copied to clipboard"](), { position: "top-center" });
-
useEffect(() => {
setOpen(isWin);
}, [isWin]);
@@ -83,7 +81,7 @@ export const ResultDialog = () => {
className="border-b-[1px] border-b-transparent hover:border-b-primary-blue focus-visible:border-b-primary-blue"
onClick={async () => {
await navigator.clipboard.writeText(window.location.href);
- copyNotification();
+ copyNotification(LL["Copied to clipboard"]());
}}
>
{LL["Share Result"]()}
diff --git a/src/components/Settings.helpers.tsx b/src/components/Settings.helpers.tsx
index b55e213..69b1839 100644
--- a/src/components/Settings.helpers.tsx
+++ b/src/components/Settings.helpers.tsx
@@ -1,18 +1,18 @@
import { useEffect } from "react";
-import { toast } from "react-hot-toast";
import type { Article } from "../stores/GameStore";
import { useSettingsStoreActions } from "../stores/SettingsStore";
+import { errorToast } from "../utils/toast";
import type { WikiRandom } from "./RandomButton/RandomButton.types";
export const getHighestLinksPage = (data: WikiRandom) => {
if (!data.query?.pages) return;
- const highestLinksPage = Object.values(data.query.pages)
- .filter((page) => Object.hasOwn(page, "linkshere"))
- .reduce((prev, current) => {
- const previousLinksphere = prev.linkshere ?? [];
- const currentLinksphere = current.linkshere ?? [];
- return previousLinksphere.length > currentLinksphere.length ? prev : current;
- });
+ const pagesWithLinks = Object.values(data.query.pages).filter((page) => Object.hasOwn(page, "linkshere"));
+ if (pagesWithLinks.length === 0) return;
+ const highestLinksPage = pagesWithLinks.reduce((prev, current) => {
+ const previousLinksphere = prev.linkshere ?? [];
+ const currentLinksphere = current.linkshere ?? [];
+ return previousLinksphere.length > currentLinksphere.length ? prev : current;
+ });
const title = highestLinksPage.title;
const pageid = highestLinksPage.pageid;
@@ -47,8 +47,6 @@ interface RandomSuccessProps {
failText: string;
}
-const errorToast = (text: string) => toast.error(text, { position: "bottom-center" });
-
export const handleOnRandomSuccess = ({ setArticle, data, failText }: RandomSuccessProps) => {
const articleWithLinks = getHighestLinksPage(data);
if (!articleWithLinks?.title || articleWithLinks.title.includes("(disambiguation)")) {
diff --git a/src/components/ThemeContext.tsx b/src/components/ThemeContext.tsx
index b0b9a0d..74b363f 100644
--- a/src/components/ThemeContext.tsx
+++ b/src/components/ThemeContext.tsx
@@ -9,7 +9,7 @@ export const ThemeContext = React.createContext(null);
export const ThemeContextProvider = ({ children }: { children: React.ReactNode }) => {
const defaultDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
- const [colorMode, setTheme] = useLocalStorage("theme", defaultDark ? "dark" : "light");
+ const [colorMode, setTheme] = useLocalStorage("color-theme", defaultDark ? "dark" : "light");
const switchTheme = () => {
const newTheme = colorMode === "light" ? "dark" : "light";
diff --git a/src/components/Wiki/Wiki.utils.ts b/src/components/Wiki/Wiki.utils.ts
index 04da644..653906c 100644
--- a/src/components/Wiki/Wiki.utils.ts
+++ b/src/components/Wiki/Wiki.utils.ts
@@ -1,10 +1,9 @@
import { useCallback, useEffect } from "react";
-import { toast } from "react-hot-toast";
import { useI18nContext } from "../../i18n/i18n-react";
import { useGameStoreActions, useIsGameRunning } from "../../stores/GameStore";
import { useIsCtrlFEnabled } from "../../stores/SettingsStore";
+import { errorToast } from "../../utils/toast";
-const errorToast = (text: string) => toast.error(text, { position: "bottom-center" });
const isNotDev = process.env.NODE_ENV !== "development";
export const useNoCheating = () => {
diff --git a/src/components/Wiki/WikiLogic.tsx b/src/components/Wiki/WikiLogic.tsx
index a0af05e..012c31b 100644
--- a/src/components/Wiki/WikiLogic.tsx
+++ b/src/components/Wiki/WikiLogic.tsx
@@ -1,12 +1,10 @@
import { useNavigate } from "@tanstack/react-router";
import type { MouseEvent } from "react";
-import { toast } from "react-hot-toast";
import { useI18nContext } from "../../i18n/i18n-react";
import { useGameStoreActions } from "../../stores/GameStore";
+import { errorToast } from "../../utils/toast";
import { useStopwatchActions } from "../StopwatchContext";
-const errorToast = (text: string) => toast.error(text, { position: "bottom-center" });
-
const IMAGE_EXT = [".jpg", ".jpeg", ".png", ".webp", ".avif", ".svg"];
const handleShowHideButton = (e: MouseEvent) => {
diff --git a/src/components/WikiLanguageSelect.tsx b/src/components/WikiLanguageSelect.tsx
index a3709e0..cff90f8 100644
--- a/src/components/WikiLanguageSelect.tsx
+++ b/src/components/WikiLanguageSelect.tsx
@@ -90,7 +90,7 @@ export const LANGUAGES: readonly WikiLanguage[] = [
{ value: "arz", label: "مصرى", isoCode: "" },
{ value: "pl", label: "Polski", isoCode: "pl" },
{ value: "ja", label: "日本語", isoCode: "jp" },
- { value: "zh", label: "中国人", isoCode: "cn" },
+ { value: "zh", label: "中文", isoCode: "zh" },
{ value: "vi", label: "Tiếng Việt", isoCode: "vi" },
{ value: "war", label: "Winaray", isoCode: "" },
{ value: "uk", label: "українська", isoCode: "" },
@@ -119,7 +119,7 @@ export const LANGUAGES: readonly WikiLanguage[] = [
{ label: "dansk", value: "da", isoCode: "" },
{ label: "български", value: "bg", isoCode: "" },
{ label: "Cymraeg", value: "cy", isoCode: "" },
- { label: "slovenčina", value: "sk ", isoCode: "" },
+ { label: "slovenčina", value: "sk", isoCode: "" },
{ label: "تۆرکجه", value: "azb", isoCode: "" },
{ label: "eesti", value: "et", isoCode: "" },
{ label: "қазақша", value: "kk", isoCode: "" },
@@ -142,7 +142,7 @@ export const LANGUAGES: readonly WikiLanguage[] = [
{ label: "বাংলা", value: "bn", isoCode: "" },
{ label: "македонски", value: "mk", isoCode: "" },
{ label: "asturianu", value: "ast", isoCode: "" },
- { label: "粵語 zh", value: "yue", isoCode: "" },
+ { label: "粵語", value: "yue", isoCode: "" },
{ label: "Ladin", value: "lld", isoCode: "" },
{ label: "latviešu", value: "lv", isoCode: "" },
{ label: "тоҷикӣ", value: "tg", isoCode: "" },
@@ -189,7 +189,7 @@ export const LANGUAGES: readonly WikiLanguage[] = [
{ label: "नेपाली", value: "ne", isoCode: "" },
{ label: "ગુજરાતી", value: "gu", isoCode: "" },
{ label: "ಕನ್ನಡ", value: "kn", isoCode: "" },
- { label: "Alemannisch", value: "als ", isoCode: "" },
+ { label: "Alemannisch", value: "als", isoCode: "" },
{ label: "interlingua", value: "ia", isoCode: "" },
{ label: "Kotava", value: "avk", isoCode: "" },
{ label: "Boarisch", value: "bar", isoCode: "" },
diff --git a/src/hooks/useStopwatch.ts b/src/hooks/useStopwatch.ts
index d75ae0d..3719804 100644
--- a/src/hooks/useStopwatch.ts
+++ b/src/hooks/useStopwatch.ts
@@ -93,7 +93,7 @@ const useStopwatch = () => {
return {
min: pad(String(minutes), 2),
sec: pad(String(seconds), 2),
- ms: pad(newMs.toFixed(0), 3),
+ ms: pad(String(Math.min(999, Math.round(newMs))), 3),
};
}, []);
diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx
index 67ee70e..647cbeb 100644
--- a/src/pages/Settings.tsx
+++ b/src/pages/Settings.tsx
@@ -1,7 +1,6 @@
import { useIsFetching } from "@tanstack/react-query";
import { useNavigate } from "@tanstack/react-router";
import { type FormEvent, useState } from "react";
-import { toast } from "react-hot-toast";
import ArticleAutocomplete from "../components/ArticleAutocomplete/ArticleAutocomplete";
import ArticlePreview from "../components/ArticlePreview/ArticlePreview";
import RandomButton from "../components/RandomButton/RandomButton";
@@ -21,6 +20,7 @@ import type { Article } from "../stores/GameStore";
import { useEndingArticle, useGameStoreActions, useStartingArticle } from "../stores/GameStore";
import { useIsCtrlFEnabled, useSettingsStoreActions, useWikiLanguage } from "../stores/SettingsStore";
import { useStatsStoreActions } from "../stores/StatisticsStore";
+import { copyNotification } from "../utils/toast";
const Settings = () => {
const { LL } = useI18nContext();
@@ -46,8 +46,6 @@ const Settings = () => {
trackedStats: ["single_random_pressed", "multiple_random_pressed", "article_preview_pressed"],
});
- const copyNotification = () => toast.success(LL["Copied to clipboard"](), { position: "top-center" });
-
const startGameHandler = (e: FormEvent) => {
e.preventDefault();
resetGame();
@@ -117,7 +115,7 @@ const Settings = () => {
className="mt-4 w-fit border-b-[1px] border-b-transparent py-3 hover:border-b-primary-blue focus-visible:border-b-primary-blue"
onClick={async () => {
await navigator.clipboard.writeText(`${window.location.href}&lang=${wikiLang}`);
- copyNotification();
+ copyNotification(LL["Copied to clipboard"]());
}}
>
{LL["Share settings"]()}
diff --git a/src/stores/GameStore.ts b/src/stores/GameStore.ts
index 556dd67..6ddf442 100644
--- a/src/stores/GameStore.ts
+++ b/src/stores/GameStore.ts
@@ -158,7 +158,6 @@ export const useStartingArticle = () => useGameStore((state) => state.startingAr
export const useEndingArticle = () => useGameStore((state) => state.endingArticle);
export const useHistory = () => useGameStore((state) => state.history);
export const useClicks = () => useGameStore((state) => (state.history.length > 1 ? state.history.length - 1 : 0));
-export const useCurrentArticle = () => useGameStore((state) => state.history.slice(-1)[0]?.title);
export const useIsWin = () => useGameStore((state) => state.isWin);
export const useCheatingAttempts = () => useGameStore((state) => state.cheatingAttempts);
diff --git a/src/stores/StatisticsStore.ts b/src/stores/StatisticsStore.ts
index d1064c7..3705283 100644
--- a/src/stores/StatisticsStore.ts
+++ b/src/stores/StatisticsStore.ts
@@ -2,7 +2,7 @@ import { produce } from "immer";
import { create } from "zustand";
import { createJSONStorage, devtools, persist } from "zustand/middleware";
import { immer } from "zustand/middleware/immer";
-import { ACHIEVEMENTS_LIST, type Achievement, achivementConditionCheckByIdMap } from "../achievements";
+import { ACHIEVEMENTS_LIST, type Achievement, achievementConditionCheckByIdMap } from "../achievements";
/*
Data gets persisted in local storage
@@ -160,7 +160,7 @@ export const useStatsStore = create()(
const typedPersistedState = persistedState as PersistedStore;
const unlockedAchievements = produce(currentState.achievements, (draftState) => {
- typedPersistedState.achievements.forEach((storageAchievement) => {
+ (typedPersistedState?.achievements ?? []).forEach((storageAchievement) => {
const completedAchievement = draftState.find(
(draftAchievement) => draftAchievement.id === storageAchievement.id,
);
@@ -186,7 +186,7 @@ export const useStatsStore = create()(
export const checkAchievements = (achievements: readonly Achievement[]) => {
return achievements.filter((achievement) => {
- const conditionFn = achivementConditionCheckByIdMap[achievement.id];
+ const conditionFn = achievementConditionCheckByIdMap[achievement.id];
return !achievement.unlocked && conditionFn();
});
};
diff --git a/src/types/typescript.ts b/src/types/typescript.ts
deleted file mode 100644
index e015060..0000000
--- a/src/types/typescript.ts
+++ /dev/null
@@ -1 +0,0 @@
-export const nameof = (name: keyof T) => name;
diff --git a/src/utils/toast.ts b/src/utils/toast.ts
new file mode 100644
index 0000000..d4e6284
--- /dev/null
+++ b/src/utils/toast.ts
@@ -0,0 +1,4 @@
+import { toast } from "react-hot-toast";
+
+export const errorToast = (text: string) => toast.error(text, { position: "bottom-center" });
+export const copyNotification = (text: string) => toast.success(text, { position: "top-center" });
\ No newline at end of file