{
),
);
seedStore(useAuthStore, { user: currentUser, isAuthenticated: true });
- seedStore(useTripStore, { trip: buildTrip({ id: 1, owner_id: 1 }) });
+ seedStore(useTripStore, { trip: buildTrip({ id: 1, user_id: 1 }) });
});
describe('CollabPolls', () => {
diff --git a/client/src/components/Collab/CollabPolls.tsx b/client/src/components/Collab/CollabPolls.tsx
index 79528518..ad09e084 100644
--- a/client/src/components/Collab/CollabPolls.tsx
+++ b/client/src/components/Collab/CollabPolls.tsx
@@ -79,7 +79,7 @@ function CreatePollModal({ onClose, onCreate, t }: CreatePollModalProps) {
if (!canSubmit) return
setSubmitting(true)
try {
- await onCreate({ question: question.trim(), options: options.filter(o => o.trim()), multiple_choice: multiChoice })
+ await onCreate({ question: question.trim(), options: options.filter(o => o.trim()), multi_choice: multiChoice })
onClose()
} catch {} finally { setSubmitting(false) }
}
@@ -231,7 +231,7 @@ function PollCard({ poll, currentUser, canEdit, onVote, onClose, onDelete, t }:
{remaining}
)}
- {poll.multiple_choice && (
+ {poll.multi_choice && (
{t('collab.polls.multiChoice')}
@@ -306,7 +306,7 @@ function PollCard({ poll, currentUser, canEdit, onVote, onClose, onDelete, t }:
flex: 1, fontSize: 13, fontWeight: myVote || isWinner ? 600 : 400,
color: 'var(--text-primary)', position: 'relative', zIndex: 1,
}}>
- {typeof opt === 'string' ? opt : opt.label || opt}
+ {typeof opt === 'string' ? opt : opt.text}
{/* Voter avatars */}
diff --git a/client/src/components/Collab/WhatsNextWidget.tsx b/client/src/components/Collab/WhatsNextWidget.tsx
index 90d39caf..4c23233b 100644
--- a/client/src/components/Collab/WhatsNextWidget.tsx
+++ b/client/src/components/Collab/WhatsNextWidget.tsx
@@ -30,6 +30,7 @@ function formatDayLabel(date, t, locale) {
interface TripMember {
id: number
username: string
+ avatar?: string | null
avatar_url?: string | null
}
diff --git a/client/src/components/Files/FileManager.test.tsx b/client/src/components/Files/FileManager.test.tsx
index 4db7b40c..323589e4 100644
--- a/client/src/components/Files/FileManager.test.tsx
+++ b/client/src/components/Files/FileManager.test.tsx
@@ -322,8 +322,8 @@ describe('FileManager', () => {
it('FE-COMP-FILEMANAGER-018: starred filter shows only starred files', async () => {
const files = [
- buildFile({ id: 1, original_name: 'starred.pdf', starred: true }),
- buildFile({ id: 2, original_name: 'normal.pdf', starred: false }),
+ buildFile({ id: 1, original_name: 'starred.pdf', starred: 1 }),
+ buildFile({ id: 2, original_name: 'normal.pdf', starred: 0 }),
];
render(
);
const user = userEvent.setup();
diff --git a/client/src/components/Files/FileManager.tsx b/client/src/components/Files/FileManager.tsx
index 00297ffd..bbf71f96 100644
--- a/client/src/components/Files/FileManager.tsx
+++ b/client/src/components/Files/FileManager.tsx
@@ -249,13 +249,13 @@ interface FileManagerProps {
files?: TripFile[]
onUpload: (fd: FormData) => Promise
onDelete: (fileId: number) => Promise
- onUpdate: (fileId: number, data: Partial) => Promise
+ onUpdate?: (fileId: number, data: Partial) => Promise
places: Place[]
days?: Day[]
assignments?: AssignmentsMap
reservations?: Reservation[]
tripId: number
- allowedFileTypes: Record
+ allowedFileTypes?: string | null
}
/**
@@ -368,11 +368,11 @@ function useFileManager({ files = [], onUpload, onDelete, onUpdate, places, days
noClick: false,
})
- const handlePaste = useCallback((e) => {
+ const handlePaste = useCallback((e: React.ClipboardEvent) => {
if (!can('file_upload', trip)) return
const items = e.clipboardData?.items
if (!items) return
- const pastedFiles = []
+ const pastedFiles: File[] = []
for (const item of Array.from(items)) {
if (item.kind === 'file') {
const file = item.getAsFile()
diff --git a/client/src/components/Layout/Navbar.tsx b/client/src/components/Layout/Navbar.tsx
index d6897aae..0f2dc59a 100644
--- a/client/src/components/Layout/Navbar.tsx
+++ b/client/src/components/Layout/Navbar.tsx
@@ -13,7 +13,7 @@ const ADDON_ICONS: Record = { CalendarDays, Briefcase, Globe
interface NavbarProps {
tripTitle?: string
- tripId?: string
+ tripId?: number | string
onBack?: () => void
showBack?: boolean
onShare?: () => void
diff --git a/client/src/components/Map/MapView.tsx b/client/src/components/Map/MapView.tsx
index affc2da8..368f4c67 100644
--- a/client/src/components/Map/MapView.tsx
+++ b/client/src/components/Map/MapView.tsx
@@ -19,8 +19,9 @@ function categoryIconSvg(iconName: string | null | undefined, size: number): str
}
import type { Place } from '../../types'
-// Fix default marker icons for vite
-delete L.Icon.Default.prototype._getIconUrl
+// Fix default marker icons for vite. `_getIconUrl` is a Leaflet-internal field
+// not present in the public typings, so narrow to delete it.
+delete (L.Icon.Default.prototype as { _getIconUrl?: unknown })._getIconUrl
L.Icon.Default.mergeOptions({
iconRetinaUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/images/marker-icon-2x.png',
iconUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/images/marker-icon.png',
@@ -121,7 +122,7 @@ interface SelectionControllerProps {
places: Place[]
selectedPlaceId: number | null
dayPlaces: Place[]
- paddingOpts: Record
+ paddingOpts: L.FitBoundsOptions
}
function SelectionController({ places, selectedPlaceId, dayPlaces, paddingOpts }: SelectionControllerProps) {
@@ -166,7 +167,7 @@ interface BoundsControllerProps {
hasDayDetail?: boolean
places: Place[]
fitKey: number
- paddingOpts: Record
+ paddingOpts: L.FitBoundsOptions
}
function BoundsController({ places, fitKey, paddingOpts, hasDayDetail }: BoundsControllerProps) {
@@ -210,7 +211,7 @@ function MapClickHandler({ onClick }: MapClickHandlerProps) {
useEffect(() => {
if (!onClick) return
map.on('click', onClick)
- return () => map.off('click', onClick)
+ return () => { map.off('click', onClick) }
}, [map, onClick])
return null
}
@@ -220,7 +221,7 @@ function MapContextMenuHandler({ onContextMenu }: { onContextMenu: ((e: L.Leafle
useEffect(() => {
if (!onContextMenu) return
map.on('contextmenu', onContextMenu)
- return () => map.off('contextmenu', onContextMenu)
+ return () => { map.off('contextmenu', onContextMenu) }
}, [map, onContextMenu])
return null
}
@@ -362,7 +363,7 @@ export const MapView = memo(function MapView({
return reservations.filter((r: Reservation) => set.has(r.id))
}, [reservations, visibleConnectionIds])
// Dynamic padding: account for sidebars + bottom inspector + day detail panel
- const paddingOpts = useMemo(() => {
+ const paddingOpts = useMemo((): L.FitBoundsOptions => {
const isMobile = typeof window !== 'undefined' && window.innerWidth < 768
if (isMobile) return { padding: [40, 20] }
const top = 60
diff --git a/client/src/components/Map/MapViewGL.tsx b/client/src/components/Map/MapViewGL.tsx
index 0d9d1c6f..73f50f1c 100644
--- a/client/src/components/Map/MapViewGL.tsx
+++ b/client/src/components/Map/MapViewGL.tsx
@@ -313,7 +313,9 @@ export function MapViewGL({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const curAlt = (ll as any).alt ?? 0
if (Math.abs(curAlt - alt) > 0.25) {
- marker.setLngLat([ll.lng, ll.lat, alt])
+ // mapbox-gl accepts a third altitude element at runtime, but its typings
+ // only model the 2-tuple form, so cast to LngLatLike.
+ marker.setLngLat([ll.lng, ll.lat, alt] as unknown as mapboxgl.LngLatLike)
}
})
}
diff --git a/client/src/components/PDF/JourneyBookPDF.tsx b/client/src/components/PDF/JourneyBookPDF.tsx
index 97f30388..21fa9699 100644
--- a/client/src/components/PDF/JourneyBookPDF.tsx
+++ b/client/src/components/PDF/JourneyBookPDF.tsx
@@ -73,7 +73,7 @@ function renderPhotoBlock(photos: JourneyPhoto[]): string {
}
export async function downloadJourneyBookPDF(journey: JourneyDetail) {
- const entries = (journey.entries || []).filter(e => e.type !== 'skeleton' && e.type !== 'gallery')
+ const entries = (journey.entries || []).filter(e => e.type !== 'skeleton')
const allPhotos = entries.flatMap(e => e.photos || [])
const coverUrl = journey.cover_image ? abs(`/uploads/${journey.cover_image}`) : (allPhotos[0] ? pSrc(allPhotos[0]) : '')
diff --git a/client/src/components/PDF/TripPDF.tsx b/client/src/components/PDF/TripPDF.tsx
index dc4016e9..cf586150 100644
--- a/client/src/components/PDF/TripPDF.tsx
+++ b/client/src/components/PDF/TripPDF.tsx
@@ -93,17 +93,19 @@ function dayCost(assignments, dayId, locale) {
}
// Pre-fetch Google Place photos for all assigned places
-async function fetchPlacePhotos(assignments) {
+async function fetchPlacePhotos(assignments: AssignmentsMap) {
const photoMap = {} // placeId → photoUrl
const allPlaces = Object.values(assignments).flatMap(a => a.map(x => x.place)).filter(Boolean)
const unique = [...new Map(allPlaces.map(p => [p.id, p])).values()]
- const toFetch = unique.filter(p => !p.image_url && (p.google_place_id || p.osm_id))
+ // Assignment places are a server-side projection that omits osm_id, so photo
+ // pre-fetch keys off the google_place_id that the projection does carry.
+ const toFetch = unique.filter(p => !p.image_url && p.google_place_id)
await Promise.allSettled(
toFetch.map(async (place) => {
try {
- const data = await mapsApi.placePhoto(place.google_place_id || place.osm_id, place.lat, place.lng, place.name)
+ const data = await mapsApi.placePhoto(place.google_place_id, place.lat, place.lng, place.name)
if (data.photoUrl) photoMap[place.id] = data.photoUrl
} catch {}
})
@@ -141,7 +143,7 @@ export async function downloadTripPDF({ trip, days, places, assignments, categor
Object.values(assignments || {}).flatMap(a => a.map(x => x.place?.id)).filter(Boolean)
).size
const totalCost = Object.values(assignments || {})
- .flatMap(a => a).reduce((s, a) => s + (parseFloat(a.place?.price) || 0), 0)
+ .flatMap(a => a).reduce((s, a) => s + (Number(a.place?.price) || 0), 0)
// Span helpers for multi-day transport (mirrors DayPlanSidebar logic)
const pdfGetDayOrder = (d: Day) => d.day_number
@@ -575,6 +577,8 @@ ${daysHtml}
overlay.appendChild(card)
document.body.appendChild(overlay)
- header.querySelector('#pdf-close-btn').onclick = () => overlay.remove()
- header.querySelector('#pdf-print-btn').onclick = () => { iframe.contentWindow?.print() }
+ const closeBtn = header.querySelector('#pdf-close-btn')
+ if (closeBtn) closeBtn.onclick = () => overlay.remove()
+ const printBtn = header.querySelector('#pdf-print-btn')
+ if (printBtn) printBtn.onclick = () => { iframe.contentWindow?.print() }
}
diff --git a/client/src/components/Packing/PackingListPanel.tsx b/client/src/components/Packing/PackingListPanel.tsx
index a02c2a93..f3a43036 100644
--- a/client/src/components/Packing/PackingListPanel.tsx
+++ b/client/src/components/Packing/PackingListPanel.tsx
@@ -732,10 +732,10 @@ interface MenuItemProps {
icon: React.ReactNode
label: string
onClick: () => void
- danger: boolean
+ danger?: boolean
}
-function MenuItem({ icon, label, onClick, danger }: MenuItemProps) {
+function MenuItem({ icon, label, onClick, danger = false }: MenuItemProps) {
return (