Your commit message

This commit is contained in:
Nalvazhuthi
2025-03-19 18:06:33 +05:30
parent 124e435504
commit 95ed64fc6f
33 changed files with 3115 additions and 10 deletions

19
app/package-lock.json generated
View File

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

View File

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

View File

@@ -100,3 +100,34 @@ export function VisualizationIcon({ isActive }: { isActive: boolean }) {
</svg>
);
}
export function CartIcon({ isActive }: { isActive: boolean }) {
return (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M1.33337 2L1.50998 2.05887C2.39001 2.35221 2.83002 2.49888 3.08169 2.84807C3.33337 3.19725 3.33337 3.66106 3.33337 4.58869V6.33333C3.33337 8.21893 3.33337 9.16173 3.91916 9.74753C4.50495 10.3333 5.44775 10.3333 7.33337 10.3333H8.66671M12.6667 10.3333H11.3334"
stroke={isActive ? "var(--primary-color)" : "var(--text-color)"}
stroke-linecap="round"
/>
<path
d="M5.00005 12C5.55233 12 6.00005 12.4477 6.00005 13C6.00005 13.5523 5.55233 14 5.00005 14C4.44776 14 4.00005 13.5523 4.00005 13C4.00005 12.4477 4.44776 12 5.00005 12Z"
stroke={isActive ? "var(--primary-color)" : "var(--text-color)"}
/>
<path
d="M11 12C11.5523 12 12 12.4477 12 13C12 13.5523 11.5523 14 11 14C10.4478 14 10 13.5523 10 13C10 12.4477 10.4478 12 11 12Z"
stroke={isActive ? "var(--primary-color)" : "var(--text-color)"}
/>
<path
d="M3.33337 4H5.33337M3.66671 8.66667H10.6812C11.3208 8.66667 11.6406 8.66667 11.8911 8.50153C12.1416 8.33633 12.2676 8.0424 12.5195 7.45453L12.8052 6.78787C13.3449 5.52863 13.6148 4.89902 13.3184 4.44951C13.0219 4 12.337 4 10.967 4H8.00004"
stroke={isActive ? "var(--primary-color)" : "var(--text-color)"}
stroke-linecap="round"
/>
</svg>
);
}

View File

