- {/* Geocoder */}
results.length > 0 && setShowDropdown(true)}
className="block w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-brand-500 focus:outline-none focus:ring-1 focus:ring-brand-500"
/>
- {searching && (
- Searching…
- )}
+ {searching && Searching…}
-
{showDropdown && results.length > 0 && (
{results.map((r) => (
))}
)}
-
- {/* Mini map */}
-
-
- {selected?.geojson && (
+
+ {selected?.geojson ? (
✓ Boundary: {selected.display_name.split(",").slice(0, 2).join(", ")} ({selected.geojson.type})
- )}
- {!selected && (
+ ) : (
Search for a city to set its administrative boundary and auto-fill the fields below.
@@ -481,71 +282,119 @@ function LocationSelector({
);
}
-// ─── Confirm step ─────────────────────────────────────────────────────────────
+// ─── Main page ────────────────────────────────────────────────────────────────
-function ConfirmStep({
- region,
- onBack,
- onConfirm,
-}: {
- region: GeofabrikFeature;
- onBack: () => void;
- onConfirm: (
- slug: string,
- name: string,
- countryCode: string,
- boundary: { type: string; coordinates: unknown } | null,
- ) => Promise
;
-}) {
- const defaultSlug = region.properties.id.replace(/\//g, "-");
- const [slug, setSlug] = useState(defaultSlug);
- const [name, setName] = useState(region.properties.name);
+export default function AddCityPage() {
+ const [name, setName] = useState("");
+ const [slug, setSlug] = useState("");
const [countryCode, setCountryCode] = useState("");
const [boundary, setBoundary] = useState<{ type: string; coordinates: unknown } | null>(null);
+ const [matchedRegion, setMatchedRegion] = useState(null);
+ const [jobId, setJobId] = useState(null);
+ const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
+ const geofabrikIndexRef = useRef(null);
+
+ // Fetch Geofabrik index eagerly so it's ready when the user picks a city.
+ useEffect(() => {
+ fetch("/api/admin/geofabrik")
+ .then((r) => r.json())
+ .then((data: GeofabrikIndex) => { geofabrikIndexRef.current = data; })
+ .catch(() => {});
+ }, []);
+
+ const geofabrikUrl = matchedRegion?.properties.urls?.pbf ?? undefined;
const handleBoundaryChange = useCallback(
(b: { type: string; coordinates: unknown } | null) => setBoundary(b),
[],
);
- const KNOWN_COUNTRIES = ["DE", "NL", "DK"];
const handleResultSelect = useCallback(
- (result: { name: string; countryCode: string } | null) => {
- if (!result) return;
+ (result: { name: string; countryCode: string; lat: number; lon: number } | null) => {
+ if (!result) { setMatchedRegion(null); return; }
setName(result.name);
setSlug(toSlug(result.name));
- setCountryCode(KNOWN_COUNTRIES.includes(result.countryCode) ? result.countryCode : "");
+ setCountryCode(COUNTRIES.some((c) => c.code === result.countryCode) ? result.countryCode : "");
+ const best = geofabrikIndexRef.current
+ ? findBestRegion(result.lat, result.lon, geofabrikIndexRef.current)
+ : null;
+ setMatchedRegion(best);
},
[],
);
- return (
-
-
Confirm City Details
+ const handleSubmit = async () => {
+ if (!slug || !name || !geofabrikUrl) return;
+ setLoading(true);
+ setError(null);
+ try {
+ const res = await fetch("/api/admin/cities", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ slug, name, countryCode, geofabrikUrl,
+ ...(boundary ? { boundary } : {}),
+ }),
+ });
+ if (!res.ok) {
+ const data = await res.json();
+ setError(data.error ?? "Failed to start ingestion");
+ return;
+ }
+ const data = await res.json();
+ setJobId(data.jobId);
+ } catch (e) {
+ setError(String(e));
+ } finally {
+ setLoading(false);
+ }
+ };
-
- {/* City boundary selector — at the top so it can auto-fill the fields below */}
+ if (jobId) {
+ return (
+
+ );
+ }
+
+ return (
+
+
Add City
+
+ Search for a city to configure and start ingestion.
+
+
+ {error && (
+
{error}
+ )}
+
+
+ {/* Geocoder + map */}
+ {/* Name + Slug */}
-
-
-
-
-
-
-
);
}
-
-// ─── Progress step ────────────────────────────────────────────────────────────
-
-
-
-// ─── Main page ────────────────────────────────────────────────────────────────
-
-export default function AddCityPage() {
- const [step, setStep] = useState
("browse");
- const [selected, setSelected] = useState(null);
- const [jobId, setJobId] = useState(null);
- const [ingestError, setIngestError] = useState(null);
-
- const handleConfirm = async (
- slug: string,
- name: string,
- countryCode: string,
- boundary: { type: string; coordinates: unknown } | null,
- ) => {
- setIngestError(null);
- try {
- const res = await fetch("/api/admin/cities", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({
- slug,
- name,
- countryCode,
- geofabrikUrl: selected!.properties.urls.pbf,
- ...(boundary ? { boundary } : {}),
- }),
- });
- if (!res.ok) {
- const data = await res.json();
- setIngestError(data.error ?? "Failed to start ingestion");
- return;
- }
- const data = await res.json();
- setJobId(data.jobId);
- setStep("ingest");
- } catch (e) {
- setIngestError(String(e));
- }
- };
-
- return (
-
-
Add City
-
- Select an OpenStreetMap region to import for 15-minute city analysis.
-
-
-
-
- {ingestError && (
-
- {ingestError}
-
- )}
-
- {step === "browse" && (
-
{
- setSelected(f);
- setStep("confirm");
- }}
- />
- )}
- {step === "confirm" && selected && (
- setStep("browse")}
- onConfirm={handleConfirm}
- />
- )}
- {step === "ingest" && }
-
- );
-}