| |
| |
| |
| |
| |
| |
| |
| |
|
|
| export type ExplorerCargo = 'container' | 'tanker' | 'bulk' | 'roro'; |
| export type ExplorerTab = 1 | 2 | 3 | 4; |
|
|
| export interface ExplorerUrlState { |
| fromIso2: string | null; |
| toIso2: string | null; |
| hs2: string | null; |
| cargo: ExplorerCargo | null; |
| tab: ExplorerTab; |
| } |
|
|
| export const DEFAULT_EXPLORER_STATE: ExplorerUrlState = { |
| fromIso2: null, |
| toIso2: null, |
| hs2: null, |
| cargo: null, |
| tab: 1, |
| }; |
|
|
| const EXPLORER_QUERY_KEY = 'explorer'; |
| const VALID_CARGO: ReadonlySet<ExplorerCargo> = new Set([ |
| 'container', |
| 'tanker', |
| 'bulk', |
| 'roro', |
| ]); |
| const VALID_TABS: ReadonlySet<ExplorerTab> = new Set([1, 2, 3, 4]); |
| const ISO2_RE = /^[A-Z]{2}$/; |
| const HS2_RE = /^\d{1,2}$/; |
|
|
| |
| |
| |
| |
| export function parseExplorerUrl(search: string): ExplorerUrlState { |
| let raw: string | null; |
| try { |
| raw = new URLSearchParams(search).get(EXPLORER_QUERY_KEY); |
| } catch { |
| return { ...DEFAULT_EXPLORER_STATE }; |
| } |
| if (!raw) return { ...DEFAULT_EXPLORER_STATE }; |
|
|
| const out: ExplorerUrlState = { ...DEFAULT_EXPLORER_STATE }; |
| const parts = raw.split(','); |
| for (const part of parts) { |
| const [key, value] = part.split(':'); |
| if (!key || value === undefined) continue; |
| switch (key.trim().toLowerCase()) { |
| case 'from': { |
| const v = value.trim().toUpperCase(); |
| if (ISO2_RE.test(v)) out.fromIso2 = v; |
| break; |
| } |
| case 'to': { |
| const v = value.trim().toUpperCase(); |
| if (ISO2_RE.test(v)) out.toIso2 = v; |
| break; |
| } |
| case 'hs': { |
| const v = value.trim(); |
| if (HS2_RE.test(v)) out.hs2 = v; |
| break; |
| } |
| case 'cargo': { |
| const v = value.trim().toLowerCase() as ExplorerCargo; |
| if (VALID_CARGO.has(v)) out.cargo = v; |
| break; |
| } |
| case 'tab': { |
| const n = Number.parseInt(value.trim(), 10) as ExplorerTab; |
| if (VALID_TABS.has(n)) out.tab = n; |
| break; |
| } |
| default: |
| break; |
| } |
| } |
| return out; |
| } |
|
|
| |
| |
| |
| |
| export function serializeExplorerUrl(state: ExplorerUrlState): string | null { |
| const parts: string[] = []; |
| if (state.fromIso2 && ISO2_RE.test(state.fromIso2)) parts.push(`from:${state.fromIso2}`); |
| if (state.toIso2 && ISO2_RE.test(state.toIso2)) parts.push(`to:${state.toIso2}`); |
| if (state.hs2 && HS2_RE.test(state.hs2)) parts.push(`hs:${state.hs2}`); |
| if (state.cargo && VALID_CARGO.has(state.cargo)) parts.push(`cargo:${state.cargo}`); |
| if (state.tab !== 1 && VALID_TABS.has(state.tab)) parts.push(`tab:${state.tab}`); |
| if (parts.length === 0) return null; |
| return parts.join(','); |
| } |
|
|
| |
| |
| |
| |
| export function writeExplorerUrl(state: ExplorerUrlState): void { |
| if (typeof window === 'undefined' || !window.history?.replaceState) return; |
| const url = new URL(window.location.href); |
| const serialized = serializeExplorerUrl(state); |
| if (serialized) { |
| url.searchParams.set(EXPLORER_QUERY_KEY, serialized); |
| } else { |
| url.searchParams.delete(EXPLORER_QUERY_KEY); |
| } |
| window.history.replaceState(window.history.state, '', url.toString()); |
| } |
|
|
| |
| |
| |
| |
| export function readExplorerUrl(): ExplorerUrlState { |
| if (typeof window === 'undefined') return { ...DEFAULT_EXPLORER_STATE }; |
| return parseExplorerUrl(window.location.search); |
| } |
|
|