@@ -0,0 +1,162 @@
export function CleanPannel() {
return (
<svg
width="12"
height="12"
viewBox="0 0 12 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clip-path="url(#clip0_1782_1158)">
<path d="M12 0H0V12H12V0Z" fill="white" fill-opacity="0.01" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M5 1.47852H7V3.47853H10.75V5.47853H1.25V3.47853H5V1.47852Z"
stroke="#2B3344"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path d="M2 10H10V5.5H2V10Z" stroke="#2B3344" stroke-linejoin="round" />
<path
d="M4 9.97439V8.47852"
stroke="#2B3344"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M6 9.97461V8.47461"
stroke="#2B3344"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M8 9.97439V8.47852"
stroke="#2B3344"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M3 10H9"
stroke="#2B3344"
stroke-linecap="round"
stroke-linejoin="round"
/>
</g>
<defs>
<clipPath id="clip0_1782_1158">
<rect width="12" height="12" fill="white" />
</clipPath>
</defs>
</svg>
);
}
export function EyeIcon() {
return (
<svg
width="14"
height="15"
viewBox="0 0 14 15"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8.75047 7.4375C8.75047 8.40402 7.967 9.1875 7.00047 9.1875C6.034 9.1875 5.25049 8.40402 5.25049 7.4375C5.25049 6.47097 6.034 5.6875 7.00047 5.6875C7.967 5.6875 8.75047 6.47097 8.75047 7.4375Z"
stroke="#1D1E21"
strokeOpacity="0.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M7.00086 3.35419C4.3889 3.35419 2.1779 5.07087 1.43457 7.43752C2.17789 9.80416 4.3889 11.5209 7.00086 11.5209C9.6128 11.5209 11.8238 9.80416 12.5671 7.43752C11.8238 5.07088 9.6128 3.35419 7.00086 3.35419Z"
stroke="#1D1E21"
strokeOpacity="0.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
export function LockIcon() {
return (
<svg
width="14"
height="15"
viewBox="0 0 14 15"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M4.0835 6.28763C4.35849 6.27083 4.69751 6.27083 5.1335 6.27083H8.86683C9.30281 6.27083 9.64185 6.27083 9.91683 6.28763M4.0835 6.28763C3.74031 6.30857 3.49683 6.35571 3.28901 6.46158C2.95973 6.62935 2.69201 6.89704 2.52423 7.22633C2.3335 7.60072 2.3335 8.09072 2.3335 9.07083V9.8875C2.3335 10.8676 2.3335 11.3576 2.52423 11.732C2.69201 12.0613 2.95973 12.329 3.28901 12.4967C3.66336 12.6875 4.1534 12.6875 5.1335 12.6875H8.86683C9.84695 12.6875 10.3369 12.6875 10.7113 12.4967C11.0406 12.329 11.3083 12.0613 11.4761 11.732C11.6668 11.3576 11.6668 10.8676 11.6668 9.8875V9.07083C11.6668 8.09072 11.6668 7.60072 11.4761 7.22633C11.3083 6.89704 11.0406 6.62935 10.7113 6.46158C10.5035 6.35571 10.26 6.30857 9.91683 6.28763M4.0835 6.28763V5.10417C4.0835 3.49334 5.38933 2.1875 7.00016 2.1875C8.61098 2.1875 9.91683 3.49334 9.91683 5.10417V6.28763"
stroke="#1D1E21"
strokeOpacity="0.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
export function PlayIcon() {
return (
<svg
width="28"
height="28"
viewBox="0 0 28 28"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19.9111 12.5144C21.363 13.3799 21.363 15.6201 19.9111 16.4856L11.1451 21.7109C9.73403 22.552 8 21.4572 8 19.7253V9.27468C8 7.54276 9.73403 6.44801 11.145 7.28911L19.9111 12.5144Z"
stroke="#1C274C"
/>
</svg>
);
}
export function CommentIcon() {
return (
<svg
width="28"
height="28"
viewBox="0 0 28 28"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8.15482 20.3118L7.30127 21.1654H8.50838H14H14.0004C15.6584 21.1641 17.2646 20.5879 18.5455 19.5352C19.8263 18.4824 20.7025 17.0181 21.0248 15.3917C21.3471 13.7654 21.0955 12.0776 20.3129 10.6159C19.5303 9.15428 18.2651 8.00918 16.7329 7.37572C15.2007 6.74227 13.4963 6.65966 11.91 7.14196C10.3238 7.62427 8.95377 8.64165 8.0335 10.0208C7.11322 11.3999 6.69958 13.0554 6.86306 14.7053L6.86513 14.7262L6.86895 14.7469C7.0346 15.6431 7.22308 16.3535 7.53795 17.0282C7.85334 17.704 8.28373 18.3189 8.90409 19.0412L8.91809 19.0575L8.93343 19.0725C8.99514 19.133 9.03091 19.215 9.03333 19.3012C9.03257 19.3438 9.02367 19.3858 9.0071 19.425L9.46767 19.6196L9.0071 19.425C8.98993 19.4656 8.96488 19.5024 8.93338 19.5333L8.93336 19.5333L8.92982 19.5368L8.15482 20.3118Z"
stroke="#2B3344"
/>
<path
d="M10.6665 12.332H17.3332"
stroke="#2B3344"
stroke-linecap="round"
/>
<path
d="M10.6665 15.668H17.3332"
stroke="#2B3344"
stroke-linecap="round"
/>
</svg>
);
}
export function SaveTeemplateIcon() {
return (
<svg
width="28"
height="28"
viewBox="0 0 28 28"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M21.5 17.4104V13.2492C21.5 9.67539 21.5 7.88847 20.4017 6.77822C19.3033 5.66797 17.5355 5.66797 14 5.66797C10.4645 5.66797 8.6967 5.66797 7.59835 6.77822C6.5 7.88847 6.5 9.67539 6.5 13.2492V17.4104C6.5 19.9909 6.5 21.2811 7.11176 21.8449C7.40351 22.1138 7.77179 22.2827 8.1641 22.3276C8.98668 22.4217 9.94727 21.5721 11.8685 19.8728C12.7177 19.1217 13.1423 18.7461 13.6336 18.6472C13.8755 18.5985 14.1245 18.5985 14.3664 18.6472C14.8577 18.7461 15.2823 19.1217 16.1315 19.8728C18.0527 21.5721 19.0133 22.4217 19.8359 22.3276C20.2282 22.2827 20.5965 22.1138 20.8882 21.8449C21.5 21.2811 21.5 19.9909 21.5 17.4104Z"
stroke="#2B3344"
/>
<path d="M16.5 9H11.5" stroke="#2B3344" stroke-linecap="round" />
</svg>
);
}

View File

@@ -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 (
<div className="sidebar-left-wrapper">
<Header />
@@ -28,11 +38,17 @@ const SideBarLeft: React.FC = () => {
{activeModule === "visualization" ? (
<>
<ToggleHeader
options={["Outline", "Widgets", "Templates"]}
options={["Widgets", "Templates"]}
activeOption={activeOption}
handleClick={handleToggleClick}
/>
<Search onChange={handleSearchChange} />
<div className="sidebar-left-content-container">
{activeOption === "Widgets" ? <Widgets /> : <Templates />}
</div>
</>
) : activeModule === "market" ? (
<></>
) : (
<>
<ToggleHeader

View File

@@ -0,0 +1,114 @@
import React from "react";
import useTemplateStore from "../../../../store/useTemplateStore";
import { useSelectedZoneStore } from "../../../../store/useZoneStore";
const Templates = () => {
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 (
<div className="template-list" style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(180px, 1fr))',
gap: '1rem',
padding: '1rem'
}}>
{templates.map((template) => (
<div key={template.id} className="template-item" style={{
border: '1px solid #e0e0e0',
borderRadius: '8px',
padding: '1rem',
transition: 'box-shadow 0.3s ease',
}}>
{template.snapshot && (
<div style={{ position: 'relative', paddingBottom: '56.25%' }}> {/* 16:9 aspect ratio */}
<img
src={template.snapshot} // Corrected from template.image to template.snapshot
alt={`${template.name} preview`}
style={{
position: 'absolute',
width: '100%',
height: '100%',
objectFit: 'contain',
borderRadius: '4px',
cursor: 'pointer',
transition: 'transform 0.3s ease',
// ':hover': {
// transform: 'scale(1.05)'
// }
}}
onClick={() => handleLoadTemplate(template)}
/>
</div>
)}
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginTop: '0.5rem'
}}>
<div
onClick={() => handleLoadTemplate(template)}
style={{
cursor: 'pointer',
fontWeight: '500',
// ':hover': {
// textDecoration: 'underline'
// }
}}
>
{template.name}
</div>
<button
onClick={() => handleDeleteTemplate(template.id)}
style={{
padding: '0.25rem 0.5rem',
background: '#ff4444',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
transition: 'opacity 0.3s ease',
// ':hover': {
// opacity: 0.8
// }
}}
aria-label="Delete template"
>
Delete
</button>
</div>
</div>
))}
{templates.length === 0 && (
<div style={{
textAlign: 'center',
color: '#666',
padding: '2rem',
gridColumn: '1 / -1'
}}>
No saved templates yet. Create one in the visualization view!
</div>
)}
</div>
);
};
export default Templates;

View File

@@ -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<HTMLCanvasElement>(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 <canvas ref={canvasRef} style={{ width: "100%", height: "100%" }} />;
};
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)
);
});

