feat: HUD bar — stats persistantes sous le header
All checks were successful
CI/CD — Build & Deploy / Build & Deploy (push) Successful in 32s
All checks were successful
CI/CD — Build & Deploy / Build & Deploy (push) Successful in 32s
Barre compacte toujours visible : nom+level, HP, endurance+timer regen, XP, or, quêtes actives (avec compteur "prêtes !"). Timer live : "+1 dans X:XX" quand endurance < max. Auto-refresh 30s pour l'endurance, 60s pour les quêtes. Chaque section cliquable vers la page correspondante.
This commit is contained in:
128
frontend/src/components/HudBar.tsx
Normal file
128
frontend/src/components/HudBar.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { characterApi, questApi } from '../api/endpoints';
|
||||
import { Heart, Zap, Star, Coins, Scroll, Clock } from 'lucide-react';
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
function RegenTimer({ endurance, enduranceMax, lastEnduranceTs }: { endurance: number; enduranceMax: number; lastEnduranceTs: string }) {
|
||||
const [now, setNow] = useState(Date.now());
|
||||
|
||||
useEffect(() => {
|
||||
if (endurance >= enduranceMax) return;
|
||||
const id = setInterval(() => setNow(Date.now()), 1000);
|
||||
return () => clearInterval(id);
|
||||
}, [endurance, enduranceMax]);
|
||||
|
||||
if (endurance >= enduranceMax) return null;
|
||||
|
||||
// Regen = 1pt every 3min = 180s
|
||||
const elapsedMs = now - new Date(lastEnduranceTs).getTime();
|
||||
const elapsedInCycle = elapsedMs % (3 * 60 * 1000);
|
||||
const remainingMs = 3 * 60 * 1000 - elapsedInCycle;
|
||||
const remainingSec = Math.max(0, Math.floor(remainingMs / 1000));
|
||||
const min = Math.floor(remainingSec / 60);
|
||||
const sec = remainingSec % 60;
|
||||
|
||||
return (
|
||||
<span style={{ fontSize: 9, color: '#5ba4f5' }}>
|
||||
<Clock size={8} style={{ display: 'inline', marginRight: 2 }} />
|
||||
+1 dans {min}:{sec.toString().padStart(2, '0')}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export function HudBar() {
|
||||
const { data: char } = useQuery({
|
||||
queryKey: ['character'],
|
||||
queryFn: characterApi.me,
|
||||
refetchInterval: 30_000, // refresh every 30s for endurance updates
|
||||
});
|
||||
|
||||
const { data: activeQuests } = useQuery({
|
||||
queryKey: ['questsActive'],
|
||||
queryFn: questApi.active,
|
||||
refetchInterval: 60_000,
|
||||
});
|
||||
|
||||
if (!char) return null;
|
||||
|
||||
const endurance = (char as any).enduranceCurrent ?? (char as any).endurance ?? 0;
|
||||
const xpNext = (char as any).xpToNextLevel ?? Math.round(100 * Math.pow(char.level, 1.5));
|
||||
const questCount = activeQuests?.filter((pq: any) => pq.status === 'active').length ?? 0;
|
||||
const questReady = activeQuests?.filter((pq: any) => pq.status === 'completed').length ?? 0;
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
background: '#111620',
|
||||
borderBottom: '1px solid #1e2535',
|
||||
padding: '4px 1rem',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 16,
|
||||
fontSize: 11,
|
||||
color: '#6b7a99',
|
||||
flexWrap: 'wrap',
|
||||
}}>
|
||||
{/* Name + Level */}
|
||||
<Link to="/dashboard" style={{ textDecoration: 'none', display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<span style={{ fontSize: 14 }}>🐸</span>
|
||||
<span style={{ fontWeight: 700, color: '#dce4f0', fontSize: 12 }}>{char.name}</span>
|
||||
<span style={{ color: '#6b7a99' }}>Niv.{char.level}</span>
|
||||
</Link>
|
||||
|
||||
<span style={{ color: '#2a3448' }}>|</span>
|
||||
|
||||
{/* HP */}
|
||||
<Link to="/dashboard" style={{ textDecoration: 'none', display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<Heart size={10} color="#e84040" />
|
||||
<span style={{ color: char.hpCurrent < char.hpMax ? '#e84040' : '#6b7a99' }}>
|
||||
{char.hpCurrent}/{char.hpMax}
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
<span style={{ color: '#2a3448' }}>|</span>
|
||||
|
||||
{/* Endurance + timer */}
|
||||
<Link to="/dashboard" style={{ textDecoration: 'none', display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<Zap size={10} color="#5ba4f5" />
|
||||
<span style={{ color: endurance < 5 ? '#e84040' : '#6b7a99' }}>
|
||||
{endurance}/{char.enduranceMax}
|
||||
</span>
|
||||
{(char as any).lastEnduranceTs && (
|
||||
<RegenTimer
|
||||
endurance={endurance}
|
||||
enduranceMax={char.enduranceMax}
|
||||
lastEnduranceTs={(char as any).lastEnduranceTs}
|
||||
/>
|
||||
)}
|
||||
</Link>
|
||||
|
||||
<span style={{ color: '#2a3448' }}>|</span>
|
||||
|
||||
{/* XP */}
|
||||
<Link to="/dashboard" style={{ textDecoration: 'none', display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<Star size={10} color="#a78bfa" />
|
||||
<span>{char.xp}/{xpNext}</span>
|
||||
</Link>
|
||||
|
||||
<span style={{ color: '#2a3448' }}>|</span>
|
||||
|
||||
{/* Gold */}
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<Coins size={10} color="#f4c94e" />
|
||||
<span>{char.gold}</span>
|
||||
</span>
|
||||
|
||||
<span style={{ color: '#2a3448' }}>|</span>
|
||||
|
||||
{/* Quests */}
|
||||
<Link to="/quests" style={{ textDecoration: 'none', display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<Scroll size={10} color={questReady > 0 ? '#f4c94e' : '#6b7a99'} />
|
||||
<span>{questCount} quête{questCount !== 1 ? 's' : ''}</span>
|
||||
{questReady > 0 && (
|
||||
<span style={{ color: '#f4c94e', fontWeight: 700 }}>({questReady} prête{questReady > 1 ? 's' : ''} !)</span>
|
||||
)}
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import { Swords, Package, Hammer, User, LogOut, Shield, Scroll } from 'lucide-react';
|
||||
import { HudBar } from './HudBar';
|
||||
|
||||
const NAV = [
|
||||
{ to: '/dashboard', icon: User, label: 'Personnage' },
|
||||
@@ -44,6 +45,7 @@ export function Layout({ children }: { children: React.ReactNode }) {
|
||||
)}
|
||||
</header>
|
||||
|
||||
<HudBar />
|
||||
<div style={{ display: 'flex', flex: 1 }}>
|
||||
{/* Sidebar nav */}
|
||||
<nav style={{
|
||||
|
||||
Reference in New Issue
Block a user