feat: Initialize Turtle Control Center with React and WebSocket server
- Added index.html for the main entry point of the client application. - Created package.json for client dependencies and scripts. - Implemented App component with view controls and layout. - Added CSS styles for the application and components. - Developed ControlPanel component for turtle management and commands. - Created Map3D component for 3D visualization of turtles. - Established Zustand store for state management of turtles and WebSocket connection. - Set up server with Express and WebSocket for handling turtle updates and commands. - Added REST API endpoints for turtle status updates and command handling. - Implemented start.sh script for setting up and running the application.
This commit is contained in:
229
client/src/components/ControlPanel.jsx
Normal file
229
client/src/components/ControlPanel.jsx
Normal file
@@ -0,0 +1,229 @@
|
||||
import React from 'react';
|
||||
import { useTurtleStore } from '../store/turtleStore';
|
||||
import './ControlPanel.css';
|
||||
|
||||
function TurtleCard({ turtle, isSelected, onSelect }) {
|
||||
const mode = turtle.mode || 'unknown';
|
||||
const fuel = turtle.fuel === 'unlimited' ? '∞' : (turtle.fuel || '?');
|
||||
const inventoryCount = turtle.inventory?.length || 0;
|
||||
|
||||
const modeColors = {
|
||||
mining: '#4ade80',
|
||||
exploring: '#60a5fa',
|
||||
returning: '#f59e0b',
|
||||
idle: '#9ca3af',
|
||||
manual: '#a78bfa',
|
||||
unknown: '#6b7280'
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`turtle-card ${isSelected ? 'selected' : ''}`}
|
||||
onClick={onSelect}
|
||||
style={{ borderColor: modeColors[mode] }}
|
||||
>
|
||||
<div className="turtle-header">
|
||||
<h3>Turtle {turtle.turtleID}</h3>
|
||||
<span className="mode-badge" style={{ background: modeColors[mode] }}>
|
||||
{mode}
|
||||
</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 sendCommand = useTurtleStore((state) => state.sendCommand);
|
||||
|
||||
if (!turtle) {
|
||||
return (
|
||||
<div className="turtle-details empty">
|
||||
<p>Select a turtle to view details and control it</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const handleCommand = (command, param = null) => {
|
||||
sendCommand(turtle.turtleID, command, param);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="turtle-details">
|
||||
<h2>Turtle {turtle.turtleID} Control</h2>
|
||||
|
||||
<div className="detail-section">
|
||||
<h3>Status</h3>
|
||||
<div className="status-grid">
|
||||
<div className="status-item">
|
||||
<span className="label">Mode:</span>
|
||||
<span className="value">{turtle.mode || 'unknown'}</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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="detail-section">
|
||||
<h3>Commands</h3>
|
||||
<div className="command-grid">
|
||||
<button
|
||||
className="command-btn explore"
|
||||
onClick={() => handleCommand('explore')}
|
||||
>
|
||||
🔍 Explore
|
||||
</button>
|
||||
<button
|
||||
className="command-btn mine"
|
||||
onClick={() => handleCommand('mine')}
|
||||
>
|
||||
⛏️ Mine
|
||||
</button>
|
||||
<button
|
||||
className="command-btn return"
|
||||
onClick={() => handleCommand('returnHome')}
|
||||
>
|
||||
🏠 Return Home
|
||||
</button>
|
||||
<button
|
||||
className="command-btn stop"
|
||||
onClick={() => handleCommand('stop')}
|
||||
>
|
||||
⏹️ Stop
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="manual-controls">
|
||||
<h4>Manual Control</h4>
|
||||
<div className="direction-pad">
|
||||
<button onClick={() => handleCommand('forward')}>↑</button>
|
||||
<div className="horizontal-controls">
|
||||
<button onClick={() => handleCommand('turnLeft')}>←</button>
|
||||
<button onClick={() => handleCommand('turnRight')}>→</button>
|
||||
</div>
|
||||
<button onClick={() => handleCommand('back')}>↓</button>
|
||||
</div>
|
||||
<div className="vertical-controls">
|
||||
<button onClick={() => handleCommand('up')}>⬆ Up</button>
|
||||
<button onClick={() => handleCommand('down')}>⬇ Down</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{turtle.inventory && turtle.inventory.length > 0 && (
|
||||
<div className="detail-section">
|
||||
<h3>Inventory</h3>
|
||||
<div className="inventory-list">
|
||||
{turtle.inventory.map((item, index) => (
|
||||
<div key={index} className="inventory-item">
|
||||
<span className="item-name">
|
||||
{item.name.replace('minecraft:', '')}
|
||||
</span>
|
||||
<span className="item-count">x{item.count}</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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user