View File

@@ -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 (
<div className="widget-left-sideBar">
<ToggleHeader
options={["2D", "3D", "Templates"]}
activeOption={activeOption}
handleClick={handleToggleClick}
/>
{activeOption === "2D" && <Widgets2D />}
{activeOption === "3D" && <Widgets3D />}
{activeOption === "Templates" && <WidgetsTemplate />}
</div>
);
};
export default Widgets;

View File

@@ -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<WidgetProps> = ({ type, index, title }) => {
const { setDraggedAsset } = useWidgetStore((state) => state);
return (
<div
className={`chart chart-${index + 1}`}
draggable
onDragStart={() => {
setDraggedAsset({
type,
id: `widget-${index + 1}`,
title,
panel: "top",
data: sampleData,
});
}}
onDragEnd={() => setDraggedAsset(null)}
>
<ChartComponent type={type} title={title} data={sampleData} />
</div>
);
};
const ProgressBarWidget = ({
id,
title,
data,
}: {
id: string;
title: string;
data: any;
}) => {
const { setDraggedAsset } = useWidgetStore((state) => state);
return (
<div
className="chart progressBar"
draggable
onDragStart={() => {
setDraggedAsset({
type: "progress",
id,
title,
panel: "top",
data,
});
}}
onDragEnd={() => setDraggedAsset(null)}
>
<div className="header">{title}</div>
{data.stocks.map((stock: any, index: number) => (
<div className="stock" key={index}>
<span className="stock-item">
<span className="stockValues">
<div className="key">{stock.key}</div>
<div className="value">{stock.value}</div>
</span>
<div className="stock-description">{stock.description}</div>
</span>
<div className="icon">Icon</div>
</div>
))}
</div>
);
};
const Widgets2D = () => {
return (
<div className="widget2D">
<div className="chart-container">
{chartTypes.map((type, index) => {
const widgetTitle = `Widget ${index + 1}`;
return (
<ChartWidget
key={index}
type={type}
index={index}
title={widgetTitle}
/>
);
})}
<ProgressBarWidget
id="widget-7"
title="Widget 7"
data={{
stocks: [
{ key: "units", value: 1000, description: "Initial stock" },
],
}}
/>
<ProgressBarWidget
id="widget-8"
title="Widget 8"
data={{
stocks: [
{ key: "units", value: 1000, description: "Initial stock" },
{ key: "units", value: 500, description: "Additional stock" },
],
}}
/>
</div>
</div>
);
};
export default Widgets2D;

