Files
TREK/client/src/components/Weather/WeatherWidget.tsx
T
jubnl 6a718fccea feat(import): selective GPX/KML element import and performance improvements
Add type-selector UI in the file import modal letting users choose which
GPX elements (waypoints, routes, tracks) or KML/KMZ elements (points,
paths) to import. KML LineString placemarks are now imported as path
places with route_geometry.

Performance improvements:
- Extract MemoPlaceRow with React.memo and contentVisibility:auto to cut
  unnecessary re-renders in PlacesSidebar
- Add weatherQueue to cap concurrent weather fetches at 3
- Replace sequential per-place deletes with a single bulkDelete API call
  (new DELETE /places/bulk endpoint + deletePlacesMany service)
- Memoize atlas/photo/weather service calls to avoid redundant requests
- Add multi-select mode to PlacesSidebar for bulk operations

Add large GPX/KML/KMZ fixtures for integration/perf testing and two
profiler analysis scripts under scripts/.
2026-04-18 01:28:37 +02:00

131 lines
4.0 KiB
TypeScript

import { useState, useEffect } from 'react'
import { Sun, Cloud, CloudRain, CloudSnow, CloudDrizzle, CloudLightning, Wind } from 'lucide-react'
import { fetchWeather } from '../../services/weatherQueue'
import { useSettingsStore } from '../../store/settingsStore'
const WEATHER_ICON_MAP = {
Clear: Sun,
Clouds: Cloud,
Rain: CloudRain,
Drizzle: CloudDrizzle,
Thunderstorm: CloudLightning,
Snow: CloudSnow,
Mist: Wind,
Fog: Wind,
Haze: Wind,
}
interface WeatherIconProps {
main: string
size?: number
}
function WeatherIcon({ main, size = 13 }: WeatherIconProps) {
const Icon = WEATHER_ICON_MAP[main] || Cloud
return <Icon size={size} strokeWidth={1.8} />
}
function getWeatherCache(key) {
try {
const raw = sessionStorage.getItem(key)
if (raw === null) return undefined
return JSON.parse(raw)
} catch { return undefined }
}
function setWeatherCache(key, value) {
try { sessionStorage.setItem(key, JSON.stringify(value)) } catch {}
}
interface WeatherWidgetProps {
lat: number | null
lng: number | null
date: string
compact?: boolean
}
export default function WeatherWidget({ lat, lng, date, compact = false }: WeatherWidgetProps) {
const [weather, setWeather] = useState(null)
const [loading, setLoading] = useState(false)
const [failed, setFailed] = useState(false)
const isFahrenheit = useSettingsStore(s => s.settings.temperature_unit) === 'fahrenheit'
useEffect(() => {
if (!lat || !lng || !date) return
const rLat = Math.round(lat * 100) / 100
const rLng = Math.round(lng * 100) / 100
const cacheKey = `weather_${rLat}_${rLng}_${date}`
const cached = getWeatherCache(cacheKey)
if (cached !== undefined) {
if (cached === null) setFailed(true)
// Climate data: use from cache but re-fetch in background to upgrade to forecast
else if (cached.type === 'climate') {
setWeather(cached)
fetchWeather(lat, lng, date)
.then(data => {
if (!data.error && data.temp !== undefined && data.type === 'forecast') {
setWeatherCache(cacheKey, data)
setWeather(data)
}
})
.catch(() => {})
return
} else {
setWeather(cached)
return
}
return
}
setLoading(true)
fetchWeather(lat, lng, date)
.then(data => {
if (data.error || data.temp === undefined) {
setFailed(true)
} else {
setWeatherCache(cacheKey, data)
setWeather(data)
}
})
.catch(() => { setFailed(true) })
.finally(() => setLoading(false))
}, [lat, lng, date])
if (!lat || !lng) return null
const fontStyle = { fontFamily: "-apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif" }
if (loading) {
return (
<span style={{ fontSize: 11, color: '#d1d5db', ...fontStyle }}></span>
)
}
if (failed || !weather) {
return (
<span style={{ fontSize: 11, color: '#9ca3af', ...fontStyle }}></span>
)
}
const rawTemp = weather.temp
const temp = rawTemp !== undefined ? Math.round(isFahrenheit ? rawTemp * 9/5 + 32 : rawTemp) : null
const unit = isFahrenheit ? '°F' : '°C'
const isClimate = weather.type === 'climate'
if (compact) {
return (
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 3, fontSize: 11, color: isClimate ? '#a1a1aa' : '#6b7280', ...fontStyle }}>
<WeatherIcon main={weather.main} size={12} />
{temp !== null && <span>{isClimate ? 'Ø ' : ''}{temp}{unit}</span>}
</span>
)
}
return (
<div style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 13, color: isClimate ? '#71717a' : '#374151', background: 'rgba(0,0,0,0.04)', borderRadius: 8, padding: '5px 10px', ...fontStyle }}>
<WeatherIcon main={weather.main} size={15} />
{temp !== null && <span style={{ fontWeight: 500 }}>{isClimate ? 'Ø ' : ''}{temp}{unit}</span>}
{weather.description && <span style={{ fontSize: 11, color: '#9ca3af', textTransform: 'capitalize' }}>{weather.description}</span>}
</div>
)
}