fifteen/apps/web/app/admin/cities/[slug]/page.tsx

131 lines
3.9 KiB
TypeScript

"use client";
import { useState, useEffect } from "react";
import { useParams, useRouter } from "next/navigation";
import { CityIngestProgress } from "@/components/city-ingest-progress";
interface CityDetail {
slug: string;
name: string;
country_code: string;
geofabrik_url: string;
status: string;
last_ingested: string | null;
poi_count: number;
grid_count: number;
error_message: string | null;
}
export default function CityDetailPage() {
const { slug } = useParams<{ slug: string }>();
const router = useRouter();
const [city, setCity] = useState<CityDetail | null>(null);
const [loading, setLoading] = useState(true);
const [jobId, setJobId] = useState<string | null>(null);
const [deleting, setDeleting] = useState(false);
useEffect(() => {
fetch(`/api/admin/cities`)
.then((r) => r.json())
.then((all: CityDetail[]) => {
setCity(all.find((c) => c.slug === slug) ?? null);
setLoading(false);
})
.catch(() => setLoading(false));
}, [slug]);
const handleReIngest = async () => {
const res = await fetch(`/api/admin/ingest/${slug}`, { method: "POST" });
if (res.ok) {
const data = await res.json();
setJobId(data.jobId);
}
};
const handleDelete = async () => {
if (!confirm(`Delete city "${city?.name}"? This will remove all POI and grid data.`)) return;
setDeleting(true);
const res = await fetch(`/api/admin/cities/${slug}`, { method: "DELETE" });
if (res.ok) router.push("/admin");
else setDeleting(false);
};
if (loading) return <div className="text-gray-500">Loading</div>;
if (!city)
return (
<div className="text-gray-500">
City not found.{" "}
<a href="/admin" className="text-brand-600 underline">
Back
</a>
</div>
);
return (
<div className="max-w-2xl mx-auto">
<div className="flex items-center gap-4 mb-6">
<a href="/admin" className="text-sm text-gray-500 hover:text-gray-700">
Back
</a>
<h1 className="text-2xl font-bold">{city.name}</h1>
<span className="badge bg-gray-100 text-gray-600 text-xs">
{city.slug}
</span>
</div>
{/* Stats */}
<div className="grid grid-cols-3 gap-4 mb-6">
{[
{ label: "POIs", value: city.poi_count.toLocaleString() },
{ label: "Grid Points", value: city.grid_count.toLocaleString() },
{
label: "Last Ingested",
value: city.last_ingested
? new Date(city.last_ingested).toLocaleDateString()
: "Never",
},
].map((s) => (
<div key={s.label} className="card text-center">
<p className="text-2xl font-bold text-gray-900">{s.value}</p>
<p className="text-sm text-gray-500 mt-1">{s.label}</p>
</div>
))}
</div>
{/* Source */}
<div className="card mb-6">
<h2 className="text-sm font-medium text-gray-700 mb-2">Data Source</h2>
<p className="text-xs text-gray-500 break-all">{city.geofabrik_url}</p>
</div>
{/* Live progress if ingesting */}
<CityIngestProgress jobId={jobId} className="card mb-6" />
{/* Actions */}
<div className="flex gap-3">
<button onClick={handleReIngest} className="btn-primary">
Re-ingest Data
</button>
<button
onClick={async () => {
const res = await fetch(`/api/admin/cities/${slug}/rerun-scores`, { method: "POST" });
if (res.ok) {
const data = await res.json();
setJobId(data.jobId);
}
}}
className="btn-secondary"
>
Rerun Scores
</button>
<button
onClick={handleDelete}
disabled={deleting}
className="btn-danger"
>
{deleting ? "Deleting…" : "Delete City"}
</button>
</div>
</div>
);
}