add rs widget with copy to clipboard and sorting capabilities
This commit is contained in:
@@ -0,0 +1,279 @@
|
||||
import {Button, Flex, Spinner, Text, Box, Collapsible} from "@chakra-ui/react";
|
||||
import {useEffect, useMemo } from 'react';
|
||||
import {useDashboardStore} from "@/store/useDashboardStore.jsx";
|
||||
import VdapApi from '../../hooks/useVdapApi.js';
|
||||
|
||||
export default function RemoteSensing({ settings, widgetID }) {
|
||||
console.log("RS widget ID: ", widgetID);
|
||||
const updateSettings = useDashboardStore(state => state.updateWidgetSettings);
|
||||
const isCopy = settings?.copyToClipboard === true; // acts as a fail-safe if no default setting is set
|
||||
const sorts = settings?.activeSorts;
|
||||
|
||||
// build url once on mount
|
||||
const seven_day_url = useMemo(() => {
|
||||
const base_url = '/api/v2/remote_sensing';
|
||||
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 and apiData remains stable since vdapApi uses state to track the data
|
||||
const {data: apiD, isLoading, error} = VdapApi(seven_day_url);
|
||||
|
||||
const apiData = useMemo(() => {
|
||||
return ([
|
||||
{
|
||||
id: 1848,
|
||||
volcano_name: "Lewotolok",
|
||||
country: "Indonesia",
|
||||
report_time: "2026-01-13T20:39:39Z",
|
||||
summary: "Dear friends,\n\nOur latest satellite view from 13 January (~0900 local time) shows that the lava has began to flow over the crater rim onto the northwest flank (see attached). The lava flow spreads into three lobes from the single source vent on the northwest flank of the active cone. The longest portion of the flow was about 530 meters from the vent with the toe draping about 30-40 meters over the rim of the main crater just north of the May 2024 lava flow. A narrow deposit of volcanoclastic debris extends about 300 meters below the toe of the lava flow (white dashed outline on sketch).\n\nPlease let us know if you have any questions.\nBest wishes,\nRick",
|
||||
sender: "rwessels@usgs.gov",
|
||||
files: [
|
||||
{
|
||||
name: "Lewotolok-ID-sketch-20260113_Jan_lavas.jpg",
|
||||
url: "https://vdap.org/commcoordashboard/rs/attachments/1848-Lewotolok-ID-sketch-20260113_Jan_lavas.jpg",
|
||||
type: "image"
|
||||
},
|
||||
{
|
||||
name: "Lewotolok-ID-sketch-20260113_Jan_lavas.jpg",
|
||||
url: "https://vdap.org/commcoordashboard/rs/attachments/1848-Lewotolok-ID-sketch-20260113_Jan_lavas.jpg",
|
||||
type: "image"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 1847,
|
||||
volcano_name: "Mayon",
|
||||
country: "Philippines",
|
||||
report_time: "2026-01-13T17:56:46Z",
|
||||
summary: "Dear PHIVOLCS friends,\nOur most recent satellite data from 13 January (1500 local) show the new lava flow extending southeast from the summit and splitting into two lobes (see attached pdf). The total length of the longest lobe was about 1 kilometer. Compared to data from 12 January, the longest part of the flow advanced almost 200 meters over 11 hours while the shorter section advanced about 80 meters over the same time. Debris falling from the front of both lava flow lobes extend another several hundred meters below each lobe. We continue to watch the activity at Mayon volcano and will share anything that might help. \n\nPlease let us know if you have any questions, requests, or comments.\n\nBest wishes,\nRick",
|
||||
sender: "rwessels@usgs.gov",
|
||||
files: [
|
||||
{
|
||||
name: "Mayon-RP-images-20260113_ICEYE_lava.pdf",
|
||||
url: "https://vdap.org/commcoordashboard/rs/attachments/1847-Mayon-RP-images-20260113_ICEYE_lava.pdf",
|
||||
type: "pdf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 1889,
|
||||
volcano_name: "Bagana",
|
||||
country: "Papua New Guinea",
|
||||
report_time: "2026-06-24T21:28:58Z",
|
||||
summary: "Hi Steve and RVO,\nI'm now back in the office after some time away and have looked closely at the recent data for Bagana Volcano. \nSatellite data from 15 June show that an increase in lava effusion caused the upper part of the active lava flow at the summit to inflate and likely contributed to the 19-20 June collapse. By 16 June, the inflated area was about 260-meters-long by 140-meters-wide. The northeast edge of the inflated section of the lava flow was the source of rockfalls down the northeast flank where some of the larger blocks tumbled over 1.3 kilometer from the flow. The 22 June data that Julie mentioned shows that all of the inflated area was removed by the collapse (~36,000 square meter area of the upper flow removed). The collapse PDC deposits scoured a deep gorge and removed a broad area of vegetation near the lake. The distal portion of the collapse deposits formed a small delta extending into the lake and possibly raising the lake level slightly based on overall shore outline changes.\n\nI've attached a sketch map showing the features described above. Please let us know if you have any questions.\n\nBest wishes,\nRick",
|
||||
sender: "rwessels@usgs.gov",
|
||||
files: [
|
||||
{
|
||||
name: "Bagana-PG-sketch-20260622_collapse+lava.jpg",
|
||||
url: "https://vdap.org/commcoordashboard/rs/attachments/1889-Bagana-PG-sketch-20260622_collapse+lava.jpg",
|
||||
type: "image"
|
||||
},
|
||||
{
|
||||
name: "Bagana-PG-sketch-20260622_3Dcollapse+lava.jpg",
|
||||
url: "https://vdap.org/commcoordashboard/rs/attachments/1889-Bagana-PG-sketch-20260622_3Dcollapse+lava.jpg",
|
||||
type: "image"
|
||||
},
|
||||
{
|
||||
name: "Bagana_22_Jun_2026_collapse2.kml",
|
||||
url: "https://vdap.org/commcoordashboard/rs/attachments/1889-Bagana_22_Jun_2026_collapse2.kml",
|
||||
type: "download"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 1845,
|
||||
volcano_name: "Lewotolok",
|
||||
country: "Indonesia",
|
||||
report_time: "2026-01-07T21:42:17Z",
|
||||
summary: "Dear friends,\nThe increased activity at Lewotolok Volcano over the past week reminded me that we have not sent you a Remote Sensing Summary since June 2025. Over the two days from 3 to 5 January 2026, explosions from the central cone intensified, doubling the area impacted by proximal ballistics and tephra to over 20 hectares, while increased effusion has produced a new ~20,000 m² lava flow on the west–northwest flank that extends ~270 meters from the vent. So far, all recent lava flows have been confined to the summit crater.\n\nPlease let us know if you have any questions or additions.\n\nBest Wishes,\nRick",
|
||||
sender: "rwessels@usgs.gov",
|
||||
files: []
|
||||
},
|
||||
])
|
||||
}, []);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
return sorted;
|
||||
}, [apiData, sorts]);
|
||||
|
||||
useEffect(() => {
|
||||
if(isCopy) {
|
||||
const dataText = JSON.stringify(data); // TODO: this is pretty ugly when copied and pasted, format it better
|
||||
navigator.clipboard.writeText(dataText)
|
||||
.then(() => {
|
||||
alert('Copied to clipboard!');
|
||||
})
|
||||
.catch((err => {
|
||||
console.error('Failed to copy: ', err);
|
||||
}));
|
||||
updateSettings(widgetID, {copyToClipboard: false});
|
||||
}
|
||||
}, [isCopy, data, widgetID, updateSettings]);
|
||||
|
||||
if (isLoading) return (
|
||||
<Flex w="100%" h="100%" align="center" justify="center" borderRadius="md" bg="gray.800">
|
||||
<Spinner size="xl" color="blue.400" thickness="4px"/>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
if (error) return (
|
||||
<Flex w="100%" h="100%" align="center" justify="center" p={4} borderRadius="md" bg="gray.800">
|
||||
<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 Remote Sensing data available this week.</Text>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
return (
|
||||
<Flex direction="column"
|
||||
w="100%" h="100%"
|
||||
pos="relative"
|
||||
justify="top" align="center"
|
||||
p={2} gap={2}
|
||||
overflow="auto"
|
||||
borderRadius="xl"
|
||||
bg="gray.800"
|
||||
>
|
||||
{data.map((item, index) => {
|
||||
return (
|
||||
<Collapsible.Root key={index} w="100%">
|
||||
{/* Using context to dynamically change the border radii of the triggers */}
|
||||
<Collapsible.Context>
|
||||
{(collapsible) => (
|
||||
<>
|
||||
<Collapsible.Trigger w="100%">
|
||||
<Flex
|
||||
direction="row"
|
||||
w="100%" h="100%"
|
||||
justify="space-between" align="center"
|
||||
borderTopRadius="lg"
|
||||
borderBottomRadius={collapsible.open ? "0" : "lg"}
|
||||
bg="gray.700"
|
||||
>
|
||||
<Flex direction="column" pl={3} textAlign="left" color="gray.100">
|
||||
<Text>{item?.volcano_name}</Text>
|
||||
<Text fontSize="xs" color="gray.300/90">{item?.country}</Text>
|
||||
</Flex>
|
||||
<Flex pr={3}>
|
||||
<Text pl={2} pr={2} borderRadius="lg" bg="gray.800/50" color="gray.100" fontSize="sm">{item?.report_time}</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Content w="100%" h="auto">
|
||||
{/* This box needs to be at h="100%" for the pdf toolbar to appear */}
|
||||
<Box w="100%" h="100%" p={1} pl={2} borderBottomRadius="lg" bg="gray.700/45">
|
||||
<Text fontSize="sm" textAlign="left" color="gray.100">{item?.summary}</Text>
|
||||
<br />
|
||||
{item?.files?.map((file, index) => {
|
||||
if (file?.type === "pdf") return (
|
||||
<Box w="100%" h="100%" key={index} p={1}>
|
||||
<Flex dir="row" justify="space-between" align="center" mb={2}>
|
||||
<Text pl={1} pr={1} bg="gray.600/50" borderRadius="md" fontSize="xs" color="gray.300">{file?.name}</Text>
|
||||
<Text pl={1} pr={1} bg="gray.600/50" borderRadius="md" fontSize="xs" color="gray.300/90">PDF</Text>
|
||||
</Flex>
|
||||
<object
|
||||
data={`${file?.url}#view=FitH&toolbar=1`}
|
||||
type="application/pdf"
|
||||
style={{width: '100%', height: '100%', display: 'block'}}
|
||||
>
|
||||
<Flex w="100%" h="100%" p={4} color="gray.400">
|
||||
<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/>
|
||||
</Box>
|
||||
);
|
||||
|
||||
else if (file?.type === "image") return (
|
||||
<Box key={index} w="100%" h="100%" p={1}>
|
||||
<Flex dir="row" justify="space-between" align="center" mb={2}>
|
||||
<Text pl={1} pr={1} bg="gray.600/50" borderRadius="md" fontSize="xs" color="gray.300">{file?.name}</Text>
|
||||
<Text pl={1} pr={1} bg="gray.600/50" borderRadius="md" fontSize="xs" color="gray.300/90">PNG</Text>
|
||||
</Flex>
|
||||
<a href={file?.url} target="_blank" rel="noopener noreferrer">
|
||||
<img src={file?.url} alt={file?.name} />
|
||||
</a>
|
||||
<br />
|
||||
</Box>
|
||||
);
|
||||
|
||||
else if (file?.type === "download") return (
|
||||
<Flex key={index} dir="row" w="100%" h="100%" p={1} align="center" justify="space-between">
|
||||
<Text pl={1} pr={1} bg="gray.600/50" borderRadius="md" fontSize="xs" color="gray.300">{file?.name}</Text>
|
||||
<Button asChild variant="surface" size="xxs" fontSize="xs" pl={2} pr={2}>
|
||||
<a href={file?.url} target="_blank" rel="noopener noreferrer">Download</a>
|
||||
</Button>
|
||||
</Flex>
|
||||
)
|
||||
})}
|
||||
</Box>
|
||||
</Collapsible.Content>
|
||||
</>
|
||||
)}
|
||||
</Collapsible.Context>
|
||||
</Collapsible.Root>
|
||||
)
|
||||
})}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
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.report_time);
|
||||
const bTime = new Date(b.report_time);
|
||||
|
||||
if (aTime > bTime) return 1;
|
||||
if (aTime < bTime) return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
function descTimeSort(a, b) {
|
||||
const aTime = new Date(a.report_time);
|
||||
const bTime = new Date(b.report_time);
|
||||
|
||||
if (aTime > bTime) return -1;
|
||||
if (aTime < bTime) return 1;
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import {Button, Flex, Switch} from "@chakra-ui/react";
|
||||
import {useDashboardStore} from "@/store/useDashboardStore.jsx";
|
||||
|
||||
export default function RemoteSensingSettings({ widgetId, currentSettings }) {
|
||||
const updateSettings = useDashboardStore(state => state.updateWidgetSettings);
|
||||
|
||||
const handleCopyToClipboard = () => {
|
||||
updateSettings(widgetId, {copyToClipboard: true});
|
||||
}
|
||||
const handleVolcanoSort = (details) => {
|
||||
const isSort = currentSettings?.activeSorts?.volcanoSort === true;
|
||||
updateSettings(widgetId, { activeSorts: { ...currentSettings?.activeSorts, volcanoSort: !isSort }});
|
||||
}
|
||||
const handleTimeSort = (details) => {
|
||||
const sort = currentSettings?.activeSorts?.timeSort === "desc" ? "asc" : "desc";
|
||||
updateSettings(widgetId, { activeSorts: { ...currentSettings?.activeSorts, timeSort: sort }});
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex flexDirection="column" justifyContent="space-between" gap={2}>
|
||||
<Button onClick={handleCopyToClipboard}>Copy All RS</Button>
|
||||
<Switch.Root
|
||||
checked={currentSettings?.activeSorts?.timeSort === "asc"}
|
||||
onCheckedChange={handleTimeSort}
|
||||
colorPalette="blue"
|
||||
>
|
||||
<Switch.HiddenInput />
|
||||
<Switch.Control>
|
||||
<Switch.Thumb />
|
||||
</Switch.Control>
|
||||
<Switch.Label>Sort Oldest to Newest</Switch.Label>
|
||||
</Switch.Root>
|
||||
<Switch.Root
|
||||
checked={currentSettings?.activeSorts?.volcanoSort}
|
||||
onCheckedChange={handleVolcanoSort}
|
||||
colorPalette="blue"
|
||||
>
|
||||
<Switch.HiddenInput />
|
||||
<Switch.Control>
|
||||
<Switch.Thumb />
|
||||
</Switch.Control>
|
||||
<Switch.Label>Sort by Volcano</Switch.Label>
|
||||
</Switch.Root>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user