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, useMemo } from 'react'; const OBJ_LABELS: Record = { kill_monster: 'Tuer', kill_any: 'Gagner des combats', gather_material: 'Récolter', craft_item: 'Crafter', forge_item: 'Forger', }; function useInvalidateQuests() { const qc = useQueryClient(); return () => { qc.invalidateQueries({ queryKey: ['quests'] }); qc.invalidateQueries({ queryKey: ['questsActive'] }); qc.invalidateQueries({ queryKey: ['questsAvailable'] }); qc.invalidateQueries({ queryKey: ['questsCompleted'] }); 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), onSuccess: invalidateAll, }); const claimMut = useMutation({ mutationFn: () => questApi.claim(pq.id), onSuccess: invalidateAll, }); const abandonMut = useMutation({ mutationFn: () => questApi.abandon(pq.id), onSuccess: invalidateAll, }); const isCompleted = status === 'completed'; const isClaimed = status === 'claimed'; return (
{isClaimed ? : isCompleted ? : } {quest.name} {quest.repeatable && répétable}

{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.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}

}
); } function ArcQuestRow({ q }: { q: any }) { const qc = useQueryClient(); const acceptMut = useMutation({ mutationFn: () => questApi.accept(q.id), onSuccess: () => { qc.invalidateQueries({ queryKey: ['questArcs'] }); qc.invalidateQueries({ queryKey: ['questsActive'] }); }, }); const claimMut = useMutation({ mutationFn: () => questApi.claim(q.playerQuestId), onSuccess: () => { qc.invalidateQueries({ queryKey: ['questArcs'] }); qc.invalidateQueries({ queryKey: ['questsActive'] }); qc.invalidateQueries({ queryKey: ['character'] }); }, }); return (
{q.playerStatus === 'claimed' ? : q.playerStatus === 'completed' ? : q.playerStatus === 'active' ? : }
{q.name} {q.playerStatus === 'active' && ( {q.progress}/{q.objectiveCount} )}
{q.rewardXp} XP {q.minLevel > 1 && !q.levelOk && Niv.{q.minLevel}} {/* Actions */} {q.canAccept && ( )} {q.playerStatus === 'completed' && ( )} {acceptMut.isError && {(acceptMut.error as Error).message}}
); } /** 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 ? : } {arc.name} {completed}/{total} {arc.completed && } {locked && 🔒 Complétez l'arc précédent}
{/* Progress bar */} {!locked && (
)} {open && !locked && ( <>

{arc.description}

{arc.quests.map((q: any) => )}
)}
); } export function QuestPage() { const { data: active, isLoading: loadActive } = useQuery({ queryKey: ['questsActive'], queryFn: questApi.active }); const { data: available, isLoading: loadAvail } = useQuery({ queryKey: ['questsAvailable'], queryFn: questApi.available }); const { data: arcs } = useQuery({ queryKey: ['questArcs'], queryFn: questApi.arcs }); const [showAllCombat, setShowAllCombat] = useState(false); // 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); 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)); const activeDaily = activeAll.filter((pq: any) => pq.quest.repeatable); const availableAll = available ?? []; const availableCombat = availableAll.filter((q: any) => !q.repeatable && isCombatQuest(q)); const availableCraft = availableAll.filter((q: any) => !q.repeatable && isCraftQuest(q)); const availableDaily = availableAll.filter((q: any) => q.repeatable); const shownCombat = showAllCombat ? availableCombat : availableCombat.slice(0, 3); const hiddenCount = availableCombat.length - 3; return (

📜 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 */}

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 */} {(activeCraft.length > 0 || availableCraft.length > 0) && (

🔨 Métiers

{activeCraft.map((pq: any) => )} {availableCraft.map((q: any) => )}
)} {/* Tâches quotidiennes */}

🔄 Tâches quotidiennes

{activeDaily.map((pq: any) => )} {availableDaily.map((q: any) => )}
{/* Arcs narratifs */} {arcs && arcs.length > 0 && (

📖 Arcs narratifs

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