Implement StatsContext for centralized statistics management and refactor App component to utilize context

This commit is contained in:
2025-10-05 09:03:40 +08:00
parent 0590e2a05e
commit 1d7075a470
3 changed files with 168 additions and 133 deletions

View File

@@ -1,29 +1,9 @@
import { useTranslation } from 'react-i18next'
import { Link } from 'react-router-dom'
import { useState, useEffect } from 'react'
import Layout from './components/Layout'
import { useStats } from './contexts/StatsContext'
import './App.css'
interface ChatMessage {
steamID64: string
userName: string
message: string
timeStamp: number
}
interface TopPlayer {
steamID64: string
userName: string
playTime: number
}
interface Stats {
totalPlayers: number
totalConnects: number
totalPlayTime: number
totalKills: number
}
// Loading Skeleton Component
const LoadingSkeleton = ({ width = '60px', height = '40px', className = '' }: { width?: string, height?: string, className?: string }) => (
<div
@@ -56,106 +36,7 @@ const LoadingSkeleton = ({ width = '60px', height = '40px', className = '' }: {
function App() {
const { t } = useTranslation()
const [stats, setStats] = useState<Stats>({
totalPlayers: 0,
totalConnects: 0,
totalPlayTime: 0,
totalKills: 0
})
const [loading, setLoading] = useState(true)
const [recentChats, setRecentChats] = useState<ChatMessage[]>([])
const [topPlayers, setTopPlayers] = useState<TopPlayer[]>([])
// Fetch statistics data from API
useEffect(() => {
const timeRangeEnd = Date.now()
const timeRangeStart = 0 // all time
const requestBody = { timeRangeStart, timeRangeEnd }
Promise.all([
fetch('/api/server/statistics/total-player-count', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(requestBody)
}),
fetch('/api/server/statistics/total-connect-count', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(requestBody)
}),
fetch('/api/server/statistics/total-play-time', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(requestBody)
}),
fetch('/api/server/statistics/total-kill-count', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...requestBody,
headshotOnly: false,
weaponFilter: [],
playerFilter: []
})
})
])
.then(responses => Promise.all(responses.map(r => r.json())))
.then(([players, connects, playTime, kills]) => {
setStats({
totalPlayers: players.count || 0,
totalConnects: connects.totalConnectCount || 0,
totalPlayTime: playTime.totalPlayTime || 0,
totalKills: kills.totalKillCount || 0
})
setLoading(false)
})
.catch(error => {
console.error('Error fetching stats:', error)
setLoading(false)
})
}, [])
// Fetch recent chat messages (limit to top 5)
useEffect(() => {
fetch('/api/server/statistics/recent-chat-message', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
timeRangeStart: Date.now() - 3600000, // last hour
timeRangeEnd: Date.now()
})
})
.then(r => r.json())
.then(data => {
// Limit to top 5 most recent messages
const messages = data.messages || []
const top5 = messages
.sort((a: ChatMessage, b: ChatMessage) => b.timeStamp - a.timeStamp)
.slice(0, 5)
setRecentChats(top5)
})
.catch(error => console.error('Error fetching chat messages:', error))
}, [])
// Fetch top players by playtime
useEffect(() => {
fetch('/api/server/statistics/top-play-time', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
timeRangeStart: 0, // all time
timeRangeEnd: Date.now()
})
})
.then(r => r.json())
.then(data => {
// Get top 3 players
const players = data.players || []
setTopPlayers(players.slice(0, 3))
})
.catch(error => console.error('Error fetching top players:', error))
}, [])
const { stats, recentChats, topPlayers, loading } = useStats()
// Format playtime from seconds to hours and minutes
const formatPlayTime = (seconds: number) => {

View File

@@ -0,0 +1,151 @@
import { createContext, useContext, useState, useEffect } from 'react'
import type { ReactNode } from 'react'
interface ChatMessage {
steamID64: string
userName: string
message: string
timeStamp: number
}
interface TopPlayer {
steamID64: string
userName: string
playTime: number
}
interface Stats {
totalPlayers: number
totalConnects: number
totalPlayTime: number
totalKills: number
}
interface StatsContextType {
stats: Stats
recentChats: ChatMessage[]
topPlayers: TopPlayer[]
loading: boolean
lastUpdated: number
}
const StatsContext = createContext<StatsContextType | undefined>(undefined)
export function StatsProvider({ children }: { children: ReactNode }) {
const [stats, setStats] = useState<Stats>({
totalPlayers: 0,
totalConnects: 0,
totalPlayTime: 0,
totalKills: 0
})
const [recentChats, setRecentChats] = useState<ChatMessage[]>([])
const [topPlayers, setTopPlayers] = useState<TopPlayer[]>([])
const [loading, setLoading] = useState(true)
const [lastUpdated, setLastUpdated] = useState(Date.now())
useEffect(() => {
const fetchAllData = async () => {
const timeRangeEnd = Date.now()
const timeRangeStart = 0 // all time
const requestBody = { timeRangeStart, timeRangeEnd }
try {
// Fetch statistics
const [playersRes, connectsRes, playTimeRes, killsRes, chatsRes, topPlayersRes] = await Promise.all([
fetch('/api/server/statistics/total-player-count', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(requestBody)
}),
fetch('/api/server/statistics/total-connect-count', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(requestBody)
}),
fetch('/api/server/statistics/total-play-time', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(requestBody)
}),
fetch('/api/server/statistics/total-kill-count', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...requestBody,
headshotOnly: false,
weaponFilter: [],
playerFilter: []
})
}),
fetch('/api/server/statistics/recent-chat-message', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
timeRangeStart: Date.now() - 3600000, // last hour
timeRangeEnd: Date.now()
})
}),
fetch('/api/server/statistics/top-play-time', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
timeRangeStart: 0, // all time
timeRangeEnd: Date.now()
})
})
])
const [players, connects, playTime, kills, chats, topPlayersData] = await Promise.all([
playersRes.json(),
connectsRes.json(),
playTimeRes.json(),
killsRes.json(),
chatsRes.json(),
topPlayersRes.json()
])
// Update statistics
setStats({
totalPlayers: players.count || 0,
totalConnects: connects.totalConnectCount || 0,
totalPlayTime: playTime.totalPlayTime || 0,
totalKills: kills.totalKillCount || 0
})
// Update recent chats (limit to top 5)
const messages = chats.messages || []
const top5 = messages
.sort((a: ChatMessage, b: ChatMessage) => b.timeStamp - a.timeStamp)
.slice(0, 5)
setRecentChats(top5)
// Update top players (limit to top 3)
const playersArray = topPlayersData.players || []
setTopPlayers(playersArray.slice(0, 3))
setLoading(false)
setLastUpdated(Date.now())
} catch (error) {
console.error('Error fetching stats:', error)
setLoading(false)
}
}
// Fetch data only once on mount
fetchAllData()
}, []) // Empty dependency array - runs only once
return (
<StatsContext.Provider value={{ stats, recentChats, topPlayers, loading, lastUpdated }}>
{children}
</StatsContext.Provider>
)
}
export function useStats() {
const context = useContext(StatsContext)
if (context === undefined) {
throw new Error('useStats must be used within a StatsProvider')
}
return context
}

View File

@@ -5,6 +5,7 @@ import './index.css'
import './i18n'
import { ThemeProvider } from './contexts/ThemeContext'
import { ServerProvider } from './contexts/ServerContext'
import { StatsProvider } from './contexts/StatsContext'
import ScrollToTop from './components/ScrollToTop'
import App from './App.tsx'
import Friends from './pages/Friends.tsx'
@@ -15,6 +16,7 @@ import Forum from './pages/Forum.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<ThemeProvider>
<StatsProvider>
<ServerProvider>
<Router>
<ScrollToTop />
@@ -27,6 +29,7 @@ createRoot(document.getElementById('root')!).render(
</Routes>
</Router>
</ServerProvider>
</StatsProvider>
</ThemeProvider>
</StrictMode>,
)