View File

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

View File

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

View File

@@ -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 = () => {
)}
</div>
)}
{toggleUI && (
{/* process builder */}
{toggleUI && activeModule === "builder" && (
<div className="sidebar-right-container">
<div className="sidebar-right-content-container">
<MachineMechanics />
</div>
</div>
)}
{/* realtime visualization */}
{activeModule === "visualization" && <Visualization />}
</div>
);
};

View File

@@ -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 (
<div className="visualization-right-sideBar">
<ToggleHeader
options={["Data", "Design"]}
activeOption={activeOption}
handleClick={handleToggleClick}
/>
<div className="sidebar-left-content-container">
{activeOption === "Data" ? <Data /> : <Design />}
</div>
</div>
);
};
export default Visualization;

View File

@@ -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<string, Group[]>
>({});
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 (
<div className="dataSideBar">
{selectedChartId?.title && (
<div className="sideBarHeader">{selectedChartId?.title}</div>
)}
{/* <MultiLevelDropDown data={DATA_STRUCTURE} /> */}
<div className="infoBox">
<span className="infoIcon">i</span>
<p>
<em>
By adding templates and widgets, you create a customizable and
dynamic environment.
</em>
</p>
</div>
</div>
);
};
export default Data;

View File

@@ -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<Widget>) => {
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 (
<div className="design">
{/* Title of the Selected Widget */}
<div className="selectedWidget">
{selectedChartId?.title || "Widget 1"}
</div>
{/* Chart Component */}
<div className="reviewChart">
{selectedChartId && (
<ChartComponent
type={selectedChartId.type}
title={selectedChartId.title}
data={selectedChartId.data || defaultChartData} // Use widget data or default
/>
)}
</div>
{/* Options Container */}
<div className="optionsContainer">
{/* Name Dropdown */}
<div className="option">
<span>Name</span>
<RegularDropDown
header={selectedChartId?.title || "Select Name"}
options={["Option 1", "Option 2", "Option 3"]}
onSelect={(value) => {
setSelectedName(value);
handleUpdateWidget({ title: value });
}}
/>
</div>
{/* Element Dropdown */}
<div className="option">
<span>Element</span>
<RegularDropDown
header={selectedChartId?.type || "Select Element"}
options={["bar", "line", "pie", "doughnut", "radar", "polarArea"]} // Valid chart types
onSelect={(value) => {
setSelectedElement(value);
handleUpdateWidget({ type: value });
}}
/>
</div>
{/* Font Family Dropdown */}
<div className="option">
<span>Font Family</span>
<RegularDropDown
header={selectedChartId?.fontFamily || "Select Font"}
options={["Arial", "Roboto", "Sans-serif"]}
onSelect={(value) => {
setSelectedFont(value);
handleUpdateWidget({ fontFamily: value });
}}
/>
</div>
{/* Size Dropdown */}
<div className="option">
<span>Size</span>
<RegularDropDown
header={selectedChartId?.fontSize || "Select Size"}
options={["12px", "14px", "16px", "18px"]}
onSelect={(value) => {
setSelectedSize(value);
handleUpdateWidget({ fontSize: value });
}}
/>
</div>
{/* Weight Dropdown */}
<div className="option">
<span>Weight</span>
<RegularDropDown
header={selectedChartId?.fontWeight || "Select Weight"}
options={["Light", "Regular", "Bold"]}
onSelect={(value) => {
setSelectedWeight(value);
handleUpdateWidget({ fontWeight: value });
}}
/>
</div>
{/* Element Color Picker */}
<div className="option">
<div
className="header"
onClick={() => setShowColorPicker((prev) => !prev)}
>
<span>Element Color</span>
<div className="icon"></div>{" "}
{/* Change icon based on the visibility */}
</div>
{/* Show color picker only when 'showColorPicker' is true */}
{showColorPicker && (
<div className="colorDisplayer">
<input
type="color"
value={elementColor}
onChange={(e) => {
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 */}
<span style={{ marginLeft: "10px" }}>{elementColor}</span>
</div>
)}
</div>
</div>
</div>
);
};
export default Design;

View File

@@ -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 = () => {
</div>
<div className="module">Visualization</div>
</div>
<div
className={`module-list ${
activeModule === "market" && "active"
}`}
onClick={() => setActiveModule("market")}
>
<div className="icon">
<CartIcon isActive={activeModule === "market"} />
</div>
<div className="module">Market Place</div>
</div>
</div>
);
};

