feat: toast system — feedback visuel global (react-hot-toast)
All checks were successful
CI/CD — Build & Deploy / Build & Deploy (push) Successful in 36s
All checks were successful
CI/CD — Build & Deploy / Build & Deploy (push) Successful in 36s
- 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)
This commit is contained in:
28
frontend/package-lock.json
generated
28
frontend/package-lock.json
generated
@@ -12,6 +12,7 @@
|
|||||||
"lucide-react": "^0.577.0",
|
"lucide-react": "^0.577.0",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.4",
|
||||||
|
"react-hot-toast": "^2.6.0",
|
||||||
"react-router-dom": "^7.13.1"
|
"react-router-dom": "^7.13.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -1545,7 +1546,6 @@
|
|||||||
"version": "3.2.3",
|
"version": "3.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
@@ -1952,6 +1952,15 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"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": {
|
"node_modules/has-flag": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||||
@@ -2673,6 +2682,23 @@
|
|||||||
"react": "^19.2.4"
|
"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": {
|
"node_modules/react-router": {
|
||||||
"version": "7.13.1",
|
"version": "7.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.1.tgz",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
"lucide-react": "^0.577.0",
|
"lucide-react": "^0.577.0",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.4",
|
||||||
|
"react-hot-toast": "^2.6.0",
|
||||||
"react-router-dom": "^7.13.1"
|
"react-router-dom": "^7.13.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
|
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
|
import { Toaster } from 'react-hot-toast';
|
||||||
import { AuthProvider, useAuth } from './context/AuthContext';
|
import { AuthProvider, useAuth } from './context/AuthContext';
|
||||||
import { Layout } from './components/Layout';
|
import { Layout } from './components/Layout';
|
||||||
import { LoginPage } from './pages/LoginPage';
|
import { LoginPage } from './pages/LoginPage';
|
||||||
@@ -54,6 +55,15 @@ export default function App() {
|
|||||||
<AppRoutes />
|
<AppRoutes />
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
|
<Toaster
|
||||||
|
position="bottom-right"
|
||||||
|
toastOptions={{
|
||||||
|
duration: 3000,
|
||||||
|
style: { background: '#1e2535', color: '#dce4f0', border: '1px solid #2a3448', fontSize: 13 },
|
||||||
|
success: { iconTheme: { primary: '#3ddc84', secondary: '#1e2535' } },
|
||||||
|
error: { iconTheme: { primary: '#e84040', secondary: '#1e2535' }, duration: 4000 },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState, useCallback } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
import { combatApi, characterApi } from '../api/endpoints';
|
import { combatApi, characterApi } from '../api/endpoints';
|
||||||
import type { Monster, CombatResult, MultiCombatResult, CombatLog } from '../api/types';
|
import type { Monster, CombatResult, MultiCombatResult, CombatLog } from '../api/types';
|
||||||
import { Swords, Trophy, Skull, Clock, Zap, Heart, Lock } from 'lucide-react';
|
import { Swords, Trophy, Skull, Clock, Zap, Heart, Lock } from 'lucide-react';
|
||||||
@@ -196,7 +197,7 @@ export function CombatPage() {
|
|||||||
qc.invalidateQueries({ queryKey: ['materialsInventory'] });
|
qc.invalidateQueries({ queryKey: ['materialsInventory'] });
|
||||||
startCooldown();
|
startCooldown();
|
||||||
},
|
},
|
||||||
onError: () => startCooldown(),
|
onError: (err: Error) => { toast.error(err.message); startCooldown(); },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isLoading) return <div style={{ padding: '2rem', color: '#6b7a99' }}>Chargement des monstres…</div>;
|
if (isLoading) return <div style={{ padding: '2rem', color: '#6b7a99' }}>Chargement des monstres…</div>;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
import { craftApi, materialApi } from '../api/endpoints';
|
import { craftApi, materialApi } from '../api/endpoints';
|
||||||
import type { Recipe, CraftJob } from '../api/types';
|
import type { Recipe, CraftJob } from '../api/types';
|
||||||
import { Hammer, Clock, CheckCircle } from 'lucide-react';
|
import { Hammer, Clock, CheckCircle } from 'lucide-react';
|
||||||
@@ -99,12 +100,20 @@ export function CraftPage() {
|
|||||||
|
|
||||||
const startMut = useMutation({
|
const startMut = useMutation({
|
||||||
mutationFn: (recipeId: string) => craftApi.start(recipeId),
|
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({
|
const collectMut = useMutation({
|
||||||
mutationFn: (jobId: string) => craftApi.collect(jobId),
|
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;
|
const hasActive = activeCraft && 'id' in activeCraft;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
import { itemApi, forgeApi, characterApi } from '../api/endpoints';
|
import { itemApi, forgeApi, characterApi } from '../api/endpoints';
|
||||||
import type { CharacterItem } from '../api/types';
|
import type { CharacterItem } from '../api/types';
|
||||||
import { Shield, CheckCircle, XCircle, AlertTriangle, Zap, Coins } from 'lucide-react';
|
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_RISK = [0, 0, 0, 20, 30, 40];
|
||||||
const FORGE_LABEL = ['—', '—', 'Garanti', '20% échec', '30% échec', '40% échec'];
|
const FORGE_LABEL = ['—', '—', 'Garanti', '20% échec', '30% échec', '40% échec'];
|
||||||
const FORGE_ENDURANCE_COST = 10;
|
const FORGE_ENDURANCE_COST = 10;
|
||||||
const FORGE_GOLD_COST: Record<number, number> = { 1: 50, 2: 100, 3: 250, 4: 500, 5: 1000 };
|
const FORGE_GOLD_COST: Record<number, number> = { 1: 50, 2: 100, 3: 200, 4: 400, 5: 700 };
|
||||||
|
|
||||||
function ForgePanel({ nextLevel, risk, endurance, gold, isPending, onForge }: {
|
function ForgePanel({ nextLevel, risk, endurance, gold, isPending, onForge }: {
|
||||||
nextLevel: number; risk: number; endurance: number; gold: number; isPending: boolean; onForge: () => void;
|
nextLevel: number; risk: number; endurance: number; gold: number; isPending: boolean; onForge: () => void;
|
||||||
@@ -75,13 +76,16 @@ export function ForgePage() {
|
|||||||
mutationFn: () => forgeApi.upgrade(selected!.id),
|
mutationFn: () => forgeApi.upgrade(selected!.id),
|
||||||
onSuccess: (res) => {
|
onSuccess: (res) => {
|
||||||
setLastResult({ success: res.success, newLevel: res.forgeLevel });
|
setLastResult({ success: res.success, newLevel: res.forgeLevel });
|
||||||
// Update selected item forgeLevel locally for immediate UI refresh
|
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
|
toast.success(`Forge réussie ! +${res.forgeLevel}`);
|
||||||
setSelected(prev => prev ? { ...prev, forgeLevel: res.forgeLevel } : null);
|
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: ['inventory'] });
|
||||||
qc.invalidateQueries({ queryKey: ['character'] });
|
qc.invalidateQueries({ queryKey: ['character'] });
|
||||||
},
|
},
|
||||||
|
onError: (err: Error) => toast.error(err.message),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isLoading) return <div style={{ padding: '2rem', color: '#6b7a99' }}>Chargement…</div>;
|
if (isLoading) return <div style={{ padding: '2rem', color: '#6b7a99' }}>Chargement…</div>;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
import { itemApi, materialApi } from '../api/endpoints';
|
import { itemApi, materialApi } from '../api/endpoints';
|
||||||
import { api } from '../api/client';
|
import { api } from '../api/client';
|
||||||
import type { CharacterItem } from '../api/types';
|
import type { CharacterItem } from '../api/types';
|
||||||
@@ -91,9 +92,11 @@ export function InventoryPage() {
|
|||||||
const sellMut = useMutation({
|
const sellMut = useMutation({
|
||||||
mutationFn: (charItemId: string) => api.post<any>(`/shop/sell/${charItemId}`),
|
mutationFn: (charItemId: string) => api.post<any>(`/shop/sell/${charItemId}`),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
|
toast.success('Item vendu !');
|
||||||
qc.invalidateQueries({ queryKey: ['inventory'] });
|
qc.invalidateQueries({ queryKey: ['inventory'] });
|
||||||
qc.invalidateQueries({ queryKey: ['character'] });
|
qc.invalidateQueries({ queryKey: ['character'] });
|
||||||
},
|
},
|
||||||
|
onError: (err: Error) => toast.error(err.message),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (loadInv || loadMat) return <div style={{ padding: '2rem', color: '#6b7a99' }}>Chargement…</div>;
|
if (loadInv || loadMat) return <div style={{ padding: '2rem', color: '#6b7a99' }}>Chargement…</div>;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
import { characterApi } from '../api/endpoints';
|
import { characterApi } from '../api/endpoints';
|
||||||
import { api } from '../api/client';
|
import { api } from '../api/client';
|
||||||
import { Coins, ShoppingBag, Sword, Shield, Heart, Zap } from 'lucide-react';
|
import { Coins, ShoppingBag, Sword, Shield, Heart, Zap } from 'lucide-react';
|
||||||
@@ -97,10 +98,12 @@ export function ShopPage() {
|
|||||||
const buyMut = useMutation({
|
const buyMut = useMutation({
|
||||||
mutationFn: (itemId: string) => api.post<any>(`/shop/buy/${itemId}`),
|
mutationFn: (itemId: string) => api.post<any>(`/shop/buy/${itemId}`),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
|
toast.success('Achat effectué !');
|
||||||
qc.invalidateQueries({ queryKey: ['character'] });
|
qc.invalidateQueries({ queryKey: ['character'] });
|
||||||
qc.invalidateQueries({ queryKey: ['shop'] });
|
qc.invalidateQueries({ queryKey: ['shop'] });
|
||||||
qc.invalidateQueries({ queryKey: ['inventory'] });
|
qc.invalidateQueries({ queryKey: ['inventory'] });
|
||||||
},
|
},
|
||||||
|
onError: (err: Error) => toast.error(err.message),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isLoading) return <div style={{ padding: '2rem', color: '#6b7a99' }}>Chargement…</div>;
|
if (isLoading) return <div style={{ padding: '2rem', color: '#6b7a99' }}>Chargement…</div>;
|
||||||
|
|||||||
Reference in New Issue
Block a user