Transport Search Params
Serialize and parse TransportSearchValue to/from URLSearchParams. Flat, human-readable param scheme (from_id, to_id, dm, pax_*) for shareable, bookmarkable transport search URLs.
Source
registry/core/lib/transport-search-params.ts/**
* URL search-param serialization for the Transport pillar.
*
* Converts `TransportSearchValue` to a flat `URLSearchParams` instance and
* back. The param scheme is intentionally human-readable and stable so that
* transport search URLs are shareable, bookmarkable, and usable across
* list/feed/map layout variants without schema migration.
*
* ## Param scheme
*
* | Param | Type | Description |
* |---------------|---------|--------------------------------------------------|
* | `from_id` | string | Origin `LocationRef.id` (opaque, provider-scoped)|
* | `from_label` | string | Origin display label |
* | `from_sub` | string? | Origin sublabel |
* | `from_kind` | string? | Origin `LocationKind` |
* | `from_lat` | number? | Origin decimal latitude |
* | `from_lng` | number? | Origin decimal longitude |
* | `to_id` | string | Destination `LocationRef.id` |
* | `to_label` | string | Destination display label |
* | `to_sub` | string? | Destination sublabel |
* | `to_kind` | string? | Destination `LocationKind` |
* | `to_lat` | number? | Destination decimal latitude |
* | `to_lng` | number? | Destination decimal longitude |
* | `dm` | string | `dateMode`: `"single"` or `"range"` |
* | `d` | string? | ISO departure date (`dateMode === "single"`) |
* | `df` | string? | ISO range start (`dateMode === "range"`) |
* | `dt` | string? | ISO range end (`dateMode === "range"`) |
* | `mode` | string? | `TransportMode` |
* | `sub` | string? | `TransportSubtype` |
* | `pax_{id}` | number | Passenger count per category id (e.g. `pax_adult=2`) |
*/
import type {
TransportSearchValue,
LocationRef,
LocationKind,
TransportMode,
TransportSubtype,
} from "@/lib/booking-search.types";
// ── LocationRef serialization ────────────────────────────────────────────────
function writeLocationRef(
params: URLSearchParams,
prefix: string,
ref: LocationRef,
): void {
params.set(`${prefix}_id`, ref.id);
params.set(`${prefix}_label`, ref.label);
if (ref.sublabel) params.set(`${prefix}_sub`, ref.sublabel);
if (ref.kind) params.set(`${prefix}_kind`, ref.kind);
if (ref.lat !== undefined) params.set(`${prefix}_lat`, String(ref.lat));
if (ref.lng !== undefined) params.set(`${prefix}_lng`, String(ref.lng));
}
function readLocationRef(
params: URLSearchParams,
prefix: string,
): LocationRef | undefined {
const id = params.get(`${prefix}_id`);
const label = params.get(`${prefix}_label`);
if (!id || !label) return undefined;
const latStr = params.get(`${prefix}_lat`);
const lngStr = params.get(`${prefix}_lng`);
return {
id,
label,
sublabel: params.get(`${prefix}_sub`) ?? undefined,
kind: (params.get(`${prefix}_kind`) as LocationKind) ?? undefined,
lat: latStr !== null ? Number(latStr) : undefined,
lng: lngStr !== null ? Number(lngStr) : undefined,
};
}
// ── Serialize ────────────────────────────────────────────────────────────────
/**
* Serialize a `TransportSearchValue` to a flat `URLSearchParams`.
*
* Only fields with meaningful values are written — omitting defaults keeps
* URLs clean. The result is suitable for `router.push(?${params})` or
* `new URL(..., params)`.
*
* @example
* ```ts
* const params = serializeTransportSearch(searchValue);
* router.push(`/transport?${params}`);
* ```
*/
export function serializeTransportSearch(
value: TransportSearchValue,
): URLSearchParams {
const params = new URLSearchParams();
if (value.origin) writeLocationRef(params, "from", value.origin);
if (value.destination) writeLocationRef(params, "to", value.destination);
params.set("dm", value.dateMode);
if (value.dateMode === "single") {
if (value.date) params.set("d", value.date);
} else {
if (value.dateFrom) params.set("df", value.dateFrom);
if (value.dateTo) params.set("dt", value.dateTo);
}
if (value.mode) params.set("mode", value.mode);
if (value.subtype) params.set("sub", value.subtype);
for (const [id, count] of Object.entries(value.party?.counts ?? {})) {
if (count > 0) params.set(`pax_${id}`, String(count));
}
return params;
}
// ── Parse ────────────────────────────────────────────────────────────────────
/**
* Parse URL search params back to a partial `TransportSearchValue`.
*
* Returns a `Partial` because the URL may be incomplete (e.g. shared without
* a date, or with only an origin set). Callers should merge with a default
* value before use.
*
* Unknown or malformed param values are silently ignored — the function never
* throws.
*
* @example
* ```ts
* const partial = parseTransportSearch(new URLSearchParams(window.location.search));
* const value: TransportSearchValue = { ...DEFAULT_SEARCH, ...partial };
* ```
*/
export function parseTransportSearch(
params: URLSearchParams,
): Partial<TransportSearchValue> {
const result: Partial<TransportSearchValue> = { domain: "transport" };
const origin = readLocationRef(params, "from");
if (origin) result.origin = origin;
const destination = readLocationRef(params, "to");
if (destination) result.destination = destination;
const dm = params.get("dm");
if (dm === "single" || dm === "range") {
result.dateMode = dm;
if (dm === "single") {
const d = params.get("d");
if (d) result.date = d;
} else {
const df = params.get("df");
const dt = params.get("dt");
if (df) result.dateFrom = df;
if (dt) result.dateTo = dt;
}
}
const mode = params.get("mode") as TransportMode | null;
if (mode) result.mode = mode;
const sub = params.get("sub") as TransportSubtype | null;
if (sub) result.subtype = sub;
const counts: Record<string, number> = {};
for (const [key, val] of params.entries()) {
if (key.startsWith("pax_")) {
const id = key.slice(4);
const n = Number(val);
if (id && !Number.isNaN(n) && n >= 0) counts[id] = n;
}
}
if (Object.keys(counts).length > 0) {
result.party = { counts };
}
return result;
}
// ── Roundtrip helper ─────────────────────────────────────────────────────────
/**
* Convenience wrapper: serialize → string → parse.
*
* Useful for testing and for deriving a canonical URL from an in-memory value.
*/
export function transportSearchToString(value: TransportSearchValue): string {
return serializeTransportSearch(value).toString();
}
/**
* Parse a raw query string (with or without leading `?`) into a partial
* `TransportSearchValue`.
*/
export function transportSearchFromString(
queryString: string,
): Partial<TransportSearchValue> {
const qs = queryString.startsWith("?") ? queryString.slice(1) : queryString;
return parseTransportSearch(new URLSearchParams(qs));
}