2026-07-22 16:29:17 -07:00
import VdapApi from '../../hooks/useVdapApi.js' ;
2026-08-05 17:16:45 +00:00
import { Button , Dialog , Flex , Portal , Spinner , Text } from "@chakra-ui/react" ;
import { useEffect , useMemo , useState } from 'react' ;
2026-07-30 09:18:47 -07:00
import { useDashboardStore } from "@/store/useDashboardStore.jsx" ;
2026-07-22 16:29:17 -07:00
2026-07-30 09:18:47 -07:00
export default function IvansAlerts ({ settings , widgetID }) {
2026-08-05 17:16:45 +00:00
console . log ( "IvansAlerts widget ID: " , widgetID );
2026-07-30 09:18:47 -07:00
const [ activeSummaryItem , setActiveSummaryItem ] = useState ( false );
const updateSettings = useDashboardStore ( state => state . updateWidgetSettings );
2026-08-05 17:16:45 +00:00
const isCopy = settings ? . copyToClipboard === true ; // acts as a fail-safe if no default setting is set
2026-07-30 09:18:47 -07:00
const sorts = settings ? . activeSorts ;
2026-07-22 16:29:17 -07:00
2026-07-30 09:18:47 -07:00
// build url once on mount
const seven_day_url = useMemo (() => {
2026-08-05 17:16:45 +00:00
const base_url = '/api/v2/comm_coor_ivans' ;
2026-07-30 09:18:47 -07:00
let today = new Date ();
let sevenDaysAgo = new Date ( today );
sevenDaysAgo . setDate ( sevenDaysAgo . getDate () - 7 );
sevenDaysAgo = sevenDaysAgo . toLocaleDateString ( 'en-CA' );
today = today . toLocaleDateString ( 'en-CA' );
return base_url + '?start_date=' + sevenDaysAgo + '&end_date=' + today ;
}, []);
2026-08-05 17:16:45 +00:00
// VdapApi hook only calls api if the url changes and apiData remains stable since vdapApi uses state to track the data
2026-07-30 09:18:47 -07:00
const { data : apiD , isLoading , error } = VdapApi ( seven_day_url );
2026-08-05 17:16:45 +00:00
const apiData = useMemo (() => {
return ([
{
id : 277 ,
volcano_number : 268010 ,
report_num : "2026-01" ,
report_date : "2026-05-08T22:05:44Z" ,
preparer : "hwright" ,
reviewer : "dramsey" ,
send_time : "2026-05-08T22:05:52Z" ,
volcano_name : "Dukono" ,
summary : "At 07:41 local time on 8 May 2026, an explosion of persistently active Dukono volcano occurred, producing an ash plume that rose over 15 km (52,000 feet) above sea level and ejecting ballistic blocks of lava beyond the crater rim. The eruption killed three tourists who were hiking within the official exclusion zone; other hikers were rescued and returned to safety." ,
files : [
{
name : "277-268010-Dukono-2026-01.pdf" ,
url : "https://vdap.org/ivans/sitrep/277-268010-Dukono-2026-01.pdf" ,
type : "pdf"
}
]
},
{
id : 279 ,
volcano_number : 250030 ,
report_num : "2026-01" ,
report_date : "2026-05-15T05:11:53Z" ,
preparer : "charpel" ,
reviewer : "jlwnstrn" ,
send_time : "2026-05-15T05:12:01Z" ,
volcano_name : "Central Bismarck Sea" ,
summary : "A M5.4 earthquake occurred on 14 May 2026 at 17:13 (UTC) during the ongoing eruption of the submarine Central Bismarck Sea volcano that began 8 May 2026. The volcano's current activity does not threaten any populated areas, but today’ s earthquake indicates that eruptive activity will continue or increase." ,
files : [
{
name : "279-250030-Central Bismarck Sea-2026-01.pdf" ,
url : "https://vdap.org/ivans/sitrep/279-250030-Central Bismarck Sea-2026-01.pdf" ,
type : "pdf"
}
]
},
{
id : 275 ,
volcano_number : 273030 ,
report_num : "2026-02" ,
report_date : "2026-05-03T05:28:12Z" ,
preparer : "dramsey" ,
reviewer : "hwright" ,
send_time : "2026-05-03T05:28:20Z" ,
volcano_name : "Mayon" ,
summary : "At around 5:30 PM local time on May 2, 2026, a pyroclastic flow (cloud of searing ash and rock particles) from erupting Mayon Volcano traveled around 4 km (~2.5 miles) down the southwestern slope of the volcano and produced an ash plume that drifted to the west-southwest. At least 52 villages in various towns in Albay province were heavily affected by thick ash fall, reducing visibility to near zero and making travel extremely difficult. There were no reports of casualties and the volcano alert level remained at Level 3 (on a 0-5 level scale)." ,
files : [
{
name : "275-273030-Mayon-2026-02.pdf" ,
url : "https://vdap.org/ivans/sitrep/275-273030-Mayon-2026-02.pdf" ,
type : "pdf"
},
{
name : "275-273030-Mayon-2026-02.pdf" ,
url : "https://vdap.org/ivans/sitrep/275-273030-Mayon-2026-02.pdf" ,
type : "pdf"
}
]
}
])
}, []);
2026-07-30 09:18:47 -07:00
const data = useMemo (() => {
2026-08-05 17:16:45 +00:00
if ( apiData ? . length === 0 ) return [];
let sorted ;
2026-07-30 09:18:47 -07:00
if ( sorts ? . volcanoSort && sorts ? . timeSort === "asc" ) {
sorted = [... apiData ]. sort ( ascTimeVolcanoSort );
} else if ( sorts ? . volcanoSort && sorts ? . timeSort === "desc" ) {
sorted = [... apiData ]. sort ( descTimeVolcanoSort );
} else if ( sorts ? . timeSort === "asc" ) {
sorted = [... apiData ]. sort ( ascTimeSort );
} else {
sorted = [... apiData ]. sort ( descTimeSort );
}
return sorted ;
}, [ apiData , sorts ]);
useEffect (() => {
if ( isCopy ) {
2026-08-05 17:16:45 +00:00
const dataText = JSON . stringify ( data ); // TODO: this is pretty ugly when copied and pasted, format it better
navigator . clipboard . writeText ( dataText )
2026-07-30 09:18:47 -07:00
. then (() => {
alert ( 'Copied to clipboard!' );
})
. catch (( err => {
console . error ( 'Failed to copy: ' , err );
}));
updateSettings ( widgetID , { copyToClipboard : false });
}
2026-08-05 17:16:45 +00:00
}, [ isCopy , data , widgetID , updateSettings ]);
2026-07-30 09:18:47 -07:00
if ( isLoading ) return (
< Flex w = "100%" h = "100%" bg = "gray.800" borderRadius = "md" align = "center" justify = "center" >
< Spinner size = "xl" color = "blue.400" thickness = "4px" />
</ Flex >
);
if ( error ) return (
< Flex w = "100%" h = "100%" bg = "gray.800" borderRadius = "md" align = "center" justify = "center" p = { 4 }>
< Text color = "red.400" >{ error }</ Text >
</ Flex >
);
if ( data . length === 0 ) return (
< Flex height = "100%" width = "100%" align = "center" justify = "center" bg = "gray.800" borderRadius = "md" >
< Text color = "gray.400" > No flight level data available this week .</ Text >
</ Flex >
);
return (
2026-08-05 17:16:45 +00:00
< Flex pos = "relative" w = "100%" h = "100%" direction = "column" bg = "gray.800" borderRadius = "xl" align = "center" justify = "center" gap = { 2 } p = { 2 }>
2026-07-30 09:18:47 -07:00
{ data . map (( item , index ) => {
2026-07-22 16:29:17 -07:00
return (
2026-08-05 17:16:45 +00:00
/* IVANS ENTRIES */
< Flex onClick = {() => setActiveSummaryItem ( item )} w = "100%" h = "100%" key = { index } borderRadius = "lg" bg = "gray.700" justify = "space-between" align = "center" >
< Flex direction = "column" w = "60%" pl = { 3 } color = "gray.100" >
< Text >{ item . volcano_name }</ Text >
< Text fontSize = "xs" color = "gray.300/90" >{ item . send_time }</ Text >
</ Flex >
< Flex direction = "row" w = "40%" justify = "right" pr = { 3 }>
< Text color = "gray.100" bg = "gray.800/50" borderRadius = "lg" pl = { 2 } pr = { 2 }>{ item . report_num }</ Text >
</ Flex >
</ Flex >
2026-07-30 09:18:47 -07:00
)
})}
2026-08-05 17:16:45 +00:00
{ /* SUMMARY/PDF VIEWER */ }
< SummaryDialog
open = { !! activeSummaryItem }
onOpenChange = {() => setActiveSummaryItem ( null )}
item = { activeSummaryItem }
/>
2026-07-30 09:18:47 -07:00
</ Flex >
);
}
function SummaryDialog ({ open , onOpenChange , item }) {
return (
2026-08-05 17:16:45 +00:00
< Dialog.Root lazyMount
open = { open }
onOpenChange = { onOpenChange }
trapFocus = { false }
modal = { false }
closeOnInteractOutside = { false }
motionPreset = "slide-in-bottom"
position = "absolute"
>
< Dialog.Positioner w = "100%" h = "100%" >
< Dialog.Content w = "100%" h = "100%" maxW = "none" p = { 0 } m = { 0 } borderRadius = "xl" boxShadow = "none" >
< Dialog.Header p = { 3 }>
2026-07-30 09:18:47 -07:00
< Dialog.Title w = "100%" >
2026-08-05 17:16:45 +00:00
< Flex w = "100%" direction = "row" justify = "space-between" align = "center" >
< Flex w = "100%" direction = "column" >
< Text w = "100%" fontSize = "lg" >
{ item ? . volcano_name }
</ Text >
< Text w = "100%" fontSize = "sm" color = "blue.400" >
{ item ? . send_time }
</ Text >
</ Flex >
< Flex w = "100%" justify = "right" align = "center" >
< Text textAlign = "center" bg = "gray.600" fontSize = "xs" borderRadius = "xl" color = "gray.400" pl = { 2 } pr = { 2 }>
prepared by : { item ? . preparer }
</ Text >
2026-07-30 09:18:47 -07:00
</ Flex >
</ Flex >
</ Dialog.Title >
</ Dialog.Header >
< Dialog.Body bg = "gray.800" overflow = "auto" >
2026-08-05 17:16:45 +00:00
< Text textAlign = "left" color = "gray.100" >{ item ? . summary }</ Text >
< br />
{ item ? . files ? . map (( file , index ) => {
return (
2026-08-05 18:58:47 +00:00
<>
< object data = { ` ${ file ? . url } #view=FitH&toolbar=1` }
type = "application/pdf"
style = {{ width : '100%' , height : '100%' , display : 'block' }}
>
< Flex w = "100%" h = "100%" color = "gray.400" p = { 4 }>
< Text mb = { 2 } color = "fg.muted" >
Your browser cannot display this PDF directly .
</ Text >
< a href = { file ? . url } target = "_blank" >
Download PDF
</ a >
</ Flex >
</ object >
< br />
</>
2026-08-05 17:16:45 +00:00
)})}
2026-07-30 09:18:47 -07:00
</ Dialog.Body >
2026-08-05 17:16:45 +00:00
< Dialog.Footer p = { 2 } pr = { 3 }>
< Button size = "xs" variant = "subtle" onClick = {() => onOpenChange ({ open : false })}>
2026-07-30 09:18:47 -07:00
Close
</ Button >
</ Dialog.Footer >
</ Dialog.Content >
</ Dialog.Positioner >
</ Dialog.Root >
)
}
function ascTimeVolcanoSort ( a , b ) {
if ( a . volcano_name < b . volcano_name ) return - 1
else if ( a . volcano_name > b . volcano_name ) return 1
else return ascTimeSort ( a , b );
}
function descTimeVolcanoSort ( a , b ) {
if ( a . volcano_name < b . volcano_name ) return - 1
else if ( a . volcano_name > b . volcano_name ) return 1
else return descTimeSort ( a , b );
}
function ascTimeSort ( a , b ) {
const aTime = new Date ( a . send_time );
const bTime = new Date ( b . send_time );
if ( aTime > bTime ) return 1 ;
if ( aTime < bTime ) return - 1 ;
return 0 ;
}
function descTimeSort ( a , b ) {
const aTime = new Date ( a . send_time );
const bTime = new Date ( b . send_time );
if ( aTime > bTime ) return - 1 ;
if ( aTime < bTime ) return 1 ;
return 0 ;
2026-07-22 16:29:17 -07:00
}