667 lines
22 KiB
TypeScript
667 lines
22 KiB
TypeScript
import { WalletIcon } from "../../../../components/icons/3dChartIcons";
|
|
import { useEffect, useRef, useState } from "react";
|
|
import {
|
|
useDroppedObjectsStore,
|
|
Zones,
|
|
} from "../../../../store/useDroppedObjectsStore";
|
|
import useModuleStore from "../../../../store/useModuleStore";
|
|
import { determinePosition } from "../../functions/determinePosition";
|
|
import { getActiveProperties } from "../../functions/getActiveProperties";
|
|
import { addingFloatingWidgets } from "../../../../services/realTimeVisulization/zoneData/addFloatingWidgets";
|
|
import {
|
|
DublicateIcon,
|
|
KebabIcon,
|
|
DeleteIcon,
|
|
} from "../../../../components/icons/ExportCommonIcons";
|
|
import DistanceLines from "./DistanceLines"; // Import the DistanceLines component
|
|
import { deleteFloatingWidgetApi } from "../../../../services/realTimeVisulization/zoneData/deleteFloatingWidget";
|
|
|
|
import TotalCardComponent from "./cards/TotalCardComponent";
|
|
import WarehouseThroughputComponent from "./cards/WarehouseThroughputComponent";
|
|
import FleetEfficiencyComponent from "./cards/FleetEfficiencyComponent";
|
|
import { useWidgetStore } from "../../../../store/useWidgetStore";
|
|
import { useSocketStore } from "../../../../store/store";
|
|
import { useClickOutside } from "../../functions/handleWidgetsOuterClick";
|
|
import { usePlayButtonStore } from "../../../../store/usePlayButtonStore";
|
|
import { useSelectedZoneStore } from "../../../../store/useZoneStore";
|
|
interface DraggingState {
|
|
zone: string;
|
|
index: number;
|
|
initialPosition: {
|
|
top: number | "auto";
|
|
left: number | "auto";
|
|
right: number | "auto";
|
|
bottom: number | "auto";
|
|
};
|
|
}
|
|
|
|
interface DraggingState {
|
|
zone: string;
|
|
index: number;
|
|
initialPosition: {
|
|
top: number | "auto";
|
|
left: number | "auto";
|
|
right: number | "auto";
|
|
bottom: number | "auto";
|
|
};
|
|
}
|
|
const DroppedObjects: React.FC = () => {
|
|
const { visualizationSocket } = useSocketStore();
|
|
const { isPlaying } = usePlayButtonStore();
|
|
const zones = useDroppedObjectsStore((state) => state.zones);
|
|
|
|
const [openKebabId, setOpenKebabId] = useState<string | null>(null);
|
|
const updateObjectPosition = useDroppedObjectsStore(
|
|
(state) => state.updateObjectPosition
|
|
);
|
|
const { setSelectedZone, selectedZone } = useSelectedZoneStore();
|
|
|
|
const deleteObject = useDroppedObjectsStore((state) => state.deleteObject);
|
|
|
|
const duplicateObject = useDroppedObjectsStore(
|
|
(state) => state.duplicateObject
|
|
);
|
|
const [draggingIndex, setDraggingIndex] = useState<DraggingState | null>(
|
|
null
|
|
);
|
|
const [offset, setOffset] = useState<[number, number] | null>(null);
|
|
const { selectedChartId, setSelectedChartId } = useWidgetStore();
|
|
|
|
const [activeEdges, setActiveEdges] = useState<{
|
|
vertical: "top" | "bottom";
|
|
horizontal: "left" | "right";
|
|
} | null>(null); // State to track active edges for distance lines
|
|
|
|
const [currentPosition, setCurrentPosition] = useState<{
|
|
top: number | "auto";
|
|
left: number | "auto";
|
|
right: number | "auto";
|
|
bottom: number | "auto";
|
|
} | null>(null); // State to track the current position during drag
|
|
const animationRef = useRef<number | null>(null);
|
|
const { activeModule } = useModuleStore();
|
|
const chartWidget = useRef<HTMLDivElement>(null);
|
|
|
|
// useClickOutside(chartWidget, () => {
|
|
// setSelectedChartId(null);
|
|
// });
|
|
const kebabRef = useRef<HTMLDivElement>(null);
|
|
|
|
// Clean up animation frame on unmount
|
|
useEffect(() => {
|
|
return () => {
|
|
if (animationRef.current) {
|
|
cancelAnimationFrame(animationRef.current);
|
|
}
|
|
};
|
|
}, []);
|
|
useEffect(() => {
|
|
const handleClickOutside = (event: MouseEvent) => {
|
|
if (
|
|
kebabRef.current &&
|
|
!kebabRef.current.contains(event.target as Node)
|
|
) {
|
|
setOpenKebabId(null);
|
|
}
|
|
};
|
|
|
|
// Add event listener when component mounts
|
|
document.addEventListener("mousedown", handleClickOutside);
|
|
|
|
// Clean up event listener when component unmounts
|
|
return () => {
|
|
document.removeEventListener("mousedown", handleClickOutside);
|
|
};
|
|
}, []);
|
|
|
|
const zoneEntries = Object.entries(zones);
|
|
if (zoneEntries.length === 0) return null;
|
|
const [zoneName, zone] = zoneEntries[0];
|
|
|
|
function handleDuplicate(zoneName: string, index: number) {
|
|
setOpenKebabId(null);
|
|
duplicateObject(zoneName, index); // Call the duplicateObject method from the store
|
|
}
|
|
|
|
async function handleDelete(zoneName: string, id: string) {
|
|
try {
|
|
const email = localStorage.getItem("email") || "";
|
|
const organization = email?.split("@")[1]?.split(".")[0];
|
|
|
|
let deleteFloatingWidget = {
|
|
floatWidgetID: id,
|
|
organization: organization,
|
|
zoneId: zone.zoneId,
|
|
};
|
|
|
|
if (visualizationSocket) {
|
|
visualizationSocket.emit("v2:viz-float:delete", deleteFloatingWidget);
|
|
}
|
|
deleteObject(zoneName, id);
|
|
|
|
// let res = await deleteFloatingWidgetApi(id, organization);
|
|
// s
|
|
|
|
// if (res.message === "FloatingWidget deleted successfully") {
|
|
// deleteObject(zoneName, id, index); // Call the deleteObject method from the store
|
|
// }
|
|
} catch (error) {}
|
|
}
|
|
|
|
const handlePointerDown = (event: React.PointerEvent, index: number) => {
|
|
if (
|
|
(event.target as HTMLElement).closest(".kebab-options") ||
|
|
(event.target as HTMLElement).closest(".kebab")
|
|
) {
|
|
return; // Prevent dragging when clicking on the kebab menu or its options
|
|
}
|
|
const obj = zone.objects[index];
|
|
const element = event.currentTarget as HTMLElement;
|
|
element.setPointerCapture(event.pointerId);
|
|
const container = document.getElementById("real-time-vis-canvas");
|
|
if (!container) return;
|
|
|
|
const rect = container.getBoundingClientRect();
|
|
const relativeX = event.clientX - rect.left;
|
|
const relativeY = event.clientY - rect.top;
|
|
|
|
// Determine active properties for the initial position
|
|
const [activeProp1, activeProp2] = getActiveProperties(obj.position);
|
|
|
|
// Set active edges for distance lines
|
|
const vertical = activeProp1 === "top" ? "top" : "bottom";
|
|
const horizontal = activeProp2 === "left" ? "left" : "right";
|
|
setActiveEdges({ vertical, horizontal });
|
|
|
|
// Store the initial position strategy and active edges
|
|
setDraggingIndex({
|
|
zone: zoneName,
|
|
index,
|
|
initialPosition: { ...obj.position },
|
|
});
|
|
|
|
// Calculate offset from mouse to object edges
|
|
let offsetX = 0;
|
|
let offsetY = 0;
|
|
|
|
if (activeProp1 === "top") {
|
|
offsetY = relativeY - (obj.position.top as number);
|
|
} else {
|
|
offsetY = rect.height - relativeY - (obj.position.bottom as number);
|
|
}
|
|
|
|
if (activeProp2 === "left") {
|
|
offsetX = relativeX - (obj.position.left as number);
|
|
} else {
|
|
offsetX = rect.width - relativeX - (obj.position.right as number);
|
|
}
|
|
|
|
setOffset([offsetY, offsetX]);
|
|
|
|
// Add native event listeners for smoother tracking
|
|
const handlePointerMoveNative = (e: PointerEvent) => {
|
|
if (!draggingIndex || !offset) return;
|
|
if (isPlaying === true) return;
|
|
|
|
const rect = container.getBoundingClientRect();
|
|
const relativeX = e.clientX - rect.left;
|
|
const relativeY = e.clientY - rect.top;
|
|
|
|
// Use requestAnimationFrame for smooth updates
|
|
animationRef.current = requestAnimationFrame(() => {
|
|
// Dynamically determine the current position strategy
|
|
const newPositionStrategy = determinePosition(
|
|
rect,
|
|
relativeX,
|
|
relativeY
|
|
);
|
|
const [activeProp1, activeProp2] =
|
|
getActiveProperties(newPositionStrategy);
|
|
|
|
// Update active edges for distance lines
|
|
const vertical = activeProp1 === "top" ? "top" : "bottom";
|
|
const horizontal = activeProp2 === "left" ? "left" : "right";
|
|
setActiveEdges({ vertical, horizontal });
|
|
|
|
// Calculate new position based on the active properties
|
|
let newY = 0;
|
|
let newX = 0;
|
|
|
|
if (activeProp1 === "top") {
|
|
newY = relativeY - offset[0];
|
|
} else {
|
|
newY = rect.height - (relativeY + offset[0]);
|
|
}
|
|
|
|
if (activeProp2 === "left") {
|
|
newX = relativeX - offset[1];
|
|
} else {
|
|
newX = rect.width - (relativeX + offset[1]);
|
|
}
|
|
|
|
// Apply boundaries
|
|
newX = Math.max(0, Math.min(rect.width - 50, newX));
|
|
newY = Math.max(0, Math.min(rect.height - 50, newY));
|
|
|
|
// Create new position object
|
|
const newPosition = {
|
|
...newPositionStrategy,
|
|
[activeProp1]: newY,
|
|
[activeProp2]: newX,
|
|
// Clear opposite properties
|
|
[activeProp1 === "top" ? "bottom" : "top"]: "auto",
|
|
[activeProp2 === "left" ? "right" : "left"]: "auto",
|
|
};
|
|
|
|
// Update the current position state for DistanceLines
|
|
setCurrentPosition(newPosition);
|
|
updateObjectPosition(zoneName, draggingIndex.index, newPosition);
|
|
});
|
|
};
|
|
|
|
const handlePointerUpNative = async (e: PointerEvent) => {
|
|
// Clean up native event listeners
|
|
element.removeEventListener("pointermove", handlePointerMoveNative);
|
|
element.removeEventListener("pointerup", handlePointerUpNative);
|
|
element.releasePointerCapture(e.pointerId);
|
|
|
|
if (!draggingIndex || !offset) return;
|
|
|
|
const rect = container.getBoundingClientRect();
|
|
const relativeX = e.clientX - rect.left;
|
|
const relativeY = e.clientY - rect.top;
|
|
|
|
// Determine final position strategy
|
|
const finalPosition = determinePosition(rect, relativeX, relativeY);
|
|
const [activeProp1, activeProp2] = getActiveProperties(finalPosition);
|
|
|
|
// Calculate final position
|
|
let finalY = 0;
|
|
let finalX = 0;
|
|
|
|
if (activeProp1 === "top") {
|
|
finalY = relativeY - offset[0];
|
|
} else {
|
|
finalY = rect.height - (relativeY + offset[0]);
|
|
}
|
|
|
|
if (activeProp2 === "left") {
|
|
finalX = relativeX - offset[1];
|
|
} else {
|
|
finalX = rect.width - (relativeX + offset[1]);
|
|
}
|
|
|
|
// Apply boundaries
|
|
finalX = Math.max(0, Math.min(rect.width - 50, finalX));
|
|
finalY = Math.max(0, Math.min(rect.height - 50, finalY));
|
|
|
|
const boundedPosition = {
|
|
...finalPosition,
|
|
[activeProp1]: finalY,
|
|
[activeProp2]: finalX,
|
|
[activeProp1 === "top" ? "bottom" : "top"]: "auto",
|
|
[activeProp2 === "left" ? "right" : "left"]: "auto",
|
|
};
|
|
|
|
try {
|
|
// Save to backend
|
|
const email = localStorage.getItem("email") || "";
|
|
const organization = email?.split("@")[1]?.split(".")[0];
|
|
const response = await addingFloatingWidgets(
|
|
zone.zoneId,
|
|
organization,
|
|
{
|
|
...zone.objects[draggingIndex.index],
|
|
position: boundedPosition,
|
|
}
|
|
);
|
|
|
|
if (response.message === "Widget updated successfully") {
|
|
updateObjectPosition(zoneName, draggingIndex.index, boundedPosition);
|
|
}
|
|
} catch (error) {
|
|
} finally {
|
|
setDraggingIndex(null);
|
|
setOffset(null);
|
|
setActiveEdges(null);
|
|
setCurrentPosition(null);
|
|
|
|
if (animationRef.current) {
|
|
cancelAnimationFrame(animationRef.current);
|
|
animationRef.current = null;
|
|
}
|
|
}
|
|
};
|
|
|
|
// Add native event listeners
|
|
element.addEventListener("pointermove", handlePointerMoveNative);
|
|
element.addEventListener("pointerup", handlePointerUpNative);
|
|
};
|
|
|
|
const handlePointerMove = (event: React.PointerEvent) => {
|
|
if (!draggingIndex || !offset) return;
|
|
if (isPlaying === true) return;
|
|
const container = document.getElementById("real-time-vis-canvas");
|
|
if (!container) return;
|
|
|
|
const rect = container.getBoundingClientRect();
|
|
const relativeX = event.clientX - rect.left;
|
|
const relativeY = event.clientY - rect.top;
|
|
|
|
// Dynamically determine the current position strategy
|
|
const newPositionStrategy = determinePosition(rect, relativeX, relativeY);
|
|
const [activeProp1, activeProp2] = getActiveProperties(newPositionStrategy);
|
|
|
|
// Update active edges for distance lines
|
|
const vertical = activeProp1 === "top" ? "top" : "bottom";
|
|
const horizontal = activeProp2 === "left" ? "left" : "right";
|
|
setActiveEdges({ vertical, horizontal });
|
|
|
|
// Calculate new position based on the active properties
|
|
let newY = 0;
|
|
let newX = 0;
|
|
|
|
if (activeProp1 === "top") {
|
|
newY = relativeY - offset[0];
|
|
} else {
|
|
newY = rect.height - (relativeY + offset[0]);
|
|
}
|
|
|
|
if (activeProp2 === "left") {
|
|
newX = relativeX - offset[1];
|
|
} else {
|
|
newX = rect.width - (relativeX + offset[1]);
|
|
}
|
|
|
|
// Apply boundaries
|
|
newX = Math.max(0, Math.min(rect.width - 50, newX));
|
|
newY = Math.max(0, Math.min(rect.height - 50, newY));
|
|
|
|
// Create new position object
|
|
const newPosition = {
|
|
...newPositionStrategy,
|
|
[activeProp1]: newY,
|
|
[activeProp2]: newX,
|
|
// Clear opposite properties
|
|
[activeProp1 === "top" ? "bottom" : "top"]: "auto",
|
|
[activeProp2 === "left" ? "right" : "left"]: "auto",
|
|
};
|
|
|
|
// Update the current position state for DistanceLines
|
|
setCurrentPosition(newPosition);
|
|
// Update position immediately without animation frame
|
|
updateObjectPosition(zoneName, draggingIndex.index, newPosition);
|
|
|
|
// if (!animationRef.current) {
|
|
// animationRef.current = requestAnimationFrame(() => {
|
|
// updateObjectPosition(zoneName, draggingIndex.index, newPosition);
|
|
// animationRef.current = null;
|
|
// });
|
|
// }
|
|
};
|
|
|
|
const handlePointerUp = async (event: React.PointerEvent<HTMLDivElement>) => {
|
|
try {
|
|
if (!draggingIndex || !offset) return;
|
|
if (isPlaying === true) return;
|
|
|
|
const container = document.getElementById("real-time-vis-canvas");
|
|
if (!container) return;
|
|
|
|
const rect = container.getBoundingClientRect();
|
|
const relativeX = event.clientX - rect.left;
|
|
const relativeY = event.clientY - rect.top;
|
|
|
|
// Determine final position strategy
|
|
const finalPosition = determinePosition(rect, relativeX, relativeY);
|
|
const [activeProp1, activeProp2] = getActiveProperties(finalPosition);
|
|
|
|
// Calculate final position
|
|
let finalY = 0;
|
|
let finalX = 0;
|
|
|
|
if (activeProp1 === "top") {
|
|
finalY = relativeY - offset[0];
|
|
} else {
|
|
finalY = rect.height - (relativeY + offset[0]);
|
|
}
|
|
|
|
if (activeProp2 === "left") {
|
|
finalX = relativeX - offset[1];
|
|
} else {
|
|
finalX = rect.width - (relativeX + offset[1]);
|
|
}
|
|
|
|
// Apply boundaries
|
|
finalX = Math.max(0, Math.min(rect.width - 50, finalX));
|
|
finalY = Math.max(0, Math.min(rect.height - 50, finalY));
|
|
|
|
const boundedPosition = {
|
|
...finalPosition,
|
|
[activeProp1]: finalY,
|
|
[activeProp2]: finalX,
|
|
// Clear opposite properties
|
|
[activeProp1 === "top" ? "bottom" : "top"]: "auto",
|
|
[activeProp2 === "left" ? "right" : "left"]: "auto",
|
|
};
|
|
|
|
// Save to backend
|
|
const email = localStorage.getItem("email") || "";
|
|
const organization = email?.split("@")[1]?.split(".")[0];
|
|
// const response = await addingFloatingWidgets(zone.zoneId, organization, {
|
|
// ...zone.objects[draggingIndex.index],
|
|
// position: boundedPosition,
|
|
// });
|
|
|
|
let updateFloatingWidget = {
|
|
organization: organization,
|
|
widget: {
|
|
...zone.objects[draggingIndex.index],
|
|
position: boundedPosition,
|
|
},
|
|
index: draggingIndex.index,
|
|
zoneId: zone.zoneId,
|
|
};
|
|
if (visualizationSocket) {
|
|
visualizationSocket.emit("v2:viz-float:add", updateFloatingWidget);
|
|
}
|
|
|
|
// if (response.message === "Widget updated successfully") {
|
|
|
|
updateObjectPosition(zoneName, draggingIndex.index, boundedPosition);
|
|
// }
|
|
|
|
// // Clean up
|
|
// setDraggingIndex(null);
|
|
// setOffset(null);
|
|
// setActiveEdges(null); // Clear active edges
|
|
// setCurrentPosition(null); // Reset current position
|
|
// if (animationRef.current) {
|
|
// cancelAnimationFrame(animationRef.current);
|
|
// animationRef.current = null;
|
|
// }
|
|
} catch (error) {
|
|
} finally {
|
|
// Clean up regardless of success or failure
|
|
setDraggingIndex(null);
|
|
setOffset(null);
|
|
setActiveEdges(null);
|
|
setCurrentPosition(null);
|
|
|
|
// Cancel any pending animation frame
|
|
if (animationRef.current) {
|
|
cancelAnimationFrame(animationRef.current);
|
|
animationRef.current = null;
|
|
}
|
|
}
|
|
};
|
|
|
|
const handleKebabClick = (id: string, event: React.MouseEvent) => {
|
|
event.stopPropagation();
|
|
setOpenKebabId((prevId) => (prevId === id ? null : id));
|
|
};
|
|
|
|
const containerHeight = getComputedStyle(
|
|
document.documentElement
|
|
).getPropertyValue("--realTimeViz-container-height");
|
|
const containerWidth = getComputedStyle(
|
|
document.documentElement
|
|
).getPropertyValue("--realTimeViz-container-width");
|
|
|
|
const heightMultiplier = parseFloat(containerHeight) * 0.14;
|
|
|
|
const widthMultiplier = parseFloat(containerWidth) * 0.13;
|
|
|
|
return (
|
|
<div
|
|
onPointerMove={handlePointerMove}
|
|
onPointerUp={handlePointerUp}
|
|
className="floating-wrapper"
|
|
>
|
|
{zone.objects.map((obj, index) => {
|
|
const topPosition =
|
|
typeof obj.position.top === "number"
|
|
? `calc(${obj.position.top}px + ${
|
|
isPlaying && selectedZone.activeSides.includes("top")
|
|
? `${heightMultiplier - 55}px`
|
|
: "0px"
|
|
})`
|
|
: "auto";
|
|
|
|
const leftPosition =
|
|
typeof obj.position.left === "number"
|
|
? `calc(${obj.position.left}px + ${
|
|
isPlaying && selectedZone.activeSides.includes("left")
|
|
? `${widthMultiplier - 100}px`
|
|
: "0px"
|
|
})`
|
|
: "auto";
|
|
|
|
const rightPosition =
|
|
typeof obj.position.right === "number"
|
|
? `calc(${obj.position.right}px + ${
|
|
isPlaying && selectedZone.activeSides.includes("right")
|
|
? `${widthMultiplier - 100}px`
|
|
: "0px"
|
|
})`
|
|
: "auto";
|
|
|
|
const bottomPosition =
|
|
typeof obj.position.bottom === "number"
|
|
? `calc(${obj.position.bottom}px + ${
|
|
isPlaying && selectedZone.activeSides.includes("bottom")
|
|
? `${heightMultiplier - 55}px`
|
|
: "0px"
|
|
})`
|
|
: "auto";
|
|
|
|
return (
|
|
<div
|
|
key={`${zoneName}-${index}`}
|
|
className={`${obj.className} ${
|
|
selectedChartId?.id === obj.id && "activeChart"
|
|
} `}
|
|
ref={chartWidget}
|
|
style={{
|
|
position: "absolute",
|
|
top: topPosition,
|
|
left: leftPosition,
|
|
right: rightPosition,
|
|
bottom: bottomPosition,
|
|
pointerEvents: isPlaying ? "none" : "auto",
|
|
minHeight: `${
|
|
obj.className === "warehouseThroughput" && "150px !important"
|
|
} `,
|
|
}}
|
|
onPointerDown={(event) => {
|
|
setSelectedChartId(obj);
|
|
handlePointerDown(event, index);
|
|
}}
|
|
>
|
|
{obj.className === "floating total-card" ? (
|
|
<TotalCardComponent object={obj} />
|
|
) : obj.className === "warehouseThroughput floating" ? (
|
|
<WarehouseThroughputComponent object={obj} />
|
|
) : obj.className === "fleetEfficiency floating" ? (
|
|
<FleetEfficiencyComponent object={obj} />
|
|
) : null}
|
|
|
|
<div
|
|
className="icon kebab"
|
|
ref={kebabRef}
|
|
onClick={(event) => {
|
|
event.stopPropagation();
|
|
handleKebabClick(obj.id, event);
|
|
}}
|
|
>
|
|
<KebabIcon />
|
|
</div>
|
|
|
|
{openKebabId === obj.id && (
|
|
<div className="kebab-options" ref={kebabRef}>
|
|
<div
|
|
className="dublicate btn"
|
|
onClick={(event) => {
|
|
event.stopPropagation();
|
|
handleDuplicate(zoneName, index); // Call the duplicate handler
|
|
}}
|
|
>
|
|
<div className="icon">
|
|
<DublicateIcon />
|
|
</div>
|
|
<div className="label">Duplicate</div>
|
|
</div>
|
|
<div
|
|
className="edit btn"
|
|
onClick={(event) => {
|
|
event.stopPropagation();
|
|
handleDelete(zoneName, obj.id); // Call the delete handler
|
|
}}
|
|
>
|
|
<div className="icon">
|
|
<DeleteIcon />
|
|
</div>
|
|
<div className="label">Delete</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
|
|
{/* Render DistanceLines component during drag */}
|
|
{isPlaying === false &&
|
|
draggingIndex !== null &&
|
|
activeEdges !== null &&
|
|
currentPosition !== null && (
|
|
<DistanceLines
|
|
obj={{
|
|
position: {
|
|
top:
|
|
currentPosition.top !== "auto"
|
|
? currentPosition.top
|
|
: undefined,
|
|
bottom:
|
|
currentPosition.bottom !== "auto"
|
|
? currentPosition.bottom
|
|
: undefined,
|
|
left:
|
|
currentPosition.left !== "auto"
|
|
? currentPosition.left
|
|
: undefined,
|
|
right:
|
|
currentPosition.right !== "auto"
|
|
? currentPosition.right
|
|
: undefined,
|
|
},
|
|
}}
|
|
activeEdges={activeEdges}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default DroppedObjects;
|
|
|