refactor(admin): share the admin settings saves and the TREK API lists between desktop and phone

useAdminSettingsActions now holds the OIDC and file type saves and the five place switches, writing through the admin bag both shells already receive. trekApiModel keeps the TREK Places API fields, facts and sources the desktop card and the phone block list.
This commit is contained in:
Maurice
2026-10-09 10:57:37 +02:00
parent 8c8069e983
commit feb99ce398
8 changed files with 371 additions and 207 deletions
@@ -1,7 +1,6 @@
import { useState } from 'react'
import { AlertTriangle, ChevronDown, RefreshCw } from 'lucide-react'
import { adminApi, authApi } from '../../../api/client'
import { getApiErrorMessage } from '../../../types'
import { useAdminSettingsActions } from '../../../components/Admin/useAdminSettingsActions'
import { placesGoogleOnlyHint } from '../../../utils/placeSource'
import type { TranslationFn } from '../../../types'
import type { useAdmin } from '../../../pages/admin/useAdmin'
@@ -29,21 +28,16 @@ interface MAdminSettingsSectionProps {
export default function MAdminSettingsSection({ admin, t }: MAdminSettingsSectionProps) {
const [providerPickerOpen, setProviderPickerOpen] = useState(false)
const {
toast,
setPlacesPhotosEnabled, setPlacesAutocompleteEnabled, setPlacesDetailsEnabled, setPlacesEnrichEnabled, setPlaceShadowEnabled,
placesPhotosEnabled, setPlacesPhotosEnabledState,
placesAutocompleteEnabled, setPlacesAutocompleteEnabledState,
placesDetailsEnabled, setPlacesDetailsEnabledState,
placesEnrichEnabled, setPlacesEnrichEnabledState,
placesPhotosEnabled, placesAutocompleteEnabled, placesDetailsEnabled, placesEnrichEnabled,
placesGoogleOnly, handleTogglePlacesGoogleOnly,
placeShadowEnabled, setPlaceShadowEnabledState,
oidcConfig, setOidcConfig, savingOidc, setSavingOidc,
placeShadowEnabled,
oidcConfig, setOidcConfig, savingOidc,
passwordLogin, setPasswordLogin, passwordRegistration, setPasswordRegistration,
oidcLogin, setOidcLogin, oidcRegistration, setOidcRegistration,
envOverrideOidcOnly, oidcConfigured, requireMfa,
passkeyLogin, setPasskeyLogin, passkeyConfigured,
webauthnRpId, setWebauthnRpId, webauthnOrigins, setWebauthnOrigins, savingWebauthn, handleSaveWebauthn,
allowedFileTypes, setAllowedFileTypes, savingFileTypes, setSavingFileTypes,
allowedFileTypes, setAllowedFileTypes, savingFileTypes,
mapsKey, setMapsKey, unsplashKey, setUnsplashKey, amapKey, setAmapKey, hasMapsKey, hasAmapKey, keyInputProps, mapsKeyTestable, savingKeys, validating, validation,
placesProvider, savingPlacesProvider, handleSavePlacesProvider,
managed,
@@ -52,36 +46,10 @@ export default function MAdminSettingsSection({ admin, t }: MAdminSettingsSectio
handleSaveApiKeys, handleValidateKey,
} = admin
const saveFileTypes = async () => {
setSavingFileTypes(true)
try {
await authApi.updateAppSettings({ allowed_file_types: allowedFileTypes })
toast.success(t('admin.fileTypesSaved'))
} catch {
toast.error(t('common.error'))
} finally {
setSavingFileTypes(false)
}
}
const saveOidc = async () => {
setSavingOidc(true)
try {
const payload: Record<string, unknown> = {
issuer: oidcConfig.issuer,
client_id: oidcConfig.client_id,
display_name: oidcConfig.display_name,
discovery_url: oidcConfig.discovery_url,
}
if (oidcConfig.client_secret) payload.client_secret = oidcConfig.client_secret
await adminApi.updateOidc(payload)
toast.success(t('admin.oidcSaved'))
} catch (err: unknown) {
toast.error(getApiErrorMessage(err, t('common.error')))
} finally {
setSavingOidc(false)
}
}
const {
saveOidc, saveFileTypes, togglePlacesPhotos, togglePlacesAutocomplete, togglePlacesDetails, togglePlacesEnrich,
togglePlaceShadow,
} = useAdminSettingsActions(admin, t)
return (
<div className="space-y-3">
@@ -279,16 +247,7 @@ export default function MAdminSettingsSection({ admin, t }: MAdminSettingsSectio
<MToggle
checked={placesPhotosEnabled}
ariaLabel={t('admin.placesPhotos.title')}
onChange={async (next) => {
setPlacesPhotosEnabledState(next)
setPlacesPhotosEnabled(next)
try {
await adminApi.updatePlacesPhotos(next)
} catch {
setPlacesPhotosEnabledState(!next)
setPlacesPhotosEnabled(!next)
}
}}
onChange={togglePlacesPhotos}
/>
}
/>
@@ -299,16 +258,7 @@ export default function MAdminSettingsSection({ admin, t }: MAdminSettingsSectio
<MToggle
checked={placesAutocompleteEnabled}
ariaLabel={t('admin.placesAutocomplete.title')}
onChange={async (next) => {
setPlacesAutocompleteEnabledState(next)
setPlacesAutocompleteEnabled(next)
try {
await adminApi.updatePlacesAutocomplete(next)
} catch {
setPlacesAutocompleteEnabledState(!next)
setPlacesAutocompleteEnabled(!next)
}
}}
onChange={togglePlacesAutocomplete}
/>
}
/>
@@ -319,16 +269,7 @@ export default function MAdminSettingsSection({ admin, t }: MAdminSettingsSectio
<MToggle
checked={placesDetailsEnabled}
ariaLabel={t('admin.placesDetails.title')}
onChange={async (next) => {
setPlacesDetailsEnabledState(next)
setPlacesDetailsEnabled(next)
try {
await adminApi.updatePlacesDetails(next)
} catch {
setPlacesDetailsEnabledState(!next)
setPlacesDetailsEnabled(!next)
}
}}
onChange={togglePlacesDetails}
/>
}
/>
@@ -339,16 +280,7 @@ export default function MAdminSettingsSection({ admin, t }: MAdminSettingsSectio
<MToggle
checked={placesEnrichEnabled}
ariaLabel={t('admin.placesEnrich.title')}
onChange={async (next) => {
setPlacesEnrichEnabledState(next)
setPlacesEnrichEnabled(next)
try {
await adminApi.updatePlacesEnrich(next)
} catch {
setPlacesEnrichEnabledState(!next)
setPlacesEnrichEnabled(!next)
}
}}
onChange={togglePlacesEnrich}
/>
}
/>
@@ -434,16 +366,7 @@ export default function MAdminSettingsSection({ admin, t }: MAdminSettingsSectio
<MToggle
checked={placeShadowEnabled}
ariaLabel={t('admin.placeShadow.title')}
onChange={async (next) => {
setPlaceShadowEnabledState(next)
setPlaceShadowEnabled(next)
try {
await adminApi.updatePlaceShadow(next)
} catch {
setPlaceShadowEnabledState(!next)
setPlaceShadowEnabled(!next)
}
}}
onChange={togglePlaceShadow}
/>
}
/>
@@ -1,6 +1,7 @@
import { useState } from 'react'
import { Check, ChevronRight, Globe2, KeyRound, Library, ShieldOff, WifiOff, X } from 'lucide-react'
import { Check, ChevronRight, Library, X } from 'lucide-react'
import TrekMark from '../../../components/shared/TrekMark'
import { TREK_API_SOURCES as SOURCES, trekApiFacts, trekApiFields } from '../../../components/Admin/trekApiModel'
import { useTranslation } from '../../../i18n'
/**
@@ -19,13 +20,6 @@ import { useTranslation } from '../../../i18n'
* wording, key for key, so the two pages cannot drift in what they claim.
*/
/**
* Proper nouns, so they are not translated. Naming them is also a licence
* obligation rather than decoration: ODbL and CC BY-SA both require attribution
* wherever their content is shown.
*/
const SOURCES = ['Overture Maps Foundation', 'OpenStreetMap', 'Wikivoyage', 'Wikimedia']
const chip = 'rounded-full border border-[color:var(--m-rowbr)] px-2 py-[2px] font-geist text-[0.625rem]'
/** One group of chips inside the disclosure: what you get, what you do not, where it comes from. */
@@ -65,26 +59,9 @@ export function MTrekApiBlock() {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
const facts = [
{ Icon: Globe2, text: t('admin.trekApi.factPlaces') },
{ Icon: KeyRound, text: t('admin.trekApi.factNoKey') },
{ Icon: WifiOff, text: t('admin.trekApi.factOffline') },
{ Icon: ShieldOff, text: t('admin.trekApi.factPrivacy') },
]
const facts = trekApiFacts(t)
// The same fields the desktop card lists, reusing words TREK already has.
const fields = [
t('places.formName'),
t('collections.coordinates'),
t('places.formCategory'),
t('places.formAddress'),
t('admin.trekApi.fieldPhone'),
t('common.email'),
t('places.formWebsite'),
t('places.formDescription'),
t('inspector.openingHours'),
t('admin.trekApi.fieldStableId'),
]
const fields = trekApiFields(t)
return (
<div className="relative mt-[18px]">