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));
}