mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 19:01:39 -04:00
Phase 2, part 4. Baseline 50 -> 15.
Migrated: all eight calculators, the rest of inventory and collections, the
Map/Zim/Docs cluster, Videos, Files, Search, Archive, DashboardPage,
DatesSelector, HelpModal, Events, Tags, Flasher's neighbours, admin Settings
and Status, Download, and App.
Also the three shared modules everything imports: api.js and customHooks.js
swap the Semantic toast library for the library's shim (same call shape), and
useForm.js renders the library's Message. useForm accepts both the old
`{content, header, positive|negative}` shape and the new `{kind, title}`,
because the pages building those objects migrate separately.
Two library gaps closed, both found by call sites needing them:
ActionInput is a forwardRef now. Its ref reaches the input rather than the
wrapper, because callers use it to focus or select the field -- a ref to the
surrounding div would do neither, which is why one call site had resorted to
hand-copying the component's markup.
Header takes a `color` token name. Three separate call sites had reached for
an inline style to colour a heading that carries meaning ("3 items to add"
against "2 to remove"), which is how a hex ends up in markup.
Two files needed repair after being interrupted mid-edit: MapViewer had a
Segment left in its error branch, and VideoPlayer had lost the useContext
import its own code still used. Neither jest nor tsc noticed; only
`npm run build` did, which is the whole reason that check exists.
Fifteen files remain: Archive, BulkTagModal, Channels, Docs, FileBrowser,
FilePreview, Flasher, Nav, Playlists, Theme.tsx, Upload, VideoPlayer, Zim,
ControllerPage, admin/Downloads. Theme.tsx is deliberately last -- the
unmigrated files still import its Semantic-backed wrappers, so it can only go
once they have.
131 lines
4.6 KiB
JavaScript
131 lines
4.6 KiB
JavaScript
import React, {useEffect, useState} from "react";
|
|
import {Link, useNavigate} from "react-router";
|
|
import {getMapSearchStatus, searchMap} from "../api";
|
|
import {Card, CardGroup, Header, Loading, Panel} from "./ui";
|
|
import {Paginator} from "./Common";
|
|
import {useLatestRequest, usePages} from "../hooks/customHooks";
|
|
import {QueryContext} from "../contexts/contexts";
|
|
import {MAP_VIEWER_URI} from "./Vars";
|
|
|
|
function formatPopulation(pop) {
|
|
if (!pop) return null;
|
|
if (pop >= 1_000_000) return `${(pop / 1_000_000).toFixed(1)}M`;
|
|
if (pop >= 1_000) return `${Math.round(pop / 1_000)}K`;
|
|
return `${pop}`;
|
|
}
|
|
|
|
function MapPlaceCard({place, index}) {
|
|
const navigate = useNavigate();
|
|
const [iframeSrc, setIframeSrc] = useState(null);
|
|
|
|
const zoom = place.min_zoom ? Math.max(8, Math.round(13 - place.min_zoom / 3)) : 11;
|
|
const embedSrc = `${MAP_VIEWER_URI}/embed.html#${place.lat},${place.lon},${zoom}`;
|
|
|
|
// Stagger iframe loading to avoid overwhelming the map viewer with simultaneous requests.
|
|
useEffect(() => {
|
|
const timer = setTimeout(() => setIframeSrc(embedSrc), 100 + index * 200);
|
|
return () => clearTimeout(timer);
|
|
}, [embedSrc, index]);
|
|
|
|
const handleClick = () => {
|
|
navigate(`/map?lat=${place.lat}&lon=${place.lon}&z=${zoom}`);
|
|
};
|
|
|
|
const subtitle = [];
|
|
if (place.region) subtitle.push(place.region);
|
|
const detail = place.kind_detail || place.kind || "";
|
|
if (detail && !place.region) subtitle.push(detail.replace(/_/g, " "));
|
|
const pop = formatPopulation(place.population);
|
|
if (pop) subtitle.push(`pop. ${pop}`);
|
|
|
|
const media = <div style={{height: 150, overflow: "hidden", position: "relative", background: "var(--head)"}}>
|
|
{iframeSrc && <iframe
|
|
src={iframeSrc}
|
|
title={place.name}
|
|
loading="lazy"
|
|
style={{
|
|
width: "100%",
|
|
height: "100%",
|
|
border: "none",
|
|
pointerEvents: "none",
|
|
}}
|
|
/>}
|
|
</div>;
|
|
|
|
return <Card
|
|
onClick={handleClick}
|
|
media={media}
|
|
title={place.name}
|
|
meta={subtitle.length > 0 ? subtitle.join(" · ") : undefined}
|
|
>
|
|
<div>{place.lat.toFixed(4)}, {place.lon.toFixed(4)}</div>
|
|
</Card>;
|
|
}
|
|
|
|
export function MapSearchView() {
|
|
const {searchParams} = React.useContext(QueryContext);
|
|
const searchStr = searchParams.get("q") || "";
|
|
const {offset, limit, activePage, setPage, totalPages, setTotal} = usePages(12);
|
|
// useLatestRequest discards stale responses and catches errors, so rapid pagination
|
|
// can't show out-of-order results and a network failure can't leave the spinner stuck.
|
|
const {data, sendRequest, loading} = useLatestRequest(300);
|
|
const [hasMaps, setHasMaps] = useState(null);
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
(async () => {
|
|
const status = await getMapSearchStatus();
|
|
if (!cancelled) {
|
|
const installed = (status.indexed || []).length + (status.missing || []).length;
|
|
setHasMaps(installed > 0);
|
|
}
|
|
})();
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (!searchStr || searchStr.length < 2) {
|
|
setTotal(0);
|
|
return;
|
|
}
|
|
sendRequest(async () => await searchMap(searchStr, limit, offset));
|
|
}, [searchStr, offset, limit, sendRequest]);
|
|
|
|
const results = (data && data.results) || [];
|
|
useEffect(() => {
|
|
setTotal((data && data.total) || 0);
|
|
}, [data, setTotal]);
|
|
|
|
if (hasMaps === false) {
|
|
return <Panel>
|
|
<Header as="h4">No maps are installed.</Header>
|
|
<p>Subscribe to a map region to search for places.</p>
|
|
<Link to="/map/manage">Go to Map Manager</Link>
|
|
</Panel>;
|
|
}
|
|
|
|
if (loading) {
|
|
return <Panel><Loading/></Panel>;
|
|
}
|
|
|
|
if (!searchStr) {
|
|
return <Panel><Header as="h4">Enter a search query to find map locations.</Header></Panel>;
|
|
}
|
|
|
|
if (results.length === 0) {
|
|
return <Panel><Header as="h4">No map locations found for "{searchStr}".</Header></Panel>;
|
|
}
|
|
|
|
return <React.Fragment>
|
|
<CardGroup minWidth={220}>
|
|
{results.map((place, i) =>
|
|
<MapPlaceCard key={`${place.name}-${place.lat}-${place.lon}-${i}`} place={place} index={i}/>
|
|
)}
|
|
</CardGroup>
|
|
<div style={{textAlign: 'center', marginTop: '2em'}}>
|
|
<Paginator activePage={activePage} totalPages={totalPages} onPageChange={setPage}/>
|
|
</div>
|
|
</React.Fragment>;
|
|
}
|