From b818a00c6491cd13a9f36bfd9736ed642539300d Mon Sep 17 00:00:00 2001 From: Nalvazhuthi Date: Tue, 18 Mar 2025 17:00:20 +0530 Subject: [PATCH] updated realTimeViz componet sturucture and data --- frontend/src/assets/images/svgExports.tsx | 28 + .../src/assets/styles/pages/_realTimeViz.scss | 94 ++- .../components/ui/inputs/regularDropDown.tsx | 21 +- .../ui/sideBar/realTimeViz/data.tsx | 79 ++- .../ui/sideBar/realTimeViz/design.tsx | 6 +- .../ui/sideBar/realTimeViz/widgets.tsx | 248 ++++---- .../realTimeViz/deleteSelectedChart.tsx | 0 .../realTimeVisualization/addButtons.tsx | 187 ++++++ .../realTimeVisualization/draggableWidget.tsx | 240 ++++++++ .../src/pages/realTimeVisualization/panel.tsx | 196 +++++++ .../{progressCard .tsx => progressCard.tsx} | 7 +- .../realTimeVisualization.tsx | 551 +++++------------- frontend/src/store/store.ts | 14 +- 13 files changed, 1079 insertions(+), 592 deletions(-) create mode 100644 frontend/src/functions/realTimeViz/deleteSelectedChart.tsx create mode 100644 frontend/src/pages/realTimeVisualization/addButtons.tsx create mode 100644 frontend/src/pages/realTimeVisualization/draggableWidget.tsx create mode 100644 frontend/src/pages/realTimeVisualization/panel.tsx rename frontend/src/pages/realTimeVisualization/{progressCard .tsx => progressCard.tsx} (92%) diff --git a/frontend/src/assets/images/svgExports.tsx b/frontend/src/assets/images/svgExports.tsx index 78ba16a..4e1471a 100644 --- a/frontend/src/assets/images/svgExports.tsx +++ b/frontend/src/assets/images/svgExports.tsx @@ -1474,3 +1474,31 @@ export function RemoveIcon() { ); } + +export function PlayIcon() { + return ( + + + + ); +} + +export function CommentIcon() { + return ( + + + + + + + ); +} diff --git a/frontend/src/assets/styles/pages/_realTimeViz.scss b/frontend/src/assets/styles/pages/_realTimeViz.scss index 6f508df..13c2293 100644 --- a/frontend/src/assets/styles/pages/_realTimeViz.scss +++ b/frontend/src/assets/styles/pages/_realTimeViz.scss @@ -76,6 +76,11 @@ } } + .activeChart { + // outline: 1px solid #4a90e2; + border-color: #4a90e2 !important; + } + .main-container { position: relative; flex: 1; @@ -83,6 +88,18 @@ background-color: rgb(235, 235, 235); margin: 0 30px; + .realTimeViz-tools { + position: absolute; + top: -20%; + left: 50%; + transform: translate(-50%, 0); + box-shadow: 0px 4px 8px 0px #3C3C431A; + background: #FCFDFD; + display: flex; + gap: 6px; + border-radius: 12px; + } + .zoon-wrapper { display: flex; background-color: #E0DFFF80; @@ -448,7 +465,28 @@ align-items: center; background-color: #FCFDFD; padding: 5px; - border-radius: 4px; + border-radius: 8px; + + .icon { + height: 24px; + } + + .icon:hover { + fill: none; + background-color: #4a90e2; + + path { + fill: var(--primary-color); + stroke: var(--primary-color); + // stroke-width: .5px; + } + } + + .extra-Bs { + align-items: center; + + gap: 6px; + } } .side-button { @@ -488,10 +526,12 @@ left: 50%; transform: translateX(-50%); flex-direction: row; + gap: 6px; .extra-buttons { display: flex; flex-direction: row; + gap: 6px; } } @@ -500,6 +540,10 @@ right: -42px; top: 50%; transform: translateY(-50%); + display: flex; + gap: 6px; + + } .bottom.side-button-container { @@ -507,10 +551,12 @@ left: 50%; transform: translateX(-50%); flex-direction: row; + gap: 6px; .extra-buttons { display: flex; flex-direction: row; + gap: 6px; } } @@ -519,4 +565,50 @@ left: -42px; top: 50%; transform: translateY(-50%); + display: flex; + gap: 6px; + + .extra-Bs { + display: flex; + flex-direction: column; + gap: 6px; + } +} + + + +/* Add transitions to smoothen state changes */ +.content-container { + transition: all 0.3s ease; + /* Adjust duration and easing */ +} + +.main-container { + transition: height 0.3s ease, margin 0.3s ease; + /* Smooth transition for height and margin */ +} + +.zoon-wrapper { + transition: transform 0.3s ease; + /* Smooth transition for transform */ +} + +.side-bar { + transition: transform 0.3s ease; + /* Slide in/out transition */ +} + +.side-bar.hidden { + transform: translateX(-100%); + /* Move sidebar off-screen when hidden */ +} + +.side-bar.visible { + transform: translateX(0); + /* Move sidebar back in */ +} + +.realTimeViz-tools { + transition: top 0.3s ease; + /* Smooth transition for tool position */ } \ No newline at end of file diff --git a/frontend/src/components/ui/inputs/regularDropDown.tsx b/frontend/src/components/ui/inputs/regularDropDown.tsx index 1108857..318913f 100644 --- a/frontend/src/components/ui/inputs/regularDropDown.tsx +++ b/frontend/src/components/ui/inputs/regularDropDown.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useRef } from "react"; interface DropdownProps { header: string; @@ -13,6 +13,7 @@ const RegularDropDown: React.FC = ({ }) => { 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(() => { @@ -26,6 +27,22 @@ const RegularDropDown: React.FC = ({ 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); }; @@ -37,7 +54,7 @@ const RegularDropDown: React.FC = ({ }; return ( -
+
{/* Dropdown Header */}
{selectedOption || header}
diff --git a/frontend/src/components/ui/sideBar/realTimeViz/data.tsx b/frontend/src/components/ui/sideBar/realTimeViz/data.tsx index a4fa12a..317f569 100644 --- a/frontend/src/components/ui/sideBar/realTimeViz/data.tsx +++ b/frontend/src/components/ui/sideBar/realTimeViz/data.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from "react"; import RegularDropDown from "../../inputs/regularDropDown"; -import { LinkIcon, RemoveIcon } from "../../../../assets/images/svgExports"; +import { RemoveIcon } from "../../../../assets/images/svgExports"; import { useWidgetStore } from "../../../../store/store"; interface Child { @@ -16,7 +16,7 @@ interface Group { const Data = () => { const { selectedChartId } = useWidgetStore(); - const [selectedWidget] = useState("Widget 1"); + useEffect(() => {}, [selectedChartId]); // State to store groups for all widgets (using Widget.id as keys) const [chartDataGroups, setChartDataGroups] = useState< @@ -24,8 +24,6 @@ const Data = () => { >({}); useEffect(() => { - console.log("Selected Chart ID:", selectedChartId); - // Initialize data groups for the newly selected widget if it doesn't exist if (selectedChartId && !chartDataGroups[selectedChartId.id]) { setChartDataGroups((prev) => ({ @@ -45,14 +43,28 @@ const Data = () => { } }, [selectedChartId]); - // Handle linking children between groups - const handleLinkClick = (childId: number) => {}; + // 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) => { - if (!selectedChartId) return currentGroups; - const currentChartData = currentGroups[selectedChartId.id] || []; return { @@ -61,8 +73,8 @@ const Data = () => { group.id === groupId ? { ...group, - children: group.children.map((child) => - child.id === childId ? { ...child, easing: "Linear" } : child + children: group.children.filter( + (child) => child.id !== childId ), } : group @@ -71,7 +83,6 @@ const Data = () => { }); }; - console.log("selectedChartId: ", selectedChartId?.title); return (
{selectedChartId?.title && ( @@ -80,50 +91,30 @@ const Data = () => { {selectedChartId && chartDataGroups[selectedChartId.id]?.map((group) => (
- {/*
- -
Data from
- { - setChartDataGroups((prev) => ({ - ...prev, - [selectedChartId.id]: prev[selectedChartId.id].map((g) => - g.id === group.id ? { ...g, easing } : g - ), - })); - }} - /> -
*/} - {/* Render children only if there is a selected chart */} - {group.children.map((child) => ( + {group.children.map((child, index) => (
-
Input
+
Input {index + 1}
{ setChartDataGroups((prev) => ({ ...prev, - [selectedChartId.id]: prev[selectedChartId.id].map( - (g) => ({ - ...g, - children: g.children.map((c) => - c.id === child.id ? { ...c, easing } : c - ), - }) + [selectedChartId.id]: prev[selectedChartId.id].map((g) => + g.id === group.id + ? { + ...g, + children: g.children.map((c) => + c.id === child.id ? { ...c, easing } : c + ), + } + : g ), })); }} /> -
handleLinkClick(child.id)}> - +
handleAddClick(group.id)}> + +
{ {/* Chart Component */}
- {/* {selectedChartId && ( + {selectedChartId && ( - )} */} + )}
{/* Options Container */} diff --git a/frontend/src/components/ui/sideBar/realTimeViz/widgets.tsx b/frontend/src/components/ui/sideBar/realTimeViz/widgets.tsx index 79746fc..2581ad7 100644 --- a/frontend/src/components/ui/sideBar/realTimeViz/widgets.tsx +++ b/frontend/src/components/ui/sideBar/realTimeViz/widgets.tsx @@ -61,136 +61,138 @@ const Widgets = () => {
- {/* Chart Widgets */} -
- {chartTypes.map((type, index) => { - const widgetTitle = `Widget ${index + 1}`; - const sampleData = { - labels: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul"], - datasets: [ - { - data: [65, 59, 80, 81, 56, 55, 40], - backgroundColor: "#5c87df", - borderColor: "#ffffff", - borderWidth: 1, - }, - ], - }; + {/* Chart Widgets */}. + {viewMode === "2D" && ( +
+ {chartTypes.map((type, index) => { + const widgetTitle = `Widget ${index + 1}`; + const sampleData = { + labels: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul"], + datasets: [ + { + data: [65, 59, 80, 81, 56, 55, 40], + backgroundColor: "#5c87df", + borderColor: "#ffffff", + borderWidth: 1, + }, + ], + }; - return ( -
{ - setDraggedAsset({ - type, - id: `widget-${index + 1}`, - title: widgetTitle, - panel: "top", // Default panel assignment - data: sampleData, // Include data in the dragged asset - }); - }} - onDragEnd={() => setDraggedAsset(null)} - > - + return ( +
{ + setDraggedAsset({ + type, + id: `widget-${index + 1}`, + title: widgetTitle, + panel: "top", // Default panel assignment + data: sampleData, // Include data in the dragged asset + }); + }} + onDragEnd={() => setDraggedAsset(null)} + > + +
+ ); + })} +
{ + setDraggedAsset({ + type: "progress", // New widget type + id: `widget-7`, + title: "Widget 7", + panel: "top", + data: { + stocks: [ + { + key: "units", + value: 1000, + description: "Initial stock", + }, + ], + }, + }); + }} + onDragEnd={() => setDraggedAsset(null)} + > + {" "} +
Widget 7
+
+ + +
units
+
1000
+
+
Initial stock
+
+
Icon
- ); - })} -
{ - setDraggedAsset({ - type: "progress", // New widget type - id: `widget-7`, - title: "Widget 7", - panel: "top", - data: { - stocks: [ - { - key: "units", - value: 1000, - description: "Initial stock", - }, - ], - }, - }); - }} - onDragEnd={() => setDraggedAsset(null)} - > - {" "} -
Widget 7
-
- - -
units
-
1000
+
+
{ + setDraggedAsset({ + type: "progress", + id: `widget-8`, + title: "Widget 8", + panel: "top", + data: { + stocks: [ + { + key: "units", + value: 1000, + description: "Initial stock", + }, + { + key: "units", + value: 500, + description: "Additional stock", + }, + ], + }, + }); + }} + onDragEnd={() => setDraggedAsset(null)} + > + {" "} +
Widget 8
+
+ + +
units
+
1000
+
+
Initial stock
-
Initial stock
- -
Icon
+
Icon
+
+
+ + +
units
+
1000
+
+
Initial stock
+
+
Icon
+
-
{ - setDraggedAsset({ - type: "progress", - id: `widget-8`, - title: "Widget 8", - panel: "top", - data: { - stocks: [ - { - key: "units", - value: 1000, - description: "Initial stock", - }, - { - key: "units", - value: 500, - description: "Additional stock", - }, - ], - }, - }); - }} - onDragEnd={() => setDraggedAsset(null)} - > - {" "} -
Widget 8
-
- - -
units
-
1000
-
-
Initial stock
-
-
Icon
-
-
- - -
units
-
1000
-
-
Initial stock
-
-
Icon
-
-
-
+ )} + {viewMode === "3D" && <>} + {viewMode === "Floating" && <>}
); }; export default Widgets; - -// along with my charts i need to additionallly drag and drop my 2 widget 7 and widget 8 styled card to pannel diff --git a/frontend/src/functions/realTimeViz/deleteSelectedChart.tsx b/frontend/src/functions/realTimeViz/deleteSelectedChart.tsx new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/pages/realTimeVisualization/addButtons.tsx b/frontend/src/pages/realTimeVisualization/addButtons.tsx new file mode 100644 index 0000000..06f44eb --- /dev/null +++ b/frontend/src/pages/realTimeVisualization/addButtons.tsx @@ -0,0 +1,187 @@ +import React from "react"; +import { CleanPannel, EyeIcon, LockIcon } from "../../assets/images/svgExports"; + +// 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/frontend/src/pages/realTimeVisualization/draggableWidget.tsx b/frontend/src/pages/realTimeVisualization/draggableWidget.tsx new file mode 100644 index 0000000..6defa37 --- /dev/null +++ b/frontend/src/pages/realTimeVisualization/draggableWidget.tsx @@ -0,0 +1,240 @@ +import { useSortable } from "@dnd-kit/sortable"; +import ChartComponent from "../../components/ui/sideBar/realTimeViz/chartComponent"; +import { ProgressCard } from "./progressCard"; +import { useWidgetStore } from "../../store/store"; +import { useMemo, useState } from "react"; + +export const DraggableWidget = ({ widget }: { widget: any }) => { + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging, + } = useSortable({ id: widget.id }); + 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", + }); + + const style = useMemo( + () => ({ + transform: transform + ? `translate3d(${transform.x}px, ${transform.y}px, 0)` + : undefined, + transition: transition || "transform 200ms ease", + }), + [transform, transition] + ); + + const handlePointerDown = () => { + if (!isDragging) { + 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/frontend/src/pages/realTimeVisualization/panel.tsx b/frontend/src/pages/realTimeVisualization/panel.tsx new file mode 100644 index 0000000..a6faed5 --- /dev/null +++ b/frontend/src/pages/realTimeVisualization/panel.tsx @@ -0,0 +1,196 @@ +import React, { useEffect, useMemo, useRef, useState } from "react"; +import { DraggableWidget } from "./draggableWidget"; +import { useWidgetStore } from "../../store/store"; + +type Side = "top" | "bottom" | "left" | "right"; + +interface PanelProps { + 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 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; // Width of each chart for top/bottom panels + const CHART_HEIGHT = 200; // Height of each chart for left/right panels + let maxCharts = 0; + + if (dimensions) { + if (panel === "top" || panel === "bottom") { + maxCharts = Math.floor(dimensions.width / CHART_WIDTH); // Use width for top/bottom + } else { + maxCharts = Math.floor(dimensions.height / CHART_HEIGHT); // Use height for left/right + } + } else { + maxCharts = panel === "top" || panel === "bottom" ? 5 : 3; // Default values + } + + if (currentWidgetsInPanel >= maxCharts) { + return; + } + + const updatedZone = { + ...selectedZone, + widgets: [ + ...selectedZone.widgets, + { + ...draggedAsset, + id: generateUniqueId(), + panel, + }, + ], + }; + + // Update the selectedZone state + 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]); + + 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; diff --git a/frontend/src/pages/realTimeVisualization/progressCard .tsx b/frontend/src/pages/realTimeVisualization/progressCard.tsx similarity index 92% rename from frontend/src/pages/realTimeVisualization/progressCard .tsx rename to frontend/src/pages/realTimeVisualization/progressCard.tsx index 8eb42ef..f7fc084 100644 --- a/frontend/src/pages/realTimeVisualization/progressCard .tsx +++ b/frontend/src/pages/realTimeVisualization/progressCard.tsx @@ -1,6 +1,9 @@ import React from "react"; -export const ProgressCard = ({ title, data }: { +export const ProgressCard = ({ + title, + data, +}: { title: string; data: { stocks: Array<{ key: string; value: number; description: string }> }; }) => ( @@ -19,4 +22,4 @@ export const ProgressCard = ({ title, data }: {
))}
-); \ No newline at end of file +); diff --git a/frontend/src/pages/realTimeVisualization/realTimeVisualization.tsx b/frontend/src/pages/realTimeVisualization/realTimeVisualization.tsx index 268d3c1..559ede4 100644 --- a/frontend/src/pages/realTimeVisualization/realTimeVisualization.tsx +++ b/frontend/src/pages/realTimeVisualization/realTimeVisualization.tsx @@ -1,94 +1,14 @@ -import React, { useMemo, useState, useEffect, useRef } from "react"; -import { - DndContext, - closestCenter, - KeyboardSensor, - PointerSensor, - useSensor, - useSensors, -} from "@dnd-kit/core"; -import { - SortableContext, - verticalListSortingStrategy, - arrayMove, - useSortable, -} from "@dnd-kit/sortable"; -import { useWidgetStore } from "../../store/store"; -import ChartComponent from "../../components/ui/sideBar/realTimeViz/chartComponent"; +import { useState, useEffect } from "react"; +import { usePlayButtonStore, useWidgetStore } from "../../store/store"; import SideBar from "../../components/layout/sideBar"; -import { - CleanPannel, - DisableSorting, - EyeIcon, - LockIcon, -} from "../../assets/images/svgExports"; -import { ProgressCard } from "./progressCard "; +import Panel from "./panel"; +import AddButtons from "./addButtons"; +import { CommentIcon, PlayIcon } from "../../assets/images/svgExports"; type Side = "top" | "bottom" | "left" | "right"; -const generateUniqueId = () => - `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; - -const DraggableWidget = ({ widget }: { widget: any }) => { - const { - attributes, - listeners, - setNodeRef, - transform, - transition, - isDragging, - } = useSortable({ id: widget.id }); - const { setSelectedChartId } = useWidgetStore(); - - const style = useMemo( - () => ({ - transform: transform - ? `translate3d(${transform.x}px, ${transform.y}px, 0)` - : undefined, - transition: transition || "transform 200ms ease", - }), - [transform, transition] - ); - - const handlePointerDown = () => { - if (!isDragging) { - setSelectedChartId(widget); - } - }; - - return ( -
- {widget.type === "progress" ? ( - - ) : ( - - )} -
- ); -}; - const RealTimeVisualization = () => { - const [selectedZone, setSelectedZone] = useState("Manufacturing unit"); - const [panelDimensions, setPanelDimensions] = useState<{ - [side in Side]?: { width: number; height: number }; - }>({}); - const panelRefs = useRef<{ [side in Side]?: HTMLDivElement }>({}); - + // Dummy database for all zones const [zonesData, setZonesData] = useState<{ [key: string]: { activeSides: Side[]; @@ -135,352 +55,153 @@ const RealTimeVisualization = () => { }, }); - useEffect(() => {}, [zonesData]); + // State to hold the currently selected zone's data, including the zone name + const [selectedZone, setSelectedZone] = useState<{ + zoneName: string; // Add zoneName property + activeSides: Side[]; + panelOrder: Side[]; + lockedPanels: Side[]; + widgets: { + id: string; + type: string; + title: string; + panel: Side; + data: any; + }[]; + }>({ + zoneName: "Manufacturing unit", + activeSides: [], + panelOrder: [], + lockedPanels: [], + widgets: [], + }); - const sensors = useSensors( - useSensor(PointerSensor), - useSensor(KeyboardSensor) - ); + useEffect(() => {}, [selectedZone]); + + const { selectedChartId, setSelectedChartId } = useWidgetStore(); + + // Function to delete the selected chart + const deleteSelectedChart = () => { + if (!selectedChartId) { + return; + } + setZonesData((prev) => { + const updatedWidgets = selectedZone.widgets.filter( + (widget) => widget.id !== selectedChartId.id + ); + return { + ...prev, + [selectedZone.zoneName]: { + ...selectedZone, + widgets: updatedWidgets, + }, + }; + }); + setSelectedChartId(null); + }; + + // Handle keyboard events for delete functionality + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if ((e.key === "Delete" || e.key === "Backspace") && selectedChartId) { + deleteSelectedChart(); + } + }; + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [selectedChartId]); useEffect(() => { - const observers: ResizeObserver[] = []; - const currentPanelRefs = panelRefs.current; + setZonesData((prev) => ({ + ...prev, + [selectedZone.zoneName]: selectedZone, + })); + }, [selectedZone]); - zonesData[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); - } - }); + const { isPlaying, setIsPlaying } = usePlayButtonStore(); // Access the store's state and setter function - return () => { - observers.forEach((observer) => observer.disconnect()); - }; - }, [zonesData[selectedZone].activeSides, selectedZone]); - - const toggleSide = (side: Side) => { - setZonesData((prev) => { - const zoneData = prev[selectedZone]; - const newActiveSides = zoneData.activeSides.includes(side) - ? zoneData.activeSides.filter((s) => s !== side) - : [...zoneData.activeSides, side]; - return { - ...prev, - [selectedZone]: { - ...zoneData, - activeSides: newActiveSides, - panelOrder: newActiveSides, - }, - }; - }); - }; - - const toggleLockPanel = (side: Side) => { - setZonesData((prev) => { - const zoneData = prev[selectedZone]; - const newLockedPanels = zoneData.lockedPanels.includes(side) - ? zoneData.lockedPanels.filter((panel) => panel !== side) - : [...zoneData.lockedPanels, side]; - return { - ...prev, - [selectedZone]: { - ...zoneData, - lockedPanels: newLockedPanels, - }, - }; - }); - }; - - const getPanelStyle = useMemo( - () => (side: Side) => { - const currentIndex = zonesData[selectedZone].panelOrder.indexOf(side); - const previousPanels = zonesData[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 {}; - } - }, - [zonesData, selectedZone] - ); - - const handleDrop = (e: React.DragEvent, panel: Side) => { - e.preventDefault(); - const { draggedAsset } = useWidgetStore.getState(); - if (draggedAsset) { - if (zonesData[selectedZone].lockedPanels.includes(panel)) return; - - const currentWidgetsInPanel = zonesData[selectedZone].widgets.filter( - (w) => w.panel === panel - ).length; - - const dimensions = panelDimensions[panel]; - const CHART_WIDTH = 200; // Width of each chart for top/bottom panels - const CHART_HEIGHT = 200; // Height of each chart for left/right panels - let maxCharts = 0; - - if (dimensions) { - if (panel === "top" || panel === "bottom") { - maxCharts = Math.floor(dimensions.width / CHART_WIDTH); // Use width for top/bottom - } else { - maxCharts = Math.floor(dimensions.height / CHART_HEIGHT); // Use height for left/right - } - } else { - maxCharts = panel === "top" || panel === "bottom" ? 5 : 3; // Default values - } - - if (currentWidgetsInPanel >= maxCharts) { - return; - } - - setZonesData((prev) => ({ - ...prev, - [selectedZone]: { - ...prev[selectedZone], - widgets: [ - ...prev[selectedZone].widgets, - { - ...draggedAsset, - id: generateUniqueId(), - panel, - }, - ], - }, - })); - } - }; - - const handleDragEnd = (event: any) => { - const { active, over } = event; - if (!over) return; - - setZonesData((prev) => { - const zoneData = prev[selectedZone]; - const oldIndex = zoneData.widgets.findIndex( - (widget) => widget.id === active.id - ); - const newPanel = - zoneData.widgets.find((widget) => widget.id === over.id)?.panel || - active.panel; - - if (active.panel === newPanel) { - const newIndex = zoneData.widgets.findIndex( - (widget) => widget.id === over.id - ); - const reorderedWidgets = arrayMove( - zoneData.widgets, - oldIndex, - newIndex - ); - return { - ...prev, - [selectedZone]: { - ...zoneData, - widgets: reorderedWidgets, - }, - }; - } else { - const updatedWidgets = zoneData.widgets.map((widget) => - widget.id === active.id ? { ...widget, panel: newPanel } : widget - ); - const widgetsInNewPanel = updatedWidgets.filter( - (w) => w.panel === newPanel - ); - const newIndex = widgetsInNewPanel.findIndex((w) => w.id === over.id); - const reorderedWidgets = arrayMove(updatedWidgets, oldIndex, newIndex); - return { - ...prev, - [selectedZone]: { - ...zoneData, - widgets: reorderedWidgets, - }, - }; - } - }); - }; + const isZonesDataEmpty = Object.keys(zonesData).length === 0; return ( - -
- -
+ <> + {zonesData ? ( +
+ {/* Sidebar for navigation */} + {!isPlaying && ( + + )} + {/* Main container */}
- {Object.keys(zonesData).map((zone, index) => ( -
setSelectedZone(zone)} - > - {zone} -
- ))} -
- {(["top", "right", "bottom", "left"] as Side[]).map((side) => ( -
- -
-
toggleLockPanel(side)} - > - -
-
toggleLockPanel(side)} - > - -
-
toggleLockPanel(side)} - > - -
-
toggleLockPanel(side)} - > - -
-
-
- ))} - {zonesData[selectedZone].activeSides.map((side) => (
handleDrop(e, side)} - onDragOver={(e) => e.preventDefault()} - ref={(el) => { - if (el) { - panelRefs.current[side] = el; - } else { - delete panelRefs.current[side]; - } + className="realTimeViz-tools" + style={{ + top: !isPlaying ? "-20%" : "10px", }} > +
+ +
setIsPlaying(!isPlaying)} > - w.panel === side) - .map((w) => w.id)} - strategy={verticalListSortingStrategy} - > - {zonesData[selectedZone].widgets - .filter((w) => w.panel === side) - .map((widget) => ( - - ))} - +
- ))} +
+ {/* Display zones as selectable buttons */} + {Object.keys(zonesData).map((zoneName, index) => ( +
+ setSelectedZone({ + zoneName, + ...zonesData[zoneName], + }) + } + > + {zoneName} +
+ ))} +
+ {/* Add buttons component */} + {!isPlaying && ( + + )} + {/* Panel component */} + +
+ {/* Sidebar for data/design options */} + {!isPlaying && ( + + )}
- -
-
+ ) : ( + <> + )} + ); }; - export default RealTimeVisualization; diff --git a/frontend/src/store/store.ts b/frontend/src/store/store.ts index a33e535..84fdb1c 100644 --- a/frontend/src/store/store.ts +++ b/frontend/src/store/store.ts @@ -286,7 +286,7 @@ export const useThemeStore = create((set) => ({ })); // Define the WidgetStore interface // Define the WidgetStore interface -interface Widget { +export interface Widget { id: string; type: string; // Can be chart type or "progress" panel: "top" | "bottom" | "left" | "right"; @@ -315,7 +315,7 @@ interface Widget { interface WidgetStore { draggedAsset: Widget | null; // The currently dragged widget asset widgets: Widget[]; // List of all widgets - selectedChartId: Widget | null; // The currently selected chart/widget + 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 @@ -333,3 +333,13 @@ export const useWidgetStore = create((set) => ({ setWidgets: (widgets) => set({ widgets }), setSelectedChartId: (widget) => set({ selectedChartId: widget }), })); + +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 +}));