diff --git a/frontend/src/components/HudBar.tsx b/frontend/src/components/HudBar.tsx index de4c374..6d392f7 100644 --- a/frontend/src/components/HudBar.tsx +++ b/frontend/src/components/HudBar.tsx @@ -15,7 +15,6 @@ function RegenTimer({ endurance, enduranceMax, lastEnduranceTs }: { endurance: n 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; @@ -24,8 +23,8 @@ function RegenTimer({ endurance, enduranceMax, lastEnduranceTs }: { endurance: n const sec = remainingSec % 60; return ( - - + + +1 dans {min}:{sec.toString().padStart(2, '0')} ); @@ -35,7 +34,7 @@ export function HudBar() { const { data: char } = useQuery({ queryKey: ['character'], queryFn: characterApi.me, - refetchInterval: 30_000, // refresh every 30s for endurance updates + refetchInterval: 30_000, }); useEffect(() => { @@ -56,41 +55,31 @@ export function HudBar() { const questReady = activeQuests?.filter((pq: any) => pq.status === 'completed').length ?? 0; return ( -
+
{/* Name + Level */} - - 🐾 - {char.name} - Niv.{char.level} + + 🐾 + {char.name} + Niv.{char.level} - | + | {/* HP */} - - - - {char.hpCurrent}/{char.hpMax} + + + + {char.hpCurrent}/{char.hpMax} - | + | {/* Endurance + timer */} - - - - {endurance}/{char.enduranceMax} + + + + {endurance}/{char.enduranceMax} {char.lastEnduranceTs && ( - | + | {/* XP */} - - - {char.xp}/{xpNext} + + + {char.xp}/{xpNext} - | + | {/* Gold */} - - + + {char.gold} - | + | {/* Quests */} - - 0 ? '#f4c94e' : '#6b7a99'} /> - {questCount} quĂȘte{questCount !== 1 ? 's' : ''} + + 0 ? 'text-rpg-gold' : 'text-rpg-muted'} /> + {questCount} quĂȘte{questCount !== 1 ? 's' : ''} {questReady > 0 && ( - ({questReady} prĂȘte{questReady > 1 ? 's' : ''} !) + ({questReady} prĂȘte{questReady > 1 ? 's' : ''} !) )}
diff --git a/frontend/src/index.css b/frontend/src/index.css index 9b0773a..7aed3e9 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -137,6 +137,7 @@ body { /* HudBar compact */ .hud-bar { font-size: 10px; gap: 6px; padding: 4px 8px; flex-wrap: wrap; } + .hud-regen { display: none; } /* Guide drawer full width mobile */ .guide-drawer { width: 100% !important; } @@ -147,3 +148,10 @@ body { /* Header compact */ .header-username { display: none; } } + +/* ── Ultra-compact mobile (petit Ă©cran) ── */ +@media (max-width: 480px) { + .hud-bar { gap: 4px; padding: 3px 6px; font-size: 9px; } + .hud-sep { display: none; } + .hud-label { display: none; } +} diff --git a/frontend/src/pages/QuestPage.tsx b/frontend/src/pages/QuestPage.tsx index fe1794f..34a3d04 100644 --- a/frontend/src/pages/QuestPage.tsx +++ b/frontend/src/pages/QuestPage.tsx @@ -1,7 +1,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { questApi } from '../api/endpoints'; import { Scroll, CheckCircle, Circle, Trophy, ChevronDown, ChevronRight, Star, Coins, Swords, Lock } from 'lucide-react'; -import { useState } from 'react'; +import { useState, useMemo } from 'react'; const OBJ_LABELS: Record = { kill_monster: 'Tuer', @@ -11,14 +11,9 @@ const OBJ_LABELS: Record = { forge_item: 'Forger', }; -function QuestCard({ pq, mode }: { pq: any; mode: 'active' | 'available' | 'completed' }) { +function useInvalidateQuests() { const qc = useQueryClient(); - const quest = mode === 'active' ? pq.quest : pq; - const progress = mode === 'active' ? pq.progress : 0; - const status = mode === 'active' ? pq.status : 'available'; - const pct = Math.min(100, Math.floor((progress / quest.objectiveCount) * 100)); - - const invalidateAll = () => { + return () => { qc.invalidateQueries({ queryKey: ['quests'] }); qc.invalidateQueries({ queryKey: ['questsActive'] }); qc.invalidateQueries({ queryKey: ['questsAvailable'] }); @@ -26,6 +21,14 @@ function QuestCard({ pq, mode }: { pq: any; mode: 'active' | 'available' | 'comp qc.invalidateQueries({ queryKey: ['questArcs'] }); qc.invalidateQueries({ queryKey: ['character'] }); }; +} + +function QuestCard({ pq, mode }: { pq: any; mode: 'active' | 'available' | 'completed' }) { + const invalidateAll = useInvalidateQuests(); + const quest = mode === 'active' ? pq.quest : pq; + const progress = mode === 'active' ? pq.progress : 0; + const status = mode === 'active' ? pq.status : 'available'; + const pct = Math.min(100, Math.floor((progress / quest.objectiveCount) * 100)); const acceptMut = useMutation({ mutationFn: () => questApi.accept(quest.id), @@ -46,72 +49,57 @@ function QuestCard({ pq, mode }: { pq: any; mode: 'active' | 'available' | 'comp const isClaimed = status === 'claimed'; return ( -
-
-
-
- {isClaimed ? : isCompleted ? : } - {quest.name} - {quest.repeatable && répétable} +
+
+
+
+ {isClaimed ? : isCompleted ? : } + {quest.name} + {quest.repeatable && répétable}
-

{quest.description}

+

{quest.description}

{/* Objectif */} -
+
{OBJ_LABELS[quest.objectiveType] ?? quest.objectiveType} — {mode === 'active' ? `${progress}/${quest.objectiveCount}` : `×${quest.objectiveCount}`}
{/* Progress bar (active quests only) */} {mode === 'active' && ( -
-
+
+
)} {/* Rewards */} -
- {quest.rewardXp} XP - {quest.rewardGold} or - {quest.rewardTitle && 🏅 {quest.rewardTitle}} +
+ {quest.rewardXp} XP + {quest.rewardGold} or + {quest.rewardTitle && 🏅 {quest.rewardTitle}} {quest.minLevel > 1 && Niv. {quest.minLevel}+}
{/* Actions */} {mode === 'available' && ( - )} {mode === 'active' && isCompleted && ( - )} {mode === 'active' && !isCompleted && ( - )} - {acceptMut.isError &&

{(acceptMut.error as Error).message}

} - {claimMut.isError &&

{(claimMut.error as Error).message}

} - {abandonMut.isError &&

{(abandonMut.error as Error).message}

} + {acceptMut.isError &&

{(acceptMut.error as Error).message}

} + {claimMut.isError &&

{(claimMut.error as Error).message}

} + {abandonMut.isError &&

{(abandonMut.error as Error).message}

}
); } @@ -137,68 +125,83 @@ function ArcQuestRow({ q }: { q: any }) { }); return ( -
+
{q.playerStatus === 'claimed' - ? + ? : q.playerStatus === 'completed' - ? + ? : q.playerStatus === 'active' - ? - : + ? + : } -
- {q.name} +
+ {q.name} {q.playerStatus === 'active' && ( - {q.progress}/{q.objectiveCount} + {q.progress}/{q.objectiveCount} )}
- {q.rewardXp} XP - {q.minLevel > 1 && !q.levelOk && Niv.{q.minLevel}} + {q.rewardXp} XP + {q.minLevel > 1 && !q.levelOk && Niv.{q.minLevel}} {/* Actions */} {q.canAccept && ( - )} {q.playerStatus === 'completed' && ( - )} - {acceptMut.isError && {(acceptMut.error as Error).message}} + {acceptMut.isError && {(acceptMut.error as Error).message}}
); } -function ArcSection({ arc }: { arc: any }) { - const [open, setOpen] = useState(true); +/** DĂ©termine si un arc doit ĂȘtre ouvert par dĂ©faut */ +function shouldArcBeOpen(arc: any): boolean { + if (!arc.zoneUnlocked) return false; + if (arc.completed) return false; + // Ouvert si au moins une quĂȘte est active ou prĂȘte Ă  rĂ©clamer + return arc.quests.some((q: any) => q.playerStatus === 'active' || q.playerStatus === 'completed'); +} + +function ArcSection({ arc, defaultOpen }: { arc: any; defaultOpen: boolean }) { + const [open, setOpen] = useState(defaultOpen); const { completed, total } = arc.progress; const locked = !arc.zoneUnlocked; + const pct = total > 0 ? Math.floor((completed / total) * 100) : 0; return ( -
+
setOpen(!open)} > - {locked ? : open ? : } - - + {locked ? : open ? : } + + {arc.name} - {completed}/{total} - {arc.completed && } - {locked && 🔒 ComplĂ©tez l'arc prĂ©cĂ©dent} + {completed}/{total} + {arc.completed && } + {locked && 🔒 ComplĂ©tez l'arc prĂ©cĂ©dent}
+ + {/* Progress bar */} + {!locked && ( +
+
+
+ )} + {open && !locked && ( <> -

{arc.description}

-
+

{arc.description}

+
{arc.quests.map((q: any) => )}
@@ -213,12 +216,21 @@ export function QuestPage() { const { data: arcs } = useQuery({ queryKey: ['questArcs'], queryFn: questApi.arcs }); const [showAllCombat, setShowAllCombat] = useState(false); - if (loadActive || loadAvail) return
Chargement

; + // Pré-calculer quels arcs sont ouverts par défaut (stable entre renders) + const arcDefaultOpen = useMemo(() => { + if (!arcs) return {}; + const map: Record = {}; + for (const arc of arcs) { + map[arc.id] = shouldArcBeOpen(arc); + } + return map; + }, [arcs]); + + if (loadActive || loadAvail) return
Chargement

; const isCraftQuest = (q: any) => ['forge_item', 'craft_item'].includes(q.objectiveType ?? q.quest?.objectiveType); const isCombatQuest = (q: any) => !isCraftQuest(q); - // Split by category const activeAll = active ?? []; const activeCombat = activeAll.filter((pq: any) => !pq.quest.repeatable && isCombatQuest(pq)); const activeCraft = activeAll.filter((pq: any) => !pq.quest.repeatable && isCraftQuest(pq)); @@ -233,37 +245,36 @@ export function QuestPage() { return (
-

📜 QuĂȘtes

+

📜 QuĂȘtes

-
+
{/* Active combat quests */}
-

+

QuĂȘtes actives ({activeCombat.length}/3)

{activeCombat.length > 0 ? ( -
+
{activeCombat.map((pq: any) => )}
) : ( -
+
Aucune quĂȘte active — acceptez-en Ă  droite
)}
- {/* Available combat quests (staggered) */} + {/* Available combat quests */}
-

+

QuĂȘtes de combat

{shownCombat.length > 0 ? ( -
+
{shownCombat.map((q: any) => )} {hiddenCount > 0 && (
) : ( -
+
Toutes les quĂȘtes de combat sont complĂ©tĂ©es
)}
- {/* MĂ©tiers (craft/forge — hors pool, comme les dailies) */} + {/* MĂ©tiers */} {(activeCraft.length > 0 || availableCraft.length > 0) && ( -
-

- 🔹 MĂ©tiers -

-
+
+

🔹 MĂ©tiers

+
{activeCraft.map((pq: any) => )} {availableCraft.map((q: any) => )}
)} - {/* TĂąches quotidiennes (rĂ©pĂ©tables — toujours en fond) */} -
-

- 🔄 Tñches quotidiennes -

-
+ {/* TĂąches quotidiennes */} +
+

🔄 Tñches quotidiennes

+
{activeDaily.map((pq: any) => )} {availableDaily.map((q: any) => )}
@@ -304,11 +311,11 @@ export function QuestPage() { {/* Arcs narratifs */} {arcs && arcs.length > 0 && ( -
-

- 📖 Arcs narratifs -

- {arcs.map((arc: any) => )} +
+

📖 Arcs narratifs

+ {arcs.map((arc: any) => ( + + ))}
)}