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
84 lines
3.0 KiB
TypeScript
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'))
|
|
}
|
|
},
|
|
})
|