From 4d254692b07c3a10866adb298d06c1f6f699fcb2 Mon Sep 17 00:00:00 2001 From: Tetardtek Date: Tue, 24 Mar 2026 17:36:20 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20page=20Achievements=20+=20soins=20renom?= =?UTF-8?q?m=C3=A9=20+=20bouton=20soins=20en=20combat?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Page /achievements : 20 succès groupés par catégorie (Combat, Progression, Économie, Équipement), progress bars, paliers bronze/silver/gold, bouton réclamer, compteur débloqués/total. Renommage "repos" → "soins" partout (dashboard, budget, messages). Bouton soins ajouté dans la page combat (accès rapide entre les fights). Icône Trophy dans la sidebar pour les succès. --- frontend/src/App.tsx | 2 + frontend/src/components/Layout.tsx | 15 +- frontend/src/pages/AchievementsPage.tsx | 208 ++++++++++++++++++++++++ frontend/src/pages/CombatPage.tsx | 24 ++- frontend/src/pages/DashboardPage.tsx | 6 +- 5 files changed, 244 insertions(+), 11 deletions(-) create mode 100644 frontend/src/pages/AchievementsPage.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index aca5b1e..6ae75d2 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -10,6 +10,7 @@ import { InventoryPage } from './pages/InventoryPage'; import { CraftPage } from './pages/CraftPage'; import { ForgePage } from './pages/ForgePage'; import { QuestPage } from './pages/QuestPage'; +import { AchievementsPage } from './pages/AchievementsPage'; const qc = new QueryClient({ defaultOptions: { queries: { retry: 1, staleTime: 30_000 } } }); @@ -35,6 +36,7 @@ function AppRoutes() { } /> } /> } /> + } /> } /> ); diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index 27b3f56..60e829a 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -1,15 +1,16 @@ import { Link, useLocation } from 'react-router-dom'; import { useAuth } from '../context/AuthContext'; -import { Swords, Package, Hammer, User, LogOut, Shield, Scroll } from 'lucide-react'; +import { Swords, Package, Hammer, User, LogOut, Shield, Scroll, Trophy } from 'lucide-react'; import { HudBar } from './HudBar'; const NAV = [ - { to: '/dashboard', icon: User, label: 'Personnage' }, - { to: '/quests', icon: Scroll, label: 'Quêtes' }, - { to: '/combat', icon: Swords, label: 'Combat' }, - { to: '/inventory', icon: Package, label: 'Inventaire' }, - { to: '/craft', icon: Hammer, label: 'Artisanat' }, - { to: '/forge', icon: Shield, label: 'Forge' }, + { to: '/dashboard', icon: User, label: 'Personnage' }, + { to: '/quests', icon: Scroll, label: 'Quêtes' }, + { to: '/combat', icon: Swords, label: 'Combat' }, + { to: '/inventory', icon: Package, label: 'Inventaire' }, + { to: '/craft', icon: Hammer, label: 'Artisanat' }, + { to: '/forge', icon: Shield, label: 'Forge' }, + { to: '/achievements', icon: Trophy, label: 'Succès' }, ]; export function Layout({ children }: { children: React.ReactNode }) { diff --git a/frontend/src/pages/AchievementsPage.tsx b/frontend/src/pages/AchievementsPage.tsx new file mode 100644 index 0000000..31d09ff --- /dev/null +++ b/frontend/src/pages/AchievementsPage.tsx @@ -0,0 +1,208 @@ +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { api } from '../api/client'; +import { Trophy, Lock, CheckCircle, Gift, Star, Coins } from 'lucide-react'; + +const CATEGORY_LABELS: Record = { + combat: { label: 'Combat', emoji: '⚔️' }, + progression: { label: 'Progression', emoji: '⭐' }, + economy: { label: 'Économie', emoji: '💰' }, + equipment: { label: 'Équipement', emoji: '🔨' }, +}; + +const TIER_COLORS: Record = { + bronze: '#cd7f32', + silver: '#c0c0c0', + gold: '#f4c94e', +}; + +interface AchievementProgress { + id: string; + key: string; + name: string; + description: string; + category: string; + tier: string; + criteriaType: string; + criteriaValue: number; + rewardGold: number; + rewardTitle: string | null; + progress: number; + unlocked: boolean; + unlockedAt: string | null; + claimed: boolean; + percentage: number; +} + +function AchievementCard({ a }: { a: AchievementProgress }) { + const qc = useQueryClient(); + + const claimMut = useMutation({ + mutationFn: () => api.post(`/achievements/claim/${a.id}`), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['achievements'] }); + qc.invalidateQueries({ queryKey: ['character'] }); + }, + }); + + const tierColor = TIER_COLORS[a.tier] ?? '#6b7a99'; + const canClaim = a.unlocked && !a.claimed; + + return ( +
+
+ {/* Icon */} +
+ {a.claimed ? + : a.unlocked ? + : + } +
+ + {/* Content */} +
+
+ + {a.name} + + + {a.tier} + +
+

{a.description}

+ + {/* Progress bar */} + {!a.claimed && ( +
+
+ {a.progress} / {a.criteriaValue} + {a.percentage}% +
+
+
+
+
+ )} + + {/* Rewards */} +
+ {a.rewardGold > 0 && ( + + {a.rewardGold} or + + )} + {a.rewardTitle && ( + + Titre : {a.rewardTitle} + + )} +
+ + {/* Claim button */} + {canClaim && ( + + )} + {a.claimed && ( +
+ Réclamé +
+ )} +
+
+
+ ); +} + +export function AchievementsPage() { + const { data: achievements, isLoading } = useQuery({ + queryKey: ['achievements'], + queryFn: () => api.get('/achievements/me'), + }); + + if (isLoading) return
Chargement…
; + if (!achievements) return
Aucun succès
; + + // Group by category + const categories = new Map(); + for (const a of achievements) { + const list = categories.get(a.category) ?? []; + list.push(a); + categories.set(a.category, list); + } + + const totalUnlocked = achievements.filter(a => a.unlocked).length; + const totalClaimed = achievements.filter(a => a.claimed).length; + const claimable = achievements.filter(a => a.unlocked && !a.claimed).length; + + return ( +
+

🏆 Succès

+ + {/* Summary */} +
+
+
{totalUnlocked}
+
Débloqués
+
+
+
{totalClaimed}
+
Réclamés
+
+
+
{achievements.length}
+
Total
+
+ {claimable > 0 && ( +
+
🎁 {claimable} à réclamer !
+
+ )} +
+ + {/* Categories */} + {Array.from(categories.entries()).map(([cat, achs]) => { + const info = CATEGORY_LABELS[cat] ?? { label: cat, emoji: '📋' }; + return ( +
+

+ {info.emoji} {info.label} +

+
+ {achs + .sort((a, b) => { + const tierOrder = { bronze: 0, silver: 1, gold: 2 }; + return (tierOrder[a.tier as keyof typeof tierOrder] ?? 0) - (tierOrder[b.tier as keyof typeof tierOrder] ?? 0); + }) + .map(a => ) + } +
+
+ ); + })} +
+ ); +} diff --git a/frontend/src/pages/CombatPage.tsx b/frontend/src/pages/CombatPage.tsx index 478909b..729f1c1 100644 --- a/frontend/src/pages/CombatPage.tsx +++ b/frontend/src/pages/CombatPage.tsx @@ -2,9 +2,10 @@ import { useState } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { combatApi, characterApi } from '../api/endpoints'; import type { Monster, CombatResult, CombatLog } from '../api/types'; -import { Swords, Trophy, Skull, Clock, Zap } from 'lucide-react'; +import { Swords, Trophy, Skull, Clock, Zap, Heart } from 'lucide-react'; const COMBAT_COST = 5; +const REST_COST = 10; const ATTACK_TYPES = [ { id: 'melee', label: 'Mêlée', emoji: '⚔️', stat: 'Force × 1.5' }, @@ -113,6 +114,13 @@ export function CombatPage() { const endurance = char?.enduranceCurrent ?? 0; const playerLevel = char?.level ?? 1; const canFight = endurance >= COMBAT_COST; + const needsHeal = char ? char.hpCurrent < char.hpMax : false; + const canHeal = needsHeal && endurance >= REST_COST; + + const healMut = useMutation({ + mutationFn: () => characterApi.rest(), + onSuccess: () => qc.invalidateQueries({ queryKey: ['character'] }), + }); const { data: monsters, isLoading } = useQuery({ queryKey: ['monsters'], @@ -190,6 +198,20 @@ export function CombatPage() { ))}
+ {/* Soins rapide */} + {needsHeal && ( + + )} + {/* Coût endurance */}
Coût : {COMBAT_COST} endurance — Disponible : {endurance} diff --git a/frontend/src/pages/DashboardPage.tsx b/frontend/src/pages/DashboardPage.tsx index a44a51a..8de8b1f 100644 --- a/frontend/src/pages/DashboardPage.tsx +++ b/frontend/src/pages/DashboardPage.tsx @@ -220,7 +220,7 @@ export function DashboardPage() { ⚡ Budget : {' '}{Math.floor(endurance / COMBAT_COST)} combats {' · '}{Math.floor(endurance / FORGE_COST)} forges - {' · '}{Math.floor(endurance / REST_COST)} repos + {' · '}{Math.floor(endurance / REST_COST)} soins
{needsHeal && ( @@ -231,11 +231,11 @@ export function DashboardPage() { onClick={() => restMut.mutate()} > - {restMut.isPending ? 'Repos…' : `Se reposer (+50% PV, ${REST_COST}⚡)`} + {restMut.isPending ? 'Soins…' : `Soins (+50% PV, ${REST_COST}⚡)`} )} {needsHeal && !canRest && endurance < REST_COST && ( -

Endurance insuffisante pour se reposer

+

Endurance insuffisante pour les soins

)} {restMut.isError &&

{(restMut.error as Error).message}

}