Compare commits

...

13 Commits

Author SHA1 Message Date
Ayobami 2100daadeb Refactor add selected dock image to appear in front of the sidebar 2025-07-02 20:58:55 +01:00
Ayobami 046280e25c ISSUE 11: add edgeDetectionAndSnap functionality 2025-07-02 20:57:31 +01:00
Ayobami 8bcd4a014c ISSUE 10: add upload file functionality 2025-07-02 18:44:52 +01:00
Ayobami 5843ffdc9b Refactor copy and paste to include dockData 2025-07-02 18:25:38 +01:00
Ayobami ab2f477b7f ISSUE 9: add download file functionality 2025-07-02 18:17:26 +01:00
Ayobami 0496a48623 ISSUE 8: add downloadImage function 2025-07-02 17:57:55 +01:00
Ayobami e089dfabec ISSUE 7: add delete functionality 2025-07-02 17:48:31 +01:00
Ayobami 4904f4d66a ISSUE 6: add print functionality 2025-07-02 17:23:00 +01:00
Ayobami 9936cd0ffc ISSUE 5: add undo functionality 2025-07-02 17:17:20 +01:00
Ayobami e3e95fd05f ISSUE 4: add redo functionality 2025-07-02 16:56:13 +01:00
Ayobami 3e3ea34e5f ISSUE 3: add paste functionality 2025-07-02 16:50:40 +01:00
Ayobami 00f9c602af ISSUE 2: fix copy selection 2025-07-02 16:02:47 +01:00
Ayobami 9ada6f005f ISSUE 1: add selected doc to canvas editor 2025-07-02 15:49:49 +01:00
2 changed files with 347 additions and 126 deletions
+94 -70
View File
@@ -5,13 +5,13 @@ import { fabric } from "fabric";
import {
GrayMaterial,
PerforatedMaterial,
WoodgrainMaterial
WoodgrainMaterial,
} from "Assets/images";
import {
DockPanelCategories,
MaterialType,
DockPanelCategoryMap,
CylinderType
CylinderType,
} from "Utils/constants";
import { Chevron } from "Assets/svgs";
import MkdSDK from "Utils/MkdSDK";
@@ -21,7 +21,7 @@ import {
getMaterial,
getRampsCategory,
getWedgesAndRampsMaterial,
getWedgesCategory
getWedgesCategory,
} from "Utils/utils";
const sdk = new MkdSDK();
@@ -32,7 +32,7 @@ export const Builder = ({ editor }) => {
DockPanelCategories.RollIn
);
const rampsInitialState = {
data: []
data: [],
};
// const [ramps, setRamps] = useState(null);
const [activeLiftRange, setActiveLiftRange] = useState(null);
@@ -44,7 +44,7 @@ export const Builder = ({ editor }) => {
wedges: [],
ramps: [],
selectedRamps: [],
selectedWedges: []
selectedWedges: [],
});
const [boatLift, setBoatLift] = useState([]);
const [liftRanges, setLiftRanges] = useState([]);
@@ -60,56 +60,80 @@ export const Builder = ({ editor }) => {
[activeMaterial]
);
const onDockSelect = useCallback((dock) => {
if (!editor) {
return;
}
const editorHeight = editor.getHeight();
const division = editorHeight / oneFeet - 4;
const onDockSelect = useCallback(
(dock) => {
if (!editor) {
return;
}
const editorHeight = editor.getHeight();
const division = editorHeight / oneFeet - 4;
let imageTopViewURL;
let materials;
let category;
if (["wedges", "ramps"].includes(dock?.type)) {
imageTopViewURL = (dock?.top_view).replace("%20", "+");
let imageTopViewURL;
let materials;
let category;
if (["wedges", "ramps"].includes(dock?.type)) {
imageTopViewURL = (dock?.top_view).replace("%20", "+");
materials = getWedgesAndRampsMaterial(dock?.material);
} else {
imageTopViewURL = dock?.top_view;
materials = getMaterial(dock?.materials);
}
materials = getWedgesAndRampsMaterial(dock?.material);
} else {
imageTopViewURL = dock?.top_view;
materials = getMaterial(dock?.materials);
}
if (["ramps"].includes(dock?.type)) {
category = getRampsCategory(dock?.category);
} else if (["wedges"].includes(dock?.type)) {
category = getWedgesCategory(dock?.category);
} else {
category = getCategory(dock?.category);
}
if (["ramps"].includes(dock?.type)) {
category = getRampsCategory(dock?.category);
} else if (["wedges"].includes(dock?.type)) {
category = getWedgesCategory(dock?.category);
} else {
category = getCategory(dock?.category);
}
const dockData = {
itemName: activeDockCategory,
image: dock?.image,
category: category,
length: dock?.length,
materials: materials,
top_view: dock?.top_view,
width: dock?.width,
lift_range: dock?.lift_range,
model: dock?.model,
no_of_cylinders: dock?.no_of_cylinders,
name: dock?.name,
thumbnail: dock?.thumbnail,
weight_capacity: dock?.weight_capacity,
};
const dockData = {
itemName: activeDockCategory,
image: dock?.image,
category: category,
length: dock?.length,
materials: materials,
top_view: dock?.top_view,
width: dock?.width,
lift_range: dock?.lift_range,
model: dock?.model,
no_of_cylinders: dock?.no_of_cylinders,
name: dock?.name,
thumbnail: dock?.thumbnail,
weight_capacity: dock?.weight_capacity
};
// Add dock to editor as an image
const imageUrl = imageTopViewURL || dock?.image || dock?.thumbnail;
// TODO: Add dock to editor
// TODO: object which is the image should have the dockData, snapAngle of 45, snapThreshold of 5
// TODO: image should be scaled down by scaleFactor
// TODO: image should be positioned at the top left of the editor
// TODO: image should be added to the editor
// TODO: render the editor
}, []);
if (!imageUrl) return;
// Add dock to editor
fabric.Image.fromURL(
imageUrl,
function (img) {
img.set({
left: 300, //To prevent appearing behind the sidebar
top: 0,
scaleX: scaleFactor,
scaleY: scaleFactor,
snapAngle: 45, // TODO: snapAngle
snapThreshold: 5, // TODO: snapThreshold
dockData: dockData, // TODO: attach dockData
selectable: true,
hasControls: true,
});
// Add to editor and make active
editor.add(img); // TODO: add to editor
editor.setActiveObject(img);
editor.requestRenderAll(); // TODO: render editor
},
{ crossOrigin: "anonymous" }
);
},
[editor, activeDockCategory]
);
const getItems = useCallback((table) => {
// console.log( category, materials );
@@ -145,8 +169,8 @@ export const Builder = ({ editor }) => {
? [
...result?.model.map((item) => ({
...item,
type: "boatlifts"
}))
type: "boatlifts",
})),
]
: []
);
@@ -158,8 +182,8 @@ export const Builder = ({ editor }) => {
? [
...result?.model.map((item) => ({
...item,
type: "accessories"
}))
type: "accessories",
})),
]
: []
);
@@ -171,10 +195,10 @@ export const Builder = ({ editor }) => {
? [
...result?.model.map((item) => ({
...item,
type: "wedges"
}))
type: "wedges",
})),
]
: []
: [],
}));
case Tables.Ramps:
// return console.log( result )
@@ -182,7 +206,7 @@ export const Builder = ({ editor }) => {
...prev,
ramps: result?.model
? [...result?.model.map((item) => ({ ...item, type: "ramps" }))]
: []
: [],
}));
// return setRamps(() => [...result?.model]);
}
@@ -231,7 +255,7 @@ export const Builder = ({ editor }) => {
[
DockPanelCategories.RollIn,
DockPanelCategories.Floating,
DockPanelCategories.Sectional
DockPanelCategories.Sectional,
].includes(activeDockCategory)
) {
// console.log( activeDockCategory, activeMaterial )
@@ -296,7 +320,7 @@ export const Builder = ({ editor }) => {
: "bg-gray-200"
}`}
>
<img className={`rounded-md`} src={GrayMaterial} alt="GreyMaterial" />
<img className={`rounded-md`} src={GrayMaterial} alt='GreyMaterial' />
</div>
<div
@@ -311,7 +335,7 @@ export const Builder = ({ editor }) => {
<img
className={`rounded-md`}
src={PerforatedMaterial}
alt="PerforatedMaterial"
alt='PerforatedMaterial'
/>
</div>
@@ -327,7 +351,7 @@ export const Builder = ({ editor }) => {
<img
className={`rounded-md`}
src={WoodgrainMaterial}
alt="WoodgrainMaterial"
alt='WoodgrainMaterial'
/>
</div>
</div>
@@ -353,7 +377,7 @@ export const Builder = ({ editor }) => {
>
{dock.length ? (
<>
<img src={dock[0].image} alt="" className={`rounded-md my-2`} />
<img src={dock[0].image} alt='' className={`rounded-md my-2`} />
<div className={`grid grid-cols-2`}>
{dock?.map((dockItem, index) => (
<button
@@ -397,7 +421,7 @@ export const Builder = ({ editor }) => {
>
{dock.length ? (
<>
<img src={dock[0].image} alt="" className={`rounded-md my-2`} />
<img src={dock[0].image} alt='' className={`rounded-md my-2`} />
<div className={`grid grid-cols-2`}>
{dock?.map((dockItem, index) => (
<button
@@ -441,7 +465,7 @@ export const Builder = ({ editor }) => {
>
{dock.length ? (
<>
<img src={dock[0].image} alt="" className={`rounded-md my-2`} />
<img src={dock[0].image} alt='' className={`rounded-md my-2`} />
<div className={`grid grid-cols-2`}>
{dock?.map((dockItem, index) => (
<button
@@ -484,7 +508,7 @@ export const Builder = ({ editor }) => {
<>
<img
src={wedgesAndRamps?.wedges[0].image}
alt=""
alt=''
className={`rounded-md my-2`}
/>
<div className={`grid grid-cols-2`}>
@@ -532,7 +556,7 @@ export const Builder = ({ editor }) => {
<>
<img
src={wedgesAndRamps.ramps[0].image}
alt=""
alt=''
className={`rounded-md my-2`}
/>
<div className={`grid grid-cols-2`}>
@@ -606,7 +630,7 @@ export const Builder = ({ editor }) => {
</div>
<img
src={boatLift[0].image}
alt=""
alt=''
className={`rounded-md my-2`}
/>
<div className={`grid grid-cols-2`}>
@@ -677,7 +701,7 @@ export const Builder = ({ editor }) => {
</div>
<img
src={boatLift[0].image}
alt=""
alt=''
className={`rounded-md my-2`}
/>
<div className={`grid grid-cols-2`}>
@@ -735,7 +759,7 @@ export const Builder = ({ editor }) => {
<span>{ dockItem.length }'</span> */}
<img
src={dockItem.thumbnail}
alt=""
alt=''
className={`rounded-md `}
/>
{/* </div> */}
+253 -56
View File
@@ -4,7 +4,7 @@ import React, {
useCallback,
useRef,
useState,
useContext
useContext,
} from "react";
import { FabricJSCanvas, useFabricJSEditor } from "fabricjs-react";
import { fabric } from "fabric";
@@ -20,7 +20,7 @@ import { clearClone, clone } from "Utils/DockBuilderUtils/clone";
import {
edgeDetection,
handleEdgeDetection,
handleIntersection
handleIntersection,
} from "Utils/DockBuilderUtils";
import {
CanvasModes,
@@ -32,12 +32,12 @@ import {
oneFeet,
rotateIcon,
scaleFactor,
twoSeventyDeg
twoSeventyDeg,
} from "Utils/constants";
import {
reScaleXY,
resolveHeight,
resolveWidth
resolveWidth,
} from "Utils/DockBuilderUtils/edgeDetection";
import { DeleteIcon, RotateIcon } from "Assets/svgs";
import { capitalize } from "Utils/helper";
@@ -90,7 +90,7 @@ export const DockBuilder = () => {
const ext = "png";
const base64 = editorMemo.toDataURL({
format: ext,
enableRetinaScaling: true
enableRetinaScaling: true,
});
setDockImage(base64);
setShowEstimateModal(true);
@@ -103,36 +103,65 @@ export const DockBuilder = () => {
// }, [ editor ] );
const toJSON = () => {
// TODO: download the json file
// TODO: get json of editor content
// TODO: Ensure dockData is included in the json
// TODO: save the json to the local storage as dock
// TODO: name the file as paradise_dock_<timestamp here>.dock
// Get JSON of editor content, including dockData and snapClone
const json = editorMemo.toJSON(["dockData", "snapClone"]);
const data = JSON.stringify(json, null, 2);
// Save to localStorage
localStorage.setItem("dock", data);
const blob = new Blob([data], { type: "application/json" });
const anchor = document.createElement("a");
const timestamp = Date.now();
anchor.href = URL.createObjectURL(blob);
anchor.download = `paradise_dock_${timestamp}.dock`; // TODO: name the file as paradise_dock_<timestamp here>.dock
anchor.click();
};
const uploadFile = (e) => {
// TODO: Our own upload the file we must have downloaded previously
// TODO: extract the json from the file
// TODO: load the json to the editor
// TODO: render all
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function (event) {
try {
const json = JSON.parse(event.target.result);
console.log(json);
editorMemo.loadFromJSON(json, () => {
editorMemo.renderAll();
});
} catch (err) {
alert("Invalid file format. Please select a valid .dock file.");
}
};
reader.readAsText(file);
// Reset file input to allow reupload of the same file
e.target.value = "";
};
const downloadImage = useCallback(() => {
// // console.log( 'Download' )
const ext = "png";
// Generate base64 image of the canvas
const base64 = editorMemo.toDataURL({
format: ext,
enableRetinaScaling: true,
});
// Download the image
const anchor = document.createElement("a");
const timestamp = Date.now();
anchor.href = base64;
anchor.download = `paradise_dock_snapshot_${timestamp}.${ext}`;
anchor.click();
// TODO: download the image
// TODO: get the json of the editor content
// TODO: extract the dockData from the json
// TODO: filter the dockData to ensure it does not contain snapClone
// TODO: generate the base64 image
// TODO: download the image and name it as paradise_dock_snapshot_<timestamp here>.${ext}
// TODO: download the excel file of the dockData you extracted
// TODO: check if dockData is empty
// TODO: generate the base64 image
// TODO: trigger download of the dockData you extracted in excel format
// Extract dockData from all objects (excluding snapClone)
const json = editorMemo.toJSON(["dockData", "snapClone"]);
const dockDataArr = (json.objects || [])
.filter((object) => object.dockData && !object.snapClone)
.map((object) => object.dockData);
if (dockDataArr.length > 0) {
// Download as Excel
const worksheet = XLSX.utils.json_to_sheet(dockDataArr);
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, "Dock Data");
XLSX.writeFile(workbook, `paradise_dock_data_${timestamp}.xlsx`);
}
}, [editorMemo]);
const renderBg = useCallback(() => {
@@ -144,14 +173,14 @@ export const DockBuilder = () => {
function (img) {
img.set({
scaleX: editorMemo.width / img.width,
scaleY: editorMemo.height / img.height
scaleY: editorMemo.height / img.height,
});
editorMemo.setBackgroundImage(img);
editorMemo.renderAll();
// updateModifications( true, )
},
{
crossOrigin: "anonymous"
crossOrigin: "anonymous",
}
);
}, [editorMemo]);
@@ -195,7 +224,7 @@ export const DockBuilder = () => {
onChange: function (value) {
editorMemo.zoomToPoint({ x: pointer.x, y: pointer.y }, value);
},
fps: 1080
fps: 1080,
});
opt.e.preventDefault();
opt.e.stopPropagation();
@@ -350,12 +379,12 @@ export const DockBuilder = () => {
let line = new fabric.Line([0, newTop, editorMemo.getWidth(), newTop], {
stroke: "#AAAAAA",
testLine: true
testLine: true,
// strokeDashArray: [ 5 ],
});
let line2 = new fabric.Line([0, objTop, editorMemo.getWidth(), objTop], {
stroke: "#AAAAAA",
testLine: true
testLine: true,
// strokeDashArray: [ 5 ],
});
editorMemo.add(line);
@@ -371,12 +400,12 @@ export const DockBuilder = () => {
let line = new fabric.Line([newLeft, 0, newLeft, editorMemo.getHeight()], {
stroke: "#AAAAAA",
testLine: true
testLine: true,
// strokeDashArray: [ 5 ],
});
let line2 = new fabric.Line([objLeft, 0, objLeft, editorMemo.getWidth()], {
stroke: "#AAAAAA",
testLine: true
testLine: true,
// strokeDashArray: [ 5 ],
});
editorMemo.add(line);
@@ -385,14 +414,96 @@ export const DockBuilder = () => {
};
function edgeDetectionAndSnap(options) {
if (this.isZoomed) {
if (editorMemo.getZoom() !== 1) {
return;
}
// TODO: Edge detection and snap to object within snap range
// TODO: Detect if the object is within the snap range of the selected object
// TODO: detect if the selected object contains dockData and dockData contains itemName of DockPanelCategories.Accessories || DockPanelCategories.BoatLift2 || DockPanelCategories.BoatLift4
// TODO: handle rotation of 0, 90, 180, 270 degrees
const selectedObj = options.target;
if (!selectedObj.dockData) return;
// TODO: Detect if the selected object is in the allowed categories
const snapCategories = [
DockPanelCategories.Accessories,
DockPanelCategories.BoatLift2,
DockPanelCategories.BoatLift4,
];
if (!snapCategories.includes(selectedObj.dockData.itemName)) return;
let closestSnapPoint = null;
let minDistance = edgeSnapThreshold;
editorMemo.forEachObject((obj) => {
if (obj === selectedObj || !obj.dockData) return;
// TODO: Only consider objects in the allowed categories
if (!snapCategories.includes(obj.dockData.itemName)) return;
// TODO: handle rotation of 0, 90, 180, 270 degrees
const rotation = obj.angle % 360;
const isRotated = rotation !== 0 && rotation !== 180;
const objCenter = obj.getCenterPoint();
const selectedCenter = selectedObj.getCenterPoint();
// Calculate potential snap points
const snapPoints = [];
// Center point
snapPoints.push(objCenter);
// Edge points
if (!isRotated || rotation === 0 || rotation === 180) {
// Horizontal edges
snapPoints.push(
new fabric.Point(objCenter.x, obj.getBoundingRect().top)
);
snapPoints.push(
new fabric.Point(
objCenter.x,
obj.getBoundingRect().top + obj.getBoundingRect().height
)
);
}
if (!isRotated || rotation === 90 || rotation === 270) {
// Vertical edges
snapPoints.push(
new fabric.Point(obj.getBoundingRect().left, objCenter.y)
);
snapPoints.push(
new fabric.Point(
obj.getBoundingRect().left + obj.getBoundingRect().width,
objCenter.y
)
);
}
// Find closest snap point
snapPoints.forEach((point) => {
const distance = Math.sqrt(
Math.pow(selectedCenter.x - point.x, 2) +
Math.pow(selectedCenter.y - point.y, 2)
);
if (distance < minDistance) {
minDistance = distance;
closestSnapPoint = point;
}
});
});
// Snap to closest point if found
if (closestSnapPoint) {
const selectedCenter = selectedObj.getCenterPoint();
selectedObj.set({
left: selectedObj.left + (closestSnapPoint.x - selectedCenter.x),
top: selectedObj.top + (closestSnapPoint.y - selectedCenter.y),
});
selectedObj.setCoords();
editorMemo.renderAll();
}
editorMemo.defaultCursor = "default";
prevPointer = null;
@@ -497,30 +608,116 @@ export const DockBuilder = () => {
}, [editor, selectedItems]);
const onUndoClick = useCallback(() => {
// TODO: Undo
const undoResult = stack.undo();
if (undoResult && undoResult.currentState) {
editorMemo.loadFromJSON(undoResult.currentState, () => {
editorMemo.renderAll();
});
}
}, [editorMemo]);
const onRedoClick = useCallback(() => {
// TODO: Redo
const redoResult = stack.redo();
if (redoResult && redoResult.currentState) {
// currentState is an array, get the first element
const state = Array.isArray(redoResult.currentState)
? redoResult.currentState[0]
: redoResult.currentState;
if (state) {
editorMemo.loadFromJSON(state, () => {
editorMemo.renderAll();
});
}
}
}, [editorMemo]);
const onPrintScreen = useCallback(() => {
// convert the canvas to a data url and download it.
// TODO: Print screen
// TODO: print screen without background image and background color, the background image should be white
// TODO: after printing, the background image and background color should be restored
// Save current background
const originalBgColor = editorMemo.backgroundColor;
const originalBgImage = editorMemo.backgroundImage;
// Set background to white for printing
editorMemo.setBackgroundColor(
"#fff",
editorMemo.renderAll.bind(editorMemo)
);
editorMemo.setBackgroundImage(null, editorMemo.renderAll.bind(editorMemo));
// Give time for background to update
setTimeout(() => {
const dataUrl = editorMemo.toDataURL({
format: "png",
enableRetinaScaling: true,
});
const printWindow = window.open("", "_blank");
if (printWindow) {
printWindow.document.write(
'<html><head><title>Print Canvas</title></head><body style="margin:0"><img src="' +
dataUrl +
'" style="width:100vw;max-width:100%"/></body></html>'
);
printWindow.document.close();
printWindow.focus();
printWindow.onload = function () {
printWindow.print();
printWindow.close();
};
}
// Restore original background
editorMemo.setBackgroundColor(
originalBgColor,
editorMemo.renderAll.bind(editorMemo)
);
if (originalBgImage) {
editorMemo.setBackgroundImage(
originalBgImage,
editorMemo.renderAll.bind(editorMemo)
);
}
}, 200);
}, [editorMemo]);
const CopySelection = () => {
// TODO: Copy selection
const activeObject = editorMemo.getActiveObject();
if (activeObject) {
activeObject.clone(function (cloned) {
// Ensure dockData is copied
if (activeObject.dockData) {
cloned.dockData = { ...activeObject.dockData };
}
clipboard = cloned;
});
}
};
const PasteSelection = () => {
// TODO: Paste selection
if (clipboard) {
clipboard.clone(function (clonedObj) {
// Ensure dockData is copied
if (clipboard.dockData) {
clonedObj.dockData = { ...clipboard.dockData };
}
// Offset the pasted object so it's visible
clonedObj.set({
left: (clonedObj.left || 0) + 20,
top: (clonedObj.top || 0) + 20,
evented: true,
});
editorMemo.add(clonedObj);
editorMemo.setActiveObject(clonedObj);
editorMemo.requestRenderAll();
});
}
};
const onDeleteSelection = () => {
// TODO: Delete selection
const activeObject = editorMemo.getActiveObject();
if (activeObject) {
editorMemo.remove(activeObject);
editorMemo.discardActiveObject();
editorMemo.renderAll();
updateModifications(true, { target: activeObject });
}
};
const addLines = useCallback(() => {
@@ -539,7 +736,7 @@ export const DockBuilder = () => {
[0, oneFeet * i, editorMemo.getWidth(), oneFeet * i],
{
stroke: "#AAAAAA",
strokeDashArray: [5]
strokeDashArray: [5],
}
);
@@ -548,7 +745,7 @@ export const DockBuilder = () => {
fill: "#AAAAAA",
fontSize: 18,
left: editorMemo.getWidth() - 40,
top: oneFeet * i
top: oneFeet * i,
});
multiplier++;
@@ -576,7 +773,7 @@ export const DockBuilder = () => {
dispatch({
type: "DOCK_LOADING",
payload: false
payload: false,
});
setInitialLoad(false);
}, [editorMemo, showBuildCanvasFromLocalModal, dispatch, initialLoad]);
@@ -625,7 +822,7 @@ export const DockBuilder = () => {
bl: false, // bottom-left
mb: false, //middle-bottom
br: false, //bottom-right
mtr: false // rotate icon
mtr: false, // rotate icon
});
// fabric.Object.prototype.controls.deleteControl = new fabric.Control( {
// x: -0.8,
@@ -654,7 +851,7 @@ export const DockBuilder = () => {
sizeX: 40,
sizeY: 40,
touchSizeX: 40,
touchSizeY: 40
touchSizeY: 40,
});
fabric.Object.prototype.hasControls = true;
fabric.Object.prototype.lockScalingX = true;
@@ -718,7 +915,7 @@ export const DockBuilder = () => {
setInitialLoad(false);
dispatch({
type: "DOCK_LOADING",
payload: false
payload: false,
});
}
}
@@ -733,7 +930,7 @@ export const DockBuilder = () => {
editorMemo.setBackgroundImage(editorMemo.backgroundImage, (bgImage) => {
bgImage.set({
scaleX: editorMemo.width / bgImage.width,
scaleY: editorMemo.height / bgImage.height
scaleY: editorMemo.height / bgImage.height,
});
});
removeLines();
@@ -789,8 +986,8 @@ export const DockBuilder = () => {
<img
ref={imageRef}
src=""
alt=""
src=''
alt=''
width={174}
height={116}
className={`rounded relative ${objHovered ? "" : "hidden"}`}