diff --git a/client/src/components/Admin/DefaultUserSettingsTab.tsx b/client/src/components/Admin/DefaultUserSettingsTab.tsx index e7d9a7269..1a08f61ff 100644 --- a/client/src/components/Admin/DefaultUserSettingsTab.tsx +++ b/client/src/components/Admin/DefaultUserSettingsTab.tsx @@ -1,8 +1,6 @@ -import React, { useEffect, useMemo, useState } from 'react' +import React from 'react' import { Loader2, Map as MapIcon, Settings2 } from 'lucide-react' -import { adminApi } from '../../api/client' import { useTranslation } from '../../i18n' -import { useToast } from '../shared/Toast' import Section from '../Settings/Section' import { SettingRows, SettingsHint } from '../Settings/settingsKit' import { fs } from '../shared/DialogShell' @@ -10,64 +8,13 @@ import { EditorField, INPUT, Segmented } from '../shared/dialogParts' import CustomSelect from '../shared/CustomSelect' import { MapView } from '../Map/MapView' import { SYMBOLS, currenciesWith } from '../Budget/BudgetPanel.constants' -import type { DistanceUnit, Place, WeekStart } from '../../types' +import type { DistanceUnit, WeekStart } from '../../types' import { weekStartOptions } from '../../utils/calendarWeek' -import { normalizeTileUrl, withTileApiKey } from '../../utils/tileUrl' -import { - MAPBOX_DEFAULT_STYLE, - defaultStyleForProvider, - getStylePresets, - isOpenFreeMapStyle, - normalizeStyleForProvider, - styleSettingKey, - type GlMapProvider, -} from '../Map/glProviders' -import { useAuthStore } from '../../store/authStore' -import RoutingInstanceFields, { type RoutingDefaults } from './RoutingInstanceFields' - -const MAP_PRESETS = [ - { name: 'OpenStreetMap', url: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png' }, - { name: 'OpenStreetMap DE', url: 'https://tile.openstreetmap.de/{z}/{x}/{y}.png' }, - // The app default, and a vector style rather than a {z}/{x}/{y} template: no - // key, no registration, no request limits. - { name: 'OpenFreeMap Positron', url: 'https://tiles.openfreemap.org/styles/positron' }, - { name: 'OpenFreeMap Bright', url: 'https://tiles.openfreemap.org/styles/bright' }, - // CARTO watermarks keyless tiles since 26.08.2026 and issues keys by mail, so - // these two need one; without it the map falls back to the default (#2054). - { 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' }, -] - -type Defaults = RoutingDefaults & { - temperature_unit?: string - distance_unit?: DistanceUnit - dark_mode?: string | boolean - time_format?: string - week_start?: WeekStart - default_currency?: string - blur_booking_codes?: boolean - map_tile_url?: string - carto_api_key?: string - map_provider?: string - mapbox_access_token?: string - mapbox_style?: string - maplibre_style?: string - mapbox_3d_enabled?: boolean - mapbox_quality_mode?: boolean -} - -type MapProvider = 'leaflet' | GlMapProvider - -function normalizeProvider(value: unknown): MapProvider { - return value === 'mapbox-gl' || value === 'maplibre-gl' ? value : 'leaflet' -} - -/** Only the GL providers keep a style — Leaflet is handled by its callers. */ -function styleForProvider(provider: GlMapProvider, style?: string | null): string { - if (provider === 'mapbox-gl' && isOpenFreeMapStyle(style)) return MAPBOX_DEFAULT_STYLE - return normalizeStyleForProvider(provider, style) -} +import { withTileApiKey } from '../../utils/tileUrl' +import { defaultStyleForProvider } from '../Map/glProviders' +import { DEFAULT_MAP_PRESETS as MAP_PRESETS, type MapProvider } from '../Settings/mapSettingsModel' +import RoutingInstanceFields from './RoutingInstanceFields' +import { useDefaultUserSettings, type Defaults } from './useDefaultUserSettings' /** * One default as a row of the card's white box: its name (with the reset link @@ -114,57 +61,18 @@ function colorModeValue(value: string | boolean | undefined): string { return value ?? '' } +/** A failed save names the error it threw, or falls back to the generic one. */ +function errorText(err: unknown, fallback: string): string { + return err instanceof Error ? err.message : fallback +} + export default function DefaultUserSettingsTab(): React.ReactElement { const { t, locale } = useTranslation() - const toast = useToast() - const [defaults, setDefaults] = useState({}) - const [loaded, setLoaded] = useState(false) - const [mapTileUrl, setMapTileUrl] = useState('') - const managed = useAuthStore((s) => s.managed) - const [mapboxToken, setMapboxToken] = useState('') - const [cartoKey, setCartoKey] = useState('') - const [mapboxStyle, setMapboxStyle] = useState('') - - useEffect(() => { - adminApi.getDefaultUserSettings().then((data: Defaults) => { - const provider = normalizeProvider(data.map_provider) - setDefaults(data) - setMapTileUrl(normalizeTileUrl(data.map_tile_url || '')) - setMapboxToken(data.mapbox_access_token || '') - setCartoKey(data.carto_api_key || '') - setMapboxStyle(provider === 'leaflet' ? (data.mapbox_style || '') : styleForProvider(provider, provider === 'maplibre-gl' ? data.maplibre_style : data.mapbox_style)) - setLoaded(true) - }).catch(() => setLoaded(true)) - }, []) - - const save = async (patch: Partial) => { - try { - const updated = await adminApi.updateDefaultUserSettings(patch as Record) - setDefaults(updated) - toast.success(t('admin.defaultSettings.saved')) - } catch (err: unknown) { - toast.error(err instanceof Error ? err.message : t('common.error')) - } - } - - const reset = async (key: keyof Defaults) => { - try { - const updated = await adminApi.updateDefaultUserSettings({ [key]: null }) - setDefaults(updated) - if (key === 'map_tile_url') setMapTileUrl('') - if (key === 'mapbox_access_token') setMapboxToken('') - if (key === 'carto_api_key') setCartoKey('') - if (key === 'mapbox_style' || key === 'maplibre_style') { - const provider = normalizeProvider(defaults.map_provider) - setMapboxStyle(provider === 'leaflet' ? '' : defaultStyleForProvider(provider)) - } - toast.success(t('admin.defaultSettings.reset')) - } catch (err: unknown) { - toast.error(err instanceof Error ? err.message : t('common.error')) - } - } - - const isSet = (key: keyof Defaults) => defaults[key] !== undefined + const { + defaults, loaded, managed, isSet, save, reset, mapTileUrl, setMapTileUrl, mapboxToken, setMapboxToken, + cartoKey, setCartoKey, mapboxStyle, setMapboxStyle, mapPreviewPlaces, mapProvider, glStylePresets, styleKey, + saveMapProvider, pickTilePreset, pickStylePreset, commitStyle, + } = useDefaultUserSettings({ errorMessage: errorText, previewCreatedAt: () => String(new Date()) }) // Inside the row's own