diff --git a/package-lock.json b/package-lock.json index 367bfcf..4e0e894 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,9 +13,11 @@ "@vercel/speed-insights": "^1.1.0", "axios": "^1.7.9", "clsx": "^2.1.1", + "nuqs": "^2.2.3", "react": "^18.3.1", "react-dom": "^18.3.1", "react-medium-image-zoom": "^5.2.12", + "react-router-dom": "^7.1.1", "tailwind-merge": "^2.6.0" }, "devDependencies": { @@ -2123,6 +2125,12 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", @@ -3358,6 +3366,15 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -5389,6 +5406,12 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -8326,6 +8349,32 @@ "inBundle": true, "license": "ISC" }, + "node_modules/nuqs": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/nuqs/-/nuqs-2.2.3.tgz", + "integrity": "sha512-nMCcUW06KSqEXA0xp+LiRqDpIE59BVYbjZLe0HUisJAlswfihHYSsAjYTzV0lcE1thfh8uh+LqUHGdQ8qq8rfA==", + "license": "MIT", + "dependencies": { + "mitt": "^3.0.1" + }, + "peerDependencies": { + "@remix-run/react": ">=2", + "next": ">=14.2.0", + "react": ">=18.2.0 || ^19.0.0-0", + "react-router-dom": ">=6" + }, + "peerDependenciesMeta": { + "@remix-run/react": { + "optional": true + }, + "next": { + "optional": true + }, + "react-router-dom": { + "optional": true + } + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -9042,6 +9091,46 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.1.1.tgz", + "integrity": "sha512-39sXJkftkKWRZ2oJtHhCxmoCrBCULr/HAH4IT5DHlgu/Q0FCPV0S4Lx+abjDTx/74xoZzNYDYbOZWlJjruyuDQ==", + "license": "MIT", + "dependencies": { + "@types/cookie": "^0.6.0", + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0", + "turbo-stream": "2.4.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.1.1.tgz", + "integrity": "sha512-vSrQHWlJ5DCfyrhgo0k6zViOe9ToK8uT5XGSmnuC2R3/g261IdIMpZVqfjD6vWSXdnf5Czs4VA/V60oVR6/jnA==", + "license": "MIT", + "dependencies": { + "react-router": "7.1.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -9628,6 +9717,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -10777,6 +10872,12 @@ "@esbuild/win32-x64": "0.23.1" } }, + "node_modules/turbo-stream": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz", + "integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==", + "license": "ISC" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/package.json b/package.json index 1580b38..b622445 100644 --- a/package.json +++ b/package.json @@ -16,9 +16,11 @@ "@vercel/speed-insights": "^1.1.0", "axios": "^1.7.9", "clsx": "^2.1.1", + "nuqs": "^2.2.3", "react": "^18.3.1", "react-dom": "^18.3.1", "react-medium-image-zoom": "^5.2.12", + "react-router-dom": "^7.1.1", "tailwind-merge": "^2.6.0" }, "devDependencies": { diff --git a/src/App.tsx b/src/App.tsx index 1bae5e1..a716f94 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,12 +1,13 @@ -import { useState } from 'react' -import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { useState, useEffect } from 'react' +import { useQueryState, parseAsInteger } from 'nuqs' +import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query' import { MapSearch } from './components/map-search' import { MapModal } from './components/map-modal' import { Navbar } from './components/navbar' import { Footer } from './components/footer' import { findPath } from './lib/pathfinding' import type { MapInfo, PathStep } from './types/map' -import { getMapImageUrl, getMapIconUrl } from './lib/api' +import { getMapImageUrl, getMapIconUrl, getAllMaps } from './lib/api' const queryClient = new QueryClient({ defaultOptions: { @@ -18,8 +19,53 @@ const queryClient = new QueryClient({ }) function PathfinderApp() { + const [sourceMapId, setSourceMapIdRaw] = useQueryState( + 'from', + parseAsInteger.withDefault(-1) + ) + + const [targetMapId, setTargetMapIdRaw] = useQueryState( + 'to', + parseAsInteger.withDefault(-1) + ) + + const setSourceMapId = (value: number | null) => + setSourceMapIdRaw(value === null ? -1 : value) + + const setTargetMapId = (value: number | null) => + setTargetMapIdRaw(value === null ? -1 : value) + + const effectiveSourceMapId = sourceMapId === -1 ? null : sourceMapId + const effectiveTargetMapId = targetMapId === -1 ? null : targetMapId + const [sourceMap, setSourceMap] = useState(null) const [targetMap, setTargetMap] = useState(null) + + // Fetch all maps once and cache them + const { data: maps } = useQuery({ + queryKey: ['maps'], + queryFn: getAllMaps, + staleTime: Infinity // Cache the maps permanently + }) + + // Update maps when IDs change or when maps data is loaded + useEffect(() => { + if (maps) { + if (effectiveSourceMapId !== null) { + const map = maps.find((m: MapInfo) => m.id === effectiveSourceMapId) + if (map) setSourceMap(map) + } else { + setSourceMap(null) + } + + if (effectiveTargetMapId !== null) { + const map = maps.find((m: MapInfo) => m.id === effectiveTargetMapId) + if (map) setTargetMap(map) + } else { + setTargetMap(null) + } + } + }, [sourceMapId, targetMapId, maps]) const [path, setPath] = useState(null) const [error, setError] = useState(null) const [isLoading, setIsLoading] = useState(false) @@ -44,9 +90,12 @@ function PathfinderApp() { } } - function handleSwapMaps() { - setSourceMap(targetMap) - setTargetMap(sourceMap) + async function handleSwapMaps() { + const tempSourceId = effectiveSourceMapId + await Promise.all([ + setSourceMapId(effectiveTargetMapId), + setTargetMapId(tempSourceId) + ]) setPath(null) setError(null) } @@ -62,7 +111,7 @@ function PathfinderApp() {
setSourceMapId(map.id)} placeholder="Where are you now?" /> {sourceMap && ( @@ -94,7 +143,7 @@ function PathfinderApp() {
setTargetMapId(map.id)} placeholder="Where do you want to go?" /> {targetMap && ( diff --git a/src/main.tsx b/src/main.tsx index 7970db1..e27d52c 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,10 +1,10 @@ import React from 'react' import ReactDOM from 'react-dom/client' -import App from './App' import './styles/globals.css' import { SpeedInsights } from "@vercel/speed-insights/react" import { Analytics } from "@vercel/analytics/react" import { initializePathfinding } from './lib/pathfinding' +import { Router } from './router' // Initialize pathfinding system initializePathfinding().catch(error => { @@ -24,7 +24,7 @@ initializePathfinding().catch(error => { ReactDOM.createRoot(document.getElementById('root')!).render( <> - + diff --git a/src/router.tsx b/src/router.tsx new file mode 100644 index 0000000..92ecd7a --- /dev/null +++ b/src/router.tsx @@ -0,0 +1,18 @@ +import { NuqsAdapter } from 'nuqs/adapters/react-router' +import { createBrowserRouter, RouterProvider } from 'react-router-dom' +import App from './App' + +const router = createBrowserRouter([ + { + path: '/', + element: + } +]) + +export function Router() { + return ( + + + + ) +}