Files
TetaRdPG/frontend/src/pages/InventoryPage.tsx
Tetardtek 8c6777c980
All checks were successful
CI/CD — Build & Deploy / Build & Deploy (push) Successful in 1m2s
feat: PKCE auth + CI/CD deploy
- Frontend: PKCE flow (oauth.ts, AuthCallback code exchange, 401 interceptor)
- Backend: token introspection via SuperOAuth (no more JWT secret)
- User model: superOauthId (unified) replaces oauthId+provider
- Cookies httpOnly session + refresh token
- POST /auth/refresh endpoint
- Gitea CI workflow (vps-runner pattern)
- DB_SYNC env var for initial schema creation
2026-03-24 13:01:14 +01:00

148 lines
6.0 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 type { CharacterItem } from '../api/types';
import { Package, Sword, Shield } from 'lucide-react';
const RARITY_LABEL: Record<string, string> = {
common: 'Commun', rare: 'Rare', epic: 'Épique', legendary: 'Légendaire',
};
function ItemCard({ ci, onEquip, onUnequip }: { ci: CharacterItem; onEquip: () => void; onUnequip: () => void }) {
const { item } = ci;
const bonuses = [
item.attackBonus && `+${item.attackBonus} ATK`,
item.defenseBonus && `+${item.defenseBonus} DEF`,
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 }}>
{!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>
}
</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'] }),
});
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')}
/>
))}
</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')}
/>
))}
</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>
);
}