import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react' import { Map, Save, Layers, Box, ChevronDown, Check } from 'lucide-react' import { useTranslation } from '../../i18n' import { useSettingsStore } from '../../store/settingsStore' import { useToast } from '../shared/Toast' import CustomSelect from '../shared/CustomSelect' import { MapView } from '../Map/MapView' import MapboxPreview from './MapboxPreview' import Section from './Section' import ToggleSwitch from './ToggleSwitch' import type { Place } from '../../types' interface MapPreset { name: string url: string } const MAP_PRESETS: MapPreset[] = [ { name: 'OpenStreetMap', url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png' }, { name: 'OpenStreetMap DE', url: 'https://tile.openstreetmap.de/{z}/{x}/{y}.png' }, { name: 'CartoDB Light', url: 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png' }, { name: 'CartoDB Dark', url: 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png' }, { name: 'Stadia Smooth', url: 'https://tiles.stadiamaps.com/tiles/alidade_smooth/{z}/{x}/{y}{r}.png' }, ] interface StylePreset { name: string url: string tags: string[] } const MAPBOX_STYLE_PRESETS: StylePreset[] = [ { name: 'Mapbox Standard', url: 'mapbox://styles/mapbox/standard', tags: ['3D', 'Apple-like'] }, { name: 'Standard Satellite', url: 'mapbox://styles/mapbox/standard-satellite', tags: ['3D', 'Satellite'] }, { name: 'Streets', url: 'mapbox://styles/mapbox/streets-v12', tags: ['3D', 'Classic'] }, { name: 'Outdoors', url: 'mapbox://styles/mapbox/outdoors-v12', tags: ['3D', 'Terrain'] }, { name: 'Light', url: 'mapbox://styles/mapbox/light-v11', tags: ['3D', 'Minimal'] }, { name: 'Dark', url: 'mapbox://styles/mapbox/dark-v11', tags: ['3D', 'Dark'] }, { name: 'Satellite', url: 'mapbox://styles/mapbox/satellite-v9', tags: ['3D', 'Satellite'] }, { name: 'Satellite Streets', url: 'mapbox://styles/mapbox/satellite-streets-v12', tags: ['3D', 'Satellite'] }, { name: 'Navigation Day', url: 'mapbox://styles/mapbox/navigation-day-v1', tags: ['3D', 'Apple-like'] }, { name: 'Navigation Night', url: 'mapbox://styles/mapbox/navigation-night-v1', tags: ['3D', 'Dark'] }, ] // Tag → chip color mapping. Keeps the dropdown readable at a glance so a // user scanning the list can spot 3D / Satellite / Apple-like styles. const TAG_STYLES: Record = { '3D': 'bg-indigo-100 text-indigo-800 dark:bg-indigo-900/40 dark:text-indigo-300', '2D': 'bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-300', 'Satellite': 'bg-emerald-100 text-emerald-800 dark:bg-emerald-900/40 dark:text-emerald-300', 'Apple-like': 'bg-sky-100 text-sky-800 dark:bg-sky-900/40 dark:text-sky-300', 'Modern': 'bg-purple-100 text-purple-800 dark:bg-purple-900/40 dark:text-purple-300', 'Dark': 'bg-zinc-800 text-zinc-100 dark:bg-zinc-900 dark:text-zinc-300', 'Minimal': 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300', 'Hillshading': 'bg-amber-100 text-amber-800 dark:bg-amber-900/40 dark:text-amber-300', 'Terrain': 'bg-lime-100 text-lime-800 dark:bg-lime-900/40 dark:text-lime-300', 'Realistic': 'bg-teal-100 text-teal-800 dark:bg-teal-900/40 dark:text-teal-300', 'Navigation': 'bg-blue-100 text-blue-800 dark:bg-blue-900/40 dark:text-blue-300', 'Classic': 'bg-stone-100 text-stone-700 dark:bg-stone-800 dark:text-stone-300', 'Hybrid': 'bg-cyan-100 text-cyan-800 dark:bg-cyan-900/40 dark:text-cyan-300', 'No labels': 'bg-neutral-100 text-neutral-700 dark:bg-neutral-800 dark:text-neutral-300', } function TagChip({ tag }: { tag: string }) { const cls = TAG_STYLES[tag] || 'bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-300' return ( {tag} ) } function StyleDropdown({ value, onChange }: { value: string; onChange: (v: string) => void }) { const { t } = useTranslation() const [open, setOpen] = useState(false) const ref = useRef(null) useEffect(() => { if (!open) return const onDoc = (e: MouseEvent) => { if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false) } document.addEventListener('mousedown', onDoc) return () => document.removeEventListener('mousedown', onDoc) }, [open]) const selected = MAPBOX_STYLE_PRESETS.find(p => p.url === value) return (
{open && (
{MAPBOX_STYLE_PRESETS.map(preset => { const isActive = preset.url === value return ( ) })}
)}
) } type Provider = 'leaflet' | 'mapbox-gl' export default function MapSettingsTab(): React.ReactElement { const { settings, updateSettings } = useSettingsStore() const { t } = useTranslation() const toast = useToast() const [saving, setSaving] = useState(false) const [provider, setProvider] = useState((settings.map_provider as Provider) || 'leaflet') const [mapTileUrl, setMapTileUrl] = useState(settings.map_tile_url || '') const [mapboxToken, setMapboxToken] = useState(settings.mapbox_access_token || '') const [mapboxStyle, setMapboxStyle] = useState(settings.mapbox_style || 'mapbox://styles/mapbox/standard') const [mapbox3d, setMapbox3d] = useState(settings.mapbox_3d_enabled !== false) const [mapboxQuality, setMapboxQuality] = useState(settings.mapbox_quality_mode === true) const [defaultLat, setDefaultLat] = useState(settings.default_lat || 48.8566) const [defaultLng, setDefaultLng] = useState(settings.default_lng || 2.3522) const [defaultZoom, setDefaultZoom] = useState(settings.default_zoom || 10) useEffect(() => { setProvider((settings.map_provider as Provider) || 'leaflet') setMapTileUrl(settings.map_tile_url || '') setMapboxToken(settings.mapbox_access_token || '') setMapboxStyle(settings.mapbox_style || 'mapbox://styles/mapbox/standard') setMapbox3d(settings.mapbox_3d_enabled !== false) setMapboxQuality(settings.mapbox_quality_mode === true) setDefaultLat(settings.default_lat || 48.8566) setDefaultLng(settings.default_lng || 2.3522) setDefaultZoom(settings.default_zoom || 10) }, [settings]) const handleMapClick = useCallback((mapInfo) => { setDefaultLat(mapInfo.latlng.lat) setDefaultLng(mapInfo.latlng.lng) }, []) const mapPlaces = useMemo((): Place[] => [{ id: 1, trip_id: 1, name: 'Default map center', description: '', lat: defaultLat as number, lng: defaultLng as number, address: '', category_id: 0, icon: null, price: null, image_url: null, google_place_id: null, osm_id: null, route_geometry: null, place_time: null, end_time: null, created_at: Date(), }], [defaultLat, defaultLng]) const saveMapSettings = async (): Promise => { setSaving(true) try { await updateSettings({ map_provider: provider, map_tile_url: mapTileUrl, mapbox_access_token: mapboxToken, mapbox_style: mapboxStyle, mapbox_3d_enabled: mapbox3d, mapbox_quality_mode: mapboxQuality, default_lat: parseFloat(String(defaultLat)), default_lng: parseFloat(String(defaultLng)), default_zoom: parseInt(String(defaultZoom)), }) toast.success(t('settings.toast.mapSaved')) } catch (err: unknown) { toast.error(err instanceof Error ? err.message : t('common.error')) } finally { setSaving(false) } } // 3D is available on every style now — pure satellite uses the // mapbox-streets-v8 tileset as a fallback building source. const supports3d = true return (
{/* Provider picker — big cards so the choice is obvious */}

{t('settings.mapProviderHint')}

{/* Leaflet settings */} {provider === 'leaflet' && (
{ if (value) setMapTileUrl(value) }} placeholder={t('settings.mapTemplatePlaceholder.select')} options={MAP_PRESETS.map(p => ({ value: p.url, label: p.name }))} size="sm" style={{ marginBottom: 8 }} /> ) => setMapTileUrl(e.target.value)} placeholder="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-slate-400 focus:border-transparent" />

