updated real time vis
This commit is contained in:
parent
4549a5cae4
commit
7950b58ba8
|
@ -11,6 +11,7 @@
|
||||||
"chart.js": "^4.4.8",
|
"chart.js": "^4.4.8",
|
||||||
"path": "^0.12.7",
|
"path": "^0.12.7",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
|
"react-chartjs-2": "^5.3.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-router-dom": "^7.3.0",
|
"react-router-dom": "^7.3.0",
|
||||||
"zustand": "^5.0.3"
|
"zustand": "^5.0.3"
|
||||||
|
@ -3247,6 +3248,16 @@
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-chartjs-2": {
|
||||||
|
"version": "5.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.0.tgz",
|
||||||
|
"integrity": "sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"chart.js": "^4.1.1",
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-dom": {
|
"node_modules/react-dom": {
|
||||||
"version": "19.0.0",
|
"version": "19.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz",
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
"chart.js": "^4.4.8",
|
"chart.js": "^4.4.8",
|
||||||
"path": "^0.12.7",
|
"path": "^0.12.7",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
|
"react-chartjs-2": "^5.3.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-router-dom": "^7.3.0",
|
"react-router-dom": "^7.3.0",
|
||||||
"zustand": "^5.0.3"
|
"zustand": "^5.0.3"
|
||||||
|
|
|
@ -5,8 +5,8 @@ import { useSelectedZoneStore } from "../../../../store/useZoneStore";
|
||||||
const Templates = () => {
|
const Templates = () => {
|
||||||
const { templates, removeTemplate } = useTemplateStore();
|
const { templates, removeTemplate } = useTemplateStore();
|
||||||
const { setSelectedZone } = useSelectedZoneStore();
|
const { setSelectedZone } = useSelectedZoneStore();
|
||||||
|
|
||||||
console.log('templates: ', templates);
|
console.log("templates: ", templates);
|
||||||
const handleDeleteTemplate = (id: string) => {
|
const handleDeleteTemplate = (id: string) => {
|
||||||
removeTemplate(id);
|
removeTemplate(id);
|
||||||
};
|
};
|
||||||
|
@ -15,40 +15,49 @@ const Templates = () => {
|
||||||
setSelectedZone((prev) => ({
|
setSelectedZone((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
panelOrder: template.panelOrder,
|
panelOrder: template.panelOrder,
|
||||||
activeSides: Array.from(new Set([...prev.activeSides, ...template.panelOrder])),
|
activeSides: Array.from(
|
||||||
|
new Set([...prev.activeSides, ...template.panelOrder])
|
||||||
|
),
|
||||||
widgets: template.widgets,
|
widgets: template.widgets,
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="template-list" style={{
|
<div
|
||||||
display: 'grid',
|
className="template-list"
|
||||||
gridTemplateColumns: 'repeat(auto-fill, minmax(180px, 1fr))',
|
style={{
|
||||||
gap: '1rem',
|
display: "grid",
|
||||||
padding: '1rem'
|
gridTemplateColumns: "repeat(auto-fill, minmax(180px, 1fr))",
|
||||||
}}>
|
gap: "1rem",
|
||||||
|
padding: "1rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
{templates.map((template) => (
|
{templates.map((template) => (
|
||||||
<div key={template.id} className="template-item" style={{
|
<div
|
||||||
border: '1px solid #e0e0e0',
|
key={template.id}
|
||||||
borderRadius: '8px',
|
className="template-item"
|
||||||
padding: '1rem',
|
style={{
|
||||||
transition: 'box-shadow 0.3s ease',
|
border: "1px solid #e0e0e0",
|
||||||
|
borderRadius: "8px",
|
||||||
|
padding: "1rem",
|
||||||
}}>
|
transition: "box-shadow 0.3s ease",
|
||||||
|
}}
|
||||||
|
>
|
||||||
{template.snapshot && (
|
{template.snapshot && (
|
||||||
<div style={{ position: 'relative', paddingBottom: '56.25%' }}> {/* 16:9 aspect ratio */}
|
<div style={{ position: "relative", paddingBottom: "56.25%" }}>
|
||||||
|
{" "}
|
||||||
|
{/* 16:9 aspect ratio */}
|
||||||
<img
|
<img
|
||||||
src={template.snapshot} // Corrected from template.image to template.snapshot
|
src={template.snapshot} // Corrected from template.image to template.snapshot
|
||||||
alt={`${template.name} preview`}
|
alt={`${template.name} preview`}
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: "absolute",
|
||||||
width: '100%',
|
width: "100%",
|
||||||
height: '100%',
|
height: "100%",
|
||||||
objectFit: 'contain',
|
objectFit: "contain",
|
||||||
borderRadius: '4px',
|
borderRadius: "4px",
|
||||||
cursor: 'pointer',
|
cursor: "pointer",
|
||||||
transition: 'transform 0.3s ease',
|
transition: "transform 0.3s ease",
|
||||||
// ':hover': {
|
// ':hover': {
|
||||||
// transform: 'scale(1.05)'
|
// transform: 'scale(1.05)'
|
||||||
// }
|
// }
|
||||||
|
@ -57,17 +66,19 @@ const Templates = () => {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div style={{
|
<div
|
||||||
display: 'flex',
|
style={{
|
||||||
justifyContent: 'space-between',
|
display: "flex",
|
||||||
alignItems: 'center',
|
justifyContent: "space-between",
|
||||||
marginTop: '0.5rem'
|
alignItems: "center",
|
||||||
}}>
|
marginTop: "0.5rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
onClick={() => handleLoadTemplate(template)}
|
onClick={() => handleLoadTemplate(template)}
|
||||||
style={{
|
style={{
|
||||||
cursor: 'pointer',
|
cursor: "pointer",
|
||||||
fontWeight: '500',
|
fontWeight: "500",
|
||||||
// ':hover': {
|
// ':hover': {
|
||||||
// textDecoration: 'underline'
|
// textDecoration: 'underline'
|
||||||
// }
|
// }
|
||||||
|
@ -75,16 +86,16 @@ const Templates = () => {
|
||||||
>
|
>
|
||||||
{template.name}
|
{template.name}
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleDeleteTemplate(template.id)}
|
onClick={() => handleDeleteTemplate(template.id)}
|
||||||
style={{
|
style={{
|
||||||
padding: '0.25rem 0.5rem',
|
padding: "0.25rem 0.5rem",
|
||||||
background: '#ff4444',
|
background: "#ff4444",
|
||||||
color: 'white',
|
color: "white",
|
||||||
border: 'none',
|
border: "none",
|
||||||
borderRadius: '4px',
|
borderRadius: "4px",
|
||||||
cursor: 'pointer',
|
cursor: "pointer",
|
||||||
transition: 'opacity 0.3s ease',
|
transition: "opacity 0.3s ease",
|
||||||
// ':hover': {
|
// ':hover': {
|
||||||
// opacity: 0.8
|
// opacity: 0.8
|
||||||
// }
|
// }
|
||||||
|
@ -97,12 +108,14 @@ const Templates = () => {
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{templates.length === 0 && (
|
{templates.length === 0 && (
|
||||||
<div style={{
|
<div
|
||||||
textAlign: 'center',
|
style={{
|
||||||
color: '#666',
|
textAlign: "center",
|
||||||
padding: '2rem',
|
color: "#666",
|
||||||
gridColumn: '1 / -1'
|
padding: "2rem",
|
||||||
}}>
|
gridColumn: "1 / -1",
|
||||||
|
}}
|
||||||
|
>
|
||||||
No saved templates yet. Create one in the visualization view!
|
No saved templates yet. Create one in the visualization view!
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -111,4 +124,3 @@ const Templates = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Templates;
|
export default Templates;
|
||||||
|
|
||||||
|
|
|
@ -2,10 +2,10 @@ import { useState } from "react";
|
||||||
import ToggleHeader from "../../../../ui/inputs/ToggleHeader";
|
import ToggleHeader from "../../../../ui/inputs/ToggleHeader";
|
||||||
import Widgets2D from "./Widgets2D";
|
import Widgets2D from "./Widgets2D";
|
||||||
import Widgets3D from "./Widgets3D";
|
import Widgets3D from "./Widgets3D";
|
||||||
import WidgetsTemplate from "./WidgetsTemplate";
|
import WidgetsFloating from "./WidgetsFloating";
|
||||||
|
|
||||||
const Widgets = () => {
|
const Widgets = () => {
|
||||||
const [activeOption, setActiveOption] = useState("2D");
|
const [activeOption, setActiveOption] = useState("Floating");
|
||||||
|
|
||||||
const handleToggleClick = (option: string) => {
|
const handleToggleClick = (option: string) => {
|
||||||
setActiveOption(option);
|
setActiveOption(option);
|
||||||
|
@ -14,13 +14,13 @@ const Widgets = () => {
|
||||||
return (
|
return (
|
||||||
<div className="widget-left-sideBar">
|
<div className="widget-left-sideBar">
|
||||||
<ToggleHeader
|
<ToggleHeader
|
||||||
options={["2D", "3D", "Templates"]}
|
options={["2D", "3D", "Floating"]}
|
||||||
activeOption={activeOption}
|
activeOption={activeOption}
|
||||||
handleClick={handleToggleClick}
|
handleClick={handleToggleClick}
|
||||||
/>
|
/>
|
||||||
{activeOption === "2D" && <Widgets2D />}
|
{activeOption === "2D" && <Widgets2D />}
|
||||||
{activeOption === "3D" && <Widgets3D />}
|
{activeOption === "3D" && <Widgets3D />}
|
||||||
{activeOption === "Templates" && <WidgetsTemplate />}
|
{activeOption === "Floating" && <WidgetsFloating />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,191 @@
|
||||||
|
import React, {
|
||||||
|
useState,
|
||||||
|
DragEvent,
|
||||||
|
MouseEvent,
|
||||||
|
useRef,
|
||||||
|
useEffect,
|
||||||
|
} from "react";
|
||||||
|
|
||||||
|
const WidgetsFloating: React.FC = () => {
|
||||||
|
const stateWorking = [
|
||||||
|
{ "Oil Tank": "24/341" },
|
||||||
|
{ "Oil Refin": "36.023" },
|
||||||
|
{ Transmission: "36.023" },
|
||||||
|
{ Fuel: "36732" },
|
||||||
|
{ Power: "1300" },
|
||||||
|
{ Time: "13-9-2023" },
|
||||||
|
];
|
||||||
|
|
||||||
|
// State for storing the dragged widget and its position
|
||||||
|
const [draggedFloating, setDraggedFloating] = useState<string | null>(null);
|
||||||
|
const [position, setPosition] = useState<{ x: number; y: number }>({
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
// State to store all placed widgets with their positions
|
||||||
|
const [placedWidgets, setPlacedWidgets] = useState<
|
||||||
|
{ name: string; x: number; y: number }[]
|
||||||
|
>([]);
|
||||||
|
|
||||||
|
const canvasRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// Handle the drag start event
|
||||||
|
const handleDragStart = (
|
||||||
|
event: DragEvent<HTMLDivElement>,
|
||||||
|
widget: string
|
||||||
|
) => {
|
||||||
|
setDraggedFloating(widget);
|
||||||
|
// Initialize position to the current position when drag starts
|
||||||
|
setPosition({ x: event.clientX, y: event.clientY });
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle the drag move event
|
||||||
|
const handleDragMove = (event: MouseEvent) => {
|
||||||
|
if (!draggedFloating) return;
|
||||||
|
|
||||||
|
// Calculate new position and update it
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
if (canvas) {
|
||||||
|
const canvasRect = canvas.getBoundingClientRect();
|
||||||
|
const newX = event.clientX - canvasRect.left;
|
||||||
|
const newY = event.clientY - canvasRect.top;
|
||||||
|
setPosition({ x: newX, y: newY });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle the drag end event
|
||||||
|
const handleDragEnd = () => {
|
||||||
|
if (draggedFloating) {
|
||||||
|
// Store the final position of the dragged widget
|
||||||
|
setPlacedWidgets((prevWidgets) => [
|
||||||
|
...prevWidgets,
|
||||||
|
{ name: draggedFloating, x: position.x, y: position.y },
|
||||||
|
]);
|
||||||
|
// Reset the dragged floating widget after dragging is completed
|
||||||
|
setDraggedFloating(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log("position: ", position);
|
||||||
|
console.log("draggedFloating: ", draggedFloating);
|
||||||
|
}, [draggedFloating, position]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
id="real-time-vis-canvas"
|
||||||
|
ref={canvasRef}
|
||||||
|
className="canvas"
|
||||||
|
style={{
|
||||||
|
position: "relative",
|
||||||
|
width: "100%",
|
||||||
|
height: "400px",
|
||||||
|
backgroundColor: "#f0f0f0",
|
||||||
|
border: "1px solid #ccc",
|
||||||
|
}}
|
||||||
|
onMouseMove={handleDragMove}
|
||||||
|
onMouseUp={handleDragEnd}
|
||||||
|
>
|
||||||
|
{/* The floating widget that's being dragged */}
|
||||||
|
{draggedFloating && (
|
||||||
|
<div
|
||||||
|
className="floating"
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: `${position.x}px`,
|
||||||
|
top: `${position.y}px`,
|
||||||
|
cursor: "move",
|
||||||
|
backgroundColor: "lightblue",
|
||||||
|
padding: "10px",
|
||||||
|
borderRadius: "5px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{draggedFloating}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Render all placed widgets */}
|
||||||
|
{placedWidgets.map((widget, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="floating"
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: `${widget.x}px`,
|
||||||
|
top: `${widget.y}px`,
|
||||||
|
backgroundColor: "lightgreen",
|
||||||
|
padding: "10px",
|
||||||
|
borderRadius: "5px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{widget.name}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* The rest of your floating widgets */}
|
||||||
|
<div
|
||||||
|
className="floating working-state"
|
||||||
|
draggable
|
||||||
|
onDragStart={(e) => handleDragStart(e, "working-state")}
|
||||||
|
style={{ position: "absolute", top: "50px", left: "50px" }}
|
||||||
|
>
|
||||||
|
<div className="state-working-top">
|
||||||
|
<div className="state-working-main">
|
||||||
|
<div className="state">State</div>
|
||||||
|
<div className="working-status">
|
||||||
|
<span className="working">Working</span>
|
||||||
|
<span className="dot"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="img">
|
||||||
|
<img
|
||||||
|
src="https://source.unsplash.com/random/150x100/?factory"
|
||||||
|
alt="Factory"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="state-working-data">
|
||||||
|
{stateWorking.map((state, index) => {
|
||||||
|
const key = Object.keys(state)[0];
|
||||||
|
const value = state[key];
|
||||||
|
return (
|
||||||
|
<div className="data-row" key={index}>
|
||||||
|
<span className="data-key">{key}:</span>
|
||||||
|
<span className="data-value">{value}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Other floating widgets */}
|
||||||
|
<div
|
||||||
|
className="floating"
|
||||||
|
draggable
|
||||||
|
onDragStart={(e) => handleDragStart(e, "floating-2")}
|
||||||
|
style={{ position: "absolute", top: "120px", left: "150px" }}
|
||||||
|
>
|
||||||
|
floating-2
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="floating"
|
||||||
|
draggable
|
||||||
|
onDragStart={(e) => handleDragStart(e, "floating-3")}
|
||||||
|
style={{ position: "absolute", top: "200px", left: "250px" }}
|
||||||
|
>
|
||||||
|
floating-3
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="floating"
|
||||||
|
draggable
|
||||||
|
onDragStart={(e) => handleDragStart(e, "floating-4")}
|
||||||
|
style={{ position: "absolute", top: "300px", left: "350px" }}
|
||||||
|
>
|
||||||
|
floating-4
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WidgetsFloating;
|
|
@ -1,12 +0,0 @@
|
||||||
import React from 'react'
|
|
||||||
|
|
||||||
const WidgetsTemplate = () => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
WidgetsTemplate
|
|
||||||
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default WidgetsTemplate
|
|
|
@ -65,7 +65,7 @@ const SideBarRight: React.FC = () => {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* realtime visualization */}
|
{/* realtime visualization */}
|
||||||
{activeModule === "visualization" && <Visualization />}
|
{toggleUI && activeModule === "visualization" && <Visualization />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,13 +1,9 @@
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useWidgetStore } from "../../../../../store/useWidgetStore";
|
import { useWidgetStore } from "../../../../../store/useWidgetStore";
|
||||||
import { RemoveIcon } from "../../../../icons/ExportCommonIcons";
|
import { AddIcon, RemoveIcon } from "../../../../icons/ExportCommonIcons";
|
||||||
import RegularDropDown from "../../../../ui/inputs/RegularDropDown";
|
|
||||||
import MultiLevelDropDown from "../../../../ui/inputs/MultiLevelDropDown";
|
import MultiLevelDropDown from "../../../../ui/inputs/MultiLevelDropDown";
|
||||||
|
|
||||||
interface Child {
|
// Define the data structure for demonstration purposes
|
||||||
id: number;
|
|
||||||
easing: string;
|
|
||||||
}
|
|
||||||
const DATA_STRUCTURE = {
|
const DATA_STRUCTURE = {
|
||||||
furnace: {
|
furnace: {
|
||||||
coolingRate: "coolingRate",
|
coolingRate: "coolingRate",
|
||||||
|
@ -35,13 +31,22 @@ const DATA_STRUCTURE = {
|
||||||
data3: "Data 3",
|
data3: "Data 3",
|
||||||
},
|
},
|
||||||
timestamp: {
|
timestamp: {
|
||||||
data1: "Data 1",
|
data1: {
|
||||||
|
Data01: "Data 01",
|
||||||
|
Data02: "Data 02",
|
||||||
|
Data03: "Data 03",
|
||||||
|
},
|
||||||
data2: "Data 2",
|
data2: "Data 2",
|
||||||
data3: "Data 3",
|
data3: "Data 3",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface Child {
|
||||||
|
id: number;
|
||||||
|
easing: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface Group {
|
interface Group {
|
||||||
id: number;
|
id: number;
|
||||||
easing: string;
|
easing: string;
|
||||||
|
@ -65,11 +70,7 @@ const Data = () => {
|
||||||
{
|
{
|
||||||
id: Date.now(),
|
id: Date.now(),
|
||||||
easing: "Connecter 1",
|
easing: "Connecter 1",
|
||||||
children: [
|
children: [{ id: Date.now(), easing: "Linear" }],
|
||||||
{ id: Date.now(), easing: "Linear" },
|
|
||||||
{ id: Date.now() + 1, easing: "Ease Out" },
|
|
||||||
{ id: Date.now() + 2, easing: "Linear" },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}));
|
}));
|
||||||
|
@ -121,7 +122,43 @@ const Data = () => {
|
||||||
{selectedChartId?.title && (
|
{selectedChartId?.title && (
|
||||||
<div className="sideBarHeader">{selectedChartId?.title}</div>
|
<div className="sideBarHeader">{selectedChartId?.title}</div>
|
||||||
)}
|
)}
|
||||||
{/* <MultiLevelDropDown data={DATA_STRUCTURE} /> */}
|
{/* Render groups dynamically */}
|
||||||
|
{chartDataGroups[selectedChartId?.id]?.map((group) => (
|
||||||
|
<div key={group.id}>
|
||||||
|
{group.children.map((child, index) => (
|
||||||
|
<div key={child.id} className="datas">
|
||||||
|
<div className="datas__label">Input {index + 1}</div>
|
||||||
|
<div className="datas__class">
|
||||||
|
<MultiLevelDropDown data={DATA_STRUCTURE} />
|
||||||
|
{/* Add Icon */}
|
||||||
|
{group.children.length < 7 && (
|
||||||
|
<div
|
||||||
|
className="icon"
|
||||||
|
onClick={() => handleAddClick(group.id)} // Pass groupId to handleAddClick
|
||||||
|
>
|
||||||
|
<AddIcon />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{/* Remove Icon */}
|
||||||
|
|
||||||
|
<span
|
||||||
|
className={`datas__separator ${
|
||||||
|
group.children.length > 1 ? "" : "disable"
|
||||||
|
}`}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation(); // Prevent event bubbling
|
||||||
|
removeChild(group.id, child.id); // Pass groupId and childId to removeChild
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RemoveIcon />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Info Box */}
|
||||||
<div className="infoBox">
|
<div className="infoBox">
|
||||||
<span className="infoIcon">i</span>
|
<span className="infoIcon">i</span>
|
||||||
<p>
|
<p>
|
||||||
|
|
|
@ -19,6 +19,7 @@ import useTemplateStore from "../../store/useTemplateStore";
|
||||||
import { useSelectedZoneStore } from "../../store/useZoneStore";
|
import { useSelectedZoneStore } from "../../store/useZoneStore";
|
||||||
|
|
||||||
const Tools: React.FC = () => {
|
const Tools: React.FC = () => {
|
||||||
|
const { templates } = useTemplateStore();
|
||||||
const [activeTool, setActiveTool] = useState("cursor");
|
const [activeTool, setActiveTool] = useState("cursor");
|
||||||
const [activeSubTool, setActiveSubTool] = useState("cursor");
|
const [activeSubTool, setActiveSubTool] = useState("cursor");
|
||||||
const [toggleThreeD, setToggleThreeD] = useState(true);
|
const [toggleThreeD, setToggleThreeD] = useState(true);
|
||||||
|
@ -196,7 +197,13 @@ const Tools: React.FC = () => {
|
||||||
<div className="draw-tools">
|
<div className="draw-tools">
|
||||||
<div
|
<div
|
||||||
className={`tool-button`}
|
className={`tool-button`}
|
||||||
onClick={() => handleSaveTemplate({ addTemplate, selectedZone })}
|
onClick={() =>
|
||||||
|
handleSaveTemplate({
|
||||||
|
addTemplate,
|
||||||
|
selectedZone,
|
||||||
|
templates,
|
||||||
|
})
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<SaveTemplateIcon isActive={false} />
|
<SaveTemplateIcon isActive={false} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,94 @@
|
||||||
|
import { useRef, useMemo } from "react";
|
||||||
|
|
||||||
|
import { Bar, Line } from "react-chartjs-2";
|
||||||
|
|
||||||
|
interface ChartComponentProps {
|
||||||
|
type: any;
|
||||||
|
title: string;
|
||||||
|
fontFamily?: string;
|
||||||
|
fontSize?: string;
|
||||||
|
fontWeight?: "Light" | "Regular" | "Bold";
|
||||||
|
data: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LineGraphComponent = ({
|
||||||
|
title,
|
||||||
|
fontFamily,
|
||||||
|
fontSize,
|
||||||
|
fontWeight = "Regular",
|
||||||
|
}: ChartComponentProps) => {
|
||||||
|
// Memoize Font Weight Mapping
|
||||||
|
const chartFontWeightMap = useMemo(
|
||||||
|
() => ({
|
||||||
|
Light: "lighter" as const,
|
||||||
|
Regular: "normal" as const,
|
||||||
|
Bold: "bold" as const,
|
||||||
|
}),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Parse and Memoize Font Size
|
||||||
|
const fontSizeValue = useMemo(
|
||||||
|
() => (fontSize ? parseInt(fontSize) : 12),
|
||||||
|
[fontSize]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Determine and Memoize Font Weight
|
||||||
|
const fontWeightValue = useMemo(
|
||||||
|
() => chartFontWeightMap[fontWeight],
|
||||||
|
[fontWeight, chartFontWeightMap]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Memoize Chart Font Style
|
||||||
|
const chartFontStyle = useMemo(
|
||||||
|
() => ({
|
||||||
|
family: fontFamily || "Arial",
|
||||||
|
size: fontSizeValue,
|
||||||
|
weight: fontWeightValue,
|
||||||
|
}),
|
||||||
|
[fontFamily, fontSizeValue, fontWeightValue]
|
||||||
|
);
|
||||||
|
|
||||||
|
const options = useMemo(
|
||||||
|
() => ({
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: title,
|
||||||
|
font: chartFontStyle,
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
ticks: {
|
||||||
|
display: false, // This hides the x-axis labels
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[title, chartFontStyle]
|
||||||
|
);
|
||||||
|
|
||||||
|
const chartData = {
|
||||||
|
labels: ["January", "February", "March", "April", "May", "June", "July"],
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: "My First Dataset",
|
||||||
|
data: [65, 59, 80, 81, 56, 55, 40],
|
||||||
|
backgroundColor: "#6f42c1",
|
||||||
|
borderColor: "#ffffff",
|
||||||
|
borderWidth: 2,
|
||||||
|
fill: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
return <Bar data={chartData} options={options} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LineGraphComponent;
|
|
@ -0,0 +1,93 @@
|
||||||
|
import { useRef, useMemo } from "react";
|
||||||
|
import { Line } from "react-chartjs-2";
|
||||||
|
|
||||||
|
interface ChartComponentProps {
|
||||||
|
type: any;
|
||||||
|
title: string;
|
||||||
|
fontFamily?: string;
|
||||||
|
fontSize?: string;
|
||||||
|
fontWeight?: "Light" | "Regular" | "Bold";
|
||||||
|
data: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LineGraphComponent = ({
|
||||||
|
title,
|
||||||
|
fontFamily,
|
||||||
|
fontSize,
|
||||||
|
fontWeight = "Regular",
|
||||||
|
}: ChartComponentProps) => {
|
||||||
|
// Memoize Font Weight Mapping
|
||||||
|
const chartFontWeightMap = useMemo(
|
||||||
|
() => ({
|
||||||
|
Light: "lighter" as const,
|
||||||
|
Regular: "normal" as const,
|
||||||
|
Bold: "bold" as const,
|
||||||
|
}),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Parse and Memoize Font Size
|
||||||
|
const fontSizeValue = useMemo(
|
||||||
|
() => (fontSize ? parseInt(fontSize) : 12),
|
||||||
|
[fontSize]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Determine and Memoize Font Weight
|
||||||
|
const fontWeightValue = useMemo(
|
||||||
|
() => chartFontWeightMap[fontWeight],
|
||||||
|
[fontWeight, chartFontWeightMap]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Memoize Chart Font Style
|
||||||
|
const chartFontStyle = useMemo(
|
||||||
|
() => ({
|
||||||
|
family: fontFamily || "Arial",
|
||||||
|
size: fontSizeValue,
|
||||||
|
weight: fontWeightValue,
|
||||||
|
}),
|
||||||
|
[fontFamily, fontSizeValue, fontWeightValue]
|
||||||
|
);
|
||||||
|
|
||||||
|
const options = useMemo(
|
||||||
|
() => ({
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: title,
|
||||||
|
font: chartFontStyle,
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
ticks: {
|
||||||
|
display: false, // This hides the x-axis labels
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[title, chartFontStyle]
|
||||||
|
);
|
||||||
|
|
||||||
|
const chartData = {
|
||||||
|
labels: ["January", "February", "March", "April", "May", "June", "July"],
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: "My First Dataset",
|
||||||
|
data: [65, 59, 80, 81, 56, 55, 40],
|
||||||
|
backgroundColor: "#6f42c1", // Updated to #6f42c1 (Purple)
|
||||||
|
borderColor: "#ffffff", // Keeping border color white
|
||||||
|
borderWidth: 2,
|
||||||
|
fill: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
return <Line data={chartData} options={options} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LineGraphComponent;
|
|
@ -0,0 +1,90 @@
|
||||||
|
import { useRef, useMemo } from "react";
|
||||||
|
import { Pie } from "react-chartjs-2";
|
||||||
|
|
||||||
|
interface ChartComponentProps {
|
||||||
|
type: any;
|
||||||
|
title: string;
|
||||||
|
fontFamily?: string;
|
||||||
|
fontSize?: string;
|
||||||
|
fontWeight?: "Light" | "Regular" | "Bold";
|
||||||
|
data: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PieChartComponent = ({
|
||||||
|
title,
|
||||||
|
fontFamily,
|
||||||
|
fontSize,
|
||||||
|
fontWeight = "Regular",
|
||||||
|
}: ChartComponentProps) => {
|
||||||
|
// Memoize Font Weight Mapping
|
||||||
|
const chartFontWeightMap = useMemo(
|
||||||
|
() => ({
|
||||||
|
Light: "lighter" as const,
|
||||||
|
Regular: "normal" as const,
|
||||||
|
Bold: "bold" as const,
|
||||||
|
}),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Parse and Memoize Font Size
|
||||||
|
const fontSizeValue = useMemo(
|
||||||
|
() => (fontSize ? parseInt(fontSize) : 12),
|
||||||
|
[fontSize]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Determine and Memoize Font Weight
|
||||||
|
const fontWeightValue = useMemo(
|
||||||
|
() => chartFontWeightMap[fontWeight],
|
||||||
|
[fontWeight, chartFontWeightMap]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Memoize Chart Font Style
|
||||||
|
const chartFontStyle = useMemo(
|
||||||
|
() => ({
|
||||||
|
family: fontFamily || "Arial",
|
||||||
|
size: fontSizeValue,
|
||||||
|
weight: fontWeightValue,
|
||||||
|
}),
|
||||||
|
[fontFamily, fontSizeValue, fontWeightValue]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Access the CSS variable for the primary accent color
|
||||||
|
const accentColor = getComputedStyle(document.documentElement)
|
||||||
|
.getPropertyValue("--accent-color")
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
const options = useMemo(
|
||||||
|
() => ({
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: title,
|
||||||
|
font: chartFontStyle,
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[title, chartFontStyle]
|
||||||
|
);
|
||||||
|
|
||||||
|
const chartData = {
|
||||||
|
labels: ["Red", "Blue", "Yellow", "Green", "Purple", "Orange"],
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: "Dataset",
|
||||||
|
data: [12, 19, 3, 5, 2, 3],
|
||||||
|
backgroundColor: ["#6f42c1"],
|
||||||
|
borderColor: "#ffffff",
|
||||||
|
borderWidth: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
return <Pie data={chartData} options={options} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PieChartComponent;
|
|
@ -11,7 +11,7 @@ type Side = "top" | "bottom" | "left" | "right";
|
||||||
// Define the type for the props passed to the Buttons component
|
// Define the type for the props passed to the Buttons component
|
||||||
interface ButtonsProps {
|
interface ButtonsProps {
|
||||||
selectedZone: {
|
selectedZone: {
|
||||||
zoneName: string; // Add zoneName property
|
zoneName: string;
|
||||||
activeSides: Side[];
|
activeSides: Side[];
|
||||||
panelOrder: Side[];
|
panelOrder: Side[];
|
||||||
lockedPanels: Side[];
|
lockedPanels: Side[];
|
||||||
|
@ -25,7 +25,7 @@ interface ButtonsProps {
|
||||||
};
|
};
|
||||||
setSelectedZone: React.Dispatch<
|
setSelectedZone: React.Dispatch<
|
||||||
React.SetStateAction<{
|
React.SetStateAction<{
|
||||||
zoneName: string; // Ensure zoneName is included in the state type
|
zoneName: string;
|
||||||
activeSides: Side[];
|
activeSides: Side[];
|
||||||
panelOrder: Side[];
|
panelOrder: Side[];
|
||||||
lockedPanels: Side[];
|
lockedPanels: Side[];
|
||||||
|
@ -38,12 +38,18 @@ interface ButtonsProps {
|
||||||
}[];
|
}[];
|
||||||
}>
|
}>
|
||||||
>;
|
>;
|
||||||
|
hiddenPanels: Side[]; // Add this prop for hidden panels
|
||||||
|
setHiddenPanels: React.Dispatch<React.SetStateAction<Side[]>>; // Add this prop for updating hidden panels
|
||||||
}
|
}
|
||||||
|
|
||||||
const AddButtons: React.FC<ButtonsProps> = ({
|
const AddButtons: React.FC<ButtonsProps> = ({
|
||||||
selectedZone,
|
selectedZone,
|
||||||
setSelectedZone,
|
setSelectedZone,
|
||||||
|
setHiddenPanels,
|
||||||
|
hiddenPanels,
|
||||||
}) => {
|
}) => {
|
||||||
|
// Local state to track hidden panels
|
||||||
|
|
||||||
// Function to toggle lock/unlock a panel
|
// Function to toggle lock/unlock a panel
|
||||||
const toggleLockPanel = (side: Side) => {
|
const toggleLockPanel = (side: Side) => {
|
||||||
const newLockedPanels = selectedZone.lockedPanels.includes(side)
|
const newLockedPanels = selectedZone.lockedPanels.includes(side)
|
||||||
|
@ -61,18 +67,14 @@ const AddButtons: React.FC<ButtonsProps> = ({
|
||||||
|
|
||||||
// Function to toggle visibility of a panel
|
// Function to toggle visibility of a panel
|
||||||
const toggleVisibility = (side: Side) => {
|
const toggleVisibility = (side: Side) => {
|
||||||
const newActiveSides = selectedZone.activeSides.includes(side)
|
const isHidden = hiddenPanels.includes(side);
|
||||||
? selectedZone.activeSides.filter((s) => s !== side)
|
if (isHidden) {
|
||||||
: [...selectedZone.activeSides, side];
|
// If the panel is already hidden, remove it from the hiddenPanels array
|
||||||
|
setHiddenPanels(hiddenPanels.filter((panel) => panel !== side));
|
||||||
const updatedZone = {
|
} else {
|
||||||
...selectedZone,
|
// If the panel is visible, add it to the hiddenPanels array
|
||||||
activeSides: newActiveSides,
|
setHiddenPanels([...hiddenPanels, side]);
|
||||||
panelOrder: newActiveSides,
|
}
|
||||||
};
|
|
||||||
|
|
||||||
// Update the selectedZone state
|
|
||||||
setSelectedZone(updatedZone);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Function to clean all widgets from a panel
|
// Function to clean all widgets from a panel
|
||||||
|
@ -145,8 +147,12 @@ const AddButtons: React.FC<ButtonsProps> = ({
|
||||||
<div className="extra-Bs">
|
<div className="extra-Bs">
|
||||||
{/* Hide Panel */}
|
{/* Hide Panel */}
|
||||||
<div
|
<div
|
||||||
className="icon"
|
className={`icon ${
|
||||||
title="Hide Panel"
|
hiddenPanels.includes(side) ? "active" : ""
|
||||||
|
}`}
|
||||||
|
title={
|
||||||
|
hiddenPanels.includes(side) ? "Show Panel" : "Hide Panel"
|
||||||
|
}
|
||||||
onClick={() => toggleVisibility(side)}
|
onClick={() => toggleVisibility(side)}
|
||||||
>
|
>
|
||||||
<EyeIcon />
|
<EyeIcon />
|
||||||
|
|
|
@ -0,0 +1,178 @@
|
||||||
|
import React, { useEffect, useRef } from "react";
|
||||||
|
import { useSelectedZoneStore } from "../../../store/useZoneStore";
|
||||||
|
import { Widget } from "../../../store/useWidgetStore";
|
||||||
|
|
||||||
|
// Define the type for `Side`
|
||||||
|
type Side = "top" | "bottom" | "left" | "right";
|
||||||
|
|
||||||
|
interface DisplayZoneProps {
|
||||||
|
zonesData: {
|
||||||
|
[key: string]: {
|
||||||
|
activeSides: Side[];
|
||||||
|
panelOrder: Side[];
|
||||||
|
lockedPanels: Side[];
|
||||||
|
widgets: Widget[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
selectedZone: {
|
||||||
|
zoneName: string;
|
||||||
|
activeSides: Side[];
|
||||||
|
panelOrder: Side[];
|
||||||
|
lockedPanels: Side[];
|
||||||
|
widgets: {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
title: string;
|
||||||
|
panel: Side;
|
||||||
|
data: any;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
setSelectedZone: React.Dispatch<
|
||||||
|
React.SetStateAction<{
|
||||||
|
zoneName: string;
|
||||||
|
activeSides: Side[];
|
||||||
|
panelOrder: Side[];
|
||||||
|
lockedPanels: Side[];
|
||||||
|
widgets: {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
title: string;
|
||||||
|
panel: Side;
|
||||||
|
data: any;
|
||||||
|
}[];
|
||||||
|
}>
|
||||||
|
>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DisplayZone: React.FC<DisplayZoneProps> = ({
|
||||||
|
zonesData,
|
||||||
|
selectedZone,
|
||||||
|
setSelectedZone,
|
||||||
|
}) => {
|
||||||
|
// Ref for the container element
|
||||||
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
// Example state for selectedOption and options (adjust based on your actual use case)
|
||||||
|
const [selectedOption, setSelectedOption] = React.useState<string | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const [options, setOptions] = React.useState<string[]>([]);
|
||||||
|
|
||||||
|
// Scroll to the selected option when it changes
|
||||||
|
useEffect(() => {
|
||||||
|
const container = containerRef.current;
|
||||||
|
|
||||||
|
if (container && selectedOption) {
|
||||||
|
// Handle scrolling to the selected option
|
||||||
|
const index = options.findIndex((option) => {
|
||||||
|
const formattedOption = formatOptionName(option);
|
||||||
|
const selectedFormattedOption =
|
||||||
|
selectedOption?.split("_")[1] || selectedOption;
|
||||||
|
return formattedOption === selectedFormattedOption;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (index !== -1) {
|
||||||
|
const optionElement = container.children[index] as HTMLElement;
|
||||||
|
if (optionElement) {
|
||||||
|
optionElement.scrollIntoView({
|
||||||
|
behavior: "smooth",
|
||||||
|
block: "nearest",
|
||||||
|
inline: "center",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [selectedOption, options]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const container = containerRef.current;
|
||||||
|
|
||||||
|
const handleWheel = (event: WheelEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (container) {
|
||||||
|
container.scrollBy({
|
||||||
|
left: event.deltaY * 2, // Adjust the multiplier for faster scrolling
|
||||||
|
behavior: "smooth",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let isDragging = false;
|
||||||
|
let startX: number;
|
||||||
|
let scrollLeft: number;
|
||||||
|
|
||||||
|
const handleMouseDown = (event: MouseEvent) => {
|
||||||
|
isDragging = true;
|
||||||
|
startX = event.pageX - (container?.offsetLeft || 0);
|
||||||
|
scrollLeft = container?.scrollLeft || 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseMove = (event: MouseEvent) => {
|
||||||
|
if (!isDragging || !container) return;
|
||||||
|
event.preventDefault();
|
||||||
|
const x = event.pageX - (container.offsetLeft || 0);
|
||||||
|
const walk = (x - startX) * 2; // Adjust the multiplier for faster dragging
|
||||||
|
container.scrollLeft = scrollLeft - walk;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseUp = () => {
|
||||||
|
isDragging = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseLeave = () => {
|
||||||
|
isDragging = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (container) {
|
||||||
|
container.addEventListener("wheel", handleWheel, { passive: false });
|
||||||
|
container.addEventListener("mousedown", handleMouseDown);
|
||||||
|
container.addEventListener("mousemove", handleMouseMove);
|
||||||
|
container.addEventListener("mouseup", handleMouseUp);
|
||||||
|
container.addEventListener("mouseleave", handleMouseLeave);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (container) {
|
||||||
|
container.removeEventListener("wheel", handleWheel);
|
||||||
|
container.removeEventListener("mousedown", handleMouseDown);
|
||||||
|
container.removeEventListener("mousemove", handleMouseMove);
|
||||||
|
container.removeEventListener("mouseup", handleMouseUp);
|
||||||
|
container.removeEventListener("mouseleave", handleMouseLeave);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Helper function to format option names (customize as needed)
|
||||||
|
const formatOptionName = (option: string): string => {
|
||||||
|
// Replace underscores with spaces and capitalize the first letter
|
||||||
|
return option.replace(/_/g, " ").replace(/^\w/, (c) => c.toUpperCase());
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className={`zoon-wrapper ${
|
||||||
|
selectedZone.activeSides.includes("bottom") && "bottom"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{Object.keys(zonesData).map((zoneName, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={`zone ${
|
||||||
|
selectedZone.zoneName === zoneName ? "active" : ""
|
||||||
|
}`}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedZone({
|
||||||
|
zoneName,
|
||||||
|
...zonesData[zoneName],
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{zoneName}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DisplayZone;
|
|
@ -1,217 +1,84 @@
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import ChartComponent from "../../layout/sidebarLeft/visualization/widgets/ChartComponent";
|
import ChartComponent from "../../layout/sidebarLeft/visualization/widgets/ChartComponent";
|
||||||
import { useWidgetStore } from "../../../store/useWidgetStore";
|
import { useWidgetStore } from "../../../store/useWidgetStore";
|
||||||
|
import PieGraphComponent from "../charts/PieGraphComponent";
|
||||||
|
import BarGraphComponent from "../charts/BarGraphComponent";
|
||||||
|
import LineGraphComponent from "../charts/LineGraphComponent";
|
||||||
|
|
||||||
export const DraggableWidget = ({ widget }: { widget: any }) => {
|
export const DraggableWidget = ({ widget }: { widget: any }) => {
|
||||||
const { selectedChartId, setSelectedChartId } = useWidgetStore();
|
const { selectedChartId, setSelectedChartId } = useWidgetStore();
|
||||||
|
|
||||||
// State for managing the popup visibility and customization options
|
|
||||||
const [isPopupOpen, setIsPopupOpen] = useState(false);
|
|
||||||
const [customizationOptions, setCustomizationOptions] = useState({
|
|
||||||
templateBackground: "#ffffff",
|
|
||||||
cardBackground: "#ffffff",
|
|
||||||
cardOpacity: 1,
|
|
||||||
cardBlur: 0,
|
|
||||||
font: "Arial",
|
|
||||||
margin: 0,
|
|
||||||
radius: 5,
|
|
||||||
shadow: "Low",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle pointer down to select the chart
|
|
||||||
const handlePointerDown = () => {
|
const handlePointerDown = () => {
|
||||||
if (selectedChartId?.id !== widget.id) {
|
if (selectedChartId?.id !== widget.id) {
|
||||||
setSelectedChartId(widget);
|
setSelectedChartId(widget);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle double-click to open the popup
|
|
||||||
const handleDoubleClick = () => {
|
|
||||||
setIsPopupOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Close the popup
|
|
||||||
const handleClosePopup = () => {
|
|
||||||
setIsPopupOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Save the changes made in the popup
|
|
||||||
const handleSaveChanges = () => {
|
|
||||||
// Here you can save the customizationOptions to your store or API
|
|
||||||
console.log("Saved Customization Options:", customizationOptions);
|
|
||||||
setIsPopupOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Compute dynamic card styles based on customizationOptions
|
|
||||||
const cardStyle = useMemo(() => {
|
|
||||||
const shadowLevels = {
|
|
||||||
Low: "0px 2px 4px rgba(0, 0, 0, 0.1)",
|
|
||||||
Medium: "0px 4px 8px rgba(0, 0, 0, 0.2)",
|
|
||||||
High: "0px 8px 16px rgba(0, 0, 0, 0.3)",
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
backgroundColor: customizationOptions.cardBackground,
|
|
||||||
opacity: customizationOptions.cardOpacity,
|
|
||||||
filter: `blur(${customizationOptions.cardBlur}px)`,
|
|
||||||
fontFamily: customizationOptions.font,
|
|
||||||
margin: `${customizationOptions.margin}px`,
|
|
||||||
borderRadius: `${customizationOptions.radius}px`,
|
|
||||||
// boxShadow: shadowLevels[customizationOptions.shadow],
|
|
||||||
};
|
|
||||||
}, [customizationOptions]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
key={widget.id}
|
key={widget.id}
|
||||||
className={`chart-container ${selectedChartId?.id === widget.id && "activeChart"}`}
|
className={`chart-container ${
|
||||||
style={cardStyle} // Apply dynamic card styles here
|
selectedChartId?.id === widget.id && "activeChart"
|
||||||
|
}`}
|
||||||
onPointerDown={handlePointerDown}
|
onPointerDown={handlePointerDown}
|
||||||
onDoubleClick={handleDoubleClick} // Add double-click event
|
|
||||||
>
|
>
|
||||||
{widget.type === "progress" ? (
|
{widget.type === "progress" ? (
|
||||||
// <ProgressCard title={widget.title} data={widget.data} />
|
// <ProgressCard title={widget.title} data={widget.data} />
|
||||||
<></>
|
<></>
|
||||||
) : (
|
) : (
|
||||||
<ChartComponent
|
<>
|
||||||
type={widget.type}
|
{widget.type === "line" && (
|
||||||
title={widget.title}
|
<LineGraphComponent
|
||||||
fontFamily={customizationOptions.font} // Pass font customization to ChartComponent
|
type={widget.type}
|
||||||
fontSize={widget.fontSize}
|
title={widget.title}
|
||||||
fontWeight={widget.fontWeight}
|
fontSize={widget.fontSize}
|
||||||
data={widget.data}
|
fontWeight={widget.fontWeight}
|
||||||
/>
|
data={{
|
||||||
|
measurements: [
|
||||||
|
{ name: "testDevice", fields: "powerConsumption" },
|
||||||
|
{ name: "furnace", fields: "powerConsumption" },
|
||||||
|
],
|
||||||
|
interval: 1000,
|
||||||
|
duration: "1h",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{widget.type === "bar" && (
|
||||||
|
<BarGraphComponent
|
||||||
|
type={widget.type}
|
||||||
|
title={widget.title}
|
||||||
|
fontSize={widget.fontSize}
|
||||||
|
fontWeight={widget.fontWeight}
|
||||||
|
data={{
|
||||||
|
measurements: [
|
||||||
|
{ name: "testDevice", fields: "powerConsumption" },
|
||||||
|
{ name: "furnace", fields: "powerConsumption" },
|
||||||
|
],
|
||||||
|
interval: 1000,
|
||||||
|
duration: "1h",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{widget.type === "pie" && (
|
||||||
|
<PieGraphComponent
|
||||||
|
type={widget.type}
|
||||||
|
title={widget.title}
|
||||||
|
fontSize={widget.fontSize}
|
||||||
|
fontWeight={widget.fontWeight}
|
||||||
|
data={{
|
||||||
|
measurements: [
|
||||||
|
{ name: "testDevice", fields: "powerConsumption" },
|
||||||
|
{ name: "furnace", fields: "powerConsumption" },
|
||||||
|
],
|
||||||
|
interval: 1000,
|
||||||
|
duration: "1h",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Popup for Customizing Template Theme */}
|
|
||||||
{isPopupOpen && (
|
|
||||||
<div className="popup-overlay">
|
|
||||||
<div className="popup-content">
|
|
||||||
<h2>Customize Template Theme</h2>
|
|
||||||
<div className="form-group">
|
|
||||||
<label>Template Background</label>
|
|
||||||
<input
|
|
||||||
type="color"
|
|
||||||
value={customizationOptions.templateBackground}
|
|
||||||
onChange={(e) =>
|
|
||||||
setCustomizationOptions((prev) => ({
|
|
||||||
...prev,
|
|
||||||
templateBackground: e.target.value,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="form-group">
|
|
||||||
<label>Card Background</label>
|
|
||||||
<input
|
|
||||||
type="color"
|
|
||||||
value={customizationOptions.cardBackground}
|
|
||||||
onChange={(e) =>
|
|
||||||
setCustomizationOptions((prev) => ({
|
|
||||||
...prev,
|
|
||||||
cardBackground: e.target.value,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="form-group">
|
|
||||||
<label>Card Opacity</label>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min="0"
|
|
||||||
max="1"
|
|
||||||
step="0.1"
|
|
||||||
value={customizationOptions.cardOpacity}
|
|
||||||
onChange={(e) =>
|
|
||||||
setCustomizationOptions((prev) => ({
|
|
||||||
...prev,
|
|
||||||
cardOpacity: parseFloat(e.target.value),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="form-group">
|
|
||||||
<label>Card Blur</label>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min="0"
|
|
||||||
max="10"
|
|
||||||
value={customizationOptions.cardBlur}
|
|
||||||
onChange={(e) =>
|
|
||||||
setCustomizationOptions((prev) => ({
|
|
||||||
...prev,
|
|
||||||
cardBlur: parseInt(e.target.value),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="form-group">
|
|
||||||
<label>Font</label>
|
|
||||||
<select
|
|
||||||
value={customizationOptions.font}
|
|
||||||
onChange={(e) =>
|
|
||||||
setCustomizationOptions((prev) => ({
|
|
||||||
...prev,
|
|
||||||
font: e.target.value,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<option value="Arial">Arial</option>
|
|
||||||
<option value="Times New Roman">Times New Roman</option>
|
|
||||||
<option value="Courier New">Courier New</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div className="form-group">
|
|
||||||
<label>Margin</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
value={customizationOptions.margin}
|
|
||||||
onChange={(e) =>
|
|
||||||
setCustomizationOptions((prev) => ({
|
|
||||||
...prev,
|
|
||||||
margin: parseInt(e.target.value),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="form-group">
|
|
||||||
<label>Radius</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
value={customizationOptions.radius}
|
|
||||||
onChange={(e) =>
|
|
||||||
setCustomizationOptions((prev) => ({
|
|
||||||
...prev,
|
|
||||||
radius: parseInt(e.target.value),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="form-group">
|
|
||||||
<label>Shadow</label>
|
|
||||||
<select
|
|
||||||
value={customizationOptions.shadow}
|
|
||||||
onChange={(e) =>
|
|
||||||
setCustomizationOptions((prev) => ({
|
|
||||||
...prev,
|
|
||||||
shadow: e.target.value,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<option value="Low">Low</option>
|
|
||||||
<option value="Medium">Medium</option>
|
|
||||||
<option value="High">High</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div className="popup-actions">
|
|
||||||
<button onClick={handleClosePopup}>Cancel</button>
|
|
||||||
<button onClick={handleSaveChanges}>Save Changes</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -83,47 +83,54 @@ const Panel: React.FC<PanelProps> = ({ selectedZone, setSelectedZone }) => {
|
||||||
const handleDrop = (e: React.DragEvent, panel: Side) => {
|
const handleDrop = (e: React.DragEvent, panel: Side) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const { draggedAsset } = useWidgetStore.getState();
|
const { draggedAsset } = useWidgetStore.getState();
|
||||||
|
|
||||||
if (draggedAsset) {
|
if (!draggedAsset) return;
|
||||||
if (selectedZone.lockedPanels.includes(panel)) return;
|
if (isPanelLocked(panel)) return;
|
||||||
|
|
||||||
const currentWidgetsInPanel = selectedZone.widgets.filter(
|
const currentWidgetsCount = getCurrentWidgetCount(panel);
|
||||||
(w) => w.panel === panel
|
const maxCapacity = calculatePanelCapacity(panel);
|
||||||
).length;
|
|
||||||
|
if (currentWidgetsCount >= maxCapacity) return;
|
||||||
const dimensions = panelDimensions[panel];
|
|
||||||
const CHART_WIDTH = 200;
|
addWidgetToPanel(draggedAsset, panel);
|
||||||
const CHART_HEIGHT = 200;
|
};
|
||||||
let maxCharts = 0;
|
|
||||||
|
// Helper functions
|
||||||
if (dimensions) {
|
const isPanelLocked = (panel: Side) =>
|
||||||
if (panel === "top" || panel === "bottom") {
|
selectedZone.lockedPanels.includes(panel);
|
||||||
maxCharts = Math.floor(dimensions.width / CHART_WIDTH);
|
|
||||||
} else {
|
const getCurrentWidgetCount = (panel: Side) =>
|
||||||
maxCharts = Math.floor(dimensions.height / CHART_HEIGHT);
|
selectedZone.widgets.filter(w => w.panel === panel).length;
|
||||||
}
|
|
||||||
} else {
|
const calculatePanelCapacity = (panel: Side) => {
|
||||||
maxCharts = panel === "top" || panel === "bottom" ? 5 : 3;
|
const CHART_WIDTH = 200;
|
||||||
}
|
const CHART_HEIGHT = 200;
|
||||||
|
const FALLBACK_HORIZONTAL_CAPACITY = 5;
|
||||||
if (currentWidgetsInPanel >= maxCharts) {
|
const FALLBACK_VERTICAL_CAPACITY = 3;
|
||||||
return;
|
|
||||||
}
|
const dimensions = panelDimensions[panel];
|
||||||
|
if (!dimensions) {
|
||||||
const updatedZone = {
|
return panel === "top" || panel === "bottom"
|
||||||
...selectedZone,
|
? FALLBACK_HORIZONTAL_CAPACITY
|
||||||
widgets: [
|
: FALLBACK_VERTICAL_CAPACITY;
|
||||||
...selectedZone.widgets,
|
|
||||||
{
|
|
||||||
...draggedAsset,
|
|
||||||
id: generateUniqueId(),
|
|
||||||
panel,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
setSelectedZone(updatedZone);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return panel === "top" || panel === "bottom"
|
||||||
|
? Math.floor(dimensions.width / CHART_WIDTH)
|
||||||
|
: Math.floor(dimensions.height / CHART_HEIGHT);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addWidgetToPanel = (asset: any, panel: Side) => {
|
||||||
|
const newWidget = {
|
||||||
|
...asset,
|
||||||
|
id: generateUniqueId(),
|
||||||
|
panel,
|
||||||
|
};
|
||||||
|
|
||||||
|
setSelectedZone(prev => ({
|
||||||
|
...prev,
|
||||||
|
widgets: [...prev.widgets, newWidget]
|
||||||
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -172,7 +179,7 @@ const Panel: React.FC<PanelProps> = ({ selectedZone, setSelectedZone }) => {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="panel-content"
|
className={`panel-content ${isPlaying && "fullScreen"}`}
|
||||||
style={{
|
style={{
|
||||||
pointerEvents: selectedZone.lockedPanels.includes(side)
|
pointerEvents: selectedZone.lockedPanels.includes(side)
|
||||||
? "none"
|
? "none"
|
||||||
|
@ -180,6 +187,7 @@ const Panel: React.FC<PanelProps> = ({ selectedZone, setSelectedZone }) => {
|
||||||
opacity: selectedZone.lockedPanels.includes(side) ? "0.8" : "1",
|
opacity: selectedZone.lockedPanels.includes(side) ? "0.8" : "1",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<>{}</>
|
||||||
{selectedZone.widgets
|
{selectedZone.widgets
|
||||||
.filter((w) => w.panel === side)
|
.filter((w) => w.panel === side)
|
||||||
.map((widget) => (
|
.map((widget) => (
|
||||||
|
@ -193,5 +201,3 @@ const Panel: React.FC<PanelProps> = ({ selectedZone, setSelectedZone }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Panel;
|
export default Panel;
|
||||||
// only load selected template
|
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { usePlayButtonStore } from "../../../store/usePlayButtonStore";
|
||||||
import Panel from "./Panel";
|
import Panel from "./Panel";
|
||||||
import AddButtons from "./AddButtons";
|
import AddButtons from "./AddButtons";
|
||||||
import { useSelectedZoneStore } from "../../../store/useZoneStore";
|
import { useSelectedZoneStore } from "../../../store/useZoneStore";
|
||||||
|
import DisplayZone from "./DisplayZone";
|
||||||
|
|
||||||
type Side = "top" | "bottom" | "left" | "right";
|
type Side = "top" | "bottom" | "left" | "right";
|
||||||
|
|
||||||
|
@ -15,6 +16,7 @@ interface Widget {
|
||||||
}
|
}
|
||||||
|
|
||||||
const RealTimeVisulization: React.FC = () => {
|
const RealTimeVisulization: React.FC = () => {
|
||||||
|
const [hiddenPanels, setHiddenPanels] = React.useState<Side[]>([]);
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const [zonesData, setZonesData] = useState<{
|
const [zonesData, setZonesData] = useState<{
|
||||||
[key: string]: {
|
[key: string]: {
|
||||||
|
@ -74,34 +76,19 @@ const RealTimeVisulization: React.FC = () => {
|
||||||
style={{
|
style={{
|
||||||
height: isPlaying ? "100vh" : "",
|
height: isPlaying ? "100vh" : "",
|
||||||
width: isPlaying ? "100%" : "",
|
width: isPlaying ? "100%" : "",
|
||||||
left: isPlaying ? "50%" : "",
|
left: isPlaying ? "0%" : "",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<DisplayZone
|
||||||
className={`zoon-wrapper ${
|
zonesData={zonesData}
|
||||||
selectedZone.activeSides.includes("bottom") && "bottom"
|
selectedZone={selectedZone}
|
||||||
}`}
|
setSelectedZone={setSelectedZone}
|
||||||
>
|
/>
|
||||||
{Object.keys(zonesData).map((zoneName, index) => (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className={`zone ${
|
|
||||||
selectedZone.zoneName === zoneName ? "active" : ""
|
|
||||||
}`}
|
|
||||||
onClick={() => {
|
|
||||||
setSelectedZone({
|
|
||||||
zoneName,
|
|
||||||
...zonesData[zoneName],
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{zoneName}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{!isPlaying && (
|
{!isPlaying && (
|
||||||
<AddButtons
|
<AddButtons
|
||||||
|
hiddenPanels={hiddenPanels}
|
||||||
|
setHiddenPanels={setHiddenPanels}
|
||||||
selectedZone={selectedZone}
|
selectedZone={selectedZone}
|
||||||
setSelectedZone={setSelectedZone}
|
setSelectedZone={setSelectedZone}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import React, { useState } from "react";
|
import React, { useState, useRef, useEffect } from "react";
|
||||||
|
|
||||||
|
|
||||||
// Dropdown Item Component
|
// Dropdown Item Component
|
||||||
const DropdownItem = ({
|
const DropdownItem = ({
|
||||||
|
@ -27,9 +26,11 @@ const DropdownItem = ({
|
||||||
const NestedDropdown = ({
|
const NestedDropdown = ({
|
||||||
label,
|
label,
|
||||||
children,
|
children,
|
||||||
|
onSelect,
|
||||||
}: {
|
}: {
|
||||||
label: string;
|
label: string;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
onSelect: (selectedLabel: string) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
@ -37,36 +38,47 @@ const NestedDropdown = ({
|
||||||
<div className="nested-dropdown">
|
<div className="nested-dropdown">
|
||||||
{/* Dropdown Trigger */}
|
{/* Dropdown Trigger */}
|
||||||
<div
|
<div
|
||||||
className="dropdown-trigger"
|
className={`dropdown-trigger ${open ? "open" : ""}`}
|
||||||
onClick={() => setOpen(!open)} // Toggle submenu on click
|
onClick={() => setOpen(!open)} // Toggle submenu on click
|
||||||
>
|
>
|
||||||
{label} <span className="icon">{open ? "▼" : "▶"}</span>
|
{label} <span className="icon">{open ? "▼" : "▶"}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Submenu */}
|
{/* Submenu */}
|
||||||
{open && <div className="submenu">{children}</div>}
|
{open && (
|
||||||
|
<div className="submenu">
|
||||||
|
{React.Children.map(children, (child) =>
|
||||||
|
React.cloneElement(child as React.ReactElement, { onSelect })
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Recursive Function to Render Nested Data
|
// Recursive Function to Render Nested Data
|
||||||
const renderNestedData = (data: Record<string, any>) => {
|
const renderNestedData = (
|
||||||
|
data: Record<string, any>,
|
||||||
|
onSelect: (selectedLabel: string) => void
|
||||||
|
) => {
|
||||||
return Object.entries(data).map(([key, value]) => {
|
return Object.entries(data).map(([key, value]) => {
|
||||||
if (typeof value === "object" && !Array.isArray(value)) {
|
if (typeof value === "object" && !Array.isArray(value)) {
|
||||||
// If the value is an object, render it as a nested dropdown
|
// If the value is an object, render it as a nested dropdown
|
||||||
return (
|
return (
|
||||||
<NestedDropdown key={key} label={key}>
|
<NestedDropdown key={key} label={key} onSelect={onSelect}>
|
||||||
{renderNestedData(value)}
|
{renderNestedData(value, onSelect)}
|
||||||
</NestedDropdown>
|
</NestedDropdown>
|
||||||
);
|
);
|
||||||
} else if (Array.isArray(value)) {
|
} else if (Array.isArray(value)) {
|
||||||
// If the value is an array, render each item as a dropdown item
|
// If the value is an array, render each item as a dropdown item
|
||||||
return value.map((item, index) => (
|
return value.map((item, index) => (
|
||||||
<DropdownItem key={index} label={item} />
|
<DropdownItem key={index} label={item} onClick={() => onSelect(item)} />
|
||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
// If the value is a simple string, render it as a dropdown item
|
// If the value is a simple string, render it as a dropdown item
|
||||||
return <DropdownItem key={key} label={value} />;
|
return (
|
||||||
|
<DropdownItem key={key} label={value} onClick={() => onSelect(value)} />
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -74,21 +86,52 @@ const renderNestedData = (data: Record<string, any>) => {
|
||||||
// Main Multi-Level Dropdown Component
|
// Main Multi-Level Dropdown Component
|
||||||
const MultiLevelDropdown = ({ data }: { data: Record<string, any> }) => {
|
const MultiLevelDropdown = ({ data }: { data: Record<string, any> }) => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
const [selectedLabel, setSelectedLabel] = useState("Dropdown trigger");
|
||||||
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// Handle outer click to close the dropdown
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
if (
|
||||||
|
dropdownRef.current &&
|
||||||
|
!dropdownRef.current.contains(event.target as Node)
|
||||||
|
) {
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("mousedown", handleClickOutside);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("mousedown", handleClickOutside);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Handle selection of an item
|
||||||
|
const handleSelect = (selectedLabel: string) => {
|
||||||
|
setSelectedLabel(selectedLabel); // Update the dropdown trigger text
|
||||||
|
setOpen(false); // Close the dropdown
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="multi-level-dropdown">
|
<div className="multi-level-dropdown" ref={dropdownRef}>
|
||||||
{/* Dropdown Trigger Button */}
|
{/* Dropdown Trigger Button */}
|
||||||
<button
|
<button
|
||||||
className="dropdown-button"
|
className={`dropdown-button ${open ? "open" : ""}`}
|
||||||
onClick={() => setOpen(!open)} // Toggle main menu on click
|
onClick={() => setOpen(!open)} // Toggle main menu on click
|
||||||
>
|
>
|
||||||
Dropdown trigger <span className="icon">▼</span>
|
{selectedLabel} <span className="icon">▾</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Dropdown Menu */}
|
{/* Dropdown Menu */}
|
||||||
{open && <div className="dropdown-menu">{renderNestedData(data)}</div>}
|
{open && (
|
||||||
|
<div className="dropdown-menu">
|
||||||
|
<div className="dropdown-content">
|
||||||
|
{renderNestedData(data, handleSelect)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default MultiLevelDropdown;
|
export default MultiLevelDropdown;
|
||||||
|
|
|
@ -2,35 +2,73 @@ import { Template } from "../../store/useTemplateStore";
|
||||||
import { captureVisualization } from "./captureVisualization";
|
import { captureVisualization } from "./captureVisualization";
|
||||||
|
|
||||||
type HandleSaveTemplateProps = {
|
type HandleSaveTemplateProps = {
|
||||||
addTemplate: (template: Template) => void;
|
addTemplate: (template: Template) => void;
|
||||||
selectedZone: {
|
selectedZone: {
|
||||||
panelOrder: string[]; // Adjust the type based on actual data structure
|
panelOrder: string[]; // Adjust the type based on actual data structure
|
||||||
widgets: any[]; // Replace `any` with the actual widget type
|
widgets: any[]; // Replace `any` with the actual widget type
|
||||||
};
|
};
|
||||||
|
templates?: Template[];
|
||||||
};
|
};
|
||||||
|
|
||||||
// Generate a unique ID (placeholder function)
|
// Generate a unique ID (placeholder function)
|
||||||
const generateUniqueId = (): string => {
|
const generateUniqueId = (): string => {
|
||||||
return Math.random().toString(36).substring(2, 15);
|
return `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Refactored function
|
// Refactored function
|
||||||
export const handleSaveTemplate = async ({
|
export const handleSaveTemplate = async ({
|
||||||
addTemplate,
|
addTemplate,
|
||||||
selectedZone,
|
selectedZone,
|
||||||
|
templates = [],
|
||||||
}: HandleSaveTemplateProps): Promise<void> => {
|
}: HandleSaveTemplateProps): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const snapshot = await captureVisualization();
|
// Check if the selected zone has any widgets
|
||||||
const template: Template = {
|
if (!selectedZone.widgets || selectedZone.widgets.length === 0) {
|
||||||
id: generateUniqueId(),
|
console.warn("Cannot save an empty template.");
|
||||||
name: `Template ${Date.now()}`,
|
return;
|
||||||
panelOrder: selectedZone.panelOrder,
|
|
||||||
widgets: selectedZone.widgets,
|
|
||||||
snapshot,
|
|
||||||
};
|
|
||||||
console.log('template: ', template);
|
|
||||||
addTemplate(template);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to save template:', error);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if the template already exists
|
||||||
|
const isDuplicate = templates.some((template) => {
|
||||||
|
const isSamePanelOrder =
|
||||||
|
JSON.stringify(template.panelOrder) ===
|
||||||
|
JSON.stringify(selectedZone.panelOrder);
|
||||||
|
const isSameWidgets =
|
||||||
|
JSON.stringify(template.widgets) ===
|
||||||
|
JSON.stringify(selectedZone.widgets);
|
||||||
|
return isSamePanelOrder && isSameWidgets;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isDuplicate) {
|
||||||
|
console.warn("This template already exists.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capture visualization snapshot
|
||||||
|
const snapshot = await captureVisualization();
|
||||||
|
if (!snapshot) {
|
||||||
|
console.error("Failed to capture visualization snapshot.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new template
|
||||||
|
const newTemplate: Template = {
|
||||||
|
id: generateUniqueId(),
|
||||||
|
name: `Template ${Date.now()}`,
|
||||||
|
panelOrder: selectedZone.panelOrder,
|
||||||
|
widgets: selectedZone.widgets,
|
||||||
|
snapshot,
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("Saving template:", newTemplate);
|
||||||
|
|
||||||
|
// Save the template
|
||||||
|
try {
|
||||||
|
addTemplate(newTemplate);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to add template:", error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to save template:", error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -35,7 +35,7 @@ $highlight-accent-color-dark: #403e6a; // Highlighted accent for dark mode
|
||||||
$background-color: #fcfdfd; // Main background color
|
$background-color: #fcfdfd; // Main background color
|
||||||
$background-color-dark: #19191d; // Main background color for dark mode
|
$background-color-dark: #19191d; // Main background color for dark mode
|
||||||
$background-color-secondary: #e1e0ff80; // Secondary background color
|
$background-color-secondary: #e1e0ff80; // Secondary background color
|
||||||
$background-color-secondary-dark: #1f1f2399; // Secondary background color for dark mode
|
$background-color-secondary-dark: #39394f99; // Secondary background color for dark mode
|
||||||
|
|
||||||
// Border colors
|
// Border colors
|
||||||
$border-color: #e0dfff; // Default border color
|
$border-color: #e0dfff; // Default border color
|
||||||
|
|
|
@ -36,9 +36,11 @@
|
||||||
overflow-y: auto; // Optional: Enable scrolling if content exceeds height
|
overflow-y: auto; // Optional: Enable scrolling if content exceeds height
|
||||||
left: 0;
|
left: 0;
|
||||||
top: 104%;
|
top: 104%;
|
||||||
|
|
||||||
.option {
|
.option {
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
flex-direction: row !important;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--primary-color); // Optional: Hover effect
|
background-color: var(--primary-color); // Optional: Hover effect
|
||||||
|
|
|
@ -0,0 +1,110 @@
|
||||||
|
@use "../../../abstracts/variables" as *;
|
||||||
|
@use "../../../abstracts/mixins" as *;
|
||||||
|
|
||||||
|
.floatingWidgets-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
padding-top: 12px;
|
||||||
|
padding: 6px;
|
||||||
|
|
||||||
|
.floating {
|
||||||
|
|
||||||
|
min-height: 170px;
|
||||||
|
background: var(--background-color);
|
||||||
|
border: 1.23px solid var(--border-color);
|
||||||
|
box-shadow: 0px 4.91px 4.91px 0px #0000001c;
|
||||||
|
border-radius: $border-radius-medium;
|
||||||
|
padding: 12px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.working-state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
|
.state-working-top {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.floatingWidgets-wrapper {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating.working-state {
|
||||||
|
width: 100%;
|
||||||
|
height: 283px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
padding: 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-working-top {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
// flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.working-status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.working {
|
||||||
|
font-size: 20px;
|
||||||
|
color: #4CAF50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
display: inline-block;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
background: #4CAF50;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.img img {
|
||||||
|
width: 150px;
|
||||||
|
height: 100px;
|
||||||
|
border-radius: 4px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-working-data {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 16px;
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-key {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-value {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
|
@ -78,14 +78,14 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding-right: 6px;
|
padding: 6px;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
|
||||||
.chart {
|
.chart {
|
||||||
min-height: 170px;
|
min-height: 170px;
|
||||||
background: var(--background-primary, #fcfdfd);
|
background: var(--background-color);
|
||||||
border: 1.23px solid var(--Grays-Gray-5, #e5e5ea);
|
border: 1.23px solid var(--border-color);
|
||||||
box-shadow: 0px 4.91px 4.91px 0px #0000001c;
|
box-shadow: 0px 4.91px 4.91px 0px #0000001c;
|
||||||
border-radius: $border-radius-medium;
|
border-radius: $border-radius-medium;
|
||||||
padding: 12px 6px;
|
padding: 12px 6px;
|
||||||
|
@ -107,7 +107,7 @@
|
||||||
|
|
||||||
.stock {
|
.stock {
|
||||||
padding: 13px 5px;
|
padding: 13px 5px;
|
||||||
background-color: #e0dfff80;
|
background-color: var(--background-color-secondary);
|
||||||
border-radius: 6.33px;
|
border-radius: 6.33px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
@ -291,8 +291,46 @@
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
|
|
||||||
|
.datas {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.datas__class {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.multi-level-dropdown {
|
||||||
|
|
||||||
|
min-width: 100px;
|
||||||
|
|
||||||
|
.dropdown-button {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.datas__class {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
.datas__separator {}
|
||||||
|
|
||||||
|
.disable {
|
||||||
|
cursor: not-allowed;
|
||||||
|
pointer-events: none;
|
||||||
|
/* Disables all mouse interactions */
|
||||||
|
opacity: 0.5;
|
||||||
|
/* Optional: Makes the button look visually disabled */
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.sideBarHeader {
|
.sideBarHeader {
|
||||||
color: #5c87df;
|
color: var(--accent-color);
|
||||||
border-bottom: 1px solid var(--border-color);
|
border-bottom: 1px solid var(--border-color);
|
||||||
padding-bottom: 6px;
|
padding-bottom: 6px;
|
||||||
}
|
}
|
||||||
|
@ -388,7 +426,7 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 150px;
|
height: 150px;
|
||||||
background: #f0f0f0;
|
background: #f0f0f0;
|
||||||
border-radius: 8px;
|
// border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.optionsContainer {
|
.optionsContainer {
|
||||||
|
@ -523,93 +561,119 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Base styles */
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.multi-level-dropdown {
|
.multi-level-dropdown {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
text-align: left;
|
|
||||||
|
|
||||||
.dropdown-button {
|
.dropdown-button {
|
||||||
background-color: #3b82f6; /* Blue background */
|
width: 100%;
|
||||||
color: white;
|
background-color: var(--background-color) !important;
|
||||||
padding: 0.5rem 1rem;
|
border: 1px solid var(--border-color) !important;
|
||||||
font-size: 0.875rem;
|
padding: 5px 10px;
|
||||||
border: none;
|
|
||||||
border-radius: 0.375rem;
|
|
||||||
|
// font-size: 12px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
border-radius: 5px;
|
||||||
align-items: center;
|
transition: background-color 0.3s ease;
|
||||||
transition: background-color 0.2s ease;
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: #2563eb; /* Darker blue on hover */
|
background-color: #333333;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
&.open {
|
||||||
margin-left: 0.5rem;
|
background-color: #333333;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-menu {
|
.dropdown-menu {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: calc(100% + 0.5rem); /* Add spacing below the button */
|
top: 100%;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 12rem;
|
background-color: #ffffff;
|
||||||
background-color: white;
|
border: 1px solid #cccccc;
|
||||||
border: 1px solid #e5e7eb; /* Light gray border */
|
border-radius: 5px;
|
||||||
border-radius: 0.375rem;
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); /* Subtle shadow */
|
z-index: 1000;
|
||||||
z-index: 10;
|
min-width: 200px;
|
||||||
}
|
overflow: auto;
|
||||||
}
|
max-height: 600px;
|
||||||
|
|
||||||
/* Dropdown Item */
|
.dropdown-content {
|
||||||
.dropdown-item {
|
display: flex;
|
||||||
display: block;
|
flex-direction: column;
|
||||||
padding: 0.5rem 1rem;
|
gap: 6px;
|
||||||
font-size: 0.875rem;
|
|
||||||
color: #4b5563; /* Gray text */
|
|
||||||
text-decoration: none;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 0.2s ease;
|
|
||||||
|
|
||||||
&:hover {
|
.nested-dropdown {
|
||||||
background-color: #f3f4f6; /* Light gray background on hover */
|
// &:first-child{
|
||||||
}
|
margin-left: 0;
|
||||||
}
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
/* Nested Dropdown */
|
padding: 10px;
|
||||||
.nested-dropdown {
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
.dropdown-trigger {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: #4b5563; /* Gray text */
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 0.2s ease;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: #f3f4f6; /* Light gray background on hover */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.dropdown-item {
|
||||||
margin-left: 0.5rem;
|
display: block;
|
||||||
|
padding: 5px 10px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #000000;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nested-dropdown {
|
||||||
|
margin-left: 20px;
|
||||||
|
|
||||||
|
.dropdown-trigger {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 5px 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #000000;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.open {
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
font-size: 12px;
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.submenu {
|
||||||
|
margin-top: 5px;
|
||||||
|
padding-left: 20px;
|
||||||
|
border-left: 2px solid #cccccc;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.submenu {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 100%; /* Position submenu to the right */
|
|
||||||
width: 12rem;
|
|
||||||
background-color: white;
|
|
||||||
border: 1px solid #e5e7eb; /* Light gray border */
|
|
||||||
border-radius: 0.375rem;
|
|
||||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); /* Subtle shadow */
|
|
||||||
z-index: 20;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -19,6 +19,7 @@
|
||||||
@use 'components/templates';
|
@use 'components/templates';
|
||||||
@use 'components/tools';
|
@use 'components/tools';
|
||||||
@use 'components/regularDropDown';
|
@use 'components/regularDropDown';
|
||||||
|
@use 'components/visualization/floating/energyConsumed';
|
||||||
|
|
||||||
// layout
|
// layout
|
||||||
@use 'layout/sidebar';
|
@use 'layout/sidebar';
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
.realTime-viz {
|
.realTime-viz {
|
||||||
background-color: var(--background-color);
|
background-color: var(--background-color);
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
box-shadow: 0px 4px 8px rgba(60, 60, 67, 0.1019607843);
|
box-shadow: $box-shadow-medium;
|
||||||
width: calc(100% - (320px + 270px + 80px));
|
width: calc(100% - (320px + 270px + 80px));
|
||||||
height: 600px;
|
height: 600px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -30,7 +30,7 @@
|
||||||
|
|
||||||
.zoon-wrapper {
|
.zoon-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
background-color: #e0dfff80;
|
background-color: var(--background-color);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 10px;
|
bottom: 10px;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
|
@ -40,14 +40,14 @@
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
max-width: 80%;
|
max-width: 80%;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
max-width: calc(100% - 450px);
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.zone {
|
.zone {
|
||||||
width: auto;
|
width: auto;
|
||||||
background-color: #fcfdfd;
|
background-color: var(--background-color);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
@ -83,43 +83,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
width: 90%; // Take even more width on very small screens
|
|
||||||
height: 400px; // Further reduce height
|
|
||||||
top: 45%; // Adjust vertical position slightly upward
|
|
||||||
|
|
||||||
.panel {
|
|
||||||
&.top-panel,
|
|
||||||
&.bottom-panel {
|
|
||||||
.panel-content {
|
|
||||||
flex-direction: column; // Stack panels vertically on small screens
|
|
||||||
|
|
||||||
.chart-container {
|
|
||||||
width: 100%; // Make charts full width
|
|
||||||
height: 150px; // Reduce chart height
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
width: 95%; // Take almost full width on very small devices
|
|
||||||
height: 350px; // Further reduce height
|
|
||||||
top: 40%; // Move slightly higher for better visibility
|
|
||||||
|
|
||||||
.side-button-container {
|
|
||||||
flex-direction: row !important; // Force buttons into a row
|
|
||||||
gap: 4px; // Reduce spacing between buttons
|
|
||||||
|
|
||||||
&.top,
|
|
||||||
&.bottom {
|
|
||||||
left: 50%; // Center horizontally
|
|
||||||
transform: translateX(-50%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-container {
|
.content-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
|
@ -189,6 +152,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
background-color: var(--background-color);
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
display: none;
|
display: none;
|
||||||
|
@ -216,18 +180,23 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
&.top-panel,
|
&.top-panel,
|
||||||
&.bottom-panel {
|
&.bottom-panel {
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
|
|
||||||
|
.fullScreen {
|
||||||
|
background-color: red;
|
||||||
|
}
|
||||||
|
|
||||||
.panel-content {
|
.panel-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
||||||
.chart-container {
|
.chart-container {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 230px;
|
width: 200px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -247,7 +216,7 @@
|
||||||
|
|
||||||
.chart-container {
|
.chart-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 200px;
|
height: 180px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -275,6 +244,15 @@
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active {
|
||||||
|
background-color: var(--accent-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
@ -409,4 +387,4 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue