Author SHA1 Message Date
vkuchenik b73ce88185 update: rounder borders for widget settings menu 2026-11-10 08:54:58 -08:00
vkuchenik b7a5ae2ae2 update: widget settings ui is now updated for RS, AL changes, Comm Coor, and IVANS widgets. it contains sections and a unified color scheme in the themes file in the popover recipe 2026-11-10 08:31:12 -08:00
vkuchenik 5798b90ab7 update: comm coor widget settings ui
wip: themes for checkboxes
2026-11-09 08:00:02 -08:00
vkuchenik 7b3e7d3d0c refactor: continueing to propogate changes made to widget setting popover in al changes to ivans widget 2026-11-06 10:53:49 -08:00
vkuchenik 58c6a40b06 refactor: adjusting colors in themes file for better visibility/distinction of entries in widgets like al changes, ivans, and rs in both lt and dark mode 2026-11-06 10:52:58 -08:00
vkuchenik e50638ab55 add: volcano drop down filter w/ multiselect capabilities in widget settings. UI only! No functionality has been built yet 2026-11-05 15:39:27 -08:00
vkuchenik 189d6e2d1d add: new filtering options were added to the alert changes widget. options are lvl inc, lvl dec, elevated lvls. additionally, updated how the UI looks for the al changes widget settings menu 2026-11-05 10:43:05 -08:00
vkuchenik d4fb170e14 fix:
- alert level changes now display accurate color.
- comm coor report no longer has dark loading screen in lt mode. login input text no longer white in dark mode
2026-11-03 13:27:47 -08:00
vkuchenik 872550c2c9 wip: small random ui changes 2026-10-23 11:23:16 -07:00
11 changed files with 1159 additions and 445 deletions
+354 -135
View File
@@ -1,28 +1,51 @@
import {useDashboardStore} from "@/store/useDashboardStore.jsx"; import { useDashboardStore } from "@/store/useDashboardStore.jsx";
import VdapApi from "../../hooks/useVdapApi.js"; import VdapApi from "../../hooks/useVdapApi.js";
import useBuildUrl from "../../hooks/useBuildUrl.js"; import useBuildUrl from "../../hooks/useBuildUrl.js";
import { Footprints, Plane, Sailboat, TrendingUp, TrendingDown } from 'lucide-react'; import {
import {Box, Collapsible, Flex, Spinner, Text, Timeline, Center} from "@chakra-ui/react"; Footprints,
Plane,
Sailboat,
TrendingUp,
TrendingDown,
} from "lucide-react";
import {
Box,
Collapsible,
Flex,
Spinner,
Text,
Timeline,
Center,
} from "@chakra-ui/react";
import { useMemo, useCallback } from "react"; import { useMemo, useCallback } from "react";
export default function AlertChanges({ settings, widgetID }) {
// 0) CREATE SELF AWARENESS, by finding this specific widgets object in the state-list of active/displayed widgets
const thisWidget = useDashboardStore(
useCallback(
(state) => state.widgets.find((widget) => widget.id === widgetID),
[widgetID],
),
);
export default function AlertChanges({ settings, widgetID}) { // 1) BUILD URL, default is 7 days past
// Find this specific widgets object in the state list of active & displayed widgets const url = useBuildUrl(
const thisWidget = useDashboardStore(useCallback((state) => "/api/v2/alert_changes",
state.widgets.find((widget) => widget.id === widgetID), [widgetID] settings?.startDate,
)); settings?.endDate,
);
// build url, default is 7 days past // 2) CALL API hook to retrieve data
const url = useBuildUrl('/api/v2/alert_changes', settings?.startDate, settings?.endDate); const { data: apiData, isLoading, error } = VdapApi(url);
// call api hook to retrieve data // 3) SORT the data by grouping into volcanoes and sorting form newest to oldest
const {data: apiData, isLoading, error} = VdapApi(url); // doing this 1st bc the filters might change w/ user input but the sorts (currently) wont and thus will
// only run when the api retrieves new data
const data = useMemo(() => { const sortedData = useMemo(() => {
if (apiData.length === 0) return []; if (apiData.length === 0) return [];
// .reduce() is a built-in array method used to transform an entire array into a single value (like an object) // .reduce() is a built-in array method used to transform an entire array into a single value (like an object)
// here it returns a single massive object w/ volcano names as keys // here we sort the data by volcano, and return it as a single massive object w/ volcano names as keys
const grouped = apiData.reduce((accumulator, item) => { const grouped = apiData.reduce((accumulator, item) => {
if (!accumulator[item.volcano]) accumulator[item.volcano] = []; if (!accumulator[item.volcano]) accumulator[item.volcano] = [];
accumulator[item.volcano].push(item); accumulator[item.volcano].push(item);
@@ -33,33 +56,97 @@ export default function AlertChanges({ settings, widgetID}) {
const group_arr = Object.entries(grouped); const group_arr = Object.entries(grouped);
// sort data newest to oldest within each volcano // sort data newest to oldest within each volcano
const sorted = group_arr.map((item) => const sorted = group_arr.map((item) => [
[ item[0], // volcano name
item[0], // volcano name [...item[1]].sort( // item[1] is the array of al changes for a volcano
[...(item[1])].sort((a, b) => (a, b) =>
new Date(b.alert_start_date_local) - new Date(a.alert_start_date_local)) new Date(b.alert_start_date_local) -
] new Date(a.alert_start_date_local),
),
]);
// the volcanoes with the most recent issued alert level change appear at the top of the list (newest -> oldest)
const sorted_entries = [...sorted].sort(
(a, b) =>
new Date(b[1][0].alert_start_date_local) -
new Date(a[1][0].alert_start_date_local),
); );
// volcano with most recent alert level change appears at top of list // additionally, seperates out data into domains within a volcano for sm widgets,
const sorted_entries = [...sorted].sort((a, b) => new Date(b[1][0].alert_start_date_local) - new Date(a[1][0].alert_start_date_local)); // to list all most recent al changes w/in a volcano w/in a domain
if (thisWidget?.size === "small") { if (thisWidget?.size === "small") {
return (sorted_entries.map((volcano) => { return sorted_entries.map((volcano) => {
const domain = volcano[1].reduce((acc, curr) => { const domain = volcano[1].reduce((acc, curr) => {
if (!acc[curr.alert_scheme_domain]) acc[curr.alert_scheme_domain] = []; if (!acc[curr.alert_scheme_domain])
acc[curr.alert_scheme_domain] = [];
acc[curr.alert_scheme_domain].push(curr); acc[curr.alert_scheme_domain].push(curr);
return acc; return acc;
}, {}); }, {});
const domain_arr = Object.entries(domain); const domain_arr = Object.entries(domain);
console.log("sm data: ", [volcano[0], domain_arr]);
return [volcano[0], domain_arr]; return [volcano[0], domain_arr];
})); });
} }
console.log("lg data: ", sorted_entries);
return sorted_entries; return sorted_entries;
}, [apiData, thisWidget]); }, [apiData, thisWidget]);
// 4) FILTER according to what is set in the widgets settings
const filteredData = useMemo(() => {
if (sortedData.length === 0) return [];
let filtered_arr = [];
// must show at least one inc or dec alert levels, otherwise no data is displayed
if (
!settings?.filters?.levelIncreases &&
!settings?.filters?.levelDecreases
)
return [];
// return all data
if (
settings?.filters?.levelIncreases &&
settings?.filters?.levelDecreases &&
!settings?.filters.elevatedLevels
)
return sortedData;
// only elevated alert levels
filtered_arr = settings?.filters.elevatedLevels
? sortedData.map((volcano) => [
volcano[0],
volcano[1].filter(
(item) =>
item.alert_scheme_highest - Number(item.alert_level_rank) <= 1,
),
])
.filter((volcano) => volcano[1].length > 0)
: sortedData;
// only decreases (rm inc)
filtered_arr = !settings?.filters.levelIncreases
? filtered_arr.map((volcano) => [
volcano[0],
volcano[1].filter((item) => !(item.change_type === "increase")),
])
.filter((volcano) => volcano[1].length > 0)
: filtered_arr;
// only increases (rm dec)
filtered_arr = !settings?.filters?.levelDecreases
? filtered_arr.map((volcano) => [
volcano[0],
volcano[1].filter((item) => !(item.change_type === "decrease")),
])
.filter((volcano) => volcano[1].length > 0)
: filtered_arr;
console.log("filtered_arr: ", filtered_arr);
return filtered_arr;
}, [sortedData, settings]);
// Alert colors are now light/dark mode responsive // Alert colors are now light/dark mode responsive
const alertColors = { const alertColors = {
Red: "alertRed", Red: "alertRed",
@@ -68,153 +155,285 @@ export default function AlertChanges({ settings, widgetID}) {
Green: "alertGreen", Green: "alertGreen",
}; };
if (isLoading) return ( const options = {
<Flex w="100%" h="100%" align="center" justify="center" bg="transparent"> year: "numeric",
<Spinner size="xl" color="blue.400" thickness="4px"/> month: "short",
</Flex> day: "numeric",
); hour: "numeric",
minute: "2-digit",
second: "2-digit",
timeZoneName: "short"
};
if (error) return ( if (isLoading)
<Flex w="100%" h="100%" align="center" justify="center" bg="transparent"> return (
<Text color="red.400">{error}</Text> <Flex w="100%" h="100%" align="center" justify="center" bg="transparent">
</Flex> <Spinner size="xl" color="blue.400" thickness="4px" />
); </Flex>
);
if (data.length === 0) return ( if (error)
<Flex w="100%" h="100%" align="center" justify="center" bg="transparent"> return (
<Text textAlign="center" color="widgetTextMuted">No flight level data available for the selected dates.</Text> <Flex w="100%" h="100%" align="center" justify="center" bg="transparent">
</Flex> <Text color="red.400">{error}</Text>
); </Flex>
);
if (filteredData.length === 0)
return (
<Flex w="100%" h="100%" align="center" justify="center" bg="transparent">
<Text textAlign="center" color="widgetTextMuted">
No alert level data available for the selected dates.
</Text>
</Flex>
);
return ( return (
// bg="transparent" to let the glass panel show through <Box
<Box display="flex" flexDirection="column" gap={2} p={2} w="100%" h="100%" bg="transparent" overflowY="auto"> display="flex"
{data.map((volcano, index) => { flexDirection="column"
if (thisWidget?.size === "large") return ( gap={2}
<Collapsible.Root key={index} w="100%"> p={2}
<Collapsible.Context> w="100%"
{(collapsible) => ( h="100%"
<> bg="transparent"
overflowY="auto"
>
{filteredData.map((volcano, index) => {
if (thisWidget?.size === "large")
return (
<Collapsible.Root key={index} w="100%">
<Collapsible.Context>
{(collapsible) => (
<Collapsible.Trigger w="100%"> <Collapsible.Trigger w="100%">
<Flex direction="row" <Flex
w="100%" direction="row"
justify="space-between" align="center" w="100%"
p={1} pl={3} pr={3} justify="space-between"
borderTopRadius="lg" align="center"
borderBottomRadius={collapsible.open ? "0" : "lg"} p={1}
bg="widgetItemBg" pl={3}
pr={3}
borderTopRadius="lg"
borderBottomRadius={collapsible.open ? "0" : "lg"}
bg="widgetItemBg"
> >
<Flex direction="column" w="100%" minW={0} textAlign="left"> <Flex
<Text truncate color="text" fontWeight="medium">{volcano[0]}</Text> direction="column"
<Text truncate fontSize="xs" color="widgetTextMuted">{volcano[1][0]?.alert_start_date_local}</Text> w="100%"
minW={0}
textAlign="left"
>
<Text truncate color="text" fontWeight="medium">
{volcano[0]}
</Text>
<Text truncate fontSize="xs" color="widgetTextMuted">
{new Date(volcano[1][0]?.alert_start_date_local)
.toLocaleString('en-US', options)
}
</Text>
</Flex> </Flex>
<Flex w="100%" minW={0} justify="right"> <Flex w="100%" minW={0} justify="right" gap={2}>
<Text truncate pl={2} pr={2} borderRadius="lg" bg="widgetBadgeBg" color="text" fontWeight="semibold"> <Flex w="fit" color="iconColor">
{volcano[1][0]?.change_type === "increase" ? (
<TrendingUp />
) : (
<TrendingDown />
)}
</Flex>
<Text
truncate
pl={2}
pr={2}
borderRadius="lg"
bg="widgetBadgeBg"
color="text"
fontWeight="semibold"
>
{volcano[1][0]?.alert_level_name} {volcano[1][0]?.alert_level_name}
</Text> </Text>
</Flex> </Flex>
</Flex> </Flex>
</Collapsible.Trigger> </Collapsible.Trigger>
)}
</Collapsible.Context>
<Collapsible.Content w="100%" p={3} borderBottomRadius="lg" bg="widgetItemExpandedBg"> <Collapsible.Content
<Timeline.Root size="md" variant="subtle"> w="100%"
{volcano[1]?.map((object, index) => ( p={3}
<Timeline.Item key={index} w="100%" h="100%"> borderBottomRadius="lg"
<Timeline.Connector > bg="widgetItemExpandedBg"
<Timeline.Indicator outline="none" bg="widgetBadgeBg" color="iconColor" size="lg"> >
{object?.alert_scheme_domain === "terrestrial" && <Footprints size={15} />} <Timeline.Root size="md" variant="subtle">
{object?.alert_scheme_domain === "aviation" && <Plane size={15} />} {volcano[1]?.map((object, index) => (
{object?.alert_scheme_domain === "maritime" && <Sailboat size={15} />} <Timeline.Item key={index} w="100%" h="100%">
</Timeline.Indicator> <Timeline.Connector>
<Timeline.Indicator h="50%" outline="none" bg="widgetBadgeBg" color="iconColor" size="lg"> <Timeline.Indicator
{object?.change_type === "increase" ? <TrendingUp /> : <TrendingDown />} outline="none"
</Timeline.Indicator> bg="transparent"
</Timeline.Connector> color="iconColor"
<Timeline.Content w="100%" h="100%"> size="lg"
<Timeline.Description w="100%" h="100%" gap={1}> >
<Flex direction="column" gap={1}> {object?.alert_scheme_domain === "terrestrial" && <Footprints size={15} />}
<Text w="100%" h="100%" color="widgetTextMuted">{object?.alert_start_date_local}</Text> {object?.alert_scheme_domain === "aviation" && <Plane size={15} />}
<Text w="100%" h="100%" {object?.alert_scheme_domain === "maritime" && <Sailboat size={15} />}
color={alertColors[object?.alert_level_name] || "text"} </Timeline.Indicator>
fontSize="md" <Timeline.Indicator
fontWeight="semibold" h="50%"
> outline="none"
{object?.alert_level_name} bg="transparent"
</Text> color="iconColor"
<Flex w="100%" h="20px" direction="row" justify="left" align="center"> size="lg"
<Text color="widgetTextMuted">Alert Scale: </Text> >
{(Array.from({ length: Number(object?.alert_scheme_highest) - Number(object?.alert_scheme_lowest) + 1}, (v, i) => Number(object?.alert_scheme_lowest) + i)) {object?.change_type === "increase" ? (
.map((num) => ( <TrendingUp />
<Center ) : (
key={num} <TrendingDown />
w="20px" h="100%" )}
color="text" </Timeline.Indicator>
bg={num === Number(object?.alert_level_rank) ? "widgetBadgeBg" : "transparent"} </Timeline.Connector>
borderRadius={num === Number(object?.alert_level_rank) ? "xl" : "none"} <Timeline.Content w="100%" h="100%">
textAlign="center" <Timeline.Description w="100%" h="100%" gap={1}>
fontSize="sm" <Flex direction="column" gap={1}>
fontWeight="bold" <Text
> w="100%"
{num} h="100%"
</Center> color="widgetTextMuted"
)) >
{new Date(object?.alert_start_date_local)
.toLocaleString('en-US', {...options, timeZone: 'UTC'})
}
</Text>
<Text
w="100%"
h="100%"
color={
alertColors[object?.alert_level_name] || "text"
}
fontSize="md"
fontWeight="semibold"
>
{object?.alert_level_name}
</Text>
<Flex
w="100%"
h="20px"
direction="row"
justify="left"
align="center"
>
<Text color="widgetTextMuted" mr={1}>
Alert Scale:
</Text>
{Array.from(
{
length:
Number(object?.alert_scheme_highest) -
Number(object?.alert_scheme_lowest) + 1,
},
(v, i) => Number(object?.alert_scheme_lowest) + i
).map((num) => (
<Center
key={num}
w="20px"
h="100%"
color="text"
bg={
num === Number(object?.alert_level_rank)
? "widgetInnerBadgeBg"
: "transparent"
} }
</Flex> borderRadius={
</Flex> num === Number(object?.alert_level_rank)
</Timeline.Description> ? "xl"
</Timeline.Content> : "none"
</Timeline.Item> }
))} textAlign="center"
</Timeline.Root> fontSize="sm"
</Collapsible.Content> fontWeight="bold"
</> >
)} {num}
</Collapsible.Context> </Center>
</Collapsible.Root> ))}
) </Flex>
else if (thisWidget?.size === "small") return ( </Flex>
volcano[1]?.map((domain, index) => ( </Timeline.Description>
</Timeline.Content>
</Timeline.Item>
))}
</Timeline.Root>
</Collapsible.Content>
</Collapsible.Root>
);
else if (thisWidget?.size === "small")
return volcano[1]?.map((domain, index) => (
<Flex <Flex
key={index} key={index}
direction="row" direction="row"
w="100%" w="100%"
gap={2} gap={2}
justify="space-between" align="center" justify="space-between"
p={1} pl={3} pr={3} align="center"
p={1}
pl={3}
pr={3}
borderRadius="lg" borderRadius="lg"
bg="widgetItemBg" bg="widgetItemBg"
> >
<Flex direction="column" w="100%" minW={0} textAlign="left" color="text"> <Flex
direction="column"
w="100%"
minW={0}
textAlign="left"
color="text"
>
<Flex direction="row" w="100%" gap={2} align="center"> <Flex direction="row" w="100%" gap={2} align="center">
<Flex color="iconColor"> <Flex color="iconColor">
{domain[0] === "aviation" && <Plane size={15}/>} {domain[0] === "aviation" && <Plane size={15} />}
{domain[0] === "terrestrial" && <Footprints size={15}/>} {domain[0] === "terrestrial" && <Footprints size={15} />}
{domain[0] === "maritime" && <Sailboat size={15}/>} {domain[0] === "maritime" && <Sailboat size={15} />}
</Flex> </Flex>
<Text truncate fontWeight="medium">{volcano[0] || ""}</Text> <Text truncate fontWeight="medium">
{volcano[0] || ""}
</Text>
</Flex> </Flex>
<Text truncate w="100%" fontSize="xs" color="widgetTextMuted">{domain[1]?.[0]?.alert_start_date_local || ""}</Text> <Text truncate w="100%" fontSize="xs" color="widgetTextMuted">
{domain[1]?.[0]?.alert_start_date_local || ""}
</Text>
</Flex> </Flex>
<Flex direction="row" w="100%" minW={0} justify="right" gap={2} color="text"> <Flex
direction="row"
w="100%"
minW={0}
justify="right"
gap={2}
color="text"
>
<Flex w="25px" justify="right" color="iconColor"> <Flex w="25px" justify="right" color="iconColor">
{domain[1]?.[0]?.change_type === "increase" ? <TrendingUp/> : <TrendingDown/>} {domain[1]?.[0]?.change_type === "increase" ? (
<TrendingUp />
) : (
<TrendingDown />
)}
</Flex> </Flex>
<Text <Text
truncate truncate
pl={2} pr={2} pl={2}
pr={2}
borderRadius="lg" borderRadius="lg"
bg="widgetBadgeBg" bg="widgetBadgeBg"
color={alertColors[domain[1]?.[0]?.alert_level_name] || "text"} color={
alertColors[domain[1]?.[0]?.alert_level_name] || "text"
}
fontWeight="semibold" fontWeight="semibold"
> >
{domain[1]?.[0]?.alert_level_name} {domain[1]?.[0]?.alert_level_name}
</Text> </Text>
</Flex> </Flex>
</Flex> </Flex>
)) ));
);
})} })}
</Box> </Box>
) );
} }
@@ -114,23 +114,26 @@ export default function CommCoorReport({ settings, widgetID }) {
} }
}, [settings, widgetID, updateSettings, apiData]); }, [settings, widgetID, updateSettings, apiData]);
if (isLoading) return ( if (isLoading)
<Flex w="100%" h="100%" align="center" justify="center" borderRadius="lg" bg="gray.800"> return (
<Spinner size="xl" color="blue.400" thickness="4px"/> <Flex w="100%" h="100%" align="center" justify="center" borderRadius="lg" bg="transparent">
</Flex> <Spinner size="xl" color="blue.400" thickness="4px"/>
); </Flex>
);
if (error) return ( if (error)
<Flex w="100%" h="100%" align="center" justify="center" textAlign="center" borderRadius="lg" bg="gray.800"> return (
<Text color="red.400">{error}</Text> <Flex w="100%" h="100%" align="center" justify="center" textAlign="center" borderRadius="lg" bg="transparent">
</Flex> <Text color="red.400">{error}</Text>
); </Flex>
);
if (!apiData) return ( if (!apiData)
<Flex w="100%" h="100%" align="center" justify="center" borderRadius="lg" bg="gray.800"> return (
<Text textAlign="center" color="gray.400">No data</Text> <Flex w="100%" h="100%" align="center" justify="center" borderRadius="lg" bg="transparent">
</Flex> <Text textAlign="center" color="gray.400">No data</Text>
); </Flex>
);
return ( return (
<Flex direction="row" w="100%" h="100%" p={1} overflowY="auto"> <Flex direction="row" w="100%" h="100%" p={1} overflowY="auto">
+16 -17
View File
@@ -1,4 +1,4 @@
import {Button, Flex, Spinner, Text, Box, Collapsible} from "@chakra-ui/react"; import {Button, Flex, Spinner, Text, Box, Collapsible, Highlight} from "@chakra-ui/react";
import {useEffect, useMemo, useCallback } from 'react'; import {useEffect, useMemo, useCallback } from 'react';
import {useDashboardStore} from "@/store/useDashboardStore.jsx"; import {useDashboardStore} from "@/store/useDashboardStore.jsx";
import VdapApi from '../../hooks/useVdapApi.js'; import VdapApi from '../../hooks/useVdapApi.js';
@@ -100,7 +100,7 @@ export default function IvansAlerts({ settings, widgetID }) {
bg="widgetItemBg" bg="widgetItemBg"
> >
<Flex direction="column" w="100%" minW={0} textAlign="left" color="text"> <Flex direction="column" w="100%" minW={0} textAlign="left" color="text">
<Text truncate>{item.volcano_name}</Text> <Text truncate color="text" fontWeight="medium">{item.volcano_name}</Text>
<Text truncate fontSize="xs" color="widgetTextMuted">{item.send_time}</Text> <Text truncate fontSize="xs" color="widgetTextMuted">{item.send_time}</Text>
</Flex> </Flex>
<Flex w="100%" minW={0} justify="right"> <Flex w="100%" minW={0} justify="right">
@@ -111,33 +111,32 @@ export default function IvansAlerts({ settings, widgetID }) {
<Collapsible.Content w="100%" h="auto"> <Collapsible.Content w="100%" h="auto">
{/* This box needs to be at h="100%" for the pdf toolbar to appear */} {/* This box needs to be at h="100%" for the pdf toolbar to appear */}
<Box w="100%" h="100%" p={1} borderBottomRadius="lg" bg="widgetItemExpandedBg"> <Box w="100%" h="100%" p={1} borderBottomRadius="lg" bg="widgetItemExpandedBg">
<Flex direction="column" gap={2} bg="widgetItemBg" borderRadius="lg" p={2} align="end"> <Flex direction="column" gap={2} borderRadius="lg" p={2} align="start">
<Text w="fit" fontSize="sm" color="text" p={1} bg="widgetInnerBadgeBg" borderRadius="md">Preparer: {item?.preparer}</Text>
<Text w="100%" h="100%" fontSize="sm" color="text" textAlign="left" whiteSpace="pre-line">{item?.summary}</Text> <Text w="100%" h="100%" fontSize="sm" color="text" textAlign="left" whiteSpace="pre-line">{item?.summary}</Text>
<Text w="fit" fontSize="sm" color="text" pl={1} pr={1} bg="widgetBadgeBg" borderRadius="md">Preparer: {item?.preparer}</Text>
</Flex> </Flex>
{item?.files?.map((file, index) => { {item?.files?.map((file, index) => {
return ( return (
<Box w="100%" h="100%" key={index} p={1}> <Box w="100%" h="100%" key={index} >
<br /> <Flex direction="row" w="100%">
<Flex mb={2} justify="space-between"> <Text w="100%" p={1} fontWeight="semibold" bg="widgetInnerBadgeBg" borderLeftRadius="lg" fontSize="sm" color="text">File: </Text>
<Text pl={1} pr={1} bg="widgetBadgeBg" borderRadius="md" fontSize="xs" color="text">{file?.name}</Text> <a href={`${file?.url}#view=FitV&toolbar=0`} target="_blank" rel="noopener noreferrer">
<Box borderRadius="lg" bg="widgetBadgeBg" color="text" pl={1} pr={1} fontSize={13} asChild> <Text truncate w="100%" p={1} bg="widgetInnerBadgeBg" borderRightRadius="lg" fontSize="sm" color="text">
<a href={`${file?.url}#view=FitV&toolbar=1`} target="_blank" rel="noopener noreferrer"> {file?.name}
Open </Text>
</a> </a>
</Box>
</Flex> </Flex>
<object <object
data={`${file?.url}#view=FitH&toolbar=1`} data={`${file?.url}#view=FitH&toolbar=1`}
type="application/pdf" type="application/pdf"
style={{ width: '100%', height: '100%', display: 'block' }} style={{ width: '100%', height: '100%', display: 'block' }}
> >
<Flex w="100%" h="100%" p={4} color="widgetTextMuted"> <Flex direction="column" w="100%" h="100%" align="center" p={4} mt={1} color="widgetTextMuted" bg="widgetInnerBadgeBg" borderRadius="lg">
<Text mb={2}> <Text mb={2} fontSize="xs" color="red.300">
Your browser cannot display this PDF directly. Your browser cannot display this PDF directly.
</Text> </Text>
<a href={file?.url} target="_blank"> <a href={file?.url} target="_blank" bg="widgetBadgeBg" rel="noopener noreferrer">
Download PDF <Text fontSize="xs" bg="widgetBadgeBg" borderRadius="md" p={1}>Download PDF Instead</Text>
</a> </a>
</Flex> </Flex>
</object> </object>
@@ -96,7 +96,7 @@ export default function RemoteSensing({ settings, widgetID }) {
<Collapsible.Content w="100%" h="auto"> <Collapsible.Content w="100%" h="auto">
{/* This box needs to be at h="100%" for the pdf toolbar to appear */} {/* This box needs to be at h="100%" for the pdf toolbar to appear */}
<Box w="100%" h="100%" p={1} borderBottomRadius="lg" bg="widgetItemExpandedBg"> <Box w="100%" h="100%" p={1} borderBottomRadius="lg" bg="widgetItemExpandedBg">
<Flex direction="column" gap={2} bg="widgetItemBg" borderRadius="lg" p={2} align="end"> <Flex direction="column" gap={2} borderRadius="lg" p={2} align="end">
<Text w="100%" h="100%" fontSize="sm" color="text" textAlign="left" whiteSpace="pre-line">{item?.summary}</Text> <Text w="100%" h="100%" fontSize="sm" color="text" textAlign="left" whiteSpace="pre-line">{item?.summary}</Text>
<Text w="fit" fontSize="sm" color="text" pl={1} pr={1} bg="widgetBadgeBg" borderRadius="md">Source: {item?.sender}</Text> <Text w="fit" fontSize="sm" color="text" pl={1} pr={1} bg="widgetBadgeBg" borderRadius="md">Source: {item?.sender}</Text>
</Flex> </Flex>
@@ -1,38 +1,283 @@
import {Flex, Slider, Switch} from "@chakra-ui/react"; import {
import {useDashboardStore} from "@/store/useDashboardStore.jsx"; Flex,
Slider,
Switch,
Text,
Field,
Fieldset,
Input,
CheckboxCard,
CheckboxGroup,
Combobox,
Portal,
useFilter,
useListCollection,
Wrap,
Badge, Separator, HStack, useSlotRecipe
} from "@chakra-ui/react";
import { useDashboardStore } from "@/store/useDashboardStore.jsx";
import { useState, useMemo } from "react";
export default function IvansAlertsSettings({ widgetId, currentSettings }) { export default function AlertChangeSettings({ widgetId, currentSettings }) {
const updateSettings = useDashboardStore(state => state.updateWidgetSettings); // STYLING
const popoverRecipe = useSlotRecipe({ key: "glassPopover" });
const popoverStyles = popoverRecipe();
console.log("currentSettings:", currentSettings); // STATE MANAGEMENT
const updateSettings = useDashboardStore(
(state) => state.updateWidgetSettings,
);
const [startError, setStartError] = useState(null);
const [endError, setEndError] = useState(null);
const [selectedVolcanoes, setSelectedVolcanoes] = useState([]);
const { startsWith } = useFilter({ sensitivity: "base" });
const { collection, filter, reset } = useListCollection({
initialItems: volcanoes,
filter: startsWith,
limit: 10,
});
// FUNCTIONS
const handleValueChange = (details) => {
setSelectedVolcanoes(details.value)
}
return ( return (
<Flex flexDirection="column" justifyContent="space-between" align="center" gap={2} pb={2}> <Flex direction="column" pb={2} overflowY="auto">
{/* SLIDER DATE RANGE SELECTION */} <Fieldset.Root>
<Slider.Root <Fieldset.Content>
w="90%" h="100%" {/* SELECTING DATE RANGE */}
min={1} max={14} <HStack>
defaultValue={[1]} <Text flexShrink="0" fontWeight="semibold">Date Range Selection</Text>
step={1} <Separator flex="1" size="sm"/> {/*line seperator*/}
onValueChangeEnd={(e) => { </HStack>
console.log("change end", e); <Flex direction="column" w="100%" h="100%" justify="center" gap={2} pr={1}>
const day = new Date(Date.now() - (e.value[0])*86400000).toLocaleDateString('en-CA'); {/* start date */}
updateSettings(widgetId, {...currentSettings, startDate: day}); <Field.Root invalid={!!startError}>
}} <Flex
> direction="row"
<Slider.Label>{currentSettings?.startDate} to {currentSettings?.endDate}</Slider.Label> w="100%"
<Slider.Control> h="100%"
<Slider.Track> align="center"
<Slider.Range /> justify="center"
</Slider.Track> >
<Slider.Thumbs /> <Text w="50%" align="center" justify="center" color="text">
<Slider.MarkerGroup p={3}> Start Date
<Slider.Marker value={1} whiteSpace="nowrap">today</Slider.Marker> </Text>
<Slider.Marker value={7} whiteSpace="nowrap">1 week ago</Slider.Marker> <Flex direction="column" w="100%">
<Slider.Marker value={14} whiteSpace="nowrap">2 weeks ago</Slider.Marker> <Input
</Slider.MarkerGroup> type="date"
</Slider.Control> css={popoverStyles.date}
</Slider.Root> value={currentSettings?.startDate || ""}
onChange={(e) => {
const value = e.target.value;
updateSettings(widgetId, {
...currentSettings,
startDate: value,
});
const dateSelected = new Date(value);
const today = new Date();
today.setHours(0, 0, 0, 0);
if (value && dateSelected > today)
setStartError("Can't Choose a Future Date");
else setStartError("");
}}
/>
{startError && (
<Field.ErrorText>{startError}</Field.ErrorText>
)}
</Flex>
</Flex>
</Field.Root>
{/* end date */}
<Field.Root invalid={!!endError}>
<Flex
direction="row"
w="100%"
h="100%"
align="center"
justify="center"
>
<Text w="50%" align="center" justify="center" color="text">
End Date
</Text>
<Flex direction="column" w="100%">
<Input
type="date"
css={popoverStyles.date}
value={currentSettings?.endDate || ""}
onChange={(e) => {
const value = e.target.value;
updateSettings(widgetId, {
...currentSettings,
endDate: value,
});
const dateSelected = new Date(value);
const today = new Date();
today.setHours(0, 0, 0, 0);
if (value && dateSelected > today)
setEndError("Can't Choose a Future Date");
else setEndError("");
}}
/>
{endError && <Field.ErrorText>{endError}</Field.ErrorText>}
</Flex>
</Flex>
</Field.Root>
</Flex>
{/* FILTERS */}
<CheckboxGroup w="100%">
<Flex direction="column" w="100%" gap={2}>
<HStack>
<Text flexShrink="0" fontWeight="semibold">Filters</Text>
<Separator flex="1" size="sm"/> {/*line seperator*/}
</HStack>
{/* level increases */}
<CheckboxCard.Root
css={popoverStyles.checkboxcard}
defaultChecked={currentSettings?.filters?.levelIncreases}
onCheckedChange={(e) => {
updateSettings(widgetId, {
...currentSettings,
filters: {
...currentSettings.filters,
levelIncreases: e.checked,
},
});
}}
>
<CheckboxCard.HiddenInput /> {/* when removed can't check/uncheck box */}
<CheckboxCard.Control>
<CheckboxCard.Content>
<CheckboxCard.Label fontWeight={400}>
Level Increases
</CheckboxCard.Label>
</CheckboxCard.Content>
<CheckboxCard.Indicator css={popoverStyles.checkbox} />
</CheckboxCard.Control>
</CheckboxCard.Root>
{/* level decreases */}
<CheckboxCard.Root
css={popoverStyles.checkboxcard}
defaultChecked={currentSettings?.filters?.levelDecreases}
onCheckedChange={(e) => {
updateSettings(widgetId, {
...currentSettings,
filters: {
...currentSettings.filters,
levelDecreases: e.checked,
},
});
}}
>
<CheckboxCard.HiddenInput />
<CheckboxCard.Control>
<CheckboxCard.Content>
<CheckboxCard.Label fontWeight={400}>
Level Decreases
</CheckboxCard.Label>
</CheckboxCard.Content>
<CheckboxCard.Indicator css={popoverStyles.checkbox} />
</CheckboxCard.Control>
</CheckboxCard.Root>
{/* elevated levels */}
<CheckboxCard.Root
css={popoverStyles.checkboxcard}
defaultChecked={currentSettings?.filters?.elevatedLevels}
onCheckedChange={(e) => {
updateSettings(widgetId, {
...currentSettings,
filters: {
...currentSettings.filters,
elevatedLevels: e.checked,
},
});
}}
>
<CheckboxCard.HiddenInput />
<CheckboxCard.Control>
<CheckboxCard.Content>
<CheckboxCard.Label fontWeight={400}>
Only Elevated Levels
</CheckboxCard.Label>
</CheckboxCard.Content>
<CheckboxCard.Indicator css={popoverStyles.checkbox}/>
</CheckboxCard.Control>
</CheckboxCard.Root>
</Flex>
</CheckboxGroup>
{/* select volcano to filter on */}
<Combobox.Root
mt={1}
variant="subtle"
multiple
openOnClick
collection={collection}
onValueChange={handleValueChange}
onInputValueChange={(e) => filter(e.inputValue)}
>
<Wrap gap="2">
{selectedVolcanoes.map((volcano) => (
<Badge key={volcano}>{volcano}</Badge>
))}
</Wrap>
<Combobox.Label fontSize="xs" >Select Volcano (s)</Combobox.Label>
<Combobox.Control>
<Combobox.Input placeholder="Type to search" borderRadius="lg"/>
<Combobox.IndicatorGroup>
<Combobox.ClearTrigger />
<Combobox.Trigger onClick={reset} />
</Combobox.IndicatorGroup>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content>
{volcanoes.map((item) => (
<Combobox.Item item={item} key={item.value}>
{item.label}
<Combobox.ItemIndicator />
</Combobox.Item>
))}
<Combobox.Empty>No items found</Combobox.Empty>
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.Root>
</Fieldset.Content>
</Fieldset.Root>
</Flex> </Flex>
) );
} }
const volcanoes = [
{ label: "Mt. St. Helens", value: "helens" },
{ label: "Mt. Adams", value: "adams" },
{ label: "Rainier", value: "rainier" },
{ label: "The Three Sisters", value: "three sisters" },
{ label: "Fuego", value: "fuego" },
{ label: "Laki Laki", value: "lakilaki" },
{ label: "Lewotobi", value: "lewotobi" },
{ label: "Karangatang", value: "karangatang" },
{ label: "Cotopaxi", value: "cotopaxi" },
{ label: "Merapi", value: "merapi" },
{ label: "Marapi", value: "marapi" },
{ label: "Kick 'Em Jenny", value: "kick em jenny"}
]
@@ -1,12 +1,18 @@
import {Button, Field, Flex, Input, Switch, Text} from "@chakra-ui/react"; import {Button, Field, Flex, Input, Switch, Text, Fieldset, Separator, HStack, useSlotRecipe} from "@chakra-ui/react";
import {useDashboardStore} from "@/store/useDashboardStore.jsx"; import {useDashboardStore} from "@/store/useDashboardStore.jsx";
import {useState} from "react"; import {useState} from "react";
export default function CommCoorReportSettings({ widgetId, currentSettings }) { export default function CommCoorReportSettings({ widgetId, currentSettings }) {
// STYLING
const popoverRecipe = useSlotRecipe({ key: "glassPopover" });
const popoverStyles = popoverRecipe();
// STATE MANAGEMENT
const updateSettings = useDashboardStore(state => state.updateWidgetSettings); const updateSettings = useDashboardStore(state => state.updateWidgetSettings);
const [startError, setStartError] = useState(null); const [startError, setStartError] = useState(null);
const [endError, setEndError] = useState(null); const [endError, setEndError] = useState(null);
// FUNCTIONS
const handleCopyToClipboard = () => { const handleCopyToClipboard = () => {
updateSettings(widgetId, {copyToClipboard: true}); updateSettings(widgetId, {copyToClipboard: true});
} }
@@ -17,67 +23,84 @@ export default function CommCoorReportSettings({ widgetId, currentSettings }) {
return ( return (
<Flex flexDirection="column" justifyContent="space-between" gap={2} overflowY="auto"> <Flex flexDirection="column" justifyContent="space-between" gap={2} overflowY="auto">
{/* COPY TO CLIPBOARD */} <Fieldset.Root>
<Button w="fit" size="xs" onClick={handleCopyToClipboard}>Copy Report</Button> <Fieldset.Content>
{/* SELECTING DATE RANGE */}
<HStack>
<Text flexShrink="0" fontWeight="semibold">Date Range Selection</Text>
<Separator flex="1" size="sm"/> {/*line seperator*/}
</HStack>
<Flex direction="column" w="100%" h="100%" justify="center" gap={2} pr={1}>
{/* start date */}
<Field.Root invalid={!!startError}>
<Flex direction="row" w="100%" h="100%" align="center" justify="center">
<Text w="50%" align="center" justify="center" mt={2} color="text">
Start Date
</Text>
<Flex direction="column" w="100%">
<Input
w="100%"
css={popoverStyles.date}
type="date"
value={currentSettings?.startDate || ""}
onChange={(e) => {
const value = e.target.value;
updateSettings(widgetId, {...currentSettings, startDate: value});
{/* DOWNLOAD REPORT */} const dateSelected = new Date(value);
<Button w="fit" size="xs" onClick={handleDownloadReport}>Download Report</Button> const today = new Date();
today.setHours(0, 0, 0, 0);
{/* SELECTING DATE RANGE */} if (value && (dateSelected > today)) setStartError("Can't Choose a Future Date");
<Flex direction="column" w="100%" h="100%" align="center" justify="center" gap="2"> else setStartError("");
<Field.Root invalid={!!startError}> }}
<Flex direction="row" w="100%" h="100%" align="center" justify="center"> />
<Text w="50%" align="center" justify="center" mt={2} color="gray.700"> {startError && <Field.ErrorText>{startError}</Field.ErrorText>}
Start Date </Flex>
</Text> </Flex>
<Flex direction="column" w="100%"> </Field.Root>
<Input {/* end date */}
w="100%" <Field.Root invalid={!!endError}>
type="date" <Flex direction="row" w="100%" h="100%" align="center" justify="center">
value={currentSettings?.startDate || ""} <Text w="50%" align="center" justify="center" mt={2} color="text">
onChange={(e) => { End Date
const value = e.target.value; </Text>
updateSettings(widgetId, {...currentSettings, startDate: value}); <Flex direction="column" w="100%">
<Input
w="100%"
css={popoverStyles.date}
type="date"
value={currentSettings?.endDate || ""}
onChange={(e) => {
const value = e.target.value;
updateSettings(widgetId, {...currentSettings, endDate: value});
const dateSelected = new Date(value); const dateSelected = new Date(value);
const today = new Date(); const today = new Date();
today.setHours(0, 0, 0, 0); today.setHours(0, 0, 0, 0);
if (value && (dateSelected > today)) setStartError("Can't Choose a Future Date"); if (value && (dateSelected > today)) setEndError("Can't Choose a Future Date");
else setStartError(""); else setEndError("");
}} }}
/> />
{startError && <Field.ErrorText>{startError}</Field.ErrorText>} {endError && <Field.ErrorText>{endError}</Field.ErrorText>}
</Flex> </Flex>
</Flex>
</Field.Root>
</Flex> </Flex>
</Field.Root>
<Field.Root invalid={!!endError}>
<Flex direction="row" w="100%" h="100%" align="center" justify="center">
<Text w="50%" align="center" justify="center" mt={2} color="gray.700">
End Date
</Text>
<Flex direction="column" w="100%">
<Input
w="100%"
type="date"
value={currentSettings?.endDate || ""}
onChange={(e) => {
const value = e.target.value;
updateSettings(widgetId, {...currentSettings, endDate: value});
const dateSelected = new Date(value); <HStack>
const today = new Date(); <Text flexShrink="0" fontWeight="semibold">Tools</Text>
today.setHours(0, 0, 0, 0); <Separator flex="1" size="sm"/> {/*line seperator*/}
</HStack>
if (value && (dateSelected > today)) setEndError("Can't Choose a Future Date"); <Flex direction="column" w="100%" h="100%" justify="center" gap={2}>
else setEndError(""); {/* COPY TO CLIPBOARD */}
}} <Button css={popoverStyles.button} onClick={handleCopyToClipboard}>Copy Report</Button>
/> {/* DOWNLOAD REPORT */}
{endError && <Field.ErrorText>{endError}</Field.ErrorText>} <Button css={popoverStyles.button} onClick={handleDownloadReport}>Download Report</Button>
</Flex>
</Flex> </Flex>
</Field.Root> </Fieldset.Content>
</Flex> </Fieldset.Root>
</Flex> </Flex>
) )
} }
@@ -1,12 +1,30 @@
import {Button, Field, Flex, Input, Switch, Text} from "@chakra-ui/react"; import {
Button,
Field,
Flex,
Input,
Switch,
Text,
Fieldset,
CheckboxCard,
Separator,
HStack,
useSlotRecipe
} from "@chakra-ui/react";
import {useDashboardStore} from "../../../store/useDashboardStore.jsx"; import {useDashboardStore} from "../../../store/useDashboardStore.jsx";
import {useState} from "react"; import {useState} from "react";
export default function IvansAlertsSettings({ widgetId, currentSettings }) { export default function IvansAlertsSettings({ widgetId, currentSettings }) {
// STYLING
const popoverRecipe = useSlotRecipe({ key: "glassPopover" });
const popoverStyles = popoverRecipe();
// STATE MANAGEMENT
const updateSettings = useDashboardStore(state => state.updateWidgetSettings); const updateSettings = useDashboardStore(state => state.updateWidgetSettings);
const [startError, setStartError] = useState(null); const [startError, setStartError] = useState(null);
const [endError, setEndError] = useState(null); const [endError, setEndError] = useState(null);
// FUNCTIONS
const handleCopyToClipboard = () => { const handleCopyToClipboard = () => {
updateSettings(widgetId, {copyToClipboard: true}); updateSettings(widgetId, {copyToClipboard: true});
} }
@@ -20,89 +38,174 @@ export default function IvansAlertsSettings({ widgetId, currentSettings }) {
} }
return ( return (
<Flex flexDirection="column" justifyContent="space-between" gap={2} overflowY="auto"> <Flex flexDirection="column" justifyContent="space-between" overflowY="auto">
{/* COPY TO CLIPBOARD */} <Fieldset.Root>
<Button w="fit" size="xs" onClick={handleCopyToClipboard}>Copy All IVANS</Button> <Fieldset.Content>
{/* SELECTING DATE RANGE */}
{/* SORTS */} <HStack>
<Switch.Root <Text flexShrink="0" fontWeight="semibold">Date Range Selection</Text>
checked={currentSettings?.activeSorts?.timeSort === "asc"} <Separator flex="1" size="sm"/> {/*line seperator*/}
onCheckedChange={handleTimeSort} </HStack>
colorPalette="blue" <Flex direction="column" w="100%" h="100%" justify="center" gap={2} pr={1}>
> {/* start date */}
<Switch.HiddenInput /> <Field.Root invalid={!!startError}>
<Switch.Control> <Flex
<Switch.Thumb /> direction="row"
</Switch.Control>
<Switch.Label>Sort Oldest to Newest</Switch.Label>
</Switch.Root>
<Switch.Root
checked={currentSettings?.activeSorts?.volcanoSort}
onCheckedChange={handleVolcanoSort}
colorPalette="blue"
>
<Switch.HiddenInput />
<Switch.Control>
<Switch.Thumb />
</Switch.Control>
<Switch.Label>Sort by Volcano</Switch.Label>
</Switch.Root>
{/* SELECTING DATE RANGE */}
<Flex direction="column" w="100%" h="100%" align="center" justify="center" gap="2">
<Field.Root invalid={!!startError}>
<Flex direction="row" w="100%" h="100%" align="center" justify="center">
<Text w="50%" align="center" justify="center" mt={2} color="gray.700">
Start Date
</Text>
<Flex direction="column" w="100%">
<Input
w="100%" w="100%"
type="date" h="100%"
value={currentSettings?.startDate || ""} align="center"
onChange={(e) => { justify="center"
const value = e.target.value; >
updateSettings(widgetId, {...currentSettings, startDate: value}); <Text w="50%" align="center" justify="center" color="text">
Start Date
</Text>
<Flex direction="column" w="100%">
<Input
type="date"
w="100%"
borderRadius="lg"
css={popoverStyles.date}
value={currentSettings?.startDate || ""}
onChange={(e) => {
const value = e.target.value;
updateSettings(widgetId, {
...currentSettings,
startDate: value,
});
const dateSelected = new Date(value); const dateSelected = new Date(value);
const today = new Date(); const today = new Date();
today.setHours(0, 0, 0, 0); today.setHours(0, 0, 0, 0);
if (value && (dateSelected > today)) setStartError("Can't Choose a Future Date"); if (value && dateSelected > today)
else setStartError(""); setStartError("Can't Choose a Future Date");
}} else setStartError("");
/> }}
{startError && <Field.ErrorText>{startError}</Field.ErrorText>} />
</Flex> {startError && (
</Flex> <Field.ErrorText>{startError}</Field.ErrorText>
</Field.Root> )}
<Field.Root invalid={!!endError}> </Flex>
<Flex direction="row" w="100%" h="100%" align="center" justify="center"> </Flex>
<Text w="50%" align="center" justify="center" mt={2} color="gray.700"> </Field.Root>
End Date
</Text> {/* end date */}
<Flex direction="column" w="100%"> <Field.Root invalid={!!endError}>
<Input <Flex
direction="row"
w="100%" w="100%"
type="date" h="100%"
value={currentSettings?.endDate || ""} align="center"
onChange={(e) => { justify="center"
const value = e.target.value; >
updateSettings(widgetId, {...currentSettings, endDate: value}); <Text w="50%" align="center" justify="center" color="text">
End Date
</Text>
<Flex direction="column" w="100%">
<Input
type="date"
w="100%"
borderRadius="lg"
css={popoverStyles.date}
value={currentSettings?.endDate || ""}
onChange={(e) => {
const value = e.target.value;
updateSettings(widgetId, {
...currentSettings,
endDate: value,
});
const dateSelected = new Date(value); const dateSelected = new Date(value);
const today = new Date(); const today = new Date();
today.setHours(0, 0, 0, 0); today.setHours(0, 0, 0, 0);
if (value && (dateSelected > today)) setEndError("Can't Choose a Future Date"); if (value && dateSelected > today)
else setEndError(""); setEndError("Can't Choose a Future Date");
}} else setEndError("");
/> }}
{endError && <Field.ErrorText>{endError}</Field.ErrorText>} />
</Flex> {endError && <Field.ErrorText>{endError}</Field.ErrorText>}
</Flex>
</Flex>
</Field.Root>
</Flex> </Flex>
</Field.Root>
</Flex> {/* SORTS */}
<HStack>
<Text flexShrink="0" fontWeight="semibold">Sorts</Text>
<Separator flex="1" size="sm"/> {/*line seperator*/}
</HStack>
{/* time sort */}
<Switch.Root
checked={currentSettings?.activeSorts?.timeSort === "asc"}
onCheckedChange={handleTimeSort}
>
<Switch.HiddenInput />
<Switch.Control css={popoverStyles.switch}>
<Switch.Thumb bg="white"/>
</Switch.Control>
<Switch.Label fontWeight={400}>Oldest to Newest</Switch.Label>
</Switch.Root>
{/* alphabetic sort */}
<Switch.Root
checked={currentSettings?.activeSorts?.volcanoSort}
onCheckedChange={handleVolcanoSort}
>
<Switch.HiddenInput />
<Switch.Control css={popoverStyles.switch}>
<Switch.Thumb bg="white" />
</Switch.Control>
<Switch.Label fontWeight={400}>Alphabetically by Volcano</Switch.Label>
</Switch.Root>
<HStack>
<Text flexShrink="0" fontWeight="semibold">Filters</Text>
<Separator flex="1" size="sm"/> {/*line seperator*/}
</HStack>
{/* elevated levels */}
<CheckboxCard.Root
// size="sm"
// variant="surface"
css={popoverStyles.checkboxcard}
defaultChecked={currentSettings?.filters?.elevatedLevels}
onCheckedChange={(e) => {
updateSettings(widgetId, {
...currentSettings,
filters: {
...currentSettings.filters,
elevatedLevels: e.checked,
},
});
}}
>
<CheckboxCard.HiddenInput />
<CheckboxCard.Control>
<CheckboxCard.Content>
<CheckboxCard.Label fontWeight={400}>
Only Elevated Levels
</CheckboxCard.Label>
</CheckboxCard.Content>
<CheckboxCard.Indicator css={popoverStyles.checkbox} />
</CheckboxCard.Control>
</CheckboxCard.Root>
{/* COPY TO CLIPBOARD */}
<HStack>
<Text flexShrink="0" fontWeight="semibold">Tools</Text>
<Separator flex="1" size="sm"/> {/*line seperator*/}
</HStack>
<Button mt={-2} w="100%" size="xs" borderRadius="lg" variant="subtle"
css={popoverStyles.button}
onClick={handleCopyToClipboard}
>
Copy All IVANS
</Button>
</Fieldset.Content>
</Fieldset.Root>
</Flex> </Flex>
) )
} }
@@ -1,12 +1,18 @@
import {Button, Field, Flex, Input, Switch, Text} from "@chakra-ui/react"; import {Button, Field, Flex, HStack, Input, Separator, Switch, Text, useSlotRecipe, Fieldset} from "@chakra-ui/react";
import {useDashboardStore} from "@/store/useDashboardStore.jsx"; import {useDashboardStore} from "@/store/useDashboardStore.jsx";
import {useState} from "react"; import {useState} from "react";
export default function RemoteSensingSettings({ widgetId, currentSettings }) { export default function RemoteSensingSettings({ widgetId, currentSettings }) {
// STYLING
const popoverRecipe = useSlotRecipe({ key: "glassPopover" });
const popoverStyles = popoverRecipe();
// STATE MANAGEMENT
const updateSettings = useDashboardStore(state => state.updateWidgetSettings); const updateSettings = useDashboardStore(state => state.updateWidgetSettings);
const [startError, setStartError] = useState(null); const [startError, setStartError] = useState(null);
const [endError, setEndError] = useState(null); const [endError, setEndError] = useState(null);
// FUNCTIONS
const handleCopyToClipboard = () => { const handleCopyToClipboard = () => {
updateSettings(widgetId, {copyToClipboard: true}); updateSettings(widgetId, {copyToClipboard: true});
} }
@@ -21,88 +27,111 @@ export default function RemoteSensingSettings({ widgetId, currentSettings }) {
return ( return (
<Flex flexDirection="column" justifyContent="space-between" gap={2} overflowY="auto"> <Flex flexDirection="column" justifyContent="space-between" gap={2} overflowY="auto">
{/* COPYING TO CLIPBOARD */} <Fieldset.Root>
<Button w="fit" size="xs" onClick={handleCopyToClipboard}>Copy All RS</Button> <Fieldset.Content>
{/* SELECTING DATE RANGE */}
<HStack>
<Text flexShrink="0" fontWeight="semibold">Date Range Selection</Text>
<Separator flex="1" size="sm"/> {/*line seperator*/}
</HStack>
<Flex direction="column" w="100%" h="100%" justify="center" gap={2} pr={1}>
{/* start date */}
<Field.Root invalid={!!startError}>
<Flex direction="row" w="100%" h="100%" align="center" justify="center">
<Text w="50%" align="center" justify="center" mt={2} color="text">
Start Date
</Text>
<Flex direction="column" w="100%">
<Input
type="date"
css={popoverStyles.date}
{/* SORTING */} value={currentSettings?.startDate || ""}
<Switch.Root onChange={(e) => {
checked={currentSettings?.activeSorts?.timeSort === "asc"} const value = e.target.value;
onCheckedChange={handleTimeSort} updateSettings(widgetId, {...currentSettings, startDate: value});
colorPalette="blue"
>
<Switch.HiddenInput />
<Switch.Control>
<Switch.Thumb />
</Switch.Control>
<Switch.Label>Sort Oldest to Newest</Switch.Label>
</Switch.Root>
<Switch.Root
checked={currentSettings?.activeSorts?.volcanoSort}
onCheckedChange={handleVolcanoSort}
colorPalette="blue"
>
<Switch.HiddenInput />
<Switch.Control>
<Switch.Thumb />
</Switch.Control>
<Switch.Label>Sort by Volcano</Switch.Label>
</Switch.Root>
{/* SELECTING DATE RANGE */} const dateSelected = new Date(value);
<Flex direction="column" w="100%" h="100%" align="center" justify="center" gap="2"> const today = new Date();
<Field.Root invalid={!!startError}> today.setHours(0, 0, 0, 0);
<Flex direction="row" w="100%" h="100%" align="center" justify="center">
<Text w="50%" align="center" justify="center" mt={2} color="gray.700">
Start Date
</Text>
<Flex direction="column" w="100%">
<Input
w="100%"
type="date"
value={currentSettings?.startDate || ""}
onChange={(e) => {
const value = e.target.value;
updateSettings(widgetId, {...currentSettings, startDate: value});
const dateSelected = new Date(value); if (value && (dateSelected > today)) setStartError("Can't Choose a Future Date");
const today = new Date(); else setStartError("");
today.setHours(0, 0, 0, 0); }}
/>
{startError && <Field.ErrorText>{startError}</Field.ErrorText>}
</Flex>
</Flex>
</Field.Root>
if (value && (dateSelected > today)) setStartError("Can't Choose a Future Date"); {/* end date */}
else setStartError(""); <Field.Root invalid={!!endError}>
}} <Flex direction="row" w="100%" h="100%" align="center" justify="center">
/> <Text w="50%" align="center" justify="center" mt={2} color="text">
{startError && <Field.ErrorText>{startError}</Field.ErrorText>} End Date
</Flex> </Text>
<Flex direction="column" w="100%">
<Input
type="date"
css={popoverStyles.date}
value={currentSettings?.endDate || ""}
onChange={(e) => {
const value = e.target.value;
updateSettings(widgetId, {...currentSettings, endDate: value});
const dateSelected = new Date(value);
const today = new Date();
today.setHours(0, 0, 0, 0);
if (value && (dateSelected > today)) setEndError("Can't Choose a Future Date");
else setEndError("");
}}
/>
{endError && <Field.ErrorText>{endError}</Field.ErrorText>}
</Flex>
</Flex>
</Field.Root>
</Flex> </Flex>
</Field.Root>
<Field.Root invalid={!!endError}>
<Flex direction="row" w="100%" h="100%" align="center" justify="center">
<Text w="50%" align="center" justify="center" mt={2} color="gray.700">
End Date
</Text>
<Flex direction="column" w="100%">
<Input
w="100%"
type="date"
value={currentSettings?.endDate || ""}
onChange={(e) => {
const value = e.target.value;
updateSettings(widgetId, {...currentSettings, endDate: value});
const dateSelected = new Date(value); {/* SORTING */}
const today = new Date(); <HStack>
today.setHours(0, 0, 0, 0); <Text flexShrink="0" fontWeight="semibold">Sorting</Text>
<Separator flex="1" size="sm"/> {/*line seperator*/}
</HStack>
<Switch.Root
checked={currentSettings?.activeSorts?.timeSort === "asc"}
onCheckedChange={handleTimeSort}
>
<Switch.HiddenInput />
<Switch.Control css={popoverStyles.switch}>
<Switch.Thumb bg="white" />
</Switch.Control>
<Switch.Label>Oldest to Newest</Switch.Label>
</Switch.Root>
if (value && (dateSelected > today)) setEndError("Can't Choose a Future Date"); <Switch.Root
else setEndError(""); checked={currentSettings?.activeSorts?.volcanoSort}
}} onCheckedChange={handleVolcanoSort}
/> colorPalette="blue"
{endError && <Field.ErrorText>{endError}</Field.ErrorText>} >
</Flex> <Switch.HiddenInput />
</Flex> <Switch.Control css={popoverStyles.switch}>
</Field.Root> <Switch.Thumb bg="white"/>
</Flex> </Switch.Control>
<Switch.Label>By Volcano</Switch.Label>
</Switch.Root>
{/* COPYING TO CLIPBOARD */}
<HStack>
<Text flexShrink="0" fontWeight="semibold">Tools</Text>
<Separator flex="1" size="sm"/> {/*line seperator*/}
</HStack>
<Button css={popoverStyles.button} onClick={handleCopyToClipboard}>
Copy All RS
</Button>
</Fieldset.Content>
</Fieldset.Root>
</Flex> </Flex>
) )
} }
+7 -3
View File
@@ -63,7 +63,6 @@ export const WIDGET_REGISTRY = {
}, },
allowedSizes: { allowedSizes: {
small: { w: 3, h: 6 }, small: { w: 3, h: 6 },
medium: { w: 3, h: 9 },
large: { w: 3, h: 12 }, large: { w: 3, h: 12 },
} }
}, },
@@ -85,7 +84,6 @@ export const WIDGET_REGISTRY = {
}, },
allowedSizes: { allowedSizes: {
small: { w: 3, h: 6 }, small: { w: 3, h: 6 },
medium: { w: 3, h: 9 },
large: { w: 3, h: 12 }, large: { w: 3, h: 12 },
} }
}, },
@@ -99,6 +97,12 @@ export const WIDGET_REGISTRY = {
defaultSettings: { defaultSettings: {
startDate: getStartDate(), startDate: getStartDate(),
endDate: getEndDate(), endDate: getEndDate(),
filters: {
levelIncreases: true,
levelDecreases: true,
elevatedLevels: false, // will filter to show only volcanoes in their top 2 highest alert levels
volcanoes: 'all', // filter on what volcanoes to display, defaults to 'all' instead of listing every volc
},
}, },
allowedSizes: { allowedSizes: {
small: { w: 3, h: 6 }, small: { w: 3, h: 6 },
@@ -180,7 +184,7 @@ export const WIDGET_REGISTRY = {
endDate: getEndDate(), endDate: getEndDate(),
}, },
allowedSizes: { allowedSizes: {
medium: { w: 3, h: 12 }, large: { w: 3, h: 12 },
} }
}, },
+4 -2
View File
@@ -65,7 +65,7 @@ const Login = () => {
return ( return (
<Flex p={8} bg="gray.50" _dark={{ bg: "gray.900" }} flex="1" minH="100vh" align="center" justify="center"> <Flex p={8} bg="gray.50" _dark={{ bg: "gray.900" }} flex="1" minH="100vh" align="center" justify="center">
<Box w="full" maxW="450px" bg="white" _dark={{ bg: "gray.800", borderColor: "gray.700" }} p={8} borderRadius="2xl" boxShadow="sm" borderWidth="1px" borderColor="gray.200"> <Box w="full" maxW="450px" bg="white" borderColor="gray.200" _dark={{ bg: "gray.800", borderColor: "gray.700" }} p={8} borderRadius="2xl" boxShadow="sm" borderWidth="1px">
<Stack gap={6} align="stretch" as="form" onSubmit={handleSubmit}> <Stack gap={6} align="stretch" as="form" onSubmit={handleSubmit}>
<Stack gap={2} textAlign="center"> <Stack gap={2} textAlign="center">
@@ -88,6 +88,7 @@ const Login = () => {
onChange={(e) => setUsername(e.target.value)} onChange={(e) => setUsername(e.target.value)}
placeholder="jdoe" placeholder="jdoe"
{...fieldStyles} {...fieldStyles}
color="gray.800"
/> />
</Field.Root> </Field.Root>
@@ -102,8 +103,9 @@ const Login = () => {
type="password" type="password"
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••" // placeholder="••••••••" // not a necessary placeholder
{...fieldStyles} {...fieldStyles}
color="gray.800"
/> />
</Field.Root> </Field.Root>
+117 -30
View File
@@ -2,7 +2,7 @@ import {defineSlotRecipe, createSystem, defaultConfig, defineConfig, defineRecip
// REUSABLE GLASS SURFACE // REUSABLE GLASS SURFACE
const glassSurface = { const glassSurface = {
bg: {base: "rgba(255, 255, 255, 0.4)", _dark: "rgba(255, 255, 255, 0.04)"}, bg: {base: "rgb(253 253 253 / 0.4)", _dark: "rgb(37 38 46 / 0.8)"},
backdropFilter: "blur(12px)", backdropFilter: "blur(12px)",
WebkitBackdropFilter: "blur(12px)", WebkitBackdropFilter: "blur(12px)",
border: "1px solid", border: "1px solid",
@@ -68,8 +68,8 @@ const sidebarRecipe = defineSlotRecipe({
root: { root: {
// 1. Semi-transparent backgrounds // 1. Semi-transparent backgrounds
bg: { bg: {
base: "linear-gradient(to bottom right, #97282880 20%, #EB7F3160 60%, #FCAD3840 100%)", base: "linear-gradient(to bottom right, #B63B1F88 20%, #FF990060 70%, #FFBF0040 90%)",
_dark: "linear-gradient(to bottom right, #9900003C 20%, #FF66003C 60%, #FFC0003C 100%)" _dark: "linear-gradient(to bottom right, #9900004C 20%, #FF66003C 60%, #FFAE003C 100%)"
}, },
// 2. The glass blur effect // 2. The glass blur effect
@@ -346,17 +346,103 @@ const widgetRecipe = defineSlotRecipe({
// 6. TRUE POPOVER: Compact floating card for 3-dots menus // 6. TRUE POPOVER: Compact floating card for 3-dots menus
const glassPopoverRecipe = defineSlotRecipe({ const glassPopoverRecipe = defineSlotRecipe({
slots: ["content", "arrow", "positioner"], slots: ["content", "arrow", "positioner", "date", "checkboxcard", "checkbox", "switch", "button"],
base: { base: {
content: { content: {
...glassSurface, ...glassSurface,
borderRadius: "xl", borderRadius: "1rem",
p: 3, p: 3,
boxShadow: "lg", boxShadow: "lg",
bg: {base: "rgba(255, 255, 255, 0.85)", _dark: "rgba(20, 22, 25, 0.85)"}, bg: {base: "rgb(246 246 246 / 0.95)", _dark: "#353742"},
backdropFilter: "blur(15px)", backdropFilter: "blur(15px)",
WebkitBackdropFilter: "blur(15px)", WebkitBackdropFilter: "blur(15px)",
}, },
date: {
color: "text",
bg: "widgetBadgeBg",
borderRadius: "lg",
h: "2rem",
_focus: {
borderStyle: "none",
}
},
checkboxcard: {
// applies to both dark & lt modes
w: "100%",
borderRadius: "lg",
color: "text",
"& div[data-part='control']": {
alignItems: "center",
padding: "6px",
},
bg: "widgetItemBg",
_checked: {
boxShadow: "none",
bg: "rgb(182 59 31 / 0.2)",
borderColor: "#B63B1F"
},
_dark: {
bg: "widgetBadgeBg",
_checked: {
boxShadow: "none",
color: "text",
bg: "#503D40",
borderColor: "#B55038"
}
}
},
checkbox: {
bg: "widgetInnerBadgeBg",
_checked: {
bg: "#B63B1F",
borderColor: "#B63B1F"
},
_dark: {
bg: "widgetBadgeBg",
_checked: {
color: "text",
bg: "#B55038",
borderColor: "#B55038"
}
}
},
switch: {
color: "text",
bg: "widgetBadgeBg",
_checked: {
bg: "#B63B1F",
_dark: {
bg: "#b55038"
}
},
},
button: {
w: "100%",
fontSize: "xs",
h: "2rem",
borderRadius: "lg",
color: "white",
bg: "#B63B1F",
_hover: {
color: "text",
bg: "#B63B1F32"
},
_dark: {
bg: "#b55038",
color: "text",
_hover: {
bg: "#503D40"
}
}
}
}, },
}); });
@@ -381,7 +467,7 @@ const glassDialogRecipe = defineSlotRecipe({
maxW: "60%", maxW: "60%",
minH: "80%", minH: "80%",
overflow: "hidden", overflow: "hidden",
bg: {base: "rgba(255, 255, 255, 0.75)", _dark: "rgba(20, 22, 25, 0.8)"}, bg: {base: "rgba(255, 255, 255, 0.75)", _dark: "rgb(22 23 28 / 0.9)"},
backdropFilter: "blur(15px)", backdropFilter: "blur(15px)",
WebkitBackdropFilter: "blur(15px)", WebkitBackdropFilter: "blur(15px)",
}, },
@@ -471,7 +557,7 @@ const glassTabRecipe = defineRecipe({
// --- THE GLASS EFFECT (Inactive) --- // --- THE GLASS EFFECT (Inactive) ---
// Uses a highly transparent white/black depending on mode // Uses a highly transparent white/black depending on mode
bg: {base: "rgba(255, 255, 255, 0.4)", _dark: "rgba(255, 255, 255, 0.03)"}, bg: {base: "rgba(255, 255, 255, 0.4)", _dark: "#1e2027"},
backdropFilter: "blur(12px)", backdropFilter: "blur(12px)",
WebkitBackdropFilter: "blur(12px)", WebkitBackdropFilter: "blur(12px)",
border: "1px solid", border: "1px solid",
@@ -485,7 +571,7 @@ const glassTabRecipe = defineRecipe({
pr: "16px", pr: "16px",
// --- THE GLASS EFFECT (Hover) --- // --- THE GLASS EFFECT (Hover) ---
// Becomes slightly more opaque when you mouse over // Becomes slightly more opaque when you mouse over
bg: {base: "rgba(255, 255, 255, 0.8)", _dark: "rgba(255, 255, 255, 0.1)"}, bg: {base: "rgba(255, 255, 255, 0.8)", _dark: "#191a22"},
borderColor: {base: "rgba(0, 0, 0, 0.1)", _dark: "rgba(255, 255, 255, 0.15)"}, borderColor: {base: "rgba(0, 0, 0, 0.1)", _dark: "rgba(255, 255, 255, 0.15)"},
color: "fg", color: "fg",
textDecoration: "none", textDecoration: "none",
@@ -530,22 +616,20 @@ export const rawWidgetColors = {
========================================================================== */ ========================================================================== */
const config = defineConfig({ const config = defineConfig({
globalCss: { globalCss: {
// the shadow box behind widgets when editing layout
".react-grid-placeholder": { ".react-grid-placeholder": {
background: "#c0d2e3ff !important", background: "#c0d2e3ff !important",
borderColor: "#a9b6c2ff !important", borderColor: "#a9b6c2ff !important",
boxShadow: "0 0 10px #c0d2e3ff !important", boxShadow: "0 0 10px #c0d2e3ff !important",
opacity: "1 !important", opacity: "1 !important",
borderRadius: "5px", borderRadius: "3xl",
_dark: { _dark: {
background: "#393c3fff !important", background: "#3B3D49FF !important",
borderColor: "#000000ff !important", borderColor: "#000000ff !important",
boxShadow: "0 0 10px #16181bff !important", boxShadow: "0 0 10px #16181bff !important",
opacity: "1 !important", opacity: "1 !important",
}, },
}, },
".react-resizable-handle-se": {
zIndex: "1",
},
"*": { "*": {
scrollbarWidth: "thin", scrollbarWidth: "thin",
scrollbarColor: "#a0aec0 transparent", scrollbarColor: "#a0aec0 transparent",
@@ -566,6 +650,7 @@ const config = defineConfig({
"::-webkit-scrollbar-track": { "::-webkit-scrollbar-track": {
background: "transparent", background: "transparent",
}, },
// this is what changes the maps colors to 'dark mode'
".leaflet-control-zoom-in, .leaflet-control-zoom-out, .leaflet-control-attribution, .leaflet-container": { ".leaflet-control-zoom-in, .leaflet-control-zoom-out, .leaflet-control-attribution, .leaflet-container": {
_dark: { _dark: {
filter: "invert(100%) hue-rotate(180deg) brightness(95%) contrast(90%)", filter: "invert(100%) hue-rotate(180deg) brightness(95%) contrast(90%)",
@@ -574,32 +659,34 @@ const config = defineConfig({
".leaflet-container": { ".leaflet-container": {
fontFamily: "system-ui, sans-serif !important", fontFamily: "system-ui, sans-serif !important",
}, },
".react-grid-item": { // removes the animation of moving widgets, seems very harsh and jumpy w/o animation
transition: "none !important" // ".react-grid-item": {
}, // transition: "none !important"
// },
}, },
theme: { theme: {
semanticTokens: { semanticTokens: {
colors: { colors: {
text: { value: { base: "#000000", _dark: "#FFFFFF" } }, text: { value: { base: "#131315", _dark: "rgb(234 234 234 / 0.94)" } },
textInverted: { value: { base: "#000000", _dark: "#FFFFFF" } }, // Fixed: Properly inverted textInverted: { value: { base: "#000000", _dark: "#FFFFFF" } }, // Fixed: Properly inverted
MainBackground: { value: { base: "#F9F9F9", _dark: "#141414" } }, MainBackground: { value: { base: "#f1f1f1", _dark: "#16171c" } },
titleColor: { value: { base: "gray.800", _dark: "#FFFFFF" } }, titleColor: { value: { base: "colors.gray.800", _dark: "#e1e1e1" } },
iconColor: { value: { base: "gray.600", _dark: "gray.300" } }, // Fixed: Visible in dark mode iconColor: { value: { base: "#191919", _dark: "rgb(255 255 255 / 0.82)" } }, // Fixed: Visible in dark mode
glassSeperator: { value: { base: "#0000001A", _dark: "#FFFFFF3F" } }, glassSeperator: { value: { base: "#0000001A", _dark: "#FFFFFF3F" } },
gooeyButtonBackground: {value: {base: "rgba(255, 255, 255, 0.1)", _dark: "rgba(255, 255, 255, 0.1)"}}, gooeyButtonBackground: {value: {base: "rgba(255, 255, 255, 0.1)", _dark: "#333540"}},
// WIDGET SPECIFIC VALUES // WIDGET SPECIFIC VALUES
widgetItemBg: { value: { base: "rgba(0, 0, 0, 0.04)", _dark: "rgba(255, 255, 255, 0.05)" } }, widgetItemBg: { value: { base: "rgb(115 132 166 / 0.17)", _dark: "rgb(51 53 64)" } },
widgetItemExpandedBg: { value: { base: "rgba(0, 0, 0, 0.08)", _dark: "rgba(0, 0, 0, 0.3)" } }, widgetItemExpandedBg: { value: { base: "rgb(184 197 220 / 0.17)", _dark: "rgb(59 61 73)" } },
widgetBadgeBg: { value: { base: "rgba(255, 255, 255, 0.6)", _dark: "rgba(0, 0, 0, 0.5)" } }, widgetBadgeBg: { value: { base: "rgb(154 163 179 / 0.45)", _dark: "rgb(75 77 90)" } },
widgetTextMuted: { value: { base: "gray.600", _dark: "gray.400" } }, widgetInnerBadgeBg: { value: { base: "rgb(224 226 232)", _dark: "rgb(100 103 119)" } },
widgetTextMuted: { value: { base: "#262627", _dark: "rgb(236 236 236 / 0.9)" } },
// ALERT COLORS // ALERT COLORS
alertRed: { value: { base: "red.600", _dark: "red.400" } }, alertRed: { value: { base: "colors.red.600", _dark: "colors.red.400" } },
alertOrange: { value: { base: "orange.600", _dark: "orange.400" } }, alertOrange: { value: { base: "colors.orange.600", _dark: "colors.orange.400" } },
alertYellow: { value: { base: "yellow.600", _dark: "yellow.400" } }, alertYellow: { value: { base: "colors.yellow.600", _dark: "colors.yellow.400" } },
alertGreen: { value: { base: "green.600", _dark: "green.400" } }, alertGreen: { value: { base: "colors.green.600", _dark: "colors.green.400" } },
}, },
}, },
tokens: { tokens: {