updated header so all styling is now in the designated themes file

This commit is contained in:
2026-09-03 14:07:39 -07:00
parent cde2c4660b
commit 4aa8eb7ff0
5 changed files with 144 additions and 115 deletions
+58 -63
View File
@@ -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;
+24 -22
View File
@@ -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>
);
} }
+1 -2
View File
@@ -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>
+12 -14
View File
@@ -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
View File
@@ -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);