diff --git a/app/package-lock.json b/app/package-lock.json index 8dfd6e7..45a89a1 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -8,6 +8,7 @@ "name": "react", "version": "0.0.0", "dependencies": { + "chart.js": "^4.4.8", "path": "^0.12.7", "react": "^19.0.0", "react-dom": "^19.0.0", @@ -1029,6 +1030,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2159,6 +2166,18 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chart.js": { + "version": "4.4.8", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.8.tgz", + "integrity": "sha512-IkGZlVpXP+83QpMm4uxEiGqSI7jFizwVtF3+n5Pc3k7sMO+tkd0qxh2OzLhenM0K80xtmAONWGBn082EiBQSDA==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, "node_modules/chokidar": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", diff --git a/app/package.json b/app/package.json index 07751c7..abba5c4 100644 --- a/app/package.json +++ b/app/package.json @@ -10,6 +10,7 @@ "preview": "vite preview" }, "dependencies": { + "chart.js": "^4.4.8", "path": "^0.12.7", "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/app/src/components/icons/ExportModuleIcons.tsx b/app/src/components/icons/ExportModuleIcons.tsx index e2547d4..ffa64c4 100644 --- a/app/src/components/icons/ExportModuleIcons.tsx +++ b/app/src/components/icons/ExportModuleIcons.tsx @@ -100,3 +100,34 @@ export function VisualizationIcon({ isActive }: { isActive: boolean }) { ); } + +export function CartIcon({ isActive }: { isActive: boolean }) { + return ( + + + + + + + ); +} diff --git a/app/src/components/icons/RealTimeVisulationIcons.tsx b/app/src/components/icons/RealTimeVisulationIcons.tsx new file mode 100644 index 0000000..0174eda --- /dev/null +++ b/app/src/components/icons/RealTimeVisulationIcons.tsx @@ -0,0 +1,162 @@ +export function CleanPannel() { + return ( + + + + + + + + + + + + + + + + + ); +} + +export function EyeIcon() { + return ( + + + + + ); +} + +export function LockIcon() { + return ( + + + + ); +} + +export function PlayIcon() { + return ( + + + + ); +} + +export function CommentIcon() { + return ( + + + + + + ); +} + +export function SaveTeemplateIcon() { + return ( + + + + + ); +} diff --git a/app/src/components/layout/sidebarLeft/SideBarLeft.tsx b/app/src/components/layout/sidebarLeft/SideBarLeft.tsx index f6b0842..fb7496a 100644 --- a/app/src/components/layout/sidebarLeft/SideBarLeft.tsx +++ b/app/src/components/layout/sidebarLeft/SideBarLeft.tsx @@ -5,21 +5,31 @@ import Header from "./Header"; import useToggleStore from "../../../store/useUIToggleStore"; import Assets from "./Assets"; import useModuleStore from "../../../store/useModuleStore"; +import Widgets from "./visualization/widgets/Widgets"; +import Templates from "./visualization/Templates"; +import Search from "../../ui/inputs/Search"; const SideBarLeft: React.FC = () => { - const [activeOption, setActiveOption] = useState("Outline"); + const [activeOption, setActiveOption] = useState("Widgets"); const { toggleUI } = useToggleStore(); const { activeModule } = useModuleStore(); - // Reset activeList whenever activeModule changes + // Reset activeOption whenever activeModule changes useEffect(() => { setActiveOption("Outline"); + if (activeModule === "visualization") setActiveOption("Widgets"); }, [activeModule]); const handleToggleClick = (option: string) => { setActiveOption(option); // Update the active option }; + + const handleSearchChange = (value: string) => { + // Log the search value for now + console.log(value); + }; + return (
@@ -28,11 +38,17 @@ const SideBarLeft: React.FC = () => { {activeModule === "visualization" ? ( <> + +
+ {activeOption === "Widgets" ? : } +
+ ) : activeModule === "market" ? ( + <> ) : ( <> { + const { templates, removeTemplate } = useTemplateStore(); + const { setSelectedZone } = useSelectedZoneStore(); + + console.log('templates: ', templates); + const handleDeleteTemplate = (id: string) => { + removeTemplate(id); + }; + + const handleLoadTemplate = (template: any) => { + setSelectedZone((prev) => ({ + ...prev, + panelOrder: template.panelOrder, + activeSides: Array.from(new Set([...prev.activeSides, ...template.panelOrder])), + widgets: template.widgets, + })); + }; + + return ( +
+ {templates.map((template) => ( +
+ {template.snapshot && ( +
{/* 16:9 aspect ratio */} + {`${template.name} handleLoadTemplate(template)} + /> +
+ )} +
+
handleLoadTemplate(template)} + style={{ + cursor: 'pointer', + fontWeight: '500', + // ':hover': { + // textDecoration: 'underline' + // } + }} + > + {template.name} +
+ +
+
+ ))} + {templates.length === 0 && ( +
+ No saved templates yet. Create one in the visualization view! +
+ )} +
+ ); +}; + +export default Templates; + diff --git a/app/src/components/layout/sidebarLeft/visualization/widgets/ChartComponent.tsx b/app/src/components/layout/sidebarLeft/visualization/widgets/ChartComponent.tsx new file mode 100644 index 0000000..39da08d --- /dev/null +++ b/app/src/components/layout/sidebarLeft/visualization/widgets/ChartComponent.tsx @@ -0,0 +1,130 @@ +import React, { useEffect, useRef, useMemo } from "react"; +import { Chart } from "chart.js/auto"; +import { useThemeStore } from "../../../../../store/useThemeStore"; + +// Define Props Interface +interface ChartComponentProps { + type: any; // Type of chart (e.g., "bar", "line", etc.) + title: string; // Title of the chart + fontFamily?: string; // Optional font family for the chart title + fontSize?: string; // Optional font size for the chart title + fontWeight?: "Light" | "Regular" | "Bold"; // Optional font weight for the chart title + data: { + labels: string[]; // Labels for the x-axis + datasets: { + data: number[]; // Data points for the chart + backgroundColor: string; // Background color for the chart + borderColor: string; // Border color for the chart + borderWidth: number; // Border width for the chart + }[]; + }; // Data for the chart +} + +const ChartComponent = ({ + type, + title, + fontFamily, + fontSize, + fontWeight = "Regular", // Default to "Regular" + data: propsData, +}: ChartComponentProps) => { + const canvasRef = useRef(null); + const { themeColor } = useThemeStore(); + + // Memoize Theme Colors to Prevent Unnecessary Recalculations + const buttonActionColor = useMemo( + () => themeColor[0] || "#5c87df", + [themeColor] + ); + const buttonAbortColor = useMemo( + () => themeColor[1] || "#ffffff", + [themeColor] + ); + + // 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, + color: "#2B3344", + }), + [fontFamily, fontSizeValue, fontWeightValue] + ); + + // Memoize Chart Data + const data = useMemo(() => propsData, [propsData]); + + // Memoize Chart Options + const options = useMemo( + () => ({ + responsive: true, + maintainAspectRatio: false, + plugins: { + title: { + display: true, + text: title, + font: chartFontStyle, + align: "start", // Left align the title + padding: { + top: 10, // Add padding above the title + bottom: 20, // Add padding between the title and the chart + }, + }, + legend: { + display: false, + }, + }, + }), + [title, chartFontStyle] + ); + + // Initialize Chart on Component Mount + useEffect(() => { + if (!canvasRef.current) return; + + const ctx = canvasRef.current.getContext("2d"); + if (!ctx) return; + + const chart = new Chart(ctx, { type, data, options }); + + // Cleanup: Destroy the chart instance when the component unmounts + return () => chart.destroy(); + }, [type, data, options]); // Only recreate the chart when these dependencies change + + return ; +}; + +export default React.memo(ChartComponent, (prevProps, nextProps) => { + // Custom comparison function to prevent unnecessary re-renders + return ( + prevProps.type === nextProps.type && + prevProps.title === nextProps.title && + prevProps.fontFamily === nextProps.fontFamily && + prevProps.fontSize === nextProps.fontSize && + prevProps.fontWeight === nextProps.fontWeight && + JSON.stringify(prevProps.data) === JSON.stringify(nextProps.data) + ); +}); diff --git a/app/src/components/layout/sidebarLeft/visualization/widgets/Widgets.tsx b/app/src/components/layout/sidebarLeft/visualization/widgets/Widgets.tsx new file mode 100644 index 0000000..3fde4c8 --- /dev/null +++ b/app/src/components/layout/sidebarLeft/visualization/widgets/Widgets.tsx @@ -0,0 +1,28 @@ +import { useState } from "react"; +import ToggleHeader from "../../../../ui/inputs/ToggleHeader"; +import Widgets2D from "./Widgets2D"; +import Widgets3D from "./Widgets3D"; +import WidgetsTemplate from "./WidgetsTemplate"; + +const Widgets = () => { + const [activeOption, setActiveOption] = useState("2D"); + + const handleToggleClick = (option: string) => { + setActiveOption(option); + }; + + return ( +
+ + {activeOption === "2D" && } + {activeOption === "3D" && } + {activeOption === "Templates" && } +
+ ); +}; + +export default Widgets; diff --git a/app/src/components/layout/sidebarLeft/visualization/widgets/Widgets2D.tsx b/app/src/components/layout/sidebarLeft/visualization/widgets/Widgets2D.tsx new file mode 100644 index 0000000..eaa453e --- /dev/null +++ b/app/src/components/layout/sidebarLeft/visualization/widgets/Widgets2D.tsx @@ -0,0 +1,138 @@ +import React from "react"; +import { useWidgetStore } from "../../../../../store/useWidgetStore"; +import { ChartType } from "chart.js/auto"; +import ChartComponent from "./ChartComponent"; + +const chartTypes: ChartType[] = [ + "bar", + "line", + "pie", + "doughnut", + "radar", + "polarArea", +]; + +const sampleData = { + labels: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul"], + datasets: [ + { + data: [65, 59, 80, 81, 56, 55, 40], + backgroundColor: "#6f42c1", + borderColor: "#ffffff", + borderWidth: 1, + }, + ], +}; + +interface WidgetProps { + type: ChartType; + index: number; + title: string; +} + +const ChartWidget: React.FC = ({ type, index, title }) => { + const { setDraggedAsset } = useWidgetStore((state) => state); + + return ( +
{ + setDraggedAsset({ + type, + id: `widget-${index + 1}`, + title, + panel: "top", + data: sampleData, + }); + }} + onDragEnd={() => setDraggedAsset(null)} + > + +
+ ); +}; + +const ProgressBarWidget = ({ + id, + title, + data, +}: { + id: string; + title: string; + data: any; +}) => { + const { setDraggedAsset } = useWidgetStore((state) => state); + + return ( +
{ + setDraggedAsset({ + type: "progress", + id, + title, + panel: "top", + data, + }); + }} + onDragEnd={() => setDraggedAsset(null)} + > +
{title}
+ {data.stocks.map((stock: any, index: number) => ( +
+ + +
{stock.key}
+
{stock.value}
+
+
{stock.description}
+
+
Icon
+
+ ))} +
+ ); +}; + +const Widgets2D = () => { + return ( +
+
+ {chartTypes.map((type, index) => { + const widgetTitle = `Widget ${index + 1}`; + return ( + + ); + })} + + +
+
+ ); +}; + +export default Widgets2D; diff --git a/app/src/components/layout/sidebarLeft/visualization/widgets/Widgets3D.tsx b/app/src/components/layout/sidebarLeft/visualization/widgets/Widgets3D.tsx new file mode 100644 index 0000000..9a14410 --- /dev/null +++ b/app/src/components/layout/sidebarLeft/visualization/widgets/Widgets3D.tsx @@ -0,0 +1,11 @@ +import React from 'react' + +const Widgets3D = () => { + return ( +
+ Widgets3D +
+ ) +} + +export default Widgets3D diff --git a/app/src/components/layout/sidebarLeft/visualization/widgets/WidgetsTemplate.tsx b/app/src/components/layout/sidebarLeft/visualization/widgets/WidgetsTemplate.tsx new file mode 100644 index 0000000..14ab07e --- /dev/null +++ b/app/src/components/layout/sidebarLeft/visualization/widgets/WidgetsTemplate.tsx @@ -0,0 +1,12 @@ +import React from 'react' + +const WidgetsTemplate = () => { + return ( +
+ WidgetsTemplate + +
+ ) +} + +export default WidgetsTemplate diff --git a/app/src/components/layout/sidebarRight/SideBarRight.tsx b/app/src/components/layout/sidebarRight/SideBarRight.tsx index 9cac07b..db44f69 100644 --- a/app/src/components/layout/sidebarRight/SideBarRight.tsx +++ b/app/src/components/layout/sidebarRight/SideBarRight.tsx @@ -8,6 +8,7 @@ import { } from "../../icons/SimulationIcons"; import useToggleStore from "../../../store/useUIToggleStore"; import MachineMechanics from "./mechanics/MachineMechanics"; +import Visualization from "./visualization/Visualization"; const SideBarRight: React.FC = () => { const { activeModule } = useModuleStore(); @@ -54,13 +55,17 @@ const SideBarRight: React.FC = () => { )}
)} - {toggleUI && ( + {/* process builder */} + {toggleUI && activeModule === "builder" && (
)} + + {/* realtime visualization */} + {activeModule === "visualization" && } ); }; diff --git a/app/src/components/layout/sidebarRight/visualization/Visualization.tsx b/app/src/components/layout/sidebarRight/visualization/Visualization.tsx new file mode 100644 index 0000000..c29aef2 --- /dev/null +++ b/app/src/components/layout/sidebarRight/visualization/Visualization.tsx @@ -0,0 +1,28 @@ +import { useState } from "react"; +import Search from "../../../ui/inputs/Search"; +import ToggleHeader from "../../../ui/inputs/ToggleHeader"; +import Data from "./data/Data"; +import Design from "./design/Design"; + +const Visualization = () => { + const [activeOption, setActiveOption] = useState("Data"); + + const handleToggleClick = (option: string) => { + setActiveOption(option); // Update the active option + }; + + return ( +
+ +
+ {activeOption === "Data" ? : } +
+
+ ); +}; + +export default Visualization; diff --git a/app/src/components/layout/sidebarRight/visualization/data/Data.tsx b/app/src/components/layout/sidebarRight/visualization/data/Data.tsx new file mode 100644 index 0000000..d1122bb --- /dev/null +++ b/app/src/components/layout/sidebarRight/visualization/data/Data.tsx @@ -0,0 +1,138 @@ +import React, { useEffect, useState } from "react"; +import { useWidgetStore } from "../../../../../store/useWidgetStore"; +import { RemoveIcon } from "../../../../icons/ExportCommonIcons"; +import RegularDropDown from "../../../../ui/inputs/RegularDropDown"; +import MultiLevelDropDown from "../../../../ui/inputs/MultiLevelDropDown"; + +interface Child { + id: number; + easing: string; +} +const DATA_STRUCTURE = { + furnace: { + coolingRate: "coolingRate", + furnaceTemperature: "furnaceTemperature", + heatingRate: "heatingRate", + machineId: "machineId", + powerConsumption: "powerConsumption", + status: "status", + timestamp: "timestamp", + vacuumLevel: "vacuumLevel", + }, + testDevice: { + abrasiveLevel: { + data1: "Data 1", + data2: "Data 2", + data3: "Data 3", + }, + airPressure: "airPressure", + machineId: "machineId", + powerConsumption: "powerConsumption", + status: "status", + temperature: { + data1: "Data 1", + data2: "Data 2", + data3: "Data 3", + }, + timestamp: { + data1: "Data 1", + data2: "Data 2", + data3: "Data 3", + }, + }, +}; + +interface Group { + id: number; + easing: string; + children: Child[]; +} + +const Data = () => { + const { selectedChartId } = useWidgetStore(); + + // State to store groups for all widgets (using Widget.id as keys) + const [chartDataGroups, setChartDataGroups] = useState< + Record + >({}); + + useEffect(() => { + // Initialize data groups for the newly selected widget if it doesn't exist + if (selectedChartId && !chartDataGroups[selectedChartId.id]) { + setChartDataGroups((prev) => ({ + ...prev, + [selectedChartId.id]: [ + { + id: Date.now(), + easing: "Connecter 1", + children: [ + { id: Date.now(), easing: "Linear" }, + { id: Date.now() + 1, easing: "Ease Out" }, + { id: Date.now() + 2, easing: "Linear" }, + ], + }, + ], + })); + } + }, [selectedChartId]); + + // Handle adding a new child to the group + const handleAddClick = (groupId: number) => { + setChartDataGroups((prevGroups) => { + const currentGroups = prevGroups[selectedChartId.id] || []; + const group = currentGroups.find((g) => g.id === groupId); + + if (group && group.children.length < 7) { + const newChild = { id: Date.now(), easing: "Linear" }; + return { + ...prevGroups, + [selectedChartId.id]: currentGroups.map((g) => + g.id === groupId ? { ...g, children: [...g.children, newChild] } : g + ), + }; + } + return prevGroups; + }); + }; + + // Remove a child from a group + const removeChild = (groupId: number, childId: number) => { + setChartDataGroups((currentGroups) => { + const currentChartData = currentGroups[selectedChartId.id] || []; + + return { + ...currentGroups, + [selectedChartId.id]: currentChartData.map((group) => + group.id === groupId + ? { + ...group, + children: group.children.filter( + (child) => child.id !== childId + ), + } + : group + ), + }; + }); + }; + + return ( +
+ {selectedChartId?.title && ( +
{selectedChartId?.title}
+ )} + {/* */} +
+ i +

+ + By adding templates and widgets, you create a customizable and + dynamic environment. + +

+
+
+ ); +}; + +export default Data; diff --git a/app/src/components/layout/sidebarRight/visualization/design/Design.tsx b/app/src/components/layout/sidebarRight/visualization/design/Design.tsx new file mode 100644 index 0000000..93332dd --- /dev/null +++ b/app/src/components/layout/sidebarRight/visualization/design/Design.tsx @@ -0,0 +1,199 @@ +import React, { useState } from "react"; +import { useWidgetStore } from "../../../../../store/useWidgetStore"; +import ChartComponent from "../../../sidebarLeft/visualization/widgets/ChartComponent"; +import RegularDropDown from "../../../../ui/inputs/RegularDropDown"; + +// Define Props Interface +interface Widget { + id: string; + type: string; // Chart type (e.g., "bar", "line") + panel: "top" | "bottom" | "left" | "right"; // Panel location + title: string; + fontFamily?: string; + fontSize?: string; + fontWeight?: string; + data: { + labels: string[]; + datasets: { + data: number[]; + backgroundColor: string; + borderColor: string; + borderWidth: number; + }[]; + }; // Data for the chart +} + +const Design = () => { + const [selectedName, setSelectedName] = useState("drop down"); + const [selectedElement, setSelectedElement] = useState("drop down"); + const [selectedFont, setSelectedFont] = useState("drop down"); + const [selectedSize, setSelectedSize] = useState("drop down"); + const [selectedWeight, setSelectedWeight] = useState("drop down"); + const [elementColor, setElementColor] = useState("#6f42c1"); // Default color for elements + const [showColorPicker, setShowColorPicker] = useState(false); // Manage visibility of the color picker + + // Zustand Store Hooks + const { selectedChartId, setSelectedChartId, widgets, setWidgets } = + useWidgetStore(); + + // Handle Widget Updates + const handleUpdateWidget = (updatedProperties: Partial) => { + if (!selectedChartId) return; + + // Update the selectedChartId + const updatedChartId = { + ...selectedChartId, + ...updatedProperties, + }; + setSelectedChartId(updatedChartId); + + // Update the widgets array + const updatedWidgets = widgets.map((widget) => + widget.id === selectedChartId.id + ? { ...widget, ...updatedProperties } + : widget + ); + setWidgets(updatedWidgets); + }; + + // Default Chart Data + const defaultChartData = { + labels: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul"], + datasets: [ + { + data: [65, 59, 80, 81, 56, 55, 40], + backgroundColor: elementColor, // Default background color + borderColor: "#ffffff", // Default border color + borderWidth: 1, + }, + ], + }; + + return ( +
+ {/* Title of the Selected Widget */} +
+ {selectedChartId?.title || "Widget 1"} +
+ + {/* Chart Component */} +
+ {selectedChartId && ( + + )} +
+ + {/* Options Container */} +
+ {/* Name Dropdown */} +
+ Name + { + setSelectedName(value); + handleUpdateWidget({ title: value }); + }} + /> +
+ + {/* Element Dropdown */} +
+ Element + { + setSelectedElement(value); + handleUpdateWidget({ type: value }); + }} + /> +
+ + {/* Font Family Dropdown */} +
+ Font Family + { + setSelectedFont(value); + handleUpdateWidget({ fontFamily: value }); + }} + /> +
+ + {/* Size Dropdown */} +
+ Size + { + setSelectedSize(value); + handleUpdateWidget({ fontSize: value }); + }} + /> +
+ + {/* Weight Dropdown */} +
+ Weight + { + setSelectedWeight(value); + handleUpdateWidget({ fontWeight: value }); + }} + /> +
+ + {/* Element Color Picker */} +
+
setShowColorPicker((prev) => !prev)} + > + Element Color +
{" "} + {/* Change icon based on the visibility */} +
+ + {/* Show color picker only when 'showColorPicker' is true */} + {showColorPicker && ( +
+ { + setElementColor(e.target.value); + handleUpdateWidget({ + data: { + labels: selectedChartId?.data?.labels || [], + datasets: [ + { + ...selectedChartId?.data?.datasets[0], + backgroundColor: e.target.value, // Update the element color + }, + ], + }, + }); + }} + /> + {/* Display the selected color value */} + {elementColor} +
+ )} +
+
+
+ ); +}; + +export default Design; diff --git a/app/src/components/ui/ModuleToggle.tsx b/app/src/components/ui/ModuleToggle.tsx index cbe64e3..bea9c43 100644 --- a/app/src/components/ui/ModuleToggle.tsx +++ b/app/src/components/ui/ModuleToggle.tsx @@ -2,6 +2,7 @@ import React from "react"; import useModuleStore from "../../store/useModuleStore"; import { BuilderIcon, + CartIcon, SimulationIcon, VisualizationIcon, } from "../icons/ExportModuleIcons"; @@ -40,6 +41,17 @@ const ModuleToggle: React.FC = () => {
Visualization
+
setActiveModule("market")} + > +
+ +
+
Market Place
+
); }; diff --git a/app/src/components/ui/componets/AddButtons.tsx b/app/src/components/ui/componets/AddButtons.tsx new file mode 100644 index 0000000..31110a6 --- /dev/null +++ b/app/src/components/ui/componets/AddButtons.tsx @@ -0,0 +1,187 @@ +import React from "react"; +import { CleanPannel, EyeIcon, LockIcon } from "../../icons/RealTimeVisulationIcons"; + +// Define the type for `Side` +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 + activeSides: Side[]; + panelOrder: Side[]; + lockedPanels: Side[]; + widgets: { + id: string; + type: string; + title: string; + panel: Side; + data: any; + }[]; + }; + setSelectedZone: React.Dispatch< + React.SetStateAction<{ + zoneName: string; // Ensure zoneName is included in the state type + activeSides: Side[]; + panelOrder: Side[]; + lockedPanels: Side[]; + widgets: { + id: string; + type: string; + title: string; + panel: Side; + data: any; + }[]; + }> + >; +} + +const AddButtons: React.FC = ({ + selectedZone, + setSelectedZone, +}) => { + // Function to toggle lock/unlock a panel + const toggleLockPanel = (side: Side) => { + const newLockedPanels = selectedZone.lockedPanels.includes(side) + ? selectedZone.lockedPanels.filter((panel) => panel !== side) + : [...selectedZone.lockedPanels, side]; + + const updatedZone = { + ...selectedZone, + lockedPanels: newLockedPanels, + }; + + // Update the selectedZone state + setSelectedZone(updatedZone); + }; + + // 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); + }; + + // Function to clean all widgets from a panel + const cleanPanel = (side: Side) => { + const cleanedWidgets = selectedZone.widgets.filter( + (widget) => widget.panel !== side + ); + + const updatedZone = { + ...selectedZone, + widgets: cleanedWidgets, + }; + + // Update the selectedZone state + setSelectedZone(updatedZone); + }; + + // Function to handle "+" button click + const handlePlusButtonClick = (side: Side) => { + if (selectedZone.activeSides.includes(side)) { + // If the panel is already active, remove all widgets and close the panel + const cleanedWidgets = selectedZone.widgets.filter( + (widget) => widget.panel !== side + ); + const newActiveSides = selectedZone.activeSides.filter((s) => s !== side); + + const updatedZone = { + ...selectedZone, + widgets: cleanedWidgets, + activeSides: newActiveSides, + panelOrder: newActiveSides, + }; + + // Update the selectedZone state + setSelectedZone(updatedZone); + } else { + // If the panel is not active, activate it + const newActiveSides = [...selectedZone.activeSides, side]; + + const updatedZone = { + ...selectedZone, + activeSides: newActiveSides, + panelOrder: newActiveSides, + }; + + // Update the selectedZone state + setSelectedZone(updatedZone); + } + }; + + return ( +
+ {(["top", "right", "bottom", "left"] as Side[]).map((side) => ( +
+ {/* "+" Button */} + + + {/* Extra Buttons */} +
+ {/* Hide Panel */} +
toggleVisibility(side)} + > + +
+ + {/* Clean Panel */} +
cleanPanel(side)} + > + +
+ + {/* Lock/Unlock Panel */} +
toggleLockPanel(side)} + > + +
+
+
+ ))} +
+ ); +}; + +export default AddButtons; diff --git a/app/src/components/ui/componets/DraggableWidget.tsx b/app/src/components/ui/componets/DraggableWidget.tsx new file mode 100644 index 0000000..342f31f --- /dev/null +++ b/app/src/components/ui/componets/DraggableWidget.tsx @@ -0,0 +1,217 @@ +import { useMemo, useState } from "react"; +import ChartComponent from "../../layout/sidebarLeft/visualization/widgets/ChartComponent"; +import { useWidgetStore } from "../../../store/useWidgetStore"; + +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" ? ( + // + <> + ) : ( + + )} +
+ + {/* 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 new file mode 100644 index 0000000..502114b --- /dev/null +++ b/app/src/components/ui/componets/Panel.tsx @@ -0,0 +1,197 @@ +import React, { useEffect, useMemo, useRef, useState } from "react"; +import { useWidgetStore } from "../../../store/useWidgetStore"; +import { usePlayButtonStore } from "../../../store/usePlayButtonStore"; +import { DraggableWidget } from "./DraggableWidget"; + +type Side = "top" | "bottom" | "left" | "right"; + +interface Widget { + id: string; + type: string; + title: string; + panel: Side; + data: any; +} + +interface PanelProps { + selectedZone: { + zoneName: string; + activeSides: Side[]; + panelOrder: Side[]; + lockedPanels: Side[]; + widgets: Widget[]; + }; + setSelectedZone: React.Dispatch< + React.SetStateAction<{ + zoneName: string; + activeSides: Side[]; + panelOrder: Side[]; + lockedPanels: Side[]; + widgets: Widget[]; + }> + >; +} + +const generateUniqueId = () => + `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + +const Panel: React.FC = ({ selectedZone, setSelectedZone }) => { + const panelRefs = useRef<{ [side in Side]?: HTMLDivElement }>({}); + const [panelDimensions, setPanelDimensions] = useState<{ + [side in Side]?: { width: number; height: number }; + }>({}); + + const getPanelStyle = useMemo( + () => (side: Side) => { + const currentIndex = selectedZone.panelOrder.indexOf(side); + const previousPanels = selectedZone.panelOrder.slice(0, currentIndex); + const leftActive = previousPanels.includes("left"); + const rightActive = previousPanels.includes("right"); + const topActive = previousPanels.includes("top"); + const bottomActive = previousPanels.includes("bottom"); + + switch (side) { + case "top": + case "bottom": + return { + width: `calc(100% - ${ + (leftActive ? 204 : 0) + (rightActive ? 204 : 0) + }px)`, + left: leftActive ? "204px" : "0", + right: rightActive ? "204px" : "0", + [side]: "0", + height: "200px", + }; + case "left": + case "right": + return { + height: `calc(100% - ${ + (topActive ? 204 : 0) + (bottomActive ? 204 : 0) + }px)`, + top: topActive ? "204px" : "0", + bottom: bottomActive ? "204px" : "0", + [side]: "0", + width: "200px", + }; + default: + return {}; + } + }, + [selectedZone.panelOrder] + ); + + 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); + } + }; + + useEffect(() => { + const observers: ResizeObserver[] = []; + const currentPanelRefs = panelRefs.current; + + selectedZone.activeSides.forEach((side) => { + const element = currentPanelRefs[side]; + if (element) { + const observer = new ResizeObserver((entries) => { + for (const entry of entries) { + const { width, height } = entry.contentRect; + setPanelDimensions((prev) => ({ + ...prev, + [side]: { width, height }, + })); + } + }); + observer.observe(element); + observers.push(observer); + } + }); + + return () => { + observers.forEach((observer) => observer.disconnect()); + }; + }, [selectedZone.activeSides]); + + const { isPlaying } = usePlayButtonStore(); + + return ( + <> + {selectedZone.activeSides.map((side) => ( +
handleDrop(e, side)} + onDragOver={(e) => e.preventDefault()} + ref={(el) => { + if (el) { + panelRefs.current[side] = el; + } else { + delete panelRefs.current[side]; + } + }} + > +
+ {selectedZone.widgets + .filter((w) => w.panel === side) + .map((widget) => ( + + ))} +
+
+ ))} + + ); +}; + +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 new file mode 100644 index 0000000..5bb2687 --- /dev/null +++ b/app/src/components/ui/componets/RealTimeVisulization.tsx @@ -0,0 +1,207 @@ +import React, { useEffect, useState, useRef } from "react"; +import { usePlayButtonStore } from "../../../store/usePlayButtonStore"; +import Panel from "./Panel"; +import AddButtons from "./AddButtons"; +import { + CommentIcon, + PlayIcon, + SaveTeemplateIcon, +} from "../../icons/RealTimeVisulationIcons"; +import useTemplateStore from "../../../store/useTemplateStore"; +import { useSelectedZoneStore } from "../../../store/useZoneStore"; + +type Side = "top" | "bottom" | "left" | "right"; + +interface Widget { + id: string; + type: string; + title: string; + panel: Side; + data: any; +} + +const RealTimeVisulization: React.FC = () => { + const containerRef = useRef(null); + const [zonesData, setZonesData] = useState<{ + [key: string]: { + activeSides: Side[]; + panelOrder: Side[]; + lockedPanels: Side[]; + widgets: Widget[]; + }; + }>({ + "Manufacturing unit": { + activeSides: [], + panelOrder: [], + lockedPanels: [], + widgets: [], + }, + "Assembly unit": { + activeSides: [], + panelOrder: [], + lockedPanels: [], + widgets: [], + }, + "Packing unit": { + activeSides: [], + panelOrder: [], + lockedPanels: [], + widgets: [], + }, + Warehouse: { + activeSides: [], + panelOrder: [], + lockedPanels: [], + widgets: [], + }, + Inventory: { + activeSides: [], + panelOrder: [], + lockedPanels: [], + widgets: [], + }, + }); + + const { isPlaying, setIsPlaying } = usePlayButtonStore(); + const { addTemplate } = useTemplateStore(); + const { selectedZone, setSelectedZone } = useSelectedZoneStore(); + + const generateUniqueId = (): string => + `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + + const captureVisualization = async (): Promise => { + const container = containerRef.current; + if (!container) return null; + + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + if (!ctx) return null; + + const rect = container.getBoundingClientRect(); + canvas.width = rect.width; + canvas.height = rect.height; + + // Draw background + ctx.fillStyle = getComputedStyle(container).backgroundColor; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + // Capture all canvas elements + const canvases = container.querySelectorAll('canvas'); + canvases.forEach(childCanvas => { + const childRect = childCanvas.getBoundingClientRect(); + const x = childRect.left - rect.left; + const y = childRect.top - rect.top; + ctx.drawImage(childCanvas, x, y, childRect.width, childRect.height); + }); + + // Capture SVG elements + const svgs = container.querySelectorAll('svg'); + for (const svg of Array.from(svgs)) { + const svgString = new XMLSerializer().serializeToString(svg); + const svgBlob = new Blob([svgString], { type: 'image/svg+xml' }); + const url = URL.createObjectURL(svgBlob); + + try { + const img = await new Promise((resolve, reject) => { + const image = new Image(); + image.onload = () => resolve(image); + image.onerror = reject; + image.src = url; + }); + + const svgRect = svg.getBoundingClientRect(); + ctx.drawImage( + img, + svgRect.left - rect.left, + svgRect.top - rect.top, + svgRect.width, + svgRect.height + ); + } finally { + URL.revokeObjectURL(url); + } + } + + return canvas.toDataURL('image/png'); + }; + + const handleSaveTemplate = async () => { + const snapshot = await captureVisualization(); + const template = { + id: generateUniqueId(), + name: `Template ${Date.now()}`, + panelOrder: selectedZone.panelOrder, + widgets: selectedZone.widgets, + snapshot, + }; + addTemplate(template); + + }; + + useEffect(() => { + setZonesData((prev) => ({ + ...prev, + [selectedZone.zoneName]: selectedZone, + })); + }, [selectedZone]); + + return ( +
+
+
+ +
+
+ +
+
setIsPlaying(!isPlaying)} + > + +
+
+ +
+ {Object.keys(zonesData).map((zoneName, index) => ( +
{ + setSelectedZone({ + zoneName, + ...zonesData[zoneName], + }); + }} + > + {zoneName} +
+ ))} +
+ + {!isPlaying && ( + + )} + + +
+ ); +}; + +export default RealTimeVisulization; \ No newline at end of file diff --git a/app/src/components/ui/inputs/MultiLevelDropDown.tsx b/app/src/components/ui/inputs/MultiLevelDropDown.tsx new file mode 100644 index 0000000..215bbd8 --- /dev/null +++ b/app/src/components/ui/inputs/MultiLevelDropDown.tsx @@ -0,0 +1,94 @@ +import React, { useState } from "react"; + + +// Dropdown Item Component +const DropdownItem = ({ + label, + href, + onClick, +}: { + label: string; + href?: string; + onClick?: () => void; +}) => ( + { + e.preventDefault(); + onClick?.(); + }} + > + {label} + +); + +// Nested Dropdown Component +const NestedDropdown = ({ + label, + children, +}: { + label: string; + children: React.ReactNode; +}) => { + const [open, setOpen] = useState(false); + + return ( +
+ {/* Dropdown Trigger */} +
setOpen(!open)} // Toggle submenu on click + > + {label} {open ? "▼" : "▶"} +
+ + {/* Submenu */} + {open &&
{children}
} +
+ ); +}; + +// Recursive Function to Render Nested Data +const renderNestedData = (data: Record) => { + 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)} + + ); + } else if (Array.isArray(value)) { + // If the value is an array, render each item as a dropdown item + return value.map((item, index) => ( + + )); + } else { + // If the value is a simple string, render it as a dropdown item + return ; + } + }); +}; + +// Main Multi-Level Dropdown Component +const MultiLevelDropdown = ({ data }: { data: Record }) => { + const [open, setOpen] = useState(false); + + return ( +
+ {/* Dropdown Trigger Button */} + + + {/* Dropdown Menu */} + {open &&
{renderNestedData(data)}
} +
+ ); +}; + +export default MultiLevelDropdown; \ No newline at end of file diff --git a/app/src/components/ui/inputs/RegularDropDown.tsx b/app/src/components/ui/inputs/RegularDropDown.tsx new file mode 100644 index 0000000..318913f --- /dev/null +++ b/app/src/components/ui/inputs/RegularDropDown.tsx @@ -0,0 +1,82 @@ +import React, { useState, useEffect, useRef } from "react"; + +interface DropdownProps { + header: string; + options: string[]; + onSelect: (option: string) => void; +} + +const RegularDropDown: React.FC = ({ + header, + options, + onSelect, +}) => { + const [isOpen, setIsOpen] = useState(false); + const [selectedOption, setSelectedOption] = useState(null); + const dropdownRef = useRef(null); // Create a ref for the dropdown container + + // Reset selectedOption when the dropdown closes + useEffect(() => { + if (!isOpen) { + setSelectedOption(null); // Clear local state when the dropdown closes + } + }, [isOpen]); + + // Reset selectedOption when the header prop changes + useEffect(() => { + setSelectedOption(null); // Ensure the dropdown reflects the updated header + }, [header]); + + // Close dropdown if clicked outside + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setIsOpen(false); // Close the dropdown if clicked outside + } + }; + + document.addEventListener("click", handleClickOutside); + + // Cleanup the event listener on component unmount + return () => { + document.removeEventListener("click", handleClickOutside); + }; + }, []); + + const toggleDropdown = () => { + setIsOpen((prev) => !prev); + }; + + const handleOptionClick = (option: string) => { + setSelectedOption(option); + onSelect(option); // Call the onSelect function passed from the parent + setIsOpen(false); // Close the dropdown after selection + }; + + return ( +
+ {/* Dropdown Header */} +
+
{selectedOption || header}
+
+
+ + {/* Dropdown Options */} + {isOpen && ( +
+ {options.map((option, index) => ( +
handleOptionClick(option)} + > + {option} +
+ ))} +
+ )} +
+ ); +}; + +export default RegularDropDown; diff --git a/app/src/pages/Project.tsx b/app/src/pages/Project.tsx index 6e0b37b..29f8c6c 100644 --- a/app/src/pages/Project.tsx +++ b/app/src/pages/Project.tsx @@ -1,14 +1,19 @@ -import React from 'react'; -import ModuleToggle from '../components/ui/ModuleToggle'; -import SideBarLeft from '../components/layout/sidebarLeft/SideBarLeft'; -import SideBarRight from '../components/layout/sidebarRight/SideBarRight'; +import React from "react"; +import ModuleToggle from "../components/ui/ModuleToggle"; +import SideBarLeft from "../components/layout/sidebarLeft/SideBarLeft"; +import SideBarRight from "../components/layout/sidebarRight/SideBarRight"; +import useModuleStore from "../store/useModuleStore"; +import RealTimeVisulization from "../components/ui/componets/RealTimeVisulization"; const Project: React.FC = () => { + const { activeModule } = useModuleStore(); + return (
+ {activeModule === "visualization" && }
); }; diff --git a/app/src/store/useModuleStore.ts b/app/src/store/useModuleStore.ts index 5281c0b..6d26a8e 100644 --- a/app/src/store/useModuleStore.ts +++ b/app/src/store/useModuleStore.ts @@ -1,5 +1,5 @@ // store/useModuleStore.ts -import { create } from 'zustand'; +import { create } from "zustand"; interface ModuleStore { activeModule: string; @@ -7,7 +7,7 @@ interface ModuleStore { } const useModuleStore = create((set) => ({ - activeModule: 'builder', // Initial state + activeModule: "visualization", // Initial state setActiveModule: (module) => set({ activeModule: module }), // Update state })); diff --git a/app/src/store/usePlayButtonStore.ts b/app/src/store/usePlayButtonStore.ts new file mode 100644 index 0000000..5b2913c --- /dev/null +++ b/app/src/store/usePlayButtonStore.ts @@ -0,0 +1,11 @@ +import { create } from "zustand"; + +type PlayButtonStore = { + isPlaying: boolean; // Updated state name to reflect the play/pause status more clearly + setIsPlaying: (value: boolean) => void; // Updated setter function name for clarity +}; + +export const usePlayButtonStore = create((set) => ({ + isPlaying: false, // Default state for play/pause + setIsPlaying: (value) => set({ isPlaying: value }), // Update isPlaying state +})); diff --git a/app/src/store/useTemplateStore.ts b/app/src/store/useTemplateStore.ts new file mode 100644 index 0000000..a91c14a --- /dev/null +++ b/app/src/store/useTemplateStore.ts @@ -0,0 +1,39 @@ +import { create } from "zustand"; + +type Side = "top" | "bottom" | "left" | "right"; + +interface Widget { + id: string; + type: string; + title: string; + panel: Side; + data: any; +} + +interface Template { + id: string; + name: string; + panelOrder: Side[]; + widgets: Widget[]; + snapshot?: string; // Add an optional image property (base64) +} + +interface TemplateStore { + templates: Template[]; + addTemplate: (template: Template) => void; + removeTemplate: (id: string) => void; +} + +export const useTemplateStore = create((set) => ({ + templates: [], + addTemplate: (template) => + set((state) => ({ + templates: [...state.templates, template], + })), + removeTemplate: (id) => + set((state) => ({ + templates: state.templates.filter((t) => t.id !== id), + })), +})); + +export default useTemplateStore; diff --git a/app/src/store/useThemeStore.ts b/app/src/store/useThemeStore.ts new file mode 100644 index 0000000..944a77f --- /dev/null +++ b/app/src/store/useThemeStore.ts @@ -0,0 +1,11 @@ +import { create } from "zustand"; + +interface ThemeState { + themeColor: string[]; // This should be an array of strings + setThemeColor: (colors: string[]) => void; // This function will accept an array of strings +} + +export const useThemeStore = create((set) => ({ + themeColor: ["#5c87df", "#EEEEFE", "#969BA7"], + setThemeColor: (colors) => set({ themeColor: colors }), +})); diff --git a/app/src/store/useWidgetStore.ts b/app/src/store/useWidgetStore.ts new file mode 100644 index 0000000..013581b --- /dev/null +++ b/app/src/store/useWidgetStore.ts @@ -0,0 +1,49 @@ +import { create } from "zustand"; + +export interface Widget { + id: string; + type: string; // Can be chart type or "progress" + panel: "top" | "bottom" | "left" | "right"; + title: string; + fontFamily?: string; + fontSize?: string; + fontWeight?: string; + data: { + // Chart data + labels?: string[]; + datasets?: Array<{ + data: number[]; + backgroundColor: string; + borderColor: string; + borderWidth: number; + }>; + // Progress card data + stocks?: Array<{ + key: string; + value: number; + description: string; + }>; + }; +} + +interface WidgetStore { + draggedAsset: Widget | null; // The currently dragged widget asset + widgets: Widget[]; // List of all widgets + selectedChartId: any; + setDraggedAsset: (asset: Widget | null) => void; // Setter for draggedAsset + addWidget: (widget: Widget) => void; // Add a new widget + setWidgets: (widgets: Widget[]) => void; // Replace the entire widgets array + setSelectedChartId: (widget: Widget | null) => void; // Set the selected chart/widget +} + +// Create the store with Zustand +export const useWidgetStore = create((set) => ({ + draggedAsset: null, + widgets: [], + selectedChartId: null, // Initialize as null, not as an array + setDraggedAsset: (asset) => set({ draggedAsset: asset }), + addWidget: (widget) => + set((state) => ({ widgets: [...state.widgets, widget] })), + setWidgets: (widgets) => set({ widgets }), + setSelectedChartId: (widget) => set({ selectedChartId: widget }), +})); diff --git a/app/src/store/useZoneStore.ts b/app/src/store/useZoneStore.ts new file mode 100644 index 0000000..5d7c4a4 --- /dev/null +++ b/app/src/store/useZoneStore.ts @@ -0,0 +1,41 @@ +import { create } from "zustand"; + +type Side = "top" | "bottom" | "left" | "right"; + +interface Widget { + id: string; + type: string; + title: string; + panel: Side; + data: any; +} + +interface SelectedZoneState { + zoneName: string; + activeSides: Side[]; + panelOrder: Side[]; + lockedPanels: Side[]; + widgets: Widget[]; +} + +interface SelectedZoneStore { + selectedZone: SelectedZoneState; + setSelectedZone: (zone: Partial | ((prev: SelectedZoneState) => SelectedZoneState)) => void; +} + +export const useSelectedZoneStore = create((set) => ({ + selectedZone: { + zoneName: "Manufacturing unit", + activeSides: [], + panelOrder: [], + lockedPanels: [], + widgets: [], + }, + setSelectedZone: (zone) => + set((state) => ({ + selectedZone: + typeof zone === "function" + ? zone(state.selectedZone) // Handle functional updates + : { ...state.selectedZone, ...zone }, // Handle partial updates + })), +})); \ No newline at end of file diff --git a/app/src/styles/components/_regularDropDown.scss b/app/src/styles/components/_regularDropDown.scss new file mode 100644 index 0000000..085a5cd --- /dev/null +++ b/app/src/styles/components/_regularDropDown.scss @@ -0,0 +1,52 @@ +@use '../abstracts/variables.scss' as *; + + +.regularDropdown-container { + width: 104px; + height: 22px; + border: 1px solid var(--text-color); // Ensure $border-color is defined + border-radius: 6px; + position: relative; + cursor: pointer; + padding: 0 6px; + + .dropdown-header { + height: 100%; + display: flex; + justify-content: space-between; + cursor: pointer; + // padding: 5px; + border: 1px solid var(--primary-color); + border-radius: 6px; + background-color: var(--background-color); + + // .icon { + // padding-right: 6px; + // } + } + + .dropdown-options { + position: absolute; // Ensure dropdown options position correctly + width: 100%; // Ensure options width matches the header + background-color: var(--primary-color); // Optional: Background color + border: 1px solid #ccc; // Optional: Border styling + border-radius: 4px; // Optional: Border radius + z-index: 10; // Ensure dropdown appears above other elements + max-height: 200px; // Optional: Limit height + overflow-y: auto; // Optional: Enable scrolling if content exceeds height + left: 0; + top: 104%; + .option { + padding: 5px; + cursor: pointer; + + &:hover { + background-color: var(--primary-color); // Optional: Hover effect + } + } + } + + .icon { + height: auto; + } +} \ No newline at end of file diff --git a/app/src/styles/layout/sidebar.scss b/app/src/styles/layout/sidebar.scss index 49ce93b..622cade 100644 --- a/app/src/styles/layout/sidebar.scss +++ b/app/src/styles/layout/sidebar.scss @@ -9,25 +9,31 @@ background-color: var(--background-color); border-radius: #{$border-radius-extra-large}; box-shadow: #{$box-shadow-medium}; + .header-container { @include flex-space-between; padding: 10px; width: 100%; + .header-content { @include flex-center; width: calc(100% - 34px); + .logo-container { @include flex-center; } + .header-title { padding: 0 8px; width: 100%; max-width: calc(100% - 32px); + .input-value { color: var(--accent-color); } } } + .toggle-sidebar-ui-button { @include flex-center; cursor: pointer; @@ -36,31 +42,108 @@ min-height: 32px; min-width: 32px; border-radius: #{$border-radius-small}; + &:hover { background-color: var(--background-color-secondary); } } + .active { background-color: var(--background-color-secondary); outline: 1px solid var(--accent-color); outline-offset: -1px; } } + .sidebar-left-container { min-height: 50vh; padding-bottom: 12px; position: relative; display: flex; flex-direction: column; + .sidebar-left-content-container { border-bottom: 1px solid var(--border-color); // flex: 1; height: calc(100% - 36px); position: relative; overflow: auto; + + .widget-left-sideBar { + min-height: 50vh; + max-height: 60vh; + + .widget2D { + .chart-container { + display: flex; + flex-direction: column; + gap: 8px; + padding-right: 6px; + flex-wrap: nowrap; + overflow: auto; + + .chart { + min-height: 170px; + background: var(--background-primary, #FCFDFD); + border: 1.23px solid var(--Grays-Gray-5, #E5E5EA); + box-shadow: 0px 4.91px 4.91px 0px #0000001C; + border-radius: $border-radius-medium; + padding: 12px 6px; + } + + .progressBar { + height: auto !important; + padding: 12px 10px 41px 10px; + display: flex; + flex-direction: column; + gap: 16px; + + .header { + display: flex; + justify-content: start; + align-items: center; + border-bottom: none; + } + + .stock { + padding: 13px 5px; + background-color: #E0DFFF80; + border-radius: 6.33px; + display: flex; + justify-content: space-between; + + .stock-item { + .stockValues { + display: flex; + flex-direction: row-reverse; + align-items: flex-end; + gap: 3px; + + .value { + color: var(--accent-color); + font-size: 16px; + } + } + + .stock-description { + font-size: 12px; + } + } + + } + } + + } + } + } + + + } + .outline-container { height: 100%; + .outline-content-container { position: relative; height: 100%; @@ -79,15 +162,18 @@ background-color: var(--background-color); border-radius: #{$border-radius-extra-large}; box-shadow: #{$box-shadow-medium}; + .header-container { @include flex-space-between; padding: 10px; width: 100%; gap: 12px; height: 52px; + .options-container { @include flex-center; gap: 8px; + .share-button { padding: 4px 12px; color: var(--primary-color); @@ -96,18 +182,22 @@ border-radius: #{$border-radius-medium}; cursor: pointer; } + .app-docker-button { @include flex-center; } } + .split { height: 20px; width: 2px; background: var(--background-color-secondary); } + .users-container { width: 100%; @include flex-space-between; + .user-profile { @include flex-center; height: 26px; @@ -118,8 +208,10 @@ font-weight: var(--font-weight-bold); color: white; } + .guest-users-container { display: flex; + .other-guest { @include flex-center; height: 26px; @@ -134,11 +226,14 @@ outline-offset: -1px; } } + .user-profile-container { display: flex; + .user-organnization { height: 100%; max-width: 52px; + img { height: 100%; width: 100%; @@ -148,9 +243,11 @@ } } } + .sidebar-actions-container { position: absolute; left: -40px; + .sidebar-action-list { margin-bottom: 12px; @include flex-center; @@ -160,16 +257,19 @@ background: var(--primary-color); box-shadow: #{$box-shadow-medium}; } + .active { background: var(--accent-color); } } + .sidebar-right-container { min-height: 50vh; padding-bottom: 12px; position: relative; display: flex; flex-direction: column; + .sidebar-right-content-container { border-bottom: 1px solid var(--border-color); // flex: 1; @@ -178,12 +278,179 @@ overflow: auto; } } + + .visualization-right-sideBar { + min-height: 50vh; + max-height: 60vh; + + .sidebar-left-content-container { + .dataSideBar { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + gap: 12px; + padding: 10px 12px; + + .sideBarHeader { + color: #5c87df; + border-bottom: 1px solid var(--border-color); + padding-bottom: 6px; + } + + .selectedMain-container { + display: flex; + flex-direction: column; + gap: 6px; + border-bottom: 1px solid var(--border-color); + padding-bottom: 10px; + + .selectedMain { + display: flex; + align-items: center; + gap: 6px; + + main { + width: 35%; + white-space: nowrap; + /* Prevent wrapping */ + } + + .icon { + padding: 0; + cursor: pointer; + } + + button { + background-color: transparent; + box-shadow: none; + color: #5273eb; + padding: 6px; + font-size: 18px; + } + + .bulletPoint { + color: #5273eb; + font-size: 16px; + } + + .regularDropdown-container { + width: 100%; + } + + &:first-child { + gap: 4px; + } + } + } + + .child { + width: 100%; + gap: 6px; + } + + .infoBox { + display: flex; + align-items: flex-start; + gap: 6px; + color: #444; + border-radius: 6px; + font-size: 14px; + + .infoIcon { + padding: 0px 7px; + border-radius: 50%; + border: 1px solid gray; + } + + p { + margin: 0; + } + } + } + + .design { + width: 100%; + display: flex; + flex-direction: column; + gap: 15px; + padding: 0; + font-size: 14px; + color: #4a4a4a; + + .selectedWidget { + padding: 6px 12px; + border-top: 1px solid var(--border-color); + border-bottom: 1px solid var(--border-color); + } + } + + .reviewChart { + width: 100%; + height: 150px; + background: #f0f0f0; + border-radius: 8px; + } + + .optionsContainer { + display: flex; + flex-direction: column; + gap: 10px; + padding: 0 12px; + + .option { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + + .regularDropdown-container { + width: 160px; + } + + &:last-child { + flex-direction: column; + + .header { + width: 100%; + display: flex; + justify-content: space-between; + } + + .colorDisplayer { + width: 100%; + display: flex; + justify-content: start; + align-items: center; + + input[type="color"] { + border: none; + outline: none; + background: none; + width: 24px; + height: 26px; + border-radius: 3.2px; + + } + + } + } + + span { + min-width: 100px; + } + } + } + + } + } } .machine-mechanics-container { .header { @include flex-space-between; padding: 6px 12px; + .add-button { @include flex-center; padding: 2px 4px; @@ -191,19 +458,23 @@ color: var(--primary-color); border-radius: #{$border-radius-small}; cursor: pointer; + path { stroke: var(--primary-color); } } } + .lists-main-container { margin: 2px 8px; width: calc(100% - 16px); background: var(--background-color-secondary); border-radius: #{$border-radius-small}; + .list-container { min-height: 120px; padding: 4px; + .list-item { @include flex-space-between; padding: 4px 12px; @@ -211,15 +482,19 @@ margin: 2px 0; border-radius: #{$border-radius-small}; } + .active { background: var(--accent-color); + .value { color: var(--primary-color); } + path { stroke: var(--primary-color); } } + .remove-button { @include flex-center; height: 12px; @@ -227,18 +502,22 @@ cursor: pointer; } } + .resize-icon { @include flex-center; padding: 4px; cursor: grab; + &:active { cursor: grabbing; } } } + .selected-properties-container { padding: 12px; } + .footer { @include flex-center; justify-content: flex-start; @@ -247,3 +526,127 @@ font-size: var(--font-size-tiny); } } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +/* 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; + cursor: pointer; + display: flex; + align-items: center; + transition: background-color 0.2s ease; + + &:hover { + background-color: #2563eb; /* Darker blue on hover */ + } + + .icon { + margin-left: 0.5rem; + } + } + + .dropdown-menu { + position: absolute; + top: calc(100% + 0.5rem); /* Add spacing below the button */ + 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; + } +} + +/* 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; + + &:hover { + background-color: #f3f4f6; /* Light gray background on hover */ + } +} + +/* 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 */ + } + + .icon { + margin-left: 0.5rem; + } + } + + .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 2c9b7df..68688f6 100644 --- a/app/src/styles/main.scss +++ b/app/src/styles/main.scss @@ -18,9 +18,11 @@ @use 'components/moduleToggle'; @use 'components/templates'; @use 'components/tools'; +@use 'components/regularDropDown'; // layout @use 'layout/sidebar'; // pages @use 'pages/home'; +@use 'pages/realTimeViz'; \ No newline at end of file diff --git a/app/src/styles/pages/realTimeViz.scss b/app/src/styles/pages/realTimeViz.scss new file mode 100644 index 0000000..6bf0ae3 --- /dev/null +++ b/app/src/styles/pages/realTimeViz.scss @@ -0,0 +1,464 @@ +@use '../abstracts/variables.scss' as *; + +// Main Container +.realTime-viz { + background-color: var(--background-color); + border-radius: 20px; + box-shadow: 0px 4px 8px rgba(60, 60, 67, 0.1019607843); + width: 55%; + height: 600px; + position: absolute; + top: 50%; + left: 48%; + transform: translate(-50%, -50%); + + .icon { + display: flex; + align-items: center; + position: relative; + + &:first-child { + &::after { + content: ""; + display: block; + width: 1px; + height: 18px; + background-color: #000; + margin-top: 10px; + position: absolute; + right: -10px; + top: -4px; + } + } + } + + .icons-container { + .icon { + &:first-child { + &::after { + display: none; + } + } + } + + + + } + + .realTimeViz-tools { + position: fixed; + bottom: -150px; + left: 50%; + transform: translateX(-50%); + box-shadow: 0px 4px 8px 0px rgba(60, 60, 67, 0.1); + background: $background-color; + display: flex; + gap: 12px; + border-radius: 12px; + z-index: 1000; + transition: bottom 0.3s ease; + padding: 8px; + + .icons-container { + padding-left: 8px; + display: flex; + align-items: center; + gap: 10px; + } + } + + + .zoon-wrapper { + display: flex; + background-color: #E0DFFF80; + position: absolute; + bottom: 10px; + left: 50%; + transform: translate(-50%, 0); + gap: 6px; + padding: 4px; + border-radius: 8px; + max-width: 80%; + overflow: auto; + + &::-webkit-scrollbar { + display: none; + } + + .zone { + width: auto; + background-color: #FCFDFD; + border-radius: 6px; + padding: 4px 8px; + white-space: nowrap; + font-size: $small; + + } + + .active { + background-color: var(--accent-color); + color: var(--background-color); + // color: #FCFDFD !important; + } + } + + .zoon-wrapper.bottom { + bottom: 210px; + } + + @media (max-width: 1024px) { + width: 80%; // Increase width to take more space on smaller screens + height: 500px; // Reduce height to fit smaller screens + left: 50%; // Center horizontally + + .main-container { + margin: 0 15px; // Reduce margin for better spacing + } + + .zoon-wrapper { + bottom: 5px; // Adjust position for smaller screens + + &.bottom { + bottom: 150px; // Adjust for bottom placement + } + } + } + + @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; + transition: all 0.3s ease; + } + + .main-container { + position: relative; + flex: 1; + height: 600px; + background-color: rgb(235, 235, 235); + margin: 0 30px; + transition: height 0.3s ease, margin 0.3s ease; + + + + .zoon-wrapper { + display: flex; + background-color: rgba(224, 223, 255, 0.5); + position: absolute; + bottom: 10px; + left: 50%; + transform: translate(-50%, 0); + gap: 6px; + padding: 4px; + border-radius: 8px; + max-width: 80%; + overflow: auto; + transition: transform 0.3s ease; + + &::-webkit-scrollbar { + display: none; + } + + .zone { + width: auto; + background-color: $background-color; + border-radius: 6px; + padding: 4px 8px; + white-space: nowrap; + cursor: pointer; + transition: background-color 0.3s ease; + + &.active { + background-color: var(--primary-color); + color: var(--accent-color); + } + } + + &.bottom { + bottom: 210px; + } + } + } + + .panel { + position: absolute; + background: white; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); + transition: all 0.3s ease; + border-radius: 6px; + overflow: visible !important; + + .panel-content { + position: relative; + height: 100%; + padding: 10px; + overflow: auto; + display: flex; + flex-direction: column; + gap: 10px; + + &::-webkit-scrollbar { + display: none; + } + + .chart-container { + width: 100%; + height: 200px; + max-height: 100%; + border: 1px dotted #a9a9a9; + border-radius: 8px; + box-shadow: 0px 2px 6px 0px rgba(60, 60, 67, 0.1); + padding: 6px 0; + background-color: white; + } + + .close-btn { + position: absolute; + top: 5px; + right: 5px; + background: none; + border: none; + cursor: pointer; + color: var(--primary-color); + } + } + + &.top-panel, + &.bottom-panel { + left: 0; + right: 0; + + .panel-content { + display: flex; + flex-direction: row; + + .chart-container { + height: 100%; + width: 230px; + } + } + } + + &.top-panel { + top: 0; + } + + &.bottom-panel { + bottom: 0; + } + + &.left-panel { + left: 0; + top: 0; + bottom: 0; + + .chart-container { + width: 100%; + height: 200px; + } + } + + &.right-panel { + right: 0; + top: 0; + bottom: 0; + } + } +} + +// Side Buttons +.side-button-container { + position: absolute; + display: flex; + background-color: $background-color; + padding: 5px; + border-radius: 8px; + transition: transform 0.3s ease; + + .extra-Bs { + display: flex; + align-items: center; + gap: 12px; + + .icon { + display: flex; + } + + &:hover { + cursor: pointer; + } + } + + .side-button { + cursor: pointer; + transition: background-color 0.3s ease; + width: 18px; + height: 18px; + display: flex; + justify-content: center; + // align-items: center; + background-color: var(--accent-color); + border: none; + color: var(--background-color); + border-radius: 4px; + + &:hover { + // background-color: var(--primary-color); + // color: var(--accent-color); + } + } + + &.top { + top: -30px; + left: 50%; + transform: translateX(-50%); + flex-direction: row; + gap: 6px; + } + + &.right { + right: -30px; + top: 50%; + transform: translateY(-50%); + flex-direction: column; + gap: 6px; + } + + &.bottom { + bottom: -30px; + left: 50%; + transform: translateX(-50%); + flex-direction: row; + gap: 6px; + } + + &.left { + left: -30px; + top: 50%; + transform: translateY(-50%); + flex-direction: column; + gap: 6px; + } +} + +.right.side-button-container { + + .extra-Bs { + flex-direction: column; + } + +} + +.left.side-button-container { + + .extra-Bs { + flex-direction: column; + } +} + +// Theme Container +.theme-container { + width: 250px; + padding: 12px; + box-shadow: 1px -3px 4px 0px rgba(0, 0, 0, 0.11); + border-radius: 8px; + background-color: white; + position: absolute; + top: 20px; + right: -100%; + transform: translate(-0%, 0); + + h2 { + font-size: 12px; + margin-bottom: 8px; + color: #2B3344; + } + + .theme-preset-wrapper { + display: flex; + gap: 5px; + flex-wrap: wrap; + + .theme-preset { + display: flex; + gap: 2px; + margin-bottom: 10px; + border: 1px solid $border-color; + padding: 5px 10px; + border-radius: 4px; + transition: border 0.3s ease; + + &.active { + border: 1px solid var(--primary-color); + + &::after { + content: ''; + position: absolute; + top: 1px; + left: 1px; + width: 10px; + height: 10px; + background-color: var(--primary-color); + border-radius: 50%; + } + } + } + } + + .custom-color { + display: flex; + justify-content: space-between; + + .color-displayer { + display: flex; + gap: 5px; + align-items: center; + border: 1px solid var(--accent-color); + border-radius: 4px; + padding: 0 5px; + + input { + border: none; + outline: none; + border-radius: 50%; + } + } + } +} \ No newline at end of file