Merge branch 'dev' into 'staging'

realized videofile component, added logic to delete uploaded video, refactored...

See merge request frontend/squiz!224
This commit is contained in:
Nastya 2024-03-31 00:13:42 +00:00
commit 18272be5ad
21 changed files with 480 additions and 266 deletions

@ -25,13 +25,15 @@ export type QuestionsResponse = {
export const getDevices = async ( export const getDevices = async (
quizId: string, quizId: string,
to: number,
from: number,
): Promise<[DevicesResponse | 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}/${quizId}/devices`, url: `${apiUrl}/${quizId}/devices`,
useToken: false,
withCredentials: true, withCredentials: true,
body: { to, from },
}); });
return [devicesResponse]; return [devicesResponse];
@ -44,13 +46,15 @@ export const getDevices = async (
export const getGeneral = async ( export const getGeneral = async (
quizId: string, quizId: string,
to: number,
from: number,
): Promise<[GeneralResponse | null, string?]> => { ): Promise<[GeneralResponse | null, string?]> => {
try { try {
const generalResponse = await makeRequest<unknown, GeneralResponse>({ const generalResponse = await makeRequest<unknown, GeneralResponse>({
method: "POST", method: "POST",
url: `${apiUrl}/${quizId}/general`, url: `${apiUrl}/${quizId}/general`,
useToken: false,
withCredentials: true, withCredentials: true,
body: { to, from },
}); });
return [generalResponse]; return [generalResponse];
@ -63,13 +67,15 @@ export const getGeneral = async (
export const getQuestions = async ( export const getQuestions = async (
quizId: string, quizId: string,
to: number,
from: number,
): Promise<[QuestionsResponse | null, string?]> => { ): Promise<[QuestionsResponse | null, string?]> => {
try { try {
const questionsResponse = await makeRequest<unknown, QuestionsResponse>({ const questionsResponse = await makeRequest<unknown, QuestionsResponse>({
method: "POST", method: "POST",
url: `${apiUrl}/${quizId}/questions`, url: `${apiUrl}/${quizId}/questions`,
useToken: false,
withCredentials: true, withCredentials: true,
body: { to, from },
}); });
return [questionsResponse]; return [questionsResponse];

@ -1,43 +1,35 @@
import { Box, useTheme } from "@mui/material"; import { Box, SxProps, Theme } from "@mui/material";
export default function ChartIcon() {
const theme = useTheme();
export default function ChartLineUp(sx: SxProps<Theme>) {
return ( return (
<Box <Box sx={sx}>
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
<svg <svg
xmlns="http://www.w3.org/2000/svg"
width="24" width="24"
height="24" height="24"
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="none" fill="none"
xmlns="http://www.w3.org/2000/svg"
> >
<path <path
d="M21 19.5H3V4.5" d="M21 19.5H3V4.5"
stroke={theme.palette.brightPurple.main} stroke="#7E2AEA"
strokeWidth="1.5" stroke-width="1.5"
strokeLinecap="round" stroke-linecap="round"
strokeLinejoin="round" stroke-linejoin="round"
/> />
<path <path
d="M19.5 6L12 13.5L9 10.5L3 16.5" d="M19.5 6L12 13.5L9 10.5L3 16.5"
stroke={theme.palette.brightPurple.main} stroke="#7E2AEA"
strokeWidth="1.5" stroke-width="1.5"
strokeLinecap="round" stroke-linecap="round"
strokeLinejoin="round" stroke-linejoin="round"
/> />
<path <path
d="M19.5 9.75V6H15.75" d="M19.5 9.75V6H15.75"
stroke={theme.palette.brightPurple.main} stroke="#7E2AEA"
strokeWidth="1.5" stroke-width="1.5"
strokeLinecap="round" stroke-linecap="round"
strokeLinejoin="round" stroke-linejoin="round"
/> />
</svg> </svg>
</Box> </Box>

@ -1,4 +1,4 @@
import { useState } from "react"; import { useLayoutEffect, useState } from "react";
import { import {
Box, Box,
Button, Button,
@ -11,6 +11,8 @@ 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 { useQuizStore } from "@root/quizes/store";
import { useAnalytics } from "@utils/hooks/useAnalytics";
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";
@ -20,11 +22,29 @@ import { AnswersStatistics } from "./Answers";
import { Devices } from "./Devices"; import { Devices } from "./Devices";
import CalendarIcon from "@icons/CalendarIcon"; import CalendarIcon from "@icons/CalendarIcon";
import { redirect } from "react-router-dom";
export default function Analytics() { export default function Analytics() {
const { editQuizId } = useQuizStore();
const [isOpen, setOpen] = useState(false); const [isOpen, setOpen] = useState(false);
const [isOpenEnd, setOpenEnd] = useState(false); const [isOpenEnd, setOpenEnd] = useState(false);
const [to, setTo] = useState(null);
const [from, setFrom] = useState(null);
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 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(600));
@ -54,6 +74,9 @@ export default function Analytics() {
handleClose(); handleClose();
} }
}; };
console.log("questions", questions);
console.log("general", general);
console.log("devices", devices);
const now = moment(); const now = moment();
return ( return (
@ -110,6 +133,8 @@ export default function Analytics() {
}, },
}, },
}} }}
value={to}
onChange={(newValue) => setTo(newValue)}
/> />
</Box> </Box>
<Box> <Box>
@ -121,6 +146,8 @@ export default function Analytics() {
color: "4D4D4D", color: "4D4D4D",
}} }}
> >
value={from}
onChange={(newValue) => setValue(setFrom)}
Дата окончания Дата окончания
</Typography> </Typography>
<DatePicker <DatePicker
@ -154,6 +181,7 @@ export default function Analytics() {
</Box> </Box>
<Button <Button
onClick={resetTime}
variant="outlined" variant="outlined"
sx={{ sx={{
minWidth: isMobile ? "144px" : "180px", minWidth: isMobile ? "144px" : "180px",
@ -172,9 +200,9 @@ export default function Analytics() {
Сбросить Сбросить
</Button> </Button>
</Box> </Box>
<General /> <General data={general} />
<AnswersStatistics /> <AnswersStatistics data={questions} />
<Devices /> <Devices data={devices} />
</SectionWrapper> </SectionWrapper>
</> </>
); );

@ -186,10 +186,16 @@ const Pagination = () => {
); );
}; };
export const Answers = () => { export const Answers = (props) => {
const [answers, setAnswers] = useState<Record<string, number>>(ANSWERS_MOCK);
const theme = useTheme(); const theme = useTheme();
console.log(props.data);
if (Object.keys(props.data).length === 0)
return (
<Typography textAlign="center" m="10px 0">
нет данных об ответах
</Typography>
);
return ( return (
<Box sx={{ flexGrow: 1 }}> <Box sx={{ flexGrow: 1 }}>
<Paper <Paper
@ -244,7 +250,7 @@ export const Answers = () => {
<NextIcon /> <NextIcon />
</ButtonBase> </ButtonBase>
</Box> </Box>
{Object.entries(answers).map(([title, percent], index) => ( {Object.entries(props.data).map(([title, percent], index) => (
<Answer <Answer
key={title} key={title}
title={title} title={title}

@ -16,9 +16,9 @@ type FunnelItemProps = {
const FUNNEL_MOCK: Record<string, number> = { const FUNNEL_MOCK: Record<string, number> = {
"Стартовая страница": 100, "Стартовая страница": 100,
"Воронка квиза": 69, "Воронка квиза": 0,
Заявки: 56, Заявки: 0,
Результаты: 56, Результаты: 0,
}; };
const FunnelItem = ({ title, percent }: FunnelItemProps) => { const FunnelItem = ({ title, percent }: FunnelItemProps) => {
@ -100,12 +100,11 @@ const FunnelItem = ({ title, percent }: FunnelItemProps) => {
); );
}; };
export const Funnel = () => { export const Funnel = (props) => {
const [funnel, setFunnel] = useState<Record<string, number>>(FUNNEL_MOCK);
const theme = useTheme(); const theme = useTheme();
const isSmallMonitor = useMediaQuery(theme.breakpoints.down(1150)); const isSmallMonitor = useMediaQuery(theme.breakpoints.down(1150));
const isMobile = useMediaQuery(theme.breakpoints.down(850)); const isMobile = useMediaQuery(theme.breakpoints.down(850));
console.log(props);
useEffect(() => { useEffect(() => {
// const requestFunnel = async () => { // const requestFunnel = async () => {
// const [funnelResponse, funnelError] = await getGeneral("14761"); // const [funnelResponse, funnelError] = await getGeneral("14761");
@ -122,6 +121,12 @@ export const Funnel = () => {
// requestFunnel(); // requestFunnel();
}, []); }, []);
if (Object.keys(props.data).length === 0)
return (
<Typography textAlign="center" m="10px 0">
нет данных о разделах
</Typography>
);
return ( return (
<Paper <Paper
sx={{ sx={{
@ -132,8 +137,12 @@ export const Funnel = () => {
maxWidth: isSmallMonitor && !isMobile ? "366px" : "none", maxWidth: isSmallMonitor && !isMobile ? "366px" : "none",
}} }}
> >
{Object.entries(funnel).map(([title, percent]) => ( {Object.entries(FUNNEL_MOCK).map(([title, percent], index) => (
<FunnelItem key={title} title={title} percent={percent} /> <FunnelItem
key={title}
title={title}
percent={index > 0 ? props.data[index - 1] : percent}
/>
))} ))}
</Paper> </Paper>
); );

@ -69,10 +69,15 @@ const Result = ({ title, percent, highlight }: ResultProps) => {
); );
}; };
export const Results = () => { export const Results = (props) => {
const [results, setResults] = useState<Record<string, number>>(RESULTS_MOCK);
const theme = useTheme(); const theme = useTheme();
if (Object.keys(props.data).length === 0)
return (
<Typography margin="20px 0 0 0" textAlign="center" m="10px 0">
нет данных о результатах
</Typography>
);
return ( return (
<Box> <Box>
<Typography <Typography
@ -93,7 +98,7 @@ export const Results = () => {
marginTop: "30px", marginTop: "30px",
}} }}
> >
{Object.entries(results).map(([title, percent], index) => ( {Object.entries(props.data).map(([title, percent], index) => (
<Result <Result
key={title} key={title}
title={title} title={title}

@ -12,11 +12,13 @@ import { Results } from "./Results";
import { ReactComponent as OpenIcon } from "@icons/Analytics/open.svg"; import { ReactComponent as OpenIcon } from "@icons/Analytics/open.svg";
export const AnswersStatistics = () => { export const AnswersStatistics = (props) => {
const theme = useTheme(); const theme = useTheme();
const isSmallMonitor = useMediaQuery(theme.breakpoints.down(1150)); const isSmallMonitor = useMediaQuery(theme.breakpoints.down(1150));
const isMobile = useMediaQuery(theme.breakpoints.down(850)); const isMobile = useMediaQuery(theme.breakpoints.down(850));
console.log(props);
return ( return (
<Box sx={{ marginTop: "120px" }}> <Box sx={{ marginTop: "120px" }}>
<Typography <Typography
@ -29,7 +31,7 @@ export const AnswersStatistics = () => {
> >
Статистика по ответам Статистика по ответам
</Typography> </Typography>
<ButtonBase {/* <ButtonBase
sx={{ sx={{
marginTop: "35px", marginTop: "35px",
display: "flex", display: "flex",
@ -50,17 +52,17 @@ export const AnswersStatistics = () => {
<Box> <Box>
<OpenIcon /> <OpenIcon />
</Box> </Box>
</ButtonBase> </ButtonBase> */}
<Box <Box
sx={{ sx={{
display: isSmallMonitor && !isMobile ? "flex" : "block", display: isSmallMonitor && !isMobile ? "flex" : "block",
gap: "40px", gap: "40px",
}} }}
> >
<Answers /> <Answers data={props.data?.Questions || {}} />
<Funnel /> <Funnel data={props.data?.Funnel || {}} />
</Box> </Box>
<Results /> <Results data={props.data?.Results || {}} />
</Box> </Box>
); );
}; };

@ -27,6 +27,9 @@ const DEVICES_MOCK: DevicesResponse = {
const Device = ({ title, devices }: DeviceProps) => { const Device = ({ title, devices }: DeviceProps) => {
const theme = useTheme(); const theme = useTheme();
console.log("devices ", devices);
if (devices === undefined || Object.keys(devices).length === 0)
return <Typography>{title} - нет данных</Typography>;
const data = Object.entries(devices).map(([id, value], index) => ({ const data = Object.entries(devices).map(([id, value], index) => ({
id, id,
value, value,
@ -95,33 +98,33 @@ const Device = ({ title, devices }: DeviceProps) => {
); );
}; };
export const Devices = () => { export const Devices = ({ data = {} }) => {
const [devices, setDevices] = useState<DevicesResponse>(DEVICES_MOCK); const [devices, setDevices] = useState<DevicesResponse>(data);
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(700)); const isMobile = useMediaQuery(theme.breakpoints.down(700));
useEffect(() => { // useEffect(() => {
const requestDevices = async () => { // const requestDevices = async () => {
const [devicesResponse, devicesError] = await getDevices("14761"); // const [devicesResponse, devicesError] = await getDevices("14761");
if (devicesError) { // if (devicesError) {
enqueueSnackbar(devicesError); // enqueueSnackbar(devicesError);
return; // return;
} // }
if (!devicesResponse) { // if (!devicesResponse) {
enqueueSnackbar("Список девайсов пуст."); // enqueueSnackbar("Список девайсов пуст.");
return; // return;
} // }
setDevices(devicesResponse); // setDevices(devicesResponse);
}; // };
// requestDevices(); // // requestDevices();
}, []); // }, []);
return ( return (
<Box sx={{ marginTop: "120px" }}> <Box sx={{ marginTop: "120px" }}>

@ -38,8 +38,7 @@ const GeneralItem = ({ title, general, color, numberType }: GeneralProps) => {
: Object.entries(general).reduce( : Object.entries(general).reduce(
(total, [key, value]) => total + (value / Number(key)) * 100, (total, [key, value]) => total + (value / Number(key)) * 100,
0, 0,
) / Object.keys(general).length; ) / Object.keys(general).length || Number(0);
return ( return (
<Paper <Paper
sx={{ sx={{
@ -66,34 +65,17 @@ const GeneralItem = ({ title, general, color, numberType }: GeneralProps) => {
); );
}; };
export const General = () => { export const General = (props: any) => {
const [general, setGeneral] = useState<GeneralResponse>(GENERAL_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(700)); const isMobile = useMediaQuery(theme.breakpoints.down(700));
useEffect(() => { if (Object.keys(props.data).length === 0)
const requestGeneral = async () => { return (
const [generalResponse, generalError] = await getGeneral("14761"); <Typography textAlign="center" m="10px 0">
нет данных о ключевых метриках
if (generalError) { </Typography>
enqueueSnackbar(generalError); );
return;
}
if (!generalResponse) {
enqueueSnackbar("Список девайсов пуст.");
return;
}
setGeneral(generalResponse);
};
// requestGeneral();
}, []);
return ( return (
<Box sx={{ marginTop: "45px" }}> <Box sx={{ marginTop: "45px" }}>
<Typography <Typography
@ -121,25 +103,25 @@ export const General = () => {
<GeneralItem <GeneralItem
title="Открыли квиз" title="Открыли квиз"
numberType="sum" numberType="sum"
general={general.open} general={props.data.open || { 0: 0 }}
color={COLORS[0]} color={COLORS[0]}
/> />
<GeneralItem <GeneralItem
title="Получено заявок" title="Получено заявок"
numberType="sum" numberType="sum"
general={general.result} general={props.data.result || { 0: 0 }}
color={COLORS[1]} color={COLORS[1]}
/> />
<GeneralItem <GeneralItem
title="Конверсия" title="Конверсия"
numberType="percent" numberType="percent"
general={general.conversation} general={props.data.conversation || { 0: 0 }}
color={COLORS[2]} color={COLORS[2]}
/> />
<GeneralItem <GeneralItem
title="Среднее время прохождения квиза" title="Среднее время прохождения квиза"
numberType="percent" numberType="percent"
general={general.avtime} general={props.data.avtime || { 0: 0 }}
color={COLORS[3]} color={COLORS[3]}
/> />
</Box> </Box>

@ -28,8 +28,8 @@ export const DraggableList = ({
useEffect(() => { useEffect(() => {
if (!isLoading && quiz && !filteredQuestions.length) { if (!isLoading && quiz && !filteredQuestions.length) {
console.log("useEffect", quiz) console.log("useEffect", quiz);
console.log(Number(quiz.backendId)) console.log(Number(quiz.backendId));
createUntypedQuestion(Number(quiz.backendId)); createUntypedQuestion(Number(quiz.backendId));
} }
}, [quiz, filteredQuestions]); }, [quiz, filteredQuestions]);

@ -36,7 +36,7 @@ export default function QuestionsPage({
updateEditSomeQuestion(); updateEditSomeQuestion();
}, []); }, []);
console.log("quiz", quiz) console.log("quiz", quiz);
if (!quiz) return null; if (!quiz) return null;
return ( return (

@ -4,8 +4,8 @@ import { CustomTab } from "./CustomTab";
type TabsProps = { type TabsProps = {
names: string[]; names: string[];
items: string[]; items: string[];
selectedItem: "count" | "day"; selectedItem: "count" | "day" | "dop";
setSelectedItem: (num: "count" | "day") => void; setSelectedItem: (num: "count" | "day" | "dop") => void;
}; };
export const Tabs = ({ export const Tabs = ({
@ -18,7 +18,7 @@ export const Tabs = ({
sx={{ m: "25px" }} sx={{ m: "25px" }}
TabIndicatorProps={{ sx: { display: "none" } }} TabIndicatorProps={{ sx: { display: "none" } }}
value={selectedItem} value={selectedItem}
onChange={(event, newValue: "count" | "day") => { onChange={(event, newValue: "count" | "day" | "dop") => {
setSelectedItem(newValue); setSelectedItem(newValue);
}} }}
variant="scrollable" variant="scrollable"

@ -38,6 +38,7 @@ import { activatePromocode } from "@api/promocode";
const StepperText: Record<string, string> = { const StepperText: Record<string, string> = {
count: "Тарифы на объём", count: "Тарифы на объём",
day: "Тарифы на время", day: "Тарифы на время",
dop: "Доп. услуги",
}; };
function TariffPage() { function TariffPage() {
@ -156,6 +157,19 @@ function TariffPage() {
); );
}); });
const filteredBadgeTariffs = tariffs.filter((tariff) => {
return (
tariff.privileges[0].serviceKey === "squiz" &&
!tariff.isDeleted &&
!tariff.isCustom &&
tariff.privileges[0].privilegeId === "squizHideBadge" &&
tariff.privileges[0]?.type === "day"
);
});
const filteredBaseTariffs = filteredTariffs.filter((tariff) => {
return tariff.privileges[0].privilegeId !== "squizHideBadge";
});
async function handleLogoutClick() { async function handleLogoutClick() {
const [, logoutError] = await logout(); const [, logoutError] = await logout();
@ -254,20 +268,54 @@ function TariffPage() {
<Box <Box
sx={{ sx={{
justifyContent: "left", justifyContent: "left",
display: "grid", display: selectedItem === "dop" ? "flex" : "grid",
gap: "40px", gap: "40px",
p: "20px", p: "20px",
gridTemplateColumns: `repeat(auto-fit, minmax(300px, ${ gridTemplateColumns: `repeat(auto-fit, minmax(300px, ${
isTablet ? "436px" : "360px" isTablet ? "436px" : "360px"
}))`, }))`,
flexDirection: selectedItem === "dop" ? "column" : undefined,
}} }}
> >
{createTariffElements( {selectedItem === "day" &&
filteredTariffs, createTariffElements(
true, filteredBaseTariffs,
user, true,
discounts, user,
openModalHC, discounts,
openModalHC,
)}
{selectedItem === "count" &&
createTariffElements(
filteredTariffs,
true,
user,
discounts,
openModalHC,
)}
{selectedItem === "dop" && (
<>
<Typography fontWeight={500}>Убрать логотип "PenaQuiz"</Typography>
<Box
sx={{
justifyContent: "left",
display: "grid",
gap: "40px",
p: "20px",
gridTemplateColumns: `repeat(auto-fit, minmax(300px, ${
isTablet ? "436px" : "360px"
}))`,
}}
>
{createTariffElements(
filteredBadgeTariffs,
false,
user,
discounts,
openModalHC,
)}
</Box>
</>
)} )}
</Box> </Box>
<Modal <Modal

@ -19,6 +19,7 @@ import { makeRequest } from "@frontend/kitui";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import { useDomainDefine } from "@utils/hooks/useDomainDefine"; import { useDomainDefine } from "@utils/hooks/useDomainDefine";
import CopyIcon from "@icons/CopyIcon"; import CopyIcon from "@icons/CopyIcon";
import ChartIcon from "@icons/ChartIcon";
interface Props { interface Props {
quiz: Quiz; quiz: Quiz;
@ -45,6 +46,10 @@ export default function QuizCard({
setEditQuizId(quiz.backendId); setEditQuizId(quiz.backendId);
navigate("/edit"); navigate("/edit");
} }
function handleStatisticClick() {
setEditQuizId(quiz.backendId);
navigate(`/analytics`);
}
const questionCount = useRef(quiz.questions_count.toString() || ""); const questionCount = useRef(quiz.questions_count.toString() || "");
@ -186,21 +191,24 @@ export default function QuizCard({
> >
{isMobile ? "" : "Редактировать"} {isMobile ? "" : "Редактировать"}
</Button> </Button>
{/* <Button <IconButton
variant="outlined" onClick={handleStatisticClick}
startIcon={<ChartIcon />}
sx={{ sx={{
minWidth: "46px", height: "44px",
padding: "10px 10px", width: "44px",
"& .MuiButton-startIcon": { border: `${theme.palette.brightPurple.main} 1px solid`,
mr: 0, borderRadius: "6px",
ml: 0,
},
}} }}
/> */} >
<ChartIcon />
</IconButton>
<IconButton <IconButton
onClick={() => onClickCopy(quiz.id)} onClick={() => onClickCopy(quiz.id)}
sx={{ borderRadius: "6px", padding: "0 4px" }} sx={{
height: "44px",
width: "44px",
borderRadius: "6px",
}}
> >
<CopyIcon <CopyIcon
color={theme.palette.brightPurple.main} color={theme.palette.brightPurple.main}

@ -20,9 +20,9 @@ import {
FormControlLabel, FormControlLabel,
MenuItem, MenuItem,
Select, Select,
Skeleton,
Tooltip, Tooltip,
Typography, Typography,
Skeleton,
useMediaQuery, useMediaQuery,
useTheme, useTheme,
} from "@mui/material"; } from "@mui/material";
@ -45,6 +45,7 @@ import SelectableIconButton from "./SelectableIconButton";
import { DropZone } from "./dropZone"; import { DropZone } from "./dropZone";
import Extra from "./extra"; import Extra from "./extra";
import TooltipClickInfo from "@ui_kit/Toolbars/TooltipClickInfo"; import TooltipClickInfo from "@ui_kit/Toolbars/TooltipClickInfo";
import { VideoElement } from "./VideoElement";
const designTypes = [ const designTypes = [
[ [
@ -386,91 +387,103 @@ export default function StartPageSettings() {
{quiz.config.startpage.background.type === "video" && ( {quiz.config.startpage.background.type === "video" && (
<> <>
<Box {!quiz.config.startpage.background.video ? (
sx={{
display: "flex",
alignItems: "center",
gap: "7px",
mt: "20px",
mb: "14px",
}}
>
<Typography
sx={{ fontWeight: 500, color: theme.palette.grey3.main }}
>
Добавить видео
</Typography>
{isMobile ? (
<TooltipClickInfo title={"Можно загрузить видео."} />
) : (
<Tooltip title="Можно загрузить видео." placement="top">
<Box>
<InfoIcon />
</Box>
</Tooltip>
)}
</Box>
{backgroundUploding ? (
<Skeleton
sx={{
width: "48px",
height: "48px",
transform: "none",
margin: "20px 0",
}}
/>
) : (
<> <>
<ButtonBase <Box
component="label"
sx={{ sx={{
justifyContent: "center",
height: "48px",
width: "48px",
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
my: "20px", gap: "7px",
mt: "20px",
mb: "14px",
}} }}
> >
<input <Typography
onChange={async (event) => { sx={{
setBackgroundUploading(true); fontWeight: 500,
const file = event.target.files?.[0]; color: theme.palette.grey3.main,
}}
if (file) { >
await uploadQuizImage( Добавить видео
quiz.id, </Typography>
file, {isMobile ? (
(quiz, url) => { <TooltipClickInfo title={"Можно загрузить видео."} />
quiz.config.startpage.background.video = url; ) : (
}, <Tooltip title="Можно загрузить видео." placement="top">
); <Box>
// setVideo(URL.createObjectURL(file)); <InfoIcon />
} </Box>
</Tooltip>
setBackgroundUploading(false); )}
}} </Box>
hidden {backgroundUploding ? (
accept=".mp4" <Skeleton
multiple
type="file"
/>
<UploadBox
icon={<UploadIcon />}
sx={{ sx={{
height: "48px",
width: "48px", width: "48px",
height: "48px",
transform: "none",
margin: "20px 0",
}} }}
/> />
</ButtonBase> ) : (
{quiz.config.startpage.background.video && ( <>
<video <ButtonBase
src={quiz.config.startpage.background.video} component="label"
width="400" sx={{
controls justifyContent: "center",
/> height: "48px",
width: "48px",
display: "flex",
alignItems: "center",
my: "20px",
}}
>
<input
onChange={async (event) => {
setBackgroundUploading(true);
const file = event.target.files?.[0];
if (file) {
await uploadQuizImage(
quiz.id,
file,
(quiz, url) => {
quiz.config.startpage.background.video =
url;
},
);
}
setBackgroundUploading(false);
}}
hidden
accept=".mp4"
multiple
type="file"
/>
<UploadBox
icon={<UploadIcon />}
sx={{
height: "48px",
width: "48px",
}}
/>
</ButtonBase>
</>
)} )}
</> </>
) : (
<Box sx={{ marginTop: "20px" }}>
<VideoElement
videoSrc={quiz.config.startpage.background.video}
theme={theme}
onDeleteClick={() => {
updateQuiz(quiz.id, (quiz) => {
quiz.config.startpage.background.video = null;
});
}}
/>
</Box>
)} )}
</> </>
)} )}

@ -0,0 +1,46 @@
import Box from "@mui/material/Box";
import { FC } from "react";
import DeleteIcon from "@mui/icons-material/Delete";
import { IconButton, SxProps, Theme } from "@mui/material";
type VideoElementProps = {
videoSrc: string;
width?: string;
theme: Theme;
onDeleteClick: () => void;
deleteIconSx?: SxProps<Theme>;
};
export const VideoElement: FC<VideoElementProps> = ({
videoSrc,
width = "300",
theme,
onDeleteClick,
deleteIconSx,
}) => {
return (
<Box sx={{ position: "relative", width: `${width}px` }}>
<video
style={{ borderRadius: "8px" }}
src={videoSrc}
width={width}
controls
/>
<IconButton
onClick={onDeleteClick}
sx={{
position: "absolute",
right: 0,
top: 0,
color: theme.palette.orange.main,
borderRadius: "8px",
borderBottomRightRadius: 0,
borderTopLeftRadius: 0,
...deleteIconSx,
}}
>
<DeleteIcon />
</IconButton>
</Box>
);
};

@ -44,7 +44,7 @@ export const createUntypedQuestion = (
) => ) =>
setProducedState( setProducedState(
(state) => { (state) => {
console.log("createUntypedQuestion", quizId) console.log("createUntypedQuestion", quizId);
const newUntypedQuestion = { const newUntypedQuestion = {
id: nanoid(), id: nanoid(),
quizId, quizId,
@ -278,7 +278,7 @@ export const updateQuestion = async <T = AnyTypedQuizQuestion>(
if (!q) return; if (!q) return;
if (q.type === null) if (q.type === null)
throw new Error("Cannot send update request for untyped question"); throw new Error("Cannot send update request for untyped question");
console.log("отправляемый квешен", q) console.log("отправляемый квешен", q);
try { try {
const response = await questionApi.edit( const response = await questionApi.edit(
questionToEditQuestionRequest(replaceEmptyLinesToSpace(q)), questionToEditQuestionRequest(replaceEmptyLinesToSpace(q)),
@ -449,8 +449,8 @@ export const createTypedQuestion = async (
requestQueue.enqueue(`createTypedQuestion-${questionId}`, async () => { requestQueue.enqueue(`createTypedQuestion-${questionId}`, async () => {
const questions = useQuestionsStore.getState().questions; const questions = useQuestionsStore.getState().questions;
const question = questions.find((q) => q.id === questionId); const question = questions.find((q) => q.id === questionId);
console.log("createTypedQuestion", question) console.log("createTypedQuestion", question);
console.log("createTypedQuestion", question?.quizId) console.log("createTypedQuestion", question?.quizId);
if (!question) return; if (!question) return;
if (question.type !== null) if (question.type !== null)
throw new Error("Cannot upgrade already typed question"); throw new Error("Cannot upgrade already typed question");

@ -21,6 +21,28 @@ export const EmojiPicker = ({ onEmojiSelect }: EmojiPickerProps) => (
onEmojiSelect={onEmojiSelect} onEmojiSelect={onEmojiSelect}
theme="light" theme="light"
locale="ru" locale="ru"
exceptEmojis={ignoreEmojis}
/> />
</Box> </Box>
); );
const ignoreEmojis = [
"two_men_holding_hands",
"two_women_holding_hands",
"man-kiss-man",
"woman-kiss-woman",
"man-heart-man",
"woman-heart-woman",
"man-man-boy",
"man-man-girl",
"man-man-girl-boy",
"man-man-girl-girl",
"man-man-boy-boy",
"woman-woman-boy",
"woman-woman-girl",
"woman-woman-girl-boy",
"woman-woman-girl-girl",
"woman-woman-boy-boy",
"rainbow-flag",
"transgender_flag",
];

@ -1,4 +1,4 @@
import { useState, FC } from "react"; import { FC, useState } from "react";
import { import {
Box, Box,
Button, Button,
@ -7,7 +7,6 @@ import {
Typography, Typography,
useTheme, useTheme,
} from "@mui/material"; } from "@mui/material";
import CustomTextField from "./CustomTextField";
import { updateQuestion, uploadQuestionImage } from "@root/questions/actions"; import { updateQuestion, uploadQuestionImage } from "@root/questions/actions";
import { CropModal, useCropModalState } from "@ui_kit/Modal/CropModal"; import { CropModal, useCropModalState } from "@ui_kit/Modal/CropModal";
@ -19,6 +18,7 @@ import { AnyTypedQuizQuestion } from "@model/questionTypes/shared";
import UploadBox from "@ui_kit/UploadBox"; import UploadBox from "@ui_kit/UploadBox";
import UploadIcon from "@icons/UploadIcon"; import UploadIcon from "@icons/UploadIcon";
import InfoIcon from "@icons/InfoIcon"; import InfoIcon from "@icons/InfoIcon";
import { VideoElement } from "../pages/startPage/VideoElement";
interface Iprops { interface Iprops {
resultData: AnyTypedQuizQuestion; resultData: AnyTypedQuizQuestion;
@ -167,67 +167,78 @@ export const MediaSelectionAndDisplay: FC<Iprops> = ({ resultData }) => {
)} )}
{!resultData.content.useImage && ( {!resultData.content.useImage && (
<> <>
<Box {!resultData.content.video ? (
sx={{ <>
display: "flex", <Box
alignItems: "center", sx={{
gap: "7px", display: "flex",
mt: "20px", alignItems: "center",
mb: "14px", gap: "7px",
}} mt: "20px",
> mb: "14px",
<Typography }}
sx={{ fontWeight: 500, color: theme.palette.grey3.main }} >
> <Typography
Добавить видео sx={{ fontWeight: 500, color: theme.palette.grey3.main }}
</Typography> >
<Tooltip title="Можно загрузить видео." placement="top"> Добавить видео
<Box> </Typography>
<InfoIcon /> <Tooltip title="Можно загрузить видео." placement="top">
<Box>
<InfoIcon />
</Box>
</Tooltip>
</Box> </Box>
</Tooltip> <ButtonBase
</Box> component="label"
<ButtonBase sx={{
component="label" justifyContent: "center",
sx={{ height: "48px",
justifyContent: "center", width: "48px",
height: "48px", display: "flex",
width: "48px", alignItems: "center",
display: "flex", my: "20px",
alignItems: "center", }}
my: "20px", >
}} <input
> onChange={(event) => {
<input const file = event.target.files?.[0];
onChange={(event) => { if (file) {
const file = event.target.files?.[0]; uploadQuestionImage(
if (file) { resultData.id,
uploadQuestionImage( quizQid,
resultData.id, file,
quizQid, (question, url) => {
file, question.content.video = url;
(question, url) => { },
question.content.video = url; );
}, }
); }}
} hidden
}} accept=".mp4"
hidden multiple
accept=".mp4" type="file"
multiple />
type="file" <UploadBox
/> icon={<UploadIcon />}
<UploadBox sx={{
icon={<UploadIcon />} height: "48px",
sx={{ width: "48px",
height: "48px", }}
width: "48px", />
</ButtonBase>
</>
) : (
<VideoElement
videoSrc={resultData.content.video}
theme={theme}
onDeleteClick={() => {
updateQuestion(resultData.id, (question) => {
question.content.video = null;
});
}} }}
/> />
</ButtonBase> )}
{resultData.content.video ? (
<video src={resultData.content.video} width="300" controls />
) : null}
</> </>
)} )}
</Box> </Box>

@ -67,7 +67,7 @@ export const CropModal: FC<Props> = ({
setCropModalImageBlob, setCropModalImageBlob,
onSaveImageClick, onSaveImageClick,
onClose, onClose,
questionId questionId,
}) => { }) => {
const theme = useTheme(); const theme = useTheme();
const [percentCrop, setPercentCrop] = useState<PercentCrop>(); const [percentCrop, setPercentCrop] = useState<PercentCrop>();
@ -297,13 +297,13 @@ export const CropModal: FC<Props> = ({
onChange={(_, newValue) => setDarken(newValue as number)} onChange={(_, newValue) => setDarken(newValue as number)}
/> />
</Box> </Box>
{questionId !== undefined && {questionId !== undefined && (
<IconButton <IconButton
onClick={() => { onClick={() => {
updateQuestion(questionId, (question) => { updateQuestion(questionId, (question) => {
question.content.back = null; question.content.back = null;
question.content.originalBack = null; question.content.originalBack = null;
}) });
onClose(); onClose();
}} }}
sx={{ sx={{
@ -316,7 +316,7 @@ export const CropModal: FC<Props> = ({
> >
<DeleteIcon /> <DeleteIcon />
</IconButton> </IconButton>
} )}
</Box> </Box>
<Box <Box
sx={{ sx={{
@ -351,8 +351,9 @@ export const CropModal: FC<Props> = ({
background: theme.palette.brightPurple.main, background: theme.palette.brightPurple.main,
fontSize: "18px", fontSize: "18px",
color: "#7E2AEA", color: "#7E2AEA",
border: `1px solid ${!completedCrop ? "rgba(0, 0, 0, 0.26)" : "#7E2AEA" border: `1px solid ${
}`, !completedCrop ? "rgba(0, 0, 0, 0.26)" : "#7E2AEA"
}`,
backgroundColor: "transparent", backgroundColor: "transparent",
}} }}
> >

@ -0,0 +1,32 @@
import { getGeneral, getDevices, getQuestions } from "@api/statistic";
import { useEffect, useState } from "react";
import moment from "moment";
interface Props {
quizId: string;
to: number;
from: number;
}
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({});
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]);
return { devices, general, questions };
}