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