From 6600ba9ea407d5aa05d8624cc0c5032067910cf3 Mon Sep 17 00:00:00 2001 From: MayaTheShy Date: Thu, 19 Feb 2026 22:51:35 -0500 Subject: [PATCH] feat: Add GroupsPanel component for managing turtle groups and commands --- client/src/components/GroupsPanel.jsx | 327 ++++++++++++++++++++++++++ 1 file changed, 327 insertions(+) create mode 100644 client/src/components/GroupsPanel.jsx diff --git a/client/src/components/GroupsPanel.jsx b/client/src/components/GroupsPanel.jsx new file mode 100644 index 0000000..e6f3b1f --- /dev/null +++ b/client/src/components/GroupsPanel.jsx @@ -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 ( +
+
+

đŸ‘Ĩ Turtle Groups

+
{groups.length} teams
+
+ + {message && ( +
+ {message.text} +
+ )} + + {/* Create Group Form */} +
+

Create New Group

+
+ setGroupName(e.target.value)} + maxLength={50} + disabled={loading} + /> +
+ {colorPresets.map(preset => ( +
+ +
+
+ + {/* Groups List */} +
+ {groups.length === 0 && ( +
+
đŸ‘Ĩ
+
No Groups Yet
+
Create a group to organize your turtles into teams
+
+ )} + + {groups.map(group => ( +
+
+
+
+

{group.groupName}

+ {group.memberCount} members +
+ +
+ + {/* Group Members */} +
+

Members:

+ {group.members && group.members.length > 0 ? ( +
+ {group.members.map(member => { + const turtle = getTurtleById(member.turtleId); + return ( +
+
+ đŸĸ + + {turtle?.name || `Turtle ${member.turtleId}`} + + {turtle && ( + + {turtle.mode === 'exploring' && '🔍'} + {turtle.mode === 'mining' && 'â›ī¸'} + {turtle.mode === 'returning' && '🏠'} + {turtle.mode === 'idle' && '💤'} + + )} +
+ +
+ ); + })} +
+ ) : ( +
No members yet
+ )} + + {/* Add Member Dropdown */} + {getAvailableTurtles(group.members || []).length > 0 && ( +
+ +
+ )} +
+ + {/* Group Commands */} + {group.members && group.members.length > 0 && ( +
+

Group Commands:

+
+ + + + +
+
+ )} +
+ ))} +
+
+ ); +}; + +export default GroupsPanel;