294 lines
10 KiB
TypeScript
294 lines
10 KiB
TypeScript
import { useTranslation } from 'react-i18next'
|
||
import { Link } from 'react-router-dom'
|
||
import { useState, useEffect } from 'react'
|
||
import Layout from './components/Layout'
|
||
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
|
||
}
|
||
|
||
function App() {
|
||
const { t } = useTranslation()
|
||
|
||
const [stats, setStats] = useState<Stats>({
|
||
totalPlayers: 0,
|
||
totalConnects: 0,
|
||
totalPlayTime: 0,
|
||
totalKills: 0
|
||
})
|
||
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
|
||
})
|
||
})
|
||
.catch(error => console.error('Error fetching stats:', error))
|
||
}, [])
|
||
|
||
// 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))
|
||
}, [])
|
||
|
||
// Format playtime from seconds to hours and minutes
|
||
const formatPlayTime = (seconds: number) => {
|
||
const hours = Math.floor(seconds / 3600)
|
||
const minutes = Math.floor((seconds % 3600) / 60)
|
||
|
||
if (hours > 0) {
|
||
if (minutes > 0) {
|
||
return `${hours}${t('time.hours')} ${minutes}${t('time.minutes')}`
|
||
}
|
||
return `${hours}${t('time.hours')}`
|
||
}
|
||
return `${minutes}${t('time.minutes')}`
|
||
}
|
||
|
||
// Format time ago from timestamp
|
||
const formatTimeAgo = (timestamp: number) => {
|
||
const seconds = Math.floor((Date.now() - timestamp) / 1000)
|
||
if (seconds < 60) return `${seconds}s ago`
|
||
const minutes = Math.floor(seconds / 60)
|
||
if (minutes < 60) return `${minutes}m ago`
|
||
const hours = Math.floor(minutes / 60)
|
||
if (hours < 24) return `${hours}h ago`
|
||
const days = Math.floor(hours / 24)
|
||
return `${days}d ago`
|
||
}
|
||
|
||
// Format playtime to hours for leaderboard
|
||
const formatPlayTimeHours = (seconds: number) => {
|
||
return Math.floor(seconds / 3600)
|
||
}
|
||
|
||
return (
|
||
<Layout currentPage="home">
|
||
{/* Hero Section */}
|
||
<section className="hero">
|
||
<div className="hero-content">
|
||
<h1 className="hero-title">
|
||
{t('hero.title')}<br />
|
||
<span className="highlight">{t('hero.titleHighlight')}</span> {t('hero.titleEnd')}
|
||
</h1>
|
||
<p className="hero-subtitle">
|
||
{t('hero.subtitle')}
|
||
</p>
|
||
<div className="hero-buttons">
|
||
<button className="btn-primary">{t('hero.startPlaying')}</button>
|
||
<button className="btn-secondary">{t('hero.viewStats')}</button>
|
||
</div>
|
||
</div>
|
||
<div className="hero-visual">
|
||
<div className="game-preview">
|
||
<div className="preview-screen">
|
||
<div className="screen-content">
|
||
<div className="crosshair">⊕</div>
|
||
<div className="health-bar">100 HP</div>
|
||
<div className="ammo-counter">30/90</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
{/* Statistics Section */}
|
||
<section className="stats-section">
|
||
<div className="stats-container">
|
||
<div className="stat-card">
|
||
<div className="stat-icon">👥</div>
|
||
<div className="stat-number">{stats.totalPlayers.toLocaleString()}</div>
|
||
<div className="stat-label">{t('stats.totalPlayers')}</div>
|
||
</div>
|
||
<div className="stat-card">
|
||
<div className="stat-icon"><EFBFBD></div>
|
||
<div className="stat-number">{stats.totalConnects.toLocaleString()}</div>
|
||
<div className="stat-label">{t('stats.totalConnects')}</div>
|
||
</div>
|
||
<div className="stat-card">
|
||
<div className="stat-icon">⏱️</div>
|
||
<div className="stat-number">{formatPlayTime(stats.totalPlayTime)}</div>
|
||
<div className="stat-label">{t('stats.totalPlayTime')}</div>
|
||
</div>
|
||
<div className="stat-card">
|
||
<div className="stat-icon">🎯</div>
|
||
<div className="stat-number">{stats.totalKills.toLocaleString()}</div>
|
||
<div className="stat-label">{t('stats.totalKills')}</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
{/* Features Section */}
|
||
<section className="features-section">
|
||
<div className="features-container">
|
||
<h2 className="section-title">{t('features.title')}</h2>
|
||
<div className="features-grid">
|
||
<div className="feature-card" id="servers">
|
||
<div className="feature-icon">🖥️</div>
|
||
<h3>{t('features.serverBrowser.title')}</h3>
|
||
<p>{t('features.serverBrowser.description')}</p>
|
||
<Link to="/servers" className="feature-btn" style={{display: 'inline-block', textDecoration: 'none'}}>{t('features.browseServers')}</Link>
|
||
</div>
|
||
<div className="feature-card" id="blog">
|
||
<div className="feature-icon">📝</div>
|
||
<h3>{t('features.blog.title')}</h3>
|
||
<p>{t('features.blog.description')}</p>
|
||
<Link to="/blog" className="feature-btn" style={{display: 'inline-block', textDecoration: 'none'}}>{t('features.readBlog')}</Link>
|
||
</div>
|
||
<div className="feature-card" id="git">
|
||
<div className="feature-icon">📦</div>
|
||
<h3>{t('features.git.title')}</h3>
|
||
<p>{t('features.git.description')}</p>
|
||
<a href="https://git.cialloo.com" target="_blank" rel="noopener noreferrer" className="feature-btn" style={{display: 'inline-block', textDecoration: 'none'}}>{t('features.viewGitHub')}</a>
|
||
</div>
|
||
<div className="feature-card" id="forum">
|
||
<div className="feature-icon">💬</div>
|
||
<h3>{t('features.forum.title')}</h3>
|
||
<p>{t('features.forum.description')}</p>
|
||
<Link to="/forum" className="feature-btn" style={{display: 'inline-block', textDecoration: 'none'}}>{t('features.joinForum')}</Link>
|
||
</div>
|
||
<div className="feature-card" id="friends">
|
||
<div className="feature-icon">🔗</div>
|
||
<h3>{t('features.friends.title')}</h3>
|
||
<p>{t('features.friends.description')}</p>
|
||
<Link to="/friends" className="feature-btn" style={{display: 'inline-block', textDecoration: 'none'}}>{t('features.viewFriends')}</Link>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
{/* Recent Activity Section */}
|
||
<section className="activity-section">
|
||
<div className="activity-container">
|
||
<div className="activity-main">
|
||
<h2 className="section-title">{t('activity.title')}</h2>
|
||
<div className="chat-feed">
|
||
{recentChats.map((chat, index) => (
|
||
<div key={index} className="chat-message">
|
||
<div className="chat-user">{chat.userName}:</div>
|
||
<div className="chat-text">{chat.message}</div>
|
||
<div className="chat-time">{formatTimeAgo(chat.timeStamp)}</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
<div className="activity-sidebar">
|
||
<div className="sidebar-card">
|
||
<h3>{t('activity.topPlayers')}</h3>
|
||
<div className="leaderboard">
|
||
{topPlayers.map((player, index) => (
|
||
<div key={player.steamID64} className="leader-item">
|
||
{index + 1}. {player.userName} - {formatPlayTimeHours(player.playTime)}h
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
<div className="sidebar-card">
|
||
<h3>{t('activity.serverInfo')}</h3>
|
||
<div className="quick-stats">
|
||
<div>{t('activity.serverStatus')}</div>
|
||
<div>{t('activity.currentMap')}</div>
|
||
<div>{t('activity.nextRestart')}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</Layout>
|
||
)
|
||
}
|
||
|
||
export default App
|
||
|