From c4e07f661fecd4102345bec077bf22b53f085ca4 Mon Sep 17 00:00:00 2001 From: WA11AX Date: Thu, 14 Aug 2025 01:14:09 +0300 Subject: [PATCH 1/4] feat: add websocket and telegram services --- client/README.md | 21 +++ client/src/App.tsx | 20 +-- client/src/lib/telegram.ts | 176 +----------------------- client/src/lib/websocket.ts | 153 +-------------------- client/src/pages/tournament-detail.tsx | 10 +- client/src/pages/tournaments.tsx | 35 ++--- client/src/services/telegram.tsx | 179 +++++++++++++++++++++++++ client/src/services/websocket.tsx | 147 ++++++++++++++++++++ 8 files changed, 382 insertions(+), 359 deletions(-) create mode 100644 client/src/services/telegram.tsx create mode 100644 client/src/services/websocket.tsx diff --git a/client/README.md b/client/README.md index 6b008e7..aada96e 100644 --- a/client/README.md +++ b/client/README.md @@ -9,6 +9,7 @@ src/ ├── components/ # Reusable UI components ├── hooks/ # Custom React hooks ├── lib/ # Utility libraries and configurations +├── services/ # Shared WebSocket and Telegram services ├── pages/ # Page components and routing ├── types/ # TypeScript type definitions ├── App.tsx # Main application component @@ -36,6 +37,26 @@ The application uses a modern component library built on: - **WebSocket**: Live tournament updates - **Telegram SDK**: Integration with Telegram Web App +### Services + +Shared logic for WebSocket connections and Telegram SDK is centralized in `src/services` and exposed via React contexts: + +```tsx +import { TelegramProvider, useTelegram } from '@/services/telegram'; +import { WebSocketProvider, useWebSocketService } from '@/services/websocket'; + +// Wrap your app + + + + +; + +// Inside components +const { getAuthHeaders, processStarsPayment } = useTelegram(); +const { addCallback } = useWebSocketService(); +``` + ### Routing - **Wouter**: Lightweight React router diff --git a/client/src/App.tsx b/client/src/App.tsx index 96fc2e1..8840aed 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,12 +1,12 @@ import { QueryClientProvider } from '@tanstack/react-query'; -import { useEffect } from 'react'; import { Switch, Route } from 'wouter'; import { queryClient } from './lib/queryClient'; import { Toaster } from '@/components/ui/toaster'; import { TooltipProvider } from '@/components/ui/tooltip'; -import { initTelegram } from '@/lib/telegram'; +import { TelegramProvider } from '@/services/telegram'; +import { WebSocketProvider } from '@/services/websocket'; import AdminPage from '@/pages/admin'; import NotFound from '@/pages/not-found'; import TournamentDetailPage from '@/pages/tournament-detail'; @@ -24,17 +24,17 @@ function Router() { } function App() { - useEffect(() => { - initTelegram(); - }, []); - return ( -
- - -
+ + +
+ + +
+
+
); diff --git a/client/src/lib/telegram.ts b/client/src/lib/telegram.ts index e8fbb42..86123f1 100644 --- a/client/src/lib/telegram.ts +++ b/client/src/lib/telegram.ts @@ -1,175 +1 @@ -// Use the global Telegram WebApp object from types/telegram.ts -import type { TelegramWebApp } from '../types/telegram'; - -let webApp: TelegramWebApp | null = null; - -export function initTelegram() { - try { - console.log('Initializing Telegram Web App'); - - // Get the WebApp from window object - const WebApp = window.Telegram?.WebApp; - - if (!WebApp) { - console.warn('Telegram WebApp not available'); - return false; - } - - // Проверяем, что мы внутри Telegram - if (!WebApp.initDataUnsafe?.user && process.env.NODE_ENV === 'production') { - console.warn('Not running inside Telegram Web App'); - return false; - } - - WebApp.ready(); - WebApp.expand(); - - // Настройка цветовой схемы - these methods might not exist outside Telegram - if (WebApp.setHeaderColor) { - WebApp.setHeaderColor('#1f2937'); - } - if (WebApp.setBottomBarColor) { - WebApp.setBottomBarColor('#ffffff'); - } - - // Скрываем главную кнопку по умолчанию - if (WebApp.MainButton) { - WebApp.MainButton.hide(); - } - - webApp = WebApp; - - return true; - } catch (error) { - console.error('Failed to initialize Telegram Web App:', error); - return false; - } -} - -export function getTelegramWebApp(): TelegramWebApp | null { - // Fallback for development/testing if webApp is not initialized but WebApp SDK is available - if (!webApp && window?.Telegram?.WebApp) { - webApp = window.Telegram.WebApp; - webApp.ready(); - webApp.expand(); - } - return webApp; -} - -export function getTelegramUser() { - try { - return window.Telegram?.WebApp?.initDataUnsafe?.user || null; - } catch (error) { - console.error('Failed to get Telegram user:', error); - return null; - } -} - -export function showMainButton(text: string, onClick: () => void) { - try { - const WebApp = window.Telegram?.WebApp; - if (WebApp?.MainButton) { - WebApp.MainButton.setText(text); - WebApp.MainButton.show(); - WebApp.MainButton.onClick(onClick); - } - } catch (error) { - console.error('Failed to show main button:', error); - } -} - -export function hideMainButton() { - try { - const WebApp = window.Telegram?.WebApp; - if (WebApp?.MainButton) { - WebApp.MainButton.hide(); - } - } catch (error) { - console.error('Failed to hide main button:', error); - } -} - -export function showBackButton(onClick: () => void) { - try { - const WebApp = window.Telegram?.WebApp; - if (WebApp?.BackButton) { - WebApp.BackButton.onClick(onClick); - WebApp.BackButton.show(); - } - } catch (error) { - console.error('Failed to show back button:', error); - } -} - -export function hideBackButton() { - try { - const WebApp = window.Telegram?.WebApp; - if (WebApp?.BackButton) { - WebApp.BackButton.hide(); - } - } catch (error) { - console.error('Failed to hide back button:', error); - } -} - -export function processStarsPayment(amount: number, tournamentId: string): Promise { - return new Promise((resolve) => { - const WebApp = window.Telegram?.WebApp; - if (WebApp?.openInvoice) { - // In a real implementation, this would use Telegram's payment API - // For now, we simulate the payment process - const invoiceUrl = `https://t.me/invoice/stars?amount=${amount}&payload=${tournamentId}`; - - WebApp.openInvoice(invoiceUrl, (status: string) => { - resolve(status === 'paid'); - }); - } else { - // Fallback for development/testing - const confirmed = window.confirm(`Pay ${amount} Telegram Stars to join tournament?`); - resolve(confirmed); - } - }); -} - -export function getAuthHeaders(): Record { - const initData = window.Telegram?.WebApp?.initData; - if (!initData) { - return {}; - } - - return { - 'x-telegram-init-data': initData, - }; -} - -export function showAlert(message: string) { - try { - const WebApp = window.Telegram?.WebApp; - if (WebApp?.showAlert) { - WebApp.showAlert(message); - } else { - // Fallback to browser alert - alert(message); - } - } catch (error) { - console.error('Failed to show alert:', error); - alert(message); - } -} - -export function hapticFeedback(type: 'impact' | 'notification' | 'selection' = 'impact') { - try { - const WebApp = window.Telegram?.WebApp; - if (!WebApp?.HapticFeedback) return; - - if (type === 'impact') { - WebApp.HapticFeedback.impactOccurred('medium'); - } else if (type === 'notification') { - WebApp.HapticFeedback.notificationOccurred('success'); - } else { - WebApp.HapticFeedback.selectionChanged(); - } - } catch (error) { - console.error('Haptic feedback failed:', error); - } -} +export * from '../services/telegram'; diff --git a/client/src/lib/websocket.ts b/client/src/lib/websocket.ts index f8bffb3..6488d24 100644 --- a/client/src/lib/websocket.ts +++ b/client/src/lib/websocket.ts @@ -1,152 +1 @@ -import { useCallback, useEffect, useRef, useState } from 'react'; - -export interface WebSocketMessage { - type: - | 'tournament_created' - | 'tournament_updated' - | 'tournament_deleted' - | 'tournament_registration' - | 'tournament_unregistration'; - tournament?: any; - tournamentId?: string; - userId?: string; -} - -export function useWebSocket(onMessage?: (message: WebSocketMessage) => void) { - const [isConnected, setIsConnected] = useState(false); - const wsRef = useRef(null); - const reconnectTimeoutRef = useRef(); - const websocketCallbacks = useRef<((message: any) => void)[]>([]); // Use a ref to hold the callbacks - - // Function to add callbacks - const addCallback = (callback: (message: any) => void) => { - websocketCallbacks.current.push(callback); - // Ensure initial connection status is reflected if already connected - if (isConnected) { - callback({ type: 'connected' }); - } - return () => { - // Remove callback on cleanup - websocketCallbacks.current = websocketCallbacks.current.filter((cb) => cb !== callback); - }; - }; - - let reconnectAttempts = 0; - const maxReconnectAttempts = 5; - const baseReconnectDelay = 1000; // 1 second - - const connect = useCallback(() => { - try { - const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; - const wsUrl = `${protocol}//${window.location.host}/ws`; - // console.log("Connecting to WebSocket at:", wsUrl); // Removed console.log - - wsRef.current = new WebSocket(wsUrl); - - wsRef.current.onopen = () => { - // console.log("WebSocket connected"); // Removed console.log - setIsConnected(true); - reconnectAttempts = 0; - // Clear any existing reconnect timeout - if (reconnectTimeoutRef.current) { - clearTimeout(reconnectTimeoutRef.current); - } - websocketCallbacks.current.forEach((callback) => callback({ type: 'connected' })); - }; - - wsRef.current.onmessage = (event) => { - try { - const message = JSON.parse(event.data); - onMessage?.(message); // Call the primary onMessage handler - websocketCallbacks.current.forEach((callback) => - callback({ type: 'message', data: message }), - ); // Call registered callbacks - } catch (_error) { - websocketCallbacks.current.forEach((callback) => - callback({ - type: 'error', - error: 'Failed to parse message', - }), - ); - } - }; - - wsRef.current.onclose = () => { - setIsConnected(false); - websocketCallbacks.current.forEach((callback) => callback({ type: 'disconnected' })); - - if (reconnectAttempts < maxReconnectAttempts) { - const delay = Math.min(baseReconnectDelay * Math.pow(2, reconnectAttempts), 30000); - const jitter = Math.random() * 1000; // Add some randomness - const finalDelay = delay + jitter; - reconnectAttempts++; - reconnectTimeoutRef.current = window.setTimeout(connect, finalDelay); - } else { - websocketCallbacks.current.forEach((callback) => - callback({ - type: 'error', - error: 'Connection failed after maximum attempts', - }), - ); - } - }; - - wsRef.current.onerror = () => { - setIsConnected(false); // Ensure isConnected is false on error - websocketCallbacks.current.forEach((callback) => - callback({ - type: 'error', - error: 'WebSocket connection error', - }), - ); - // The onclose event will handle reconnection logic, so we don't need to call connect() here again. - }; - } catch (error) { - // console.error("Failed to connect to WebSocket:", error); // Removed console.error - setIsConnected(false); - websocketCallbacks.current.forEach((callback) => - callback({ - type: 'error', - error: 'Failed to initiate WebSocket connection', - }), - ); - // Attempt to reconnect even if initial connection fails - if (reconnectAttempts < maxReconnectAttempts) { - const delay = Math.min(baseReconnectDelay * Math.pow(2, reconnectAttempts), 30000); - const jitter = Math.random() * 1000; // Add some randomness - const finalDelay = delay + jitter; - // console.log( // Removed console.log - // `Attempting to reconnect after initial failure in ${Math.round(finalDelay)}ms... (attempt ${reconnectAttempts + 1}/${maxReconnectAttempts})`, - // ); - reconnectAttempts++; - reconnectTimeoutRef.current = window.setTimeout(connect, finalDelay); - } else { - // console.error("Max reconnection attempts reached after initial failure"); // Removed console.error - } - } - }, [onMessage]); - - useEffect(() => { - connect(); - - return () => { - if (reconnectTimeoutRef.current) { - clearTimeout(reconnectTimeoutRef.current); - } - if (wsRef.current) { - wsRef.current.close(); - } - }; - }, [connect]); - - const sendMessage = (message: any) => { - if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) { - wsRef.current.send(JSON.stringify(message)); - } else { - // console.warn("Cannot send message: WebSocket is not open."); // Removed console.warn - // Optionally, queue message or notify user - } - }; - - return { isConnected, sendMessage, addCallback }; -} +export * from '../services/websocket'; diff --git a/client/src/pages/tournament-detail.tsx b/client/src/pages/tournament-detail.tsx index 3cbfedc..e81418c 100644 --- a/client/src/pages/tournament-detail.tsx +++ b/client/src/pages/tournament-detail.tsx @@ -10,24 +10,20 @@ import { Card, CardContent } from '@/components/ui/card'; import { Skeleton } from '@/components/ui/skeleton'; import { useToast } from '@/hooks/use-toast'; import { queryClient } from '@/lib/queryClient'; -import { - getAuthHeaders, - processStarsPayment, - showBackButton, - hideBackButton, -} from '@/lib/telegram'; +import { useTelegram } from '@/services/telegram'; export default function TournamentDetailPage() { const params = useParams(); const [, setLocation] = useLocation(); const { toast } = useToast(); const tournamentId = params.id; + const { getAuthHeaders, processStarsPayment, showBackButton, hideBackButton } = useTelegram(); // Setup Telegram back button useEffect(() => { showBackButton(() => setLocation('/')); return () => hideBackButton(); - }, [setLocation]); + }, [setLocation, showBackButton, hideBackButton]); // Fetch tournament details const { data: tournament, isLoading: tournamentLoading } = useQuery({ diff --git a/client/src/pages/tournaments.tsx b/client/src/pages/tournaments.tsx index 87e08f1..b2d297a 100644 --- a/client/src/pages/tournaments.tsx +++ b/client/src/pages/tournaments.tsx @@ -2,6 +2,7 @@ import type { Tournament } from '@shared/schema'; import { useQuery, useMutation } from '@tanstack/react-query'; import { Trophy, Star, Plus } from 'lucide-react'; import { useLocation } from 'wouter'; +import { useEffect } from 'react'; import Header from '@/components/header'; import TournamentCard from '@/components/tournament-card'; @@ -10,12 +11,14 @@ import { Card, CardContent } from '@/components/ui/card'; import { Skeleton } from '@/components/ui/skeleton'; import { useToast } from '@/hooks/use-toast'; import { queryClient } from '@/lib/queryClient'; -import { getAuthHeaders, processStarsPayment } from '@/lib/telegram'; -import { useWebSocket, type WebSocketMessage } from '@/lib/websocket'; +import { useTelegram } from '@/services/telegram'; +import { useWebSocketService, type WebSocketMessage } from '@/services/websocket'; export default function TournamentsPage() { const { toast } = useToast(); const [, setLocation] = useLocation(); + const { getAuthHeaders, processStarsPayment } = useTelegram(); + const { addCallback } = useWebSocketService(); // Fetch tournaments const { data: tournaments = [], isLoading: tournamentsLoading } = useQuery({ @@ -76,19 +79,21 @@ export default function TournamentsPage() { }); // WebSocket for real-time updates - useWebSocket((message: WebSocketMessage) => { - switch (message.type) { - case 'tournament_created': - case 'tournament_updated': - case 'tournament_registration': - case 'tournament_unregistration': - queryClient.invalidateQueries({ queryKey: ['/api/tournaments'] }); - break; - case 'tournament_deleted': - queryClient.invalidateQueries({ queryKey: ['/api/tournaments'] }); - break; - } - }); + useEffect(() => { + return addCallback((message: WebSocketMessage) => { + switch (message.type) { + case 'tournament_created': + case 'tournament_updated': + case 'tournament_registration': + case 'tournament_unregistration': + queryClient.invalidateQueries({ queryKey: ['/api/tournaments'] }); + break; + case 'tournament_deleted': + queryClient.invalidateQueries({ queryKey: ['/api/tournaments'] }); + break; + } + }); + }, [addCallback]); const handleJoinTournament = async (tournament: Tournament) => { if (!user) { diff --git a/client/src/services/telegram.tsx b/client/src/services/telegram.tsx new file mode 100644 index 0000000..e01fed1 --- /dev/null +++ b/client/src/services/telegram.tsx @@ -0,0 +1,179 @@ +import { createContext, useContext, useEffect, type ReactNode } from 'react'; +import type { TelegramWebApp } from '../types/telegram'; + +let webApp: TelegramWebApp | null = null; + +function initTelegram() { + try { + const WebApp = window.Telegram?.WebApp; + if (!WebApp) return false; + if (!WebApp.initDataUnsafe?.user && process.env.NODE_ENV === 'production') { + return false; + } + WebApp.ready(); + WebApp.expand(); + if (WebApp.setHeaderColor) { + WebApp.setHeaderColor('#1f2937'); + } + if (WebApp.setBottomBarColor) { + WebApp.setBottomBarColor('#ffffff'); + } + if (WebApp.MainButton) { + WebApp.MainButton.hide(); + } + webApp = WebApp; + return true; + } catch (_error) { + return false; + } +} + +function getTelegramWebApp(): TelegramWebApp | null { + if (!webApp && window?.Telegram?.WebApp) { + webApp = window.Telegram.WebApp; + webApp.ready(); + webApp.expand(); + } + return webApp; +} + +function getTelegramUser() { + try { + return window.Telegram?.WebApp?.initDataUnsafe?.user || null; + } catch (_error) { + return null; + } +} + +function showMainButton(text: string, onClick: () => void) { + try { + const WebApp = window.Telegram?.WebApp; + if (WebApp?.MainButton) { + WebApp.MainButton.setText(text); + WebApp.MainButton.show(); + WebApp.MainButton.onClick(onClick); + } + } catch (_error) { + // ignore + } +} + +function hideMainButton() { + try { + const WebApp = window.Telegram?.WebApp; + if (WebApp?.MainButton) { + WebApp.MainButton.hide(); + } + } catch (_error) { + // ignore + } +} + +function showBackButton(onClick: () => void) { + try { + const WebApp = window.Telegram?.WebApp; + if (WebApp?.BackButton) { + WebApp.BackButton.onClick(onClick); + WebApp.BackButton.show(); + } + } catch (_error) { + // ignore + } +} + +function hideBackButton() { + try { + const WebApp = window.Telegram?.WebApp; + if (WebApp?.BackButton) { + WebApp.BackButton.hide(); + } + } catch (_error) { + // ignore + } +} + +function processStarsPayment(amount: number, tournamentId: string): Promise { + return new Promise((resolve) => { + const WebApp = window.Telegram?.WebApp; + if (WebApp?.openInvoice) { + const invoiceUrl = `https://t.me/invoice/stars?amount=${amount}&payload=${tournamentId}`; + WebApp.openInvoice(invoiceUrl, (status: string) => { + resolve(status === 'paid'); + }); + } else { + const confirmed = window.confirm(`Pay ${amount} Telegram Stars to join tournament?`); + resolve(confirmed); + } + }); +} + +function getAuthHeaders(): Record { + const initData = window.Telegram?.WebApp?.initData; + if (!initData) { + return {}; + } + return { 'x-telegram-init-data': initData }; +} + +function showAlert(message: string) { + try { + const WebApp = window.Telegram?.WebApp; + if (WebApp?.showAlert) { + WebApp.showAlert(message); + } else { + alert(message); + } + } catch (_error) { + alert(message); + } +} + +function hapticFeedback(type: 'impact' | 'notification' | 'selection' = 'impact') { + try { + const WebApp = window.Telegram?.WebApp; + if (!WebApp?.HapticFeedback) return; + if (type === 'impact') { + WebApp.HapticFeedback.impactOccurred('medium'); + } else if (type === 'notification') { + WebApp.HapticFeedback.notificationOccurred('success'); + } else { + WebApp.HapticFeedback.selectionChanged(); + } + } catch (_error) { + // ignore + } +} + +const telegramService = { + getAuthHeaders, + processStarsPayment, + showBackButton, + hideBackButton, +}; + +const TelegramContext = createContext(telegramService); + +export function TelegramProvider({ children }: { children: ReactNode }) { + useEffect(() => { + initTelegram(); + }, []); + return {children}; +} + +export function useTelegram() { + return useContext(TelegramContext); +} + +export { + initTelegram, + getTelegramWebApp, + getTelegramUser, + showMainButton, + hideMainButton, + showBackButton, + hideBackButton, + processStarsPayment, + getAuthHeaders, + showAlert, + hapticFeedback, +}; diff --git a/client/src/services/websocket.tsx b/client/src/services/websocket.tsx new file mode 100644 index 0000000..2b73b86 --- /dev/null +++ b/client/src/services/websocket.tsx @@ -0,0 +1,147 @@ +import { createContext, useContext, type ReactNode } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; + +export interface WebSocketMessage { + type: + | 'tournament_created' + | 'tournament_updated' + | 'tournament_deleted' + | 'tournament_registration' + | 'tournament_unregistration'; + tournament?: any; + tournamentId?: string; + userId?: string; +} + +function useWebSocket(onMessage?: (message: WebSocketMessage) => void) { + const [isConnected, setIsConnected] = useState(false); + const wsRef = useRef(null); + const reconnectTimeoutRef = useRef(); + const websocketCallbacks = useRef<((message: any) => void)[]>([]); + + const addCallback = (callback: (message: any) => void) => { + websocketCallbacks.current.push(callback); + if (isConnected) { + callback({ type: 'connected' }); + } + return () => { + websocketCallbacks.current = websocketCallbacks.current.filter((cb) => cb !== callback); + }; + }; + + let reconnectAttempts = 0; + const maxReconnectAttempts = 5; + const baseReconnectDelay = 1000; + + const connect = useCallback(() => { + try { + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + const wsUrl = `${protocol}//${window.location.host}/ws`; + + wsRef.current = new WebSocket(wsUrl); + + wsRef.current.onopen = () => { + setIsConnected(true); + reconnectAttempts = 0; + if (reconnectTimeoutRef.current) { + clearTimeout(reconnectTimeoutRef.current); + } + websocketCallbacks.current.forEach((callback) => callback({ type: 'connected' })); + }; + + wsRef.current.onmessage = (event) => { + try { + const message = JSON.parse(event.data); + onMessage?.(message); + websocketCallbacks.current.forEach((callback) => + callback({ type: 'message', data: message }), + ); + } catch (_error) { + websocketCallbacks.current.forEach((callback) => + callback({ type: 'error', error: 'Failed to parse message' }), + ); + } + }; + + wsRef.current.onclose = () => { + setIsConnected(false); + websocketCallbacks.current.forEach((callback) => callback({ type: 'disconnected' })); + + if (reconnectAttempts < maxReconnectAttempts) { + const delay = Math.min(baseReconnectDelay * Math.pow(2, reconnectAttempts), 30000); + const jitter = Math.random() * 1000; + const finalDelay = delay + jitter; + reconnectAttempts++; + reconnectTimeoutRef.current = window.setTimeout(connect, finalDelay); + } else { + websocketCallbacks.current.forEach((callback) => + callback({ type: 'error', error: 'Connection failed after maximum attempts' }), + ); + } + }; + + wsRef.current.onerror = () => { + setIsConnected(false); + websocketCallbacks.current.forEach((callback) => + callback({ type: 'error', error: 'WebSocket connection error' }), + ); + }; + } catch (_error) { + setIsConnected(false); + websocketCallbacks.current.forEach((callback) => + callback({ type: 'error', error: 'Failed to initiate WebSocket connection' }), + ); + if (reconnectAttempts < maxReconnectAttempts) { + const delay = Math.min(baseReconnectDelay * Math.pow(2, reconnectAttempts), 30000); + const jitter = Math.random() * 1000; + const finalDelay = delay + jitter; + reconnectAttempts++; + reconnectTimeoutRef.current = window.setTimeout(connect, finalDelay); + } + } + }, [onMessage]); + + useEffect(() => { + connect(); + + return () => { + if (reconnectTimeoutRef.current) { + clearTimeout(reconnectTimeoutRef.current); + } + if (wsRef.current) { + wsRef.current.close(); + } + }; + }, [connect]); + + const sendMessage = (message: any) => { + if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) { + wsRef.current.send(JSON.stringify(message)); + } + }; + + return { isConnected, sendMessage, addCallback }; +} + +interface WebSocketContextValue { + isConnected: boolean; + sendMessage: (message: any) => void; + addCallback: (callback: (message: any) => void) => () => void; +} + +const WebSocketContext = createContext(null); + +export function WebSocketProvider({ children }: { children: ReactNode }) { + const ws = useWebSocket(); + return {children}; +} + +export function useWebSocketService() { + const ctx = useContext(WebSocketContext); + if (!ctx) { + throw new Error('useWebSocketService must be used within a WebSocketProvider'); + } + return ctx; +} + +export { useWebSocket }; From 6a40de97e79ec9ac96ee4a26d8a4c775d408bd0e Mon Sep 17 00:00:00 2001 From: WA11AX Date: Thu, 14 Aug 2025 01:17:12 +0300 Subject: [PATCH 2/4] Update client/src/services/websocket.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- client/src/services/websocket.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/services/websocket.tsx b/client/src/services/websocket.tsx index 2b73b86..52e900b 100644 --- a/client/src/services/websocket.tsx +++ b/client/src/services/websocket.tsx @@ -29,7 +29,7 @@ function useWebSocket(onMessage?: (message: WebSocketMessage) => void) { }; }; - let reconnectAttempts = 0; + const reconnectAttemptsRef = useRef(0); const maxReconnectAttempts = 5; const baseReconnectDelay = 1000; From 5bb116f8647960c66418813494664361789cbcb5 Mon Sep 17 00:00:00 2001 From: WA11AX Date: Thu, 14 Aug 2025 01:17:20 +0300 Subject: [PATCH 3/4] Update client/src/services/websocket.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- client/src/services/websocket.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/src/services/websocket.tsx b/client/src/services/websocket.tsx index 52e900b..de3c306 100644 --- a/client/src/services/websocket.tsx +++ b/client/src/services/websocket.tsx @@ -126,7 +126,8 @@ function useWebSocket(onMessage?: (message: WebSocketMessage) => void) { interface WebSocketContextValue { isConnected: boolean; sendMessage: (message: any) => void; - addCallback: (callback: (message: any) => void) => () => void; + sendMessage: (message: WebSocketMessage) => void; + addCallback: (callback: (message: WebSocketMessage) => void) => () => void; } const WebSocketContext = createContext(null); From f90e6f816d862f18975914c6f7c127070309e836 Mon Sep 17 00:00:00 2001 From: WA11AX Date: Thu, 14 Aug 2025 01:17:31 +0300 Subject: [PATCH 4/4] Update client/src/services/websocket.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- client/src/services/websocket.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/services/websocket.tsx b/client/src/services/websocket.tsx index de3c306..870bba7 100644 --- a/client/src/services/websocket.tsx +++ b/client/src/services/websocket.tsx @@ -8,7 +8,7 @@ export interface WebSocketMessage { | 'tournament_deleted' | 'tournament_registration' | 'tournament_unregistration'; - tournament?: any; + tournament?: Tournament; tournamentId?: string; userId?: string; }