Merge branch 'perf-test' of https://github.com/jubnl/TREK into dev
# Conflicts: # client/src/components/Map/MapView.tsx
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef, useState, useMemo, useCallback, createElement } from 'react'
|
||||
import { useEffect, useRef, useState, useMemo, useCallback, createElement, memo } from 'react'
|
||||
import DOM from 'react-dom'
|
||||
import { renderToStaticMarkup } from 'react-dom/server'
|
||||
import { MapContainer, TileLayer, Marker, Tooltip, Polyline, CircleMarker, Circle, useMap } from 'react-leaflet'
|
||||
@@ -73,7 +73,7 @@ function createPlaceIcon(place, orderNumbers, isSelected) {
|
||||
cursor:pointer;flex-shrink:0;position:relative;
|
||||
">
|
||||
<div style="width:100%;height:100%;border-radius:50%;overflow:hidden;">
|
||||
<img src="${escAttr(place.image_url)}" loading="lazy" style="width:100%;height:100%;object-fit:cover;" />
|
||||
<img src="${escAttr(place.image_url)}" loading="lazy" decoding="async" style="width:100%;height:100%;object-fit:cover;" />
|
||||
</div>
|
||||
${badgeHtml}
|
||||
</div>`,
|
||||
@@ -338,7 +338,7 @@ function LocationTracker() {
|
||||
)
|
||||
}
|
||||
|
||||
export function MapView({
|
||||
export const MapView = memo(function MapView({
|
||||
places = [],
|
||||
dayPlaces = [],
|
||||
route = null,
|
||||
@@ -412,6 +412,63 @@ export function MapView({
|
||||
fetchNext()
|
||||
}, [places])
|
||||
|
||||
const clusterIconCreateFunction = useCallback((cluster) => {
|
||||
const count = cluster.getChildCount()
|
||||
const size = count < 10 ? 36 : count < 50 ? 42 : 48
|
||||
return L.divIcon({
|
||||
html: `<div class="marker-cluster-custom" style="width:${size}px;height:${size}px;"><span>${count}</span></div>`,
|
||||
className: 'marker-cluster-wrapper',
|
||||
iconSize: L.point(size, size),
|
||||
})
|
||||
}, [])
|
||||
|
||||
const markers = useMemo(() => places.map((place) => {
|
||||
const isSelected = place.id === selectedPlaceId
|
||||
const cacheKey = place.google_place_id || place.osm_id || `${place.lat},${place.lng}`
|
||||
const resolvedPhotoUrl = place.image_url || (cacheKey && photoUrls[cacheKey]) || null
|
||||
const orderNumbers = dayOrderMap[place.id] ?? null
|
||||
const icon = createPlaceIcon({ ...place, image_url: resolvedPhotoUrl }, orderNumbers, isSelected)
|
||||
|
||||
return (
|
||||
<Marker
|
||||
key={place.id}
|
||||
position={[place.lat, place.lng]}
|
||||
icon={icon}
|
||||
eventHandlers={{
|
||||
click: () => onMarkerClick && onMarkerClick(place.id),
|
||||
}}
|
||||
zIndexOffset={isSelected ? 1000 : 0}
|
||||
>
|
||||
<Tooltip
|
||||
direction="right"
|
||||
offset={[0, 0]}
|
||||
opacity={1}
|
||||
className="map-tooltip"
|
||||
>
|
||||
<div style={{ fontFamily: "-apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif" }}>
|
||||
<div style={{ fontWeight: 600, fontSize: 12, color: 'var(--text-primary)', whiteSpace: 'nowrap' }}>
|
||||
{place.name}
|
||||
</div>
|
||||
{place.category_name && (() => {
|
||||
const CatIcon = getCategoryIcon(place.category_icon)
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 3, marginTop: 1 }}>
|
||||
<CatIcon size={10} style={{ color: place.category_color || 'var(--text-muted)', flexShrink: 0 }} />
|
||||
<span style={{ fontSize: 11, color: 'var(--text-muted)' }}>{place.category_name}</span>
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
{place.address && (
|
||||
<div style={{ fontSize: 11, color: 'var(--text-faint)', marginTop: 2, maxWidth: 180, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
{place.address}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Marker>
|
||||
)
|
||||
}), [places, selectedPlaceId, dayOrderMap, photoUrls, onMarkerClick])
|
||||
|
||||
return (
|
||||
<MapContainer
|
||||
center={center}
|
||||
@@ -424,6 +481,7 @@ export function MapView({
|
||||
url={tileUrl}
|
||||
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
||||
maxZoom={19}
|
||||
keepBuffer={4}
|
||||
/>
|
||||
|
||||
<MapController center={center} zoom={zoom} />
|
||||
@@ -441,65 +499,9 @@ export function MapView({
|
||||
showCoverageOnHover={false}
|
||||
zoomToBoundsOnClick
|
||||
singleMarkerMode
|
||||
iconCreateFunction={(cluster) => {
|
||||
const count = cluster.getChildCount()
|
||||
const size = count < 10 ? 36 : count < 50 ? 42 : 48
|
||||
return L.divIcon({
|
||||
html: `<div class="marker-cluster-custom"
|
||||
style="width:${size}px;height:${size}px;">
|
||||
<span>${count}</span>
|
||||
</div>`,
|
||||
className: 'marker-cluster-wrapper',
|
||||
iconSize: L.point(size, size),
|
||||
})
|
||||
}}
|
||||
iconCreateFunction={clusterIconCreateFunction}
|
||||
>
|
||||
{places.map((place) => {
|
||||
const isSelected = place.id === selectedPlaceId
|
||||
const cacheKey = place.google_place_id || place.osm_id || `${place.lat},${place.lng}`
|
||||
const resolvedPhotoUrl = place.image_url || (cacheKey && photoUrls[cacheKey]) || null
|
||||
const orderNumbers = dayOrderMap[place.id] ?? null
|
||||
const icon = createPlaceIcon({ ...place, image_url: resolvedPhotoUrl }, orderNumbers, isSelected)
|
||||
|
||||
return (
|
||||
<Marker
|
||||
key={place.id}
|
||||
position={[place.lat, place.lng]}
|
||||
icon={icon}
|
||||
eventHandlers={{
|
||||
click: () => onMarkerClick && onMarkerClick(place.id),
|
||||
}}
|
||||
zIndexOffset={isSelected ? 1000 : 0}
|
||||
>
|
||||
<Tooltip
|
||||
direction="right"
|
||||
offset={[0, 0]}
|
||||
opacity={1}
|
||||
className="map-tooltip"
|
||||
>
|
||||
<div style={{ fontFamily: "-apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif" }}>
|
||||
<div style={{ fontWeight: 600, fontSize: 12, color: 'var(--text-primary)', whiteSpace: 'nowrap' }}>
|
||||
{place.name}
|
||||
</div>
|
||||
{place.category_name && (() => {
|
||||
const CatIcon = getCategoryIcon(place.category_icon)
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 3, marginTop: 1 }}>
|
||||
<CatIcon size={10} style={{ color: place.category_color || 'var(--text-muted)', flexShrink: 0 }} />
|
||||
<span style={{ fontSize: 11, color: 'var(--text-muted)' }}>{place.category_name}</span>
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
{place.address && (
|
||||
<div style={{ fontSize: 11, color: 'var(--text-faint)', marginTop: 2, maxWidth: 180, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
{place.address}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Marker>
|
||||
)
|
||||
})}
|
||||
{markers}
|
||||
</MarkerClusterGroup>
|
||||
|
||||
{route && route.length > 1 && (
|
||||
@@ -536,4 +538,4 @@ export function MapView({
|
||||
})}
|
||||
</MapContainer>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
interface DragDataPayload { placeId?: string; assignmentId?: string; noteId?: string; fromDayId?: string }
|
||||
declare global { interface Window { __dragData: DragDataPayload | null } }
|
||||
|
||||
import React, { useState, useEffect, useRef } from 'react'
|
||||
import React, { useState, useEffect, useRef, useMemo } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { ChevronDown, ChevronRight, ChevronUp, Navigation, RotateCcw, ExternalLink, Clock, Pencil, GripVertical, Ticket, Plus, FileText, Check, Trash2, Info, MapPin, Star, Heart, Camera, Lightbulb, Flag, Bookmark, Train, Bus, Plane, Car, Ship, Coffee, ShoppingBag, AlertTriangle, FileDown, Lock, Hotel, Utensils, Users } from 'lucide-react'
|
||||
|
||||
@@ -78,7 +78,7 @@ interface DayPlanSidebarProps {
|
||||
onNavigateToFiles?: () => void
|
||||
}
|
||||
|
||||
export default function DayPlanSidebar({
|
||||
const DayPlanSidebar = React.memo(function DayPlanSidebar({
|
||||
tripId,
|
||||
trip, days, places, categories, assignments,
|
||||
selectedDayId, selectedPlaceId, selectedAssignmentId,
|
||||
@@ -323,6 +323,16 @@ export default function DayPlanSidebar({
|
||||
return result.sort((a, b) => a.sortKey - b.sortKey)
|
||||
}
|
||||
|
||||
// Pre-compute merged items for all days so the render loop doesn't recompute on unrelated state changes (e.g. hover)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const mergedItemsMap = useMemo(() => {
|
||||
const map: Record<number, ReturnType<typeof getMergedItems>> = {}
|
||||
days.forEach(day => { map[day.id] = getMergedItems(day.id) })
|
||||
return map
|
||||
// getMergedItems is redefined each render but captures assignments/dayNotes/reservations/days via closure
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [days, assignments, dayNotes, reservations])
|
||||
|
||||
const openAddNote = (dayId, e) => {
|
||||
e?.stopPropagation()
|
||||
_openAddNote(dayId, getMergedItems, (id) => {
|
||||
@@ -669,10 +679,10 @@ export default function DayPlanSidebar({
|
||||
setDraggingId(null)
|
||||
}
|
||||
|
||||
const totalCost = days.reduce((s, d) => {
|
||||
const totalCost = useMemo(() => days.reduce((s, d) => {
|
||||
const da = assignments[String(d.id)] || []
|
||||
return s + da.reduce((s2, a) => s2 + (parseFloat(a.place?.price) || 0), 0)
|
||||
}, 0)
|
||||
}, 0), [days, assignments])
|
||||
|
||||
// Bester verfügbarer Standort für Wetter: zugewiesene Orte zuerst, dann beliebiger Reiseort
|
||||
const anyGeoAssignment = Object.values(assignments).flatMap(da => da).find(a => a.place?.lat && a.place?.lng)
|
||||
@@ -756,12 +766,12 @@ export default function DayPlanSidebar({
|
||||
const formattedDate = formatDate(day.date, locale)
|
||||
const loc = da.find(a => a.place?.lat && a.place?.lng)
|
||||
const isDragTarget = dragOverDayId === day.id
|
||||
const merged = getMergedItems(day.id)
|
||||
const merged = mergedItemsMap[day.id] || []
|
||||
const dayNoteUi = noteUi[day.id]
|
||||
const placeItems = merged.filter(i => i.type === 'place')
|
||||
|
||||
return (
|
||||
<div key={day.id} style={{ borderBottom: '1px solid var(--border-faint)' }}>
|
||||
<div key={day.id} style={{ borderBottom: '1px solid var(--border-faint)', contentVisibility: 'auto', containIntrinsicSize: '0 64px' }}>
|
||||
{/* Tages-Header — akzeptiert Drops aus der PlacesSidebar */}
|
||||
<div
|
||||
onClick={() => { onSelectDay(day.id); if (onDayDetail) onDayDetail(day) }}
|
||||
@@ -1663,4 +1673,6 @@ export default function DayPlanSidebar({
|
||||
<ContextMenu menu={ctxMenu.menu} onClose={ctxMenu.close} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
export default DayPlanSidebar
|
||||
|
||||
@@ -30,7 +30,7 @@ interface PlacesSidebarProps {
|
||||
onCategoryFilterChange?: (categoryId: string) => void
|
||||
}
|
||||
|
||||
export default function PlacesSidebar({
|
||||
const PlacesSidebar = React.memo(function PlacesSidebar({
|
||||
tripId, places, categories, assignments, selectedDayId, selectedPlaceId,
|
||||
onPlaceClick, onAddPlace, onAssignToDay, onEditPlace, onDeletePlace, days, isMobile, onCategoryFilterChange,
|
||||
}: PlacesSidebarProps) {
|
||||
@@ -69,9 +69,9 @@ export default function PlacesSidebar({
|
||||
const [catDropOpen, setCatDropOpen] = useState(false)
|
||||
|
||||
// Alle geplanten Ort-IDs abrufen (einem Tag zugewiesen)
|
||||
const plannedIds = new Set(
|
||||
const plannedIds = useMemo(() => new Set(
|
||||
Object.values(assignments).flatMap(da => da.map(a => a.place?.id).filter(Boolean))
|
||||
)
|
||||
), [assignments])
|
||||
|
||||
const filtered = useMemo(() => places.filter(p => {
|
||||
if (filter === 'unplanned' && plannedIds.has(p.id)) return false
|
||||
@@ -79,7 +79,7 @@ export default function PlacesSidebar({
|
||||
if (search && !p.name.toLowerCase().includes(search.toLowerCase()) &&
|
||||
!(p.address || '').toLowerCase().includes(search.toLowerCase())) return false
|
||||
return true
|
||||
}), [places, filter, categoryFilters, search, plannedIds.size])
|
||||
}), [places, filter, categoryFilters, search, plannedIds])
|
||||
|
||||
const isAssignedToSelectedDay = (placeId) =>
|
||||
selectedDayId && (assignments[String(selectedDayId)] || []).some(a => a.place?.id === placeId)
|
||||
@@ -363,4 +363,6 @@ export default function PlacesSidebar({
|
||||
<ContextMenu menu={ctxMenu.menu} onClose={ctxMenu.close} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
export default PlacesSidebar
|
||||
|
||||
@@ -86,6 +86,7 @@ export default React.memo(function PlaceAvatar({ place, size = 32, category }: P
|
||||
src={photoSrc}
|
||||
alt={place.name}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
||||
onError={() => setPhotoSrc(null)}
|
||||
/>
|
||||
|
||||
1620
server/public/assets/index-DI3G9BqA.js
Normal file
1
server/public/assets/index-DJwRRNTT.css
Normal file
BIN
server/public/fonts/Poppins-Bold.ttf
Normal file
BIN
server/public/fonts/Poppins-Italic.ttf
Normal file
BIN
server/public/fonts/Poppins-Medium.ttf
Normal file
BIN
server/public/fonts/Poppins-Regular.ttf
Normal file
BIN
server/public/fonts/Poppins-SemiBold.ttf
Normal file
BIN
server/public/icons/apple-touch-icon-180x180.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
server/public/icons/icon-192x192.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
server/public/icons/icon-512x512.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
1
server/public/icons/icon-dark.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="2000" zoomAndPan="magnify" viewBox="0 0 1500 1499.999933" height="2000" preserveAspectRatio="xMidYMid meet" version="1.0"><defs><clipPath id="a5b4275efd"><path d="M 45 5.265625 L 1455 5.265625 L 1455 1494.765625 L 45 1494.765625 Z M 45 5.265625 " clip-rule="nonzero"/></clipPath><clipPath id="61932b752f"><path d="M 855.636719 699.203125 L 222.246094 699.203125 C 197.679688 699.203125 179.90625 675.753906 186.539062 652.101562 L 360.429688 32.390625 C 364.921875 16.386719 379.511719 5.328125 396.132812 5.328125 L 1029.527344 5.328125 C 1054.089844 5.328125 1071.867188 28.777344 1065.230469 52.429688 L 891.339844 672.136719 C 886.851562 688.140625 872.257812 699.203125 855.636719 699.203125 Z M 444.238281 1166.980469 L 533.773438 847.898438 C 540.410156 824.246094 522.632812 800.796875 498.070312 800.796875 L 172.472656 800.796875 C 155.851562 800.796875 141.261719 811.855469 136.769531 827.859375 L 47.234375 1146.941406 C 40.597656 1170.59375 58.375 1194.042969 82.9375 1194.042969 L 408.535156 1194.042969 C 425.15625 1194.042969 439.75 1182.984375 444.238281 1166.980469 Z M 609.003906 827.859375 L 435.113281 1447.570312 C 428.476562 1471.21875 446.253906 1494.671875 470.816406 1494.671875 L 1104.210938 1494.671875 C 1120.832031 1494.671875 1135.421875 1483.609375 1139.914062 1467.605469 L 1313.804688 847.898438 C 1320.441406 824.246094 1302.664062 800.796875 1278.101562 800.796875 L 644.707031 800.796875 C 628.085938 800.796875 613.492188 811.855469 609.003906 827.859375 Z M 1056.105469 333.019531 L 966.570312 652.101562 C 959.933594 675.753906 977.710938 699.203125 1002.273438 699.203125 L 1327.871094 699.203125 C 1344.492188 699.203125 1359.085938 688.140625 1363.574219 672.136719 L 1453.109375 353.054688 C 1459.746094 329.40625 1441.96875 305.953125 1417.40625 305.953125 L 1091.808594 305.953125 C 1075.1875 305.953125 1060.597656 317.015625 1056.105469 333.019531 Z M 1056.105469 333.019531 " clip-rule="nonzero"/></clipPath></defs><g clip-path="url(#a5b4275efd)"><g clip-path="url(#61932b752f)"><path fill="#000000" d="M 40.597656 5.328125 L 40.597656 1494.671875 L 1459.472656 1494.671875 L 1459.472656 5.328125 Z M 40.597656 5.328125 " fill-opacity="1" fill-rule="nonzero"/></g></g></svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
1
server/public/icons/icon-white.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="2000" zoomAndPan="magnify" viewBox="0 0 1500 1499.999933" height="2000" preserveAspectRatio="xMidYMid meet" version="1.0"><defs><clipPath id="ff6253e8fa"><path d="M 45 5.265625 L 1455 5.265625 L 1455 1494.765625 L 45 1494.765625 Z M 45 5.265625 " clip-rule="nonzero"/></clipPath><clipPath id="c6b14a8188"><path d="M 855.636719 699.203125 L 222.246094 699.203125 C 197.679688 699.203125 179.90625 675.75 186.539062 652.101562 L 360.429688 32.390625 C 364.921875 16.386719 379.511719 5.328125 396.132812 5.328125 L 1029.527344 5.328125 C 1054.089844 5.328125 1071.867188 28.777344 1065.230469 52.429688 L 891.339844 672.136719 C 886.851562 688.140625 872.257812 699.203125 855.636719 699.203125 Z M 444.238281 1166.980469 L 533.773438 847.898438 C 540.410156 824.246094 522.632812 800.796875 498.070312 800.796875 L 172.472656 800.796875 C 155.851562 800.796875 141.261719 811.855469 136.769531 827.859375 L 47.234375 1146.941406 C 40.597656 1170.59375 58.375 1194.042969 82.9375 1194.042969 L 408.535156 1194.042969 C 425.15625 1194.042969 439.75 1182.984375 444.238281 1166.980469 Z M 609.003906 827.859375 L 435.113281 1447.570312 C 428.476562 1471.21875 446.253906 1494.671875 470.816406 1494.671875 L 1104.210938 1494.671875 C 1120.832031 1494.671875 1135.421875 1483.609375 1139.914062 1467.605469 L 1313.804688 847.898438 C 1320.441406 824.246094 1302.664062 800.796875 1278.101562 800.796875 L 644.707031 800.796875 C 628.085938 800.796875 613.492188 811.855469 609.003906 827.859375 Z M 1056.105469 333.019531 L 966.570312 652.101562 C 959.933594 675.75 977.710938 699.203125 1002.273438 699.203125 L 1327.871094 699.203125 C 1344.492188 699.203125 1359.085938 688.140625 1363.574219 672.136719 L 1453.109375 353.054688 C 1459.746094 329.40625 1441.96875 305.953125 1417.40625 305.953125 L 1091.808594 305.953125 C 1075.1875 305.953125 1060.597656 317.015625 1056.105469 333.019531 Z M 1056.105469 333.019531 " clip-rule="nonzero"/></clipPath></defs><g clip-path="url(#ff6253e8fa)"><g clip-path="url(#c6b14a8188)"><path fill="#ffffff" d="M 40.597656 5.328125 L 40.597656 1494.671875 L 1459.472656 1494.671875 L 1459.472656 5.328125 Z M 40.597656 5.328125 " fill-opacity="1" fill-rule="nonzero"/></g></g></svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
15
server/public/icons/icon.svg
Normal file
@@ -0,0 +1,15 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stop-color="#1e293b"/>
|
||||
<stop offset="100%" stop-color="#0f172a"/>
|
||||
</linearGradient>
|
||||
<clipPath id="icon">
|
||||
<path d="M 855.636719 699.203125 L 222.246094 699.203125 C 197.679688 699.203125 179.90625 675.75 186.539062 652.101562 L 360.429688 32.390625 C 364.921875 16.386719 379.511719 5.328125 396.132812 5.328125 L 1029.527344 5.328125 C 1054.089844 5.328125 1071.867188 28.777344 1065.230469 52.429688 L 891.339844 672.136719 C 886.851562 688.140625 872.257812 699.203125 855.636719 699.203125 Z M 444.238281 1166.980469 L 533.773438 847.898438 C 540.410156 824.246094 522.632812 800.796875 498.070312 800.796875 L 172.472656 800.796875 C 155.851562 800.796875 141.261719 811.855469 136.769531 827.859375 L 47.234375 1146.941406 C 40.597656 1170.59375 58.375 1194.042969 82.9375 1194.042969 L 408.535156 1194.042969 C 425.15625 1194.042969 439.75 1182.984375 444.238281 1166.980469 Z M 609.003906 827.859375 L 435.113281 1447.570312 C 428.476562 1471.21875 446.253906 1494.671875 470.816406 1494.671875 L 1104.210938 1494.671875 C 1120.832031 1494.671875 1135.421875 1483.609375 1139.914062 1467.605469 L 1313.804688 847.898438 C 1320.441406 824.246094 1302.664062 800.796875 1278.101562 800.796875 L 644.707031 800.796875 C 628.085938 800.796875 613.492188 811.855469 609.003906 827.859375 Z M 1056.105469 333.019531 L 966.570312 652.101562 C 959.933594 675.75 977.710938 699.203125 1002.273438 699.203125 L 1327.871094 699.203125 C 1344.492188 699.203125 1359.085938 688.140625 1363.574219 672.136719 L 1453.109375 353.054688 C 1459.746094 329.40625 1441.96875 305.953125 1417.40625 305.953125 L 1091.808594 305.953125 C 1075.1875 305.953125 1060.597656 317.015625 1056.105469 333.019531 Z"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<rect width="512" height="512" fill="url(#bg)"/>
|
||||
<g transform="translate(56,51) scale(0.267)">
|
||||
<rect width="1500" height="1500" fill="#ffffff" clip-path="url(#icon)"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
33
server/public/index.html
Normal file
@@ -0,0 +1,33 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<title>TREK</title>
|
||||
|
||||
<!-- PWA / iOS -->
|
||||
<meta name="theme-color" content="#09090b" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
||||
<meta name="apple-mobile-web-app-title" content="TREK" />
|
||||
<link rel="apple-touch-icon" href="/icons/apple-touch-icon-180x180.png" />
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/svg+xml" href="/icons/icon-dark.svg" />
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=MuseoModerno:wght@400;700;800&display=swap" rel="stylesheet" />
|
||||
|
||||
<!-- Leaflet -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
||||
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
|
||||
crossorigin="" />
|
||||
<script type="module" crossorigin src="/assets/index-DI3G9BqA.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DJwRRNTT.css">
|
||||
<link rel="manifest" href="/manifest.webmanifest"><script id="vite-plugin-pwa:register-sw" src="/registerSW.js"></script></head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
</body>
|
||||
1
server/public/logo-dark.svg
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
1
server/public/logo-light.svg
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
1
server/public/manifest.webmanifest
Normal file
@@ -0,0 +1 @@
|
||||
{"name":"TREK — Travel Planner","short_name":"TREK","description":"Travel Resource & Exploration Kit","start_url":"/","display":"standalone","background_color":"#0f172a","theme_color":"#111827","lang":"en","scope":"/","orientation":"any","categories":["travel","navigation"],"icons":[{"src":"icons/apple-touch-icon-180x180.png","sizes":"180x180","type":"image/png"},{"src":"icons/icon-192x192.png","sizes":"192x192","type":"image/png"},{"src":"icons/icon-512x512.png","sizes":"512x512","type":"image/png"},{"src":"icons/icon-512x512.png","sizes":"512x512","type":"image/png","purpose":"maskable"},{"src":"icons/icon.svg","sizes":"any","type":"image/svg+xml"}]}
|
||||
1
server/public/registerSW.js
Normal file
@@ -0,0 +1 @@
|
||||
if('serviceWorker' in navigator) {window.addEventListener('load', () => {navigator.serviceWorker.register('/sw.js', { scope: '/' })})}
|
||||
1
server/public/sw.js
Normal file
@@ -0,0 +1 @@
|
||||
if(!self.define){let e,s={};const i=(i,n)=>(i=new URL(i+".js",n).href,s[i]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=i,e.onload=s,document.head.appendChild(e)}else e=i,importScripts(i),s()}).then(()=>{let e=s[i];if(!e)throw new Error(`Module ${i} didn’t register its module`);return e}));self.define=(n,c)=>{const o=e||("document"in self?document.currentScript.src:"")||location.href;if(s[o])return;let a={};const t=e=>i(e,o),r={module:{uri:o},exports:a,require:t};s[o]=Promise.all(n.map(e=>r[e]||t(e))).then(e=>(c(...e),a))}}define(["./workbox-58bd4dca"],function(e){"use strict";self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"text-light.svg",revision:"8456421c45ccd1b881b1755949fb9891"},{url:"text-dark.svg",revision:"e86569d59169a1076a92a1d47cb94abf"},{url:"registerSW.js",revision:"1872c500de691dce40960bb85481de07"},{url:"logo-light.svg",revision:"e9a2e3363fed4298cb422332b8cb03e9"},{url:"logo-dark.svg",revision:"c7b85b3bdf9e73222bcd91f396b829b5"},{url:"index.html",revision:"669f01b197b6e32a76e365d38b8783c4"},{url:"icons/icon.svg",revision:"8b49f04dc5ebfc2688777f548f6248a1"},{url:"icons/icon-white.svg",revision:"f437d171b083ee2463e3c44eb3785291"},{url:"icons/icon-dark.svg",revision:"cf48a00cd2b6393eb0c8ac67d821ec84"},{url:"icons/icon-512x512.png",revision:"e9813f28d172940286269b92c961bd9a"},{url:"icons/icon-192x192.png",revision:"4549ed2c430764d6eda6b12a326e6d58"},{url:"icons/apple-touch-icon-180x180.png",revision:"ba88094c86c61709a98adae54488508f"},{url:"fonts/Poppins-SemiBold.ttf",revision:"2c63e05091c7d89f6149c274971c7c23"},{url:"fonts/Poppins-Regular.ttf",revision:"09acac7457bdcf80af5cc3d1116208c5"},{url:"fonts/Poppins-Medium.ttf",revision:"20aaac2ef92cddeb0f12e67a443b0b9f"},{url:"fonts/Poppins-Italic.ttf",revision:"4a37e40ddcd3e0da0a1db26ce8704eff"},{url:"fonts/Poppins-Bold.ttf",revision:"92934d92f57e49fc6f61075c2aeb7689"},{url:"assets/index-DJwRRNTT.css",revision:null},{url:"assets/index-DI3G9BqA.js",revision:null},{url:"icons/apple-touch-icon-180x180.png",revision:"ba88094c86c61709a98adae54488508f"},{url:"icons/icon-192x192.png",revision:"4549ed2c430764d6eda6b12a326e6d58"},{url:"icons/icon-512x512.png",revision:"e9813f28d172940286269b92c961bd9a"},{url:"icons/icon.svg",revision:"8b49f04dc5ebfc2688777f548f6248a1"},{url:"manifest.webmanifest",revision:"99e6d32e351da90e7659354c2dc39bfb"}],{}),e.cleanupOutdatedCaches(),e.registerRoute(new e.NavigationRoute(e.createHandlerBoundToURL("index.html"),{denylist:[/^\/api/,/^\/uploads/,/^\/mcp/]})),e.registerRoute(/^https:\/\/[a-d]\.basemaps\.cartocdn\.com\/.*/i,new e.CacheFirst({cacheName:"map-tiles",plugins:[new e.ExpirationPlugin({maxEntries:1e3,maxAgeSeconds:2592e3}),new e.CacheableResponsePlugin({statuses:[0,200]})]}),"GET"),e.registerRoute(/^https:\/\/[a-c]\.tile\.openstreetmap\.org\/.*/i,new e.CacheFirst({cacheName:"map-tiles",plugins:[new e.ExpirationPlugin({maxEntries:1e3,maxAgeSeconds:2592e3}),new e.CacheableResponsePlugin({statuses:[0,200]})]}),"GET"),e.registerRoute(/^https:\/\/unpkg\.com\/.*/i,new e.CacheFirst({cacheName:"cdn-libs",plugins:[new e.ExpirationPlugin({maxEntries:30,maxAgeSeconds:31536e3}),new e.CacheableResponsePlugin({statuses:[0,200]})]}),"GET"),e.registerRoute(/\/api\/(?!auth|admin|backup|settings).*/i,new e.NetworkFirst({cacheName:"api-data",networkTimeoutSeconds:5,plugins:[new e.ExpirationPlugin({maxEntries:200,maxAgeSeconds:86400}),new e.CacheableResponsePlugin({statuses:[200]})]}),"GET"),e.registerRoute(/\/uploads\/(?:covers|avatars)\/.*/i,new e.CacheFirst({cacheName:"user-uploads",plugins:[new e.ExpirationPlugin({maxEntries:300,maxAgeSeconds:604800}),new e.CacheableResponsePlugin({statuses:[200]})]}),"GET")});
|
||||
1
server/public/text-dark.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="230" zoomAndPan="magnify" viewBox="49 2 124 56" height="80" preserveAspectRatio="xMidYMid meet" version="1.0"><defs><g/><clipPath id="c5c1a398e1"><path d="M 49 0.015625 L 174 0.015625 L 174 59.984375 L 49 59.984375 Z M 49 0.015625 " clip-rule="nonzero"/></clipPath><clipPath id="9b226024c5"><rect x="0" width="125" y="0" height="60"/></clipPath></defs><g clip-path="url(#c5c1a398e1)"><g transform="matrix(1, 0, 0, 1, 49, -0.000000000000011803)"><g clip-path="url(#9b226024c5)"><g fill="#000000" fill-opacity="1"><g transform="translate(0.740932, 54.900246)"><g><path d="M 17.53125 0 C 14.15625 0 11.515625 -0.960938 9.609375 -2.890625 C 7.710938 -4.816406 6.765625 -7.414062 6.765625 -10.6875 L 6.765625 -45.484375 L 17.890625 -45.484375 L 17.890625 -11.328125 C 17.890625 -10.765625 18.09375 -10.28125 18.5 -9.875 C 18.90625 -9.46875 19.390625 -9.265625 19.953125 -9.265625 L 27.9375 -9.265625 L 27.9375 0 Z M 0.78125 -27.515625 L 0.78125 -36.5625 L 27.9375 -36.5625 L 27.9375 -27.515625 Z M 0.78125 -27.515625 "/></g></g></g><g fill="#000000" fill-opacity="1"><g transform="translate(26.403702, 54.900246)"><g><path d="M 3.84375 0 L 3.84375 -25.796875 C 3.84375 -29.128906 4.789062 -31.742188 6.6875 -33.640625 C 8.59375 -35.546875 11.234375 -36.5 14.609375 -36.5 L 25.234375 -36.5 L 25.234375 -27.515625 L 17.328125 -27.515625 C 16.660156 -27.515625 16.085938 -27.285156 15.609375 -26.828125 C 15.128906 -26.378906 14.890625 -25.800781 14.890625 -25.09375 L 14.890625 0 Z M 3.84375 0 "/></g></g></g><g fill="#000000" fill-opacity="1"><g transform="translate(47.504187, 54.900246)"><g><path d="M 23.234375 0 C 19.097656 0 15.460938 -0.769531 12.328125 -2.3125 C 9.191406 -3.863281 6.753906 -6.003906 5.015625 -8.734375 C 3.285156 -11.460938 2.421875 -14.632812 2.421875 -18.25 C 2.421875 -22.238281 3.25 -25.660156 4.90625 -28.515625 C 6.570312 -31.367188 8.796875 -33.566406 11.578125 -35.109375 C 14.359375 -36.648438 17.4375 -37.421875 20.8125 -37.421875 C 24.664062 -37.421875 27.882812 -36.613281 30.46875 -35 C 33.0625 -33.382812 35.019531 -31.1875 36.34375 -28.40625 C 37.675781 -25.625 38.34375 -22.453125 38.34375 -18.890625 C 38.34375 -18.273438 38.304688 -17.550781 38.234375 -16.71875 C 38.171875 -15.882812 38.09375 -15.226562 38 -14.75 L 14.046875 -14.75 C 14.328125 -13.519531 14.867188 -12.472656 15.671875 -11.609375 C 16.484375 -10.753906 17.503906 -10.125 18.734375 -9.71875 C 19.972656 -9.320312 21.351562 -9.125 22.875 -9.125 L 34.078125 -9.125 L 34.078125 0 Z M 13.75 -21.671875 L 27.65625 -21.671875 C 27.5625 -22.429688 27.414062 -23.164062 27.21875 -23.875 C 27.03125 -24.59375 26.734375 -25.222656 26.328125 -25.765625 C 25.929688 -26.316406 25.472656 -26.789062 24.953125 -27.1875 C 24.429688 -27.59375 23.820312 -27.914062 23.125 -28.15625 C 22.4375 -28.394531 21.664062 -28.515625 20.8125 -28.515625 C 19.71875 -28.515625 18.742188 -28.320312 17.890625 -27.9375 C 17.035156 -27.5625 16.320312 -27.050781 15.75 -26.40625 C 15.175781 -25.769531 14.734375 -25.035156 14.421875 -24.203125 C 14.117188 -23.367188 13.894531 -22.523438 13.75 -21.671875 Z M 13.75 -21.671875 "/></g></g></g><g fill="#000000" fill-opacity="1"><g transform="translate(83.218247, 54.900246)"><g><path d="M 4.140625 0 L 4.140625 -52.03125 L 15.1875 -52.03125 L 15.1875 -22.734375 L 20.03125 -22.734375 L 28.375 -36.5625 L 40.703125 -36.5625 L 30.859375 -21.3125 C 33.710938 -20.125 35.9375 -18.304688 37.53125 -15.859375 C 39.125 -13.410156 39.921875 -10.546875 39.921875 -7.265625 L 39.921875 0 L 28.796875 0 L 28.796875 -7.265625 C 28.796875 -8.597656 28.472656 -9.796875 27.828125 -10.859375 C 27.191406 -11.929688 26.347656 -12.773438 25.296875 -13.390625 C 24.253906 -14.015625 23.066406 -14.328125 21.734375 -14.328125 L 15.1875 -14.328125 L 15.1875 0 Z M 4.140625 0 "/></g></g></g></g></g></g></svg>
|
||||
|
After Width: | Height: | Size: 3.8 KiB |
1
server/public/text-light.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="230" zoomAndPan="magnify" viewBox="49 2 124 56" height="80" preserveAspectRatio="xMidYMid meet" version="1.0"><defs><g/><clipPath id="7fc4e3f80b"><path d="M 49 0.015625 L 174 0.015625 L 174 59.984375 L 49 59.984375 Z M 49 0.015625 " clip-rule="nonzero"/></clipPath><clipPath id="086ce69399"><rect x="0" width="125" y="0" height="60"/></clipPath></defs><g clip-path="url(#7fc4e3f80b)"><g transform="matrix(1, 0, 0, 1, 49, -0.000000000000011803)"><g clip-path="url(#086ce69399)"><g fill="#ffffff" fill-opacity="1"><g transform="translate(0.740932, 54.900246)"><g><path d="M 17.53125 0 C 14.15625 0 11.515625 -0.960938 9.609375 -2.890625 C 7.710938 -4.816406 6.765625 -7.414062 6.765625 -10.6875 L 6.765625 -45.484375 L 17.890625 -45.484375 L 17.890625 -11.328125 C 17.890625 -10.765625 18.09375 -10.28125 18.5 -9.875 C 18.90625 -9.46875 19.390625 -9.265625 19.953125 -9.265625 L 27.9375 -9.265625 L 27.9375 0 Z M 0.78125 -27.515625 L 0.78125 -36.5625 L 27.9375 -36.5625 L 27.9375 -27.515625 Z M 0.78125 -27.515625 "/></g></g></g><g fill="#ffffff" fill-opacity="1"><g transform="translate(26.403702, 54.900246)"><g><path d="M 3.84375 0 L 3.84375 -25.796875 C 3.84375 -29.128906 4.789062 -31.742188 6.6875 -33.640625 C 8.59375 -35.546875 11.234375 -36.5 14.609375 -36.5 L 25.234375 -36.5 L 25.234375 -27.515625 L 17.328125 -27.515625 C 16.660156 -27.515625 16.085938 -27.285156 15.609375 -26.828125 C 15.128906 -26.378906 14.890625 -25.800781 14.890625 -25.09375 L 14.890625 0 Z M 3.84375 0 "/></g></g></g><g fill="#ffffff" fill-opacity="1"><g transform="translate(47.504187, 54.900246)"><g><path d="M 23.234375 0 C 19.097656 0 15.460938 -0.769531 12.328125 -2.3125 C 9.191406 -3.863281 6.753906 -6.003906 5.015625 -8.734375 C 3.285156 -11.460938 2.421875 -14.632812 2.421875 -18.25 C 2.421875 -22.238281 3.25 -25.660156 4.90625 -28.515625 C 6.570312 -31.367188 8.796875 -33.566406 11.578125 -35.109375 C 14.359375 -36.648438 17.4375 -37.421875 20.8125 -37.421875 C 24.664062 -37.421875 27.882812 -36.613281 30.46875 -35 C 33.0625 -33.382812 35.019531 -31.1875 36.34375 -28.40625 C 37.675781 -25.625 38.34375 -22.453125 38.34375 -18.890625 C 38.34375 -18.273438 38.304688 -17.550781 38.234375 -16.71875 C 38.171875 -15.882812 38.09375 -15.226562 38 -14.75 L 14.046875 -14.75 C 14.328125 -13.519531 14.867188 -12.472656 15.671875 -11.609375 C 16.484375 -10.753906 17.503906 -10.125 18.734375 -9.71875 C 19.972656 -9.320312 21.351562 -9.125 22.875 -9.125 L 34.078125 -9.125 L 34.078125 0 Z M 13.75 -21.671875 L 27.65625 -21.671875 C 27.5625 -22.429688 27.414062 -23.164062 27.21875 -23.875 C 27.03125 -24.59375 26.734375 -25.222656 26.328125 -25.765625 C 25.929688 -26.316406 25.472656 -26.789062 24.953125 -27.1875 C 24.429688 -27.59375 23.820312 -27.914062 23.125 -28.15625 C 22.4375 -28.394531 21.664062 -28.515625 20.8125 -28.515625 C 19.71875 -28.515625 18.742188 -28.320312 17.890625 -27.9375 C 17.035156 -27.5625 16.320312 -27.050781 15.75 -26.40625 C 15.175781 -25.769531 14.734375 -25.035156 14.421875 -24.203125 C 14.117188 -23.367188 13.894531 -22.523438 13.75 -21.671875 Z M 13.75 -21.671875 "/></g></g></g><g fill="#ffffff" fill-opacity="1"><g transform="translate(83.218247, 54.900246)"><g><path d="M 4.140625 0 L 4.140625 -52.03125 L 15.1875 -52.03125 L 15.1875 -22.734375 L 20.03125 -22.734375 L 28.375 -36.5625 L 40.703125 -36.5625 L 30.859375 -21.3125 C 33.710938 -20.125 35.9375 -18.304688 37.53125 -15.859375 C 39.125 -13.410156 39.921875 -10.546875 39.921875 -7.265625 L 39.921875 0 L 28.796875 0 L 28.796875 -7.265625 C 28.796875 -8.597656 28.472656 -9.796875 27.828125 -10.859375 C 27.191406 -11.929688 26.347656 -12.773438 25.296875 -13.390625 C 24.253906 -14.015625 23.066406 -14.328125 21.734375 -14.328125 L 15.1875 -14.328125 L 15.1875 0 Z M 4.140625 0 "/></g></g></g></g></g></g></svg>
|
||||
|
After Width: | Height: | Size: 3.8 KiB |
1
server/public/workbox-58bd4dca.js
Normal file
@@ -123,18 +123,19 @@ async function fetchWikimediaPhoto(lat: number, lng: number, name?: string): Pro
|
||||
action: 'query', format: 'json',
|
||||
titles: name,
|
||||
prop: 'pageimages',
|
||||
piprop: 'original',
|
||||
piprop: 'thumbnail',
|
||||
pithumbsize: '400',
|
||||
pilimit: '1',
|
||||
redirects: '1',
|
||||
});
|
||||
const res = await fetch(`https://en.wikipedia.org/w/api.php?${searchParams}`, { headers: { 'User-Agent': UA } });
|
||||
if (res.ok) {
|
||||
const data = await res.json() as { query?: { pages?: Record<string, { original?: { source?: string } }> } };
|
||||
const data = await res.json() as { query?: { pages?: Record<string, { thumbnail?: { source?: string } }> } };
|
||||
const pages = data.query?.pages;
|
||||
if (pages) {
|
||||
for (const page of Object.values(pages)) {
|
||||
if (page.original?.source) {
|
||||
return { photoUrl: page.original.source, attribution: 'Wikipedia' };
|
||||
if (page.thumbnail?.source) {
|
||||
return { photoUrl: page.thumbnail.source, attribution: 'Wikipedia' };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -204,7 +205,7 @@ function getMapsKey(userId: number): string | null {
|
||||
return decrypt_api_key(admin?.maps_api_key) || null;
|
||||
}
|
||||
|
||||
const photoCache = new Map<string, { photoUrl: string; attribution: string | null; fetchedAt: number }>();
|
||||
const photoCache = new Map<string, { photoUrl: string; attribution: string | null; fetchedAt: number; error?: boolean }>();
|
||||
const PHOTO_TTL = 12 * 60 * 60 * 1000; // 12 hours
|
||||
const CACHE_MAX_ENTRIES = 1000;
|
||||
const CACHE_PRUNE_TARGET = 500;
|
||||
@@ -380,6 +381,9 @@ router.get('/place-photo/:placeId', authenticate, async (req: Request, res: Resp
|
||||
|
||||
const cached = photoCache.get(placeId);
|
||||
if (cached && Date.now() - cached.fetchedAt < PHOTO_TTL) {
|
||||
if (cached.error) {
|
||||
return res.status(404).json({ error: `(Cache) No photo available` });
|
||||
}
|
||||
return res.json({ photoUrl: cached.photoUrl, attribution: cached.attribution });
|
||||
}
|
||||
|
||||
@@ -398,10 +402,12 @@ router.get('/place-photo/:placeId', authenticate, async (req: Request, res: Resp
|
||||
if (wiki) {
|
||||
photoCache.set(placeId, { ...wiki, fetchedAt: Date.now() });
|
||||
return res.json(wiki);
|
||||
} else {
|
||||
photoCache.set(placeId, { photoUrl: '', attribution: null, fetchedAt: Date.now(), error: true });
|
||||
}
|
||||
} catch { /* fall through */ }
|
||||
}
|
||||
return res.status(404).json({ error: 'No photo available' });
|
||||
return res.status(404).json({ error: '(Wikimedia) No photo available' });
|
||||
}
|
||||
|
||||
// Google Photos
|
||||
@@ -416,11 +422,13 @@ router.get('/place-photo/:placeId', authenticate, async (req: Request, res: Resp
|
||||
|
||||
if (!detailsRes.ok) {
|
||||
console.error('Google Places photo details error:', details.error?.message || detailsRes.status);
|
||||
return res.status(404).json({ error: 'Photo could not be retrieved' });
|
||||
photoCache.set(placeId, { photoUrl: '', attribution: null, fetchedAt: Date.now(), error: true });
|
||||
return res.status(404).json({ error: '(Google Places) Photo could not be retrieved' });
|
||||
}
|
||||
|
||||
if (!details.photos?.length) {
|
||||
return res.status(404).json({ error: 'No photo available' });
|
||||
photoCache.set(placeId, { photoUrl: '', attribution: null, fetchedAt: Date.now(), error: true });
|
||||
return res.status(404).json({ error: '(Google Places) No photo available' });
|
||||
}
|
||||
|
||||
const photo = details.photos[0];
|
||||
@@ -435,7 +443,8 @@ router.get('/place-photo/:placeId', authenticate, async (req: Request, res: Resp
|
||||
const photoUrl = mediaData.photoUri;
|
||||
|
||||
if (!photoUrl) {
|
||||
return res.status(404).json({ error: 'Photo URL not available' });
|
||||
photoCache.set(placeId, { photoUrl: '', attribution, fetchedAt: Date.now(), error: true });
|
||||
return res.status(404).json({ error: '(Google Places) Photo URL not available' });
|
||||
}
|
||||
|
||||
photoCache.set(placeId, { photoUrl, attribution, fetchedAt: Date.now() });
|
||||
@@ -451,6 +460,7 @@ router.get('/place-photo/:placeId', authenticate, async (req: Request, res: Resp
|
||||
res.json({ photoUrl, attribution });
|
||||
} catch (err: unknown) {
|
||||
console.error('Place photo error:', err);
|
||||
photoCache.set(placeId, { photoUrl: '', attribution: null, fetchedAt: Date.now(), error: true });
|
||||
res.status(500).json({ error: 'Error fetching photo' });
|
||||
}
|
||||
});
|
||||
|
||||