some routes added

This commit is contained in:
2026-06-04 15:30:01 -07:00
parent dd70ec077c
commit fd3cdc69ad
24 changed files with 636 additions and 874 deletions
+2 -1
View File
@@ -6,6 +6,7 @@
"skipLibCheck": true, "skipLibCheck": true,
"paths": { "paths": {
"@/*": ["./src/*"] "@/*": ["./src/*"]
} },
"jsx": "react"
} }
} }
+116 -624
View File
File diff suppressed because it is too large Load Diff
+1 -3
View File
@@ -13,7 +13,6 @@
"@chakra-ui/react": "^3.22.0", "@chakra-ui/react": "^3.22.0",
"@emotion/react": "^11.14.0", "@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1", "@emotion/styled": "^11.14.1",
"@tailwindcss/vite": "^4.1.11",
"framer-motion": "^12.23.0", "framer-motion": "^12.23.0",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"lucide-react": "^0.539.0", "lucide-react": "^0.539.0",
@@ -22,10 +21,9 @@
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-grid-layout": "^1.5.2", "react-grid-layout": "^1.5.2",
"react-hook-form": "^7.62.0", "react-hook-form": "^7.62.0",
"react-icons": "^5.5.0",
"react-leaflet": "^5.0.0", "react-leaflet": "^5.0.0",
"react-resizable": "^3.0.5", "react-resizable": "^3.0.5",
"tailwindcss": "^4.1.11", "react-router-dom": "^7.16.0",
"uuid": "^11.1.0" "uuid": "^11.1.0"
}, },
"devDependencies": { "devDependencies": {
+51 -2
View File
@@ -1,10 +1,59 @@
import Dashboard from './components/Dashboard.jsx'
import 'leaflet/dist/leaflet.css'; import 'leaflet/dist/leaflet.css';
import { Routes, Route } from 'react-router-dom';
import { useRecipe, Grid, GridItem, useDialog } from "@chakra-ui/react";
import { createContext } from "react";
import Login from "@/Login.jsx";
import Sidebar from "./components/Sidebar/Sidebar.jsx";
import DialogPopup from "@/components/Canvas/CanvasComponents/DialogPopup.jsx";
import Header from "@/components/Header/Header.jsx";
import Home from "./components/Canvas/views/Home.jsx";
import RegionalMap from "./components/Canvas/views/RegionalMap.jsx";
import Volcano from "@/components/Canvas/views/Volcano.jsx";
function AppFoundation({children}) {
const recipe = useRecipe({ key: "dashboard" });
const styles = recipe();
const dialog = useDialog();
return (
<>
<DialogPopup dialog={dialog} />
<Grid css={styles}
templateRows="1fr 11fr"
templateColumns="auto 1fr"
height="100vh"
width="100vw"
>
<GridItem rowSpan={1} colSpan={2}>
<Header />
</GridItem>
<GridItem colSpan={1}>
<Sidebar />
</GridItem>
<GridItem colSpan={1} width="100%" overflow="auto">
{children}
</GridItem>
</Grid>
</>
)
}
// function ProtectedRoute({ user, children }) {
// if (user) return children;
// else return <Navigate to="/login" replace />;
// }
function App() { function App() {
const sidebarContext = createContext(["Home", "Global Map", "Regional Map", "Volcano", "Admin"]);
// const [user, setUser] = useState(null);
return ( return (
<Dashboard /> <Routes>
<Route path="/login" element={<Login />} />
<Route path="/home" element={<AppFoundation> <Home/> </AppFoundation>} />
<Route path="/regional-map" element={<AppFoundation> <RegionalMap/> </AppFoundation>} />
<Route path="/volcano" element={<AppFoundation> <Volcano/> </AppFoundation>} />
</Routes>
); );
} }
+91
View File
@@ -0,0 +1,91 @@
import { useContext } from 'react';
import { useForm } from 'react-hook-form';
import {
Button,
Field,
Input,
Stack,
useRecipe,
Center,
Text,
Flex,
Heading,
Highlight,
GridItem,
Grid
} from "@chakra-ui/react"
import { PasswordInput } from "@/components/ui/password-input.jsx";
export default function Login( signedIn, setSignedIn) {
const recipe = useRecipe({ key: "dashboard" }); // styling recipes option
const styles = recipe(); // use style
const {
register,
handleSubmit,
formState: { errors, isSubmitSuccessful },
} = useForm({
defaultValues: {
username: "",
password: "",
}
});
const onSubmit = (data) => {
console.log(data);
// setUser(data.username);
};
return (
<Flex css={styles} width="100%" height="100%">
<Grid
templateRows="1fr 11fr"
height="100vh"
width="100vw"
>
<GridItem rowSpan={1} >
<Flex width="100%" height="100%" align="center" >
<Heading size="4xl" fontWeight="bold" pl="1rem" cursor="default" >
<Highlight query="A" styles={{ color: 'red.600'}}>
VDAP
</Highlight>
</Heading>
</Flex>
</GridItem>
<GridItem rowSpan={11} >
<Flex width="100%" height="100%" justify="center" align="center" direction="column" >
<Center background="white" width="40%" height="50%" borderRadius="1rem">
<form onSubmit={handleSubmit(onSubmit)}>
<Stack gap="4" align="center" maxW="sm">
<Text textStyle="2xl" fontWeight="bold" cursor="default">Sign In</Text>
<Field.Root invalid={!!errors.username}>
<Input
placeholder="username"
{...register("username", {required: "Required"})}
/>
<Field.ErrorText>{errors.username?.message}</Field.ErrorText>
</Field.Root>
<Field.Root invalid={!!errors.password}>
<PasswordInput
placeholder="password"
{...register("password", {required: "Required"})}
/>
<Field.ErrorText>{errors.password?.message}</Field.ErrorText>
</Field.Root>
<Button
type="submit"
disabled={isSubmitSuccessful}
>
Submit
</Button>
</Stack>
</form>
</Center>
</Flex>
</GridItem>
</Grid>
</Flex>
);
}
@@ -2,7 +2,7 @@ import Widget from "./Widget";
import { Stack, Box, Field, Input, Button, Text, Flex } from "@chakra-ui/react"; import { Stack, Box, Field, Input, Button, Text, Flex } from "@chakra-ui/react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { useState } from "react"; import { useState } from "react";
import { LuAsterisk } from "react-icons/lu"; import { Asterisk } from 'lucide-react';
export default function AlertLevel({id, isEditable, isDelete, DeleteWidget}) { export default function AlertLevel({id, isEditable, isDelete, DeleteWidget}) {
const { register, handleSubmit, reset, formState: {errors} } = useForm(); const { register, handleSubmit, reset, formState: {errors} } = useForm();
@@ -44,7 +44,7 @@ export default function AlertLevel({id, isEditable, isDelete, DeleteWidget}) {
<Flex position="relative" dir="column" gap="0.5" width="100%"> <Flex position="relative" dir="column" gap="0.5" width="100%">
<Stack direction="row" width="100%"> <Stack direction="row" width="100%">
<Field.Label display="flex" whiteSpace="nowrap" fontWeight="bold"> <Field.Label display="flex" whiteSpace="nowrap" fontWeight="bold">
Volcano Name <LuAsterisk color="tomato" /> Volcano Name <Asterisk color="tomato" />
</Field.Label> </Field.Label>
<Input <Input
border={errors.volcanoname ? "2px solid" : undefined} border={errors.volcanoname ? "2px solid" : undefined}
@@ -1,4 +1,4 @@
import { RiExpandDiagonal2Line } from "react-icons/ri"; import { MoveDiagonal2 } from 'lucide-react';
import React from "react"; import React from "react";
const CustomResizeHandle = React.forwardRef((props, ref) => { const CustomResizeHandle = React.forwardRef((props, ref) => {
@@ -19,7 +19,7 @@ const CustomResizeHandle = React.forwardRef((props, ref) => {
zIndex: "1", zIndex: "1",
}} }}
> >
<RiExpandDiagonal2Line /> <MoveDiagonal2 />
</div> </div>
); );
}); });
@@ -1,36 +1,58 @@
import { Box, Menu, Portal } from '@chakra-ui/react' import { Box, Menu, Portal } from '@chakra-ui/react'
import WidgetMenu from './WidgetMenu'; import WidgetMenu from './WidgetMenu';
import AlertLevel from "@/components/Canvas/CanvasComponents/AlertLevelWidget.jsx";
export default function FirstWidgetButton({ appendWidget }) { export default function FirstWidgetButton({ appendWidget }) {
const triggerItem = (<> const triggerItem = (
<Menu.Trigger asChild> <>
<Box <Menu.Trigger asChild>
p={6} <Box
m="auto" p={6}
mt={6} m="auto"
align="center" mt={6}
textAlign="center" align="center"
justify="center" textAlign="center"
width="400px" justify="center"
borderRadius="md" width="400px"
boxShadow="md" borderRadius="md"
border="2px dashed" boxShadow="md"
borderColor="gray.300" border="2px dashed"
bg="gray.50" borderColor="gray.300"
cursor="pointer" bg="gray.50"
_hover = {{ cursor="pointer"
bg: "gray.100", _hover = {{
borderColor: "gray.400", bg: "gray.100",
boxShadow: "lg", borderColor: "gray.400",
}} boxShadow: "lg",
> }}
Add Your First Widget >
</Box> Add Your First Widget
</Menu.Trigger> </Box>
</>); </Menu.Trigger>
</>
);
const positioning = { placement: "bottom-middle" }; const positioning = { placement: "bottom-middle" };
const WidgetMenuProps = { appendWidget, triggerItem, positioning }; // const WidgetMenuProps = { appendWidget, triggerItem, positioning };
return ( return (
<WidgetMenu {...WidgetMenuProps} /> // <WidgetMenu {...WidgetMenuProps} />
<Menu.Root positioning={positioning}>
{triggerItem}
<Portal>
<Menu.Positioner>
<Menu.Content>
<Menu.Item
value="AlertLevel"
onClick={() => appendWidget(AlertLevel, 4, 7, 4, 7, 8, 14)} // width and height are passed through here for each widget
>
Alert Level
</Menu.Item>
<Menu.Item value="CO2">CO2</Menu.Item>
<Menu.Item value="Ivans">IVANS</Menu.Item>
<Menu.Item value="PlumeHeights">Plume Heights</Menu.Item>
<Menu.Item value="AlertLevelChanges">Alert Level Changes</Menu.Item>
</Menu.Content>
</Menu.Positioner>
</Portal>
</Menu.Root>
); );
} }
@@ -1,8 +1,7 @@
import { Box, Checkbox, Separator , CloseButton, Text, chakra, Field, IconButton, Icon, Input, Fieldset, Flex } from "@chakra-ui/react"; import { Box, Checkbox, Separator , CloseButton, Text, chakra, Field, IconButton, Icon, Input, Fieldset, Flex } from "@chakra-ui/react";
import { useState } from "react"; import { useState } from "react";
import { FiSettings } from "react-icons/fi"; import { ChevronDown } from 'lucide-react';
import { IoIosArrowDown } from "react-icons/io"; import { ChevronUp } from 'lucide-react';
import { IoIosArrowUp } from "react-icons/io";
export default function MapSettingsButton() { export default function MapSettingsButton() {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
@@ -16,7 +15,7 @@ export default function MapSettingsButton() {
</Box> </Box>
):( ):(
<IconButton boxShadow="0 0 0 2px rgba(119, 119, 119, 0.4)" size="sm" variant="subtle" onClick={() => setOpen(true)}> <IconButton boxShadow="0 0 0 2px rgba(119, 119, 119, 0.4)" size="sm" variant="subtle" onClick={() => setOpen(true)}>
<FiSettings /> <Settings />
</IconButton> </IconButton>
); );
} }
@@ -57,7 +56,7 @@ function Lightning() {
<Checkbox.Label >Lightning</Checkbox.Label> <Checkbox.Label >Lightning</Checkbox.Label>
</Checkbox.Root> </Checkbox.Root>
<Icon pt="1" size="md" variant="none" onClick={() => setOpen(!open)}> <Icon pt="1" size="md" variant="none" onClick={() => setOpen(!open)}>
{open ? (<IoIosArrowUp />):(<IoIosArrowDown />)} {open ? (<ChevronUp />):(<ChevronDown />)}
</Icon> </Icon>
{open && (<LightningOptions />)} {open && (<LightningOptions />)}
</Box> </Box>
@@ -1,9 +1,9 @@
import { Input, InputGroup } from "@chakra-ui/react"; import { Input, InputGroup } from "@chakra-ui/react";
import { FiSearch } from "react-icons/fi" import { Search } from 'lucide-react';
export function SearchBar({ placeholder="Enter a volcano to search", size="sm", width="210px" }) { export function SearchBar({ placeholder="Enter a volcano to search", size="sm", width="210px" }) {
return ( return (
<InputGroup startElement={<FiSearch />}> <InputGroup startElement={<Search />}>
<Input <Input
placeholder={placeholder} placeholder={placeholder}
size={size} size={size}
@@ -1,16 +1,16 @@
import { IconButton, Menu, Portal } from "@chakra-ui/react"; import { IconButton, Menu, Portal } from "@chakra-ui/react";
import { IoIosArrowBack } from "react-icons/io"; import { ArrowLeft } from 'lucide-react';
import { FiSettings } from "react-icons/fi"; import { Settings } from 'lucide-react';
import WidgetMenu from "./WidgetMenu"; import WidgetMenu from "./WidgetMenu";
export function SettingsButton({ onEditWidgets, appendWidget}) { export function SettingsButton({ onEditWidgets, appendWidget}) {
const triggerItem = (<> <Menu.TriggerItem> <IoIosArrowBack /> Add Widget</Menu.TriggerItem> </>); const triggerItem = (<> <Menu.TriggerItem> <ArrowLeft /> Add Widget</Menu.TriggerItem> </>);
const WidgetMenuProps = { appendWidget, triggerItem } const WidgetMenuProps = { appendWidget, triggerItem }
return ( return (
<Menu.Root> <Menu.Root>
<Menu.Trigger asChild > <Menu.Trigger asChild >
<IconButton variant="ghost" size="md" borderRadius="xl"> <IconButton variant="ghost" size="md" borderRadius="xl">
<FiSettings /> <Settings />
</IconButton> </IconButton>
</Menu.Trigger> </Menu.Trigger>
<Portal> <Portal>
@@ -1,11 +1,9 @@
import { useState, useRef } from "react"; import { useState, useRef } from "react";
import { chakra, useSlotRecipe } from "@chakra-ui/react"; import { chakra } from "@chakra-ui/react";
export default function Tooltip ({tooltipkey, content, sidebarIsOpen, children}) { export default function Tooltip ({tooltipkey, content, sidebarIsOpen, children}) {
const [isVisible, setIsVisible] = useState(false); const [isVisible, setIsVisible] = useState(false);
const recipe = useSlotRecipe({ key: "sidebar" });
const styles = recipe();
const timeoutId = useRef(null); const timeoutId = useRef(null);
return ( return (
@@ -30,22 +28,20 @@ export default function Tooltip ({tooltipkey, content, sidebarIsOpen, children})
> >
{children} {children}
{isVisible && !sidebarIsOpen && ( {isVisible && !sidebarIsOpen && (
<> <chakra.div
<chakra.div fontSize="xs"
fontSize="xs" position="absolute"
position="absolute" bg="gray.700"
bg="gray.700" color="white"
color="white" left="65px"
left="65px" px={2}
px={2} py={1}
py={1} borderRadius="md"
borderRadius="md" whiteSpace="nowrap"
whiteSpace="nowrap" zIndex="overlay" // zIndex = 1300
zIndex="overlay" // zIndex = 1300 >
> {content}
{content} </chakra.div>
</chakra.div>
</>
)} )}
</chakra.div> </chakra.div>
) )
@@ -1,37 +1,30 @@
import { useSlotRecipe, chakra, Flex, CloseButton, Text, IconButton } from "@chakra-ui/react" import { useSlotRecipe, chakra, Flex, CloseButton, Text, IconButton } from "@chakra-ui/react"
import { GrUndo } from "react-icons/gr"; import { Undo } from 'lucide-react';
export default function Widget(props) { export default function Widget(props) {
// deconstructing props // deconstructing props
const { id, title, isEditable, isDelete, DeleteWidget, isSubmit, setIsSubmit, children } = props; const { id, title, isEditable, isDelete, DeleteWidget, isSubmit, setIsSubmit, children } = props;
const recipe = useSlotRecipe({ key: "widget" }); const recipe = useSlotRecipe({ key: "widget" });
const styles = recipe(); const styles = recipe();
return ( return (
<> <>
<chakra.div css={styles.header} cursor={isEditable ? "move":"default"}> <chakra.div css={styles.header} cursor={isEditable ? "move":"default"}>
{isDelete ? ( <Flex flexDirection="row" justifyContent="space-between" alignItems="center">
<> <Text className="widget-handle" w="100%" display="flex" alignItems="center" height="32px" fontWeight="bold">{title}</Text>
<Flex flexDirection="row" justifyContent="space-between" alignItems="center"> {isDelete ? (
<Text className="widget-handle" w="100%" fontWeight="bold">{title}</Text> <CloseButton onClick={() => DeleteWidget(id)} size="xs" _hover={{bg: "tomato", color: "white"}}/>
<CloseButton onClick={() => DeleteWidget(id)} size="xs" _hover={{bg: "tomato", color: "white"}}/> ):(
</Flex> isSubmit ? (
</> <IconButton onClick={() => setIsSubmit(false)} size="xs" bg="base300" cursor="pointer"><Undo fill="text" /></IconButton>
):( ):null
<> )}
<Flex flexDirection="row" justifyContent="space-between" alignItems="center"> </Flex>
<Text className="widget-handle" w="100%" display="flex" alignItems="center" height="32px" fontWeight="bold">{title}</Text>
{isSubmit ? (
<IconButton onClick={() => setIsSubmit(false)} size="xs" bg="base300" cursor="pointer"><GrUndo fill="text" /></IconButton>
):(null)}
</Flex>
</>
)}
</chakra.div> </chakra.div>
<chakra.div height="100%" mb="2"> <Flex height="100%" mb="2">
{children} {children}
</chakra.div> </Flex>
</> </>
); );
} }
+58 -41
View File
@@ -1,4 +1,5 @@
import { HStack, Flex, Tabs } from "@chakra-ui/react"; import { HStack, Flex, Tabs } from "@chakra-ui/react";
import { useState } from "react";
import MyDashboard from "./HomeTabs/MyDashboard"; import MyDashboard from "./HomeTabs/MyDashboard";
import { SearchBar } from "../CanvasComponents/SearchBar"; import { SearchBar } from "../CanvasComponents/SearchBar";
import { SettingsButton } from "../CanvasComponents/SettingsButton"; import { SettingsButton } from "../CanvasComponents/SettingsButton";
@@ -13,14 +14,17 @@ import { v4 as uuid } from 'uuid';
import { CanvasHeaderTabs } from '../CanvasComponents/CanvasHeaderTabs'; import { CanvasHeaderTabs } from '../CanvasComponents/CanvasHeaderTabs';
import { DASHBOARD_TABS } from "@/constants/viewKeys"; import { DASHBOARD_TABS } from "@/constants/viewKeys";
export default function Home(props) { export default function Home() {
// deconstructing props const [isEditable, setIsEditable] = useState(false); // editable=true means static=false (vice versa)
const { view, layout, setLayout, setOriginalLayout, onLayoutChange, onChangeView, onCancel, setOriginalWidgets, const [isDelete, setIsDelete] = useState(false);
isDelete, setIsDelete, beginEditIfNeeded, isEditable, setIsEditable, widgetArray, setWidgetArray } = props; const [originalLayout, setOriginalLayout] = useState([]);
const [originalWidgets, setOriginalWidgets] = useState([]);
const [layout, setLayout] = useState([]);
const [widgetArray, setWidgetArray] = useState([]);
function appendWidget(name, w, h, min_w, minh, max_w, max_h) {
saveLayoutState(); // Save state before anything else
function appendWidget(name, w, h, minw, minh, maxw, maxh) {
beginEditIfNeeded(); // Save state before anything else
const newID = uuid(); // could be replaced with something else like guid const newID = uuid(); // could be replaced with something else like guid
const COLS = 16; const COLS = 16;
const W = w ?? 3; const W = w ?? 3;
@@ -30,7 +34,7 @@ export default function Home(props) {
...widgetArray, ...widgetArray,
{i: newID, widget: name } {i: newID, widget: name }
]); ]);
// onAddWidget(newID, w, h, minw, minh, maxw, maxh); // updating array containing info on layout of widgets // onAddWidget(newID, w, h, min_w, minh, max_w, max_h); // updating array containing info on layout of widgets
setLayout(prev => [ setLayout(prev => [
...prev, ...prev,
@@ -40,10 +44,10 @@ export default function Home(props) {
y: 0, y: 0,
w: w, w: w,
h: h, h: h,
minW: minw, minW: min_w,
minH: minh, minH: minh,
maxW: maxw, maxW: max_w,
maxH: maxh, maxH: max_h,
static: false, static: false,
resizeHandles: ['se'] resizeHandles: ['se']
}, },
@@ -67,7 +71,7 @@ export default function Home(props) {
const onEditWidgets = () => { const onEditWidgets = () => {
console.log("Start deleting widgets..."); console.log("Start deleting widgets...");
beginEditIfNeeded(); // Save state before anything else saveLayoutState(); // Save state before anything else
setLayout(prevLayout => setLayout(prevLayout =>
prevLayout.map(item => ({ prevLayout.map(item => ({
...item, ...item,
@@ -96,39 +100,52 @@ export default function Home(props) {
else return; else return;
} }
const saveLayoutState = () => {
if ( !isEditable ) {
setOriginalLayout(layout);
setOriginalWidgets(widgetArray);
}
};
const onCancel = () => {
console.log("canceling changes...");
console.log(originalWidgets);
if (originalLayout.length) {
setLayout(originalLayout.map(item => ({
...item,
static: true
})));
}
setWidgetArray(originalWidgets);
setIsEditable(false);
setIsDelete(false);
setOriginalLayout([]);
setOriginalWidgets([]);
}
const onLayoutChange = (newLayout) => setLayout(newLayout);
// Prop Forwarding // Prop Forwarding
const MyDashboardProps = { layout, onLayoutChange, isEditable, beginEditIfNeeded, isDelete, DeleteWidget, widgetArray, appendWidget }; const MyDashboardProps = { layout, onLayoutChange, isEditable, saveLayoutState, isDelete, DeleteWidget, widgetArray, appendWidget };
const settingsButtonProps = { onEditWidgets, appendWidget }; const settingsButtonProps = { onEditWidgets, appendWidget };
return ( return (
/* HEADER */ /* SUB HEADER */
<Tabs.Root <Tabs.Root width="100%" height="100%" lazyMount variant="line">
width="100%" <Flex as="header" position="sticky" top="0" zIndex="dropdown" bg="base200" p={5} align="center" justify="space-between" width="100%">
height="100%" {/* TABS */}
lazyMount {/* <CanvasHeaderTabs tabs={DASHBOARD_TABS} />*/} {/* TODO: rename to SubHeaderTabs */}
value={ view } <Tabs.List>
variant="line" {DASHBOARD_TABS.map(({ value, label }) =>
// Prevent navigation away from 'My Dashboard' while in Edit mode <Tabs.Trigger key={value} value={value} fontSize="lg">
onValueChange={(e) => { { label }
const nextView = e.value; </Tabs.Trigger>
onChangeView(nextView); )}
}} </Tabs.List>
>
<Flex {/* EDIT MODE BUTTONS */}
as="header"
position="sticky"
top="0"
zIndex="dropdown" // zIndex: 1100
bg="base200"
pl={5}
pr={2}
pt={3}
pb={2}
align="center"
justify="space-between"
width="100%"
>
<CanvasHeaderTabs tabs={DASHBOARD_TABS} />
<HStack width="258px" justifyContent="flex-end"> <HStack width="258px" justifyContent="flex-end">
{isEditable ? ( {isEditable ? (
<> <>
@@ -4,7 +4,6 @@ import CustomResizeHandle from '../../CanvasComponents/CustomResizeHandle';
import FirstWidgetButton from '../../CanvasComponents/FirstWidgetButton'; import FirstWidgetButton from '../../CanvasComponents/FirstWidgetButton';
export default function MyDashboard({layout, onLayoutChange, isEditable, isDelete, DeleteWidget, widgetArray, appendWidget}) { export default function MyDashboard({layout, onLayoutChange, isEditable, isDelete, DeleteWidget, widgetArray, appendWidget}) {
console.log("rendering MyDashboard");
const recipe = useSlotRecipe({ key: "widget" }); const recipe = useSlotRecipe({ key: "widget" });
const styles = recipe(); // using root recipe for widget below const styles = recipe(); // using root recipe for widget below
const ResponsiveGridLayout = WidthProvider(GridLayout); // canvas width now adjusts to window size const ResponsiveGridLayout = WidthProvider(GridLayout); // canvas width now adjusts to window size
@@ -12,9 +11,6 @@ export default function MyDashboard({layout, onLayoutChange, isEditable, isDelet
const handleDragStop = () => { document.body.style.userSelect = ""; }; // allow text highlighting all other times const handleDragStop = () => { document.body.style.userSelect = ""; }; // allow text highlighting all other times
let merged = []; let merged = [];
console.log("widgetArray: ", widgetArray);
console.log("layout: ", layout);
if (widgetArray.length && layout.length) { if (widgetArray.length && layout.length) {
const widgetMap = new Map(widgetArray.map(item => [item.i, item])); const widgetMap = new Map(widgetArray.map(item => [item.i, item]));
merged = layout.map(item => { merged = layout.map(item => {
@@ -23,7 +19,7 @@ export default function MyDashboard({layout, onLayoutChange, isEditable, isDelet
...widgetMap.get(item.i) ...widgetMap.get(item.i)
}; };
}); });
}; }
// Show large 'Add Widget' button when dashboard is empty // Show large 'Add Widget' button when dashboard is empty
if ( !layout.length ) { if ( !layout.length ) {
@@ -36,39 +32,43 @@ export default function MyDashboard({layout, onLayoutChange, isEditable, isDelet
bg="gray.150" bg="gray.150"
borderRadius="md" borderRadius="md"
> >
<FirstWidgetButton appendWidget={ appendWidget } /> <FirstWidgetButton appendWidget={ appendWidget } />
</Flex> </Flex>
); );
} }
// Show responsive grid of widgets // Show responsive grid of widgets
return ( return (
<chakra.div width="100%"> <ResponsiveGridLayout
<ResponsiveGridLayout useCSSTransforms={false} // remove the sliding animation of the widgets
useCSSTransforms={false} // remove the sliding animation of the widgets className="layout"
className="layout" layout={layout}
layout={layout} cols={16} // Total columns in grid
cols={16} // Total columns in grid rowHeight={30} // Height of a single row in px
rowHeight={30} // Height of a single row in px isBounded={true} // Total width of the grid
isBounded={true} // Total width of the grid margin={[15, 5]} // [horizontal, vertical] gap
margin={[15, 5]} // [horizontal, vertical] gap onLayoutChange={onLayoutChange}
onLayoutChange={onLayoutChange} draggableHandle=".widget-handle" // Make only the header draggable
draggableHandle=".widget-handle" // Make only the header draggable onDragStart={handleDragStart}
onDragStart={handleDragStart} onDragStop={handleDragStop}
onDragStop={handleDragStop} position="relative"
position="relative" resizeHandle={<CustomResizeHandle isVisable={isEditable} />}
resizeHandle={<CustomResizeHandle isVisable={isEditable} />} >
> { merged.map((item) => {
{ merged.map((item) => { const Widget = item.widget;
const Widget = item.widget; return (
return ( <Flex key={item.i} css={styles.root} boxShadow="md" height="100%" >
<Flex key={item.i} css={styles.root} boxShadow="md" height="100%" > <Widget
<Widget id={item.i} isEditable={isEditable} isDelete={isDelete} DeleteWidget={DeleteWidget} appendWidget={appendWidget}/> id={item.i}
</Flex> isEditable={isEditable}
); isDelete={isDelete}
}) DeleteWidget={DeleteWidget}
} appendWidget={appendWidget}
</ResponsiveGridLayout> />
</chakra.div> </Flex>
);
})
}
</ResponsiveGridLayout>
); );
} }
+10 -6
View File
@@ -1,5 +1,7 @@
import { chakra, useRecipe, Box, Button, Stack, Text, Flex } from "@chakra-ui/react" import { useRecipe, Button, Stack, Text, Flex } from "@chakra-ui/react"
import UserAvatarMenu from "./UserAvatarMenu.jsx"; import UserAvatarMenu from "./UserAvatarMenu.jsx";
import { useNavigate } from "react-router-dom";
import { NavLink } from "react-router-dom";
//TODO: Dynamically populate per each user //TODO: Dynamically populate per each user
const user = { const user = {
@@ -8,13 +10,16 @@ const user = {
// avatar: "src\\assets\\user_profile.svg" // avatar: "src\\assets\\user_profile.svg"
} }
export default function Header({ onChangeView }) { export default function Header() {
const navigate = useNavigate();
const recipe = useRecipe({ key: "header" }); const recipe = useRecipe({ key: "header" });
const styles = recipe(); const styles = recipe();
return ( return (
<chakra.div css={styles}> <Flex css={styles}>
<Button onClick={() => onChangeView("mydashboard")} color="color" variant="plain" textStyle="5xl" fontWeight="bold">VDAP</Button> <Button as={NavLink} to={"/home"} color="color" variant="plain" textStyle="5xl" fontWeight="bold">
VDAP
</Button>
<Flex align="center" gap={3} pr={6}> <Flex align="center" gap={3} pr={6}>
<Stack gap="0" textAlign="center" cursor="default"> <Stack gap="0" textAlign="center" cursor="default">
<Text color="color" fontWeight="bold" textStyle="lg">{user.name}</Text> <Text color="color" fontWeight="bold" textStyle="lg">{user.name}</Text>
@@ -23,10 +28,9 @@ export default function Header({ onChangeView }) {
<UserAvatarMenu <UserAvatarMenu
name={user.name} name={user.name}
avatar={user.avatar} avatar={user.avatar}
onChangeView={ onChangeView }
/> />
</Flex> </Flex>
</chakra.div> </Flex>
); );
} }
@@ -1,7 +1,8 @@
import { Avatar, Menu, Portal, Box } from "@chakra-ui/react" import { Avatar, Menu, Portal, Box } from "@chakra-ui/react";
import { NavLink } from "react-router-dom";
export default function UserAvatarMenu({ onChangeView, name, avatar }) { export default function UserAvatarMenu({ name, avatar }) {
return ( return (
<Menu.Root> <Menu.Root>
<Menu.Trigger focusRing="none"> <Menu.Trigger focusRing="none">
@@ -21,7 +22,7 @@ export default function UserAvatarMenu({ onChangeView, name, avatar }) {
{/* zIndex: overlay = 1300 */} {/* zIndex: overlay = 1300 */}
<Menu.Positioner zIndex="overlay"> <Menu.Positioner zIndex="overlay">
<Menu.Content> <Menu.Content>
<Menu.Item value="settings" onClick={() => onChangeView("personal")}> <Menu.Item value="settings" as={NavLink} to={"/settings"}>
Settings Settings
</Menu.Item> </Menu.Item>
<Menu.Item value="logout" onClick={() => console.log("Log out triggered")}> <Menu.Item value="logout" onClick={() => console.log("Log out triggered")}>
+36 -65
View File
@@ -1,89 +1,61 @@
import { useSlotRecipe, Box, Button, IconButton } from "@chakra-ui/react"; import { useSlotRecipe, Box, Button, IconButton } from "@chakra-ui/react";
import { ColorModeButton, useColorMode } from "@/components/ui/color-mode"; import { ColorModeButton, useColorMode } from "@/components/ui/color-mode";
import { useState } from "react";
import { NavLink } from "react-router-dom";
import Tooltip from "../Canvas/CanvasComponents/Tooltip.jsx"; import Tooltip from "../Canvas/CanvasComponents/Tooltip.jsx";
import { FaVolcano } from "react-icons/fa6"; import { Mountain, House, Earth, MapPin, ShieldUser, Menu, CircleUserRound, KeyRound } from 'lucide-react';
import { FaHome } from "react-icons/fa";
import { FaGlobeAmericas } from "react-icons/fa";
import { FaMapMarkerAlt } from "react-icons/fa";
import { HiUsers } from "react-icons/hi2";
import { VscThreeBars } from "react-icons/vsc";
import { MdOutlineSecurity } from "react-icons/md";
import { IoPersonCircleSharp } from "react-icons/io5";
import { useState, useEffect, useMemo } from "react";
import { DASHBOARD_VIEWS } from "@/constants/viewKeys";
import { KeyRound } from 'lucide-react';
export default function Sidebar({ view, onChangeView, darkMode, setDarkMode }) { export default function Sidebar() {
const { toggleColorMode } = useColorMode();
const [isOpen, setIsOpen] = useState(true); const [isOpen, setIsOpen] = useState(true);
const [activeButton, setActiveButton] = useState(0) const [activeButton, setActiveButton] = useState(0)
const recipe = useSlotRecipe({ key: "sidebar" }); const recipe = useSlotRecipe({ key: "sidebar" });
const styles = recipe(); const styles = recipe();
const { toggleColorMode } = useColorMode();
const buttons = useMemo(() => { // const buttons = useMemo(() => {
if (["settings", "personal", "security"].includes(view)) { // if (["settings", "personal", "security"].includes(view)) {
return [ // return [
{icon: IoPersonCircleSharp, name: "Personal Info", view: "personal"}, // {icon: CircleUserRound, name: "Personal Info", url: "/personal"},
{icon: KeyRound, name: "Security", view: "security"}, // {icon: KeyRound, name: "Security", url: "/security"},
]; // ];
} // }
return [ // return [
{icon: FaHome, name: "Home", view: "mydashboard"}, // {icon: House, name: "Home", url: "/mydashboard"},
{icon: FaGlobeAmericas, name: "Global Map", view: "global"}, // {icon: Earth, name: "Global Map", url: "/global"},
{icon: FaMapMarkerAlt, name: "Regional Map", view: "regional"}, // {icon: MapPin, name: "Regional Map", url: "/regional"},
{icon: FaVolcano, name: "Volcano", view: "volcanohome"}, // {icon: Mountain, name: "Volcano", url: "/volcanohome"},
{icon: HiUsers, name: "Admin", view: "medataentry"} // {icon: ShieldUser, name: "Admin", url: "/medataentry"}
]; // ];
}, [view]); // }, [view]);
const buttons = [
{icon: House, name: "Home", url: "/home"},
{icon: Earth, name: "Global Map", url: "/GlobalMap"},
{icon: MapPin, name: "Regional Map", url: "/regional-map"},
{icon: Mountain, name: "Volcano", url: "/volcano"},
{icon: ShieldUser, name: "Admin", url: "/medataentry"}
];
const toggleSidebar = () => { const toggleSidebar = () => {
if (isOpen) setIsOpen(false); if (isOpen) setIsOpen(false);
else setIsOpen(true); else setIsOpen(true);
}; };
// Track currrent view from both Sidebar and Canvas when 'view' changes
useEffect(() => {
console.log("Current view = ", view);
const newIndex = buttons.findIndex((btn) => {
if (btn.view === "mydashboard") {
return DASHBOARD_VIEWS.includes(view);
}
return btn.view === view;
});
if (newIndex !== -1 ) {
setActiveButton(newIndex);
}
}, [view]);
return ( return (
<Box <Box css={styles.root} width={isOpen ? "210px" : "71px"} transition="width 0.4s ease" position="relative">
css={styles.root} <IconButton aria-label="Toggle Sidebar" variant="plain" size="md" onClick={toggleSidebar} width="54px">
width={isOpen ? "210px" : "71px"} <Menu />
transition="width 0.4s ease"
position="relative"
>
<IconButton
aria-label="Toggle Sidebar"
variant="plain"
size="md"
onClick={toggleSidebar}
width="54px"
>
<VscThreeBars />
</IconButton> </IconButton>
{buttons.map((button, idx) => { {buttons.map((button, idx) => {
const Icon = button.icon; const Icon = button.icon;
return ( return (
<Tooltip key={button.name} tooltipkeykey={idx} content={button.name} sidebarIsOpen={isOpen}> <Tooltip key={button.name} tooltipkey={idx} content={button.name} sidebarIsOpen={isOpen}>
<Button <Button
as={NavLink}
to={button.url}
key={button.name} key={button.name}
css={styles.buttons} css={styles.buttons}
onClick={() => {
onChangeView(button.view); // Trigger state change in Dashboard
}}
bg={activeButton === idx ? "primary":undefined}
color={activeButton === idx ? "#FFFFFF":undefined}
justifyContent="flex-start" justifyContent="flex-start"
> >
<Icon /> <Icon />
@@ -105,7 +77,6 @@ export default function Sidebar({ view, onChangeView, darkMode, setDarkMode }) {
})} })}
<ColorModeButton <ColorModeButton
onClick={() => { onClick={() => {
setDarkMode(!darkMode);
toggleColorMode(); toggleColorMode();
}} }}
position="absolute" position="absolute"
+3 -2
View File
@@ -4,7 +4,8 @@ import { ClientOnly, IconButton, Skeleton, Span } from '@chakra-ui/react'
import { ThemeProvider, useTheme } from 'next-themes' import { ThemeProvider, useTheme } from 'next-themes'
import * as React from 'react' import * as React from 'react'
import { LuMoon, LuSun } from 'react-icons/lu' import { Moon, Sun } from 'lucide-react';
export function ColorModeProvider(props) { export function ColorModeProvider(props) {
return ( return (
@@ -32,7 +33,7 @@ export function useColorModeValue(light, dark) {
export function ColorModeIcon() { export function ColorModeIcon() {
const { colorMode } = useColorMode() const { colorMode } = useColorMode()
return colorMode === 'dark' ? <LuMoon /> : <LuSun /> return colorMode === 'dark' ? <Moon /> : <Sun />
} }
export const ColorModeButton = React.forwardRef( export const ColorModeButton = React.forwardRef(
+122
View File
@@ -0,0 +1,122 @@
'use client'
import {
Box,
HStack,
IconButton,
Input,
InputGroup,
Stack,
mergeRefs,
useControllableState,
} from '@chakra-ui/react'
import * as React from 'react'
import { Eye, EyeClosed } from 'lucide-react';
// import { LuEye, LuEyeOff } from 'react-icons/lu'
export const PasswordInput = React.forwardRef(
function PasswordInput(props, ref) {
const {
rootProps,
defaultVisible,
visible: visibleProp,
onVisibleChange,
visibilityIcon = { on: <Eye />, off: <EyeClosed /> },
...rest
} = props
const [visible, setVisible] = useControllableState({
value: visibleProp,
defaultValue: defaultVisible || false,
onChange: onVisibleChange,
})
const inputRef = React.useRef(null)
return (
<InputGroup
endElement={
<VisibilityTrigger
disabled={rest.disabled}
onPointerDown={(e) => {
if (rest.disabled) return
if (e.button !== 0) return
e.preventDefault()
setVisible(!visible)
}}
>
{visible ? visibilityIcon.off : visibilityIcon.on}
</VisibilityTrigger>
}
{...rootProps}
>
<Input
{...rest}
ref={mergeRefs(ref, inputRef)}
type={visible ? 'text' : 'password'}
/>
</InputGroup>
)
},
)
const VisibilityTrigger = React.forwardRef(
function VisibilityTrigger(props, ref) {
return (
<IconButton
tabIndex={-1}
ref={ref}
me='-2'
aspectRatio='square'
size='sm'
variant='ghost'
height='calc(100% - {spacing.2})'
aria-label='Toggle password visibility'
{...props}
/>
)
},
)
export const PasswordStrengthMeter = React.forwardRef(
function PasswordStrengthMeter(props, ref) {
const { max = 4, value, ...rest } = props
const percent = (value / max) * 100
const { label, colorPalette } = getColorPalette(percent)
return (
<Stack align='flex-end' gap='1' ref={ref} {...rest}>
<HStack width='full' {...rest}>
{Array.from({ length: max }).map((_, index) => (
<Box
key={index}
height='1'
flex='1'
rounded='sm'
data-selected={index < value ? '' : undefined}
layerStyle='fill.subtle'
colorPalette='gray'
_selected={{
colorPalette,
layerStyle: 'fill.solid',
}}
/>
))}
</HStack>
{label && <HStack textStyle='xs'>{label}</HStack>}
</Stack>
)
},
)
function getColorPalette(percent) {
switch (true) {
case percent < 33:
return { label: 'Low', colorPalette: 'red' }
case percent < 66:
return { label: 'Medium', colorPalette: 'orange' }
default:
return { label: 'High', colorPalette: 'green' }
}
}
+3 -1
View File
@@ -2,10 +2,12 @@
import { ChakraProvider, defaultSystem } from '@chakra-ui/react' import { ChakraProvider, defaultSystem } from '@chakra-ui/react'
import { ColorModeProvider } from './color-mode' import { ColorModeProvider } from './color-mode'
import { system } from '@/theme.js';
export function Provider(props) { export function Provider(props) {
return ( return (
<ChakraProvider value={defaultSystem}> <ChakraProvider value={system}>
<ColorModeProvider {...props} /> <ColorModeProvider {...props} />
</ChakraProvider> </ChakraProvider>
) )
+8 -8
View File
@@ -1,19 +1,19 @@
import { ChakraProvider } from '@chakra-ui/react'; import { Provider } from '@/components/ui/provider'
import { ColorModeProvider } from '@/components/ui/color-mode';
import { StrictMode } from 'react'; import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import './css/index.css'; import './css/index.css';
import 'react-grid-layout/css/styles.css'; import 'react-grid-layout/css/styles.css';
import 'react-resizable/css/styles.css'; import 'react-resizable/css/styles.css';
import App from './App.jsx'; import App from './App.jsx';
import { system } from './theme.js';
createRoot(document.getElementById('root')).render( createRoot(document.getElementById('root')).render(
<StrictMode> <StrictMode>
<ChakraProvider value={system}> <Provider>
<ColorModeProvider> <BrowserRouter>
<App /> <App />
</ColorModeProvider> </BrowserRouter>
</ChakraProvider> </Provider>
</StrictMode>, </StrictMode>
) )
+1
View File
@@ -49,6 +49,7 @@ const sidebarRecipe = defineSlotRecipe({
buttons: { buttons: {
bg: "transparent", bg: "transparent",
_hover: {bg: "{primaryLt}", color: "{textInverted}"}, _hover: {bg: "{primaryLt}", color: "{textInverted}"},
"&.active": {bg: "{primary}", color: "{textInverted}"},
fontSize: "xl", fontSize: "xl",
color: {base: "{text}"}, color: {base: "{text}"},
variant: "ghost", variant: "ghost",
+4 -2
View File
@@ -1,9 +1,11 @@
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc' import react from '@vitejs/plugin-react-swc'
import jsconfigPaths from "vite-jsconfig-paths" import jsconfigPaths from "vite-jsconfig-paths"
import tailwindcss from '@tailwindcss/vite'
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react(), jsconfigPaths()], plugins: [
react(),
jsconfigPaths()
],
}) })