From 90b1e9fdeca1f2906a13f1aed13af53ccfc56f9d Mon Sep 17 00:00:00 2001 From: MayaTheShy Date: Thu, 19 Feb 2026 23:02:17 -0500 Subject: [PATCH] feat: Add MiningAreasPanel component for managing mining areas with create, update, and delete functionalities --- client/src/App.jsx | 12 +- client/src/components/Map3D.jsx | 129 ++++++ client/src/components/MiningAreasPanel.css | 434 ++++++++++++++++++++ client/src/components/MiningAreasPanel.jsx | 443 +++++++++++++++++++++ 4 files changed, 1017 insertions(+), 1 deletion(-) create mode 100644 client/src/components/MiningAreasPanel.css create mode 100644 client/src/components/MiningAreasPanel.jsx diff --git a/client/src/App.jsx b/client/src/App.jsx index 70d6d9e..9b76aa5 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -6,13 +6,14 @@ import StatsPanel from './components/StatsPanel'; import GroupsPanel from './components/GroupsPanel'; import TaskPanel from './components/TaskPanel'; import PathRecorder from './components/PathRecorder'; +import MiningAreasPanel from './components/MiningAreasPanel'; import { useTurtleStore } from './store/turtleStore'; import './App.css'; function App() { const connect = useTurtleStore((state) => state.connect); const [view, setView] = useState('split'); // 'split', 'map', 'panel' - const [panelTab, setPanelTab] = useState('control'); // 'control', 'voice', 'stats', 'groups', 'tasks', 'paths' + const [panelTab, setPanelTab] = useState('control'); // 'control', 'voice', 'stats', 'groups', 'tasks', 'paths', 'areas' const turtles = useTurtleStore((state) => state.getTurtleArray()); const selectedTurtle = useTurtleStore((state) => state.getSelectedTurtle()); @@ -37,6 +38,8 @@ function App() { return ; case 'paths': return ; + case 'areas': + return ; default: return ; } @@ -116,6 +119,13 @@ function App() { > 🛤️ Paths +
{renderPanelContent()} diff --git a/client/src/components/Map3D.jsx b/client/src/components/Map3D.jsx index 675cc46..2753860 100644 --- a/client/src/components/Map3D.jsx +++ b/client/src/components/Map3D.jsx @@ -414,12 +414,127 @@ function HomeMarker({ position }) { ); } +// Mining area wireframe component +function MiningArea({ area, turtle, isSelected, onClick }) { + const meshRef = useRef(); + const [hovered, setHovered] = useState(false); + + // Calculate dimensions + const width = Math.abs(area.endX - area.startX) + 1; + const height = Math.abs(area.endY - area.startY) + 1; + const depth = Math.abs(area.endZ - area.startZ) + 1; + + // Calculate center position + const centerX = (area.startX + area.endX) / 2; + const centerY = (area.startY + area.endY) / 2; + const centerZ = (area.startZ + area.endZ) / 2; + + // Color based on turtle or status + const getColor = () => { + if (area.status === 'completed') return '#10b981'; // green + if (area.status === 'mining') return '#f59e0b'; // orange + if (turtle) return turtle.color || '#3b82f6'; // turtle color or blue + return '#6366f1'; // default purple + }; + + const color = getColor(); + const opacity = isSelected ? 0.3 : hovered ? 0.2 : 0.1; + const lineWidth = isSelected ? 2.5 : hovered ? 2 : 1.5; + + // Animate rotation slightly + useFrame((state) => { + if (meshRef.current && isSelected) { + meshRef.current.rotation.y = Math.sin(state.clock.elapsedTime * 0.5) * 0.1; + } + }); + + return ( + setHovered(true)} + onPointerOut={() => setHovered(false)} + ref={meshRef} + > + {/* Wireframe box */} + + + + + + {/* Semi-transparent fill */} + + + + + + {/* Label */} + + {area.areaName || `Area ${area.areaID}`} + + + {/* Dimensions label */} + + {width}×{height}×{depth} + + + {/* Status indicator */} + {area.status === 'mining' && ( + + + + + )} + + ); +} + // Main scene component function Scene() { const turtles = useTurtleStore((state) => state.getTurtleArray()); const selectedTurtleId = useTurtleStore((state) => state.selectedTurtleId); const selectTurtle = useTurtleStore((state) => state.selectTurtle); const worldBlocks = useTurtleStore((state) => state.worldBlocks || []); + + // Mining areas state + const [miningAreas, setMiningAreas] = useState([]); + const [selectedAreaId, setSelectedAreaId] = useState(null); + + // Load mining areas + useEffect(() => { + const fetchMiningAreas = async () => { + try { + const response = await fetch('http://localhost:3001/api/mining-areas'); + if (response.ok) { + const data = await response.json(); + setMiningAreas(data); + } + } catch (error) { + console.error('Failed to fetch mining areas:', error); + } + }; + + fetchMiningAreas(); + const interval = setInterval(fetchMiningAreas, 5000); // Refresh every 5 seconds + return () => clearInterval(interval); + }, []); // Calculate center point for camera focus const centerPoint = useMemo(() => { @@ -469,6 +584,20 @@ function Scene() { {/* Render discovered blocks */} + {/* Mining areas */} + {miningAreas.map((area) => { + const turtle = turtles.find(t => t.turtleID === area.turtleID); + return ( + setSelectedAreaId(selectedAreaId === area.areaID ? null : area.areaID)} + /> + ); + })} + {/* Home marker */} {homePosition && } diff --git a/client/src/components/MiningAreasPanel.css b/client/src/components/MiningAreasPanel.css new file mode 100644 index 0000000..434164f --- /dev/null +++ b/client/src/components/MiningAreasPanel.css @@ -0,0 +1,434 @@ +.mining-areas-panel { + height: 100%; + display: flex; + flex-direction: column; + background: #0f172a; + color: #e2e8f0; + overflow: hidden; +} + +.panel-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem 1.5rem; + background: #1e293b; + border-bottom: 2px solid #334155; +} + +.panel-header h2 { + margin: 0; + font-size: 1.5rem; + color: #f8fafc; +} + +.btn-create { + padding: 0.5rem 1rem; + background: #3b82f6; + color: white; + border: none; + border-radius: 6px; + font-size: 0.9rem; + font-weight: 600; + cursor: pointer; + transition: all 0.2s; +} + +.btn-create:hover { + background: #2563eb; + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4); +} + +/* Filter buttons */ +.filter-buttons { + display: flex; + gap: 0.5rem; + padding: 1rem 1.5rem; + background: #1e293b; + border-bottom: 1px solid #334155; + flex-wrap: wrap; +} + +.filter-btn { + padding: 0.5rem 1rem; + background: #334155; + color: #94a3b8; + border: none; + border-radius: 6px; + font-size: 0.875rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; + white-space: nowrap; +} + +.filter-btn:hover { + background: #475569; + color: #e2e8f0; +} + +.filter-btn.active { + background: #3b82f6; + color: white; +} + +/* Create form */ +.create-form { + padding: 1.5rem; + background: #1e293b; + border-bottom: 1px solid #334155; + max-height: 60vh; + overflow-y: auto; +} + +.create-form h3 { + margin: 0 0 1rem 0; + color: #f8fafc; +} + +.form-group { + margin-bottom: 1rem; +} + +.form-group label { + display: block; + margin-bottom: 0.5rem; + color: #cbd5e1; + font-size: 0.875rem; + font-weight: 500; +} + +.form-group input, +.form-group select { + width: 100%; + padding: 0.625rem; + background: #0f172a; + border: 1px solid #334155; + border-radius: 6px; + color: #e2e8f0; + font-size: 0.875rem; + transition: all 0.2s; +} + +.form-group input:focus, +.form-group select:focus { + outline: none; + border-color: #3b82f6; + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); +} + +.coordinates-section { + margin-bottom: 1rem; +} + +.coordinates-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.5rem; +} + +.coordinates-header label { + color: #cbd5e1; + font-size: 0.875rem; + font-weight: 500; +} + +.btn-use-position { + padding: 0.375rem 0.75rem; + background: #10b981; + color: white; + border: none; + border-radius: 4px; + font-size: 0.75rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; +} + +.btn-use-position:hover:not(:disabled) { + background: #059669; +} + +.btn-use-position:disabled { + background: #334155; + color: #64748b; + cursor: not-allowed; +} + +.coordinate-inputs { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 0.5rem; +} + +.coordinate-inputs input { + width: 100%; +} + +.btn-submit { + width: 100%; + padding: 0.75rem; + background: #3b82f6; + color: white; + border: none; + border-radius: 6px; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: all 0.2s; + margin-top: 1rem; +} + +.btn-submit:hover { + background: #2563eb; + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4); +} + +/* Areas list */ +.areas-list { + flex: 1; + overflow-y: auto; + padding: 1rem; +} + +.empty-state { + text-align: center; + padding: 3rem 1rem; + color: #64748b; +} + +.empty-state p { + margin: 0.5rem 0; +} + +/* Area card */ +.area-card { + background: #1e293b; + border: 1px solid #334155; + border-radius: 8px; + padding: 1rem; + margin-bottom: 1rem; + transition: all 0.2s; +} + +.area-card:hover { + border-color: #475569; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); +} + +.area-card.has-conflict { + border-color: #ef4444; + background: linear-gradient(135deg, #1e293b 0%, #2d1818 100%); +} + +.area-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; + padding-bottom: 0.75rem; + border-bottom: 1px solid #334155; +} + +.area-header h3 { + margin: 0; + color: #f8fafc; + font-size: 1.125rem; +} + +.status-badge { + padding: 0.25rem 0.75rem; + border-radius: 12px; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + color: white; +} + +/* Area info */ +.area-info { + margin-bottom: 1rem; +} + +.info-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.5rem 0; + border-bottom: 1px solid #334155; +} + +.info-row:last-child { + border-bottom: none; +} + +.info-label { + color: #94a3b8; + font-size: 0.875rem; + font-weight: 500; +} + +.info-value { + color: #e2e8f0; + font-size: 0.875rem; + font-weight: 600; + text-align: right; +} + +.coordinate-value { + font-family: 'Courier New', monospace; + font-size: 0.75rem; + word-break: break-all; +} + +/* Conflict warning */ +.conflict-warning { + background: #7f1d1d; + border: 1px solid #ef4444; + border-radius: 6px; + padding: 0.75rem; + margin-bottom: 1rem; + color: #fca5a5; + font-size: 0.875rem; +} + +.conflict-warning ul { + margin: 0.5rem 0 0 0; + padding-left: 1.5rem; +} + +.conflict-warning li { + margin: 0.25rem 0; +} + +/* Area actions */ +.area-actions { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; +} + +.btn-action { + flex: 1; + min-width: 120px; + padding: 0.5rem 1rem; + border: none; + border-radius: 6px; + font-size: 0.875rem; + font-weight: 600; + cursor: pointer; + transition: all 0.2s; + white-space: nowrap; +} + +.btn-start { + background: #10b981; + color: white; +} + +.btn-start:hover { + background: #059669; + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4); +} + +.btn-complete { + background: #3b82f6; + color: white; +} + +.btn-complete:hover { + background: #2563eb; + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4); +} + +.btn-delete { + background: #ef4444; + color: white; +} + +.btn-delete:hover { + background: #dc2626; + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(239, 68, 68, 0.4); +} + +/* Scrollbar styling */ +.areas-list::-webkit-scrollbar, +.create-form::-webkit-scrollbar { + width: 8px; +} + +.areas-list::-webkit-scrollbar-track, +.create-form::-webkit-scrollbar-track { + background: #0f172a; +} + +.areas-list::-webkit-scrollbar-thumb, +.create-form::-webkit-scrollbar-thumb { + background: #334155; + border-radius: 4px; +} + +.areas-list::-webkit-scrollbar-thumb:hover, +.create-form::-webkit-scrollbar-thumb:hover { + background: #475569; +} + +/* Responsive design */ +@media (max-width: 768px) { + .panel-header { + padding: 0.75rem 1rem; + } + + .panel-header h2 { + font-size: 1.25rem; + } + + .filter-buttons { + padding: 0.75rem 1rem; + } + + .create-form { + padding: 1rem; + } + + .areas-list { + padding: 0.75rem; + } + + .area-actions { + flex-direction: column; + } + + .btn-action { + width: 100%; + } +} + +@media (max-width: 480px) { + .coordinate-inputs { + grid-template-columns: 1fr; + } + + .area-header { + flex-direction: column; + align-items: flex-start; + gap: 0.5rem; + } + + .info-row { + flex-direction: column; + align-items: flex-start; + gap: 0.25rem; + } + + .info-value { + text-align: left; + } +} diff --git a/client/src/components/MiningAreasPanel.jsx b/client/src/components/MiningAreasPanel.jsx new file mode 100644 index 0000000..8687555 --- /dev/null +++ b/client/src/components/MiningAreasPanel.jsx @@ -0,0 +1,443 @@ +import React, { useState, useEffect } from 'react'; +import './MiningAreasPanel.css'; + +export default function MiningAreasPanel({ turtles, selectedTurtle, apiUrl }) { + const [areas, setAreas] = useState([]); + const [showCreateForm, setShowCreateForm] = useState(false); + const [newArea, setNewArea] = useState({ + areaName: '', + startX: '', + startY: '', + startZ: '', + endX: '', + endY: '', + endZ: '', + turtleID: '' + }); + const [filterStatus, setFilterStatus] = useState('all'); // all, planned, mining, completed + + // Load mining areas + useEffect(() => { + loadAreas(); + const interval = setInterval(loadAreas, 5000); + return () => clearInterval(interval); + }, [apiUrl]); + + const loadAreas = async () => { + try { + const response = await fetch(`${apiUrl}/api/mining-areas`); + if (response.ok) { + const data = await response.json(); + setAreas(data); + } + } catch (error) { + console.error('Failed to load mining areas:', error); + } + }; + + // Create new mining area + const handleCreateArea = async (e) => { + e.preventDefault(); + + if (!newArea.areaName || !newArea.turtleID) { + alert('Please provide area name and assign a turtle'); + return; + } + + // Validate coordinates + const coords = ['startX', 'startY', 'startZ', 'endX', 'endY', 'endZ']; + for (const coord of coords) { + if (newArea[coord] === '' || isNaN(Number(newArea[coord]))) { + alert(`Please provide valid ${coord} coordinate`); + return; + } + } + + try { + const response = await fetch(`${apiUrl}/api/mining-areas`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + ...newArea, + startX: Number(newArea.startX), + startY: Number(newArea.startY), + startZ: Number(newArea.startZ), + endX: Number(newArea.endX), + endY: Number(newArea.endY), + endZ: Number(newArea.endZ), + status: 'planned' + }) + }); + + if (response.ok) { + setShowCreateForm(false); + setNewArea({ + areaName: '', + startX: '', + startY: '', + startZ: '', + endX: '', + endY: '', + endZ: '', + turtleID: '' + }); + loadAreas(); + } else { + const error = await response.json(); + alert(`Failed to create area: ${error.error || 'Unknown error'}`); + } + } catch (error) { + console.error('Failed to create mining area:', error); + alert('Failed to create mining area'); + } + }; + + // Delete mining area + const handleDeleteArea = async (areaID) => { + if (!confirm('Are you sure you want to delete this mining area?')) return; + + try { + const response = await fetch(`${apiUrl}/api/mining-areas/${areaID}`, { + method: 'DELETE' + }); + + if (response.ok) { + loadAreas(); + } else { + alert('Failed to delete area'); + } + } catch (error) { + console.error('Failed to delete area:', error); + alert('Failed to delete area'); + } + }; + + // Update area status + const handleUpdateStatus = async (areaID, newStatus) => { + try { + const response = await fetch(`${apiUrl}/api/mining-areas/${areaID}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ status: newStatus }) + }); + + if (response.ok) { + loadAreas(); + } else { + alert('Failed to update status'); + } + } catch (error) { + console.error('Failed to update status:', error); + alert('Failed to update status'); + } + }; + + // Auto-fill from selected turtle position + const handleUseCurrentPosition = () => { + if (!selectedTurtle || !selectedTurtle.position) { + alert('No turtle selected or turtle has no position'); + return; + } + + const pos = selectedTurtle.position; + setNewArea(prev => ({ + ...prev, + startX: pos.x.toString(), + startY: pos.y.toString(), + startZ: pos.z.toString(), + turtleID: selectedTurtle.turtleID.toString() + })); + }; + + // Calculate volume + const calculateVolume = (area) => { + const width = Math.abs(area.endX - area.startX) + 1; + const height = Math.abs(area.endY - area.startY) + 1; + const depth = Math.abs(area.endZ - area.startZ) + 1; + return width * height * depth; + }; + + // Check for overlapping areas + const checkOverlap = (area1, area2) => { + const overlapX = Math.max( + Math.min(area1.endX, area2.endX) - Math.max(area1.startX, area2.startX) + 1, + 0 + ); + const overlapY = Math.max( + Math.min(area1.endY, area2.endY) - Math.max(area1.startY, area2.startY) + 1, + 0 + ); + const overlapZ = Math.max( + Math.min(area1.endZ, area2.endZ) - Math.max(area1.startZ, area2.startZ) + 1, + 0 + ); + return overlapX > 0 && overlapY > 0 && overlapZ > 0; + }; + + // Find conflicts + const findConflicts = (area) => { + return areas.filter(other => + other.areaID !== area.areaID && + checkOverlap(area, other) + ); + }; + + // Filter areas + const filteredAreas = areas.filter(area => { + if (filterStatus === 'all') return true; + return area.status === filterStatus; + }); + + // Status badge component + const StatusBadge = ({ status }) => { + const colors = { + planned: '#6366f1', + mining: '#f59e0b', + completed: '#10b981' + }; + return ( + + {status} + + ); + }; + + return ( +
+
+

