Files
Web-Frontend/client/src/pages/home/tabs/MyDashboard.jsx
T

195 lines
7.1 KiB
React
Raw Normal View History

2026-07-29 13:48:38 -07:00
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';
// 1. Import database messenger
import useAuthStore from "@/store/authStore";
// 2. Import registry
2026-07-29 13:48:38 -07:00
import {WIDGET_REGISTRY} from '@/constants/widgetRegistry.jsx';
2025-07-21 14:27:53 -07:00
// React-Grid-Layout requires this wrapper to automatically calculate screen width
const ResponsiveGridLayout = WidthProvider(Responsive);
2025-07-22 11:58:10 -07:00
export default function MyDashboard() {
2026-07-29 13:48:38 -07:00
// ☁️ Database Connection
const {user} = useAuthStore();
2026-07-29 13:48:38 -07:00
// ☁️ 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
2025-07-22 13:41:32 -07:00
2026-07-29 13:48:38 -07:00
// 🔒 Safety lock to prevent infinite re-renders
const hasHydrated = useRef(false);
2026-07-29 13:48:38 -07:00
// 1. Pull the new global slot recipe
const recipe = useSlotRecipe({key: "glassPanel"});
const styles = recipe({ layout: "widget" });
const popoverStyles = recipe({ layout: "popover" });
2026-07-29 13:48:38 -07:00
useEffect(() => {
if (user?.preferences?.dashboard_layout && !hasHydrated.current) {
const savedLayout = user.preferences.dashboard_layout;
2026-07-29 13:48:38 -07:00
// 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]);
2026-07-29 13:48:38 -07:00
// 🛠️ 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.
}));
2026-07-08 15:05:50 -07:00
2026-07-29 13:48:38 -07:00
// 3. THE COMPONENT MAPPER
const renderWidget = (widget) => {
const registryEntry = WIDGET_REGISTRY[widget.type];
2026-07-08 15:05:50 -07:00
2026-07-29 13:48:38 -07:00
if (registryEntry && registryEntry.component) {
const Component = registryEntry.component;
2026-07-08 15:05:50 -07:00
2026-07-29 13:48:38 -07:00
// 1. Check if this widget type has a settings component registered
const SettingsUI = registryEntry.settingsComponent;
2026-07-10 14:56:11 -07:00
2026-07-29 13:48:38 -07:00
return (
<>
<Box css={styles.header}>
<Text color="titleColor" fontWeight="semibold" fontSize="md" isTruncated>
{registryEntry.name}
</Text>
2026-07-08 15:05:50 -07:00
2026-07-29 13:48:38 -07:00
{/* THE SETTINGS ELLIPSIS */}
{SettingsUI ? (
<Popover.Root positioning={{placement: "bottom-end"}}>
<Popover.Trigger asChild>
<IconButton
size="xs"
variant="ghost"
color="gray.400"
borderRadius="xl"
_hover={{color: "white", bg: "gray.700"}}
aria-label="Widget Settings"
>
<MoreVertical size={16}/>
</IconButton>
</Popover.Trigger>
2026-07-08 15:05:50 -07:00
2026-07-29 13:48:38 -07:00
{/* NEW: The Positioner lifts the content out of the Flexbox flow */}
<Popover.Positioner zIndex={10}>
<Popover.Content css={popoverStyles.root}>
2026-07-29 13:48:38 -07:00
<Suspense fallback={<Spinner size="sm"/>}>
<SettingsUI
widgetId={widget.id}
currentSettings={widget.settings || {}}
/>
</Suspense>
</Popover.Content>
</Popover.Positioner>
</Popover.Root>
) : (
<Box w="20px" h="20px"/>
2026-07-08 15:05:50 -07:00
)}
2026-07-29 13:48:38 -07:00
</Box>
<Box css={styles.body}>
<Component settings={widget.settings || {}}/>
</Box>
</>
);
}
// Fallback if someone asks for a widget that doesn't exist
return (
<Flex css={styles.body} align="center" justify="center" direction="column">
<Text fontWeight="bold" color="red.400">Error</Text>
<Text fontSize="sm" color="gray.400">Widget '{widget.type}' not found.</Text>
</Flex>
2026-07-08 15:05:50 -07:00
);
2026-07-29 13:48:38 -07:00
};
return (
<Box width="100%" height="100%">
<ResponsiveGridLayout
className="layout"
layouts={{lg: rglLayout}} // We feed it the formatted layout map
breakpoints={{lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0}}
cols={{lg: 12, md: 10, sm: 6, xs: 4, xxs: 2}} // Our 12-column master grid
rowHeight={30}
isDraggable={isEditing} // Only allows dragging when Settings tools toggle this
isResizable={false} // Double lock-down
onLayoutChange={(newLayout) => updateLayoutPositions(newLayout)} // Fires when user drops a widget
compactType="vertical" // Auto-packs widgets to the top
>
{/* 3. 🎨 Paint the Widgets */}
{widgets.map(widget => (
<Box
key={widget.id}
css={styles.root}
// 3. Grid-specific state overrides remain inline
boxShadow={isEditing ? "outline" : styles.root.boxShadow}
borderStyle={isEditing ? "dashed" : "solid"}
borderWidth={isEditing ? "2px" : "1px"}
borderColor={isEditing ? "orange.300" : styles.root.borderColor}
2026-07-29 13:48:38 -07:00
cursor={isEditing ? "grab" : "default"}
position="relative" // 3. REQUIRED so the absolute button stays inside this box
>
{/* 4. THE DELETE BUTTON */}
{isEditing && (
<IconButton
aria-label="Remove widget"
position="absolute"
top={2}
right={2}
size="xs"
bg="white"
color="black"
border="1px solid"
borderRadius="xl"
borderColor="gray.200"
_hover={{bg: "red.50", color: "red.500", borderColor: "red.200"}}
zIndex={2}
onClick={() => removeWidget(widget.id)}
onMouseDown={(e) => e.stopPropagation()} // 🚫 Stops RGL from grabbing the widget
onTouchStart={(e) => e.stopPropagation()} // 🚫 Same protection for mobile touches
>
<X size={14}/>
</IconButton>
)}
{/* 4. SUSPENSE BOUNDARY wrapping the render function */}
<Suspense fallback={
<Flex css={styles.body} align="center" justify="center">
<Spinner size="md" color="blue.400" thickness="3px"/>
</Flex>
}>
{renderWidget(widget)}
</Suspense>
</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>
)}
</Box>
);
}