diff --git a/app/src/components/ui/Tools.tsx b/app/src/components/ui/Tools.tsx
index a182e1d..f6389e9 100644
--- a/app/src/components/ui/Tools.tsx
+++ b/app/src/components/ui/Tools.tsx
@@ -19,6 +19,7 @@ import useTemplateStore from "../../store/useTemplateStore";
import { useSelectedZoneStore } from "../../store/useZoneStore";
const Tools: React.FC = () => {
+ const { templates } = useTemplateStore();
const [activeTool, setActiveTool] = useState("cursor");
const [activeSubTool, setActiveSubTool] = useState("cursor");
const [toggleThreeD, setToggleThreeD] = useState(true);
@@ -196,7 +197,13 @@ const Tools: React.FC = () => {
handleSaveTemplate({ addTemplate, selectedZone })}
+ onClick={() =>
+ handleSaveTemplate({
+ addTemplate,
+ selectedZone,
+ templates,
+ })
+ }
>
diff --git a/app/src/components/ui/charts/BarGraphComponent.tsx b/app/src/components/ui/charts/BarGraphComponent.tsx
new file mode 100644
index 0000000..466d4df
--- /dev/null
+++ b/app/src/components/ui/charts/BarGraphComponent.tsx
@@ -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
;
+};
+
+export default LineGraphComponent;
diff --git a/app/src/components/ui/charts/LineGraphComponent.tsx b/app/src/components/ui/charts/LineGraphComponent.tsx
new file mode 100644
index 0000000..98b9d61
--- /dev/null
+++ b/app/src/components/ui/charts/LineGraphComponent.tsx
@@ -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
;
+};
+
+export default LineGraphComponent;
diff --git a/app/src/components/ui/charts/PieGraphComponent.tsx b/app/src/components/ui/charts/PieGraphComponent.tsx
new file mode 100644
index 0000000..fb553fb
--- /dev/null
+++ b/app/src/components/ui/charts/PieGraphComponent.tsx
@@ -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
;
+};
+
+export default PieChartComponent;
diff --git a/app/src/components/ui/componets/AddButtons.tsx b/app/src/components/ui/componets/AddButtons.tsx
index 0a810f7..4ff9ebb 100644
--- a/app/src/components/ui/componets/AddButtons.tsx
+++ b/app/src/components/ui/componets/AddButtons.tsx
@@ -11,7 +11,7 @@ type Side = "top" | "bottom" | "left" | "right";
// Define the type for the props passed to the Buttons component
interface ButtonsProps {
selectedZone: {
- zoneName: string; // Add zoneName property
+ zoneName: string;
activeSides: Side[];
panelOrder: Side[];
lockedPanels: Side[];
@@ -25,7 +25,7 @@ interface ButtonsProps {
};
setSelectedZone: React.Dispatch<
React.SetStateAction<{
- zoneName: string; // Ensure zoneName is included in the state type
+ zoneName: string;
activeSides: Side[];
panelOrder: Side[];
lockedPanels: Side[];
@@ -38,12 +38,18 @@ interface ButtonsProps {
}[];
}>
>;
+ hiddenPanels: Side[]; // Add this prop for hidden panels
+ setHiddenPanels: React.Dispatch
>; // Add this prop for updating hidden panels
}
const AddButtons: React.FC = ({
selectedZone,
setSelectedZone,
+ setHiddenPanels,
+ hiddenPanels,
}) => {
+ // Local state to track hidden panels
+
// Function to toggle lock/unlock a panel
const toggleLockPanel = (side: Side) => {
const newLockedPanels = selectedZone.lockedPanels.includes(side)
@@ -61,18 +67,14 @@ const AddButtons: React.FC = ({
// Function to toggle visibility of a panel
const toggleVisibility = (side: Side) => {
- const newActiveSides = selectedZone.activeSides.includes(side)
- ? selectedZone.activeSides.filter((s) => s !== side)
- : [...selectedZone.activeSides, side];
-
- const updatedZone = {
- ...selectedZone,
- activeSides: newActiveSides,
- panelOrder: newActiveSides,
- };
-
- // Update the selectedZone state
- setSelectedZone(updatedZone);
+ const isHidden = hiddenPanels.includes(side);
+ if (isHidden) {
+ // If the panel is already hidden, remove it from the hiddenPanels array
+ setHiddenPanels(hiddenPanels.filter((panel) => panel !== side));
+ } else {
+ // If the panel is visible, add it to the hiddenPanels array
+ setHiddenPanels([...hiddenPanels, side]);
+ }
};
// Function to clean all widgets from a panel
@@ -145,8 +147,12 @@ const AddButtons: React.FC = ({
{/* Hide Panel */}
toggleVisibility(side)}
>
diff --git a/app/src/components/ui/componets/DisplayZone.tsx b/app/src/components/ui/componets/DisplayZone.tsx
new file mode 100644
index 0000000..b5fe0f0
--- /dev/null
+++ b/app/src/components/ui/componets/DisplayZone.tsx
@@ -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
= ({
+ zonesData,
+ selectedZone,
+ setSelectedZone,
+}) => {
+ // Ref for the container element
+ const containerRef = useRef(null);
+
+ // Example state for selectedOption and options (adjust based on your actual use case)
+ const [selectedOption, setSelectedOption] = React.useState(
+ null
+ );
+ const [options, setOptions] = React.useState([]);
+
+ // 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 (
+
+ {Object.keys(zonesData).map((zoneName, index) => (
+
{
+ setSelectedZone({
+ zoneName,
+ ...zonesData[zoneName],
+ });
+ }}
+ >
+ {zoneName}
+
+ ))}
+
+ );
+};
+
+export default DisplayZone;
\ No newline at end of file
diff --git a/app/src/components/ui/componets/DraggableWidget.tsx b/app/src/components/ui/componets/DraggableWidget.tsx
index 342f31f..6769890 100644
--- a/app/src/components/ui/componets/DraggableWidget.tsx
+++ b/app/src/components/ui/componets/DraggableWidget.tsx
@@ -1,217 +1,84 @@
import { useMemo, useState } from "react";
import ChartComponent from "../../layout/sidebarLeft/visualization/widgets/ChartComponent";
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 }) => {
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 = () => {
if (selectedChartId?.id !== widget.id) {
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 (
<>
{widget.type === "progress" ? (
- //
- <>>
+ //
+ <>>
) : (
-
+ <>
+ {widget.type === "line" && (
+
+ )}
+ {widget.type === "bar" && (
+
+ )}
+ {widget.type === "pie" && (
+
+ )}
+ >
)}
-
- {/* Popup for Customizing Template Theme */}
- {isPopupOpen && (
-
-
-
Customize Template Theme
-
-
-
- setCustomizationOptions((prev) => ({
- ...prev,
- templateBackground: e.target.value,
- }))
- }
- />
-
-
-
-
- setCustomizationOptions((prev) => ({
- ...prev,
- cardBackground: e.target.value,
- }))
- }
- />
-
-
-
-
- setCustomizationOptions((prev) => ({
- ...prev,
- cardOpacity: parseFloat(e.target.value),
- }))
- }
- />
-
-
-
-
- setCustomizationOptions((prev) => ({
- ...prev,
- cardBlur: parseInt(e.target.value),
- }))
- }
- />
-
-
-
-
-
-
-
-
- setCustomizationOptions((prev) => ({
- ...prev,
- margin: parseInt(e.target.value),
- }))
- }
- />
-
-
-
-
- setCustomizationOptions((prev) => ({
- ...prev,
- radius: parseInt(e.target.value),
- }))
- }
- />
-
-
-
-
-
-
-
-
-
-
-
- )}
>
);
};
diff --git a/app/src/components/ui/componets/Panel.tsx b/app/src/components/ui/componets/Panel.tsx
index 502114b..4a1305f 100644
--- a/app/src/components/ui/componets/Panel.tsx
+++ b/app/src/components/ui/componets/Panel.tsx
@@ -83,47 +83,54 @@ const Panel: React.FC = ({ selectedZone, setSelectedZone }) => {
const handleDrop = (e: React.DragEvent, panel: Side) => {
e.preventDefault();
const { draggedAsset } = useWidgetStore.getState();
-
- if (draggedAsset) {
- if (selectedZone.lockedPanels.includes(panel)) return;
-
- const currentWidgetsInPanel = selectedZone.widgets.filter(
- (w) => w.panel === panel
- ).length;
-
- const dimensions = panelDimensions[panel];
- const CHART_WIDTH = 200;
- const CHART_HEIGHT = 200;
- let maxCharts = 0;
-
- if (dimensions) {
- if (panel === "top" || panel === "bottom") {
- maxCharts = Math.floor(dimensions.width / CHART_WIDTH);
- } else {
- maxCharts = Math.floor(dimensions.height / CHART_HEIGHT);
- }
- } else {
- maxCharts = panel === "top" || panel === "bottom" ? 5 : 3;
- }
-
- if (currentWidgetsInPanel >= maxCharts) {
- return;
- }
-
- const updatedZone = {
- ...selectedZone,
- widgets: [
- ...selectedZone.widgets,
- {
- ...draggedAsset,
- id: generateUniqueId(),
- panel,
- },
- ],
- };
-
- setSelectedZone(updatedZone);
+
+ if (!draggedAsset) return;
+ if (isPanelLocked(panel)) return;
+
+ const currentWidgetsCount = getCurrentWidgetCount(panel);
+ const maxCapacity = calculatePanelCapacity(panel);
+
+ if (currentWidgetsCount >= maxCapacity) return;
+
+ addWidgetToPanel(draggedAsset, panel);
+ };
+
+ // Helper functions
+ const isPanelLocked = (panel: Side) =>
+ selectedZone.lockedPanels.includes(panel);
+
+ const getCurrentWidgetCount = (panel: Side) =>
+ selectedZone.widgets.filter(w => w.panel === panel).length;
+
+ const calculatePanelCapacity = (panel: Side) => {
+ const CHART_WIDTH = 200;
+ const CHART_HEIGHT = 200;
+ const FALLBACK_HORIZONTAL_CAPACITY = 5;
+ const FALLBACK_VERTICAL_CAPACITY = 3;
+
+ const dimensions = panelDimensions[panel];
+ if (!dimensions) {
+ return panel === "top" || panel === "bottom"
+ ? FALLBACK_HORIZONTAL_CAPACITY
+ : FALLBACK_VERTICAL_CAPACITY;
}
+
+ 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(() => {
@@ -172,7 +179,7 @@ const Panel: React.FC = ({ selectedZone, setSelectedZone }) => {
}}
>
= ({ selectedZone, setSelectedZone }) => {
opacity: selectedZone.lockedPanels.includes(side) ? "0.8" : "1",
}}
>
+ <>{}>
{selectedZone.widgets
.filter((w) => w.panel === side)
.map((widget) => (
@@ -193,5 +201,3 @@ const Panel: React.FC
= ({ selectedZone, setSelectedZone }) => {
};
export default Panel;
-// only load selected template
-
diff --git a/app/src/components/ui/componets/RealTimeVisulization.tsx b/app/src/components/ui/componets/RealTimeVisulization.tsx
index fb2d653..6daa075 100644
--- a/app/src/components/ui/componets/RealTimeVisulization.tsx
+++ b/app/src/components/ui/componets/RealTimeVisulization.tsx
@@ -3,6 +3,7 @@ import { usePlayButtonStore } from "../../../store/usePlayButtonStore";
import Panel from "./Panel";
import AddButtons from "./AddButtons";
import { useSelectedZoneStore } from "../../../store/useZoneStore";
+import DisplayZone from "./DisplayZone";
type Side = "top" | "bottom" | "left" | "right";
@@ -15,6 +16,7 @@ interface Widget {
}
const RealTimeVisulization: React.FC = () => {
+ const [hiddenPanels, setHiddenPanels] = React.useState([]);
const containerRef = useRef(null);
const [zonesData, setZonesData] = useState<{
[key: string]: {
@@ -74,34 +76,19 @@ const RealTimeVisulization: React.FC = () => {
style={{
height: isPlaying ? "100vh" : "",
width: isPlaying ? "100%" : "",
- left: isPlaying ? "50%" : "",
+ left: isPlaying ? "0%" : "",
}}
>
-
- {Object.keys(zonesData).map((zoneName, index) => (
-
{
- setSelectedZone({
- zoneName,
- ...zonesData[zoneName],
- });
- }}
- >
- {zoneName}
-
- ))}
-
+
{!isPlaying && (
diff --git a/app/src/components/ui/inputs/MultiLevelDropDown.tsx b/app/src/components/ui/inputs/MultiLevelDropDown.tsx
index 215bbd8..477111a 100644
--- a/app/src/components/ui/inputs/MultiLevelDropDown.tsx
+++ b/app/src/components/ui/inputs/MultiLevelDropDown.tsx
@@ -1,5 +1,4 @@
-import React, { useState } from "react";
-
+import React, { useState, useRef, useEffect } from "react";
// Dropdown Item Component
const DropdownItem = ({
@@ -27,9 +26,11 @@ const DropdownItem = ({
const NestedDropdown = ({
label,
children,
+ onSelect,
}: {
label: string;
children: React.ReactNode;
+ onSelect: (selectedLabel: string) => void;
}) => {
const [open, setOpen] = useState(false);
@@ -37,36 +38,47 @@ const NestedDropdown = ({
{/* Dropdown Trigger */}
setOpen(!open)} // Toggle submenu on click
>
{label} {open ? "▼" : "▶"}
{/* Submenu */}
- {open &&
{children}
}
+ {open && (
+
+ {React.Children.map(children, (child) =>
+ React.cloneElement(child as React.ReactElement, { onSelect })
+ )}
+
+ )}
);
};
// Recursive Function to Render Nested Data
-const renderNestedData = (data: Record) => {
+const renderNestedData = (
+ data: Record,
+ onSelect: (selectedLabel: string) => void
+) => {
return Object.entries(data).map(([key, value]) => {
if (typeof value === "object" && !Array.isArray(value)) {
// If the value is an object, render it as a nested dropdown
return (
-
- {renderNestedData(value)}
+
+ {renderNestedData(value, onSelect)}
);
} else if (Array.isArray(value)) {
// If the value is an array, render each item as a dropdown item
return value.map((item, index) => (
-
+ onSelect(item)} />
));
} else {
// If the value is a simple string, render it as a dropdown item
- return ;
+ return (
+ onSelect(value)} />
+ );
}
});
};
@@ -74,21 +86,52 @@ const renderNestedData = (data: Record) => {
// Main Multi-Level Dropdown Component
const MultiLevelDropdown = ({ data }: { data: Record }) => {
const [open, setOpen] = useState(false);
+ const [selectedLabel, setSelectedLabel] = useState("Dropdown trigger");
+ const dropdownRef = useRef(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 (
-
+
{/* Dropdown Trigger Button */}
{/* Dropdown Menu */}
- {open &&
{renderNestedData(data)}
}
+ {open && (
+
+
+ {renderNestedData(data, handleSelect)}
+
+
+ )}
);
};
-export default MultiLevelDropdown;
\ No newline at end of file
+export default MultiLevelDropdown;
diff --git a/app/src/modules/visualization/handleSaveTemplate.ts b/app/src/modules/visualization/handleSaveTemplate.ts
index 290395a..c489688 100644
--- a/app/src/modules/visualization/handleSaveTemplate.ts
+++ b/app/src/modules/visualization/handleSaveTemplate.ts
@@ -2,35 +2,73 @@ import { Template } from "../../store/useTemplateStore";
import { captureVisualization } from "./captureVisualization";
type HandleSaveTemplateProps = {
- addTemplate: (template: Template) => void;
- selectedZone: {
- panelOrder: string[]; // Adjust the type based on actual data structure
- widgets: any[]; // Replace `any` with the actual widget type
- };
+ addTemplate: (template: Template) => void;
+ selectedZone: {
+ panelOrder: string[]; // Adjust the type based on actual data structure
+ widgets: any[]; // Replace `any` with the actual widget type
+ };
+ templates?: Template[];
};
// Generate a unique ID (placeholder function)
const generateUniqueId = (): string => {
- return Math.random().toString(36).substring(2, 15);
+ return `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
};
// Refactored function
export const handleSaveTemplate = async ({
- addTemplate,
- selectedZone,
+ addTemplate,
+ selectedZone,
+ templates = [],
}: HandleSaveTemplateProps): Promise
=> {
- try {
- const snapshot = await captureVisualization();
- const template: Template = {
- id: generateUniqueId(),
- name: `Template ${Date.now()}`,
- panelOrder: selectedZone.panelOrder,
- widgets: selectedZone.widgets,
- snapshot,
- };
- console.log('template: ', template);
- addTemplate(template);
- } catch (error) {
- console.error('Failed to save template:', error);
+ try {
+ // Check if the selected zone has any widgets
+ if (!selectedZone.widgets || selectedZone.widgets.length === 0) {
+ console.warn("Cannot save an empty template.");
+ return;
}
+
+ // 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);
+ }
};
diff --git a/app/src/styles/abstracts/variables.scss b/app/src/styles/abstracts/variables.scss
index eaab86b..b5244a6 100644
--- a/app/src/styles/abstracts/variables.scss
+++ b/app/src/styles/abstracts/variables.scss
@@ -35,7 +35,7 @@ $highlight-accent-color-dark: #403e6a; // Highlighted accent for dark mode
$background-color: #fcfdfd; // Main background color
$background-color-dark: #19191d; // Main background color for dark mode
$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-color: #e0dfff; // Default border color
diff --git a/app/src/styles/components/_regularDropDown.scss b/app/src/styles/components/_regularDropDown.scss
index 085a5cd..86ac078 100644
--- a/app/src/styles/components/_regularDropDown.scss
+++ b/app/src/styles/components/_regularDropDown.scss
@@ -36,9 +36,11 @@
overflow-y: auto; // Optional: Enable scrolling if content exceeds height
left: 0;
top: 104%;
+
.option {
padding: 5px;
cursor: pointer;
+ flex-direction: row !important;
&:hover {
background-color: var(--primary-color); // Optional: Hover effect
diff --git a/app/src/styles/components/visualization/floating/energyConsumed.scss b/app/src/styles/components/visualization/floating/energyConsumed.scss
new file mode 100644
index 0000000..daec86c
--- /dev/null
+++ b/app/src/styles/components/visualization/floating/energyConsumed.scss
@@ -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;
+}
\ No newline at end of file
diff --git a/app/src/styles/layout/sidebar.scss b/app/src/styles/layout/sidebar.scss
index 0daeca3..54ebf0d 100644
--- a/app/src/styles/layout/sidebar.scss
+++ b/app/src/styles/layout/sidebar.scss
@@ -78,14 +78,14 @@
display: flex;
flex-direction: column;
gap: 8px;
- padding-right: 6px;
+ padding: 6px;
flex-wrap: nowrap;
overflow: auto;
.chart {
min-height: 170px;
- background: var(--background-primary, #fcfdfd);
- border: 1.23px solid var(--Grays-Gray-5, #e5e5ea);
+ 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;
@@ -107,7 +107,7 @@
.stock {
padding: 13px 5px;
- background-color: #e0dfff80;
+ background-color: var(--background-color-secondary);
border-radius: 6.33px;
display: flex;
justify-content: space-between;
@@ -291,8 +291,46 @@
gap: 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 {
- color: #5c87df;
+ color: var(--accent-color);
border-bottom: 1px solid var(--border-color);
padding-bottom: 6px;
}
@@ -388,7 +426,7 @@
width: 100%;
height: 150px;
background: #f0f0f0;
- border-radius: 8px;
+ // border-radius: 8px;
}
.optionsContainer {
@@ -523,93 +561,119 @@
}
}
-/* Base styles */
+
+
+
+
+
+
+
+
+
+
+
+
.multi-level-dropdown {
position: relative;
display: inline-block;
- text-align: left;
.dropdown-button {
- background-color: #3b82f6; /* Blue background */
- color: white;
- padding: 0.5rem 1rem;
- font-size: 0.875rem;
- border: none;
- border-radius: 0.375rem;
+ width: 100%;
+ background-color: var(--background-color) !important;
+ border: 1px solid var(--border-color) !important;
+ padding: 5px 10px;
+
+
+ // font-size: 12px;
cursor: pointer;
- display: flex;
- align-items: center;
- transition: background-color 0.2s ease;
+ border-radius: 5px;
+ transition: background-color 0.3s ease;
&:hover {
- background-color: #2563eb; /* Darker blue on hover */
+ background-color: #333333;
}
- .icon {
- margin-left: 0.5rem;
+ &.open {
+ background-color: #333333;
}
}
.dropdown-menu {
position: absolute;
- top: calc(100% + 0.5rem); /* Add spacing below the button */
+ top: 100%;
left: 0;
- 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: 10;
- }
-}
+ background-color: #ffffff;
+ border: 1px solid #cccccc;
+ border-radius: 5px;
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+ z-index: 1000;
+ min-width: 200px;
+ overflow: auto;
+ max-height: 600px;
-/* Dropdown Item */
-.dropdown-item {
- display: block;
- padding: 0.5rem 1rem;
- font-size: 0.875rem;
- color: #4b5563; /* Gray text */
- text-decoration: none;
- cursor: pointer;
- transition: background-color 0.2s ease;
+ .dropdown-content {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
- &:hover {
- background-color: #f3f4f6; /* Light gray background on hover */
- }
-}
+ .nested-dropdown {
+ // &:first-child{
+ margin-left: 0;
+ // }
+ }
-/* Nested Dropdown */
-.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 */
+ padding: 10px;
}
- .icon {
- margin-left: 0.5rem;
+ .dropdown-item {
+ 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;
- }
-}
+}
\ No newline at end of file
diff --git a/app/src/styles/main.scss b/app/src/styles/main.scss
index 68688f6..90f6419 100644
--- a/app/src/styles/main.scss
+++ b/app/src/styles/main.scss
@@ -19,6 +19,7 @@
@use 'components/templates';
@use 'components/tools';
@use 'components/regularDropDown';
+@use 'components/visualization/floating/energyConsumed';
// layout
@use 'layout/sidebar';
diff --git a/app/src/styles/pages/realTimeViz.scss b/app/src/styles/pages/realTimeViz.scss
index 828cc7b..a7c1181 100644
--- a/app/src/styles/pages/realTimeViz.scss
+++ b/app/src/styles/pages/realTimeViz.scss
@@ -4,7 +4,7 @@
.realTime-viz {
background-color: var(--background-color);
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));
height: 600px;
position: absolute;
@@ -30,7 +30,7 @@
.zoon-wrapper {
display: flex;
- background-color: #e0dfff80;
+ background-color: var(--background-color);
position: absolute;
bottom: 10px;
left: 50%;
@@ -40,14 +40,14 @@
border-radius: 8px;
max-width: 80%;
overflow: auto;
-
+ max-width: calc(100% - 450px);
&::-webkit-scrollbar {
display: none;
}
.zone {
width: auto;
- background-color: #fcfdfd;
+ background-color: var(--background-color);
border-radius: 6px;
padding: 4px 8px;
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 {
display: flex;
height: 100vh;
@@ -189,6 +152,7 @@
display: flex;
flex-direction: column;
gap: 10px;
+ background-color: var(--background-color);
&::-webkit-scrollbar {
display: none;
@@ -216,18 +180,23 @@
}
}
+
&.top-panel,
&.bottom-panel {
left: 0;
right: 0;
+ .fullScreen {
+ background-color: red;
+ }
+
.panel-content {
display: flex;
flex-direction: row;
.chart-container {
height: 100%;
- width: 230px;
+ width: 200px;
}
}
}
@@ -247,7 +216,7 @@
.chart-container {
width: 100%;
- height: 200px;
+ height: 180px;
}
}
@@ -275,6 +244,15 @@
.icon {
display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 18px;
+ height: 18px;
+ border-radius: 4px;
+ }
+
+ .active {
+ background-color: var(--accent-color);
}
&:hover {
@@ -409,4 +387,4 @@
}
}
}
-}
+}
\ No newline at end of file