feat: Add GroupsPanel component for managing turtle groups and commands

This commit is contained in:
MayaTheShy
2026-02-19 22:51:35 -05:00
parent a9bf76bd57
commit 6600ba9ea4

View File

@@ -0,0 +1,327 @@
import { useState, useEffect } from 'react';
import './GroupsPanel.css';
const GroupsPanel = ({ turtles, apiUrl, wsUrl }) => {
const [groups, setGroups] = useState([]);
const [groupName, setGroupName] = useState('');
const [groupColor, setGroupColor] = useState('#3b82f6');
const [selectedGroup, setSelectedGroup] = useState(null);
const [loading, setLoading] = useState(false);
const [message, setMessage] = useState(null);
const colorPresets = [
{ name: 'Blue', value: '#3b82f6' },
{ name: 'Green', value: '#10b981' },
{ name: 'Red', value: '#ef4444' },
{ name: 'Yellow', value: '#f59e0b' },
{ name: 'Purple', value: '#8b5cf6' },
{ name: 'Pink', value: '#ec4899' },
{ name: 'Cyan', value: '#06b6d4' },
{ name: 'Orange', value: '#f97316' },
];
useEffect(() => {
loadGroups();
// Refresh groups every 10 seconds
const interval = setInterval(loadGroups, 10000);
return () => clearInterval(interval);
}, []);
const loadGroups = async () => {
try {
const response = await fetch(`${apiUrl}/api/groups`);
if (response.ok) {
const data = await response.json();
setGroups(data);
}
} catch (error) {
console.error('Failed to load groups:', error);
}
};
const createGroup = async (e) => {
e.preventDefault();
if (!groupName.trim()) {
showMessage('Please enter a group name', 'error');
return;
}
setLoading(true);
try {
const response = await fetch(`${apiUrl}/api/groups`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
groupName: groupName.trim(),
color: groupColor
})
});
if (response.ok) {
showMessage('Group created successfully!', 'success');
setGroupName('');
loadGroups();
} else {
showMessage('Failed to create group', 'error');
}
} catch (error) {
showMessage('Error creating group', 'error');
} finally {
setLoading(false);
}
};
const deleteGroup = async (groupId) => {
if (!confirm('Are you sure you want to delete this group?')) return;
try {
const response = await fetch(`${apiUrl}/api/groups/${groupId}`, {
method: 'DELETE'
});
if (response.ok) {
showMessage('Group deleted', 'success');
if (selectedGroup?.groupId === groupId) {
setSelectedGroup(null);
}
loadGroups();
} else {
showMessage('Failed to delete group', 'error');
}
} catch (error) {
showMessage('Error deleting group', 'error');
}
};
const addTurtleToGroup = async (groupId, turtleId) => {
try {
const response = await fetch(`${apiUrl}/api/groups/${groupId}/members`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ turtleId })
});
if (response.ok) {
showMessage('Turtle added to group', 'success');
loadGroups();
} else {
showMessage('Failed to add turtle', 'error');
}
} catch (error) {
showMessage('Error adding turtle', 'error');
}
};
const removeTurtleFromGroup = async (groupId, turtleId) => {
try {
const response = await fetch(`${apiUrl}/api/groups/${groupId}/members/${turtleId}`, {
method: 'DELETE'
});
if (response.ok) {
showMessage('Turtle removed from group', 'success');
loadGroups();
} else {
showMessage('Failed to remove turtle', 'error');
}
} catch (error) {
showMessage('Error removing turtle', 'error');
}
};
const sendGroupCommand = async (groupId, command) => {
try {
const response = await fetch(`${apiUrl}/api/groups/${groupId}/command`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ command })
});
if (response.ok) {
const data = await response.json();
showMessage(`Command sent to ${data.sentTo} turtles`, 'success');
} else {
showMessage('Failed to send command', 'error');
}
} catch (error) {
showMessage('Error sending command', 'error');
}
};
const showMessage = (text, type) => {
setMessage({ text, type });
setTimeout(() => setMessage(null), 3000);
};
const getTurtleById = (turtleId) => {
return turtles.find(t => t.turtleID === turtleId);
};
const getAvailableTurtles = (groupMembers) => {
const memberIds = groupMembers.map(m => m.turtleId);
return turtles.filter(t => !memberIds.includes(t.turtleID));
};
return (
<div className="groups-panel">
<div className="groups-header">
<h2>👥 Turtle Groups</h2>
<div className="group-count">{groups.length} teams</div>
</div>
{message && (
<div className={`message ${message.type}`}>
{message.text}
</div>
)}
{/* Create Group Form */}
<div className="create-group-section">
<h3>Create New Group</h3>
<form onSubmit={createGroup} className="create-group-form">
<input
type="text"
placeholder="Group name..."
value={groupName}
onChange={(e) => setGroupName(e.target.value)}
maxLength={50}
disabled={loading}
/>
<div className="color-picker">
{colorPresets.map(preset => (
<button
key={preset.value}
type="button"
className={`color-option ${groupColor === preset.value ? 'active' : ''}`}
style={{ backgroundColor: preset.value }}
onClick={() => setGroupColor(preset.value)}
title={preset.name}
/>
))}
</div>
<button type="submit" disabled={loading || !groupName.trim()}>
{loading ? 'Creating...' : ' Create Group'}
</button>
</form>
</div>
{/* Groups List */}
<div className="groups-list">
{groups.length === 0 && (
<div className="empty-state">
<div className="empty-icon">👥</div>
<div className="empty-title">No Groups Yet</div>
<div className="empty-text">Create a group to organize your turtles into teams</div>
</div>
)}
{groups.map(group => (
<div key={group.groupId} className="group-card">
<div className="group-header" style={{ borderLeftColor: group.color }}>
<div className="group-title">
<div
className="group-color-indicator"
style={{ backgroundColor: group.color }}
/>
<h3>{group.groupName}</h3>
<span className="member-count">{group.memberCount} members</span>
</div>
<button
className="delete-group-btn"
onClick={() => deleteGroup(group.groupId)}
title="Delete group"
>
🗑
</button>
</div>
{/* Group Members */}
<div className="group-members">
<h4>Members:</h4>
{group.members && group.members.length > 0 ? (
<div className="members-list">
{group.members.map(member => {
const turtle = getTurtleById(member.turtleId);
return (
<div key={member.turtleId} className="member-item">
<div className="member-info">
<span className="member-icon">🐢</span>
<span className="member-name">
{turtle?.name || `Turtle ${member.turtleId}`}
</span>
{turtle && (
<span className="member-status" title={turtle.mode}>
{turtle.mode === 'exploring' && '🔍'}
{turtle.mode === 'mining' && '⛏️'}
{turtle.mode === 'returning' && '🏠'}
{turtle.mode === 'idle' && '💤'}
</span>
)}
</div>
<button
className="remove-member-btn"
onClick={() => removeTurtleFromGroup(group.groupId, member.turtleId)}
title="Remove from group"
>
</button>
</div>
);
})}
</div>
) : (
<div className="no-members">No members yet</div>
)}
{/* Add Member Dropdown */}
{getAvailableTurtles(group.members || []).length > 0 && (
<div className="add-member-section">
<select
onChange={(e) => {
if (e.target.value) {
addTurtleToGroup(group.groupId, parseInt(e.target.value));
e.target.value = '';
}
}}
defaultValue=""
>
<option value=""> Add turtle...</option>
{getAvailableTurtles(group.members || []).map(turtle => (
<option key={turtle.turtleID} value={turtle.turtleID}>
🐢 {turtle.name || `Turtle ${turtle.turtleID}`}
</option>
))}
</select>
</div>
)}
</div>
{/* Group Commands */}
{group.members && group.members.length > 0 && (
<div className="group-commands">
<h4>Group Commands:</h4>
<div className="command-buttons">
<button onClick={() => sendGroupCommand(group.groupId, 'explore')}>
🔍 Explore
</button>
<button onClick={() => sendGroupCommand(group.groupId, 'mine')}>
Mine
</button>
<button onClick={() => sendGroupCommand(group.groupId, 'returnHome')}>
🏠 Return Home
</button>
<button onClick={() => sendGroupCommand(group.groupId, 'stop')}>
Stop
</button>
</div>
</div>
)}
</div>
))}
</div>
</div>
);
};
export default GroupsPanel;