updated header so all styling is now in the designated themes file
This commit is contained in:
+59
-64
@@ -1,7 +1,7 @@
|
|||||||
import 'leaflet/dist/leaflet.css';
|
import 'leaflet/dist/leaflet.css';
|
||||||
import { useEffect } from 'react';
|
import {useEffect} from 'react';
|
||||||
import { Routes, Route, Navigate, Outlet } from 'react-router-dom';
|
import {Routes, Route, Navigate, Outlet} from 'react-router-dom';
|
||||||
import { useRecipe, Grid, GridItem, useDialog, Flex, Heading } from "@chakra-ui/react";
|
import {useRecipe, Grid, GridItem, useDialog, Flex, Heading} from "@chakra-ui/react";
|
||||||
|
|
||||||
// --- NEW AUTH IMPORTS ---
|
// --- NEW AUTH IMPORTS ---
|
||||||
import useAuthStore from './store/authStore';
|
import useAuthStore from './store/authStore';
|
||||||
@@ -19,80 +19,75 @@ import GlobalMap from "./pages/maps/GlobalMap.jsx";
|
|||||||
import Admin from "./pages/admin/Admin.jsx";
|
import Admin from "./pages/admin/Admin.jsx";
|
||||||
import MyDashboard from "./pages/home/tabs/MyDashboard.jsx";
|
import MyDashboard from "./pages/home/tabs/MyDashboard.jsx";
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
// 1. Pull the initialization function from Zustand
|
||||||
|
const checkAuth = useAuthStore((state) => state.checkAuth);
|
||||||
|
|
||||||
|
// 2. Ping Django to verify the HttpOnly cookie the moment the app boots
|
||||||
|
useEffect(() => {
|
||||||
|
checkAuth();
|
||||||
|
}, [checkAuth]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Routes>
|
||||||
|
{/* --- Public Routes --- */}
|
||||||
|
<Route path="/login" element={<Login/>}/>
|
||||||
|
|
||||||
|
{/* --- Secure Routes --- */}
|
||||||
|
<Route element={<ProtectedRoute/>}>
|
||||||
|
|
||||||
|
{/* --- Non-Public Routes (Now completely locked down) --- */}
|
||||||
|
<Route element={<AppFoundation/>}>
|
||||||
|
|
||||||
|
<Route path="/home" element={<Home/>}>
|
||||||
|
<Route index element={<MyDashboard/>}/>
|
||||||
|
<Route path="gas" element={<Placeholder name="Gas"/>}/>
|
||||||
|
<Route path="seismic" element={<Placeholder name="Seismic"/>}/>
|
||||||
|
<Route path="remote" element={<Placeholder name="Remote Sensing"/>}/>
|
||||||
|
<Route path="daily" element={<Placeholder name="Daily Activity"/>}/>
|
||||||
|
</Route>
|
||||||
|
|
||||||
|
<Route path="/global-map" element={<GlobalMap/>}/>
|
||||||
|
<Route path="/regional-map" element={<RegionalMap/>}/>
|
||||||
|
<Route path="/volcano" element={<Volcano/>}/>
|
||||||
|
<Route path="/admin" element={<Admin/>}/>
|
||||||
|
</Route>
|
||||||
|
|
||||||
|
</Route>
|
||||||
|
|
||||||
|
{/* --- Redirect Routes --- */}
|
||||||
|
<Route path="/" element={<Navigate to="/home" replace/>}/>
|
||||||
|
</Routes>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The fundamental layout of the webpage
|
||||||
function AppFoundation() {
|
function AppFoundation() {
|
||||||
const recipe = useRecipe({ key: "dashboard" });
|
const recipe = useRecipe({key: "dashboard"});
|
||||||
const styles = recipe();
|
const styles = recipe();
|
||||||
const dialog = useDialog();
|
const dialog = useDialog();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DialogPopup dialog={dialog} />
|
<DialogPopup dialog={dialog}/>
|
||||||
<Grid css={styles}
|
<Grid css={styles} templateRows="1fr 11fr" templateColumns="auto 1fr">
|
||||||
templateRows="1fr 11fr"
|
|
||||||
templateColumns="auto 1fr"
|
|
||||||
height="100vh"
|
|
||||||
width="100vw"
|
|
||||||
>
|
|
||||||
<GridItem rowSpan={2} colSpan={1}>
|
<GridItem rowSpan={2} colSpan={1}>
|
||||||
<Sidebar />
|
<Sidebar/>
|
||||||
</GridItem>
|
</GridItem>
|
||||||
<GridItem rowSpan={1} colSpan={1}>
|
<GridItem rowSpan={1} colSpan={1}>
|
||||||
<Header />
|
<Header/>
|
||||||
</GridItem>
|
</GridItem>
|
||||||
<GridItem colSpan={1} width="100%" overflow="auto">
|
<GridItem colSpan={1}>
|
||||||
<Outlet />
|
<Outlet/>
|
||||||
</GridItem>
|
</GridItem>
|
||||||
</Grid>
|
</Grid>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const Placeholder = ({ name }) => (
|
// A dummy page that acts as a placeholder for pages that aren't developed yet but are navigatable to
|
||||||
<Flex width="100%" height="100%" align="center" justify="center">
|
const Placeholder = ({name}) => (
|
||||||
<Heading color="gray.500" _dark={{ color: "gray.200" }} size="lg">{name} Dashboard - Coming Soon</Heading>
|
<Flex width="100%" height="100%" align="center" justify="center">
|
||||||
</Flex>
|
<Heading color="gray.500" _dark={{color: "gray.200"}} size="lg">{name} Dashboard - Coming Soon</Heading>
|
||||||
);
|
</Flex>
|
||||||
|
);
|
||||||
function App() {
|
|
||||||
// 1. Pull the initialization function from Zustand
|
|
||||||
const checkAuth = useAuthStore((state) => state.checkAuth);
|
|
||||||
|
|
||||||
// 2. Ping Django to verify the HttpOnly cookie the moment the app boots
|
|
||||||
useEffect(() => {
|
|
||||||
checkAuth();
|
|
||||||
}, [checkAuth]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Routes>
|
|
||||||
{/* --- Public Routes --- */}
|
|
||||||
<Route path="/login" element={<Login />} />
|
|
||||||
|
|
||||||
{/* --- SECURE ROUTES --- */}
|
|
||||||
<Route element={<ProtectedRoute />}>
|
|
||||||
|
|
||||||
{/* --- Non-Public Routes (Now completely locked down) --- */}
|
|
||||||
<Route element={<AppFoundation />}>
|
|
||||||
|
|
||||||
<Route path="/home" element={<Home />}>
|
|
||||||
<Route index element={<MyDashboard />} />
|
|
||||||
<Route path="gas" element={<Placeholder name="Gas" />} />
|
|
||||||
<Route path="seismic" element={<Placeholder name="Seismic" />} />
|
|
||||||
<Route path="remote" element={<Placeholder name="Remote Sensing" />} />
|
|
||||||
<Route path="daily" element={<Placeholder name="Daily Activity" />} />
|
|
||||||
</Route>
|
|
||||||
|
|
||||||
<Route path="/global-map" element={<GlobalMap />} />
|
|
||||||
<Route path="/regional-map" element={<RegionalMap />} />
|
|
||||||
<Route path="/volcano" element={<Volcano />} />
|
|
||||||
<Route path="/admin" element={<Admin />} />
|
|
||||||
</Route>
|
|
||||||
|
|
||||||
</Route>
|
|
||||||
|
|
||||||
{/* --- Redirect Routes --- */}
|
|
||||||
<Route path="/" element={<Navigate to="/home" replace />} />
|
|
||||||
</Routes>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default App;
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { useRecipe, Button, Stack, Text, Flex } from "@chakra-ui/react"
|
import {useSlotRecipe, Text, Flex} from "@chakra-ui/react"
|
||||||
import UserAvatarMenu from "./UserAvatarMenu.jsx";
|
import UserAvatarMenu from "./UserAvatarMenu.jsx";
|
||||||
import { NavLink } from "react-router-dom";
|
|
||||||
import useAuthStore from "@/store/authStore";
|
import useAuthStore from "@/store/authStore";
|
||||||
import {SearchBar} from "@/components/dashboard/SearchBar.jsx";
|
import {SearchBar} from "@/components/dashboard/SearchBar.jsx";
|
||||||
|
|
||||||
@@ -9,27 +8,30 @@ import {SearchBar} from "@/components/dashboard/SearchBar.jsx";
|
|||||||
|
|
||||||
// Accept a 'user' prop now, fallback to placeholder if empty
|
// Accept a 'user' prop now, fallback to placeholder if empty
|
||||||
export default function Header() {
|
export default function Header() {
|
||||||
const recipe = useRecipe({ key: "header" });
|
const recipe = useSlotRecipe({key: "header"});
|
||||||
const styles = recipe();
|
const styles = recipe();
|
||||||
|
|
||||||
const { user } = useAuthStore();
|
const {user} = useAuthStore();
|
||||||
console.log(user);
|
console.log(user);
|
||||||
const displayName = user ? (user.firstName || user.username) : "Loading...";
|
const displayName = user ? (user.firstName || user.username) : "Loading...";
|
||||||
const avatarname = user ? (user.fullName || user.username) : "Loading...";
|
const avatarname = user ? (user.fullName || user.username) : "Loading...";
|
||||||
|
|
||||||
const displayEmail = user?.email || "LDAP User";
|
const displayEmail = user?.email || "LDAP User";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex css={styles} >
|
<Flex css={styles.root}>
|
||||||
<SearchBar placeholder="Enter a volcano to search" size="sm" width="410px" />
|
<Flex css={styles.search_bar}>
|
||||||
{/* User Profile Section */}
|
<SearchBar placeholder="Enter a volcano to search"/>
|
||||||
<Flex align="center" gap={3} pr={6}>
|
</Flex>
|
||||||
<Text fontWeight="bold" textStyle="md" >{displayName}</Text>
|
|
||||||
<UserAvatarMenu
|
{/* User Profile Section */}
|
||||||
name={avatarname}
|
<Flex css={styles.user_info}>
|
||||||
// avatar={user.avatar}
|
<Text>{displayName}</Text>
|
||||||
/>
|
<UserAvatarMenu
|
||||||
</Flex>
|
name={avatarname}
|
||||||
</Flex>
|
// avatar={user.avatar}
|
||||||
);
|
/>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@@ -49,7 +49,6 @@ export default function Home() {
|
|||||||
|
|
||||||
{/* The Settings button is now inline with the tabs and only renders on the personal dashboard */}
|
{/* The Settings button is now inline with the tabs and only renders on the personal dashboard */}
|
||||||
{isPersonalDashboard && <SettingsButton/>}
|
{isPersonalDashboard && <SettingsButton/>}
|
||||||
|
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
{/* EDIT MODE BUTTONS & CONTROLS */}
|
{/* EDIT MODE BUTTONS & CONTROLS */}
|
||||||
@@ -57,9 +56,9 @@ export default function Home() {
|
|||||||
<HStack width="258px" justifyContent="flex-end">
|
<HStack width="258px" justifyContent="flex-end">
|
||||||
{/* Future global controls go here */}
|
{/* Future global controls go here */}
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
|
{/* TABS CONTENT */}
|
||||||
<Flex flex="1" overflow="auto">
|
<Flex flex="1" overflow="auto">
|
||||||
<Outlet/>
|
<Outlet/>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ 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() {
|
||||||
// ☁️ Database Connection
|
// ☁️ Database Connection
|
||||||
const {user} = useAuthStore();
|
const {user} = useAuthStore();
|
||||||
|
|
||||||
@@ -28,25 +28,23 @@ export default function MyDashboard() {
|
|||||||
// 🔒 Safety lock to prevent infinite re-renders
|
// 🔒 Safety lock to prevent infinite re-renders
|
||||||
const hasHydrated = useRef(false);
|
const hasHydrated = useRef(false);
|
||||||
|
|
||||||
// 1. Pull the new global slot recipe
|
// 1. Pull the global slot recipes
|
||||||
const recipe = useSlotRecipe({key: "glassPanel"});
|
const recipe = useSlotRecipe({key: "glassPanel"});
|
||||||
const styles = recipe({ layout: "widget" });
|
const styles = recipe({ layout: "widget" });
|
||||||
const popoverStyles = recipe({ layout: "popover" });
|
const popoverStyles = recipe({ layout: "popover" });
|
||||||
|
|
||||||
|
// 2. Populate widgets
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (user?.preferences?.dashboard_layout && !hasHydrated.current) {
|
if (user?.preferences?.dashboard_layout && !hasHydrated.current) {
|
||||||
const savedLayout = user.preferences.dashboard_layout;
|
const savedLayout = user.preferences.dashboard_layout;
|
||||||
|
|
||||||
// Safety check: Only inject if it's actually an array of widgets.
|
// Safety check: Only inject if it's actually an array of widgets.
|
||||||
// (Prevents a crash if MariaDB returns a default empty object {})
|
// (Prevents a crash if MariaDB returns a default empty object {})
|
||||||
if (Array.isArray(savedLayout)) {
|
if (Array.isArray(savedLayout)) setWidgets(savedLayout);
|
||||||
setWidgets(savedLayout);
|
|
||||||
}
|
|
||||||
hasHydrated.current = true;
|
hasHydrated.current = true;
|
||||||
}
|
}
|
||||||
}, [user, setWidgets]);
|
}, [user, setWidgets]);
|
||||||
|
|
||||||
|
|
||||||
// 🛠️ Format the data for RGL
|
// 🛠️ Format the data for RGL
|
||||||
const rglLayout = widgets.map(w => ({
|
const rglLayout = widgets.map(w => ({
|
||||||
i: w.id,
|
i: w.id,
|
||||||
@@ -54,10 +52,10 @@ export default function MyDashboard() {
|
|||||||
y: w.y,
|
y: w.y,
|
||||||
w: w.w,
|
w: w.w,
|
||||||
h: w.h,
|
h: w.h,
|
||||||
isResizable: false // 🚫 The magic lock! No resizing allowed globally.
|
isResizable: false // The magic lock! No resizing allowed globally.
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// 3. THE COMPONENT MAPPER
|
// 3. The component mapper
|
||||||
const renderWidget = (widget) => {
|
const renderWidget = (widget) => {
|
||||||
const registryEntry = WIDGET_REGISTRY[widget.type];
|
const registryEntry = WIDGET_REGISTRY[widget.type];
|
||||||
|
|
||||||
@@ -74,7 +72,7 @@ export default function MyDashboard() {
|
|||||||
{registryEntry.name}
|
{registryEntry.name}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{/* THE SETTINGS ELLIPSIS */}
|
{/* WIDGET SETTINGS BUTTON (ELLIPSES) */}
|
||||||
{SettingsUI ? (
|
{SettingsUI ? (
|
||||||
<Popover.Root positioning={{placement: "bottom-end"}}>
|
<Popover.Root positioning={{placement: "bottom-end"}}>
|
||||||
<Popover.Trigger asChild>
|
<Popover.Trigger asChild>
|
||||||
@@ -136,20 +134,20 @@ export default function MyDashboard() {
|
|||||||
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
|
||||||
>
|
>
|
||||||
{/* 3. 🎨 Paint the Widgets */}
|
{/* 🎨 Paint the Widgets */}
|
||||||
{widgets.map(widget => (
|
{widgets.map(widget => (
|
||||||
<Box
|
<Box
|
||||||
key={widget.id}
|
key={widget.id}
|
||||||
css={styles.root}
|
css={styles.root}
|
||||||
// 3. Grid-specific state overrides remain inline
|
// Grid-specific state overrides remain inline
|
||||||
boxShadow={isEditing ? "outline" : styles.root.boxShadow}
|
boxShadow={isEditing ? "outline" : styles.root.boxShadow}
|
||||||
borderStyle={isEditing ? "dashed" : "solid"}
|
borderStyle={isEditing ? "dashed" : "solid"}
|
||||||
borderWidth={isEditing ? "2px" : "1px"}
|
borderWidth={isEditing ? "2px" : "1px"}
|
||||||
borderColor={isEditing ? "orange.300" : styles.root.borderColor}
|
borderColor={isEditing ? "orange.300" : styles.root.borderColor}
|
||||||
cursor={isEditing ? "grab" : "default"}
|
cursor={isEditing ? "grab" : "default"}
|
||||||
position="relative" // 3. REQUIRED so the absolute button stays inside this box
|
position="relative" // REQUIRED so the absolute button stays inside this box
|
||||||
>
|
>
|
||||||
{/* 4. THE DELETE BUTTON */}
|
{/* THE DELETE BUTTON */}
|
||||||
{isEditing && (
|
{isEditing && (
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label="Remove widget"
|
aria-label="Remove widget"
|
||||||
@@ -172,7 +170,7 @@ export default function MyDashboard() {
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 4. SUSPENSE BOUNDARY wrapping the render function */}
|
{/* SUSPENSE BOUNDARY wrapping the render function */}
|
||||||
<Suspense fallback={
|
<Suspense fallback={
|
||||||
<Flex css={styles.body} align="center" justify="center">
|
<Flex css={styles.body} align="center" justify="center">
|
||||||
<Spinner size="md" color="blue.400" thickness="3px"/>
|
<Spinner size="md" color="blue.400" thickness="3px"/>
|
||||||
|
|||||||
+48
-13
@@ -1,17 +1,41 @@
|
|||||||
import {defineSlotRecipe, createSystem, defaultConfig, defineConfig, defineRecipe} from "@chakra-ui/react";
|
import {defineSlotRecipe, createSystem, defaultConfig, defineConfig, defineRecipe} from "@chakra-ui/react";
|
||||||
|
|
||||||
//—————RECIPES—————//
|
// CONSOLIDATED ✔
|
||||||
const headerRecipe = defineRecipe({
|
const headerRecipe = defineSlotRecipe({
|
||||||
|
slots: ["root", "search_bar", "user_info"],
|
||||||
base: {
|
base: {
|
||||||
bg: "{transparent}",
|
root: {
|
||||||
color: "text",
|
bg: "{transparent}",
|
||||||
width: "100%",
|
color: "text",
|
||||||
height: "100%",
|
width: "100%",
|
||||||
display: "flex",
|
height: "100%",
|
||||||
flexDirection: "row",
|
display: "flex",
|
||||||
justifyContent: "space-between",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
justifyContent: "space-between",
|
||||||
}
|
alignItems: "center",
|
||||||
|
pl: 3,
|
||||||
|
},
|
||||||
|
search_bar: {
|
||||||
|
width: "100%",
|
||||||
|
// input element in search bar component
|
||||||
|
"& input": {
|
||||||
|
width: "410px",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
user_info: {
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "flex-end",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 3,
|
||||||
|
pr: 6,
|
||||||
|
|
||||||
|
// The Text component
|
||||||
|
"& p": {
|
||||||
|
fontWeight: "bold",
|
||||||
|
fontSize:"16px" // equivalent to chakra 'md'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// CONSOLIDATED ✔
|
// CONSOLIDATED ✔
|
||||||
@@ -208,9 +232,18 @@ const sidebarRecipe = defineSlotRecipe({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// CONSOLIDATED ✔
|
||||||
const dashboardRecipe = defineRecipe({
|
const dashboardRecipe = defineRecipe({
|
||||||
base: {
|
base: {
|
||||||
bg: "{MainBackground}"
|
height: "100vh",
|
||||||
|
width: "100vw",
|
||||||
|
bg: "{MainBackground}",
|
||||||
|
|
||||||
|
// selects the third div in the grid layout (the canvas)
|
||||||
|
"& > div:nth-of-type(3)": {
|
||||||
|
width: "100%",
|
||||||
|
overflow: "auto",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -484,12 +517,12 @@ const config= defineConfig({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
recipes: {
|
recipes: {
|
||||||
header: headerRecipe,
|
|
||||||
dashboard: dashboardRecipe,
|
dashboard: dashboardRecipe,
|
||||||
canvas: canvasRecipe,
|
canvas: canvasRecipe,
|
||||||
glassTab: glassTabRecipe
|
glassTab: glassTabRecipe
|
||||||
},
|
},
|
||||||
slotRecipes: {
|
slotRecipes: {
|
||||||
|
header: headerRecipe,
|
||||||
sidebar: sidebarRecipe,
|
sidebar: sidebarRecipe,
|
||||||
// widget: widgetRecipe,
|
// widget: widgetRecipe,
|
||||||
glassPanel: glassPanelRecipe,
|
glassPanel: glassPanelRecipe,
|
||||||
@@ -497,4 +530,6 @@ const config= defineConfig({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Define semantic tokens and export recipes
|
||||||
export const system = createSystem(defaultConfig, config);
|
export const system = createSystem(defaultConfig, config);
|
||||||
Reference in New Issue
Block a user