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/styled": "^11.10.5",
"@frontend/kitui": "^1.0.82",
"@frontend/squzanswerer": "^1.0.44",
"@frontend/squzanswerer": "^1.0.45",
"@mui/icons-material": "^5.10.14",
"@mui/material": "^5.10.14",
"@mui/x-charts": "^6.19.5",

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

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

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

@ -1,16 +1,8 @@
import {
Box,
Button,
ButtonBase,
Modal,
Typography,
useTheme,
} from "@mui/material";
import SelectableButton from "@ui_kit/SelectableButton";
import { Box, Button, ButtonBase, Dialog, Typography, useTheme } from "@mui/material";
import CustomTextField from "@ui_kit/CustomTextField";
import SelectableButton from "@ui_kit/SelectableButton";
import { useState } from "react";
import UploadIcon from "../../assets/icons/UploadIcon";
import type { DragEvent } from "react";
type BackgroundTypeModal = "linkVideo" | "ownVideo";
@ -18,18 +10,12 @@ type BackgroundTypeModal = "linkVideo" | "ownVideo";
type HelpQuestionsProps = {
open: boolean;
onClose: () => void;
video: string;
video: string | null;
onUpload: (number: string) => void;
};
export const UploadVideoModal = ({
open,
onClose,
video,
onUpload,
}: HelpQuestionsProps) => {
const [backgroundTypeModal, setBackgroundTypeModal] =
useState<BackgroundTypeModal>("linkVideo");
export default function UploadVideoModal({ open, onClose, video, onUpload }: HelpQuestionsProps) {
const [backgroundTypeModal, setBackgroundTypeModal] = useState<BackgroundTypeModal>("linkVideo");
const theme = useTheme();
const handleDrop = (event: DragEvent<HTMLDivElement>) => {
@ -42,118 +28,102 @@ export const UploadVideoModal = ({
};
return (
<Modal
<Dialog
open={open}
onClose={onClose}
aria-labelledby="modal-modal-title"
aria-describedby="modal-modal-description"
>
<Box
sx={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
maxWidth: "690px",
bgcolor: "background.paper",
PaperProps={{
sx: {
maxWidth: "640px",
borderRadius: "12px",
boxShadow: 24,
p: 0,
overflow: "hidden",
},
}}
>
<Box
sx={{
display: "flex",
padding: "20px",
background: theme.palette.background.default,
}}
>
<Box
sx={{
display: "flex",
padding: "20px",
background: theme.palette.background.default,
}}
<Typography sx={{ color: "#9A9AAF" }}>
Видео можно вставить с любого хостинга: YouTube, Vimeo или загрузить собственное
</Typography>
<Button
onClick={onClose}
variant="contained"
>
<Typography sx={{ color: "#9A9AAF" }}>
Видео можно вставить с любого хостинга: YouTube, Vimeo или загрузить
собственное
</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>
)}
Готово
</Button>
</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 { memo, useState } from "react";
import UploadIcon from "../../assets/icons/UploadIcon";
import { UploadVideoModal } from "./UploadVideoModal";
import UploadVideoModal from "./UploadVideoModal";
type BackgroundType = "text" | "video";
@ -15,11 +15,7 @@ type HelpQuestionsProps = {
hintText: string;
};
const HelpQuestions = memo<HelpQuestionsProps>(function ({
questionId,
hintVideo,
hintText,
}) {
const HelpQuestions = memo<HelpQuestionsProps>(function ({ questionId, hintVideo, hintText }) {
const [open, setOpen] = useState(false);
const [backgroundType, setBackgroundType] = useState<BackgroundType>("text");
@ -71,15 +67,17 @@ const HelpQuestions = memo<HelpQuestionsProps>(function ({
</>
) : (
<Box>
<Typography sx={{ paddingBottom: "15px", fontWeight: "500" }}>
Загрузите видео
</Typography>
<Typography sx={{ paddingBottom: "15px", fontWeight: "500" }}>Загрузите видео</Typography>
<ButtonBase
onClick={() => setOpen(true)}
sx={{ justifyContent: "flex-start" }}
>
{hintVideo ? (
<video src={hintVideo} width="400" controls />
<video
src={hintVideo}
width="400"
controls
/>
) : (
<>
<UploadBox

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

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

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

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

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

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

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

@ -1521,10 +1521,10 @@
immer "^10.0.2"
reconnecting-eventsource "^1.6.2"
"@frontend/squzanswerer@^1.0.44":
version "1.0.44"
resolved "https://penahub.gitlab.yandexcloud.net/api/v4/projects/43/packages/npm/@frontend/squzanswerer/-/@frontend/squzanswerer-1.0.44.tgz#12c19b23a1e1eff4d0cbfeffbc9ed1160c49cde2"
integrity sha1-EsGbI6Hh7/TQy/7/vJ7RFgxJzeI=
"@frontend/squzanswerer@^1.0.45":
version "1.0.45"
resolved "https://penahub.gitlab.yandexcloud.net/api/v4/projects/43/packages/npm/@frontend/squzanswerer/-/@frontend/squzanswerer-1.0.45.tgz#1124aaa099034b0b75eda7b5c91f457db47872ab"
integrity sha1-ESSqoJkDSwt17ae1yR9FfbR4cqs=
dependencies:
bowser "1.9.4"
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"
integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==
"string-width-cjs@npm: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:
"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==
@ -10662,14 +10653,7 @@ stringify-object@^3.3.0:
is-obj "^1.0.1"
is-regexp "^1.0.0"
"strip-ansi-cjs@npm: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:
"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==
@ -11856,7 +11840,7 @@ workbox-window@6.6.1:
"@types/trusted-types" "^2.0.2"
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"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@ -11874,15 +11858,6 @@ wrap-ansi@^6.2.0:
string-width "^4.1.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:
version "8.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"