refactoring: TypeScript migration, security fixes,

This commit is contained in:
Maurice
2026-03-27 18:40:18 +01:00
parent 510475a46f
commit 8396a75223
150 changed files with 8116 additions and 8467 deletions

View File

@@ -0,0 +1,78 @@
import { useState, useRef } from 'react'
import { useTripStore } from '../store/tripStore'
import { useToast } from '../components/shared/Toast'
import type { MergedItem, DayNotesMap, DayNote } from '../types'
interface NoteUiState {
mode: 'add' | 'edit'
noteId?: number
text: string
time: string
icon: string
sortOrder?: number
}
interface NoteUiMap {
[dayId: string]: NoteUiState
}
export function useDayNotes(tripId: number | string) {
const [noteUi, setNoteUi] = useState<NoteUiMap>({})
const noteInputRef = useRef<HTMLInputElement | null>(null)
const tripStore = useTripStore()
const toast = useToast()
const dayNotes: DayNotesMap = tripStore.dayNotes || {}
const openAddNote = (dayId: number, getMergedItems: (dayId: number) => MergedItem[], expandDay?: (dayId: number) => void) => {
const merged = getMergedItems(dayId)
const maxKey = merged.length > 0 ? Math.max(...merged.map((i) => i.sortKey)) : -1
setNoteUi((prev) => ({ ...prev, [dayId]: { mode: 'add', text: '', time: '', icon: 'FileText', sortOrder: maxKey + 1 } }))
expandDay?.(dayId)
setTimeout(() => noteInputRef.current?.focus(), 50)
}
const openEditNote = (dayId: number, note: DayNote) => {
setNoteUi((prev) => ({ ...prev, [dayId]: { mode: 'edit', noteId: note.id, text: note.text, time: note.time || '', icon: note.icon || 'FileText' } }))
setTimeout(() => noteInputRef.current?.focus(), 50)
}
const cancelNote = (dayId: number) => {
setNoteUi((prev) => { const n = { ...prev }; delete n[dayId]; return n })
}
const saveNote = async (dayId: number) => {
const ui = noteUi[dayId]
if (!ui?.text?.trim()) return
try {
if (ui.mode === 'add') {
await tripStore.addDayNote(tripId, dayId, { text: ui.text.trim(), time: ui.time || null, icon: ui.icon || 'FileText', sort_order: ui.sortOrder })
} else {
await tripStore.updateDayNote(tripId, dayId, ui.noteId!, { text: ui.text.trim(), time: ui.time || null, icon: ui.icon || 'FileText' })
}
cancelNote(dayId)
} catch (err: unknown) { toast.error(err instanceof Error ? err.message : 'Unknown error') }
}
const deleteNote = async (dayId: number, noteId: number) => {
try { await tripStore.deleteDayNote(tripId, dayId, noteId) }
catch (err: unknown) { toast.error(err instanceof Error ? err.message : 'Unknown error') }
}
const moveNote = async (dayId: number, noteId: number, direction: 'up' | 'down', getMergedItems: (dayId: number) => MergedItem[]) => {
const merged = getMergedItems(dayId)
const idx = merged.findIndex((i) => i.type === 'note' && (i.data as DayNote).id === noteId)
if (idx === -1) return
let newSortOrder: number
if (direction === 'up') {
if (idx === 0) return
newSortOrder = idx >= 2 ? (merged[idx - 2].sortKey + merged[idx - 1].sortKey) / 2 : merged[idx - 1].sortKey - 1
} else {
if (idx >= merged.length - 1) return
newSortOrder = idx < merged.length - 2 ? (merged[idx + 1].sortKey + merged[idx + 2].sortKey) / 2 : merged[idx + 1].sortKey + 1
}
try { await tripStore.updateDayNote(tripId, dayId, noteId, { sort_order: newSortOrder }) }
catch (err: unknown) { toast.error(err instanceof Error ? err.message : 'Unknown error') }
}
return { noteUi, setNoteUi, noteInputRef, dayNotes, openAddNote, openEditNote, cancelNote, saveNote, deleteNote, moveNote }
}

View File

@@ -0,0 +1,18 @@
import { useState, useCallback } from 'react'
export function usePlaceSelection() {
const [selectedPlaceId, _setSelectedPlaceId] = useState<number | null>(null)
const [selectedAssignmentId, setSelectedAssignmentId] = useState<number | null>(null)
const setSelectedPlaceId = useCallback((placeId: number | null) => {
_setSelectedPlaceId(placeId)
setSelectedAssignmentId(null)
}, [])
const selectAssignment = useCallback((assignmentId: number | null, placeId: number | null) => {
setSelectedAssignmentId(assignmentId)
_setSelectedPlaceId(placeId)
}, [])
return { selectedPlaceId, selectedAssignmentId, setSelectedPlaceId, selectAssignment }
}

View File

