updated realTimeViz componet sturucture and data

This commit is contained in:
Nalvazhuthi 2025-03-18 17:00:20 +05:30
parent 92676cd12c
commit b818a00c64
13 changed files with 1079 additions and 592 deletions

View File

@ -1474,3 +1474,31 @@ export function RemoveIcon() {
</svg>
);
}
export function PlayIcon() {
return (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M15.9111 8.51444C17.363 9.37988 17.363 11.6201 15.9111 12.4856L7.14505 17.7109C5.73403 18.552 4 17.4572 4 15.7253V5.27468C4 3.54276 5.73403 2.44801 7.14505 3.28911L15.9111 8.51444Z"
stroke="#1C274C"
/>
</svg>
);
}
export function CommentIcon() {
return (
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.15482 16.3138L3.30127 17.1673H4.50838H10H10.0004C11.6584 17.166 13.2646 16.5899 14.5455 15.5371C15.8263 14.4843 16.7025 13.02 17.0248 11.3937C17.3471 9.76732 17.0955 8.07954 16.3129 6.61789C15.5303 5.15623 14.2651 4.01113 12.7329 3.37768C11.2007 2.74423 9.4963 2.66161 7.91002 3.14392C6.32375 3.62622 4.95377 4.6436 4.0335 6.02272C3.11322 7.40184 2.69958 9.05738 2.86306 10.7073L2.86513 10.7282L2.86895 10.7489C3.0346 11.6451 3.22308 12.3554 3.53795 13.0301C3.85334 13.7059 4.28373 14.3209 4.90409 15.0431L4.91809 15.0594L4.93343 15.0744C4.99514 15.1349 5.03091 15.2169 5.03333 15.3032C5.03257 15.3457 5.02367 15.3877 5.0071 15.4269L5.46767 15.6215L5.0071 15.4269C4.98993 15.4676 4.96488 15.5044 4.93338 15.5352L4.93336 15.5352L4.92982 15.5388L4.15482 16.3138Z" stroke="#2B3344"/>
<path d="M6.66663 8.33398H13.3333" stroke="#2B3344" stroke-linecap="round"/>
<path d="M6.66663 11.666H13.3333" stroke="#2B3344" stroke-linecap="round"/>
</svg>
);
}

View File

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

View File

