Files
echoes-of-the-ash/pwa/src/components/GameHeader.tsx

212 lines
7.0 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 { 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>
)
}