diff --git a/AGENTS.md b/AGENTS.md
index 4405f55..72e70ae 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -123,15 +123,15 @@ session with **no other memory of this project**. So:
`find_spots` / `find_events` / `find_coaches` (all Lab kinds 35001–35014) /
`find_gear` / `find_crews` / `find_shops` / `find_tours` +
`get_community_stats` — kept at parity with the app's `useCVMClient`.
-- **Routing / directions**: `RouteDialog` (opened from the spot / shop / event /
- food detail pages) draws an in-app route on Leaflet. Road modes
- (skate-walk / bike / moto / car) come from keyless **OSRM** via the
+- **Routing / directions**: `RouteDialog` is the single in-app router, opened
+ from the map spot sheet, the food cards, and the spot / shop / event detail
+ pages (MapPage's old external "Open with" Google/OSM/Waze menu was removed).
+ Road modes (skate-walk / bike / moto / car) come from keyless **OSRM** via the
`proxy.shakespeare.diy` CORS proxy (`src/lib/routing.ts`); moto≈car is
labelled honestly. **Transit** mode uses **Transitous** (`api.transitous.org`,
MOTIS 2, open CORS) — note origin/destination go to that service, unlike road
- modes. An opt-in **"Open in"** handoff (`src/lib/directions.ts`) deep-links
- Google / Apple / OSM (+ Organic Maps via `geo:` on Android). No scooter/moped
- profile (deliberate).
+ modes. **No external map-app handoff anywhere** (product decision) and no
+ scooter/moped profile.
- **Mints directory**: `/mints` page lists Cashu mints from the community
**MintRadar** project. MintRadar has **no CORS**, so it's fetched same-origin
at `/api/mints/*` — prod proxies via `public/_redirects` (`/api/mints/* →
diff --git a/src/components/RouteDialog.tsx b/src/components/RouteDialog.tsx
index c801b7f..a4cce27 100644
--- a/src/components/RouteDialog.tsx
+++ b/src/components/RouteDialog.tsx
@@ -22,7 +22,6 @@ import {
type TravelMode,
type RouteResult,
} from '@/lib/routing';
-import { buildDirectionsLinks, openExternal } from '@/lib/directions';
// ─── Transit presentation helpers ────────────────────────────────────────────
@@ -415,27 +414,6 @@ export function RouteDialog({
)}
-
- {/* Open in maps — opt-in handoff to an external app (this shares the trip) */}
- {origin && destination && (
-
-
- Open in
-
- {buildDirectionsLinks(origin, destination, mode, destinationLabel).map((link) => (
-
- ))}
-
- )}
);
diff --git a/src/lib/directions.ts b/src/lib/directions.ts
deleted file mode 100644
index 5403f16..0000000
--- a/src/lib/directions.ts
+++ /dev/null
@@ -1,101 +0,0 @@
-/**
- * "Open in maps" handoff — deep links to external navigation apps.
- * ─────────────────────────────────────────────────────────────────────────────
- * The in-app RouteDialog deliberately keeps routing on our own map (OSRM +
- * Transitous) so nothing goes to Google by default. This module is the *opt-in*
- * escape hatch: build links so a user who wants real turn-by-turn / offline
- * navigation can hand the trip to another app.
- *
- * Providers are chosen for reliable, keyless web URLs that also deep-link into
- * their native apps:
- * • Google Maps — supports driving / walking / bicycling / transit
- * • Apple Maps — iOS/macOS (opens the web version elsewhere)
- * • OpenStreetMap — car / bike / foot (no transit)
- * • Organic Maps — Android `geo:` intent (privacy-first, offline)
- *
- * Handing off *does* share the trip with that provider — hence opt-in.
- */
-import { Capacitor } from '@capacitor/core';
-import type { TravelMode } from '@/lib/routing';
-
-export interface DirectionsLink {
- id: string;
- label: string;
- url: string;
-}
-
-interface Point {
- lat: number;
- lng: number;
-}
-
-const GOOGLE_MODE: Record = {
- foot: 'walking',
- bike: 'bicycling',
- moto: 'driving',
- car: 'driving',
- transit: 'transit',
-};
-
-function googleUrl(from: Point, to: Point, mode: TravelMode): string {
- return `https://www.google.com/maps/dir/?api=1&origin=${from.lat},${from.lng}&destination=${to.lat},${to.lng}&travelmode=${GOOGLE_MODE[mode]}`;
-}
-
-/** Apple Maps "dirflg": w = walking, r = public transit, d = driving. */
-function appleFlag(mode: TravelMode): string {
- if (mode === 'foot') return 'w';
- if (mode === 'transit') return 'r';
- return 'd'; // car / moto / bike
-}
-
-function appleUrl(from: Point, to: Point, mode: TravelMode): string {
- return `https://maps.apple.com/?saddr=${from.lat},${from.lng}&daddr=${to.lat},${to.lng}&dirflg=${appleFlag(mode)}`;
-}
-
-/** OSM has no public-transport routing; returns null for transit. */
-function osmEngine(mode: TravelMode): string | null {
- if (mode === 'foot') return 'fossgis_osrm_foot';
- if (mode === 'bike') return 'fossgis_osrm_bike';
- if (mode === 'transit') return null;
- return 'fossgis_osrm_car'; // car / moto
-}
-
-function osmUrl(from: Point, to: Point, mode: TravelMode): string | null {
- const engine = osmEngine(mode);
- if (!engine) return null;
- return `https://www.openstreetmap.org/directions?engine=${engine}&route=${from.lat},${from.lng};${to.lat},${to.lng}`;
-}
-
-/** `geo:` intent — Android hands it to Organic Maps (or the default map app). */
-function organicUrl(to: Point, label: string): string {
- return `geo:${to.lat},${to.lng}?q=${to.lat},${to.lng}(${encodeURIComponent(label)})`;
-}
-
-export function buildDirectionsLinks(
- from: Point,
- to: Point,
- mode: TravelMode,
- destinationLabel: string,
-): DirectionsLink[] {
- const links: DirectionsLink[] = [
- { id: 'google', label: 'Google Maps', url: googleUrl(from, to, mode) },
- { id: 'apple', label: 'Apple Maps', url: appleUrl(from, to, mode) },
- ];
-
- const osm = osmUrl(from, to, mode);
- if (osm) links.push({ id: 'osm', label: 'OpenStreetMap', url: osm });
-
- if (Capacitor.getPlatform() === 'android') {
- links.push({ id: 'organic', label: 'Organic Maps', url: organicUrl(to, destinationLabel) });
- }
-
- return links;
-}
-
-/**
- * Open an external URL. Capacitor intercepts the `_system` target and hands the
- * URL to the OS (system browser / registered app); on the web it's a new tab.
- */
-export function openExternal(url: string): void {
- window.open(url, '_system');
-}
diff --git a/src/pages/FoodPage.tsx b/src/pages/FoodPage.tsx
index 8f1307e..458a4ef 100644
--- a/src/pages/FoodPage.tsx
+++ b/src/pages/FoodPage.tsx
@@ -3,6 +3,7 @@ import { useSeoMeta } from '@unhead/react';
import { Layout } from '@/components/Layout';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
+import { RouteDialog } from '@/components/RouteDialog';
import { Badge } from '@/components/ui/badge';
import { Skeleton } from '@/components/ui/skeleton';
import {
@@ -48,13 +49,10 @@ const FOOD_TYPE_LABELS: Record = {
function FoodSpotCard({ spot }: { spot: FoodSpot }) {
const Icon = FOOD_TYPE_ICONS[spot.type] || Utensils;
+ const [showRoute, setShowRoute] = useState(false);
- const handleDirections = () => {
- window.open(
- `https://www.google.com/maps/dir/?api=1&destination=${spot.lat},${spot.lng}`,
- '_blank'
- );
- };
+ // In-app routing only — no external map handoff.
+ const handleDirections = () => setShowRoute(true);
return (
@@ -133,6 +131,12 @@ function FoodSpotCard({ spot }: { spot: FoodSpot }) {
+
);
}
diff --git a/src/pages/MapPage.tsx b/src/pages/MapPage.tsx
index 930995f..2894736 100644
--- a/src/pages/MapPage.tsx
+++ b/src/pages/MapPage.tsx
@@ -7,6 +7,7 @@ import { Layout } from '@/components/Layout';
const SpotMap = lazy(() => import('@/components/SpotMap').then(m => ({ default: m.SpotMap })));
import { SpotCard, SpotCardSkeleton } from '@/components/SpotCard';
import { WeatherToggle } from '@/components/WeatherToggle';
+import { RouteDialog } from '@/components/RouteDialog';
import { TrustFilter } from '@/components/TrustFilter';
import { filterByTrustLevel, type TrustFilterLevel } from '@/lib/trustFilter';
import { SortDropdown, type SortOption } from '@/components/SortDropdown';
@@ -345,52 +346,15 @@ function StashDrawer() {
);
}
-// Detect iOS/macOS for smarter maps ordering
-function isApplePlatform(): boolean {
- if (typeof navigator === 'undefined') return false;
- return /iPhone|iPad|iPod|Macintosh/.test(navigator.userAgent) && !/(Windows|Android)/.test(navigator.userAgent);
-}
-
-// Universal directions picker — works on iOS, Android, desktop
-function DirectionsMenu({ lat, lon, label }: { lat: string; lon: string; label: string }) {
+// In-app directions — opens the same RouteDialog used on the detail pages.
+// Deliberately NO external map handoff: routing stays inside SkateSpots.
+function DirectionsButton({ lat, lon, label }: { lat: string; lon: string; label: string }) {
const [open, setOpen] = useState(false);
- const onApple = isApplePlatform();
-
- const allOptions = [
- {
- name: 'Apple Maps',
- sub: 'iOS & macOS',
- href: `https://maps.apple.com/?ll=${lat},${lon}&q=${label}&dirflg=d`,
- emoji: '🍎',
- appleOnly: true,
- },
- {
- name: 'Google Maps',
- sub: 'Opens in browser or app',
- href: `https://maps.google.com/?q=${lat},${lon}`,
- emoji: '🔵',
- appleOnly: false,
- },
- {
- name: 'OpenStreetMap',
- sub: 'Privacy-friendly, no account',
- href: `https://www.openstreetmap.org/?mlat=${lat}&mlon=${lon}&zoom=17`,
- emoji: '🗺️',
- appleOnly: false,
- },
- {
- name: 'Waze',
- sub: 'Community-powered navigation',
- href: `https://waze.com/ul?ll=${lat},${lon}&navigate=yes`,
- emoji: '🔸',
- appleOnly: false,
- },
- ];
-
- // On Apple platforms: Apple Maps first. On others: Google first, no Apple.
- const options = onApple
- ? allOptions
- : allOptions.filter(o => !o.appleOnly);
+ const destination = useMemo(() => {
+ const la = parseFloat(lat);
+ const lo = parseFloat(lon);
+ return Number.isFinite(la) && Number.isFinite(lo) ? { lat: la, lng: lo } : null;
+ }, [lat, lon]);
return (
<>
@@ -398,39 +362,17 @@ function DirectionsMenu({ lat, lon, label }: { lat: string; lon: string; label:
variant="outline"
className="h-12 w-full font-bold text-sm border-white/20 hover:bg-white/10 rounded-xl"
onClick={() => setOpen(true)}
+ disabled={!destination}
>
Directions
-
- {open && (
- <>
- {/* Backdrop */}
-
setOpen(false)} />
- {/* Menu — fixed, centered, never clips behind Sheet stacking context */}
-