Обработка ошибки, когда вопросы для аудитории ещё не созданы
This commit is contained in:
parent
c186a04fa5
commit
12a1aab506
@ -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"
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user