Обработка ошибки, когда вопросы для аудитории ещё не созданы
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,18 +93,21 @@ export async function getData({ quizId }: { quizId: string }): Promise<{
if (paudParam) body.auditory = Number(paudParam);
try {
const { data, headers } = await axios<GetQuizDataResponse>(domain + `/answer/v1.0.0/settings`, {
method: "POST",
headers: {
"X-Sessionkey": SESSIONS,
"Content-Type": "application/json",
DeviceType: DeviceType,
Device: Device,
OS: OSDevice,
Browser: userAgent,
},
data: body,
});
const { data, headers } = await axios<GetQuizDataResponse>(
domain + `/answer/v1.0.0/settings${window.location.search}`,
{
method: "POST",
headers: {
"X-Sessionkey": SESSIONS,
"Content-Type": "application/json",
DeviceType: DeviceType,
Device: Device,
OS: OSDevice,
Browser: userAgent,
},
data: body,
}
);
const sessions = JSON.parse(localStorage.getItem("sessions") || "{}");
//Тут ещё проверка на антифрод без парса конфига. Нам не интересно время если не нужно запрещать проходить чаще чем в сутки
@ -133,23 +136,26 @@ export async function getDataSingle({ quizId, page }: { quizId: string; page?: n
try {
// Первый запрос: 1 вопрос + конфиг
if (isFirstRequest) {
const { data, headers } = await axios<GetQuizDataResponse>(domain + `/answer/v1.0.0/settings`, {
method: "POST",
headers: {
"X-Sessionkey": SESSIONS,
"Content-Type": "application/json",
DeviceType: DeviceType,
Device: Device,
OS: OSDevice,
Browser: userAgent,
},
data: {
quiz_id: quizId,
limit: 1,
page: 0,
need_config: true,
},
});
const { data, headers } = await axios<GetQuizDataResponse>(
domain + `/answer/v1.0.0/settings${window.location.search}`,
{
method: "POST",
headers: {
"X-Sessionkey": SESSIONS,
"Content-Type": "application/json",
DeviceType: DeviceType,
Device: Device,
OS: OSDevice,
Browser: userAgent,
},
data: {
quiz_id: quizId,
limit: 1,
page: 0,
need_config: true,
},
}
);
globalStatus = data.settings.status;
isFirstRequest = false;
@ -165,23 +171,26 @@ export async function getDataSingle({ quizId, page }: { quizId: string; page?: n
// Если статус не AI - сразу делаем запрос за всеми вопросами
if (globalStatus !== "ai") {
const secondResponse = await axios<GetQuizDataResponse>(domain + `/answer/v1.0.0/settings`, {
method: "POST",
headers: {
"X-Sessionkey": SESSIONS,
"Content-Type": "application/json",
DeviceType: DeviceType,
Device: Device,
OS: OSDevice,
Browser: userAgent,
},
data: {
quiz_id: quizId,
limit: 100,
page: 0,
need_config: false,
},
});
const secondResponse = await axios<GetQuizDataResponse>(
domain + `/answer/v1.0.0/settings${window.location.search}`,
{
method: "POST",
headers: {
"X-Sessionkey": SESSIONS,
"Content-Type": "application/json",
DeviceType: DeviceType,
Device: Device,
OS: OSDevice,
Browser: userAgent,
},
data: {
quiz_id: quizId,
limit: 100,
page: 0,
need_config: false,
},
}
);
return {
data: { ...data, items: secondResponse.data.items },
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",
headers: {
"X-Sessionkey": SESSIONS,
@ -418,7 +427,7 @@ type Answer = {
};
export function sendFile({ questionId, body, qid }: SendFileParams) {
if (body.preview) return;
if (body.preview) return Promise.resolve();
const formData = new FormData();
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`,
body: formData,
method: "POST",
});
}).then((response) => ({
fileName: nameImage,
response,
}));
}
//форма контактов

@ -22,12 +22,10 @@ import { useQuizStore } from "@/stores/useQuizStore";
export default function ViewPublicationPage() {
const { settings, recentlyCompleted, quizId, preview, changeFaviconAndTitle } = useQuizStore();
const answers = useQuizViewStore((state) => state.answers);
const ownVariants = useQuizViewStore((state) => state.ownVariants);
let currentQuizStep = useQuizViewStore((state) => state.currentQuizStep);
const { currentQuestion, currentQuestionStepNumber, currentQuizStep, answers, ownVariants } = useQuizViewStore();
const uploadingFiles = useQuizViewStore((state) => state.uploadingFiles);
const isFileUploading = Object.values(uploadingFiles).some((isUploading) => isUploading);
const {
currentQuestion,
currentQuestionStepNumber,
nextQuestion,
isNextButtonEnabled,
isPreviousButtonEnabled,
@ -106,7 +104,7 @@ export default function ViewPublicationPage() {
}
nextButton={
<NextButton
isNextButtonEnabled={settings.status === "ai" || isNextButtonEnabled}
isNextButtonEnabled={settings.status === "ai" || (isNextButtonEnabled && !isFileUploading)}
moveToNextQuestion={async () => {
if (!preview) {
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 { enqueueSnackbar } from "notistack";
@ -16,18 +16,28 @@ import Info from "@icons/Info";
import UploadIcon from "@icons/UploadIcon";
import type { QuizQuestionFile } from "@model/questionTypes/file";
import type { ModalWarningType } from "./index";
import { useQuizStore } from "@/stores/useQuizStore";
import { useTranslation } from "react-i18next";
export type ModalWarningType = "errorType" | "errorSize" | "picture" | "video" | "audio" | "document" | null;
type UploadFileProps = {
currentQuestion: QuizQuestionFile;
setModalWarningType: (modalType: ModalWarningType) => void;
setModalWarningType: Dispatch<SetStateAction<ModalWarningType>>;
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 [isDropzoneHighlighted, setIsDropzoneHighlighted] = useState<boolean>(false);
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 uploadFile = async (file: File | undefined) => {
if (isSending) return;
const handleFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) return;
if (file.size > MAX_FILE_SIZE) return setModalWarningType("errorSize");
const isFileTypeAccepted = ACCEPT_SEND_FILE_TYPES_MAP[currentQuestion.content.type].some((fileType) =>
file.name.toLowerCase().endsWith(fileType)
);
const fileType = file.type.split("/")[0];
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);
try {
const data = await sendFile({
questionId: currentQuestion.id,
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"));
await onFileUpload(file);
} finally {
setIsSending(false);
}
setIsSending(false);
};
const onDrop = (event: React.DragEvent<HTMLDivElement>) => {
const onDrop = async (event: React.DragEvent<HTMLLabelElement>) => {
event.preventDefault();
setIsDropzoneHighlighted(false);
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 (
@ -93,52 +109,48 @@ export const UploadFile = ({ currentQuestion, setModalWarningType, isSending, se
sx={{ width: "100%", height: "120px", maxWidth: "560px" }}
/>
) : (
<ButtonBase
<Box
component="label"
sx={{ justifyContent: "flex-start", width: "100%" }}
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)}
onDragOver={(event) => event.preventDefault()}
onDrop={onDrop}
>
<input
onChange={({ target }) => uploadFile(target.files?.[0])}
onChange={handleFileChange}
hidden
accept={ACCEPT_SEND_FILE_TYPES_MAP[currentQuestion.content.type].join(",")}
multiple
type="file"
/>
<Box
onDragEnter={() => !answer?.split("|")[0] && setIsDropzoneHighlighted(true)}
onDragLeave={() => setIsDropzoneHighlighted(false)}
onDragOver={(event) => event.preventDefault()}
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 color={isDropzoneHighlighted ? "primary" : "grey"} />
<Typography
variant="body1"
color={isDropzoneHighlighted ? "primary" : "text.secondary"}
sx={{ mt: 2 }}
>
<UploadIcon />
<Box>
<Typography sx={{ color: "#9A9AAF", fontWeight: 500 }}>
{t(UPLOAD_FILE_DESCRIPTIONS_MAP[currentQuestion.content.type].title)}
</Typography>
<Typography
sx={{
color: "#9A9AAF",
fontSize: "16px",
lineHeight: "19px",
}}
>
{t(UPLOAD_FILE_DESCRIPTIONS_MAP[currentQuestion.content.type].description)}
</Typography>
</Box>
</Box>
</ButtonBase>
{isUploading ? t("Uploading...") : t("Drop file here or click to upload")}
</Typography>
</Box>
)}
<Info
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 { useTranslation } from "react-i18next";
import { sendFile } from "@api/quizRelase";
import { enqueueSnackbar } from "notistack";
export type ModalWarningType = "errorType" | "errorSize" | "picture" | "video" | "audio" | "document" | null;
@ -20,13 +22,46 @@ type FileProps = {
export const File = ({ currentQuestion }: FileProps) => {
const theme = useTheme();
const { t } = useTranslation();
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 [isSending, setIsSending] = useState<boolean>(false);
const [isUploading, setIsUploading] = useState<boolean>(false);
const isMobile = useRootContainerSize() < 500;
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 (
<Box>
<Typography
@ -56,6 +91,8 @@ export const File = ({ currentQuestion }: FileProps) => {
setModalWarningType={setModalWarningType}
isSending={isSending}
setIsSending={setIsSending}
onFileUpload={handleFileUpload}
isUploading={isUploading}
/>
)}
{answer && currentQuestion.content.type === "picture" && (

@ -1,147 +1,163 @@
import { QuestionVariant } from "@model/questionTypes/shared";
import { QuizStep } from "@model/settingsData";
import type { Moment } from "moment";
import { nanoid } from "nanoid";
import { createContext, useContext } from "react";
import { createStore, useStore } from "zustand";
import { immer } from "zustand/middleware/immer";
import { devtools } from "zustand/middleware";
export type Answer = string | string[] | Moment;
export type QuestionAnswer = {
questionId: string;
answer: Answer;
};
export type OwnVariant = {
id: string;
variant: QuestionVariant;
};
interface QuizViewStore {
answers: QuestionAnswer[];
ownVariants: OwnVariant[];
pointsSum: number;
points: Record<string, number>;
currentQuizStep: QuizStep;
}
interface QuizViewActions {
updateAnswer: (questionId: string, answer: string | string[] | Moment, points: number) => void;
deleteAnswer: (questionId: string) => void;
updateOwnVariant: (id: string, answer: string) => void;
deleteOwnVariant: (id: string) => void;
setCurrentQuizStep: (step: QuizStep) => void;
}
export const QuizViewContext = createContext<ReturnType<typeof createQuizViewStore> | null>(null);
export function useQuizViewStore<U>(selector: (state: QuizViewStore & QuizViewActions) => U): U {
const store = useContext(QuizViewContext);
if (!store) throw new Error("QuizViewStore context is null");
return useStore(store, selector);
}
export const createQuizViewStore = () =>
createStore<QuizViewStore & QuizViewActions>()(
immer(
devtools(
(set, get) => ({
answers: [],
ownVariants: [],
points: {},
pointsSum: 0,
currentQuizStep: "startpage",
updateAnswer(questionId, answer, points) {
set(
(state) => {
const index = state.answers.findIndex((answer) => questionId === answer.questionId);
if (index < 0) {
state.answers.push({ questionId, answer });
} else {
state.answers[index] = { questionId, answer };
}
state.points = { ...state.points, ...{ [questionId]: points } };
state.pointsSum = Object.values(state.points).reduce((sum, value) => sum + value);
},
false,
{
type: "updateAnswer",
questionId,
answer,
points,
}
);
},
deleteAnswer(questionId) {
set(
(state) => {
state.answers = state.answers.filter((answer) => questionId !== answer.questionId);
},
false,
{
type: "deleteAnswer",
questionId,
}
);
},
updateOwnVariant(id, answer) {
set(
(state) => {
const index = state.ownVariants.findIndex((variant) => variant.id === id);
if (index < 0) {
state.ownVariants.push({
id,
variant: {
id: id,
answer,
extendedText: "",
hints: "",
originalImageUrl: "",
},
});
} else {
state.ownVariants[index].variant.answer = answer;
}
},
false,
{
type: "updateOwnVariant",
id,
answer,
}
);
},
deleteOwnVariant(id) {
set(
(state) => {
state.ownVariants = state.ownVariants.filter((variant) => variant.id !== id);
},
false,
{
type: "deleteOwnVariant",
id,
}
);
},
setCurrentQuizStep(step) {
set({ currentQuizStep: step }, false, {
type: "setCurrentQuizStep",
step,
});
},
}),
{
name: "QuizViewStore-" + nanoid(4),
enabled: import.meta.env.DEV,
trace: import.meta.env.DEV,
}
)
)
);
import { QuestionVariant } from "@model/questionTypes/shared";
import { QuizStep } from "@model/settingsData";
import type { Moment } from "moment";
import { nanoid } from "nanoid";
import { createContext, useContext } from "react";
import { createStore, useStore } from "zustand";
import { immer } from "zustand/middleware/immer";
import { devtools } from "zustand/middleware";
export type Answer = string | string[] | Moment;
export type QuestionAnswer = {
questionId: string;
answer: Answer;
};
export type OwnVariant = {
id: string;
variant: QuestionVariant;
};
interface QuizViewStore {
answers: QuestionAnswer[];
ownVariants: OwnVariant[];
pointsSum: number;
points: Record<string, number>;
currentQuizStep: QuizStep;
uploadingFiles: Record<string, boolean>;
}
interface QuizViewActions {
updateAnswer: (questionId: string, answer: string | string[] | Moment, points: number) => void;
deleteAnswer: (questionId: string) => void;
updateOwnVariant: (id: string, answer: string) => void;
deleteOwnVariant: (id: string) => void;
setCurrentQuizStep: (step: QuizStep) => void;
setFileUploading: (questionId: string, isUploading: boolean) => void;
}
export const QuizViewContext = createContext<ReturnType<typeof createQuizViewStore> | null>(null);
export function useQuizViewStore<U>(selector: (state: QuizViewStore & QuizViewActions) => U): U {
const store = useContext(QuizViewContext);
if (!store) throw new Error("QuizViewStore context is null");
return useStore(store, selector);
}
export const createQuizViewStore = () =>
createStore<QuizViewStore & QuizViewActions>()(
immer(
devtools(
(set, get) => ({
answers: [],
ownVariants: [],
points: {},
pointsSum: 0,
currentQuizStep: "startpage",
uploadingFiles: {},
updateAnswer(questionId, answer, points) {
set(
(state) => {
const index = state.answers.findIndex((answer) => questionId === answer.questionId);
if (index < 0) {
state.answers.push({ questionId, answer });
} else {
state.answers[index] = { questionId, answer };
}
state.points = { ...state.points, ...{ [questionId]: points } };
state.pointsSum = Object.values(state.points).reduce((sum, value) => sum + value);
},
false,
{
type: "updateAnswer",
questionId,
answer,
points,
}
);
},
deleteAnswer(questionId) {
set(
(state) => {
state.answers = state.answers.filter((answer) => questionId !== answer.questionId);
},
false,
{
type: "deleteAnswer",
questionId,
}
);
},
updateOwnVariant(id, answer) {
set(
(state) => {
const index = state.ownVariants.findIndex((variant) => variant.id === id);
if (index < 0) {
state.ownVariants.push({
id,
variant: {
id: id,
answer,
extendedText: "",
hints: "",
originalImageUrl: "",
},
});
} else {
state.ownVariants[index].variant.answer = answer;
}
},
false,
{
type: "updateOwnVariant",
id,
answer,
}
);
},
deleteOwnVariant(id) {
set(
(state) => {
state.ownVariants = state.ownVariants.filter((variant) => variant.id !== id);
},
false,
{
type: "deleteOwnVariant",
id,
}
);
},
setCurrentQuizStep(step) {
set({ currentQuizStep: step }, false, {
type: "setCurrentQuizStep",
step,
});
},
setFileUploading(questionId, isUploading) {
set(
(state) => {
state.uploadingFiles[questionId] = isUploading;
},
false,
{
type: "setFileUploading",
questionId,
isUploading,
}
);
},
}),
{
name: "QuizViewStore-" + nanoid(4),
enabled: import.meta.env.DEV,
trace: import.meta.env.DEV,
}
)
)
);

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

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