{t('settings.mapDefaultHint')}

)} {/* Mapbox GL settings */} {provider === 'mapbox-gl' && (
setMapboxToken(e.target.value)} placeholder="pk.eyJ1Ijoi..." className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm font-mono focus:ring-2 focus:ring-slate-400 focus:border-transparent" />

{t('settings.mapMapboxTokenHint')}{' '} {t('settings.mapMapboxTokenLink')}

setMapboxStyle(e.target.value)} placeholder="mapbox://styles/mapbox/standard" className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm font-mono focus:ring-2 focus:ring-slate-400 focus:border-transparent" />

{t('settings.mapStyleHint')}

{t('settings.map3dBuildings')}
{t('settings.map3dHint')}
{ if (supports3d) setMapbox3d(!mapbox3d) }} />
{t('settings.mapHighQuality')} {t('settings.mapExperimental')}
{t('settings.mapHighQualityHint')}{' '} {t('settings.mapHighQualityWarning')}
setMapboxQuality(!mapboxQuality)} />
{t('settings.mapTipLabel')} {t('settings.mapTip')}
)} {/* Default map position — applies regardless of provider */}
setDefaultLat(e.target.value)} className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-slate-400 focus:border-transparent" />
setDefaultLng(e.target.value)} className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-slate-400 focus:border-transparent" />
{provider === 'mapbox-gl' ? ( { setDefaultLat(ll.lat); setDefaultLng(ll.lng) }} /> ) : ( // eslint-disable-next-line @typescript-eslint/no-explicit-any React.createElement(MapView as any, { places: mapPlaces, dayPlaces: [], route: null, routeSegments: null, selectedPlaceId: null, onMarkerClick: null, onMapClick: handleMapClick, onMapContextMenu: null, center: [settings.default_lat, settings.default_lng], zoom: defaultZoom, tileUrl: mapTileUrl, fitKey: null, dayOrderMap: [], leftWidth: 0, rightWidth: 0, hasInspector: false, }) )}
) }