Files
TREK/client/src/components/Map/placePhoto.ts
T
Maurice 0dd07befa7 feat(planner): bring the plan tab and every trip dialog into the new look
The Plan tab was the last trip tab still in the old design. It now reads
like Bookings, Costs, Lists, Files and Collab, without losing a function:

- Day plan: each day is a card with a head band (day tile with weather,
  stay and rental pills, a "+" menu for place, stay, transport, transit
  and note), rows with "..." menus, white badges and a white route bar.
- Places column: add, search and filters in three rows, a show dropdown,
  category and rating filters, and a selection bar at the foot.
- Place inspector, day details and the stay editor on the shared dialog
  frame; the place form in the editor look with the name in the head band.
- Clicking a booking or transport anywhere on the plan opens the booking
  detail popup first; Edit leads to the editor. The popup is one host
  shared with the Bookings and Transports tabs.
- Files and Collab dialogs, notes, polls, links and chat moved to the same
  frame; shared DialogShell and dialogParts hold the building blocks.
- The map hover card shows the place photo and follows the theme.

Every new and reworded string is translated into all locales, and a few
older English leftovers (help center, cost split, import hints) with them.
Phone screens keep their own look.
2026-09-27 21:00:19 +02:00

90 lines
4.0 KiB
TypeScript

import { escapeHtml } from '@trek/shared'
import { getCached } from '../../services/photoService'
const PHOTO_PROXY_PREFIX = '/api/maps/place-photo/'
/** The fields of a place that decide which picture its marker gets. */
interface PhotoSource {
image_url?: string | null
google_place_id?: string | null
osm_id?: string | null
lat?: number | null
lng?: number | null
}
/**
* A place's image_url that is directly displayable on a map marker and should
* win over the auto-fetched provider thumbnail — a custom uploaded image (#1136)
* or an inline data URL. Provider proxy URLs (/api/maps/place-photo/…) are
* deliberately excluded: those still go through the downscale/thumb path so many
* markers don't lag on zoom.
*/
export function isCustomPlaceImage(url: string | null | undefined): boolean {
return !!url && (url.startsWith('/uploads/') || url.startsWith('data:'))
}
/**
* Cache key under which a place's auto-fetched thumbnail is stored: the provider
* photo the user picked when there is one, else the provider id, otherwise the
* coordinates. The picked photo comes first because picking another picture of the
* same place leaves its provider id alone, and the thumb cached under that id
* would go on answering for the new one. A place with none of these has no
* identity to cache under, so it gets the empty string: callers skip it rather
* than filing every coordinate-less place under one shared entry.
*/
export function photoCacheKey(place: PhotoSource): string {
if (place.image_url?.startsWith(PHOTO_PROXY_PREFIX)) return place.image_url
if (place.google_place_id) return place.google_place_id
if (place.osm_id) return place.osm_id
if (place.lat == null || place.lng == null) return ''
return `${place.lat},${place.lng}`
}
/**
* What the maps' thumb loading depends on, as one string to key the effect on.
* Place ids alone missed every change of picture: taking an upload off a place or
* picking another suggested photo changes no id, so the marker kept a category
* dot or the old thumb until the page was reloaded.
*/
export function photoSourcesKey(places: Array<PhotoSource & { id: number }>): string {
return places.map(p => `${p.id}:${isCustomPlaceImage(p.image_url) ? 'own' : photoCacheKey(p)}`).join('|')
}
/**
* The picture a place wears on the map: a custom uploaded image wins over the
* auto-fetched thumb, which wins over whatever image_url says. The marker and the
* hover card both ask this, so they never show two different pictures.
*/
export function placePhotoUrl(place: PhotoSource, thumbs: Record<string, string>): string | null {
if (isCustomPlaceImage(place.image_url)) return place.image_url!
const key = photoCacheKey(place)
return (key && thumbs[key]) || place.image_url || null
}
/**
* The same picture at full size, for the hover card. The marker's thumb is a 48px
* circle cut for a round pin, which blurs and keeps its round cut-out when it is
* shown any larger, so the card asks the photo cache for the picture it came from.
*/
export function placePhotoFull(place: PhotoSource): string | null {
if (isCustomPlaceImage(place.image_url)) return place.image_url!
const key = photoCacheKey(place)
const full = key ? getCached(key)?.photoUrl : null
if (full) return full
return place.image_url?.startsWith(PHOTO_PROXY_PREFIX) ? place.image_url : null
}
/**
* The picture inside a round place marker, for both the Leaflet and the GL map.
*
* Sized by its style and not by width/height attributes, because stylesheet rules
* beat attributes: Tailwind's preflight gives every img `height: auto`, and
* Leaflet's marker pane adds `width: auto`. The 48px square thumbs got away with
* that. An uploaded photo kept its own proportions on the GL map and its full pixel
* size on Leaflet, where only the rounded-off corner of a 4000px image lands inside
* the circle, so the marker showed nothing but its category colour.
*/
export function markerPhotoHtml(url: string): string {
return `<img src="${escapeHtml(url)}" alt="" style="display:block;width:100%;height:100%;border-radius:50%;object-fit:cover;" />`
}