src/lib/trip-locations.ts
Rama main · réplica de lectura del repositorio original.
// Where a trip went, as text. Every compact surface - the trip card on /trips
// and on the Home page, a detail page subtitle - shows the same joined line, so
// it is composed once here rather than slightly differently in each of them.
interface NamedLocation {
name: string;
}
// What separates one place from the next. A semicolon rather than a comma
// because a place's own name carries its country now ("Dahab, Egypt"), so a
// comma join would read as one list of five things instead of two places:
// "Dahab, Egypt, Sharm El Sheikh, Egypt". The same reason the CSV export joins
// a trip's places this way.
const PLACE_SEPARATOR = "; ";
export interface FormatTripLocationNamesOptions {
// Show at most this many names, and count the rest as "+N". Omit to show all.
max?: number;
}
/**
* The trip's locations as "Dahab, Egypt +2", or `undefined` when there are
* none.
*
* Callers test it for a place to name - the trip card and the trip page decide
* their " · " separator by it - rather than checking a length.
*/
export function formatTripLocationNames(
locations?: NamedLocation[] | null,
{ max }: FormatTripLocationNamesOptions = {},
): string | undefined {
const names = usableNames(locations);
if (names.length === 0) return undefined;
const limit = resolveLimit(names.length, max);
const shown = names.slice(0, limit).join(PLACE_SEPARATOR);
const hidden = names.length - Math.min(limit, names.length);
return hidden > 0 ? `${shown} +${hidden}` : shown;
}
/**
* Every name in full, for the `title` beside a compacted label - or `undefined`
* when that label already shows them all.
*
* `undefined` rather than the label itself, so the same `max` that produced the
* "+N" decides whether there is anything left to reveal: a tooltip repeating
* the text under the cursor is worse than no tooltip at all.
*/
export function formatTripLocationNamesHint(
locations?: NamedLocation[] | null,
{ max }: FormatTripLocationNamesOptions = {},
): string | undefined {
const names = usableNames(locations);
const limit = resolveLimit(names.length, max);
if (names.length <= limit) return undefined;
return names.join(PLACE_SEPARATOR);
}
// How many names are on screen. Shared rather than repeated in both functions:
// the label and the hint disagreeing about it is the whole failure the hint's
// `undefined` exists to avoid, and it would look right at either call site.
function resolveLimit(count: number, max?: number): number {
return max !== undefined && max > 0 ? max : count;
}
// A name is all a location is guaranteed to have, so a blank one is unusable
// rather than merely unhelpful - dropping it beats rendering ", , Bohol".
function usableNames(locations?: NamedLocation[] | null): string[] {
return (locations ?? [])
.map((location) => location.name?.trim())
.filter((name): name is string => !!name);
}