Merge branch 'analytics' into dev

This commit is contained in:
Nastya 2024-03-27 09:36:43 +03:00
commit a980fe5fe4
18 changed files with 1487 additions and 94 deletions

@ -10,6 +10,7 @@
"@frontend/squzanswerer": "^1.0.6",
"@mui/icons-material": "^5.10.14",
"@mui/material": "^5.10.14",
"@mui/x-charts": "^6.19.5",
"@mui/x-date-pickers": "^6.16.1",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^13.0.0",
@ -48,6 +49,7 @@
"react-error-boundary": "^4.0.11",
"react-image-crop": "^10.1.5",
"react-image-file-resizer": "^0.4.8",
"react-lazily": "^0.9.2",
"react-rnd": "^10.4.1",
"react-router-dom": "^6.6.2",
"react-scripts": "5.0.1",

@ -1,11 +1,10 @@
import { Suspense, lazy } from "react";
import { lazily } from "react-lazily";
import ContactFormModal from "@ui_kit/ContactForm";
import ImageCrop from "@ui_kit/Modal/ImageCrop";
import dayjs from "dayjs";
import "dayjs/locale/ru";
import SigninDialog from "./pages/auth/Signin";
import SignupDialog from "./pages/auth/Signup";
import ViewPage from "./pages/ViewPublicationPage";
import { DesignPage } from "./pages/DesignPage/DesignPage";
import {
Route,
Routes,
@ -14,16 +13,8 @@ import {
Navigate,
} from "react-router-dom";
import "./index.css";
import ContactFormPage from "./pages/ContactFormPage/ContactFormPage";
import InstallQuiz from "./pages/InstallQuiz/InstallQuiz";
import Landing from "./pages/Landing/Landing";
import QuestionsPage from "./pages/Questions/QuestionsPage";
import { Result } from "./pages/ResultPage/Result";
import { ResultSettings } from "./pages/ResultPage/ResultSettings";
import MyQuizzesFull from "./pages/createQuize/MyQuizzesFull";
import Main from "./pages/main";
import EditPage from "./pages/startPage/EditPage";
import { Tariffs } from "./pages/Tariffs/Tariffs";
import {
clearAuthToken,
getMessageFromFetchError,
@ -50,12 +41,48 @@ import { isAxiosError } from "axios";
import { useEffect, useLayoutEffect, useRef } from "react";
import RecoverPassword from "./pages/auth/RecoverPassword";
import OutdatedLink from "./pages/auth/OutdatedLink";
import { QuizAnswersPage } from "./pages/QuizAnswersPage/QuizAnswersPage";
import type { OriginalUserAccount } from "@root/user";
import ChatImageNewWindow from "@ui_kit/FloatingSupportChat/ChatImageNewWindow";
export function useUserAccountFetcher<T = UserAccount>({
import type { SuspenseProps } from "react";
const MyQuizzesFull = lazy(() => import("./pages/createQuize/MyQuizzesFull"));
const ViewPage = lazy(() => import("./pages/ViewPublicationPage"));
const Analytics = lazy(() => import("./pages/Analytics/Analytics"));
const EditPage = lazy(() => import("./pages/startPage/EditPage"));
const { Tariffs } = lazily(() => import("./pages/Tariffs/Tariffs"));
const { DesignPage } = lazily(() => import("./pages/DesignPage/DesignPage"));
const { QuizAnswersPage } = lazily(
() => import("./pages/QuizAnswersPage/QuizAnswersPage"),
);
const ChatImageNewWindow = lazy(
() => import("@ui_kit/FloatingSupportChat/ChatImageNewWindow"),
);
dayjs.locale("ru");
const routeslink = [
{
path: "/edit",
page: EditPage,
header: true,
sidebar: true,
footer: true,
},
{
path: "/design",
page: DesignPage,
header: true,
sidebar: true,
footer: true,
},
] as const;
const LazyLoading = ({ children, fallback }: SuspenseProps) => (
<Suspense fallback={fallback ?? <></>}>{children}</Suspense>
);
function useUserAccountFetcher<T = UserAccount>({
onError,
onNewUserAccount,
url,
@ -107,25 +134,6 @@ export function useUserAccountFetcher<T = UserAccount>({
}, [url, userId]);
}
dayjs.locale("ru");
const routeslink = [
{
path: "/edit",
page: EditPage,
header: true,
sidebar: true,
footer: true,
},
{
path: "/design",
page: DesignPage,
header: true,
sidebar: true,
footer: true,
},
] as const;
export default function App() {
const userId = useUserStore((state) => state.userId);
const location = useLocation();
@ -239,10 +247,26 @@ export default function App() {
/>
}
/>
<Route path="/list" element={<MyQuizzesFull />} />
<Route path={"/view/:quizId"} element={<ViewPage />} />
<Route path={"/tariffs"} element={<Tariffs />} />
<Route path={"/results/:quizId"} element={<QuizAnswersPage />} />
<Route
path="/list"
element={<LazyLoading children={<MyQuizzesFull />} />}
/>
<Route
path={"/view/:quizId"}
element={<LazyLoading children={<ViewPage />} />}
/>
<Route
path={"/tariffs"}
element={<LazyLoading children={<Tariffs />} />}
/>
<Route
path={"/analytics"}
element={<LazyLoading children={<Analytics />} />}
/>
<Route
path={"/results/:quizId"}
element={<LazyLoading children={<QuizAnswersPage />} />}
/>
<Route element={<PrivateRoute />}>
<Route path={"/image/:srcImage"} element={<ChatImageNewWindow />} />
{routeslink.map((e, i) => (
@ -250,11 +274,15 @@ export default function App() {
key={i}
path={e.path}
element={
<Main
Page={e.page}
header={e.header}
sidebar={e.sidebar}
footer={e.footer}
<LazyLoading
children={
<Main
Page={e.page}
header={e.header}
sidebar={e.sidebar}
footer={e.footer}
/>
}
/>
}
/>

81
src/api/statistic.ts Normal file

@ -0,0 +1,81 @@
import { makeRequest } from "@frontend/kitui";
import { parseAxiosError } from "@utils/parse-error";
const apiUrl = process.env.REACT_APP_DOMAIN + "/squiz/statistic";
export type DevicesResponse = {
device: Record<string, number>;
os: Record<string, number>;
browser: Record<string, number>;
};
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,
): Promise<[DevicesResponse | null, string?]> => {
try {
const devicesResponse = await makeRequest<unknown, DevicesResponse>({
method: "POST",
url: `${apiUrl}/${quizId}/devices`,
useToken: false,
withCredentials: true,
});
return [devicesResponse];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
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}`];
}
};

@ -0,0 +1,16 @@
<svg
width="30"
height="30"
viewBox="0 0 30 30"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect width="30" height="30" rx="6" fill="#FEDFD0" />
<path
d="M18.7891 11.2006L14.526 15.4943M10.3154 15.2084L14.526 19.2993L22.7891 10.7006M7.21053 15.4144L11 19.0962"
stroke="#FC712F"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>

After

Width:  |  Height:  |  Size: 395 B

@ -0,0 +1,3 @@
<svg width="7" height="12" viewBox="0 0 7 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 1.5L1 6L6 10.5" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 212 B

@ -0,0 +1,23 @@
<svg
width="30"
height="30"
viewBox="0 0 30 30"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect width="30" height="30" rx="6" fill="#FEDFD0" />
<path
d="M11.5 10.5L15.5 15L11.5 19.5"
stroke="#FC712F"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M15.5 10.5L19.5 15L15.5 19.5"
stroke="#FC712F"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>

After

Width:  |  Height:  |  Size: 468 B

@ -0,0 +1,3 @@
<svg width="14" height="7" viewBox="0 0 14 7" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 1L7 6L13 1" stroke="#7E2AEA" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 210 B

@ -0,0 +1,3 @@
<svg width="7" height="12" viewBox="0 0 7 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 1.5L6 6L1 10.5" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 212 B

@ -0,0 +1,181 @@
import { useState } from "react";
import {
Box,
Button,
IconButton,
Paper,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import { DatePicker } from "@mui/x-date-pickers";
import { LineChart } from "@mui/x-charts";
import moment from "moment";
import HeaderFull from "@ui_kit/Header/HeaderFull";
import SectionWrapper from "@ui_kit/SectionWrapper";
import { General } from "./General";
import { AnswersStatistics } from "./Answers";
import { Devices } from "./Devices";
import CalendarIcon from "@icons/CalendarIcon";
export default function Analytics() {
const [isOpen, setOpen] = useState(false);
const [isOpenEnd, setOpenEnd] = useState(false);
const theme = useTheme();
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
const isMobile = useMediaQuery(theme.breakpoints.down(600));
const handleClose = () => {
setOpen(false);
};
const handleOpen = () => {
setOpen(true);
};
const onAdornmentClick = () => {
setOpen((old) => !old);
if (isOpenEnd === true) {
handleCloseEnd();
}
};
const handleCloseEnd = () => {
setOpenEnd(false);
};
const handleOpenEnd = () => {
setOpenEnd(true);
};
const onAdornmentClickEnd = () => {
setOpenEnd((old) => !old);
if (isOpen === true) {
handleClose();
}
};
const now = moment();
return (
<>
<HeaderFull isRequest />
<SectionWrapper component={"section"} sx={{ padding: "60px 20px" }}>
<Typography variant={"h4"}>Аналитика</Typography>
<Box
sx={{
display: "flex",
gap: isMobile ? "15px" : "20px",
alignItems: "end",
justifyContent: "space-between",
padding: "40px 0 20px",
borderBottom: `1px solid ${theme.palette.grey2.main}`,
}}
>
<Box sx={{ display: "flex", gap: isMobile ? "15px" : "20px" }}>
<Box>
<Typography
sx={{
fontSize: "16px",
marginBottom: "5px",
fontWeight: 500,
color: "4D4D4D",
}}
>
Дата начала
</Typography>
<DatePicker
open={isOpen}
onClose={handleClose}
onOpen={handleOpen}
// defaultValue={now}
sx={{
width: isMobile ? "146px" : "169px",
"& .MuiOutlinedInput-root": {
borderRadius: "10px",
fontSize: "16px",
},
"& .MuiInputBase-input": {
padding: "12.5px 14px",
},
}}
slotProps={{
textField: {
InputProps: {
endAdornment: (
<IconButton onClick={onAdornmentClick}>
<CalendarIcon />
</IconButton>
),
},
},
}}
/>
</Box>
<Box>
<Typography
sx={{
fontSize: "16px",
marginBottom: "5px",
fontWeight: 500,
color: "4D4D4D",
}}
>
Дата окончания
</Typography>
<DatePicker
open={isOpenEnd}
onClose={handleCloseEnd}
onOpen={handleOpenEnd}
// defaultValue={now}
sx={{
width: isMobile ? "146px" : "169px",
"& .MuiOutlinedInput-root": {
borderRadius: "10px",
fontSize: "16px",
},
"& .MuiInputBase-input": {
padding: "12.5px 14px",
},
}}
slotProps={{
textField: {
InputProps: {
endAdornment: (
<IconButton onClick={onAdornmentClickEnd}>
<CalendarIcon />
</IconButton>
),
},
},
}}
/>
</Box>
</Box>
<Button
variant="outlined"
sx={{
minWidth: isMobile ? "144px" : "180px",
px: isMobile ? "31px" : "43px",
color: theme.palette.brightPurple.main,
"&:hover": {
backgroundColor: "#581CA7",
color: "#FFFFFF",
},
"&:active": {
backgroundColor: "#000000",
color: "#FFFFFF",
},
}}
>
Сбросить
</Button>
</Box>
<General />
<AnswersStatistics />
<Devices />
</SectionWrapper>
</>
);
}

@ -0,0 +1,259 @@
import { useState } from "react";
import {
Box,
Paper,
Typography,
LinearProgress,
Pagination as MuiPagination,
PaginationItem,
Input,
ButtonBase,
useTheme,
useMediaQuery,
} from "@mui/material";
import { ReactComponent as DoubleCheckIcon } from "@icons/Analytics/doubleCheck.svg";
import { ReactComponent as NextIcon } from "@icons/Analytics/next.svg";
import { ReactComponent as LeftArrowIcon } from "@icons/Analytics/leftArrow.svg";
import { ReactComponent as RightArrowIcon } from "@icons/Analytics/rightArrow.svg";
import type { PaginationRenderItemParams } from "@mui/material";
type AnswerProps = {
title: string;
percent: number;
highlight?: boolean;
};
const ANSWERS_MOCK: Record<string, number> = {
"Добавьте ответ": 67,
"Вопрос пропущен": 7,
Другое: 27,
};
const Answer = ({ title, percent, highlight }: AnswerProps) => {
const theme = useTheme();
return (
<Box sx={{ padding: "15px 25px" }}>
<Box
sx={{
position: "relative",
display: "flex",
gap: "15px",
alignItems: "center",
flexGrow: 1,
}}
>
<LinearProgress
variant="determinate"
title={title}
value={percent}
sx={{
width: "100%",
height: "44px",
background: theme.palette.background.default,
borderRadius: "10px",
border: `1px solid ${highlight ? theme.palette.brightPurple.main : theme.palette.grey2.main}`,
"& > span": { background: highlight ? "#D9C0F9" : "#9A9AAF1A" },
"&::before": {
content: `"${title}"`,
position: "absolute",
zIndex: 1,
left: "20px",
top: "50%",
transform: "translateY(-50%)",
color: highlight
? theme.palette.brightPurple.main
: theme.palette.grey3.main,
},
}}
/>
<Box sx={{ minWidth: 35 }}>
<Typography
sx={{
minWidth: "45px",
fontWeight: highlight ? "bold" : "normal",
color: highlight
? theme.palette.brightPurple.main
: theme.palette.text.primary,
}}
>{`${percent}%`}</Typography>
</Box>
</Box>
</Box>
);
};
const Pagination = () => {
const [count, setCount] = useState<number>(50);
const [page, setPage] = useState<number>(1);
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(855));
const getPaginationItem = (props: PaginationRenderItemParams) => {
if (props.type === "start-ellipsis" || props.type === "end-ellipsis") {
return;
}
if (props.type !== "previous" && props.type !== "next" && props.page) {
if (isMobile) {
const allowedPages = [
page - 1 < 1 ? page + 2 : page - 1,
page,
page + 1 > count ? page - 2 : page + 1,
];
if (!allowedPages.includes(props.page)) {
return;
}
}
const allowedPages = [
page - 2 < 1 ? page + 3 : page - 2,
page - 1 < 1 ? page + 4 : page - 1,
page,
page + 1 > count ? page - 4 : page + 1,
page + 2 > count ? page - 3 : page + 2,
];
if (!allowedPages.includes(props.page)) {
return;
}
}
return (
<PaginationItem
component="div"
slots={{ previous: LeftArrowIcon, next: RightArrowIcon }}
{...{ ...props, variant: undefined }}
/>
);
};
return (
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "center",
marginTop: "30px",
}}
>
<Input
disableUnderline
placeholder="1"
value={page}
onChange={({ target }) =>
setPage(Number(target.value.replace(/\D/, "")))
}
sx={{
height: "30px",
width: "65px",
borderRadius: "5px",
padding: "10px",
marginRight: "5px",
boxShadow: "0 0 20px rgba(0, 0, 0, 0.15)",
backgroundColor: theme.palette.background.paper,
}}
/>
<MuiPagination
page={page}
count={count}
siblingCount={isMobile ? 1 : 2}
boundaryCount={isMobile ? 1 : 2}
onChange={(_, page) => setPage(page)}
renderItem={getPaginationItem}
sx={{
"& .MuiButtonBase-root": {
borderRadius: "5px",
margin: "0 5px",
height: "30px",
background: theme.palette.background.paper,
boxShadow: "0 0 20px rgba(0, 0, 0, 0.15)",
"&.Mui-selected": {
background: theme.palette.background.paper,
color: theme.palette.brightPurple.main,
},
"&.MuiPaginationItem-previousNext": {
margin: "0 15px",
background: theme.palette.brightPurple.main,
},
},
}}
/>
</Box>
);
};
export const Answers = () => {
const [answers, setAnswers] = useState<Record<string, number>>(ANSWERS_MOCK);
const theme = useTheme();
return (
<Box sx={{ flexGrow: 1 }}>
<Paper
sx={{
borderRadius: "12px",
boxShadow: "0 0 20px rgba(0, 0, 0, 0.15)",
marginTop: "20px",
}}
>
<Box
sx={{
display: "flex",
gap: "10px",
alignItems: "center",
padding: "25px",
}}
>
<Typography
component="h3"
sx={{
flexGrow: 1,
position: "relative",
fontSize: "18px",
fontWeight: "bold",
paddingLeft: "40px",
color: theme.palette.text.primary,
"&::before": {
content: "'1'",
position: "absolute",
top: "50%",
left: "0",
transform: "translateY(-50%)",
display: "flex",
alignItems: "center",
justifyContent: "center",
width: "30px",
height: "30px",
borderRadius: "50%",
fontSize: "14px",
fontWeight: "normal",
background: "#EEE4FC",
color: theme.palette.brightPurple.main,
},
}}
>
Заголовок вопроса. Варианты ответов
</Typography>
<ButtonBase>
<DoubleCheckIcon />
</ButtonBase>
<ButtonBase>
<NextIcon />
</ButtonBase>
</Box>
{Object.entries(answers).map(([title, percent], index) => (
<Answer
key={title}
title={title}
percent={percent}
highlight={!index}
/>
))}
</Paper>
<Pagination />
</Box>
);
};

@ -0,0 +1,140 @@
import { useEffect, useState } from "react";
import {
Box,
Paper,
Typography,
LinearProgress,
useTheme,
useMediaQuery,
} from "@mui/material";
import { enqueueSnackbar } from "notistack";
type FunnelItemProps = {
title: string;
percent: number;
};
const FUNNEL_MOCK: Record<string, number> = {
"Стартовая страница": 100,
"Воронка квиза": 69,
Заявки: 56,
Результаты: 56,
};
const FunnelItem = ({ title, percent }: FunnelItemProps) => {
const theme = useTheme();
return (
<Box
sx={{
padding: "15px 25px",
"&:last-child div::after": { display: "none" },
}}
>
<Typography sx={{ marginBottom: "10px", fontWeight: "bold" }}>
{title}
</Typography>
<Box
sx={{
position: "relative",
display: "flex",
gap: "15px",
"&::after": {
content: "''",
position: "absolute",
left: 0,
bottom: "-15px",
height: "1px",
width: "100%",
maxWidth: "300px",
background: "#9A9AAF80",
},
}}
>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "center",
width: "30px",
height: "30px",
border: "1px solid black",
borderRadius: "8px",
fontSize: "14px",
}}
>
1
</Box>
<Box sx={{ display: "flex", alignItems: "center", flexGrow: 1 }}>
<LinearProgress
variant="determinate"
value={percent}
sx={{
width: "100%",
marginRight: "15px",
height: "12px",
background: theme.palette.background.default,
borderRadius: "6px",
border:
percent === 100
? `1px solid ${theme.palette.brightPurple.main}`
: null,
"& > span": { background: "#D9C0F9" },
}}
/>
<Box sx={{ minWidth: 35 }}>
<Typography
sx={{
minWidth: "45px",
color:
percent === 100
? theme.palette.text.secondary
: theme.palette.text.primary,
}}
>{`${percent}%`}</Typography>
</Box>
</Box>
</Box>
</Box>
);
};
export const Funnel = () => {
const [funnel, setFunnel] = useState<Record<string, number>>(FUNNEL_MOCK);
const theme = useTheme();
const isSmallMonitor = useMediaQuery(theme.breakpoints.down(1150));
const isMobile = useMediaQuery(theme.breakpoints.down(850));
useEffect(() => {
// const requestFunnel = async () => {
// const [funnelResponse, funnelError] = await getGeneral("14761");
// if (funnelError) {
// enqueueSnackbar(funnelError);
// return;
// }
// if (!funnelResponse) {
// enqueueSnackbar("Воронка пуста.");
// return;
// }
// setFunnel(funnelResponse);
// };
// requestFunnel();
}, []);
return (
<Paper
sx={{
borderRadius: "12px",
boxShadow: "0 0 20px rgba(0, 0, 0, 0.15)",
marginTop: isSmallMonitor && !isMobile ? 0 : "60px",
width: "100%",
maxWidth: isSmallMonitor && !isMobile ? "366px" : "none",
}}
>
{Object.entries(funnel).map(([title, percent]) => (
<FunnelItem key={title} title={title} percent={percent} />
))}
</Paper>
);
};

@ -0,0 +1,107 @@
import { useState } from "react";
import {
Box,
Paper,
Typography,
LinearProgress,
useTheme,
} from "@mui/material";
type ResultProps = {
title: string;
percent: number;
highlight?: boolean;
};
const RESULTS_MOCK: Record<string, number> = {
"Заголовок результата": 100,
"Результат пропущен": 7,
Другое: 27,
};
const Result = ({ title, percent, highlight }: ResultProps) => {
const theme = useTheme();
return (
<Box sx={{ padding: "15px 25px" }}>
<Box
sx={{
position: "relative",
display: "flex",
gap: "15px",
alignItems: "center",
flexGrow: 1,
}}
>
<LinearProgress
variant="determinate"
title={title}
value={percent}
sx={{
width: "100%",
height: "44px",
background: theme.palette.background.default,
borderRadius: "10px",
border: `1px solid ${highlight ? "#FC7230" : theme.palette.grey2.main}`,
"& > span": { background: highlight ? "#FDE9E0" : "#9A9AAF1A" },
"&::before": {
content: `"${title}"`,
position: "absolute",
zIndex: 1,
left: "20px",
top: "50%",
transform: "translateY(-50%)",
color: highlight ? "#FC7230" : theme.palette.grey3.main,
},
}}
/>
<Box sx={{ minWidth: 35 }}>
<Typography
sx={{
minWidth: "45px",
fontWeight: highlight ? "bold" : "normal",
color: highlight ? "#FC7230" : theme.palette.text.primary,
}}
>{`${percent}%`}</Typography>
</Box>
</Box>
</Box>
);
};
export const Results = () => {
const [results, setResults] = useState<Record<string, number>>(RESULTS_MOCK);
const theme = useTheme();
return (
<Box>
<Typography
component="h3"
sx={{
marginTop: "100px",
fontSize: "24px",
fontWeight: "bold",
color: theme.palette.text.primary,
}}
>
Статистика по результатам
</Typography>
<Paper
sx={{
borderRadius: "12px",
boxShadow: "0 0 20px rgba(0, 0, 0, 0.15)",
marginTop: "30px",
}}
>
{Object.entries(results).map(([title, percent], index) => (
<Result
key={title}
title={title}
percent={percent}
highlight={!index}
/>
))}
</Paper>
</Box>
);
};

@ -0,0 +1,66 @@
import {
Box,
ButtonBase,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import { Answers } from "./Answers";
import { Funnel } from "./Funnel";
import { Results } from "./Results";
import { ReactComponent as OpenIcon } from "@icons/Analytics/open.svg";
export const AnswersStatistics = () => {
const theme = useTheme();
const isSmallMonitor = useMediaQuery(theme.breakpoints.down(1150));
const isMobile = useMediaQuery(theme.breakpoints.down(850));
return (
<Box sx={{ marginTop: "120px" }}>
<Typography
component="h3"
sx={{
fontSize: "24px",
fontWeight: "bold",
color: theme.palette.text.primary,
}}
>
Статистика по ответам
</Typography>
<ButtonBase
sx={{
marginTop: "35px",
display: "flex",
justifyContent: "center",
alignItems: "center",
gap: "5px",
padding: "10px",
width: "100%",
maxWidth: "160px",
background: "#FFFFFF",
border: "1px solid #9A9AAF",
borderRadius: "8px",
color: theme.palette.brightPurple.main,
boxShadow: "0 0 20px rgba(0, 0, 0, 0.15)",
}}
>
<Typography>Фильтры</Typography>
<Box>
<OpenIcon />
</Box>
</ButtonBase>
<Box
sx={{
display: isSmallMonitor && !isMobile ? "flex" : "block",
gap: "40px",
}}
>
<Answers />
<Funnel />
</Box>
<Results />
</Box>
);
};

@ -0,0 +1,156 @@
import { useEffect, useState } from "react";
import { Box, Paper, Typography, useMediaQuery, useTheme } from "@mui/material";
import { PieChart } from "@mui/x-charts";
import { enqueueSnackbar } from "notistack";
import { getDevices } from "@api/statistic";
import type { DevicesResponse } from "@api/statistic";
type DeviceProps = {
title: string;
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 theme = useTheme();
const data = Object.entries(devices).map(([id, value], index) => ({
id,
value,
color: COLORS[index],
}));
return (
<Paper
sx={{
overflow: "hidden",
minHeight: "500px",
display: "flex",
flexDirection: "column",
gap: "30px",
borderRadius: "12px",
boxShadow: "0 0 20px rgba(0, 0, 0, 0.15)",
}}
>
<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
key={id}
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>
</Paper>
);
};
export const Devices = () => {
const [devices, setDevices] = useState<DevicesResponse>(DEVICES_MOCK);
const theme = useTheme();
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
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 (
<Box sx={{ marginTop: "120px" }}>
<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: "30px",
}}
>
<Device title="Устройства" devices={devices.device} />
<Device title="Операционные системы" devices={devices.os} />
<Device title="Браузеры" devices={devices.browser} />
</Box>
</Box>
);
};

@ -0,0 +1,148 @@
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>
);
};

@ -206,7 +206,7 @@ function TariffPage() {
<ArrowLeft color="black" />
</IconButton>
<Box sx={{ display: "flex", ml: "auto" }}>
<Box sx={{ whiteSpace: "nowrap" }} onClick={() => console.log(cash)}>
<Box sx={{ whiteSpace: "nowrap" }}>
<Typography
sx={{
fontSize: "12px",

@ -23,12 +23,12 @@ import { ToTariffsButton } from "@ui_kit/Toolbars/ToTariffsButton";
import ArrowLeft from "@icons/questionsPage/arrowLeft";
import { cleanAuthTicketData } from "@root/ticket";
interface Props {
interface HeaderFullProps {
isRequest: boolean;
sx?: SxProps<Theme>;
}
export default function HeaderFull({ isRequest, sx }: Props) {
export default function HeaderFull({ isRequest = false, sx }: HeaderFullProps) {
const theme = useTheme();
const navigate = useNavigate();
const isTablet = useMediaQuery(theme.breakpoints.down(1000));

275
yarn.lock

@ -1112,13 +1112,20 @@
resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310"
integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==
"@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.16.3", "@babel/runtime@^7.18.3", "@babel/runtime@^7.23.2", "@babel/runtime@^7.23.9", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
"@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.16.3", "@babel/runtime@^7.18.3", "@babel/runtime@^7.23.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
version "7.23.9"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.9.tgz#47791a15e4603bb5f905bc0753801cf21d6345f7"
integrity sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==
dependencies:
regenerator-runtime "^0.14.0"
"@babel/runtime@^7.20.1", "@babel/runtime@^7.23.9", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.7":
version "7.24.1"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.1.tgz#431f9a794d173b53720e69a6464abc6f0e2a5c57"
integrity sha512-+BIznRzyqBf+2wCTxcKE3wDjfGeCoVE61KSHGpkzqrLi8qxqFwBeUFyId2cxkTmm55fzDGnm0+yCxaxygrLUnQ==
dependencies:
regenerator-runtime "^0.14.0"
"@babel/template@^7.22.15", "@babel/template@^7.23.9", "@babel/template@^7.3.3":
version "7.23.9"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.23.9.tgz#f881d0487cba2828d3259dcb9ef5005a9731011a"
@ -1369,6 +1376,13 @@
resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.9.1.tgz#4ffb0055f7ef676ebc3a5a91fb621393294e2f43"
integrity sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==
"@emotion/is-prop-valid@^1.2.0":
version "1.2.2"
resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz#d4175076679c6a26faa92b03bb786f9e52612337"
integrity sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==
dependencies:
"@emotion/memoize" "^0.8.1"
"@emotion/is-prop-valid@^1.2.1":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-1.2.1.tgz#23116cf1ed18bfeac910ec6436561ecb1a3885cc"
@ -1854,7 +1868,21 @@
resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b"
integrity sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==
"@mui/base@5.0.0-beta.36", "@mui/base@^5.0.0-beta.22":
"@mui/base@5.0.0-alpha.106":
version "5.0.0-alpha.106"
resolved "https://registry.yarnpkg.com/@mui/base/-/base-5.0.0-alpha.106.tgz#23e5f61639b5786318be873d7e6d26db412c5211"
integrity sha512-xJQQtwPCPwr6hGWTBdvDwHYwExn3Bw7nPQkN8Fuz8kHpZqoMVWQvvaFS557AIkkI2AFLV3DxVIMjbCvrIntBWg==
dependencies:
"@babel/runtime" "^7.20.1"
"@emotion/is-prop-valid" "^1.2.0"
"@mui/types" "^7.2.1"
"@mui/utils" "^5.10.14"
"@popperjs/core" "^2.11.6"
clsx "^1.2.1"
prop-types "^15.8.1"
react-is "^18.2.0"
"@mui/base@^5.0.0-beta.22":
version "5.0.0-beta.36"
resolved "https://registry.yarnpkg.com/@mui/base/-/base-5.0.0-beta.36.tgz#29ca2de9d387f6d3943b6f18a84415c43e5f206c"
integrity sha512-6A8fYiXgjqTO6pgj31Hc8wm1M3rFYCxDRh09dBVk0L0W4cb2lnurRJa3cAyic6hHY+we1S58OdGYRbKmOsDpGQ==
@ -1867,10 +1895,10 @@
clsx "^2.1.0"
prop-types "^15.8.1"
"@mui/core-downloads-tracker@^5.15.10":
version "5.15.10"
resolved "https://registry.yarnpkg.com/@mui/core-downloads-tracker/-/core-downloads-tracker-5.15.10.tgz#616bfb54e3860268d56ff59cd187d47044d954f3"
integrity sha512-qPv7B+LeMatYuzRjB3hlZUHqinHx/fX4YFBiaS19oC02A1e9JFuDKDvlyRQQ5oRSbJJt0QlaLTlr0IcauVcJRQ==
"@mui/core-downloads-tracker@^5.10.14":
version "5.15.14"
resolved "https://registry.yarnpkg.com/@mui/core-downloads-tracker/-/core-downloads-tracker-5.15.14.tgz#f7c57b261904831877220182303761c012d05046"
integrity sha512-on75VMd0XqZfaQW+9pGjSNiqW+ghc5E2ZSLRBXwcXl/C4YzjfyjrLPhrEpKnR9Uym9KXBvxrhoHfPcczYHweyA==
"@mui/icons-material@^5.10.14":
version "5.15.10"
@ -1879,63 +1907,73 @@
dependencies:
"@babel/runtime" "^7.23.9"
"@mui/material@^5.10.14":
version "5.15.10"
resolved "https://registry.yarnpkg.com/@mui/material/-/material-5.15.10.tgz#6533ba53edbd0790dbc5bb7e9e173f6069ffd7e6"
integrity sha512-YJJGHjwDOucecjDEV5l9ISTCo+l9YeWrho623UajzoHRYxuKUmwrGVYOW4PKwGvCx9SU9oklZnbbi2Clc5XZHw==
"@mui/material@5.10.14":
version "5.10.14"
resolved "https://registry.yarnpkg.com/@mui/material/-/material-5.10.14.tgz#fcd687c92b22a71dc139376a5bc2d0bc578a172b"
integrity sha512-HWzKVAykePMx54WtxVwZyL1W4k3xlHYIqwMw0CaXAvgB3UE9yjABZuuGr8vG5Z6CSNWamzd+s1x8u7pQPFl9og==
dependencies:
"@babel/runtime" "^7.23.9"
"@mui/base" "5.0.0-beta.36"
"@mui/core-downloads-tracker" "^5.15.10"
"@mui/system" "^5.15.9"
"@mui/types" "^7.2.13"
"@mui/utils" "^5.15.9"
"@types/react-transition-group" "^4.4.10"
clsx "^2.1.0"
csstype "^3.1.3"
"@babel/runtime" "^7.20.1"
"@mui/base" "5.0.0-alpha.106"
"@mui/core-downloads-tracker" "^5.10.14"
"@mui/system" "^5.10.14"
"@mui/types" "^7.2.1"
"@mui/utils" "^5.10.14"
"@types/react-transition-group" "^4.4.5"
clsx "^1.2.1"
csstype "^3.1.1"
prop-types "^15.8.1"
react-is "^18.2.0"
react-transition-group "^4.4.5"
"@mui/private-theming@^5.15.9":
version "5.15.9"
resolved "https://registry.yarnpkg.com/@mui/private-theming/-/private-theming-5.15.9.tgz#3ea3514ed2f6bf68541dbe9206665a82cd89cb01"
integrity sha512-/aMJlDOxOTAXyp4F2rIukW1O0anodAMCkv1DfBh/z9vaKHY3bd5fFf42wmP+0GRmwMinC5aWPpNfHXOED1fEtg==
"@mui/private-theming@^5.15.14":
version "5.15.14"
resolved "https://registry.yarnpkg.com/@mui/private-theming/-/private-theming-5.15.14.tgz#edd9a82948ed01586a01c842eb89f0e3f68970ee"
integrity sha512-UH0EiZckOWcxiXLX3Jbb0K7rC8mxTr9L9l6QhOZxYc4r8FHUkefltV9VDGLrzCaWh30SQiJvAEd7djX3XXY6Xw==
dependencies:
"@babel/runtime" "^7.23.9"
"@mui/utils" "^5.15.9"
"@mui/utils" "^5.15.14"
prop-types "^15.8.1"
"@mui/styled-engine@^5.15.9":
version "5.15.9"
resolved "https://registry.yarnpkg.com/@mui/styled-engine/-/styled-engine-5.15.9.tgz#444605039ec3fe456bdd5d5cb94330183be62b91"
integrity sha512-NRKtYkL5PZDH7dEmaLEIiipd3mxNnQSO+Yo8rFNBNptY8wzQnQ+VjayTq39qH7Sast5cwHKYFusUrQyD+SS4Og==
"@mui/styled-engine@^5.15.14":
version "5.15.14"
resolved "https://registry.yarnpkg.com/@mui/styled-engine/-/styled-engine-5.15.14.tgz#168b154c4327fa4ccc1933a498331d53f61c0de2"
integrity sha512-RILkuVD8gY6PvjZjqnWhz8fu68dVkqhM5+jYWfB5yhlSQKg+2rHkmEwm75XIeAqI3qwOndK6zELK5H6Zxn4NHw==
dependencies:
"@babel/runtime" "^7.23.9"
"@emotion/cache" "^11.11.0"
csstype "^3.1.3"
prop-types "^15.8.1"
"@mui/system@^5.15.9":
version "5.15.9"
resolved "https://registry.yarnpkg.com/@mui/system/-/system-5.15.9.tgz#8a34ac0ab133af2550cc7ab980a35174142fd265"
integrity sha512-SxkaaZ8jsnIJ77bBXttfG//LUf6nTfOcaOuIgItqfHv60ZCQy/Hu7moaob35kBb+guxVJnoSZ+7vQJrA/E7pKg==
"@mui/system@^5.10.14":
version "5.15.14"
resolved "https://registry.yarnpkg.com/@mui/system/-/system-5.15.14.tgz#8a0c6571077eeb6b5f1ff7aa7ff6a3dc4a14200d"
integrity sha512-auXLXzUaCSSOLqJXmsAaq7P96VPRXg2Rrz6OHNV7lr+kB8lobUF+/N84Vd9C4G/wvCXYPs5TYuuGBRhcGbiBGg==
dependencies:
"@babel/runtime" "^7.23.9"
"@mui/private-theming" "^5.15.9"
"@mui/styled-engine" "^5.15.9"
"@mui/types" "^7.2.13"
"@mui/utils" "^5.15.9"
"@mui/private-theming" "^5.15.14"
"@mui/styled-engine" "^5.15.14"
"@mui/types" "^7.2.14"
"@mui/utils" "^5.15.14"
clsx "^2.1.0"
csstype "^3.1.3"
prop-types "^15.8.1"
"@mui/types@^7.2.13":
version "7.2.13"
resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.2.13.tgz#d1584912942f9dc042441ecc2d1452be39c666b8"
integrity sha512-qP9OgacN62s+l8rdDhSFRe05HWtLLJ5TGclC9I1+tQngbssu0m2dmFZs+Px53AcOs9fD7TbYd4gc9AXzVqO/+g==
"@mui/types@^7.2.1", "@mui/types@^7.2.13", "@mui/types@^7.2.14":
version "7.2.14"
resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.2.14.tgz#8a02ac129b70f3d82f2f9b76ded2c8d48e3fc8c9"
integrity sha512-MZsBZ4q4HfzBsywtXgM1Ksj6HDThtiwmOKUXH1pKYISI9gAVXCNHNpo7TlGoGrBaYWZTdNoirIN7JsQcQUjmQQ==
"@mui/utils@^5.14.16", "@mui/utils@^5.15.9":
"@mui/utils@^5.10.14", "@mui/utils@^5.15.14", "@mui/utils@^5.15.9":
version "5.15.14"
resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.15.14.tgz#e414d7efd5db00bfdc875273a40c0a89112ade3a"
integrity sha512-0lF/7Hh/ezDv5X7Pry6enMsbYyGKjADzvHyo3Qrc/SSlTsQ1VkbDMbH0m2t3OR5iIVLwMoxwM7yGd+6FCMtTFA==
dependencies:
"@babel/runtime" "^7.23.9"
"@types/prop-types" "^15.7.11"
prop-types "^15.8.1"
react-is "^18.2.0"
"@mui/utils@^5.14.16":
version "5.15.9"
resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.15.9.tgz#2bdf925e274d87cbe90c14eb52d0835318205e86"
integrity sha512-yDYfr61bCYUz1QtwvpqYy/3687Z8/nS4zv7lv/ih/6ZFGMl1iolEvxRmR84v2lOYxlds+kq1IVYbXxDKh8Z9sg==
@ -1945,6 +1983,21 @@
prop-types "^15.8.1"
react-is "^18.2.0"
"@mui/x-charts@^6.19.5":
version "6.19.5"
resolved "https://registry.yarnpkg.com/@mui/x-charts/-/x-charts-6.19.5.tgz#1c427961b87ba12ea3407ce1623e235a090828f4"
integrity sha512-BBRGLup5gpaLkhECv+J2ahFbDDgqK4BgLyLXLHKUASoWSU3YRCyDt9ifBREspEPfTZXgrcqNkybAl5b+l6baFQ==
dependencies:
"@babel/runtime" "^7.23.2"
"@mui/base" "^5.0.0-beta.22"
"@react-spring/rafz" "^9.7.3"
"@react-spring/web" "^9.7.3"
clsx "^2.0.0"
d3-color "^3.1.0"
d3-scale "^4.0.2"
d3-shape "^3.2.0"
prop-types "^15.8.1"
"@mui/x-date-pickers@^6.16.1":
version "6.19.4"
resolved "https://registry.yarnpkg.com/@mui/x-date-pickers/-/x-date-pickers-6.19.4.tgz#7df5ea081bdf9e2ce75c346bd95af340b1b56e9f"
@ -2006,7 +2059,7 @@
schema-utils "^3.0.0"
source-map "^0.7.3"
"@popperjs/core@^2.0.0", "@popperjs/core@^2.11.8":
"@popperjs/core@^2.0.0", "@popperjs/core@^2.11.6", "@popperjs/core@^2.11.8":
version "2.11.8"
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f"
integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==
@ -2026,6 +2079,50 @@
resolved "https://registry.yarnpkg.com/@react-dnd/shallowequal/-/shallowequal-4.0.2.tgz#d1b4befa423f692fa4abf1c79209702e7d8ae4b4"
integrity sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==
"@react-spring/animated@~9.7.3":
version "9.7.3"
resolved "https://registry.yarnpkg.com/@react-spring/animated/-/animated-9.7.3.tgz#4211b1a6d48da0ff474a125e93c0f460ff816e0f"
integrity sha512-5CWeNJt9pNgyvuSzQH+uy2pvTg8Y4/OisoscZIR8/ZNLIOI+CatFBhGZpDGTF/OzdNFsAoGk3wiUYTwoJ0YIvw==
dependencies:
"@react-spring/shared" "~9.7.3"
"@react-spring/types" "~9.7.3"
"@react-spring/core@~9.7.3":
version "9.7.3"
resolved "https://registry.yarnpkg.com/@react-spring/core/-/core-9.7.3.tgz#60056bcb397f2c4f371c6c9a5f882db77ae90095"
integrity sha512-IqFdPVf3ZOC1Cx7+M0cXf4odNLxDC+n7IN3MDcVCTIOSBfqEcBebSv+vlY5AhM0zw05PDbjKrNmBpzv/AqpjnQ==
dependencies:
"@react-spring/animated" "~9.7.3"
"@react-spring/shared" "~9.7.3"
"@react-spring/types" "~9.7.3"
"@react-spring/rafz@^9.7.3":
version "9.7.3"
resolved "https://registry.yarnpkg.com/@react-spring/rafz/-/rafz-9.7.3.tgz#d6a9695c581f58a49e047ff7ed5646e0b681396c"
integrity sha512-9vzW1zJPcC4nS3aCV+GgcsK/WLaB520Iyvm55ARHfM5AuyBqycjvh1wbmWmgCyJuX4VPoWigzemq1CaaeRSHhQ==
"@react-spring/shared@~9.7.3":
version "9.7.3"
resolved "https://registry.yarnpkg.com/@react-spring/shared/-/shared-9.7.3.tgz#4cf29797847c689912aec4e62e34c99a4d5d9e53"
integrity sha512-NEopD+9S5xYyQ0pGtioacLhL2luflh6HACSSDUZOwLHoxA5eku1UPuqcJqjwSD6luKjjLfiLOspxo43FUHKKSA==
dependencies:
"@react-spring/types" "~9.7.3"
"@react-spring/types@~9.7.3":
version "9.7.3"
resolved "https://registry.yarnpkg.com/@react-spring/types/-/types-9.7.3.tgz#ea78fd447cbc2612c1f5d55852e3c331e8172a0b"
integrity sha512-Kpx/fQ/ZFX31OtlqVEFfgaD1ACzul4NksrvIgYfIFq9JpDHFwQkMVZ10tbo0FU/grje4rcL4EIrjekl3kYwgWw==
"@react-spring/web@^9.7.3":
version "9.7.3"
resolved "https://registry.yarnpkg.com/@react-spring/web/-/web-9.7.3.tgz#d9f4e17fec259f1d65495a19502ada4f5b57fa3d"
integrity sha512-BXt6BpS9aJL/QdVqEIX9YoUy8CE6TJrU0mNCqSoxdXlIeNcEBWOfIyE6B14ENNsyQKS3wOWkiJfco0tCr/9tUg==
dependencies:
"@react-spring/animated" "~9.7.3"
"@react-spring/core" "~9.7.3"
"@react-spring/shared" "~9.7.3"
"@react-spring/types" "~9.7.3"
"@remix-run/router@1.15.0":
version "1.15.0"
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.15.0.tgz#461a952c2872dd82c8b2e9b74c4dfaff569123e2"
@ -2534,9 +2631,9 @@
integrity sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==
"@types/prop-types@*", "@types/prop-types@^15.7.11":
version "15.7.11"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.11.tgz#2596fb352ee96a1379c657734d4b913a613ad563"
integrity sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==
version "15.7.12"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.12.tgz#12bb1e2be27293c1406acb6af1c3f3a1481d98c6"
integrity sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==
"@types/q@^1.5.1":
version "1.5.8"
@ -2599,14 +2696,23 @@
dependencies:
"@types/react" "*"
"@types/react-transition-group@^4.4.10", "@types/react-transition-group@^4.4.8":
"@types/react-transition-group@^4.4.5", "@types/react-transition-group@^4.4.8":
version "4.4.10"
resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.10.tgz#6ee71127bdab1f18f11ad8fb3322c6da27c327ac"
integrity sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==
dependencies:
"@types/react" "*"
"@types/react@*", "@types/react@^18.2.55":
"@types/react@*":
version "18.2.70"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.70.tgz#89a37f9e0a6a4931f4259c598f40fd44dd6abf71"
integrity sha512-hjlM2hho2vqklPhopNkXkdkeq6Lv8WSZTpr7956zY+3WS5cfYUewtCzsJLsbW5dEv3lfSeQ4W14ZFeKC437JRQ==
dependencies:
"@types/prop-types" "*"
"@types/scheduler" "*"
csstype "^3.0.2"
"@types/react@^18.2.55":
version "18.2.55"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.55.tgz#38141821b7084404b5013742bc4ae08e44da7a67"
integrity sha512-Y2Tz5P4yz23brwm2d7jNon39qoAtMMmalOQv6+fEFt1mT+FcM3D841wDpoUvFXhaYenuROCy3FZYqdTjM7qVyA==
@ -3914,7 +4020,7 @@ clone-deep@^4.0.1:
kind-of "^6.0.2"
shallow-clone "^3.0.0"
clsx@^1.1.0, clsx@^1.1.1:
clsx@^1.1.0, clsx@^1.1.1, clsx@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12"
integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==
@ -4379,7 +4485,7 @@ cssstyle@^2.3.0:
dependencies:
cssom "~0.3.6"
csstype@^3.0.2, csstype@^3.1.3:
csstype@^3.0.2, csstype@^3.1.1, csstype@^3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81"
integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==
@ -4452,6 +4558,67 @@ cytoscape@^3.26.0:
heap "^0.2.6"
lodash "^4.17.21"
"d3-array@2 - 3", "d3-array@2.10.0 - 3":
version "3.2.4"
resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-3.2.4.tgz#15fec33b237f97ac5d7c986dc77da273a8ed0bb5"
integrity sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==
dependencies:
internmap "1 - 2"
"d3-color@1 - 3", d3-color@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2"
integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==
"d3-format@1 - 3":
version "3.1.0"
resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-3.1.0.tgz#9260e23a28ea5cb109e93b21a06e24e2ebd55641"
integrity sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==
"d3-interpolate@1.2.0 - 3":
version "3.0.1"
resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d"
integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==
dependencies:
d3-color "1 - 3"
d3-path@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-3.1.0.tgz#22df939032fb5a71ae8b1800d61ddb7851c42526"
integrity sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==
d3-scale@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-4.0.2.tgz#82b38e8e8ff7080764f8dcec77bd4be393689396"
integrity sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==
dependencies:
d3-array "2.10.0 - 3"
d3-format "1 - 3"
d3-interpolate "1.2.0 - 3"
d3-time "2.1.1 - 3"
d3-time-format "2 - 4"
d3-shape@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-3.2.0.tgz#a1a839cbd9ba45f28674c69d7f855bcf91dfc6a5"
integrity sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==
dependencies:
d3-path "^3.1.0"
"d3-time-format@2 - 4":
version "4.1.0"
resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-4.1.0.tgz#7ab5257a5041d11ecb4fe70a5c7d16a195bb408a"
integrity sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==
dependencies:
d3-time "1 - 3"
"d3-time@1 - 3", "d3-time@2.1.1 - 3":
version "3.1.0"
resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-3.1.0.tgz#9310db56e992e3c0175e1ef385e545e48a9bb5c7"
integrity sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==
dependencies:
d3-array "2 - 3"
damerau-levenshtein@^1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7"
@ -6408,6 +6575,11 @@ internal-slot@^1.0.4, internal-slot@^1.0.5, internal-slot@^1.0.7:
hasown "^2.0.0"
side-channel "^1.0.4"
"internmap@1 - 2":
version "2.0.3"
resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009"
integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==
ipaddr.js@1.9.1:
version "1.9.1"
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
@ -9400,6 +9572,11 @@ react-is@^18.0.0, react-is@^18.2.0:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b"
integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==
react-lazily@^0.9.2:
version "0.9.2"
resolved "https://registry.yarnpkg.com/react-lazily/-/react-lazily-0.9.2.tgz#74596dbde43c8e0f607445da5c4839cf6cc48ab5"
integrity sha512-oBVRDQ+SuMPWenBO/0Kq+iZk34lOYJEmjiTto4bYRufndf8pux3E50BT3mJZbsq0vBsAVbX3fpQjlUvsXgDVag==
react-redux@^7.2.0:
version "7.2.9"
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.9.tgz#09488fbb9416a4efe3735b7235055442b042481d"