Refactor updating how widgets work and removed prop drilling
This commit is contained in:
Generated
+136
@@ -0,0 +1,136 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ChangeListManager">
|
||||||
|
<list default="true" id="e601b4a6-af0f-4558-b267-11e152fb4728" name="Changes" comment="">
|
||||||
|
<change afterPath="$PROJECT_DIR$/client/src/components/SettingsButton.jsx" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/client/src/store/useDashboardStore.jsx" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/client/package-lock.json" beforeDir="false" afterPath="$PROJECT_DIR$/client/package-lock.json" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/client/package.json" beforeDir="false" afterPath="$PROJECT_DIR$/client/package.json" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/client/src/App.jsx" beforeDir="false" afterPath="$PROJECT_DIR$/client/src/App.jsx" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/client/src/components/Canvas/CanvasComponents/AlertLevelWidget.jsx" beforeDir="false" afterPath="$PROJECT_DIR$/client/src/components/Widgets/AlertLevelWidget.jsx" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/client/src/components/Canvas/CanvasComponents/FirstWidgetButton.jsx" beforeDir="false" afterPath="$PROJECT_DIR$/client/src/components/Canvas/CanvasComponents/FirstWidgetButton.jsx" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/client/src/components/Canvas/CanvasComponents/SettingsButton.jsx" beforeDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/client/src/components/Canvas/CanvasComponents/Widget.jsx" beforeDir="false" afterPath="$PROJECT_DIR$/client/src/components/Widgets/Widget.jsx" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/client/src/components/Canvas/CanvasComponents/WidgetMenu.jsx" beforeDir="false" afterPath="$PROJECT_DIR$/client/src/components/Canvas/CanvasComponents/WidgetMenu.jsx" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/client/src/components/Canvas/views/Admin.jsx" beforeDir="false" afterPath="$PROJECT_DIR$/client/src/components/Canvas/views/Admin.jsx" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/client/src/components/Canvas/views/Home.jsx" beforeDir="false" afterPath="$PROJECT_DIR$/client/src/components/Canvas/views/Home.jsx" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/client/src/components/Canvas/views/HomeTabs/MyDashboard.jsx" beforeDir="false" afterPath="$PROJECT_DIR$/client/src/components/Canvas/views/HomeTabs/MyDashboard.jsx" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/client/src/components/Canvas/views/Volcano.jsx" beforeDir="false" afterPath="$PROJECT_DIR$/client/src/components/Canvas/views/Volcano.jsx" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/client/src/constants/widgetRegistry.jsx" beforeDir="false" afterPath="$PROJECT_DIR$/client/src/constants/widgetRegistry.jsx" afterDir="false" />
|
||||||
|
</list>
|
||||||
|
<option name="SHOW_DIALOG" value="false" />
|
||||||
|
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||||
|
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
||||||
|
<option name="LAST_RESOLUTION" value="IGNORE" />
|
||||||
|
</component>
|
||||||
|
<component name="Git.Settings">
|
||||||
|
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
||||||
|
</component>
|
||||||
|
<component name="ProjectColorInfo">{
|
||||||
|
"associatedIndex": 7,
|
||||||
|
"fromUser": false
|
||||||
|
}</component>
|
||||||
|
<component name="ProjectId" id="3Cm41UmuQbyMzWIumGVSngrYAr7" />
|
||||||
|
<component name="ProjectViewState">
|
||||||
|
<option name="hideEmptyMiddlePackages" value="true" />
|
||||||
|
<option name="showLibraryContents" value="true" />
|
||||||
|
</component>
|
||||||
|
<component name="PropertiesComponent">{
|
||||||
|
"keyToString": {
|
||||||
|
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
||||||
|
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||||
|
"RunOnceActivity.git.unshallow": "true",
|
||||||
|
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
|
||||||
|
"codeWithMe.voiceChat.enabledByDefault": "false",
|
||||||
|
"git-widget-placeholder": "20260605__1123__Hight__updates",
|
||||||
|
"javascript.preferred.runtime.type.id": "node",
|
||||||
|
"last_opened_file_path": "/Users/chight/Documents/Development/website-framework",
|
||||||
|
"node.js.detected.package.eslint": "true",
|
||||||
|
"node.js.selected.package.eslint": "(autodetect)",
|
||||||
|
"nodejs_package_manager_path": "npm",
|
||||||
|
"npm.build.executor": "Run",
|
||||||
|
"npm.dev.executor": "Run",
|
||||||
|
"ts.external.directory.path": "/Users/chight/Applications/WebStorm.app/Contents/plugins/javascript-plugin/jsLanguageServicesImpl/external",
|
||||||
|
"vue.rearranger.settings.migration": "true"
|
||||||
|
}
|
||||||
|
}</component>
|
||||||
|
<component name="RecentsManager">
|
||||||
|
<key name="MoveFile.RECENT_KEYS">
|
||||||
|
<recent name="$PROJECT_DIR$/client/src/components/Widgets" />
|
||||||
|
<recent name="$PROJECT_DIR$/client/src/components" />
|
||||||
|
</key>
|
||||||
|
</component>
|
||||||
|
<component name="RunManager">
|
||||||
|
<configuration name="dev" type="js.build_tools.npm" nameIsGenerated="true">
|
||||||
|
<package-json value="$PROJECT_DIR$/client/package.json" />
|
||||||
|
<command value="run" />
|
||||||
|
<scripts>
|
||||||
|
<script value="dev" />
|
||||||
|
</scripts>
|
||||||
|
<node-interpreter value="project" />
|
||||||
|
<envs />
|
||||||
|
<method v="2" />
|
||||||
|
</configuration>
|
||||||
|
</component>
|
||||||
|
<component name="SharedIndexes">
|
||||||
|
<attachedChunks>
|
||||||
|
<set>
|
||||||
|
<option value="bundled-js-predefined-d6986cc7102b-3bd3a6803838-JavaScript-WS-261.22158.274" />
|
||||||
|
</set>
|
||||||
|
</attachedChunks>
|
||||||
|
</component>
|
||||||
|
<component name="TaskManager">
|
||||||
|
<task active="true" id="Default" summary="Default task">
|
||||||
|
<changelist id="e601b4a6-af0f-4558-b267-11e152fb4728" name="Changes" comment="" />
|
||||||
|
<created>1776977196451</created>
|
||||||
|
<option name="number" value="Default" />
|
||||||
|
<option name="presentableId" value="Default" />
|
||||||
|
<updated>1776977196451</updated>
|
||||||
|
<workItem from="1776977197454" duration="13121000" />
|
||||||
|
<workItem from="1777909177585" duration="1804000" />
|
||||||
|
<workItem from="1779808433040" duration="66743000" />
|
||||||
|
</task>
|
||||||
|
<servers />
|
||||||
|
</component>
|
||||||
|
<component name="TypeScriptGeneratedFilesManager">
|
||||||
|
<option name="version" value="3" />
|
||||||
|
</component>
|
||||||
|
<component name="Vcs.Log.Tabs.Properties">
|
||||||
|
<option name="RECENT_FILTERS">
|
||||||
|
<map>
|
||||||
|
<entry key="Branch">
|
||||||
|
<value>
|
||||||
|
<list>
|
||||||
|
<RecentGroup>
|
||||||
|
<option name="FILTER_VALUES">
|
||||||
|
<option value="authentication" />
|
||||||
|
</option>
|
||||||
|
</RecentGroup>
|
||||||
|
</list>
|
||||||
|
</value>
|
||||||
|
</entry>
|
||||||
|
</map>
|
||||||
|
</option>
|
||||||
|
<option name="TAB_STATES">
|
||||||
|
<map>
|
||||||
|
<entry key="MAIN">
|
||||||
|
<value>
|
||||||
|
<State>
|
||||||
|
<option name="FILTERS">
|
||||||
|
<map>
|
||||||
|
<entry key="branch">
|
||||||
|
<value>
|
||||||
|
<list>
|
||||||
|
<option value="authentication" />
|
||||||
|
</list>
|
||||||
|
</value>
|
||||||
|
</entry>
|
||||||
|
</map>
|
||||||
|
</option>
|
||||||
|
</State>
|
||||||
|
</value>
|
||||||
|
</entry>
|
||||||
|
</map>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+32
-2
@@ -22,7 +22,8 @@
|
|||||||
"react-leaflet": "^5.0.0",
|
"react-leaflet": "^5.0.0",
|
||||||
"react-resizable": "^3.0.5",
|
"react-resizable": "^3.0.5",
|
||||||
"react-router-dom": "^7.16.0",
|
"react-router-dom": "^7.16.0",
|
||||||
"uuid": "^11.1.0"
|
"uuid": "^11.1.0",
|
||||||
|
"zustand": "^5.0.14"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.29.0",
|
"@eslint/js": "^9.29.0",
|
||||||
@@ -1635,7 +1636,7 @@
|
|||||||
"version": "19.1.8",
|
"version": "19.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz",
|
||||||
"integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==",
|
"integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
}
|
}
|
||||||
@@ -4541,6 +4542,35 @@
|
|||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zustand": {
|
||||||
|
"version": "5.0.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.14.tgz",
|
||||||
|
"integrity": "sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.20.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": ">=18.0.0",
|
||||||
|
"immer": ">=9.0.6",
|
||||||
|
"react": ">=18.0.0",
|
||||||
|
"use-sync-external-store": ">=1.2.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"immer": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"use-sync-external-store": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -24,7 +24,8 @@
|
|||||||
"react-leaflet": "^5.0.0",
|
"react-leaflet": "^5.0.0",
|
||||||
"react-resizable": "^3.0.5",
|
"react-resizable": "^3.0.5",
|
||||||
"react-router-dom": "^7.16.0",
|
"react-router-dom": "^7.16.0",
|
||||||
"uuid": "^11.1.0"
|
"uuid": "^11.1.0",
|
||||||
|
"zustand": "^5.0.14"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.29.0",
|
"@eslint/js": "^9.29.0",
|
||||||
|
|||||||
+3
-5
@@ -11,6 +11,7 @@ import RegionalMap from "./components/Canvas/views/RegionalMap.jsx";
|
|||||||
import Volcano from "@/components/Canvas/views/Volcano.jsx";
|
import Volcano from "@/components/Canvas/views/Volcano.jsx";
|
||||||
import GlobalMap from "./components/Canvas/views/GlobalMap.jsx";
|
import GlobalMap from "./components/Canvas/views/GlobalMap.jsx";
|
||||||
import Admin from "./components/Canvas/views/Admin.jsx";
|
import Admin from "./components/Canvas/views/Admin.jsx";
|
||||||
|
import MyDashboard from "./components/Canvas/views/HomeTabs/MyDashboard.jsx";
|
||||||
|
|
||||||
function AppFoundation() {
|
function AppFoundation() {
|
||||||
const recipe = useRecipe({ key: "dashboard" });
|
const recipe = useRecipe({ key: "dashboard" });
|
||||||
@@ -50,9 +51,6 @@ const Placeholder = ({ name }) => (
|
|||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
function App() {
|
function App() {
|
||||||
const sidebarContext = createContext(["Home", "Global Map", "Regional Map", "Volcano", "Admin"]);
|
|
||||||
// const [user, setUser] = useState(null);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Routes>
|
<Routes>
|
||||||
{/*Public Routes*/}
|
{/*Public Routes*/}
|
||||||
@@ -63,8 +61,8 @@ function App() {
|
|||||||
|
|
||||||
<Route path="/home" element={<Home />}>
|
<Route path="/home" element={<Home />}>
|
||||||
{/* We use our "pass" component to fill the Outlet until the real pages are built */}
|
{/* We use our "pass" component to fill the Outlet until the real pages are built */}
|
||||||
<Route index element={<Placeholder name="My" />} />
|
<Route index element={<MyDashboard />} />
|
||||||
<Route path="gas" element={<Placeholder name="Gas" />} />
|
<Route path="gas" element={<MyDashboard />} />
|
||||||
<Route path="seismic" element={<Placeholder name="Seismic" />} />
|
<Route path="seismic" element={<Placeholder name="Seismic" />} />
|
||||||
<Route path="remote" element={<Placeholder name="Remote Sensing" />} />
|
<Route path="remote" element={<Placeholder name="Remote Sensing" />} />
|
||||||
<Route path="daily" element={<Placeholder name="Daily Activity" />} />
|
<Route path="daily" element={<Placeholder name="Daily Activity" />} />
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Box, Menu, Portal } from '@chakra-ui/react'
|
import { Box, Menu, Portal } from '@chakra-ui/react'
|
||||||
import WidgetMenu from './WidgetMenu';
|
import WidgetMenu from './WidgetMenu';
|
||||||
import AlertLevel from "@/components/Canvas/CanvasComponents/AlertLevelWidget.jsx";
|
import AlertLevel from "@/components/Widgets/AlertLevelWidget.jsx";
|
||||||
|
|
||||||
export default function FirstWidgetButton({ appendWidget }) {
|
export default function FirstWidgetButton({ appendWidget }) {
|
||||||
const triggerItem = (
|
const triggerItem = (
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
import { IconButton, Menu, Portal } from "@chakra-ui/react";
|
|
||||||
import { ArrowLeft } from 'lucide-react';
|
|
||||||
import { Settings } from 'lucide-react';
|
|
||||||
import WidgetMenu from "./WidgetMenu";
|
|
||||||
|
|
||||||
export function SettingsButton({ onEditWidgets, appendWidget}) {
|
|
||||||
const triggerItem = (<> <Menu.TriggerItem> <ArrowLeft /> Add Widget</Menu.TriggerItem> </>);
|
|
||||||
const WidgetMenuProps = { appendWidget, triggerItem }
|
|
||||||
return (
|
|
||||||
<Menu.Root>
|
|
||||||
<Menu.Trigger asChild >
|
|
||||||
<IconButton variant="ghost" size="md" borderRadius="xl">
|
|
||||||
<Settings />
|
|
||||||
</IconButton>
|
|
||||||
</Menu.Trigger>
|
|
||||||
<Portal>
|
|
||||||
<Menu.Positioner>
|
|
||||||
<Menu.Content>
|
|
||||||
<WidgetMenu {...WidgetMenuProps}/>
|
|
||||||
<Menu.Item value="eidt" onClick={onEditWidgets} >
|
|
||||||
Edit Widgets
|
|
||||||
</Menu.Item>
|
|
||||||
</Menu.Content>
|
|
||||||
</Menu.Positioner>
|
|
||||||
</Portal>
|
|
||||||
</Menu.Root>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Menu, Portal } from "@chakra-ui/react";
|
import { Menu, Portal } from "@chakra-ui/react";
|
||||||
import AlertLevel from "./AlertLevelWidget";
|
import AlertLevel from "../../Widgets/AlertLevelWidget.jsx";
|
||||||
|
|
||||||
export default function WidgetMenu({appendWidget, triggerItem, positioning}) {
|
export default function WidgetMenu({appendWidget, triggerItem, positioning}) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { ProjectCollector } from './AdminTabs/ProjectCollector';
|
|||||||
import { NASApplication } from './AdminTabs/NASApplication';
|
import { NASApplication } from './AdminTabs/NASApplication';
|
||||||
import { IVANS } from './AdminTabs/IVANS'
|
import { IVANS } from './AdminTabs/IVANS'
|
||||||
import { SearchBar } from "../CanvasComponents/SearchBar";
|
import { SearchBar } from "../CanvasComponents/SearchBar";
|
||||||
import { SettingsButton } from "../CanvasComponents/SettingsButton";
|
import { SettingsButton } from "../../SettingsButton.jsx";
|
||||||
import { CanvasHeaderTabs } from '../CanvasComponents/CanvasHeaderTabs';
|
import { CanvasHeaderTabs } from '../CanvasComponents/CanvasHeaderTabs';
|
||||||
import { ADMIN_TABS } from '@/constants/viewKeys';
|
import { ADMIN_TABS } from '@/constants/viewKeys';
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { HStack, Flex, Box } from "@chakra-ui/react";
|
import { HStack, Flex, Box } from "@chakra-ui/react";
|
||||||
import { NavLink, Outlet, useLocation } from "react-router-dom";
|
import { NavLink, Outlet, useLocation } from "react-router-dom";
|
||||||
import { SearchBar } from "../CanvasComponents/SearchBar";
|
import { SearchBar } from "../CanvasComponents/SearchBar";
|
||||||
import { SettingsButton } from "../CanvasComponents/SettingsButton";
|
import { SettingsButton } from "../../SettingsButton.jsx";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const isPersonalDashboard = location.pathname === "/home" || location.pathname === "/home/";
|
const isPersonalDashboard = location.pathname === "/home" || location.pathname === "/home/";
|
||||||
|
|
||||||
// 1. 🎨 CSS Magic: Replicates Chakra's `<Tabs variant="line">` exactly
|
// 1. CSS making this look like how we want it, TODO turn to theme?
|
||||||
const tabStyles = {
|
const tabStyles = {
|
||||||
fontSize: "lg",
|
fontSize: "lg",
|
||||||
fontWeight: "normal",
|
fontWeight: "normal",
|
||||||
|
|||||||
@@ -1,74 +1,70 @@
|
|||||||
import GridLayout, { WidthProvider } from 'react-grid-layout';
|
import { Box, Text, Flex } from "@chakra-ui/react";
|
||||||
import { useSlotRecipe, chakra, Flex, SimpleGrid } from '@chakra-ui/react';
|
import { Responsive, WidthProvider } from "react-grid-layout";
|
||||||
import CustomResizeHandle from '../../CanvasComponents/CustomResizeHandle';
|
import { useDashboardStore } from "@/store/useDashboardStore.jsx";
|
||||||
import FirstWidgetButton from '../../CanvasComponents/FirstWidgetButton';
|
|
||||||
|
|
||||||
export default function MyDashboard({layout, onLayoutChange, isEditable, isDelete, DeleteWidget, widgetArray, appendWidget}) {
|
// React-Grid-Layout requires this wrapper to automatically calculate screen width
|
||||||
const recipe = useSlotRecipe({ key: "widget" });
|
const ResponsiveGridLayout = WidthProvider(Responsive);
|
||||||
const styles = recipe(); // using root recipe for widget below
|
|
||||||
const ResponsiveGridLayout = WidthProvider(GridLayout); // canvas width now adjusts to window size
|
|
||||||
const handleDragStart = () => { document.body.style.userSelect = "none"; }; // don't allow text highlighting when dragging
|
|
||||||
const handleDragStop = () => { document.body.style.userSelect = ""; }; // allow text highlighting all other times
|
|
||||||
let merged = [];
|
|
||||||
|
|
||||||
if (widgetArray.length && layout.length) {
|
export default function MyDashboard() {
|
||||||
const widgetMap = new Map(widgetArray.map(item => [item.i, item]));
|
// 1. ☁️ Connect to the Cloud (Zustand)
|
||||||
merged = layout.map(item => {
|
const widgets = useDashboardStore(state => state.widgets);
|
||||||
return {
|
const isEditing = useDashboardStore(state => state.isEditing);
|
||||||
...item,
|
const updateLayoutPositions = useDashboardStore(state => state.updateLayoutPositions);
|
||||||
...widgetMap.get(item.i)
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show large 'Add Widget' button when dashboard is empty
|
// 2. 🛠️ Format the data for RGL
|
||||||
if ( !layout.length ) {
|
// RGL expects a very specific array format for its layout prop
|
||||||
return (
|
const rglLayout = widgets.map(w => ({
|
||||||
<Flex
|
i: w.id,
|
||||||
align="center"
|
x: w.x,
|
||||||
justify="center"
|
y: w.y,
|
||||||
h="100%"
|
w: w.w,
|
||||||
w="100%"
|
h: w.h,
|
||||||
bg="gray.150"
|
isResizable: false // 🚫 The magic lock! No resizing allowed globally.
|
||||||
borderRadius="md"
|
}));
|
||||||
>
|
|
||||||
<FirstWidgetButton appendWidget={ appendWidget } />
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show responsive grid of widgets
|
|
||||||
return (
|
return (
|
||||||
<ResponsiveGridLayout
|
<Box width="100%" height="100%">
|
||||||
useCSSTransforms={false} // remove the sliding animation of the widgets
|
<ResponsiveGridLayout
|
||||||
className="layout"
|
className="layout"
|
||||||
layout={layout}
|
layouts={{ lg: rglLayout }} // We feed it the formatted layout map
|
||||||
cols={16} // Total columns in grid
|
breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }}
|
||||||
rowHeight={40} // Height of a single row in px
|
cols={{ lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 }} // Our 12-column master grid
|
||||||
isBounded={true} // Total width of the grid
|
rowHeight={30}
|
||||||
margin={[15, 5]} // [horizontal, vertical] gap
|
isDraggable={isEditing} // Only allows dragging when your Settings tools toggle this
|
||||||
onLayoutChange={onLayoutChange}
|
isResizable={false} // Double lock-down
|
||||||
draggableHandle=".widget-handle" // Make only the header draggable
|
onLayoutChange={(newLayout) => updateLayoutPositions(newLayout)} // Fires when user drops a widget
|
||||||
onDragStart={handleDragStart}
|
compactType="vertical" // Auto-packs widgets to the top
|
||||||
onDragStop={handleDragStop}
|
>
|
||||||
position="relative"
|
{/* 3. 🎨 Paint the Widgets */}
|
||||||
resizeHandle={<CustomResizeHandle isVisable={isEditable} />}
|
{widgets.map(widget => (
|
||||||
>
|
<Box
|
||||||
{ merged.map((item) => {
|
key={widget.id}
|
||||||
const Widget = item.widget;
|
bg="white"
|
||||||
return (
|
borderRadius="md"
|
||||||
<Flex key={item.i} css={styles.root} boxShadow="md" height="100%" >
|
boxShadow={isEditing ? "outline" : "sm"} // Visual cue when editing
|
||||||
<Widget
|
border={isEditing ? "2px dashed gray" : "1px solid"}
|
||||||
id={item.i}
|
borderColor={isEditing ? "gray.400" : "gray.200"}
|
||||||
isEditable={isEditable}
|
overflow="hidden"
|
||||||
isDelete={isDelete}
|
cursor={isEditing ? "grab" : "default"}
|
||||||
DeleteWidget={DeleteWidget}
|
>
|
||||||
appendWidget={appendWidget}
|
{/* 🚧 Temporary Placeholder!
|
||||||
/>
|
Eventually, we will swap this block out for a dynamic component loader
|
||||||
|
that looks at widget.type and loads the correct graph or table.
|
||||||
|
*/}
|
||||||
|
<Flex width="100%" height="100%" align="center" justify="center" direction="column">
|
||||||
|
<Text fontWeight="bold" color="gray.600">{widget.type}</Text>
|
||||||
|
<Text fontSize="sm" color="gray.400">Size: {widget.size}</Text>
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</ResponsiveGridLayout>
|
||||||
|
|
||||||
|
{/* Empty State Helper */}
|
||||||
|
{widgets.length === 0 && (
|
||||||
|
<Flex width="100%" height="200px" align="center" justify="center">
|
||||||
|
<Text color="gray.400">Your dashboard is empty. Click Settings to add widgets.</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
)}
|
||||||
})
|
</Box>
|
||||||
}
|
|
||||||
</ResponsiveGridLayout>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -8,7 +8,7 @@ import { Reports } from './VolcanoTabs/Reports';
|
|||||||
import { OfficialInstitutions } from './VolcanoTabs/OfficialInstitutions';
|
import { OfficialInstitutions } from './VolcanoTabs/OfficialInstitutions';
|
||||||
import { SocialMedia } from './VolcanoTabs/SocialMedia';
|
import { SocialMedia } from './VolcanoTabs/SocialMedia';
|
||||||
import { SearchBar } from "../CanvasComponents/SearchBar";
|
import { SearchBar } from "../CanvasComponents/SearchBar";
|
||||||
import { SettingsButton } from "../CanvasComponents/SettingsButton";
|
import { SettingsButton } from "../../SettingsButton.jsx";
|
||||||
|
|
||||||
export default function Volcano() {
|
export default function Volcano() {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -0,0 +1,148 @@
|
|||||||
|
import { useState, useMemo } from "react";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
IconButton,
|
||||||
|
Box,
|
||||||
|
Text,
|
||||||
|
Flex,
|
||||||
|
Grid,
|
||||||
|
Dialog,
|
||||||
|
Tabs,
|
||||||
|
Badge,
|
||||||
|
Menu,
|
||||||
|
HStack
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import { Settings, Plus, LayoutGrid, Move, Save, X } from 'lucide-react';
|
||||||
|
import { useDashboardStore } from "../store/useDashboardStore.jsx";
|
||||||
|
import { WIDGET_REGISTRY } from "../constants/widgetRegistry.jsx";
|
||||||
|
|
||||||
|
export function SettingsButton() {
|
||||||
|
// 1. ☁️ Store connections
|
||||||
|
const isEditing = useDashboardStore(state => state.isEditing);
|
||||||
|
const enterEditMode = useDashboardStore(state => state.enterEditMode);
|
||||||
|
const saveEdit = useDashboardStore(state => state.saveEdit);
|
||||||
|
const cancelEdit = useDashboardStore(state => state.cancelEdit);
|
||||||
|
const addWidget = useDashboardStore(state => state.addWidget);
|
||||||
|
|
||||||
|
// 2. 🎛️ Local state to control the Widget Library popup independently of the dropdown
|
||||||
|
const [isLibraryOpen, setIsLibraryOpen] = useState(false);
|
||||||
|
|
||||||
|
// 3. 🧠 Categorize widgets for the tabs
|
||||||
|
const categorizedWidgets = useMemo(() => {
|
||||||
|
return Object.entries(WIDGET_REGISTRY).reduce((acc, [id, widget]) => {
|
||||||
|
const cat = widget.category || 'General';
|
||||||
|
if (!acc[cat]) acc[cat] = [];
|
||||||
|
acc[cat].push({ id, ...widget });
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const categories = Object.keys(categorizedWidgets);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* THE STATIC DROPDOWN MENU */}
|
||||||
|
<Menu.Root>
|
||||||
|
<Menu.Trigger asChild>
|
||||||
|
<IconButton
|
||||||
|
aria-label="Settings"
|
||||||
|
bg="vdap.darkGreen" // Or #1a2e24
|
||||||
|
color="white"
|
||||||
|
size="sm"
|
||||||
|
borderRadius="md"
|
||||||
|
_hover={{ bg: "vdap.darkGreenHover" }} // Or #111f18
|
||||||
|
>
|
||||||
|
<Settings size={18} />
|
||||||
|
</IconButton>
|
||||||
|
</Menu.Trigger>
|
||||||
|
|
||||||
|
<Menu.Content bg="white" boxShadow="lg" borderRadius="md" p={1} zIndex="dropdown">
|
||||||
|
|
||||||
|
{/* Dynamic Menu Options: What shows up depends on isEditing */}
|
||||||
|
{isEditing ? (
|
||||||
|
<>
|
||||||
|
<Menu.Item onClick={saveEdit} color="green.600" fontWeight="bold" cursor="pointer" _hover={{ bg: "green.50" }}>
|
||||||
|
<Save size={16} style={{ marginRight: '8px' }} /> Save Layout
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item onClick={cancelEdit} color="red.500" cursor="pointer" _hover={{ bg: "red.50" }}>
|
||||||
|
<X size={16} style={{ marginRight: '8px' }} /> Discard Changes
|
||||||
|
</Menu.Item>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Menu.Item onClick={enterEditMode} cursor="pointer" _hover={{ bg: "gray.100" }}>
|
||||||
|
<Move size={16} style={{ marginRight: '8px' }} /> Edit Layout
|
||||||
|
</Menu.Item>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Menu.Separator my={1} borderColor="gray.200" />
|
||||||
|
|
||||||
|
<Menu.Item onClick={() => setIsLibraryOpen(true)} cursor="pointer" _hover={{ bg: "gray.100" }}>
|
||||||
|
<LayoutGrid size={16} style={{ marginRight: '8px' }} /> Widget Library
|
||||||
|
</Menu.Item>
|
||||||
|
|
||||||
|
</Menu.Content>
|
||||||
|
</Menu.Root>
|
||||||
|
|
||||||
|
{/* THE WIDGET LIBRARY MODAL (Controlled by isLibraryOpen) */}
|
||||||
|
<Dialog.Root open={isLibraryOpen} onOpenChange={(e) => setIsLibraryOpen(e.open)} size="xl" placement="center">
|
||||||
|
<Dialog.Backdrop bg="blackAlpha.600" />
|
||||||
|
<Dialog.Positioner>
|
||||||
|
<Dialog.Content bg="gray.50" borderRadius="xl" overflow="hidden" boxShadow="xl" maxW="800px">
|
||||||
|
|
||||||
|
<Dialog.Header bg="vdap.darkGreen" color="white" py={4}>
|
||||||
|
<Dialog.Title fontSize="xl" fontWeight="bold">Add Dashboard Widgets</Dialog.Title>
|
||||||
|
</Dialog.Header>
|
||||||
|
|
||||||
|
<Dialog.Body p={0}>
|
||||||
|
<Tabs.Root defaultValue={categories[0]} variant="enclosed" colorScheme="blue">
|
||||||
|
<Tabs.List bg="white" px={4} pt={4} borderBottom="1px solid" borderColor="gray.200">
|
||||||
|
{categories.map(cat => (
|
||||||
|
<Tabs.Trigger key={cat} value={cat} pb={2}>{cat}</Tabs.Trigger>
|
||||||
|
))}
|
||||||
|
</Tabs.List>
|
||||||
|
|
||||||
|
{categories.map(cat => (
|
||||||
|
<Tabs.Content key={cat} value={cat} p={6}>
|
||||||
|
<Grid templateColumns="repeat(auto-fill, minmax(300px, 1fr))" gap={4}>
|
||||||
|
{categorizedWidgets[cat].map(widget => (
|
||||||
|
<Flex key={widget.id} direction="column" bg="white" p={4} borderRadius="md" border="1px solid" borderColor="gray.200" boxShadow="sm">
|
||||||
|
<Text fontWeight="bold" fontSize="md" mb={1}>{widget.name}</Text>
|
||||||
|
<Text fontSize="sm" color="gray.500" mb={4} flex="1">{widget.description}</Text>
|
||||||
|
|
||||||
|
<Box borderTop="1px solid" borderColor="gray.100" pt={3}>
|
||||||
|
<Text fontSize="xs" fontWeight="bold" color="gray.400" textTransform="uppercase" mb={2}>Add to Grid:</Text>
|
||||||
|
<HStack gap={2} flexWrap="wrap">
|
||||||
|
{Object.keys(widget.allowedSizes).map(sizeKey => (
|
||||||
|
<Badge
|
||||||
|
key={sizeKey}
|
||||||
|
as="button"
|
||||||
|
onClick={() => addWidget(widget.id, sizeKey)}
|
||||||
|
colorScheme="gray"
|
||||||
|
variant="subtle"
|
||||||
|
px={3}
|
||||||
|
py={1}
|
||||||
|
borderRadius="full"
|
||||||
|
cursor="pointer"
|
||||||
|
_hover={{ bg: "blue.50", color: "blue.600", transform: "scale(1.05)" }}
|
||||||
|
transition="all 0.2s"
|
||||||
|
>
|
||||||
|
<Plus size={12} style={{ display: 'inline', marginRight: '4px' }}/>
|
||||||
|
{sizeKey.charAt(0).toUpperCase() + sizeKey.slice(1)}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</HStack>
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
</Tabs.Content>
|
||||||
|
))}
|
||||||
|
</Tabs.Root>
|
||||||
|
</Dialog.Body>
|
||||||
|
<Dialog.CloseTrigger position="absolute" top={3} right={3} color="white" />
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Positioner>
|
||||||
|
</Dialog.Root>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import Widget from "./Widget";
|
import Widget from "./Widget.jsx";
|
||||||
import { Fieldset, NativeSelect, For, Box, Field, Input, Button, Text, Flex } from "@chakra-ui/react";
|
import { Fieldset, NativeSelect, For, Box, Field, Input, Button, Text, Flex } from "@chakra-ui/react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
export const WIDGET_REGISTRY = {
|
export const WIDGET_REGISTRY = {
|
||||||
"vaa_activity": {
|
"vaa_activity": {
|
||||||
name: "VAA 7 Day Activity",
|
name: "VAA 7 Day Activity",
|
||||||
|
category: "Aviation",
|
||||||
description: "Displays 7-day flight level timelines across multiple volcanoes.",
|
description: "Displays 7-day flight level timelines across multiple volcanoes.",
|
||||||
allowedSizes: {
|
allowedSizes: {
|
||||||
large: { w: 12, h: 8 }, // Spans the entire width of the screen
|
large: { w: 12, h: 8 }, // Spans the entire width of the screen
|
||||||
@@ -17,6 +18,7 @@ export const WIDGET_REGISTRY = {
|
|||||||
|
|
||||||
"db_cache_health": {
|
"db_cache_health": {
|
||||||
name: "DB Cache Health",
|
name: "DB Cache Health",
|
||||||
|
category: "Computer Science",
|
||||||
description: "Bar chart showing cache table operational status.",
|
description: "Bar chart showing cache table operational status.",
|
||||||
allowedSizes: {
|
allowedSizes: {
|
||||||
medium: { w: 4, h: 6 }, // 1/3 width
|
medium: { w: 4, h: 6 }, // 1/3 width
|
||||||
@@ -26,6 +28,7 @@ export const WIDGET_REGISTRY = {
|
|||||||
|
|
||||||
"ivans_alerts": {
|
"ivans_alerts": {
|
||||||
name: "IVANS",
|
name: "IVANS",
|
||||||
|
category: "Communications",
|
||||||
description: "Feed of recent IVANS reports and reviewer statuses.",
|
description: "Feed of recent IVANS reports and reviewer statuses.",
|
||||||
allowedSizes: {
|
allowedSizes: {
|
||||||
small: { w: 3, h: 6 }, // 1/4 width (tall and skinny)
|
small: { w: 3, h: 6 }, // 1/4 width (tall and skinny)
|
||||||
@@ -35,6 +38,7 @@ export const WIDGET_REGISTRY = {
|
|||||||
|
|
||||||
"remote_sensing": {
|
"remote_sensing": {
|
||||||
name: "Remote Sensing",
|
name: "Remote Sensing",
|
||||||
|
category: "Remote Sensing",
|
||||||
description: "Feed of recent remote sensing reports.",
|
description: "Feed of recent remote sensing reports.",
|
||||||
allowedSizes: {
|
allowedSizes: {
|
||||||
medium: { w: 4, h: 5 }, // 1/3 width
|
medium: { w: 4, h: 5 }, // 1/3 width
|
||||||
@@ -43,6 +47,7 @@ export const WIDGET_REGISTRY = {
|
|||||||
|
|
||||||
"alert_levels": {
|
"alert_levels": {
|
||||||
name: "Alert Level Changes",
|
name: "Alert Level Changes",
|
||||||
|
category: "Daily Alerts",
|
||||||
description: "List of 7-day alert level changes with trend arrows.",
|
description: "List of 7-day alert level changes with trend arrows.",
|
||||||
allowedSizes: {
|
allowedSizes: {
|
||||||
small: { w: 3, h: 5 },
|
small: { w: 3, h: 5 },
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
|
import { WIDGET_REGISTRY } from '../constants/widgetRegistry.jsx';
|
||||||
|
|
||||||
|
export const useDashboardStore = create((set, get) => ({
|
||||||
|
// ==========================================
|
||||||
|
// 1. STATE (The Data)
|
||||||
|
// ==========================================
|
||||||
|
isEditing: false,
|
||||||
|
widgets: [], // The master array of what is currently on the screen
|
||||||
|
backupWidgets: [], // Holds the layout snapshot for the "Cancel" button
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 2. ACTIONS (The Logic)
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
enterEditMode: () => set({
|
||||||
|
isEditing: true,
|
||||||
|
backupWidgets: get().widgets // 📸 Take a snapshot of current positions
|
||||||
|
}),
|
||||||
|
|
||||||
|
cancelEdit: () => set({
|
||||||
|
isEditing: false,
|
||||||
|
widgets: get().backupWidgets // ⏪ Restore snapshot, instantly undoing any drags
|
||||||
|
}),
|
||||||
|
|
||||||
|
saveEdit: async () => {
|
||||||
|
const payload = get().widgets;
|
||||||
|
|
||||||
|
// 💾 This is perfectly formatted for your backend DB right now
|
||||||
|
console.log("Saving dashboard to API:", JSON.stringify(payload, null, 2));
|
||||||
|
|
||||||
|
// Example: await fetch('/api/user/dashboard', { method: 'POST', body: JSON.stringify(payload) });
|
||||||
|
|
||||||
|
set({ isEditing: false, backupWidgets: [] });
|
||||||
|
},
|
||||||
|
|
||||||
|
addWidget: (widgetId, sizeKey) => {
|
||||||
|
// Look up the exact rules for this widget from your constant
|
||||||
|
const registryEntry = WIDGET_REGISTRY[widgetId];
|
||||||
|
|
||||||
|
if (!registryEntry || !registryEntry.allowedSizes[sizeKey]) {
|
||||||
|
console.error(`Invalid widget ID or Size: ${widgetId} - ${sizeKey}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dimensions = registryEntry.allowedSizes[sizeKey];
|
||||||
|
|
||||||
|
const newWidget = {
|
||||||
|
id: uuid(),
|
||||||
|
type: widgetId,
|
||||||
|
size: sizeKey,
|
||||||
|
x: 0,
|
||||||
|
y: Infinity, // RGL's auto-packer drops this exactly into the first available slot at the bottom
|
||||||
|
w: dimensions.w,
|
||||||
|
h: dimensions.h
|
||||||
|
};
|
||||||
|
|
||||||
|
set((state) => ({ widgets: [...state.widgets, newWidget] }));
|
||||||
|
},
|
||||||
|
|
||||||
|
removeWidget: (id) => set((state) => ({
|
||||||
|
widgets: state.widgets.filter(w => w.id !== id)
|
||||||
|
})),
|
||||||
|
|
||||||
|
// Called automatically by React-Grid-Layout when the user lets go of the mouse
|
||||||
|
updateLayoutPositions: (rglLayout) => {
|
||||||
|
set((state) => ({
|
||||||
|
widgets: state.widgets.map(widget => {
|
||||||
|
// Find where RGL says this widget currently is
|
||||||
|
const gridItem = rglLayout.find(l => l.i === widget.id);
|
||||||
|
|
||||||
|
// Only update X and Y. We completely ignore W and H so users can't resize!
|
||||||
|
return gridItem ? { ...widget, x: gridItem.x, y: gridItem.y } : widget;
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}));
|
||||||
Reference in New Issue
Block a user