diff --git a/client/src/components/dashboard/SettingsButton.jsx b/client/src/components/dashboard/SettingsButton.jsx index d1fbfb0..cf2a376 100644 --- a/client/src/components/dashboard/SettingsButton.jsx +++ b/client/src/components/dashboard/SettingsButton.jsx @@ -1,162 +1,170 @@ -import { useState, useMemo, useRef } from "react"; +import {useState, useMemo, useRef} from "react"; import { - Button, - IconButton, - Box, - Text, - Flex, - Grid, - Dialog, - Tabs, - Badge, - Menu, - HStack + Button, + Box, + Text, + Flex, + Grid, + Dialog, + Tabs, + Badge, + Menu, + HStack, + useRecipe, useSlotRecipe } 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"; +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); + // 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); - // This now handles both local UI lock AND database saving! - const saveEdit = useDashboardStore(state => state.saveEdit); + const recipe = useRecipe({key: "glassTab"}); + const tabStyles = recipe(); - const cancelEdit = useDashboardStore(state => state.cancelEdit); - const addWidget = useDashboardStore(state => state.addWidget); + // Pull the glass container styles for the dropdown + const panelRecipe = useSlotRecipe({key: "glassPanel"}); + const panelStyles = panelRecipe({ layout: "dropdown" }); - const [isLibraryOpen, setIsLibraryOpen] = useState(false); + const [isLibraryOpen, setIsLibraryOpen] = useState(false); - // Smart Add Handler - const handleAddWidget = (widgetId, sizeKey) => { - enterEditMode(); - addWidget(widgetId, sizeKey); - }; + // Smart Add Handler + const handleAddWidget = (widgetId, sizeKey) => { + enterEditMode(); + addWidget(widgetId, sizeKey); + }; - 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 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); + const categories = Object.keys(categorizedWidgets); - // use ref to capture html node for placing menu accordingly - const menuRef = useRef(null); - const getAnchorRect = () => { - if (!menuRef.current) return null; - return menuRef.current.getBoundingClientRect(); - }; + // use ref to capture html node for placing menu accordingly + const menuRef = useRef(null); + const getAnchorRect = () => { + if (!menuRef.current) return null; + return menuRef.current.getBoundingClientRect(); + }; - return ( - <> - {/* THE STATIC DROPDOWN MENU */} - - - - - - - - - {isEditing ? ( - <> - {/* WIRE DIRECTLY TO saveEdit */} - - Save Layout - + return ( + <> + {/* THE STATIC DROPDOWN MENU */} + + + + {/* size increased to 24 to match tabs. flexShrink: 0 is CRITICAL */} + - - Discard Changes - - - ) : ( - - Edit Layout - - )} + {/* Dynamic text based on editing state! */} + + {isEditing ? "Editing Layout..." : "Dashboard Settings"} + + + - + + + {isEditing ? ( + <> + + Save Layout + - setIsLibraryOpen(true)} cursor="pointer" _hover={{ bg: "gray.100" }}> - Widget Library - - - - + + Discard Changes + + + ) : ( + + Edit Layout + + )} - {/* THE WIDGET LIBRARY MODAL (Controlled by isLibraryOpen) */} - setIsLibraryOpen(e.open)} size="xl" placement="center"> - - - - - Add Dashboard Widgets - + - - - - {categories.map(cat => ( - {cat} - ))} - + setIsLibraryOpen(true)} cursor="pointer" _hover={{fontWeight: "semibold"}}> + Widget Library + + + + - {categories.map(cat => ( - - - {categorizedWidgets[cat].map(widget => ( - - {widget.name} - {widget.description} + {/* THE WIDGET LIBRARY MODAL (Controlled by isLibraryOpen) */} + setIsLibraryOpen(e.open)} size="xl" placement="center"> + + + + + Add Dashboard Widgets + - - Add to Grid: - - {Object.keys(widget.allowedSizes).map(sizeKey => ( - handleAddWidget(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" - > - - {sizeKey.charAt(0).toUpperCase() + sizeKey.slice(1)} - - ))} - - - - ))} - - - ))} - - - - - - - - ); + + + + {categories.map(cat => ( + {cat} + ))} + + + {categories.map(cat => ( + + + {categorizedWidgets[cat].map(widget => ( + + {widget.name} + {widget.description} + + + Add + to Grid: + + {Object.keys(widget.allowedSizes).map(sizeKey => ( + handleAddWidget(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" + > + + {sizeKey.charAt(0).toUpperCase() + sizeKey.slice(1)} + + ))} + + + + ))} + + + ))} + + + + + + + + ); } \ No newline at end of file diff --git a/client/src/components/deprecated/AlertLevelWidget.jsx b/client/src/components/deprecated/AlertLevelWidget.jsx deleted file mode 100644 index b3b3afb..0000000 --- a/client/src/components/deprecated/AlertLevelWidget.jsx +++ /dev/null @@ -1,79 +0,0 @@ -import Widget from "./Widget.jsx"; -import { Fieldset, NativeSelect, For, Box, Field, Input, Button, Text, Flex } from "@chakra-ui/react"; -import { useForm } from "react-hook-form"; -import { useState } from "react"; -import { Asterisk } from 'lucide-react'; - -export default function AlertLevel({id, isEditable, isDelete, DeleteWidget}) { - const { register, handleSubmit, reset, formState: {errors} } = useForm(); - const [isSubmit, setIsSubmit] = useState(false); - - const onSubmit = (data) => { - setIsSubmit(true); - reset(); - } - - // Prop Forwarding - const widgetProps = { id, isEditable, isDelete, DeleteWidget, isSubmit, setIsSubmit } - - return ( - - {!isSubmit ? ( - // DISPLAYING INPUT FIElDS AND LABELS - - - - - Volcano - - - - - Country - - - - {(item) => ( - - )} - - - - - - - - - - - ):( - // DISPLAYING ALERT LEVEL DATA - - - Alert Level Data Coming Soon. - - - )} - - ); -} \ No newline at end of file diff --git a/client/src/components/deprecated/CancelButton.jsx b/client/src/components/deprecated/CancelButton.jsx deleted file mode 100644 index 28b68a7..0000000 --- a/client/src/components/deprecated/CancelButton.jsx +++ /dev/null @@ -1,7 +0,0 @@ -import { Button } from "@chakra-ui/react"; - -export function CancelButton ({onCancel}) { - return ( - - ) -} \ No newline at end of file diff --git a/client/src/components/deprecated/Canvas.jsx b/client/src/components/deprecated/Canvas.jsx deleted file mode 100644 index 5b449f1..0000000 --- a/client/src/components/deprecated/Canvas.jsx +++ /dev/null @@ -1,39 +0,0 @@ -// DEPRECATED - -// import GlobalMap from "./views/GlobalMap"; -// import RegionalMap from "./views/RegionalMap"; -// import Volcano from "./views/Volcano"; -// import Admin from "./views/Admin"; -// import Account from "./views/Account" -// import Home from "./views/Home"; -// import { DASHBOARD_VIEWS, ADMIN_VIEWS, VOLCANO_VIEWS } from "@/constants/viewKeys"; -// import { chakra } from "@chakra-ui/react"; -// import Personal from "./views/Personal"; -// import Security from "./views/Security"; -// -// export default function Canvas(props) { -// const { view } = props; -// -// return ( -// -// {/* Check if view is a tab inside the Home Header */} -// { DASHBOARD_VIEWS.includes(view) && ( -// -// )} -// {/* Check if view is a tab inside the Volcano Header */} -// { VOLCANO_VIEWS.includes(view) && ( -// -// )} -// {/* Check if view is a tab inside the Admin Header */} -// { ADMIN_VIEWS.includes(view) && ( -// -// )} -// {view === "global" && } -// {view === "regional" && } -// {view === "account" && } -// {view === "settings" && } -// {view === "personal" && } -// {view === "security" && } -// -// ); -// } \ No newline at end of file diff --git a/client/src/components/deprecated/CustomResizeHandle.jsx b/client/src/components/deprecated/CustomResizeHandle.jsx deleted file mode 100644 index 9fa3730..0000000 --- a/client/src/components/deprecated/CustomResizeHandle.jsx +++ /dev/null @@ -1,27 +0,0 @@ -import { MoveDiagonal2 } from 'lucide-react'; -import React from "react"; - -const CustomResizeHandle = React.forwardRef((props, ref) => { - const { handleAxis, isVisable, ...restProps } = props; - if (!isVisable) return null; - return ( -
- -
- ); -}); - -export default CustomResizeHandle; \ No newline at end of file diff --git a/client/src/components/deprecated/Dashboard.jsx b/client/src/components/deprecated/Dashboard.jsx deleted file mode 100644 index 275e680..0000000 --- a/client/src/components/deprecated/Dashboard.jsx +++ /dev/null @@ -1,95 +0,0 @@ -// DEPRECATED - -// import Header from './Header/Header.jsx'; -// import Sidebar from './Sidebar/Sidebar.jsx'; -// import Canvas from './Canvas/Canvas.jsx'; -// import { Grid, GridItem, useRecipe, useDialog } from '@chakra-ui/react'; -// import { useState } from 'react'; -// import DialogPopup from './Canvas/CanvasComponents/DialogPopup.jsx'; -// -// export default function Dashboard() { -// const recipe = useRecipe({ key: "dashboard" }); -// const styles = recipe(); -// -// const [darkMode, setDarkMode] = useState(false); -// const [isEditable, setIsEditable] = useState(false); // editable=true means static=false (vice versa) -// const [isDelete, setIsDelete] = useState(false); -// const [originalLayout, setOriginalLayout] = useState([]); -// const [originalWidgets, setOriginalWidgets] = useState([]); -// const [view, setView] = useState("mydashboard"); -// const [layout, setLayout] = useState([]); -// const [widgetArray, setWidgetArray] = useState([]); -// const dialog = useDialog(); -// -// const beginEditIfNeeded = () => { -// if ( !isEditable ) { -// setOriginalLayout(layout); -// setOriginalWidgets(widgetArray); -// } -// }; -// -// const onCancel = () => { -// console.log("canceling changes..."); -// console.log(originalWidgets); -// if (originalLayout.length) { -// setLayout(originalLayout.map(item => ({ -// ...item, -// static: true -// }))); -// }; -// setWidgetArray(originalWidgets); -// setIsEditable(false); -// setIsDelete(false); -// setOriginalLayout([]); -// setOriginalWidgets([]); -// } -// -// // Prevent navigation while in Edit mode without saving or cancelling -// const handleViewChange = (nextView) => { -// console.log("1. handling view change") -// if (view === "mydashboard" && isEditable) { -// console.log("2. currently editing"); -// dialog.setOpen(true); -// return; -// -// // const confirmed = window.confirm( -// // "You have unsaved changes. Discard them and continue?" -// // ); -// // if (!confirmed) return; -// // onCancel(); -// } -// setView(nextView); -// }; -// -// const onLayoutChange = (newLayout) => setLayout(newLayout); -// -// // Prop Forwarding -// const canvasProps = { view, layout, setLayout, setOriginalLayout, onLayoutChange, onChangeView: handleViewChange, -// onCancel, isEditable, setIsEditable, isDelete, setIsDelete, widgetArray, setWidgetArray, originalWidgets, setOriginalWidgets, beginEditIfNeeded -// }; -// const sidebarProps = { view, onChangeView: handleViewChange, darkMode, setDarkMode }; -// const headerProps = { onChangeView: handleViewChange }; -// -// return ( -// <> -// -// -// -//
-// -// -// -// -// -// -// -// -// -// ); -// } \ No newline at end of file diff --git a/client/src/components/deprecated/DeleteAllButton.jsx b/client/src/components/deprecated/DeleteAllButton.jsx deleted file mode 100644 index 016d49a..0000000 --- a/client/src/components/deprecated/DeleteAllButton.jsx +++ /dev/null @@ -1,9 +0,0 @@ -import { Button } from "@chakra-ui/react"; - -export default function DeleteAllButton ({onDeleteAll}) { - return ( - - ) -} \ No newline at end of file diff --git a/client/src/components/deprecated/FirstWidgetButton.jsx b/client/src/components/deprecated/FirstWidgetButton.jsx deleted file mode 100644 index 5c19f03..0000000 --- a/client/src/components/deprecated/FirstWidgetButton.jsx +++ /dev/null @@ -1,57 +0,0 @@ -import { Box, Menu, Portal } from '@chakra-ui/react' -import AlertLevel from "@/components/deprecated/AlertLevelWidget.jsx"; - -export default function FirstWidgetButton({ appendWidget }) { - const triggerItem = ( - <> - - - Add Your First Widget - - - - ); - const positioning = { placement: "bottom-middle" }; - // const WidgetMenuProps = { appendWidget, triggerItem, positioning }; - return ( - // - - {triggerItem} - - - - appendWidget(AlertLevel, 4, 7, 4, 7, 8, 14)} // width and height are passed through here for each widget - > - Alert Level - - CO2 - IVANS - Plume Heights - Alert Level Changes - - - - - ); -} \ No newline at end of file diff --git a/client/src/components/deprecated/SaveButton.jsx b/client/src/components/deprecated/SaveButton.jsx deleted file mode 100644 index 19365f0..0000000 --- a/client/src/components/deprecated/SaveButton.jsx +++ /dev/null @@ -1,7 +0,0 @@ -import { Button } from "@chakra-ui/react"; - -export function SaveButton ({onSave}) { - return ( - - ) -} \ No newline at end of file diff --git a/client/src/components/deprecated/Widget.jsx b/client/src/components/deprecated/Widget.jsx deleted file mode 100644 index c05678f..0000000 --- a/client/src/components/deprecated/Widget.jsx +++ /dev/null @@ -1,32 +0,0 @@ -import { useSlotRecipe, chakra, Flex, CloseButton, Text, IconButton } from "@chakra-ui/react" -import { Undo } from 'lucide-react'; - -export default function Widget(props) { - // deconstructing props - const { id, title, isEditable, isDelete, DeleteWidget, isSubmit, setIsSubmit, children } = props; - - const recipe = useSlotRecipe({ key: "widget" }); - const styles = recipe(); - - return ( - - {/* HEADER */} - - {title} - {isDelete ? ( - DeleteWidget(id)} size="xs" _hover={{bg: "tomato", color: "white"}}/> - ):( - isSubmit ? ( - setIsSubmit(false)} size="xs" bg="base300" cursor="pointer"> - ):null - )} - - - {/* CHILDREN */} - - {children} - - - ); -} - diff --git a/client/src/components/deprecated/WidgetMenu.jsx b/client/src/components/deprecated/WidgetMenu.jsx deleted file mode 100644 index 140af76..0000000 --- a/client/src/components/deprecated/WidgetMenu.jsx +++ /dev/null @@ -1,26 +0,0 @@ -import { Menu, Portal } from "@chakra-ui/react"; -import AlertLevel from "./AlertLevelWidget.jsx"; - -export default function WidgetMenu({appendWidget, triggerItem, positioning}) { - return ( - - {triggerItem} - - - - appendWidget(AlertLevel, 4, 7, 4, 7, 8, 14)} // width and height are passed through here for each widget - > - Alert Level - - CO2 - IVANS - Plume Heights - Alert Level Changes - - - - - ) -} \ No newline at end of file diff --git a/client/src/components/deprecated/tooltip.jsx b/client/src/components/deprecated/tooltip.jsx deleted file mode 100644 index ab225cc..0000000 --- a/client/src/components/deprecated/tooltip.jsx +++ /dev/null @@ -1,35 +0,0 @@ -import { Tooltip as ChakraTooltip, Portal } from '@chakra-ui/react' -import * as React from 'react' - -export const Tooltip = React.forwardRef(function Tooltip(props, ref) { - const { - showArrow, - children, - disabled, - portalled = true, - content, - contentProps, - portalRef, - ...rest - } = props - - if (disabled) return children - - return ( - - {children} - - - - {showArrow && ( - - - - )} - {content} - - - - - ) -}) diff --git a/client/src/pages/home/Home.jsx b/client/src/pages/home/Home.jsx index fab657a..4f34457 100644 --- a/client/src/pages/home/Home.jsx +++ b/client/src/pages/home/Home.jsx @@ -1,64 +1,69 @@ -import { HStack, Flex, Box } from "@chakra-ui/react"; -import { NavLink, Outlet, useLocation } from "react-router-dom"; -import { SearchBar } from "../../components/dashboard/SearchBar.jsx"; -import { SettingsButton } from "../../components/dashboard/SettingsButton.jsx"; +import {HStack, Flex, Box, useRecipe} from "@chakra-ui/react"; +import {NavLink, Outlet, useLocation} from "react-router-dom"; +import {SearchBar} from "../../components/dashboard/SearchBar.jsx"; +import {SettingsButton} from "../../components/dashboard/SettingsButton.jsx"; + +// 1. Import your icons (Swap these for whichever you prefer!) +import {LayoutDashboard, CalendarDays, Cloud, Satellite, Activity} from "lucide-react"; + +// 2. Define the tabs array +const HOME_TABS = [ + {name: "My Dashboard", url: "/home", icon: LayoutDashboard, exact: true}, + {name: "Daily Activity", url: "/home/daily", icon: CalendarDays}, + {name: "Gas", url: "/home/gas", icon: Cloud}, + {name: "Remote Sensing", url: "/home/remote", icon: Satellite}, + {name: "Seismic", url: "/home/seismic", icon: Activity}, +]; export default function Home() { + const recipe = useRecipe({key: "glassTab"}); + const tabStyles = recipe(); + const location = useLocation(); const isPersonalDashboard = location.pathname === "/home" || location.pathname === "/home/"; - // 1. CSS making this look like how we want it, TODO turn to theme? - const tabStyles = { - fontSize: "lg", - fontWeight: "bold", - color: "fg.muted", - px: 2, // Horizontal padding - // py: 2, // Vertical padding - borderBottom: "1px solid transparent", - transition: "all 0.2s", - _hover: { color: "fg" }, - - "&.active": { // Target the active class applied by React Router NavLink - color: "fg", - borderColor: "fg", - } - }; - return ( - + - {/* 2. πŸ› οΈ SUB HEADER: Matches your original padding and background */} - + - {/* TABS CONTAINER - Replicates the Tabs.List line container */} - - My Dashboard - Daily Activity - Gas - Remote Sensing - Seismic - + {/* TABS CONTAINER */} + + {HOME_TABS.map((tab) => { + const Icon = tab.icon; + return ( + + {/* flexShrink: 0 ensures the icon never squishes during the gooey animation */} + + {tab.name} + + ); + })} - {/* EDIT MODE BUTTONS & CONTROLS */} - - {isPersonalDashboard ? ( - <> - - {/* This will eventually trigger your Zustand store's edit mode! */} - - - ) : ( - - )} - + {/* The Settings button is now inline with the tabs and only renders on the personal dashboard */} + {isPersonalDashboard && } - {/* 3. πŸ› οΈ CANVAS AREA */} - - - + {/* EDIT MODE BUTTONS & CONTROLS */} + {/* Leaving this HStack here in case you plan to put the SearchBar or other global tools back on the right side */} + + {/* Future global controls go here */} + + + + + + + ); } \ No newline at end of file diff --git a/client/src/pages/home/tabs/MyDashboard.jsx b/client/src/pages/home/tabs/MyDashboard.jsx index e062b6c..fb98af2 100644 --- a/client/src/pages/home/tabs/MyDashboard.jsx +++ b/client/src/pages/home/tabs/MyDashboard.jsx @@ -1,208 +1,199 @@ -import { Box, Text, Flex, IconButton, Spinner, Popover } from "@chakra-ui/react"; -import { X, MoreVertical } from "lucide-react"; -import { Responsive, WidthProvider } from "react-grid-layout"; -import { useDashboardStore } from "@/store/useDashboardStore.jsx"; -import { Suspense, useEffect, useRef } from 'react'; -import { useColorModeValue } from "@/components/ui/color-mode"; +import {Box, Text, Flex, IconButton, Spinner, Popover, useSlotRecipe} from "@chakra-ui/react"; +import {X, MoreVertical} from "lucide-react"; +import {Responsive, WidthProvider} from "react-grid-layout"; +import {useDashboardStore} from "@/store/useDashboardStore.jsx"; +import {Suspense, useEffect, useRef} from 'react'; +import {useColorModeValue} from "@/components/ui/color-mode"; // 1. Import database messenger import useAuthStore from "@/store/authStore"; // 2. Import registry -import { WIDGET_REGISTRY } from '@/constants/widgetRegistry.jsx'; +import {WIDGET_REGISTRY} from '@/constants/widgetRegistry.jsx'; // React-Grid-Layout requires this wrapper to automatically calculate screen width const ResponsiveGridLayout = WidthProvider(Responsive); export default function MyDashboard() { - // πŸŒ— Let's adjust for our light and dark mode - const frameBg = useColorModeValue("FFFFFF0A", "#FFFFFF0A"); - const titleColor = useColorModeValue("gray.800", "#FFFFFF"); - const iconColor = useColorModeValue("gray.600", "#FFFFFF"); - const iconHoverBg = useColorModeValue("gray.100", "gray.700"); + // πŸŒ— Let's adjust for our light and dark mode + const frameBg = useColorModeValue("FFFFFF0A", "#FFFFFF0A"); + const titleColor = useColorModeValue("gray.800", "#FFFFFF"); + const iconColor = useColorModeValue("gray.600", "#FFFFFF"); + const iconHoverBg = useColorModeValue("gray.100", "gray.700"); - // ☁️ Database Connection - const { user } = useAuthStore(); + // ☁️ Database Connection + const {user} = useAuthStore(); - // ☁️ Local UI Connections - const widgets = useDashboardStore(state => state.widgets); - const isEditing = useDashboardStore(state => state.isEditing); - const updateLayoutPositions = useDashboardStore(state => state.updateLayoutPositions); - const removeWidget = useDashboardStore(state => state.removeWidget); - const setWidgets = useDashboardStore(state => state.setWidgets); // The injection action + // ☁️ Local UI Connections + const widgets = useDashboardStore(state => state.widgets); + const isEditing = useDashboardStore(state => state.isEditing); + const updateLayoutPositions = useDashboardStore(state => state.updateLayoutPositions); + const removeWidget = useDashboardStore(state => state.removeWidget); + const setWidgets = useDashboardStore(state => state.setWidgets); // The injection action - // πŸ”’ Safety lock to prevent infinite re-renders - const hasHydrated = useRef(false); + // πŸ”’ Safety lock to prevent infinite re-renders + const hasHydrated = useRef(false); - // ========================================== - // INITIAL LOAD: Grab data from Django - // ========================================== - useEffect(() => { - if (user?.preferences?.dashboard_layout && !hasHydrated.current) { - const savedLayout = user.preferences.dashboard_layout; + // 1. Pull the new global slot recipe + const recipe = useSlotRecipe({key: "glassPanel"}); + const styles = recipe({ layout: "widget" }); - // Safety check: Only inject if it's actually an array of widgets. - // (Prevents a crash if MariaDB returns a default empty object {}) - if (Array.isArray(savedLayout)) { - setWidgets(savedLayout); - } + useEffect(() => { + if (user?.preferences?.dashboard_layout && !hasHydrated.current) { + const savedLayout = user.preferences.dashboard_layout; - hasHydrated.current = true; - } - }, [user, setWidgets]); + // Safety check: Only inject if it's actually an array of widgets. + // (Prevents a crash if MariaDB returns a default empty object {}) + if (Array.isArray(savedLayout)) { + setWidgets(savedLayout); + } + hasHydrated.current = true; + } + }, [user, setWidgets]); - // πŸ› οΈ Format the data for RGL - const rglLayout = widgets.map(w => ({ - i: w.id, - x: w.x, - y: w.y, - w: w.w, - h: w.h, - isResizable: false // 🚫 The magic lock! No resizing allowed globally. - })); + // πŸ› οΈ Format the data for RGL + const rglLayout = widgets.map(w => ({ + i: w.id, + x: w.x, + y: w.y, + w: w.w, + h: w.h, + isResizable: false // 🚫 The magic lock! No resizing allowed globally. + })); - // 3. THE COMPONENT MAPPER - const renderWidget = (widget) => { - const registryEntry = WIDGET_REGISTRY[widget.type]; + // 3. THE COMPONENT MAPPER + const renderWidget = (widget) => { + const registryEntry = WIDGET_REGISTRY[widget.type]; - if (registryEntry && registryEntry.component) { - const Component = registryEntry.component; + if (registryEntry && registryEntry.component) { + const Component = registryEntry.component; - // 1. Check if this widget type has a settings component registered - const SettingsUI = registryEntry.settingsComponent; + // 1. Check if this widget type has a settings component registered + const SettingsUI = registryEntry.settingsComponent; - return ( - // The "Frame" that holds both the Title and the Chart - + return ( + <> + + + {registryEntry.name} + - {/* THE TITLE BAR */} - - - {registryEntry.name} - + {/* THE SETTINGS ELLIPSIS */} + {SettingsUI ? ( + + + + + + - {/* THE SETTINGS ELLIPSIS */} - {SettingsUI ? ( - - - - - - - - {/* NEW: The Positioner lifts the content out of the Flexbox flow */} - - - }> - - - - - - ) : ( - - )} - - - {/* THE CHART CANVAS */} - - {/* Pass the settings object into the actual chart component */} - - - - - ); - } - - // Fallback if someone asks for a widget that doesn't exist - return ( - - Error - Widget '{widget.type}' not found. - - ); - }; - - return ( - - updateLayoutPositions(newLayout)} // Fires when user drops a widget - compactType="vertical" // Auto-packs widgets to the top - > - {/* 3. 🎨 Paint the Widgets */} - {widgets.map(widget => ( - - {/* 4. THE DELETE BUTTON */} - {isEditing && ( - removeWidget(widget.id)} - onMouseDown={(e) => e.stopPropagation()} // 🚫 Stops RGL from grabbing the widget - onTouchStart={(e) => e.stopPropagation()} // 🚫 Same protection for mobile touches - > - - - )} - - {/* 4. SUSPENSE BOUNDARY wrapping the render function */} - - - - }> - {renderWidget(widget)} - - - - ))} - - - {/* Empty State Helper */} - {widgets.length === 0 && ( - - Your dashboard is empty. Click Settings to add widgets. - + {/* NEW: The Positioner lifts the content out of the Flexbox flow */} + + + }> + + + + + + ) : ( + )} - + + + + + + + ); + } + + // Fallback if someone asks for a widget that doesn't exist + return ( + + Error + Widget '{widget.type}' not found. + ); + }; + + return ( + + updateLayoutPositions(newLayout)} // Fires when user drops a widget + compactType="vertical" // Auto-packs widgets to the top + > + {/* 3. 🎨 Paint the Widgets */} + {widgets.map(widget => ( + + {/* 4. THE DELETE BUTTON */} + {isEditing && ( + removeWidget(widget.id)} + onMouseDown={(e) => e.stopPropagation()} // 🚫 Stops RGL from grabbing the widget + onTouchStart={(e) => e.stopPropagation()} // 🚫 Same protection for mobile touches + > + + + )} + + {/* 4. SUSPENSE BOUNDARY wrapping the render function */} + + + + }> + {renderWidget(widget)} + + + ))} + + + {/* Empty State Helper */} + {widgets.length === 0 && ( + + Your dashboard is empty. Click Settings to add widgets. + + )} + + ); } \ No newline at end of file diff --git a/client/src/theme.js b/client/src/theme.js index a3fb5a8..6140c0d 100644 --- a/client/src/theme.js +++ b/client/src/theme.js @@ -1,4 +1,4 @@ -import { defineSlotRecipe, createSystem, defaultConfig, defineConfig, defineRecipe } from "@chakra-ui/react"; +import {defineSlotRecipe, createSystem, defaultConfig, defineConfig, defineRecipe} from "@chakra-ui/react"; // RECIPES const headerRecipe = defineRecipe({ @@ -16,7 +16,7 @@ const headerRecipe = defineRecipe({ const dashboardRecipe = defineRecipe({ base: { - bg: "{testBackground}" + bg: "{MainBackground}" } }); @@ -33,7 +33,10 @@ const sidebarRecipe = defineSlotRecipe({ base: { root: { // 1. Semi-transparent backgrounds - bg: { base: "linear-gradient(to bottom right, #99000040 20%, #FF660040 60%, #EEEE0040 100%)", _dark: "linear-gradient(to bottom right, #9900003C 20%, #FF66003C 60%, #FFC0003C 100%)" }, + bg: { + base: "linear-gradient(to bottom right, #99000040 20%, #FF660040 60%, #EEEE0040 100%)", + _dark: "linear-gradient(to bottom right, #9900003C 20%, #FF66003C 60%, #FFC0003C 100%)" + }, // 2. The glass blur effect backdropFilter: "blur(12px)", @@ -41,7 +44,7 @@ const sidebarRecipe = defineSlotRecipe({ // 3. Subtle semi-transparent borders border: "1px solid", - borderColor: { base: "rgba(255, 255, 255, 0.8)", _dark: "rgba(255, 255, 255, 0.1)" }, + borderColor: {base: "rgba(255, 255, 255, 0.8)", _dark: "rgba(255, 255, 255, 0.1)"}, // CRITICAL: Remove the right border so it doesn't draw a line through your cutout borderRight: "none", @@ -94,11 +97,11 @@ const sidebarRecipe = defineSlotRecipe({ borderLeftRadius: "4xl", "&.active": { - bg: "{testBackground}", + bg: "{MainBackground}", color: "{textInverted}", _hover: { - bg: "{testBackground}", + bg: "{MainBackground}", color: "{textInverted}", fontWeight: "normal", "& svg": { @@ -114,7 +117,7 @@ const sidebarRecipe = defineSlotRecipe({ top: "-25px", width: "25px", height: "25px", - bg: "inherit", // This forces the curve to match {testButton} + bg: "inherit", WebkitMaskImage: "radial-gradient(circle at top left, transparent 25px, black 25.5px)", maskImage: "radial-gradient(circle at top left, transparent 25px, black 25.5px)", pointerEvents: "none", @@ -176,19 +179,155 @@ const widgetRecipe = defineSlotRecipe({ } }); +// const glassPanelRecipe = defineSlotRecipe({ +// slots: ["root", "header", "body"], +// base: { +// root: { +// // Structure +// display: "flex", +// flexDirection: "column", +// width: "100%", +// height: "100%", +// position: "relative", +// overflow: "hidden", +// borderRadius: "3xl", +// +// // Glass Aesthetics (Syncs nicely with your sidebar/tabs) +// bg: { base: "rgba(255, 255, 255, 0.4)", _dark: "rgba(255, 255, 255, 0.04)" }, +// backdropFilter: "blur(12px)", +// WebkitBackdropFilter: "blur(12px)", +// border: "1px solid", +// borderColor: { base: "gray.200", _dark: "whiteAlpha.200" }, +// boxShadow: "sm", +// }, +// header: { +// display: "flex", +// justifyContent: "space-between", +// alignItems: "center", +// px: 3, +// py: 2, +// }, +// body: { +// flex: "1", +// overflow: "hidden", +// position: "relative", +// p: 1, +// } +// } +// }); + +const glassPanelRecipe = defineSlotRecipe({ + slots: ["root", "header", "body"], + + // 🧊 THE GLASS MATH (Applies to everything) + base: { + root: { + display: "flex", + flexDirection: "column", + position: "relative", + overflow: "hidden", + bg: {base: "rgba(255, 255, 255, 0.4)", _dark: "rgba(255, 255, 255, 0.04)"}, + backdropFilter: "blur(12px)", + WebkitBackdropFilter: "blur(12px)", + border: "1px solid", + borderColor: {base: "gray.200", _dark: "whiteAlpha.200"}, + boxShadow: "sm", + } + }, + + // πŸ“ THE LAYOUTS (Applies selectively) + variants: { + layout: { + widget: { + root: {width: "100%", height: "100%", borderRadius: "3xl"}, + header: {display: "flex", justifyContent: "space-between", alignItems: "center", px: 3, pt: 2, pb: 0}, + body: {flex: "1", overflow: "hidden", position: "relative", px: 1, pb: 1, pt: 0} + }, + dropdown: { + root: {borderRadius: "xl", p: 1} + // (Header and body slots are ignored here because dropdowns don't use them) + } + } + }, + + defaultVariants: { + layout: "widget" + } +}); +const glassTabRecipe = defineRecipe({ + base: { + display: "flex", + alignItems: "center", + height: "40px", + + // The Circle State (Inactive) + maxWidth: "40px", + minWidth: "40px", + px: "8px", + gap: 2, + + // CRITICAL: Locks the shape to a perfect circle/pill + borderRadius: "full", + + overflow: "hidden", + whiteSpace: "nowrap", + textDecoration: "none", + fontWeight: "bold", + color: "fg.muted", + + // --- THE GLASS EFFECT (Inactive) --- + // Uses a highly transparent white/black depending on mode + bg: {base: "rgba(255, 255, 255, 0.4)", _dark: "rgba(255, 255, 255, 0.03)"}, + backdropFilter: "blur(12px)", + WebkitBackdropFilter: "blur(12px)", + border: "1px solid", + borderColor: {base: "rgba(0, 0, 0, 0.05)", _dark: "rgba(255, 255, 255, 0.08)"}, + boxShadow: "0 4px 6px rgba(0, 0, 0, 0.02)", // Tiny shadow to lift it off the canvas + + transition: "all 0.85s cubic-bezier(0.34, 1.56, 0.64, 1)", + + "&:hover, &[data-state='open'], &[aria-expanded='true']": { + maxWidth: "250px", + pr: "16px", + // --- THE GLASS EFFECT (Hover) --- + // Becomes slightly more opaque when you mouse over + bg: {base: "rgba(255, 255, 255, 0.8)", _dark: "rgba(255, 255, 255, 0.1)"}, + borderColor: {base: "rgba(0, 0, 0, 0.1)", _dark: "rgba(255, 255, 255, 0.15)"}, + color: "fg", + textDecoration: "none", + }, + + "&.active": { + maxWidth: "250px", + pr: "16px", + + bg: "{gooeyButtonBackground}", + borderColor: "transparent", + color: "{textInverted}", + boxShadow: "0 4px 12px rgba(0, 0, 0, 0.1)", // Stronger shadow for the active tab + + _hover: { + bg: "{gooeyButtonBackground}", + cursor: "default", + textDecoration: "none", + } + } + } +}); + const config = defineConfig({ globalCss: { ".react-grid-placeholder": { - background: "#c0d2e3ff !important", - borderColor: "#a9b6c2ff !important", - boxShadow: "0 0 10px #c0d2e3ff !important", - opacity: "1 !important", - borderRadius: "5px", - _dark: { - background: "#393c3fff !important", - borderColor: "#000000ff !important", - boxShadow: "0 0 10px #16181bff !important", + background: "#c0d2e3ff !important", + borderColor: "#a9b6c2ff !important", + boxShadow: "0 0 10px #c0d2e3ff !important", opacity: "1 !important", + borderRadius: "5px", + _dark: { + background: "#393c3fff !important", + borderColor: "#000000ff !important", + boxShadow: "0 0 10px #16181bff !important", + opacity: "1 !important", }, }, ".react-resizable-handle-se": { @@ -251,35 +390,38 @@ const config = defineConfig({ theme: { semanticTokens: { colors: { - base100: { value: {base: "#f6f9faff", _dark: "#1617199e"}}, - base200: { value: {base: "#e6ecf1ff", _dark: "#242424"}}, - base300: { value: {base: "#d3dae4ff", _dark: "#0a0b0bff"}}, - primary: { value: {base: "#2B98F8", _dark: "#2B98F8"}}, - primaryLt: { value: {base: "#98cdffff", _dark: "#98cdffff"}}, - secondaryBG: { value: {base: "#EDF1F1", _dark: "#121212"}}, + base100: {value: {base: "#f6f9faff", _dark: "#1617199e"}}, + base200: {value: {base: "#e6ecf1ff", _dark: "#242424"}}, + base300: {value: {base: "#d3dae4ff", _dark: "#0a0b0bff"}}, + primary: {value: {base: "#2B98F8", _dark: "#2B98F8"}}, + primaryLt: {value: {base: "#98cdffff", _dark: "#98cdffff"}}, + secondaryBG: {value: {base: "#EDF1F1", _dark: "#121212"}}, border: {value: {base: "#000000", _dark: "#cbcbcb84"}}, text: {value: {base: "#000000", _dark: "#FFFFFF"}}, textInverted: {value: {base: "#000000", _dark: "#FFFFFF"}}, icon: {value: {base: "#000000", _dark: "#FFFFFF"}}, - testBackground: { value: {base: "#F9F9F9", _dark: "#141414"} }, - testSidebar: { value: {base: "#FBFBFB", _dark: "#272421"} }, - testButton: { value: {base: "#FFFFFF", _dark: "#C09754"} }, - + gooeyButtonBackground: {value: {base: "rgba(255, 255, 255, 0.1)", _dark: "rgba(255, 255, 255, 0.1)"}}, + MainBackground: {value: {base: "#F9F9F9", _dark: "#141414"}}, + titleColor: {value: {base: "gray.800", _dark: "#FFFFFF"}}, + iconColor: {value: {base: "gray.600", _dark: "#000000"}}, + glassSeperator: {value: {base: "#0000001A", _dark: "#FFFFFF3F"}}, }, }, tokens: { fonts: { - body: { value: "system-ui, sans-serif" }, + body: {value: "system-ui, sans-serif"}, }, }, recipes: { header: headerRecipe, dashboard: dashboardRecipe, canvas: canvasRecipe, + glassTab: glassTabRecipe }, slotRecipes: { sidebar: sidebarRecipe, widget: widgetRecipe, + glassPanel: glassPanelRecipe, }, }, });