View File

@@ -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<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,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 (
<>
<div
key={widget.id}
className={`chart-container ${selectedChartId?.id === widget.id && "activeChart"}`}
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,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<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;
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) => (
<div
key={side}
className={`panel ${side}-panel absolute ${isPlaying && ""}`}
style={getPanelStyle(side)}
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;
// only load selected template

View File

@@ -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<HTMLDivElement>(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<string | null> => {
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<HTMLImageElement>((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 (
<div
ref={containerRef}
className="realTime-viz canvas"
style={{
height: isPlaying ? "100vh" : "600px",
width: isPlaying ? "100%" : "55%",
left: isPlaying ? "50%" : "48%",
}}
>
<div className="realTimeViz-tools">
<div
className="icon save"
title="Save Template"
onClick={handleSaveTemplate}
>
<SaveTeemplateIcon />
</div>
<div className="icon comment" title="Comment">
<CommentIcon />
</div>
<div
className="play icon"
title="Play"
onClick={() => setIsPlaying(!isPlaying)}
>
<PlayIcon />
</div>
</div>
<div className={`zoon-wrapper ${selectedZone.activeSides.includes("bottom") && "bottom"}`}>
{Object.keys(zonesData).map((zoneName, index) => (
<div
key={index}
className={`zone ${selectedZone.zoneName === zoneName ? "active" : ""}`}
onClick={() => {
setSelectedZone({
zoneName,
...zonesData[zoneName],
});
}}
>
{zoneName}
</div>
))}
</div>
{!isPlaying && (
<AddButtons
selectedZone={selectedZone}
setSelectedZone={setSelectedZone}
/>
)}
<Panel selectedZone={selectedZone} setSelectedZone={setSelectedZone} />
</div>
);
};
export default RealTimeVisulization;

View File

@@ -0,0 +1,94 @@
import React, { useState } from "react";
// Dropdown Item Component
const DropdownItem = ({
label,
href,
onClick,
}: {
label: string;
href?: string;
onClick?: () => void;
}) => (
<a
href={href || "#"}
className="dropdown-item"
onClick={(e) => {
e.preventDefault();
onClick?.();
}}
>
{label}
</a>
);
// Nested Dropdown Component
const NestedDropdown = ({
label,
children,
}: {
label: string;
children: React.ReactNode;
}) => {
const [open, setOpen] = useState(false);
return (
<div className="nested-dropdown">
{/* Dropdown Trigger */}
<div
className="dropdown-trigger"
onClick={() => setOpen(!open)} // Toggle submenu on click
>
{label} <span className="icon">{open ? "▼" : "▶"}</span>
</div>
{/* Submenu */}
{open && <div className="submenu">{children}</div>}
</div>
);
};
// Recursive Function to Render Nested Data
const renderNestedData = (data: Record<string, any>) => {
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 (
<NestedDropdown key={key} label={key}>
{renderNestedData(value)}
</NestedDropdown>
);
} else if (Array.isArray(value)) {
// If the value is an array, render each item as a dropdown item
return value.map((item, index) => (
<DropdownItem key={index} label={item} />
));
} else {
// If the value is a simple string, render it as a dropdown item
return <DropdownItem key={key} label={value} />;
}
});
};
// Main Multi-Level Dropdown Component
const MultiLevelDropdown = ({ data }: { data: Record<string, any> }) => {
const [open, setOpen] = useState(false);
return (
<div className="multi-level-dropdown">
{/* Dropdown Trigger Button */}
<button
className="dropdown-button"
onClick={() => setOpen(!open)} // Toggle main menu on click
>
Dropdown trigger <span className="icon"></span>
</button>
{/* Dropdown Menu */}
{open && <div className="dropdown-menu">{renderNestedData(data)}</div>}
</div>
);
};
export default MultiLevelDropdown;

View File

@@ -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<DropdownProps> = ({
header,
options,
onSelect,
}) => {
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(() => {
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 (
<div className="regularDropdown-container" ref={dropdownRef}>
{/* Dropdown Header */}
<div className="dropdown-header flex-sb" onClick={toggleDropdown}>
<div className="key">{selectedOption || header}</div>
<div className="icon"></div>
</div>
{/* Dropdown Options */}
{isOpen && (
<div className="dropdown-options">
{options.map((option, index) => (
<div
className="option"
key={index}
onClick={() => handleOptionClick(option)}
>
{option}
</div>
))}
</div>
)}
</div>
);
};
export default RegularDropDown;

View File

@@ -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 (
<div className="project-main">
<ModuleToggle />
<SideBarLeft />
<SideBarRight />
{activeModule === "visualization" && <RealTimeVisulization />}
</div>
);
};

View File

@@ -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<ModuleStore>((set) => ({
activeModule: 'builder', // Initial state
activeModule: "visualization", // Initial state
setActiveModule: (module) => set({ activeModule: module }), // Update state
}));

View File

@@ -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<PlayButtonStore>((set) => ({
isPlaying: false, // Default state for play/pause
setIsPlaying: (value) => set({ isPlaying: value }), // Update isPlaying state
}));

View File

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

View File

@@ -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<ThemeState>((set) => ({
themeColor: ["#5c87df", "#EEEEFE", "#969BA7"],
setThemeColor: (colors) => set({ themeColor: colors }),
}));

View File

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

View File

@@ -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<SelectedZoneState> | ((prev: SelectedZoneState) => SelectedZoneState)) => void;
}
export const useSelectedZoneStore = create<SelectedZoneStore>((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
})),
}));

View File

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

View File

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

View File

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

View File

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