diff --git a/client/src/components/dashboard/DialogPopup.jsx b/client/src/components/dashboard/DialogPopup.jsx index 9957dc9..bd50b91 100644 --- a/client/src/components/dashboard/DialogPopup.jsx +++ b/client/src/components/dashboard/DialogPopup.jsx @@ -1,7 +1,6 @@ import { Dialog, Portal, CloseButton, Button } from "@chakra-ui/react"; export default function DialogPopup({dialog}) { - console.log("3. dialog popup") return( diff --git a/client/src/components/dashboard/SettingsButton.jsx b/client/src/components/dashboard/SettingsButton.jsx index 1ed500f..fa3aff3 100644 --- a/client/src/components/dashboard/SettingsButton.jsx +++ b/client/src/components/dashboard/SettingsButton.jsx @@ -20,14 +20,21 @@ export function SettingsButton() { // 1. ☁Store connections const isEditing = useDashboardStore(state => state.isEditing); const enterEditMode = useDashboardStore(state => state.enterEditMode); + + // This now handles both local UI lock AND database saving! 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 + // 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'; @@ -46,24 +53,24 @@ export function SettingsButton() { - - {/* Dynamic Menu Options: What shows up depends on isEditing */} {isEditing ? ( <> + {/* WIRE DIRECTLY TO saveEdit */} Save Layout + Discard Changes @@ -79,7 +86,6 @@ export function SettingsButton() { setIsLibraryOpen(true)} cursor="pointer" _hover={{ bg: "gray.100" }}> Widget Library - @@ -88,7 +94,6 @@ export function SettingsButton() { - Add Dashboard Widgets @@ -116,7 +121,7 @@ export function SettingsButton() { addWidget(widget.id, sizeKey)} + onClick={() => handleAddWidget(widget.id, sizeKey)} colorScheme="gray" variant="subtle" px={3} diff --git a/client/src/pages/home/tabs/MyDashboard.jsx b/client/src/pages/home/tabs/MyDashboard.jsx index be7d108..9889124 100644 --- a/client/src/pages/home/tabs/MyDashboard.jsx +++ b/client/src/pages/home/tabs/MyDashboard.jsx @@ -2,23 +2,50 @@ import { Box, Text, Flex, IconButton, Spinner } from "@chakra-ui/react"; import { X } from "lucide-react"; import { Responsive, WidthProvider } from "react-grid-layout"; import { useDashboardStore } from "@/store/useDashboardStore.jsx"; -import { Suspense } from 'react'; +import { Suspense, useEffect, useRef } from 'react'; -// 2. Import registry! +// 1. Import database messenger +import useAuthStore from "@/store/authStore"; + +// 2. Import registry 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() { - // 1. ☁️ Connect to the Cloud (Zustand) + // ☁️ 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 - // 2. 🛠️ Format the data for RGL - // RGL expects a very specific array format for its layout prop + // 🔒 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; + + // 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, @@ -82,7 +109,7 @@ export default function MyDashboard() { 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 your Settings tools toggle this + 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 diff --git a/client/src/store/authStore.js b/client/src/store/authStore.js index 3c815f9..abafe24 100644 --- a/client/src/store/authStore.js +++ b/client/src/store/authStore.js @@ -19,7 +19,9 @@ const useAuthStore = create((set) => ({ firstName: res.data.first_name, lastName: res.data.last_name, email: res.data.email, - fullName: [res.data.first_name, res.data.last_name].filter(Boolean).join(' ') + fullName: [res.data.first_name, res.data.last_name].filter(Boolean).join(' '), + // Catch the layout and map data on fresh login! + preferences: res.data.preferences || {} } }); } catch (err) { @@ -42,7 +44,7 @@ const useAuthStore = create((set) => ({ checkAuth: async () => { try { // Hit the new lightweight endpoint - const res = await api.get('session'); + const res = await api.get('session/'); // Rehydrate the store with the user's info! set({ @@ -53,13 +55,37 @@ const useAuthStore = create((set) => ({ firstName: res.data.first_name, lastName: res.data.last_name, email: res.data.email, - fullName: [res.data.first_name, res.data.last_name].filter(Boolean).join(' ') + fullName: [res.data.first_name, res.data.last_name].filter(Boolean).join(' '), + // Catch the layout and map data on page refresh! + preferences: res.data.preferences || {} } }); } catch { // If it fails, ensure the store is completely wiped set({ isAuthenticated: false, isLoading: false, user: null }); } + }, + + // Allow the UI to save widget placement + saveDashboardLayout: async (newLayout) => { + try { + // 1. Optimistically update React state for zero UI lag + set((state) => ({ + user: { + ...state.user, + preferences: { + ...state.user.preferences, + dashboard_layout: newLayout + } + } + })); + + // 2. Quietly push the save command to Django + await api.post('dashboard/save/', { layout: newLayout }); + + } catch (error) { + console.error("Failed to save layout to database", error); + } } })); diff --git a/client/src/store/useDashboardStore.jsx b/client/src/store/useDashboardStore.jsx index 273d245..cfde6d7 100644 --- a/client/src/store/useDashboardStore.jsx +++ b/client/src/store/useDashboardStore.jsx @@ -2,6 +2,9 @@ import { create } from 'zustand'; import { v4 as uuid } from 'uuid'; import { WIDGET_REGISTRY } from '../constants/widgetRegistry.jsx'; +// 1. Import your database messenger +import useAuthStore from './authStore'; + export const useDashboardStore = create((set, get) => ({ // ========================================== // 1. STATE (The Data) @@ -25,18 +28,22 @@ export const useDashboardStore = create((set, get) => ({ }), 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) }); + // 1. Grab the exact current state of the grid + const currentLayout = get().widgets; + // 2. Lock the UI immediately so the user feels zero lag set({ isEditing: false, backupWidgets: [] }); + + // 3. Reach outside this store and trigger the Django API call! + try { + await useAuthStore.getState().saveDashboardLayout(currentLayout); + } catch (error) { + console.error("Failed to sync layout to MariaDB:", error); + // If the database save fails, you could technically revert the UI here if needed + } }, addWidget: (widgetId, sizeKey) => { - // Look up the exact rules for this widget from your constant const registryEntry = WIDGET_REGISTRY[widgetId]; if (!registryEntry || !registryEntry.allowedSizes[sizeKey]) { @@ -47,21 +54,14 @@ export const useDashboardStore = create((set, get) => ({ const dimensions = registryEntry.allowedSizes[sizeKey]; const widgets = get().widgets; - // Default to spawning on the far left on a new row let startX = 0; let startY = Infinity; if (widgets.length > 0) { - // 1. Find the lowest 'y' value (the start of the bottom row) const bottomRowY = Math.max(...widgets.map(w => w.y)); - - // 2. Grab all the widgets that are sitting on that exact row const bottomRowWidgets = widgets.filter(w => w.y === bottomRowY); - - // 3. Find the right-most edge of those widgets (x position + width) const rightmostEdge = Math.max(...bottomRowWidgets.map(w => w.x + w.w)); - // 4. If the right-most edge PLUS the new widget's width fits in 12 columns... if (rightmostEdge + dimensions.w <= 12) { startX = rightmostEdge; // ...tuck it right next to the last widget! startY = bottomRowY; // ...and keep it on the same row. @@ -84,16 +84,15 @@ export const useDashboardStore = create((set, get) => ({ 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; }) })); - } + }, + + // NEW: Action to hydrate the store on initial page load + setWidgets: (savedWidgets) => set({ widgets: savedWidgets || [] }) })); \ No newline at end of file