Rename and Restructure

+ took out extra 'layout' directory.
+ 'Canvas' postfix removed from all files in the views directory.
+ "WidgetCanvas" renamed to "MyDashboard"
This commit is contained in:
2025-08-05 12:57:18 -07:00
parent 03a335710f
commit ff95f2cbc0
27 changed files with 20 additions and 20 deletions
+27
View File
@@ -0,0 +1,27 @@
import { Box } from "@chakra-ui/react";
import GlobalMapCanvas from "./views/GlobalMap";
import RegionalMapCanvas from "./views/RegionalMap";
import VolcanoCanvas from "./views/Volcano";
import AdminCanvas from "./views/Admin";
import AccountCanvas from "./views/Account"
import SettingsCanvas from "./views/Settings"
import { HomeCanvas } from "./views/Home";
import { DASHBOARD_VIEWS } from "@/constants/viewKeys";
export default function Canvas({...props}) {
const { view } = props;
return (
<>
{/* Check if view is a tab inside the Canvas Header */}
{ DASHBOARD_VIEWS.includes(view) && (
<HomeCanvas width="100%" height="100%" {...props}/>
)}
{view === "global" && <GlobalMapCanvas />}
{view === "regional" && <RegionalMapCanvas />}
{view === "volcano" && <VolcanoCanvas />}
{view === "admin" && <AdminCanvas />}
{view === "account" && <AccountCanvas />}
{view === "settings" && <SettingsCanvas />}
</>
);
}
@@ -0,0 +1,35 @@
import { IoIosArrowBack } from "react-icons/io";
import { Menu, Portal } from "@chakra-ui/react";
import { v4 as uuid } from 'uuid';
import AlertLevel from "./AlertLevelWidget";
export default function AddWidgetMenu({onAddWidget, widgetArray, setWidgetArray}) {
function appendWidget(name) {
const newID = uuid(); // could be replaced with something else like guid
setWidgetArray([ // updating widget array containing info on the types of widgets
...widgetArray,
{i: newID, widget: name}
]);
onAddWidget(newID); // updating array containing info on layout of widgets
}
return (
<Menu.Root>
<Menu.TriggerItem>
<IoIosArrowBack /> Add Widget
</Menu.TriggerItem>
<Portal>
<Menu.Positioner>
<Menu.Content>
<Menu.Item
value="AlertLevel"
onClick={() => appendWidget(AlertLevel)}
>
Alert Level
</Menu.Item>
</Menu.Content>
</Menu.Positioner>
</Portal>
</Menu.Root>
)
}
@@ -0,0 +1,10 @@
import Widget from "./Widget";
import { Text } from "@chakra-ui/react";
export default function AlertLevel({id, isEditable, isDelete, DeleteWidget}) {
return (
<Widget id={id} title="Alert Level" isEditable={isEditable} isDelete={isDelete} DeleteWidget={DeleteWidget}>
<Text>Input volcano and country</Text>
</Widget>
);
}
@@ -0,0 +1,7 @@
import { Button } from "@chakra-ui/react";
export function CancelButton ({onCancel}) {
return (
<Button onClick={onCancel} borderRadius="md">Cancel</Button>
)
}
@@ -0,0 +1,9 @@
import { Button } from "@chakra-ui/react";
export default function DeleteAllButton ({onDeleteAll}) {
return (
<Button onClick={onDeleteAll} color="#FFFFFF" bg="#EF4444" _hover={{ bg: "bg.error", color: "#EF4444"}}>
Delete All
</Button>
)
}
@@ -0,0 +1,7 @@
import { Button } from "@chakra-ui/react";
export function SaveButton ({onSave}) {
return (
<Button onClick={onSave} borderRadius="md" bg="primary" _hover={{bg: "primaryLt"}}>Save</Button>
)
}
@@ -0,0 +1,16 @@
import { Input, InputGroup } from "@chakra-ui/react";
import { FiSearch } from "react-icons/fi"
export function SearchBar({ placeholder="Enter a volcano to search", size="sm", width="210px" }) {
return (
<InputGroup startElement={<FiSearch />}>
<Input
placeholder={placeholder}
size={size}
width={width}
borderRadius="full"
bg="base100"
/>
</InputGroup>
);
}
@@ -0,0 +1,35 @@
import { IconButton, Menu, Portal } from "@chakra-ui/react";
import { FiSettings } from "react-icons/fi";
import AddWidgetMenu from "./AddWidgetMenu";
export function SettingsButton({onAddWidget, onEditWidgets, onDeleteWidgets, widgetArray, setWidgetArray}) {
return (
<Menu.Root>
<Menu.Trigger asChild >
<IconButton variant="ghost" size="md" borderRadius="xl">
<FiSettings />
</IconButton>
</Menu.Trigger>
<Portal>
<Menu.Positioner>
<Menu.Content>
<AddWidgetMenu onAddWidget={onAddWidget} setWidgetArray={setWidgetArray} widgetArray={widgetArray}/>
<Menu.Item onClick={onEditWidgets} value ="editwidget">Edit Widgets</Menu.Item>
<Menu.Item
onClick={onDeleteWidgets}
color="fg.error"
_hover={{ bg: "bg.error", color: "fg.error "}}
>
Delete Widgets
</Menu.Item>
</Menu.Content>
</Menu.Positioner>
</Portal>
</Menu.Root>
);
}
@@ -0,0 +1,52 @@
import { useState, useRef } from "react";
import { chakra, useSlotRecipe } from "@chakra-ui/react";
export default function Tooltip ({tooltipkey, content, sidebarIsOpen, children}) {
const [isVisible, setIsVisible] = useState(!sidebarIsOpen);
const recipe = useSlotRecipe({ key: "sidebar" });
const styles = recipe();
const timeoutId = useRef(null);
return (
<chakra.div
width={sidebarIsOpen ? "194px" : "55px"}
transition="width 0.4s ease"
onMouseEnter={() => { timeoutId.current = setTimeout(() => setIsVisible(true), 1100); }}
onMouseLeave={() => {
if (timeoutId.current) {
clearTimeout(timeoutId.current);
timeoutId.current = null;
}
setIsVisible(false);
}}
onClick={() => {
if (timeoutId.current) {
clearTimeout(timeoutId.current);
timeoutId.current = null;
}
setIsVisible(false);
}}
>
{children}
{isVisible && !sidebarIsOpen && (
<>
<chakra.div
fontSize="xs"
position="absolute"
bg="gray.700"
color="white"
left="65px"
px={2}
py={1}
borderRadius="md"
whiteSpace="nowrap"
zIndex={10}
>
{content}
</chakra.div>
</>
)}
</chakra.div>
)
}
@@ -0,0 +1,29 @@
import { useSlotRecipe, chakra, Flex, CloseButton, Text } from "@chakra-ui/react"
export default function Widget({ id, title, isEditable, isDelete, DeleteWidget, children }) {
const recipe = useSlotRecipe({ key: "widget" });
const styles = recipe();
return (
<>
<chakra.div css={styles.header} cursor={isEditable ? "move":"default"}>
{isDelete ? (
<>
<Flex flexDirection="row" justifyContent="space-between" alignItems="center">
<Text className="widget-handle" w="100%" fontWeight="bold">{title}</Text>
<CloseButton onClick={() => DeleteWidget(id)} size="xs" _hover={{bg: "tomato", color: "white"}}/>
</Flex>
</>
):(
<>
<Text className="widget-handle" fontWeight="bold">{title}</Text>
</>
)}
</chakra.div>
<chakra.div css={styles.canvas} >
{children}
</chakra.div>
</>
);
}
@@ -0,0 +1,75 @@
import { Box, Flex, Field, Input, Stack, Heading, Button } from "@chakra-ui/react"
import { useForm } from 'react-hook-form';
export default function AccountCanvas() {
const {
register,
handleSubmit,
watch,
formState: { errors },
} = useForm();
const onSubmit = (data) => {
console.log("Form submitted:", data);
}
console.log(watch("example")); // watch input value by passing the name of it
return (
<Flex
height="100vh"
direction="column"
align="start"
justify="start"
bg="gray.50"
pt={6}
pl={6}
>
<Heading as="h1" mb={2}>Account</Heading>
<Box
as="form"
onSubmit={handleSubmit(onSubmit)}
border="1px solid"
borderColor="gray.300"
borderRadius="md"
padding={4}
bg="white"
boxShadow="md"
width="100%"
maxW="sm"
>
{/* <Text fontSize="xl" fontWeight="bold" mb={6}>
Account
</Text> */}
<Stack gap="6" maxW="sm" css={{ "--field-label-width": "96px" }}>
<Field.Root orientation="horizontal">
<Field.Label>User Name</Field.Label>
<Input {...register("username")} placeholder="David Johnston" flex="1" />
</Field.Root>
<Field.Root orientation="horizontal">
<Field.Label> First Name</Field.Label>
<Input {...register("firstname")} placeholder="David" flex="1" />
</Field.Root>
<Field.Root orientation="horizontal">
<Field.Label>Last Name</Field.Label>
<Input {...register("lastname")} placeholder="Johnston" flex="1" />
</Field.Root>
<Field.Root orientation="horizontal">
<Field.Label>Email</Field.Label>
<Input {...register("email")} placeholder="djohntson@usgs.gov" flex="1" />
</Field.Root>
<Button type="submit">
Save
</Button>
</Stack>
</Box>
</Flex>
)
}
@@ -0,0 +1,24 @@
import { Box, Text } from '@chakra-ui/react';
export default function AdminCanvas() {
return (
<Box p={8} mt={4} ml="auto" mr="auto" width="600px">
<Box
bg="bg"
shadow="md"
borderRadius="md"
p={8}
width="600px"
height="100px"
textAlign="center"
display="flex"
alignItems="center"
justifyContent="center"
>
<Text fontsize="2xl" fontWeight="bold" color="gray.700">
Admin Canvas coming soon!
</Text>
</Box>
</Box>
);
}
@@ -0,0 +1,19 @@
import React from "react";
import { MapContainer, TileLayer, Marker, Popup } from "react-leaflet";
export default function GlobalMapCanvas() {
const position = [45.61142478054226, -122.49630033167544]; // [latitude, longitude]
return (
<MapContainer center={position} zoom={3} style={{ height: "100%", width: "100%" }}>
<TileLayer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
attribution='&copy; OpenStreetMap contributors'
/>
<Marker position={position}>
<Popup>
CVO Home of the volcanoligists
</Popup>
</Marker>
</MapContainer>
);
}
+170
View File
@@ -0,0 +1,170 @@
import { HStack, Flex, Tabs } from "@chakra-ui/react";
import { useState } from "react";
import WidgetCanvas from "./HomeTabs/MyDashboard";
import { SearchBar } from "../CanvasComponents/SearchBar";
import { SettingsButton } from "../CanvasComponents/SettingsButton";
import { DailyActivity } from "./HomeTabs/DailyActivity";
import { Gas } from "./HomeTabs/Gas";
import { Seismic } from "./HomeTabs/Seismic";
import { RemoteSensing } from "./HomeTabs/RemoteSensing";
import { CancelButton } from "../CanvasComponents/CancelButton";
import { SaveButton } from "../CanvasComponents/SaveButton";
import DeleteAllButton from "../CanvasComponents/DeleteAllButton";
export function HomeCanvas({...props}) {
// deconstructing props
const { width, height, view, layout, setLayout, setOriginalLayout, onLayoutChange, onChangeView, onCancel,
isDelete, setIsDelete, isEditable, setIsEditable, widgetArray, setWidgetArray } = props;
const onAddWidget = (ID) => {
setLayout(prev => [
...prev,
{
i: ID,
x: (prev.length * 3) % 12,
y: Infinity,
w: 3,
h: 3,
static: false
}
]);
onEditWidgets();
};
const onEditWidgets = () => {
console.log("widgets are now editable");
setOriginalLayout(layout); // Backup layout before editing
// Set all widgets to editable, static: false
setLayout(prevLayout =>
prevLayout.map(item => ({
...item,
static: false
}))
);
setIsEditable(true); // Enable edit mode
}
const onSave = () => {
console.log("saving widgets...");
// Set all widgets back to static: true
setLayout(prevLayout =>
prevLayout.map(item => ({
...item,
static: true
}))
);
setIsEditable(false);
setIsDelete(false);
setOriginalLayout([]); // Clear backup layout
}
const onDeleteWidgets = () => {
console.log("Start deleting widgets...");
setOriginalLayout(layout);
setIsEditable(true);
setIsDelete(true);
}
const DeleteWidget = (ID) => {
console.log("Deleting widget with id: ", ID);
setLayout(layout => layout.filter(w => w.i !== ID));
// add removal for widgetArray here
}
const DeleteAll = () => {
const confirm = window.confirm(
"Are you sure you want to delete all widgets?"
);
if (confirm) {
setLayout([]);
setWidgetArray([]);
onSave();
}
else return;
}
// Prop Forwarding
const widgetCanvasProps = { layout, onLayoutChange, isEditable, isDelete, DeleteWidget, widgetArray };
const settingsButtonProps = { onAddWidget, onEditWidgets, onDeleteWidgets, setWidgetArray, widgetArray };
return (
/* HEADER */
<Tabs.Root
width={width}
height={height}
lazyMount
value={ view }
variant="line"
// Prevent navigation away from 'My Dashboard' while in Edit mode
onValueChange={(e) => {
const nextView = e.value;
onChangeView(nextView);
}}
>
<Flex
as="header"
position="sticky"
top="0"
zIndex="dropdown" // zIndex: 1100
bg="base200"
px={6}
pt={3}
align="center"
justify="space-between"
>
<Tabs.List>
<Tabs.Trigger value="widget" fontSize="lg">
My Dashboard
</Tabs.Trigger>
<Tabs.Trigger value="gas" fontSize="lg">
Gas
</Tabs.Trigger>
<Tabs.Trigger value="seismic" fontSize="lg">
Seismic
</Tabs.Trigger>
<Tabs.Trigger value="remote" fontSize="lg">
Remote Sensing
</Tabs.Trigger>
<Tabs.Trigger value="daily" fontSize="lg">
Daily Activity
</Tabs.Trigger>
</Tabs.List>
<HStack >
{isEditable ? (
<>
<SaveButton onSave={ onSave } />
<CancelButton onCancel={ onCancel } />
{isDelete ? (
<DeleteAllButton onDeleteAll={ DeleteAll } />
):(null)
}
</>
):(
<>
<SearchBar />
<SettingsButton {...settingsButtonProps}/>
</>
)}
</HStack>
</Flex>
{/* CANVAS */}
<Tabs.Content value="widget">
<WidgetCanvas {...widgetCanvasProps}/>
</Tabs.Content>
<Tabs.Content value="gas">
<Gas />
</Tabs.Content>
<Tabs.Content value="seismic">
<Seismic />
</Tabs.Content>
<Tabs.Content value="remote">
<RemoteSensing />
</Tabs.Content>
<Tabs.Content value="daily">
<DailyActivity />
</Tabs.Content>
</Tabs.Root>
);
}
@@ -0,0 +1,22 @@
import { Box, Text } from "@chakra-ui/react";
export function DailyActivity() {
return(
<Box p={8} mt={4} ml="auto" mr="auto" width="600px">
<Box
bg="bg"
shadow="md"
borderRadius="md"
p={8}
width="600px"
height="100px"
textAlign="center"
display="flex"
alignItems="center"
justifyContent="center"
>
<Text fontsize="2xl" fontWeight="bold" color="gray.700">Daily Activity dashboard coming soon.</Text>
</Box>
</Box>
);
}
@@ -0,0 +1,22 @@
import { Box, Text } from "@chakra-ui/react";
export function Gas() {
return(
<Box p={8} mt={4} ml="auto" mr="auto" width="600px">
<Box
bg="bg"
shadow="md"
borderRadius="md"
p={8}
width="600px"
height="100px"
textAlign="center"
display="flex"
alignItems="center"
justifyContent="center"
>
<Text fontsize="2xl" fontWeight="bold" color="gray.700">Gas dashboard coming soon.</Text>
</Box>
</Box>
);
}
@@ -0,0 +1,50 @@
import GridLayout, { WidthProvider } from 'react-grid-layout';
import { useSlotRecipe, chakra, Flex } from '@chakra-ui/react';
export default function WidgetCanvas({layout, onLayoutChange, isEditable, isDelete, DeleteWidget, widgetArray}) {
console.log("rendering MyDashboard");
const recipe = useSlotRecipe({ key: "widget" });
const styles = recipe(); // using root recipe for widget below
const ResponsiveGridLayout = WidthProvider(GridLayout); // canvas width now adjusts to window size
const handleDragStart = () => { document.body.style.userSelect = "none"; }; // don't allow text highlighting when dragging
const handleDragStop = () => { document.body.style.userSelect = ""; }; // allow text highlighting all other times
let merged = [];
console.log("widgetArray: ", widgetArray);
console.log("layout: ", layout);
if (widgetArray.length && layout.length) {
const widgetMap = new Map(widgetArray.map(item => [item.i, item]));
merged = layout.map(item => {
return {
...item,
...widgetMap.get(item.i)
};
});
};
return (
<chakra.div width="100%">
<ResponsiveGridLayout
className="layout"
layout={layout}
cols={12} // Total columns in grid
rowHeight={30} //Height of a single row in px
isBounded={true} // Total width of the grid
margin={[10, 10]} // [horizontal, vertical] gap
onLayoutChange={onLayoutChange}
draggableHandle=".widget-handle" // Make only the header draggable
onDragStart={handleDragStart}
onDragStop={handleDragStop}
>
{merged.map((item) => {
const Widget = item.widget;
return (
<Flex key={item.i} css={styles.root} height="100%" >
<Widget id={item.i} isEditable={isEditable} isDelete={isDelete} DeleteWidget={DeleteWidget}/>
</Flex>
)
})}
</ResponsiveGridLayout>
</chakra.div>
);
}
@@ -0,0 +1,22 @@
import { Box, Text } from "@chakra-ui/react";
export function RemoteSensing() {
return(
<Box p={8} mt={4} ml="auto" mr="auto" width="600px">
<Box
bg="bg"
shadow="md"
borderRadius="md"
p={8}
width="600px"
height="100px"
textAlign="center"
display="flex"
alignItems="center"
justifyContent="center"
>
<Text fontsize="2xl" fontWeight="bold" color="gray.700">Remote Sensing dashboard coming soon.</Text>
</Box>
</Box>
);
}
@@ -0,0 +1,22 @@
import { Box, Text } from "@chakra-ui/react";
export function Seismic() {
return(
<Box p={8} mt={4} ml="auto" mr="auto" width="600px">
<Box
bg="bg"
shadow="md"
borderRadius="md"
p={8}
width="600px"
height="100px"
textAlign="center"
display="flex"
alignItems="center"
justifyContent="center"
>
<Text fontsize="2xl" fontWeight="bold" color="gray.700">Seismic dashboard coming soon.</Text>
</Box>
</Box>
);
}
@@ -0,0 +1,19 @@
import React from "react";
import { MapContainer, TileLayer, Marker, Popup } from "react-leaflet";
export default function GlobalMapCanvas() {
const position = [45.61142478054226, -122.49630033167544]; // [latitude, longitude]
return (
<MapContainer center={position} zoom={13} style={{ height: "100%", width: "100%" }}>
<TileLayer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
attribution='&copy; OpenStreetMap contributors'
/>
<Marker position={position}>
<Popup>
CVO Home of the volcanoligists
</Popup>
</Marker>
</MapContainer>
);
}
@@ -0,0 +1,24 @@
import { Box, Text } from '@chakra-ui/react';
export default function SettingsCanvas() {
return (
<Box p={8} mt={4} ml="auto" mr="auto" width="600px">
<Box
bg="bg"
shadow="md"
borderRadius="md"
p={8}
width="600px"
height="100px"
textAlign="center"
display="flex"
alignItems="center"
justifyContent="center"
>
<Text fontsize="2xl" fontWeight="bold" color="gray.700">
Settings Canvas coming soon!
</Text>
</Box>
</Box>
);
}
@@ -0,0 +1,24 @@
import { Box, Text } from '@chakra-ui/react';
export default function VolcanoCanvas() {
return (
<Box p={8} mt={4} ml="auto" mr="auto" width="600px">
<Box
bg="bg"
shadow="md"
borderRadius="md"
p={8}
width="600px"
height="100px"
textAlign="center"
display="flex"
alignItems="center"
justifyContent="center"
>
<Text fontsize="2xl" fontWeight="bold" color="gray.700">
Volcano Canvas coming soon!
</Text>
</Box>
</Box>
);
}