Files
TetaRdPG/frontend/src/pages/InventoryPage.tsx
Tetardtek bf896a797f
All checks were successful
CI/CD — Build & Deploy / Build & Deploy (push) Successful in 33s
feat: vente items + stats combat avec équipement + forge visible
Inventaire: bouton Vendre sur items non équipés (40% du prix d'achat).
Stats forge visibles: "+5 ATK (3+2)" montre base + bonus forge.
Dashboard combat: attaque/défense calculés avec arme+armure+forge équipées.
10 side quests Égouts seedées (level 5-7).
2026-03-24 18:58:15 +01:00

175 lines
7.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { itemApi, materialApi } from '../api/endpoints';
import { api } from '../api/client';
import type { CharacterItem } from '../api/types';
import { Package, Sword, Shield, Coins } from 'lucide-react';
const RARITY_LABEL: Record<string, string> = {
common: 'Commun', rare: 'Rare', epic: 'Épique', legendary: 'Légendaire',
};
function ItemCard({ ci, onEquip, onUnequip, onSell, selling }: {
ci: CharacterItem; onEquip: () => void; onUnequip: () => void; onSell: () => void; selling: boolean;
}) {
const { item } = ci;
const forgeBonusATK = item.type === 'weapon' ? ci.forgeLevel * 2 : 0;
const forgeBonusDEF = item.type === 'armor' ? ci.forgeLevel * 2 : 0;
const totalATK = item.attackBonus + forgeBonusATK;
const totalDEF = item.defenseBonus + forgeBonusDEF;
const sellPrice = Math.floor((item as any).buyPrice * 0.4) || 0;
const bonuses = [
totalATK > 0 && `+${totalATK} ATK${forgeBonusATK > 0 ? ` (${item.attackBonus}+${forgeBonusATK})` : ''}`,
totalDEF > 0 && `+${totalDEF} DEF${forgeBonusDEF > 0 ? ` (${item.defenseBonus}+${forgeBonusDEF})` : ''}`,
item.forceBonus && `+${item.forceBonus} FOR`,
item.agiliteBonus && `+${item.agiliteBonus} AGI`,
item.intelligenceBonus && `+${item.intelligenceBonus} INT`,
item.chanceBonus && `+${item.chanceBonus} CHA`,
item.vitaliteBonus && `+${item.vitaliteBonus} VIT`,
].filter(Boolean).join(' · ');
return (
<div className={`card ${ci.equipped ? 'card-gold' : ''}`} style={{ position: 'relative' }}>
{ci.equipped && (
<span className="badge badge-gold" style={{ position: 'absolute', top: 8, right: 8, fontSize: 9 }}>Équipé</span>
)}
{ci.forgeLevel > 0 && (
<span className="badge badge-blue" style={{ position: 'absolute', top: ci.equipped ? 28 : 8, right: 8, fontSize: 9 }}>
+{ci.forgeLevel}
</span>
)}
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
<span style={{ fontSize: 20 }}>{item.type === 'weapon' ? '⚔️' : '🛡️'}</span>
<div>
<div style={{ fontWeight: 700, fontSize: 13 }}>{item.name}</div>
<div className={`rarity-${item.rarity}`} style={{ fontSize: 11 }}>{RARITY_LABEL[item.rarity]}</div>
</div>
</div>
{bonuses && <div style={{ fontSize: 11, color: '#3ddc84', marginBottom: 8 }}>{bonuses}</div>}
<div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
{!ci.equipped
? <button className="btn btn-ghost" style={{ fontSize: 11, padding: '0.2rem 0.6rem' }} onClick={onEquip}>Équiper</button>
: <button className="btn btn-ghost" style={{ fontSize: 11, padding: '0.2rem 0.6rem' }} onClick={onUnequip}>Déséquiper</button>
}
{!ci.equipped && sellPrice > 0 && (
<button className="btn btn-ghost" style={{ fontSize: 10, padding: '0.15rem 0.5rem', color: '#6b7a99' }}
disabled={selling} onClick={onSell}>
<Coins size={10} style={{ display: 'inline', marginRight: 3 }} />{selling ? '...' : `Vendre (${sellPrice}💰)`}
</button>
)}
</div>
</div>
);
}
export function InventoryPage() {
const qc = useQueryClient();
const { data: inventory, isLoading: loadInv } = useQuery({
queryKey: ['inventory'],
queryFn: itemApi.inventory,
});
const { data: materials, isLoading: loadMat } = useQuery({
queryKey: ['materials'],
queryFn: materialApi.inventory,
});
const equipMut = useMutation({
mutationFn: (id: string) => itemApi.equip(id),
onSuccess: () => qc.invalidateQueries({ queryKey: ['inventory'] }),
});
const unequipMut = useMutation({
mutationFn: (slot: 'weapon' | 'armor') => itemApi.unequip(slot),
onSuccess: () => qc.invalidateQueries({ queryKey: ['inventory'] }),
});
const sellMut = useMutation({
mutationFn: (charItemId: string) => api.post<any>(`/shop/sell/${charItemId}`),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['inventory'] });
qc.invalidateQueries({ queryKey: ['character'] });
},
});
if (loadInv || loadMat) return <div style={{ padding: '2rem', color: '#6b7a99' }}>Chargement</div>;
const weapons = inventory?.filter(ci => ci.item.type === 'weapon') ?? [];
const armors = inventory?.filter(ci => ci.item.type === 'armor') ?? [];
return (
<div>
<h2 style={{ margin: '0 0 1rem', color: '#f4c94e', fontSize: 20 }}>
<Package size={18} style={{ display: 'inline', marginRight: 8 }} />Inventaire
</h2>
{inventory?.length === 0 && (
<div className="card" style={{ textAlign: 'center', padding: '2rem', color: '#6b7a99' }}>
Inventaire vide gagne des combats pour lootter des matériaux et crafter des équipements !
</div>
)}
{/* Armes */}
{weapons.length > 0 && (
<div style={{ marginBottom: '1.25rem' }}>
<p style={{ margin: '0 0 0.5rem', fontSize: 12, fontWeight: 700, color: '#6b7a99', textTransform: 'uppercase', display:'flex', alignItems:'center', gap:4 }}>
<Sword size={11} /> Armes ({weapons.length})
</p>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(220px, 1fr))', gap: '0.75rem' }}>
{weapons.map(ci => (
<ItemCard
key={ci.id} ci={ci}
onEquip={() => equipMut.mutate(ci.id)}
onUnequip={() => unequipMut.mutate('weapon')}
onSell={() => sellMut.mutate(ci.id)}
selling={sellMut.isPending}
/>
))}
</div>
</div>
)}
{/* Armures */}
{armors.length > 0 && (
<div style={{ marginBottom: '1.25rem' }}>
<p style={{ margin: '0 0 0.5rem', fontSize: 12, fontWeight: 700, color: '#6b7a99', textTransform: 'uppercase', display:'flex', alignItems:'center', gap:4 }}>
<Shield size={11} /> Armures ({armors.length})
</p>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(220px, 1fr))', gap: '0.75rem' }}>
{armors.map(ci => (
<ItemCard
key={ci.id} ci={ci}
onEquip={() => equipMut.mutate(ci.id)}
onUnequip={() => unequipMut.mutate('armor')}
onSell={() => sellMut.mutate(ci.id)}
selling={sellMut.isPending}
/>
))}
</div>
</div>
)}
{/* Matériaux */}
{materials && materials.length > 0 && (
<div>
<p style={{ margin: '0 0 0.5rem', fontSize: 12, fontWeight: 700, color: '#6b7a99', textTransform: 'uppercase' }}>
🌿 Matériaux
</p>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(180px, 1fr))', gap: '0.5rem' }}>
{materials.map(cm => (
<div key={cm.id} className="card" style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '0.625rem' }}>
<span style={{ fontSize: 18 }}>🌿</span>
<div>
<div style={{ fontSize: 13, fontWeight: 600 }}>{cm.material.name}</div>
<div className={`rarity-${cm.material.rarity}`} style={{ fontSize: 11 }}>×{cm.quantity}</div>
</div>
</div>
))}
</div>
</div>
)}
</div>
);
}