mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-20 05:56:53 +00:00
73237ffedb
The tour type is a dropdown; a bike tour picks its profile, routes for it, says when a point lies off every way, and shows what it runs on: surfaces, kinds of way and, for a mountain bike, the trail grades, each pointed at on the map. A type's own map (OpenHikingMap for a hike, CyclOSM for a bike tour) is offered while a tour of that type is open, with an admin switch each. From the planned cards: a hike avoids steep climbs, steps or ferries, nothing else; a saved tour can be shared where the platform takes GPX files; and the tours rail adds a tour from one menu: plan new, import GPX, or take one over from another trip.
148 lines
5.5 KiB
TypeScript
148 lines
5.5 KiB
TypeScript
import { Bike, Footprints } from 'lucide-react';
|
|
import { useEffect, useEffectEvent, useState } from 'react';
|
|
|
|
import { adminApi } from '../../../api/client';
|
|
import { cycleMapAdminApi, hikingMapAdminApi } from '../../../api/tourBaseMaps';
|
|
import { wandererAdminApi } from '../../../api/wanderer';
|
|
import { useTranslation } from '../../../i18n';
|
|
import { useAddonStore } from '../../../store/addonStore';
|
|
import { useToast } from '../../shared/Toast';
|
|
import { type Addon, type ProviderOption, isPhotosAddon } from './addonModel';
|
|
|
|
/**
|
|
* The addon switches behind both admin shells: the addon list loaded on mount, an
|
|
* optimistic toggle with a per row rollback, and the lists each shell groups its tiles
|
|
* and provider shelves from. The desktop columns and the phone cards render their own
|
|
* markup over it.
|
|
*/
|
|
export function useAddonManager() {
|
|
const { t } = useTranslation();
|
|
const toast = useToast();
|
|
const refreshGlobalAddons = useAddonStore((s) => s.loadAddons);
|
|
const [addons, setAddons] = useState<Addon[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [toursWanderer, setToursWanderer] = useState(false);
|
|
const [toursHikingMap, setToursHikingMap] = useState(false);
|
|
const [toursCycleMap, setToursCycleMap] = useState(false);
|
|
|
|
const loadAddons = async () => {
|
|
try {
|
|
const data = await adminApi.addons();
|
|
setAddons(data.addons);
|
|
} catch {
|
|
toast.error(t('admin.addons.toast.error'));
|
|
}
|
|
};
|
|
|
|
const loadOnMount = useEffectEvent(() => {
|
|
void loadAddons().finally(() => setLoading(false));
|
|
wandererAdminApi
|
|
.get()
|
|
.then((flag) => setToursWanderer(flag.enabled))
|
|
.catch(() => toast.error(t('admin.addons.toast.error')));
|
|
hikingMapAdminApi
|
|
.get()
|
|
.then((flag) => setToursHikingMap(flag.enabled))
|
|
.catch(() => toast.error(t('admin.addons.toast.error')));
|
|
cycleMapAdminApi
|
|
.get()
|
|
.then((flag) => setToursCycleMap(flag.enabled))
|
|
.catch(() => toast.error(t('admin.addons.toast.error')));
|
|
});
|
|
useEffect(() => {
|
|
loadOnMount();
|
|
}, []);
|
|
|
|
/** Optimistic flip, per-row rollback on failure. Rolling the whole addons
|
|
* snapshot back instead would undo a toggle the user hit in parallel. */
|
|
const handleToggle = async (addon: Addon) => {
|
|
const newEnabled = !addon.enabled;
|
|
setAddons((prev) => prev.map((a) => (a.id === addon.id ? { ...a, enabled: newEnabled } : a)));
|
|
try {
|
|
await adminApi.updateAddon(addon.id, { enabled: newEnabled });
|
|
} catch {
|
|
setAddons((prev) => prev.map((a) => (a.id === addon.id ? { ...a, enabled: !newEnabled } : a)));
|
|
toast.error(t('admin.addons.toast.error'));
|
|
return;
|
|
}
|
|
refreshGlobalAddons();
|
|
// Journey off disables every photo provider with it, and the response carries
|
|
// only Journey. Without re-reading, switching Journey back on brings the shelf
|
|
// up with providers the database has long since turned off.
|
|
// Same for Documents: switching it off disables every document provider in
|
|
// the database, and the response carries only the addon itself.
|
|
if (addon.id === 'journey' || addon.id === 'documents') await loadAddons();
|
|
toast.success(t('admin.addons.toast.updated'));
|
|
};
|
|
|
|
/** A switch under Tours (wanderer, OpenHikingMap, CyclOSM): flipped at once, put back when the save fails. */
|
|
const flipToursSwitch = async (
|
|
current: boolean,
|
|
set: (enabled: boolean) => void,
|
|
save: (enabled: boolean) => Promise<unknown>
|
|
) => {
|
|
const next = !current;
|
|
set(next);
|
|
try {
|
|
await save(next);
|
|
} catch {
|
|
set(!next);
|
|
toast.error(t('admin.addons.toast.error'));
|
|
return;
|
|
}
|
|
void refreshGlobalAddons();
|
|
toast.success(t('admin.addons.toast.updated'));
|
|
};
|
|
const toggleToursWanderer = () => flipToursSwitch(toursWanderer, setToursWanderer, wandererAdminApi.update);
|
|
/** The maps the Tours planner offers on their operators' terms, one switch each (MAP-01, BIK-06). */
|
|
const mapSwitches = [
|
|
{
|
|
key: 'hiking',
|
|
Icon: Footprints,
|
|
titleKey: 'admin.toursHikingMap.title',
|
|
subtitleKey: 'admin.toursHikingMap.subtitle',
|
|
enabled: toursHikingMap,
|
|
toggle: () => flipToursSwitch(toursHikingMap, setToursHikingMap, hikingMapAdminApi.update),
|
|
},
|
|
{
|
|
key: 'cycle',
|
|
Icon: Bike,
|
|
titleKey: 'admin.toursCycleMap.title',
|
|
subtitleKey: 'admin.toursCycleMap.subtitle',
|
|
enabled: toursCycleMap,
|
|
toggle: () => flipToursSwitch(toursCycleMap, setToursCycleMap, cycleMapAdminApi.update),
|
|
},
|
|
] as const;
|
|
|
|
const asOption = (provider: Addon): ProviderOption => ({
|
|
key: provider.id,
|
|
label: provider.name,
|
|
description: provider.description,
|
|
enabled: provider.enabled,
|
|
toggle: () => handleToggle(provider),
|
|
});
|
|
|
|
const tripAddons = addons.filter((a) => a.type === 'trip' && !isPhotosAddon(a));
|
|
const globalAddons = addons.filter((a) => a.type === 'global');
|
|
const integrationAddons = addons.filter((a) => a.type === 'integration');
|
|
const providerOptions = addons.filter((a) => a.type === 'photo_provider').map(asOption);
|
|
// No credential form under these, unlike the photo providers: a document connection
|
|
// belongs to a trip and is entered there. The admin only decides whether a provider
|
|
// may be offered at all.
|
|
const documentProviderOptions = addons.filter((a) => a.type === 'document_provider').map(asOption);
|
|
|
|
return {
|
|
addons,
|
|
loading,
|
|
handleToggle,
|
|
tripAddons,
|
|
globalAddons,
|
|
integrationAddons,
|
|
providerOptions,
|
|
documentProviderOptions,
|
|
toursWanderer,
|
|
toggleToursWanderer,
|
|
mapSwitches,
|
|
};
|
|
}
|