add components
This commit is contained in:
1 parent
a5ba103148
commit
e6947be2f1
10 files changed
+653
-10
No files matched your search
@@ -11,6 +11,7 @@ const ChartBox = ({
|
||||
justifyContent = "space-between",
|
||||
Legend,
|
||||
borderRadiusRight = 4,
|
||||
sx,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
@@ -70,8 +71,8 @@ const ChartBox = ({
|
||||
alignItems="center"
|
||||
gap={theme.spacing(6)}
|
||||
>
|
||||
<IconBox>{icon}</IconBox>
|
||||
<Typography component="h2">{header}</Typography>
|
||||
{icon && <IconBox>{icon}</IconBox>}
|
||||
{header && <Typography component="h2">{header}</Typography>}
|
||||
</Stack>
|
||||
{children}
|
||||
</Stack>
|
||||
@@ -84,7 +85,7 @@ export default ChartBox;
|
||||
|
||||
ChartBox.propTypes = {
|
||||
children: PropTypes.node,
|
||||
icon: PropTypes.node.isRequired,
|
||||
header: PropTypes.string.isRequired,
|
||||
icon: PropTypes.node,
|
||||
header: PropTypes.string,
|
||||
height: PropTypes.string,
|
||||
};
|
||||
@@ -14,6 +14,7 @@ const Image = ({
|
||||
maxWidth = "auto",
|
||||
maxHeight = "auto",
|
||||
base64,
|
||||
sx,
|
||||
}) => {
|
||||
if (shouldRender === false) {
|
||||
return null;
|
||||
@@ -36,6 +37,7 @@ const Image = ({
|
||||
maxHeight={maxHeight}
|
||||
width={width}
|
||||
height={height}
|
||||
sx={{ ...sx }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -49,6 +51,7 @@ Image.propTypes = {
|
||||
maxWidth: PropTypes.string,
|
||||
maxHeight: PropTypes.string,
|
||||
base64: PropTypes.string,
|
||||
sx: PropTypes.object,
|
||||
};
|
||||
|
||||
export default Image;
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Box, Typography } from "@mui/material";
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import Image from "../Image";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import PropTypes from "prop-types";
|
||||
import useUtils from "../../Pages/Uptime/Monitors/Hooks/useUtils";
|
||||
@@ -29,7 +30,15 @@ import useUtils from "../../Pages/Uptime/Monitors/Hooks/useUtils";
|
||||
* @returns {React.ReactElement} A styled box containing the statistic
|
||||
*/
|
||||
|
||||
const StatBox = ({ heading, subHeading, gradient = false, status = "", sx }) => {
|
||||
const StatBox = ({
|
||||
img,
|
||||
alt,
|
||||
heading,
|
||||
subHeading,
|
||||
gradient = false,
|
||||
status = "",
|
||||
sx,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const { statusToTheme } = useUtils();
|
||||
const themeColor = statusToTheme[status];
|
||||
@@ -70,7 +79,8 @@ const StatBox = ({ heading, subHeading, gradient = false, status = "", sx }) =>
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
<Stack
|
||||
direction="row"
|
||||
sx={{
|
||||
padding: `${theme.spacing(4)} ${theme.spacing(8)}`,
|
||||
/* TODO why are we using width and min width here? */
|
||||
@@ -95,9 +105,20 @@ const StatBox = ({ heading, subHeading, gradient = false, status = "", sx }) =>
|
||||
...sx,
|
||||
}}
|
||||
>
|
||||
<Typography component="h2">{heading}</Typography>
|
||||
<Typography>{subHeading}</Typography>
|
||||
</Box>
|
||||
{img && (
|
||||
<Image
|
||||
src={img}
|
||||
height={"30px"}
|
||||
width={"30px"}
|
||||
alt={alt}
|
||||
sx={{ marginRight: theme.spacing(8) }}
|
||||
/>
|
||||
)}
|
||||
<Stack>
|
||||
<Typography component="h2">{heading}</Typography>
|
||||
<Typography>{subHeading}</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import { Stack, Typography, List, ListItem } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import PulseDot from "../../../../../Components/Animated/PulseDot";
|
||||
import "/node_modules/flag-icons/css/flag-icons.min.css";
|
||||
|
||||
const BASE_BOX_PADDING_VERTICAL = 16;
|
||||
const BASE_BOX_PADDING_HORIZONTAL = 8;
|
||||
|
||||
const DeviceTicker = ({ data, width = "100%", connectionStatus }) => {
|
||||
const theme = useTheme();
|
||||
const statusColor = {
|
||||
up: theme.palette.success.main,
|
||||
down: theme.palette.error.main,
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="column"
|
||||
gap={theme.spacing(2)}
|
||||
width={width}
|
||||
sx={{
|
||||
padding: `${theme.spacing(BASE_BOX_PADDING_VERTICAL)} ${theme.spacing(BASE_BOX_PADDING_HORIZONTAL)}`,
|
||||
backgroundColor: theme.palette.background.main,
|
||||
border: 1,
|
||||
borderStyle: "solid",
|
||||
borderColor: theme.palette.primary.lowContrast,
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent={"center"}
|
||||
gap={theme.spacing(4)}
|
||||
>
|
||||
<PulseDot color={statusColor[connectionStatus]} />
|
||||
|
||||
<Typography
|
||||
variant="h1"
|
||||
mb={theme.spacing(8)}
|
||||
sx={{ alignSelf: "center" }}
|
||||
>
|
||||
{connectionStatus === "up" ? "Connected" : "No connection"}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<List>
|
||||
{data.slice(Math.max(data.length - 5, 0)).map((dataPoint) => {
|
||||
const countryCode = dataPoint?.countryCode?.toLowerCase() ?? null;
|
||||
const flag = countryCode ? `fi fi-${countryCode}` : null;
|
||||
return (
|
||||
<ListItem key={Math.random()}>
|
||||
<Stack direction="column">
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(4)}
|
||||
>
|
||||
{flag && <span className={flag} />}
|
||||
<Typography variant="h2">{dataPoint?.city || "Unknown"}</Typography>
|
||||
</Stack>
|
||||
<Typography variant="p">{`Response time: ${Math.floor(dataPoint?.responseTime ?? 0)} ms`}</Typography>
|
||||
<Typography variant="p">{`UPT burned: ${dataPoint.uptBurnt}`}</Typography>
|
||||
<Typography variant="p">{`${dataPoint?.device?.manufacturer} ${dataPoint?.device?.model}`}</Typography>
|
||||
</Stack>
|
||||
</ListItem>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeviceTicker;
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
{
|
||||
"id": "43f36e14-e3f5-43c1-84c0-50a9c80dc5c7",
|
||||
"name": "MapLibre",
|
||||
"zoom": 0.861983335785597,
|
||||
"pitch": 0,
|
||||
"center": [17.6543171043124, 32.9541203267468],
|
||||
"glyphs": "https://demotiles.maplibre.org/font/{fontstack}/{range}.pbf",
|
||||
"layers": [
|
||||
{
|
||||
"id": "background",
|
||||
"type": "background",
|
||||
"paint": {
|
||||
"background-color": "#121217"
|
||||
},
|
||||
"filter": ["all"],
|
||||
"layout": {
|
||||
"visibility": "visible"
|
||||
},
|
||||
"maxzoom": 24
|
||||
},
|
||||
{
|
||||
"id": "coastline",
|
||||
"type": "line",
|
||||
"paint": {
|
||||
"line-blur": 0.5,
|
||||
"line-color": "#000000",
|
||||
"line-width": {
|
||||
"stops": [
|
||||
[0, 2],
|
||||
[6, 6],
|
||||
[14, 9],
|
||||
[22, 18]
|
||||
]
|
||||
}
|
||||
},
|
||||
"filter": ["all"],
|
||||
"layout": {
|
||||
"line-cap": "round",
|
||||
"line-join": "round",
|
||||
"visibility": "visible"
|
||||
},
|
||||
"source": "maplibre",
|
||||
"maxzoom": 24,
|
||||
"minzoom": 0,
|
||||
"source-layer": "countries"
|
||||
},
|
||||
{
|
||||
"id": "countries-fill",
|
||||
"type": "fill",
|
||||
"paint": {
|
||||
"fill-color": "#292929"
|
||||
},
|
||||
"filter": ["all"],
|
||||
"layout": {
|
||||
"visibility": "visible"
|
||||
},
|
||||
"source": "maplibre",
|
||||
"maxzoom": 24,
|
||||
"source-layer": "countries"
|
||||
},
|
||||
{
|
||||
"id": "countries-boundary",
|
||||
"type": "line",
|
||||
"paint": {
|
||||
"line-color": "#484848",
|
||||
"line-width": {
|
||||
"stops": [
|
||||
[1, 1],
|
||||
[6, 2],
|
||||
[14, 6],
|
||||
[22, 12]
|
||||
]
|
||||
},
|
||||
"line-opacity": {
|
||||
"stops": [
|
||||
[3, 0.5],
|
||||
[6, 1]
|
||||
]
|
||||
}
|
||||
},
|
||||
"layout": {
|
||||
"line-cap": "round",
|
||||
"line-join": "round",
|
||||
"visibility": "visible"
|
||||
},
|
||||
"source": "maplibre",
|
||||
"maxzoom": 24,
|
||||
"source-layer": "countries"
|
||||
},
|
||||
{
|
||||
"id": "countries-label",
|
||||
"type": "symbol",
|
||||
"paint": {
|
||||
"text-color": "rgba(8, 37, 77, 1)",
|
||||
"text-halo-blur": {
|
||||
"stops": [
|
||||
[2, 0.2],
|
||||
[6, 0]
|
||||
]
|
||||
},
|
||||
"text-halo-color": "rgba(255, 255, 255, 1)",
|
||||
"text-halo-width": {
|
||||
"stops": [
|
||||
[2, 1],
|
||||
[6, 1.6]
|
||||
]
|
||||
}
|
||||
},
|
||||
"filter": ["all"],
|
||||
"layout": {
|
||||
"text-font": ["Open Sans Semibold"],
|
||||
"text-size": {
|
||||
"stops": [
|
||||
[2, 10],
|
||||
[4, 12],
|
||||
[6, 16]
|
||||
]
|
||||
},
|
||||
"text-field": {
|
||||
"stops": [
|
||||
[2, "{ABBREV}"],
|
||||
[4, "{NAME}"]
|
||||
]
|
||||
},
|
||||
"visibility": "visible",
|
||||
"text-max-width": 10,
|
||||
"text-transform": {
|
||||
"stops": [
|
||||
[0, "uppercase"],
|
||||
[2, "none"]
|
||||
]
|
||||
}
|
||||
},
|
||||
"source": "maplibre",
|
||||
"maxzoom": 24,
|
||||
"minzoom": 2,
|
||||
"source-layer": "centroids"
|
||||
},
|
||||
{
|
||||
"id": "data-dots",
|
||||
"type": "circle",
|
||||
"source": "data-dots",
|
||||
"paint": {
|
||||
"circle-radius": 3,
|
||||
"circle-color": ["get", "color"],
|
||||
"circle-opacity": 0.5
|
||||
}
|
||||
}
|
||||
],
|
||||
"bearing": 0,
|
||||
"sources": {
|
||||
"maplibre": {
|
||||
"url": "https://demotiles.maplibre.org/tiles/tiles.json",
|
||||
"type": "vector"
|
||||
},
|
||||
"data-dots": {
|
||||
"type": "geojson",
|
||||
"data": {
|
||||
"type": "FeatureCollection",
|
||||
"features": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"version": 8,
|
||||
"metadata": {
|
||||
"maptiler:copyright": "This style was generated on MapTiler Cloud. Usage is governed by the license terms in https://github.com/maplibre/demotiles/blob/gh-pages/LICENSE",
|
||||
"openmaptiles:version": "3.x"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import "maplibre-gl/dist/maplibre-gl.css";
|
||||
import PropTypes from "prop-types";
|
||||
import { useRef, useState, useEffect } from "react";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import style from "./DistributedUptimeMapStyle.json";
|
||||
import maplibregl from "maplibre-gl";
|
||||
const DistributedUptimeMap = ({ width = "100%", height = "100%", checks }) => {
|
||||
const mapContainer = useRef(null);
|
||||
const map = useRef(null);
|
||||
const theme = useTheme();
|
||||
const [mapLoaded, setMapLoaded] = useState(false);
|
||||
|
||||
const colorLookup = (avgResponseTime) => {
|
||||
if (avgResponseTime <= 150) {
|
||||
return "#00FF00"; // Green
|
||||
} else if (avgResponseTime <= 250) {
|
||||
return "#FFFF00"; // Yellow
|
||||
} else {
|
||||
return "#FF0000"; // Red
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (mapContainer.current && !map.current) {
|
||||
map.current = new maplibregl.Map({
|
||||
container: mapContainer.current,
|
||||
style,
|
||||
center: [0, 20],
|
||||
zoom: 0.8,
|
||||
});
|
||||
}
|
||||
map.current.on("load", () => {
|
||||
setMapLoaded(true);
|
||||
});
|
||||
|
||||
return () => {
|
||||
if (map.current) {
|
||||
map.current.remove();
|
||||
map.current = null;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (map.current && checks?.length > 0) {
|
||||
// Convert dots to GeoJSON
|
||||
const geojson = {
|
||||
type: "FeatureCollection",
|
||||
features: checks.map((check) => {
|
||||
return {
|
||||
type: "Feature",
|
||||
geometry: {
|
||||
type: "Point",
|
||||
coordinates: [check.lng, check.lat],
|
||||
},
|
||||
properties: {
|
||||
color: theme.palette.accent.main,
|
||||
// color: colorLookup(check.avgResponseTime) || "blue", // Default to blue if no color specified
|
||||
},
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
||||
// Update the source with new dots
|
||||
const source = map.current.getSource("data-dots");
|
||||
if (source) {
|
||||
source.setData(geojson);
|
||||
}
|
||||
}
|
||||
}, [checks, theme, mapLoaded]);
|
||||
return (
|
||||
<div
|
||||
ref={mapContainer}
|
||||
style={{
|
||||
width: width,
|
||||
height: height,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
DistributedUptimeMap.propTypes = {
|
||||
checks: PropTypes.array,
|
||||
width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
};
|
||||
|
||||
export default DistributedUptimeMap;
|
||||
+214
@@ -0,0 +1,214 @@
|
||||
import PropTypes from "prop-types";
|
||||
import {
|
||||
AreaChart,
|
||||
Area,
|
||||
XAxis,
|
||||
Tooltip,
|
||||
CartesianGrid,
|
||||
ResponsiveContainer,
|
||||
Text,
|
||||
} from "recharts";
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useState } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { formatDateWithTz } from "../../../../../Utils/timeUtils";
|
||||
|
||||
const CustomToolTip = ({ active, payload, label }) => {
|
||||
const uiTimezone = useSelector((state) => state.ui.timezone);
|
||||
const theme = useTheme();
|
||||
if (active && payload && payload.length) {
|
||||
const responseTime = payload[0]?.payload?.originalAvgResponseTime
|
||||
? payload[0]?.payload?.originalAvgResponseTime
|
||||
: (payload[0]?.payload?.avgResponseTime ?? 0);
|
||||
return (
|
||||
<Box
|
||||
className="area-tooltip"
|
||||
sx={{
|
||||
backgroundColor: theme.palette.background.main,
|
||||
border: 1,
|
||||
borderColor: theme.palette.primary.lowContrast,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
py: theme.spacing(2),
|
||||
px: theme.spacing(4),
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
color: theme.palette.text.tertiary,
|
||||
fontSize: 12,
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
{formatDateWithTz(label, "ddd, MMMM D, YYYY, h:mm A", uiTimezone)}
|
||||
</Typography>
|
||||
<Box mt={theme.spacing(1)}>
|
||||
<Box
|
||||
display="inline-block"
|
||||
width={theme.spacing(4)}
|
||||
height={theme.spacing(4)}
|
||||
backgroundColor={theme.palette.primary.main}
|
||||
sx={{ borderRadius: "50%" }}
|
||||
/>
|
||||
<Stack
|
||||
display="inline-flex"
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
ml={theme.spacing(3)}
|
||||
sx={{
|
||||
"& span": {
|
||||
color: theme.palette.text.tertiary,
|
||||
fontSize: 11,
|
||||
fontWeight: 500,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
component="span"
|
||||
sx={{ opacity: 0.8 }}
|
||||
>
|
||||
Response Time
|
||||
</Typography>{" "}
|
||||
<Typography component="span">
|
||||
{Math.floor(responseTime)}
|
||||
<Typography
|
||||
component="span"
|
||||
sx={{ opacity: 0.8 }}
|
||||
>
|
||||
{" "}
|
||||
ms
|
||||
</Typography>
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
{/* Display original value */}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
CustomToolTip.propTypes = {
|
||||
active: PropTypes.bool,
|
||||
payload: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
value: PropTypes.number,
|
||||
payload: PropTypes.shape({
|
||||
_id: PropTypes.string,
|
||||
avgResponseTime: PropTypes.number,
|
||||
originalAvgResponseTime: PropTypes.number,
|
||||
}),
|
||||
})
|
||||
),
|
||||
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
};
|
||||
const CustomTick = ({ x, y, payload, index }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const uiTimezone = useSelector((state) => state.ui.timezone);
|
||||
// Render nothing for the first tick
|
||||
if (index === 0) return null;
|
||||
return (
|
||||
<Text
|
||||
x={x}
|
||||
y={y + 10}
|
||||
textAnchor="middle"
|
||||
fill={theme.palette.text.tertiary}
|
||||
fontSize={11}
|
||||
fontWeight={400}
|
||||
>
|
||||
{formatDateWithTz(payload?.value, "h:mm a", uiTimezone)}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
CustomTick.propTypes = {
|
||||
x: PropTypes.number,
|
||||
y: PropTypes.number,
|
||||
payload: PropTypes.object,
|
||||
index: PropTypes.number,
|
||||
};
|
||||
|
||||
const DistributedUptimeResponseChart = ({ checks }) => {
|
||||
const theme = useTheme();
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
if (checks.length === 0) return null;
|
||||
return (
|
||||
<ResponsiveContainer
|
||||
width="100%"
|
||||
minWidth={25}
|
||||
height={220}
|
||||
>
|
||||
<AreaChart
|
||||
width="100%"
|
||||
height="100%"
|
||||
data={checks}
|
||||
margin={{
|
||||
top: 10,
|
||||
right: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
}}
|
||||
onMouseMove={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
<CartesianGrid
|
||||
stroke={theme.palette.primary.lowContrast}
|
||||
strokeWidth={1}
|
||||
strokeOpacity={1}
|
||||
fill="transparent"
|
||||
vertical={false}
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="colorUv"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="0"
|
||||
y2="1"
|
||||
>
|
||||
<stop
|
||||
offset="0%"
|
||||
stopColor={theme.palette.accent.darker}
|
||||
stopOpacity={0.8}
|
||||
/>
|
||||
<stop
|
||||
offset="100%"
|
||||
stopColor={theme.palette.accent.main}
|
||||
stopOpacity={0}
|
||||
/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<XAxis
|
||||
stroke={theme.palette.primary.lowContrast}
|
||||
dataKey="_id"
|
||||
tick={<CustomTick />}
|
||||
minTickGap={0}
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
height={20}
|
||||
/>
|
||||
<Tooltip
|
||||
cursor={{ stroke: theme.palette.primary.lowContrast }}
|
||||
content={<CustomToolTip />}
|
||||
wrapperStyle={{ pointerEvents: "none" }}
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="avgResponseTime"
|
||||
stroke={theme.palette.primary.accent}
|
||||
fill="url(#colorUv)"
|
||||
strokeWidth={isHovered ? 2.5 : 1.5}
|
||||
activeDot={{ stroke: theme.palette.background.main, r: 5 }}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
};
|
||||
|
||||
DistributedUptimeResponseChart.propTypes = {
|
||||
checks: PropTypes.array,
|
||||
};
|
||||
|
||||
export default DistributedUptimeResponseChart;
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Stack, Typography, Box } from "@mui/material";
|
||||
import SolanaLogo from "../../../../../assets/icons/solana_logo.svg?react";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
const Footer = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Stack
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
spacing={2}
|
||||
>
|
||||
<Typography variant="h2">Made with ❤️ by UpRock & Bluewave Labs</Typography>
|
||||
<Stack
|
||||
width="100%"
|
||||
direction="row"
|
||||
gap={theme.spacing(2)}
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
>
|
||||
<Typography variant="h2">Built on</Typography>
|
||||
<SolanaLogo
|
||||
width={15}
|
||||
height={15}
|
||||
/>
|
||||
<Typography variant="h2">Solana</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
@@ -0,0 +1,15 @@
|
||||
import { useState, useEffect } from "react";
|
||||
const LastUpdate = ({ suffix, lastUpdateTime, trigger }) => {
|
||||
const [elapsedMs, setElapsedMs] = useState(lastUpdateTime);
|
||||
|
||||
useEffect(() => {
|
||||
setElapsedMs(lastUpdateTime);
|
||||
const timer = setInterval(() => {
|
||||
setElapsedMs((prev) => prev + 1000);
|
||||
}, 1000);
|
||||
return () => clearInterval(timer);
|
||||
}, [lastUpdateTime, trigger]);
|
||||
|
||||
return `${Math.floor(elapsedMs / 1000)} ${suffix}`;
|
||||
};
|
||||
export default LastUpdate;
|
||||
@@ -0,0 +1,30 @@
|
||||
import { LinearProgress } from "@mui/material";
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
const NextExpectedCheck = ({ lastUpdateTime, interval, trigger }) => {
|
||||
const [elapsedMs, setElapsedMs] = useState(lastUpdateTime);
|
||||
|
||||
useEffect(() => {
|
||||
setElapsedMs(lastUpdateTime);
|
||||
const timer = setInterval(() => {
|
||||
setElapsedMs((prev) => {
|
||||
const newElapsedMs = prev + 100;
|
||||
return newElapsedMs;
|
||||
});
|
||||
}, 100);
|
||||
return () => clearInterval(timer);
|
||||
}, [interval, trigger]);
|
||||
|
||||
return (
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
color="accent"
|
||||
value={Math.min((elapsedMs / interval) * 100, 100)}
|
||||
sx={{
|
||||
transition: "width 1s linear", // Smooth transition over 1 second
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default NextExpectedCheck;
|
||||
Reference in new issue
Block a user