86 lines
1.9 KiB
TypeScript
86 lines
1.9 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useMemo, useState } from 'react';
|
|
|
|
type OddsPayload = {
|
|
matchId: string;
|
|
market: string;
|
|
bookmaker: string;
|
|
odds: number;
|
|
};
|
|
|
|
type MatchEventPayload = {
|
|
matchId: string;
|
|
type: 'goal' | 'yellow' | 'red' | 'substitution';
|
|
minute: number;
|
|
detail: string;
|
|
};
|
|
|
|
type WSMessage =
|
|
| ({ eventType: 'odds'; payload: OddsPayload })
|
|
| ({ eventType: 'event'; payload: MatchEventPayload })
|
|
| Record<string, unknown>;
|
|
|
|
export function useLiveMatchData(matchId: string | null) {
|
|
const [odds, setOdds] = useState<OddsPayload[]>([]);
|
|
const [events, setEvents] = useState<MatchEventPayload[]>([]);
|
|
|
|
const wsBase = process.env.NEXT_PUBLIC_WS_URL || 'ws://localhost:8000/ws/matches';
|
|
|
|
useEffect(() => {
|
|
if (!matchId) {
|
|
return undefined;
|
|
}
|
|
|
|
let isMounted = true;
|
|
const ws = new WebSocket(`${wsBase}/${matchId}`);
|
|
|
|
ws.onopen = () => {
|
|
if (!isMounted) return;
|
|
ws.send(JSON.stringify({ action: 'subscribe', matchId }));
|
|
};
|
|
|
|
ws.onmessage = (event) => {
|
|
let message: WSMessage;
|
|
try {
|
|
message = JSON.parse(event.data);
|
|
} catch {
|
|
return;
|
|
}
|
|
|
|
if (message.eventType === 'odds') {
|
|
const payload = message.payload as OddsPayload;
|
|
setOdds((prev) => {
|
|
const next = [...prev.filter((item) => item.market !== payload.market), payload];
|
|
return next;
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (message.eventType === 'event') {
|
|
const payload = message.payload as MatchEventPayload;
|
|
setEvents((prev) => [payload, ...prev].slice(0, 80));
|
|
}
|
|
};
|
|
|
|
ws.onerror = () => {
|
|
console.error('WebSocket 錯誤', matchId);
|
|
};
|
|
|
|
return () => {
|
|
isMounted = false;
|
|
ws.close(1000);
|
|
};
|
|
}, [matchId, wsBase]);
|
|
|
|
const liveState = useMemo(
|
|
() => ({
|
|
odds,
|
|
events,
|
|
}),
|
|
[odds, events],
|
|
);
|
|
|
|
return liveState;
|
|
}
|