Imagery imported for icons and items. Live updates working.
Hover tooltip working. Features.md added for better tracking.
This commit is contained in:
177
os-league-tools-master/src/pages/GroupPanel.js
Normal file
177
os-league-tools-master/src/pages/GroupPanel.js
Normal 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>
|
||||
);
|
||||
}
|
||||
91
os-league-tools-master/src/pages/Groups.js
Normal file
91
os-league-tools-master/src/pages/Groups.js
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user