Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/RailRound.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2591,7 +2591,7 @@ function RailRoundContent() {
onClose={() => setStationMenu(null)}
/>
)}
<Chest />
<Chest activeTab={activeTab} />

<nav className="bg-white border-t p-2 flex justify-around shrink-0 pb-safe z-30">
{['records', 'map', 'stats'].map(t => <button id={`tab-btn-${t}`} key={t} onClick={()=>setActiveTab(t)} className={`p-2 rounded-lg ${activeTab===t ? 'text-emerald-600 bg-emerald-50' : 'text-slate-400'}`}>{t==='records' ? <Layers/> : t==='map' ? <MapIcon/> : <PieChart/>}</button>)}
Expand Down
99 changes: 71 additions & 28 deletions src/components/Chest.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,35 @@ import chestGif from './../assets/chest_animated.gif';

const CHEST_GIF = chestGif;

const Chest = ({ onDropItem }) => {
const Chest = ({ onDropItem, activeTab }) => {
const { isDragging } = useDrag();
const [isOpen, setIsOpen] = useState(false);
const [items, setItems] = useState([]);
const [animating, setAnimating] = useState(false);
const [staticChestSrc, setStaticChestSrc] = useState(null);
const [animationKey, setAnimationKey] = useState(0); // Key to force restart GIF
const timerRef = useRef(null);
const inventoryRef = useRef(null);
const chestIconRef = useRef(null);

useEffect(() => {
const handleClickOutside = (e) => {
if (!isOpen) return;
// If backdrop is present (activeTab === 'map'), it handles clicks.
// If NOT map, we need manual detection.
if (activeTab === 'map') return;

if (
inventoryRef.current && !inventoryRef.current.contains(e.target) &&
chestIconRef.current && !chestIconRef.current.contains(e.target) &&
!isDragging // Don't close if currently dragging
) {
setIsOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, [isOpen, activeTab, isDragging]);

useEffect(() => {
const saved = localStorage.getItem('rail_chest_items');
Expand Down Expand Up @@ -39,7 +60,19 @@ const Chest = ({ onDropItem }) => {
const handleDrop = (item) => {
if (!item) return;

// Prevent duplicates? Maybe not.
// Prevent self-drop (dragging from chest back to chest)
// Check if item id exists in current items?
// Actually, item.id passed from dragItem might match one in items.
// If we just dropped it, it's fine (maybe reordering, but here we just append).
// For simplicity, if it's already in the chest (by ID check), we ignore or remove/re-add.
// But since we generate a NEW ID on drop, we just need to know if the SOURCE was this chest.
// We can't easily know source unless item has a flag.
// User didn't ask for reordering, just fixing the bug.
// If I drag from chest, `dragItem` has an ID.
// If I drop on chest, `handleDrop` is called.
// I should probably Check if item is already in list?
if (items.some(i => i.id === item.id)) return;

const newItems = [...items, { ...item, id: Date.now() }]; // unique ID for chest
saveItems(newItems);

Expand All @@ -63,21 +96,20 @@ const Chest = ({ onDropItem }) => {
return (
<>
{/* The Chest Icon / Drop Target */}
<div className="fixed bottom-24 right-4 z-[500]">
<DropZone onDrop={handleDrop} className="relative group">
<div className="fixed bottom-24 right-4 z-[500]" ref={chestIconRef}>
<DropZone
onDrop={handleDrop}
className="relative group"
activeClassName="ring-4 ring-white bg-transparent rounded-sm"
>
<div
className={`w-16 h-16 transition-transform duration-200 cursor-pointer ${isDragging ? 'scale-110' : ''} ${animating ? 'animate-bounce' : ''}`}
onClick={() => setIsOpen(!isOpen)}
>
{/* Chest Image */}
{/*
Logic:
- If animating, show the GIF (with unique key to restart).
- If not animating, show the Static Frame (generated via canvas) or fallback to GIF (grayed out).
*/}
{animating ? (
<img
key={animationKey} // Forces remount -> restarts GIF
key={animationKey}
src={CHEST_GIF}
alt="Chest"
className="w-full h-full object-contain pixelated"
Expand All @@ -100,25 +132,36 @@ const Chest = ({ onDropItem }) => {
</DropZone>
</div>

{/* The "Open" Chest Inventory (Modal-ish) */}
{/* Backdrop - Only on Map */}
{isOpen && activeTab === 'map' && (
<div
className={`fixed inset-0 z-[490] bg-black/20 backdrop-blur-sm transition-opacity duration-200 ${isDragging ? 'opacity-0 pointer-events-none' : 'opacity-100'}`}
onClick={() => setIsOpen(false)}
/>
)}

{/* Inventory Container - Positioned relative to chest icon (Fixed Bottom Right) */}
{isOpen && (
<div className="fixed inset-0 z-[490] bg-black/20 backdrop-blur-sm" onClick={() => setIsOpen(false)}>
<div
className="absolute bottom-28 left-1/2 -translate-x-1/2 w-full max-w-sm bg-[#c6c6c6] border-4 border-[#373737] p-4 rounded-lg shadow-2xl animate-slide-up"
style={{ boxShadow: 'inset -4px -4px #555, inset 4px 4px #fff' }}
onClick={e => e.stopPropagation()}
>
<h3 className="text-gray-800 font-bold mb-4 flex justify-between items-center pixel-font">
<span>Inventory</span>
<span className="text-xs text-gray-600">{items.length} slots</span>
</h3>

<div className="grid grid-cols-4 gap-1 max-h-60 overflow-y-auto p-2 bg-[#C6C6C6]" style={{ }}>
{items.length === 0 && <div className="col-span-4 text-center text-gray-500 text-xs py-4 pixel-font">Empty Inventory</div>}
{items.map(item => (
<ChestItem key={item.id} item={item} onRemove={() => removeItem(item.id)} />
))}
</div>
<div
ref={inventoryRef}
className="fixed bottom-44 right-4 z-[495] w-64 bg-[#c6c6c6] border-4 border-[#373737] p-3 rounded-lg shadow-2xl animate-pop-in origin-bottom-right"
style={{ boxShadow: 'inset -4px -4px #555, inset 4px 4px #fff' }}
onClick={e => e.stopPropagation()}
>
{/* Bubble Tail */}
<div className="absolute -bottom-[14px] right-6 w-0 h-0 border-l-[10px] border-l-transparent border-r-[10px] border-r-transparent border-t-[14px] border-t-[#373737]"></div>
<div className="absolute -bottom-[8px] right-[26px] w-0 h-0 border-l-[8px] border-l-transparent border-r-[8px] border-r-transparent border-t-[12px] border-t-[#c6c6c6] z-10"></div>

<h3 className="text-gray-800 font-bold mb-2 flex justify-between items-center pixel-font">
<span>Inventory</span>
<span className="text-xs text-gray-600">{items.length} slots</span>
</h3>

<div className="grid grid-cols-4 gap-0.5 max-h-60 overflow-y-auto p-1 bg-[#C6C6C6]">
{items.length === 0 && <div className="col-span-4 text-center text-gray-500 text-xs py-4 pixel-font">Empty Inventory</div>}
{items.map(item => (
<ChestItem key={item.id} item={item} onRemove={() => removeItem(item.id)} />
))}
</div>
</div>
)}
Expand Down
14 changes: 14 additions & 0 deletions src/components/DragContext.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@ export const DragProvider = ({ children }) => {
const [cursorPos, setCursorPos] = useState({ x: 0, y: 0 });
const [dropZone, setDropZone] = useState(null);

useEffect(() => {
if (isDragging) {
document.body.style.userSelect = 'none';
document.body.style.webkitUserSelect = 'none';
} else {
document.body.style.userSelect = '';
document.body.style.webkitUserSelect = '';
}
}, [isDragging]);

// Global Mouse/Touch Handlers
useEffect(() => {
if (!isDragging) return;
Expand Down Expand Up @@ -104,6 +114,10 @@ export const DropZone = ({ onDrop, children, className = "", activeClassName = "
return () => { delete window.__dropZoneRegistry[idRef.current]; };
}, [onDrop]);

useEffect(() => {
if (!isDragging) setIsOver(false);
}, [isDragging]);

// Polling or Context-driven active state check would be cleaner, but for now
// we let the Provider setDropZone, and we check if *we* are the active one.
// However, DropZone doesn't know if IT is the active one from context easily without an ID.
Expand Down