From cfdc5c9b02b3257cdaf582618c9beb9bdfd9deec Mon Sep 17 00:00:00 2001 From: Tetardtek Date: Tue, 24 Mar 2026 17:03:31 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20HUD=20bar=20=E2=80=94=20stats=20persist?= =?UTF-8?q?antes=20sous=20le=20header?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- frontend/src/components/HudBar.tsx | 128 +++++++++++++++++++++++++++++ frontend/src/components/Layout.tsx | 2 + 2 files changed, 130 insertions(+) create mode 100644 frontend/src/components/HudBar.tsx diff --git a/frontend/src/components/HudBar.tsx b/frontend/src/components/HudBar.tsx new file mode 100644 index 0000000..93011ce --- /dev/null +++ b/frontend/src/components/HudBar.tsx @@ -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 ( + + + +1 dans {min}:{sec.toString().padStart(2, '0')} + + ); +} + +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 ( +
+ {/* Name + Level */} + + 🐸 + {char.name} + Niv.{char.level} + + + | + + {/* HP */} + + + + {char.hpCurrent}/{char.hpMax} + + + + | + + {/* Endurance + timer */} + + + + {endurance}/{char.enduranceMax} + + {(char as any).lastEnduranceTs && ( + + )} + + + | + + {/* XP */} + + + {char.xp}/{xpNext} + + + | + + {/* Gold */} + + + {char.gold} + + + | + + {/* Quests */} + + 0 ? '#f4c94e' : '#6b7a99'} /> + {questCount} quête{questCount !== 1 ? 's' : ''} + {questReady > 0 && ( + ({questReady} prête{questReady > 1 ? 's' : ''} !) + )} + +
+ ); +} diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index 42ee373..27b3f56 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -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 }) { )} +
{/* Sidebar nav */}