From ef389f97b1df4908af1b05468b15be6fa73ab3be Mon Sep 17 00:00:00 2001 From: nflnkr Date: Fri, 8 Dec 2023 14:57:11 +0300 Subject: [PATCH 1/5] remove edit modal from favicon dropzone --- src/model/quizSettings.ts | 2 - src/pages/startPage/FaviconDropZone.tsx | 125 ++++++++++++++++++++++ src/pages/startPage/StartPageSettings.tsx | 53 +-------- 3 files changed, 129 insertions(+), 51 deletions(-) create mode 100644 src/pages/startPage/FaviconDropZone.tsx diff --git a/src/model/quizSettings.ts b/src/model/quizSettings.ts index c2a84ffa..510438de 100644 --- a/src/model/quizSettings.ts +++ b/src/model/quizSettings.ts @@ -38,7 +38,6 @@ export interface QuizConfig { button: string; position: QuizStartpageAlignType; favIcon: string | null; - originalFavIcon: string | null; logo: string | null; originalLogo: string | null; background: { @@ -72,7 +71,6 @@ export const defaultQuizConfig: QuizConfig = { button: "", position: "left", favIcon: null, - originalFavIcon: null, logo: null, originalLogo: null, background: { diff --git a/src/pages/startPage/FaviconDropZone.tsx b/src/pages/startPage/FaviconDropZone.tsx new file mode 100644 index 00000000..c37ae46f --- /dev/null +++ b/src/pages/startPage/FaviconDropZone.tsx @@ -0,0 +1,125 @@ +import UploadIcon from "@icons/UploadIcon"; +import { Box, ButtonBase, Typography, useTheme } from "@mui/material"; +import { useCurrentQuiz } from "@root/quizes/hooks"; +import { enqueueSnackbar } from "notistack"; +import { useState } from "react"; +import { UploadImageModal } from "../../pages/Questions/UploadImage/UploadImageModal"; +import { useDisclosure } from "../../utils/useDisclosure"; + + +const allowedFileTypes = ["image/png", "image/jpeg", "image/gif"]; + +interface Props { + imageUrl: string | null; + onImageUploadClick: (image: Blob) => void; + onDeleteClick: () => void; +} + +export default function FaviconDropZone({ imageUrl, onImageUploadClick, onDeleteClick }: Props) { + const theme = useTheme(); + const quiz = useCurrentQuiz(); + const [isDropReady, setIsDropReady] = useState(false); + const [isImageUploadOpen, openImageUploadModal, closeImageUploadModal] = useDisclosure(); + + if (!quiz) return null; // TODO throw and catch with error boundary + + async function handleImageUpload(file: File) { + if (file.size > 5 * 2 ** 20) return enqueueSnackbar("Размер картинки слишком велик"); + if (!allowedFileTypes.includes(file.type)) return enqueueSnackbar("Допустимые форматы изображений: png, jpeg, gif"); + + onImageUploadClick(file); + closeImageUploadModal(); + } + + const onDrop = (event: React.DragEvent) => { + event.preventDefault(); + setIsDropReady(false); + + const file = event.dataTransfer.files[0]; + if (!file || imageUrl) return; + + handleImageUpload(file); + }; + + return ( + + + !imageUrl && setIsDropReady(true)} + onDragExit={() => setIsDropReady(false)} + onDragOver={e => e.preventDefault()} + onDrop={onDrop} + sx={{ + width: "48px", + height: "48px", + backgroundColor: theme.palette.background.default, + border: `1px solid ${isDropReady ? "red" : theme.palette.grey2.main}`, + borderRadius: "8px", + }}> + + {imageUrl ? + + : + + } + + + + {imageUrl && + + + Удалить + + + } + + 5 MB максимум + + + + ); +}; diff --git a/src/pages/startPage/StartPageSettings.tsx b/src/pages/startPage/StartPageSettings.tsx index e51ae8a0..e77be9b4 100755 --- a/src/pages/startPage/StartPageSettings.tsx +++ b/src/pages/startPage/StartPageSettings.tsx @@ -37,6 +37,7 @@ import SelectableIconButton from "./SelectableIconButton"; import { DropZone } from "./dropZone"; import Extra from "./extra"; import { resizeFavIcon } from "@ui_kit/reactImageFileResizer"; +import FaviconDropZone from "./FaviconDropZone"; const designTypes = [ @@ -86,19 +87,9 @@ export default function StartPageSettings() { if (!quiz) return null; // TODO throw and catch with error boundary const favIconDropZoneElement = ( - { - const resizedImage = await resizeFavIcon(file); - uploadQuizImage(quiz.id, resizedImage, (quiz, url) => { - quiz.config.startpage.favIcon = url; - quiz.config.startpage.originalFavIcon = url; - }); - }} - onImageSaveClick={async file => { const resizedImage = await resizeFavIcon(file); uploadQuizImage(quiz.id, resizedImage, (quiz, url) => { quiz.config.startpage.favIcon = url; @@ -613,25 +604,7 @@ export default function StartPageSettings() { > Favicon - - {favIconDropZoneElement} - - 5 MB максимум - - + {favIconDropZoneElement} )} @@ -695,25 +668,7 @@ export default function StartPageSettings() { > Favicon - - {favIconDropZoneElement} - - 5 MB максимум - - + {favIconDropZoneElement} )} {(!isSmallMonitor || (isSmallMonitor && formState === "content")) && ( From a5e76a20eaff6057c47fdc4960486c1761c6135a Mon Sep 17 00:00:00 2001 From: nflnkr Date: Fri, 8 Dec 2023 15:22:52 +0300 Subject: [PATCH 2/5] use favicon on quiz view page --- src/pages/ViewPublicationPage/index.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/pages/ViewPublicationPage/index.tsx b/src/pages/ViewPublicationPage/index.tsx index d14ca886..e1212e42 100644 --- a/src/pages/ViewPublicationPage/index.tsx +++ b/src/pages/ViewPublicationPage/index.tsx @@ -1,4 +1,4 @@ -import { useLayoutEffect, useState } from "react"; +import { useEffect, useState } from "react"; import { Box } from "@mui/material"; import { StartPageViewPublication } from "./StartPageViewPublication"; @@ -13,6 +13,13 @@ export const ViewPage = () => { const { questions } = useQuestions(); const [visualStartPage, setVisualStartPage] = useState(!quiz?.config.noStartPage); + useEffect(() => { + const link = document.querySelector('link[rel="icon"]'); + + if (link && quiz?.config.startpage.favIcon) { + link.setAttribute("href", quiz.config.startpage.favIcon); + } + }, [quiz?.config.startpage.favIcon]); const filteredQuestions = questions.filter( ({ type }) => type From 6fb6b7dae3f810d789f51c3c0e8be176e6e49aa7 Mon Sep 17 00:00:00 2001 From: nflnkr Date: Fri, 8 Dec 2023 16:36:00 +0300 Subject: [PATCH 3/5] save order of questions --- .../DraggableList/QuestionPageCard.tsx | 48 +++++----- src/stores/questions/actions.ts | 96 ++++++++++++------- 2 files changed, 86 insertions(+), 58 deletions(-) diff --git a/src/pages/Questions/DraggableList/QuestionPageCard.tsx b/src/pages/Questions/DraggableList/QuestionPageCard.tsx index 1a64c47b..4d167b4f 100644 --- a/src/pages/Questions/DraggableList/QuestionPageCard.tsx +++ b/src/pages/Questions/DraggableList/QuestionPageCard.tsx @@ -39,7 +39,7 @@ import SwitchQuestionsPage from "../SwitchQuestionsPage"; import { ChooseAnswerModal } from "./ChooseAnswerModal"; import TypeQuestions from "../TypeQuestions"; import { QuestionType } from "@model/question/question"; -import { useCurrentQuiz } from "@root/quizes/hooks" +import { useCurrentQuiz } from "@root/quizes/hooks"; interface Props { question: AnyTypedQuizQuestion | UntypedQuizQuestion; @@ -98,7 +98,7 @@ export default function QuestionsPageCard({ question, draggableProps, isDragging setTitle(target.value)} + onChange={({ target }: { target: HTMLInputElement; }) => setTitle(target.value)} InputProps={{ startAdornment: ( @@ -238,7 +238,7 @@ export default function QuestionsPageCard({ question, draggableProps, isDragging margin: "0 5px 0 10px", }} onClick={() => { // TODO - const removedId = question.id; + const removedId = question.id; // if (question.deleteTimeoutId) { // clearTimeout(question.deleteTimeoutId); // } @@ -264,26 +264,28 @@ export default function QuestionsPageCard({ question, draggableProps, isDragging )} - - {index + 1} - + {question.type !== null && + + {question.page + 1} + + } setProdu backendId, }); +const updateQuestionOrders = () => { + const questions = useQuestionsStore.getState().questions.filter( + (question): question is AnyTypedQuizQuestion => question.type !== null + ); + + questions.forEach((question, index) => { + updateQuestion(question.id, question => { + question.page = index; + }, true); + }); +}; + export const reorderQuestions = ( sourceIndex: number, destinationIndex: number, @@ -99,6 +110,8 @@ export const reorderQuestions = ( sourceIndex, destinationIndex, }); + + updateQuestionOrders(); }; export const toggleExpandQuestion = (questionId: string) => setProducedState(state => { @@ -123,6 +136,7 @@ let requestTimeoutId: ReturnType; export const updateQuestion = ( questionId: string, updateFn: (question: AnyTypedQuizQuestion) => void, + skipQueue = false, ) => { setProducedState(state => { const question = state.questions.find(q => q.id === questionId) || state.questions.find(q => q.type !== null && q.content.id === questionId); @@ -137,21 +151,31 @@ export const updateQuestion = ( }); // clearTimeout(requestTimeoutId); - // requestTimeoutId = setTimeout(() => { - requestQueue.enqueue(async () => { + + const request = async () => { const q = useQuestionsStore.getState().questions.find(q => q.id === questionId) || useQuestionsStore.getState().questions.find(q => q.type !== null && q.content.id === questionId); if (!q) return; if (q.type === null) throw new Error("Cannot send update request for untyped question"); - const response = await questionApi.edit(questionToEditQuestionRequest(q)); + try { + const response = await questionApi.edit(questionToEditQuestionRequest(q)); - setQuestionBackendId(questionId, response.updated); - }).catch(error => { - if (isAxiosCanceledError(error)) return; + setQuestionBackendId(questionId, response.updated); + } catch (error) { + if (isAxiosCanceledError(error)) return; - devlog("Error editing question", { error, questionId }); - enqueueSnackbar("Не удалось сохранить вопрос"); - }); + devlog("Error editing question", { error, questionId }); + enqueueSnackbar("Не удалось сохранить вопрос"); + } + }; + + if (skipQueue) { + request(); + return; + } + + // requestTimeoutId = setTimeout(() => { + requestQueue.enqueue(request); // }, REQUEST_DEBOUNCE); }; @@ -261,13 +285,13 @@ export const changeQuestionType = ( type: QuestionType, ) => { updateQuestion(questionId, question => { - const oldId = question.content.id - const oldRule = question.content.rule - oldRule.main = [] + const oldId = question.content.id; + const oldRule = question.content.rule; + oldRule.main = []; question.type = type; question.content = defaultQuestionByType[type].content; - question.content.id = oldId - question.content.rule = oldRule + question.content.id = oldId; + question.content.rule = oldRule; }); }; @@ -275,7 +299,8 @@ export const createTypedQuestion = async ( questionId: string, type: QuestionType, ) => requestQueue.enqueue(async () => { - const question = useQuestionsStore.getState().questions.find(q => q.id === questionId); + const questions = useQuestionsStore.getState().questions; + const question = questions.find(q => q.id === questionId); if (!question) return; if (question.type !== null) throw new Error("Cannot upgrade already typed question"); @@ -285,7 +310,7 @@ export const createTypedQuestion = async ( type, title: question.title, description: question.description, - page: 0, + page: questions.length, required: true, content: JSON.stringify(defaultQuestionByType[type].content), }); @@ -320,8 +345,8 @@ export const deleteQuestion = async (questionId: string, quizId: string) => requ try { await questionApi.delete(question.backendId); if (question.content.rule.parentId === "root") { //удалить из стора root и очистить rule всем вопросам - updateRootContentId(quizId, "") - clearRoleForAll() + updateRootContentId(quizId, ""); + clearRoleForAll(); } removeQuestion(questionId); @@ -338,8 +363,7 @@ export const copyQuestion = async (questionId: string, quizId: number) => reques const frontId = nanoid(); if (question.type === null) { const copiedQuestion = structuredClone(question); - copiedQuestion.id = frontId - copiedQuestion.content.id = frontId + copiedQuestion.id = frontId; setProducedState(state => { state.questions.push(copiedQuestion); @@ -357,9 +381,9 @@ export const copyQuestion = async (questionId: string, quizId: number) => reques const copiedQuestion = structuredClone(question); copiedQuestion.backendId = newQuestionId; - copiedQuestion.id = frontId - copiedQuestion.content.id = frontId - copiedQuestion.content.rule = { main: [], parentId: "", default: "" } + copiedQuestion.id = frontId; + copiedQuestion.content.id = frontId; + copiedQuestion.content.rule = { main: [], parentId: "", default: "" }; setProducedState(state => { state.questions.push(copiedQuestion); @@ -368,6 +392,8 @@ export const copyQuestion = async (questionId: string, quizId: number) => reques questionId, quizId, }); + + updateQuestionOrders(); } catch (error) { devlog("Error copying question", error); enqueueSnackbar("Не удалось скопировать вопрос"); @@ -394,7 +420,7 @@ export const getQuestionByContentId = (questionContentId: string | null) => { if (questionContentId === null) return null; return useQuestionsStore.getState().questions.find(q => { if (q.type === null) return false; - + return (q.content.id === questionContentId); }) || null; }; @@ -402,19 +428,19 @@ export const getQuestionByContentId = (questionContentId: string | null) => { export const updateOpenedModalSettingsId = (id?: string) => useQuestionsStore.setState({ openedModalSettingsId: id ? id : null }); export const updateDragQuestionContentId = (contentId?: string) => { useQuestionsStore.setState({ dragQuestionContentId: contentId ? contentId : null }); -} +}; export const clearRoleForAll = () => { - const { questions } = useQuestionsStore.getState() + const { questions } = useQuestionsStore.getState(); questions.forEach(question => { if (question.type !== null && (question.content.rule.main.length > 0 || question.content.rule.default.length > 0 || question.content.rule.parentId.length > 0)) { updateQuestion(question.content.id, question => { - question.content.rule.parentId = "" - question.content.rule.main = [] - question.content.rule.default = "" - }) + question.content.rule.parentId = ""; + question.content.rule.main = []; + question.content.rule.default = ""; + }); } }); -} +}; -export const updateOpenBranchingPanel = (value: boolean) => useQuestionsStore.setState({openBranchingPanel: !value}); +export const updateOpenBranchingPanel = (value: boolean) => useQuestionsStore.setState({ openBranchingPanel: !value }); From 1bbfe804d3cc842b32ae4e664b7bebbcdd6a1b4c Mon Sep 17 00:00:00 2001 From: nflnkr Date: Fri, 8 Dec 2023 19:23:06 +0300 Subject: [PATCH 4/5] add quiz startpage video iframe --- src/pages/startPage/StartPageSettings.tsx | 80 ++++--------------- src/stores/quizes/actions.ts | 2 +- .../StartPagePreview/QuizPreviewLayout.tsx | 33 +++----- .../StartPagePreview/YoutubeEmbedIframe.tsx | 34 ++++++++ 4 files changed, 64 insertions(+), 85 deletions(-) create mode 100644 src/ui_kit/StartPagePreview/YoutubeEmbedIframe.tsx diff --git a/src/pages/startPage/StartPageSettings.tsx b/src/pages/startPage/StartPageSettings.tsx index e77be9b4..c1b0d330 100755 --- a/src/pages/startPage/StartPageSettings.tsx +++ b/src/pages/startPage/StartPageSettings.tsx @@ -2,7 +2,6 @@ import AlignCenterIcon from "@icons/AlignCenterIcon"; import AlignLeftIcon from "@icons/AlignLeftIcon"; import AlignRightIcon from "@icons/AlignRightIcon"; import ArrowDown from "@icons/ArrowDownIcon"; -import InfoIcon from "@icons/InfoIcon"; import LayoutCenteredIcon from "@icons/LayoutCenteredIcon"; import LayoutExpandedIcon from "@icons/LayoutExpandedIcon"; import LayoutStandartIcon from "@icons/LayoutStandartIcon"; @@ -11,16 +10,14 @@ import { QuizStartpageType } from "@model/quizSettings"; import { Box, Button, - ButtonBase, Checkbox, FormControl, FormControlLabel, MenuItem, Select, - Tooltip, Typography, useMediaQuery, - useTheme, + useTheme } from "@mui/material"; import { incrementCurrentStep, updateQuiz, uploadQuizImage } from "@root/quizes/actions"; import { useCurrentQuiz } from "@root/quizes/hooks"; @@ -28,16 +25,14 @@ import CustomCheckbox from "@ui_kit/CustomCheckbox"; import CustomTextField from "@ui_kit/CustomTextField"; import SelectableButton from "@ui_kit/SelectableButton"; import { StartPagePreview } from "@ui_kit/StartPagePreview"; -import UploadBox from "@ui_kit/UploadBox"; +import { resizeFavIcon } from "@ui_kit/reactImageFileResizer"; import { useState } from "react"; import { createPortal } from "react-dom"; -import UploadIcon from "../../assets/icons/UploadIcon"; +import FaviconDropZone from "./FaviconDropZone"; import ModalSizeImage from "./ModalSizeImage"; import SelectableIconButton from "./SelectableIconButton"; import { DropZone } from "./dropZone"; import Extra from "./extra"; -import { resizeFavIcon } from "@ui_kit/reactImageFileResizer"; -import FaviconDropZone from "./FaviconDropZone"; const designTypes = [ @@ -66,25 +61,15 @@ export default function StartPageSettings() { const isTablet = useMediaQuery(theme.breakpoints.down(950)); const quiz = useCurrentQuiz(); const [formState, setFormState] = useState<"design" | "content">("design"); - - const designType = quiz?.config?.startpageType; - - const videoHC = (videoInp: HTMLInputElement) => { - const file = videoInp.files?.[0]; - - if (file) { - setVideo(URL.createObjectURL(file)); - } - }; - - const [video, setVideo] = useState(""); - const [mobileVersion, setMobileVersion] = useState(false); + + if (!quiz) return null; // TODO throw and catch with error boundary + const MobileVersionHC = (bool: boolean) => { setMobileVersion(bool); }; - - if (!quiz) return null; // TODO throw and catch with error boundary + + const designType = quiz?.config?.startpageType; const favIconDropZoneElement = ( - - - Добавить видео - - - - - - - - - videoHC(event.target)} - hidden - accept=".mp4" - multiple - type="file" - /> - } - sx={{ - height: "48px", - width: "48px", - marginBottom: "20px", - }} - /> - - {video ?