@@ -0,0 +1,45 @@
import { useState, useEffect, useRef } from 'react'
const MIN_SIDEBAR = 200
const MAX_SIDEBAR = 520
export function useResizablePanels() {
const [leftWidth, setLeftWidth] = useState<number>(() => parseInt(localStorage.getItem('sidebarLeftWidth') || '') || 340)
const [rightWidth, setRightWidth] = useState<number>(() => parseInt(localStorage.getItem('sidebarRightWidth') || '') || 300)
const [leftCollapsed, setLeftCollapsed] = useState(false)
const [rightCollapsed, setRightCollapsed] = useState(false)
const isResizingLeft = useRef(false)
const isResizingRight = useRef(false)
useEffect(() => {
const onMove = (e: MouseEvent) => {
if (isResizingLeft.current) {
const w = Math.max(MIN_SIDEBAR, Math.min(MAX_SIDEBAR, e.clientX - 10))
setLeftWidth(w)
localStorage.setItem('sidebarLeftWidth', String(w))
}
if (isResizingRight.current) {
const w = Math.max(MIN_SIDEBAR, Math.min(MAX_SIDEBAR, window.innerWidth - e.clientX - 10))
setRightWidth(w)
localStorage.setItem('sidebarRightWidth', String(w))
}
}
const onUp = () => {
isResizingLeft.current = false
isResizingRight.current = false
document.body.style.cursor = ''
document.body.style.userSelect = ''
}
document.addEventListener('mousemove', onMove)
document.addEventListener('mouseup', onUp)
return () => {
document.removeEventListener('mousemove', onMove)
document.removeEventListener('mouseup', onUp)
}
}, [])
const startResizeLeft = () => { isResizingLeft.current = true; document.body.style.cursor = 'col-resize'; document.body.style.userSelect = 'none' }
const startResizeRight = () => { isResizingRight.current = true; document.body.style.cursor = 'col-resize'; document.body.style.userSelect = 'none' }
return { leftWidth, rightWidth, leftCollapsed, rightCollapsed, setLeftCollapsed, setRightCollapsed, startResizeLeft, startResizeRight }
}

View File

@@ -0,0 +1,44 @@
import { useState, useCallback, useRef, useEffect } from 'react'
import { useSettingsStore } from '../store/settingsStore'
import { calculateSegments } from '../components/Map/RouteCalculator'
import type { TripStoreState } from '../store/tripStore'
import type { RouteSegment, RouteResult } from '../types'
/**
* Manages route calculation state for a selected day. Extracts geo-coded waypoints from
* day assignments, draws a straight-line route, and optionally fetches per-segment
* driving/walking durations via OSRM. Aborts in-flight requests when the day changes.
*/
export function useRouteCalculation(tripStore: TripStoreState, selectedDayId: number | null) {
const [route, setRoute] = useState<[number, number][] | null>(null)
const [routeInfo, setRouteInfo] = useState<RouteResult | null>(null)
const [routeSegments, setRouteSegments] = useState<RouteSegment[]>([])
const routeCalcEnabled = useSettingsStore((s) => s.settings.route_calculation) !== false
const routeAbortRef = useRef<AbortController | null>(null)
const updateRouteForDay = useCallback(async (dayId: number | null) => {
if (routeAbortRef.current) routeAbortRef.current.abort()
if (!dayId) { setRoute(null); setRouteSegments([]); return }
const da = (tripStore.assignments[String(dayId)] || []).slice().sort((a, b) => a.order_index - b.order_index)
const waypoints = da.map((a) => a.place).filter((p) => p?.lat && p?.lng)
if (waypoints.length < 2) { setRoute(null); setRouteSegments([]); return }
setRoute(waypoints.map((p) => [p.lat!, p.lng!]))
if (!routeCalcEnabled) { setRouteSegments([]); return }
const controller = new AbortController()
routeAbortRef.current = controller
try {
const segments = await calculateSegments(waypoints as { lat: number; lng: number }[], { signal: controller.signal })
if (!controller.signal.aborted) setRouteSegments(segments)
} catch (err: unknown) {
if (err instanceof Error && err.name !== 'AbortError') setRouteSegments([])
else if (!(err instanceof Error)) setRouteSegments([])
}
}, [tripStore, routeCalcEnabled])
useEffect(() => {
if (!selectedDayId) { setRoute(null); setRouteSegments([]); return }
updateRouteForDay(selectedDayId)
}, [selectedDayId, tripStore.assignments])
return { route, routeSegments, routeInfo, setRoute, setRouteInfo, updateRouteForDay }
}

View File

@@ -0,0 +1,29 @@
import { useEffect } from 'react'
import { useTripStore } from '../store/tripStore'
import { joinTrip, leaveTrip, addListener, removeListener } from '../api/websocket'
import type { WebSocketEvent } from '../types'
export function useTripWebSocket(tripId: number | string | undefined) {
const tripStore = useTripStore()
useEffect(() => {
if (!tripId) return
const handler = useTripStore.getState().handleRemoteEvent
joinTrip(tripId)
addListener(handler)
const collabFileSync = (event: WebSocketEvent) => {
if (event?.type === 'collab:note:deleted' || event?.type === 'collab:note:updated') {
tripStore.loadFiles?.(tripId)
}
}
addListener(collabFileSync)
const localFileSync = () => tripStore.loadFiles?.(tripId)
window.addEventListener('collab-files-changed', localFileSync)
return () => {
leaveTrip(tripId)
removeListener(handler)
removeListener(collabFileSync)
window.removeEventListener('collab-files-changed', localFileSync)
}
}, [tripId])
}