All checks were successful
CI - Build and Push / Build and Push Docker Image (push) Successful in 13s
431 lines
15 KiB
TypeScript
431 lines
15 KiB
TypeScript
import { useTranslation } from 'react-i18next'
|
||
import { useState } from 'react'
|
||
import Layout from '../components/Layout'
|
||
import { useServers } from '../contexts/ServerContext'
|
||
import '../App.css'
|
||
|
||
// Loading Skeleton Component
|
||
const LoadingSkeleton = ({ width = '60px', height = '40px', className = '', style = {} }: { width?: string, height?: string, className?: string, style?: React.CSSProperties }) => (
|
||
<div
|
||
className={`loading-skeleton ${className}`}
|
||
style={{
|
||
width,
|
||
height,
|
||
background: 'linear-gradient(135deg, var(--bg-secondary) 0%, var(--border-color) 50%, var(--bg-secondary) 100%)',
|
||
backgroundSize: '200% 200%',
|
||
animation: 'loading-shimmer 2.5s infinite ease-in-out, loading-pulse 1.5s infinite ease-in-out',
|
||
borderRadius: '8px',
|
||
position: 'relative',
|
||
overflow: 'hidden',
|
||
boxShadow: '0 2px 8px rgba(0,0,0,0.08)',
|
||
...style
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
position: 'absolute',
|
||
top: 0,
|
||
left: '-100%',
|
||
width: '100%',
|
||
height: '100%',
|
||
background: 'linear-gradient(90deg, transparent, rgba(255,255,255,0.12), transparent)',
|
||
animation: 'loading-wave 2.5s infinite ease-in-out'
|
||
}}
|
||
/>
|
||
</div>
|
||
)
|
||
|
||
function Servers() {
|
||
const { t } = useTranslation()
|
||
const { servers, loading } = useServers()
|
||
const [selectedCategory, setSelectedCategory] = useState<string>('All')
|
||
|
||
// Filter servers by category
|
||
const filteredServers = selectedCategory === 'All'
|
||
? servers
|
||
: servers.filter(server => server.category === selectedCategory)
|
||
|
||
const categories = ['All', ...Array.from(new Set(servers.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'
|
||
}}>
|
||
{loading ? (
|
||
// Show loading skeletons
|
||
servers.length > 0 ? servers.map((_, index) => (
|
||
<div
|
||
key={`loading-${index}`}
|
||
className="server-card"
|
||
style={{
|
||
background: 'var(--bg-secondary)',
|
||
border: '1px solid var(--border-color)',
|
||
borderRadius: '12px',
|
||
padding: '1.5rem',
|
||
position: 'relative',
|
||
overflow: 'hidden'
|
||
}}
|
||
>
|
||
{/* Status Indicator Skeleton */}
|
||
<div style={{
|
||
position: 'absolute',
|
||
top: '1rem',
|
||
right: '1rem',
|
||
width: '12px',
|
||
height: '12px',
|
||
borderRadius: '50%',
|
||
backgroundColor: 'var(--border-color)'
|
||
}} />
|
||
|
||
{/* Server Header Skeleton */}
|
||
<div style={{ marginBottom: '1rem' }}>
|
||
<LoadingSkeleton width="180px" height="24px" style={{ marginBottom: '0.5rem' }} />
|
||
<div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center' }}>
|
||
<LoadingSkeleton width="60px" height="16px" />
|
||
<LoadingSkeleton width="40px" height="16px" />
|
||
</div>
|
||
</div>
|
||
|
||
{/* Server Stats Skeleton */}
|
||
<div style={{
|
||
display: 'grid',
|
||
gridTemplateColumns: '1fr 1fr',
|
||
gap: '1rem',
|
||
marginBottom: '1rem'
|
||
}}>
|
||
<div>
|
||
<LoadingSkeleton width="50px" height="14px" style={{ marginBottom: '0.25rem' }} />
|
||
<LoadingSkeleton width="40px" height="20px" />
|
||
</div>
|
||
<div>
|
||
<LoadingSkeleton width="50px" height="14px" style={{ marginBottom: '0.25rem' }} />
|
||
<LoadingSkeleton width="40px" height="20px" />
|
||
</div>
|
||
</div>
|
||
|
||
{/* Server Details Skeleton */}
|
||
<div style={{ marginBottom: '1rem' }}>
|
||
<LoadingSkeleton width="120px" height="16px" style={{ marginBottom: '0.5rem' }} />
|
||
</div>
|
||
|
||
{/* Join Button Skeleton */}
|
||
<LoadingSkeleton width="100%" height="40px" />
|
||
</div>
|
||
)) : []
|
||
) : (
|
||
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.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.status === 'online' ? server.playerCount : '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>
|
||
</div>
|
||
|
||
{/* Join Button */}
|
||
<button
|
||
style={{
|
||
width: '100%',
|
||
padding: '0.75rem',
|
||
background: 'transparent',
|
||
color: server.status === 'online' ? 'var(--accent-primary)' : 'var(--text-secondary)',
|
||
border: `2px solid ${server.status === 'online' ? 'var(--accent-primary)' : 'var(--text-secondary)'}`,
|
||
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') {
|
||
window.open(`steam://connect/${server.ip}:${server.port}`)
|
||
}
|
||
}}
|
||
>
|
||
{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.status === 'online' ? s.playerCount : 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 |