212 lines
7.0 KiB
TypeScript
212 lines
7.0 KiB
TypeScript
import { useState, useEffect, useCallback } from 'react'
|
||
import { useNavigate, useLocation } from 'react-router-dom'
|
||
import { useAuth } from '../hooks/useAuth'
|
||
import { useGameWebSocket } from '../hooks/useGameWebSocket'
|
||
import api from '../services/api'
|
||
import { useTranslation } from 'react-i18next'
|
||
import LanguageSelector from './LanguageSelector'
|
||
import { useOptionalGame } from '../contexts/GameContext'
|
||
import { getTranslatedText } from '../utils/i18nUtils'
|
||
import './Game.css'
|
||
|
||
import { GameTooltip } from './common/GameTooltip'
|
||
|
||
// Import the new specific header styles
|
||
import './GameHeader.css'
|
||
|
||
interface CombatInfo {
|
||
enemyName: string
|
||
yourTurn: boolean
|
||
}
|
||
|
||
interface GameHeaderProps {
|
||
className?: string
|
||
locationName?: string
|
||
combatInfo?: CombatInfo | null
|
||
dangerLevel?: number
|
||
}
|
||
|
||
export default function GameHeader({
|
||
className = ''
|
||
}: GameHeaderProps) {
|
||
const navigate = useNavigate()
|
||
const location = useLocation()
|
||
const { currentCharacter, logout } = useAuth()
|
||
const { t } = useTranslation()
|
||
const [playerCount, setPlayerCount] = useState<number>(0)
|
||
const [isFullscreen, setIsFullscreen] = useState<boolean>(!!document.fullscreenElement)
|
||
|
||
// Fullscreen toggle
|
||
const toggleFullscreen = useCallback(() => {
|
||
if (!document.fullscreenElement) {
|
||
document.documentElement.requestFullscreen().catch((err) => {
|
||
console.error('Failed to enter fullscreen:', err)
|
||
})
|
||
} else {
|
||
document.exitFullscreen().catch((err) => {
|
||
console.error('Failed to exit fullscreen:', err)
|
||
})
|
||
}
|
||
}, [])
|
||
|
||
useEffect(() => {
|
||
const handleFullscreenChange = () => {
|
||
setIsFullscreen(!!document.fullscreenElement)
|
||
}
|
||
document.addEventListener('fullscreenchange', handleFullscreenChange)
|
||
return () => document.removeEventListener('fullscreenchange', handleFullscreenChange)
|
||
}, [])
|
||
|
||
// Get game state from context (undefined when outside GameProvider)
|
||
const gameContext = useOptionalGame()
|
||
const gameState = gameContext?.state
|
||
|
||
// Extract location and combat info from game state
|
||
const locationName = gameState?.location?.name ? getTranslatedText(gameState.location.name) : undefined
|
||
const dangerLevel = gameState?.location?.danger_level
|
||
const combatInfo = gameState?.combatState ? {
|
||
enemyName: getTranslatedText(gameState.enemyName) || 'Enemy',
|
||
yourTurn: gameState.combatState.yourTurn || false
|
||
} : null
|
||
|
||
// Fetch initial player count
|
||
useEffect(() => {
|
||
const fetchPlayerCount = async () => {
|
||
try {
|
||
const response = await api.get('/api/statistics/online-players')
|
||
if (response.data && typeof response.data.count === 'number') {
|
||
setPlayerCount(response.data.count)
|
||
}
|
||
} catch (error) {
|
||
console.error('Failed to fetch player count:', error)
|
||
}
|
||
}
|
||
|
||
fetchPlayerCount()
|
||
}, [])
|
||
|
||
// Connect to WebSocket for player count updates
|
||
// We use a separate connection here to ensure the header always has live data
|
||
// regardless of which page is active (Game, Leaderboards, Profile)
|
||
const token = localStorage.getItem('token')
|
||
|
||
useGameWebSocket({
|
||
token,
|
||
enabled: !!token,
|
||
onMessage: (message) => {
|
||
if (message.type === 'player_count_update' && message.data?.count !== undefined) {
|
||
//console.log('🔢 GameHeader received count update:', message.data.count)
|
||
setPlayerCount(message.data.count)
|
||
}
|
||
}
|
||
})
|
||
|
||
const isActive = (path: string) => {
|
||
return location.pathname === path || location.pathname.startsWith(path)
|
||
}
|
||
|
||
const isOnOwnProfile = location.pathname === `/profile/${currentCharacter?.id}`
|
||
|
||
// Helper for danger badge class
|
||
const getDangerClass = (level: number | undefined) => {
|
||
if (level === undefined || level === 0) return 'danger-safe'
|
||
return `danger-${Math.min(level, 5)}`
|
||
}
|
||
|
||
return (
|
||
<header className={`game-header ${className}`}>
|
||
{/* Left: Logo and Version */}
|
||
<div className="header-left">
|
||
<div className="header-title-container">
|
||
<h1>Echoes of the Ash</h1>
|
||
<span className="header-version">v0.9</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Center: Location/Combat Title */}
|
||
<div className="header-center">
|
||
{combatInfo ? (
|
||
<div className="header-location-title combat">
|
||
<span className="combat-indicator">⚔️</span>
|
||
<span className="location-name">{combatInfo.enemyName}</span>
|
||
<span className={`turn-indicator ${combatInfo.yourTurn ? 'your-turn' : 'enemy-turn'}`}>
|
||
{combatInfo.yourTurn ? t('combat.yourTurn') : t('combat.enemyTurn')}
|
||
</span>
|
||
</div>
|
||
) : locationName ? (
|
||
<div className="header-location-title">
|
||
<span className="location-name">{locationName}</span>
|
||
{dangerLevel !== undefined && (
|
||
<span className={`danger-badge ${getDangerClass(dangerLevel)}`}>
|
||
{dangerLevel === 0 ? t('danger.safe') : `⚠️ ${dangerLevel}`}
|
||
</span>
|
||
)}
|
||
</div>
|
||
) : null}
|
||
</div>
|
||
|
||
{/* Right: Navigation + User Info */}
|
||
<div className="header-right">
|
||
<nav className="nav-links">
|
||
<button
|
||
onClick={() => navigate('/game')}
|
||
className={`nav-link ${isActive('/game') ? 'active' : ''}`}
|
||
>
|
||
🎮 {t('common.game')}
|
||
</button>
|
||
<button
|
||
onClick={() => navigate('/leaderboards')}
|
||
className={`nav-link ${isActive('/leaderboards') ? 'active' : ''}`}
|
||
>
|
||
🏆 {t('common.leaderboards')}
|
||
</button>
|
||
</nav>
|
||
|
||
<GameTooltip content={t('game.onlineCount', { count: playerCount })}>
|
||
<div className="player-count-badge">
|
||
<span className="status-dot"></span>
|
||
<span className="count-text">{playerCount}</span>
|
||
</div>
|
||
</GameTooltip>
|
||
|
||
<LanguageSelector />
|
||
|
||
<GameTooltip content={isFullscreen ? t('common.exitFullscreen', 'Exit Fullscreen') : t('common.fullscreen', 'Fullscreen')}>
|
||
<button
|
||
onClick={toggleFullscreen}
|
||
className="header-icon-btn"
|
||
aria-label={isFullscreen ? 'Exit Fullscreen' : 'Enter Fullscreen'}
|
||
>
|
||
{isFullscreen ? '🗗' : '🗖'}
|
||
</button>
|
||
</GameTooltip>
|
||
|
||
<button
|
||
onClick={() => navigate(`/profile/${currentCharacter?.id}`)}
|
||
className={`username-link ${isOnOwnProfile ? 'active' : ''}`}
|
||
>
|
||
{currentCharacter?.name}
|
||
</button>
|
||
|
||
<div className="header-sep"></div>
|
||
|
||
<GameTooltip content={t('common.account')}>
|
||
<button
|
||
onClick={() => navigate('/account')}
|
||
className="header-icon-btn"
|
||
aria-label={t('common.account')}
|
||
>
|
||
⚙️
|
||
</button>
|
||
</GameTooltip>
|
||
|
||
<GameTooltip content={t('auth.logout')}>
|
||
<button onClick={logout} className="header-icon-btn" aria-label={t('auth.logout')}>
|
||
🚪
|
||
</button>
|
||
</GameTooltip>
|
||
</div>
|
||
</header>
|
||
)
|
||
}
|