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