diff --git a/client/src/components/Widgets/AlertChanges.jsx b/client/src/components/Widgets/AlertChanges.jsx index d448b46..497e008 100644 --- a/client/src/components/Widgets/AlertChanges.jsx +++ b/client/src/components/Widgets/AlertChanges.jsx @@ -1,28 +1,51 @@ -import {useDashboardStore} from "@/store/useDashboardStore.jsx"; +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 { + 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 }) { + // 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}) { - // 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] - )); + // 1) BUILD URL, default is 7 days past + const url = useBuildUrl( + "/api/v2/alert_changes", + settings?.startDate, + settings?.endDate, + ); - // build url, default is 7 days past - const url = useBuildUrl('/api/v2/alert_changes', settings?.startDate, settings?.endDate); + // 2) CALL API hook to retrieve data + const { data: apiData, isLoading, error } = VdapApi(url); - // call api hook to retrieve data - const {data: apiData, isLoading, error} = VdapApi(url); - - const data = useMemo(() => { + // 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(() => { 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 + // 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) => { if (!accumulator[item.volcano]) accumulator[item.volcano] = []; accumulator[item.volcano].push(item); @@ -33,33 +56,97 @@ export default function AlertChanges({ settings, widgetID}) { 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)) - ] + 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), ); - // 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)); - + // 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 if (thisWidget?.size === "small") { - return (sorted_entries.map((volcano) => { + return sorted_entries.map((volcano) => { 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); return acc; }, {}); const domain_arr = Object.entries(domain); + console.log("sm data: ", [volcano[0], domain_arr]); return [volcano[0], domain_arr]; - })); + }); } + console.log("lg data: ", sorted_entries); return sorted_entries; }, [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 const alertColors = { Red: "alertRed", @@ -68,149 +155,283 @@ export default function AlertChanges({ settings, widgetID}) { Green: "alertGreen", }; - if (isLoading) return ( - - - - ); + if (isLoading) + return ( + + + + ); - if (error) return ( - - {error} - - ); + if (error) + return ( + + {error} + + ); - if (data.length === 0) return ( - - No flight level data available for the selected dates. - - ); + if (filteredData.length === 0) + return ( + + + No alert level data available for the selected dates. + + + ); return ( - // bg="transparent" to let the glass panel show through - - {data.map((volcano, index) => { - if (thisWidget?.size === "large") return ( - - - {(collapsible) => ( - <> - - + {filteredData.map((volcano, index) => { + if (thisWidget?.size === "large") + return ( + + + {(collapsible) => ( + <> + + + - - {volcano[0]} - {volcano[1][0]?.alert_start_date_local} + minW={0} + textAlign="left" + > + + {volcano[0]} + + + {volcano[1][0]?.alert_start_date_local} + + + + + {volcano[1][0]?.change_type === "increase" ? ( + + ) : ( + + )} + + + {volcano[1][0]?.alert_level_name} + + - - - {volcano[1][0]?.alert_level_name} - - - - + - - - {volcano[1]?.map((object, index) => ( - - - - {object?.alert_scheme_domain === "terrestrial" && } - {object?.alert_scheme_domain === "aviation" && } - {object?.alert_scheme_domain === "maritime" && } - - - {object?.change_type === "increase" ? : } - - - - - - {object?.alert_start_date_local} - - {object?.alert_level_name} - - - Alert Scale: - {(Array.from({ length: Number(object?.alert_scheme_highest) - Number(object?.alert_scheme_lowest) + 1}, (v, i) => Number(object?.alert_scheme_lowest) + i)) - .map((num) => ( + + + {volcano[1]?.map((object, index) => ( + + + + {object?.alert_scheme_domain === + "terrestrial" && } + {object?.alert_scheme_domain === "aviation" && ( + + )} + {object?.alert_scheme_domain === "maritime" && ( + + )} + + + {object?.change_type === "increase" ? ( + + ) : ( + + )} + + + + + + + {object?.alert_start_date_local} + + + {object?.alert_level_name} + + + + Alert Scale:{" "} + + {Array.from( + { + length: + Number(object?.alert_scheme_highest) - + Number(object?.alert_scheme_lowest) + + 1, + }, + (v, i) => + Number(object?.alert_scheme_lowest) + i, + ).map((num) => (
{num}
- )) - } + ))} +
-
-
-
-
- ))} -
-
- - )} -
-
- ) - else if (thisWidget?.size === "small") return ( - volcano[1]?.map((domain, index) => ( + + + + ))} + + + + )} + + + ); + else if (thisWidget?.size === "small") + return volcano[1]?.map((domain, index) => ( - + - {domain[0] === "aviation" && } - {domain[0] === "terrestrial" && } - {domain[0] === "maritime" && } + {domain[0] === "aviation" && } + {domain[0] === "terrestrial" && } + {domain[0] === "maritime" && } - {volcano[0] || ""} + + {volcano[0] || ""} + - {domain[1]?.[0]?.alert_start_date_local || ""} + + {domain[1]?.[0]?.alert_start_date_local || ""} + - + - {domain[1]?.[0]?.change_type === "increase" ? : } + {domain[1]?.[0]?.change_type === "increase" ? ( + + ) : ( + + )} {domain[1]?.[0]?.alert_level_name} - )) - ); + )); })}
- ) -} \ No newline at end of file + ); +} diff --git a/client/src/components/Widgets/settings/AlertChangeSettings.jsx b/client/src/components/Widgets/settings/AlertChangeSettings.jsx index b4b041f..9b4d4cc 100644 --- a/client/src/components/Widgets/settings/AlertChangeSettings.jsx +++ b/client/src/components/Widgets/settings/AlertChangeSettings.jsx @@ -1,38 +1,127 @@ -import {Flex, Slider, Switch} from "@chakra-ui/react"; +import {Flex, Slider, Switch, Text, Field, Fieldset, Input, CheckboxCard, CheckboxGroup} from "@chakra-ui/react"; import {useDashboardStore} from "@/store/useDashboardStore.jsx"; +import { useState } from "react"; export default function IvansAlertsSettings({ widgetId, currentSettings }) { const updateSettings = useDashboardStore(state => state.updateWidgetSettings); + const [startError, setStartError] = useState(null); + const [endError, setEndError] = useState(null); console.log("currentSettings:", currentSettings); return ( - - {/* SLIDER DATE RANGE SELECTION */} - { - console.log("change end", e); - const day = new Date(Date.now() - (e.value[0])*86400000).toLocaleDateString('en-CA'); - updateSettings(widgetId, {...currentSettings, startDate: day}); - }} - > - {currentSettings?.startDate} to {currentSettings?.endDate} - - - - - - - today - 1 week ago - 2 weeks ago - - - - + + + + {/* SELECTING DATE RANGE */} + + + Date Range Selection +
{/*line seperator*/} +
+ + {/* start date */} + + + Start Date + + { + 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 && {startError}} + + + + + {/* end date */} + + + End Date + + { + 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 && {endError}} + + + +
+ + {/* FILTER ON CHANGE TYPE */} + + + + Filters +
{/*line seperator*/} +
+ { + updateSettings(widgetId, {...currentSettings, filters: {...currentSettings.filters, levelIncreases: e.checked}}) + }} + > + {/* when removed can't check/uncheck box */} + + + Level Increases + + + + + + { + updateSettings(widgetId, {...currentSettings, filters: {...currentSettings.filters, levelDecreases: e.checked}}) + }} + > + + + + Level Decreases + + + + + + { + updateSettings(widgetId, {...currentSettings, filters: {...currentSettings.filters, elevatedLevels: e.checked}}) + }} + > + + + + Only Elevated Levels + + + + +
+
+
+
+
) } \ No newline at end of file diff --git a/client/src/constants/widgetRegistry.jsx b/client/src/constants/widgetRegistry.jsx index 93919fa..79a9c19 100644 --- a/client/src/constants/widgetRegistry.jsx +++ b/client/src/constants/widgetRegistry.jsx @@ -99,6 +99,11 @@ export const WIDGET_REGISTRY = { defaultSettings: { startDate: getStartDate(), endDate: getEndDate(), + filters: { + levelIncreases: true, + levelDecreases: true, + elevatedLevels: false, // will filter to show only volcanoes in their 2nd highest to highest al + }, }, allowedSizes: { small: { w: 3, h: 6 }, diff --git a/client/src/theme.js b/client/src/theme.js index 2c50ce3..f1ff888 100644 --- a/client/src/theme.js +++ b/client/src/theme.js @@ -353,7 +353,7 @@ const glassPopoverRecipe = defineSlotRecipe({ borderRadius: "xl", p: 3, boxShadow: "lg", - bg: {base: "rgba(255, 255, 255, 0.85)", _dark: "rgba(20, 22, 25, 0.85)"}, + bg: {base: "rgba(255, 255, 255, 0.95)", _dark: "rgba(20, 22, 25, 0.95)"}, backdropFilter: "blur(15px)", WebkitBackdropFilter: "blur(15px)", },