2026-08-18 21:20:49 +00:00
|
|
|
import {useDashboardStore} from "@/store/useDashboardStore.jsx";
|
|
|
|
|
import VdapApi from "../../hooks/useVdapApi.js";
|
|
|
|
|
import useBuildUrl from "../../hooks/useBuildUrl.js";
|
|
|
|
|
import { 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";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export default function AlertChanges({ settings, widgetID}) {
|
|
|
|
|
// Find 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]
|
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
// build url, default is 7 days past
|
|
|
|
|
const url = useBuildUrl('/api/v2/alert_changes', settings?.startDate, settings?.endDate);
|
|
|
|
|
|
|
|
|
|
// call api hook to retrieve data
|
|
|
|
|
const {data: apiData, isLoading, error} = VdapApi(url);
|
|
|
|
|
|
|
|
|
|
const data = useMemo(() => {
|
|
|
|
|
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)
|
|
|
|
|
// here it returns a single massive object w/ volcano names as keys
|
|
|
|
|
const grouped = apiData.reduce((accumulator, item) => {
|
|
|
|
|
if (!accumulator[item.volcano]) accumulator[item.volcano] = [];
|
|
|
|
|
accumulator[item.volcano].push(item);
|
|
|
|
|
return accumulator;
|
|
|
|
|
}, {});
|
|
|
|
|
|
|
|
|
|
// converts the 'grouped' object back into an array of [key, value] pairs
|
|
|
|
|
const group_arr = Object.entries(grouped);
|
|
|
|
|
|
|
|
|
|
// sort data newest to oldest within each volcano
|
|
|
|
|
const sorted = group_arr.map((item) =>
|
|
|
|
|
[
|
|
|
|
|
item[0], // volcano name
|
|
|
|
|
[...(item[1])].sort((a, b) =>
|
|
|
|
|
new Date(b.alert_start_date_local) - new Date(a.alert_start_date_local))
|
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// volcano with most recent alert level change appears at top of list
|
|
|
|
|
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));
|
|
|
|
|
|
|
|
|
|
if (thisWidget?.size === "small") {
|
|
|
|
|
return (sorted_entries.map((volcano) => {
|
|
|
|
|
const domain = volcano[1].reduce((acc, curr) => {
|
|
|
|
|
if (!acc[curr.alert_scheme_domain]) acc[curr.alert_scheme_domain] = [];
|
|
|
|
|
acc[curr.alert_scheme_domain].push(curr);
|
|
|
|
|
return acc;
|
|
|
|
|
}, {});
|
|
|
|
|
|
|
|
|
|
const domain_arr = Object.entries(domain);
|
|
|
|
|
return [volcano[0], domain_arr];
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return sorted_entries;
|
|
|
|
|
}, [apiData, thisWidget]);
|
|
|
|
|
|
2026-10-20 14:05:52 -07:00
|
|
|
// Alert colors are now light/dark mode responsive
|
2026-08-18 21:20:49 +00:00
|
|
|
const alertColors = {
|
2026-10-23 11:23:16 -07:00
|
|
|
Red: "Red",
|
|
|
|
|
Orange: "Orange",
|
|
|
|
|
Yellow: "Yellow",
|
|
|
|
|
Green: "Green",
|
2026-08-18 21:20:49 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (isLoading) return (
|
2026-10-20 14:05:52 -07:00
|
|
|
<Flex w="100%" h="100%" align="center" justify="center" bg="transparent">
|
2026-08-18 21:20:49 +00:00
|
|
|
<Spinner size="xl" color="blue.400" thickness="4px"/>
|
|
|
|
|
</Flex>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (error) return (
|
2026-10-20 14:05:52 -07:00
|
|
|
<Flex w="100%" h="100%" align="center" justify="center" bg="transparent">
|
2026-08-18 21:20:49 +00:00
|
|
|
<Text color="red.400">{error}</Text>
|
|
|
|
|
</Flex>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (data.length === 0) return (
|
2026-10-20 14:05:52 -07:00
|
|
|
<Flex w="100%" h="100%" align="center" justify="center" bg="transparent">
|
|
|
|
|
<Text textAlign="center" color="widgetTextMuted">No flight level data available for the selected dates.</Text>
|
2026-08-18 21:20:49 +00:00
|
|
|
</Flex>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return (
|
2026-10-20 14:05:52 -07:00
|
|
|
// bg="transparent" to let the glass panel show through
|
|
|
|
|
<Box display="flex" flexDirection="column" gap={2} p={2} w="100%" h="100%" bg="transparent" overflowY="auto">
|
2026-08-18 21:20:49 +00:00
|
|
|
{data.map((volcano, index) => {
|
|
|
|
|
if (thisWidget?.size === "large") return (
|
|
|
|
|
<Collapsible.Root key={index} w="100%">
|
|
|
|
|
<Collapsible.Context>
|
|
|
|
|
{(collapsible) => (
|
|
|
|
|
<>
|
|
|
|
|
<Collapsible.Trigger w="100%">
|
|
|
|
|
<Flex direction="row"
|
|
|
|
|
w="100%"
|
|
|
|
|
justify="space-between" align="center"
|
|
|
|
|
p={1} pl={3} pr={3}
|
|
|
|
|
borderTopRadius="lg"
|
|
|
|
|
borderBottomRadius={collapsible.open ? "0" : "lg"}
|
2026-10-20 14:05:52 -07:00
|
|
|
bg="widgetItemBg"
|
2026-08-18 21:20:49 +00:00
|
|
|
>
|
2026-10-20 14:05:52 -07:00
|
|
|
<Flex direction="column" w="100%" minW={0} textAlign="left">
|
|
|
|
|
<Text truncate color="text" fontWeight="medium">{volcano[0]}</Text>
|
|
|
|
|
<Text truncate fontSize="xs" color="widgetTextMuted">{volcano[1][0]?.alert_start_date_local}</Text>
|
2026-08-18 21:20:49 +00:00
|
|
|
</Flex>
|
|
|
|
|
<Flex w="100%" minW={0} justify="right">
|
2026-10-20 14:05:52 -07:00
|
|
|
<Text truncate pl={2} pr={2} borderRadius="lg" bg="widgetBadgeBg" color="text" fontWeight="semibold">
|
|
|
|
|
{volcano[1][0]?.alert_level_name}
|
|
|
|
|
</Text>
|
2026-08-18 21:20:49 +00:00
|
|
|
</Flex>
|
|
|
|
|
</Flex>
|
|
|
|
|
</Collapsible.Trigger>
|
2026-10-20 14:05:52 -07:00
|
|
|
|
|
|
|
|
<Collapsible.Content w="100%" p={3} borderBottomRadius="lg" bg="widgetItemExpandedBg">
|
2026-08-18 21:20:49 +00:00
|
|
|
<Timeline.Root size="md" variant="subtle">
|
|
|
|
|
{volcano[1]?.map((object, index) => (
|
|
|
|
|
<Timeline.Item key={index} w="100%" h="100%">
|
|
|
|
|
<Timeline.Connector >
|
2026-10-20 14:05:52 -07:00
|
|
|
<Timeline.Indicator outline="none" bg="widgetBadgeBg" color="iconColor" size="lg">
|
2026-08-18 21:20:49 +00:00
|
|
|
{object?.alert_scheme_domain === "terrestrial" && <Footprints size={15} />}
|
|
|
|
|
{object?.alert_scheme_domain === "aviation" && <Plane size={15} />}
|
|
|
|
|
{object?.alert_scheme_domain === "maritime" && <Sailboat size={15} />}
|
|
|
|
|
</Timeline.Indicator>
|
2026-10-23 11:23:16 -07:00
|
|
|
<Timeline.Indicator h="50%" outline="none" bg="transparent" color="iconColor" size="lg">
|
2026-08-18 21:20:49 +00:00
|
|
|
{object?.change_type === "increase" ? <TrendingUp /> : <TrendingDown />}
|
|
|
|
|
</Timeline.Indicator>
|
|
|
|
|
</Timeline.Connector>
|
|
|
|
|
<Timeline.Content w="100%" h="100%">
|
|
|
|
|
<Timeline.Description w="100%" h="100%" gap={1}>
|
|
|
|
|
<Flex direction="column" gap={1}>
|
2026-10-20 14:05:52 -07:00
|
|
|
<Text w="100%" h="100%" color="widgetTextMuted">{object?.alert_start_date_local}</Text>
|
2026-08-18 21:20:49 +00:00
|
|
|
<Text w="100%" h="100%"
|
2026-10-20 14:05:52 -07:00
|
|
|
color={alertColors[object?.alert_level_name] || "text"}
|
2026-08-18 21:20:49 +00:00
|
|
|
fontSize="md"
|
|
|
|
|
fontWeight="semibold"
|
|
|
|
|
>
|
|
|
|
|
{object?.alert_level_name}
|
|
|
|
|
</Text>
|
|
|
|
|
<Flex w="100%" h="20px" direction="row" justify="left" align="center">
|
2026-10-20 14:05:52 -07:00
|
|
|
<Text color="widgetTextMuted">Alert Scale: </Text>
|
2026-08-18 21:20:49 +00:00
|
|
|
{(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%"
|
2026-10-20 14:05:52 -07:00
|
|
|
color="text"
|
|
|
|
|
bg={num === Number(object?.alert_level_rank) ? "widgetBadgeBg" : "transparent"}
|
|
|
|
|
borderRadius={num === Number(object?.alert_level_rank) ? "xl" : "none"}
|
2026-08-18 21:20:49 +00:00
|
|
|
textAlign="center"
|
|
|
|
|
fontSize="sm"
|
2026-10-20 14:05:52 -07:00
|
|
|
fontWeight="bold"
|
2026-08-18 21:20:49 +00:00
|
|
|
>
|
|
|
|
|
{num}
|
|
|
|
|
</Center>
|
|
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
</Flex>
|
|
|
|
|
</Flex>
|
|
|
|
|
</Timeline.Description>
|
|
|
|
|
</Timeline.Content>
|
|
|
|
|
</Timeline.Item>
|
|
|
|
|
))}
|
|
|
|
|
</Timeline.Root>
|
|
|
|
|
</Collapsible.Content>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</Collapsible.Context>
|
|
|
|
|
</Collapsible.Root>
|
|
|
|
|
)
|
|
|
|
|
else if (thisWidget?.size === "small") return (
|
|
|
|
|
volcano[1]?.map((domain, index) => (
|
|
|
|
|
<Flex
|
|
|
|
|
key={index}
|
|
|
|
|
direction="row"
|
|
|
|
|
w="100%"
|
|
|
|
|
gap={2}
|
|
|
|
|
justify="space-between" align="center"
|
|
|
|
|
p={1} pl={3} pr={3}
|
|
|
|
|
borderRadius="lg"
|
2026-10-20 14:05:52 -07:00
|
|
|
bg="widgetItemBg"
|
2026-08-18 21:20:49 +00:00
|
|
|
>
|
2026-10-20 14:05:52 -07:00
|
|
|
<Flex direction="column" w="100%" minW={0} textAlign="left" color="text">
|
2026-08-18 21:20:49 +00:00
|
|
|
<Flex direction="row" w="100%" gap={2} align="center">
|
2026-10-20 14:05:52 -07:00
|
|
|
<Flex color="iconColor">
|
2026-08-18 21:20:49 +00:00
|
|
|
{domain[0] === "aviation" && <Plane size={15}/>}
|
|
|
|
|
{domain[0] === "terrestrial" && <Footprints size={15}/>}
|
|
|
|
|
{domain[0] === "maritime" && <Sailboat size={15}/>}
|
|
|
|
|
</Flex>
|
2026-10-20 14:05:52 -07:00
|
|
|
<Text truncate fontWeight="medium">{volcano[0] || ""}</Text>
|
2026-08-18 21:20:49 +00:00
|
|
|
</Flex>
|
2026-10-20 14:05:52 -07:00
|
|
|
<Text truncate w="100%" fontSize="xs" color="widgetTextMuted">{domain[1]?.[0]?.alert_start_date_local || ""}</Text>
|
2026-08-18 21:20:49 +00:00
|
|
|
</Flex>
|
|
|
|
|
|
2026-10-20 14:05:52 -07:00
|
|
|
<Flex direction="row" w="100%" minW={0} justify="right" gap={2} color="text">
|
|
|
|
|
<Flex w="25px" justify="right" color="iconColor">
|
2026-08-18 21:20:49 +00:00
|
|
|
{domain[1]?.[0]?.change_type === "increase" ? <TrendingUp/> : <TrendingDown/>}
|
|
|
|
|
</Flex>
|
|
|
|
|
<Text
|
|
|
|
|
truncate
|
|
|
|
|
pl={2} pr={2}
|
|
|
|
|
borderRadius="lg"
|
2026-10-20 14:05:52 -07:00
|
|
|
bg="widgetBadgeBg"
|
|
|
|
|
color={alertColors[domain[1]?.[0]?.alert_level_name] || "text"}
|
2026-08-18 21:20:49 +00:00
|
|
|
fontWeight="semibold"
|
|
|
|
|
>
|
|
|
|
|
{domain[1]?.[0]?.alert_level_name}
|
|
|
|
|
</Text>
|
|
|
|
|
</Flex>
|
|
|
|
|
</Flex>
|
|
|
|
|
))
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</Box>
|
|
|
|
|
)
|
|
|
|
|
}
|