2026-11-05 10:43:05 -08:00
|
|
|
import { useDashboardStore } from "@/store/useDashboardStore.jsx";
|
2026-08-18 21:20:49 +00:00
|
|
|
import VdapApi from "../../hooks/useVdapApi.js";
|
|
|
|
|
import useBuildUrl from "../../hooks/useBuildUrl.js";
|
2026-11-05 10:43:05 -08:00
|
|
|
import {
|
|
|
|
|
Footprints,
|
|
|
|
|
Plane,
|
|
|
|
|
Sailboat,
|
|
|
|
|
TrendingUp,
|
|
|
|
|
TrendingDown,
|
|
|
|
|
} from "lucide-react";
|
|
|
|
|
import {
|
|
|
|
|
Box,
|
|
|
|
|
Collapsible,
|
|
|
|
|
Flex,
|
|
|
|
|
Spinner,
|
|
|
|
|
Text,
|
|
|
|
|
Timeline,
|
|
|
|
|
Center,
|
|
|
|
|
} from "@chakra-ui/react";
|
2026-08-18 21:20:49 +00:00
|
|
|
import { useMemo, useCallback } from "react";
|
|
|
|
|
|
2026-11-05 10:43:05 -08:00
|
|
|
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],
|
|
|
|
|
),
|
|
|
|
|
);
|
2026-08-18 21:20:49 +00:00
|
|
|
|
2026-11-05 10:43:05 -08:00
|
|
|
// 1) BUILD URL, default is 7 days past
|
|
|
|
|
const url = useBuildUrl(
|
|
|
|
|
"/api/v2/alert_changes",
|
|
|
|
|
settings?.startDate,
|
|
|
|
|
settings?.endDate,
|
|
|
|
|
);
|
2026-08-18 21:20:49 +00:00
|
|
|
|
2026-11-05 10:43:05 -08:00
|
|
|
// 2) CALL API hook to retrieve data
|
|
|
|
|
const { data: apiData, isLoading, error } = VdapApi(url);
|
2026-08-18 21:20:49 +00:00
|
|
|
|
2026-11-05 10:43:05 -08:00
|
|
|
// 3) SORT the data by grouping into volcanoes and sorting form newest to oldest
|
|
|
|
|
// 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 sortedData = useMemo(() => {
|
2026-08-18 21:20:49 +00:00
|
|
|
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)
|
2026-11-05 10:43:05 -08:00
|
|
|
// here we sort the data by volcano, and return it as a single massive object w/ volcano names as keys
|
2026-08-18 21:20:49 +00:00
|
|
|
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
|
2026-11-05 10:43:05 -08:00
|
|
|
const sorted = group_arr.map((item) => [
|
|
|
|
|
item[0], // volcano name
|
|
|
|
|
[...item[1]].sort( // item[1] is the array of al changes for a volcano
|
|
|
|
|
(a, b) =>
|
|
|
|
|
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),
|
2026-08-18 21:20:49 +00:00
|
|
|
);
|
|
|
|
|
|
2026-11-05 10:43:05 -08:00
|
|
|
// additionally, seperates out data into domains within a volcano for sm widgets,
|
|
|
|
|
// to list all most recent al changes w/in a volcano w/in a domain
|
2026-08-18 21:20:49 +00:00
|
|
|
if (thisWidget?.size === "small") {
|
2026-11-05 10:43:05 -08:00
|
|
|
return sorted_entries.map((volcano) => {
|
2026-08-18 21:20:49 +00:00
|
|
|
const domain = volcano[1].reduce((acc, curr) => {
|
2026-11-05 10:43:05 -08:00
|
|
|
if (!acc[curr.alert_scheme_domain])
|
|
|
|
|
acc[curr.alert_scheme_domain] = [];
|
2026-08-18 21:20:49 +00:00
|
|
|
acc[curr.alert_scheme_domain].push(curr);
|
|
|
|
|
return acc;
|
|
|
|
|
}, {});
|
|
|
|
|
|
|
|
|
|
const domain_arr = Object.entries(domain);
|
2026-11-05 10:43:05 -08:00
|
|
|
console.log("sm data: ", [volcano[0], domain_arr]);
|
2026-08-18 21:20:49 +00:00
|
|
|
return [volcano[0], domain_arr];
|
2026-11-05 10:43:05 -08:00
|
|
|
});
|
2026-08-18 21:20:49 +00:00
|
|
|
}
|
|
|
|
|
|
2026-11-05 10:43:05 -08:00
|
|
|
console.log("lg data: ", sorted_entries);
|
2026-08-18 21:20:49 +00:00
|
|
|
return sorted_entries;
|
|
|
|
|
}, [apiData, thisWidget]);
|
|
|
|
|
|
2026-11-05 10:43:05 -08:00
|
|
|
// 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]);
|
|
|
|
|
|
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-11-03 13:27:47 -08:00
|
|
|
Red: "alertRed",
|
|
|
|
|
Orange: "alertOrange",
|
|
|
|
|
Yellow: "alertYellow",
|
|
|
|
|
Green: "alertGreen",
|
2026-08-18 21:20:49 +00:00
|
|
|
};
|
|
|
|
|
|
2026-11-05 15:39:27 -08:00
|
|
|
const options = {
|
|
|
|
|
year: "numeric",
|
|
|
|
|
month: "short",
|
|
|
|
|
day: "numeric",
|
|
|
|
|
hour: "numeric",
|
|
|
|
|
minute: "2-digit",
|
|
|
|
|
second: "2-digit",
|
|
|
|
|
timeZoneName: "short"
|
|
|
|
|
};
|
|
|
|
|
|
2026-11-05 10:43:05 -08:00
|
|
|
if (isLoading)
|
|
|
|
|
return (
|
|
|
|
|
<Flex w="100%" h="100%" align="center" justify="center" bg="transparent">
|
|
|
|
|
<Spinner size="xl" color="blue.400" thickness="4px" />
|
|
|
|
|
</Flex>
|
|
|
|
|
);
|
2026-08-18 21:20:49 +00:00
|
|
|
|
2026-11-05 10:43:05 -08:00
|
|
|
if (error)
|
|
|
|
|
return (
|
|
|
|
|
<Flex w="100%" h="100%" align="center" justify="center" bg="transparent">
|
|
|
|
|
<Text color="red.400">{error}</Text>
|
|
|
|
|
</Flex>
|
|
|
|
|
);
|
2026-08-18 21:20:49 +00:00
|
|
|
|
2026-11-05 10:43:05 -08:00
|
|
|
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>
|
|
|
|
|
);
|
2026-08-18 21:20:49 +00:00
|
|
|
|
|
|
|
|
return (
|
2026-11-05 10:43:05 -08:00
|
|
|
<Box
|
|
|
|
|
display="flex"
|
|
|
|
|
flexDirection="column"
|
|
|
|
|
gap={2}
|
|
|
|
|
p={2}
|
|
|
|
|
w="100%"
|
|
|
|
|
h="100%"
|
|
|
|
|
bg="transparent"
|
|
|
|
|
overflowY="auto"
|
|
|
|
|
>
|
|
|
|
|
{filteredData.map((volcano, index) => {
|
|
|
|
|
if (thisWidget?.size === "large")
|
|
|
|
|
return (
|
|
|
|
|
<Collapsible.Root key={index} w="100%">
|
|
|
|
|
<Collapsible.Context>
|
|
|
|
|
{(collapsible) => (
|
2026-11-05 15:39:27 -08:00
|
|
|
<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"}
|
|
|
|
|
bg="widgetItemBg"
|
|
|
|
|
>
|
2026-11-05 10:43:05 -08:00
|
|
|
<Flex
|
2026-11-05 15:39:27 -08:00
|
|
|
direction="column"
|
2026-11-05 10:43:05 -08:00
|
|
|
w="100%"
|
2026-11-05 15:39:27 -08:00
|
|
|
minW={0}
|
|
|
|
|
textAlign="left"
|
2026-11-05 10:43:05 -08:00
|
|
|
>
|
2026-11-05 15:39:27 -08:00
|
|
|
<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 w="100%" minW={0} justify="right" gap={2}>
|
|
|
|
|
<Text
|
|
|
|
|
truncate
|
|
|
|
|
pl={2}
|
|
|
|
|
pr={2}
|
|
|
|
|
borderRadius="lg"
|
|
|
|
|
bg="widgetBadgeBg"
|
|
|
|
|
color="text"
|
|
|
|
|
fontWeight="semibold"
|
2026-11-05 10:43:05 -08:00
|
|
|
>
|
2026-11-05 15:39:27 -08:00
|
|
|
{volcano[1][0]?.alert_level_name}
|
|
|
|
|
</Text>
|
|
|
|
|
<Flex w="fit">
|
|
|
|
|
{volcano[1][0]?.change_type === "increase" ? (
|
|
|
|
|
<TrendingUp />
|
|
|
|
|
) : (
|
|
|
|
|
<TrendingDown />
|
|
|
|
|
)}
|
2026-11-05 10:43:05 -08:00
|
|
|
</Flex>
|
2026-08-18 21:20:49 +00:00
|
|
|
</Flex>
|
2026-11-05 15:39:27 -08:00
|
|
|
</Flex>
|
|
|
|
|
</Collapsible.Trigger>
|
2026-11-05 10:43:05 -08:00
|
|
|
)}
|
|
|
|
|
</Collapsible.Context>
|
2026-11-05 15:39:27 -08:00
|
|
|
|
|
|
|
|
<Collapsible.Content
|
|
|
|
|
w="100%"
|
|
|
|
|
p={3}
|
|
|
|
|
borderBottomRadius="lg"
|
|
|
|
|
bg="widgetItemExpandedBg"
|
|
|
|
|
>
|
|
|
|
|
<Timeline.Root size="md" variant="subtle">
|
|
|
|
|
{volcano[1]?.map((object, index) => (
|
|
|
|
|
<Timeline.Item key={index} w="100%" h="100%">
|
|
|
|
|
<Timeline.Connector>
|
|
|
|
|
<Timeline.Indicator
|
|
|
|
|
outline="none"
|
|
|
|
|
bg="widgetBadgeBg"
|
|
|
|
|
color="iconColor"
|
|
|
|
|
size="lg"
|
|
|
|
|
>
|
|
|
|
|
{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>
|
|
|
|
|
<Timeline.Indicator
|
|
|
|
|
h="50%"
|
|
|
|
|
outline="none"
|
|
|
|
|
bg="transparent"
|
|
|
|
|
color="iconColor"
|
|
|
|
|
size="lg"
|
|
|
|
|
>
|
|
|
|
|
{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}>
|
|
|
|
|
<Text
|
|
|
|
|
w="100%"
|
|
|
|
|
h="100%"
|
|
|
|
|
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">
|
|
|
|
|
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)
|
|
|
|
|
? "widgetBadgeBg"
|
|
|
|
|
: "transparent"
|
|
|
|
|
}
|
|
|
|
|
borderRadius={
|
|
|
|
|
num ===
|
|
|
|
|
Number(object?.alert_level_rank)
|
|
|
|
|
? "xl"
|
|
|
|
|
: "none"
|
|
|
|
|
}
|
|
|
|
|
textAlign="center"
|
|
|
|
|
fontSize="sm"
|
|
|
|
|
fontWeight="bold"
|
|
|
|
|
>
|
|
|
|
|
{num}
|
|
|
|
|
</Center>
|
|
|
|
|
))}
|
|
|
|
|
</Flex>
|
|
|
|
|
</Flex>
|
|
|
|
|
</Timeline.Description>
|
|
|
|
|
</Timeline.Content>
|
|
|
|
|
</Timeline.Item>
|
|
|
|
|
))}
|
|
|
|
|
</Timeline.Root>
|
|
|
|
|
</Collapsible.Content>
|
2026-11-05 10:43:05 -08:00
|
|
|
</Collapsible.Root>
|
|
|
|
|
);
|
|
|
|
|
else if (thisWidget?.size === "small")
|
|
|
|
|
return volcano[1]?.map((domain, index) => (
|
2026-08-18 21:20:49 +00:00
|
|
|
<Flex
|
|
|
|
|
key={index}
|
|
|
|
|
direction="row"
|
|
|
|
|
w="100%"
|
|
|
|
|
gap={2}
|
2026-11-05 10:43:05 -08:00
|
|
|
justify="space-between"
|
|
|
|
|
align="center"
|
|
|
|
|
p={1}
|
|
|
|
|
pl={3}
|
|
|
|
|
pr={3}
|
2026-08-18 21:20:49 +00:00
|
|
|
borderRadius="lg"
|
2026-10-20 14:05:52 -07:00
|
|
|
bg="widgetItemBg"
|
2026-08-18 21:20:49 +00:00
|
|
|
>
|
2026-11-05 10:43:05 -08: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-11-05 10:43:05 -08:00
|
|
|
{domain[0] === "aviation" && <Plane size={15} />}
|
|
|
|
|
{domain[0] === "terrestrial" && <Footprints size={15} />}
|
|
|
|
|
{domain[0] === "maritime" && <Sailboat size={15} />}
|
2026-08-18 21:20:49 +00:00
|
|
|
</Flex>
|
2026-11-05 10:43:05 -08:00
|
|
|
<Text truncate fontWeight="medium">
|
|
|
|
|
{volcano[0] || ""}
|
|
|
|
|
</Text>
|
2026-08-18 21:20:49 +00:00
|
|
|
</Flex>
|
2026-11-05 10:43:05 -08: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-11-05 10:43:05 -08:00
|
|
|
<Flex
|
|
|
|
|
direction="row"
|
|
|
|
|
w="100%"
|
|
|
|
|
minW={0}
|
|
|
|
|
justify="right"
|
|
|
|
|
gap={2}
|
|
|
|
|
color="text"
|
|
|
|
|
>
|
2026-10-20 14:05:52 -07:00
|
|
|
<Flex w="25px" justify="right" color="iconColor">
|
2026-11-05 10:43:05 -08:00
|
|
|
{domain[1]?.[0]?.change_type === "increase" ? (
|
|
|
|
|
<TrendingUp />
|
|
|
|
|
) : (
|
|
|
|
|
<TrendingDown />
|
|
|
|
|
)}
|
2026-08-18 21:20:49 +00:00
|
|
|
</Flex>
|
|
|
|
|
<Text
|
|
|
|
|
truncate
|
2026-11-05 10:43:05 -08:00
|
|
|
pl={2}
|
|
|
|
|
pr={2}
|
2026-08-18 21:20:49 +00:00
|
|
|
borderRadius="lg"
|
2026-10-20 14:05:52 -07:00
|
|
|
bg="widgetBadgeBg"
|
2026-11-05 10:43:05 -08:00
|
|
|
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>
|
2026-11-05 10:43:05 -08:00
|
|
|
));
|
2026-08-18 21:20:49 +00:00
|
|
|
})}
|
|
|
|
|
</Box>
|
2026-11-05 10:43:05 -08:00
|
|
|
);
|
|
|
|
|
}
|