mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 13:36:48 +00:00
114 lines
3.4 KiB
TypeScript
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
|
|
}
|