Files
www.cialloo.com/src/pages/Servers.tsx

427 lines
14 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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