Implement centralized server data polling with context management
This commit is contained in:
118
src/contexts/ServerContext.tsx
Normal file
118
src/contexts/ServerContext.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
import { createContext, useContext, useState, useEffect } from 'react'
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
interface ServerInfo {
|
||||
name: string
|
||||
ip: string
|
||||
port: number
|
||||
category: 'Surf' | 'Kz' | 'Bhop'
|
||||
}
|
||||
|
||||
interface A2SResponse {
|
||||
appID: number
|
||||
botCount: number
|
||||
environment: string
|
||||
gameDescription: string
|
||||
gameDirectory: string
|
||||
mapName: string
|
||||
maxPlayers: number
|
||||
playerCount: number
|
||||
serverName: string
|
||||
serverType: string
|
||||
vac: number
|
||||
version: string
|
||||
visibility: number
|
||||
}
|
||||
|
||||
export interface ServerData extends ServerInfo {
|
||||
a2sData?: A2SResponse
|
||||
status: 'online' | 'offline' | 'loading'
|
||||
error?: string
|
||||
}
|
||||
|
||||
interface ServerContextType {
|
||||
servers: ServerData[]
|
||||
loading: boolean
|
||||
lastUpdated: number
|
||||
}
|
||||
|
||||
const ServerContext = createContext<ServerContextType | undefined>(undefined)
|
||||
|
||||
// Server list from database export
|
||||
const serverList: ServerInfo[] = [
|
||||
{ name: 'Surf 66 Tick #1', ip: '14.103.233.1', port: 27015, category: 'Surf' },
|
||||
{ name: 'Surf 66 Tick #2', ip: '14.103.233.1', port: 27016, category: 'Surf' },
|
||||
{ name: 'Surf 66 Tick #3', ip: '14.103.233.1', port: 27017, category: 'Surf' },
|
||||
{ name: 'Surf 100 Tick #1', ip: '14.103.233.1', port: 28015, category: 'Surf' },
|
||||
{ name: 'Surf 100 Tick #2', ip: '14.103.233.1', port: 28016, category: 'Surf' },
|
||||
{ name: 'Kz #1', ip: '14.103.233.1', port: 29015, category: 'Kz' },
|
||||
{ name: 'Kz #2', ip: '14.103.233.1', port: 29016, category: 'Kz' },
|
||||
{ name: 'Bhop #1', ip: '14.103.233.1', port: 30015, category: 'Bhop' },
|
||||
{ name: 'Bhop #2', ip: '14.103.233.1', port: 30016, category: 'Bhop' }
|
||||
]
|
||||
|
||||
export function ServerProvider({ children }: { children: ReactNode }) {
|
||||
const [servers, setServers] = useState<ServerData[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [lastUpdated, setLastUpdated] = useState(Date.now())
|
||||
|
||||
const fetchServerData = async () => {
|
||||
const serverPromises = serverList.map(async (server) => {
|
||||
try {
|
||||
const response = await fetch('/api/server/statistics/a2s-query', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
serverIP: server.ip,
|
||||
serverPort: server.port,
|
||||
timeout: 3000
|
||||
})
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`)
|
||||
}
|
||||
|
||||
const a2sData: A2SResponse = await response.json()
|
||||
return { ...server, a2sData, status: 'online' as const }
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch data for ${server.name}:`, error)
|
||||
return {
|
||||
...server,
|
||||
status: 'offline' as const,
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const results = await Promise.all(serverPromises)
|
||||
setServers(results)
|
||||
setLoading(false)
|
||||
setLastUpdated(Date.now())
|
||||
}
|
||||
|
||||
// Initial fetch and polling every 2 seconds
|
||||
useEffect(() => {
|
||||
fetchServerData()
|
||||
|
||||
const interval = setInterval(() => {
|
||||
fetchServerData()
|
||||
}, 2000) // Poll every 2 seconds
|
||||
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<ServerContext.Provider value={{ servers, loading, lastUpdated }}>
|
||||
{children}
|
||||
</ServerContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useServers() {
|
||||
const context = useContext(ServerContext)
|
||||
if (context === undefined) {
|
||||
throw new Error('useServers must be used within a ServerProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
23
src/main.tsx
23
src/main.tsx
@@ -4,6 +4,7 @@ import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
|
||||
import './index.css'
|
||||
import './i18n'
|
||||
import { ThemeProvider } from './contexts/ThemeContext'
|
||||
import { ServerProvider } from './contexts/ServerContext'
|
||||
import ScrollToTop from './components/ScrollToTop'
|
||||
import App from './App.tsx'
|
||||
import Friends from './pages/Friends.tsx'
|
||||
@@ -14,16 +15,18 @@ import Forum from './pages/Forum.tsx'
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<ThemeProvider>
|
||||
<Router>
|
||||
<ScrollToTop />
|
||||
<Routes>
|
||||
<Route path="/" element={<App />} />
|
||||
<Route path="/friends" element={<Friends />} />
|
||||
<Route path="/blog" element={<Blog />} />
|
||||
<Route path="/servers" element={<Servers />} />
|
||||
<Route path="/forum" element={<Forum />} />
|
||||
</Routes>
|
||||
</Router>
|
||||
<ServerProvider>
|
||||
<Router>
|
||||
<ScrollToTop />
|
||||
<Routes>
|
||||
<Route path="/" element={<App />} />
|
||||
<Route path="/friends" element={<Friends />} />
|
||||
<Route path="/blog" element={<Blog />} />
|
||||
<Route path="/servers" element={<Servers />} />
|
||||
<Route path="/forum" element={<Forum />} />
|
||||
</Routes>
|
||||
</Router>
|
||||
</ServerProvider>
|
||||
</ThemeProvider>
|
||||
</StrictMode>,
|
||||
)
|
||||
|
||||
@@ -1,37 +1,9 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useState } from 'react'
|
||||
import Layout from '../components/Layout'
|
||||
import { useServers } from '../contexts/ServerContext'
|
||||
import '../App.css'
|
||||
|
||||
interface ServerInfo {
|
||||
name: string
|
||||
ip: string
|
||||
port: number
|
||||
category: 'Surf' | 'Kz' | 'Bhop'
|
||||
}
|
||||
|
||||
interface A2SResponse {
|
||||
appID: number
|
||||
botCount: number
|
||||
environment: string
|
||||
gameDescription: string
|
||||
gameDirectory: string
|
||||
mapName: string
|
||||
maxPlayers: number
|
||||
playerCount: number
|
||||
serverName: string
|
||||
serverType: string
|
||||
vac: number
|
||||
version: string
|
||||
visibility: number
|
||||
}
|
||||
|
||||
interface ServerData extends ServerInfo {
|
||||
a2sData?: A2SResponse
|
||||
status: 'online' | 'offline' | 'loading'
|
||||
error?: string
|
||||
}
|
||||
|
||||
// Loading Skeleton Component
|
||||
const LoadingSkeleton = ({ width = '60px', height = '40px', className = '', style = {} }: { width?: string, height?: string, className?: string, style?: React.CSSProperties }) => (
|
||||
<div
|
||||
@@ -65,69 +37,15 @@ const LoadingSkeleton = ({ width = '60px', height = '40px', className = '', styl
|
||||
|
||||
function Servers() {
|
||||
const { t } = useTranslation()
|
||||
|
||||
// Server list from database export
|
||||
const serverList: ServerInfo[] = [
|
||||
{ name: 'Surf 66 Tick #1', ip: '14.103.233.1', port: 27015, category: 'Surf' },
|
||||
{ name: 'Surf 66 Tick #2', ip: '14.103.233.1', port: 27016, category: 'Surf' },
|
||||
{ name: 'Surf 66 Tick #3', ip: '14.103.233.1', port: 27017, category: 'Surf' },
|
||||
{ name: 'Surf 100 Tick #1', ip: '14.103.233.1', port: 28015, category: 'Surf' },
|
||||
{ name: 'Surf 100 Tick #2', ip: '14.103.233.1', port: 28016, category: 'Surf' },
|
||||
{ name: 'Kz #1', ip: '14.103.233.1', port: 29015, category: 'Kz' },
|
||||
{ name: 'Kz #2', ip: '14.103.233.1', port: 29016, category: 'Kz' },
|
||||
{ name: 'Bhop #1', ip: '14.103.233.1', port: 30015, category: 'Bhop' },
|
||||
{ name: 'Bhop #2', ip: '14.103.233.1', port: 30016, category: 'Bhop' }
|
||||
]
|
||||
|
||||
const [servers, setServers] = useState<ServerData[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const { servers, loading } = useServers()
|
||||
const [selectedCategory, setSelectedCategory] = useState<string>('All')
|
||||
|
||||
// Fetch A2S data for all servers
|
||||
useEffect(() => {
|
||||
const fetchServerData = async () => {
|
||||
const serverPromises = serverList.map(async (server) => {
|
||||
try {
|
||||
const response = await fetch('/api/server/statistics/a2s-query', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
serverIP: server.ip,
|
||||
serverPort: server.port,
|
||||
timeout: 3000
|
||||
})
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`)
|
||||
}
|
||||
|
||||
const a2sData: A2SResponse = await response.json()
|
||||
return { ...server, a2sData, status: 'online' as const }
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch data for ${server.name}:`, error)
|
||||
return {
|
||||
...server,
|
||||
status: 'offline' as const,
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const results = await Promise.all(serverPromises)
|
||||
setServers(results)
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
fetchServerData()
|
||||
}, [])
|
||||
|
||||
// Filter servers by category
|
||||
const filteredServers = selectedCategory === 'All'
|
||||
? servers
|
||||
: servers.filter(server => server.category === selectedCategory)
|
||||
|
||||
const categories = ['All', ...Array.from(new Set(serverList.map(s => s.category)))]
|
||||
const categories = ['All', ...Array.from(new Set(servers.map(s => s.category)))]
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
@@ -209,7 +127,7 @@ function Servers() {
|
||||
}}>
|
||||
{loading ? (
|
||||
// Show loading skeletons
|
||||
serverList.map((_, index) => (
|
||||
servers.length > 0 ? servers.map((_, index) => (
|
||||
<div
|
||||
key={`loading-${index}`}
|
||||
className="server-card"
|
||||
@@ -267,7 +185,7 @@ function Servers() {
|
||||
{/* Join Button Skeleton */}
|
||||
<LoadingSkeleton width="100%" height="40px" />
|
||||
</div>
|
||||
))
|
||||
)) : []
|
||||
) : (
|
||||
filteredServers.map((server) => (
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user