597 lines
24 KiB
JavaScript
597 lines
24 KiB
JavaScript
import React, { useState, useCallback } from 'react';
|
||
import { useTurtleStore } from '../store/turtleStore';
|
||
import './ControlPanel.css';
|
||
|
||
function TurtleCard({ turtle, isSelected, onSelect }) {
|
||
const activeState = turtle.state || turtle.mode || 'idle';
|
||
const fuel = turtle.fuel === 'unlimited' ? '∞' : (turtle.fuel || '?');
|
||
const inventoryCount = Array.isArray(turtle.inventory)
|
||
? turtle.inventory.length
|
||
: (turtle.inventory ? Object.keys(turtle.inventory).length : 0);
|
||
const displayName = turtle.label || `Turtle ${turtle.turtleID}`;
|
||
|
||
const modeColors = {
|
||
mining: '#55ff55',
|
||
exploring: '#55ffff',
|
||
returning: '#ffaa00',
|
||
goHome: '#ffaa00',
|
||
idle: '#aaaaaa',
|
||
manual: '#ff55ff',
|
||
refueling: '#ff5555',
|
||
farming: '#55ff55',
|
||
dumpInventory: '#aa00aa',
|
||
dumping: '#aa00aa',
|
||
moving: '#55ffff',
|
||
scan: '#5555ff',
|
||
extraction: '#ffaa00',
|
||
building: '#00aaaa',
|
||
autocraft: '#ff55ff',
|
||
unknown: '#555555'
|
||
};
|
||
|
||
return (
|
||
<div
|
||
className={`turtle-card ${isSelected ? 'selected' : ''}`}
|
||
onClick={onSelect}
|
||
style={{ borderColor: modeColors[activeState] || modeColors.unknown }}
|
||
>
|
||
<div className="turtle-header">
|
||
<h3>{displayName}</h3>
|
||
<span className="mode-badge" style={{ background: modeColors[activeState] || modeColors.unknown }}>
|
||
{activeState}
|
||
</span>
|
||
</div>
|
||
|
||
<div className="turtle-stats">
|
||
<div className="stat">
|
||
<span className="label">Position:</span>
|
||
<span className="value">
|
||
{turtle.position
|
||
? `${turtle.position.x}, ${turtle.position.y}, ${turtle.position.z}`
|
||
: 'No GPS'
|
||
}
|
||
</span>
|
||
</div>
|
||
|
||
<div className="stat">
|
||
<span className="label">Fuel:</span>
|
||
<span className="value">{fuel}</span>
|
||
</div>
|
||
|
||
<div className="stat">
|
||
<span className="label">Inventory:</span>
|
||
<span className="value">{inventoryCount} items</span>
|
||
</div>
|
||
|
||
{turtle.position && turtle.homePosition && (
|
||
<div className="stat">
|
||
<span className="label">Home Distance:</span>
|
||
<span className="value">
|
||
{Math.abs(turtle.position.x - turtle.homePosition.x) +
|
||
Math.abs(turtle.position.y - turtle.homePosition.y) +
|
||
Math.abs(turtle.position.z - turtle.homePosition.z)} blocks
|
||
</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function TurtleDetails({ turtle }) {
|
||
const setTurtleState = useTurtleStore((state) => state.setTurtleState);
|
||
const renameTurtle = useTurtleStore((state) => state.renameTurtle);
|
||
const equipLeft = useTurtleStore((state) => state.equipLeft);
|
||
const equipRight = useTurtleStore((state) => state.equipRight);
|
||
const sortInventory = useTurtleStore((state) => state.sortInventory);
|
||
const selectSlot = useTurtleStore((state) => state.selectSlot);
|
||
const dropItems = useTurtleStore((state) => state.dropItems);
|
||
const suckItems = useTurtleStore((state) => state.suckItems);
|
||
const connectToInventory = useTurtleStore((state) => state.connectToInventory);
|
||
const updateTurtleConfig = useTurtleStore((state) => state.updateTurtleConfig);
|
||
const exploreTurtle = useTurtleStore((state) => state.exploreTurtle);
|
||
const gpsLocateTurtle = useTurtleStore((state) => state.gpsLocateTurtle);
|
||
const moveForward = useTurtleStore((state) => state.moveForward);
|
||
const moveBack = useTurtleStore((state) => state.moveBack);
|
||
const moveUp = useTurtleStore((state) => state.moveUp);
|
||
const moveDown = useTurtleStore((state) => state.moveDown);
|
||
const turnLeft = useTurtleStore((state) => state.turnLeft);
|
||
const turnRight = useTurtleStore((state) => state.turnRight);
|
||
const digBlock = useTurtleStore((state) => state.digBlock);
|
||
const digBlockUp = useTurtleStore((state) => state.digBlockUp);
|
||
const digBlockDown = useTurtleStore((state) => state.digBlockDown);
|
||
const placeBlock = useTurtleStore((state) => state.placeBlock);
|
||
|
||
const [renameValue, setRenameValue] = useState('');
|
||
const [showConfig, setShowConfig] = useState(false);
|
||
const [configValues, setConfigValues] = useState({ maxDistance: 200, autoRefuel: true });
|
||
|
||
if (!turtle) {
|
||
return (
|
||
<div className="turtle-details empty">
|
||
<p>Select a turtle to view details and control it</p>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
const handleStateChange = (stateName, data = {}) => {
|
||
setTurtleState(turtle.turtleID, stateName, data);
|
||
};
|
||
|
||
const handleRename = async () => {
|
||
if (renameValue.trim()) {
|
||
await renameTurtle(turtle.turtleID, renameValue.trim());
|
||
setRenameValue('');
|
||
}
|
||
};
|
||
|
||
const handleSlotClick = async (slotIndex) => {
|
||
await selectSlot(turtle.turtleID, slotIndex + 1);
|
||
};
|
||
|
||
const handleConfigSave = async () => {
|
||
await updateTurtleConfig(turtle.turtleID, configValues);
|
||
setShowConfig(false);
|
||
};
|
||
|
||
const activeState = turtle.state || turtle.mode || 'idle';
|
||
const displayName = turtle.label || `Turtle ${turtle.turtleID}`;
|
||
|
||
return (
|
||
<div className="turtle-details">
|
||
<h2>{displayName} <span style={{color: '#aaaaaa', fontSize: '0.8em'}}>#{turtle.turtleID}</span></h2>
|
||
|
||
{/* Rename + Config bar */}
|
||
<div className="detail-section" style={{ display: 'flex', gap: '8px', alignItems: 'center', flexWrap: 'wrap' }}>
|
||
<input
|
||
type="text"
|
||
value={renameValue}
|
||
onChange={(e) => setRenameValue(e.target.value)}
|
||
onKeyDown={(e) => e.key === 'Enter' && handleRename()}
|
||
placeholder="Rename turtle..."
|
||
className="rename-input"
|
||
style={{ flex: 1, minWidth: '120px', padding: '4px 8px', border: '2px solid #333', background: '#1a1a1a', color: '#e0e0e0' }}
|
||
/>
|
||
<button onClick={handleRename} className="command-btn" style={{ padding: '4px 12px' }}>📝 Rename</button>
|
||
<button onClick={() => setShowConfig(!showConfig)} className="command-btn" style={{ padding: '4px 12px' }}>⚙️ Config</button>
|
||
<button onClick={() => gpsLocateTurtle(turtle.turtleID)} className="command-btn" style={{ padding: '4px 12px' }}>📡 GPS</button>
|
||
<button onClick={() => exploreTurtle(turtle.turtleID)} className="command-btn" style={{ padding: '4px 12px' }}>🔎 Inspect</button>
|
||
</div>
|
||
|
||
{/* Config Modal */}
|
||
{showConfig && (
|
||
<div className="detail-section" style={{ background: '#2c2c2c', padding: '12px', border: '3px solid #1a1a1a' }}>
|
||
<h3>⚙️ Configuration</h3>
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px', marginTop: '8px' }}>
|
||
<label style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||
<span>Max Distance:</span>
|
||
<input type="number" value={configValues.maxDistance} onChange={(e) => setConfigValues({...configValues, maxDistance: parseInt(e.target.value)})} style={{ width: '80px', padding: '2px 6px', border: '2px solid #333', background: '#1a1a1a', color: '#e0e0e0' }} />
|
||
</label>
|
||
<label style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||
<span>Auto Refuel:</span>
|
||
<input type="checkbox" checked={configValues.autoRefuel} onChange={(e) => setConfigValues({...configValues, autoRefuel: e.target.checked})} />
|
||
</label>
|
||
<div style={{ display: 'flex', gap: '8px', justifyContent: 'flex-end' }}>
|
||
<button onClick={() => setShowConfig(false)} className="command-btn" style={{ padding: '4px 12px' }}>Cancel</button>
|
||
<button onClick={handleConfigSave} className="command-btn explore" style={{ padding: '4px 12px' }}>💾 Save</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<div className="detail-section">
|
||
<h3>Status</h3>
|
||
<div className="status-grid">
|
||
<div className="status-item">
|
||
<span className="label">State:</span>
|
||
<span className="value">{activeState}</span>
|
||
</div>
|
||
{turtle.stateDescription && (
|
||
<div className="status-item">
|
||
<span className="label">Activity:</span>
|
||
<span className="value">{turtle.stateDescription}</span>
|
||
</div>
|
||
)}
|
||
<div className="status-item">
|
||
<span className="label">Fuel:</span>
|
||
<span className="value">
|
||
{turtle.fuel === 'unlimited' ? 'Unlimited' : turtle.fuel}
|
||
</span>
|
||
</div>
|
||
<div className="status-item">
|
||
<span className="label">Position:</span>
|
||
<span className="value">
|
||
{turtle.position
|
||
? `X: ${turtle.position.x}, Y: ${turtle.position.y}, Z: ${turtle.position.z}`
|
||
: 'No GPS'
|
||
}
|
||
</span>
|
||
</div>
|
||
<div className="status-item">
|
||
<span className="label">Home:</span>
|
||
<span className="value">
|
||
{turtle.homePosition
|
||
? `X: ${turtle.homePosition.x}, Y: ${turtle.homePosition.y}, Z: ${turtle.homePosition.z}`
|
||
: 'Not set'
|
||
}
|
||
</span>
|
||
</div>
|
||
{turtle.error && (
|
||
<div className="status-item" style={{ color: '#ff5555' }}>
|
||
<span className="label">Error:</span>
|
||
<span className="value">{turtle.error}</span>
|
||
</div>
|
||
)}
|
||
{turtle.warning && (
|
||
<div className="status-item" style={{ color: '#ffaa00' }}>
|
||
<span className="label">Warning:</span>
|
||
<span className="value">{turtle.warning}</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Peripherals section */}
|
||
{turtle.peripherals && Object.keys(turtle.peripherals).length > 0 && (
|
||
<div className="detail-section">
|
||
<h3>Peripherals</h3>
|
||
<div className="status-grid">
|
||
{Object.entries(turtle.peripherals).map(([side, info]) => (
|
||
<div key={side} className="status-item">
|
||
<span className="label" style={{ textTransform: 'capitalize' }}>{side}:</span>
|
||
<span className="value" style={{ color: '#ff55ff' }}>
|
||
{typeof info === 'string' ? info : (info?.types?.join(', ') || 'unknown')}
|
||
</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<div className="detail-section">
|
||
<h3>State Machine</h3>
|
||
<div className="command-grid">
|
||
<button
|
||
className={`command-btn ${activeState === 'idle' ? 'active' : ''}`}
|
||
onClick={() => handleStateChange('idle')}
|
||
title="Stop and go idle"
|
||
style={activeState === 'idle' ? { outline: '2px solid #fff' } : {}}
|
||
>
|
||
⏸️ Idle
|
||
</button>
|
||
<button
|
||
className={`command-btn explore ${activeState === 'exploring' ? 'active' : ''}`}
|
||
onClick={() => handleStateChange('exploring')}
|
||
title="Autonomous exploration"
|
||
style={activeState === 'exploring' ? { outline: '2px solid #fff' } : {}}
|
||
>
|
||
🔍 Explore
|
||
</button>
|
||
<button
|
||
className={`command-btn mine ${activeState === 'mining' ? 'active' : ''}`}
|
||
onClick={() => handleStateChange('mining')}
|
||
title="Mining with ore priority"
|
||
style={activeState === 'mining' ? { outline: '2px solid #fff' } : {}}
|
||
>
|
||
⛏️ Mine
|
||
</button>
|
||
<button
|
||
className={`command-btn ${activeState === 'farming' ? 'active' : ''}`}
|
||
onClick={() => handleStateChange('farming')}
|
||
title="Automated farming"
|
||
style={activeState === 'farming' ? { outline: '2px solid #fff' } : {}}
|
||
>
|
||
🌾 Farm
|
||
</button>
|
||
<button
|
||
className={`command-btn return ${activeState === 'goHome' || activeState === 'returning' ? 'active' : ''}`}
|
||
onClick={() => handleStateChange('goHome')}
|
||
title="Navigate home"
|
||
style={activeState === 'goHome' || activeState === 'returning' ? { outline: '2px solid #fff' } : {}}
|
||
>
|
||
🏠 Go Home
|
||
</button>
|
||
<button
|
||
className={`command-btn refuel ${activeState === 'refueling' ? 'active' : ''}`}
|
||
onClick={() => handleStateChange('refueling')}
|
||
title="Auto-refuel from inventory"
|
||
style={activeState === 'refueling' ? { outline: '2px solid #fff' } : {}}
|
||
>
|
||
⛽ Refuel
|
||
</button>
|
||
<button
|
||
className={`command-btn ${activeState === 'dumpInventory' || activeState === 'dumping' ? 'active' : ''}`}
|
||
onClick={() => handleStateChange('dumpInventory')}
|
||
title="Dump inventory into nearby container"
|
||
style={activeState === 'dumpInventory' || activeState === 'dumping' ? { outline: '2px solid #fff' } : {}}
|
||
>
|
||
📦 Dump
|
||
</button>
|
||
<button
|
||
className={`command-btn ${activeState === 'moving' ? 'active' : ''}`}
|
||
onClick={() => handleStateChange('moving')}
|
||
title="Navigate to target"
|
||
style={activeState === 'moving' ? { outline: '2px solid #fff' } : {}}
|
||
>
|
||
🧭 Move To
|
||
</button>
|
||
<button
|
||
className={`command-btn ${activeState === 'scan' ? 'active' : ''}`}
|
||
onClick={() => handleStateChange('scan')}
|
||
title="Scan surroundings with peripheral scanner"
|
||
style={activeState === 'scan' ? { outline: '2px solid #fff' } : {}}
|
||
>
|
||
📡 Scan
|
||
</button>
|
||
<button
|
||
className={`command-btn ${activeState === 'extraction' ? 'active' : ''}`}
|
||
onClick={() => {
|
||
const area = prompt('Enter extraction area (startX,startY,startZ,endX,endY,endZ):');
|
||
if (area) {
|
||
const [sx,sy,sz,ex,ey,ez] = area.split(',').map(Number);
|
||
if (!isNaN(sx) && !isNaN(sy) && !isNaN(sz) && !isNaN(ex) && !isNaN(ey) && !isNaN(ez)) {
|
||
const points = [];
|
||
for (let x = sx; x <= ex; x++) for (let y = sy; y <= ey; y++) for (let z = sz; z <= ez; z++) {
|
||
points.push({x, y, z});
|
||
}
|
||
handleStateChange('extraction', { area: points });
|
||
}
|
||
}
|
||
}}
|
||
title="Smart ore extraction in an area"
|
||
style={activeState === 'extraction' ? { outline: '2px solid #fff' } : {}}
|
||
>
|
||
💎 Extract
|
||
</button>
|
||
<button
|
||
className={`command-btn ${activeState === 'building' ? 'active' : ''}`}
|
||
onClick={() => {
|
||
const input = prompt('Enter blueprint JSON (array of {x,y,z,name} blocks):');
|
||
if (input) {
|
||
try {
|
||
const blocks = JSON.parse(input);
|
||
handleStateChange('building', { blocks });
|
||
} catch (e) {
|
||
alert('Invalid JSON blueprint');
|
||
}
|
||
}
|
||
}}
|
||
title="Build from blueprint"
|
||
style={activeState === 'building' ? { outline: '2px solid #fff' } : {}}
|
||
>
|
||
🏗️ Build
|
||
</button>
|
||
<button
|
||
className={`command-btn ${activeState === 'autocraft' ? 'active' : ''}`}
|
||
onClick={() => {
|
||
const input = prompt('Enter recipe JSON (array of 16 slot items, null for empty):');
|
||
if (input) {
|
||
try {
|
||
const recipe = JSON.parse(input);
|
||
handleStateChange('autocraft', { recipe });
|
||
} catch (e) {
|
||
alert('Invalid JSON recipe');
|
||
}
|
||
}
|
||
}}
|
||
title="Automated crafting with workbench"
|
||
style={activeState === 'autocraft' ? { outline: '2px solid #fff' } : {}}
|
||
>
|
||
🔨 Autocraft
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="detail-section">
|
||
<h3>Movement</h3>
|
||
<div className="movement-controls">
|
||
<div className="movement-row">
|
||
<button onClick={() => moveForward(turtle.turtleID)} title="Move forward">↑</button>
|
||
</div>
|
||
<div className="movement-row">
|
||
<button onClick={() => turnLeft(turtle.turtleID)} title="Turn left">←</button>
|
||
<button onClick={() => moveBack(turtle.turtleID)} title="Move backward">↓</button>
|
||
<button onClick={() => turnRight(turtle.turtleID)} title="Turn right">→</button>
|
||
</div>
|
||
<div className="movement-row vertical">
|
||
<button onClick={() => moveUp(turtle.turtleID)} title="Move up">⬆ Up</button>
|
||
<button onClick={() => moveDown(turtle.turtleID)} title="Move down">⬇ Down</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="detail-section">
|
||
<h3>Actions</h3>
|
||
<div className="action-grid">
|
||
<button
|
||
className="action-btn dig"
|
||
onClick={() => digBlock(turtle.turtleID)}
|
||
title="Dig block in front"
|
||
>
|
||
⛏️ Dig
|
||
</button>
|
||
<button
|
||
className="action-btn digup"
|
||
onClick={() => digBlockUp(turtle.turtleID)}
|
||
title="Dig block above"
|
||
>
|
||
⬆️ Dig Up
|
||
</button>
|
||
<button
|
||
className="action-btn digdown"
|
||
onClick={() => digBlockDown(turtle.turtleID)}
|
||
title="Dig block below"
|
||
>
|
||
⬇️ Dig Down
|
||
</button>
|
||
<button
|
||
className="action-btn place"
|
||
onClick={() => placeBlock(turtle.turtleID)}
|
||
title="Place block from inventory"
|
||
>
|
||
🧱 Place
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Equipment & Inventory Actions */}
|
||
<div className="detail-section">
|
||
<h3>Equipment & Inventory</h3>
|
||
<div className="action-grid">
|
||
<button
|
||
className="action-btn"
|
||
onClick={() => equipLeft(turtle.turtleID)}
|
||
title="Equip selected item on left side"
|
||
>
|
||
🛡️ Equip L
|
||
</button>
|
||
<button
|
||
className="action-btn"
|
||
onClick={() => equipRight(turtle.turtleID)}
|
||
title="Equip selected item on right side"
|
||
>
|
||
🗡️ Equip R
|
||
</button>
|
||
<button
|
||
className="action-btn"
|
||
onClick={() => sortInventory(turtle.turtleID)}
|
||
title="Sort and compact inventory"
|
||
>
|
||
📋 Sort
|
||
</button>
|
||
<button
|
||
className="action-btn"
|
||
onClick={() => dropItems(turtle.turtleID, 'front')}
|
||
title="Drop items forward"
|
||
>
|
||
📤 Drop
|
||
</button>
|
||
<button
|
||
className="action-btn"
|
||
onClick={() => dropItems(turtle.turtleID, 'up')}
|
||
title="Drop items upward"
|
||
>
|
||
⬆️ Drop Up
|
||
</button>
|
||
<button
|
||
className="action-btn"
|
||
onClick={() => dropItems(turtle.turtleID, 'down')}
|
||
title="Drop items downward"
|
||
>
|
||
⬇️ Drop Down
|
||
</button>
|
||
<button
|
||
className="action-btn"
|
||
onClick={() => suckItems(turtle.turtleID, 'front')}
|
||
title="Suck items from front"
|
||
>
|
||
📥 Suck
|
||
</button>
|
||
<button
|
||
className="action-btn"
|
||
onClick={() => {
|
||
const side = prompt('Enter peripheral side (front/top/bottom/left/right):');
|
||
if (side) connectToInventory(turtle.turtleID, side);
|
||
}}
|
||
title="Read adjacent inventory peripheral contents"
|
||
>
|
||
📦 Read Inv
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{turtle.inventory && turtle.inventory.length > 0 && (
|
||
<div className="detail-section">
|
||
<h3>Inventory ({turtle.inventoryCount || turtle.inventory.length}/16) — Slot: {turtle.selectedSlot || 1}</h3>
|
||
<div className="inventory-grid">
|
||
{Array.from({ length: 16 }, (_, slotIndex) => {
|
||
const item = turtle.inventory[slotIndex];
|
||
const isSelected = (turtle.selectedSlot || 1) === (slotIndex + 1);
|
||
return (
|
||
<div
|
||
key={slotIndex}
|
||
className={`inventory-slot ${item ? 'filled' : 'empty'} ${isSelected ? 'selected-slot' : ''}`}
|
||
title={item ? `${item.name.replace('minecraft:', '').replace(/_/g, ' ')} (${item.count}) — Click to select` : `Slot ${slotIndex + 1} — Click to select`}
|
||
onClick={() => handleSlotClick(slotIndex)}
|
||
style={isSelected ? { outline: '2px solid #55ffff', outlineOffset: '-2px' } : {}}
|
||
>
|
||
{item ? (
|
||
<>
|
||
<div className="slot-item">
|
||
<span className="item-icon">
|
||
{item.name.includes('diamond') ? '💎' :
|
||
item.name.includes('gold') ? '<27>' :
|
||
item.name.includes('iron') ? '⚪' :
|
||
item.name.includes('coal') ? '⚫' :
|
||
item.name.includes('emerald') ? '🟢' :
|
||
item.name.includes('redstone') ? '🔴' :
|
||
item.name.includes('lapis') ? '🔵' :
|
||
item.name.includes('stone') ? '🗿' :
|
||
item.name.includes('dirt') ? '🟤' :
|
||
item.name.includes('wood') || item.name.includes('log') ? '🪵' :
|
||
item.name.includes('cobble') ? '🪨' : '<27>📦'}
|
||
</span>
|
||
<span className="item-count">{item.count}</span>
|
||
</div>
|
||
<div className="slot-name">
|
||
{item.name.replace('minecraft:', '').replace(/_/g, ' ').split(' ').slice(0, 2).join(' ')}
|
||
</div>
|
||
</>
|
||
) : (
|
||
<span className="slot-number">{slotIndex + 1}</span>
|
||
)}
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export default function ControlPanel() {
|
||
const turtles = useTurtleStore((state) => state.getTurtleArray());
|
||
const selectedTurtleId = useTurtleStore((state) => state.selectedTurtleId);
|
||
const selectTurtle = useTurtleStore((state) => state.selectTurtle);
|
||
const selectedTurtle = useTurtleStore((state) => state.getSelectedTurtle());
|
||
const connected = useTurtleStore((state) => state.connected);
|
||
|
||
return (
|
||
<div className="control-panel">
|
||
<div className="panel-header">
|
||
<h1>🐢 Turtle Control Center</h1>
|
||
<div className={`connection-status ${connected ? 'connected' : 'disconnected'}`}>
|
||
<span className="status-dot"></span>
|
||
{connected ? 'Connected' : 'Disconnected'}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="panel-content">
|
||
<div className="turtle-list">
|
||
<h2>Turtles ({turtles.length})</h2>
|
||
{turtles.length === 0 ? (
|
||
<div className="empty-state">
|
||
<p>No turtles connected</p>
|
||
<p className="hint">Waiting for turtles to come online...</p>
|
||
</div>
|
||
) : (
|
||
<div className="turtle-cards">
|
||
{turtles.map((turtle) => (
|
||
<TurtleCard
|
||
key={turtle.turtleID}
|
||
turtle={turtle}
|
||
isSelected={selectedTurtleId === turtle.turtleID}
|
||
onSelect={() => selectTurtle(turtle.turtleID)}
|
||
/>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<TurtleDetails turtle={selectedTurtle} />
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|