feat(frontend): load systems from PocketBase and add system focus to region page

This commit is contained in:
2025-08-10 22:18:17 +02:00
parent 9c40135102
commit 2561cd7d30
2 changed files with 23 additions and 19 deletions

View File

@@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { AhoCorasick } from '@/lib/aho'; import { AhoCorasick } from '@/lib/aho';
import pb from '@/lib/pocketbase';
interface SearchResult { interface SearchResult {
system: string; system: string;
@@ -10,21 +11,25 @@ interface SearchResult {
} }
async function loadAllSystems(): Promise<Array<SearchResult>> { async function loadAllSystems(): Promise<Array<SearchResult>> {
// Fetch the list of regions from universe.json then fetch each region JSON for systems // Fetch system names with regions from PocketBase, paginated and minimal fields
const res = await fetch('/universe.json'); const perPage = 1000;
if (!res.ok) return []; let page = 1;
const regions: Array<{ regionName: string } & Record<string, any>> = await res.json();
const out: Array<SearchResult> = []; const out: Array<SearchResult> = [];
await Promise.all(regions.map(async (r) => { const seen = new Set<string>();
try { // loop pages until fewer than perPage items
const rr = await fetch(`/${encodeURIComponent(r.regionName)}.json`); while (true) {
if (!rr.ok) return; const res = await pb.collection('regionview').getList(page, perPage, { fields: 'sysname,sysregion' });
const systems: Array<{ solarSystemName: string } & Record<string, any>> = await rr.json(); for (const item of res.items as any[]) {
for (const s of systems) { const system: string = item.sysname;
out.push({ system: s.solarSystemName, region: r.regionName }); const region: string = item.sysregion;
if (!seen.has(system)) {
seen.add(system);
out.push({ system, region });
} }
} catch (_) { /* noop */ } }
})); if (res.items.length < perPage) break;
page += 1;
}
return out; return out;
} }
@@ -36,7 +41,6 @@ export const SearchDialog: React.FC = () => {
const [results, setResults] = useState<Array<SearchResult>>([]); const [results, setResults] = useState<Array<SearchResult>>([]);
const inputRef = useRef<HTMLInputElement | null>(null); const inputRef = useRef<HTMLInputElement | null>(null);
// Build AC automaton of single query to enable contains search efficiently (case-insensitive)
const automaton = useMemo(() => { const automaton = useMemo(() => {
const ac = new AhoCorasick(); const ac = new AhoCorasick();
if (query.trim().length > 0) ac.add(query.toLowerCase()); if (query.trim().length > 0) ac.add(query.toLowerCase());
@@ -60,17 +64,14 @@ export const SearchDialog: React.FC = () => {
useEffect(() => { useEffect(() => {
if (!open) return; if (!open) return;
if (all.length === 0) { if (all.length === 0) {
// lazy load once
loadAllSystems().then(setAll); loadAllSystems().then(setAll);
} }
}, [open, all.length]); }, [open, all.length]);
useEffect(() => { useEffect(() => {
if (query.trim().length === 0) { setResults([]); return; } if (query.trim().length === 0) { setResults([]); return; }
const q = query.toLowerCase();
const out: Array<SearchResult> = []; const out: Array<SearchResult> = [];
for (const r of all) { for (const r of all) {
// contains via AhoCorasick over the single query pattern
if (automaton.searchHas(r.system.toLowerCase())) { if (automaton.searchHas(r.system.toLowerCase())) {
out.push(r); out.push(r);
if (out.length >= 10) break; if (out.length >= 10) break;
@@ -82,7 +83,7 @@ export const SearchDialog: React.FC = () => {
const onSelect = (r: SearchResult) => { const onSelect = (r: SearchResult) => {
setOpen(false); setOpen(false);
setQuery(''); setQuery('');
navigate(`/regions/${encodeURIComponent(r.region)}/${encodeURIComponent(r.system)}`); navigate(`/regions/${encodeURIComponent(r.region)}?focus=${encodeURIComponent(r.system)}`);
}; };
return ( return (

View File

@@ -1,4 +1,4 @@
import { useParams } from 'react-router-dom'; import { useParams, useSearchParams } from 'react-router-dom';
import { RegionMap } from '@/components/RegionMap'; import { RegionMap } from '@/components/RegionMap';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { ArrowLeft } from 'lucide-react'; import { ArrowLeft } from 'lucide-react';
@@ -6,6 +6,8 @@ import { useNavigate } from 'react-router-dom';
export const RegionPage = () => { export const RegionPage = () => {
const { region } = useParams<{ region: string }>(); const { region } = useParams<{ region: string }>();
const [sp] = useSearchParams();
const focus = sp.get('focus') || undefined;
const navigate = useNavigate(); const navigate = useNavigate();
if (!region) { if (!region) {
@@ -31,6 +33,7 @@ export const RegionPage = () => {
<RegionMap <RegionMap
regionName={region} regionName={region}
isWormholeRegion={region === "Wormhole"} isWormholeRegion={region === "Wormhole"}
focusSystem={focus}
/> />
</div> </div>
); );