updated real time vis

This commit is contained in:
Nalvazhuthi 2025-03-20 16:30:43 +05:30
parent 4549a5cae4
commit 7950b58ba8
25 changed files with 1310 additions and 506 deletions

11
app/package-lock.json generated
View File

@ -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",

View File

@ -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"

View File

@ -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;

View File

@ -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>
); );
}; };

View File

@ -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;

View File

@ -1,12 +0,0 @@
import React from 'react'
const WidgetsTemplate = () => {
return (
<div>
WidgetsTemplate
</div>
)
}
export default WidgetsTemplate

View File

@ -65,7 +65,7 @@ const SideBarRight: React.FC = () => {
)} )}
{/* realtime visualization */} {/* realtime visualization */}
{activeModule === "visualization" && <Visualization />} {toggleUI && activeModule === "visualization" && <Visualization />}
</div> </div>
); );
}; };

View File

@ -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>

View File

@ -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>

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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 />

View File

@ -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;

View File

@ -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>
)}
</> </>
); );
}; };

View File

@ -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

View File

@ -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}
/> />

View File

@ -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;

View File

@ -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);
}
}; };

View File

@ -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

View File

@ -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

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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';

View File

@ -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 @@
} }
} }
} }
} }