This commit is contained in:
Jake Kasper
2025-08-21 17:50:33 -04:00
parent 79bff678f7
commit 2c54a60d84

View File

@@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom'; import { useParams, useNavigate } from 'react-router-dom';
import { MapContainer, TileLayer, Marker, Popup, Polygon, useMapEvents } from 'react-leaflet'; import { MapContainer, TileLayer, Marker, Popup, Polygon, useMapEvents, useMap } from 'react-leaflet';
import { Icon } from 'leaflet'; import { Icon } from 'leaflet';
import * as turf from '@turf/turf'; import * as turf from '@turf/turf';
import { import {
@@ -88,6 +88,141 @@ function PolygonDrawer({ isDrawing, onPolygonComplete, currentColor }) {
) : null; ) : null;
} }
// Component for editable polygons
function EditablePolygon({ section, onUpdate, onEdit, onDelete }) {
// Import toast function from the module scope
const [isEditing, setIsEditing] = useState(false);
const [editedCoordinates, setEditedCoordinates] = useState(section.coordinates);
const map = useMap();
const handleMarkerDrag = (index, newLatLng) => {
const newCoords = [...editedCoordinates];
newCoords[index] = [newLatLng.lat, newLatLng.lng];
setEditedCoordinates(newCoords);
// Recalculate area
const area = calculateAreaInSqFt([...newCoords, newCoords[0]]);
onUpdate(section.id, { ...section, coordinates: newCoords, area });
};
const addPointAtIndex = (index, e) => {
e.originalEvent.stopPropagation();
const newCoords = [...editedCoordinates];
const newPoint = [e.latlng.lat, e.latlng.lng];
newCoords.splice(index + 1, 0, newPoint);
setEditedCoordinates(newCoords);
const area = calculateAreaInSqFt([...newCoords, newCoords[0]]);
onUpdate(section.id, { ...section, coordinates: newCoords, area });
};
const removePoint = (index) => {
if (editedCoordinates.length <= 3) {
toast.error('Polygon must have at least 3 points');
return;
}
const newCoords = editedCoordinates.filter((_, i) => i !== index);
setEditedCoordinates(newCoords);
const area = calculateAreaInSqFt([...newCoords, newCoords[0]]);
onUpdate(section.id, { ...section, coordinates: newCoords, area });
};
return (
<>
<Polygon
positions={editedCoordinates}
pathOptions={{
color: section.color.value,
fillColor: section.color.value,
fillOpacity: isEditing ? 0.3 : 0.4,
weight: isEditing ? 3 : 2
}}
eventHandlers={{
click: () => {
if (!isEditing) {
setIsEditing(true);
toast.info('Edit mode: Drag points to move, right-click to add/remove points');
}
}
}}
>
<Popup>
<div className="text-center">
<strong>{section.name}</strong><br />
{section.area.toLocaleString()} sq ft<br />
<div className="flex gap-2 mt-2">
<button
onClick={() => {
setIsEditing(!isEditing);
toast.info(isEditing ? 'Edit mode disabled' : 'Edit mode enabled');
}}
className={`text-sm ${isEditing ? 'text-green-600' : 'text-blue-600'}`}
>
{isEditing ? 'Done' : 'Edit Points'}
</button>
<button
onClick={() => onEdit(section)}
className="text-blue-600 text-sm"
>
Edit Name
</button>
<button
onClick={() => onDelete(section.id)}
className="text-red-600 text-sm"
>
Delete
</button>
</div>
</div>
</Popup>
</Polygon>
{/* Editable markers for each point */}
{isEditing && editedCoordinates.map((coord, index) => (
<Marker
key={`${section.id}-${index}`}
position={coord}
draggable={true}
eventHandlers={{
dragend: (e) => {
handleMarkerDrag(index, e.target.getLatLng());
},
contextmenu: (e) => {
e.originalEvent.preventDefault();
if (editedCoordinates.length > 3) {
removePoint(index);
}
}
}}
icon={new Icon({
iconUrl: 'data:image/svg+xml;base64,' + btoa(`
<svg width="12" height="12" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
<circle cx="6" cy="6" r="5" fill="${section.color.value}" stroke="white" stroke-width="2"/>
</svg>
`),
iconSize: [12, 12],
iconAnchor: [6, 6]
})}
>
<Popup>
<div className="text-center">
<p className="text-xs">Point {index + 1}</p>
<button
onClick={() => removePoint(index)}
className="text-red-600 text-xs"
disabled={editedCoordinates.length <= 3}
>
Remove Point
</button>
</div>
</Popup>
</Marker>
))}
</>
);
}
const PropertyDetail = () => { const PropertyDetail = () => {
const { id } = useParams(); const { id } = useParams();
const navigate = useNavigate(); const navigate = useNavigate();
@@ -212,6 +347,10 @@ const PropertyDetail = () => {
setShowEditModal(false); setShowEditModal(false);
}; };
const updateSection = (sectionId, updatedSection) => {
setLawnSections(prev => prev.map(s => s.id === sectionId ? updatedSection : s));
};
const getTotalArea = () => { const getTotalArea = () => {
return lawnSections.reduce((total, section) => total + section.area, 0); return lawnSections.reduce((total, section) => total + section.area, 0);
}; };
@@ -290,13 +429,14 @@ const PropertyDetail = () => {
<div style={{ height: '600px', width: '100%' }}> <div style={{ height: '600px', width: '100%' }}>
<MapContainer <MapContainer
center={mapCenter} center={mapCenter}
zoom={hasValidCoordinates ? 21 : 13} zoom={hasValidCoordinates ? 18 : 13}
maxZoom={23} maxZoom={19}
style={{ height: '100%', width: '100%' }} style={{ height: '100%', width: '100%' }}
> >
<TileLayer <TileLayer
attribution='&copy; <a href="https://www.esri.com/">Esri</a>' attribution='&copy; <a href="https://www.esri.com/">Esri</a>'
url="https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}" url="https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}"
maxZoom={19}
/> />
{hasValidCoordinates && ( {hasValidCoordinates && (
@@ -306,37 +446,13 @@ const PropertyDetail = () => {
)} )}
{lawnSections.map((section) => ( {lawnSections.map((section) => (
<Polygon <EditablePolygon
key={section.id} key={section.id}
positions={section.coordinates} section={section}
pathOptions={{ onUpdate={updateSection}
color: section.color.value, onEdit={startEditSection}
fillColor: section.color.value, onDelete={deleteLawnSection}
fillOpacity: 0.4, />
weight: 2
}}
>
<Popup>
<div className="text-center">
<strong>{section.name}</strong><br />
{section.area.toLocaleString()} sq ft<br />
<div className="flex gap-2 mt-2">
<button
onClick={() => startEditSection(section)}
className="text-blue-600 text-sm"
>
Edit
</button>
<button
onClick={() => deleteLawnSection(section.id)}
className="text-red-600 text-sm"
>
Delete
</button>
</div>
</div>
</Popup>
</Polygon>
))} ))}
{isDrawing && ( {isDrawing && (
@@ -360,6 +476,9 @@ const PropertyDetail = () => {
<p className="text-xs text-blue-600 mt-1"> <p className="text-xs text-blue-600 mt-1">
Need at least 3 points to create a section. Press ESC to cancel. Need at least 3 points to create a section. Press ESC to cancel.
</p> </p>
<p className="text-xs text-blue-500 mt-1">
💡 After creating: Click any polygon to edit its points by dragging
</p>
</div> </div>
)} )}
</div> </div>