Added saves state of dashboard and reloads it now
This commit is contained in:
@@ -1,7 +1,6 @@
|
|||||||
import { Dialog, Portal, CloseButton, Button } from "@chakra-ui/react";
|
import { Dialog, Portal, CloseButton, Button } from "@chakra-ui/react";
|
||||||
|
|
||||||
export default function DialogPopup({dialog}) {
|
export default function DialogPopup({dialog}) {
|
||||||
console.log("3. dialog popup")
|
|
||||||
return(
|
return(
|
||||||
<Dialog.RootProvider value={dialog}>
|
<Dialog.RootProvider value={dialog}>
|
||||||
<Portal>
|
<Portal>
|
||||||
|
|||||||
@@ -20,14 +20,21 @@ export function SettingsButton() {
|
|||||||
// 1. ☁Store connections
|
// 1. ☁Store connections
|
||||||
const isEditing = useDashboardStore(state => state.isEditing);
|
const isEditing = useDashboardStore(state => state.isEditing);
|
||||||
const enterEditMode = useDashboardStore(state => state.enterEditMode);
|
const enterEditMode = useDashboardStore(state => state.enterEditMode);
|
||||||
|
|
||||||
|
// This now handles both local UI lock AND database saving!
|
||||||
const saveEdit = useDashboardStore(state => state.saveEdit);
|
const saveEdit = useDashboardStore(state => state.saveEdit);
|
||||||
|
|
||||||
const cancelEdit = useDashboardStore(state => state.cancelEdit);
|
const cancelEdit = useDashboardStore(state => state.cancelEdit);
|
||||||
const addWidget = useDashboardStore(state => state.addWidget);
|
const addWidget = useDashboardStore(state => state.addWidget);
|
||||||
|
|
||||||
// 2. Local state to control the Widget Library popup independently of the dropdown
|
|
||||||
const [isLibraryOpen, setIsLibraryOpen] = useState(false);
|
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(() => {
|
const categorizedWidgets = useMemo(() => {
|
||||||
return Object.entries(WIDGET_REGISTRY).reduce((acc, [id, widget]) => {
|
return Object.entries(WIDGET_REGISTRY).reduce((acc, [id, widget]) => {
|
||||||
const cat = widget.category || 'General';
|
const cat = widget.category || 'General';
|
||||||
@@ -46,24 +53,24 @@ export function SettingsButton() {
|
|||||||
<Menu.Trigger asChild>
|
<Menu.Trigger asChild>
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label="Settings"
|
aria-label="Settings"
|
||||||
bg="vdap.darkGreen" // Or #1a2e24
|
bg="vdap.darkGreen"
|
||||||
color="white"
|
color="white"
|
||||||
size="sm"
|
size="sm"
|
||||||
borderRadius="md"
|
borderRadius="md"
|
||||||
_hover={{ bg: "vdap.darkGreenHover" }} // Or #111f18
|
_hover={{ bg: "vdap.darkGreenHover" }}
|
||||||
>
|
>
|
||||||
<Settings size={18} />
|
<Settings size={18} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Menu.Trigger>
|
</Menu.Trigger>
|
||||||
|
|
||||||
<Menu.Content bg="white" boxShadow="lg" borderRadius="md" p={1} zIndex="dropdown">
|
<Menu.Content bg="white" boxShadow="lg" borderRadius="md" p={1} zIndex="dropdown">
|
||||||
|
|
||||||
{/* Dynamic Menu Options: What shows up depends on isEditing */}
|
|
||||||
{isEditing ? (
|
{isEditing ? (
|
||||||
<>
|
<>
|
||||||
|
{/* WIRE DIRECTLY TO saveEdit */}
|
||||||
<Menu.Item onClick={saveEdit} color="green.600" fontWeight="bold" cursor="pointer" _hover={{ bg: "green.50" }}>
|
<Menu.Item onClick={saveEdit} color="green.600" fontWeight="bold" cursor="pointer" _hover={{ bg: "green.50" }}>
|
||||||
<Save size={16} style={{ marginRight: '8px' }} /> Save Layout
|
<Save size={16} style={{ marginRight: '8px' }} /> Save Layout
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
|
||||||
<Menu.Item onClick={cancelEdit} color="red.500" cursor="pointer" _hover={{ bg: "red.50" }}>
|
<Menu.Item onClick={cancelEdit} color="red.500" cursor="pointer" _hover={{ bg: "red.50" }}>
|
||||||
<X size={16} style={{ marginRight: '8px' }} /> Discard Changes
|
<X size={16} style={{ marginRight: '8px' }} /> Discard Changes
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
@@ -79,7 +86,6 @@ export function SettingsButton() {
|
|||||||
<Menu.Item onClick={() => setIsLibraryOpen(true)} cursor="pointer" _hover={{ bg: "gray.100" }}>
|
<Menu.Item onClick={() => setIsLibraryOpen(true)} cursor="pointer" _hover={{ bg: "gray.100" }}>
|
||||||
<LayoutGrid size={16} style={{ marginRight: '8px' }} /> Widget Library
|
<LayoutGrid size={16} style={{ marginRight: '8px' }} /> Widget Library
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
|
||||||
</Menu.Content>
|
</Menu.Content>
|
||||||
</Menu.Root>
|
</Menu.Root>
|
||||||
|
|
||||||
@@ -88,7 +94,6 @@ export function SettingsButton() {
|
|||||||
<Dialog.Backdrop bg="blackAlpha.600" />
|
<Dialog.Backdrop bg="blackAlpha.600" />
|
||||||
<Dialog.Positioner>
|
<Dialog.Positioner>
|
||||||
<Dialog.Content bg="gray.50" borderRadius="xl" overflow="hidden" boxShadow="xl" maxW="800px">
|
<Dialog.Content bg="gray.50" borderRadius="xl" overflow="hidden" boxShadow="xl" maxW="800px">
|
||||||
|
|
||||||
<Dialog.Header bg="vdap.darkGreen" color="white" py={4}>
|
<Dialog.Header bg="vdap.darkGreen" color="white" py={4}>
|
||||||
<Dialog.Title fontSize="xl" fontWeight="bold">Add Dashboard Widgets</Dialog.Title>
|
<Dialog.Title fontSize="xl" fontWeight="bold">Add Dashboard Widgets</Dialog.Title>
|
||||||
</Dialog.Header>
|
</Dialog.Header>
|
||||||
@@ -116,7 +121,7 @@ export function SettingsButton() {
|
|||||||
<Badge
|
<Badge
|
||||||
key={sizeKey}
|
key={sizeKey}
|
||||||
as="button"
|
as="button"
|
||||||
onClick={() => addWidget(widget.id, sizeKey)}
|
onClick={() => handleAddWidget(widget.id, sizeKey)}
|
||||||
colorScheme="gray"
|
colorScheme="gray"
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
px={3}
|
px={3}
|
||||||
|
|||||||
@@ -2,23 +2,50 @@ import { Box, Text, Flex, IconButton, Spinner } from "@chakra-ui/react";
|
|||||||
import { X } from "lucide-react";
|
import { X } from "lucide-react";
|
||||||
import { Responsive, WidthProvider } from "react-grid-layout";
|
import { Responsive, WidthProvider } from "react-grid-layout";
|
||||||
import { useDashboardStore } from "@/store/useDashboardStore.jsx";
|
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';
|
import { WIDGET_REGISTRY } from '@/constants/widgetRegistry.jsx';
|
||||||
|
|
||||||
// React-Grid-Layout requires this wrapper to automatically calculate screen width
|
// React-Grid-Layout requires this wrapper to automatically calculate screen width
|
||||||
const ResponsiveGridLayout = WidthProvider(Responsive);
|
const ResponsiveGridLayout = WidthProvider(Responsive);
|
||||||
|
|
||||||
export default function MyDashboard() {
|
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 widgets = useDashboardStore(state => state.widgets);
|
||||||
const isEditing = useDashboardStore(state => state.isEditing);
|
const isEditing = useDashboardStore(state => state.isEditing);
|
||||||
const updateLayoutPositions = useDashboardStore(state => state.updateLayoutPositions);
|
const updateLayoutPositions = useDashboardStore(state => state.updateLayoutPositions);
|
||||||
const removeWidget = useDashboardStore(state => state.removeWidget);
|
const removeWidget = useDashboardStore(state => state.removeWidget);
|
||||||
|
const setWidgets = useDashboardStore(state => state.setWidgets); // The injection action
|
||||||
|
|
||||||
// 2. 🛠️ Format the data for RGL
|
// 🔒 Safety lock to prevent infinite re-renders
|
||||||
// RGL expects a very specific array format for its layout prop
|
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 => ({
|
const rglLayout = widgets.map(w => ({
|
||||||
i: w.id,
|
i: w.id,
|
||||||
x: w.x,
|
x: w.x,
|
||||||
@@ -82,7 +109,7 @@ export default function MyDashboard() {
|
|||||||
breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }}
|
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
|
cols={{ lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 }} // Our 12-column master grid
|
||||||
rowHeight={30}
|
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
|
isResizable={false} // Double lock-down
|
||||||
onLayoutChange={(newLayout) => updateLayoutPositions(newLayout)} // Fires when user drops a widget
|
onLayoutChange={(newLayout) => updateLayoutPositions(newLayout)} // Fires when user drops a widget
|
||||||
compactType="vertical" // Auto-packs widgets to the top
|
compactType="vertical" // Auto-packs widgets to the top
|
||||||
|
|||||||
@@ -19,7 +19,9 @@ const useAuthStore = create((set) => ({
|
|||||||
firstName: res.data.first_name,
|
firstName: res.data.first_name,
|
||||||
lastName: res.data.last_name,
|
lastName: res.data.last_name,
|
||||||
email: res.data.email,
|
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) {
|
} catch (err) {
|
||||||
@@ -42,7 +44,7 @@ const useAuthStore = create((set) => ({
|
|||||||
checkAuth: async () => {
|
checkAuth: async () => {
|
||||||
try {
|
try {
|
||||||
// Hit the new lightweight endpoint
|
// 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!
|
// Rehydrate the store with the user's info!
|
||||||
set({
|
set({
|
||||||
@@ -53,13 +55,37 @@ const useAuthStore = create((set) => ({
|
|||||||
firstName: res.data.first_name,
|
firstName: res.data.first_name,
|
||||||
lastName: res.data.last_name,
|
lastName: res.data.last_name,
|
||||||
email: res.data.email,
|
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 {
|
} catch {
|
||||||
// If it fails, ensure the store is completely wiped
|
// If it fails, ensure the store is completely wiped
|
||||||
set({ isAuthenticated: false, isLoading: false, user: null });
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ import { create } from 'zustand';
|
|||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import { WIDGET_REGISTRY } from '../constants/widgetRegistry.jsx';
|
import { WIDGET_REGISTRY } from '../constants/widgetRegistry.jsx';
|
||||||
|
|
||||||
|
// 1. Import your database messenger
|
||||||
|
import useAuthStore from './authStore';
|
||||||
|
|
||||||
export const useDashboardStore = create((set, get) => ({
|
export const useDashboardStore = create((set, get) => ({
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// 1. STATE (The Data)
|
// 1. STATE (The Data)
|
||||||
@@ -25,18 +28,22 @@ export const useDashboardStore = create((set, get) => ({
|
|||||||
}),
|
}),
|
||||||
|
|
||||||
saveEdit: async () => {
|
saveEdit: async () => {
|
||||||
const payload = get().widgets;
|
// 1. Grab the exact current state of the grid
|
||||||
|
const currentLayout = 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) });
|
|
||||||
|
|
||||||
|
// 2. Lock the UI immediately so the user feels zero lag
|
||||||
set({ isEditing: false, backupWidgets: [] });
|
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) => {
|
addWidget: (widgetId, sizeKey) => {
|
||||||
// Look up the exact rules for this widget from your constant
|
|
||||||
const registryEntry = WIDGET_REGISTRY[widgetId];
|
const registryEntry = WIDGET_REGISTRY[widgetId];
|
||||||
|
|
||||||
if (!registryEntry || !registryEntry.allowedSizes[sizeKey]) {
|
if (!registryEntry || !registryEntry.allowedSizes[sizeKey]) {
|
||||||
@@ -47,21 +54,14 @@ export const useDashboardStore = create((set, get) => ({
|
|||||||
const dimensions = registryEntry.allowedSizes[sizeKey];
|
const dimensions = registryEntry.allowedSizes[sizeKey];
|
||||||
const widgets = get().widgets;
|
const widgets = get().widgets;
|
||||||
|
|
||||||
// Default to spawning on the far left on a new row
|
|
||||||
let startX = 0;
|
let startX = 0;
|
||||||
let startY = Infinity;
|
let startY = Infinity;
|
||||||
|
|
||||||
if (widgets.length > 0) {
|
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));
|
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);
|
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));
|
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) {
|
if (rightmostEdge + dimensions.w <= 12) {
|
||||||
startX = rightmostEdge; // ...tuck it right next to the last widget!
|
startX = rightmostEdge; // ...tuck it right next to the last widget!
|
||||||
startY = bottomRowY; // ...and keep it on the same row.
|
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)
|
widgets: state.widgets.filter(w => w.id !== id)
|
||||||
})),
|
})),
|
||||||
|
|
||||||
// Called automatically by React-Grid-Layout when the user lets go of the mouse
|
|
||||||
updateLayoutPositions: (rglLayout) => {
|
updateLayoutPositions: (rglLayout) => {
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
widgets: state.widgets.map(widget => {
|
widgets: state.widgets.map(widget => {
|
||||||
// Find where RGL says this widget currently is
|
|
||||||
const gridItem = rglLayout.find(l => l.i === widget.id);
|
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;
|
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 || [] })
|
||||||
}));
|
}));
|
||||||
Reference in New Issue
Block a user