Files
TREK/client/src/store/slices/placesSlice.ts
Maurice 95cb81b0e5 perf: major trip planner performance overhaul (#218)
Store & re-render optimization:
- TripPlannerPage uses selective Zustand selectors instead of full store
- placesSlice only updates affected days on place update/delete
- Route calculation only reacts to selected day's assignments
- DayPlanSidebar uses stable action refs instead of full store

Map marker performance:
- Shared photoService for PlaceAvatar and MapView (single cache, no duplicate requests)
- Client-side base64 thumbnail generation via canvas (CORS-safe for Wikimedia)
- Map markers use base64 data URL <img> tags for smooth zoom (no external image decode)
- Sidebar uses same base64 thumbnails with IntersectionObserver for visible-first loading
- Icon cache prevents duplicate L.divIcon creation
- MarkerClusterGroup with animate:false and optimized chunk settings
- Photo fetch deduplication and batched state updates

Server optimizations:
- Wikimedia image size reduced to 400px (from 600px)
- Photo cache: 5min TTL for errors (was 12h), prevents stale 404 caching
- Removed unused image-proxy endpoint

UX improvements:
- Splash screen with plane animation during initial photo preload
- Markdown rendering in DayPlanSidebar place descriptions
- Missing i18n keys added, all 12 languages synced to 1376 keys
2026-04-01 14:56:01 +02:00

84 lines
3.0 KiB
TypeScript

import { placesApi } from '../../api/client'
import type { StoreApi } from 'zustand'
import type { TripStoreState } from '../tripStore'
import type { Place, Assignment } from '../../types'
import { getApiErrorMessage } from '../../types'
type SetState = StoreApi<TripStoreState>['setState']
type GetState = StoreApi<TripStoreState>['getState']
export interface PlacesSlice {
refreshPlaces: (tripId: number | string) => Promise<void>
addPlace: (tripId: number | string, placeData: Partial<Place>) => Promise<Place>
updatePlace: (tripId: number | string, placeId: number, placeData: Partial<Place>) => Promise<Place>
deletePlace: (tripId: number | string, placeId: number) => Promise<void>
}
export const createPlacesSlice = (set: SetState, get: GetState): PlacesSlice => ({
refreshPlaces: async (tripId) => {
try {
const data = await placesApi.list(tripId)
set({ places: data.places })
} catch (err: unknown) {
console.error('Failed to refresh places:', err)
}
},
addPlace: async (tripId, placeData) => {
try {
const data = await placesApi.create(tripId, placeData)
set(state => ({ places: [data.place, ...state.places] }))
return data.place
} catch (err: unknown) {
throw new Error(getApiErrorMessage(err, 'Error adding place'))
}
},
updatePlace: async (tripId, placeId, placeData) => {
try {
const data = await placesApi.update(tripId, placeId, placeData)
set(state => {
const updatedAssignments = { ...state.assignments }
let changed = false
for (const [dayId, items] of Object.entries(state.assignments)) {
if (items.some((a: Assignment) => a.place?.id === placeId)) {
updatedAssignments[dayId] = items.map((a: Assignment) =>
a.place?.id === placeId ? { ...a, place: { ...data.place, place_time: a.place.place_time, end_time: a.place.end_time } } : a
)
changed = true
}
}
return {
places: state.places.map(p => p.id === placeId ? data.place : p),
...(changed ? { assignments: updatedAssignments } : {}),
}
})
return data.place
} catch (err: unknown) {
throw new Error(getApiErrorMessage(err, 'Error updating place'))
}
},
deletePlace: async (tripId, placeId) => {
try {
await placesApi.delete(tripId, placeId)
set(state => {
const updatedAssignments = { ...state.assignments }
let changed = false
for (const [dayId, items] of Object.entries(state.assignments)) {
if (items.some((a: Assignment) => a.place?.id === placeId)) {
updatedAssignments[dayId] = items.filter((a: Assignment) => a.place?.id !== placeId)
changed = true
}
}
return {
places: state.places.filter(p => p.id !== placeId),
...(changed ? { assignments: updatedAssignments } : {}),
}
})
} catch (err: unknown) {
throw new Error(getApiErrorMessage(err, 'Error deleting place'))
}
},
})