add using video embeds by url

This commit is contained in:
nflnkr 2024-06-17 17:28:24 +03:00
parent f516897dad
commit b41251966a
14 changed files with 347 additions and 587 deletions

@ -7,7 +7,7 @@
"@emotion/react": "^11.10.5", "@emotion/react": "^11.10.5",
"@emotion/styled": "^11.10.5", "@emotion/styled": "^11.10.5",
"@frontend/kitui": "^1.0.82", "@frontend/kitui": "^1.0.82",
"@frontend/squzanswerer": "^1.0.44", "@frontend/squzanswerer": "^1.0.45",
"@mui/icons-material": "^5.10.14", "@mui/icons-material": "^5.10.14",
"@mui/material": "^5.10.14", "@mui/material": "^5.10.14",
"@mui/x-charts": "^6.19.5", "@mui/x-charts": "^6.19.5",

@ -12,7 +12,7 @@ export interface QuizQuestionPage extends QuizQuestionBase {
picture: string; picture: string;
originalPicture: string; originalPicture: string;
useImage: boolean; useImage: boolean;
video: string; video: string | null;
hint: QuestionHint; hint: QuestionHint;
rule: PreviewRule; rule: PreviewRule;
back: string; back: string;

@ -1,8 +1,4 @@
import type { import type { QuizQuestionBase, QuestionBranchingRule, QuestionHint } from "./shared";
QuizQuestionBase,
QuestionBranchingRule,
QuestionHint,
} from "./shared";
export interface QuizQuestionResult extends QuizQuestionBase { export interface QuizQuestionResult extends QuizQuestionBase {
type: "result"; type: "result";
@ -10,7 +6,7 @@ export interface QuizQuestionResult extends QuizQuestionBase {
id: string; id: string;
back: string; back: string;
originalBack: string; originalBack: string;
video: string; video: string | null;
innerName: string; innerName: string;
text: string; text: string;
price: [number] | [number, number]; price: [number] | [number, number];

@ -1,18 +1,5 @@
import { import { Box, Button, IconButton, Modal, Typography, useMediaQuery, useTheme } from "@mui/material";
Box, import { copyQuestion, deleteQuestion, deleteQuestionWithTimeout, updateQuestion } from "@root/questions/actions";
Button,
IconButton,
Modal,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import {
copyQuestion,
deleteQuestion,
deleteQuestionWithTimeout,
updateQuestion,
} from "@root/questions/actions";
import CustomTextField from "@ui_kit/CustomTextField"; import CustomTextField from "@ui_kit/CustomTextField";
import { CopyIcon } from "@icons/questionsPage/CopyIcon"; import { CopyIcon } from "@icons/questionsPage/CopyIcon";
@ -71,7 +58,7 @@ export default function PageOptions({ disableInput, question }: Props) {
</Box> </Box>
<MediaSelectionAndDisplay <MediaSelectionAndDisplay
resultData={question} question={question}
cropAspectRatio={{ width: 1388.8, height: 793.2 }} cropAspectRatio={{ width: 1388.8, height: 793.2 }}
/> />
</Box> </Box>
@ -107,16 +94,17 @@ export default function PageOptions({ disableInput, question }: Props) {
if (question.content.rule.parentId.length !== 0) { if (question.content.rule.parentId.length !== 0) {
setOpenDelete(true); setOpenDelete(true);
} else { } else {
deleteQuestionWithTimeout(question.id, () => deleteQuestionWithTimeout(question.id, () => DeleteFunction(question.id));
DeleteFunction(question.id),
);
} }
}} }}
data-cy="delete-question" data-cy="delete-question"
> >
<DeleteIcon color={"#4D4D4D"} /> <DeleteIcon color={"#4D4D4D"} />
</IconButton> </IconButton>
<Modal open={openDelete} onClose={() => setOpenDelete(false)}> <Modal
open={openDelete}
onClose={() => setOpenDelete(false)}
>
<Box <Box
sx={{ sx={{
position: "absolute", position: "absolute",
@ -128,10 +116,12 @@ export default function PageOptions({ disableInput, question }: Props) {
background: "#FFFFFF", background: "#FFFFFF",
}} }}
> >
<Typography variant="h6" sx={{ textAlign: "center" }}> <Typography
Вы удаляете вопрос, участвующий в ветвлении. Все его потомки variant="h6"
потеряют данные ветвления. Вы уверены, что хотите удалить sx={{ textAlign: "center" }}
вопрос? >
Вы удаляете вопрос, участвующий в ветвлении. Все его потомки потеряют данные ветвления. Вы уверены, что
хотите удалить вопрос?
</Typography> </Typography>
<Box <Box
sx={{ sx={{
@ -152,9 +142,7 @@ export default function PageOptions({ disableInput, question }: Props) {
variant="contained" variant="contained"
sx={{ minWidth: "150px" }} sx={{ minWidth: "150px" }}
onClick={() => { onClick={() => {
deleteQuestionWithTimeout(question.id, () => deleteQuestionWithTimeout(question.id, () => DeleteFunction(question.id));
DeleteFunction(question.id),
);
}} }}
> >
Подтвердить Подтвердить

@ -1,16 +1,8 @@
import { import { Box, Button, ButtonBase, Dialog, Typography, useTheme } from "@mui/material";
Box,
Button,
ButtonBase,
Modal,
Typography,
useTheme,
} from "@mui/material";
import SelectableButton from "@ui_kit/SelectableButton";
import CustomTextField from "@ui_kit/CustomTextField"; import CustomTextField from "@ui_kit/CustomTextField";
import SelectableButton from "@ui_kit/SelectableButton";
import { useState } from "react"; import { useState } from "react";
import UploadIcon from "../../assets/icons/UploadIcon"; import UploadIcon from "../../assets/icons/UploadIcon";
import type { DragEvent } from "react"; import type { DragEvent } from "react";
type BackgroundTypeModal = "linkVideo" | "ownVideo"; type BackgroundTypeModal = "linkVideo" | "ownVideo";
@ -18,18 +10,12 @@ type BackgroundTypeModal = "linkVideo" | "ownVideo";
type HelpQuestionsProps = { type HelpQuestionsProps = {
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
video: string; video: string | null;
onUpload: (number: string) => void; onUpload: (number: string) => void;
}; };
export const UploadVideoModal = ({ export default function UploadVideoModal({ open, onClose, video, onUpload }: HelpQuestionsProps) {
open, const [backgroundTypeModal, setBackgroundTypeModal] = useState<BackgroundTypeModal>("linkVideo");
onClose,
video,
onUpload,
}: HelpQuestionsProps) => {
const [backgroundTypeModal, setBackgroundTypeModal] =
useState<BackgroundTypeModal>("linkVideo");
const theme = useTheme(); const theme = useTheme();
const handleDrop = (event: DragEvent<HTMLDivElement>) => { const handleDrop = (event: DragEvent<HTMLDivElement>) => {
@ -42,118 +28,102 @@ export const UploadVideoModal = ({
}; };
return ( return (
<Modal <Dialog
open={open} open={open}
onClose={onClose} onClose={onClose}
aria-labelledby="modal-modal-title" PaperProps={{
aria-describedby="modal-modal-description" sx: {
> maxWidth: "640px",
<Box
sx={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
maxWidth: "690px",
bgcolor: "background.paper",
borderRadius: "12px", borderRadius: "12px",
boxShadow: 24, boxShadow: 24,
p: 0, p: 0,
overflow: "hidden", overflow: "hidden",
},
}}
>
<Box
sx={{
display: "flex",
padding: "20px",
background: theme.palette.background.default,
}} }}
> >
<Box <Typography sx={{ color: "#9A9AAF" }}>
sx={{ Видео можно вставить с любого хостинга: YouTube, Vimeo или загрузить собственное
display: "flex", </Typography>
padding: "20px", <Button
background: theme.palette.background.default, onClick={onClose}
}} variant="contained"
> >
<Typography sx={{ color: "#9A9AAF" }}> Готово
Видео можно вставить с любого хостинга: YouTube, Vimeo или загрузить </Button>
собственное
</Typography>
<Button onClick={onClose} variant="contained">
Готово
</Button>
</Box>
<Box sx={{ padding: "20px", gap: "10px", display: "flex" }}>
<SelectableButton
isSelected={backgroundTypeModal === "linkVideo"}
onClick={() => setBackgroundTypeModal("linkVideo")}
sx={{ maxWidth: "170px", padding: "10px" }}
>
Ссылка на видео
</SelectableButton>
<SelectableButton
isSelected={backgroundTypeModal === "ownVideo"}
onClick={() => setBackgroundTypeModal("ownVideo")}
sx={{ maxWidth: "170px", padding: "10px" }}
>
Загрузить свое
</SelectableButton>
</Box>
{backgroundTypeModal === "linkVideo" ? (
<Box sx={{ padding: "20px" }}>
<Typography sx={{ paddingBottom: "15px", fontWeight: "500" }}>
Ссылка на видео
</Typography>
<CustomTextField
placeholder={"http://example.com"}
text={video}
onChange={({ target }) => onUpload(target.value || " ")}
/>
</Box>
) : (
<Box sx={{ padding: "20px" }}>
<Typography sx={{ paddingBottom: "15px", fontWeight: "500" }}>
Загрузите видео
</Typography>
<ButtonBase
component="label"
sx={{ justifyContent: "flex-start", width: "100%" }}
>
<input
onChange={({ target }) => {
if (target.files?.length) {
onUpload(URL.createObjectURL(target.files[0] || " "));
}
}}
hidden
accept="video/*"
multiple
type="file"
/>
<Box
onDragOver={(event: DragEvent<HTMLDivElement>) =>
event.preventDefault()
}
onDrop={handleDrop}
sx={{
width: "580px",
padding: "33px 33px 33px 50px",
display: "flex",
alignItems: "center",
backgroundColor: theme.palette.background.default,
border: `1px solid ${theme.palette.grey2.main}`,
borderRadius: "8px",
gap: "50px",
}}
>
<UploadIcon />
<Box sx={{ color: "#9A9AAF" }}>
<Typography sx={{ fontWeight: "500" }}>
Добавить видео
</Typography>
<Typography sx={{ fontSize: "16px" }}>
Принимает .mp4 и .mov формат максимум 100мб
</Typography>
</Box>
</Box>
</ButtonBase>
</Box>
)}
</Box> </Box>
</Modal> <Box sx={{ padding: "20px", gap: "10px", display: "flex" }}>
<SelectableButton
isSelected={backgroundTypeModal === "linkVideo"}
onClick={() => setBackgroundTypeModal("linkVideo")}
sx={{ maxWidth: "170px", padding: "10px" }}
>
Ссылка на видео
</SelectableButton>
<SelectableButton
isSelected={backgroundTypeModal === "ownVideo"}
onClick={() => setBackgroundTypeModal("ownVideo")}
sx={{ maxWidth: "170px", padding: "10px" }}
>
Загрузить свое
</SelectableButton>
</Box>
{backgroundTypeModal === "linkVideo" ? (
<Box sx={{ padding: "20px" }}>
<Typography sx={{ paddingBottom: "15px", fontWeight: "500" }}>Ссылка на видео</Typography>
<CustomTextField
placeholder={"http://example.com"}
value={video || ""}
onChange={({ target }) => onUpload(target.value || " ")}
/>
</Box>
) : (
<Box sx={{ padding: "20px" }}>
<Typography sx={{ paddingBottom: "15px", fontWeight: "500" }}>Загрузите видео</Typography>
<ButtonBase
component="label"
sx={{ justifyContent: "flex-start", width: "100%" }}
>
<input
onChange={({ target }) => {
if (target.files?.length) {
onUpload(URL.createObjectURL(target.files[0] || " "));
}
}}
hidden
accept="video/*"
multiple
type="file"
/>
<Box
onDragOver={(event: DragEvent<HTMLDivElement>) => event.preventDefault()}
onDrop={handleDrop}
sx={{
width: "580px",
padding: "33px 33px 33px 50px",
display: "flex",
alignItems: "center",
backgroundColor: theme.palette.background.default,
border: `1px solid ${theme.palette.grey2.main}`,
borderRadius: "8px",
gap: "50px",
}}
>
<UploadIcon />
<Box sx={{ color: "#9A9AAF" }}>
<Typography sx={{ fontWeight: "500" }}>Добавить видео</Typography>
<Typography sx={{ fontSize: "16px" }}>Принимает .mp4 и .mov формат максимум 100мб</Typography>
</Box>
</Box>
</ButtonBase>
</Box>
)}
</Dialog>
); );
}; }

@ -5,7 +5,7 @@ import SelectableButton from "@ui_kit/SelectableButton";
import UploadBox from "@ui_kit/UploadBox"; import UploadBox from "@ui_kit/UploadBox";
import { memo, useState } from "react"; import { memo, useState } from "react";
import UploadIcon from "../../assets/icons/UploadIcon"; import UploadIcon from "../../assets/icons/UploadIcon";
import { UploadVideoModal } from "./UploadVideoModal"; import UploadVideoModal from "./UploadVideoModal";
type BackgroundType = "text" | "video"; type BackgroundType = "text" | "video";
@ -15,11 +15,7 @@ type HelpQuestionsProps = {
hintText: string; hintText: string;
}; };
const HelpQuestions = memo<HelpQuestionsProps>(function ({ const HelpQuestions = memo<HelpQuestionsProps>(function ({ questionId, hintVideo, hintText }) {
questionId,
hintVideo,
hintText,
}) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [backgroundType, setBackgroundType] = useState<BackgroundType>("text"); const [backgroundType, setBackgroundType] = useState<BackgroundType>("text");
@ -71,15 +67,17 @@ const HelpQuestions = memo<HelpQuestionsProps>(function ({
</> </>
) : ( ) : (
<Box> <Box>
<Typography sx={{ paddingBottom: "15px", fontWeight: "500" }}> <Typography sx={{ paddingBottom: "15px", fontWeight: "500" }}>Загрузите видео</Typography>
Загрузите видео
</Typography>
<ButtonBase <ButtonBase
onClick={() => setOpen(true)} onClick={() => setOpen(true)}
sx={{ justifyContent: "flex-start" }} sx={{ justifyContent: "flex-start" }}
> >
{hintVideo ? ( {hintVideo ? (
<video src={hintVideo} width="400" controls /> <video
src={hintVideo}
width="400"
controls
/>
) : ( ) : (
<> <>
<UploadBox <UploadBox

@ -1,9 +1,6 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { import { getQuestionByContentId, updateQuestion } from "@root/questions/actions";
getQuestionByContentId,
updateQuestion,
} from "@root/questions/actions";
import { useCurrentQuiz } from "@root/quizes/hooks"; import { useCurrentQuiz } from "@root/quizes/hooks";
import CustomTextField from "@ui_kit/CustomTextField"; import CustomTextField from "@ui_kit/CustomTextField";
@ -40,11 +37,7 @@ interface Props {
resultData: QuizQuestionResult; resultData: QuizQuestionResult;
} }
export const checkEmptyData = ({ export const checkEmptyData = ({ resultData }: { resultData: QuizQuestionResult }) => {
resultData,
}: {
resultData: QuizQuestionResult;
}) => {
let check = true; let check = true;
if ( if (
resultData.title?.length > 0 || resultData.title?.length > 0 ||
@ -109,15 +102,9 @@ const InfoView = ({ resultData }: { resultData: QuizQuestionResult }) => {
<Typography> <Typography>
{resultData?.content.rule.parentId === "line" {resultData?.content.rule.parentId === "line"
? "Единый результат в конце прохождения опроса без ветвления" ? "Единый результат в конце прохождения опроса без ветвления"
: `Заголовок вопроса, после которого появится результат: "${ : `Заголовок вопроса, после которого появится результат: "${question?.title || "нет заголовка"}"`}
question?.title || "нет заголовка"
}"`}
</Typography> </Typography>
{checkEmpty && ( {checkEmpty && <Typography color="red">Вы не заполнили этот результат никакими данными</Typography>}
<Typography color="red">
Вы не заполнили этот результат никакими данными
</Typography>
)}
</Paper> </Paper>
</Popover> </Popover>
</> </>
@ -140,8 +127,7 @@ export const ResultCard = ({ resultContract, resultData }: Props) => {
useEffect(() => { useEffect(() => {
if ( if (
resultData.content.hint.text || resultData.content.hint.text ||
(quiz?.config.resultInfo.showResultForm === "after" && (quiz?.config.resultInfo.showResultForm === "after" && resultData.content.redirect)
resultData.content.redirect)
) { ) {
setButtonPlus(false); setButtonPlus(false);
} }
@ -167,9 +153,7 @@ export const ResultCard = ({ resultContract, resultData }: Props) => {
<Typography sx={{ color: theme.palette.grey2.main, padding: "5px 20px" }}> <Typography sx={{ color: theme.palette.grey2.main, padding: "5px 20px" }}>
{resultData?.content.rule.parentId === "line" {resultData?.content.rule.parentId === "line"
? "Единый результат в конце прохождения опроса без ветвления" ? "Единый результат в конце прохождения опроса без ветвления"
: `Заголовок вопроса, после которого появится результат: "${ : `Заголовок вопроса, после которого появится результат: "${question?.title || "нет заголовка"}"`}
question?.title || "нет заголовка"
}"`}
</Typography> </Typography>
<Box <Box
sx={{ sx={{
@ -194,16 +178,11 @@ export const ResultCard = ({ resultContract, resultData }: Props) => {
placeholder={"Заголовок результата"} placeholder={"Заголовок результата"}
maxLength={200} maxLength={200}
onChange={({ target }: { target: HTMLInputElement }) => onChange={({ target }: { target: HTMLInputElement }) =>
updateQuestion( updateQuestion(resultData.id, (question) => (question.title = target.value))
resultData.id,
(question) => (question.title = target.value),
)
} }
sx={{ sx={{
margin: isMobile ? "10px 0" : 0, margin: isMobile ? "10px 0" : 0,
backgroundColor: expand backgroundColor: expand ? theme.palette.background.default : "transparent",
? theme.palette.background.default
: "transparent",
height: "48px", height: "48px",
borderRadius: "10px", borderRadius: "10px",
borderWidth: "1px !important", borderWidth: "1px !important",
@ -273,11 +252,7 @@ export const ResultCard = ({ resultContract, resultData }: Props) => {
<Typography id={"id-copy"}>{resultData.backendId}</Typography> <Typography id={"id-copy"}>{resultData.backendId}</Typography>
<IconButton <IconButton
edge="end" edge="end"
onClick={() => onClick={() => navigator.clipboard.writeText(document.querySelector("#id-copy").innerText)}
navigator.clipboard.writeText(
document.querySelector("#id-copy").innerText,
)
}
> >
<CopyIcon <CopyIcon
color={"#ffffff"} color={"#ffffff"}
@ -300,10 +275,7 @@ export const ResultCard = ({ resultContract, resultData }: Props) => {
id="headline-is-bolder" id="headline-is-bolder"
value={resultData.description} value={resultData.description}
onChange={({ target }: { target: HTMLInputElement }) => onChange={({ target }: { target: HTMLInputElement }) =>
updateQuestion( updateQuestion(resultData.id, (question) => (question.description = target.value))
resultData.id,
(question) => (question.description = target.value),
)
} }
placeholder={"Заголовок пожирнее"} placeholder={"Заголовок пожирнее"}
maxLength={200} maxLength={200}
@ -335,10 +307,7 @@ export const ResultCard = ({ resultContract, resultData }: Props) => {
placeholder={"Заголовок результата"} placeholder={"Заголовок результата"}
maxLength={200} maxLength={200}
onChange={({ target }: { target: HTMLInputElement }) => onChange={({ target }: { target: HTMLInputElement }) =>
updateQuestion( updateQuestion(resultData.id, (question) => (question.title = target.value))
resultData.id,
(question) => (question.title = target.value),
)
} }
/> />
</Box> </Box>
@ -350,10 +319,7 @@ export const ResultCard = ({ resultContract, resultData }: Props) => {
if (target.value.length <= 3000) { if (target.value.length <= 3000) {
setInputValue(target.value); setInputValue(target.value);
} }
updateQuestion( updateQuestion(resultData.id, (question) => (question.content.text = target.value));
resultData.id,
(question) => (question.content.text = target.value),
);
}} }}
fullWidth fullWidth
placeholder="Описание" placeholder="Описание"
@ -379,7 +345,7 @@ export const ResultCard = ({ resultContract, resultData }: Props) => {
/> />
<MediaSelectionAndDisplay <MediaSelectionAndDisplay
resultData={resultData} question={resultData}
cropAspectRatio={{ width: 305.9, height: 305.9 }} cropAspectRatio={{ width: 305.9, height: 305.9 }}
/> />
@ -422,10 +388,7 @@ export const ResultCard = ({ resultContract, resultData }: Props) => {
<IconButton <IconButton
onClick={() => { onClick={() => {
setButtonPlus(true); setButtonPlus(true);
updateQuestion( updateQuestion(resultData.id, (q) => (q.content.hint.text = ""));
resultData.id,
(q) => (q.content.hint.text = ""),
);
}} }}
> >
<Trash /> <Trash />
@ -475,12 +438,9 @@ export const ResultCard = ({ resultContract, resultData }: Props) => {
id="link-page-result" id="link-page-result"
value={resultData.content.redirect} value={resultData.content.redirect}
onChange={({ target }: { target: HTMLInputElement }) => onChange={({ target }: { target: HTMLInputElement }) =>
updateQuestion<QuizQuestionResult>( updateQuestion<QuizQuestionResult>(resultData.id, (question) => {
resultData.id, question.content.redirect = target.value;
(question) => { })
question.content.redirect = target.value;
},
)
} }
placeholder="https://penahub.ru" placeholder="https://penahub.ru"
maxLength={200} maxLength={200}

@ -6,16 +6,7 @@ import { useCurrentQuiz } from "@root/quizes/hooks";
import { SwitchSetting } from "../SwichResult"; import { SwitchSetting } from "../SwichResult";
import Info from "@icons/Info"; import Info from "@icons/Info";
import { import { Box, IconButton, Paper, Button, Typography, useMediaQuery, useTheme, Popover } from "@mui/material";
Box,
IconButton,
Paper,
Button,
Typography,
useMediaQuery,
useTheme,
Popover,
} from "@mui/material";
import ExpandLessIconBG from "@icons/ExpandLessIconBG"; import ExpandLessIconBG from "@icons/ExpandLessIconBG";
import ExpandLessIcon from "@mui/icons-material/ExpandLess"; import ExpandLessIcon from "@mui/icons-material/ExpandLess";
@ -88,10 +79,7 @@ const InfoView = () => {
flexDirection: "column", flexDirection: "column",
}} }}
> >
<Typography> <Typography>Oтправка письма с результатом респонденту после отображения на экране</Typography>
Oтправка письма с результатом респонденту после отображения на
экране
</Typography>
</Paper> </Paper>
</Popover> </Popover>
</> </>
@ -190,7 +178,10 @@ export const WhenCard = ({ quizExpand }: Props) => {
}} }}
> >
{whenValues.map(({ title, value, id }, index) => ( {whenValues.map(({ title, value, id }, index) => (
<Box display="flex"> <Box
display="flex"
key={id}
>
<Button <Button
id={id} id={id}
onClick={() => { onClick={() => {
@ -201,32 +192,16 @@ export const WhenCard = ({ quizExpand }: Props) => {
}} }}
key={title} key={title}
sx={{ sx={{
bgcolor: bgcolor: quiz?.config.resultInfo.showResultForm === value ? " #7E2AEA" : "#F2F3F7",
quiz?.config.resultInfo.showResultForm === value color: quiz?.config.resultInfo.showResultForm === value ? " white" : "#9A9AAF",
? " #7E2AEA" minWidth: isSmallMonitor ? (isMobile ? undefined : "310px") : "auto",
: "#F2F3F7",
color:
quiz?.config.resultInfo.showResultForm === value
? " white"
: "#9A9AAF",
minWidth: isSmallMonitor
? isMobile
? undefined
: "310px"
: "auto",
borderRadius: "8px", borderRadius: "8px",
width: isMobile ? "100%" : "220px", width: isMobile ? "100%" : "220px",
height: "44px", height: "44px",
fontSize: "17px", fontSize: "17px",
border: border: quiz?.config.resultInfo.showResultForm === value ? "none" : "1px solid #9A9AAF",
quiz?.config.resultInfo.showResultForm === value
? "none"
: "1px solid #9A9AAF",
"&:hover": { "&:hover": {
backgroundColor: backgroundColor: quiz?.config.resultInfo.showResultForm === value ? "#581CA7" : "#7E2AEA",
quiz?.config.resultInfo.showResultForm === value
? "#581CA7"
: "#7E2AEA",
color: "white", color: "white",
}, },
}} }}
@ -252,32 +227,16 @@ export const WhenCard = ({ quizExpand }: Props) => {
}); });
}} }}
sx={{ sx={{
bgcolor: bgcolor: quiz?.config.resultInfo.when === "email" ? " #7E2AEA" : "#F2F3F7",
quiz?.config.resultInfo.when === "email" color: quiz?.config.resultInfo.when === "email" ? " white" : "#9A9AAF",
? " #7E2AEA" minWidth: isSmallMonitor ? (isMobile ? undefined : "310px") : "auto",
: "#F2F3F7",
color:
quiz?.config.resultInfo.when === "email"
? " white"
: "#9A9AAF",
minWidth: isSmallMonitor
? isMobile
? undefined
: "310px"
: "auto",
borderRadius: "8px", borderRadius: "8px",
width: isMobile ? "100%" : "220px", width: isMobile ? "100%" : "220px",
height: "44px", height: "44px",
fontSize: "17px", fontSize: "17px",
border: border: quiz?.config.resultInfo.when === "email" ? "none" : "1px solid #9A9AAF",
quiz?.config.resultInfo.when === "email"
? "none"
: "1px solid #9A9AAF",
"&:hover": { "&:hover": {
backgroundColor: backgroundColor: quiz?.config.resultInfo.when === "email" ? "#581CA7" : "#7E2AEA",
quiz?.config.resultInfo.when === "email"
? "#581CA7"
: "#7E2AEA",
color: "white", color: "white",
}, },
}} }}

@ -27,11 +27,7 @@ import {
useMediaQuery, useMediaQuery,
useTheme, useTheme,
} from "@mui/material"; } from "@mui/material";
import { import { incrementCurrentStep, updateQuiz, uploadQuizImage } from "@root/quizes/actions";
incrementCurrentStep,
updateQuiz,
uploadQuizImage,
} from "@root/quizes/actions";
import { useCurrentQuiz } from "@root/quizes/hooks"; import { useCurrentQuiz } from "@root/quizes/hooks";
import CustomCheckbox from "@ui_kit/CustomCheckbox"; import CustomCheckbox from "@ui_kit/CustomCheckbox";
import CustomTextField from "@ui_kit/CustomTextField"; import CustomTextField from "@ui_kit/CustomTextField";
@ -47,24 +43,12 @@ 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"; import { VideoElement } from "./VideoElement";
import * as React from "react"; import UploadVideoModal from "../Questions/UploadVideoModal";
const designTypes = [ const designTypes = [
[ ["standard", (color: string) => <LayoutStandartIcon color={color} />, "Standard"],
"standard", ["expanded", (color: string) => <LayoutExpandedIcon color={color} />, "Expanded"],
(color: string) => <LayoutStandartIcon color={color} />, ["centered", (color: string) => <LayoutCenteredIcon color={color} />, "Centered"],
"Standard",
],
[
"expanded",
(color: string) => <LayoutExpandedIcon color={color} />,
"Expanded",
],
[
"centered",
(color: string) => <LayoutCenteredIcon color={color} />,
"Centered",
],
] as const; ] as const;
export default function StartPageSettings() { export default function StartPageSettings() {
@ -78,12 +62,28 @@ export default function StartPageSettings() {
const [faviconUploding, setFaviconUploading] = useState<boolean>(false); const [faviconUploding, setFaviconUploading] = useState<boolean>(false);
const [backgroundUploding, setBackgroundUploading] = useState<boolean>(false); const [backgroundUploding, setBackgroundUploading] = useState<boolean>(false);
const [logoUploding, setLogoUploading] = useState<boolean>(false); const [logoUploding, setLogoUploading] = useState<boolean>(false);
const [isVideoUploadDialogOpen, setIsVideoUploadDialogOpen] = useState<boolean>(false);
if (!quiz) return null; if (!quiz) return null;
const MobileVersionHC = (bool: boolean) => { async function handleVideoUpload(videoUrl: string) {
setMobileVersion(bool); if (!quiz) return;
};
setBackgroundUploading(true);
if (videoUrl.startsWith("blob:")) {
const videoBlob = await (await fetch(videoUrl)).blob();
uploadQuizImage(quiz.id, videoBlob, (quiz, url) => {
quiz.config.startpage.background.video = url;
});
} else {
updateQuiz(quiz.id, (quiz) => {
quiz.config.startpage.background.video = videoUrl;
});
}
setBackgroundUploading(false);
}
const designType = quiz?.config?.startpageType; const designType = quiz?.config?.startpageType;
let cropAspectRatio: let cropAspectRatio:
@ -129,6 +129,12 @@ export default function StartPageSettings() {
return ( return (
<> <>
<UploadVideoModal
open={isVideoUploadDialogOpen}
onClose={() => setIsVideoUploadDialogOpen(false)}
onUpload={handleVideoUpload}
video={quiz.config.startpage.background.video}
/>
<Typography <Typography
variant="h5" variant="h5"
sx={{ marginTop: "60px", marginBottom: isSmallMonitor ? "0" : "40px" }} sx={{ marginTop: "60px", marginBottom: isSmallMonitor ? "0" : "40px" }}
@ -143,25 +149,22 @@ export default function StartPageSettings() {
fontWeight: 500, fontWeight: 500,
fontSize: "16px", fontSize: "16px",
color: formState === "design" ? "#7E2AEA" : "#7D7E86", color: formState === "design" ? "#7E2AEA" : "#7D7E86",
borderBottom: borderBottom: formState === "design" ? "2px solid #7E2AEA" : "1px solid transparent",
formState === "design"
? "2px solid #7E2AEA"
: "1px solid transparent",
}} }}
> >
Дизайн Дизайн
</Typography> </Typography>
</Button> </Button>
<Button id="contentButton" onClick={() => setFormState("content")}> <Button
id="contentButton"
onClick={() => setFormState("content")}
>
<Typography <Typography
sx={{ sx={{
fontWeight: 500, fontWeight: 500,
fontSize: "16px", fontSize: "16px",
color: formState === "content" ? "#7E2AEA" : "#7D7E86", color: formState === "content" ? "#7E2AEA" : "#7D7E86",
borderBottom: borderBottom: formState === "content" ? "2px solid #7E2AEA" : "1px solid transparent",
formState === "content"
? "2px solid #7E2AEA"
: "1px solid transparent",
}} }}
> >
Контент Контент
@ -222,8 +225,7 @@ export default function StartPageSettings() {
displayEmpty displayEmpty
onChange={(e) => onChange={(e) =>
updateQuiz(quiz.id, (quiz) => { updateQuiz(quiz.id, (quiz) => {
quiz.config.startpageType = e.target quiz.config.startpageType = e.target.value as QuizStartpageType;
.value as QuizStartpageType;
}) })
} }
sx={{ sx={{
@ -280,11 +282,7 @@ export default function StartPageSettings() {
color: theme.palette.grey2.main, color: theme.palette.grey2.main,
}} }}
> >
{type[1]( {type[1](type[0] === designType ? theme.palette.orange.main : theme.palette.grey2.main)}
type[0] === designType
? theme.palette.orange.main
: theme.palette.grey2.main,
)}
{type[2]} {type[2]}
</MenuItem> </MenuItem>
))} ))}
@ -331,10 +329,7 @@ export default function StartPageSettings() {
{quiz.config.startpage.background.type === "image" && ( {quiz.config.startpage.background.type === "image" && (
<Box <Box
sx={{ sx={{
display: display: quiz.config.startpage.background.type === "image" ? "flex" : "none",
quiz.config.startpage.background.type === "image"
? "flex"
: "none",
flexDirection: "column", flexDirection: "column",
}} }}
> >
@ -368,15 +363,12 @@ export default function StartPageSettings() {
sx={{ maxWidth: "300px" }} sx={{ maxWidth: "300px" }}
cropAspectRatio={cropAspectRatio} cropAspectRatio={cropAspectRatio}
imageUrl={quiz.config.startpage.background.desktop} imageUrl={quiz.config.startpage.background.desktop}
originalImageUrl={ originalImageUrl={quiz.config.startpage.background.originalDesktop}
quiz.config.startpage.background.originalDesktop
}
onImageUploadClick={async (file) => { onImageUploadClick={async (file) => {
setBackgroundUploading(true); setBackgroundUploading(true);
await uploadQuizImage(quiz.id, file, (quiz, url) => { await uploadQuizImage(quiz.id, file, (quiz, url) => {
quiz.config.startpage.background.desktop = url; quiz.config.startpage.background.desktop = url;
quiz.config.startpage.background.originalDesktop = quiz.config.startpage.background.originalDesktop = url;
url;
}); });
setBackgroundUploading(false); setBackgroundUploading(false);
@ -426,7 +418,10 @@ export default function StartPageSettings() {
{isMobile ? ( {isMobile ? (
<TooltipClickInfo title={"Можно загрузить видео."} /> <TooltipClickInfo title={"Можно загрузить видео."} />
) : ( ) : (
<Tooltip title="Можно загрузить видео." placement="top"> <Tooltip
title="Можно загрузить видео."
placement="top"
>
<Box> <Box>
<InfoIcon /> <InfoIcon />
</Box> </Box>
@ -445,7 +440,7 @@ export default function StartPageSettings() {
) : ( ) : (
<> <>
<ButtonBase <ButtonBase
component="label" onClick={() => setIsVideoUploadDialogOpen(true)}
sx={{ sx={{
justifyContent: "center", justifyContent: "center",
height: "48px", height: "48px",
@ -455,29 +450,6 @@ export default function StartPageSettings() {
my: "20px", 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 <UploadBox
icon={<UploadIcon />} icon={<UploadIcon />}
sx={{ sx={{
@ -559,10 +531,7 @@ export default function StartPageSettings() {
<> <>
<Box <Box
sx={{ sx={{
display: display: quiz.config.startpage.background.type === "image" ? "flex" : "none",
quiz.config.startpage.background.type === "image"
? "flex"
: "none",
flexDirection: "column", flexDirection: "column",
}} }}
> >
@ -641,10 +610,7 @@ export default function StartPageSettings() {
<> <>
<Box <Box
sx={{ sx={{
display: display: quiz.config.startpage.background.type === "image" ? "flex" : "none",
quiz.config.startpage.background.type === "image"
? "flex"
: "none",
flexDirection: "column", flexDirection: "column",
}} }}
> >
@ -870,22 +836,24 @@ export default function StartPageSettings() {
maxLength={1000} maxLength={1000}
/> />
<Extra /> <Extra />
<Box sx={{display: "flex", gap: "20px", alignItems: "center"}}> <Box sx={{ display: "flex", gap: "20px", alignItems: "center" }}>
<CustomizedSwitch <CustomizedSwitch
checked={quiz.config.antifraud} checked={quiz.config.antifraud}
onChange={(e) => { onChange={(e) => {
updateQuiz(quiz.id, (quiz) => { updateQuiz(quiz.id, (quiz) => {
quiz.config.antifraud = e.target.checked; quiz.config.antifraud = e.target.checked;
});
}) }}
}} />
/> <Typography
<Typography sx={{fontWeight: 500, sx={{
color: theme.palette.grey3.main,}} fontWeight: 500,
> color: theme.palette.grey3.main,
Включить антифрод</Typography> }}
</Box> >
Включить антифрод
</Typography>
</Box>
</> </>
)} )}
</Box> </Box>

@ -2,6 +2,7 @@ import Box from "@mui/material/Box";
import { FC } from "react"; import { FC } from "react";
import DeleteIcon from "@mui/icons-material/Delete"; import DeleteIcon from "@mui/icons-material/Delete";
import { IconButton, SxProps, Theme } from "@mui/material"; import { IconButton, SxProps, Theme } from "@mui/material";
import { QuizVideo } from "@frontend/squzanswerer";
type VideoElementProps = { type VideoElementProps = {
videoSrc: string; videoSrc: string;
@ -20,12 +21,7 @@ export const VideoElement: FC<VideoElementProps> = ({
}) => { }) => {
return ( return (
<Box sx={{ position: "relative", width: `${width}px` }}> <Box sx={{ position: "relative", width: `${width}px` }}>
<video <QuizVideo videoUrl={videoSrc} />
style={{ borderRadius: "8px" }}
src={videoSrc}
width={width}
controls
/>
<IconButton <IconButton
onClick={onDeleteClick} onClick={onDeleteClick}
sx={{ sx={{

@ -20,7 +20,7 @@ export const setEditQuizId = (quizId: number | null) =>
{ {
type: "setEditQuizId", type: "setEditQuizId",
quizId, quizId,
}, }
); );
export const resetEditConfig = () => export const resetEditConfig = () =>
@ -37,7 +37,7 @@ export const setQuizes = (quizes: RawQuiz[] | null) =>
{ {
type: "setQuizes", type: "setQuizes",
quizes, quizes,
}, }
); );
const addQuiz = (quiz: Quiz) => const addQuiz = (quiz: Quiz) =>
@ -48,7 +48,7 @@ const addQuiz = (quiz: Quiz) =>
{ {
type: "addQuiz", type: "addQuiz",
quiz, quiz,
}, }
); );
const removeQuiz = (quizId: string) => const removeQuiz = (quizId: string) =>
@ -62,7 +62,7 @@ const removeQuiz = (quizId: string) =>
{ {
type: "removeQuiz", type: "removeQuiz",
quizId, quizId,
}, }
); );
const setQuizBackendId = (quizId: string, backendId: number) => const setQuizBackendId = (quizId: string, backendId: number) =>
@ -77,20 +77,17 @@ const setQuizBackendId = (quizId: string, backendId: number) =>
type: "setQuizBackendId", type: "setQuizBackendId",
quizId, quizId,
backendId, backendId,
}, }
); );
export const incrementCurrentStep = () => export const incrementCurrentStep = () =>
setProducedState( setProducedState(
(state) => { (state) => {
state.currentStep = Math.min( state.currentStep = Math.min(maxQuizSetupSteps - 1, state.currentStep + 1);
maxQuizSetupSteps - 1,
state.currentStep + 1,
);
}, },
{ {
type: "incrementCurrentStep", type: "incrementCurrentStep",
}, }
); );
export const decrementCurrentStep = () => export const decrementCurrentStep = () =>
@ -100,7 +97,7 @@ export const decrementCurrentStep = () =>
}, },
{ {
type: "decrementCurrentStep", type: "decrementCurrentStep",
}, }
); );
export const setCurrentStep = (step: number) => export const setCurrentStep = (step: number) =>
@ -111,7 +108,7 @@ export const setCurrentStep = (step: number) =>
{ {
type: "setCurrentStep", type: "setCurrentStep",
step, step,
}, }
); );
export const setQuizType = (quizId: string, quizType: QuizConfig["type"]) => { export const setQuizType = (quizId: string, quizType: QuizConfig["type"]) => {
@ -120,10 +117,7 @@ export const setQuizType = (quizId: string, quizType: QuizConfig["type"]) => {
}); });
}; };
export const setQuizStartpageType = ( export const setQuizStartpageType = (quizId: string, startpageType: QuizConfig["startpageType"]) => {
quizId: string,
startpageType: QuizConfig["startpageType"],
) => {
updateQuiz(quizId, (quiz) => { updateQuiz(quizId, (quiz) => {
quiz.config.startpageType = startpageType; quiz.config.startpageType = startpageType;
}); });
@ -133,10 +127,7 @@ const REQUEST_DEBOUNCE = 200;
const requestQueue = new RequestQueue(); const requestQueue = new RequestQueue();
let requestTimeoutId: ReturnType<typeof setTimeout>; let requestTimeoutId: ReturnType<typeof setTimeout>;
export const updateQuiz = ( export const updateQuiz = (quizId: string | null | undefined, updateFn: (quiz: Quiz) => void) => {
quizId: string | null | undefined,
updateFn: (quiz: Quiz) => void,
) => {
if (!quizId) return; if (!quizId) return;
setProducedState( setProducedState(
@ -150,7 +141,7 @@ export const updateQuiz = (
type: "updateQuiz", type: "updateQuiz",
quizId, quizId,
updateFn: updateFn.toString(), updateFn: updateFn.toString(),
}, }
); );
clearTimeout(requestTimeoutId); clearTimeout(requestTimeoutId);
@ -159,9 +150,7 @@ export const updateQuiz = (
const quiz = useQuizStore.getState().quizes.find((q) => q.id === quizId); const quiz = useQuizStore.getState().quizes.find((q) => q.id === quizId);
if (!quiz) return; if (!quiz) return;
const [editedQuiz, editedQuizError] = await quizApi.edit( const [editedQuiz, editedQuizError] = await quizApi.edit(quizToEditQuizRequest(quiz));
quizToEditQuizRequest(quiz),
);
if (editedQuizError || !editedQuiz) { if (editedQuizError || !editedQuiz) {
devlog("Error editing quiz", editedQuizError, quizId); devlog("Error editing quiz", editedQuizError, quizId);
@ -270,22 +259,15 @@ export const copyQuiz = async (quizId: string) =>
(state) => { (state) => {
state.quizes.unshift(newQuiz); state.quizes.unshift(newQuiz);
}, },
{ type: "addQuiz", quiz }, { type: "addQuiz", quiz }
); );
}); });
export const uploadQuizImage = async ( export const uploadQuizImage = async (quizId: string, blob: Blob, updateFn: (quiz: Quiz, imageId: string) => void) => {
quizId: string,
blob: Blob,
updateFn: (quiz: Quiz, imageId: string) => void,
) => {
const quiz = useQuizStore.getState().quizes.find((q) => q.id === quizId); const quiz = useQuizStore.getState().quizes.find((q) => q.id === quizId);
if (!quiz) return; if (!quiz) return;
const [addedImages, addImagesError] = await quizApi.addImages( const [addedImages, addImagesError] = await quizApi.addImages(quiz.backendId, blob);
quiz.backendId,
blob,
);
if (addImagesError || !addedImages) { if (addImagesError || !addedImages) {
devlog("Error uploading quiz image", addImagesError); devlog("Error uploading quiz image", addImagesError);
@ -305,14 +287,11 @@ export const uploadQuizImage = async (
updateQuiz(quizId, (quiz) => { updateQuiz(quizId, (quiz) => {
updateFn( updateFn(
quiz, quiz,
`https://s3.timeweb.cloud/3c580be9-cf31f296-d055-49cf-b39e-30c7959dc17b/squizimages/${quiz.qid}/${imageId}`, `https://s3.timeweb.cloud/3c580be9-cf31f296-d055-49cf-b39e-30c7959dc17b/squizimages/${quiz.qid}/${imageId}`
); );
}); });
}; };
function setProducedState<A extends string | { type: unknown }>( function setProducedState<A extends string | { type: string }>(recipe: (state: QuizStore) => void, action?: A) {
recipe: (state: QuizStore) => void,
action?: A,
) {
useQuizStore.setState((state) => produce(state, recipe), false, action); useQuizStore.setState((state) => produce(state, recipe), false, action);
} }

@ -6,19 +6,13 @@ import type { SxProps, Theme } from "@mui/material";
interface Props { interface Props {
sx?: SxProps<Theme>; sx?: SxProps<Theme>;
imageSrc?: string; imageSrc?: string | null;
onImageClick?: () => void; onImageClick?: () => void;
onPlusClick?: () => void; onPlusClick?: () => void;
uploading: boolean; uploading: boolean;
} }
export default function AddOrEditImageButton({ export default function AddOrEditImageButton({ onImageClick, onPlusClick, sx, imageSrc, uploading = false }: Props) {
onImageClick,
onPlusClick,
sx,
imageSrc,
uploading = false,
}: Props) {
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(790)); const isMobile = useMediaQuery(theme.breakpoints.down(790));

@ -1,64 +1,44 @@
import { FC, useState } from "react"; import { QuizQuestionPage } from "@/model/questionTypes/page";
import { import { QuizQuestionResult } from "@/model/questionTypes/result";
Box,
Button,
ButtonBase,
Skeleton,
Tooltip,
Typography,
useTheme,
} from "@mui/material";
import { updateQuestion, uploadQuestionImage } from "@root/questions/actions";
import { CropModal, useCropModalState } from "@ui_kit/Modal/CropModal";
import AddOrEditImageButton from "@ui_kit/AddOrEditImageButton";
import { UploadImageModal } from "../pages/Questions/UploadImage/UploadImageModal";
import { useDisclosure } from "../utils/useDisclosure";
import { useCurrentQuiz } from "../stores/quizes/hooks";
import { AnyTypedQuizQuestion } from "@model/questionTypes/shared";
import UploadBox from "@ui_kit/UploadBox";
import UploadIcon from "@icons/UploadIcon";
import InfoIcon from "@icons/InfoIcon"; import InfoIcon from "@icons/InfoIcon";
import UploadIcon from "@icons/UploadIcon";
import { Box, Button, ButtonBase, Skeleton, Tooltip, Typography, useTheme } from "@mui/material";
import { updateQuestion, uploadQuestionImage } from "@root/questions/actions";
import AddOrEditImageButton from "@ui_kit/AddOrEditImageButton";
import { CropModal, useCropModalState } from "@ui_kit/Modal/CropModal";
import UploadBox from "@ui_kit/UploadBox";
import { FC, useState } from "react";
import { UploadImageModal } from "../pages/Questions/UploadImage/UploadImageModal";
import { VideoElement } from "../pages/startPage/VideoElement"; import { VideoElement } from "../pages/startPage/VideoElement";
import { useCurrentQuiz } from "../stores/quizes/hooks";
import { useDisclosure } from "../utils/useDisclosure";
import UploadVideoModal from "@/pages/Questions/UploadVideoModal";
interface Iprops { interface Props {
resultData: AnyTypedQuizQuestion; question: QuizQuestionPage | QuizQuestionResult;
cropAspectRatio: { cropAspectRatio: {
width: number; width: number;
height: number; height: number;
}; };
} }
export const MediaSelectionAndDisplay: FC<Iprops> = ({ export const MediaSelectionAndDisplay: FC<Props> = ({ question, cropAspectRatio }) => {
resultData,
cropAspectRatio,
}) => {
const [pictureUploding, setPictureUploading] = useState<boolean>(false); const [pictureUploding, setPictureUploading] = useState<boolean>(false);
const [backgroundUploding, setBackgroundUploading] = useState<boolean>(false); const [backgroundUploding, setBackgroundUploading] = useState<boolean>(false);
const quizQid = useCurrentQuiz()?.qid; const quizQid = useCurrentQuiz()?.qid;
const theme = useTheme(); const theme = useTheme();
const { const { isCropModalOpen, openCropModal, closeCropModal, imageBlob, originalImageUrl, setCropModalImageBlob } =
isCropModalOpen, useCropModalState();
openCropModal, const [isImageUploadOpen, openImageUploadModal, closeImageUploadModal] = useDisclosure();
closeCropModal, const [isVideoUploadDialogOpen, setIsVideoUploadDialogOpen] = useState<boolean>(false);
imageBlob,
originalImageUrl,
setCropModalImageBlob,
} = useCropModalState();
const [isImageUploadOpen, openImageUploadModal, closeImageUploadModal] =
useDisclosure();
async function handleImageUpload(file: File) { async function handleImageUpload(file: File) {
setPictureUploading(true); setPictureUploading(true);
const url = await uploadQuestionImage( const url = await uploadQuestionImage(question.id, quizQid, file, (question, url) => {
resultData.id, question.content.back = url;
quizQid, question.content.originalBack = url;
file, });
(question, url) => {
question.content.back = url;
question.content.originalBack = url;
},
);
closeImageUploadModal(); closeImageUploadModal();
openCropModal(file, url); openCropModal(file, url);
@ -66,11 +46,32 @@ export const MediaSelectionAndDisplay: FC<Iprops> = ({
} }
function handleCropModalSaveClick(imageBlob: Blob) { function handleCropModalSaveClick(imageBlob: Blob) {
uploadQuestionImage(resultData.id, quizQid, imageBlob, (question, url) => { uploadQuestionImage(question.id, quizQid, imageBlob, (question, url) => {
question.content.back = url; question.content.back = url;
}); });
} }
async function handleVideoUpload(videoUrl: string) {
setBackgroundUploading(true);
if (videoUrl.startsWith("blob:")) {
const videoBlob = await (await fetch(videoUrl)).blob();
uploadQuestionImage(question.id, quizQid, videoBlob, (question, url) => {
if (!("video" in question.content)) return;
question.content.video = url;
});
} else {
updateQuestion(question.id, (question) => {
if (!("video" in question.content)) return;
question.content.video = videoUrl;
});
}
setBackgroundUploading(false);
}
return ( return (
<Box <Box
sx={{ sx={{
@ -87,7 +88,7 @@ export const MediaSelectionAndDisplay: FC<Iprops> = ({
> >
<Button <Button
sx={{ sx={{
color: resultData.content.useImage ? "#7E2AEA" : "#9A9AAF", color: question.content.useImage ? "#7E2AEA" : "#9A9AAF",
fontSize: "16px", fontSize: "16px",
"&:hover": { "&:hover": {
background: "none", background: "none",
@ -95,17 +96,18 @@ export const MediaSelectionAndDisplay: FC<Iprops> = ({
}} }}
variant="text" variant="text"
onClick={() => onClick={() =>
updateQuestion( updateQuestion(question.id, (question) => {
resultData.id, if (!("useImage" in question.content)) return;
(question) => (question.content.useImage = true),
) question.content.useImage = true;
})
} }
> >
Изображение Изображение
</Button> </Button>
<Button <Button
sx={{ sx={{
color: resultData.content.useImage ? "#9A9AAF" : "#7E2AEA", color: question.content.useImage ? "#9A9AAF" : "#7E2AEA",
fontSize: "16px", fontSize: "16px",
"&:hover": { "&:hover": {
background: "none", background: "none",
@ -113,45 +115,43 @@ export const MediaSelectionAndDisplay: FC<Iprops> = ({
}} }}
variant="text" variant="text"
onClick={() => onClick={() =>
updateQuestion( updateQuestion(question.id, (question) => {
resultData.id, if (!("useImage" in question.content)) return;
(question) => (question.content.useImage = false),
) question.content.useImage = false;
})
} }
> >
Видео Видео
</Button> </Button>
</Box> </Box>
<UploadImageModal
<Box isOpen={isImageUploadOpen}
sx={{ onClose={closeImageUploadModal}
display: "flex", handleImageChange={handleImageUpload}
flexDirection: "column", />
<CropModal
isOpen={isCropModalOpen}
imageBlob={imageBlob}
originalImageUrl={originalImageUrl}
setCropModalImageBlob={setCropModalImageBlob}
onClose={closeCropModal}
onSaveImageClick={handleCropModalSaveClick}
onDeleteClick={() => {
updateQuestion(question.id, (question) => {
question.content.back = null;
question.content.originalBack = null;
});
}} }}
> cropAspectRatio={cropAspectRatio}
<UploadImageModal />
isOpen={isImageUploadOpen} <UploadVideoModal
onClose={closeImageUploadModal} open={isVideoUploadDialogOpen}
handleImageChange={handleImageUpload} onClose={() => setIsVideoUploadDialogOpen(false)}
/> onUpload={handleVideoUpload}
<CropModal video={question.content.video}
isOpen={isCropModalOpen} />
imageBlob={imageBlob} {question.content.useImage && (
originalImageUrl={originalImageUrl}
setCropModalImageBlob={setCropModalImageBlob}
onClose={closeCropModal}
onSaveImageClick={handleCropModalSaveClick}
onDeleteClick={() => {
updateQuestion(resultData.id, (question) => {
question.content.back = null;
question.content.originalBack = null;
});
}}
cropAspectRatio={cropAspectRatio}
/>
</Box>
{resultData.content.useImage && (
<Box <Box
sx={{ sx={{
cursor: "pointer", cursor: "pointer",
@ -162,14 +162,11 @@ export const MediaSelectionAndDisplay: FC<Iprops> = ({
}} }}
> >
<AddOrEditImageButton <AddOrEditImageButton
imageSrc={resultData.content.back} imageSrc={question.content.back}
uploading={pictureUploding} uploading={pictureUploding}
onImageClick={() => { onImageClick={() => {
if (resultData.content.back) { if (question.content.back) {
return openCropModal( return openCropModal(question.content.back, question.content.originalBack);
resultData.content.back,
resultData.content.originalBack,
);
} }
openImageUploadModal(); openImageUploadModal();
@ -180,9 +177,9 @@ export const MediaSelectionAndDisplay: FC<Iprops> = ({
/> />
</Box> </Box>
)} )}
{!resultData.content.useImage && ( {!question.content.useImage && (
<> <>
{!resultData.content.video ? ( {!question.content.video ? (
<> <>
<Box <Box
sx={{ sx={{
@ -193,12 +190,11 @@ export const MediaSelectionAndDisplay: FC<Iprops> = ({
mb: "14px", mb: "14px",
}} }}
> >
<Typography <Typography sx={{ fontWeight: 500, color: theme.palette.grey3.main }}>Добавить видео</Typography>
sx={{ fontWeight: 500, color: theme.palette.grey3.main }} <Tooltip
title="Можно загрузить видео."
placement="top"
> >
Добавить видео
</Typography>
<Tooltip title="Можно загрузить видео." placement="top">
<Box> <Box>
<InfoIcon /> <InfoIcon />
</Box> </Box>
@ -216,7 +212,7 @@ export const MediaSelectionAndDisplay: FC<Iprops> = ({
) : ( ) : (
<> <>
<ButtonBase <ButtonBase
component="label" onClick={() => setIsVideoUploadDialogOpen(true)}
sx={{ sx={{
justifyContent: "center", justifyContent: "center",
height: "48px", height: "48px",
@ -226,27 +222,6 @@ export const MediaSelectionAndDisplay: FC<Iprops> = ({
my: "20px", my: "20px",
}} }}
> >
<input
onChange={async (event) => {
setBackgroundUploading(true);
const file = event.target.files?.[0];
if (file) {
await uploadQuestionImage(
resultData.id,
quizQid,
file,
(question, url) => {
question.content.video = url;
},
);
}
setBackgroundUploading(false);
}}
hidden
accept=".mp4"
multiple
type="file"
/>
<UploadBox <UploadBox
icon={<UploadIcon />} icon={<UploadIcon />}
sx={{ sx={{
@ -260,10 +235,12 @@ export const MediaSelectionAndDisplay: FC<Iprops> = ({
</> </>
) : ( ) : (
<VideoElement <VideoElement
videoSrc={resultData.content.video} videoSrc={question.content.video}
theme={theme} theme={theme}
onDeleteClick={() => { onDeleteClick={() => {
updateQuestion(resultData.id, (question) => { updateQuestion(question.id, (question) => {
if (!("video" in question.content)) return;
question.content.video = null; question.content.video = null;
}); });
}} }}

@ -1521,10 +1521,10 @@
immer "^10.0.2" immer "^10.0.2"
reconnecting-eventsource "^1.6.2" reconnecting-eventsource "^1.6.2"
"@frontend/squzanswerer@^1.0.44": "@frontend/squzanswerer@^1.0.45":
version "1.0.44" version "1.0.45"
resolved "https://penahub.gitlab.yandexcloud.net/api/v4/projects/43/packages/npm/@frontend/squzanswerer/-/@frontend/squzanswerer-1.0.44.tgz#12c19b23a1e1eff4d0cbfeffbc9ed1160c49cde2" resolved "https://penahub.gitlab.yandexcloud.net/api/v4/projects/43/packages/npm/@frontend/squzanswerer/-/@frontend/squzanswerer-1.0.45.tgz#1124aaa099034b0b75eda7b5c91f457db47872ab"
integrity sha1-EsGbI6Hh7/TQy/7/vJ7RFgxJzeI= integrity sha1-ESSqoJkDSwt17ae1yR9FfbR4cqs=
dependencies: dependencies:
bowser "1.9.4" bowser "1.9.4"
country-flag-emoji-polyfill "^0.1.8" country-flag-emoji-polyfill "^0.1.8"
@ -10557,16 +10557,7 @@ string-natural-compare@^3.0.1:
resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4" resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4"
integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw== integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==
"string-width-cjs@npm:string-width@^4.2.0": "string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^4.1.0, string-width@^4.2.0:
version "4.2.3" version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@ -10662,14 +10653,7 @@ stringify-object@^3.3.0:
is-obj "^1.0.1" is-obj "^1.0.1"
is-regexp "^1.0.0" is-regexp "^1.0.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1": "strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1" version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@ -11856,7 +11840,7 @@ workbox-window@6.6.1:
"@types/trusted-types" "^2.0.2" "@types/trusted-types" "^2.0.2"
workbox-core "6.6.1" workbox-core "6.6.1"
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
version "7.0.0" version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@ -11874,15 +11858,6 @@ wrap-ansi@^6.2.0:
string-width "^4.1.0" string-width "^4.1.0"
strip-ansi "^6.0.0" strip-ansi "^6.0.0"
wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^8.1.0: wrap-ansi@^8.1.0:
version "8.1.0" version "8.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"