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 ( +