|
|
@@ -0,0 +1,108 @@
|
|
|
+'use client'
|
|
|
+
|
|
|
+import { useEffect, useRef, useState, useCallback } from 'react'
|
|
|
+import type { VisitorDot } from '@/types'
|
|
|
+
|
|
|
+interface WSMessage {
|
|
|
+ type: string
|
|
|
+ data: { visitors?: VisitorDot[] }
|
|
|
+}
|
|
|
+
|
|
|
+const INITIAL_RECONNECT_DELAY = 1000
|
|
|
+const MAX_RECONNECT_DELAY = 30000
|
|
|
+
|
|
|
+export function useWebSocket(enabled: boolean) {
|
|
|
+ const [visitors, setVisitors] = useState<VisitorDot[]>([])
|
|
|
+ const [connected, setConnected] = useState(false)
|
|
|
+ const wsRef = useRef<WebSocket | null>(null)
|
|
|
+ const reconnectTimerRef = useRef<ReturnType<typeof setTimeout>>()
|
|
|
+ const reconnectAttemptRef = useRef(0)
|
|
|
+ const enabledRef = useRef(enabled)
|
|
|
+ enabledRef.current = enabled
|
|
|
+
|
|
|
+ const sendPosition = useCallback(() => {
|
|
|
+ if (!navigator.geolocation) return
|
|
|
+
|
|
|
+ navigator.geolocation.getCurrentPosition(
|
|
|
+ (pos) => {
|
|
|
+ const ws = wsRef.current
|
|
|
+ if (ws && ws.readyState === WebSocket.OPEN) {
|
|
|
+ ws.send(JSON.stringify({
|
|
|
+ type: 'visitor_update',
|
|
|
+ data: { lat: pos.coords.latitude, lng: pos.coords.longitude },
|
|
|
+ }))
|
|
|
+ }
|
|
|
+ },
|
|
|
+ () => {},
|
|
|
+ { enableHighAccuracy: false, timeout: 5000, maximumAge: 60000 },
|
|
|
+ )
|
|
|
+ }, [])
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ if (!enabled) return
|
|
|
+
|
|
|
+ const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8080/api/v1'
|
|
|
+ const wsUrl = apiUrl.replace(/^http/, 'ws') + '/ws/visitors'
|
|
|
+
|
|
|
+ function connect() {
|
|
|
+ if (!enabledRef.current) return
|
|
|
+
|
|
|
+ const ws = new WebSocket(wsUrl)
|
|
|
+ wsRef.current = ws
|
|
|
+
|
|
|
+ ws.onopen = () => {
|
|
|
+ setConnected(true)
|
|
|
+ reconnectAttemptRef.current = 0
|
|
|
+ sendPosition()
|
|
|
+ }
|
|
|
+
|
|
|
+ ws.onmessage = (event) => {
|
|
|
+ try {
|
|
|
+ const msg: WSMessage = JSON.parse(event.data)
|
|
|
+ if (msg.type === 'visitors' && msg.data?.visitors) {
|
|
|
+ setVisitors(msg.data.visitors)
|
|
|
+ }
|
|
|
+ } catch { /* ignore malformed */ }
|
|
|
+ }
|
|
|
+
|
|
|
+ ws.onclose = () => {
|
|
|
+ setConnected(false)
|
|
|
+ wsRef.current = null
|
|
|
+ scheduleReconnect()
|
|
|
+ }
|
|
|
+
|
|
|
+ ws.onerror = () => {
|
|
|
+ ws.close()
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function scheduleReconnect() {
|
|
|
+ if (!enabledRef.current) return
|
|
|
+ const delay = Math.min(
|
|
|
+ INITIAL_RECONNECT_DELAY * Math.pow(2, reconnectAttemptRef.current),
|
|
|
+ MAX_RECONNECT_DELAY,
|
|
|
+ )
|
|
|
+ reconnectAttemptRef.current++
|
|
|
+ reconnectTimerRef.current = setTimeout(connect, delay)
|
|
|
+ }
|
|
|
+
|
|
|
+ connect()
|
|
|
+
|
|
|
+ return () => {
|
|
|
+ clearTimeout(reconnectTimerRef.current)
|
|
|
+ if (wsRef.current) {
|
|
|
+ wsRef.current.onclose = null
|
|
|
+ wsRef.current.close()
|
|
|
+ wsRef.current = null
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }, [enabled, sendPosition])
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ if (!connected) return
|
|
|
+ const interval = setInterval(sendPosition, 60000)
|
|
|
+ return () => clearInterval(interval)
|
|
|
+ }, [connected, sendPosition])
|
|
|
+
|
|
|
+ return { visitors, connected }
|
|
|
+}
|