From faf2a98227cf16424618a67a2a4389cd173051fe Mon Sep 17 00:00:00 2001 From: Tetardtek Date: Tue, 24 Mar 2026 22:15:28 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20toast=20system=20=E2=80=94=20feedback?= =?UTF-8?q?=20visuel=20global=20(react-hot-toast)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Toaster dark theme (bottom-right, 3s/4s) - Combat: erreur cooldown/endurance en toast - Craft: toast start + collect + erreurs - Forge: toast succès/échec + erreurs - Shop: toast achat + erreurs - Inventaire: toast vente + erreurs - Fix forge costs frontend (200/400/700) --- frontend/package-lock.json | 28 +++++++++++++++++++++++++++- frontend/package.json | 1 + frontend/src/App.tsx | 10 ++++++++++ frontend/src/pages/CombatPage.tsx | 3 ++- frontend/src/pages/CraftPage.tsx | 13 +++++++++++-- frontend/src/pages/ForgePage.tsx | 8 ++++++-- frontend/src/pages/InventoryPage.tsx | 3 +++ frontend/src/pages/ShopPage.tsx | 3 +++ 8 files changed, 63 insertions(+), 6 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b3e61d0..fbbc1c3 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -12,6 +12,7 @@ "lucide-react": "^0.577.0", "react": "^19.2.4", "react-dom": "^19.2.4", + "react-hot-toast": "^2.6.0", "react-router-dom": "^7.13.1" }, "devDependencies": { @@ -1545,7 +1546,6 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, "license": "MIT" }, "node_modules/debug": { @@ -1952,6 +1952,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/goober": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.18.tgz", + "integrity": "sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw==", + "license": "MIT", + "peerDependencies": { + "csstype": "^3.0.10" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -2673,6 +2682,23 @@ "react": "^19.2.4" } }, + "node_modules/react-hot-toast": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.6.0.tgz", + "integrity": "sha512-bH+2EBMZ4sdyou/DPrfgIouFpcRLCJ+HoCA32UoAYHn6T3Ur5yfcDCeSr5mwldl6pFOsiocmrXMuoCJ1vV8bWg==", + "license": "MIT", + "dependencies": { + "csstype": "^3.1.3", + "goober": "^2.1.16" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, "node_modules/react-router": { "version": "7.13.1", "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 7447688..93b3109 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,6 +14,7 @@ "lucide-react": "^0.577.0", "react": "^19.2.4", "react-dom": "^19.2.4", + "react-hot-toast": "^2.6.0", "react-router-dom": "^7.13.1" }, "devDependencies": { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 3788622..2110a76 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,5 +1,6 @@ import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { Toaster } from 'react-hot-toast'; import { AuthProvider, useAuth } from './context/AuthContext'; import { Layout } from './components/Layout'; import { LoginPage } from './pages/LoginPage'; @@ -54,6 +55,15 @@ export default function App() { + ); } diff --git a/frontend/src/pages/CombatPage.tsx b/frontend/src/pages/CombatPage.tsx index 6ed54d0..a50fb78 100644 --- a/frontend/src/pages/CombatPage.tsx +++ b/frontend/src/pages/CombatPage.tsx @@ -1,5 +1,6 @@ import { useState, useCallback } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import toast from 'react-hot-toast'; import { combatApi, characterApi } from '../api/endpoints'; import type { Monster, CombatResult, MultiCombatResult, CombatLog } from '../api/types'; import { Swords, Trophy, Skull, Clock, Zap, Heart, Lock } from 'lucide-react'; @@ -196,7 +197,7 @@ export function CombatPage() { qc.invalidateQueries({ queryKey: ['materialsInventory'] }); startCooldown(); }, - onError: () => startCooldown(), + onError: (err: Error) => { toast.error(err.message); startCooldown(); }, }); if (isLoading) return
Chargement des monstres…
; diff --git a/frontend/src/pages/CraftPage.tsx b/frontend/src/pages/CraftPage.tsx index 0acc4a2..89879ac 100644 --- a/frontend/src/pages/CraftPage.tsx +++ b/frontend/src/pages/CraftPage.tsx @@ -1,5 +1,6 @@ import { useState, useEffect } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import toast from 'react-hot-toast'; import { craftApi, materialApi } from '../api/endpoints'; import type { Recipe, CraftJob } from '../api/types'; import { Hammer, Clock, CheckCircle } from 'lucide-react'; @@ -99,12 +100,20 @@ export function CraftPage() { const startMut = useMutation({ mutationFn: (recipeId: string) => craftApi.start(recipeId), - onSuccess: () => { qc.invalidateQueries({ queryKey: ['activeCraft'] }); qc.invalidateQueries({ queryKey: ['character'] }); qc.invalidateQueries({ queryKey: ['materials'] }); }, + onSuccess: () => { + toast.success('Craft lancé !'); + qc.invalidateQueries({ queryKey: ['activeCraft'] }); qc.invalidateQueries({ queryKey: ['character'] }); qc.invalidateQueries({ queryKey: ['materials'] }); + }, + onError: (err: Error) => toast.error(err.message), }); const collectMut = useMutation({ mutationFn: (jobId: string) => craftApi.collect(jobId), - onSuccess: () => { qc.invalidateQueries({ queryKey: ['activeCraft'] }); qc.invalidateQueries({ queryKey: ['inventory'] }); refetchActive(); }, + onSuccess: () => { + toast.success('Item récupéré !'); + qc.invalidateQueries({ queryKey: ['activeCraft'] }); qc.invalidateQueries({ queryKey: ['inventory'] }); refetchActive(); + }, + onError: (err: Error) => toast.error(err.message), }); const hasActive = activeCraft && 'id' in activeCraft; diff --git a/frontend/src/pages/ForgePage.tsx b/frontend/src/pages/ForgePage.tsx index bc92981..17d9af4 100644 --- a/frontend/src/pages/ForgePage.tsx +++ b/frontend/src/pages/ForgePage.tsx @@ -1,5 +1,6 @@ import { useState } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import toast from 'react-hot-toast'; import { itemApi, forgeApi, characterApi } from '../api/endpoints'; import type { CharacterItem } from '../api/types'; import { Shield, CheckCircle, XCircle, AlertTriangle, Zap, Coins } from 'lucide-react'; @@ -7,7 +8,7 @@ import { Shield, CheckCircle, XCircle, AlertTriangle, Zap, Coins } from 'lucide- const FORGE_RISK = [0, 0, 0, 20, 30, 40]; const FORGE_LABEL = ['—', '—', 'Garanti', '20% échec', '30% échec', '40% échec']; const FORGE_ENDURANCE_COST = 10; -const FORGE_GOLD_COST: Record = { 1: 50, 2: 100, 3: 250, 4: 500, 5: 1000 }; +const FORGE_GOLD_COST: Record = { 1: 50, 2: 100, 3: 200, 4: 400, 5: 700 }; function ForgePanel({ nextLevel, risk, endurance, gold, isPending, onForge }: { nextLevel: number; risk: number; endurance: number; gold: number; isPending: boolean; onForge: () => void; @@ -75,13 +76,16 @@ export function ForgePage() { mutationFn: () => forgeApi.upgrade(selected!.id), onSuccess: (res) => { setLastResult({ success: res.success, newLevel: res.forgeLevel }); - // Update selected item forgeLevel locally for immediate UI refresh if (res.success) { + toast.success(`Forge réussie ! +${res.forgeLevel}`); setSelected(prev => prev ? { ...prev, forgeLevel: res.forgeLevel } : null); + } else { + toast.error('Forge échouée — or et endurance perdus'); } qc.invalidateQueries({ queryKey: ['inventory'] }); qc.invalidateQueries({ queryKey: ['character'] }); }, + onError: (err: Error) => toast.error(err.message), }); if (isLoading) return
Chargement…
; diff --git a/frontend/src/pages/InventoryPage.tsx b/frontend/src/pages/InventoryPage.tsx index 6375436..eaa834b 100644 --- a/frontend/src/pages/InventoryPage.tsx +++ b/frontend/src/pages/InventoryPage.tsx @@ -1,4 +1,5 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import toast from 'react-hot-toast'; import { itemApi, materialApi } from '../api/endpoints'; import { api } from '../api/client'; import type { CharacterItem } from '../api/types'; @@ -91,9 +92,11 @@ export function InventoryPage() { const sellMut = useMutation({ mutationFn: (charItemId: string) => api.post(`/shop/sell/${charItemId}`), onSuccess: () => { + toast.success('Item vendu !'); qc.invalidateQueries({ queryKey: ['inventory'] }); qc.invalidateQueries({ queryKey: ['character'] }); }, + onError: (err: Error) => toast.error(err.message), }); if (loadInv || loadMat) return
Chargement…
; diff --git a/frontend/src/pages/ShopPage.tsx b/frontend/src/pages/ShopPage.tsx index 51473d7..013e0e0 100644 --- a/frontend/src/pages/ShopPage.tsx +++ b/frontend/src/pages/ShopPage.tsx @@ -1,4 +1,5 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import toast from 'react-hot-toast'; import { characterApi } from '../api/endpoints'; import { api } from '../api/client'; import { Coins, ShoppingBag, Sword, Shield, Heart, Zap } from 'lucide-react'; @@ -97,10 +98,12 @@ export function ShopPage() { const buyMut = useMutation({ mutationFn: (itemId: string) => api.post(`/shop/buy/${itemId}`), onSuccess: () => { + toast.success('Achat effectué !'); qc.invalidateQueries({ queryKey: ['character'] }); qc.invalidateQueries({ queryKey: ['shop'] }); qc.invalidateQueries({ queryKey: ['inventory'] }); }, + onError: (err: Error) => toast.error(err.message), }); if (isLoading) return
Chargement…
;