Show empty-state CTA on Route detail page when no waypoints

Observed: both non-maintainer users (pistazie, nelli) created a route,
never planned in it, and stopped. The Route detail page rendered as a
dead end — name, a row of buttons, then blank.

Changes:
- Detect "empty" as no geometry AND no distance (the state every new
  route has before any planning happens).
- When empty, hide the top-row button cluster (Edit in Planner / Edit
  / Export GPX) and render a centered dashed-border card in its place.
- Card shows: large headline, short body (owner-specific vs
  viewer-only copy), primary "Open in Planner" button (same action as
  the existing top-row button, just visually prominent), and a
  secondary "or upload a GPX file" link to the edit page.
- Bilingual copy added under routes.empty.*.

No non-empty route loses any UI — those paths stay identical.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ullrich Schäfer 2026-04-19 08:13:24 +02:00
parent 1158080b26
commit 5b40bd9b00
No known key found for this signature in database
GPG key ID: A32FF691A0F752D9
3 changed files with 49 additions and 1 deletions

View file

@ -99,6 +99,11 @@ export default function RouteDetailPage({ loaderData }: Route.ComponentProps) {
const [editLoading, setEditLoading] = useState(false);
const [highlightedDay, setHighlightedDay] = useState<number | null>(null);
// A route is "empty" when it has no geometry and no computed distance —
// i.e. nobody has planned any waypoints yet. Surfaced as a dedicated
// empty-state below so the page isn't a dead end.
const isEmpty = !route.geojson && route.distance == null;
const handleEditInPlanner = useCallback(async () => {
setEditLoading(true);
try {
@ -121,7 +126,7 @@ export default function RouteDetailPage({ loaderData }: Route.ComponentProps) {
<p className="mt-2 text-gray-600">{route.description}</p>
)}
</div>
{isOwner && (
{isOwner && !isEmpty && (
<div className="flex flex-wrap gap-2">
<button
onClick={handleEditInPlanner}
@ -222,6 +227,35 @@ export default function RouteDetailPage({ loaderData }: Route.ComponentProps) {
</div>
)}
{isEmpty && (
<div className="mt-6 flex flex-col items-center rounded-lg border border-dashed border-gray-300 bg-gray-50 px-6 py-12 text-center">
<div className="text-4xl" aria-hidden="true">🗺</div>
<h2 className="mt-4 text-lg font-semibold text-gray-900">
{t("routes.empty.heading")}
</h2>
<p className="mt-2 max-w-md text-sm text-gray-600">
{isOwner ? t("routes.empty.bodyOwner") : t("routes.empty.bodyViewer")}
</p>
{isOwner && (
<div className="mt-6 flex flex-wrap items-center justify-center gap-3">
<button
onClick={handleEditInPlanner}
disabled={editLoading}
className="rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 disabled:opacity-50"
>
{editLoading ? t("routes.opening") : t("routes.empty.openInPlanner")}
</button>
<a
href={`/routes/${route.id}/edit`}
className="text-sm text-gray-600 underline hover:text-gray-900"
>
{t("routes.empty.uploadGpx")}
</a>
</div>
)}
</div>
)}
{versions.length > 0 && (
<div className="mt-8">
<h2 className="text-lg font-semibold text-gray-900">Version History</h2>

View file

@ -179,6 +179,13 @@ export default {
noRoutesYet: "Noch keine Routen. Erstelle deine erste Route!",
noMapPreview: "Keine Kartenvorschau",
saveChanges: "Änderungen speichern",
empty: {
heading: "Diese Route ist leer",
bodyOwner: "Du hast noch keine Wegpunkte geplant. Öffne sie im Planer, um eine Strecke zu skizzieren, oder lade eine GPX-Datei hoch.",
bodyViewer: "Diese Route enthält noch keine Wegpunkte.",
openInPlanner: "Im Planer öffnen",
uploadGpx: "oder eine GPX-Datei hochladen",
},
},
activities: {
title: "Aktivitäten",

View file

@ -179,6 +179,13 @@ export default {
noRoutesYet: "No routes yet. Create your first route!",
noMapPreview: "No map preview",
saveChanges: "Save Changes",
empty: {
heading: "This route is empty",
bodyOwner: "You haven't planned any waypoints yet. Open it in the Planner to sketch a path, or upload a GPX file.",
bodyViewer: "This route doesn't have any waypoints yet.",
openInPlanner: "Open in Planner",
uploadGpx: "or upload a GPX file",
},
},
activities: {
title: "Activities",