@ -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<DropdownProps> = ({
}) => {
const [isOpen, setIsOpen] = useState(false);
const [selectedOption, setSelectedOption] = useState<string | null>(null);
const dropdownRef = useRef<HTMLDivElement>(null); // Create a ref for the dropdown container
// Reset selectedOption when the dropdown closes
useEffect(() => {
@ -26,6 +27,22 @@ const RegularDropDown: React.FC<DropdownProps> = ({
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<DropdownProps> = ({
};
return (
<div className="regularDropdown-container">
<div className="regularDropdown-container" ref={dropdownRef}>
{/* Dropdown Header */}
<div className="dropdown-header flex-sb" onClick={toggleDropdown}>
<div className="key">{selectedOption || header}</div>

View File

@ -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 (
<div className="dataSideBar">
{selectedChartId?.title && (
@ -80,50 +91,30 @@ const Data = () => {
{selectedChartId &&
chartDataGroups[selectedChartId.id]?.map((group) => (
<div key={group.id} className="selectedMain-container">
{/* <div className="selectedMain">
<span className="bulletPoint"></span>
<main>Data from</main>
<RegularDropDown
header={group.easing}
options={[
"Connecter 1",
"Connecter 2",
"Connecter 3",
"Connecter 4",
]}
onSelect={(easing) => {
setChartDataGroups((prev) => ({
...prev,
[selectedChartId.id]: prev[selectedChartId.id].map((g) =>
g.id === group.id ? { ...g, easing } : g
),
}));
}}
/>
</div> */}
{/* Render children only if there is a selected chart */}
{group.children.map((child) => (
{group.children.map((child, index) => (
<div key={child.id} className="selectedMain child">
<main>Input</main>
<main>Input {index + 1}</main>
<RegularDropDown
header={child.easing}
options={["Linear", "Ease In", "Ease Out", "Ease In-Out"]}
onSelect={(easing) => {
setChartDataGroups((prev) => ({
...prev,
[selectedChartId.id]: prev[selectedChartId.id].map(
(g) => ({
[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
),
}));
}}
/>
<div className="icon" onClick={() => handleLinkClick(child.id)}>
<LinkIcon />
<div className="icon" onClick={() => handleAddClick(group.id)}>
+
</div>
<div
className="icon"

View File

@ -4,7 +4,7 @@ import { useWidgetStore } from "../../../../store/store";
import ChartComponent from "./chartComponent";
// Define Props Interface
interface DesignProps {}
interface Widget {
id: string;
type: string; // Chart type (e.g., "bar", "line")
@ -78,13 +78,13 @@ const Design = () => {
{/* Chart Component */}
<div className="reviewChart">
{/* {selectedChartId && (
{selectedChartId && (
<ChartComponent
type={selectedChartId.type}
title={selectedChartId.title}
data={selectedChartId.data || defaultChartData} // Use widget data or default
/>
)} */}
)}
</div>
{/* Options Container */}

View File

@ -61,7 +61,8 @@ const Widgets = () => {
</div>
</div>
</div>
{/* Chart Widgets */}
{/* Chart Widgets */}.
{viewMode === "2D" && (
<div className="chart-container">
{chartTypes.map((type, index) => {
const widgetTitle = `Widget ${index + 1}`;
@ -187,10 +188,11 @@ const Widgets = () => {
</div>
</div>
</div>
)}
{viewMode === "3D" && <></>}
{viewMode === "Floating" && <></>}
</div>
);
};
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

View File

@ -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<ButtonsProps> = ({
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 (
<div>
{(["top", "right", "bottom", "left"] as Side[]).map((side) => (
<div key={side} className={`side-button-container ${side}`}>
{/* "+" Button */}
<button
className={`side-button ${side}`}
onClick={() => handlePlusButtonClick(side)}
title={
selectedZone.activeSides.includes(side)
? `Remove all items and close ${side} panel`
: `Activate ${side} panel`
}
>
+
</button>
{/* Extra Buttons */}
<div
className="extra-Bs"
style={{
display: selectedZone.activeSides.includes(side)
? "flex"
: "none",
}}
>
{/* Hide Panel */}
<div
className="icon"
title="Hide Panel"
onClick={() => toggleVisibility(side)}
>
<EyeIcon />
</div>
{/* Clean Panel */}
<div
className="icon"
title="Clean Panel"
onClick={() => cleanPanel(side)}
>
<CleanPannel />
</div>
{/* Lock/Unlock Panel */}
<div
className={`icon ${
selectedZone.lockedPanels.includes(side) ? "active" : ""
}`}
title={
selectedZone.lockedPanels.includes(side)
? "Unlock Panel"
: "Lock Panel"
}
onClick={() => toggleLockPanel(side)}
>
<LockIcon />
</div>
</div>
</div>
))}
</div>
);
};
export default AddButtons;

View File

@ -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 (
<>
<div
key={widget.id}
className={`chart-container ${
selectedChartId?.id === widget.id && "activeChart"
}`}
ref={setNodeRef}
{...attributes}
{...listeners}
style={{ ...style, ...cardStyle }} // Apply dynamic card styles here
onPointerDown={handlePointerDown}
onDoubleClick={handleDoubleClick} // Add double-click event
>
{widget.type === "progress" ? (
<ProgressCard title={widget.title} data={widget.data} />
) : (
<ChartComponent
type={widget.type}
title={widget.title}
fontFamily={customizationOptions.font} // Pass font customization to ChartComponent
fontSize={widget.fontSize}
fontWeight={widget.fontWeight}
data={widget.data}
/>
)}
</div>
{/* Popup for Customizing Template Theme */}
{/* {isPopupOpen && (
<div className="popup-overlay">
<div className="popup-content">
<h2>Customize Template Theme</h2>
<div className="form-group">
<label>Template Background</label>
<input
type="color"
value={customizationOptions.templateBackground}
onChange={(e) =>
setCustomizationOptions((prev) => ({
...prev,
templateBackground: e.target.value,
}))
}
/>
</div>
<div className="form-group">
<label>Card Background</label>
<input
type="color"
value={customizationOptions.cardBackground}
onChange={(e) =>
setCustomizationOptions((prev) => ({
...prev,
cardBackground: e.target.value,
}))
}
/>
</div>
<div className="form-group">
<label>Card Opacity</label>
<input
type="range"
min="0"
max="1"
step="0.1"
value={customizationOptions.cardOpacity}
onChange={(e) =>
setCustomizationOptions((prev) => ({
...prev,
cardOpacity: parseFloat(e.target.value),
}))
}
/>
</div>
<div className="form-group">
<label>Card Blur</label>
<input
type="range"
min="0"
max="10"
value={customizationOptions.cardBlur}
onChange={(e) =>
setCustomizationOptions((prev) => ({
...prev,
cardBlur: parseInt(e.target.value),
}))
}
/>
</div>
<div className="form-group">
<label>Font</label>
<select
value={customizationOptions.font}
onChange={(e) =>
setCustomizationOptions((prev) => ({
...prev,
font: e.target.value,
}))
}
>
<option value="Arial">Arial</option>
<option value="Times New Roman">Times New Roman</option>
<option value="Courier New">Courier New</option>
</select>
</div>
<div className="form-group">
<label>Margin</label>
<input
type="number"
value={customizationOptions.margin}
onChange={(e) =>
setCustomizationOptions((prev) => ({
...prev,
margin: parseInt(e.target.value),
}))
}
/>
</div>
<div className="form-group">
<label>Radius</label>
<input
type="number"
value={customizationOptions.radius}
onChange={(e) =>
setCustomizationOptions((prev) => ({
...prev,
radius: parseInt(e.target.value),
}))
}
/>
</div>
<div className="form-group">
<label>Shadow</label>
<select
value={customizationOptions.shadow}
onChange={(e) =>
setCustomizationOptions((prev) => ({
...prev,
shadow: e.target.value,
}))
}
>
<option value="Low">Low</option>
<option value="Medium">Medium</option>
<option value="High">High</option>
</select>
</div>
<div className="popup-actions">
<button onClick={handleClosePopup}>Cancel</button>
<button onClick={handleSaveChanges}>Save Changes</button>
</div>
</div>
</div>
)} */}
</>
);
};

View File

@ -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<PanelProps> = ({ 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) => (
<div
key={side}
className={`panel ${side}-panel absolute`}
style={getPanelStyle(side)} // Pass `side` to `getPanelStyle`
onDrop={(e) => handleDrop(e, side)}
onDragOver={(e) => e.preventDefault()}
ref={(el) => {
if (el) {
panelRefs.current[side] = el;
} else {
delete panelRefs.current[side];
}
}}
>
<div
className="panel-content"
style={{
pointerEvents: selectedZone.lockedPanels.includes(side)
? "none"
: "auto",
opacity: selectedZone.lockedPanels.includes(side) ? "0.8" : "1",
}}
>
{selectedZone.widgets
.filter((w) => w.panel === side)
.map((widget) => (
<DraggableWidget widget={widget} key={widget.id} />
))}
</div>
</div>
))}
</>
);
};
export default Panel;

View File

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

View File

@ -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 (
<div
key={widget.id}
className="chart-container"
ref={setNodeRef}
{...attributes}
{...listeners}
style={style}
onPointerDown={handlePointerDown}
>
{widget.type === "progress" ? (
<ProgressCard title={widget.title} data={widget.data} />
) : (
<ChartComponent
type={widget.type}
title={widget.title}
fontFamily={widget.fontFamily}
fontSize={widget.fontSize}
fontWeight={widget.fontWeight}
data={widget.data}
/>
)}
</div>
);
};
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]);
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor)
);
useEffect(() => {
const observers: ResizeObserver[] = [];
const currentPanelRefs = panelRefs.current;
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);
}
// 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: [],
});
return () => {
observers.forEach((observer) => observer.disconnect());
};
}, [zonesData[selectedZone].activeSides, selectedZone]);
useEffect(() => {}, [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 { selectedChartId, setSelectedChartId } = useWidgetStore();
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) {
// 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(() => {
setZonesData((prev) => ({
...prev,
[selectedZone]: {
...prev[selectedZone],
widgets: [
...prev[selectedZone].widgets,
{
...draggedAsset,
id: generateUniqueId(),
panel,
},
],
},
[selectedZone.zoneName]: selectedZone,
}));
}
};
}, [selectedZone]);
const handleDragEnd = (event: any) => {
const { active, over } = event;
if (!over) return;
const { isPlaying, setIsPlaying } = usePlayButtonStore(); // Access the store's state and setter function
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 (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
>
<>
{zonesData ? (
<div className="content-container realTime-viz flex">
{/* Sidebar for navigation */}
{!isPlaying && (
<SideBar
header={["Overview", "Widgets", "Templates"]}
defaultActive={"Widgets"}
/>
<div className="main-container relative">
)}
{/* Main container */}
<div
className="main-container relative"
style={{
height: !isPlaying ? "600px" : "100%",
margin: !isPlaying ? "0 30px" : "0",
}} // If isPlaying is true, set height to "100%", else set it to "auto"
>
<div
className="realTimeViz-tools"
style={{
top: !isPlaying ? "-20%" : "10px",
}}
>
<div className="icon comment">
<CommentIcon />
</div>
<div
className="play icon"
onClick={() => setIsPlaying(!isPlaying)}
>
<PlayIcon />
</div>
</div>
<div
className={`zoon-wrapper ${
zonesData[selectedZone].activeSides.includes("bottom") && "bottom"
selectedZone.activeSides.includes("bottom") && "bottom"
}`}
>
{Object.keys(zonesData).map((zone, index) => (
{/* Display zones as selectable buttons */}
{Object.keys(zonesData).map((zoneName, index) => (
<div
key={index}
className={`zone ${selectedZone === zone && "active"}`}
onClick={() => setSelectedZone(zone)}
>
{zone}
</div>
))}
</div>
{(["top", "right", "bottom", "left"] as Side[]).map((side) => (
<div key={side} className={`side-button-container ${side}`}>
<button
className={`side-button ${side}`}
onClick={() => toggleSide(side)}
title={`Toggle ${side} panel`}
>
+
</button>
<div
className="extra-buttons"
style={{
display: zonesData[selectedZone].activeSides.includes(side)
? "flex"
: "none",
}}
>
<div
className={`icon ${
zonesData[selectedZone].lockedPanels.includes(side)
? "active"
: ""
className={`zone ${
selectedZone.zoneName === zoneName ? "active" : ""
}`}
title="Disable Sorting"
onClick={() => toggleLockPanel(side)}
>
<DisableSorting />
</div>
<div
className={`icon ${
zonesData[selectedZone].lockedPanels.includes(side)
? "active"
: ""
}`}
title="Hide Panel"
onClick={() => toggleLockPanel(side)}
>
<EyeIcon />
</div>
<div
className={`icon ${
zonesData[selectedZone].lockedPanels.includes(side)
? "active"
: ""
}`}
title="Clean Panel"
onClick={() => toggleLockPanel(side)}
>
<CleanPannel />
</div>
<div
className={`icon ${
zonesData[selectedZone].lockedPanels.includes(side)
? "active"
: ""
}`}
title={
zonesData[selectedZone].lockedPanels.includes(side)
? "Unlock Panel"
: "Lock Panel"
onClick={() =>
setSelectedZone({
zoneName,
...zonesData[zoneName],
})
}
onClick={() => toggleLockPanel(side)}
>
<LockIcon />
</div>
</div>
</div>
))}
{zonesData[selectedZone].activeSides.map((side) => (
<div
key={side}
className={`panel ${side}-panel absolute`}
style={getPanelStyle(side)} // Pass `side` to `getPanelStyle`
onDrop={(e) => handleDrop(e, side)}
onDragOver={(e) => e.preventDefault()}
ref={(el) => {
if (el) {
panelRefs.current[side] = el;
} else {
delete panelRefs.current[side];
}
}}
>
<div
className="panel-content"
style={{
pointerEvents: zonesData[selectedZone].lockedPanels.includes(
side
)
? "none"
: "auto",
opacity: zonesData[selectedZone].lockedPanels.includes(side)
? "0.8"
: "1",
}}
>
<SortableContext
items={zonesData[selectedZone].widgets
.filter((w) => w.panel === side)
.map((w) => w.id)}
strategy={verticalListSortingStrategy}
>
{zonesData[selectedZone].widgets
.filter((w) => w.panel === side)
.map((widget) => (
<DraggableWidget widget={widget} key={widget.id} />
))}
</SortableContext>
</div>
{zoneName}
</div>
))}
</div>
{/* Add buttons component */}
{!isPlaying && (
<AddButtons
selectedZone={selectedZone}
setSelectedZone={setSelectedZone}
/>
)}
{/* Panel component */}
<Panel
selectedZone={selectedZone}
setSelectedZone={setSelectedZone}
/>
</div>
{/* Sidebar for data/design options */}
{!isPlaying && (
<SideBar header={["Data", "Design"]} defaultActive={"Data"} />
)}
</div>
</DndContext>
) : (
<></>
)}
</>
);
};
export default RealTimeVisualization;

View File

@ -286,7 +286,7 @@ export const useThemeStore = create<ThemeState>((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<WidgetStore>((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<PlayButtonStore>((set) => ({
isPlaying: false, // Default state for play/pause
setIsPlaying: (value) => set({ isPlaying: value }), // Update isPlaying state
}));