From 6458bdd7c234df7fcd1af2ed972acfb7a15501f0 Mon Sep 17 00:00:00 2001 From: Mygod Date: Mon, 6 Oct 2025 11:34:54 -0700 Subject: [PATCH 1/2] feat: jump to area --- packages/locales/lib/human/en.json | 2 + src/features/drawer/areas/AreaTable.jsx | 156 +++++++++++++++++++++++- 2 files changed, 155 insertions(+), 3 deletions(-) diff --git a/packages/locales/lib/human/en.json b/packages/locales/lib/human/en.json index 16feaeaff..67801d731 100644 --- a/packages/locales/lib/human/en.json +++ b/packages/locales/lib/human/en.json @@ -65,6 +65,8 @@ "pokemon": "Pokémon", "wayfarer": "Wayfarer", "scan_areas": "Scan Areas", + "jump_to_areas": "Jump to areas", + "jump_to_areas_attribution": "Search powered by OpenStreetMap", "s2cells": "S2 Cells", "weather": "Weather", "admin": "Admin", diff --git a/src/features/drawer/areas/AreaTable.jsx b/src/features/drawer/areas/AreaTable.jsx index 9bd65221d..669c2381b 100644 --- a/src/features/drawer/areas/AreaTable.jsx +++ b/src/features/drawer/areas/AreaTable.jsx @@ -1,15 +1,24 @@ // @ts-check import * as React from 'react' import { useQuery } from '@apollo/client' +import { useTranslation } from 'react-i18next' +import Box from '@mui/material/Box' +import Button from '@mui/material/Button' +import CircularProgress from '@mui/material/CircularProgress' import Paper from '@mui/material/Paper' import Table from '@mui/material/Table' import TableBody from '@mui/material/TableBody' +import TableCell from '@mui/material/TableCell' import TableRow from '@mui/material/TableRow' import TableContainer from '@mui/material/TableContainer' +import Typography from '@mui/material/Typography' import { Query } from '@services/queries' import { useMemory } from '@store/useMemory' import { useStorage } from '@store/useStorage' +import { useMapStore } from '@store/useMapStore' + +/** @typedef {{ id: string, name: string, lat: number, lon: number }} JumpResult */ import { AreaParent } from './Parent' import { AreaChild } from './Child' @@ -17,10 +26,26 @@ import { AreaChild } from './Child' export function ScanAreasTable() { /** @type {import('@apollo/client').QueryResult<{ scanAreasMenu: import('@rm/types').Config['areas']['scanAreasMenu'][string] }>} */ const { data, loading, error } = useQuery(Query.scanAreasMenu()) - const search = useStorage( - (s) => s.filters.scanAreas?.filter?.search?.toLowerCase() || '', + const { t, i18n } = useTranslation() + const rawSearch = useStorage((s) => s.filters.scanAreas?.filter?.search || '') + const search = React.useMemo(() => rawSearch.toLowerCase(), [rawSearch]) + const trimmedSearch = React.useMemo(() => rawSearch.trim(), [rawSearch]) + const { misc, general } = useMemory.getState().config + const jumpZoom = general?.scanAreasZoom || general?.startZoom || 12 + /** @type {[JumpResult[], React.Dispatch>]} */ + const [jumpResults, setJumpResults] = React.useState([]) + const [jumpLoading, setJumpLoading] = React.useState(false) + const [jumpError, setJumpError] = React.useState(false) + + const handleJump = React.useCallback( + (target) => { + const mapInstance = useMapStore.getState().map + if (mapInstance) { + mapInstance.flyTo([target.lat, target.lon], jumpZoom) + } + }, + [jumpZoom], ) - const { misc } = useMemory.getState().config /** @type {string[]} */ const allAreas = React.useMemo( @@ -64,6 +89,73 @@ export function ScanAreasTable() { [data, search], ) + const totalMatches = React.useMemo( + () => allRows.reduce((sum, area) => sum + area.children.length, 0), + [allRows], + ) + + const showJumpResults = + trimmedSearch.length >= 3 && totalMatches === 0 && !loading && !error + + React.useEffect(() => { + if (!showJumpResults) { + if (trimmedSearch.length < 3) { + setJumpResults([]) + } + setJumpLoading(false) + setJumpError(false) + return + } + + setJumpResults([]) + const controller = new AbortController() + const timer = window.setTimeout(() => { + setJumpLoading(true) + fetch( + `https://nominatim.openstreetmap.org/search?format=json&limit=5&q=${encodeURIComponent(trimmedSearch)}&accept-language=${encodeURIComponent(i18n.language || 'en')}`, + { signal: controller.signal, headers: { Accept: 'application/json' } }, + ) + .then((res) => { + if (!res.ok) throw new Error('Nominatim request failed') + return res.json() + }) + .then((json) => { + if (!Array.isArray(json)) { + setJumpResults([]) + return + } + setJumpResults( + json + .slice(0, 5) + .map((item) => ({ + id: String(item.place_id), + name: item.display_name, + lat: Number(item.lat), + lon: Number(item.lon), + })) + .filter( + (item) => + Number.isFinite(item.lat) && Number.isFinite(item.lon), + ), + ) + setJumpError(false) + }) + .catch((err) => { + if (err.name === 'AbortError') return + setJumpResults([]) + setJumpError(true) + }) + .finally(() => { + setJumpLoading(false) + }) + }, 400) + + return () => { + clearTimeout(timer) + controller.abort() + } + }, [showJumpResults, trimmedSearch, i18n.language]) + if (loading || error) return null return ( @@ -122,6 +214,64 @@ export function ScanAreasTable() { ) })} + {showJumpResults && ( + + + + + {t('jump_to_areas')} + + {jumpLoading ? ( + + + + {t('searching')} + + + ) : jumpError ? ( + + {t('local_error')} + + ) : jumpResults.length ? ( + jumpResults.map((result) => ( + + )) + ) : ( + {t('no_options')} + )} + + {t('jump_to_areas_attribution')} + + + + + )} From 59b9609c206f4f3ccb3edd94749a16c747b98ce7 Mon Sep 17 00:00:00 2001 From: Mygod Date: Fri, 17 Oct 2025 16:17:21 -0700 Subject: [PATCH 2/2] fix: code review --- packages/locales/lib/human/en.json | 1 - src/features/drawer/areas/AreaTable.jsx | 5 +---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/locales/lib/human/en.json b/packages/locales/lib/human/en.json index daf77811b..5a030c1f4 100644 --- a/packages/locales/lib/human/en.json +++ b/packages/locales/lib/human/en.json @@ -65,7 +65,6 @@ "pokemon": "Pokémon", "wayfarer": "Wayfarer", "scan_areas": "Scan Areas", - "jump_to_areas": "Jump to areas", "jump_to_areas_attribution": "Search powered by OpenStreetMap", "s2cells": "S2 Cells", "weather": "Weather", diff --git a/src/features/drawer/areas/AreaTable.jsx b/src/features/drawer/areas/AreaTable.jsx index 669c2381b..ffdfcf83d 100644 --- a/src/features/drawer/areas/AreaTable.jsx +++ b/src/features/drawer/areas/AreaTable.jsx @@ -108,9 +108,9 @@ export function ScanAreasTable() { } setJumpResults([]) + setJumpLoading(true) const controller = new AbortController() const timer = window.setTimeout(() => { - setJumpLoading(true) fetch( `https://nominatim.openstreetmap.org/search?format=json&limit=5&q=${encodeURIComponent(trimmedSearch)}&accept-language=${encodeURIComponent(i18n.language || 'en')}`, { signal: controller.signal, headers: { Accept: 'application/json' } }, @@ -218,9 +218,6 @@ export function ScanAreasTable() { - - {t('jump_to_areas')} - {jumpLoading ? (