diff --git a/src/api/statistic.ts b/src/api/statistic.ts index 47ac40c0..938a8e3d 100644 --- a/src/api/statistic.ts +++ b/src/api/statistic.ts @@ -5,22 +5,27 @@ import { parseAxiosError } from "@utils/parse-error"; const apiUrl = process.env.REACT_APP_DOMAIN + "/squiz/statistic"; export type DevicesResponse = { - device: Record; - os: Record; - browser: Record; + Device: Record; + OS: Record; + Browser: Record; }; export type GeneralResponse = { - open: Record; - result: Record; - avtime: Record; - conversation: Record; + Open: Record; + Result: Record; + AvTime: Record; + Conversion: Record; }; export type QuestionsResponse = { - funnel: number[]; - results: Record; - questions: Record>; + Funnel: number[]; + Results: Record; + Questions: Record>; +}; + +type TRequest = { + to: number; + from: number; }; export const getDevices = async ( @@ -29,7 +34,7 @@ export const getDevices = async ( from: number, ): Promise<[DevicesResponse | null, string?]> => { try { - const devicesResponse = await makeRequest({ + const devicesResponse = await makeRequest({ method: "POST", url: `${apiUrl}/${quizId}/devices`, withCredentials: true, @@ -50,7 +55,7 @@ export const getGeneral = async ( from: number, ): Promise<[GeneralResponse | null, string?]> => { try { - const generalResponse = await makeRequest({ + const generalResponse = await makeRequest({ method: "POST", url: `${apiUrl}/${quizId}/general`, withCredentials: true, @@ -71,7 +76,7 @@ export const getQuestions = async ( from: number, ): Promise<[QuestionsResponse | null, string?]> => { try { - const questionsResponse = await makeRequest({ + const questionsResponse = await makeRequest({ method: "POST", url: `${apiUrl}/${quizId}/questions`, withCredentials: true, diff --git a/src/assets/icons/Analytics/doubleCheck.svg b/src/assets/icons/Analytics/doubleCheck.svg index a0cdbba4..a53d41df 100644 --- a/src/assets/icons/Analytics/doubleCheck.svg +++ b/src/assets/icons/Analytics/doubleCheck.svg @@ -10,7 +10,7 @@ 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" + strokeLinecap="round" stroke-linejoin="round" /> \ No newline at end of file diff --git a/src/assets/icons/Analytics/leftArrow.svg b/src/assets/icons/Analytics/leftArrow.svg index 5fba1769..84a3406e 100644 --- a/src/assets/icons/Analytics/leftArrow.svg +++ b/src/assets/icons/Analytics/leftArrow.svg @@ -1,3 +1,3 @@ - + diff --git a/src/assets/icons/Analytics/next.svg b/src/assets/icons/Analytics/next.svg index 1fd3b611..e97a3905 100644 --- a/src/assets/icons/Analytics/next.svg +++ b/src/assets/icons/Analytics/next.svg @@ -10,14 +10,14 @@ d="M11.5 10.5L15.5 15L11.5 19.5" stroke="#FC712F" stroke-width="1.5" - stroke-linecap="round" + strokeLinecap="round" stroke-linejoin="round" /> diff --git a/src/assets/icons/Analytics/open.svg b/src/assets/icons/Analytics/open.svg index 72c6afe4..01d26697 100644 --- a/src/assets/icons/Analytics/open.svg +++ b/src/assets/icons/Analytics/open.svg @@ -1,3 +1,3 @@ - + diff --git a/src/assets/icons/Analytics/reset.svg b/src/assets/icons/Analytics/reset.svg new file mode 100644 index 00000000..462c5276 --- /dev/null +++ b/src/assets/icons/Analytics/reset.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/icons/Analytics/rightArrow.svg b/src/assets/icons/Analytics/rightArrow.svg index 6858a01a..748aa3ef 100644 --- a/src/assets/icons/Analytics/rightArrow.svg +++ b/src/assets/icons/Analytics/rightArrow.svg @@ -1,3 +1,3 @@ - + diff --git a/src/assets/icons/ChartIcon.tsx b/src/assets/icons/ChartIcon.tsx index 7b7ffa7b..256f51fe 100755 --- a/src/assets/icons/ChartIcon.tsx +++ b/src/assets/icons/ChartIcon.tsx @@ -14,21 +14,21 @@ export default function ChartLineUp(sx: SxProps) { d="M21 19.5H3V4.5" stroke="#7E2AEA" stroke-width="1.5" - stroke-linecap="round" + strokeLinecap="round" stroke-linejoin="round" /> diff --git a/src/assets/icons/arrow_left.svg b/src/assets/icons/arrow_left.svg index ab458806..fd726bd8 100644 --- a/src/assets/icons/arrow_left.svg +++ b/src/assets/icons/arrow_left.svg @@ -4,7 +4,7 @@ viewBox="0 0 50 50" style="enable-background:new 0 0 50 50;" xml:space="preserve"> diff --git a/src/assets/icons/arrow_right.svg b/src/assets/icons/arrow_right.svg index ba5ffa5d..8ed6173f 100644 --- a/src/assets/icons/arrow_right.svg +++ b/src/assets/icons/arrow_right.svg @@ -4,7 +4,7 @@ viewBox="0 0 50 50" style="enable-background:new 0 0 50 50;" xml:space="preserve"> diff --git a/src/pages/Analytics/Analytics.tsx b/src/pages/Analytics/Analytics.tsx index e06841ee..e6805fc5 100644 --- a/src/pages/Analytics/Analytics.tsx +++ b/src/pages/Analytics/Analytics.tsx @@ -1,63 +1,78 @@ -import { useLayoutEffect, useState } from "react"; +import { ReactNode, useEffect, useLayoutEffect, 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 { redirect } from "react-router-dom"; +import { LocalizationProvider, DatePicker } from "@mui/x-date-pickers"; +import { AdapterMoment } from "@mui/x-date-pickers/AdapterMoment"; import moment from "moment"; import { useQuizStore } from "@root/quizes/store"; +import { useCurrentQuiz } from "@root/quizes/hooks"; import { useAnalytics } from "@utils/hooks/useAnalytics"; import HeaderFull from "@ui_kit/Header/HeaderFull"; import SectionWrapper from "@ui_kit/SectionWrapper"; import { General } from "./General"; -import { AnswersStatistics } from "./Answers"; +import { AnswersStatistics } from "./Answers/AnswersStatistics"; import { Devices } from "./Devices"; import CalendarIcon from "@icons/CalendarIcon"; -import { redirect } from "react-router-dom"; +import { ReactComponent as ResetIcon } from "@icons/Analytics/reset.svg"; + +import type { Moment } from "moment"; export default function Analytics() { + const quiz = useCurrentQuiz(); const { editQuizId } = useQuizStore(); - const [isOpen, setOpen] = useState(false); - const [isOpenEnd, setOpenEnd] = useState(false); - const [to, setTo] = useState(null); - const [from, setFrom] = useState(null); + const [isOpen, setOpen] = useState(false); + const [isOpenEnd, setOpenEnd] = useState(false); + const [from, setFrom] = useState(null); + const [to, setTo] = useState(moment(Date.now())); - const { devices, general, questions } = useAnalytics({ - quizId: editQuizId?.toString(), - to, - from, - }); - - const resetTime = () => { - setTo(null); - setFrom(null); - }; - useLayoutEffect(() => { - if (editQuizId === undefined) redirect("/list"); - }, [editQuizId]); const theme = useTheme(); const isTablet = useMediaQuery(theme.breakpoints.down(1000)); const isMobile = useMediaQuery(theme.breakpoints.down(600)); + const { devices, general, questions } = useAnalytics({ + quizId: editQuizId?.toString() || "", + from, + to, + }); + + const resetTime = () => { + setFrom(moment(0)); + setTo(moment(Date.now())); + }; + + useEffect(() => { + if (quiz) { + console.log(moment(new Date(quiz.created_at))); + setFrom(moment(new Date(quiz.created_at))); + } + }, []); + + useLayoutEffect(() => { + if (editQuizId === undefined) redirect("/list"); + }, [editQuizId]); + const handleClose = () => { setOpen(false); }; + const handleOpen = () => { setOpen(true); }; + const onAdornmentClick = () => { setOpen((old) => !old); - if (isOpenEnd === true) { + if (isOpenEnd) { handleCloseEnd(); } }; @@ -65,22 +80,21 @@ export default function Analytics() { const handleCloseEnd = () => { setOpenEnd(false); }; + const handleOpenEnd = () => { setOpenEnd(true); }; + const onAdornmentClickEnd = () => { setOpenEnd((old) => !old); - if (isOpen === true) { + if (isOpen) { handleClose(); } }; - console.log("questions", questions); - console.log("general", general); - console.log("devices", devices); const now = moment(); return ( - <> + Аналитика @@ -88,13 +102,19 @@ export default function Analytics() { sx={{ display: "flex", gap: isMobile ? "15px" : "20px", - alignItems: "end", + alignItems: isMobile ? "center" : "end", justifyContent: "space-between", padding: "40px 0 20px", borderBottom: `1px solid ${theme.palette.grey2.main}`, }} > - + - ), + ) as ReactNode, }, }, }} - value={to} - onChange={(newValue) => setTo(newValue)} + value={from} + onChange={setFrom} /> @@ -146,8 +168,6 @@ export default function Analytics() { color: "4D4D4D", }} > - value={from} - onChange={(newValue) => setValue(setFrom)} Дата окончания @@ -184,8 +207,9 @@ export default function Analytics() { onClick={resetTime} variant="outlined" sx={{ - minWidth: isMobile ? "144px" : "180px", - px: isMobile ? "31px" : "43px", + padding: isMobile ? "8px" : "9px 48px", + minWidth: "auto", + marginTop: isMobile ? "25px" : null, color: theme.palette.brightPurple.main, "&:hover": { backgroundColor: "#581CA7", @@ -197,13 +221,13 @@ export default function Analytics() { }, }} > - Сбросить + {isMobile ? : "Сбросить"} - + ); } diff --git a/src/pages/Analytics/Answers/Answers.tsx b/src/pages/Analytics/Answers/Answers.tsx index acbcad48..6fa9803f 100644 --- a/src/pages/Analytics/Answers/Answers.tsx +++ b/src/pages/Analytics/Answers/Answers.tsx @@ -1,15 +1,16 @@ -import { useState } from "react"; +import { FC, useState } from "react"; +import type { PaginationRenderItemParams } from "@mui/material"; import { Box, - Paper, - Typography, + ButtonBase, + Input, LinearProgress, Pagination as MuiPagination, PaginationItem, - Input, - ButtonBase, - useTheme, + Paper, + Typography, useMediaQuery, + useTheme, } from "@mui/material"; import { ReactComponent as DoubleCheckIcon } from "@icons/Analytics/doubleCheck.svg"; @@ -17,14 +18,16 @@ 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; }; +type AnswersProps = { + data: Record> | null; +}; + const ANSWERS_MOCK: Record = { "Добавьте ответ": 67, "Вопрос пропущен": 7, @@ -186,16 +189,16 @@ const Pagination = () => { ); }; -export const Answers = (props) => { +export const Answers: FC = ({ data }) => { const theme = useTheme(); - console.log(props.data); - - if (Object.keys(props.data).length === 0) + if (!data) { return ( нет данных об ответах ); + } + return ( { - {Object.entries(props.data).map(([title, percent], index) => ( - - ))} + {/*{Object.entries(data).map(([title, percent], index) => (*/} + {/* */} + {/*))}*/} + {Object.entries(data).map(([title, values], index) => + Object.entries(values).map(([subTitle, percent]) => ( + + )), + )} diff --git a/src/pages/Analytics/Answers/index.tsx b/src/pages/Analytics/Answers/AnswersStatistics.tsx similarity index 75% rename from src/pages/Analytics/Answers/index.tsx rename to src/pages/Analytics/Answers/AnswersStatistics.tsx index 72306a26..8af97e8a 100644 --- a/src/pages/Analytics/Answers/index.tsx +++ b/src/pages/Analytics/Answers/AnswersStatistics.tsx @@ -1,24 +1,20 @@ -import { - Box, - ButtonBase, - Typography, - useMediaQuery, - useTheme, -} from "@mui/material"; +import { Box, Typography, useMediaQuery, useTheme } from "@mui/material"; import { Answers } from "./Answers"; +import { QuestionsResponse } from "@api/statistic"; +import { FC } from "react"; import { Funnel } from "./Funnel"; import { Results } from "./Results"; -import { ReactComponent as OpenIcon } from "@icons/Analytics/open.svg"; +type AnswersStatisticsProps = { + data: QuestionsResponse | null; +}; -export const AnswersStatistics = (props) => { +export const AnswersStatistics: FC = ({ data }) => { const theme = useTheme(); const isSmallMonitor = useMediaQuery(theme.breakpoints.down(1150)); const isMobile = useMediaQuery(theme.breakpoints.down(850)); - console.log(props); - return ( { gap: "40px", }} > - - + + - + ); }; diff --git a/src/pages/Analytics/Answers/Funnel.tsx b/src/pages/Analytics/Answers/Funnel.tsx index 48f7e9ec..6b9df5b3 100644 --- a/src/pages/Analytics/Answers/Funnel.tsx +++ b/src/pages/Analytics/Answers/Funnel.tsx @@ -1,19 +1,22 @@ -import { useEffect, useState } from "react"; +import { FC, useEffect } from "react"; import { Box, + LinearProgress, Paper, Typography, - LinearProgress, - useTheme, useMediaQuery, + useTheme, } from "@mui/material"; -import { enqueueSnackbar } from "notistack"; type FunnelItemProps = { title: string; percent: number; }; +type FunnelProps = { + data: number[] | null; +}; + const FUNNEL_MOCK: Record = { "Стартовая страница": 100, "Воронка квиза": 0, @@ -100,11 +103,10 @@ const FunnelItem = ({ title, percent }: FunnelItemProps) => { ); }; -export const Funnel = (props) => { +export const Funnel: FC = ({ data }) => { const theme = useTheme(); const isSmallMonitor = useMediaQuery(theme.breakpoints.down(1150)); const isMobile = useMediaQuery(theme.breakpoints.down(850)); - console.log(props); useEffect(() => { // const requestFunnel = async () => { // const [funnelResponse, funnelError] = await getGeneral("14761"); @@ -121,7 +123,7 @@ export const Funnel = (props) => { // requestFunnel(); }, []); - if (Object.keys(props.data).length === 0) + if (!data) return ( нет данных о разделах @@ -141,7 +143,7 @@ export const Funnel = (props) => { 0 ? props.data[index - 1] : percent} + percent={index > 0 ? data[index - 1] : percent} /> ))} diff --git a/src/pages/Analytics/Answers/Results.tsx b/src/pages/Analytics/Answers/Results.tsx index b9d3afe1..9d192457 100644 --- a/src/pages/Analytics/Answers/Results.tsx +++ b/src/pages/Analytics/Answers/Results.tsx @@ -1,11 +1,11 @@ -import { useState } from "react"; import { Box, + LinearProgress, Paper, Typography, - LinearProgress, useTheme, } from "@mui/material"; +import { FC } from "react"; type ResultProps = { title: string; @@ -13,6 +13,10 @@ type ResultProps = { highlight?: boolean; }; +type ResultsProps = { + data: Record | null; +}; + const RESULTS_MOCK: Record = { "Заголовок результата": 100, "Результат пропущен": 7, @@ -69,10 +73,10 @@ const Result = ({ title, percent, highlight }: ResultProps) => { ); }; -export const Results = (props) => { +export const Results: FC = ({ data }) => { const theme = useTheme(); - if (Object.keys(props.data).length === 0) + if (!data) return ( нет данных о результатах @@ -98,7 +102,7 @@ export const Results = (props) => { marginTop: "30px", }} > - {Object.entries(props.data).map(([title, percent], index) => ( + {Object.entries(data).map(([title, percent], index) => ( ; + devices: Record | null; +}; + +type DevicesProps = { + data: DevicesResponse | null; }; const COLORS: Record = { @@ -19,17 +20,12 @@ const COLORS: Record = { 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(); - console.log("devices ", devices); - if (devices === undefined || Object.keys(devices).length === 0) + if (!devices) { return {title} - нет данных; + } + const data = Object.entries(devices).map(([id, value], index) => ({ id, value, @@ -98,8 +94,7 @@ const Device = ({ title, devices }: DeviceProps) => { ); }; -export const Devices = ({ data = {} }) => { - const [devices, setDevices] = useState(data); +export const Devices: FC = ({ data }) => { const theme = useTheme(); const isTablet = useMediaQuery(theme.breakpoints.down(1000)); const isMobile = useMediaQuery(theme.breakpoints.down(700)); @@ -150,9 +145,9 @@ export const Devices = ({ data = {} }) => { marginTop: "30px", }} > - - - + + + ); diff --git a/src/pages/Analytics/General.tsx b/src/pages/Analytics/General.tsx index 23aa55ea..144c0472 100644 --- a/src/pages/Analytics/General.tsx +++ b/src/pages/Analytics/General.tsx @@ -1,19 +1,20 @@ -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"; +import { FC } from "react"; -type GeneralProps = { +type GeneralItemsProps = { title: string; general: Record; color: string; numberType: "sum" | "percent"; }; +type GeneralProps = { + data: GeneralResponse | null; +}; + const COLORS: Record = { 0: "#61BB1A", 1: "#7E2AEA", @@ -21,14 +22,12 @@ const COLORS: Record = { 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 GeneralItem = ({ + title, + general, + color, + numberType, +}: GeneralItemsProps) => { const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down(700)); @@ -65,17 +64,18 @@ const GeneralItem = ({ title, general, color, numberType }: GeneralProps) => { ); }; -export const General = (props: any) => { +export const General: FC = ({ data }) => { const theme = useTheme(); const isTablet = useMediaQuery(theme.breakpoints.down(1000)); const isMobile = useMediaQuery(theme.breakpoints.down(700)); - if (Object.keys(props.data).length === 0) + if (!data) { return ( нет данных о ключевых метриках ); + } return ( { diff --git a/src/utils/hooks/useAnalytics.ts b/src/utils/hooks/useAnalytics.ts index 68b0cc9b..2974f3fd 100644 --- a/src/utils/hooks/useAnalytics.ts +++ b/src/utils/hooks/useAnalytics.ts @@ -1,32 +1,60 @@ -import { getGeneral, getDevices, getQuestions } from "@api/statistic"; import { useEffect, useState } from "react"; -import moment from "moment"; +import { + DevicesResponse, + GeneralResponse, + getDevices, + getGeneral, + getQuestions, + QuestionsResponse, +} from "@api/statistic"; -interface Props { - quizId: string; - to: number; - from: number; +import type { Moment } from "moment"; + +interface useAnalyticsProps { + quizId: string | undefined; + to: Moment | null; + from: Moment | null; } -export function useAnalytics({ quizId, to, from }: Props) { - const formatTo = to === null ? 0 : moment(to).unix(); - const formatFrom = from === null ? 0 : moment(from).unix(); - console.log(to, from); - if (quizId === undefined) return {}; - const [devices, setDevices] = useState({}); - const [general, setGeneral] = useState({}); - const [questions, setQuestions] = useState({}); +export function useAnalytics({ quizId, to, from }: useAnalyticsProps) { + const formatTo = to?.unix(); + const formatFrom = from?.unix(); + + const [devices, setDevices] = useState(null); + const [general, setGeneral] = useState(null); + const [questions, setQuestions] = useState(null); useEffect(() => { - (async () => { - const gottenGeneral = await getGeneral(quizId, formatTo, formatFrom); - const gottenDevices = await getDevices(quizId, formatTo, formatFrom); - const gottenQuestions = await getQuestions(quizId, formatTo, formatFrom); - setDevices(gottenGeneral[0]); - setGeneral(gottenDevices[0]); - setQuestions(gottenQuestions[0]); - })(); - }, [to, from]); + if (!quizId) return; + + const requestStatistics = async () => { + if (!formatTo || !formatFrom) { + return; + } + + const [gottenGeneral] = await getGeneral(quizId, formatTo, formatFrom); + const [gottenDevices] = await getDevices(quizId, formatTo, formatFrom); + const [gottenQuestions] = await getQuestions( + quizId, + formatTo, + formatFrom, + ); + + if (gottenGeneral) { + setGeneral(gottenGeneral); + } + + if (gottenDevices) { + setDevices(gottenDevices); + } + + if (gottenQuestions) { + setQuestions(gottenQuestions); + } + }; + + requestStatistics(); + }, [quizId, to, from]); return { devices, general, questions }; }