Files
TREK/client/src/components/Planner/PlaceFormModal.helpers.ts
T

114 lines
3.4 KiB
TypeScript

export interface PlaceFormData {
name: string
description: string
address: string
lat: string
lng: string
category_id: string
place_time: string
end_time: string
notes: string
transport_mode: string
website: string
// Populated from a maps-search pick (not part of the initial blank form).
phone?: string
google_place_id?: string
google_ftid?: string
osm_id?: string
// Hero image picked from the detail column. Optional and absent from
// DEFAULT_FORM on purpose: the mobile sheet shares this type and never sets
// it, and places.service already writes image_url through on create/update.
image_url?: string
}
export function isGoogleMapsUrl(input: string): boolean {
try {
const { hostname, pathname } = new URL(input.trim())
const h = hostname.toLowerCase()
// maps.app.goo.gl, goo.gl/maps
if (h === 'maps.app.goo.gl') return true
if (h === 'goo.gl' && pathname.startsWith('/maps')) return true
// maps.google.* (e.g. maps.google.com, maps.google.co.uk)
// Must be maps.google.<tld> or maps.google.<sld>.<tld> — reject maps.google.evil.com
if (/^maps\.google\.[a-z]{2,3}(\.[a-z]{2})?$/.test(h)) return true
// google.*/maps (e.g. google.com/maps, www.google.co.uk/maps)
const bare = h.startsWith('www.') ? h.slice(4) : h
if (/^google\.[a-z]{2,3}(\.[a-z]{2})?$/.test(bare) && pathname.startsWith('/maps')) return true
return false
} catch {
return false
}
}
export const DEFAULT_FORM: PlaceFormData = {
name: '',
description: '',
address: '',
lat: '',
lng: '',
category_id: '',
place_time: '',
end_time: '',
notes: '',
transport_mode: 'walking',
website: '',
}
/**
* The fields a maps result owns. Everything else in the form belongs to the
* user and is never touched by picking a place.
*/
export const RESULT_FIELDS = [
'name',
'address',
'lat',
'lng',
'google_place_id',
'google_ftid',
'osm_id',
'website',
'phone',
] as const
export type ResultField = (typeof RESULT_FIELDS)[number]
/**
* Folds a picked search result into the form.
*
* The obvious version is `result.x || prev.x`, and it has a bug that is easy to
* miss and hard to spot as a user: that expression cannot tell "the user typed
* this" from "the previous search result wrote this". Pick Hamburg Airport,
* then pick Berlin Hauptbahnhof — which has no website in OpenStreetMap — and
* the airport's website is still sitting in the field. Save it and the station
* now links to an airport.
*
* So the caller tracks which fields it filled in itself. A field the last pick
* wrote belongs to the last place and is cleared when the new one says nothing
* about it; a field the user typed survives untouched. `autoFilled` is mutated
* in place — it is the caller's record of what it owns.
*/
export function mergeResult(
prev: PlaceFormData,
result: Record<string, unknown>,
autoFilled: Set<ResultField>,
): PlaceFormData {
const next = { ...prev } as PlaceFormData & Record<string, string | undefined>
for (const field of RESULT_FIELDS) {
const raw = result[field]
const value = raw == null ? '' : String(raw)
if (value) {
next[field] = value
autoFilled.add(field)
} else if (autoFilled.has(field)) {
// Belonged to the place that is no longer selected.
next[field] = ''
autoFilled.delete(field)
}
// Otherwise the user put it there; leave it alone.
}
return next
}