Update | Project Ready
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
|
||||
import React, { useCallback, useState } from 'react'
|
||||
// import { GrayMaterial, PerforatedMaterial, WoodgrainMaterial } from 'Assets/images'
|
||||
// import { MaterialType } from 'Utils/constants'
|
||||
import { CopyIcon, HandIcon, PasteIcon, PrinterIcon, ReverseLeftIcon, ReverseRightIcon, TrashIcon } from 'Assets/svgs'
|
||||
import { CanvasModes } from 'Utils/constants'
|
||||
|
||||
|
||||
|
||||
export const ActionButtons = ( { className, onRedoClick, onUndoClick, onCopy, onPaste, onDeleteSelection, onPrintScreen } ) => {
|
||||
const [ mode, setMode ] = useState( CanvasModes.Still )
|
||||
// const [ activeMaterial, setActiveMaterial ] = useState( MaterialType.Gray )
|
||||
|
||||
// const changeMode = useCallback( () => {
|
||||
// if ( mode === CanvasModes.Pan ) {
|
||||
// setMode( CanvasModes.Still )
|
||||
// onCanvasModeChange( CanvasModes.Still )
|
||||
// } else if ( mode === CanvasModes.Still ) {
|
||||
// setMode( CanvasModes.Pan )
|
||||
// onCanvasModeChange( CanvasModes.Pan )
|
||||
// }
|
||||
// }, [ mode ] )
|
||||
|
||||
return (
|
||||
<div className={ `flex gap-x-[8px] bg-transparent border-none ${ className }` }>
|
||||
|
||||
<button onClick={ () => onPrintScreen() }>
|
||||
<div
|
||||
className={ `flex justify-center items-center px-[9px] py-[8px] bg-[#ffffff14] border-2 border-[#404968] rounded cursor-pointer w-[42px] h-[40px]` }
|
||||
>
|
||||
<PrinterIcon />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* <button onClick={ changeMode }>
|
||||
<div
|
||||
className={ `flex justify-center items-center px-[9px] py-[8px] bg-[#ffffff14] border-2 border-[#404968] rounded cursor-pointer w-[42px] h-[40px]` }
|
||||
>
|
||||
<HandIcon />
|
||||
</div>
|
||||
</button> */}
|
||||
<button onClick={ onPaste }>
|
||||
<div
|
||||
className={ `flex justify-center items-center px-[9px] py-[8px] bg-[#ffffff14] border-2 border-[#404968] rounded cursor-pointer w-[42px] h-[40px]` }
|
||||
>
|
||||
<PasteIcon />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button onClick={ onCopy }>
|
||||
<div
|
||||
className={ `flex justify-center items-center px-[9px] py-[8px] bg-[#ffffff14] border-2 border-[#404968] rounded cursor-pointer w-[42px] h-[40px]` }
|
||||
>
|
||||
<CopyIcon />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button onClick={ onUndoClick }>
|
||||
<div
|
||||
className={ `flex justify-center items-center px-[9px] py-[8px] bg-[#ffffff14] border-2 border-[#404968] rounded cursor-pointer w-[42px] h-[40px]` }
|
||||
>
|
||||
<ReverseLeftIcon />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button onClick={ onRedoClick }>
|
||||
<div
|
||||
className={ `flex justify-center items-center px-[9px] py-[8px] bg-[#ffffff14] border-2 border-[#404968] rounded cursor-pointer w-[42px] h-[40px]` }
|
||||
>
|
||||
<ReverseRightIcon />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button onClick={ onDeleteSelection }>
|
||||
<div
|
||||
className={ `flex justify-center items-center px-[9px] py-[8px] bg-[#ffffff14] border-2 border-[#404968] rounded cursor-pointer w-[42px] h-[40px]` }
|
||||
>
|
||||
<TrashIcon />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { ActionButtons } from './ActionButtons'
|
||||
@@ -0,0 +1,16 @@
|
||||
import React from "react";
|
||||
import { NavLink } from "react-router-dom";
|
||||
const AddButton = ({ link }) => {
|
||||
return (
|
||||
<>
|
||||
<NavLink to={link} className="w-7 h-7 text-center center-svg text-white font-bold bg-blue-500 hover:bg-blue-700 flex">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" className="bi bi-plus" viewBox="0 0 16 16">
|
||||
{" "}
|
||||
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z" />{" "}
|
||||
</svg>
|
||||
</NavLink>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddButton;
|
||||
@@ -0,0 +1,205 @@
|
||||
import React from "react";
|
||||
import { AuthContext } from "../authContext";
|
||||
import { NavLink } from "react-router-dom";
|
||||
import { GlobalContext } from "../globalContext";
|
||||
export const AdminHeader = () => {
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const { state } = React.useContext(GlobalContext);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={`sidebar-holder ${!state.isOpen ? "open-nav" : ""}`}>
|
||||
<div className="sticky top-0 h-fit">
|
||||
<div className="w-full p-4 bg-sky-500">
|
||||
<div className="text-white font-bold text-center text-2xl">
|
||||
Admin
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full sidebar-list">
|
||||
<ul className="flex flex-wrap">
|
||||
<li className="list-none block w-full">
|
||||
<NavLink
|
||||
to="/admin/dashboard"
|
||||
className={`${
|
||||
state.path == "admin" ? "text-black bg-gray-200" : ""
|
||||
}`}
|
||||
>
|
||||
Dashboard
|
||||
</NavLink>
|
||||
</li>
|
||||
|
||||
<li className="list-none block w-full">
|
||||
<NavLink
|
||||
to="/admin/accessories"
|
||||
className={`${
|
||||
state.path == "accessories" ? "text-black bg-gray-200" : ""
|
||||
}`}
|
||||
>
|
||||
Accessories
|
||||
</NavLink>
|
||||
</li>
|
||||
|
||||
<li className="list-none block w-full">
|
||||
<NavLink
|
||||
to="/admin/email"
|
||||
className={`${
|
||||
state.path == "email" ? "text-black bg-gray-200" : ""
|
||||
}`}
|
||||
>
|
||||
Email
|
||||
</NavLink>
|
||||
</li>
|
||||
|
||||
<li className="list-none block w-full">
|
||||
<NavLink
|
||||
to="/admin/photo"
|
||||
className={`${
|
||||
state.path == "photo" ? "text-black bg-gray-200" : ""
|
||||
}`}
|
||||
>
|
||||
Photo
|
||||
</NavLink>
|
||||
</li>
|
||||
|
||||
<li className="list-none block w-full">
|
||||
<NavLink
|
||||
to="/admin/boat_lifts"
|
||||
className={`${
|
||||
state.path == "boat_lifts" ? "text-black bg-gray-200" : ""
|
||||
}`}
|
||||
>
|
||||
Boat Lifts
|
||||
</NavLink>
|
||||
</li>
|
||||
|
||||
<li className="list-none block w-full">
|
||||
<NavLink
|
||||
to="/admin/dealers"
|
||||
className={`${
|
||||
state.path == "dealers" ? "text-black bg-gray-200" : ""
|
||||
}`}
|
||||
>
|
||||
Dealers
|
||||
</NavLink>
|
||||
</li>
|
||||
|
||||
<li className="list-none block w-full">
|
||||
<NavLink
|
||||
to="/admin/docks"
|
||||
className={`${
|
||||
state.path == "docks" ? "text-black bg-gray-200" : ""
|
||||
}`}
|
||||
>
|
||||
Docks
|
||||
</NavLink>
|
||||
</li>
|
||||
|
||||
<li className="list-none block w-full">
|
||||
<NavLink
|
||||
to="/admin/cms"
|
||||
className={`${
|
||||
state.path == "cms" ? "text-black bg-gray-200" : ""
|
||||
}`}
|
||||
>
|
||||
Cms
|
||||
</NavLink>
|
||||
</li>
|
||||
|
||||
<li className="list-none block w-full">
|
||||
<NavLink
|
||||
to="/admin/instructions"
|
||||
className={`${
|
||||
state.path == "instructions" ? "text-black bg-gray-200" : ""
|
||||
}`}
|
||||
>
|
||||
Instructions
|
||||
</NavLink>
|
||||
</li>
|
||||
|
||||
<li className="list-none block w-full">
|
||||
<NavLink
|
||||
to="/admin/user"
|
||||
className={`${
|
||||
state.path == "user" ? "text-black bg-gray-200" : ""
|
||||
}`}
|
||||
>
|
||||
User
|
||||
</NavLink>
|
||||
</li>
|
||||
|
||||
<li className="list-none block w-full">
|
||||
<NavLink
|
||||
to="/admin/quotes"
|
||||
className={`${
|
||||
state.path == "quotes" ? "text-black bg-gray-200" : ""
|
||||
}`}
|
||||
>
|
||||
Quotes
|
||||
</NavLink>
|
||||
</li>
|
||||
|
||||
<li className="list-none block w-full">
|
||||
<NavLink
|
||||
to="/admin/wedges"
|
||||
className={`${
|
||||
state.path == "wedges" ? "text-black bg-gray-200" : ""
|
||||
}`}
|
||||
>
|
||||
Wedges
|
||||
</NavLink>
|
||||
</li>
|
||||
<li className="list-none block w-full">
|
||||
<NavLink
|
||||
to="/admin/ramps"
|
||||
className={`${
|
||||
state.path == "ramps" ? "text-black bg-gray-200" : ""
|
||||
}`}
|
||||
>
|
||||
Ramps
|
||||
</NavLink>
|
||||
</li>
|
||||
|
||||
<li className="list-none block w-full">
|
||||
<NavLink
|
||||
to="/admin/quotes_mail_recipients"
|
||||
className={`${
|
||||
state.path == "quotes_mail_recipients"
|
||||
? "text-black bg-gray-200"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
Quotes Mail Recipients
|
||||
</NavLink>
|
||||
</li>
|
||||
|
||||
<li className="list-none block w-full">
|
||||
<NavLink
|
||||
to="/admin/profile"
|
||||
className={`${
|
||||
state.path == "profile" ? "text-black bg-gray-200" : ""
|
||||
}`}
|
||||
>
|
||||
Profile
|
||||
</NavLink>
|
||||
</li>
|
||||
<li className="list-none block w-full">
|
||||
<NavLink
|
||||
to="/admin/login"
|
||||
onClick={() =>
|
||||
dispatch({
|
||||
type: "LOGOUT",
|
||||
})
|
||||
}
|
||||
>
|
||||
Logout
|
||||
</NavLink>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminHeader;
|
||||
@@ -0,0 +1,82 @@
|
||||
import React, { memo, useState, useCallback } from 'react';
|
||||
import { Modal } from 'Components/Modal';
|
||||
import { InteractiveButton } from 'Components/InteractiveButton';
|
||||
// import { ArrowLeftIcon, ArrowRrightIcon, GreenTickIcon } from 'Assets/svgs';
|
||||
// import { dock_image, EstimateSteps, Tables, Truthy } from 'Utils/constants';
|
||||
// import { ContactInformation } from 'Components/ContactInformation';
|
||||
// import { LakeSurroundings } from 'Components/LakeSurroundings';
|
||||
// import { Comments } from 'Components/Comments';
|
||||
import MkdSDK from 'Utils/MkdSDK';
|
||||
|
||||
const sdk = new MkdSDK()
|
||||
const classes = {
|
||||
modal: 'string',
|
||||
modalDialog: 'relative bg-white w-[500px]',
|
||||
modalContent: 'string',
|
||||
modalHeader: 'string',
|
||||
modalTitle: 'string',
|
||||
modalBody: 'string',
|
||||
modalFooter: 'string',
|
||||
closeButtonClass: 'string',
|
||||
saveButtonClass: 'string',
|
||||
}
|
||||
|
||||
const BuildCanvasFromLocalModal = ( {
|
||||
showBuildCanvasFromLocalModal,
|
||||
modalCloseClick,
|
||||
editor
|
||||
} ) => {
|
||||
// const [ step, setStep ] = useState( EstimateSteps.ContactInformation )
|
||||
// const [ submitLoading, setSubmitLoading ] = useState( false )
|
||||
// const [ errorMessage, setErrorMessage ] = useState( null )
|
||||
// const [ hasDealer, setHasDealer ] = useState( Truthy.False )
|
||||
|
||||
|
||||
const onConfirm = useCallback( () => {
|
||||
const json = JSON.parse( localStorage.getItem( "dock" ) )
|
||||
editor.loadFromJSON( json, () => {
|
||||
editor.renderAll()
|
||||
}, ( o ) => {
|
||||
// console.log( o )
|
||||
} )
|
||||
modalCloseClick()
|
||||
}, [ editor ] )
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="CONTINUE FROM LAST SAVE"
|
||||
isOpen={ showBuildCanvasFromLocalModal }
|
||||
classes={ classes }
|
||||
modalHeader
|
||||
modalCloseClick={ modalCloseClick }
|
||||
>
|
||||
<div>
|
||||
<div className={ `my-3` }>
|
||||
Would You Like to Continue Building Dock From Last Save?
|
||||
</div>
|
||||
|
||||
<div className={ `w-full flex gap-x-2 justify-between items-center` }>
|
||||
<button
|
||||
className={ `h-[44px] w-full rounded border-2 border-[#0F75BC] flex justify-center gap-x-2 items-center bg-[#0F75BC] text-[#ffffff] uppercase font-medium tracking-wide leading-[24px] text-[16px]` }
|
||||
onClick={ modalCloseClick }
|
||||
>No
|
||||
</button>
|
||||
|
||||
<button
|
||||
className={ `h-[44px] w-full rounded border-2 border-[#0F75BC] flex justify-center gap-x-2 items-center bg-[#0F75BC] text-[#ffffff] uppercase font-medium tracking-wide leading-[24px] text-[16px]` }
|
||||
onClick={ onConfirm }
|
||||
>Yes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const BuildCanvasFromLocalModalMemo = memo( BuildCanvasFromLocalModal );
|
||||
|
||||
export { BuildCanvasFromLocalModalMemo as BuildCanvasFromLocalModal };
|
||||
@@ -0,0 +1 @@
|
||||
export { BuildCanvasFromLocalModal } from './BuildCanvasFromLocalModal'
|
||||
@@ -0,0 +1,758 @@
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
// import { Tabs } from 'Components/Tabs'
|
||||
// import { TabNames } from 'Utils'
|
||||
import { fabric } from "fabric";
|
||||
import {
|
||||
GrayMaterial,
|
||||
PerforatedMaterial,
|
||||
WoodgrainMaterial
|
||||
} from "Assets/images";
|
||||
import {
|
||||
DockPanelCategories,
|
||||
MaterialType,
|
||||
DockPanelCategoryMap,
|
||||
CylinderType
|
||||
} from "Utils/constants";
|
||||
import { Chevron } from "Assets/svgs";
|
||||
import MkdSDK from "Utils/MkdSDK";
|
||||
import { oneFeet, scaleFactor, Tables } from "Utils/constants";
|
||||
import {
|
||||
getCategory,
|
||||
getMaterial,
|
||||
getRampsCategory,
|
||||
getWedgesAndRampsMaterial,
|
||||
getWedgesCategory
|
||||
} from "Utils/utils";
|
||||
|
||||
const sdk = new MkdSDK();
|
||||
export const Builder = ({ editor }) => {
|
||||
// let scaleFactor = 0.2;
|
||||
const [activeMaterial, setActiveMaterial] = useState(MaterialType.Gray);
|
||||
const [activeDockCategory, setActiveDockCategory] = useState(
|
||||
DockPanelCategories.RollIn
|
||||
);
|
||||
const rampsInitialState = {
|
||||
data: []
|
||||
};
|
||||
// const [ramps, setRamps] = useState(null);
|
||||
const [activeLiftRange, setActiveLiftRange] = useState(null);
|
||||
const [dock, setDock] = useState([]);
|
||||
const [docks, setDocks] = useState([]);
|
||||
const [accessories, setAccessories] = useState([]);
|
||||
const [boatlifts, setBoatlifts] = useState([]);
|
||||
const [wedgesAndRamps, setWedgesAndRamps] = useState({
|
||||
wedges: [],
|
||||
ramps: [],
|
||||
selectedRamps: [],
|
||||
selectedWedges: []
|
||||
});
|
||||
const [boatLift, setBoatLift] = useState([]);
|
||||
const [liftRanges, setLiftRanges] = useState([]);
|
||||
const [left, setLeft] = useState(300);
|
||||
// const [ rollinDock, setRollinDock ] = useState( null )
|
||||
// const [ floatingDock, setFloatingDock ] = useState( null )
|
||||
// const [ sectionalDock, setSectionalDock ] = useState( null )
|
||||
|
||||
const onMaterialClick = useCallback(
|
||||
(material) => {
|
||||
setActiveMaterial(material);
|
||||
},
|
||||
[activeMaterial]
|
||||
);
|
||||
|
||||
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", "+");
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
|
||||
// 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
|
||||
}, []);
|
||||
|
||||
const getItems = useCallback((table) => {
|
||||
// console.log( category, materials );
|
||||
(async () => {
|
||||
try {
|
||||
// sdk.setTable( table );
|
||||
const result = await sdk.getItems(table);
|
||||
// console.log( result )
|
||||
switch (table) {
|
||||
case Tables.Docks:
|
||||
return setDocks(() =>
|
||||
result?.model
|
||||
? [...result?.model.map((item) => ({ ...item, type: "docks" }))]
|
||||
: []
|
||||
);
|
||||
|
||||
case Tables.Boat_lifts:
|
||||
// return console.log( result )
|
||||
const liftRanges = result?.model
|
||||
.filter((boatLift, index, self) => {
|
||||
return (
|
||||
index ===
|
||||
self.findIndex(
|
||||
(selfItem) => selfItem.lift_range === boatLift.lift_range
|
||||
)
|
||||
);
|
||||
})
|
||||
.map((item) => item.lift_range);
|
||||
setLiftRanges(liftRanges.sort());
|
||||
setActiveLiftRange(liftRanges.sort()[0]);
|
||||
return setBoatlifts(() =>
|
||||
result?.model
|
||||
? [
|
||||
...result?.model.map((item) => ({
|
||||
...item,
|
||||
type: "boatlifts"
|
||||
}))
|
||||
]
|
||||
: []
|
||||
);
|
||||
|
||||
case Tables.Accessories:
|
||||
// return console.log( result )
|
||||
return setAccessories(() =>
|
||||
result?.model
|
||||
? [
|
||||
...result?.model.map((item) => ({
|
||||
...item,
|
||||
type: "accessories"
|
||||
}))
|
||||
]
|
||||
: []
|
||||
);
|
||||
case Tables.Wedges:
|
||||
// return console.log( result )
|
||||
return setWedgesAndRamps((prev) => ({
|
||||
...prev,
|
||||
wedges: result?.model
|
||||
? [
|
||||
...result?.model.map((item) => ({
|
||||
...item,
|
||||
type: "wedges"
|
||||
}))
|
||||
]
|
||||
: []
|
||||
}));
|
||||
case Tables.Ramps:
|
||||
// return console.log( result )
|
||||
return setWedgesAndRamps((prev) => ({
|
||||
...prev,
|
||||
ramps: result?.model
|
||||
? [...result?.model.map((item) => ({ ...item, type: "ramps" }))]
|
||||
: []
|
||||
}));
|
||||
// return setRamps(() => [...result?.model]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error.message);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const onDockPanelClick = useCallback(
|
||||
(dockCategory) => {
|
||||
if (dockCategory !== activeDockCategory) {
|
||||
setActiveDockCategory(dockCategory);
|
||||
} else {
|
||||
setActiveDockCategory("");
|
||||
}
|
||||
},
|
||||
[activeDockCategory]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!docks.length) {
|
||||
// console.log( "I ran Docks" )
|
||||
getItems(Tables.Docks);
|
||||
}
|
||||
if (!accessories.length) {
|
||||
// console.log( "I ran Accessories" )
|
||||
getItems(Tables.Accessories);
|
||||
}
|
||||
if (!wedgesAndRamps?.wedges?.length) {
|
||||
// console.log( "I ran Wedges" )
|
||||
getItems(Tables.Wedges);
|
||||
}
|
||||
if (!wedgesAndRamps?.ramps?.length) {
|
||||
// console.log( "I ran Ramps" )
|
||||
getItems(Tables.Ramps);
|
||||
}
|
||||
if (!boatlifts.length) {
|
||||
// console.log( "I ran Boat_lifts" )
|
||||
getItems(Tables.Boat_lifts);
|
||||
}
|
||||
}, [docks, accessories, boatlifts]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
[
|
||||
DockPanelCategories.RollIn,
|
||||
DockPanelCategories.Floating,
|
||||
DockPanelCategories.Sectional
|
||||
].includes(activeDockCategory)
|
||||
) {
|
||||
// console.log( activeDockCategory, activeMaterial )
|
||||
const category =
|
||||
activeDockCategory === DockPanelCategories.RollIn
|
||||
? DockPanelCategoryMap.RollIn
|
||||
: activeDockCategory === DockPanelCategories.Floating
|
||||
? DockPanelCategoryMap.Floating
|
||||
: activeDockCategory === DockPanelCategories.Sectional
|
||||
? DockPanelCategoryMap.Sectional
|
||||
: null;
|
||||
|
||||
const dockObj = docks
|
||||
.map((dock) => {
|
||||
if (dock.category === category && dock.materials === activeMaterial) {
|
||||
return dock;
|
||||
}
|
||||
})
|
||||
.filter(Boolean);
|
||||
// console.log( dockObj )
|
||||
setDock(() => [...dockObj]);
|
||||
}
|
||||
if (DockPanelCategories.Wedges === activeDockCategory) {
|
||||
}
|
||||
}, [activeMaterial, activeDockCategory, docks, wedgesAndRamps]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
[DockPanelCategories.BoatLift2, DockPanelCategories.BoatLift4].includes(
|
||||
activeDockCategory
|
||||
)
|
||||
) {
|
||||
// console.log( activeDockCategory, activeLiftRange )
|
||||
const cylinder = CylinderType[activeDockCategory];
|
||||
|
||||
const boatliftsObj = boatlifts
|
||||
.map((boatlift) => {
|
||||
if (boatlift.no_of_cylinders === cylinder) {
|
||||
return boatlift;
|
||||
}
|
||||
})
|
||||
.filter(Boolean);
|
||||
// console.log( boatliftsObj )
|
||||
setBoatLift(() => [...boatliftsObj]);
|
||||
}
|
||||
}, [activeLiftRange, boatlifts, activeDockCategory]);
|
||||
// useEffect(() => {
|
||||
// (async () => {
|
||||
// await sdk.projectHealth();
|
||||
// })();
|
||||
// }, []);
|
||||
|
||||
return (
|
||||
<div className={`flex flex-col gap-x-[8px] border-t-2 w-full`}>
|
||||
<div className={`w-full flex h-[54px] bg-gray-200`}>
|
||||
<div
|
||||
onClick={() => onMaterialClick(MaterialType.Gray)}
|
||||
className={`grow h-full px-[12px] py-[5px] flex justify-center items-center cursor-pointer
|
||||
${
|
||||
activeMaterial === MaterialType.Gray
|
||||
? " bg-white border-transparent"
|
||||
: "bg-gray-200"
|
||||
}`}
|
||||
>
|
||||
<img className={`rounded-md`} src={GrayMaterial} alt="GreyMaterial" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
onClick={() => onMaterialClick(MaterialType.Perforated)}
|
||||
className={`grow h-full px-[12px] py-[5px] flex justify-center items-center cursor-pointer
|
||||
${
|
||||
activeMaterial === MaterialType.Perforated
|
||||
? " bg-white border-transparent"
|
||||
: "bg-gray-200"
|
||||
}`}
|
||||
>
|
||||
<img
|
||||
className={`rounded-md`}
|
||||
src={PerforatedMaterial}
|
||||
alt="PerforatedMaterial"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
onClick={() => onMaterialClick(MaterialType.Woodgrain)}
|
||||
className={`grow h-full px-[12px] py-[5px] flex justify-center items-center cursor-pointer
|
||||
${
|
||||
activeMaterial === MaterialType.Woodgrain
|
||||
? " bg-white border-transparent"
|
||||
: "bg-gray-200"
|
||||
}`}
|
||||
>
|
||||
<img
|
||||
className={`rounded-md`}
|
||||
src={WoodgrainMaterial}
|
||||
alt="WoodgrainMaterial"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`h-fit `}>
|
||||
<div className={`text-black w-full h-fit overflow-y-auto`}>
|
||||
<div
|
||||
onClick={() => onDockPanelClick(DockPanelCategories.RollIn)}
|
||||
className={`w-full h-[67px] z-30 flex items-center pl-3 gap-x-1 font-bold text-[18px] leading-[150%] tracking-tight text-[#111322] cursor-pointer`}
|
||||
>
|
||||
<Chevron
|
||||
active={
|
||||
activeDockCategory === DockPanelCategories.RollIn ? true : false
|
||||
}
|
||||
/>
|
||||
{DockPanelCategories.RollIn}
|
||||
</div>
|
||||
<div
|
||||
className={`${
|
||||
activeDockCategory === DockPanelCategories.RollIn
|
||||
? "block"
|
||||
: "close-dock-panel hidden"
|
||||
} px-[12px]`}
|
||||
>
|
||||
{dock.length ? (
|
||||
<>
|
||||
<img src={dock[0].image} alt="" className={`rounded-md my-2`} />
|
||||
<div className={`grid grid-cols-2`}>
|
||||
{dock?.map((dockItem, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => onDockSelect(dockItem)}
|
||||
className={`w-[100px] h-[47px] rounded py-2 mx-1 mt-1 border border-[#B9C0D4]`}
|
||||
>
|
||||
<div
|
||||
className={`flex items-center justify-center gap-x-1`}
|
||||
>
|
||||
<span>{dockItem.width}'</span>
|
||||
<span>x</span>
|
||||
<span>{dockItem.length}'</span>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div
|
||||
onClick={() => onDockPanelClick(DockPanelCategories.Floating)}
|
||||
className={`w-full h-[67px] z-30 border-b-2 border-b-[#B9C0D4] flex items-center pl-3 gap-x-1 font-bold text-[18px] leading-[150%] tracking-tight text-[#111322] cursor-pointer`}
|
||||
>
|
||||
<Chevron
|
||||
active={
|
||||
activeDockCategory === DockPanelCategories.Floating
|
||||
? true
|
||||
: false
|
||||
}
|
||||
/>
|
||||
{DockPanelCategories.Floating}
|
||||
</div>
|
||||
<div
|
||||
className={`${
|
||||
activeDockCategory === DockPanelCategories.Floating
|
||||
? "block"
|
||||
: "close-dock-panel hidden"
|
||||
} px-[12px]`}
|
||||
>
|
||||
{dock.length ? (
|
||||
<>
|
||||
<img src={dock[0].image} alt="" className={`rounded-md my-2`} />
|
||||
<div className={`grid grid-cols-2`}>
|
||||
{dock?.map((dockItem, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => onDockSelect(dockItem)}
|
||||
className={`w-[100px] h-[47px] rounded py-2 mx-1 mt-1 border border-[#B9C0D4]`}
|
||||
>
|
||||
<div
|
||||
className={`flex items-center justify-center gap-x-1`}
|
||||
>
|
||||
<span>{dockItem.width}'</span>
|
||||
<span>x</span>
|
||||
<span>{dockItem.length}'</span>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div
|
||||
onClick={() => onDockPanelClick(DockPanelCategories.Sectional)}
|
||||
className={`w-full h-[67px] z-30 border-b-2 border-b-[#B9C0D4] flex items-center pl-3 gap-x-1 font-bold text-[18px] leading-[150%] tracking-tight text-[#111322] cursor-pointer`}
|
||||
>
|
||||
<Chevron
|
||||
active={
|
||||
activeDockCategory === DockPanelCategories.Sectional
|
||||
? true
|
||||
: false
|
||||
}
|
||||
/>
|
||||
{DockPanelCategories.Sectional}
|
||||
</div>
|
||||
<div
|
||||
className={`${
|
||||
activeDockCategory === DockPanelCategories.Sectional
|
||||
? "block"
|
||||
: "close-dock-panel hidden"
|
||||
} px-[12px]`}
|
||||
>
|
||||
{dock.length ? (
|
||||
<>
|
||||
<img src={dock[0].image} alt="" className={`rounded-md my-2`} />
|
||||
<div className={`grid grid-cols-2`}>
|
||||
{dock?.map((dockItem, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => onDockSelect(dockItem)}
|
||||
className={`w-[100px] h-[47px] rounded py-2 mx-1 mt-1 border border-[#B9C0D4]`}
|
||||
>
|
||||
<div
|
||||
className={`flex items-center justify-center gap-x-1`}
|
||||
>
|
||||
<span>{dockItem.width}'</span>
|
||||
<span>x</span>
|
||||
<span>{dockItem.length}'</span>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
<div
|
||||
onClick={() => onDockPanelClick(DockPanelCategories.Wedges)}
|
||||
className={`w-full h-[67px] z-30 border-b-2 border-b-[#B9C0D4] flex items-center pl-3 gap-x-1 font-bold text-[18px] leading-[150%] tracking-tight text-[#111322] cursor-pointer`}
|
||||
>
|
||||
<Chevron
|
||||
active={
|
||||
activeDockCategory === DockPanelCategories.Wedges ? true : false
|
||||
}
|
||||
/>
|
||||
{DockPanelCategories.Wedges}
|
||||
</div>
|
||||
<div
|
||||
className={`${
|
||||
activeDockCategory === DockPanelCategories.Wedges
|
||||
? "block"
|
||||
: "close-dock-panel hidden"
|
||||
} px-[12px]`}
|
||||
>
|
||||
{wedgesAndRamps?.wedges.length ? (
|
||||
<>
|
||||
<img
|
||||
src={wedgesAndRamps?.wedges[0].image}
|
||||
alt=""
|
||||
className={`rounded-md my-2`}
|
||||
/>
|
||||
<div className={`grid grid-cols-2`}>
|
||||
{wedgesAndRamps?.wedges?.map((dockItem, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => onDockSelect(dockItem)}
|
||||
className={`w-[100px] flex flex-col items-center flex-1 h-[47px] rounded py-1 mx-1 mt-1 border border-[#B9C0D4]`}
|
||||
>
|
||||
<div
|
||||
className={`flex items-center justify-center gap-x-1`}
|
||||
>
|
||||
<span>{dockItem.width}'</span>
|
||||
<span>x</span>
|
||||
<span>{dockItem.length}'</span>
|
||||
</div>
|
||||
<span className={`text-xs`}>
|
||||
{getWedgesCategory(dockItem.category)}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
<div
|
||||
onClick={() => onDockPanelClick(DockPanelCategories.Ramps)}
|
||||
className={`w-full h-[67px] z-30 border-b-2 border-b-[#B9C0D4] flex items-center pl-3 gap-x-1 font-bold text-[18px] leading-[150%] tracking-tight text-[#111322] cursor-pointer`}
|
||||
>
|
||||
<Chevron
|
||||
active={
|
||||
activeDockCategory === DockPanelCategories.Ramps ? true : false
|
||||
}
|
||||
/>
|
||||
{DockPanelCategories.Ramps}
|
||||
</div>
|
||||
<div
|
||||
className={`${
|
||||
activeDockCategory === DockPanelCategories.Ramps
|
||||
? "block"
|
||||
: "close-dock-panel hidden"
|
||||
} px-[12px]`}
|
||||
>
|
||||
{wedgesAndRamps.ramps.length ? (
|
||||
<>
|
||||
<img
|
||||
src={wedgesAndRamps.ramps[0].image}
|
||||
alt=""
|
||||
className={`rounded-md my-2`}
|
||||
/>
|
||||
<div className={`grid grid-cols-2`}>
|
||||
{wedgesAndRamps.ramps?.map((dockItem, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => onDockSelect(dockItem)}
|
||||
className={`w-[100px] flex flex-col items-center flex-1 h-[47px] rounded py-1 mx-1 mt-1 border border-[#B9C0D4]`}
|
||||
>
|
||||
<div
|
||||
className={`flex items-center justify-center gap-x-1`}
|
||||
>
|
||||
<span>{dockItem.width}'</span>
|
||||
<span>x</span>
|
||||
<span>{dockItem.length}'</span>
|
||||
</div>
|
||||
<span className={`text-xs`}>
|
||||
{getRampsCategory(dockItem?.category)}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
<div
|
||||
onClick={() => onDockPanelClick(DockPanelCategories.BoatLift2)}
|
||||
className={`w-full h-[67px] z-30 border-b-2 border-b-[#B9C0D4] flex items-center pl-3 gap-x-1 font-bold text-[18px] leading-[150%] tracking-tight text-[#111322] cursor-pointer`}
|
||||
>
|
||||
<Chevron
|
||||
active={
|
||||
activeDockCategory === DockPanelCategories.BoatLift2
|
||||
? true
|
||||
: false
|
||||
}
|
||||
/>
|
||||
{DockPanelCategories.BoatLift2}
|
||||
</div>
|
||||
<div
|
||||
className={`${
|
||||
activeDockCategory === DockPanelCategories.BoatLift2
|
||||
? "block"
|
||||
: "close-dock-panel hidden"
|
||||
} px-[12px]`}
|
||||
>
|
||||
{boatLift.length ? (
|
||||
<>
|
||||
<div
|
||||
className={`flex flex-col w-full justify-center items-center `}
|
||||
>
|
||||
<div
|
||||
className={`w-full h-5 text-center text-[#4A5578] font-normal text-[14px] leading-[20px]`}
|
||||
>
|
||||
Lift Range
|
||||
</div>
|
||||
<div className={`flex w-full justify-between items-center`}>
|
||||
{liftRanges.map((liftRange) => (
|
||||
<button
|
||||
onClick={() => setActiveLiftRange(liftRange)}
|
||||
className={`grow h-[43px] text-[#4A5578] font-normal text-[18px] leading-[150%] tracking-wide bg-white p-2 ${
|
||||
activeLiftRange === liftRange
|
||||
? "border-b-2 border-b-[#0F75BC]"
|
||||
: ""
|
||||
} `}
|
||||
key={liftRange}
|
||||
>
|
||||
{liftRange}ft
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<img
|
||||
src={boatLift[0].image}
|
||||
alt=""
|
||||
className={`rounded-md my-2`}
|
||||
/>
|
||||
<div className={`grid grid-cols-2`}>
|
||||
{boatLift?.map((dockItem, index) => {
|
||||
if (dockItem?.lift_range === activeLiftRange) {
|
||||
return (
|
||||
<div key={index}>
|
||||
<button
|
||||
onClick={() => onDockSelect(dockItem)}
|
||||
className={`w-[100px] h-[47px] rounded py-2 mx-1 mt-1 border border-[#B9C0D4] `}
|
||||
>
|
||||
{dockItem.weight_capacity}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div
|
||||
onClick={() => onDockPanelClick(DockPanelCategories.BoatLift4)}
|
||||
className={`w-full h-[67px] z-30 border-b-2 border-b-[#B9C0D4] flex items-center pl-3 gap-x-1 font-bold text-[18px] leading-[150%] tracking-tight text-[#111322] cursor-pointer`}
|
||||
>
|
||||
<Chevron
|
||||
active={
|
||||
activeDockCategory === DockPanelCategories.BoatLift4
|
||||
? true
|
||||
: false
|
||||
}
|
||||
/>
|
||||
{DockPanelCategories.BoatLift4}
|
||||
</div>
|
||||
<div
|
||||
className={`${
|
||||
activeDockCategory === DockPanelCategories.BoatLift4
|
||||
? "block"
|
||||
: "close-dock-panel hidden"
|
||||
} px-[12px]`}
|
||||
>
|
||||
{boatLift.length ? (
|
||||
<>
|
||||
<div
|
||||
className={`flex flex-col w-full justify-center items-center `}
|
||||
>
|
||||
<div
|
||||
className={`w-full h-5 text-center text-[#4A5578] font-normal text-[14px] leading-[20px]`}
|
||||
>
|
||||
Lift Range
|
||||
</div>
|
||||
<div className={`flex w-full justify-between items-center`}>
|
||||
{liftRanges.map((liftRange) => (
|
||||
<button
|
||||
onClick={() => setActiveLiftRange(liftRange)}
|
||||
className={`grow h-[43px] text-[#4A5578] font-normal text-[18px] leading-[150%] tracking-wide bg-white p-2 ${
|
||||
activeLiftRange === liftRange
|
||||
? "border-b-2 border-b-[#0F75BC]"
|
||||
: ""
|
||||
}`}
|
||||
key={liftRange}
|
||||
>
|
||||
{liftRange}ft
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<img
|
||||
src={boatLift[0].image}
|
||||
alt=""
|
||||
className={`rounded-md my-2`}
|
||||
/>
|
||||
<div className={`grid grid-cols-2`}>
|
||||
{boatLift?.map((dockItem, index) => {
|
||||
if (dockItem?.lift_range === activeLiftRange) {
|
||||
return (
|
||||
<div key={index}>
|
||||
<button
|
||||
onClick={() => onDockSelect(dockItem)}
|
||||
className={`w-[100px] h-[47px] rounded py-2 mx-1 mt-1 border border-[#B9C0D4] `}
|
||||
>
|
||||
{dockItem.weight_capacity}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div
|
||||
onClick={() => onDockPanelClick(DockPanelCategories.Accessories)}
|
||||
className={`w-full h-[67px] z-30 border-b-2 border-b-[#B9C0D4] flex items-center pl-3 gap-x-1 font-bold text-[18px] leading-[150%] tracking-tight text-[#111322] cursor-pointer`}
|
||||
>
|
||||
<Chevron
|
||||
active={
|
||||
activeDockCategory === DockPanelCategories.Accessories
|
||||
? true
|
||||
: false
|
||||
}
|
||||
/>
|
||||
{DockPanelCategories.Accessories}
|
||||
</div>
|
||||
<div
|
||||
className={`${
|
||||
activeDockCategory === DockPanelCategories.Accessories
|
||||
? "block"
|
||||
: "close-dock-panel hidden"
|
||||
} px-[12px]`}
|
||||
>
|
||||
{accessories.length ? (
|
||||
<>
|
||||
<div className={`grid grid-cols-2`}>
|
||||
{accessories?.map((dockItem, index) => (
|
||||
<div
|
||||
key={index}
|
||||
onClick={() => onDockSelect(dockItem)}
|
||||
className={`h-fit rounded mx-1 mt-1 border border-[#B9C0D4] cursor-pointer`}
|
||||
>
|
||||
{/* <div className={ `flex items-center justify-center gap-x-1` }>
|
||||
<span>{ dockItem.width }'</span>
|
||||
<span>x</span>
|
||||
<span>{ dockItem.length }'</span> */}
|
||||
<img
|
||||
src={dockItem.thumbnail}
|
||||
alt=""
|
||||
className={`rounded-md `}
|
||||
/>
|
||||
{/* </div> */}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
{
|
||||
/* <div className={ `grow flex flex-col justify-center items-start bg-transparent` }>
|
||||
|
||||
</div> */
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { Builder } from './Builder'
|
||||
@@ -0,0 +1,80 @@
|
||||
import React, { memo } from 'react';
|
||||
import { ArrowLeftIcon, ArrowRrightIcon, } from 'Assets/svgs';
|
||||
import { EstimateSteps, } from 'Utils/constants';
|
||||
|
||||
const Comments = ( { step, onStepChange, onUpdateComment, onSubmit, errorMessage, submitLoading, comments } ) => {
|
||||
|
||||
{/* Comments */ }
|
||||
|
||||
return (
|
||||
|
||||
<div className={ `${ step === EstimateSteps.Comments ? '' : 'hidden' }` }>
|
||||
|
||||
<div>
|
||||
<button
|
||||
disabled={ submitLoading }
|
||||
className={ `text-[#4A5578] leading-[150%] tracking-tight text-[14px] font-medium flex items-center` }
|
||||
onClick={ () => onStepChange( EstimateSteps.LakeSurrounding ) }
|
||||
>
|
||||
<ArrowLeftIcon /> Back
|
||||
</button>
|
||||
<div className={ `flex items-center gap-x-2` }>
|
||||
<p className={ ` uppercase text-[#0F75BC] leading-[150%] tracking-wide text-[18px] font-bold` }>Comments</p>
|
||||
<span className={ `text-[#4A5578] leading-[150%] tracking-tight text-[14px] font-medium` }>(3 of 3)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<fieldset className="cus-input">
|
||||
<label
|
||||
htmlFor="comments"
|
||||
className="text-[#4A5578] leading-[20px] tracking-tight text-[14px] font-medium"
|
||||
>
|
||||
<span className={ `text-red-600 mr-2` }>*</span>Comments
|
||||
</label>
|
||||
<div className="relative mb-6">
|
||||
<div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
|
||||
{/* <img
|
||||
className="w-5 h-5 object-contain"
|
||||
src={ `` }
|
||||
/> */}
|
||||
</div>
|
||||
<textarea
|
||||
name="comments" cols="30" rows="10"
|
||||
type="text"
|
||||
id="comments"
|
||||
disabled={ submitLoading }
|
||||
className="bg-gray-50 border-2 border-[#D1D5DB] text-black text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-3 dark:bg-gray-200 dark:border-gray-600 dark:placeholder-gray-400 dark:focus:ring-blue-500 dark:focus:border-blue-500 outline-none"
|
||||
placeholder="Comments"
|
||||
onKeyUp={ ( e ) => onUpdateComment( e.target.value ) }
|
||||
>
|
||||
</textarea>
|
||||
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div className={ `flex w-full justify-center bg-transparent ` }>
|
||||
<button
|
||||
disabled={ submitLoading || !comments }
|
||||
className={ `h-[44px] w-full rounded border-2 border-[#0F75BC] flex justify-center gap-x-2 items-center bg-[#0F75BC] text-[#ffffff] uppercase font-medium tracking-wide leading-[24px] text-[16px] ${ submitLoading || !comments ? "cursor-not-allowed" : "" }` }
|
||||
onClick={ onSubmit }
|
||||
>
|
||||
{ submitLoading ?
|
||||
<>
|
||||
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white inline" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg> <>Submit Estimate Inquiry <ArrowRrightIcon /></>
|
||||
</>
|
||||
: <>Submit Estimate Inquiry <ArrowRrightIcon /></>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-red-500 text-xs italic capitalize">{ errorMessage }</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const CommentsMemo = memo( Comments );
|
||||
|
||||
export { CommentsMemo as Comments };
|
||||
@@ -0,0 +1 @@
|
||||
export { Comments } from './Comments'
|
||||
@@ -0,0 +1,324 @@
|
||||
import React, { memo, useState, useCallback, useEffect } from 'react';
|
||||
import { ArrowRrightIcon } from 'Assets/svgs';
|
||||
import { EstimateSteps, Tables, Truthy } from 'Utils/constants';
|
||||
import MkdSDK from 'Utils/MkdSDK';
|
||||
|
||||
const sdk = new MkdSDK()
|
||||
const ContactInformation = ( {
|
||||
step,
|
||||
onStepChange,
|
||||
hasDealer,
|
||||
onHasDealerChange,
|
||||
onDealerChange,
|
||||
setFirstName,
|
||||
setLastName,
|
||||
setEmail,
|
||||
setPhone,
|
||||
setLake,
|
||||
setCity,
|
||||
setCountry,
|
||||
} ) => {
|
||||
|
||||
const [ TC, setTC ] = useState( false )
|
||||
const [ dealers, setDealers ] = useState( [] )
|
||||
|
||||
const getDealers = useCallback( ( table ) => {
|
||||
( async () => {
|
||||
try {
|
||||
const result = await sdk.getItems( table )
|
||||
setDealers( result?.model )
|
||||
} catch ( error ) {
|
||||
console.log( error.message )
|
||||
}
|
||||
} )()
|
||||
}, [ dealers ] )
|
||||
|
||||
useEffect( () => {
|
||||
if ( !dealers.length ) {
|
||||
getDealers( Tables.Dealers )
|
||||
}
|
||||
}, [ dealers ] )
|
||||
|
||||
{/* ContactInformation */ }
|
||||
return (
|
||||
<div className={ `h-[500px] max-h-[500px] overflow-auto ${ step === EstimateSteps.ContactInformation ? '' : 'hidden' }` }>
|
||||
|
||||
<div>
|
||||
<div className={ `flex items-center gap-x-2` }>
|
||||
<p className={ ` uppercase text-[#0F75BC] leading-[150%] tracking-wide text-[18px] font-bold` }>Contact information</p>
|
||||
<span className={ `text-[#4A5578] leading-[150%] tracking-tight text-[14px] font-medium` }>(1 of 3)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={ `` }>
|
||||
<div className={ `flex w-full px-0 gap-x-2 justify-between items-center ` }>
|
||||
<fieldset className="cus-input w-1/2">
|
||||
<label
|
||||
htmlFor="firstName"
|
||||
className="text-[#4A5578] leading-[20px] tracking-tight text-[14px] font-medium"
|
||||
>
|
||||
<span className={ `text-red-600 mr-2` }>*</span>First Name
|
||||
</label>
|
||||
<div className="relative mb-6">
|
||||
{/* <div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
|
||||
<img
|
||||
className="w-5 h-5 object-contain"
|
||||
src={ `` }
|
||||
/>
|
||||
</div> */}
|
||||
<input
|
||||
type="text"
|
||||
id="firstName"
|
||||
className="bg-gray-50 border-2 border-[#D1D5DB] text-black text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full pl-3 p-3 dark:bg-gray-200 dark:border-gray-600 dark:placeholder-gray-400 dark:focus:ring-blue-500 dark:focus:border-blue-500 outline-none"
|
||||
placeholder="John"
|
||||
onChange={ ( e ) => setFirstName( e.target.value ) }
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset className="cus-input w-1/2">
|
||||
<label
|
||||
htmlFor="lastName"
|
||||
className="text-[#4A5578] leading-[20px] tracking-tight text-[14px] font-medium"
|
||||
>
|
||||
<span className={ `text-red-600 mr-2` }>*</span>Last Name
|
||||
</label>
|
||||
<div className="relative mb-6">
|
||||
<div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
|
||||
{/* <img
|
||||
className="w-5 h-5 object-contain"
|
||||
src={ `` }
|
||||
/> */}
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
id="lastName"
|
||||
className="bg-gray-50 border-2 border-[#D1D5DB] text-black text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full pl-3 p-3 dark:bg-gray-200 dark:border-gray-600 dark:placeholder-gray-400 dark:focus:ring-blue-500 dark:focus:border-blue-500 outline-none"
|
||||
placeholder="Doe"
|
||||
onChange={ ( e ) => setLastName( e.target.value ) }
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
<fieldset className="cus-input">
|
||||
<label
|
||||
htmlFor="email"
|
||||
className="text-[#4A5578] leading-[20px] tracking-tight text-[14px] font-medium"
|
||||
>
|
||||
<span className={ `text-red-600 mr-2` }>*</span>Email
|
||||
</label>
|
||||
<div className="relative mb-6">
|
||||
<div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
|
||||
{/* <img
|
||||
className="w-5 h-5 object-contain"
|
||||
src={ `` }
|
||||
/> */}
|
||||
</div>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
className="bg-gray-50 border-2 border-[#D1D5DB] text-black text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full pl-3 p-3 dark:bg-gray-200 dark:border-gray-600 dark:placeholder-gray-400 dark:focus:ring-blue-500 dark:focus:border-blue-500 outline-none"
|
||||
placeholder="JohnDoe@example.com"
|
||||
onChange={ ( e ) => setEmail( e.target.value ) }
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset className="cus-input">
|
||||
<label
|
||||
htmlFor="phoneNumber"
|
||||
className="text-[#4A5578] leading-[20px] tracking-tight text-[14px] font-medium"
|
||||
>
|
||||
<span className={ `text-red-600 mr-2` }>*</span>Telephone
|
||||
</label>
|
||||
<div className="relative mb-6">
|
||||
<div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
|
||||
{/* <img
|
||||
className="w-5 h-5 object-contain"
|
||||
src={ `` }
|
||||
/> */}
|
||||
</div>
|
||||
<input
|
||||
type="tel"
|
||||
id="phoneNumber"
|
||||
className="bg-gray-50 border-2 border-[#D1D5DB] text-black text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full pl-3 p-3 dark:bg-gray-200 dark:border-gray-600 dark:placeholder-gray-400 dark:focus:ring-blue-500 dark:focus:border-blue-500 outline-none"
|
||||
// placeholder="James"
|
||||
onChange={ ( e ) => setPhone( e.target.value ) }
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
<div className={ `flex w-full px-0 gap-x-2 justify-between items-center ` }>
|
||||
<fieldset className="cus-input w-1/3">
|
||||
<label
|
||||
htmlFor="lake"
|
||||
className="text-[#4A5578] leading-[20px] tracking-tight text-[14px] font-medium"
|
||||
>
|
||||
<span className={ `text-red-600 mr-2` }>*</span>Location
|
||||
</label>
|
||||
<div className="relative mb-6">
|
||||
<div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
|
||||
{/* <img
|
||||
className="w-5 h-5 object-contain"
|
||||
src={ `` }
|
||||
/> */}
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
id="lake"
|
||||
className="bg-gray-50 border-2 border-[#D1D5DB] text-black text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full pl-3 p-3 dark:bg-gray-200 dark:border-gray-600 dark:placeholder-gray-400 dark:focus:ring-blue-500 dark:focus:border-blue-500 outline-none"
|
||||
placeholder="Lake"
|
||||
onChange={ ( e ) => setLake( e.target.value ) }
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset className="cus-input w-1/3">
|
||||
<label
|
||||
htmlFor="city"
|
||||
className="text-[#4A5578] leading-[20px] tracking-tight text-[14px] font-medium"
|
||||
>
|
||||
<span className={ `text-red-600 mr-2` }>*</span>City
|
||||
</label>
|
||||
<div className="relative mb-6">
|
||||
<div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
|
||||
{/* <img
|
||||
className="w-5 h-5 object-contain"
|
||||
src={ `` }
|
||||
/> */}
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
id="city"
|
||||
className="bg-gray-50 border-2 border-[#D1D5DB] text-black text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full pl-3 p-3 dark:bg-gray-200 dark:border-gray-600 dark:placeholder-gray-400 dark:focus:ring-blue-500 dark:focus:border-blue-500 outline-none"
|
||||
placeholder="City"
|
||||
onChange={ ( e ) => setCity( e.target.value ) }
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset className="cus-input w-1/3">
|
||||
<label
|
||||
htmlFor="country"
|
||||
className="text-[#4A5578] leading-[20px] tracking-tight text-[14px] font-medium"
|
||||
>
|
||||
<span className={ `text-red-600 mr-2` }>*</span>Country
|
||||
</label>
|
||||
<div className="relative mb-6">
|
||||
{/* <div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
|
||||
<img
|
||||
className="w-5 h-5 object-contain"
|
||||
src={ `` }
|
||||
/>
|
||||
</div> */}
|
||||
<select
|
||||
type="text"
|
||||
id="country"
|
||||
className="bg-gray-50 border-2 border-[#D1D5DB] text-black text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full pl-3 p-3 dark:bg-gray-200 dark:border-gray-600 dark:placeholder-gray-400 dark:focus:ring-blue-500 dark:focus:border-blue-500 outline-none"
|
||||
placeholder="Country"
|
||||
onChange={ ( e ) => setCountry( e.target.value ) }
|
||||
>
|
||||
<option></option>
|
||||
<option value={ `Canada` }>Canada</option>
|
||||
</select>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
<fieldset className="cus-input">
|
||||
<label
|
||||
// htmlFor="city"
|
||||
className="text-[#4A5578] leading-[20px] tracking-tight text-[14px] font-medium"
|
||||
>
|
||||
<span className={ `text-red-600 mr-2` }>*</span>Are you currently working with a Dealer in your area?
|
||||
</label>
|
||||
<div className="relative flex items-center justify-start mb-6">
|
||||
<input
|
||||
type="radio"
|
||||
name="dealer"
|
||||
id="yesDealer"
|
||||
className="bg-gray-50 border-2 border-[#D1D5DB] text-black text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-[44px] pl-3 p-3 dark:bg-gray-200 dark:border-gray-600 dark:placeholder-gray-400 dark:focus:ring-blue-500 dark:focus:border-blue-500 outline-none"
|
||||
onChange={ () => onHasDealerChange( Truthy.True ) }
|
||||
/>
|
||||
<label
|
||||
htmlFor="yesDealer"
|
||||
className="text-[#4A5578] leading-[20px] tracking-tight text-[14px] font-medium"
|
||||
>Yes
|
||||
</label>
|
||||
<input
|
||||
type="radio"
|
||||
name="dealer"
|
||||
id="noDealer"
|
||||
className="bg-gray-50 border-2 border-[#D1D5DB] text-black text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-[44px] pl-3 p-3 dark:bg-gray-200 dark:border-gray-600 dark:placeholder-gray-400 dark:focus:ring-blue-500 dark:focus:border-blue-500 outline-none"
|
||||
onChange={ () => onHasDealerChange( Truthy.False ) }
|
||||
/>
|
||||
<label
|
||||
htmlFor="noDealer"
|
||||
className="text-[#4A5578] leading-[20px] tracking-tight text-[14px] font-medium"
|
||||
>No
|
||||
</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
{ hasDealer ?
|
||||
<fieldset className="cus-input">
|
||||
<label
|
||||
htmlFor="dealers"
|
||||
className="text-[#4A5578] leading-[20px] tracking-tight text-[14px] font-medium"
|
||||
>
|
||||
<span className={ `text-red-600 mr-2` }>*</span>Dealers
|
||||
</label>
|
||||
<div className="relative mb-6">
|
||||
{/* <div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
|
||||
<img
|
||||
className="w-5 h-5 object-contain"
|
||||
src={ `` }
|
||||
/>
|
||||
</div> */}
|
||||
<select
|
||||
id="dealers"
|
||||
className="bg-gray-50 border-2 border-[#D1D5DB] text-black text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full pl-3 p-3 dark:bg-gray-200 dark:border-gray-600 dark:placeholder-gray-400 dark:focus:ring-blue-500 dark:focus:border-blue-500 outline-none"
|
||||
placeholder="Delaers"
|
||||
onChange={ ( e ) => onDealerChange( e.target.value ) }
|
||||
>
|
||||
<>
|
||||
<option></option>
|
||||
{ dealers?.length ? dealers.map( ( dealer ) => (
|
||||
<option key={ dealer?.id } value={ dealer?.id }>{ dealer?.name }</option>
|
||||
) )
|
||||
: null }
|
||||
</>
|
||||
</select>
|
||||
</div>
|
||||
</fieldset>
|
||||
: null
|
||||
}
|
||||
<fieldset className="cus-input">
|
||||
<div className="relative flex items-center justify-start mb-6">
|
||||
|
||||
<input
|
||||
// ref={tcRef}
|
||||
type="checkbox"
|
||||
name="desclaimer"
|
||||
id="desclaimer"
|
||||
checked={ TC }
|
||||
className="bg-gray-50 border-2 border-[#D1D5DB] text-black text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-[44px] pl-0 p-3 dark:bg-gray-200 dark:border-gray-600 dark:placeholder-gray-400 dark:focus:ring-blue-500 dark:focus:border-blue-500 outline-none self-start mt-1"
|
||||
onChange={ ( e ) => setTC( e.target.checked ) }
|
||||
/>
|
||||
<label
|
||||
htmlFor="desclaimer"
|
||||
className="text-[#4A5578] leading-[20px] tracking-tight text-[14px] font-medium"
|
||||
>Terms And Conditions
|
||||
</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div className={ `flex w-full justify-center bg-transparent ` }>
|
||||
<button
|
||||
disabled={ !TC }
|
||||
className={ `h-[44px] w-full rounded border-2 border-[#0F75BC] flex justify-center gap-x-2 items-center bg-[#0F75BC] text-[#ffffff] uppercase font-medium tracking-wide leading-[24px] text-[16px]` }
|
||||
onClick={ () => onStepChange( EstimateSteps.LakeSurrounding ) }
|
||||
>
|
||||
Continue <ArrowRrightIcon />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const ContactInformationMemo = memo( ContactInformation );
|
||||
|
||||
export { ContactInformationMemo as ContactInformation };
|
||||
@@ -0,0 +1 @@
|
||||
export { ContactInformation } from './ContactInformation'
|
||||
@@ -0,0 +1,823 @@
|
||||
import React, {
|
||||
useEffect,
|
||||
useMemo,
|
||||
useCallback,
|
||||
useRef,
|
||||
useState,
|
||||
useContext
|
||||
} from "react";
|
||||
import { FabricJSCanvas, useFabricJSEditor } from "fabricjs-react";
|
||||
import { fabric } from "fabric";
|
||||
import { DockSidebar } from "Components/DockSidebar";
|
||||
import { ActionButtons } from "Components/ActionButtons";
|
||||
import MkdSDK from "Utils/MkdSDK";
|
||||
import { Stack } from "Utils/utils";
|
||||
import { EstimateModal } from "Components/EstimateModal";
|
||||
import * as XLSX from "xlsx";
|
||||
import { BuildCanvasFromLocalModal } from "Components/BuildCanvasFromLocalModal";
|
||||
import { GlobalContext } from "../../globalContext";
|
||||
import { clearClone, clone } from "Utils/DockBuilderUtils/clone";
|
||||
import {
|
||||
edgeDetection,
|
||||
handleEdgeDetection,
|
||||
handleIntersection
|
||||
} from "Utils/DockBuilderUtils";
|
||||
import {
|
||||
CanvasModes,
|
||||
deleteIcon,
|
||||
DockPanelCategories,
|
||||
edgeSnapThreshold,
|
||||
nintyDeg,
|
||||
oneEightyDeg,
|
||||
oneFeet,
|
||||
rotateIcon,
|
||||
scaleFactor,
|
||||
twoSeventyDeg
|
||||
} from "Utils/constants";
|
||||
import {
|
||||
reScaleXY,
|
||||
resolveHeight,
|
||||
resolveWidth
|
||||
} from "Utils/DockBuilderUtils/edgeDetection";
|
||||
import { DeleteIcon, RotateIcon } from "Assets/svgs";
|
||||
import { capitalize } from "Utils/helper";
|
||||
|
||||
const sdk = new MkdSDK();
|
||||
const stack = new Stack();
|
||||
|
||||
export const DockBuilder = () => {
|
||||
let clipboard = null;
|
||||
let mouseDownPoint = null;
|
||||
let shiftKeyDown = false;
|
||||
window.counter = 0;
|
||||
let prevPointer = null;
|
||||
let isDragging = false;
|
||||
let isZoomed = false;
|
||||
let startX, startY;
|
||||
|
||||
const { dispatch } = useContext(GlobalContext);
|
||||
const { editor, onReady } = useFabricJSEditor({ selection: true });
|
||||
|
||||
const canvasModeRef = useRef(null);
|
||||
const fileRef = useRef();
|
||||
const imageRef = useRef();
|
||||
|
||||
const editorMemo = useMemo(() => editor?.canvas, [editor?.canvas]);
|
||||
// const { dispatch } = React.useContext( AuthContext );
|
||||
const [objHovered, setObjHovered] = useState(false);
|
||||
const [selectedItems, setSelectedItems] = useState([]);
|
||||
const [estimateLoading, setEstimateLoading] = useState(false);
|
||||
const [showEstimateModal, setShowEstimateModal] = useState(false);
|
||||
const [showBuildCanvasFromLocalModal, setShowBuildCanvasFromLocalModal] =
|
||||
useState(false);
|
||||
const [initialLoad, setInitialLoad] = useState(true);
|
||||
const [linesAdded, setLinesAdded] = useState(false);
|
||||
const [dockImage, setDockImage] = useState(null);
|
||||
const [editorReady, setEditorReady] = useState(false);
|
||||
const [dockLabel, setDockLabel] = useState(null);
|
||||
const [dockDimensions, setDockDimensions] = useState(null);
|
||||
|
||||
const deleteImg = document.createElement("img");
|
||||
deleteImg.src = deleteIcon;
|
||||
const rotateImg = document.createElement("img");
|
||||
rotateImg.src = rotateIcon;
|
||||
|
||||
const onEstimateModalClose = useCallback(() => {
|
||||
setShowEstimateModal(false);
|
||||
}, [showEstimateModal]);
|
||||
|
||||
const onEstimateModalOpen = useCallback(() => {
|
||||
const ext = "png";
|
||||
const base64 = editorMemo.toDataURL({
|
||||
format: ext,
|
||||
enableRetinaScaling: true
|
||||
});
|
||||
setDockImage(base64);
|
||||
setShowEstimateModal(true);
|
||||
}, [showEstimateModal, editorMemo, dockImage]);
|
||||
|
||||
// const onAddRect = useCallback( () => {
|
||||
// editor?.canvas?.add( rect );
|
||||
// window.counter++;
|
||||
// // newleft += 200;
|
||||
// }, [ 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
|
||||
};
|
||||
|
||||
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 downloadImage = useCallback(() => {
|
||||
// // console.log( 'Download' )
|
||||
const ext = "png";
|
||||
|
||||
// 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
|
||||
}, [editorMemo]);
|
||||
|
||||
const renderBg = useCallback(() => {
|
||||
const imageURL =
|
||||
"https://s3.us-east-2.amazonaws.com/com.nds.images/download.png";
|
||||
|
||||
new fabric.Image.fromURL(
|
||||
imageURL,
|
||||
function (img) {
|
||||
img.set({
|
||||
scaleX: editorMemo.width / img.width,
|
||||
scaleY: editorMemo.height / img.height
|
||||
});
|
||||
editorMemo.setBackgroundImage(img);
|
||||
editorMemo.renderAll();
|
||||
// updateModifications( true, )
|
||||
},
|
||||
{
|
||||
crossOrigin: "anonymous"
|
||||
}
|
||||
);
|
||||
}, [editorMemo]);
|
||||
|
||||
const downloadExcel = useCallback((data) => {
|
||||
const worksheet = XLSX.utils.json_to_sheet(data);
|
||||
const workbook = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(workbook, worksheet, "Selected Items");
|
||||
XLSX.writeFile(workbook, "Paradise_dock_selected_items.xlsx");
|
||||
}, []);
|
||||
|
||||
function updateZoom(opt) {
|
||||
let delta = opt.e.deltaY;
|
||||
let pointer = editorMemo.getPointer(opt.e);
|
||||
let zoom = editorMemo.getZoom();
|
||||
|
||||
if (delta > 0) {
|
||||
zoom *= 1.1;
|
||||
} else {
|
||||
zoom /= 1.1;
|
||||
}
|
||||
if (zoom > 5) zoom = 5;
|
||||
if (zoom < 1) zoom = 1;
|
||||
// console.log( zoom )
|
||||
// editorMemo.setBackgroundImageStretch=false
|
||||
var vpt = editorMemo.viewportTransform;
|
||||
if (zoom === 1) {
|
||||
vpt[4] = 0;
|
||||
vpt[5] = 0;
|
||||
editorMemo.selection = true;
|
||||
this.isZoomed = false;
|
||||
}
|
||||
if (zoom > 1) {
|
||||
this.isZoomed = true;
|
||||
}
|
||||
|
||||
fabric.util.animate({
|
||||
startValue: editorMemo.getZoom(),
|
||||
endValue: zoom,
|
||||
// duration: 0,
|
||||
onChange: function (value) {
|
||||
editorMemo.zoomToPoint({ x: pointer.x, y: pointer.y }, value);
|
||||
},
|
||||
fps: 1080
|
||||
});
|
||||
opt.e.preventDefault();
|
||||
opt.e.stopPropagation();
|
||||
}
|
||||
|
||||
function handleZoomMouseDown(options) {
|
||||
var pointer = editorMemo.getPointer(options.e, true);
|
||||
mouseDownPoint = new fabric.Point(pointer.x, pointer.y);
|
||||
if (options.target === null && this.isZoomed) {
|
||||
editorMemo.upperCanvasEl.style.cursor = "grabbing";
|
||||
editorMemo.selection = false;
|
||||
// this.lastPosX = options.e.movementX;
|
||||
// this.lastPosY = options.e.movementY;
|
||||
this.lastPosX = options.e.clientX;
|
||||
this.lastPosY = options.e.clientY;
|
||||
|
||||
//
|
||||
}
|
||||
// else if ( options.target !== null && this.isZoomed ) {
|
||||
// var activeObject = editorMemo.getActiveObject();
|
||||
// if ( activeObject ) {
|
||||
// activeObject.lockMovementX = true;
|
||||
// activeObject.lockMovementY = true;
|
||||
// activeObject.lockScalingX = true;
|
||||
// activeObject.lockScalingY = true;
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
const handleZoomMouseUp = useCallback(() => {
|
||||
editorMemo.defaultCursor = "default";
|
||||
mouseDownPoint = null;
|
||||
editorMemo.selection = true;
|
||||
}, [editor]);
|
||||
|
||||
function handleZoomMouseMove(options) {
|
||||
if (options.target === null && mouseDownPoint && this.isZoomed) {
|
||||
editorMemo.selection = false;
|
||||
editorMemo.upperCanvasEl.style.cursor = "grabbing";
|
||||
|
||||
var delta = new fabric.Point(options.e.movementX, options.e.movementY);
|
||||
editorMemo.relativePan(delta);
|
||||
editorMemo.viewportTransform[4] = Math.max(
|
||||
Math.min(editorMemo.viewportTransform[4], 0),
|
||||
editorMemo.getWidth() - editorMemo.getWidth() * editorMemo.getZoom()
|
||||
);
|
||||
editorMemo.viewportTransform[5] = Math.max(
|
||||
Math.min(editorMemo.viewportTransform[5], 0),
|
||||
editorMemo.getHeight() - editorMemo.getHeight() * editorMemo.getZoom()
|
||||
);
|
||||
|
||||
// var vpt = this.viewportTransform;
|
||||
// vpt[ 4 ] += options.e.clientX - this.lastPosX;
|
||||
// vpt[ 5 ] += options.e.clientY - this.lastPosY;
|
||||
requestAnimationFrame(function () {
|
||||
editorMemo.renderAll();
|
||||
this.requestRenderAll();
|
||||
});
|
||||
// this.lastPosX = options.e.movementX;
|
||||
// this.lastPosY = options.e.movementY;
|
||||
// this.lastPosX = options.e.clientX;
|
||||
// this.lastPosY = options.e.clientY;
|
||||
}
|
||||
}
|
||||
|
||||
const addEdgeDetectionClone = useCallback(
|
||||
(options) => {
|
||||
objectMoving = true;
|
||||
if (options) {
|
||||
const selectedObj = options.target.setCoords();
|
||||
editorMemo.defaultCursor = "grabbing";
|
||||
if (
|
||||
selectedObj.dockData &&
|
||||
selectedObj.dockData.itemName === DockPanelCategories.Accessories
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
editorMemo.forEachObject((obj) => {
|
||||
const pointer = { x: options.e.clientX, y: options.e.clientY };
|
||||
|
||||
if (obj === selectedObj) return;
|
||||
// canvas.setCursor('grabbing');
|
||||
// // console.log(pointer.x, pointer.y);
|
||||
if (!obj.snapClone && obj.type === "image") {
|
||||
// // console.log( pointer, prevPointer )
|
||||
if (
|
||||
prevPointer &&
|
||||
(prevPointer.x !== pointer.x || prevPointer.y !== pointer.y)
|
||||
) {
|
||||
switch (true) {
|
||||
case edgeDetection(selectedObj, obj, "left"):
|
||||
clone(selectedObj, editorMemo, obj, "left");
|
||||
break;
|
||||
case edgeDetection(selectedObj, obj, "right"):
|
||||
clone(selectedObj, editorMemo, obj, "right");
|
||||
break;
|
||||
case edgeDetection(selectedObj, obj, "top"):
|
||||
clone(selectedObj, editorMemo, obj, "top");
|
||||
break;
|
||||
case edgeDetection(selectedObj, obj, "bottom"):
|
||||
clone(selectedObj, editorMemo, obj, "bottom");
|
||||
break;
|
||||
case edgeDetection(selectedObj, obj, "neutral"):
|
||||
clearClone(editorMemo);
|
||||
break;
|
||||
default:
|
||||
// console.log( "edge detection switch default" )
|
||||
}
|
||||
// prevPointer = null;
|
||||
}
|
||||
prevPointer = pointer;
|
||||
// // console.log( pointer, prevPointer )
|
||||
}
|
||||
// if ( edgeDetection( selectedObj, obj, "left" ) ) {
|
||||
// clone( selectedObj, editorMemo, obj, "left" )
|
||||
// } else {
|
||||
// // clearClone( editorMemo )
|
||||
// }
|
||||
|
||||
// if ( edgeDetection( selectedObj, obj, 'right' ) ) {
|
||||
// clone( selectedObj, editorMemo, obj, "right" )
|
||||
|
||||
// } else {
|
||||
// // clearClone( editorMemo )
|
||||
// }
|
||||
|
||||
// if ( edgeDetection( selectedObj, obj, 'top' ) ) {
|
||||
// clone( selectedObj, editorMemo, obj, "top" )
|
||||
|
||||
// } else {
|
||||
// // clearClone( editorMemo )
|
||||
// }
|
||||
|
||||
// if ( edgeDetection( selectedObj, obj, 'bottom' ) ) {
|
||||
// clone( selectedObj, editorMemo, obj, "bottom" )
|
||||
// } else {
|
||||
// // clearClone( editorMemo )
|
||||
// }
|
||||
});
|
||||
}
|
||||
},
|
||||
[editor, editorMemo]
|
||||
);
|
||||
|
||||
const horizontalIndicatorLines = (newTop, objTop) => {
|
||||
editorMemo.getObjects("line").forEach((obj) => {
|
||||
if (obj.testLine) {
|
||||
editorMemo.remove(obj);
|
||||
}
|
||||
});
|
||||
|
||||
let line = new fabric.Line([0, newTop, editorMemo.getWidth(), newTop], {
|
||||
stroke: "#AAAAAA",
|
||||
testLine: true
|
||||
// strokeDashArray: [ 5 ],
|
||||
});
|
||||
let line2 = new fabric.Line([0, objTop, editorMemo.getWidth(), objTop], {
|
||||
stroke: "#AAAAAA",
|
||||
testLine: true
|
||||
// strokeDashArray: [ 5 ],
|
||||
});
|
||||
editorMemo.add(line);
|
||||
editorMemo.add(line2);
|
||||
editorMemo.renderAll();
|
||||
};
|
||||
const verticleIndicatorLines = (newLeft, objLeft) => {
|
||||
editorMemo.getObjects("line").forEach((obj) => {
|
||||
if (obj.testLine) {
|
||||
editorMemo.remove(obj);
|
||||
}
|
||||
});
|
||||
|
||||
let line = new fabric.Line([newLeft, 0, newLeft, editorMemo.getHeight()], {
|
||||
stroke: "#AAAAAA",
|
||||
testLine: true
|
||||
// strokeDashArray: [ 5 ],
|
||||
});
|
||||
let line2 = new fabric.Line([objLeft, 0, objLeft, editorMemo.getWidth()], {
|
||||
stroke: "#AAAAAA",
|
||||
testLine: true
|
||||
// strokeDashArray: [ 5 ],
|
||||
});
|
||||
editorMemo.add(line);
|
||||
editorMemo.add(line2);
|
||||
editorMemo.renderAll();
|
||||
};
|
||||
|
||||
function edgeDetectionAndSnap(options) {
|
||||
if (this.isZoomed) {
|
||||
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
|
||||
|
||||
editorMemo.defaultCursor = "default";
|
||||
prevPointer = null;
|
||||
clearClone(editorMemo);
|
||||
}
|
||||
|
||||
const showHighlight = useCallback(
|
||||
(event) => {
|
||||
if (event.target) {
|
||||
if (event.target.type === "image") {
|
||||
setObjHovered(true);
|
||||
// console.log(event.target.dockData)
|
||||
if (
|
||||
event.target &&
|
||||
event.target.dockData &&
|
||||
event.target.dockData.image
|
||||
) {
|
||||
imageRef.current.src = event.target.dockData.image;
|
||||
setDockLabel(
|
||||
`${
|
||||
event.target.dockData.materials
|
||||
? capitalize(event.target.dockData.itemName)
|
||||
: event.target.dockData.itemName
|
||||
} (${
|
||||
event.target.dockData.materials
|
||||
? event.target.dockData.materials
|
||||
: event.target.dockData.model
|
||||
})`
|
||||
);
|
||||
setDockDimensions(
|
||||
`${event.target.dockData.width}' x ${event.target.dockData.length}'`
|
||||
);
|
||||
} else if (
|
||||
event.target &&
|
||||
event.target.dockData &&
|
||||
event.target.dockData.thumbnail
|
||||
) {
|
||||
setDockLabel(
|
||||
`${capitalize(event.target.dockData.itemName)} (${
|
||||
event.target.dockData.name
|
||||
})`
|
||||
);
|
||||
setDockDimensions(
|
||||
`${event.target.dockData.width}' x ${event.target.dockData.length}'`
|
||||
);
|
||||
imageRef.current.src = event.target.dockData.thumbnail;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!event.target && objHovered) {
|
||||
setObjHovered(false);
|
||||
setDockLabel(null);
|
||||
setDockDimensions(null);
|
||||
setDockDimensions(null);
|
||||
imageRef.current.src = "";
|
||||
}
|
||||
},
|
||||
[objHovered]
|
||||
);
|
||||
|
||||
const hideHighlight = useCallback(() => {
|
||||
if (objHovered) {
|
||||
setObjHovered(false);
|
||||
setDockLabel(null);
|
||||
setDockDimensions(null);
|
||||
imageRef.current.src = "";
|
||||
}
|
||||
}, [objHovered]);
|
||||
|
||||
const updateModifications = useCallback(
|
||||
(savehistory, event) => {
|
||||
if (savehistory === true) {
|
||||
if ((event && event === "update") || event.target.dockData) {
|
||||
const json = editor?.canvas?.toJSON(["dockData", "snapClone"]);
|
||||
const data = JSON.stringify(json);
|
||||
stack.updateStack(data);
|
||||
|
||||
updateLocalstore();
|
||||
}
|
||||
}
|
||||
},
|
||||
[selectedItems, editor]
|
||||
);
|
||||
|
||||
const updateLocalstore = useCallback(() => {
|
||||
const json = editor?.canvas?.toJSON(["dockData", "snapClone"]);
|
||||
const newObjects = json.objects
|
||||
.filter((object) => !object.snapClone)
|
||||
.filter(Boolean);
|
||||
json.objects = newObjects;
|
||||
const items = json.objects
|
||||
.map((object) => {
|
||||
if (object.dockData && !object.snapClone) {
|
||||
return object.dockData;
|
||||
}
|
||||
})
|
||||
.filter(Boolean);
|
||||
setSelectedItems(() => [...items]);
|
||||
|
||||
const data = JSON.stringify(json);
|
||||
localStorage.setItem("dock", data);
|
||||
}, [editor, selectedItems]);
|
||||
|
||||
const onUndoClick = useCallback(() => {
|
||||
// TODO: Undo
|
||||
}, [editorMemo]);
|
||||
|
||||
const onRedoClick = useCallback(() => {
|
||||
// TODO: Redo
|
||||
}, [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
|
||||
}, [editorMemo]);
|
||||
|
||||
const CopySelection = () => {
|
||||
// TODO: Copy selection
|
||||
};
|
||||
|
||||
const PasteSelection = () => {
|
||||
// TODO: Paste selection
|
||||
};
|
||||
|
||||
const onDeleteSelection = () => {
|
||||
// TODO: Delete selection
|
||||
};
|
||||
|
||||
const addLines = useCallback(() => {
|
||||
// Initiate a line instance
|
||||
const editorHeight = editorMemo.getHeight();
|
||||
const division = editorHeight / oneFeet;
|
||||
|
||||
if (!linesAdded) {
|
||||
let initialFt = 8;
|
||||
let multiplier = 1;
|
||||
for (let i = division - 3; i > 0; i--) {
|
||||
if (multiplier > division - 4) {
|
||||
break;
|
||||
}
|
||||
let line = new fabric.Line(
|
||||
[0, oneFeet * i, editorMemo.getWidth(), oneFeet * i],
|
||||
{
|
||||
stroke: "#AAAAAA",
|
||||
strokeDashArray: [5]
|
||||
}
|
||||
);
|
||||
|
||||
let text = new fabric.Text(`${initialFt * multiplier}ft`, {
|
||||
// stroke: '#000000',
|
||||
fill: "#AAAAAA",
|
||||
fontSize: 18,
|
||||
left: editorMemo.getWidth() - 40,
|
||||
top: oneFeet * i
|
||||
});
|
||||
|
||||
multiplier++;
|
||||
// Render the rectangle in canvas
|
||||
editorMemo.add(line).sendToBack(line);
|
||||
editorMemo.add(text).sendToBack(text);
|
||||
editorMemo.renderAll();
|
||||
}
|
||||
setLinesAdded(true);
|
||||
}
|
||||
}, [editorMemo, linesAdded]);
|
||||
|
||||
const removeLines = useCallback(() => {
|
||||
editorMemo.getObjects("line").forEach((line) => {
|
||||
editorMemo.remove(line);
|
||||
});
|
||||
editorMemo.getObjects("text").forEach((text) => {
|
||||
editorMemo.remove(text);
|
||||
});
|
||||
setLinesAdded(false);
|
||||
}, [editorMemo, linesAdded]);
|
||||
|
||||
const onCloseBuildFromLocalModal = useCallback(() => {
|
||||
setShowBuildCanvasFromLocalModal(false);
|
||||
|
||||
dispatch({
|
||||
type: "DOCK_LOADING",
|
||||
payload: false
|
||||
});
|
||||
setInitialLoad(false);
|
||||
}, [editorMemo, showBuildCanvasFromLocalModal, dispatch, initialLoad]);
|
||||
function renderIcon(icon) {
|
||||
return function renderIcon(ctx, left, top, styleOverride, fabricObject) {
|
||||
var size = this.cornerSize;
|
||||
ctx.save();
|
||||
ctx.translate(left, top);
|
||||
ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle));
|
||||
ctx.drawImage(icon, -size / 2, -size / 2, size, size);
|
||||
ctx.restore();
|
||||
};
|
||||
}
|
||||
function deleteObject(eventData, transform) {
|
||||
var target = transform.target;
|
||||
var canvas = target.canvas;
|
||||
canvas.remove(target);
|
||||
canvas.requestRenderAll();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (editor) {
|
||||
editorMemo.backgroundColor = "#011e23";
|
||||
editorMemo.enableRetinaScaling = false;
|
||||
|
||||
// Use hardware acceleration
|
||||
// editorMemo.renderOnAddRemove = false;
|
||||
editorMemo.skipOffscreen = true;
|
||||
// Lower render quality setting
|
||||
fabric.devicePixelRatio = 1;
|
||||
fabric.Group.prototype.hasControls = true;
|
||||
fabric.Group.prototype.snapAngle = 45;
|
||||
fabric.Group.prototype.snapThreshold = 5;
|
||||
fabric.Group.prototype.stroke = "#0f75bc";
|
||||
fabric.Object.prototype.snapAngle = 45;
|
||||
// Optimize object rendering
|
||||
fabric.Object.prototype.objectCaching = true;
|
||||
|
||||
fabric.Object.prototype.snapThreshold = 5;
|
||||
fabric.Object.prototype.setControlsVisibility({
|
||||
tl: false, //top-left
|
||||
mt: false, // middle-top
|
||||
tr: true, //top-right
|
||||
ml: false, //middle-left
|
||||
mr: false, //middle-right
|
||||
bl: false, // bottom-left
|
||||
mb: false, //middle-bottom
|
||||
br: false, //bottom-right
|
||||
mtr: false // rotate icon
|
||||
});
|
||||
// fabric.Object.prototype.controls.deleteControl = new fabric.Control( {
|
||||
// x: -0.8,
|
||||
// y: -1,
|
||||
// offsetY: 16,
|
||||
// offsetX: 16,
|
||||
// cursorStyle: 'pointer',
|
||||
// mouseUpHandler: deleteObject,
|
||||
// render: renderIcon( deleteImg ),
|
||||
// cornerPadding: 0,
|
||||
// cornerSize: 24
|
||||
// } );
|
||||
fabric.Object.prototype.controls.tr = new fabric.Control({
|
||||
x: 0.5,
|
||||
y: -0.5,
|
||||
cursorStyle: "rotate",
|
||||
offsetY: -16,
|
||||
offsetX: 16,
|
||||
cornerPadding: 0,
|
||||
actionHandler: fabric.controlsUtils.rotationWithSnapping,
|
||||
cursorStyleHandler: fabric.controlsUtils.rotationStyleHandler,
|
||||
withConnection: true,
|
||||
render: renderIcon(rotateImg),
|
||||
cornerSize: 20,
|
||||
actionName: "rotate",
|
||||
sizeX: 40,
|
||||
sizeY: 40,
|
||||
touchSizeX: 40,
|
||||
touchSizeY: 40
|
||||
});
|
||||
fabric.Object.prototype.hasControls = true;
|
||||
fabric.Object.prototype.lockScalingX = true;
|
||||
fabric.Object.prototype.lockScalingY = true;
|
||||
fabric.Object.prototype.cornerSize = 5;
|
||||
fabric.Line.prototype.hasBorders = false;
|
||||
fabric.Line.prototype.cornerSize = 0;
|
||||
fabric.Line.prototype.borderColor = "transparent";
|
||||
fabric.Line.prototype.hasRotatingPoint = false;
|
||||
fabric.Line.prototype.hasControls = false;
|
||||
fabric.Line.prototype.lockRotation = true;
|
||||
fabric.Line.prototype.hoverCursor = "default";
|
||||
fabric.Line.prototype.selectable = false;
|
||||
fabric.Line.prototype.lockMovementX = true;
|
||||
fabric.Line.prototype.lockMovementY = true;
|
||||
fabric.Text.prototype.hasBorders = false;
|
||||
fabric.Text.prototype.cornerSize = 0;
|
||||
fabric.Text.prototype.borderColor = "transparent";
|
||||
fabric.Text.prototype.hasRotatingPoint = false;
|
||||
fabric.Text.prototype.hoverCursor = "default";
|
||||
fabric.Text.prototype.selectable = false;
|
||||
fabric.Text.prototype.hasControls = false;
|
||||
fabric.Text.prototype.lockRotation = true;
|
||||
fabric.Text.prototype.lockMovementX = true;
|
||||
fabric.Text.prototype.lockMovementY = true;
|
||||
|
||||
// fabric.Text.prototype.viewportCenter = true
|
||||
setEditorReady(true);
|
||||
editorMemo.setHeight(window.innerHeight);
|
||||
editorMemo.setWidth(window.innerWidth);
|
||||
fabric.Object.prototype.cornerColor = "#B9C0D4";
|
||||
|
||||
renderBg();
|
||||
addLines();
|
||||
|
||||
// Intersection
|
||||
// TODO: Edge detection and snap to object within snap range
|
||||
editorMemo.on("object:moving", edgeDetectionAndSnap);
|
||||
|
||||
editorMemo.on("mouse:over", showHighlight);
|
||||
editorMemo.on("mouse:out", hideHighlight);
|
||||
editorMemo.on("object:modified", (e) => updateModifications(true, e));
|
||||
editorMemo.on("object:added", (e) => updateModifications(true, e));
|
||||
|
||||
// Listen for the delete key event
|
||||
document.onkeydown = (event) => {
|
||||
if (event.keyCode === 46) {
|
||||
// 46 is the keyCode for the delete key
|
||||
onDeleteSelection();
|
||||
}
|
||||
};
|
||||
|
||||
const savedDock = localStorage.getItem("dock")
|
||||
? JSON.parse(localStorage.getItem("dock"))
|
||||
: null;
|
||||
if (savedDock) {
|
||||
if (initialLoad) {
|
||||
setShowBuildCanvasFromLocalModal(true);
|
||||
}
|
||||
} else if (initialLoad) {
|
||||
setInitialLoad(false);
|
||||
dispatch({
|
||||
type: "DOCK_LOADING",
|
||||
payload: false
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [editor]);
|
||||
|
||||
useEffect(() => {
|
||||
if (editor) {
|
||||
const handleResize = () => {
|
||||
// let canvas = canvasRef.current;
|
||||
editorMemo.setWidth(window.innerWidth);
|
||||
editorMemo.setHeight(window.innerHeight);
|
||||
editorMemo.setBackgroundImage(editorMemo.backgroundImage, (bgImage) => {
|
||||
bgImage.set({
|
||||
scaleX: editorMemo.width / bgImage.width,
|
||||
scaleY: editorMemo.height / bgImage.height
|
||||
});
|
||||
});
|
||||
removeLines();
|
||||
addLines();
|
||||
editorMemo.renderAll();
|
||||
};
|
||||
window.addEventListener("resize", handleResize);
|
||||
handleResize();
|
||||
return () => {
|
||||
window.removeEventListener("resize", handleResize);
|
||||
};
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={`relative`}>
|
||||
<>
|
||||
<DockSidebar
|
||||
onDownloadImage={downloadImage}
|
||||
onDownloadFile={toJSON}
|
||||
onUploadFile={uploadFile}
|
||||
fileRef={fileRef}
|
||||
editor={editorMemo}
|
||||
updateModifications={updateModifications}
|
||||
selectedItems={selectedItems}
|
||||
onEstimateModalOpen={onEstimateModalOpen}
|
||||
/>
|
||||
|
||||
<FabricJSCanvas className={`floor-canvas`} onReady={onReady} />
|
||||
|
||||
<ActionButtons
|
||||
className={`absolute w-fit top-4 right-8`}
|
||||
onCopy={CopySelection}
|
||||
onPaste={PasteSelection}
|
||||
onRedoClick={onRedoClick}
|
||||
onUndoClick={onUndoClick}
|
||||
onPrintScreen={onPrintScreen}
|
||||
onDeleteSelection={onDeleteSelection}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={`rounded w-[10.875rem] absolute top-0 inset-x-0 m-auto ${
|
||||
objHovered ? "" : "hidden"
|
||||
}`}
|
||||
>
|
||||
{dockLabel ? (
|
||||
<div
|
||||
className={`w-full shadow text-white text-center font-medium text-[1.125rem] leading-[1.6875rem] tracking-[2%] font-['Rajdhani']`}
|
||||
>
|
||||
{dockLabel}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<img
|
||||
ref={imageRef}
|
||||
src=""
|
||||
alt=""
|
||||
width={174}
|
||||
height={116}
|
||||
className={`rounded relative ${objHovered ? "" : "hidden"}`}
|
||||
/>
|
||||
|
||||
{dockDimensions ? (
|
||||
<div
|
||||
className={`w-full shadow text-white text-center font-medium text-[1.125rem] leading-[1.6875rem] tracking-[2%] font-['Rajdhani']`}
|
||||
>
|
||||
{dockDimensions}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
|
||||
<EstimateModal
|
||||
loading={estimateLoading}
|
||||
modalCloseClick={onEstimateModalClose}
|
||||
showEstimateModal={showEstimateModal}
|
||||
dockImage={dockImage}
|
||||
selectedItems={selectedItems}
|
||||
/>
|
||||
<BuildCanvasFromLocalModal
|
||||
modalCloseClick={onCloseBuildFromLocalModal}
|
||||
showBuildCanvasFromLocalModal={showBuildCanvasFromLocalModal}
|
||||
editor={editorMemo}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { DockBuilder } from './DockBuilder'
|
||||
@@ -0,0 +1,73 @@
|
||||
import React, { useCallback, useContext } from 'react'
|
||||
import { SidebarLogo } from 'Components/SidebarLogo'
|
||||
import { GlobalContext } from '../../globalContext'
|
||||
import { SidebarFoot } from 'Components/SidebarFoot'
|
||||
import { SidebarBuilder } from 'Components/SidebarBuilder'
|
||||
|
||||
export const DockSidebar = ( { onAddItem, onDownloadImage, onDownloadFile, onUploadFile, fileRef, editor, updateModifications, selectedItems, onEstimateModalOpen } ) => {
|
||||
const { state, dispatch } = useContext( GlobalContext )
|
||||
const { dockSideBarOpen } = state
|
||||
let toggleOpen = ( open ) =>
|
||||
dispatch( {
|
||||
type: "TOGGLE_DOCK_SIDEBAR",
|
||||
payload: { dockSideBarOpen: open },
|
||||
} );
|
||||
|
||||
const onWheel = useCallback( ( e ) => {
|
||||
if ( e.ctrlKey ) {
|
||||
return e.preventDefault();//prevent zoom
|
||||
}
|
||||
|
||||
}, [] );
|
||||
|
||||
return (
|
||||
<div style={ { zIndex: 10000000 } } className={ `flex absolute` }>
|
||||
<div className={ `sidebar-holder flex flex-col bg-white max-h-screen ${ !dockSideBarOpen ? "open-nav" : "" }` } onWheel={ onWheel }>
|
||||
<SidebarLogo />
|
||||
|
||||
<SidebarBuilder
|
||||
onAddItem={ onAddItem }
|
||||
editor={ editor }
|
||||
updateModifications={ updateModifications }
|
||||
selectedItems={ selectedItems }
|
||||
/>
|
||||
<SidebarFoot onDownloadImage={ onDownloadImage } onDownloadFile={ onDownloadFile } onUploadFile={ onUploadFile } fileRef={ fileRef } onEstimateModalOpen={ onEstimateModalOpen } />
|
||||
</div>
|
||||
<span className='bg-white h-fit' onClick={ () => toggleOpen( !dockSideBarOpen ) }>
|
||||
{ !dockSideBarOpen ? (
|
||||
<svg
|
||||
className="block h-6 w-6"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M4 6h16M4 12h16M4 18h16"
|
||||
/>
|
||||
</svg>
|
||||
) : (
|
||||
<svg
|
||||
className="block h-6 w-6"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
) }
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { DockSidebar } from './DockSidebar'
|
||||
@@ -0,0 +1,464 @@
|
||||
import React from "react";
|
||||
import MkdSDK from "../utils/MkdSDK";
|
||||
import {empty} from "../utils/utils";
|
||||
|
||||
const sdk = new MkdSDK();
|
||||
const defaultImage = 'https://via.placeholder.com/150?text=%20';
|
||||
const DynamicContentType = ({contentType, contentValue, setContentValue}) => {
|
||||
const [previewUrl, setPreviewUrl] = React.useState(defaultImage);
|
||||
const handleImageChange = async (e) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', e.target.files[0]);
|
||||
try {
|
||||
const result = await sdk.uploadImage(formData);
|
||||
setPreviewUrl(result.url);
|
||||
setContentValue(result.url);
|
||||
}catch(err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
}
|
||||
switch (contentType) {
|
||||
case "text":
|
||||
return (
|
||||
<>
|
||||
<textarea className={`shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline`}
|
||||
rows={15}
|
||||
placeholder="Content"
|
||||
onChange={e => setContentValue(e.target.value)}
|
||||
defaultValue={contentValue}
|
||||
></textarea>
|
||||
</>
|
||||
)
|
||||
|
||||
case "image":
|
||||
return (
|
||||
<>
|
||||
<img src={empty(contentValue) ? previewUrl : contentValue } alt="preview" height={150} width={150} />
|
||||
<input
|
||||
type="file"
|
||||
onChange={handleImageChange}
|
||||
className={`shadow appearance-none border block rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline`}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
case "number":
|
||||
return (
|
||||
<input
|
||||
type="number"
|
||||
className={`shadow appearance-none border block rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline`}
|
||||
onChange={e => setContentValue(e.target.value)}
|
||||
defaultValue={contentValue}
|
||||
/>
|
||||
)
|
||||
|
||||
case "team-list":
|
||||
return (
|
||||
<TeamList setContentValue={setContentValue} contentValue={contentValue} />
|
||||
)
|
||||
|
||||
case "image-list":
|
||||
return (
|
||||
<ImageList setContentValue={setContentValue} contentValue={contentValue} />
|
||||
)
|
||||
|
||||
case "captioned-image-list":
|
||||
return (
|
||||
<CaptionedImageList setContentValue={setContentValue} contentValue={contentValue} />
|
||||
)
|
||||
|
||||
case "kvp":
|
||||
return (
|
||||
<KeyValuePair setContentValue={setContentValue} contentValue={contentValue} />
|
||||
)
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
export default DynamicContentType;
|
||||
|
||||
const ImageList = ({contentValue, setContentValue}) => {
|
||||
let itemsObj = [
|
||||
{key: '', value_type: 'image', value: null}
|
||||
];
|
||||
if (!empty(contentValue)) {
|
||||
itemsObj = JSON.parse(contentValue);
|
||||
}
|
||||
const [items, setItems] = React.useState(itemsObj);
|
||||
|
||||
|
||||
const handleImageChange = async (e) => {
|
||||
const listKey = e.target.getAttribute('listkey');
|
||||
const formData = new FormData();
|
||||
formData.append('file', e.target.files[0]);
|
||||
try {
|
||||
const result = await sdk.uploadImage(formData);
|
||||
setItems(oldItems => {
|
||||
let updatedItems = oldItems.map((item, index) => {
|
||||
if (index == listKey) {
|
||||
item.value = result.url;
|
||||
return item;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
return updatedItems;
|
||||
})
|
||||
setContentValue(JSON.stringify(items))
|
||||
}catch(err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeyChange = (e) => {
|
||||
const listKey = e.target.getAttribute('listkey');
|
||||
setItems(oldItems => {
|
||||
let updatedItems = oldItems.map((item, index) => {
|
||||
if (index == listKey) {
|
||||
item.key = e.target.value;
|
||||
return item;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
return updatedItems;
|
||||
})
|
||||
setContentValue(JSON.stringify(items))
|
||||
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="block">
|
||||
{items.map( (item, index) => <div key={index*0.23}>
|
||||
<img src={item.value !== null ? item.value : defaultImage} alt="preview" height={150} width={150} />
|
||||
<div className="flex">
|
||||
<input
|
||||
className={`shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline`}
|
||||
type="text" placeholder="key" listkey={index} onChange={handleKeyChange} defaultValue={item.key} />
|
||||
<input
|
||||
listkey={index}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleImageChange}
|
||||
className={`shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>)
|
||||
}
|
||||
<button
|
||||
type="button"
|
||||
className="bg-blue-400 hover:bg-blue-700 text-white font-bold py-1 px-2 my-4 rounded focus:outline-none focus:shadow-outline"
|
||||
onClick={ e => setItems(old => [...old, {key: '', value_type: 'image', value: null}])}>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
const CaptionedImageList = ({setContentValue, contentValue}) => {
|
||||
let itemsObj = [
|
||||
{key: '', value_type: 'image', value: null, caption: ''}
|
||||
]
|
||||
|
||||
if (!empty(contentValue)) {
|
||||
itemsObj = JSON.parse(contentValue);
|
||||
}
|
||||
const [items, setItems] = React.useState(itemsObj);
|
||||
|
||||
const handleImageChange = async (e) => {
|
||||
const listKey = e.target.getAttribute('listkey');
|
||||
const formData = new FormData();
|
||||
formData.append('file', e.target.files[0]);
|
||||
try {
|
||||
const result = await sdk.uploadImage(formData);
|
||||
setItems(oldItems => {
|
||||
let updatedItems = oldItems.map((item, index) => {
|
||||
if (index == listKey) {
|
||||
item.value = result.url;
|
||||
return item;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
return updatedItems;
|
||||
})
|
||||
setContentValue(JSON.stringify(items))
|
||||
}catch(err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeyChange = (e) => {
|
||||
const listKey = e.target.getAttribute('listkey');
|
||||
setItems(oldItems => {
|
||||
let updatedItems = oldItems.map((item, index) => {
|
||||
if (index == listKey) {
|
||||
item.key = e.target.value;
|
||||
return item;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
return updatedItems;
|
||||
})
|
||||
setContentValue(JSON.stringify(items))
|
||||
}
|
||||
|
||||
const handleCaptionChange = (e) => {
|
||||
const listKey = e.target.getAttribute('listkey');
|
||||
setItems(oldItems => {
|
||||
let updatedItems = oldItems.map((item, index) => {
|
||||
if (index == listKey) {
|
||||
item.caption = e.target.value;
|
||||
return item;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
return updatedItems;
|
||||
})
|
||||
setContentValue(JSON.stringify(items))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="block">
|
||||
{items.map( (item, index) => <div key={index*0.23} >
|
||||
<img src={item.value !== null ? item.value : defaultImage} alt="preview" height={150} width={150} />
|
||||
<div className="flex">
|
||||
<input
|
||||
className={`shadow appearance-none border rounded w-full py-2 px-3 mr-2 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline`}
|
||||
type="text" placeholder="Key" listkey={index} onChange={handleKeyChange} defaultValue={item.key}
|
||||
/>
|
||||
<input
|
||||
listkey={index}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleImageChange}
|
||||
className={`shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline`}
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
className={`shadow block appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline`}
|
||||
type="text"
|
||||
placeholder="Caption" listkey={index} onChange={handleCaptionChange} defaultValue={item.caption}
|
||||
/>
|
||||
|
||||
</div>)
|
||||
}
|
||||
<button
|
||||
type="button"
|
||||
className="bg-blue-400 hover:bg-blue-700 text-white font-bold py-1 px-2 my-4 rounded focus:outline-none focus:shadow-outline"
|
||||
onClick={ e => setItems(old => [...old, {key: '', value_type: 'image', value: null, caption: ''}])}>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
const TeamList = ({setContentValue, contentValue}) => {
|
||||
let itemsObj = [
|
||||
{name: '', image: null, title: ''}
|
||||
]
|
||||
|
||||
if (!empty(contentValue)) {
|
||||
itemsObj = JSON.parse(contentValue);
|
||||
}
|
||||
const [items, setItems] = React.useState(itemsObj);
|
||||
|
||||
const handleImageChange = async (e) => {
|
||||
const listKey = e.target.getAttribute('listkey');
|
||||
const formData = new FormData();
|
||||
formData.append('file', e.target.files[0]);
|
||||
try {
|
||||
const result = await sdk.uploadImage(formData);
|
||||
setItems(oldItems => {
|
||||
let updatedItems = oldItems.map((item, index) => {
|
||||
if (index == listKey) {
|
||||
item.image = result.url;
|
||||
return item;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
return updatedItems;
|
||||
})
|
||||
setContentValue(JSON.stringify(items))
|
||||
}catch(err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
const handleNameChange = (e) => {
|
||||
const listKey = e.target.getAttribute('listkey');
|
||||
setItems(oldItems => {
|
||||
let updatedItems = oldItems.map((item, index) => {
|
||||
if (index == listKey) {
|
||||
item.name = e.target.value;
|
||||
return item;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
return updatedItems;
|
||||
})
|
||||
setContentValue(JSON.stringify(items))
|
||||
}
|
||||
|
||||
const handleTitleChange = (e) => {
|
||||
const listKey = e.target.getAttribute('listkey');
|
||||
setItems(oldItems => {
|
||||
let updatedItems = oldItems.map((item, index) => {
|
||||
if (index == listKey) {
|
||||
item.title = e.target.value;
|
||||
return item;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
return updatedItems;
|
||||
})
|
||||
setContentValue(JSON.stringify(items))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="block my-4">
|
||||
{items.map( (item, index) => <div key={index*0.23} className="my-4" >
|
||||
|
||||
{/* <div className="flex"> */}
|
||||
<input
|
||||
className={`shadow block appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline`}
|
||||
type="text"
|
||||
placeholder="Title" listkey={index} onChange={handleTitleChange} defaultValue={item.title}
|
||||
/>
|
||||
<input
|
||||
className={`shadow appearance-none border rounded w-full py-2 px-3 mr-2 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline`}
|
||||
type="text" placeholder="Name" listkey={index} onChange={handleNameChange} defaultValue={item.name}
|
||||
/>
|
||||
<img src={item.image !== null ? item.image : defaultImage} alt="preview" height={150} width={150} />
|
||||
<input
|
||||
listkey={index}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleImageChange}
|
||||
className={`shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline`}
|
||||
/>
|
||||
{/* </div> */}
|
||||
|
||||
|
||||
</div>)
|
||||
}
|
||||
<button
|
||||
type="button"
|
||||
className="bg-blue-400 hover:bg-blue-700 text-white font-bold py-1 px-2 my-4 rounded focus:outline-none focus:shadow-outline"
|
||||
onClick={ e => setItems(old => [...old, {name: '', image: null, title: ''}])}>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
const KeyValuePair = ({setContentValue, contentValue}) => {
|
||||
let itemsObj = [
|
||||
{key: '', value_type: 'text', value: ''}
|
||||
]
|
||||
|
||||
if (!empty(contentValue)) {
|
||||
itemsObj = JSON.parse(contentValue);
|
||||
}
|
||||
|
||||
const [items, setItems] = React.useState(itemsObj);
|
||||
const valueTypeMap = [
|
||||
{
|
||||
key: "text",
|
||||
value: "Text"
|
||||
},
|
||||
{
|
||||
key: "number",
|
||||
value: "Number"
|
||||
},
|
||||
{
|
||||
key: "json",
|
||||
value: "JSON Object"
|
||||
},
|
||||
{
|
||||
key: "url",
|
||||
value: "URL"
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
const handleKeyChange = (e) => {
|
||||
const listKey = e.target.getAttribute('listkey');
|
||||
setItems(oldItems => {
|
||||
let updatedItems = oldItems.map((item, index) => {
|
||||
if (index == listKey) {
|
||||
item.key = e.target.value;
|
||||
return item;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
return updatedItems;
|
||||
})
|
||||
setContentValue(JSON.stringify(items))
|
||||
}
|
||||
|
||||
const handleValueChange = (e) => {
|
||||
const listKey = e.target.getAttribute('listkey');
|
||||
setItems(oldItems => {
|
||||
let updatedItems = oldItems.map((item, index) => {
|
||||
if (index == listKey) {
|
||||
item.value = e.target.value;
|
||||
return item;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
return updatedItems;
|
||||
})
|
||||
setContentValue(JSON.stringify(items))
|
||||
}
|
||||
|
||||
const handleValueTypeChange = (e) => {
|
||||
const listKey = e.target.getAttribute('listkey');
|
||||
setItems(oldItems => {
|
||||
let updatedItems = oldItems.map((item, index) => {
|
||||
if (index == listKey) {
|
||||
item.value_type = e.target.value;
|
||||
return item;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
return updatedItems;
|
||||
})
|
||||
setContentValue(JSON.stringify(items))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="block">
|
||||
{items.map( (item, index) => <div key={index*0.23} className="my-4" >
|
||||
<input
|
||||
className={`shadow appearance-none border rounded w-full py-2 px-3 mr-2 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline`}
|
||||
type="text" placeholder="Key" listkey={index} onChange={handleKeyChange} defaultValue={item.key}
|
||||
/>
|
||||
<select
|
||||
className={`shadow block border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline`}
|
||||
listkey={index} onChange={handleValueTypeChange} defaultValue={item.value_type}>
|
||||
{valueTypeMap.map( (type, index) => <option key={index*122} value={type.key}>{type.value}</option>)}
|
||||
</select>
|
||||
<input
|
||||
className={`shadow block appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline`}
|
||||
type="text"
|
||||
required
|
||||
placeholder="Value" listkey={index} onChange={handleValueChange} defaultValue={item.value}
|
||||
/>
|
||||
|
||||
|
||||
</div>)
|
||||
}
|
||||
<button
|
||||
type="button"
|
||||
className="bg-blue-400 hover:bg-blue-700 text-white font-bold py-1 px-2 my-4 rounded focus:outline-none focus:shadow-outline"
|
||||
onClick={ e => setItems(old => [...old, {key: '', value_type: 'text', value: ''}])}>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
import React, { memo, useState, useCallback } from 'react';
|
||||
import { Modal } from 'Components/Modal';
|
||||
import { InteractiveButton } from 'Components/InteractiveButton';
|
||||
import { ArrowLeftIcon, ArrowRrightIcon, GreenTickIcon } from 'Assets/svgs';
|
||||
import { dock_image, EstimateSteps, Tables, Truthy } from 'Utils/constants';
|
||||
import { ContactInformation } from 'Components/ContactInformation';
|
||||
import { LakeSurroundings } from 'Components/LakeSurroundings';
|
||||
import { Comments } from 'Components/Comments';
|
||||
import MkdSDK from 'Utils/MkdSDK';
|
||||
|
||||
const sdk = new MkdSDK()
|
||||
const classes = {
|
||||
modal: 'string',
|
||||
modalDialog: 'relative bg-white w-[500px]',
|
||||
modalContent: 'string',
|
||||
modalHeader: 'string',
|
||||
modalTitle: 'string',
|
||||
modalBody: 'string',
|
||||
modalFooter: 'string',
|
||||
closeButtonClass: 'string',
|
||||
saveButtonClass: 'string',
|
||||
}
|
||||
|
||||
const EstimateModal = ( {
|
||||
showEstimateModal,
|
||||
modalCloseClick,
|
||||
selectedItems,
|
||||
dockImage,
|
||||
} ) => {
|
||||
const [ step, setStep ] = useState( EstimateSteps.ContactInformation )
|
||||
const [ submitLoading, setSubmitLoading ] = useState( false )
|
||||
const [ errorMessage, setErrorMessage ] = useState( null )
|
||||
const [ hasDealer, setHasDealer ] = useState( Truthy.False )
|
||||
|
||||
// Contact Information
|
||||
const [ dealer, setDealer ] = useState( 0 )
|
||||
const [ firstName, setFirstName ] = useState( '' )
|
||||
const [ lastName, setLastName ] = useState( '' )
|
||||
const [ email, setEmail ] = useState( '' )
|
||||
const [ phone, setPhone ] = useState( '' )
|
||||
const [ lake, setLake ] = useState( '' )
|
||||
const [ city, setCity ] = useState( '' )
|
||||
const [ country, setCountry ] = useState( '' )
|
||||
|
||||
// Lake Surroundings
|
||||
const [ dockConnection, setDockConnection ] = useState( '' )
|
||||
const [ lakeBottom, setLakeBottom ] = useState( '' )
|
||||
const [ willDockBoat, setWillDockBoat ] = useState( Truthy.False )
|
||||
|
||||
// Comments
|
||||
const [ comments, setComments ] = useState( "" )
|
||||
|
||||
const onStepChange = useCallback( ( next ) => {
|
||||
setStep( next )
|
||||
}, [ step ] )
|
||||
|
||||
const onUpdateComment = useCallback( ( comment ) => {
|
||||
setComments( comment )
|
||||
}, [ comments ] )
|
||||
|
||||
const onSubmit = useCallback( () => {
|
||||
( async () => {
|
||||
try {
|
||||
setSubmitLoading( true )
|
||||
const quoteBody = {
|
||||
first_name: firstName,
|
||||
last_name: lastName,
|
||||
email: email,
|
||||
phone: phone,
|
||||
lake: lake,
|
||||
city: city,
|
||||
dock_connection: dockConnection,
|
||||
lake_bottom: lakeBottom,
|
||||
country: country,
|
||||
dock_image: dockImage,
|
||||
has_dealer: hasDealer,
|
||||
dealer_id: dealer,
|
||||
will_dock_boat: willDockBoat,
|
||||
comments: comments,
|
||||
selected_items: JSON.stringify( selectedItems )
|
||||
}
|
||||
const result = await sdk.requestQuote( quoteBody )
|
||||
if ( !result?.error ) {
|
||||
setSubmitLoading( false )
|
||||
setStep( EstimateSteps.InquirySubmitted )
|
||||
}
|
||||
} catch ( error ) {
|
||||
setErrorMessage( error.message )
|
||||
setSubmitLoading( false )
|
||||
}
|
||||
} )()
|
||||
}, [ step, submitLoading, errorMessage, comments, hasDealer, dealer, firstName, lastName, email, phone, lake, city, country, dockConnection, lakeBottom, willDockBoat, selectedItems, dockImage ] )
|
||||
|
||||
const onClose = useCallback( () => {
|
||||
setStep( EstimateSteps.ContactInformation )
|
||||
modalCloseClick()
|
||||
}, [ step ] )
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="REQUEST AN ESTIMATE"
|
||||
isOpen={ showEstimateModal }
|
||||
classes={ classes }
|
||||
modalHeader
|
||||
modalCloseClick={ modalCloseClick }
|
||||
>
|
||||
{/* ContactInformation */ }
|
||||
<ContactInformation
|
||||
step={ step }
|
||||
onStepChange={ onStepChange }
|
||||
hasDealer={ hasDealer }
|
||||
onHasDealerChange={ setHasDealer }
|
||||
onDealerChange={ setDealer }
|
||||
setFirstName={ setFirstName }
|
||||
setLastName={ setLastName }
|
||||
setEmail={ setEmail }
|
||||
setPhone={ setPhone }
|
||||
setLake={ setLake }
|
||||
setCity={ setCity }
|
||||
setCountry={ setCountry }
|
||||
/>
|
||||
|
||||
{/* LakeSurroundings */ }
|
||||
<LakeSurroundings
|
||||
step={ step }
|
||||
onStepChange={ onStepChange }
|
||||
setDockConnection={ setDockConnection }
|
||||
setLakeBottom={ setLakeBottom }
|
||||
setWillDockBoat={ setWillDockBoat }
|
||||
/>
|
||||
|
||||
{/* Comments */ }
|
||||
<Comments
|
||||
step={ step }
|
||||
onSubmit={ onSubmit }
|
||||
onUpdateComment={ onUpdateComment }
|
||||
onStepChange={ onStepChange }
|
||||
errorMessage={ errorMessage }
|
||||
submitLoading={ submitLoading }
|
||||
comments={ comments }
|
||||
/>
|
||||
|
||||
{/* InquirySubmitted */ }
|
||||
<div className={ `${ step === EstimateSteps.InquirySubmitted ? '' : 'hidden' }` }>
|
||||
|
||||
<div>
|
||||
<div className={ `flex items-center gap-x-2` }>
|
||||
<GreenTickIcon />
|
||||
<p className={ ` uppercase text-[#0F75BC] leading-[150%] tracking-wide text-[18px] font-bold` }>Inquiry Submitted</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className={ `flex w-full justify-center bg-transparent ` }>
|
||||
<button
|
||||
className={ `h-[44px] w-full rounded border-2 border-[#0F75BC] flex justify-center gap-x-2 items-center bg-[#0F75BC] text-[#ffffff] uppercase font-medium tracking-wide leading-[24px] text-[16px]` }
|
||||
onClick={ onClose }
|
||||
>
|
||||
close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const EstimateModalMemo = memo( EstimateModal );
|
||||
|
||||
export { EstimateModalMemo as EstimateModal };
|
||||
@@ -0,0 +1 @@
|
||||
export { EstimateModal } from './EstimateModal'
|
||||
@@ -0,0 +1,63 @@
|
||||
import React from "react";
|
||||
import { fabric } from "fabric";
|
||||
|
||||
function Rect({ left, top, width, height, fill }) {
|
||||
const rectRef = React.useRef(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
const rect = new fabric.Rect({
|
||||
left,
|
||||
top,
|
||||
width,
|
||||
height,
|
||||
fill,
|
||||
});
|
||||
|
||||
rectRef.current = rect;
|
||||
}, [left, top, width, height, fill]);
|
||||
|
||||
return <></>;
|
||||
}
|
||||
|
||||
export default Rect;
|
||||
|
||||
// const addTestRect = () => {
|
||||
// if (editor) {
|
||||
// const rect = new fabric.Rect({
|
||||
// left: 200,
|
||||
// top: 50,
|
||||
// width: 500,
|
||||
// height: 50,
|
||||
// fill: "red",
|
||||
// });
|
||||
|
||||
// editorMemo.add(rect);
|
||||
// const rect2 = new fabric.Rect({
|
||||
// left: 250,
|
||||
// top: 100,
|
||||
// width: 200,
|
||||
// height: 50,
|
||||
// fill: "red",
|
||||
// });
|
||||
|
||||
// editorMemo.add(rect2);
|
||||
// const rect3 = new fabric.Rect({
|
||||
// left: 300,
|
||||
// top: 150,
|
||||
// width: 200,
|
||||
// height: 50,
|
||||
// fill: "red",
|
||||
// });
|
||||
|
||||
// editorMemo.add(rect3);
|
||||
// const rect4 = new fabric.Rect({
|
||||
// left: 350,
|
||||
// top: 200,
|
||||
// width: 200,
|
||||
// height: 50,
|
||||
// fill: "red",
|
||||
// });
|
||||
|
||||
// editorMemo.add(rect4);
|
||||
// }
|
||||
// }
|
||||
@@ -0,0 +1,48 @@
|
||||
import React from "react";
|
||||
|
||||
export const InteractiveButton = ({
|
||||
loading,
|
||||
children,
|
||||
type = "button",
|
||||
className,
|
||||
backgroundColor,
|
||||
onClick,
|
||||
}) => {
|
||||
return (
|
||||
<button
|
||||
style={{ backgroundColor: backgroundColor && backgroundColor }}
|
||||
onClick={onClick}
|
||||
type={type}
|
||||
disabled={loading}
|
||||
className={`flex justify-center items-center gap-5 bg-[#0F75BC] ${className}`}
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<svg
|
||||
className="animate-spin -ml-1 mr-3 h-5 w-5 text-white inline"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
className="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"
|
||||
></circle>
|
||||
<path
|
||||
className="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
></path>
|
||||
</svg>{" "}
|
||||
{children}
|
||||
</>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { InteractiveButton } from './InteractiveButton'
|
||||
@@ -0,0 +1,141 @@
|
||||
import React, { memo } from 'react';
|
||||
import { ArrowLeftIcon, ArrowRrightIcon } from 'Assets/svgs';
|
||||
import { EstimateSteps, Truthy } from 'Utils/constants';
|
||||
|
||||
const LakeSurroundings = ( { step, onStepChange, setWillDockBoat, setDockConnection, setLakeBottom } ) => {
|
||||
|
||||
const dockConnectionMap = [ 'Resting', 'Bolted', 'Other' ]
|
||||
const lakeBottomMap = [ 'Rocky', 'Silty', 'Sandy' ]
|
||||
// const dock_connection = { 0: 'Resting', 1: 'Bolted', 2: 'Other' }
|
||||
// const lake_bottom = { 0: 'Rocky', 1: 'Silty', 2: 'Sandy' }
|
||||
// const has_dealer = { 0: 'No', 1: 'Yes' }
|
||||
// const will_dock_boat = { 0: 'No', 1: 'Yes' }
|
||||
{/* LakeSurroundings */ }
|
||||
|
||||
return (
|
||||
|
||||
<div className={ `${ step === EstimateSteps.LakeSurrounding ? '' : 'hidden' }` }>
|
||||
|
||||
<div>
|
||||
<button
|
||||
className={ `text-[#4A5578] leading-[150%] tracking-tight text-[14px] font-medium flex items-center` }
|
||||
onClick={ () => onStepChange( EstimateSteps.ContactInformation ) }
|
||||
>
|
||||
<ArrowLeftIcon /> Back
|
||||
</button>
|
||||
<div className={ `flex items-center gap-x-2` }>
|
||||
<p className={ ` uppercase text-[#0F75BC] leading-[150%] tracking-wide text-[18px] font-bold` }>Lake Surroundings</p>
|
||||
<span className={ `text-[#4A5578] leading-[150%] tracking-tight text-[14px] font-medium` }>(2 of 3)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<fieldset className="cus-input">
|
||||
<label
|
||||
htmlFor="dockConnection"
|
||||
className="text-[#4A5578] leading-[20px] tracking-tight text-[14px] font-medium"
|
||||
>
|
||||
<span className={ `text-red-600 mr-2` }>*</span> How Will The Dock Be Connected to The Shore?
|
||||
</label>
|
||||
<div className="relative mb-6">
|
||||
<div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
|
||||
{/* <img
|
||||
className="w-5 h-5 object-contain"
|
||||
src={ `` }
|
||||
/> */}
|
||||
</div>
|
||||
<select
|
||||
id="dockConnection"
|
||||
className="bg-gray-50 border-2 border-[#D1D5DB] text-black text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-3 dark:bg-gray-200 dark:border-gray-600 dark:placeholder-gray-400 dark:focus:ring-blue-500 dark:focus:border-blue-500 outline-none"
|
||||
onChange={ ( e ) => setDockConnection( e.target.value ) }
|
||||
>
|
||||
<>
|
||||
<option></option>
|
||||
{ dockConnectionMap.map( ( dockConnection, index ) => (
|
||||
<option key={ index } value={ index }>{ dockConnection }</option>
|
||||
) ) }
|
||||
</>
|
||||
</select>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset className="cus-input">
|
||||
<label
|
||||
htmlFor="lakeBottom"
|
||||
className="text-[#4A5578] leading-[20px] tracking-tight text-[14px] font-medium"
|
||||
>
|
||||
<span className={ `text-red-600 mr-2 capitalize` }>*</span> What is the Lake Bottom Like?
|
||||
|
||||
</label>
|
||||
<div className="relative mb-6">
|
||||
<div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
|
||||
{/* <img
|
||||
className="w-5 h-5 object-contain"
|
||||
src={ `` }
|
||||
/> */}
|
||||
</div>
|
||||
<select
|
||||
id="lakeBottom"
|
||||
className="bg-gray-50 border-2 border-[#D1D5DB] text-black text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-3 dark:bg-gray-200 dark:border-gray-600 dark:placeholder-gray-400 dark:focus:ring-blue-500 dark:focus:border-blue-500 outline-none"
|
||||
onChange={ ( e ) => setLakeBottom( e.target.value ) }
|
||||
>
|
||||
<>
|
||||
<option></option>
|
||||
{ lakeBottomMap.map( ( lakeBottom, index ) => (
|
||||
<option key={ index } value={ index }>{ lakeBottom }</option>
|
||||
) ) }
|
||||
</>
|
||||
</select>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset className="cus-input">
|
||||
<label
|
||||
// htmlFor="willDockBoat"
|
||||
className="text-[#4A5578] leading-[20px] tracking-tight text-[14px] font-medium"
|
||||
>
|
||||
<span className={ `text-red-600 mr-2 capitalize` }>*</span>Will you be docking a boat to this system?
|
||||
</label>
|
||||
<div className="relative flex items-center justify-start mb-6">
|
||||
<input
|
||||
type="radio"
|
||||
name="willDockBoat"
|
||||
id="yesDockBoat"
|
||||
className="bg-gray-50 border-2 border-[#D1D5DB] text-black text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-[44px] p-3 dark:bg-gray-200 dark:border-gray-600 dark:placeholder-gray-400 dark:focus:ring-blue-500 dark:focus:border-blue-500 outline-none"
|
||||
onChange={ () => setWillDockBoat( Truthy.True ) }
|
||||
/>
|
||||
<label
|
||||
htmlFor="yesDockBoat"
|
||||
className="text-[#4A5578] leading-[20px] tracking-tight text-[14px] font-medium"
|
||||
>Yes
|
||||
</label>
|
||||
<input
|
||||
type="radio"
|
||||
name="willDockBoat"
|
||||
id="noDockBoat"
|
||||
className="bg-gray-50 border-2 border-[#D1D5DB] text-black text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-[44px] pl-10 p-3 dark:bg-gray-200 dark:border-gray-600 dark:placeholder-gray-400 dark:focus:ring-blue-500 dark:focus:border-blue-500 outline-none"
|
||||
onChange={ () => setWillDockBoat( Truthy.False ) }
|
||||
/>
|
||||
<label
|
||||
htmlFor="noDockBoat"
|
||||
className="text-[#4A5578] leading-[20px] tracking-tight text-[14px] font-medium"
|
||||
>No
|
||||
</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
|
||||
</div>
|
||||
<div className={ `flex w-full justify-center bg-transparent ` }>
|
||||
<button
|
||||
className={ `h-[44px] w-full rounded border-2 border-[#0F75BC] flex justify-center gap-x-2 items-center bg-[#0F75BC] text-[#ffffff] uppercase font-medium tracking-wide leading-[24px] text-[16px]` }
|
||||
onClick={ () => onStepChange( EstimateSteps.Comments ) }
|
||||
>
|
||||
Continue <ArrowRrightIcon />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const LakeSurroundingsMemo = memo( LakeSurroundings );
|
||||
|
||||
export { LakeSurroundingsMemo as LakeSurroundings };
|
||||
@@ -0,0 +1 @@
|
||||
export { LakeSurroundings } from './LakeSurroundings'
|
||||
@@ -0,0 +1,22 @@
|
||||
|
||||
import Indicator from "./LoadingIndicator";
|
||||
|
||||
const Loader = ({ style }) => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
width: "100vw",
|
||||
height: "100vh",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
...style
|
||||
}}
|
||||
>
|
||||
<Indicator />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Loader;
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
|
||||
|
||||
import React from "react";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
const containerVariant = {
|
||||
start: {
|
||||
transition: { staggerChildren: 0.2 }
|
||||
},
|
||||
end: {
|
||||
transition: { staggerChildren: 0.2 }
|
||||
}
|
||||
};
|
||||
|
||||
const dotsVariants = {
|
||||
start: {
|
||||
y: "0%"
|
||||
},
|
||||
end: {
|
||||
y: "100%"
|
||||
}
|
||||
};
|
||||
|
||||
const loadingTransition = {
|
||||
duration: 0.4,
|
||||
yoyo: Infinity,
|
||||
ease: "easeIn"
|
||||
};
|
||||
|
||||
export default function Indicator({ dotsClasses, size, style }) {
|
||||
const dotsStyles = "block w-[9px] h-[9px] bg-slate-900 rounded-md shrink-0 " + dotsClasses;
|
||||
return (
|
||||
<motion.div
|
||||
variants={containerVariant}
|
||||
className={`flex justify-between items-center w-[40px] pb-[10px]`}
|
||||
initial="start"
|
||||
animate="end"
|
||||
style={{ ...style }}
|
||||
>
|
||||
<motion.span
|
||||
className={dotsStyles}
|
||||
variants={dotsVariants}
|
||||
transition={loadingTransition}
|
||||
/>
|
||||
<motion.span
|
||||
className={dotsStyles}
|
||||
variants={dotsVariants}
|
||||
transition={loadingTransition}
|
||||
/>
|
||||
<motion.span
|
||||
className={dotsStyles}
|
||||
variants={dotsVariants}
|
||||
transition={loadingTransition}
|
||||
/>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import React, { useEffect, useRef, useState, memo } from 'react';
|
||||
import { CloseIcon } from 'Assets/svgs';
|
||||
|
||||
const Modal = ( {
|
||||
children,
|
||||
title,
|
||||
isOpen = false,
|
||||
modalCloseClick,
|
||||
modalHeader,
|
||||
classes
|
||||
} ) => {
|
||||
|
||||
|
||||
return (
|
||||
<div
|
||||
style={ { zIndex: 100000002 } }
|
||||
className={ `modal-holder bg-[#00000099] items-center justify-center ${ isOpen ? 'flex' : 'hidden' }` }>
|
||||
<div className={ `shadow p-4 bg-white rounded-lg ${ classes?.modalDialog }` }>
|
||||
{ modalHeader &&
|
||||
<div className={ `flex justify-between border-b pb-2` }>
|
||||
<h5 className="font-bold text-center text-lg">{ title }</h5>
|
||||
<div className="modal-close" onClick={ modalCloseClick }>
|
||||
<CloseIcon />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div className="mt-4">
|
||||
{ children }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
const ModalMemo = memo( Modal );
|
||||
export { ModalMemo as Modal };
|
||||
@@ -0,0 +1 @@
|
||||
export { Modal } from './Modal'
|
||||
@@ -0,0 +1,41 @@
|
||||
import React from "react";
|
||||
const PaginationBar = ({ currentPage, pageCount, pageSize, canPreviousPage, canNextPage, updatePageSize, previousPage, nextPage }) => {
|
||||
return (
|
||||
<>
|
||||
<div className="flex justify-between ">
|
||||
<div className="mt-2">
|
||||
<span>
|
||||
Page{" "}
|
||||
<strong>
|
||||
{+currentPage} of {pageCount}
|
||||
</strong>{" "}
|
||||
</span>
|
||||
<select
|
||||
className="mt-2"
|
||||
value={pageSize}
|
||||
onChange={(e) => {
|
||||
updatePageSize(Number(e.target.value));
|
||||
}}
|
||||
>
|
||||
{[5, 10, 20, 30, 40, 50].map((pageSize) => (
|
||||
<option key={pageSize} value={pageSize}>
|
||||
Show {pageSize}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
{/* */}
|
||||
<div>
|
||||
<button onClick={previousPage} disabled={!canPreviousPage} className={`font-bold h-10 w-10 `}>
|
||||
←
|
||||
</button>{" "}
|
||||
<button onClick={nextPage} disabled={!canNextPage} className={`font-bold h-10 w-10 `}>
|
||||
→
|
||||
</button>{" "}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PaginationBar;
|
||||
@@ -0,0 +1,15 @@
|
||||
import React, { useState } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { AuthContext } from "../authContext";
|
||||
|
||||
export const PublicHeader = () => {
|
||||
const { state, dispatch } = React.useContext(AuthContext);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
return <div>
|
||||
|
||||
</div>;
|
||||
};
|
||||
|
||||
export default PublicHeader;
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from 'react'
|
||||
import { capitalize } from 'Utils/helper'
|
||||
|
||||
export const Field = ( { item, property } ) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{ item[ property ] ? (
|
||||
<div className={ `flex items-center justify-between ${ property === "name" ? "text-sm" : "" }` }>
|
||||
<div>{ capitalize( property ) }:</div>
|
||||
<div>{ item[ property ] }{ " " }{ [ "length", "width" ].includes( property ) ? 'ft' : '' }</div>
|
||||
</div> )
|
||||
|
||||
: null
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { Chevron } from 'Assets/svgs'
|
||||
import React from 'react'
|
||||
import { Field } from './Field'
|
||||
|
||||
export const SelectedItem = ( { selectedItem } ) => {
|
||||
const itemRef = React.useRef( null )
|
||||
|
||||
const [ open, setOpen ] = React.useState( false )
|
||||
|
||||
const onOpen = React.useCallback( ( isTrue ) => {
|
||||
if ( isTrue ) {
|
||||
itemRef.current.style.maxHeight = null
|
||||
} else {
|
||||
itemRef.current.style.maxHeight = `${ itemRef.current.scrollHeight }px`
|
||||
}
|
||||
setOpen( !isTrue )
|
||||
}, [ open, itemRef ] )
|
||||
|
||||
return (
|
||||
<div className={ ` mb-2` }>
|
||||
|
||||
<button onClick={ () => onOpen( open ) } className={ `flex items-center text-black max-w-full min-w-full mb-2 text-base border-b-2 border-b-state-200 py-2` }>
|
||||
|
||||
<div className={ `w-5` }>
|
||||
<Chevron active={ open } />
|
||||
</div>
|
||||
|
||||
<div className={ `truncate w-full text-left` }>
|
||||
{ selectedItem.itemName } { `(${ selectedItem?.width }' x ${ selectedItem?.length }')` }
|
||||
</div>
|
||||
|
||||
</button>
|
||||
|
||||
<div ref={ itemRef } className={ `max-h-0 overflow-hidden shadow-lg text-black` }>
|
||||
<Field item={ selectedItem } property={ `name` } />
|
||||
<Field item={ selectedItem } property={ `length` } />
|
||||
<Field item={ selectedItem } property={ `width` } />
|
||||
<Field item={ selectedItem } property={ `lift_range` } />
|
||||
<Field item={ selectedItem } property={ `model` } />
|
||||
<Field item={ selectedItem } property={ `no_of_cylinders` } />
|
||||
<Field item={ selectedItem } property={ `weight_capacity` } />
|
||||
<Field item={ selectedItem } property={ `category` } />
|
||||
<Field item={ selectedItem } property={ `materials` } />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import React from 'react'
|
||||
import { SelectedItem } from './SelectedItem'
|
||||
|
||||
export const SelectedItems = ( { selectedItems } ) => {
|
||||
|
||||
return (
|
||||
<div className={ `gap-x-[8px] border-t-2 w-full px-3` }>
|
||||
{ selectedItems?.length ? selectedItems?.map( ( item, index ) => (
|
||||
<SelectedItem key={ index } selectedItem={ item } />
|
||||
) )
|
||||
|
||||
: null }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export { SelectedItems } from './SelectedItems'
|
||||
export { SelectedItem } from './SelectedItem'
|
||||
export { Field } from './Field'
|
||||
@@ -0,0 +1,51 @@
|
||||
import React, { useCallback, useState } from "react";
|
||||
import { Tabs } from "Components/Tabs";
|
||||
import { Builder } from "Components/Builder";
|
||||
import { SelectedItems } from "Components/SelectedItems";
|
||||
import { TabNames } from "Utils/constants";
|
||||
|
||||
export const SidebarBuilder = ({
|
||||
editor,
|
||||
updateModifications,
|
||||
selectedItems,
|
||||
}) => {
|
||||
const [activeTab, setActiveTab] = useState(TabNames.Builder);
|
||||
// const [ramps, setRamps] = useState([]);
|
||||
|
||||
const onTabClick = useCallback(
|
||||
(tab) => {
|
||||
setActiveTab(tab);
|
||||
},
|
||||
[activeTab]
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex flex-col max-h-[80%] grow relative overflow-auto gap-x-[8px] border-2 w-full`}
|
||||
>
|
||||
<Tabs
|
||||
className={`flex gap-x-0 justify-center items-center min-h-[50px] max-h-[50px] h-[50px] w-full`}
|
||||
activeTab={activeTab}
|
||||
onTabClick={onTabClick}
|
||||
/>
|
||||
|
||||
{activeTab === TabNames.Builder ? (
|
||||
<Builder
|
||||
editor={editor}
|
||||
updateModifications={updateModifications}
|
||||
// ramps={ramps}
|
||||
// setRamps={setRamps}
|
||||
/>
|
||||
) : null}
|
||||
{activeTab === TabNames.SelectedItems ? (
|
||||
<SelectedItems selectedItems={selectedItems} />
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
{
|
||||
/* <div className={ `grow flex flex-col justify-center items-start bg-transparent` }>
|
||||
|
||||
</div> */
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { SidebarBuilder } from './SidebarBuilder'
|
||||
@@ -0,0 +1,38 @@
|
||||
import React from 'react'
|
||||
import { DownloadIcon, UploadIcon } from 'Assets/svgs'
|
||||
|
||||
export const SidebarFoot = ( { onDownloadImage, onDownloadFile, fileRef, onUploadFile, onEstimateModalOpen } ) => {
|
||||
|
||||
return (
|
||||
<div className={ `flex flex-col justify-end items-end m-auto h-fit z-50 w-full py-2 px-[20px] bg-white` }>
|
||||
<button
|
||||
onClick={ () => fileRef.current.click() }
|
||||
className={ `text-[#000000] flex justify-start items-center gap-x-5 w-full h-[45px] rounded` }
|
||||
>
|
||||
<UploadIcon /> Upload File
|
||||
<input ref={ fileRef } type="file" hidden onChangeCapture={ ( e ) => onUploadFile( e ) } accept={ `.dock` } />
|
||||
</button>
|
||||
|
||||
<button
|
||||
className={ `text-[#000000] flex justify-start items-center gap-x-5 w-full h-[45px] rounded` }
|
||||
onClick={ onDownloadFile }
|
||||
>
|
||||
<DownloadIcon /> Download File
|
||||
</button>
|
||||
|
||||
<button
|
||||
className={ `text-[#000000] flex justify-start items-center gap-x-5 w-full h-[45px] rounded` }
|
||||
onClick={ onDownloadImage }
|
||||
>
|
||||
<DownloadIcon /> Download Image
|
||||
</button>
|
||||
|
||||
<button
|
||||
className={ `text-white flex justify-center items-center w-full h-[35px] rounded bg-[#0F75BC]` }
|
||||
onClick={ onEstimateModalOpen }
|
||||
>
|
||||
GET ESTIMATE
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { SidebarFoot } from './SidebarFoot'
|
||||
@@ -0,0 +1,15 @@
|
||||
import { BrandLogo } from 'Assets/images'
|
||||
import React from 'react'
|
||||
|
||||
export const SidebarLogo = () =>
|
||||
{
|
||||
return (
|
||||
<div className={ `flex h-[96px] px-[12px] py-[20px] gap-x-[8px]` }>
|
||||
<img src={ BrandLogo } alt="Brand Logo" width={ 43.87 } height={ 56 } className={ `relative h-[56px] w-[43.87px] rounded-[8px]` } />
|
||||
<div className={ `grow flex flex-col justify-center items-start bg-transparent` }>
|
||||
<div className={ `w-full text-[#005C9B] font-bold text-[14px] leading-[150%] tracking-[-0.02em]` }>Paradise Dock & Lift Inc.</div>
|
||||
<div className={ `w-full uppercase text-[#30374F] font-bold text-[14px] leading-[150%] tracking-[-0.02em]` }>Dock builder tool</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { SidebarLogo } from './SidebarLogo'
|
||||
@@ -0,0 +1,32 @@
|
||||
import React from "react";
|
||||
import { GlobalContext } from "../globalContext";
|
||||
const SnackBar = () => {
|
||||
const { state, dispatch } = React.useContext(GlobalContext);
|
||||
const show = state.globalMessage.length > 0;
|
||||
return show ? (
|
||||
<div id="mkd-toast" className="absolute top-5 right-5 flex items-center w-full max-w-xs p-4 text-gray-500 bg-slate-100 rounded-lg shadow dark:text-gray-400" role="alert">
|
||||
<div className="text-sm font-normal">{state.globalMessage}</div>
|
||||
<div className="flex items-center ml-auto space-x-2">
|
||||
<button
|
||||
type="button"
|
||||
className="bg-white text-gray-400 hover:text-gray-900 rounded-lg focus:ring-2 focus:ring-gray-300 p-1.5 hover:bg-gray-100 inline-flex h-8 w-8 dark:text-gray-500 dark:hover:text-white dark:hover:bg-gray-700"
|
||||
aria-label="Close"
|
||||
onClick={() => {
|
||||
dispatch({ type: "SNACKBAR", payload: { message: "" } });
|
||||
}}
|
||||
>
|
||||
<span className="sr-only">Close</span>
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export default SnackBar;
|
||||
@@ -0,0 +1,10 @@
|
||||
|
||||
import React from 'react'
|
||||
|
||||
export const Tab = ( { name, className, active, onTabClick } ) => {
|
||||
return (
|
||||
<button className={ `${ className } ${ active ? "border-b-2 border-b-[#0F75BC]" : "" }` } onClick={ () => onTabClick( name ) }>
|
||||
{ name }
|
||||
</button>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { Tab } from './Tab'
|
||||
@@ -0,0 +1,95 @@
|
||||
import React from 'react'
|
||||
|
||||
export const Table = ( { className, tableColumns, tableData } ) => {
|
||||
return (
|
||||
<div className={ `shadow overflow-x-auto border-b border-gray-200 ${ className }` }>
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
{ tableColumns.map( ( column, i ) => (
|
||||
<th
|
||||
key={ i }
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
onClick={ () => onSort( i ) }
|
||||
>
|
||||
{ column.header }
|
||||
<span>
|
||||
{ column.isSorted
|
||||
? column.isSortedDesc
|
||||
? " ▼"
|
||||
: " ▲"
|
||||
: "" }
|
||||
</span>
|
||||
</th>
|
||||
) ) }
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{ tableData.map( ( row, i ) => {
|
||||
return (
|
||||
<tr key={ i }>
|
||||
{ tableColumns.map( ( cell, index ) => {
|
||||
if ( cell.accessor == "" ) {
|
||||
return (
|
||||
<td
|
||||
key={ index }
|
||||
className="px-6 py-4 whitespace-nowrap"
|
||||
>
|
||||
{/* <button
|
||||
className="text-xs"
|
||||
onClick={ () => {
|
||||
navigate( "/admin/edit-quotes/" + row.id, {
|
||||
state: row,
|
||||
} );
|
||||
} }
|
||||
>
|
||||
{ " " }
|
||||
Edit
|
||||
</button> */}
|
||||
<button
|
||||
className="text-xs px-1 text-blue-500"
|
||||
onClick={ () => {
|
||||
navigate( "/admin/view-quotes/" + row.id, {
|
||||
state: row,
|
||||
} );
|
||||
} }
|
||||
>
|
||||
{ " " }
|
||||
View
|
||||
</button>
|
||||
<button
|
||||
className="text-xs px-1 text-red-500"
|
||||
onClick={ () => deleteItem( row.id ) }
|
||||
>
|
||||
{ " " }
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
if ( cell.mappingExist ) {
|
||||
return (
|
||||
<td
|
||||
key={ index }
|
||||
className="px-6 py-4 whitespace-nowrap"
|
||||
>
|
||||
{ cell.mappings[ row[ cell.accessor ] ] }
|
||||
</td>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<td key={ index } className="px-6 py-4 whitespace-nowrap">
|
||||
{ [ "image", "top_view", "thumbnail" ].includes( cell.accessor ) ? <img className={ `rounded-md` } src={ row[ cell.accessor ] } /> : row[ cell.accessor ] }
|
||||
</td>
|
||||
);
|
||||
} ) }
|
||||
</tr>
|
||||
);
|
||||
} ) }
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { Table } from './Table'
|
||||
@@ -0,0 +1,25 @@
|
||||
import React from 'react'
|
||||
import { Tab } from 'Components/Tab'
|
||||
import { TabNames } from 'Utils/constants'
|
||||
|
||||
|
||||
|
||||
export const Tabs = ( { activeTab, className, onTabClick } ) => {
|
||||
return (
|
||||
<div className={ `${ className }` }>
|
||||
<Tab
|
||||
name={ TabNames.Builder }
|
||||
className={ `w-1/2 text-[#111322] h-full text-[14px] leading-[150%] font-medium tracking-tight` }
|
||||
active={ activeTab === TabNames.Builder ? true : false }
|
||||
onTabClick={ onTabClick }
|
||||
/>
|
||||
|
||||
<Tab
|
||||
name={ TabNames.SelectedItems }
|
||||
className={ `w-1/2 text-[#111322] h-full text-[14px] leading-[150%] font-medium tracking-tight` }
|
||||
active={ activeTab === TabNames.SelectedItems ? true : false }
|
||||
onTabClick={ onTabClick }
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { Tabs } from './Tabs'
|
||||
@@ -0,0 +1,52 @@
|
||||
import React from "react";
|
||||
import { GlobalContext } from "../globalContext";
|
||||
const TopHeader = () => {
|
||||
const { state, dispatch } = React.useContext(GlobalContext);
|
||||
let { isOpen } = state;
|
||||
let toggleOpen = (open) =>
|
||||
dispatch({
|
||||
type: "OPEN_SIDEBAR",
|
||||
payload: { isOpen: open },
|
||||
});
|
||||
return (
|
||||
<div className="page-header shadow">
|
||||
<span onClick={() => toggleOpen(!isOpen)}>
|
||||
{!isOpen ? (
|
||||
<svg
|
||||
className="block h-6 w-6"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M4 6h16M4 12h16M4 18h16"
|
||||
/>
|
||||
</svg>
|
||||
) : (
|
||||
<svg
|
||||
className="block h-6 w-6"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TopHeader;
|
||||
@@ -0,0 +1,77 @@
|
||||
import React, { memo, useState, useCallback } from 'react';
|
||||
import { Modal } from 'Components/Modal';
|
||||
import { InteractiveButton } from 'Components/InteractiveButton';
|
||||
// import { ArrowLeftIcon, ArrowRrightIcon, GreenTickIcon } from 'Assets/svgs';
|
||||
// import { dock_image, EstimateSteps, Tables, Truthy } from 'Utils/constants';
|
||||
// import { ContactInformation } from 'Components/ContactInformation';
|
||||
// import { LakeSurroundings } from 'Components/LakeSurroundings';
|
||||
// import { Comments } from 'Components/Comments';
|
||||
import MkdSDK from 'Utils/MkdSDK';
|
||||
|
||||
const sdk = new MkdSDK()
|
||||
const classes = {
|
||||
modal: 'string',
|
||||
modalDialog: 'relative bg-white w-[800px]',
|
||||
modalContent: 'string',
|
||||
modalHeader: 'string',
|
||||
modalTitle: 'string',
|
||||
modalBody: 'string',
|
||||
modalFooter: 'string',
|
||||
closeButtonClass: 'string',
|
||||
saveButtonClass: 'string',
|
||||
}
|
||||
|
||||
const ViewDockImageModal = ( {
|
||||
showViewDockImageModal,
|
||||
modalCloseClick,
|
||||
dockImage
|
||||
} ) => {
|
||||
// const [ step, setStep ] = useState( EstimateSteps.ContactInformation )
|
||||
// const [ submitLoading, setSubmitLoading ] = useState( false )
|
||||
// const [ errorMessage, setErrorMessage ] = useState( null )
|
||||
// const [ hasDealer, setHasDealer ] = useState( Truthy.False )
|
||||
|
||||
|
||||
const onDownloadClick = useCallback( () => {
|
||||
const Anchor = document.createElement( "a" )
|
||||
// console.log( "viewDockImageModal", dockImage )
|
||||
Anchor.href = dockImage;
|
||||
Anchor.download = `dock_image_snapshot.png`;
|
||||
Anchor.click();
|
||||
// modalCloseClick()
|
||||
}, [ dockImage ] )
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="DOCK IMAGE"
|
||||
isOpen={ showViewDockImageModal }
|
||||
classes={ classes }
|
||||
modalHeader
|
||||
modalCloseClick={ modalCloseClick }
|
||||
>
|
||||
<div>
|
||||
<div className={ `w-full h-[400px] rounded-xl my-3` }>
|
||||
<img src={ dockImage } className={ `w-full h-full rounded-xl` } alt="dock_image" />
|
||||
</div>
|
||||
|
||||
<div className={ `w-full flex gap-x-2 justify-between items-center` }>
|
||||
<InteractiveButton
|
||||
className={ `h-[44px] w-full rounded border-2 border-[#0F75BC] flex justify-center gap-x-2 items-center hover:scale-95 hover:border-[#084c7c] text-[#ffffff] uppercase font-medium tracking-wide leading-[24px] text-[16px]` }
|
||||
backgroundColor={ `#0F75BC` }
|
||||
onClick={ onDownloadClick }
|
||||
>
|
||||
Download
|
||||
</InteractiveButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const ViewDockImageModalMemo = memo( ViewDockImageModal );
|
||||
|
||||
export { ViewDockImageModalMemo as ViewDockImageModal };
|
||||
@@ -0,0 +1 @@
|
||||
export { ViewDockImageModal } from './ViewDockImageModal'
|
||||
@@ -0,0 +1,6 @@
|
||||
export { default as AddButton } from './AddButton'
|
||||
export { default as AdminHeader } from './AdminHeader'
|
||||
export { default as PaginationBar } from './PaginationBar'
|
||||
export { default as PublicHeader } from './PublicHeader'
|
||||
export { default as SnackBar } from './SnackBar'
|
||||
export { default as TopHeader } from './TopHeader'
|
||||
Reference in New Issue
Block a user