Files
TREK/client/src/components/Tours/ski/useSkiClassification.ts
T
Maurice 204ee2ef86 chore(ci): give the server test typecheck room, format new client files
The server's src and tests together are past 570,000 lines and the test
typecheck needs about 4.3 GB, over Node's default heap; it now gets 8 GB like
the test jobs. Forty new client files are formatted, and the react-hooks/refs
baseline drops by the one warning a split hook removed.
2026-10-20 18:39:58 +02:00

80 lines
3.2 KiB
TypeScript

import type { TourListItem } from '@trek/shared';
import { useCallback, useMemo, useState } from 'react';
import { useTranslation } from '../../../i18n';
import { tourRepo } from '../../../repo/tourRepo';
import { useToast } from '../../shared/Toast';
import type { TourPlannerController } from '../planner/useTourPlanner';
import { revisionConflictOf } from '../planner/useTourRevisionGuard';
import { NO_SKI_CHOICE, skiChoiceChanged, skiChoiceOf, skiFieldsOf, type SkiChoice } from './skiChoice';
interface SkiClassificationOptions {
tripId: number;
planner: Pick<TourPlannerController, 'mode'>;
/** The trip's tours, which follow every save, so the card shows the classification as it stands. */
tours: readonly TourListItem[];
canEdit: boolean;
/** The tour as it stands after the save, for the lists and the map to take it in. */
onSaved: (tour: TourListItem) => Promise<void> | void;
}
/**
* SKI-02: the ski track open in the planner and its classification, changed
* on purpose in a dialog: the discipline, and for cross-country the
* technique. Saving goes through the offline core and changes nothing of the
* track; closing the dialog changes nothing at all.
*/
export function useSkiClassification({ tripId, planner, tours, canEdit, onSaved }: SkiClassificationOptions) {
const { t } = useTranslation();
const toast = useToast();
const [draft, setDraft] = useState<SkiChoice | null>(null);
const [saving, setSaving] = useState(false);
const viewed = planner.mode.type === 'view-gpx' ? planner.mode.tour : null;
const tour = viewed?.tour_type === 'ski' ? (tours.find((item) => item.place_id === viewed.place_id) ?? viewed) : null;
const saved = useMemo(() => (tour ? skiChoiceOf(tour) : NO_SKI_CHOICE), [tour]);
const open = useCallback(() => {
if (tour && canEdit) setDraft(saved);
}, [canEdit, saved, tour]);
const close = useCallback(() => {
if (!saving) setDraft(null);
}, [saving]);
const save = useCallback(async () => {
const fields = draft ? skiFieldsOf(draft) : null;
if (!tour || !fields || saving || !canEdit) return;
setSaving(true);
try {
const result = await tourRepo.classifySki(tripId, tour.place_id, {
...fields,
expected_revision: tour.revision ?? null,
});
await onSaved(result.tour);
setDraft(null);
toast.success(t('tours.ski.saved'));
} catch (error: unknown) {
// Saved elsewhere meanwhile: the list brings the newer classification, the dialog stays to choose again.
toast.error(t(revisionConflictOf(error) === null ? 'tours.ski.saveFailed' : 'tours.revision.conflictTitle'));
} finally {
setSaving(false);
}
}, [canEdit, draft, onSaved, saving, t, toast, tour, tripId]);
return {
/** The ski track in view, or null while none is. */
tour,
canEdit,
saving,
/** The choice in the open dialog; null while it is closed. */
draft,
setDraft,
/** The dialog's choice is complete and says something else than the saved one. */
canSave: draft !== null && skiFieldsOf(draft) !== null && skiChoiceChanged(draft, saved),
open,
close,
save,
};
}
export type SkiClassification = ReturnType<typeof useSkiClassification>;