427 lines
14 KiB
TypeScript
427 lines
14 KiB
TypeScript
import { useTranslation } from 'react-i18next'
|
||
import { useState, useEffect } from 'react'
|
||
import Layout from '../components/Layout'
|
||
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
|
||
}
|
||
|
||
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 [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)
|
||
}
|
||
|
||
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 getStatusColor = (status: string) => {
|
||
switch (status) {
|
||
case 'online': return '#10b981'
|
||
case 'offline': return '#ef4444'
|
||
case 'loading': return '#f59e0b'
|
||
default: return '#6b7280'
|
||
}
|
||
}
|
||
|
||
return (
|
||
<Layout currentPage="servers">
|
||
{/* Servers Page Header */}
|
||
<section className="servers-header" style={{
|
||
padding: '120px 2rem 60px',
|
||
background: 'linear-gradient(135deg, var(--bg-primary) 0%, var(--bg-secondary) 100%)',
|
||
textAlign: 'center'
|
||
}}>
|
||
<div style={{ maxWidth: '800px', margin: '0 auto' }}>
|
||
<h1 style={{
|
||
fontSize: '3rem',
|
||
fontWeight: 'bold',
|
||
marginBottom: '1rem',
|
||
background: 'linear-gradient(45deg, var(--accent-primary), var(--accent-secondary))',
|
||
WebkitBackgroundClip: 'text',
|
||
WebkitTextFillColor: 'transparent',
|
||
backgroundClip: 'text'
|
||
}}>
|
||
{t('servers.title')}
|
||
</h1>
|
||
<p style={{
|
||
fontSize: '1.2rem',
|
||
color: 'var(--text-secondary)',
|
||
marginBottom: '2rem'
|
||
}}>
|
||
{t('servers.subtitle')}
|
||
</p>
|
||
|
||
{/* Category Filter */}
|
||
<div style={{
|
||
display: 'flex',
|
||
justifyContent: 'center',
|
||
gap: '1rem',
|
||
marginBottom: '2rem',
|
||
flexWrap: 'wrap'
|
||
}}>
|
||
{categories.map(category => (
|
||
<button
|
||
key={category}
|
||
onClick={() => setSelectedCategory(category)}
|
||
style={{
|
||
padding: '0.5rem 1rem',
|
||
border: `2px solid ${selectedCategory === category ? 'var(--accent-primary)' : 'var(--border-color)'}`,
|
||
background: selectedCategory === category ? 'var(--accent-primary)' : 'var(--bg-secondary)',
|
||
color: selectedCategory === category ? 'white' : 'var(--text-primary)',
|
||
borderRadius: '20px',
|
||
cursor: 'pointer',
|
||
fontWeight: 'bold',
|
||
transition: 'all 0.3s ease'
|
||
}}
|
||
>
|
||
{category}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
{/* Servers Grid */}
|
||
<section className="servers-grid" style={{
|
||
padding: '2rem',
|
||
maxWidth: '1200px',
|
||
margin: '0 auto'
|
||
}}>
|
||
<div style={{
|
||
display: 'grid',
|
||
gridTemplateColumns: 'repeat(auto-fit, minmax(350px, 1fr))',
|
||
gap: '2rem'
|
||
}}>
|
||
{filteredServers.map((server) => (
|
||
<div
|
||
key={`${server.ip}:${server.port}`}
|
||
className="server-card"
|
||
style={{
|
||
background: 'var(--bg-secondary)',
|
||
border: '1px solid var(--border-color)',
|
||
borderRadius: '12px',
|
||
padding: '1.5rem',
|
||
transition: 'all 0.3s ease',
|
||
cursor: 'pointer',
|
||
position: 'relative',
|
||
overflow: 'hidden'
|
||
}}
|
||
onMouseEnter={(e) => {
|
||
e.currentTarget.style.transform = 'translateY(-5px)'
|
||
e.currentTarget.style.boxShadow = '0 10px 25px rgba(0,0,0,0.2)'
|
||
}}
|
||
onMouseLeave={(e) => {
|
||
e.currentTarget.style.transform = 'translateY(0)'
|
||
e.currentTarget.style.boxShadow = 'none'
|
||
}}
|
||
>
|
||
{/* Status Indicator */}
|
||
<div style={{
|
||
position: 'absolute',
|
||
top: '1rem',
|
||
right: '1rem',
|
||
width: '12px',
|
||
height: '12px',
|
||
borderRadius: '50%',
|
||
backgroundColor: getStatusColor(server.status)
|
||
}} />
|
||
|
||
{/* Server Header */}
|
||
<div style={{ marginBottom: '1rem' }}>
|
||
<h3 style={{
|
||
fontSize: '1.2rem',
|
||
fontWeight: 'bold',
|
||
color: 'var(--text-primary)',
|
||
marginBottom: '0.5rem'
|
||
}}>
|
||
{server.name}
|
||
</h3>
|
||
<div style={{
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: '0.5rem',
|
||
fontSize: '0.9rem',
|
||
color: 'var(--text-secondary)'
|
||
}}>
|
||
<span>🗺️ {server.a2sData?.mapName || 'Loading...'}</span>
|
||
<span>•</span>
|
||
<span>{server.category}</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Server Stats */}
|
||
<div style={{
|
||
display: 'grid',
|
||
gridTemplateColumns: '1fr 1fr',
|
||
gap: '1rem',
|
||
marginBottom: '1rem'
|
||
}}>
|
||
<div>
|
||
<div style={{
|
||
fontSize: '0.8rem',
|
||
color: 'var(--text-secondary)',
|
||
marginBottom: '0.25rem'
|
||
}}>
|
||
{t('servers.players')}
|
||
</div>
|
||
<div style={{
|
||
fontSize: '1.1rem',
|
||
fontWeight: 'bold',
|
||
color: 'var(--text-primary)'
|
||
}}>
|
||
{server.a2sData ? `${server.a2sData.playerCount - server.a2sData.botCount}/${server.a2sData.maxPlayers}` : 'Loading...'}
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<div style={{
|
||
fontSize: '0.8rem',
|
||
color: 'var(--text-secondary)',
|
||
marginBottom: '0.25rem'
|
||
}}>
|
||
{t('servers.status')}
|
||
</div>
|
||
<div style={{
|
||
fontSize: '1.1rem',
|
||
fontWeight: 'bold',
|
||
color: server.status === 'online' ? '#10b981' : server.status === 'offline' ? '#ef4444' : '#f59e0b'
|
||
}}>
|
||
{server.status === 'online' ? 'Online' : server.status === 'offline' ? 'Offline' : 'Loading'}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Server Details */}
|
||
<div style={{ marginBottom: '1rem' }}>
|
||
<div style={{
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: '1rem',
|
||
fontSize: '0.9rem',
|
||
color: 'var(--text-secondary)',
|
||
marginBottom: '0.5rem'
|
||
}}>
|
||
<span><EFBFBD> {server.ip}:{server.port}</span>
|
||
<span style={{
|
||
background: 'var(--accent-primary)',
|
||
color: 'white',
|
||
padding: '0.25rem 0.5rem',
|
||
borderRadius: '4px',
|
||
fontSize: '0.8rem',
|
||
fontWeight: 'bold'
|
||
}}>
|
||
{server.category}
|
||
</span>
|
||
</div>
|
||
{server.error && (
|
||
<p style={{
|
||
fontSize: '0.9rem',
|
||
color: '#ef4444',
|
||
marginBottom: '1rem'
|
||
}}>
|
||
Error: {server.error}
|
||
</p>
|
||
)}
|
||
</div>
|
||
|
||
{/* Join Button */}
|
||
<button
|
||
style={{
|
||
width: '100%',
|
||
padding: '0.75rem',
|
||
background: server.status === 'online' ? 'var(--accent-primary)' : 'var(--text-secondary)',
|
||
color: 'white',
|
||
border: 'none',
|
||
borderRadius: '6px',
|
||
fontWeight: 'bold',
|
||
cursor: server.status === 'online' ? 'pointer' : 'not-allowed',
|
||
transition: 'all 0.3s ease'
|
||
}}
|
||
disabled={server.status !== 'online'}
|
||
onClick={() => {
|
||
if (server.status === 'online') {
|
||
// In a real app, this would connect to the server
|
||
alert(`Connecting to ${server.name}...`)
|
||
}
|
||
}}
|
||
>
|
||
{server.status === 'online' ? t('servers.joinServer') : t('servers.offline')}
|
||
</button>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</section>
|
||
|
||
{/* Server Stats Summary */}
|
||
<section className="server-stats" style={{
|
||
padding: '4rem 2rem',
|
||
background: 'var(--bg-secondary)',
|
||
textAlign: 'center'
|
||
}}>
|
||
<div style={{ maxWidth: '800px', margin: '0 auto' }}>
|
||
<h2 style={{
|
||
fontSize: '2rem',
|
||
fontWeight: 'bold',
|
||
marginBottom: '2rem',
|
||
color: 'var(--text-primary)'
|
||
}}>
|
||
{t('servers.serverStats')}
|
||
</h2>
|
||
<div style={{
|
||
display: 'grid',
|
||
gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))',
|
||
gap: '2rem'
|
||
}}>
|
||
<div>
|
||
<div style={{
|
||
fontSize: '2.5rem',
|
||
fontWeight: 'bold',
|
||
color: 'var(--accent-primary)',
|
||
marginBottom: '0.5rem'
|
||
}}>
|
||
{servers.filter(s => s.status === 'online').length}
|
||
</div>
|
||
<div style={{
|
||
color: 'var(--text-secondary)',
|
||
fontSize: '1.1rem'
|
||
}}>
|
||
{t('servers.onlineServers')}
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<div style={{
|
||
fontSize: '2.5rem',
|
||
fontWeight: 'bold',
|
||
color: 'var(--accent-primary)',
|
||
marginBottom: '0.5rem'
|
||
}}>
|
||
{servers.reduce((sum, s) => sum + (s.a2sData ? (s.a2sData.playerCount - s.a2sData.botCount) : 0), 0)}
|
||
</div>
|
||
<div style={{
|
||
color: 'var(--text-secondary)',
|
||
fontSize: '1.1rem'
|
||
}}>
|
||
{t('servers.totalPlayers')}
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<div style={{
|
||
fontSize: '2.5rem',
|
||
fontWeight: 'bold',
|
||
color: 'var(--accent-primary)',
|
||
marginBottom: '0.5rem'
|
||
}}>
|
||
{Array.from(new Set(servers.map(s => s.category))).length}
|
||
</div>
|
||
<div style={{
|
||
color: 'var(--text-secondary)',
|
||
fontSize: '1.1rem'
|
||
}}>
|
||
{t('servers.categories')}
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<div style={{
|
||
fontSize: '2.5rem',
|
||
fontWeight: 'bold',
|
||
color: 'var(--accent-primary)',
|
||
marginBottom: '0.5rem'
|
||
}}>
|
||
{servers.length}
|
||
</div>
|
||
<div style={{
|
||
color: 'var(--text-secondary)',
|
||
fontSize: '1.1rem'
|
||
}}>
|
||
{t('servers.totalServers')}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</Layout>
|
||
)
|
||
}
|
||
|
||
export default Servers |