⛏️ Mining Areas

+ +
+ + {/* Filter buttons */} +
+ + + + +
+ + {/* Create form */} + {showCreateForm && ( +
+

Create Mining Area

+
+
+ + setNewArea({ ...newArea, areaName: e.target.value })} + placeholder="e.g., Diamond Mine #1" + required + /> +
+ +
+ + +
+ +
+
+ + +
+
+ setNewArea({ ...newArea, startX: e.target.value })} + required + /> + setNewArea({ ...newArea, startY: e.target.value })} + required + /> + setNewArea({ ...newArea, startZ: e.target.value })} + required + /> +
+
+ +
+ +
+ setNewArea({ ...newArea, endX: e.target.value })} + required + /> + setNewArea({ ...newArea, endY: e.target.value })} + required + /> + setNewArea({ ...newArea, endZ: e.target.value })} + required + /> +
+
+ + +
+
+ )} + + {/* Areas list */} +
+ {filteredAreas.length === 0 ? ( +
+

No mining areas {filterStatus !== 'all' ? `with status "${filterStatus}"` : ''}

+ {filterStatus === 'all' && ( +

Create a new area to get started

+ )} +
+ ) : ( + filteredAreas.map(area => { + const turtle = turtles.find(t => t.turtleID === area.turtleID); + const conflicts = findConflicts(area); + const volume = calculateVolume(area); + + return ( +
0 ? 'has-conflict' : ''}`}> +
+

{area.areaName}

+ +
+ +
+
+ Turtle: + + 🐢 {turtle ? `${turtle.turtleID} ${turtle.label || ''}` : area.turtleID} + +
+ +
+ Coordinates: + + ({area.startX}, {area.startY}, {area.startZ}) → + ({area.endX}, {area.endY}, {area.endZ}) + +
+ +
+ Volume: + {volume.toLocaleString()} blocks +
+ + {area.createdAt && ( +
+ Created: + + {new Date(area.createdAt).toLocaleDateString()} + +
+ )} +
+ + {conflicts.length > 0 && ( +
+ ⚠️ Overlaps with {conflicts.length} other area(s): +
    + {conflicts.map(c => ( +
  • {c.areaName}
  • + ))} +
+
+ )} + +
+ {area.status === 'planned' && ( + + )} + {area.status === 'mining' && ( + + )} + +
+
+ ); + }) + )} +
+
+ ); +}