feat(frontend): load systems from PocketBase and add system focus to region page
This commit is contained in:
@@ -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 Aho–Corasick 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 (
|
||||||
|
@@ -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>
|
||||||
);
|
);
|
||||||
|
Reference in New Issue
Block a user