Обработка ошибки, когда вопросы для аудитории ещё не созданы
Some checks failed
Deploy / DeployService (push) Failing after 28s
Deploy / CreateImage (push) Has been cancelled

This commit is contained in:
Nastya 2025-06-07 06:26:56 +03:00
parent c186a04fa5
commit 12a1aab506
8 changed files with 357 additions and 279 deletions

@ -93,7 +93,9 @@ export async function getData({ quizId }: { quizId: string }): Promise<{
if (paudParam) body.auditory = Number(paudParam); if (paudParam) body.auditory = Number(paudParam);
try { try {
const { data, headers } = await axios<GetQuizDataResponse>(domain + `/answer/v1.0.0/settings`, { const { data, headers } = await axios<GetQuizDataResponse>(
domain + `/answer/v1.0.0/settings${window.location.search}`,
{
method: "POST", method: "POST",
headers: { headers: {
"X-Sessionkey": SESSIONS, "X-Sessionkey": SESSIONS,
@ -104,7 +106,8 @@ export async function getData({ quizId }: { quizId: string }): Promise<{
Browser: userAgent, Browser: userAgent,
}, },
data: body, data: body,
}); }
);
const sessions = JSON.parse(localStorage.getItem("sessions") || "{}"); const sessions = JSON.parse(localStorage.getItem("sessions") || "{}");
//Тут ещё проверка на антифрод без парса конфига. Нам не интересно время если не нужно запрещать проходить чаще чем в сутки //Тут ещё проверка на антифрод без парса конфига. Нам не интересно время если не нужно запрещать проходить чаще чем в сутки
@ -133,7 +136,9 @@ export async function getDataSingle({ quizId, page }: { quizId: string; page?: n
try { try {
// Первый запрос: 1 вопрос + конфиг // Первый запрос: 1 вопрос + конфиг
if (isFirstRequest) { if (isFirstRequest) {
const { data, headers } = await axios<GetQuizDataResponse>(domain + `/answer/v1.0.0/settings`, { const { data, headers } = await axios<GetQuizDataResponse>(
domain + `/answer/v1.0.0/settings${window.location.search}`,
{
method: "POST", method: "POST",
headers: { headers: {
"X-Sessionkey": SESSIONS, "X-Sessionkey": SESSIONS,
@ -149,7 +154,8 @@ export async function getDataSingle({ quizId, page }: { quizId: string; page?: n
page: 0, page: 0,
need_config: true, need_config: true,
}, },
}); }
);
globalStatus = data.settings.status; globalStatus = data.settings.status;
isFirstRequest = false; isFirstRequest = false;
@ -165,7 +171,9 @@ export async function getDataSingle({ quizId, page }: { quizId: string; page?: n
// Если статус не AI - сразу делаем запрос за всеми вопросами // Если статус не AI - сразу делаем запрос за всеми вопросами
if (globalStatus !== "ai") { if (globalStatus !== "ai") {
const secondResponse = await axios<GetQuizDataResponse>(domain + `/answer/v1.0.0/settings`, { const secondResponse = await axios<GetQuizDataResponse>(
domain + `/answer/v1.0.0/settings${window.location.search}`,
{
method: "POST", method: "POST",
headers: { headers: {
"X-Sessionkey": SESSIONS, "X-Sessionkey": SESSIONS,
@ -181,7 +189,8 @@ export async function getDataSingle({ quizId, page }: { quizId: string; page?: n
page: 0, page: 0,
need_config: false, need_config: false,
}, },
}); }
);
return { return {
data: { ...data, items: secondResponse.data.items }, data: { ...data, items: secondResponse.data.items },
isRecentlyCompleted: false, isRecentlyCompleted: false,
@ -192,7 +201,7 @@ export async function getDataSingle({ quizId, page }: { quizId: string; page?: n
} }
// Последующие запросы // Последующие запросы
const response = await axios<GetQuizDataResponse>(domain + `/answer/v1.0.0/settings`, { const response = await axios<GetQuizDataResponse>(domain + `/answer/v1.0.0/settings${window.location.search}`, {
method: "POST", method: "POST",
headers: { headers: {
"X-Sessionkey": SESSIONS, "X-Sessionkey": SESSIONS,
@ -418,7 +427,7 @@ type Answer = {
}; };
export function sendFile({ questionId, body, qid }: SendFileParams) { export function sendFile({ questionId, body, qid }: SendFileParams) {
if (body.preview) return; if (body.preview) return Promise.resolve();
const formData = new FormData(); const formData = new FormData();
const file = new File([body.file], body.file.name.replace(/\s/g, "_")); const file = new File([body.file], body.file.name.replace(/\s/g, "_"));
@ -439,7 +448,10 @@ export function sendFile({ questionId, body, qid }: SendFileParams) {
url: domain + `/answer/v1.0.0/answer`, url: domain + `/answer/v1.0.0/answer`,
body: formData, body: formData,
method: "POST", method: "POST",
}); }).then((response) => ({
fileName: nameImage,
response,
}));
} }
//форма контактов //форма контактов

@ -22,12 +22,10 @@ import { useQuizStore } from "@/stores/useQuizStore";
export default function ViewPublicationPage() { export default function ViewPublicationPage() {
const { settings, recentlyCompleted, quizId, preview, changeFaviconAndTitle } = useQuizStore(); const { settings, recentlyCompleted, quizId, preview, changeFaviconAndTitle } = useQuizStore();
const answers = useQuizViewStore((state) => state.answers); const { currentQuestion, currentQuestionStepNumber, currentQuizStep, answers, ownVariants } = useQuizViewStore();
const ownVariants = useQuizViewStore((state) => state.ownVariants); const uploadingFiles = useQuizViewStore((state) => state.uploadingFiles);
let currentQuizStep = useQuizViewStore((state) => state.currentQuizStep); const isFileUploading = Object.values(uploadingFiles).some((isUploading) => isUploading);
const { const {
currentQuestion,
currentQuestionStepNumber,
nextQuestion, nextQuestion,
isNextButtonEnabled, isNextButtonEnabled,
isPreviousButtonEnabled, isPreviousButtonEnabled,
@ -106,7 +104,7 @@ export default function ViewPublicationPage() {
} }
nextButton={ nextButton={
<NextButton <NextButton
isNextButtonEnabled={settings.status === "ai" || isNextButtonEnabled} isNextButtonEnabled={settings.status === "ai" || (isNextButtonEnabled && !isFileUploading)}
moveToNextQuestion={async () => { moveToNextQuestion={async () => {
if (!preview) { if (!preview) {
await sendQuestionAnswer(quizId, currentQuestion, currentAnswer, ownVariants)?.catch((e) => { await sendQuestionAnswer(quizId, currentQuestion, currentAnswer, ownVariants)?.catch((e) => {

@ -1,4 +1,4 @@
import { useState } from "react"; import { useState, Dispatch, SetStateAction } from "react";
import { Box, ButtonBase, Skeleton, Typography, useTheme } from "@mui/material"; import { Box, ButtonBase, Skeleton, Typography, useTheme } from "@mui/material";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
@ -16,18 +16,28 @@ import Info from "@icons/Info";
import UploadIcon from "@icons/UploadIcon"; import UploadIcon from "@icons/UploadIcon";
import type { QuizQuestionFile } from "@model/questionTypes/file"; import type { QuizQuestionFile } from "@model/questionTypes/file";
import type { ModalWarningType } from "./index";
import { useQuizStore } from "@/stores/useQuizStore"; import { useQuizStore } from "@/stores/useQuizStore";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
export type ModalWarningType = "errorType" | "errorSize" | "picture" | "video" | "audio" | "document" | null;
type UploadFileProps = { type UploadFileProps = {
currentQuestion: QuizQuestionFile; currentQuestion: QuizQuestionFile;
setModalWarningType: (modalType: ModalWarningType) => void; setModalWarningType: Dispatch<SetStateAction<ModalWarningType>>;
isSending: boolean; isSending: boolean;
setIsSending: (isSending: boolean) => void; setIsSending: Dispatch<SetStateAction<boolean>>;
onFileUpload: (file: File) => Promise<void>;
isUploading: boolean;
}; };
export const UploadFile = ({ currentQuestion, setModalWarningType, isSending, setIsSending }: UploadFileProps) => { export const UploadFile = ({
currentQuestion,
setModalWarningType,
isSending,
setIsSending,
onFileUpload,
isUploading,
}: UploadFileProps) => {
const { quizId, preview } = useQuizStore(); const { quizId, preview } = useQuizStore();
const [isDropzoneHighlighted, setIsDropzoneHighlighted] = useState<boolean>(false); const [isDropzoneHighlighted, setIsDropzoneHighlighted] = useState<boolean>(false);
const theme = useTheme(); const theme = useTheme();
@ -38,51 +48,57 @@ export const UploadFile = ({ currentQuestion, setModalWarningType, isSending, se
const answer = answers.find(({ questionId }) => questionId === currentQuestion.id)?.answer as string; const answer = answers.find(({ questionId }) => questionId === currentQuestion.id)?.answer as string;
const uploadFile = async (file: File | undefined) => { const handleFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
if (isSending) return; const file = event.target.files?.[0];
if (!file) return; if (!file) return;
if (file.size > MAX_FILE_SIZE) return setModalWarningType("errorSize");
const isFileTypeAccepted = ACCEPT_SEND_FILE_TYPES_MAP[currentQuestion.content.type].some((fileType) => const fileType = file.type.split("/")[0];
file.name.toLowerCase().endsWith(fileType) const fileExtension = file.name.split(".").pop()?.toLowerCase();
);
if (!isFileTypeAccepted) return setModalWarningType("errorType"); if (!ACCEPT_SEND_FILE_TYPES_MAP[currentQuestion.content.type].includes(`.${fileExtension}`)) {
setModalWarningType("errorType");
return;
}
if (file.size > 50 * 1024 * 1024) {
setModalWarningType("errorSize");
return;
}
setIsSending(true); setIsSending(true);
try { try {
const data = await sendFile({ await onFileUpload(file);
questionId: currentQuestion.id, } finally {
body: {
file: file,
name: file.name,
preview,
},
qid: quizId,
});
await sendAnswer({
questionId: currentQuestion.id,
body: `${data!.data.fileIDMap[currentQuestion.id]}`,
qid: quizId,
preview,
});
updateAnswer(currentQuestion.id, `${file.name}|${URL.createObjectURL(file)}`, 0);
} catch (error) {
console.error(error);
enqueueSnackbar(t("The answer was not counted"));
}
setIsSending(false); setIsSending(false);
}
}; };
const onDrop = (event: React.DragEvent<HTMLDivElement>) => { const onDrop = async (event: React.DragEvent<HTMLLabelElement>) => {
event.preventDefault(); event.preventDefault();
setIsDropzoneHighlighted(false); setIsDropzoneHighlighted(false);
const file = event.dataTransfer.files[0]; const file = event.dataTransfer.files[0];
if (!file) return;
uploadFile(file); const fileType = file.type.split("/")[0];
const fileExtension = file.name.split(".").pop()?.toLowerCase();
if (!ACCEPT_SEND_FILE_TYPES_MAP[currentQuestion.content.type].includes(`.${fileExtension}`)) {
setModalWarningType("errorType");
return;
}
if (file.size > 50 * 1024 * 1024) {
setModalWarningType("errorSize");
return;
}
setIsSending(true);
try {
await onFileUpload(file);
} finally {
setIsSending(false);
}
}; };
return ( return (
@ -93,52 +109,48 @@ export const UploadFile = ({ currentQuestion, setModalWarningType, isSending, se
sx={{ width: "100%", height: "120px", maxWidth: "560px" }} sx={{ width: "100%", height: "120px", maxWidth: "560px" }}
/> />
) : ( ) : (
<ButtonBase
component="label"
sx={{ justifyContent: "flex-start", width: "100%" }}
>
<input
onChange={({ target }) => uploadFile(target.files?.[0])}
hidden
accept={ACCEPT_SEND_FILE_TYPES_MAP[currentQuestion.content.type].join(",")}
multiple
type="file"
/>
<Box <Box
onDragEnter={() => !answer?.split("|")[0] && setIsDropzoneHighlighted(true)} component="label"
sx={{
width: "100%",
height: "300px",
border: "2px dashed",
borderColor: isDropzoneHighlighted ? "primary.main" : "grey.300",
borderRadius: "12px",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
cursor: "pointer",
transition: "all 0.2s",
backgroundColor: isDropzoneHighlighted ? "action.hover" : "background.paper",
opacity: isSending || isUploading ? 0.7 : 1,
pointerEvents: isSending || isUploading ? "none" : "auto",
"&:hover": {
borderColor: "primary.main",
backgroundColor: "action.hover",
},
}}
onDragEnter={() => setIsDropzoneHighlighted(true)}
onDragLeave={() => setIsDropzoneHighlighted(false)} onDragLeave={() => setIsDropzoneHighlighted(false)}
onDragOver={(event) => event.preventDefault()} onDragOver={(event) => event.preventDefault()}
onDrop={onDrop} onDrop={onDrop}
sx={{
width: "100%",
height: isMobile ? undefined : "120px",
display: "flex",
gap: "50px",
justifyContent: "flex-start",
alignItems: "center",
padding: "33px 44px 33px 55px",
backgroundColor: "#F2F3F7",
border: `1px solid ${isDropzoneHighlighted ? "red" : "#9A9AAF"}`,
borderRadius: "8px",
}}
> >
<UploadIcon /> <input
<Box> onChange={handleFileChange}
<Typography sx={{ color: "#9A9AAF", fontWeight: 500 }}> hidden
{t(UPLOAD_FILE_DESCRIPTIONS_MAP[currentQuestion.content.type].title)} accept={ACCEPT_SEND_FILE_TYPES_MAP[currentQuestion.content.type].join(",")}
</Typography> type="file"
/>
<UploadIcon color={isDropzoneHighlighted ? "primary" : "grey"} />
<Typography <Typography
sx={{ variant="body1"
color: "#9A9AAF", color={isDropzoneHighlighted ? "primary" : "text.secondary"}
fontSize: "16px", sx={{ mt: 2 }}
lineHeight: "19px",
}}
> >
{t(UPLOAD_FILE_DESCRIPTIONS_MAP[currentQuestion.content.type].description)} {isUploading ? t("Uploading...") : t("Drop file here or click to upload")}
</Typography> </Typography>
</Box> </Box>
</Box>
</ButtonBase>
)} )}
<Info <Info
sx={{ width: "40px", height: "40px" }} sx={{ width: "40px", height: "40px" }}

@ -11,6 +11,8 @@ import { ACCEPT_SEND_FILE_TYPES_MAP } from "@/components/ViewPublicationPage/too
import type { QuizQuestionFile } from "@model/questionTypes/file"; import type { QuizQuestionFile } from "@model/questionTypes/file";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { sendFile } from "@api/quizRelase";
import { enqueueSnackbar } from "notistack";
export type ModalWarningType = "errorType" | "errorSize" | "picture" | "video" | "audio" | "document" | null; export type ModalWarningType = "errorType" | "errorSize" | "picture" | "video" | "audio" | "document" | null;
@ -20,13 +22,46 @@ type FileProps = {
export const File = ({ currentQuestion }: FileProps) => { export const File = ({ currentQuestion }: FileProps) => {
const theme = useTheme(); const theme = useTheme();
const { t } = useTranslation();
const answers = useQuizViewStore((state) => state.answers); const answers = useQuizViewStore((state) => state.answers);
const updateAnswer = useQuizViewStore((state) => state.updateAnswer);
const setFileUploading = useQuizViewStore((state) => state.setFileUploading);
const [modalWarningType, setModalWarningType] = useState<ModalWarningType>(null); const [modalWarningType, setModalWarningType] = useState<ModalWarningType>(null);
const [isSending, setIsSending] = useState<boolean>(false); const [isSending, setIsSending] = useState<boolean>(false);
const [isUploading, setIsUploading] = useState<boolean>(false);
const isMobile = useRootContainerSize() < 500; const isMobile = useRootContainerSize() < 500;
const answer = answers.find(({ questionId }) => questionId === currentQuestion.id)?.answer as string; const answer = answers.find(({ questionId }) => questionId === currentQuestion.id)?.answer as string;
const handleFileUpload = async (file: File) => {
if (!file) return;
try {
setIsUploading(true);
setFileUploading(currentQuestion.id, true);
const result = await sendFile({
questionId: currentQuestion.id,
body: {
name: file.name,
file,
preview: false,
},
qid: window.location.pathname.split("/").pop() || "",
});
if (result) {
updateAnswer(currentQuestion.id, result.fileName, 0);
enqueueSnackbar(t("File uploaded successfully"));
}
} catch (error) {
console.error(error);
enqueueSnackbar(t("Failed to upload file"));
} finally {
setIsUploading(false);
setFileUploading(currentQuestion.id, false);
}
};
return ( return (
<Box> <Box>
<Typography <Typography
@ -56,6 +91,8 @@ export const File = ({ currentQuestion }: FileProps) => {
setModalWarningType={setModalWarningType} setModalWarningType={setModalWarningType}
isSending={isSending} isSending={isSending}
setIsSending={setIsSending} setIsSending={setIsSending}
onFileUpload={handleFileUpload}
isUploading={isUploading}
/> />
)} )}
{answer && currentQuestion.content.type === "picture" && ( {answer && currentQuestion.content.type === "picture" && (

@ -25,6 +25,7 @@ interface QuizViewStore {
pointsSum: number; pointsSum: number;
points: Record<string, number>; points: Record<string, number>;
currentQuizStep: QuizStep; currentQuizStep: QuizStep;
uploadingFiles: Record<string, boolean>;
} }
interface QuizViewActions { interface QuizViewActions {
@ -33,6 +34,7 @@ interface QuizViewActions {
updateOwnVariant: (id: string, answer: string) => void; updateOwnVariant: (id: string, answer: string) => void;
deleteOwnVariant: (id: string) => void; deleteOwnVariant: (id: string) => void;
setCurrentQuizStep: (step: QuizStep) => void; setCurrentQuizStep: (step: QuizStep) => void;
setFileUploading: (questionId: string, isUploading: boolean) => void;
} }
export const QuizViewContext = createContext<ReturnType<typeof createQuizViewStore> | null>(null); export const QuizViewContext = createContext<ReturnType<typeof createQuizViewStore> | null>(null);
@ -54,6 +56,7 @@ export const createQuizViewStore = () =>
points: {}, points: {},
pointsSum: 0, pointsSum: 0,
currentQuizStep: "startpage", currentQuizStep: "startpage",
uploadingFiles: {},
updateAnswer(questionId, answer, points) { updateAnswer(questionId, answer, points) {
set( set(
(state) => { (state) => {
@ -136,6 +139,19 @@ export const createQuizViewStore = () =>
step, step,
}); });
}, },
setFileUploading(questionId, isUploading) {
set(
(state) => {
state.uploadingFiles[questionId] = isUploading;
},
false,
{
type: "setFileUploading",
questionId,
isUploading,
}
);
},
}), }),
{ {
name: "QuizViewStore-" + nanoid(4), name: "QuizViewStore-" + nanoid(4),

@ -53,5 +53,6 @@
"and": "and", "and": "and",
"Get results": "Get results", "Get results": "Get results",
"Data sent successfully": "Data sent successfully", "Data sent successfully": "Data sent successfully",
"Step": "Step" "Step": "Step",
"questions are not ready yet": "There are no questions for the audience yet. Please wait"
} }

@ -53,5 +53,6 @@
"and": "и", "and": "и",
"Get results": "Получить результаты", "Get results": "Получить результаты",
"Data sent successfully": "Данные успешно отправлены", "Data sent successfully": "Данные успешно отправлены",
"Step": "Шаг" "Step": "Шаг",
"questions are not ready yet": "Вопросы для аудитории ещё не созданы. Пожалуйста, подождите"
} }

@ -53,5 +53,6 @@
"and": "va", "and": "va",
"Get results": "Natijalarni olish", "Get results": "Natijalarni olish",
"Data sent successfully": "Ma'lumotlar muvaffaqiyatli yuborildi", "Data sent successfully": "Ma'lumotlar muvaffaqiyatli yuborildi",
"Step": "Qadam" "Step": "Qadam",
"questions are not ready yet": "Tomoshabinlar uchun hozircha savollar yo'q. Iltimos kuting"
} }