Merge pull request #834 from bluewave-labs/feat/pagespeed-details-revamp

Pagespeed Details Revamp
This commit is contained in:
Alexander Holliday
2024-09-14 08:07:37 -07:00
committed by GitHub
22 changed files with 1039 additions and 672 deletions
@@ -6,6 +6,7 @@ import {
Tooltip,
CartesianGrid,
ResponsiveContainer,
Text,
} from "recharts";
import { Box, Stack, Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
@@ -80,16 +81,37 @@ const CustomToolTip = ({ active, payload, label }) => {
return null;
};
const MonitorDetailsAreaChart = ({ checks }) => {
const CustomTick = ({ x, y, payload, index }) => {
const theme = useTheme();
const uiTimezone = useSelector((state) => state.ui.timezone);
const formatDate = (timestamp) => {
return formatDateWithTz(timestamp, "HH:mm:ss", uiTimezone);
};
// 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, "HH:mm:ss", uiTimezone)}
</Text>
);
};
const memoizedChecks = useMemo(() => checks, [checks[0]]);
CustomTick.propTypes = {
x: PropTypes.number,
y: PropTypes.number,
payload: PropTypes.object,
index: PropTypes.number,
};
const MonitorDetailsAreaChart = ({ checks }) => {
const theme = useTheme();
const memoizedChecks = useMemo(() => checks, [checks[0]]);
return (
<ResponsiveContainer width="100%" minWidth={25} height={220}>
@@ -128,10 +150,12 @@ const MonitorDetailsAreaChart = ({ checks }) => {
<XAxis
stroke={theme.palette.border.dark}
dataKey="createdAt"
tickFormatter={formatDate}
tick={{ fontSize: "13px" }}
tick={<CustomTick />}
minTickGap={0}
axisLine={false}
tickLine={false}
height={18}
height={20}
interval="equidistantPreserveStart"
/>
<Tooltip
cursor={{ stroke: theme.palette.border.light }}
@@ -1,71 +0,0 @@
import { LineChart } from "@mui/x-charts/LineChart";
import PropTypes from "prop-types";
const PageSpeedLineChart = ({ pageSpeedChecks = [] }) => {
const keyToLabel = {
performance: "Performance",
seo: "SEO",
bestPractices: "Best practices",
accessibility: "Accessibility",
};
const colors = {
performance: "#2aa02b",
seo: "#9467bd",
bestPractices: "#ff7f0e",
accessibility: "#1f76b3",
};
const customize = {
legend: { position: { vertical: "bottom", horizontal: "middle" } },
margin: { bottom: 75 },
};
const xLabels = pageSpeedChecks.map((check) => {
return check.createdAt;
});
return (
<LineChart
series={Object.keys(keyToLabel).map((key) => ({
dataKey: key,
label: keyToLabel[key],
color: colors[key],
showMark: false,
}))}
yAxis={[{ min: 0, max: 100 }]}
xAxis={[
{
scaleType: "point",
data: xLabels,
valueFormatter: (val) => new Date(val).toLocaleDateString(),
},
]}
dataset={pageSpeedChecks}
{...customize}
grid={{ vertical: true, horizontal: true }}
tooltip={{ trigger: "none" }}
slotProps={{
legend: {
direction: "row",
position: { vertical: "bottom", horizontal: "middle" },
padding: 2,
itemMarkWidth: 8,
itemMarkHeight: 8,
markGap: 5,
itemGap: 15,
labelStyle: {
fontSize: 13,
color: "#344054"
}
},
}}
/>
);
};
PageSpeedLineChart.propTypes = {
pageSpeedChecks: PropTypes.array,
};
export default PageSpeedLineChart;
@@ -113,6 +113,7 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => {
{...item}
/>
)}
sx={{ mt: "auto" }}
/>
);
}
+1 -1
View File
@@ -58,7 +58,7 @@ const Incidents = () => {
};
return (
<Stack className="incidents" pt={theme.spacing(20)} gap={theme.spacing(12)}>
<Stack className="incidents" pt={theme.spacing(6)} gap={theme.spacing(12)}>
{loading ? (
<SkeletonLayout />
) : (
@@ -42,8 +42,8 @@ const CustomLabels = ({
CustomLabels.propTypes = {
x: PropTypes.number.isRequired,
width: PropTypes.number.isRequired,
height: PropTypes.number.isRequired,
width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
height: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
firstDataPoint: PropTypes.object.isRequired,
lastDataPoint: PropTypes.object.isRequired,
type: PropTypes.string.isRequired,
+7 -8
View File
@@ -218,6 +218,11 @@ const DetailsPage = ({ isAdmin }) => {
onClick={openCertificate}
sx={{
cursor: "pointer",
"& svg": {
width: 23,
height: 23,
top: "52%",
},
}}
>
<CertificateIcon />
@@ -444,14 +449,8 @@ const DetailsPage = ({ isAdmin }) => {
data={[{ response: monitor?.periodAvgResponseTime }]}
/>
</ChartBox>
<ChartBox
sx={{
"& tspan": {
fontSize: 11,
},
}}
>
<Stack>
<ChartBox sx={{ padding: 0 }}>
<Stack pt={theme.spacing(8)} pl={theme.spacing(8)}>
<IconBox>
<ResponseTimeIcon />
</IconBox>
@@ -76,6 +76,7 @@ export const StatBox = styled(Box)(({ theme }) => ({
borderColor: theme.palette.border.light,
borderRadius: 4,
backgroundColor: theme.palette.background.main,
background: `linear-gradient(340deg, ${theme.palette.background.accent} 20%, ${theme.palette.background.main} 45%)`,
"& h2": {
fontSize: 13,
fontWeight: 500,
+6 -2
View File
@@ -29,22 +29,26 @@ const useUtils = () => {
const statusStyles = {
up: {
backgroundColor: theme.palette.success.bg,
background: `linear-gradient(340deg, ${theme.palette.success.light} -60%, ${theme.palette.success.bg} 35%)`,
borderColor: theme.palette.success.light,
"& h2": { color: theme.palette.success.main },
},
down: {
backgroundColor: theme.palette.error.bg,
background: `linear-gradient(340deg, ${theme.palette.error.light} -60%, ${theme.palette.error.bg} 35%)`,
borderColor: theme.palette.error.light,
"& h2": { color: theme.palette.error.main },
},
paused: {
backgroundColor: theme.palette.warning.light,
borderColor: theme.palette.warning.border,
background: `linear-gradient(340deg, ${theme.palette.warning.dark} -60%, ${theme.palette.warning.light} 35%)`,
borderColor: theme.palette.warning.dark,
"& h2": { color: theme.palette.warning.main },
},
pending: {
backgroundColor: theme.palette.warning.light,
borderColor: theme.palette.warning.border,
background: `linear-gradient(340deg, ${theme.palette.warning.dark} -60%, ${theme.palette.warning.light} 35%)`,
borderColor: theme.palette.warning.dark,
"& h2": { color: theme.palette.warning.main },
},
};
@@ -0,0 +1,313 @@
import PropTypes from "prop-types";
import {
AreaChart,
Area,
XAxis,
Tooltip,
CartesianGrid,
ResponsiveContainer,
Text,
} from "recharts";
import { useTheme } from "@emotion/react";
import { useMemo } from "react";
import { Box, Stack, Typography } from "@mui/material";
import { formatDate } from "../../../../Utils/timeUtils";
const config = {
seo: {
id: "seo",
text: "SEO",
color: "unresolved",
},
performance: {
id: "performance",
text: "performance",
color: "success",
},
bestPractices: {
id: "bestPractices",
text: "best practices",
color: "warning",
},
accessibility: {
id: "accessibility",
text: "accessibility",
color: "primary",
},
};
/**
* Custom tooltip for the area chart.
* @param {Object} props
* @param {boolean} props.active - Whether the tooltip is active.
* @param {Array} props.payload - The payload data for the tooltip.
* @param {string} props.label - The label for the tooltip.
* @returns {JSX.Element|null} The tooltip element or null if not active.
*/
const CustomToolTip = ({ active, payload, label, config }) => {
const theme = useTheme();
console.log(payload);
if (active && payload && payload.length) {
return (
<Box
sx={{
backgroundColor: theme.palette.background.main,
border: 1,
borderColor: theme.palette.border.dark,
borderRadius: theme.shape.borderRadius,
py: theme.spacing(2),
px: theme.spacing(4),
}}
>
<Typography
sx={{
color: theme.palette.text.tertiary,
fontSize: 12,
fontWeight: 500,
}}
>
{formatDate(new Date(label))}
</Typography>
{Object.keys(config)
.reverse()
.map((key) => {
const { color } = config[key];
const dotColor = theme.palette[color].main;
return (
<Stack
key={`${key}-tooltip`}
direction="row"
alignItems="center"
gap={theme.spacing(3)}
mt={theme.spacing(1)}
sx={{
"& span": {
color: theme.palette.text.tertiary,
fontSize: 11,
fontWeight: 500,
},
}}
>
<Box
width={theme.spacing(4)}
height={theme.spacing(4)}
backgroundColor={dotColor}
sx={{ borderRadius: "50%" }}
/>
<Typography
component="span"
textTransform="capitalize"
sx={{ opacity: 0.8 }}
>
{config[key].text}
</Typography>{" "}
<Typography component="span">
{payload[0].payload[key]}
</Typography>
</Stack>
);
})}
</Box>
);
}
return null;
};
CustomToolTip.propTypes = {
active: PropTypes.bool,
payload: PropTypes.array,
label: PropTypes.string,
config: PropTypes.object,
};
/**
* Processes data to insert gaps with null values based on the interval.
* @param {Array} data
* @param {number} interval - The interval in milliseconds for gaps.
* @returns {Array} The formatted data with gaps.
*/
const processDataWithGaps = (data, interval) => {
if (data.length === 0) return [];
let formattedData = [];
let last = new Date(data[0].createdAt).getTime();
// Helper function to add a null entry
const addNullEntry = (timestamp) => {
formattedData.push({
accessibility: "N/A",
bestPractices: "N/A",
performance: "N/A",
seo: "N/A",
createdAt: timestamp,
});
};
data.forEach((entry) => {
const current = new Date(entry.createdAt).getTime();
if (current - last > interval * 2) {
// Insert null entries for each interval
let temp = last + interval;
while (temp < current) {
addNullEntry(new Date(temp).toISOString());
temp += interval;
}
}
formattedData.push(entry);
last = current;
});
return formattedData;
};
/**
* Custom tick component to render ticks on the XAxis.
*
* @param {Object} props
* @param {number} props.x - The x coordinate for the tick.
* @param {number} props.y - The y coordinate for the tick.
* @param {Object} props.payload - The data object containing the tick value.
* @param {number} props.index - The index of the tick in the array of ticks.
*
* @returns {JSX.Element|null} The tick element or null if the tick should be hidden.
*/
const CustomTick = ({ x, y, payload, index }) => {
const theme = useTheme();
// Render nothing for the first tick
if (index === 0) return null;
return (
<Text
x={x}
y={y + 8}
textAnchor="middle"
fill={theme.palette.text.tertiary}
fontSize={11}
fontWeight={400}
>
{formatDate(new Date(payload.value), {
year: undefined,
month: undefined,
day: undefined,
})}
</Text>
);
};
CustomTick.propTypes = {
x: PropTypes.number,
y: PropTypes.number,
payload: PropTypes.shape({
value: PropTypes.string.isRequired,
}),
index: PropTypes.number,
};
/**
* A chart displaying pagespeed details over time.
* @param {Object} props
* @param {Array} props.data - The data to display in the chart.
* @param {number} props.interval - The interval in milliseconds for processing gaps.
* @returns {JSX.Element} The area chart component.
*/
const PagespeedDetailsAreaChart = ({ data, interval, metrics }) => {
const theme = useTheme();
const memoizedData = useMemo(
() => processDataWithGaps(data, interval),
[data[0]]
);
const filteredConfig = Object.keys(config).reduce((result, key) => {
if (metrics[key]) {
result[key] = config[key];
}
return result;
}, {});
return (
<ResponsiveContainer width="100%" minWidth={25} height={215}>
<AreaChart
width="100%"
height="100%"
data={memoizedData}
margin={{ top: 10 }}
>
<CartesianGrid
stroke={theme.palette.border.light}
strokeWidth={1}
strokeOpacity={1}
fill="transparent"
vertical={false}
/>
<XAxis
stroke={theme.palette.border.dark}
dataKey="createdAt"
tick={<CustomTick />}
axisLine={false}
tickLine={false}
height={18}
minTickGap={0}
interval="equidistantPreserveStart"
/>
<Tooltip
cursor={{ stroke: theme.palette.border.light }}
content={<CustomToolTip config={filteredConfig} />}
/>
<defs>
{Object.values(filteredConfig).map(({ id, color }) => {
const startColor = theme.palette[color].main;
const endColor = theme.palette[color].light;
return (
<linearGradient id={id} x1="0" y1="0" x2="0" y2="1" key={id}>
<stop offset="0%" stopColor={startColor} stopOpacity={0.8} />
<stop offset="100%" stopColor={endColor} stopOpacity={0} />
</linearGradient>
);
})}
</defs>
{Object.keys(filteredConfig).map((key) => {
const { color } = filteredConfig[key];
const strokeColor = theme.palette[color].main;
const bgColor = theme.palette.background.main;
return (
<Area
connectNulls
key={key}
dataKey={key}
stackId={1}
stroke={strokeColor}
strokeWidth={1.5}
fill={`url(#${filteredConfig[key].id})`}
activeDot={{ stroke: bgColor, fill: strokeColor, r: 4.5 }}
/>
);
})}
</AreaChart>
</ResponsiveContainer>
);
};
PagespeedDetailsAreaChart.propTypes = {
data: PropTypes.arrayOf(
PropTypes.shape({
createdAt: PropTypes.string.isRequired,
accessibility: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
.isRequired,
bestPractices: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
.isRequired,
performance: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
.isRequired,
seo: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
})
).isRequired,
interval: PropTypes.number.isRequired,
metrics: PropTypes.object,
};
export default PagespeedDetailsAreaChart;
@@ -0,0 +1,312 @@
import PropTypes from "prop-types";
import { PieChart as MuiPieChart } from "@mui/x-charts/PieChart";
import { useDrawingArea } from "@mui/x-charts";
import { useState } from "react";
import { useTheme } from "@emotion/react";
import { Box } from "@mui/material";
/**
* Renders a centered label within a pie chart.
*
* @param {Object} props
* @param {string | number} props.value - The value to display in the label.
* @param {string} props.color - The color of the text.
* @returns {JSX.Element}
*/
const PieCenterLabel = ({ value, color, setExpand }) => {
const { width, height } = useDrawingArea();
return (
<g
transform={`translate(${width / 2}, ${height / 2})`}
onMouseEnter={() => setExpand(true)}
>
<circle cx={0} cy={0} r={width / 4} fill="transparent" />
<text
className="pie-label"
style={{
fill: color,
fontSize: 48,
textAnchor: "middle",
dominantBaseline: "central",
userSelect: "none",
}}
>
{value}
</text>
</g>
);
};
PieCenterLabel.propTypes = {
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
color: PropTypes.string,
setExpand: PropTypes.func,
};
/**
* A component that renders a label on a pie chart slice.
* The label is positioned relative to the center of the pie chart and is optionally highlighted.
*
* @param {Object} props
* @param {number} props.value - The value to display inside the pie slice.
* @param {number} props.startAngle - The starting angle of the pie slice in degrees.
* @param {number} props.endAngle - The ending angle of the pie slice in degrees.
* @param {string} props.color - The color of the label text when highlighted.
* @param {boolean} props.highlighted - Determines if the label should be highlighted or not.
* @returns {JSX.Element}
*/
const PieValueLabel = ({ value, startAngle, endAngle, color, highlighted }) => {
const { width, height } = useDrawingArea();
// Compute the midpoint angle in radians
const angle = (((startAngle + endAngle) / 2) * Math.PI) / 180;
const radius = height / 4; // length from center of the circle to where the text is positioned
// Calculate x and y positions
const x = Math.sin(angle) * radius;
const y = -Math.cos(angle) * radius;
return (
<g transform={`translate(${width / 2}, ${height / 2})`}>
<text
className="pie-value-label"
x={x}
y={y}
style={{
fill: highlighted ? color : "rgba(0,0,0,0)",
fontSize: 12,
textAnchor: "middle",
dominantBaseline: "central",
userSelect: "none",
pointerEvents: "none",
}}
>
+{value}
</text>
</g>
);
};
// Validate props using PropTypes
PieValueLabel.propTypes = {
value: PropTypes.number.isRequired,
startAngle: PropTypes.number.isRequired,
endAngle: PropTypes.number.isRequired,
color: PropTypes.string.isRequired,
highlighted: PropTypes.bool.isRequired,
};
const PieChart = ({ audits }) => {
const theme = useTheme();
/**
* Weight constants for different performance metrics.
* @type {Object}
*/
const weights = {
fcp: 10,
si: 10,
lcp: 25,
tbt: 30,
cls: 25,
};
/**
* Retrieves color properties based on the performance value.
*
* @param {number} value - The performance score used to determine the color properties.
* @returns {{stroke: string, strokeBg: string, text: string, bg: string}} The color properties for the given performance value.
*/
const getColors = (value) => {
if (value >= 90 && value <= 100)
return {
stroke: theme.palette.success.main,
strokeBg: theme.palette.success.light,
text: theme.palette.success.text,
bg: theme.palette.success.bg,
};
else if (value >= 50 && value < 90)
return {
stroke: theme.palette.warning.main,
strokeBg: theme.palette.warning.light,
text: theme.palette.warning.text,
bg: theme.palette.warning.bg,
};
else if (value >= 0 && value < 50)
return {
stroke: theme.palette.error.text,
strokeBg: theme.palette.error.light,
text: theme.palette.error.text,
bg: theme.palette.error.bg,
};
return {
stroke: theme.palette.unresolved.main,
strokeBg: theme.palette.unresolved.light,
text: theme.palette.unresolved.main,
bg: theme.palette.unresolved.bg,
};
};
/**
* Calculates and formats the data needed for rendering a pie chart based on audit scores and weights.
* This function generates properties for each pie slice, including angles, radii, and colors.
* It also calculates performance based on the weighted values.
*
* @returns {Array<Object>} An array of objects, each representing the properties for a slice of the pie chart.
* @returns {number} performance - A variable updated with the rounded sum of weighted values.
*/
let performance = 0;
const getPieData = (audits) => {
let data = [];
let startAngle = 0;
const padding = 3; // padding between arcs
const max = 360 - padding * (Object.keys(audits).length - 1); // _id is a child of audits
Object.keys(audits).forEach((key) => {
if (audits[key].score) {
let value = audits[key].score * weights[key];
let endAngle = startAngle + (weights[key] * max) / 100;
let theme = getColors(audits[key].score * 100);
data.push({
id: key,
data: [
{
value: value,
color: theme.stroke,
label: key.toUpperCase(),
},
{
value: weights[key] - value,
color: theme.strokeBg,
label: "",
},
],
arcLabel: (item) => `${item.label}`,
arcLabelRadius: 95,
startAngle: startAngle,
endAngle: endAngle,
innerRadius: 73,
outerRadius: 80,
cornerRadius: 2,
highlightScope: { faded: "global", highlighted: "series" },
faded: {
innerRadius: 73,
outerRadius: 80,
additionalRadius: -20,
arcLabelRadius: 5,
},
cx: pieSize.width / 2,
});
performance += Math.floor(value);
startAngle = endAngle + padding;
}
});
return data;
};
const pieSize = { width: 230, height: 230 };
const pieData = getPieData(audits);
const colorMap = getColors(performance);
const [highlightedItem, setHighLightedItem] = useState(null);
const [expand, setExpand] = useState(false);
return (
<Box onMouseLeave={() => setExpand(false)}>
{expand ? (
<MuiPieChart
series={[
{
data: [
{
value: 100,
color: colorMap.bg,
},
],
outerRadius: 77,
cx: pieSize.width / 2,
},
...pieData,
]}
width={pieSize.width}
height={pieSize.height}
margin={{ left: 0, top: 0, right: 0, bottom: 0 }}
onHighlightChange={setHighLightedItem}
slotProps={{
legend: { hidden: true },
}}
tooltip={{ trigger: "none" }}
sx={{
"&:has(.MuiPieArcLabel-faded) .pie-label": {
fill: "rgba(0,0,0,0) !important",
},
}}
>
<PieCenterLabel
value={performance}
color={colorMap.text}
setExpand={setExpand}
/>
{pieData?.map((pie) => (
<PieValueLabel
key={pie.id}
value={Math.round(pie.data[0].value * 10) / 10}
startAngle={pie.startAngle}
endAngle={pie.endAngle}
color={pie.data[0].color}
highlighted={highlightedItem?.seriesId === pie.id}
/>
))}
</MuiPieChart>
) : (
<MuiPieChart
series={[
{
data: [
{
value: 100,
color: colorMap.bg,
},
],
outerRadius: 77,
cx: pieSize.width / 2,
},
{
data: [
{
value: performance,
color: colorMap.stroke,
},
],
innerRadius: 73,
outerRadius: 80,
paddingAngle: 5,
cornerRadius: 2,
startAngle: 0,
endAngle: (performance / 100) * 360,
cx: pieSize.width / 2,
},
]}
width={pieSize.width}
height={pieSize.height}
margin={{ left: 0, top: 0, right: 0, bottom: 0 }}
tooltip={{ trigger: "none" }}
>
<PieCenterLabel
value={performance}
color={colorMap.text}
setExpand={setExpand}
/>
</MuiPieChart>
)}
</Box>
);
};
PieChart.propTypes = {
audits: PropTypes.object,
};
export default PieChart;
@@ -1,41 +1,7 @@
.page-speed-details h1 {
font-size: var(--env-var-font-size-large-plus);
}
.page-speed-details h2,
.page-speed-details h6 {
font-size: var(--env-var-font-size-large);
}
.page-speed-details h4,
.page-speed-details p,
.page-speed-details p > span {
font-size: var(--env-var-font-size-medium);
}
.page-speed-details h1 + span,
.page-speed-details h4 > span {
font-size: var(--env-var-font-size-small-plus);
}
.page-speed-details h6 {
line-height: 22px;
}
.page-speed-details h1,
.page-speed-details h2,
.page-speed-details h6 {
font-weight: 600;
}
.page-speed-details button.MuiButtonBase-root {
height: 34px;
}
.page-speed-details .stat-box {
flex: 1;
}
.page-speed-details .stat-box:last-of-type {
flex-shrink: 1;
}
.page-speed-details .stat-box svg {
width: 22px;
height: 22px;
}
.page-speed-details .MuiPieArcLabel-root {
font-size: var(--env-var-font-size-small-plus);
transition: fill 300ms ease;
@@ -50,8 +16,3 @@
.page-speed-details .MuiPieArc-root {
stroke: inherit;
}
.page-speed-details .metric {
min-width: 200px;
flex: 1;
}
+255 -526
View File
@@ -1,156 +1,35 @@
import PropTypes from "prop-types";
import { Box, Button, Skeleton, Stack, Typography } from "@mui/material";
import { PieChart } from "@mui/x-charts/PieChart";
import { useDrawingArea } from "@mui/x-charts";
import {
Box,
Button,
Divider,
Stack,
Tooltip,
Typography,
} from "@mui/material";
import { useEffect, useState } from "react";
import { useTheme } from "@emotion/react";
import { useNavigate, useParams } from "react-router-dom";
import { useSelector } from "react-redux";
import {
formatDuration,
formatDurationRounded,
formatDurationSplit,
} from "../../../Utils/timeUtils";
import { ChartBox, IconBox, StatBox } from "./styled";
import { logger } from "../../../Utils/Logger";
import { networkService } from "../../../main";
import SkeletonLayout from "./skeleton";
import SettingsIcon from "../../../assets/icons/settings-bold.svg?react";
import LastCheckedIcon from "../../../assets/icons/calendar-check.svg?react";
import ClockIcon from "../../../assets/icons/maintenance.svg?react";
import IntervalCheckIcon from "../../../assets/icons/interval-check.svg?react";
import PageSpeedLineChart from "../../../Components/Charts/PagespeedLineChart";
import SpedometerIcon from "../../../assets/icons/spedometer-icon.svg?react";
import MetricsIcon from "../../../assets/icons/ruler-icon.svg?react";
import ScoreIcon from "../../../assets/icons/monitor-graph-line.svg?react";
import PerformanceIcon from "../../../assets/icons/performance-report.svg?react";
import Breadcrumbs from "../../../Components/Breadcrumbs";
import PulseDot from "../../../Components/Animated/PulseDot";
import PagespeedDetailsAreaChart from "./Charts/AreaChart";
import Checkbox from "../../../Components/Inputs/Checkbox";
import PieChart from "./Charts/PieChart";
import "./index.css";
import useUtils from "../../Monitors/utils";
import SkeletonLayout from "./skeleton";
const StatBox = ({ icon, title, value }) => {
const theme = useTheme();
return (
<Stack
className="stat-box"
direction="row"
gap={theme.spacing(4)}
pt={theme.spacing(8)}
px={theme.spacing(8)}
pb={theme.spacing(10)}
border={1}
borderColor={theme.palette.border.light}
borderRadius={theme.shape.borderRadius}
backgroundColor={theme.palette.background.main}
minWidth="200px"
>
{icon}
<Box>
<Typography
variant="h6"
color={theme.palette.primary.main}
mb={theme.spacing(6)}
>
{title}
</Typography>
<Typography variant="h4" color={theme.palette.text.secondary}>
{value}
</Typography>
</Box>
</Stack>
);
};
StatBox.propTypes = {
icon: PropTypes.element,
title: PropTypes.string,
value: PropTypes.node,
};
/**
* Renders a centered label within a pie chart.
*
* @param {Object} props
* @param {string | number} props.value - The value to display in the label.
* @param {string} props.color - The color of the text.
* @returns {JSX.Element}
*/
const PieCenterLabel = ({ value, color, setExpand }) => {
const { width, height } = useDrawingArea();
return (
<g
transform={`translate(${width / 2}, ${height / 2})`}
onMouseEnter={() => setExpand(true)}
>
<circle cx={0} cy={0} r={width / 3} fill="transparent" />
<text
className="pie-label"
style={{
fill: color,
fontSize: "45px",
textAnchor: "middle",
dominantBaseline: "central",
userSelect: "none",
}}
>
{value}
</text>
</g>
);
};
PieCenterLabel.propTypes = {
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
color: PropTypes.string,
setExpand: PropTypes.func,
};
/**
* A component that renders a label on a pie chart slice.
* The label is positioned relative to the center of the pie chart and is optionally highlighted.
*
* @param {Object} props
* @param {number} props.value - The value to display inside the pie slice.
* @param {number} props.startAngle - The starting angle of the pie slice in degrees.
* @param {number} props.endAngle - The ending angle of the pie slice in degrees.
* @param {string} props.color - The color of the label text when highlighted.
* @param {boolean} props.highlighted - Determines if the label should be highlighted or not.
* @returns {JSX.Element}
*/
const PieValueLabel = ({ value, startAngle, endAngle, color, highlighted }) => {
const { width, height } = useDrawingArea();
// Compute the midpoint angle in radians
const angle = (((startAngle + endAngle) / 2) * Math.PI) / 180;
const radius = height / 3.5; // length from center of the circle to where the text is positioned
// Calculate x and y positions
const x = Math.sin(angle) * radius;
const y = -Math.cos(angle) * radius;
return (
<g transform={`translate(${width / 2}, ${height / 2})`}>
<text
className="pie-value-label"
x={x}
y={y}
style={{
fill: highlighted ? color : "rgba(0,0,0,0)",
fontSize: "12px",
textAnchor: "middle",
dominantBaseline: "central",
userSelect: "none",
}}
>
+{value}
</text>
</g>
);
};
PieValueLabel.propTypes = {
value: PropTypes.number,
startAngle: PropTypes.number,
endAngle: PropTypes.number,
color: PropTypes.string,
highlighted: PropTypes.bool,
};
const PageSpeedDetails = () => {
const theme = useTheme();
@@ -159,7 +38,6 @@ const PageSpeedDetails = () => {
const [audits, setAudits] = useState({});
const { monitorId } = useParams();
const { authToken } = useSelector((state) => state.auth);
const { determineState, statusColor, pagespeedStatusMsg } = useUtils();
useEffect(() => {
const fetchMonitor = async () => {
@@ -184,138 +62,31 @@ const PageSpeedDetails = () => {
fetchMonitor();
}, [monitorId, authToken, navigate]);
/**
* Weight constants for different performance metrics.
* @type {Object}
*/
const weights = {
fcp: 10,
si: 10,
lcp: 25,
tbt: 30,
cls: 25,
};
/**
* Retrieves color properties based on the performance value.
*
* @param {number} value - The performance score used to determine the color properties.
* @returns {{stroke: string, text: string, bg: string}} The color properties for the given performance value.
*/
const getColors = (value) => {
if (value >= 90 && value <= 100)
return {
stroke: theme.palette.success.main,
strokeBg: theme.palette.success.light,
text: theme.palette.success.text,
bg: theme.palette.success.bg,
shape: "circle",
};
else if (value >= 50 && value < 90)
return {
stroke: theme.palette.warning.main,
strokeBg: theme.palette.warning.bg,
text: theme.palette.warning.text,
bg: theme.palette.warning.light,
shape: "square",
};
else if (value >= 0 && value < 50)
return {
stroke: theme.palette.error.text,
strokeBg: theme.palette.error.light,
text: theme.palette.error.text,
bg: theme.palette.error.bg,
shape: "circle",
};
return {
stroke: theme.palette.unresolved.main,
strokeBg: theme.palette.unresolved.light,
text: theme.palette.unresolved.main,
bg: theme.palette.unresolved.bg,
shape: "",
};
};
/**
* Calculates and formats the data needed for rendering a pie chart based on audit scores and weights.
* This function generates properties for each pie slice, including angles, radii, and colors.
* It also calculates performance based on the weighted values.
*
* @returns {Array<Object>} An array of objects, each representing the properties for a slice of the pie chart.
* @returns {number} performance - A variable updated with the rounded sum of weighted values.
*/
let performance = 0;
const getPieData = (audits) => {
let props = [];
let startAngle = 0;
const padding = 3; // padding between arcs
const max = 360 - padding * (Object.keys(audits).length - 1); // _id is a child of audits
Object.keys(audits).forEach((key) => {
if (audits[key].score) {
let value = audits[key].score * weights[key];
let endAngle = startAngle + (weights[key] * max) / 100;
let theme = getColors(audits[key].score * 100);
props.push({
id: key,
data: [
{
value: value,
color: theme.stroke,
label: key.toUpperCase(),
},
{
value: weights[key] - value,
color: theme.strokeBg,
label: "",
},
],
arcLabel: (item) => `${item.label}`,
arcLabelRadius: 95,
startAngle: startAngle,
endAngle: endAngle,
innerRadius: 73,
outerRadius: 80,
cornerRadius: 2,
highlightScope: { faded: "global", highlighted: "series" },
faded: {
innerRadius: 63,
outerRadius: 70,
additionalRadius: -20,
arcLabelRadius: 5,
},
cx: pieSize.width / 2,
});
performance += Math.round(value);
startAngle = endAngle + padding;
}
});
return props;
};
const pieSize = { width: 210, height: 200 };
const pieData = getPieData(audits);
const colorMap = getColors(performance);
const [highlightedItem, setHighLightedItem] = useState(null);
const [expand, setExpand] = useState(false);
let loading = Object.keys(monitor).length === 0;
const data = monitor?.checks ? [...monitor.checks].reverse() : [];
let sharedStyles = {
border: 1,
borderColor: theme.palette.border.light,
borderRadius: theme.shape.borderRadius,
backgroundColor: theme.palette.background.main,
const splitDuration = (duration) => {
const { time, format } = formatDurationSplit(duration);
return (
<>
{time}
<Typography component="span">{format}</Typography>
</>
);
};
const [metrics, setMetrics] = useState({
accessibility: true,
bestPractices: true,
performance: true,
seo: true,
});
const handleMetrics = (id) => {
setMetrics((prev) => ({ ...prev, [id]: !prev[id] }));
};
const monitorState = determineState(monitor);
return (
<Stack className="page-speed-details" gap={theme.spacing(12)}>
<Stack className="page-speed-details" gap={theme.spacing(10)}>
{loading ? (
<SkeletonLayout />
) : (
@@ -327,19 +98,82 @@ const PageSpeedDetails = () => {
]}
/>
<Stack direction="row" gap={theme.spacing(2)}>
<PulseDot color={statusColor[monitorState]} />
<Box>
<Typography
component="h1"
mb={theme.spacing(2)}
fontSize={22}
fontWeight={500}
color={theme.palette.text.primary}
sx={{ lineHeight: 1 }}
>
{monitor?.url}
</Typography>
<Typography component="span" color={statusColor[monitorState]}>
{pagespeedStatusMsg[monitorState] || "Pending..."}
{monitor.name}
</Typography>
<Stack
direction="row"
alignItems="center"
height="fit-content"
gap={theme.spacing(2)}
>
<Tooltip
title={
monitor?.status
? "Your pagespeed monitor is live."
: "Your pagespeed monitor is down."
}
disableInteractive
slotProps={{
popper: {
modifiers: [
{
name: "offset",
options: {
offset: [0, -8],
},
},
],
},
}}
>
<Box>
<PulseDot
color={
monitor?.status
? theme.palette.success.main
: theme.palette.error.main
}
/>
</Box>
</Tooltip>
<Typography
component="h2"
fontSize={14.5}
color={theme.palette.text.secondary}
>
{monitor?.url}
</Typography>
<Typography
mt={theme.spacing(1)}
ml={theme.spacing(6)}
fontSize={12}
position="relative"
color={theme.palette.text.tertiary}
sx={{
"&:before": {
position: "absolute",
content: `""`,
width: 4,
height: 4,
borderRadius: "50%",
backgroundColor: theme.palette.text.tertiary,
opacity: 0.8,
left: -9,
top: "50%",
transform: "translateY(-50%)",
},
}}
>
Checking every {formatDurationRounded(monitor?.interval)}.
</Typography>
</Stack>
</Box>
<Button
variant="contained"
@@ -361,275 +195,165 @@ const PageSpeedDetails = () => {
Configure
</Button>
</Stack>
<Stack
direction="row"
justifyContent="space-between"
gap={theme.spacing(12)}
flexWrap="wrap"
>
<StatBox
icon={<LastCheckedIcon />}
title="Last checked"
value={
<>
{formatDuration(monitor?.lastChecked)}{" "}
<Typography
component="span"
fontStyle="italic"
sx={{ opacity: 0.8 }}
>
ago
</Typography>
</>
}
/>
<StatBox
icon={<ClockIcon />}
title="Checks since"
value={
<>
{formatDuration(monitor?.uptimeDuration)}{" "}
<Typography
component="span"
fontStyle="italic"
sx={{ opacity: 0.8 }}
>
ago
</Typography>
</>
}
></StatBox>
<StatBox
icon={<IntervalCheckIcon />}
title="Checks every"
value={formatDurationRounded(monitor?.interval)}
></StatBox>
<Stack direction="row" gap={theme.spacing(8)}>
<StatBox>
<Typography component="h2">checks since</Typography>
<Typography>
{splitDuration(monitor?.uptimeDuration)}
<Typography component="span">ago</Typography>
</Typography>
</StatBox>
<StatBox>
<Typography component="h2">last check</Typography>
<Typography>
{splitDuration(monitor?.lastChecked)}
<Typography component="span">ago</Typography>
</Typography>
</StatBox>
</Stack>
<Typography component="h2" color={theme.palette.text.secondary}>
Score history
</Typography>
<Box
height="300px"
sx={{
...sharedStyles,
}}
>
<PageSpeedLineChart pageSpeedChecks={data} />
<Box>
<Typography
fontSize={12}
color={theme.palette.text.tertiary}
my={theme.spacing(8)}
>
Showing statistics for past 24 hours.
</Typography>
<ChartBox sx={{ gridTemplateColumns: "75% 25%" }}>
<Stack direction="row" alignItems="center" gap={theme.spacing(6)}>
<IconBox>
<ScoreIcon />
</IconBox>
<Typography component="h2">Score history</Typography>
</Stack>
<Box>
<PagespeedDetailsAreaChart
data={data}
interval={monitor?.interval}
metrics={metrics}
/>
</Box>
<Box>
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(6)}
>
<IconBox>
<MetricsIcon />
</IconBox>
<Typography component="h2">Metrics</Typography>
</Stack>
<Stack
gap={theme.spacing(4)}
mt={theme.spacing(16)}
sx={{
"& label": { pl: theme.spacing(6) },
}}
>
<Box>
<Typography fontSize={11} fontWeight={500}>
Shown
</Typography>
<Divider sx={{ mt: theme.spacing(2) }} />
</Box>
<Checkbox
id="accessibility-toggle"
label="Accessibility"
isChecked={metrics.accessibility}
onChange={() => handleMetrics("accessibility")}
/>
<Divider />
<Checkbox
id="best-practices-toggle"
label="Best Practices"
isChecked={metrics.bestPractices}
onChange={() => handleMetrics("bestPractices")}
/>
<Divider />
<Checkbox
id="performance-toggle"
label="Performance"
isChecked={metrics.performance}
onChange={() => handleMetrics("performance")}
/>
<Divider />
<Checkbox
id="seo-toggle"
label="Search Engine Optimization"
isChecked={metrics.seo}
onChange={() => handleMetrics("seo")}
/>
<Divider />
</Stack>
</Box>
</ChartBox>
</Box>
<Typography component="h2" color={theme.palette.text.secondary}>
Performance report
</Typography>
<Stack
direction="row"
alignItems="center"
overflow="hidden"
<ChartBox
flex={1}
sx={{
"& p": {
color: theme.palette.text.secondary,
},
...sharedStyles,
}}
sx={{ gridTemplateColumns: "50% 50%", gridTemplateRows: "15% 85%" }}
>
<Stack direction="row" alignItems="center" gap={theme.spacing(6)}>
<IconBox>
<PerformanceIcon />
</IconBox>
<Typography component="h2">Performance report</Typography>
</Stack>
<Stack
alignItems="center"
textAlign="center"
minWidth="300px"
flex={1}
px={theme.spacing(20)}
py={theme.spacing(8)}
pt={theme.spacing(6)}
pb={theme.spacing(12)}
>
<Box onMouseLeave={() => setExpand(false)}>
{expand ? (
<PieChart
series={[
{
data: [
{
value: 100,
color: colorMap.bg,
},
],
outerRadius: 67,
cx: pieSize.width / 2,
},
...pieData,
]}
width={pieSize.width}
height={pieSize.height}
margin={{ left: 0, top: 0, right: 0, bottom: 0 }}
onHighlightChange={setHighLightedItem}
slotProps={{
legend: { hidden: true },
}}
tooltip={{ trigger: "none" }}
sx={{
"&:has(.MuiPieArcLabel-faded) .pie-label": {
fill: "rgba(0,0,0,0) !important",
},
}}
>
<PieCenterLabel
value={performance}
color={colorMap.text}
setExpand={setExpand}
/>
{pieData?.map((pie) => (
<PieValueLabel
key={pie.id}
value={Math.round(pie.data[0].value * 10) / 10}
startAngle={pie.startAngle}
endAngle={pie.endAngle}
color={pie.data[0].color}
highlighted={highlightedItem?.seriesId === pie.id}
/>
))}
</PieChart>
) : (
<PieChart
series={[
{
data: [
{
value: 100,
color: colorMap.bg,
},
],
outerRadius: 67,
cx: pieSize.width / 2,
},
{
data: [
{
value: performance,
color: colorMap.stroke,
},
],
innerRadius: 63,
outerRadius: 70,
paddingAngle: 5,
cornerRadius: 2,
startAngle: 0,
endAngle: (performance / 100) * 360,
cx: pieSize.width / 2,
},
]}
width={pieSize.width}
height={pieSize.height}
margin={{ left: 0, top: 0, right: 0, bottom: 0 }}
tooltip={{ trigger: "none" }}
>
<PieCenterLabel
value={performance}
color={colorMap.text}
setExpand={setExpand}
/>
</PieChart>
)}
</Box>
<Typography mt={theme.spacing(6)}>
<PieChart audits={audits} />
<Typography fontSize={13} mt="auto">
Values are estimated and may vary.{" "}
<Typography
component="span"
fontSize="inherit"
sx={{
color: theme.palette.primary.main,
fontWeight: 500,
textDecoration: "underline",
textUnderlineOffset: 2,
transition: "all 200ms",
cursor: "pointer",
"&:hover": {
textUnderlineOffset: 4,
},
}}
>
See calculator
</Typography>
</Typography>
</Stack>
<Box
px={theme.spacing(20)}
py={theme.spacing(8)}
height="100%"
flex={1}
sx={{
borderLeft: 1,
borderLeftColor: theme.palette.border.light,
}}
>
<Typography
mb={theme.spacing(6)}
pb={theme.spacing(8)}
color={theme.palette.text.secondary}
textAlign="center"
sx={{
borderBottom: 1,
borderBottomColor: theme.palette.border.light,
borderBottomStyle: "dashed",
}}
>
The{" "}
<Typography
component="span"
sx={{
color: theme.palette.primary.main,
fontWeight: 500,
textDecoration: "underline",
cursor: "pointer",
}}
>
performance score is calculated
</Typography>{" "}
directly from these{" "}
<Typography component="span" fontWeight={600}>
metrics
</Typography>
.
</Typography>
<Box px={theme.spacing(20)} py={theme.spacing(8)} height="100%">
<Stack direction="row" alignItems="center" gap={theme.spacing(6)}>
<IconBox>
<SpedometerIcon />
</IconBox>
<Typography component="h2">Performance Metrics</Typography>
</Stack>
<Stack
direction="row"
flexWrap="wrap"
pt={theme.spacing(8)}
gap={theme.spacing(8)}
mt={theme.spacing(4)}
gap={theme.spacing(4)}
>
{Object.keys(audits).map((key) => {
if (key === "_id") return;
let audit = audits[key];
let metricParams = getColors(audit.score * 100);
let shape = (
<Box
sx={{
width: theme.spacing(6),
height: theme.spacing(6),
borderRadius: "50%",
backgroundColor: metricParams.stroke,
}}
></Box>
);
if (metricParams.shape === "square")
shape = (
<Box
sx={{
width: theme.spacing(6),
height: theme.spacing(6),
backgroundColor: metricParams.stroke,
}}
></Box>
);
else if (metricParams.shape === "triangle")
shape = (
<Box
sx={{
width: 0,
height: 0,
ml: `calc((${theme.spacing(6)} - ${theme.spacing(
4
)}) / -2)`,
borderLeft: `${theme.spacing(4)} solid transparent`,
borderRight: `${theme.spacing(4)} solid transparent`,
borderBottom: `${theme.spacing(6)} solid ${
metricParams.stroke
}`,
}}
></Box>
);
let score = audit.score * 100;
let bg =
score >= 90
? theme.palette.success.main
: score >= 50
? theme.palette.warning.main
: score >= 0
? theme.palette.error.text
: theme.palette.unresolved.main;
// Find the position where the number ends and the unit begins
const match = audit.displayValue.match(
@@ -639,50 +363,55 @@ const PageSpeedDetails = () => {
let unit;
if (match) {
value = match[1];
unit = match[2];
match[2] === "s" ? (unit = "seconds") : (unit = match[2]);
} else {
value = audit.displayValue;
}
return (
<Stack
className="metric"
key={`${key}-box`}
justifyContent="space-between"
direction="row"
gap={theme.spacing(4)}
p={theme.spacing(3)}
border={1}
borderColor={theme.palette.border.light}
borderRadius={4}
>
{shape}
<Box>
<Typography sx={{ lineHeight: 1 }}>
<Typography
fontSize={12}
fontWeight={500}
lineHeight={1}
mb={1}
textTransform="uppercase"
>
{audit.title}
</Typography>
<Typography
component="span"
sx={{
color: metricParams.text,
fontSize: "16px",
fontWeight: 600,
}}
fontSize={14}
fontWeight={500}
>
{value}
<Typography
component="span"
ml="2px"
sx={{
color: theme.palette.text.secondary,
fontSize: 13,
}}
fontSize={12}
color={theme.palette.text.tertiary}
ml={2}
>
{unit}
</Typography>
</Typography>
</Box>
<Box width={4} backgroundColor={bg} borderRadius={4} />
</Stack>
);
})}
</Stack>
</Box>
</Stack>
</ChartBox>
</>
)}
</Stack>
@@ -1,5 +1,4 @@
import { Box, Skeleton, Stack } from "@mui/material";
import { useTheme } from "@emotion/react";
/**
* Renders a skeleton layout.
@@ -7,21 +6,14 @@ import { useTheme } from "@emotion/react";
* @returns {JSX.Element}
*/
const SkeletonLayout = () => {
const theme = useTheme();
return (
<>
<Skeleton variant="rounded" width="15%" height={34} />
<Stack direction="row" gap={theme.spacing(4)}>
<Stack direction="row" gap={4}>
<Skeleton variant="circular" style={{ minWidth: 24, minHeight: 24 }} />
<Box width="85%">
<Skeleton variant="rounded" width="50%" height={24} />
<Skeleton
variant="rounded"
width="50%"
height={18}
sx={{ mt: theme.spacing(4) }}
/>
<Skeleton variant="rounded" width="50%" height={18} sx={{ mt: 4 }} />
</Box>
<Skeleton
variant="rounded"
@@ -33,7 +25,7 @@ const SkeletonLayout = () => {
<Stack
direction="row"
justifyContent="space-between"
gap={theme.spacing(20)}
gap={20}
flexWrap="wrap"
>
<Skeleton variant="rounded" width="30%" height={90} sx={{ flex: 1 }} />
@@ -0,0 +1,90 @@
import { Box, Stack, styled } from "@mui/material";
export const ChartBox = styled(Stack)(({ theme }) => ({
display: "grid",
height: 300,
minWidth: 250,
border: 1,
borderStyle: "solid",
borderColor: theme.palette.border.light,
borderRadius: 4,
borderTopRightRadius: 16,
borderBottomRightRadius: 16,
backgroundColor: theme.palette.background.main,
"& h2": {
color: theme.palette.text.secondary,
fontSize: 15,
fontWeight: 500,
},
"& p": { color: theme.palette.text.tertiary },
"& > :nth-of-type(1)": {
gridColumn: 1,
gridRow: 1,
height: "fit-content",
paddingTop: theme.spacing(8),
paddingLeft: theme.spacing(8),
},
"& > :nth-of-type(2)": { gridColumn: 1, gridRow: 2 },
"& > :nth-of-type(3)": {
gridColumn: 2,
gridRow: "span 2",
padding: theme.spacing(8),
borderLeft: 1,
borderLeftStyle: "solid",
borderLeftColor: theme.palette.border.light,
borderRadius: 16,
backgroundColor: theme.palette.background.main,
background: `linear-gradient(325deg, ${theme.palette.background.accent} 20%, ${theme.palette.background.main} 45%)`,
},
}));
export const IconBox = styled(Box)(({ theme }) => ({
height: 34,
minWidth: 34,
width: 34,
position: "relative",
border: 1,
borderStyle: "solid",
borderColor: theme.palette.border.dark,
borderRadius: 4,
backgroundColor: theme.palette.background.accent,
"& svg": {
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: 20,
height: 20,
"& path": {
stroke: theme.palette.text.tertiary,
},
},
}));
export const StatBox = styled(Box)(({ theme }) => ({
padding: `${theme.spacing(4)} ${theme.spacing(8)}`,
minWidth: 200,
width: 225,
border: 1,
borderStyle: "solid",
borderColor: theme.palette.border.light,
borderRadius: 4,
backgroundColor: theme.palette.background.main,
background: `linear-gradient(340deg, ${theme.palette.background.accent} 20%, ${theme.palette.background.main} 45%)`,
"& h2": {
fontSize: 13,
fontWeight: 500,
color: theme.palette.text.secondary,
textTransform: "uppercase",
},
"& p": {
fontSize: 18,
color: theme.palette.text.primary,
marginTop: theme.spacing(2),
"& span": {
color: theme.palette.text.tertiary,
marginLeft: theme.spacing(2),
fontSize: 15,
},
},
}));
+1 -1
View File
@@ -63,7 +63,7 @@ const darkTheme = createTheme({
uptimeGood: "#ffd600",
uptimeExcellent: "#079455",
},
unresolved: { main: "#4e5ba6", light: "#e2eaf7", bg: "#f2f4f7" },
unresolved: { main: "#664eff", light: "#3a1bff", bg: "#f2f4f7" },
divider: border.light,
other: {
icon: "#e6e6e6",
+1 -1
View File
@@ -51,7 +51,7 @@ const lightTheme = createTheme({
text: "#DC6803",
main: "#fdb022",
light: "#ffecbc",
bg: "#fffcf5",
bg: "#fef8ea",
border: "#fec84b",
},
percentage: {
+2 -2
View File
@@ -1,3 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21 10H3M21 12.5V8.8C21 7.11984 21 6.27976 20.673 5.63803C20.3854 5.07354 19.9265 4.6146 19.362 4.32698C18.7202 4 17.8802 4 16.2 4H7.8C6.11984 4 5.27976 4 4.63803 4.32698C4.07354 4.6146 3.6146 5.07354 3.32698 5.63803C3 6.27976 3 7.11984 3 8.8V17.2C3 18.8802 3 19.7202 3.32698 20.362C3.6146 20.9265 4.07354 21.3854 4.63803 21.673C5.27976 22 6.11984 22 7.8 22H12M16 2V6M8 2V6M14.5 19L16.5 21L21 16.5" stroke="black" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<path d="M6.5 20H5C3.89543 20 3 19.1046 3 18V4C3 2.89543 3.89543 2 5 2H19C20.1046 2 21 2.89543 21 4V18C21 19.1046 20.1046 20 19 20H17.5M12 19C13.6569 19 15 17.6569 15 16C15 14.3431 13.6569 13 12 13C10.3431 13 9 14.3431 9 16C9 17.6569 10.3431 19 12 19ZM12 19L12.0214 18.9998L8.82867 22.1926L6.00024 19.3641L9.01965 16.3447M12 19L15.1928 22.1926L18.0212 19.3641L15.0018 16.3447M9 6H15M7 9.5H17" stroke="#475466" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 594 B

After

Width:  |  Height:  |  Size: 578 B

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22 12H18L15 21L9 3L6 12H2" stroke="black" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 223 B

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 18L2 22V6L9 2M9 18L16 22M9 18V2M16 22L22 18V2L16 6M16 22V6M16 6L9 2" stroke="black" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 267 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.5 5.50001L16 7.00001M11.5 8.50001L13 10M8.49996 11.5L9.99996 13M5.49996 14.5L6.99996 16M2.56561 17.5657L6.43424 21.4344C6.63225 21.6324 6.73125 21.7314 6.84542 21.7685C6.94584 21.8011 7.05401 21.8011 7.15443 21.7685C7.2686 21.7314 7.3676 21.6324 7.56561 21.4344L21.4342 7.56573C21.6322 7.36772 21.7313 7.26872 21.7683 7.15455C21.801 7.05413 21.801 6.94596 21.7683 6.84554C21.7313 6.73137 21.6322 6.63237 21.4342 6.43436L17.5656 2.56573C17.3676 2.36772 17.2686 2.26872 17.1544 2.23163C17.054 2.199 16.9458 2.199 16.8454 2.23163C16.7313 2.26872 16.6322 2.36772 16.4342 2.56573L2.56561 16.4344C2.3676 16.6324 2.2686 16.7314 2.2315 16.8455C2.19887 16.946 2.19887 17.0541 2.2315 17.1546C2.2686 17.2687 2.3676 17.3677 2.56561 17.5657Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 929 B

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12M22 12C22 6.47715 17.5228 2 12 2M22 12H19.5M2 12C2 6.47715 6.47715 2 12 2M2 12H4.5M12 2V4.5M19.0784 5L13.4999 10.5M19.0784 19.0784L18.8745 18.8745C18.1827 18.1827 17.8368 17.8368 17.4331 17.5894C17.0753 17.3701 16.6851 17.2085 16.2769 17.1105C15.8166 17 15.3274 17 14.349 17L9.65096 17C8.6726 17 8.18342 17 7.72307 17.1106C7.31493 17.2086 6.92475 17.3702 6.56686 17.5895C6.1632 17.8369 5.8173 18.1828 5.12549 18.8746L4.92163 19.0784M4.92163 5L6.65808 6.73645M14 12C14 13.1046 13.1046 14 12 14C10.8954 14 10 13.1046 10 12C10 10.8954 10.8954 10 12 10C13.1046 10 14 10.8954 14 12Z" stroke="black" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 834 B