Files
pet-companion-front/src/components/modal/InventoryModal.tsx
T

202 lines
7.2 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { Inventory, InvItemInteraction, Pet } from '../../types/Pet';
import { putPetItemInteract, getItemIcon, getItemInfo } from '../../services/api/api';
import { Loader2 } from 'lucide-react';
import { GameItem, ItemRarity } from '../../types/GameItem';
interface InventoryModalProps {
inventory: Inventory;
petId: string;
onClose: () => void;
onPetUpdate: (updatedPet: Pet) => void;
}
export default function InventoryModal({ inventory, petId, onClose, onPetUpdate }: InventoryModalProps) {
const capacity = inventory.capacity;
const items = inventory.items;
const gridSlots = Array.from({ length: capacity }, (_, i) => items[i]);
const [selectedItemIndex, setSelectedItemIndex] = useState<number | null>(null);
const [itemIcons, setItemIcons] = useState<Map<number, string>>(new Map());
const [loadingIcons, setLoadingIcons] = useState<Set<number>>(new Set());
const [selectedItemDetails, setSelectedItemDetails] = useState<GameItem | null>(null);
const [loadingDetails, setLoadingDetails] = useState(false);
const rarityColors = {
[ItemRarity.Common]: 'text-gray-200',
[ItemRarity.Uncommon]: 'text-green-400',
[ItemRarity.Rare]: 'text-blue-400',
[ItemRarity.Legendary]: 'text-purple-400'
};
useEffect(() => {
const fetchItemIcons = async () => {
const newIcons = new Map<number, string>();
const loadingItems = new Set<number>();
for (const itemId of items) {
if (itemId !== undefined && !itemIcons.has(itemId)) {
loadingItems.add(itemId);
}
}
setLoadingIcons(loadingItems);
for (const itemId of loadingItems) {
if (itemId !== undefined && !itemIcons.has(itemId)) {
try {
const blob = await getItemIcon(itemId);
const imageUrl = URL.createObjectURL(blob);
newIcons.set(itemId, imageUrl);
} catch (error) {
console.error(`Failed to load icon for item ${itemId}:`, error);
} finally {
loadingItems.delete(itemId);
setLoadingIcons(new Set(loadingItems));
}
}
}
setItemIcons(new Map([...itemIcons, ...newIcons]));
};
fetchItemIcons();
return () => {
// Cleanup object URLs on unmount
itemIcons.forEach(url => URL.revokeObjectURL(url));
};
}, [items]);
const handleItemClick = async (index: number) => {
if (selectedItemIndex === index || gridSlots[index] === undefined) {
setSelectedItemIndex(null);
setSelectedItemDetails(null);
return;
}
setSelectedItemIndex(index);
const itemId = gridSlots[index];
if (itemId !== undefined) {
setLoadingDetails(true);
try {
const details = await getItemInfo(itemId);
setSelectedItemDetails(details);
} catch (error) {
console.error('Failed to load item details:', error);
} finally {
setLoadingDetails(false);
}
}
};
const handleInteraction = async (interaction: InvItemInteraction) => {
if (selectedItemIndex === null || gridSlots[selectedItemIndex] === undefined) return;
try {
const updatedPet = await putPetItemInteract(petId, gridSlots[selectedItemIndex], interaction);
onPetUpdate(updatedPet);
} catch (error) {
console.error('Item interaction failed:', error);
}
};
return (
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4 z-50">
<div className="bg-gray-800 p-6 rounded-xl max-w-2xl w-full">
<div className="flex justify-between items-center mb-4">
<h2 className="text-2xl font-bold">Inventory</h2>
<button
onClick={onClose}
className="bg-red-600 hover:bg-red-700 text-white px-2 py-1 rounded"
>
Close
</button>
</div>
<div className="grid grid-cols-5 grid-rows-4 gap-4">
{gridSlots.map((itemId, index) => (
<div
key={index}
onClick={() => handleItemClick(index)}
className={`border rounded p-2 flex items-center justify-center h-24 w-24
${itemId !== undefined ? 'bg-gray-700 cursor-pointer' : 'bg-gray-800 text-gray-500'}
${selectedItemIndex === index ? 'border-blue-500 animate-pulse' : 'border-gray-600'}`}
>
{itemId !== undefined ? (
loadingIcons.has(itemId) ? (
<Loader2 className="w-6 h-6 animate-spin" />
) : (
<img
src={itemIcons.get(itemId)}
alt={`Item ${itemId}`}
className="w-full h-full object-contain"
/>
)
) : (
<span className="text-gray-500">Empty</span>
)}
</div>
))}
</div>
{selectedItemIndex !== null && (
<div className="mt-4 mb-4 p-4 bg-gray-700 rounded">
{loadingDetails ? (
<div className="flex justify-center">
<Loader2 className="w-6 h-6 animate-spin" />
</div>
) : selectedItemDetails ? (
<div className="space-y-2">
<h3 className="text-lg font-semibold text-white mb-1">
{selectedItemDetails.name}
</h3>
<div className="flex gap-2 items-center">
<span className={rarityColors[selectedItemDetails.rarity]}>
{selectedItemDetails.rarity}
</span>
<span className="text-gray-300"></span>
<span className="text-gray-200">{selectedItemDetails.type}</span>
</div>
<div className="text-gray-300 text-sm space-y-1">
{selectedItemDetails.description.split(';').map((line, index) => (
<p key={index}>{line.trim()}</p>
))}
</div>
</div>
) : null}
</div>
)}
<div className="mt-4 grid grid-cols-4 gap-4">
<button
disabled={selectedItemIndex === null}
onClick={() => handleInteraction('USE')}
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded disabled:opacity-50"
>
Use
</button>
<button
disabled={selectedItemIndex === null}
onClick={() => handleInteraction('DROP')}
className="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded disabled:opacity-50"
>
Drop
</button>
<button
disabled={selectedItemIndex === null}
onClick={() => handleInteraction('EQUIP')}
className="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded disabled:opacity-50"
>
Equip
</button>
<button
disabled={selectedItemIndex === null}
onClick={() => handleInteraction('UNEQUIP')}
className="bg-yellow-600 hover:bg-yellow-700 text-white px-4 py-2 rounded disabled:opacity-50"
>
Unequip
</button>
</div>
</div>
</div>
);
}