feat: Devices and General
This commit is contained in:
parent
35a84f301f
commit
31a8b613bb
@ -1,9 +1,8 @@
|
|||||||
import { makeRequest } from "@frontend/kitui";
|
import { makeRequest } from "@frontend/kitui";
|
||||||
|
|
||||||
import type { LoginRequest, LoginResponse } from "@frontend/kitui";
|
import { parseAxiosError } from "@utils/parse-error";
|
||||||
import { parseAxiosError } from "../utils/parse-error";
|
|
||||||
|
|
||||||
const apiUrl = process.env.REACT_APP_DOMAIN + "/statistic";
|
const apiUrl = process.env.REACT_APP_DOMAIN + "/squiz/statistic";
|
||||||
|
|
||||||
export type DevicesResponse = {
|
export type DevicesResponse = {
|
||||||
device: Record<string, number>;
|
device: Record<string, number>;
|
||||||
@ -11,13 +10,26 @@ export type DevicesResponse = {
|
|||||||
browser: Record<string, number>;
|
browser: Record<string, number>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getDevicesList = async (
|
export type GeneralResponse = {
|
||||||
|
open: Record<string, number>;
|
||||||
|
result: Record<string, number>;
|
||||||
|
avtime: Record<string, number>;
|
||||||
|
conversation: Record<string, number>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type QuestionsResponse = {
|
||||||
|
funnel: number[];
|
||||||
|
results: Record<string, number>;
|
||||||
|
questions: Record<string, Record<string, number>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDevices = async (
|
||||||
quizId: string,
|
quizId: string,
|
||||||
): Promise<[any | null, string?]> => {
|
): Promise<[DevicesResponse | null, string?]> => {
|
||||||
try {
|
try {
|
||||||
const devicesResponse = await makeRequest<unknown, DevicesResponse>({
|
const devicesResponse = await makeRequest<unknown, DevicesResponse>({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: `${apiUrl}/devices?quizID=${quizId}`,
|
url: `${apiUrl}/${quizId}/devices`,
|
||||||
useToken: false,
|
useToken: false,
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
});
|
});
|
||||||
@ -26,6 +38,44 @@ export const getDevicesList = async (
|
|||||||
} catch (nativeError) {
|
} catch (nativeError) {
|
||||||
const [error] = parseAxiosError(nativeError);
|
const [error] = parseAxiosError(nativeError);
|
||||||
|
|
||||||
return [null, `Не получить статистику о девайсах. ${error}`];
|
return [null, `Не удалось получить статистику о девайсах. ${error}`];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getGeneral = async (
|
||||||
|
quizId: string,
|
||||||
|
): Promise<[GeneralResponse | null, string?]> => {
|
||||||
|
try {
|
||||||
|
const generalResponse = await makeRequest<unknown, GeneralResponse>({
|
||||||
|
method: "POST",
|
||||||
|
url: `${apiUrl}/${quizId}/general`,
|
||||||
|
useToken: false,
|
||||||
|
withCredentials: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return [generalResponse];
|
||||||
|
} catch (nativeError) {
|
||||||
|
const [error] = parseAxiosError(nativeError);
|
||||||
|
|
||||||
|
return [null, `Не удалось получить ключевые метрики. ${error}`];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getQuestions = async (
|
||||||
|
quizId: string,
|
||||||
|
): Promise<[QuestionsResponse | null, string?]> => {
|
||||||
|
try {
|
||||||
|
const questionsResponse = await makeRequest<unknown, QuestionsResponse>({
|
||||||
|
method: "POST",
|
||||||
|
url: `${apiUrl}/${quizId}/questions`,
|
||||||
|
useToken: false,
|
||||||
|
withCredentials: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return [questionsResponse];
|
||||||
|
} catch (nativeError) {
|
||||||
|
const [error] = parseAxiosError(nativeError);
|
||||||
|
|
||||||
|
return [null, `Не удалось получить статистику по результатам. ${error}`];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState } from "react";
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
@ -11,39 +11,16 @@ import {
|
|||||||
import { DatePicker } from "@mui/x-date-pickers";
|
import { DatePicker } from "@mui/x-date-pickers";
|
||||||
import { LineChart } from "@mui/x-charts";
|
import { LineChart } from "@mui/x-charts";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { enqueueSnackbar } from "notistack";
|
|
||||||
|
|
||||||
import HeaderFull from "@ui_kit/Header/HeaderFull";
|
import HeaderFull from "@ui_kit/Header/HeaderFull";
|
||||||
import SectionWrapper from "@ui_kit/SectionWrapper";
|
import SectionWrapper from "@ui_kit/SectionWrapper";
|
||||||
|
|
||||||
|
import { General } from "./General";
|
||||||
import { Devices } from "./Devices";
|
import { Devices } from "./Devices";
|
||||||
|
|
||||||
import { getDevicesList } from "@api/statistic";
|
|
||||||
|
|
||||||
import CalendarIcon from "@icons/CalendarIcon";
|
import CalendarIcon from "@icons/CalendarIcon";
|
||||||
|
|
||||||
import type { DevicesResponse } from "@api/statistic";
|
|
||||||
|
|
||||||
const DEVICES_MOCK: DevicesResponse = {
|
|
||||||
device: {
|
|
||||||
additionalProp1: 10,
|
|
||||||
additionalProp2: 20,
|
|
||||||
additionalProp3: 30,
|
|
||||||
},
|
|
||||||
os: {
|
|
||||||
additionalProp1: 20,
|
|
||||||
additionalProp2: 78,
|
|
||||||
additionalProp3: 2,
|
|
||||||
},
|
|
||||||
browser: {
|
|
||||||
additionalProp1: 55,
|
|
||||||
additionalProp2: 11,
|
|
||||||
additionalProp3: 3,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function Analytics() {
|
export default function Analytics() {
|
||||||
const [devices, setDevices] = useState<DevicesResponse>(DEVICES_MOCK);
|
|
||||||
const [isOpen, setOpen] = useState(false);
|
const [isOpen, setOpen] = useState(false);
|
||||||
const [isOpenEnd, setOpenEnd] = useState(false);
|
const [isOpenEnd, setOpenEnd] = useState(false);
|
||||||
|
|
||||||
@ -80,8 +57,8 @@ export default function Analytics() {
|
|||||||
const now = moment();
|
const now = moment();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<HeaderFull isRequest={true} />
|
<HeaderFull isRequest />
|
||||||
<SectionWrapper component={"section"} sx={{ paddingTop: "60px" }}>
|
<SectionWrapper component={"section"} sx={{ padding: "60px 20px" }}>
|
||||||
<Typography variant={"h4"}>Аналитика</Typography>
|
<Typography variant={"h4"}>Аналитика</Typography>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
@ -194,22 +171,8 @@ export default function Analytics() {
|
|||||||
Сбросить
|
Сбросить
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<General />
|
||||||
<Paper>
|
<Devices />
|
||||||
<Typography>Открыли quiz</Typography>
|
|
||||||
<LineChart
|
|
||||||
xAxis={[{ data: [1, 2, 3, 5, 8, 10] }]}
|
|
||||||
series={[
|
|
||||||
{
|
|
||||||
data: [2, 5.5, 2, 8.5, 1.5, 5],
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
width={500}
|
|
||||||
height={300}
|
|
||||||
/>
|
|
||||||
</Paper>
|
|
||||||
</Box>
|
|
||||||
<Devices devices={devices} />
|
|
||||||
</SectionWrapper>
|
</SectionWrapper>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -1,58 +1,166 @@
|
|||||||
import { Box, Paper, useMediaQuery, useTheme } from "@mui/material";
|
import { useEffect, useState } from "react";
|
||||||
|
import { Box, Paper, Typography, useMediaQuery, useTheme } from "@mui/material";
|
||||||
import { PieChart } from "@mui/x-charts";
|
import { PieChart } from "@mui/x-charts";
|
||||||
|
import { enqueueSnackbar } from "notistack";
|
||||||
|
|
||||||
|
import { getDevices } from "@api/statistic";
|
||||||
|
|
||||||
import type { DevicesResponse } from "@api/statistic";
|
import type { DevicesResponse } from "@api/statistic";
|
||||||
|
|
||||||
type DevicesProps = {
|
|
||||||
devices: DevicesResponse;
|
|
||||||
};
|
|
||||||
|
|
||||||
type DeviceProps = {
|
type DeviceProps = {
|
||||||
title: string;
|
title: string;
|
||||||
devices: Record<string, number>;
|
devices: Record<string, number>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const COLORS: Record<number, string> = {
|
||||||
|
0: "#7E2AEA",
|
||||||
|
1: "#FA7738",
|
||||||
|
2: "#62BB1C",
|
||||||
|
3: "#0886FB",
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEVICES_MOCK: DevicesResponse = {
|
||||||
|
device: {
|
||||||
|
PC: 75,
|
||||||
|
Mobile: 25,
|
||||||
|
},
|
||||||
|
os: {
|
||||||
|
Windows: 44,
|
||||||
|
AndroidOS: 25,
|
||||||
|
"OS X": 19,
|
||||||
|
Linux: 13,
|
||||||
|
},
|
||||||
|
browser: {
|
||||||
|
Chrome: 75,
|
||||||
|
Firefox: 25,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const Device = ({ title, devices }: DeviceProps) => {
|
const Device = ({ title, devices }: DeviceProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const data = Object.entries(devices).map(([id, value], index) => ({
|
||||||
|
id,
|
||||||
|
value,
|
||||||
|
color: COLORS[index],
|
||||||
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper>
|
<Paper
|
||||||
<Box sx={{ height: "235px" }}>
|
sx={{
|
||||||
<PieChart
|
overflow: "hidden",
|
||||||
series={[
|
minHeight: "500px",
|
||||||
{
|
display: "flex",
|
||||||
data: Object.entries(devices).map(([id, value]) => ({
|
flexDirection: "column",
|
||||||
id,
|
gap: "30px",
|
||||||
value,
|
borderRadius: "12px",
|
||||||
})),
|
boxShadow: "0 0 20px rgba(0, 0, 0, 0.15)",
|
||||||
innerRadius: 50,
|
}}
|
||||||
},
|
>
|
||||||
]}
|
<Typography sx={{ margin: "20px" }}>{title}</Typography>
|
||||||
/>
|
<Box sx={{ flexGrow: 1 }}>
|
||||||
|
<Box sx={{ display: "flex", alignItems: "center" }}>
|
||||||
|
<PieChart
|
||||||
|
height={245}
|
||||||
|
width={245}
|
||||||
|
margin={{ right: 0 }}
|
||||||
|
series={[{ data, innerRadius: 50 }]}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{ background: theme.palette.background.default, padding: "20px" }}
|
||||||
|
>
|
||||||
|
{data.map(({ id, value, color }) => (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
marginBottom: "10px",
|
||||||
|
"&:last-child": { margin: 0 },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
flexGrow: 1,
|
||||||
|
position: "relative",
|
||||||
|
paddingLeft: "30px",
|
||||||
|
"&::before": {
|
||||||
|
content: "''",
|
||||||
|
display: "block",
|
||||||
|
position: "absolute",
|
||||||
|
left: "0",
|
||||||
|
background: color,
|
||||||
|
height: "20px",
|
||||||
|
width: "20px",
|
||||||
|
borderRadius: "6px",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{id}
|
||||||
|
</Typography>
|
||||||
|
<Typography>{value} %</Typography>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Devices = ({ devices }: DevicesProps) => {
|
export const Devices = () => {
|
||||||
|
const [devices, setDevices] = useState<DevicesResponse>(DEVICES_MOCK);
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
|
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down(600));
|
const isMobile = useMediaQuery(theme.breakpoints.down(700));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const requestDevices = async () => {
|
||||||
|
const [devicesResponse, devicesError] = await getDevices("14761");
|
||||||
|
|
||||||
|
if (devicesError) {
|
||||||
|
enqueueSnackbar(devicesError);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!devicesResponse) {
|
||||||
|
enqueueSnackbar("Список девайсов пуст.");
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setDevices(devicesResponse);
|
||||||
|
};
|
||||||
|
|
||||||
|
// requestDevices();
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box sx={{ marginTop: "120px" }}>
|
||||||
sx={{
|
<Typography
|
||||||
display: "grid",
|
component="h3"
|
||||||
gridTemplateColumns: isTablet
|
sx={{
|
||||||
? isMobile
|
fontSize: "24px",
|
||||||
? "1fr"
|
fontWeight: "bold",
|
||||||
: "1fr 1fr"
|
color: theme.palette.text.primary,
|
||||||
: "1fr 1fr 1fr",
|
}}
|
||||||
gap: "20px",
|
>
|
||||||
marginTop: "30px",
|
Статистика пользователей
|
||||||
}}
|
</Typography>
|
||||||
>
|
<Box
|
||||||
<Device title="Устройства" devices={devices.device} />
|
sx={{
|
||||||
<Device title="Операционные системы" devices={devices.os} />
|
display: "grid",
|
||||||
<Device title="Браузеры" devices={devices.browser} />
|
gridTemplateColumns: isTablet
|
||||||
|
? isMobile
|
||||||
|
? "1fr"
|
||||||
|
: "1fr 1fr"
|
||||||
|
: "1fr 1fr 1fr",
|
||||||
|
gap: "20px",
|
||||||
|
marginTop: "30px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Device title="Устройства" devices={devices.device} />
|
||||||
|
<Device title="Операционные системы" devices={devices.os} />
|
||||||
|
<Device title="Браузеры" devices={devices.browser} />
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
164
src/pages/Analytics/General.tsx
Normal file
164
src/pages/Analytics/General.tsx
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Box, Paper, Typography, useMediaQuery, useTheme } from "@mui/material";
|
||||||
|
import { LineChart } from "@mui/x-charts";
|
||||||
|
import { enqueueSnackbar } from "notistack";
|
||||||
|
|
||||||
|
import { getGeneral } from "@api/statistic";
|
||||||
|
|
||||||
|
import type { GeneralResponse } from "@api/statistic";
|
||||||
|
|
||||||
|
type GeneralProps = {
|
||||||
|
title: string;
|
||||||
|
general: Record<string, number>;
|
||||||
|
color: string;
|
||||||
|
numberType: "sum" | "percent";
|
||||||
|
};
|
||||||
|
|
||||||
|
const COLORS: Record<number, string> = {
|
||||||
|
0: "#61BB1A",
|
||||||
|
1: "#7E2AEA",
|
||||||
|
2: "#FB5607",
|
||||||
|
3: "#0886FB",
|
||||||
|
};
|
||||||
|
|
||||||
|
const GENERAL_MOCK: GeneralResponse = {
|
||||||
|
open: {
|
||||||
|
100: 20,
|
||||||
|
50: 10,
|
||||||
|
60: 5,
|
||||||
|
},
|
||||||
|
result: {
|
||||||
|
100: 90,
|
||||||
|
10: 3,
|
||||||
|
50: 48,
|
||||||
|
},
|
||||||
|
avtime: {
|
||||||
|
100: 0,
|
||||||
|
2000: 550,
|
||||||
|
60: 0,
|
||||||
|
},
|
||||||
|
conversation: {
|
||||||
|
100: 50,
|
||||||
|
1000: 50,
|
||||||
|
10000: 50,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const GeneralItem = ({ title, general, color, numberType }: GeneralProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const isMobile = useMediaQuery(theme.breakpoints.down(700));
|
||||||
|
|
||||||
|
const numberValue =
|
||||||
|
numberType === "sum"
|
||||||
|
? Object.values(general).reduce((total, item) => total + item, 0)
|
||||||
|
: Object.entries(general).reduce(
|
||||||
|
(total, [key, value]) => total + (value / Number(key)) * 100,
|
||||||
|
0,
|
||||||
|
) / Object.keys(general).length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper
|
||||||
|
sx={{
|
||||||
|
overflow: "hidden",
|
||||||
|
borderRadius: "12px",
|
||||||
|
boxShadow: "0 0 20px rgba(0, 0, 0, 0.15)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography sx={{ margin: "20px 20px 0" }}>{title}</Typography>
|
||||||
|
<Typography sx={{ margin: "10px 20px 0", fontWeight: "bold" }}>
|
||||||
|
{numberType === "sum" ? numberValue : `${numberValue.toFixed()}%`}
|
||||||
|
</Typography>
|
||||||
|
<LineChart
|
||||||
|
xAxis={[{ data: Object.keys(general) }]}
|
||||||
|
series={[{ data: Object.values(general) }]}
|
||||||
|
height={220}
|
||||||
|
colors={[color]}
|
||||||
|
sx={{
|
||||||
|
transform: isMobile ? "scale(1.1)" : "scale(1.2)",
|
||||||
|
"& .MuiChartsAxis-tickContainer": { display: "none" },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const General = () => {
|
||||||
|
const [general, setGeneral] = useState<GeneralResponse>(GENERAL_MOCK);
|
||||||
|
const theme = useTheme();
|
||||||
|
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
|
||||||
|
const isMobile = useMediaQuery(theme.breakpoints.down(700));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const requestGeneral = async () => {
|
||||||
|
const [generalResponse, generalError] = await getGeneral("14761");
|
||||||
|
|
||||||
|
if (generalError) {
|
||||||
|
enqueueSnackbar(generalError);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!generalResponse) {
|
||||||
|
enqueueSnackbar("Список девайсов пуст.");
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setGeneral(generalResponse);
|
||||||
|
};
|
||||||
|
|
||||||
|
// requestGeneral();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ marginTop: "45px" }}>
|
||||||
|
<Typography
|
||||||
|
component="h3"
|
||||||
|
sx={{
|
||||||
|
fontSize: "24px",
|
||||||
|
fontWeight: "bold",
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Ключевые метрики
|
||||||
|
</Typography>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: isTablet
|
||||||
|
? isMobile
|
||||||
|
? "1fr"
|
||||||
|
: "1fr 1fr"
|
||||||
|
: "1fr 1fr 1fr",
|
||||||
|
gap: "20px",
|
||||||
|
marginTop: "40px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<GeneralItem
|
||||||
|
title="Открыли квиз"
|
||||||
|
numberType="sum"
|
||||||
|
general={general.open}
|
||||||
|
color={COLORS[0]}
|
||||||
|
/>
|
||||||
|
<GeneralItem
|
||||||
|
title="Получено заявок"
|
||||||
|
numberType="sum"
|
||||||
|
general={general.result}
|
||||||
|
color={COLORS[1]}
|
||||||
|
/>
|
||||||
|
<GeneralItem
|
||||||
|
title="Конверсия"
|
||||||
|
numberType="percent"
|
||||||
|
general={general.conversation}
|
||||||
|
color={COLORS[2]}
|
||||||
|
/>
|
||||||
|
<GeneralItem
|
||||||
|
title="Среднее время прохождения квиза"
|
||||||
|
numberType="percent"
|
||||||
|
general={general.avtime}
|
||||||
|
color={COLORS[3]}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
@ -21,7 +21,11 @@ import { ToTariffsButton } from "@ui_kit/Toolbars/ToTariffsButton";
|
|||||||
import ArrowLeft from "@icons/questionsPage/arrowLeft";
|
import ArrowLeft from "@icons/questionsPage/arrowLeft";
|
||||||
import { cleanAuthTicketData } from "@root/ticket";
|
import { cleanAuthTicketData } from "@root/ticket";
|
||||||
|
|
||||||
export default function HeaderFull({ isRequest }: boolean) {
|
type HeaderFullProps = {
|
||||||
|
isRequest?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function HeaderFull({ isRequest = false }: HeaderFullProps) {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
|
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
|
||||||
|
Loading…
Reference in New Issue
Block a user