@@ -1,164 +1,354 @@
import VdapApi from '../../hooks/useVdapApi.js' ;
import { Flex , Spinner , Text , Button , Dialog , Box } from "@chakra-ui/react" ;
import * as url from "node:url" ; // Adjust path as needed
import { useState } from 'react' ;
import { Button , Dialog , Flex , Grid , GridItem , Spinner , Text } from "@chakra-ui/react" ;
import { useEffect , useMemo , useState } from 'react' ;
import { useDashboardStore } from "@/store/useDashboardStore.jsx" ;
export default function IvansAlerts ( ) {
console . log ( "IvansAlerts" ) ;
const [ isSummaryOpen , setIsSummaryOpen ] = useState ( false ) ;
const [ isFileOpen , setIsFileOpen ] = useState ( false ) ;
export default function IvansAlerts ( { settings , widgetID } ) {
console . log ( "IvansAlerts" ) ;
const [ activeSummaryItem , setActiveSummaryItem ] = useState ( false ) ;
const [ activeFileItem , setActiveFileItem ] = useState ( false ) ;
const [ activeImageItem , setActiveImageItem ] = useState ( false ) ;
const widgets = useDashboardStore ( state => state . widgets ) ;
console . log ( "widgets: " , widgets ) ;
const updateSettings = useDashboardStore ( state => state . updateWidgetSettings ) ;
const isCopy = settings ? . copyToClipboard === true ; // acts as a failsafe if no default setting is set
const sorts = settings ? . activeSorts ;
function SummaryDialog ( { open , onOpenChange , item } ) {
// build url once on mount
const seven _day _url = useMemo ( ( ) => {
const base _url = '/api/v2/comm_coor_ivans'
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 ;
} , [ ] ) ;
// VdapApi hook only calls api if the url changes
const { data : apiD , isLoading , error } = VdapApi ( seven _day _url ) ;
const apiData = [
{
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"
}
]
}
] ;
const data = useMemo ( ( ) => {
if ( apiData . length === 0 ) return [ ] ;
let sorted = [ ] ;
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 ) ;
}
console . log ( "sorted data: " , sorted ) ;
return sorted ;
} , [ apiData , sorts ] ) ;
useEffect ( ( ) => {
if ( isCopy ) {
navigator . clipboard . writeText ( data )
. then ( ( ) => {
alert ( 'Copied to clipboard!' ) ;
} )
. catch ( ( err => {
console . error ( 'Failed to copy: ' , err ) ;
} ) ) ;
updateSettings ( widgetID , { copyToClipboard : false } ) ;
}
} , [ isCopy ] ) ;
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 (
< Flex w = "100%" h = "100%" direction = "column" bg = "gray.800" borderRadius = "xl" align = "center" justify = "center" gap = { 2 } p = { 2 } overflow = "auto" >
{ /*<Grid w="100%" h="100%" templateColumns="repeat(6, 1fr)" alignItems="center">*/ }
{ /* <GridItem colSpan={1} color="gray.100" fontWeight="bold" pl={4}>Volcano</GridItem>*/ }
{ /* <GridItem colSpan={1} color="gray.100" fontWeight="bold">Report Number</GridItem>*/ }
{ /* <GridItem colSpan={1} color="gray.100" fontWeight="bold" pl={3}>Report Send Time</GridItem>*/ }
{ /* <GridItem colSpan={1} color="gray.100" fontWeight="bold" textAlign="center">Preparer</GridItem>*/ }
{ /* <GridItem colSpan={1} color="gray.100" fontWeight="bold" textAlign="center">Summary</GridItem>*/ }
{ /* <GridItem colSpan={1} color="gray.100" fontWeight="bold" textAlign="center">Attachments</GridItem>*/ }
{ /*</Grid>*/ }
{ data . map ( ( item , index ) => {
const context _string = item . volcano _name + ' • ' + item . send _time ;
return (
< Dialog.Root open = { open } onOpenChange = { onOpenChange } >
< Dialog.Backdrop / >
< Dialog.Positioner >
< Dialog.Content >
< Dialog.Header >
< Dialog.Title > Summary < / Dialog.Title >
< / Dialog.Header >
< Dialog.Body >
< Text > { item . summary } < / Text >
< / Dialog.Body >
< Dialog.Footer >
< Button variant = "outline" onClick = { ( ) => onOpenChange ( { open : false } ) } >
Close
< / Button >
< / Dialog.Footer >
< Dialog.CloseTrigger / >
< / Dialog.Content >
< / Dialog.Positioner >
< / Dialog.Root >
)
}
function FileDialog ( { open , onOpenChange , item , context } ) {
return (
< Dialog.Root open = { open } onOpenChange = { onOpenChange } >
< Dialog.Backdrop / >
< Dialog.Positioner >
< Dialog.Content >
< Dialog.Header >
< Flex width = "100%" direction = "row" align = "center" justifyContent = "space-between" >
< Flex direction = "column" >
< Dialog.Title > { item . name ? item . name : 'PDF Attachment' } < / Dialog.Title >
< Text color = "gray.200" > { context } < / Text >
< / Flex >
< Flex >
< Text bg = "gray.800" p = "1" borderRadius = "sm" > { item . type } < / Text >
< / Flex >
< / Flex >
< / Dialog.Header >
< Dialog.Body h = "75vh" p = { 0 } >
< Box bg = "gray.900" w = "100%" h = "100%" display = "flex" alignItems = "center" justifyContent = "center" >
< object display = "block" data = { ` ${ item . url } #view=FitH ` } type = "application/pdf" >
< Flex height = "100%" color = "gray.400" p = { 4 } >
< Text mb = { 2 } color = "fg.muted" > Your browser cannot display this PDF directly . < / Text >
< a href = { item . url } target = "_blank" >
Download PDF
< / a >
< / Flex >
< / object >
< / Box >
< / Dialog.Body >
< Dialog.Footer >
< Button variant = "outline" onClick = { ( ) => onOpenChange ( { open : false } ) } >
Close
< / Button >
< / Dialog.Footer >
< Dialog.CloseTrigger / >
< / Dialog.Content >
< / Dialog.Positioner >
< / Dialog.Root >
)
}
const get _7 _day _range _url = ( ) => {
const base _url = '/api/v2/comm_coor_ivans'
let today = new Date ( ) ;
let sevenDaysAgo = new Date ( today ) ;
sevenDaysAgo . setDate ( sevenDaysAgo . getDate ( ) - 7 ) ;
sevenDaysAgo = sevenDaysAgo . toLocaleDateString ( 'en-CA' ) ;
today = today . toLocaleDateString ( 'en-CA' ) ;
const url _with _dates = base _url + '?start_date=' + sevenDaysAgo + '&end_date=' + today ;
return url _with _dates ;
}
const seven _day _url = get _7 _day _range _url ( ) ;
const { data : apiData , isLoading , error } = VdapApi ( seven _day _url ) ;
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 ( apiData . 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 . < / Text >
< / Flex >
) ;
return (
< Flex w = "100%" h = "100%" direction = "column" bg = "gray.800" borderRadius = "md" align = "center" justify = "center" p = { 4 } >
< Flex direction = "row" gap = { 4 } >
< Text color = "gray.400" > Volcano < / Text >
< Text color = "gray.400" > Report Number < / Text >
< Text color = "gray.400" > Report Send Time < / Text >
< Text color = "gray.400" > Preparer < / Text >
< Text color = "gray.400" > Summary < / Text >
< Text color = "gray.400" > Attachments < / Text >
< / Flex >
{ apiData . map ( ( item , index ) => {
const context _str = item . volcano _name + " " + "•" + " " + item . send _time ;
return (
< Flex direction = "row" key = { index } gap = { 4 } >
< Text color = "gray.400" > { item . volcano _name } < / Text >
< Text color = "gray.400" > { item . report _num } < / Text >
< Text color = "gray.400" > { item . send _time } < / Text >
< Text color = "gray.400" > { item . preparer } < / Text >
< Button onClick = { ( ) => setIsSummaryOpen ( true ) } color = "gray.400" > View < / Button >
< SummaryDialog
open = { isSummaryOpen }
onOpenChange = { ( e ) => setIsSummaryOpen ( e . open ) }
item = { item }
/ >
{ item . files . map ( ( item , index ) => {
const $file _name = item . name ;
const $url = item . url ;
const $type = item . type ;
if ( $type === "pdf" ) return (
< >
< Button onClick = { ( ) => setIsFileOpen ( true ) } > { $type } < / Button >
< FileDialog
open = { isFileOpen }
onOpenChange = { ( e ) => setIsFileOpen ( e . open ) }
item = { item }
context = { context _str }
/ >
< / >
)
} ) }
< / Flex >
< Grid w = "100%" h = "100%" templateColumns = "repeat(8, 1fr)" key = { index } borderRadius = "lg" bg = "gray.700" alignItems = "center" justifyContent = "center" >
< GridItem colSpan = { 2 } color = "gray.100" pl = { 4 } > { item . volcano _name } < / GridItem >
< GridItem colSpan = { 1 } color = "gray.100" > { item . report _num } < / GridItem >
< GridItem colSpan = { 2 } color = "gray.100" > { item . send _time } < / GridItem >
< GridItem colSpan = { 1 } color = "gray.100" > { item . preparer } < / GridItem >
< GridItem colSpan = { 1 } >
< >
< Button w = "7rem" h = { 0 } mb = { 1 } onClick = { ( ) => setActiveSummaryItem ( item ) } color = "purple.300" variant = "plain" textDecor = "underline" > View Summary < / Button >
< SummaryDialog
open = { ! ! activeSummaryItem }
onOpenChange = { ( ) => setActiveSummaryItem ( null ) }
item = { activeSummaryItem }
context = { context _string }
/ >
< / >
< / GridItem >
< GridItem colSpan = { 1 } >
< Flex direction = "column" justify = "center" alignItems = "center" gap = { 0 } >
{ item . files . length === 0 ? (
< Text color = "gray.600" fontSize = "sm" > No Attachments < / Text >
) : (
item . files . map ( ( file , index ) => {
if ( file . type === "pdf" ) return (
< div key = { index } >
< Button w = "7rem" h = { 0 } mb = { 1 } onClick = { ( ) => setActiveFileItem ( file ) } color = "purple.300" variant = "plain" textDecor = "underline" > View PDF < / Button >
< FileDialog
open = { ! ! activeFileItem }
onOpenChange = { ( ) => setActiveFileItem ( null ) }
item = { activeFileItem }
context = { context _string }
/ >
< / div >
)
} ) }
< Flex >
else if ( file . type === "image" ) return (
< div key = { index } >
< Button w = "7rem" onClick = { ( ) => setActiveImageItem ( file ) } color = "purple.300" variant = "plain" textDecor = "underline" > View Image < / Button >
< ImageDialog
open = { ! ! activeImageItem }
onOpenChange = { ( ) => setActiveImageItem ( null ) }
item = { activeImageItem }
context = { context _string }
/ >
< / div >
)
else if ( file . type === "download" ) return (
< Button key = { index } w = "7rem" onClick = { ( ) => downloadReport ( file . url , file . name ) } color = "purple.300" variant = "plain" textDecor = "underline" > Download < / Button >
)
} )
) }
< / Flex >
< / GridItem >
< / Grid >
)
} ) }
< / Flex >
) ;
}
function SummaryDialog ( { open , onOpenChange , item } ) {
return (
< Dialog.Root lazyMount open = { open } onOpenChange = { onOpenChange } >
< Dialog.Backdrop / >
< Dialog.Positioner >
< Dialog.Content w = "100vw" h = "40vh" >
< Dialog.Header >
< Dialog.Title w = "100%" >
< Flex w = "100%" h = "100%" direction = "column" textAlign = "left" >
< Flex direction = "row" justify = "space-between" align = "center" >
< Text fontSize = "md" > { item ? . volcano _name } < / Text >
< Text bg = "gray.800" fontSize = "xs" borderRadius = "sm" color = "gray.500" pl = { 2 } pr = { 2 } > Preparer : { item ? . preparer } < / Text >
< / Flex >
< Text fontSize = "xs" color = "blue.400" > { item ? . send _time } < / Text >
< / Flex >
< / Dialog.Title >
< / Dialog.Header >
< Dialog.Body bg = "gray.800" overflow = "auto" >
< Text textAlign = "left" > { item ? . summary } < / Text >
< / Dialog.Body >
< Dialog.Footer >
< Button variant = "outline" onClick = { ( ) => onOpenChange ( { open : false } ) } >
Close
< / Button >
< / Dialog.Footer >
< Dialog.CloseTrigger / >
< / Dialog.Content >
< / Dialog.Positioner >
< / Dialog.Root >
)
}
function FileDialog ( { open , onOpenChange , item , context } ) {
return (
< Dialog.Root lazyMount open = { open } onOpenChange = { onOpenChange } >
< Dialog.Backdrop / >
< Dialog.Positioner >
< Dialog.Content w = "100vw" h = "75vh" >
< Dialog.Header >
< Flex height = "100%" width = "100%" direction = "row" align = "center" justifyContent = "space-between" >
< Flex direction = "column" textAlign = "left" >
< Dialog.Title > { item ? . name ? item ? . name : 'PDF Attachment' } < / Dialog.Title >
< Text color = "gray.200" > { context } < / Text >
< / Flex >
< Text bg = "gray.800" p = "1" borderRadius = "sm" > { ( item ? . type || '' ) . toUpperCase ( ) } < / Text >
< / Flex >
< / Flex >
) ;
< / Dialog.Header >
< Dialog.Body p = { 0 } >
< Flex bg = "gray.800" w = "100%" h = "100%" alignItems = "center" justifyContent = "center" >
< object data = { ` ${ item ? . url } #view=FitH ` } 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 = { item ? . url } target = "_blank" >
Download PDF
< / a >
< / Flex >
< / object >
< / Flex >
< / Dialog.Body >
< Dialog.Footer >
< Button variant = "outline" onClick = { ( ) => onOpenChange ( { open : false } ) } >
Close
< / Button >
< / Dialog.Footer >
< Dialog.CloseTrigger / >
< / Dialog.Content >
< / Dialog.Positioner >
< / Dialog.Root >
)
}
function ImageDialog ( { open , onOpenChange , item , context } ) {
return (
< Dialog.Root lazyMount open = { open } onOpenChange = { onOpenChange } >
< Dialog.Backdrop / >
< Dialog.Positioner >
< Dialog.Content w = "100vw" h = "40vh" >
< Dialog.Header >
< Dialog.Title > { item ? . name } < / Dialog.Title >
< Text color = "gray.200" > { context } < / Text >
< / Dialog.Header >
< Dialog.Body bg = "gray.800" overflow = "auto" >
< img src = { item ? . url } alt = { item ? . name } / >
< / Dialog.Body >
< Dialog.Footer >
< Button variant = "outline" onClick = { ( ) => onOpenChange ( { open : false } ) } >
Close
< / Button >
< / Dialog.Footer >
< Dialog.CloseTrigger / >
< / Dialog.Content >
< / Dialog.Positioner >
< / Dialog.Root >
)
}
const downloadReport = ( url , name ) => {
const link = document . createElement ( 'a' ) ;
link . href = url ;
link . download = name ;
document . body . appendChild ( link ) ;
link . click ( ) ;
document . body . removeChild ( link ) ;
}
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 ;
}