Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 67 additions & 52 deletions src/hooks/useLocations.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client"

import { useState, useEffect, useCallback } from "react"
import { useState, useEffect, useCallback, useRef } from "react"
import { useClient, useCampaign } from "@/contexts/AppContext"
import type { Location } from "@/types"

Expand Down Expand Up @@ -34,70 +34,85 @@ export function useLocations(fightId: string | undefined): UseLocationsResult {
const [locations, setLocations] = useState<Location[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
// Track fight_id from websocket payloads to reliably filter cross-fight updates.
const latestWsFightId = useRef<string | undefined>(fightId)

const fetchLocations = useCallback(async () => {
if (!fightId) {
setLocations([])
useEffect(() => {
latestWsFightId.current = fightId
}, [fightId])

const fetchLocations = useCallback(
async (showLoading: boolean = true) => {
if (!fightId) {
setLocations([])
setError(null)
setLoading(false)
return
}

if (showLoading) {
setLoading(true)
}
setError(null)
setLoading(false)
return
}

setLoading(true)
setError(null)

try {
const response = await client.getFightLocations(fightId)
setLocations(response.data.locations || [])
} catch (err) {
console.error("Error fetching locations:", err)
setError("Failed to load locations")
setLocations([])
} finally {
setLoading(false)
}
}, [fightId, client])

try {
const response = await client.getFightLocations(fightId)
setLocations(response.data.locations || [])
} catch (err) {
console.error("Error fetching locations:", err)
setError("Failed to load locations")
setLocations([])
} finally {
if (showLoading) {
setLoading(false)
}
}
},
[fightId, client]
)

// Initial fetch
useEffect(() => {
fetchLocations()
}, [fetchLocations])

// Subscribe to WebSocket updates for locations
// The locations array contains location objects with fight_id, so we can filter directly
// Subscribe to fight_id updates for precise fight scoping.
useEffect(() => {
if (!fightId) return

const unsubscribe = subscribeToEntity("locations", (data: unknown) => {
// Data is an array of Location objects
if (Array.isArray(data)) {
// Filter to only include locations for this fight
// Each location has a fight_id property we can use for verification
const locationsData = data as Location[]

// If locations array is empty, it might be for any fight - accept it
// If locations have fight_id, verify they're for this fight
const isForThisFight =
locationsData.length === 0 ||
locationsData.some(loc => loc.fight_id === fightId)

if (!isForThisFight) {
console.log(
"📍 [useLocations] Ignoring locations update for different fight"
)
return
}
const unsubscribe = subscribeToEntity("fight_id", (wsFightId: unknown) => {
if (typeof wsFightId === "string") {
latestWsFightId.current = wsFightId
}
})

console.log(
"📍 [useLocations] WebSocket locations update received for fight:",
fightId,
"with",
locationsData.length,
"locations"
)
// Update locations without setting loading=true to avoid flicker
setLocations(locationsData)
return unsubscribe
}, [fightId, subscribeToEntity])

// Subscribe to WebSocket updates for locations.
// Contract: "locations" payload is always Location[].
useEffect(() => {
if (!fightId) return

const unsubscribe = subscribeToEntity("locations", (data: unknown) => {
if (!Array.isArray(data)) {
console.warn("[useLocations] Ignoring invalid locations payload", data)
return
}

const locationsData = data as Location[]

const hasFightIds = locationsData.some(loc => !!loc.fight_id)
const isForThisFight = hasFightIds
? locationsData.some(loc => loc.fight_id === fightId)
: locationsData.length === 0
? !latestWsFightId.current || latestWsFightId.current === fightId
: true

if (!isForThisFight) return

// Update locations without loading state changes to avoid UI flicker.
setLocations(locationsData)
})

return unsubscribe
Expand Down
Loading