202 lines
7.2 KiB
TypeScript
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>
|
|
);
|
|
}
|