Imagery imported for icons and items. Live updates working.

Hover tooltip working. Features.md added for better tracking.
This commit is contained in:
2025-10-28 08:41:04 +08:00
parent 4ea30cc12e
commit ea8484fca7
16068 changed files with 3097 additions and 6 deletions

View File

@@ -0,0 +1,177 @@
import React, { useEffect, useState, useRef } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { fetchGroupData, selectGroupData } from '../store/group/groupState';
import Spinner from '../components/common/Spinner';
import PlayerStats from '../components/group/PlayerStats';
import PlayerSkills from '../components/group/PlayerSkills';
import PlayerInventory from '../components/group/PlayerInventory';
import PlayerEquipment from '../components/group/PlayerEquipment';
import XpDropper from '../components/group/XpDropper';
export default function GroupPanel({ group }) {
const dispatch = useDispatch();
const groupData = useSelector(state => selectGroupData(state, group.name));
useEffect(() => {
// Fetch group data on mount and set up auto-refresh
dispatch(fetchGroupData(group.name, group.token));
// Auto-refresh every 2 seconds for more responsive XP drops
const interval = setInterval(() => {
dispatch(fetchGroupData(group.name, group.token, true)); // Force reload to bypass cache
}, 2000);
return () => clearInterval(interval);
}, [dispatch, group.name, group.token]);
const handleRefresh = () => {
dispatch(fetchGroupData(group.name, group.token, true));
};
if (!groupData) {
return (
<div className='flex justify-center items-center py-12'>
<Spinner />
</div>
);
}
if (groupData.loading && !groupData.data) {
return (
<div className='flex justify-center items-center py-12'>
<Spinner />
<span className='ml-3 text-body-secondary'>Loading group data...</span>
</div>
);
}
if (groupData.error) {
return (
<div className='text-center py-12'>
<p className='text-error mb-4'>Error loading group data</p>
<p className='text-body-secondary text-sm mb-4'>{groupData.error}</p>
<button
type='button'
onClick={handleRefresh}
className='px-4 py-2 bg-primary text-white rounded hover:bg-primary-hover transition-colors'
>
Retry
</button>
</div>
);
}
const members = groupData.data || [];
// Debug logging
// eslint-disable-next-line no-console
console.log('GroupPanel - groupData:', groupData);
// eslint-disable-next-line no-console
console.log('GroupPanel - members:', members);
return (
<div>
<div className='flex justify-between items-center mb-4'>
<h2 className='text-xl font-semibold text-body-text'>{group.name}</h2>
<div className='flex gap-2'>
<button
type='button'
onClick={handleRefresh}
disabled={groupData.loading}
className='px-3 py-1 text-sm bg-background-light text-body-text rounded hover:bg-hover transition-colors disabled:opacity-50'
>
{groupData.loading ? 'Refreshing...' : 'Refresh'}
</button>
<button
type='button'
onClick={() => {
navigator.clipboard.writeText(group.token);
alert('Token copied to clipboard!');
}}
className='px-3 py-1 text-sm bg-background-light text-body-text rounded hover:bg-hover transition-colors'
>
Copy Token
</button>
</div>
</div>
<div className='bg-background-light rounded-lg p-4'>
<h3 className='text-lg font-medium text-body-text mb-3'>
Group Members ({members.length}/5)
</h3>
{members.length === 0 ? (
<p className='text-body-secondary text-center py-8'>
No member data available yet. Members will appear here once they start using the RuneLite plugin.
</p>
) : (
<div className='space-y-3'>
{members.map(member => (
<MemberCard key={member.name} member={member} />
))}
</div>
)}
</div>
<div className='mt-4 p-3 bg-background-light rounded text-sm text-body-secondary'>
<p className='font-medium mb-1'>How to use:</p>
<ol className='list-decimal list-inside space-y-1'>
<li>Share your group token with your team members</li>
<li>Install the Group Ironmen RuneLite plugin</li>
<li>Enter your group name and token in the plugin settings</li>
<li>Your progress will automatically sync to this tracker!</li>
</ol>
</div>
</div>
);
}
function MemberCard({ member }) {
const [expanded, setExpanded] = useState(false);
const previousSkillsRef = useRef(null);
return (
<div className='bg-gray-900 rounded-lg border border-gray-700 overflow-hidden'>
{/* Member Header */}
<div
className='flex justify-between items-center p-4 cursor-pointer hover:bg-gray-800 transition-colors'
onClick={() => setExpanded(!expanded)}
>
<div>
<h4 className='text-lg font-bold text-white'>{member.name || 'Unknown Member'}</h4>
<p className='text-sm text-gray-400'>
{member.last_updated
? `Last updated: ${new Date(member.last_updated).toLocaleString()}`
: 'No data yet'}
</p>
</div>
<div className='text-gray-400'>
<span className='text-2xl'>{expanded ? '▼' : '▶'}</span>
</div>
</div>
{/* Expanded Member Details */}
{expanded && (
<div className='p-4 pt-0 space-y-4' style={{ position: 'relative' }}>
{/* XP Dropper - positioned absolutely over the content */}
<XpDropper member={member} previousSkillsRef={previousSkillsRef} />
{/* Player Stats (HP/Prayer/Energy) */}
<PlayerStats member={member} />
{/* Skills Grid */}
<PlayerSkills member={member} />
{/* Side-by-side layout for Inventory and Equipment, aligned left */}
<div className='flex gap-4 items-start'>
{/* Inventory */}
<PlayerInventory member={member} />
{/* Equipment */}
<PlayerEquipment member={member} />
</div>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,91 @@
import React, { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import PageWrapper from '../components/PageWrapper';
import Card from '../components/common/Card';
import { selectActiveGroup, selectAllGroups, updateActiveGroup } from '../store/group/groupState';
import GroupPanel from './GroupPanel';
import CreateGroupModal from '../components/modals/CreateGroupModal';
export default function Groups() {
const dispatch = useDispatch();
const activeGroupIndex = useSelector(state => state.group?.activeGroup ?? null);
const activeGroup = useSelector(selectActiveGroup);
const allGroups = useSelector(selectAllGroups) || [];
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
// eslint-disable-next-line no-console
console.log('Groups component rendered:', { activeGroupIndex, activeGroup, allGroups });
const handleGroupSelect = index => {
dispatch(updateActiveGroup(index));
};
const handleCreateGroup = () => {
setIsCreateModalOpen(true);
};
return (
<PageWrapper>
<Card>
<Card.Body>
<div className='p-4'>
<div className='flex justify-between items-center mb-6'>
<h1 className='text-2xl font-bold'>Group Ironmen Tracker</h1>
<button
type='button'
onClick={handleCreateGroup}
className='px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700'
>
Create Group
</button>
</div>
{!allGroups || !allGroups.length ? (
<div className='text-center py-12'>
<p className='mb-4'>You haven't joined any groups yet.</p>
<p className='text-sm text-gray-600'>
Create a new group or join an existing one using your group token.
</p>
</div>
) : (
<div className='grid grid-cols-1 lg:grid-cols-4 gap-4'>
{/* Group List Sidebar */}
<div className='lg:col-span-1'>
<h2 className='text-lg font-semibold mb-3'>Your Groups</h2>
<div className='space-y-2'>
{allGroups.map((group, index) => (
<button
key={group.name}
type='button'
onClick={() => handleGroupSelect(index)}
className={`w-full text-left px-4 py-3 rounded transition-colors ${
index === activeGroupIndex
? 'bg-blue-600 text-white'
: 'bg-gray-700 hover:bg-gray-600'
}`}
>
<div className='font-medium truncate'>{group.name}</div>
<div className='text-xs opacity-75 truncate'>
{new Date(group.createdAt).toLocaleDateString()}
</div>
</button>
))}
</div>
</div>
{/* Group Details Panel */}
<div className='lg:col-span-3'>
{activeGroup && <GroupPanel group={activeGroup} />}
</div>
</div>
)}
</div>
</Card.Body>
</Card>
{isCreateModalOpen && (
<CreateGroupModal isOpen={isCreateModalOpen} onClose={() => setIsCreateModalOpen(false)} />
)}
</PageWrapper>
);
}