Files
TREK/client/src/api/tourGpxExport.ts
T
Maurice 134ab64bce feat(tours): download a saved tour as a GPX file (PR1-06A)
A saved tour can be handed over as a GPX file on purpose: its whole saved
line as one track with the heights it has and none it lacks, its name,
description and link, and the activity in <type>. The file is named after
the tour. A tour without a line of two points is refused with 422 rather
than written as a file that imports as nothing; the export changes nothing.

GET /api/trips/:tripId/tours/:placeId/export.gpx, readable by anyone who
sees the trip, and the MCP tool export_tour_gpx with the same rules. The
tour detail gets a "GPX" action beside the navigation, the planner head a
download button that waits while the tour has unsaved changes.

The tours rail head now uses the same plain icon buttons as the editor.
2026-10-19 09:42:13 +02:00

34 lines
1.1 KiB
TypeScript

import apiClient from './client';
/**
* The file name a download's Content-Disposition gives: the UTF-8 `filename*`
* first, since it keeps the tour's own script, the plain `filename` after.
*/
export function attachmentFilename(header: unknown): string | null {
if (typeof header !== 'string') return null;
const encoded = /filename\*=UTF-8''([^;]+)/i.exec(header)?.[1];
if (encoded) {
try {
return decodeURIComponent(encoded.trim());
} catch {
// A malformed escape falls through to the plain name.
}
}
return /filename="([^"]+)"/i.exec(header)?.[1] ?? null;
}
/** PR1-06A: one saved tour as the GPX file the server writes. */
export const tourGpxExportApi = {
download: async (
tripId: number | string,
placeId: number,
signal?: AbortSignal
): Promise<{ blob: Blob; filename: string | null }> => {
const response = await apiClient.get<Blob>(`/trips/${tripId}/tours/${placeId}/export.gpx`, {
responseType: 'blob',
signal,
});
return { blob: response.data, filename: attachmentFilename(response.headers['content-disposition']) };
},
};