Compare commits
22 Commits
Author | SHA1 | Date | |
---|---|---|---|
a532eecdca | |||
326e2c98b3 | |||
b58042554f | |||
147b776550 | |||
0c9b6e5b7a | |||
785e56f9b0 | |||
f330c5c05a | |||
b1c3ab7314 | |||
3baaef83fe | |||
9e301e694f | |||
f13df84fec | |||
4e2e591cd4 | |||
670d2bcb3f | |||
c8ebf9cff0 | |||
656ba9473c | |||
17bd5cd53f | |||
b495974dba | |||
87f78ff8cf | |||
c5e931702b | |||
106d8b6ad7 | |||
32e5853ee8 | |||
bcf2ca0842 |
@ -2,23 +2,29 @@ name: Deploy
|
||||
run-name: ${{ gitea.actor }} build image and push to container registry
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
registry_package:
|
||||
types: [published]
|
||||
#package_name: "gitea.pena/squiz/frontanswerer/main:latest"
|
||||
|
||||
jobs:
|
||||
CreateImage:
|
||||
runs-on: [skeris]
|
||||
uses: https://gitea.pena/PenaDevops/actions.git/.gitea/workflows/build-image.yml@v1.1.6-p
|
||||
with:
|
||||
runner: skeris
|
||||
secrets:
|
||||
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
|
||||
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
# CreateImage:
|
||||
# runs-on: [skeris]
|
||||
# uses: https://gitea.pena/PenaDevops/actions.git/.gitea/workflows/build-image.yml@v1.1.6-p
|
||||
# with:
|
||||
# runner: skeris
|
||||
# secrets:
|
||||
# REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
|
||||
# REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
DeployService:
|
||||
if: contains(github.event.package.name, 'main')
|
||||
runs-on: [frontprod]
|
||||
needs: CreateImage
|
||||
uses: https://gitea.pena/PenaDevops/actions.git/.gitea/workflows/deploy.yml@v1.1.4-p7
|
||||
with:
|
||||
runner: hubprod
|
||||
actionid: ${{ gitea.run_id }}
|
||||
container:
|
||||
image: gitea.pena/penadevops/container-images/node-compose:main
|
||||
env:
|
||||
GITHUB_RUN_NUMBER: "${{ inputs.actionid }}"
|
||||
volumes:
|
||||
- /run/user/1000/podman/podman.sock:/run/user/1000/podman/podman.sock
|
||||
steps:
|
||||
- name: Check out repository code
|
||||
uses: http://gitea.pena/PenaDevops/actions.git/checkout@v1
|
||||
- run: compose -f deployments/main/docker-compose.yaml up -d
|
||||
|
@ -2,23 +2,28 @@ name: Deploy
|
||||
run-name: ${{ gitea.actor }} build image and push to container registry
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "staging"
|
||||
registry_package:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
CreateImage:
|
||||
runs-on: [skeris]
|
||||
uses: http://gitea.pena/PenaDevops/actions.git/.gitea/workflows/build-image.yml@v1.1.6-p
|
||||
with:
|
||||
runner: hubstaging
|
||||
secrets:
|
||||
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
|
||||
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
# CreateImage:
|
||||
# runs-on: [skeris]
|
||||
# uses: http://gitea.pena/PenaDevops/actions.git/.gitea/workflows/build-image.yml@v1.1.6-p
|
||||
# with:
|
||||
# runner: hubstaging
|
||||
# secrets:
|
||||
# REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
|
||||
# REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
DeployService:
|
||||
if: contains(github.event.package.name, 'staging')
|
||||
runs-on: [frontstaging]
|
||||
needs: CreateImage
|
||||
uses: http://gitea.pena/PenaDevops/actions.git/.gitea/workflows/deploy.yml@v1.1.4-p7
|
||||
with:
|
||||
runner: frontstaging
|
||||
actionid: ${{ gitea.run_id }}
|
||||
container:
|
||||
image: gitea.pena:3000/penadevops/container-images/node-compose:main
|
||||
env:
|
||||
GITHUB_RUN_NUMBER: "${{ inputs.actionid }}"
|
||||
volumes:
|
||||
- /run/user/1000/podman/podman.sock:/run/user/1000/podman/podman.sock
|
||||
steps:
|
||||
- name: Check out repository code
|
||||
uses: http://gitea.pena:3000/PenaDevops/actions.git/checkout@v1
|
||||
- run: compose -f deployments/staging/docker-compose.yaml up -d
|
||||
|
@ -2,6 +2,7 @@ services:
|
||||
respondent:
|
||||
container_name: respondent
|
||||
restart: unless-stopped
|
||||
image: gitea.pena/squiz/frontanswerer/main:$GITHUB_RUN_NUMBER
|
||||
image: gitea.pena/squiz/frontanswerer/main:latest
|
||||
hostname: respondent
|
||||
tty: true
|
||||
pull_policy: always
|
||||
|
@ -2,9 +2,7 @@ services:
|
||||
respondent:
|
||||
container_name: respondent
|
||||
restart: unless-stopped
|
||||
labels:
|
||||
com.pena.domains: s.hbpn.link
|
||||
com.pena.front_headers: "Access-Control-Allow-Origin $$http_origin always"
|
||||
image: gitea.pena:3000/squiz/frontanswerer/staging:$GITHUB_RUN_NUMBER
|
||||
image: gitea.pena/squiz/frontanswerer/staging:latest
|
||||
hostname: respondent
|
||||
tty: true
|
||||
pull_policy: always
|
||||
|
@ -39,7 +39,6 @@ export function useQuizData(quizId: string, preview: boolean = false) {
|
||||
});
|
||||
setQuizData(firstData);
|
||||
|
||||
// Определяем нужно ли загружать все данные
|
||||
if (!["ai"].includes(firstData.settings.status)) {
|
||||
setNeedFullLoad(true); // Триггерит новый запрос через изменение ключа
|
||||
return firstData;
|
||||
@ -57,7 +56,13 @@ export function useQuizData(quizId: string, preview: boolean = false) {
|
||||
});
|
||||
|
||||
addQuestions(data.questions.slice(1));
|
||||
return data;
|
||||
|
||||
// Возвращаем полную структуру данных с настройками из store
|
||||
const currentState = useQuizStore.getState();
|
||||
return {
|
||||
...currentState,
|
||||
questions: [...currentState.questions, ...data.questions.slice(1)],
|
||||
};
|
||||
}
|
||||
|
||||
if (currentPage >= questions.length) {
|
||||
@ -71,7 +76,13 @@ export function useQuizData(quizId: string, preview: boolean = false) {
|
||||
});
|
||||
addQuestions(data.questions);
|
||||
changeNextLoading(false);
|
||||
return data;
|
||||
|
||||
// Возвращаем полную структуру данных с настройками из store
|
||||
const currentState = useQuizStore.getState();
|
||||
return {
|
||||
...currentState,
|
||||
questions: [...currentState.questions, ...data.questions],
|
||||
};
|
||||
} catch (p) {
|
||||
setPage(questions.length);
|
||||
changeNextLoading(false);
|
||||
|
216
lib/assets/icons/NameplateLogoDark.tsx
Normal file
216
lib/assets/icons/NameplateLogoDark.tsx
Normal file
File diff suppressed because one or more lines are too long
@ -22,6 +22,7 @@ import { HelmetProvider } from "react-helmet-async";
|
||||
import "moment/dist/locale/ru";
|
||||
import { useQuizStore, setQuizData, addquizid } from "@/stores/useQuizStore";
|
||||
import { initDataManager, statusOfQuiz } from "@/utils/hooks/useQuestionFlowControl";
|
||||
|
||||
moment.locale("ru");
|
||||
const localeText = ruRU.components.MuiLocalizationProvider.defaultProps.localeText;
|
||||
|
||||
@ -90,15 +91,26 @@ function QuizAnswererInner({
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (isLoading && !questions.length) return <LoadingSkeleton />;
|
||||
if (error) return <ApologyPage error={error} />;
|
||||
if (isLoading && !questions.length) {
|
||||
return <LoadingSkeleton />;
|
||||
}
|
||||
if (error) {
|
||||
return <ApologyPage error={error} />;
|
||||
}
|
||||
|
||||
if (Object.keys(settings).length == 0) return <ApologyPage error={new Error("quiz data is null")} />;
|
||||
if (questions.length === 0) return <ApologyPage error={new Error("No questions found")} />;
|
||||
if (Object.keys(settings).length == 0) {
|
||||
return <ApologyPage error={new Error("quiz data is null")} />;
|
||||
}
|
||||
if (questions.length === 0) {
|
||||
return <ApologyPage error={new Error("No questions found")} />;
|
||||
}
|
||||
|
||||
if (questions.length === 1 && settings.cfg.noStartPage && statusOfQuiz != "ai")
|
||||
if (questions.length === 1 && settings.cfg.noStartPage && statusOfQuiz != "ai") {
|
||||
return <ApologyPage error={new Error("quiz is empty")} />;
|
||||
if (!quizId) return <ApologyPage error={new Error("no quiz id")} />;
|
||||
}
|
||||
if (!quizId) {
|
||||
return <ApologyPage error={new Error("no quiz id")} />;
|
||||
}
|
||||
|
||||
const quizContainer = (
|
||||
<Box
|
||||
|
472
lib/components/ViewPublicationPage/ContactForm/ContactForm.tsx
Normal file
472
lib/components/ViewPublicationPage/ContactForm/ContactForm.tsx
Normal file
@ -0,0 +1,472 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Box, Button, Link, Typography, useTheme } from "@mui/material";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
|
||||
import CustomCheckbox from "@ui_kit/CustomCheckbox";
|
||||
|
||||
import { Inputs } from "@/components/ViewPublicationPage/ContactForm/Inputs/Inputs";
|
||||
import { ContactTextBlock } from "./ContactTextBlock";
|
||||
|
||||
import { useRootContainerSize } from "@contexts/RootContainerWidthContext";
|
||||
|
||||
import { sendFC, SendFCParams } from "@api/quizRelase";
|
||||
|
||||
import { useVkMetricsGoals } from "@/utils/hooks/metrics/useVkMetricsGoals";
|
||||
import { useYandexMetricsGoals } from "@/utils/hooks/metrics/useYandexMetricsGoals";
|
||||
|
||||
import { EMAIL_REGEXP } from "@utils/emailRegexp";
|
||||
import { quizThemes } from "@utils/themes/Publication/themePublication";
|
||||
import { DESIGN_LIST } from "@utils/designList";
|
||||
|
||||
import { NameplateLogo } from "@icons/NameplateLogo";
|
||||
|
||||
import type { FormContactFieldData, FormContactFieldName } from "@model/settingsData";
|
||||
import type { QuizQuestionResult } from "@model/questionTypes/result";
|
||||
import type { AnyTypedQuizQuestion } from "@model/questionTypes/shared";
|
||||
import { isProduction } from "@/utils/defineDomain";
|
||||
import { useQuizStore } from "@/stores/useQuizStore";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { NameplateLogoDark } from "@/assets/icons/NameplateLogoDark";
|
||||
|
||||
type Props = {
|
||||
currentQuestion: AnyTypedQuizQuestion;
|
||||
onShowResult: () => void;
|
||||
};
|
||||
//Костыль для особого квиза. Для него не нужно показывать email адрес
|
||||
const isDisableEmail = window.location.pathname.includes("/377c7570-1bee-4320-ac1e-d731b6223ce8");
|
||||
|
||||
export const ContactForm = ({ currentQuestion, onShowResult }: Props) => {
|
||||
const theme = useTheme();
|
||||
const { settings, questions, quizId, show_badge, preview } = useQuizStore();
|
||||
|
||||
const [ready, setReady] = useState(false);
|
||||
const [name, setName] = useState("");
|
||||
const [email, setEmail] = useState("");
|
||||
const [phone, setPhone] = useState("");
|
||||
const [text, setText] = useState("");
|
||||
const [adress, setAdress] = useState("");
|
||||
const [screenHeight, setScreenHeight] = useState<number>(window.innerHeight);
|
||||
const [emailError, setEmailError] = useState("");
|
||||
const [phoneError, setPhoneError] = useState("");
|
||||
|
||||
const fireOnce = useRef(true);
|
||||
const [fire, setFire] = useState(false);
|
||||
const isMobile = useRootContainerSize() < 850;
|
||||
const isTablet = useRootContainerSize() < 1000;
|
||||
const { t } = useTranslation();
|
||||
|
||||
const vkMetrics = useVkMetricsGoals(settings.cfg.vkMetricsNumber);
|
||||
const yandexMetrics = useYandexMetricsGoals(settings.cfg.yandexMetricsNumber);
|
||||
|
||||
useEffect(() => {
|
||||
function handleResize() {
|
||||
setScreenHeight(window.innerHeight);
|
||||
}
|
||||
|
||||
window.addEventListener("resize", handleResize);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", handleResize);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const resultQuestion =
|
||||
currentQuestion.type === "result"
|
||||
? currentQuestion
|
||||
: questions.find((question): question is QuizQuestionResult => {
|
||||
if (settings?.cfg.haveRoot) {
|
||||
return question.type === "result" && question.content.rule.parentId === currentQuestion.content.id;
|
||||
} else {
|
||||
return question.type === "result" && question.content.rule.parentId === "line";
|
||||
}
|
||||
});
|
||||
|
||||
if (!resultQuestion) throw new Error("Result question not found");
|
||||
|
||||
const inputHC = async () => {
|
||||
const FC = settings.cfg.formContact.fields || settings.cfg.formContact;
|
||||
const body: SendFCParams["body"] = {};
|
||||
if (name.length > 0) body.name = name;
|
||||
if (email.length > 0) body.email = email;
|
||||
if (phone.length > 0) body.phone = phone;
|
||||
if (adress.length > 0) body.address = adress;
|
||||
if (text.length > 0) body.customs = { [FC.text.text || t("Last name")]: text };
|
||||
|
||||
if (Object.keys(body).length > 0) {
|
||||
try {
|
||||
await sendFC({
|
||||
questionId: currentQuestion.id,
|
||||
body: body,
|
||||
qid: quizId,
|
||||
preview,
|
||||
});
|
||||
|
||||
const sessions = JSON.parse(localStorage.getItem("sessions") || "{}");
|
||||
localStorage.setItem("sessions", JSON.stringify({ ...sessions, [quizId]: new Date().getTime() }));
|
||||
} catch (e) {
|
||||
enqueueSnackbar(t("The answer was not counted"));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const FCcopy: Record<FormContactFieldName, FormContactFieldData> =
|
||||
settings.cfg.formContact.fields || settings.cfg.formContact;
|
||||
|
||||
const filteredFC: Partial<Record<FormContactFieldName, FormContactFieldData>> = {};
|
||||
for (const i in FCcopy) {
|
||||
const field = FCcopy[i as keyof typeof FCcopy];
|
||||
if (field.used) {
|
||||
filteredFC[i as FormContactFieldName] = field;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleShowResultsClick() {
|
||||
const FC = settings.cfg.formContact.fields;
|
||||
|
||||
// Проверяем email только если поле отображается
|
||||
if (isEmailFieldVisible && !EMAIL_REGEXP.test(email)) {
|
||||
return enqueueSnackbar("Incorrect email entered");
|
||||
}
|
||||
|
||||
if (fireOnce.current) {
|
||||
// Проверяем, что хотя бы одно видимое поле заполнено
|
||||
const hasVisibleFieldsFilled =
|
||||
(isNameFieldVisible() && name.length > 0) ||
|
||||
(isEmailFieldVisible && email.length > 0) ||
|
||||
(isPhoneFieldVisible() && phone.length > 0) ||
|
||||
(isTextFieldVisible() && text.length > 0) ||
|
||||
(isAddressFieldVisible() && adress.length > 0);
|
||||
|
||||
if (!hasVisibleFieldsFilled) {
|
||||
return enqueueSnackbar(t("Please fill in the fields"));
|
||||
}
|
||||
|
||||
//почта валидна, хоть одно поле заполнено
|
||||
setFire(true);
|
||||
try {
|
||||
await inputHC();
|
||||
fireOnce.current = false;
|
||||
const sessions = JSON.parse(localStorage.getItem("sessions") || "{}");
|
||||
sessions[quizId] = Date.now();
|
||||
localStorage.setItem("sessions", JSON.stringify(sessions));
|
||||
|
||||
vkMetrics.contactsFormFilled();
|
||||
yandexMetrics.contactsFormFilled();
|
||||
//Оповещаем какие поля были заполнены
|
||||
if (name.length !== 0) {
|
||||
vkMetrics.contactsFormField("name");
|
||||
yandexMetrics.contactsFormField("name");
|
||||
}
|
||||
if (email.length !== 0) {
|
||||
vkMetrics.contactsFormField("email");
|
||||
yandexMetrics.contactsFormField("email");
|
||||
}
|
||||
if (phone.length !== 0) {
|
||||
vkMetrics.contactsFormField("phone");
|
||||
yandexMetrics.contactsFormField("phone");
|
||||
}
|
||||
if (text.length !== 0) {
|
||||
vkMetrics.contactsFormField("text");
|
||||
yandexMetrics.contactsFormField("text");
|
||||
}
|
||||
if (adress.length !== 0) {
|
||||
vkMetrics.contactsFormField("address");
|
||||
yandexMetrics.contactsFormField("address");
|
||||
}
|
||||
} catch (e) {
|
||||
enqueueSnackbar(t("Please try again later"));
|
||||
}
|
||||
if (settings.cfg.resultInfo.showResultForm === "after") {
|
||||
onShowResult();
|
||||
}
|
||||
enqueueSnackbar(t("Data sent successfully"));
|
||||
}
|
||||
|
||||
setFire(false);
|
||||
}
|
||||
useEffect(() => {
|
||||
vkMetrics.contactsFormOpened();
|
||||
yandexMetrics.contactsFormOpened();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
// Функция валидации телефона
|
||||
const validatePhone = (phoneValue: string) => {
|
||||
// Убираем все нецифровые символы и считаем только цифры
|
||||
const digitsOnly = phoneValue.replace(/\D/g, "");
|
||||
|
||||
// Для российских номеров (начинающихся с +7) нужно 11 цифр
|
||||
// Для остальных стран - минимум 10 цифр
|
||||
const isRussianNumber = phoneValue.startsWith("+7");
|
||||
const minDigits = isRussianNumber ? 11 : 10;
|
||||
|
||||
// Если есть какие-то символы в инпуте, но цифр меньше минимума - это ошибка
|
||||
if (phoneValue.trim().length > 0 && digitsOnly.length < minDigits) {
|
||||
return t("Please complete the phone number");
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
// Проверяем валидность телефона при каждом изменении
|
||||
const digitsOnly = phone.replace(/\D/g, "");
|
||||
const isRussianNumber = phone.startsWith("+7");
|
||||
const minDigits = isRussianNumber ? 11 : 10;
|
||||
const isPhoneValid = phone.trim().length === 0 || digitsOnly.length >= minDigits;
|
||||
|
||||
// Проверяем валидность email - должен быть заполнен и соответствовать формату
|
||||
const validateEmail = (emailValue: string) => {
|
||||
if (emailValue.trim().length === 0) return false;
|
||||
|
||||
// Проверяем наличие @ и .
|
||||
const atIndex = emailValue.indexOf("@");
|
||||
const dotIndex = emailValue.lastIndexOf(".");
|
||||
|
||||
if (atIndex === -1 || dotIndex === -1) return false;
|
||||
|
||||
// Точка должна быть после @
|
||||
if (dotIndex <= atIndex) return false;
|
||||
|
||||
// Между @ и . должно быть минимум 3 символа
|
||||
const domainPart = emailValue.substring(atIndex + 1, dotIndex);
|
||||
if (domainPart.length < 3) return false;
|
||||
|
||||
// После точки должно быть минимум 2 символа
|
||||
const tldPart = emailValue.substring(dotIndex + 1);
|
||||
if (tldPart.length < 2) return false;
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const isEmailValid = validateEmail(email);
|
||||
|
||||
// Определяем, отображается ли поле email
|
||||
const isEmailFieldVisible = settings.cfg.formContact.fields?.email?.used && !isDisableEmail;
|
||||
|
||||
// Функции для определения видимости полей
|
||||
const isNameFieldVisible = () => {
|
||||
const FC = settings.cfg.formContact.fields;
|
||||
return Object.values(FC).some((data) => data.used) ? FC["name"].used : true;
|
||||
};
|
||||
|
||||
const isPhoneFieldVisible = () => {
|
||||
const FC = settings.cfg.formContact.fields;
|
||||
return Object.values(FC).some((data) => data.used) ? FC["phone"].used : true;
|
||||
};
|
||||
|
||||
const isTextFieldVisible = () => {
|
||||
const FC = settings.cfg.formContact.fields;
|
||||
return Object.values(FC).some((data) => data.used) ? FC["text"].used : false;
|
||||
};
|
||||
|
||||
const isAddressFieldVisible = () => {
|
||||
const FC = settings.cfg.formContact.fields;
|
||||
return Object.values(FC).some((data) => data.used) ? FC["address"].used : false;
|
||||
};
|
||||
|
||||
// Обработчик изменения телефона
|
||||
const handlePhoneChange = (newPhone: string) => {
|
||||
setPhone(newPhone);
|
||||
// Очищаем ошибку если поле стало пустым
|
||||
if (newPhone.trim().length === 0) {
|
||||
setPhoneError("");
|
||||
}
|
||||
};
|
||||
|
||||
// Обработчик изменения email
|
||||
const handleEmailChange = (newEmail: string) => {
|
||||
setEmail(newEmail);
|
||||
// Очищаем ошибку если поле стало пустым
|
||||
if (newEmail.trim().length === 0) {
|
||||
setEmailError("");
|
||||
}
|
||||
};
|
||||
|
||||
// Обработчик потери фокуса для email
|
||||
const handleEmailBlur = () => {
|
||||
if (email.trim().length > 0 && !validateEmail(email)) {
|
||||
setEmailError(t("Please enter a valid email"));
|
||||
} else {
|
||||
setEmailError("");
|
||||
}
|
||||
};
|
||||
|
||||
// Обработчик потери фокуса для телефона
|
||||
const handlePhoneBlur = () => {
|
||||
if (phone.trim().length > 0 && !isPhoneValid) {
|
||||
setPhoneError(t("Please enter a valid phone number"));
|
||||
} else {
|
||||
setPhoneError("");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
backgroundColor: theme.palette.background.default,
|
||||
height: screenHeight > 500 ? "100%" : "auto",
|
||||
overflow: "auto",
|
||||
"&::-webkit-scrollbar": {
|
||||
width: "0",
|
||||
display: "none",
|
||||
msOverflowStyle: "none",
|
||||
},
|
||||
scrollbarWidth: "none",
|
||||
msOverflowStyle: "none",
|
||||
backgroundPosition: "center",
|
||||
backgroundSize: "cover",
|
||||
backgroundImage:
|
||||
settings.cfg.design && !isMobile
|
||||
? quizThemes[settings.cfg.theme].isLight
|
||||
? `url(${DESIGN_LIST[settings.cfg.theme]})`
|
||||
: `linear-gradient(90deg, rgba(39, 38, 38, 0.95) 7.66%, rgba(42, 42, 46, 0.85) 42.12%, rgba(51, 54, 71, 0.4) 100%), url(${
|
||||
DESIGN_LIST[settings.cfg.theme]
|
||||
})`
|
||||
: null,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: !isMobile ? "100%" : isMobile ? undefined : "530px",
|
||||
borderRadius: "4px",
|
||||
height: isMobile ? "100%" : "auto",
|
||||
minHeight: "100%",
|
||||
display: "flex",
|
||||
flexDirection: isMobile ? "column" : "row",
|
||||
background: settings.cfg.design && !isMobile ? undefined : theme.palette.background.default,
|
||||
}}
|
||||
>
|
||||
<ContactTextBlock settings={settings} />
|
||||
<Box
|
||||
sx={{
|
||||
flexGrow: isMobile ? 1 : 0,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
flexDirection: "column",
|
||||
backgroundColor: theme.palette.background.default,
|
||||
height: "auto",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: isMobile ? undefined : "center",
|
||||
justifyContent: "center",
|
||||
flexDirection: "column",
|
||||
p: isMobile ? "0 20px" : isTablet ? "105px 40px 0 60px" : "105px 60px 0 60px",
|
||||
margin: isMobile ? "0" : "auto 0",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
mt: isMobile ? "10px" : "20px",
|
||||
mb: "20px",
|
||||
}}
|
||||
>
|
||||
<Inputs
|
||||
name={name}
|
||||
setName={setName}
|
||||
email={email}
|
||||
setEmail={handleEmailChange}
|
||||
phone={phone}
|
||||
setPhone={handlePhoneChange}
|
||||
text={text}
|
||||
setText={setText}
|
||||
adress={adress}
|
||||
setAdress={setAdress}
|
||||
emailError={emailError}
|
||||
phoneError={phoneError}
|
||||
onEmailBlur={handleEmailBlur}
|
||||
onPhoneBlur={handlePhoneBlur}
|
||||
crutch={{
|
||||
disableEmail: isDisableEmail,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
width: isMobile ? "300px" : "390px",
|
||||
}}
|
||||
>
|
||||
<CustomCheckbox
|
||||
label=""
|
||||
handleChange={({ target }) => {
|
||||
setReady(target.checked);
|
||||
}}
|
||||
checked={ready}
|
||||
colorIcon={theme.palette.primary.main}
|
||||
sx={{ marginRight: "0" }}
|
||||
/>
|
||||
<Typography
|
||||
sx={{
|
||||
color: theme.palette.text.primary,
|
||||
lineHeight: "18.96px",
|
||||
}}
|
||||
fontSize={"16px"}
|
||||
>
|
||||
С 
|
||||
<Link
|
||||
href={"https://shub.pena.digital/ppdd"}
|
||||
target="_blank"
|
||||
>
|
||||
{`${t("Regulation on the processing of personal data")} `}
|
||||
</Link>
|
||||
 {t("and")} 
|
||||
<Link
|
||||
href={"https://shub.pena.digital/docs/privacy"}
|
||||
target="_blank"
|
||||
>
|
||||
{" "}
|
||||
{`${t("Privacy Policy")} `}
|
||||
</Link>
|
||||
 {t("familiarized")}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Button
|
||||
disabled={!(ready && !fire && isPhoneValid && (isEmailFieldVisible ? isEmailValid : true))}
|
||||
variant="contained"
|
||||
onClick={handleShowResultsClick}
|
||||
sx={{
|
||||
border: `1px solid ${theme.palette.primary.main}`,
|
||||
margin: isMobile ? "auto" : undefined,
|
||||
mt: "20px",
|
||||
p: "10px 20px",
|
||||
"&:disabled": {
|
||||
border: "1px solid #9A9AAF",
|
||||
color: "#9A9AAF",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{settings.cfg.formContact?.button || t("Get results")}
|
||||
</Button>
|
||||
</Box>
|
||||
{show_badge && (
|
||||
<Box
|
||||
component={Link}
|
||||
target={"_blank"}
|
||||
href={`https://${isProduction ? "" : "s"}quiz.pena.digital/answer/v1.0.0/logo?q=${quizId}`}
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
mt: "55px",
|
||||
mb: isMobile ? "30px" : isTablet ? "40px" : "50px",
|
||||
gap: "10px",
|
||||
textDecoration: "none",
|
||||
margitTop: "auto",
|
||||
}}
|
||||
>
|
||||
{quizThemes[settings.cfg.theme].isLight ? <NameplateLogoDark /> : <NameplateLogo />}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
@ -0,0 +1,67 @@
|
||||
import { Box, Typography, useTheme } from "@mui/material";
|
||||
import { useRootContainerSize } from "@contexts/RootContainerWidthContext.ts";
|
||||
import { QuizSettingsConfig } from "@model/settingsData.ts";
|
||||
import { FC } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type ContactTextBlockProps = {
|
||||
settings: QuizSettingsConfig;
|
||||
};
|
||||
|
||||
export const ContactTextBlock: FC<ContactTextBlockProps> = ({ settings }) => {
|
||||
const theme = useTheme();
|
||||
const isMobile = useRootContainerSize() < 850;
|
||||
const isTablet = useRootContainerSize() < 1000;
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
flexGrow: isMobile ? 0 : 1,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderRight: isMobile ? undefined : "1px solid #9A9AAF80",
|
||||
margin: isMobile ? 0 : "40px 0",
|
||||
padding: isMobile ? "0" : "0 40px",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
maxWidth: isMobile ? "100%" : isTablet ? "410px" : "630px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "flex-start",
|
||||
justifyContent: "center",
|
||||
padding: isMobile ? "40px 20px 0 20px" : "0",
|
||||
mt: isMobile ? 0 : isTablet ? "-180px" : "-47px",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
textAlign: isTablet ? undefined : "center",
|
||||
fontSize: "24px",
|
||||
lineHeight: "normal",
|
||||
fontWeight: 501,
|
||||
color: theme.palette.text.primary,
|
||||
wordBreak: "break-word",
|
||||
}}
|
||||
>
|
||||
{settings.cfg.formContact.title || t("Fill out the form to receive your test results")}
|
||||
</Typography>
|
||||
{settings.cfg.formContact.desc && (
|
||||
<Typography
|
||||
sx={{
|
||||
color: theme.palette.text.primary,
|
||||
m: "20px 0",
|
||||
fontSize: "18px",
|
||||
wordBreak: "break-word",
|
||||
}}
|
||||
>
|
||||
{settings.cfg.formContact.desc}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
66
lib/components/ViewPublicationPage/ContactForm/CustomInput/CountrySelector/CountrySelector.tsx
Normal file
66
lib/components/ViewPublicationPage/ContactForm/CustomInput/CountrySelector/CountrySelector.tsx
Normal file
@ -0,0 +1,66 @@
|
||||
import { MenuItem, Select, SelectChangeEvent, useTheme } from "@mui/material";
|
||||
import { Dispatch, FC, SetStateAction, useState } from "react";
|
||||
import { phoneMasksByCountry } from "@utils/phoneMasksByCountry.tsx";
|
||||
import { Value } from "react-phone-number-input";
|
||||
|
||||
type CountrySelectorProps = {
|
||||
setMask: Dispatch<SetStateAction<string>>;
|
||||
};
|
||||
|
||||
export const CountrySelector: FC<CountrySelectorProps> = ({ setMask }) => {
|
||||
const theme = useTheme();
|
||||
const [country, setCountry] = useState("RU");
|
||||
|
||||
const handleChange = (e: SelectChangeEvent<Value>) => {
|
||||
setCountry(e.target.value);
|
||||
setMask(phoneMasksByCountry[e.target.value][1]);
|
||||
};
|
||||
return (
|
||||
<Select
|
||||
//@ts-ignore
|
||||
value={country}
|
||||
onChange={handleChange}
|
||||
renderValue={(value) => value}
|
||||
// autoComplete={true}
|
||||
MenuProps={{
|
||||
PaperProps: {
|
||||
style: {
|
||||
backgroundColor: theme.palette.background.default,
|
||||
borderRadius: "12px",
|
||||
scrollbarWidth: "none",
|
||||
},
|
||||
},
|
||||
}}
|
||||
sx={{
|
||||
minWidth: 50,
|
||||
backgroundColor: theme.palette.background.default,
|
||||
"& .MuiSelect-select": {
|
||||
paddingLeft: "5px",
|
||||
paddingRight: "5px",
|
||||
color: "gray",
|
||||
fontSize: "12px",
|
||||
border: "none",
|
||||
},
|
||||
"& .MuiOutlinedInput-notchedOutline": {
|
||||
border: "none",
|
||||
},
|
||||
"&:hover .MuiOutlinedInput-notchedOutline": {
|
||||
border: "none",
|
||||
},
|
||||
"&:hover:before": {
|
||||
border: "none",
|
||||
},
|
||||
"&.Mui-focused .MuiOutlinedInput-notchedOutline": {
|
||||
border: "none",
|
||||
},
|
||||
"&.Mui-focused:hover .MuiOutlinedInput-notchedOutline": {
|
||||
border: "none",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{Object.keys(phoneMasksByCountry).map((countryCode) => {
|
||||
return <MenuItem value={countryCode}>{phoneMasksByCountry[countryCode][0]}</MenuItem>;
|
||||
})}
|
||||
</Select>
|
||||
);
|
||||
};
|
@ -0,0 +1,115 @@
|
||||
import { Box, InputAdornment, TextField as MuiTextField, TextFieldProps, Typography, useTheme } from "@mui/material";
|
||||
import { useRootContainerSize } from "@contexts/RootContainerWidthContext.ts";
|
||||
import { useIMask, IMask } from "react-imask";
|
||||
import { quizThemes } from "@utils/themes/Publication/themePublication.ts";
|
||||
import { ChangeEvent, FC, HTMLInputTypeAttribute, useEffect, useState } from "react";
|
||||
import { CountrySelector } from "@/components/ViewPublicationPage/ContactForm/CustomInput/CountrySelector/CountrySelector.tsx";
|
||||
import { phoneMasksByCountry } from "@utils/phoneMasksByCountry.tsx";
|
||||
import { useQuizStore } from "@/stores/useQuizStore";
|
||||
|
||||
type InputProps = {
|
||||
title: string;
|
||||
desc: string;
|
||||
Icon: FC<{ color: string; backgroundColor: string }>;
|
||||
onChange: TextFieldProps["onChange"];
|
||||
onChangePhone?: (phone: string) => void;
|
||||
id: string;
|
||||
isPhone?: boolean;
|
||||
type?: HTMLInputTypeAttribute;
|
||||
value?: string;
|
||||
onBlur?: () => void;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
const TextField = MuiTextField as unknown as FC<TextFieldProps>;
|
||||
let first = true;
|
||||
|
||||
function phoneChange(e: ChangeEvent<HTMLInputElement>, mask: string) {
|
||||
const masked = IMask.createMask({
|
||||
mask: "+7 (000) 000-00-00",
|
||||
// ...and other options
|
||||
});
|
||||
masked.value = e.target.value;
|
||||
const a = IMask.pipe(e.target.value, {
|
||||
mask,
|
||||
});
|
||||
return a || "";
|
||||
}
|
||||
|
||||
export const CustomInput = ({
|
||||
title,
|
||||
desc,
|
||||
Icon,
|
||||
onChange,
|
||||
onChangePhone,
|
||||
isPhone,
|
||||
type,
|
||||
value,
|
||||
onBlur,
|
||||
error,
|
||||
}: InputProps) => {
|
||||
const theme = useTheme();
|
||||
const isMobile = useRootContainerSize() < 600;
|
||||
const { settings } = useQuizStore();
|
||||
const [mask, setMask] = useState(phoneMasksByCountry["RU"][1]);
|
||||
// const { ref } = useIMask({ mask });
|
||||
|
||||
return (
|
||||
<Box m="10px 0">
|
||||
<Typography
|
||||
mb="7px"
|
||||
color={theme.palette.text.primary}
|
||||
fontSize={"16px"}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
|
||||
<TextField
|
||||
// inputRef={isPhone ? ref : null}
|
||||
//@ts-ignore
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||
isPhone ? onChangePhone?.(phoneChange(e, mask)) : onChange?.(e)
|
||||
}
|
||||
onBlur={onBlur}
|
||||
type={isPhone ? "tel" : type}
|
||||
value={value}
|
||||
error={!!error}
|
||||
helperText={error}
|
||||
sx={{
|
||||
width: isMobile ? "100%" : "390px",
|
||||
backgroundColor: theme.palette.background.default,
|
||||
fontSize: "16px",
|
||||
"& .MuiOutlinedInput-notchedOutline": {
|
||||
borderColor: "#9A9AAF80",
|
||||
borderRadius: "12px",
|
||||
},
|
||||
"& .MuiInputBase-root": {
|
||||
paddingLeft: 0,
|
||||
},
|
||||
"& .MuiOutlinedInput-input": {
|
||||
paddingLeft: "10px",
|
||||
},
|
||||
"& .MuiOutlinedInput-root": {
|
||||
"&:hover fieldset": {
|
||||
borderColor: theme.palette.primary.main,
|
||||
},
|
||||
},
|
||||
}}
|
||||
placeholder={desc}
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<Icon
|
||||
color="gray"
|
||||
backgroundColor={quizThemes[settings.cfg.theme].isLight ? "#F2F3F7" : "#F2F3F71A"}
|
||||
/>
|
||||
</InputAdornment>
|
||||
),
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">{isPhone && <CountrySelector setMask={setMask} />}</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
138
lib/components/ViewPublicationPage/ContactForm/Inputs/Inputs.tsx
Normal file
138
lib/components/ViewPublicationPage/ContactForm/Inputs/Inputs.tsx
Normal file
@ -0,0 +1,138 @@
|
||||
import NameIcon from "@icons/ContactFormIcon/NameIcon.tsx";
|
||||
import EmailIcon from "@icons/ContactFormIcon/EmailIcon.tsx";
|
||||
import TextIcon from "@icons/ContactFormIcon/TextIcon.tsx";
|
||||
import AddressIcon from "@icons/ContactFormIcon/AddressIcon.tsx";
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
import { CustomInput } from "@/components/ViewPublicationPage/ContactForm/CustomInput/CustomInput.tsx";
|
||||
import PhoneIcon from "@icons/ContactFormIcon/PhoneIcon.tsx";
|
||||
import PhoneInput from "react-phone-number-input";
|
||||
import { useQuizStore } from "@/stores/useQuizStore";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type InputsProps = {
|
||||
name: string;
|
||||
setName: Dispatch<SetStateAction<string>>;
|
||||
email: string;
|
||||
setEmail: (email: string) => void;
|
||||
phone: string;
|
||||
setPhone: (phone: string) => void;
|
||||
text: string;
|
||||
setText: Dispatch<SetStateAction<string>>;
|
||||
adress: string;
|
||||
setAdress: Dispatch<SetStateAction<string>>;
|
||||
emailError?: string;
|
||||
phoneError?: string;
|
||||
onEmailBlur?: () => void;
|
||||
onPhoneBlur?: () => void;
|
||||
crutch: {
|
||||
disableEmail: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
const iscrutch = "/cc006b40-ccbd-4600-a1d3-f902f85aa0a0";
|
||||
const pathOnly = window.location.pathname;
|
||||
|
||||
export const Inputs = ({
|
||||
name,
|
||||
setName,
|
||||
email,
|
||||
setEmail,
|
||||
phone,
|
||||
setPhone,
|
||||
text,
|
||||
setText,
|
||||
adress,
|
||||
setAdress,
|
||||
emailError,
|
||||
phoneError,
|
||||
onEmailBlur,
|
||||
onPhoneBlur,
|
||||
crutch,
|
||||
}: InputsProps) => {
|
||||
const { settings } = useQuizStore();
|
||||
const { t } = useTranslation();
|
||||
const FC = settings.cfg.formContact.fields;
|
||||
|
||||
if (!FC) return null;
|
||||
const Name = (
|
||||
<CustomInput
|
||||
onChange={({ target }) => setName(target.value)}
|
||||
id={name}
|
||||
title={
|
||||
pathOnly === iscrutch
|
||||
? "Введите имя и фамилию"
|
||||
: FC["name"].innerText || `${t("Enter")} ${t("Name").toLowerCase()}`
|
||||
}
|
||||
desc={FC["name"].text || t("Name")}
|
||||
Icon={NameIcon}
|
||||
/>
|
||||
);
|
||||
const Email = (
|
||||
<CustomInput
|
||||
onChange={({ target }) => {
|
||||
setEmail(target.value.replaceAll(/\s/g, ""));
|
||||
}}
|
||||
onBlur={onEmailBlur}
|
||||
id={email}
|
||||
title={FC["email"].innerText || `${t("Enter")} Email`}
|
||||
desc={FC["email"].text || "Email"}
|
||||
Icon={EmailIcon}
|
||||
type="email"
|
||||
error={emailError}
|
||||
/>
|
||||
);
|
||||
const Phone = (
|
||||
<CustomInput
|
||||
onChange={({ target }) => setText(target.value)}
|
||||
onChangePhone={(phone: string) => {
|
||||
setPhone(phone);
|
||||
}}
|
||||
onBlur={onPhoneBlur}
|
||||
value={phone}
|
||||
id={phone}
|
||||
title={FC["phone"].innerText || `${t("Enter")} ${t("Phone number").toLowerCase()}`}
|
||||
desc={FC["phone"].text || t("Phone number")}
|
||||
Icon={PhoneIcon}
|
||||
isPhone={true}
|
||||
error={phoneError}
|
||||
/>
|
||||
);
|
||||
const Text = (
|
||||
<CustomInput
|
||||
onChange={({ target }) => setText(target.value)}
|
||||
id={text}
|
||||
title={FC["text"].text || `${t("Enter")} ${t("Last name").toLowerCase()}`}
|
||||
desc={FC["text"].innerText || t("Last name")}
|
||||
Icon={TextIcon}
|
||||
/>
|
||||
);
|
||||
const Adress = (
|
||||
<CustomInput
|
||||
onChange={({ target }) => setAdress(target.value)}
|
||||
id={adress}
|
||||
title={FC["address"].innerText || `${t("Enter")} ${t("Address").toLowerCase()}`}
|
||||
desc={FC["address"].text || t("Address")}
|
||||
Icon={AddressIcon}
|
||||
/>
|
||||
);
|
||||
|
||||
if (Object.values(FC).some((data) => data.used)) {
|
||||
return (
|
||||
<>
|
||||
{FC["name"].used ? Name : <></>}
|
||||
{FC["email"].used && !crutch.disableEmail ? Email : <></>}
|
||||
{FC["phone"].used ? Phone : <></>}
|
||||
{FC["text"].used ? Text : <></>}
|
||||
{FC["address"].used ? Adress : <></>}
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
{Name}
|
||||
{Email}
|
||||
{Phone}
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
@ -15,7 +15,7 @@ export const Footer = ({ stepNumber, nextButton, prevButton }: FooterProps) => {
|
||||
const theme = useTheme();
|
||||
const { questions, settings } = useQuizStore();
|
||||
const questionsAmount = questions.filter(({ type }) => type !== "result").length;
|
||||
const { t } = useTranslation();
|
||||
const { t, i18n } = useTranslation();
|
||||
|
||||
return (
|
||||
<Box
|
||||
|
166
lib/components/ViewPublicationPage/PointSystemResultList.tsx
Normal file
166
lib/components/ViewPublicationPage/PointSystemResultList.tsx
Normal file
@ -0,0 +1,166 @@
|
||||
import { IncorrectAnswer } from "@/assets/icons/IncorrectAnswer";
|
||||
import { CorrectAnswer } from "@/assets/icons/CorrectAnswer";
|
||||
import { Box, Typography, useTheme } from "@mui/material";
|
||||
import { useQuizViewStore } from "@/stores/quizView";
|
||||
import { AnyTypedQuizQuestion, QuizQuestionVariant } from "@/index";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useQuizStore } from "@/stores/useQuizStore";
|
||||
|
||||
const dinocrutch = window.location.pathname === "/413b9e24-996a-400e-9076-c158f64b9bd7";
|
||||
|
||||
// Функция для определения вопроса "спасибо"
|
||||
const isThankYouQuestion = (question: QuizQuestionVariant): boolean => {
|
||||
// Проверяем что у вопроса только один вариант ответа
|
||||
if (question.content.variants.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Проверяем что текст варианта полностью состоит из слова "спасибо"
|
||||
const variant = question.content.variants[0];
|
||||
const answerText = variant.answer.toLowerCase().trim();
|
||||
|
||||
// Проверяем точное совпадение со словом "спасибо"
|
||||
return answerText === "спасибо";
|
||||
};
|
||||
|
||||
export const PointSystemResultList = () => {
|
||||
const theme = useTheme();
|
||||
const { questions } = useQuizStore();
|
||||
const answers = useQuizViewStore((state) => state.answers);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const questionsWothoutResult = questions.filter<QuizQuestionVariant>(
|
||||
(q: AnyTypedQuizQuestion): q is QuizQuestionVariant => q.type === "variant"
|
||||
);
|
||||
|
||||
// Фильтруем вопросы "спасибо" только для указанного квиза
|
||||
const filteredQuestions = dinocrutch
|
||||
? questionsWothoutResult.filter((q) => !isThankYouQuestion(q))
|
||||
: questionsWothoutResult;
|
||||
|
||||
return filteredQuestions.map((currentQuestion, index) => {
|
||||
let answerIndex = 0;
|
||||
let currentVariants = currentQuestion.content.variants;
|
||||
|
||||
const currentAnswer = answers.find((a) => a.questionId === currentQuestion.id);
|
||||
const answeredVariant = currentVariants.find((v, i) => {
|
||||
if (v.id === currentAnswer?.answer) {
|
||||
answerIndex = i;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "inline-flex",
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "inline-flex",
|
||||
gap: "16px",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
color: theme.palette.grey[500],
|
||||
}}
|
||||
>
|
||||
{index + 1}.
|
||||
</Typography>
|
||||
<Typography
|
||||
sx={{
|
||||
color: theme.palette.text.primary,
|
||||
}}
|
||||
>
|
||||
{currentQuestion.title || t("Question without a title")}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography
|
||||
sx={{
|
||||
color: answeredVariant?.points ? theme.palette.primary.main : theme.palette.grey[500],
|
||||
}}
|
||||
>
|
||||
{answeredVariant?.points || "0"}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: "inline-flex",
|
||||
mt: "15px",
|
||||
gap: "10px",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
color: theme.palette.grey[500],
|
||||
}}
|
||||
>
|
||||
{t("Your answer")}:
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<Line
|
||||
checkTrue={Boolean(answeredVariant?.points)}
|
||||
text={answeredVariant?.answer}
|
||||
/>
|
||||
{/* {Boolean(answeredVariant?.points) ? <CorrectAnswer /> : <IncorrectAnswer />}
|
||||
<Typography>{answeredVariant?.answer || "не выбрано"}</Typography> */}
|
||||
{currentVariants.map((v) => {
|
||||
if (v.id === currentAnswer?.answer) {
|
||||
return <></>;
|
||||
} else
|
||||
return (
|
||||
<Line
|
||||
checkTrue={Boolean(v?.points)}
|
||||
text={v.answer}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
interface LineProps {
|
||||
checkTrue: boolean;
|
||||
text?: string;
|
||||
}
|
||||
|
||||
const Line = ({ checkTrue, text }: LineProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "inline-flex",
|
||||
gap: "10px",
|
||||
mb: "10px",
|
||||
}}
|
||||
>
|
||||
{checkTrue ? <CorrectAnswer /> : <IncorrectAnswer />}
|
||||
<Typography
|
||||
sx={{
|
||||
color: theme.palette.grey[500],
|
||||
}}
|
||||
>
|
||||
{text || "не выбрано"}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
};
|
@ -1,12 +1,23 @@
|
||||
import { Box, Link, useTheme } from "@mui/material";
|
||||
|
||||
import { Footer } from "./Footer";
|
||||
import { Date } from "./questions/Date";
|
||||
import { Emoji } from "./questions/Emoji";
|
||||
import { File } from "./questions/File";
|
||||
import { Images } from "./questions/Images";
|
||||
import { Number } from "./questions/Number";
|
||||
import { Page } from "./questions/Page";
|
||||
import { Rating } from "./questions/Rating";
|
||||
import { Select } from "./questions/Select";
|
||||
import { Text } from "./questions/Text";
|
||||
import { Variant } from "./questions/Variant";
|
||||
import { Varimg } from "./questions/Varimg";
|
||||
|
||||
import type { RealTypedQuizQuestion } from "../../model/questionTypes/shared";
|
||||
|
||||
import { NameplateLogoFQ } from "@icons/NameplateLogoFQ";
|
||||
import { NameplateLogoFQDark } from "@icons/NameplateLogoFQDark";
|
||||
import { notReachable } from "@utils/notReachable";
|
||||
import { quizThemes } from "@utils/themes/Publication/themePublication";
|
||||
|
||||
import { DESIGN_LIST } from "@/utils/designList";
|
||||
@ -77,8 +88,9 @@ export const Question = ({
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
currentQuestion={currentQuestion}
|
||||
<QuestionByType
|
||||
key={currentQuestion.id}
|
||||
question={currentQuestion}
|
||||
stepNumber={currentQuestionStepNumber}
|
||||
/>
|
||||
{show_badge && (
|
||||
@ -121,3 +133,37 @@ export const Question = ({
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
function QuestionByType({ question, stepNumber }: { question: RealTypedQuizQuestion; stepNumber: number | null }) {
|
||||
switch (question.type) {
|
||||
case "variant":
|
||||
return <Variant currentQuestion={question} />;
|
||||
case "images":
|
||||
return <Images currentQuestion={question} />;
|
||||
case "varimg":
|
||||
return <Varimg currentQuestion={question} />;
|
||||
case "emoji":
|
||||
return <Emoji currentQuestion={question} />;
|
||||
case "text":
|
||||
return (
|
||||
<Text
|
||||
currentQuestion={question}
|
||||
stepNumber={stepNumber}
|
||||
/>
|
||||
);
|
||||
case "select":
|
||||
return <Select currentQuestion={question} />;
|
||||
case "date":
|
||||
return <Date currentQuestion={question} />;
|
||||
case "number":
|
||||
return <Number currentQuestion={question} />;
|
||||
case "file":
|
||||
return <File currentQuestion={question} />;
|
||||
case "page":
|
||||
return <Page currentQuestion={question} />;
|
||||
case "rating":
|
||||
return <Rating currentQuestion={question} />;
|
||||
default:
|
||||
notReachable(question);
|
||||
}
|
||||
}
|
||||
|
@ -12,17 +12,36 @@ import { quizThemes } from "@utils/themes/Publication/themePublication";
|
||||
import { NameplateLogo } from "@icons/NameplateLogo";
|
||||
|
||||
import type { QuizQuestionResult } from "@/model/questionTypes/result";
|
||||
import type { QuizQuestionVariant } from "@/model/questionTypes/variant";
|
||||
import QuizVideo from "@/ui_kit/VideoIframe/VideoIframe";
|
||||
import { TextAccordion } from "./tools/TextAccordion";
|
||||
import { PointSystemResultList } from "./PointSystemResultList";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
import { sendFC, sendResult } from "@/api/quizRelase";
|
||||
import { isProduction } from "@/utils/defineDomain";
|
||||
import { useQuizStore } from "@/stores/useQuizStore";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { NameplateLogoDark } from "@/assets/icons/NameplateLogoDark";
|
||||
|
||||
type ResultFormProps = {
|
||||
resultQuestion: QuizQuestionResult;
|
||||
};
|
||||
|
||||
// Функция для определения вопроса "спасибо"
|
||||
const isThankYouQuestion = (question: QuizQuestionVariant): boolean => {
|
||||
// Проверяем что у вопроса только один вариант ответа
|
||||
if (question.content.variants.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Проверяем что текст варианта полностью состоит из слова "спасибо"
|
||||
const variant = question.content.variants[0];
|
||||
const answerText = variant.answer.toLowerCase().trim();
|
||||
|
||||
// Проверяем точное совпадение со словом "спасибо"
|
||||
return answerText === "спасибо";
|
||||
};
|
||||
|
||||
export const ResultForm = ({ resultQuestion }: ResultFormProps) => {
|
||||
const theme = useTheme();
|
||||
const isMobile = useRootContainerSize() < 650;
|
||||
@ -36,6 +55,22 @@ export const ResultForm = ({ resultQuestion }: ResultFormProps) => {
|
||||
const yandexMetrics = useYandexMetricsGoals(settings.cfg.yandexMetricsNumber);
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Проверяем, является ли это квизом с костылем
|
||||
const dinocrutch = window.location.pathname === "/413b9e24-996a-400e-9076-c158f64b9bd7";
|
||||
|
||||
// Вычисляем общее количество вопросов с учетом фильтрации
|
||||
const totalQuestions = useMemo(() => {
|
||||
if (dinocrutch) {
|
||||
// Для квиза с костылем: исключаем вопросы "спасибо" и вопросы типа "result"
|
||||
const variantQuestions = questions.filter((e) => e.type === "variant") as QuizQuestionVariant[];
|
||||
const filteredQuestions = variantQuestions.filter((q) => !isThankYouQuestion(q));
|
||||
return filteredQuestions.length;
|
||||
}
|
||||
|
||||
// Для обычных квизов: исключаем только вопросы типа "result"
|
||||
return questions.filter((e) => e.type !== "result").length;
|
||||
}, [questions, dinocrutch]);
|
||||
|
||||
useEffect(() => {
|
||||
vkMetrics.resultIdShown(resultQuestion.id);
|
||||
yandexMetrics.resultIdShown(resultQuestion.id);
|
||||
@ -238,6 +273,55 @@ export const ResultForm = ({ resultQuestion }: ResultFormProps) => {
|
||||
{resultQuestion.content.text}
|
||||
</Typography>
|
||||
)}
|
||||
{settings.cfg?.score && (
|
||||
<>
|
||||
<Typography
|
||||
sx={{
|
||||
color: theme.palette.primary.main,
|
||||
fontSize: "30px",
|
||||
m: "30px 0",
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
{t("Your points")}
|
||||
</Typography>
|
||||
<Typography
|
||||
sx={{
|
||||
color: theme.palette.primary.main,
|
||||
fontSize: "30px",
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
{pointsSum} {t("of")} {totalQuestions}
|
||||
</Typography>
|
||||
<TextAccordion
|
||||
headerText={
|
||||
<Typography
|
||||
sx={{
|
||||
color: theme.palette.primary.main,
|
||||
"&:hover": {
|
||||
color: theme.palette.primary.dark,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{t("View answers")}
|
||||
</Typography>
|
||||
}
|
||||
sx={{
|
||||
mt: "60px",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
mt: "25px",
|
||||
}}
|
||||
>
|
||||
<PointSystemResultList />
|
||||
</Box>
|
||||
</TextAccordion>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
{show_badge && (
|
||||
@ -257,12 +341,7 @@ export const ResultForm = ({ resultQuestion }: ResultFormProps) => {
|
||||
bottom: "90px",
|
||||
}}
|
||||
>
|
||||
<NameplateLogo
|
||||
style={{
|
||||
fontSize: "23px",
|
||||
color: quizThemes[settings.cfg.theme].isLight ? "#000000" : "#F5F7FF",
|
||||
}}
|
||||
/>
|
||||
{quizThemes[settings.cfg.theme].isLight ? <NameplateLogoDark /> : <NameplateLogo />}
|
||||
</Box>
|
||||
)}
|
||||
<Box
|
||||
|
41
lib/components/ViewPublicationPage/StartPageViewPublication/QuizPreviewLayoutByType.tsx
Normal file
41
lib/components/ViewPublicationPage/StartPageViewPublication/QuizPreviewLayoutByType.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import { StartPageDesktop } from "./StartPageDesktop";
|
||||
import { StartPageMobile } from "./StartPageMobile";
|
||||
|
||||
import { useRootContainerSize } from "@contexts/RootContainerWidthContext";
|
||||
|
||||
import type { QuizStartpageAlignType, QuizStartpageType } from "@model/settingsData";
|
||||
|
||||
type QuizPreviewLayoutByTypeProps = {
|
||||
quizHeaderBlock: JSX.Element;
|
||||
quizMainBlock: JSX.Element;
|
||||
backgroundBlock: JSX.Element | null;
|
||||
startpageType: QuizStartpageType;
|
||||
alignType: QuizStartpageAlignType;
|
||||
};
|
||||
|
||||
export const QuizPreviewLayoutByType = ({
|
||||
quizHeaderBlock,
|
||||
quizMainBlock,
|
||||
backgroundBlock,
|
||||
startpageType,
|
||||
alignType,
|
||||
}: QuizPreviewLayoutByTypeProps) => {
|
||||
const isMobile = useRootContainerSize() < 700;
|
||||
|
||||
return isMobile ? (
|
||||
<StartPageMobile
|
||||
quizHeaderBlock={quizHeaderBlock}
|
||||
quizMainBlock={quizMainBlock}
|
||||
backgroundBlock={backgroundBlock}
|
||||
startpageType={startpageType}
|
||||
/>
|
||||
) : (
|
||||
<StartPageDesktop
|
||||
alignType={alignType}
|
||||
startpageType={startpageType}
|
||||
quizHeaderBlock={quizHeaderBlock}
|
||||
quizMainBlock={quizMainBlock}
|
||||
backgroundBlock={backgroundBlock}
|
||||
/>
|
||||
);
|
||||
};
|
@ -0,0 +1,263 @@
|
||||
import { Box } from "@mui/material";
|
||||
|
||||
import { useRootContainerSize } from "@contexts/RootContainerWidthContext";
|
||||
import { useQuizStore } from "@/stores/useQuizStore";
|
||||
|
||||
import { notReachable } from "@utils/notReachable";
|
||||
import { quizThemes } from "@utils/themes/Publication/themePublication";
|
||||
|
||||
import type { QuizStartpageAlignType, QuizStartpageType } from "@model/settingsData";
|
||||
import { DESIGN_LIST } from "@/utils/designList";
|
||||
|
||||
type StartPageDesktopProps = {
|
||||
quizHeaderBlock: JSX.Element;
|
||||
quizMainBlock: JSX.Element;
|
||||
backgroundBlock: JSX.Element | null;
|
||||
startpageType: QuizStartpageType;
|
||||
alignType: QuizStartpageAlignType;
|
||||
};
|
||||
|
||||
type LayoutProps = Omit<StartPageDesktopProps, "startpageType">;
|
||||
|
||||
const StandartLayout = ({ alignType, quizHeaderBlock, quizMainBlock, backgroundBlock }: LayoutProps) => {
|
||||
const size = useRootContainerSize();
|
||||
const isTablet = size >= 700 && size < 1100;
|
||||
const { settings } = useQuizStore();
|
||||
|
||||
return (
|
||||
<Box
|
||||
id="pain"
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: alignType === "left" ? "row" : "row-reverse",
|
||||
height: "100%",
|
||||
backgroundPosition: "center",
|
||||
backgroundSize: "cover",
|
||||
backgroundImage: settings.cfg.design ? `url(${DESIGN_LIST[settings.cfg.theme]})` : null,
|
||||
scrollbarWidth: "none",
|
||||
"&::-webkit-scrollbar": {
|
||||
width: 0,
|
||||
},
|
||||
overflowY: "auto",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: alignType === "left" ? "row" : "row-reverse",
|
||||
padding: isTablet ? "15px" : "0",
|
||||
width: "100%",
|
||||
background:
|
||||
settings.cfg.design && !quizThemes[settings.cfg.theme].isLight
|
||||
? alignType === "left"
|
||||
? "linear-gradient(90deg, #272626, transparent)"
|
||||
: alignType === "right"
|
||||
? "linear-gradient(-90deg, #272626, transparent)"
|
||||
: "linear-gradient(0deg, #272626, transparent)"
|
||||
: null,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: settings.cfg.startpage.background.desktop ? "40%" : undefined,
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "flex-start",
|
||||
p: isTablet ? "25px" : alignType === "left" ? "25px 25px 25px 35px" : "25px 35px 25px 25px",
|
||||
overflowY: "auto",
|
||||
scrollbarWidth: "none",
|
||||
"&::-webkit-scrollbar": {
|
||||
width: 0,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{quizHeaderBlock}
|
||||
{quizMainBlock}
|
||||
</Box>
|
||||
{settings.cfg.startpage.background.desktop && (
|
||||
<Box sx={{ width: "60%", overflow: "hidden" }}>
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
padding: alignType === "left" ? "25px 25px 25px 15px" : "25px 15px 25px 25px",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
"& > img": { width: "100%", borderRadius: "12px" },
|
||||
}}
|
||||
onClick={(event) => event.preventDefault()}
|
||||
>
|
||||
{backgroundBlock}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const ExpandedLayout = ({ alignType, quizHeaderBlock, quizMainBlock, backgroundBlock }: LayoutProps) => {
|
||||
const size = useRootContainerSize();
|
||||
const isTablet = size >= 700 && size < 1100;
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
sx={{
|
||||
height: "100%",
|
||||
width: alignType === "center" ? "100%" : isTablet ? "46%" : "42%",
|
||||
display: "flex",
|
||||
padding:
|
||||
alignType === "center"
|
||||
? isTablet
|
||||
? "30px 40px"
|
||||
: "30px 35px"
|
||||
: alignType === "left"
|
||||
? isTablet
|
||||
? "25px 0 31px 40px"
|
||||
: "25px 0 31px 35px"
|
||||
: isTablet
|
||||
? "25px 40px 31px 0"
|
||||
: "25px 35px 31px 0",
|
||||
margin: alignType === "center" ? "0 auto" : alignType === "left" ? "0" : "0 0 0 auto",
|
||||
scrollbarWidth: "none",
|
||||
"&::-webkit-scrollbar": {
|
||||
width: 0,
|
||||
},
|
||||
overflowY: "auto",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
minHeight: "calc(100% - 32px)",
|
||||
position: "relative",
|
||||
width: "100%",
|
||||
|
||||
padding: alignType === "center" ? "0" : alignType === "left" ? "0 40px 0 0" : "0 0 0 40px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-between",
|
||||
alignItems: alignType === "center" ? "center" : "start",
|
||||
borderRight: alignType === "left" ? "1px solid #9A9AAF80" : null,
|
||||
borderLeft: alignType === "right" ? "1px solid #9A9AAF80" : null,
|
||||
scrollbarWidth: "none",
|
||||
"&::-webkit-scrollbar": {
|
||||
width: 0,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{alignType !== "center" && quizHeaderBlock}
|
||||
{quizMainBlock}
|
||||
</Box>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
zIndex: -1,
|
||||
left: 0,
|
||||
top: 0,
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{backgroundBlock}
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const CenteredLayout = ({ quizHeaderBlock, quizMainBlock, backgroundBlock }: LayoutProps) => {
|
||||
const isTablet = useRootContainerSize() < 1100;
|
||||
const { settings } = useQuizStore();
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
overflow: "auto",
|
||||
padding: isTablet ? "25px 40px 40px" : "25px 25px 25px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
height: "100%",
|
||||
backgroundPosition: "center",
|
||||
backgroundSize: "cover",
|
||||
backgroundImage: !settings.cfg.design
|
||||
? null
|
||||
: settings.cfg.design && !quizThemes[settings.cfg.theme].isLight
|
||||
? `linear-gradient(0deg, #272626, transparent), url(${DESIGN_LIST[settings.cfg.theme]})`
|
||||
: `url(${DESIGN_LIST[settings.cfg.theme]})`,
|
||||
scrollbarWidth: "none",
|
||||
"&::-webkit-scrollbar": {
|
||||
width: 0,
|
||||
},
|
||||
overflowY: "auto",
|
||||
}}
|
||||
>
|
||||
{quizHeaderBlock}
|
||||
{backgroundBlock && settings.cfg.startpage.background.desktop && (
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
maxWidth: "844px",
|
||||
height: isTablet ? "530px" : "306px",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
"& > img": { width: "100%", borderRadius: "12px" },
|
||||
}}
|
||||
onClick={(event) => event.preventDefault()}
|
||||
>
|
||||
{backgroundBlock}
|
||||
</Box>
|
||||
)}
|
||||
{quizMainBlock}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export const StartPageDesktop = ({
|
||||
quizHeaderBlock,
|
||||
quizMainBlock,
|
||||
backgroundBlock,
|
||||
startpageType,
|
||||
alignType,
|
||||
}: StartPageDesktopProps) => {
|
||||
switch (startpageType) {
|
||||
case null:
|
||||
case "standard": {
|
||||
return (
|
||||
<StandartLayout
|
||||
alignType={alignType}
|
||||
quizHeaderBlock={quizHeaderBlock}
|
||||
quizMainBlock={quizMainBlock}
|
||||
backgroundBlock={backgroundBlock}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
case "expanded": {
|
||||
return (
|
||||
<ExpandedLayout
|
||||
alignType={alignType}
|
||||
quizHeaderBlock={quizHeaderBlock}
|
||||
quizMainBlock={quizMainBlock}
|
||||
backgroundBlock={backgroundBlock}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
case "centered": {
|
||||
return (
|
||||
<CenteredLayout
|
||||
alignType={alignType}
|
||||
quizHeaderBlock={quizHeaderBlock}
|
||||
quizMainBlock={quizMainBlock}
|
||||
backgroundBlock={backgroundBlock}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
default:
|
||||
notReachable(startpageType);
|
||||
}
|
||||
};
|
@ -0,0 +1,273 @@
|
||||
import { Box } from "@mui/material";
|
||||
|
||||
import { useQuizStore } from "@/stores/useQuizStore";
|
||||
|
||||
import { notReachable } from "@utils/notReachable";
|
||||
import { quizThemes } from "@utils/themes/Publication/themePublication";
|
||||
|
||||
import type { QuizStartpageType } from "@model/settingsData";
|
||||
import { DESIGN_LIST } from "@/utils/designList";
|
||||
|
||||
type StartPageMobileProps = {
|
||||
quizHeaderBlock: JSX.Element;
|
||||
quizMainBlock: JSX.Element;
|
||||
backgroundBlock: JSX.Element | null;
|
||||
startpageType: QuizStartpageType;
|
||||
};
|
||||
|
||||
type MobileLayoutProps = Omit<StartPageMobileProps, "startpageType">;
|
||||
|
||||
const StandartMobileLayout = ({ quizHeaderBlock, quizMainBlock, backgroundBlock }: MobileLayoutProps) => {
|
||||
const { settings } = useQuizStore();
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
flexGrow: 1,
|
||||
justifyContent: "flex-end",
|
||||
minHeight: "100%",
|
||||
height: "100%",
|
||||
"&::-webkit-scrollbar": { width: 0 },
|
||||
backgroundPosition: "center",
|
||||
backgroundSize: "cover",
|
||||
backgroundImage: settings.cfg.design ? `url(${DESIGN_LIST[settings.cfg.theme]})` : null,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
flexGrow: 1,
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "flex-start",
|
||||
p: "20px",
|
||||
height: "100%",
|
||||
overflowY: "auto",
|
||||
overflowX: "hidden",
|
||||
background:
|
||||
settings.cfg.design && !quizThemes[settings.cfg.theme].isLight
|
||||
? "linear-gradient(90deg,#272626,transparent)"
|
||||
: null,
|
||||
"&::-webkit-scrollbar": {
|
||||
width: "4px",
|
||||
},
|
||||
"&::-webkit-scrollbar-thumb": {
|
||||
backgroundColor: "#b8babf",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box sx={{ marginBottom: "13px" }}>{quizHeaderBlock}</Box>
|
||||
{settings.cfg.startpage.background.desktop && (
|
||||
<Box sx={{ width: "100%", overflow: "hidden" }}>
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
"& > img": {
|
||||
width: "100%",
|
||||
borderRadius: "12px",
|
||||
},
|
||||
}}
|
||||
onClick={(event) => event.preventDefault()}
|
||||
>
|
||||
{backgroundBlock}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
<Box
|
||||
sx={{
|
||||
height: "80%",
|
||||
display: "flex",
|
||||
flexGrow: 1,
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
marginTop: "30px",
|
||||
}}
|
||||
>
|
||||
{quizMainBlock}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const ExpandedMobileLayout = ({ quizHeaderBlock, quizMainBlock, backgroundBlock }: MobileLayoutProps) => (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column-reverse",
|
||||
flexGrow: 1,
|
||||
justifyContent: "flex-end",
|
||||
minHeight: "100%",
|
||||
height: "100%",
|
||||
"&::-webkit-scrollbar": { width: 0 },
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
zIndex: 3,
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
flexGrow: 1,
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "flex-start",
|
||||
height: "100%",
|
||||
overflowY: "auto",
|
||||
overflowX: "hidden",
|
||||
"&::-webkit-scrollbar": { width: "4px" },
|
||||
"&::-webkit-scrollbar-thumb": { backgroundColor: "#b8babf" },
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
padding: "20px",
|
||||
height: "80%",
|
||||
display: "flex",
|
||||
flexGrow: 1,
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
{quizHeaderBlock}
|
||||
{quizMainBlock}
|
||||
</Box>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
zIndex: -1,
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
// minHeight: "100%",
|
||||
overflow: "hidden",
|
||||
"& > img": {
|
||||
display: "block",
|
||||
minHeight: "100%",
|
||||
},
|
||||
}}
|
||||
onClick={(event) => event.preventDefault()}
|
||||
>
|
||||
{backgroundBlock}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
||||
const CenteredMobileLayout = ({ quizHeaderBlock, quizMainBlock, backgroundBlock }: MobileLayoutProps) => {
|
||||
const { settings } = useQuizStore();
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column-reverse",
|
||||
flexGrow: 1,
|
||||
justifyContent: "flex-end",
|
||||
minHeight: "100%",
|
||||
height: "100%",
|
||||
backgroundPosition: "center",
|
||||
backgroundSize: "cover",
|
||||
backgroundImage: !settings.cfg.design
|
||||
? null
|
||||
: settings.cfg.design && !quizThemes[settings.cfg.theme].isLight
|
||||
? `linear-gradient(0deg, #272626, transparent), url(${DESIGN_LIST[settings.cfg.theme]})`
|
||||
: `url(${DESIGN_LIST[settings.cfg.theme]})`,
|
||||
"&::-webkit-scrollbar": { width: 0 },
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
flexGrow: 1,
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "flex-start",
|
||||
padding: "20px",
|
||||
height: "100%",
|
||||
overflowY: "auto",
|
||||
overflowX: "hidden",
|
||||
"&::-webkit-scrollbar": { width: "4px" },
|
||||
"&::-webkit-scrollbar-thumb": { backgroundColor: "#b8babf" },
|
||||
}}
|
||||
>
|
||||
{quizHeaderBlock}
|
||||
{settings.cfg.startpage.background.desktop && (
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
overflow: "hidden",
|
||||
"& > img": { width: "100%", borderRadius: "12px" },
|
||||
}}
|
||||
onClick={(event) => event.preventDefault()}
|
||||
>
|
||||
{backgroundBlock}
|
||||
</Box>
|
||||
)}
|
||||
<Box
|
||||
sx={{
|
||||
height: "80%",
|
||||
display: "flex",
|
||||
flexGrow: 1,
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
{quizMainBlock}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export const StartPageMobile = ({
|
||||
quizHeaderBlock,
|
||||
quizMainBlock,
|
||||
backgroundBlock,
|
||||
startpageType,
|
||||
}: StartPageMobileProps) => {
|
||||
switch (startpageType) {
|
||||
case null:
|
||||
case "standard": {
|
||||
return (
|
||||
<StandartMobileLayout
|
||||
quizHeaderBlock={quizHeaderBlock}
|
||||
quizMainBlock={quizMainBlock}
|
||||
backgroundBlock={backgroundBlock}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
case "expanded": {
|
||||
return (
|
||||
<ExpandedMobileLayout
|
||||
quizHeaderBlock={quizHeaderBlock}
|
||||
quizMainBlock={quizMainBlock}
|
||||
backgroundBlock={backgroundBlock}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
case "centered": {
|
||||
return (
|
||||
<CenteredMobileLayout
|
||||
quizHeaderBlock={quizHeaderBlock}
|
||||
quizMainBlock={quizMainBlock}
|
||||
backgroundBlock={backgroundBlock}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
default:
|
||||
notReachable(startpageType);
|
||||
}
|
||||
};
|
@ -0,0 +1,477 @@
|
||||
import { Box, Button, ButtonBase, Link, Paper, Typography, useTheme } from "@mui/material";
|
||||
|
||||
import { QuizPreviewLayoutByType } from "./QuizPreviewLayoutByType";
|
||||
|
||||
import { useQuizStore } from "@/stores/useQuizStore";
|
||||
import { useRootContainerSize } from "@contexts/RootContainerWidthContext";
|
||||
|
||||
import { useUADevice } from "@utils/hooks/useUADevice";
|
||||
import { quizThemes } from "@utils/themes/Publication/themePublication";
|
||||
|
||||
import { NameplateLogo } from "@icons/NameplateLogo";
|
||||
import { NameplateLogoDark } from "@icons/NameplateLogoDark";
|
||||
import { useQuizViewStore } from "@/stores/quizView";
|
||||
import { DESIGN_LIST } from "@/utils/designList";
|
||||
|
||||
import { useVkMetricsGoals } from "@/utils/hooks/metrics/useVkMetricsGoals";
|
||||
import { useYandexMetricsGoals } from "@/utils/hooks/metrics/useYandexMetricsGoals";
|
||||
import QuizVideo from "@/ui_kit/VideoIframe/VideoIframe";
|
||||
|
||||
import { isProduction } from "@/utils/defineDomain";
|
||||
|
||||
export const StartPageViewPublication = () => {
|
||||
const theme = useTheme();
|
||||
const { settings, show_badge, quizId, questions } = useQuizStore();
|
||||
const { isMobileDevice } = useUADevice();
|
||||
const setCurrentQuizStep = useQuizViewStore((state) => state.setCurrentQuizStep);
|
||||
|
||||
const size = useRootContainerSize();
|
||||
const isMobile = size < 700;
|
||||
const isTablet = size >= 700 && size < 1100;
|
||||
|
||||
const vkMetrics = useVkMetricsGoals(settings.cfg.vkMetricsNumber);
|
||||
const yandexMetrics = useYandexMetricsGoals(settings.cfg.yandexMetricsNumber);
|
||||
|
||||
const handleCopyNumber = () => {
|
||||
navigator.clipboard.writeText(settings.cfg.info.phonenumber);
|
||||
|
||||
vkMetrics.phoneNumberOpened();
|
||||
yandexMetrics.phoneNumberOpened();
|
||||
};
|
||||
|
||||
const background =
|
||||
settings.cfg.startpage.background.type === "image" ? (
|
||||
<img
|
||||
src={settings.cfg.startpage.background.desktop || DESIGN_LIST[settings.cfg.theme] || ""}
|
||||
alt=""
|
||||
style={{
|
||||
display: "block",
|
||||
width: isMobile || settings.cfg.startpageType === "expanded" ? "100%" : undefined,
|
||||
height: "100%",
|
||||
minWidth: "100%",
|
||||
maxHeight: "100%",
|
||||
objectFit: "cover",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
/>
|
||||
) : settings.cfg.startpage.background.type === "video" ? (
|
||||
settings.cfg.startpage.background.video ? (
|
||||
<QuizVideo
|
||||
videoUrl={settings.cfg.startpage.background.video}
|
||||
containerSX={{
|
||||
width: settings.cfg.startpageType === "centered" ? "550px" : "100%",
|
||||
height: settings.cfg.startpageType === "centered" ? "275px" : "100%",
|
||||
borderRadius: settings.cfg.startpageType === "centered" ? "10px" : "0",
|
||||
overflow: "hidden",
|
||||
"& iframe": {
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
transform:
|
||||
settings.cfg.startpageType === "centered"
|
||||
? ""
|
||||
: settings.cfg.startpageType === "expanded"
|
||||
? "scale(1.5)"
|
||||
: "scale(2.4)",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
) : null
|
||||
) : null;
|
||||
|
||||
const quizHeaderBlock = (
|
||||
<Box
|
||||
sx={{
|
||||
margin: settings.cfg.startpageType === "centered" ? "0 auto" : null,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
flexWrap:
|
||||
settings.cfg.startpageType === "expanded" && settings.cfg.startpage.position === "center"
|
||||
? "nowrap"
|
||||
: "wrap",
|
||||
gap: isMobile ? "20px" : "30px",
|
||||
mb:
|
||||
settings.cfg.startpageType === "centered"
|
||||
? isMobile
|
||||
? "20px"
|
||||
: "25px"
|
||||
: settings.cfg.startpageType === "expanded" && settings.cfg.startpage.position === "center" && !isMobile
|
||||
? 0
|
||||
: "7px",
|
||||
justifyContent:
|
||||
settings.cfg.startpageType === "expanded" && settings.cfg.startpage.position === "center" && isMobile
|
||||
? "center"
|
||||
: undefined,
|
||||
}}
|
||||
onClick={(event) => event.preventDefault()}
|
||||
>
|
||||
{settings.cfg.startpage.logo && (
|
||||
<img
|
||||
src={settings.cfg.startpage.logo}
|
||||
style={{
|
||||
maxHeight: isMobile ? "30px" : "40px",
|
||||
maxWidth: isMobile ? "100px" : "110px",
|
||||
objectFit: "cover",
|
||||
}}
|
||||
alt=""
|
||||
/>
|
||||
)}
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "12px",
|
||||
color: settings.cfg.startpageType === "expanded" ? "white" : theme.palette.text.primary,
|
||||
wordBreak:
|
||||
settings.cfg.startpageType === "expanded" && settings.cfg.startpage.position === "center"
|
||||
? "normal"
|
||||
: "break-word",
|
||||
}}
|
||||
>
|
||||
{settings.cfg.info.orgname}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
||||
const PenaBadge = (
|
||||
<Box
|
||||
component={Link}
|
||||
target={"_blank"}
|
||||
href={`https://${isProduction ? "" : "s"}quiz.pena.digital/answer/v1.0.0/logo?q=${quizId}`}
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "7px",
|
||||
textDecoration: "none",
|
||||
marginLeft:
|
||||
settings.cfg.startpageType === "expanded" &&
|
||||
settings.cfg.startpage.position === "center" &&
|
||||
!isTablet &&
|
||||
!isMobile
|
||||
? "61px"
|
||||
: undefined,
|
||||
}}
|
||||
>
|
||||
{settings.cfg.startpageType === "expanded" ? (
|
||||
<NameplateLogo />
|
||||
) : quizThemes[settings.cfg.theme].isLight ? (
|
||||
<NameplateLogoDark />
|
||||
) : (
|
||||
<NameplateLogo />
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
|
||||
const realQuestionsCount = questions.filter(
|
||||
(question) => question.type !== null && question.type !== "result"
|
||||
).length;
|
||||
|
||||
const onQuizStart = () => {
|
||||
setCurrentQuizStep("question");
|
||||
|
||||
vkMetrics.firstPageOpened();
|
||||
yandexMetrics.firstPageOpened();
|
||||
};
|
||||
|
||||
const onSiteClick = () => {
|
||||
vkMetrics.emailOpened();
|
||||
yandexMetrics.emailOpened();
|
||||
|
||||
setTimeout(() => {
|
||||
location.href = (
|
||||
settings.cfg.info.site.includes("https") ? settings.cfg.info.site : `https://${settings.cfg.info.site}`
|
||||
).replace(/\s+/g, "");
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
return (
|
||||
<Paper
|
||||
className="settings-preview-draghandle"
|
||||
sx={{
|
||||
borderRadius: 0,
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
background:
|
||||
settings.cfg.startpageType === "expanded"
|
||||
? settings.cfg.startpage.position === "left" || (isMobile && settings.cfg.startpage.position === "right")
|
||||
? "linear-gradient(90deg, rgba(39, 38, 38, 0.95) 7.66%, rgba(42, 42, 46, 0.85) 42.12%, rgba(51, 54, 71, 0.4) 100%)"
|
||||
: settings.cfg.startpage.position === "center"
|
||||
? "linear-gradient(0deg, rgba(39, 38, 38, 0.95) 7.66%, rgba(42, 42, 46, 0.85) 42.12%, rgba(51, 54, 71, 0.4) 100%)"
|
||||
: "linear-gradient(-90deg, rgba(39, 38, 38, 0.95) 7.66%, rgba(42, 42, 46, 0.85) 42.12%, rgba(51, 54, 71, 0.4) 100%)"
|
||||
: theme.palette.background.default,
|
||||
|
||||
color: settings.cfg.startpageType === "expanded" ? "white" : "black",
|
||||
}}
|
||||
onClick={(event) => event.preventDefault()}
|
||||
>
|
||||
<QuizPreviewLayoutByType
|
||||
quizHeaderBlock={quizHeaderBlock}
|
||||
quizMainBlock={
|
||||
<>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: settings.cfg.startpageType === "standard" && isMobile ? "start" : "center",
|
||||
flexGrow: settings.cfg.startpageType === "centered" ? 0 : 1,
|
||||
wordBreak: "break-word",
|
||||
alignItems:
|
||||
settings.cfg.startpageType === "centered"
|
||||
? "center"
|
||||
: settings.cfg.startpageType === "expanded"
|
||||
? settings.cfg.startpage.position === "center"
|
||||
? "center"
|
||||
: "start"
|
||||
: "start",
|
||||
marginTop: settings.cfg.startpageType === "centered" ? "30px" : isMobile ? "0px" : "5px",
|
||||
maxWidth: isMobile
|
||||
? "100%"
|
||||
: settings.cfg.startpageType === "centered"
|
||||
? "700px"
|
||||
: isTablet &&
|
||||
settings.cfg.startpageType !== "expanded" &&
|
||||
settings.cfg.startpage.position !== "center"
|
||||
? "380px"
|
||||
: "531px",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
fontWeight: "700",
|
||||
fontSize: isMobile ? "24px" : "27px",
|
||||
fontStyle: "normal",
|
||||
fontStretch: "normal",
|
||||
lineHeight: isMobile ? "26.4px" : "normal",
|
||||
overflowWrap: "break-word",
|
||||
width: "100%",
|
||||
textAlign:
|
||||
settings.cfg.startpageType === "centered" || settings.cfg.startpage.position === "center"
|
||||
? "center"
|
||||
: "-moz-initial",
|
||||
color: settings.cfg.startpageType === "expanded" ? "white" : theme.palette.text.primary,
|
||||
}}
|
||||
>
|
||||
{settings.name}
|
||||
</Typography>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: isMobile ? "16px" : "17px",
|
||||
fontWeight: "400",
|
||||
lineHeight: isMobile ? "19.2px" : "normal",
|
||||
margin: "12px 0 30px",
|
||||
overflowWrap: "break-word",
|
||||
width: "100%",
|
||||
textAlign:
|
||||
settings.cfg.startpageType === "centered" || settings.cfg.startpage.position === "center"
|
||||
? "center"
|
||||
: "-moz-initial",
|
||||
color: settings.cfg.startpageType === "expanded" ? "white" : theme.palette.text.primary,
|
||||
}}
|
||||
>
|
||||
{settings.cfg.startpage.description}
|
||||
</Typography>
|
||||
<Box width={settings.cfg.startpageType === "standard" ? "100%" : "auto"}>
|
||||
<Button
|
||||
variant="contained"
|
||||
disabled={realQuestionsCount === 0}
|
||||
sx={{
|
||||
fontSize: "18px",
|
||||
padding: "10px 20px",
|
||||
width: "auto",
|
||||
background: theme.palette.primary.main,
|
||||
borderRadius: "12px",
|
||||
}}
|
||||
onClick={onQuizStart}
|
||||
>
|
||||
{settings.cfg.startpage.button.trim() ? settings.cfg.startpage.button : "Пройти тест"}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexGrow: settings.cfg.startpageType === "centered" ? (isMobile ? 0 : 1) : 0,
|
||||
gap: isMobile ? "30px" : "40px",
|
||||
alignItems: "flex-end",
|
||||
justifyContent:
|
||||
(settings.cfg.startpageType === "expanded" &&
|
||||
settings.cfg.startpage.position === "center" &&
|
||||
isMobile) ||
|
||||
(settings.cfg.startpageType === "centered" && isMobile)
|
||||
? "center"
|
||||
: "space-between",
|
||||
width: "100%",
|
||||
flexWrap:
|
||||
settings.cfg.startpageType === "expanded" && settings.cfg.startpage.position === "center"
|
||||
? isMobile
|
||||
? "wrap-reverse"
|
||||
: "nowrap"
|
||||
: "wrap",
|
||||
}}
|
||||
>
|
||||
{settings.cfg.startpageType === "expanded" &&
|
||||
settings.cfg.startpage.position === "center" &&
|
||||
!isMobile &&
|
||||
quizHeaderBlock}
|
||||
<Box
|
||||
sx={{
|
||||
maxWidth: "300px",
|
||||
display:
|
||||
(settings.cfg.startpageType === "centered" && isMobile) ||
|
||||
(settings.cfg.startpageType === "expanded" &&
|
||||
settings.cfg.startpage.position === "center" &&
|
||||
isMobile)
|
||||
? "flex"
|
||||
: "block",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
order:
|
||||
settings.cfg.startpageType === "expanded" && settings.cfg.startpage.position === "center"
|
||||
? "2"
|
||||
: "0",
|
||||
}}
|
||||
>
|
||||
{settings.cfg.info.site && (
|
||||
<ButtonBase
|
||||
onClick={onSiteClick}
|
||||
sx={{
|
||||
display: "block",
|
||||
width: "100%",
|
||||
marginTop: "10px",
|
||||
marginLeft:
|
||||
settings.cfg.startpageType === "expanded" &&
|
||||
settings.cfg.startpage.position === "center" &&
|
||||
!isMobile
|
||||
? "auto"
|
||||
: undefined,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
lineHeight: "19px",
|
||||
fontSize: "16px",
|
||||
textAlign:
|
||||
settings.cfg.startpageType === "expanded" &&
|
||||
settings.cfg.startpage.position === "center" &&
|
||||
!isMobile
|
||||
? "end"
|
||||
: (settings.cfg.startpageType === "expanded" &&
|
||||
settings.cfg.startpage.position === "center" &&
|
||||
isMobile) ||
|
||||
(settings.cfg.startpageType === "centered" && isMobile)
|
||||
? "center"
|
||||
: "start",
|
||||
color: theme.palette.primary.main,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{settings.cfg.info.site}
|
||||
</Typography>
|
||||
</ButtonBase>
|
||||
)}
|
||||
{settings.cfg.info.clickable ? (
|
||||
isMobileDevice ? (
|
||||
<Link href={`tel:${settings.cfg.info.phonenumber}`}>
|
||||
<Typography
|
||||
sx={{
|
||||
lineHeight: "19px",
|
||||
textAlign:
|
||||
settings.cfg.startpageType === "expanded" && settings.cfg.startpage.position === "center"
|
||||
? "end"
|
||||
: "none",
|
||||
fontSize: "16px",
|
||||
color: settings.cfg.startpageType === "expanded" ? "#FFFFFF" : theme.palette.text.primary,
|
||||
}}
|
||||
>
|
||||
{settings.cfg.info.phonenumber}
|
||||
</Typography>
|
||||
</Link>
|
||||
) : (
|
||||
<ButtonBase
|
||||
onClick={handleCopyNumber}
|
||||
sx={{
|
||||
display: "block",
|
||||
marginTop: "10px",
|
||||
marginLeft:
|
||||
settings.cfg.startpageType === "expanded" &&
|
||||
settings.cfg.startpage.position === "center" &&
|
||||
!isMobile
|
||||
? "auto"
|
||||
: undefined,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
textAlign:
|
||||
settings.cfg.startpageType === "expanded" && settings.cfg.startpage.position === "center"
|
||||
? "end"
|
||||
: "none",
|
||||
fontSize: "16px",
|
||||
lineHeight: "19px",
|
||||
color: settings.cfg.startpageType === "expanded" ? "#FFFFFF" : theme.palette.text.primary,
|
||||
}}
|
||||
>
|
||||
{settings.cfg.info.phonenumber}
|
||||
</Typography>
|
||||
</ButtonBase>
|
||||
)
|
||||
) : (
|
||||
<Typography
|
||||
sx={{
|
||||
lineHeight: "19px",
|
||||
textAlign:
|
||||
settings.cfg.startpageType === "expanded" && settings.cfg.startpage.position === "center"
|
||||
? "end"
|
||||
: "none",
|
||||
fontSize: "16px",
|
||||
marginTop: "10px",
|
||||
color: settings.cfg.startpageType === "expanded" ? "#FFFFFF" : theme.palette.text.primary,
|
||||
}}
|
||||
>
|
||||
{settings.cfg.info.phonenumber}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
<Typography
|
||||
sx={{
|
||||
lineHeight: "14px",
|
||||
width: "100%",
|
||||
overflowWrap: "break-word",
|
||||
fontSize: "12px",
|
||||
textAlign:
|
||||
settings.cfg.startpageType === "expanded" &&
|
||||
settings.cfg.startpage.position === "center" &&
|
||||
!isMobile
|
||||
? "end"
|
||||
: (settings.cfg.startpageType === "expanded" &&
|
||||
settings.cfg.startpage.position === "center" &&
|
||||
isMobile) ||
|
||||
(settings.cfg.startpageType === "centered" && isMobile)
|
||||
? "center"
|
||||
: "none",
|
||||
maxHeight: "120px",
|
||||
overflow: "auto",
|
||||
marginTop: "10px",
|
||||
"&::-webkit-scrollbar": { width: 0 },
|
||||
color: settings.cfg.startpageType === "expanded" ? "white" : theme.palette.text.primary,
|
||||
}}
|
||||
>
|
||||
{settings.cfg.info.law}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{show_badge && PenaBadge}
|
||||
</Box>
|
||||
</>
|
||||
}
|
||||
backgroundBlock={background}
|
||||
startpageType={settings.cfg.startpageType}
|
||||
alignType={settings.cfg.startpage.position}
|
||||
/>
|
||||
</Paper>
|
||||
);
|
||||
};
|
@ -1,3 +1,4 @@
|
||||
import { ContactForm } from "@/components/ViewPublicationPage/ContactForm/ContactForm.tsx";
|
||||
import { extractImageLinksFromQuestion } from "@/utils/extractImageLinks";
|
||||
import { useVKMetrics } from "@/utils/hooks/metrics/useVKMetrics";
|
||||
import { useYandexMetrics } from "@/utils/hooks/metrics/useYandexMetrics";
|
||||
@ -13,6 +14,7 @@ import { Helmet } from "react-helmet-async";
|
||||
import { Question } from "./Question";
|
||||
import QuestionSelect from "./QuestionSelect";
|
||||
import { ResultForm } from "./ResultForm";
|
||||
import { StartPageViewPublication } from "./StartPageViewPublication";
|
||||
import NextButton from "./tools/NextButton";
|
||||
import PrevButton from "./tools/PrevButton";
|
||||
import unscreen from "@/ui_kit/unscreen";
|
||||
@ -68,7 +70,7 @@ export default function ViewPublicationPage() {
|
||||
if (settings.cfg.antifraud && recentlyCompleted) throw new Error("Quiz already completed");
|
||||
if (currentQuizStep === "startpage" && settings.cfg.noStartPage) currentQuizStep = "question";
|
||||
|
||||
if (!currentQuestion)
|
||||
if (!currentQuestion) {
|
||||
return (
|
||||
<ThemeProvider theme={quizThemes[settings.cfg.theme || "StandardTheme"].theme}>
|
||||
<Typography
|
||||
@ -79,13 +81,22 @@ export default function ViewPublicationPage() {
|
||||
</Typography>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
const currentAnswer = answers.find(({ questionId }) => questionId === currentQuestion.id);
|
||||
|
||||
let quizStepElement: ReactElement;
|
||||
switch (currentQuizStep) {
|
||||
case "startpage": {
|
||||
quizStepElement = <StartPageViewPublication />;
|
||||
break;
|
||||
}
|
||||
case "question": {
|
||||
if (currentQuestion.type === "result") {
|
||||
quizStepElement = <ResultForm resultQuestion={currentQuestion} />;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
|
||||
quizStepElement = (
|
||||
<Question
|
||||
key={currentQuestion.id}
|
||||
@ -120,6 +131,19 @@ export default function ViewPublicationPage() {
|
||||
}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "contactform": {
|
||||
quizStepElement = (
|
||||
<ContactForm
|
||||
currentQuestion={currentQuestion}
|
||||
onShowResult={showResultAfterContactForm}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
notReachable(currentQuizStep);
|
||||
}
|
||||
|
||||
const preloadLinks = new Set([
|
||||
|
@ -0,0 +1,77 @@
|
||||
import { useQuizViewStore } from "@/stores/quizView";
|
||||
import { useQuizStore } from "@/stores/useQuizStore";
|
||||
import CalendarIcon from "@icons/CalendarIcon";
|
||||
import type { QuizQuestionDate } from "@model/questionTypes/date";
|
||||
import { Box, Typography, useTheme } from "@mui/material";
|
||||
import { DatePicker } from "@mui/x-date-pickers";
|
||||
import { quizThemes } from "@utils/themes/Publication/themePublication";
|
||||
import type { Moment } from "moment";
|
||||
import moment from "moment";
|
||||
|
||||
type DateProps = {
|
||||
currentQuestion: QuizQuestionDate;
|
||||
};
|
||||
|
||||
export default ({ currentQuestion }: DateProps) => {
|
||||
const { settings } = useQuizStore();
|
||||
const answers = useQuizViewStore((state) => state.answers);
|
||||
const { updateAnswer } = useQuizViewStore((state) => state);
|
||||
const theme = useTheme();
|
||||
const answer = answers.find(({ questionId }) => questionId === currentQuestion.id)?.answer as string;
|
||||
const currentAnswer = moment(answer) || moment();
|
||||
|
||||
const onDateChange = async (date: Moment | null) => {
|
||||
if (!date) return;
|
||||
|
||||
updateAnswer(currentQuestion.id, date, 0);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
width: "100%",
|
||||
marginTop: "20px",
|
||||
}}
|
||||
>
|
||||
<DatePicker
|
||||
format="DD/MM/YYYY"
|
||||
slots={{
|
||||
openPickerIcon: () => (
|
||||
<CalendarIcon
|
||||
sx={{
|
||||
"& path": { stroke: theme.palette.primary.main },
|
||||
"& rect": { stroke: theme.palette.primary.main },
|
||||
}}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
value={currentAnswer}
|
||||
onChange={onDateChange}
|
||||
slotProps={{
|
||||
openPickerButton: { sx: { p: 0 }, "data-cy": "open-datepicker" },
|
||||
layout: {
|
||||
sx: { backgroundColor: theme.palette.background.default },
|
||||
},
|
||||
}}
|
||||
sx={{
|
||||
"& .MuiInputBase-root": {
|
||||
backgroundColor: settings.cfg.design
|
||||
? quizThemes[settings.cfg.theme].isLight
|
||||
? "#F2F3F7"
|
||||
: "rgba(154,154,175, 0.2)"
|
||||
: quizThemes[settings.cfg.theme].isLight
|
||||
? "white"
|
||||
: theme.palette.background.default,
|
||||
borderRadius: "10px",
|
||||
maxWidth: "250px",
|
||||
pr: "30px",
|
||||
"& input": { py: "11px", pl: "20px", lineHeight: "19px" },
|
||||
"& fieldset": { borderColor: "#9A9AAF" },
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
104
lib/components/ViewPublicationPage/questions/Date/DateRange.tsx
Normal file
104
lib/components/ViewPublicationPage/questions/Date/DateRange.tsx
Normal file
@ -0,0 +1,104 @@
|
||||
import { useQuizViewStore } from "@/stores/quizView";
|
||||
import type { QuizQuestionDate } from "@model/questionTypes/date";
|
||||
import { DateCalendar } from "@mui/x-date-pickers";
|
||||
import { quizThemes } from "@utils/themes/Publication/themePublication";
|
||||
import type { Moment } from "moment";
|
||||
import moment from "moment";
|
||||
import { Box, Paper, TextField, useTheme } from "@mui/material";
|
||||
import { useRootContainerSize } from "@/contexts/RootContainerWidthContext";
|
||||
import { useQuizStore } from "@/stores/useQuizStore";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type DateProps = {
|
||||
currentQuestion: QuizQuestionDate;
|
||||
};
|
||||
|
||||
export default ({ currentQuestion }: DateProps) => {
|
||||
const theme = useTheme();
|
||||
const today = moment();
|
||||
const isMobile = useRootContainerSize() < 690;
|
||||
const { settings } = useQuizStore();
|
||||
const { updateAnswer } = useQuizViewStore((state) => state);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const answers = useQuizViewStore((state) => state.answers);
|
||||
const answer = (answers.find(({ questionId }) => questionId === currentQuestion.id)?.answer as string) || ["0", "0"];
|
||||
|
||||
const currentFrom = Number(answer[0]) ? moment(Number(answer[0])) : moment().utc();
|
||||
const currentTo = Number(answer[1]) ? moment(Number(answer[1])) : moment().utc();
|
||||
|
||||
const onDateChange = async (date: Moment | null, index: number) => {
|
||||
if (!date) return;
|
||||
let newAnswer = [...answer];
|
||||
newAnswer[index] = (moment(date).unix() * 1000).toString();
|
||||
|
||||
updateAnswer(currentQuestion.id, newAnswer, 0);
|
||||
};
|
||||
|
||||
return (
|
||||
<Paper
|
||||
sx={{
|
||||
backgroundColor: settings.cfg.design
|
||||
? quizThemes[settings.cfg.theme].isLight
|
||||
? "#F2F3F7"
|
||||
: "rgba(154,154,175, 0.2)"
|
||||
: quizThemes[settings.cfg.theme].isLight
|
||||
? "white"
|
||||
: theme.palette.background.default,
|
||||
width: isMobile ? "min-content" : "auto",
|
||||
display: "inline-flex",
|
||||
flexWrap: "wrap",
|
||||
marginTop: "20px",
|
||||
p: "20px",
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<span style={{ marginLeft: "25px", color: theme.palette.text.primary }}>{t("From")}</span>
|
||||
<DateCalendar
|
||||
sx={{
|
||||
"& .MuiInputBase-root": {
|
||||
backgroundColor: settings.cfg.design
|
||||
? quizThemes[settings.cfg.theme].isLight
|
||||
? "#F2F3F7"
|
||||
: "rgba(154,154,175, 0.2)"
|
||||
: quizThemes[settings.cfg.theme].isLight
|
||||
? "white"
|
||||
: theme.palette.background.default,
|
||||
borderRadius: "10px",
|
||||
maxWidth: "250px",
|
||||
pr: "30px",
|
||||
"& input": { py: "11px", pl: "20px", lineHeight: "19px" },
|
||||
"& fieldset": { borderColor: "#9A9AAF" },
|
||||
},
|
||||
}}
|
||||
value={currentFrom}
|
||||
onChange={(data) => onDateChange(data, 0)}
|
||||
/>
|
||||
</Box>
|
||||
<Box>
|
||||
<span style={{ marginLeft: "25px", color: theme.palette.text.primary }}>{t("До")}</span>
|
||||
<DateCalendar
|
||||
minDate={today}
|
||||
sx={{
|
||||
"& .MuiInputBase-root": {
|
||||
backgroundColor: settings.cfg.design
|
||||
? quizThemes[settings.cfg.theme].isLight
|
||||
? "#F2F3F7"
|
||||
: "rgba(154,154,175, 0.2)"
|
||||
: quizThemes[settings.cfg.theme].isLight
|
||||
? "white"
|
||||
: theme.palette.background.default,
|
||||
borderRadius: "10px",
|
||||
maxWidth: "250px",
|
||||
pr: "30px",
|
||||
"& input": { py: "11px", pl: "20px", lineHeight: "19px" },
|
||||
"& fieldset": { borderColor: "#9A9AAF" },
|
||||
},
|
||||
}}
|
||||
value={currentTo}
|
||||
onChange={(data) => onDateChange(data, 1)}
|
||||
/>
|
||||
</Box>
|
||||
</Paper>
|
||||
);
|
||||
};
|
29
lib/components/ViewPublicationPage/questions/Date/index.tsx
Normal file
29
lib/components/ViewPublicationPage/questions/Date/index.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import type { QuizQuestionDate } from "@model/questionTypes/date";
|
||||
import DateRange from "./DateRange";
|
||||
import DatePicker from "./DatePicker";
|
||||
import { Box, Typography, useTheme } from "@mui/material";
|
||||
|
||||
type DateProps = {
|
||||
currentQuestion: QuizQuestionDate;
|
||||
};
|
||||
|
||||
export const Date = ({ currentQuestion }: DateProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Typography
|
||||
variant="h5"
|
||||
color={theme.palette.text.primary}
|
||||
sx={{ wordBreak: "break-word" }}
|
||||
>
|
||||
{currentQuestion.title}
|
||||
</Typography>
|
||||
{currentQuestion.content.isRange ? (
|
||||
<DateRange currentQuestion={currentQuestion} />
|
||||
) : (
|
||||
<DatePicker currentQuestion={currentQuestion} />
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
@ -0,0 +1,49 @@
|
||||
import EmojiPickerOriginal from "@emoji-mart/react";
|
||||
import { Box } from "@mui/material";
|
||||
|
||||
type Emoji = {
|
||||
emoticons: string[];
|
||||
id: string;
|
||||
keywords: string[];
|
||||
name: string;
|
||||
native: string;
|
||||
shortcodes: string;
|
||||
unified: string;
|
||||
};
|
||||
|
||||
type EmojiPickerProps = {
|
||||
onEmojiSelect: (emoji: Emoji) => void;
|
||||
};
|
||||
|
||||
export const EmojiPicker = ({ onEmojiSelect }: EmojiPickerProps) => (
|
||||
<Box sx={{ minWidth: "352px" }}>
|
||||
<EmojiPickerOriginal
|
||||
onEmojiSelect={onEmojiSelect}
|
||||
theme="light"
|
||||
locale="ru"
|
||||
exceptEmojis={ignoreEmojis}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
|
||||
const ignoreEmojis = [
|
||||
"two_men_holding_hands",
|
||||
"two_women_holding_hands",
|
||||
"man-kiss-man",
|
||||
"woman-kiss-woman",
|
||||
"man-heart-man",
|
||||
"woman-heart-woman",
|
||||
"man-man-boy",
|
||||
"man-man-girl",
|
||||
"man-man-girl-boy",
|
||||
"man-man-girl-girl",
|
||||
"man-man-boy-boy",
|
||||
"woman-woman-boy",
|
||||
"woman-woman-girl",
|
||||
"woman-woman-girl-boy",
|
||||
"woman-woman-girl-girl",
|
||||
"woman-woman-boy-boy",
|
||||
"rainbow-flag",
|
||||
"transgender_flag",
|
||||
"transgender_symbol",
|
||||
];
|
@ -0,0 +1,271 @@
|
||||
import type { QuestionVariant } from "@/model/questionTypes/shared";
|
||||
import { useQuizStore } from "@/stores/useQuizStore";
|
||||
import { useQuizViewStore, type OwnVariant } from "@stores/quizView";
|
||||
import {
|
||||
Box,
|
||||
Checkbox,
|
||||
FormControl,
|
||||
FormControlLabel,
|
||||
Input,
|
||||
Radio,
|
||||
TextareaAutosize,
|
||||
Typography,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import RadioCheck from "@ui_kit/RadioCheck";
|
||||
import RadioIcon from "@ui_kit/RadioIcon";
|
||||
import { quizThemes } from "@utils/themes/Publication/themePublication";
|
||||
import { polyfillCountryFlagEmojis } from "country-flag-emoji-polyfill";
|
||||
import type { MouseEvent } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useEffect } from "react";
|
||||
import { OwnEmojiPicker } from "./OwnEmojiPicker";
|
||||
|
||||
polyfillCountryFlagEmojis();
|
||||
|
||||
type EmojiVariantProps = {
|
||||
questionId: string;
|
||||
variant: QuestionVariant;
|
||||
index: number;
|
||||
isMulti: boolean;
|
||||
own: boolean;
|
||||
questionLargeCheck: boolean;
|
||||
ownPlaceholder: string;
|
||||
answer: string | string[] | undefined;
|
||||
};
|
||||
|
||||
interface OwnInputProps {
|
||||
questionId: string;
|
||||
variant: QuestionVariant;
|
||||
largeCheck: boolean;
|
||||
ownPlaceholder: string;
|
||||
}
|
||||
const OwnInput = ({ questionId, variant, largeCheck, ownPlaceholder }: OwnInputProps) => {
|
||||
const theme = useTheme();
|
||||
const ownVariants = useQuizViewStore((state) => state.ownVariants);
|
||||
const { updateOwnVariant } = useQuizViewStore((state) => state);
|
||||
|
||||
const ownAnswer = ownVariants[ownVariants.findIndex((v: OwnVariant) => v.id === variant.id)]?.variant.answer || "";
|
||||
|
||||
return largeCheck ? (
|
||||
<Box sx={{ overflow: "auto" }}>
|
||||
<TextareaAutosize
|
||||
placeholder={ownPlaceholder || "|"}
|
||||
style={{
|
||||
resize: "none",
|
||||
width: "100%",
|
||||
fontSize: "16px",
|
||||
color: ownAnswer.length === 0 ? "ownPlaceholder" : theme.palette.text.primary,
|
||||
letterSpacing: "-0.4px",
|
||||
wordSpacing: "-3px",
|
||||
outline: "0px none",
|
||||
backgroundColor: "inherit",
|
||||
border: "none",
|
||||
//@ts-ignore
|
||||
"&::-webkit-scrollbar": {
|
||||
width: "4px",
|
||||
},
|
||||
"&::-webkit-scrollbar-thumb": {
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
},
|
||||
scrollbarColor: theme.palette.primary.main,
|
||||
overflow: "auto",
|
||||
}}
|
||||
value={ownAnswer}
|
||||
onClick={(e: React.MouseEvent<HTMLTextAreaElement>) => e.stopPropagation()}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
updateOwnVariant(variant.id, e.target.value);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
<Input
|
||||
placeholder={ownPlaceholder || "|"}
|
||||
sx={{
|
||||
backgroundColor: "inherit",
|
||||
width: "100%",
|
||||
fontSize: "18px",
|
||||
color: ownAnswer.length === 0 ? "ownPlaceholder" : theme.palette.text.primary,
|
||||
}}
|
||||
value={ownAnswer}
|
||||
disableUnderline
|
||||
onClick={(e: React.MouseEvent<HTMLElement>) => e.stopPropagation()}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
updateOwnVariant(variant.id, e.target.value);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const EmojiVariant = ({
|
||||
answer,
|
||||
variant,
|
||||
index,
|
||||
questionId,
|
||||
isMulti,
|
||||
own,
|
||||
questionLargeCheck,
|
||||
ownPlaceholder,
|
||||
}: EmojiVariantProps) => {
|
||||
const { settings } = useQuizStore();
|
||||
const { updateAnswer, deleteAnswer, updateOwnVariant, ownVariants } = useQuizViewStore((state) => state);
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const customEmoji = ownVariants.find((v: OwnVariant) => v.id === variant.id)?.variant.extendedText || "";
|
||||
|
||||
const onVariantClick = async (event: MouseEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
const variantId = variant.id;
|
||||
|
||||
if (isMulti) {
|
||||
const currentAnswer = Array.isArray(answer) ? answer : [];
|
||||
const newAnswer = currentAnswer.includes(variantId)
|
||||
? currentAnswer.filter((item) => item !== variantId)
|
||||
: [...currentAnswer, variantId];
|
||||
updateAnswer(questionId, newAnswer, variant.points || 0);
|
||||
} else {
|
||||
if (answer === variant.id) {
|
||||
deleteAnswer(questionId);
|
||||
} else {
|
||||
updateAnswer(questionId, variant.id, variant.points || 0);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleEmojiSelect = (emoji: string) => {
|
||||
// We store custom emoji in ownVariants store, with a specific field to differentiate
|
||||
const currentOwnAnswer = ownVariants.find((v: OwnVariant) => v.id === variant.id)?.variant.answer || "";
|
||||
updateOwnVariant(variant.id, currentOwnAnswer, emoji);
|
||||
};
|
||||
|
||||
const handleEmojiRemove = () => {
|
||||
// Сохраняем текущий answer, очищаем только extendedText (эмодзи)
|
||||
const currentOwnAnswer = ownVariants.find((v: OwnVariant) => v.id === variant.id)?.variant.answer || "";
|
||||
updateOwnVariant(variant.id, currentOwnAnswer, "");
|
||||
};
|
||||
|
||||
const isSelected = isMulti ? Array.isArray(answer) && answer.includes(variant.id) : answer === variant.id;
|
||||
|
||||
return (
|
||||
<FormControl
|
||||
key={index}
|
||||
sx={{
|
||||
borderRadius: "12px",
|
||||
border: `1px solid`,
|
||||
borderColor: isSelected ? theme.palette.primary.main : "#9A9AAF",
|
||||
overflow: "hidden",
|
||||
maxWidth: "317px",
|
||||
width: "100%",
|
||||
height: "255px",
|
||||
background:
|
||||
settings.cfg.design && !quizThemes[settings.cfg.theme].isLight
|
||||
? "rgba(255,255,255, 0.3)"
|
||||
: (settings.cfg.design && quizThemes[settings.cfg.theme].isLight) || quizThemes[settings.cfg.theme].isLight
|
||||
? "#FFFFFF"
|
||||
: "transparent",
|
||||
"&:hover": { borderColor: theme.palette.primary.main },
|
||||
}}
|
||||
onClick={onVariantClick}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
height: "193px",
|
||||
background: "#ffffff",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
{own ? (
|
||||
<OwnEmojiPicker
|
||||
emoji={customEmoji || variant.extendedText}
|
||||
onEmojiSelect={handleEmojiSelect}
|
||||
onEmojiRemove={customEmoji ? handleEmojiRemove : undefined}
|
||||
/>
|
||||
) : (
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
{variant.extendedText && <Typography fontSize="100px">{variant.extendedText}</Typography>}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
{own && (
|
||||
<Typography
|
||||
sx={{
|
||||
color: theme.palette.text.primary,
|
||||
fontSize: "14px",
|
||||
pl: "15px",
|
||||
}}
|
||||
>
|
||||
{t("Enter your answer")}
|
||||
</Typography>
|
||||
)}
|
||||
<FormControlLabel
|
||||
key={variant.id}
|
||||
sx={{
|
||||
textAlign: "center",
|
||||
color: theme.palette.text.primary,
|
||||
margin: 0,
|
||||
padding: "15px",
|
||||
display: "flex",
|
||||
alignItems: variant.answer.length <= 60 ? "center" : "flex-start",
|
||||
position: "relative",
|
||||
height: "80px",
|
||||
justifyContent: "center",
|
||||
"& .MuiFormControlLabel-label": {
|
||||
wordBreak: "break-word",
|
||||
height: variant.answer.length <= 60 ? "100%" : "60px",
|
||||
overflow: "auto",
|
||||
"&::-webkit-scrollbar": { width: "4px" },
|
||||
"&::-webkit-scrollbar-thumb": {
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
},
|
||||
scrollbarColor: theme.palette.primary.main,
|
||||
width: "100%",
|
||||
},
|
||||
"& .MuiFormControlLabel-label.Mui-disabled": {
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
}}
|
||||
value={index}
|
||||
control={
|
||||
isMulti ? (
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
checkedIcon={<RadioCheck color={theme.palette.primary.main} />}
|
||||
icon={<RadioIcon />}
|
||||
sx={{ position: "absolute", top: "-162px", right: "12px" }}
|
||||
/>
|
||||
) : (
|
||||
<Radio
|
||||
checked={isSelected}
|
||||
checkedIcon={<RadioCheck color={theme.palette.primary.main} />}
|
||||
icon={<RadioIcon />}
|
||||
sx={{ position: "absolute", top: "-162px", right: "12px" }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
label={
|
||||
own ? (
|
||||
<OwnInput
|
||||
questionId={questionId}
|
||||
variant={variant}
|
||||
largeCheck={questionLargeCheck}
|
||||
ownPlaceholder={ownPlaceholder || "|"}
|
||||
/>
|
||||
) : (
|
||||
<Box sx={{ display: "flex", gap: "10px" }}>
|
||||
<Typography sx={{ wordBreak: "break-word", lineHeight: "normal" }}>{variant.answer}</Typography>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
};
|
@ -0,0 +1,103 @@
|
||||
import { Box, ButtonBase, Typography, useTheme, Modal, IconButton } from "@mui/material";
|
||||
import { useState } from "react";
|
||||
import { EmojiPicker } from "./EmojiPicker";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
|
||||
interface Props {
|
||||
emoji: string;
|
||||
onEmojiSelect?: (emoji: string) => void;
|
||||
onEmojiRemove?: () => void;
|
||||
}
|
||||
|
||||
export const OwnEmojiPicker = ({ emoji = "", onEmojiSelect, onEmojiRemove }: Props) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const [isPickerOpen, setIsPickerOpen] = useState(false);
|
||||
|
||||
const handleEmojiSelect = (emojiData: any) => {
|
||||
onEmojiSelect?.(emojiData.native);
|
||||
setIsPickerOpen(false);
|
||||
};
|
||||
|
||||
const handleClick = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
setIsPickerOpen(true);
|
||||
};
|
||||
|
||||
const handleClose = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
setIsPickerOpen(false);
|
||||
};
|
||||
|
||||
const handleRemoveEmoji = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
onEmojiRemove?.();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box sx={{ width: "100%", height: "100%", position: "relative" }}>
|
||||
<ButtonBase
|
||||
onClick={handleClick}
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
"&:hover": {
|
||||
bgcolor: theme.palette.grey[100],
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography fontSize={emoji ? "100px" : "18px"}>{emoji || t("select emoji")}</Typography>
|
||||
</ButtonBase>
|
||||
|
||||
{onEmojiRemove && (
|
||||
<IconButton
|
||||
onClick={handleRemoveEmoji}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: 8,
|
||||
left: 8,
|
||||
zIndex: 1,
|
||||
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
||||
color: "white",
|
||||
height: "25px",
|
||||
width: "25px",
|
||||
"&:hover": {
|
||||
backgroundColor: "rgba(0, 0, 0, 0.7)",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Modal
|
||||
open={isPickerOpen}
|
||||
onClose={handleClose}
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
keepMounted
|
||||
>
|
||||
<Box
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
sx={{
|
||||
bgcolor: "background.paper",
|
||||
borderRadius: 2,
|
||||
p: 2,
|
||||
boxShadow: 24,
|
||||
}}
|
||||
>
|
||||
<EmojiPicker onEmojiSelect={handleEmojiSelect} />
|
||||
</Box>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
66
lib/components/ViewPublicationPage/questions/Emoji/index.tsx
Normal file
66
lib/components/ViewPublicationPage/questions/Emoji/index.tsx
Normal file
@ -0,0 +1,66 @@
|
||||
import type { QuizQuestionEmoji } from "@model/questionTypes/emoji";
|
||||
import { Box, RadioGroup, Typography, useTheme } from "@mui/material";
|
||||
import { useQuizViewStore } from "@stores/quizView";
|
||||
import { polyfillCountryFlagEmojis } from "country-flag-emoji-polyfill";
|
||||
import { EmojiVariant } from "./EmojiVariant";
|
||||
import moment from "moment";
|
||||
|
||||
polyfillCountryFlagEmojis();
|
||||
|
||||
type EmojiProps = {
|
||||
currentQuestion: QuizQuestionEmoji;
|
||||
};
|
||||
|
||||
export const Emoji = ({ currentQuestion }: EmojiProps) => {
|
||||
const answers = useQuizViewStore((state) => state.answers);
|
||||
const theme = useTheme();
|
||||
const { answer } = answers.find(({ questionId }) => questionId === currentQuestion.id) ?? {};
|
||||
|
||||
const selectedVariantId = Array.isArray(answer) ? answer[0] : answer;
|
||||
|
||||
if (moment.isMoment(answer)) throw new Error("Answer is Moment in Variant question");
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Typography
|
||||
variant="h5"
|
||||
color={theme.palette.text.primary}
|
||||
sx={{ wordBreak: "break-word" }}
|
||||
>
|
||||
{currentQuestion.title}
|
||||
</Typography>
|
||||
<RadioGroup
|
||||
name={currentQuestion.id}
|
||||
value={selectedVariantId}
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
marginTop: "20px",
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: "flex", width: "100%", gap: "42px", flexWrap: "wrap" }}>
|
||||
{currentQuestion.content.variants
|
||||
.filter((v) => {
|
||||
if (!v.isOwn) return true;
|
||||
return v.isOwn && currentQuestion.content.own;
|
||||
})
|
||||
.map((variant, index) => (
|
||||
<EmojiVariant
|
||||
key={variant.id}
|
||||
questionId={currentQuestion.id}
|
||||
variant={variant}
|
||||
index={index}
|
||||
isMulti={Boolean(currentQuestion.content.multi)}
|
||||
own={Boolean(variant.isOwn)}
|
||||
questionLargeCheck={true}
|
||||
answer={answer}
|
||||
ownPlaceholder={currentQuestion.content?.ownPlaceholder || ""}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</RadioGroup>
|
||||
</Box>
|
||||
);
|
||||
};
|
150
lib/components/ViewPublicationPage/questions/File/UploadFile.tsx
Normal file
150
lib/components/ViewPublicationPage/questions/File/UploadFile.tsx
Normal file
@ -0,0 +1,150 @@
|
||||
import { useState } from "react";
|
||||
import { Box, ButtonBase, Skeleton, Typography, useTheme } from "@mui/material";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
|
||||
import { sendAnswer, sendFile } from "@api/quizRelase";
|
||||
import { useRootContainerSize } from "@contexts/RootContainerWidthContext";
|
||||
import { useQuizViewStore } from "@stores/quizView";
|
||||
|
||||
import {
|
||||
ACCEPT_SEND_FILE_TYPES_MAP,
|
||||
MAX_FILE_SIZE,
|
||||
UPLOAD_FILE_DESCRIPTIONS_MAP,
|
||||
} from "@/components/ViewPublicationPage/tools/fileUpload";
|
||||
|
||||
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";
|
||||
|
||||
type UploadFileProps = {
|
||||
currentQuestion: QuizQuestionFile;
|
||||
setModalWarningType: (modalType: ModalWarningType) => void;
|
||||
isSending: boolean;
|
||||
setIsSending: (isSending: boolean) => void;
|
||||
};
|
||||
|
||||
export const UploadFile = ({ currentQuestion, setModalWarningType, isSending, setIsSending }: UploadFileProps) => {
|
||||
const { quizId, preview } = useQuizStore();
|
||||
const [isDropzoneHighlighted, setIsDropzoneHighlighted] = useState<boolean>(false);
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const answers = useQuizViewStore((state) => state.answers);
|
||||
const { updateAnswer } = useQuizViewStore((state) => state);
|
||||
const isMobile = useRootContainerSize() < 500;
|
||||
|
||||
const answer = answers.find(({ questionId }) => questionId === currentQuestion.id)?.answer as string;
|
||||
|
||||
const uploadFile = async (file: File | undefined) => {
|
||||
if (isSending) 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) =>
|
||||
file.name.toLowerCase().endsWith(fileType)
|
||||
);
|
||||
|
||||
if (!isFileTypeAccepted) return setModalWarningType("errorType");
|
||||
|
||||
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"));
|
||||
}
|
||||
|
||||
setIsSending(false);
|
||||
};
|
||||
|
||||
const onDrop = (event: React.DragEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
setIsDropzoneHighlighted(false);
|
||||
|
||||
const file = event.dataTransfer.files[0];
|
||||
|
||||
uploadFile(file);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ display: "flex", alignItems: "center" }}>
|
||||
{isSending ? (
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
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
|
||||
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 />
|
||||
<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>
|
||||
)}
|
||||
<Info
|
||||
sx={{ width: "40px", height: "40px" }}
|
||||
color={theme.palette.primary.main}
|
||||
onClick={() => setModalWarningType(currentQuestion.content.type)}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
@ -0,0 +1,75 @@
|
||||
import { Box, IconButton, Typography, useTheme } from "@mui/material";
|
||||
|
||||
import { sendAnswer } from "@api/quizRelase";
|
||||
import { useQuizViewStore } from "@stores/quizView";
|
||||
|
||||
import CloseBold from "@icons/CloseBold";
|
||||
|
||||
import type { QuizQuestionFile } from "@model/questionTypes/file";
|
||||
import { useQuizStore } from "@/stores/useQuizStore";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type UploadedFileProps = {
|
||||
currentQuestion: QuizQuestionFile;
|
||||
setIsSending: (isSending: boolean) => void;
|
||||
};
|
||||
|
||||
export const UploadedFile = ({ currentQuestion, setIsSending }: UploadedFileProps) => {
|
||||
const { quizId, preview } = useQuizStore();
|
||||
const answers = useQuizViewStore((state) => state.answers);
|
||||
const { updateAnswer } = useQuizViewStore((state) => state);
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const answer = answers.find(({ questionId }) => questionId === currentQuestion.id)?.answer as string;
|
||||
|
||||
const deleteFile = async () => {
|
||||
if (answer.length > 0) {
|
||||
setIsSending(true);
|
||||
|
||||
await sendAnswer({
|
||||
questionId: currentQuestion.id,
|
||||
body: "",
|
||||
qid: quizId,
|
||||
preview,
|
||||
});
|
||||
}
|
||||
|
||||
updateAnswer(currentQuestion.id, "", 0);
|
||||
setIsSending(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: "15px" }}>
|
||||
<Typography color={theme.palette.text.primary}>{t("You have uploaded")}:</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
padding: "5px 5px 5px 16px",
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
borderRadius: "8px",
|
||||
color: "#FFFFFF",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
overflow: "hidden",
|
||||
gap: "15px",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
whiteSpace: "nowrap",
|
||||
textOverflow: "ellipsis",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{answer?.split("|")[0]}
|
||||
</Typography>
|
||||
<IconButton
|
||||
sx={{ p: 0 }}
|
||||
onClick={deleteFile}
|
||||
>
|
||||
<CloseBold />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
122
lib/components/ViewPublicationPage/questions/File/index.tsx
Normal file
122
lib/components/ViewPublicationPage/questions/File/index.tsx
Normal file
@ -0,0 +1,122 @@
|
||||
import { useState } from "react";
|
||||
import { Box, Modal, Typography, useTheme } from "@mui/material";
|
||||
|
||||
import { UploadFile } from "./UploadFile";
|
||||
import { UploadedFile } from "./UploadedFile";
|
||||
|
||||
import { useRootContainerSize } from "@contexts/RootContainerWidthContext";
|
||||
import { useQuizViewStore } from "@stores/quizView";
|
||||
|
||||
import { ACCEPT_SEND_FILE_TYPES_MAP } from "@/components/ViewPublicationPage/tools/fileUpload";
|
||||
|
||||
import type { QuizQuestionFile } from "@model/questionTypes/file";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export type ModalWarningType = "errorType" | "errorSize" | "picture" | "video" | "audio" | "document" | null;
|
||||
|
||||
type FileProps = {
|
||||
currentQuestion: QuizQuestionFile;
|
||||
};
|
||||
|
||||
export const File = ({ currentQuestion }: FileProps) => {
|
||||
const theme = useTheme();
|
||||
const answers = useQuizViewStore((state) => state.answers);
|
||||
const [modalWarningType, setModalWarningType] = useState<ModalWarningType>(null);
|
||||
const [isSending, setIsSending] = useState<boolean>(false);
|
||||
const isMobile = useRootContainerSize() < 500;
|
||||
|
||||
const answer = answers.find(({ questionId }) => questionId === currentQuestion.id)?.answer as string;
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Typography
|
||||
variant="h5"
|
||||
color={theme.palette.text.primary}
|
||||
sx={{ wordBreak: "break-word" }}
|
||||
>
|
||||
{currentQuestion.title}
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
width: "100%",
|
||||
marginTop: "20px",
|
||||
maxWidth: answer?.split("|")[0] ? "640px" : "600px",
|
||||
}}
|
||||
>
|
||||
{answer?.split("|")[0] ? (
|
||||
<UploadedFile
|
||||
currentQuestion={currentQuestion}
|
||||
setIsSending={setIsSending}
|
||||
/>
|
||||
) : (
|
||||
<UploadFile
|
||||
currentQuestion={currentQuestion}
|
||||
setModalWarningType={setModalWarningType}
|
||||
isSending={isSending}
|
||||
setIsSending={setIsSending}
|
||||
/>
|
||||
)}
|
||||
{answer && currentQuestion.content.type === "picture" && (
|
||||
<img
|
||||
src={answer.split("|")[1]}
|
||||
style={{ marginTop: "15px", maxWidth: "300px", maxHeight: "300px" }}
|
||||
alt=""
|
||||
/>
|
||||
)}
|
||||
{answer && currentQuestion.content.type === "video" && (
|
||||
<video
|
||||
src={answer.split("|")[1]}
|
||||
style={{
|
||||
marginTop: "15px",
|
||||
maxWidth: "300px",
|
||||
maxHeight: "300px",
|
||||
objectFit: "cover",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
<Modal
|
||||
open={modalWarningType !== null}
|
||||
onClose={() => setModalWarningType(null)}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
width: isMobile ? 300 : 400,
|
||||
bgcolor: "background.paper",
|
||||
borderRadius: 3,
|
||||
boxShadow: 24,
|
||||
p: 4,
|
||||
}}
|
||||
>
|
||||
<CurrentModal status={modalWarningType} />
|
||||
</Box>
|
||||
</Modal>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const CurrentModal = ({ status }: { status: ModalWarningType }) => {
|
||||
const { t } = useTranslation();
|
||||
switch (status) {
|
||||
case null:
|
||||
return null;
|
||||
case "errorType":
|
||||
return <Typography>{t("Incorrect file type selected")}</Typography>;
|
||||
case "errorSize":
|
||||
return <Typography>{t("File is too big. Maximum size is 50 MB")}</Typography>;
|
||||
|
||||
default:
|
||||
return (
|
||||
<>
|
||||
<Typography>{t("Acceptable file extensions")}:</Typography>
|
||||
<Typography>{ACCEPT_SEND_FILE_TYPES_MAP[status].join(" ")}</Typography>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
@ -0,0 +1,282 @@
|
||||
import type { QuestionVariant, QuestionVariantWithEditedImages } from "@/model/questionTypes/shared";
|
||||
import { Box, Checkbox, FormControlLabel, Input, Radio, TextareaAutosize, Typography, useTheme } from "@mui/material";
|
||||
import { useQuizViewStore } from "@stores/quizView";
|
||||
import RadioCheck from "@ui_kit/RadioCheck";
|
||||
import RadioIcon from "@ui_kit/RadioIcon";
|
||||
import { quizThemes } from "@utils/themes/Publication/themePublication";
|
||||
import { useMemo, type MouseEvent, useRef, useEffect } from "react";
|
||||
import { useRootContainerSize } from "@contexts/RootContainerWidthContext";
|
||||
import { useQuizStore } from "@/stores/useQuizStore";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { OwnImage } from "./OwnImage";
|
||||
import { useSnackbar } from "notistack";
|
||||
|
||||
type ImagesProps = {
|
||||
questionId: string;
|
||||
variant: QuestionVariantWithEditedImages;
|
||||
index: number;
|
||||
answer: string | string[] | undefined;
|
||||
isMulti: boolean;
|
||||
own: boolean;
|
||||
questionLargeCheck: boolean;
|
||||
ownPlaceholder: string;
|
||||
};
|
||||
|
||||
interface OwnInputProps {
|
||||
questionId: string;
|
||||
variant: QuestionVariant;
|
||||
largeCheck: boolean;
|
||||
ownPlaceholder: string;
|
||||
}
|
||||
const OwnInput = ({ variant, largeCheck, ownPlaceholder }: OwnInputProps) => {
|
||||
const theme = useTheme();
|
||||
const ownVariants = useQuizViewStore((state) => state.ownVariants);
|
||||
const { updateOwnVariant } = useQuizViewStore((state) => state);
|
||||
|
||||
const ownAnswer = ownVariants[ownVariants.findIndex((v) => v.id === variant.id)]?.variant.answer || "";
|
||||
|
||||
return largeCheck ? (
|
||||
<Box sx={{ overflow: "auto" }}>
|
||||
<TextareaAutosize
|
||||
placeholder={ownPlaceholder || "|"}
|
||||
style={{
|
||||
resize: "none",
|
||||
width: "100%",
|
||||
fontSize: "16px",
|
||||
color: ownAnswer.length === 0 ? "ownPlaceholder" : theme.palette.text.primary,
|
||||
letterSpacing: "-0.4px",
|
||||
wordSpacing: "-3px",
|
||||
outline: "0px none",
|
||||
backgroundColor: "inherit",
|
||||
border: "none",
|
||||
//@ts-ignore
|
||||
"&::-webkit-scrollbar": {
|
||||
width: "4px",
|
||||
},
|
||||
"&::-webkit-scrollbar-thumb": {
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
},
|
||||
scrollbarColor: theme.palette.primary.main,
|
||||
}}
|
||||
value={ownAnswer}
|
||||
onClick={(e: React.MouseEvent<HTMLTextAreaElement>) => e.stopPropagation()}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
updateOwnVariant(variant.id, e.target.value);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
<Input
|
||||
placeholder={ownPlaceholder || "|"}
|
||||
sx={{
|
||||
backgroundColor: "inherit",
|
||||
width: "100%",
|
||||
fontSize: "18px",
|
||||
color: ownAnswer.length === 0 ? "ownPlaceholder" : theme.palette.text.primary,
|
||||
}}
|
||||
value={ownAnswer}
|
||||
disableUnderline
|
||||
onClick={(e: React.MouseEvent<HTMLElement>) => e.stopPropagation()}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
updateOwnVariant(variant.id, e.target.value);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const ImageVariant = ({
|
||||
questionId,
|
||||
answer,
|
||||
isMulti,
|
||||
variant,
|
||||
index,
|
||||
own,
|
||||
questionLargeCheck,
|
||||
ownPlaceholder,
|
||||
}: ImagesProps) => {
|
||||
const { settings } = useQuizStore();
|
||||
const { deleteAnswer, updateAnswer } = useQuizViewStore((state) => state);
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const isMobile = useRootContainerSize() < 450;
|
||||
const isTablet = useRootContainerSize() < 850;
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
|
||||
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||
|
||||
const onVariantClick = async (event: MouseEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
|
||||
const variantId = variant.id;
|
||||
if (isMulti) {
|
||||
const currentAnswer = typeof answer !== "string" ? answer || [] : [];
|
||||
|
||||
return updateAnswer(
|
||||
questionId,
|
||||
currentAnswer.includes(variantId)
|
||||
? currentAnswer?.filter((item) => item !== variantId)
|
||||
: [...currentAnswer, variantId],
|
||||
variant.points || 0
|
||||
);
|
||||
}
|
||||
|
||||
updateAnswer(questionId, variantId, variant.points || 0);
|
||||
|
||||
if (answer === variantId) {
|
||||
deleteAnswer(questionId);
|
||||
}
|
||||
};
|
||||
|
||||
const choiceImgUrl = useMemo(() => {
|
||||
if (variant.editedUrlImagesList !== undefined && variant.editedUrlImagesList !== null) {
|
||||
return variant.editedUrlImagesList[isMobile ? "mobile" : isTablet ? "tablet" : "desktop"];
|
||||
} else {
|
||||
return variant.extendedText;
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (canvasRef.current !== null) {
|
||||
const canvas = canvasRef.current;
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (ctx !== null) {
|
||||
const img = new Image();
|
||||
img.src = choiceImgUrl;
|
||||
|
||||
img.onload = () => {
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
||||
};
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
position: "relative",
|
||||
cursor: "pointer",
|
||||
borderRadius: "12px",
|
||||
border: `1px solid`,
|
||||
borderColor: !!answer?.includes(variant.id) ? theme.palette.primary.main : "#9A9AAF",
|
||||
"&:hover": { borderColor: theme.palette.primary.main },
|
||||
background:
|
||||
settings.cfg.design && !quizThemes[settings.cfg.theme].isLight
|
||||
? "rgba(255,255,255, 0.3)"
|
||||
: (settings.cfg.design && quizThemes[settings.cfg.theme].isLight) || quizThemes[settings.cfg.theme].isLight
|
||||
? "#FFFFFF"
|
||||
: "transparent",
|
||||
}}
|
||||
onClick={onVariantClick}
|
||||
>
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: "10px" }}>
|
||||
<Box sx={{ width: "100%", height: "300px" }}>
|
||||
{own ? (
|
||||
<OwnImage
|
||||
imageUrl={choiceImgUrl}
|
||||
questionId={questionId}
|
||||
variantId={variant.id}
|
||||
onValidationError={(errorType) => {
|
||||
enqueueSnackbar(errorType === "size" ? t("file is too big") : t("file type is not supported"), {
|
||||
variant: "warning",
|
||||
});
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
variant.extendedText && (
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
style={{
|
||||
display: "block",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "cover",
|
||||
borderRadius: "12px 12px 0 0",
|
||||
}}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
{own && (
|
||||
<Typography
|
||||
sx={{
|
||||
color: theme.palette.text.primary,
|
||||
fontSize: "14px",
|
||||
pl: "15px",
|
||||
}}
|
||||
>
|
||||
{t("Enter your answer")}
|
||||
</Typography>
|
||||
)}
|
||||
<FormControlLabel
|
||||
key={variant.id}
|
||||
sx={{
|
||||
textAlign: "center",
|
||||
color: theme.palette.text.primary,
|
||||
marginTop: "10px",
|
||||
marginLeft: 0,
|
||||
padding: "10px",
|
||||
display: "flex",
|
||||
alignItems: variant.answer.length <= 60 ? "center" : "flex-start",
|
||||
justifyContent: "center",
|
||||
position: "relative",
|
||||
height: "80px",
|
||||
"& .MuiFormControlLabel-label": {
|
||||
wordBreak: "break-word",
|
||||
height: variant.answer.length <= 60 ? undefined : "60px",
|
||||
lineHeight: "normal",
|
||||
overflow: "auto",
|
||||
maxHeight: "100%",
|
||||
width: "100%",
|
||||
"&::-webkit-scrollbar": {
|
||||
width: "4px",
|
||||
},
|
||||
"&::-webkit-scrollbar-thumb": {
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
},
|
||||
scrollbarColor: theme.palette.primary.main,
|
||||
},
|
||||
}}
|
||||
value={index}
|
||||
control={
|
||||
isMulti ? (
|
||||
<Checkbox
|
||||
checked={!!answer?.includes(variant.id)}
|
||||
checkedIcon={<RadioCheck color={theme.palette.primary.main} />}
|
||||
icon={<RadioIcon />}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: "-297px",
|
||||
right: 0,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Radio
|
||||
checkedIcon={<RadioCheck color={theme.palette.primary.main} />}
|
||||
icon={<RadioIcon />}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: "-297px",
|
||||
right: 0,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
label={
|
||||
own ? (
|
||||
<OwnInput
|
||||
questionId={questionId}
|
||||
variant={variant}
|
||||
largeCheck={questionLargeCheck}
|
||||
ownPlaceholder={ownPlaceholder || "|"}
|
||||
/>
|
||||
) : (
|
||||
variant.answer
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
187
lib/components/ViewPublicationPage/questions/Images/OwnImage.tsx
Normal file
187
lib/components/ViewPublicationPage/questions/Images/OwnImage.tsx
Normal file
@ -0,0 +1,187 @@
|
||||
import { Box, ButtonBase, IconButton, Typography, useTheme } from "@mui/material";
|
||||
import { useState, useRef } from "react";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useQuizStore } from "@/stores/useQuizStore";
|
||||
import { useQuizViewStore } from "@/stores/quizView";
|
||||
import { useSnackbar } from "notistack";
|
||||
import { Skeleton } from "@mui/material";
|
||||
import UploadIcon from "@/assets/icons/UploadIcon";
|
||||
import { sendFile } from "@/api/quizRelase";
|
||||
import { ACCEPT_SEND_FILE_TYPES_MAP, MAX_FILE_SIZE } from "../../tools/fileUpload";
|
||||
|
||||
// Пропсы компонента
|
||||
export type OwnImageProps = {
|
||||
imageUrl?: string;
|
||||
questionId: string;
|
||||
variantId: string;
|
||||
onValidationError: (error: "size" | "type") => void;
|
||||
};
|
||||
|
||||
export const OwnImage = ({ imageUrl, questionId, variantId, onValidationError }: OwnImageProps) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const { quizId, preview } = useQuizStore();
|
||||
const { ownVariants, updateOwnVariant } = useQuizViewStore((state) => state);
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// Получаем ownVariant для этого варианта
|
||||
const ownVariantData = ownVariants.find((v) => v.id === variantId);
|
||||
|
||||
// Загрузка файла
|
||||
const uploadImage = async (file: File) => {
|
||||
if (isUploading) return;
|
||||
if (!file) return;
|
||||
if (file.size > MAX_FILE_SIZE) {
|
||||
onValidationError("size");
|
||||
return;
|
||||
}
|
||||
const isFileTypeAccepted = ACCEPT_SEND_FILE_TYPES_MAP.picture.some((fileType) =>
|
||||
file.name.toLowerCase().endsWith(fileType)
|
||||
);
|
||||
if (!isFileTypeAccepted) {
|
||||
onValidationError("type");
|
||||
return;
|
||||
}
|
||||
setIsUploading(true);
|
||||
try {
|
||||
const data = await sendFile({
|
||||
questionId,
|
||||
body: { file, name: file.name, preview },
|
||||
qid: quizId,
|
||||
});
|
||||
const fileId = data?.data.fileIDMap[questionId];
|
||||
const localImageUrl = URL.createObjectURL(file);
|
||||
updateOwnVariant(variantId, "", "", fileId, localImageUrl);
|
||||
} catch (error) {
|
||||
console.error("Error uploading image:", error);
|
||||
enqueueSnackbar(t("The answer was not counted"));
|
||||
} finally {
|
||||
setIsUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Обработчик выбора файла
|
||||
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (file) {
|
||||
uploadImage(file);
|
||||
}
|
||||
};
|
||||
|
||||
// Открытие диалога выбора файла
|
||||
const handleClick = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
if (fileInputRef.current) fileInputRef.current.value = "";
|
||||
fileInputRef.current?.click();
|
||||
};
|
||||
|
||||
// Удаление изображения
|
||||
const handleRemoveImage = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
updateOwnVariant(variantId, ownVariantData?.variant.answer || "", "", "", "");
|
||||
/*
|
||||
1 - answer - письменный ответ
|
||||
2 - extendedText - строка используется в эмодзи-вопросах для хранения выбранного эмодзи
|
||||
3 - originalImageUrl - полный URL изображения, загруженного на сервер
|
||||
4 - localImageUrl - временный URL для отображения изображения в браузере
|
||||
*/
|
||||
};
|
||||
|
||||
// Определяем, что показывать
|
||||
let imageToDisplay: string | null = null;
|
||||
if (ownVariantData?.variant.localImageUrl) {
|
||||
imageToDisplay = ownVariantData.variant.localImageUrl;
|
||||
} else if (imageUrl) {
|
||||
imageToDisplay = imageUrl;
|
||||
}
|
||||
|
||||
if (isUploading) {
|
||||
return (
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
sx={{ width: "100%", height: "100%", borderRadius: "12px" }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ButtonBase
|
||||
component="div"
|
||||
onClick={handleClick}
|
||||
disabled={isUploading}
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderRadius: "12px",
|
||||
transition: "border-color 0.3s, background-color 0.3s",
|
||||
overflow: "hidden",
|
||||
position: "relative",
|
||||
opacity: isUploading ? 0.7 : 1,
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
onChange={handleFileChange}
|
||||
accept={ACCEPT_SEND_FILE_TYPES_MAP.picture.join(",")}
|
||||
hidden
|
||||
/>
|
||||
{imageToDisplay ? (
|
||||
<>
|
||||
<Box sx={{ width: "100%", height: "100%", position: "relative" }}>
|
||||
<img
|
||||
src={imageToDisplay}
|
||||
alt="Preview"
|
||||
style={{ width: "100%", height: "100%", objectFit: "cover" }}
|
||||
/>
|
||||
</Box>
|
||||
<IconButton
|
||||
onClick={handleRemoveImage}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: 8,
|
||||
left: 8,
|
||||
zIndex: 1,
|
||||
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
||||
color: "white",
|
||||
height: "25px",
|
||||
width: "25px",
|
||||
display: ownVariantData?.variant.localImageUrl ? "inherit" : "none",
|
||||
"&:hover": {
|
||||
backgroundColor: "rgba(0, 0, 0, 0.7)",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</>
|
||||
) : (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
opacity: 0.5,
|
||||
}}
|
||||
>
|
||||
<UploadIcon />
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
sx={{ p: 2, textAlign: "center" }}
|
||||
>
|
||||
{t("Add your image")}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</ButtonBase>
|
||||
);
|
||||
};
|
@ -0,0 +1,71 @@
|
||||
import { useRootContainerSize } from "@contexts/RootContainerWidthContext";
|
||||
import type { QuizQuestionImages } from "@model/questionTypes/images";
|
||||
import { Box, RadioGroup, Typography, useTheme } from "@mui/material";
|
||||
import { createQuizViewStore, useQuizViewStore } from "@stores/quizView";
|
||||
import { ImageVariant } from "./ImageVariant";
|
||||
import moment from "moment";
|
||||
|
||||
type ImagesProps = {
|
||||
currentQuestion: QuizQuestionImages;
|
||||
};
|
||||
|
||||
export const Images = ({ currentQuestion }: ImagesProps) => {
|
||||
const answers = useQuizViewStore((state) => state.answers);
|
||||
const theme = useTheme();
|
||||
const answer = answers.find(({ questionId }) => questionId === currentQuestion.id)?.answer;
|
||||
const isTablet = useRootContainerSize() < 1000;
|
||||
const isMobile = useRootContainerSize() < 500;
|
||||
|
||||
if (moment.isMoment(answer)) throw new Error("Answer is Moment in Variant question");
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Typography
|
||||
variant="h5"
|
||||
color={theme.palette.text.primary}
|
||||
sx={{ wordBreak: "break-word" }}
|
||||
>
|
||||
{currentQuestion.title}
|
||||
</Typography>
|
||||
<RadioGroup
|
||||
name={currentQuestion.id.toString()}
|
||||
value={currentQuestion.content.variants.findIndex(({ id }) => answer === id)}
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
marginTop: "20px",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "grid",
|
||||
gap: "15px",
|
||||
gridTemplateColumns: isTablet ? (isMobile ? "repeat(1, 1fr)" : "repeat(2, 1fr)") : "repeat(3, 1fr)",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
{currentQuestion.content.variants
|
||||
.filter((v) => {
|
||||
if (!v.isOwn) return true;
|
||||
return v.isOwn && currentQuestion.content.own;
|
||||
})
|
||||
.map((variant, index) => (
|
||||
<ImageVariant
|
||||
key={variant.id}
|
||||
questionId={currentQuestion.id}
|
||||
variant={variant}
|
||||
index={index}
|
||||
answer={answer}
|
||||
isMulti={Boolean(currentQuestion.content.multi)}
|
||||
own={Boolean(variant.isOwn)}
|
||||
questionLargeCheck={true}
|
||||
ownPlaceholder={currentQuestion.content?.ownPlaceholder || ""}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</RadioGroup>
|
||||
</Box>
|
||||
);
|
||||
};
|
387
lib/components/ViewPublicationPage/questions/Number/index.tsx
Normal file
387
lib/components/ViewPublicationPage/questions/Number/index.tsx
Normal file
@ -0,0 +1,387 @@
|
||||
import { useQuizStore } from "@/stores/useQuizStore";
|
||||
import type { QuizQuestionNumber } from "@model/questionTypes/number";
|
||||
import { Box, Typography, useTheme } from "@mui/material";
|
||||
import { useQuizViewStore } from "@stores/quizView";
|
||||
import { CustomSlider } from "@ui_kit/CustomSlider";
|
||||
import CustomTextField from "@ui_kit/CustomTextField";
|
||||
import { quizThemes } from "@utils/themes/Publication/themePublication";
|
||||
import type { ChangeEvent, SyntheticEvent } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
|
||||
type NumberProps = {
|
||||
currentQuestion: QuizQuestionNumber;
|
||||
};
|
||||
|
||||
export const Number = ({ currentQuestion }: NumberProps) => {
|
||||
const [inputValue, setInputValue] = useState<string>("0");
|
||||
const [minRange, setMinRange] = useState<string>("0");
|
||||
const [maxRange, setMaxRange] = useState<string>("100000000000");
|
||||
const [reversedInputValue, setReversedInputValue] = useState<string>("0");
|
||||
const [reversedMinRange, setReversedMinRange] = useState<string>("0");
|
||||
const [reversedMaxRange, setReversedMaxRange] = useState<string>("100000000000");
|
||||
const { settings } = useQuizStore();
|
||||
const { updateAnswer } = useQuizViewStore((state) => state);
|
||||
const answers = useQuizViewStore((state) => state.answers);
|
||||
const theme = useTheme();
|
||||
|
||||
const [minBorder, maxBorder] = currentQuestion.content.range.split("—").map(window.Number);
|
||||
const min = minBorder < maxBorder ? minBorder : maxBorder;
|
||||
const max = minBorder < maxBorder ? maxBorder : minBorder;
|
||||
const reversed = minBorder > maxBorder;
|
||||
|
||||
const answer = answers.find(({ questionId }) => questionId === currentQuestion.id)?.answer as string;
|
||||
|
||||
const sliderValue =
|
||||
answer ||
|
||||
(reversed ? max + min - currentQuestion.content.start + "—" + max : currentQuestion.content.start + "—" + max);
|
||||
|
||||
const sendAnswerToBackend = async (value: string, noUpdate = false) => {
|
||||
if (!noUpdate) {
|
||||
updateAnswer(currentQuestion.id, value, 0);
|
||||
}
|
||||
};
|
||||
|
||||
const updateValueDebounced = useDebouncedCallback(async (value: string) => {
|
||||
if (reversed) {
|
||||
const newValue =
|
||||
window.Number(value) < window.Number(min)
|
||||
? String(min)
|
||||
: window.Number(value) > window.Number(max)
|
||||
? String(max)
|
||||
: value;
|
||||
|
||||
setReversedInputValue(newValue);
|
||||
updateAnswer(currentQuestion.id, String(max + min - window.Number(newValue)), 0);
|
||||
await sendAnswerToBackend(String(window.Number(newValue)), true);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const newValue =
|
||||
window.Number(value) < window.Number(minRange)
|
||||
? minRange
|
||||
: window.Number(value) > window.Number(maxRange)
|
||||
? maxRange
|
||||
: value;
|
||||
|
||||
setInputValue(newValue);
|
||||
await sendAnswerToBackend(newValue);
|
||||
}, 1000);
|
||||
|
||||
const updateMinRangeDebounced = useDebouncedCallback(async (value: string, crowded = false) => {
|
||||
if (reversed) {
|
||||
const newMinRange = crowded
|
||||
? window.Number(value.split("—")[1])
|
||||
: max + min - window.Number(value.split("—")[0]) < min
|
||||
? min
|
||||
: max + min - window.Number(value.split("—")[0]);
|
||||
|
||||
const newMinValue = window.Number(value.split("—")[0]) > max ? String(max) : value.split("—")[0];
|
||||
|
||||
setReversedMinRange(crowded ? String(max + min - window.Number(newMinValue)) : newMinValue);
|
||||
updateAnswer(currentQuestion.id, `${newMinRange}—${value.split("—")[1]}`, 0);
|
||||
await sendAnswerToBackend(`${newMinValue}—${value.split("—")[1]}`, true);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const newMinValue = crowded
|
||||
? maxRange
|
||||
: window.Number(value.split("—")[0]) < min
|
||||
? String(min)
|
||||
: value.split("—")[0];
|
||||
|
||||
setMinRange(newMinValue);
|
||||
await sendAnswerToBackend(`${newMinValue}—${value.split("—")[1]}`);
|
||||
}, 1000);
|
||||
|
||||
const updateMaxRangeDebounced = useDebouncedCallback(async (value: string, crowded = false) => {
|
||||
if (reversed) {
|
||||
const newMaxRange = crowded
|
||||
? window.Number(value.split("—")[1])
|
||||
: max + min - window.Number(value.split("—")[1]) > max
|
||||
? max
|
||||
: max + min - window.Number(value.split("—")[1]);
|
||||
|
||||
const newMaxValue = window.Number(value.split("—")[1]) < min ? String(min) : value.split("—")[1];
|
||||
|
||||
setReversedMaxRange(crowded ? String(max + min - window.Number(newMaxValue)) : newMaxValue);
|
||||
updateAnswer(currentQuestion.id, `${value.split("—")[0]}—${newMaxRange}`, 0);
|
||||
await sendAnswerToBackend(`${value.split("—")[0]}—${newMaxValue}`, true);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const newMaxValue = crowded
|
||||
? minRange
|
||||
: window.Number(value.split("—")[1]) > max
|
||||
? String(max)
|
||||
: value.split("—")[1];
|
||||
|
||||
setMaxRange(newMaxValue);
|
||||
await sendAnswerToBackend(`${value.split("—")[0]}—${newMaxValue}`);
|
||||
}, 1000);
|
||||
|
||||
useEffect(() => {
|
||||
if (answer) {
|
||||
if (answer.includes("—")) {
|
||||
if (reversed) {
|
||||
setReversedMinRange(String(max + min - window.Number(answer.split("—")[0])));
|
||||
setReversedMaxRange(String(max + min - window.Number(answer.split("—")[1])));
|
||||
} else {
|
||||
setMinRange(answer.split("—")[0]);
|
||||
setMaxRange(answer.split("—")[1]);
|
||||
}
|
||||
} else {
|
||||
if (reversed) {
|
||||
setReversedInputValue(String(max + min - window.Number(answer)));
|
||||
} else {
|
||||
setInputValue(answer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!answer) {
|
||||
setMinRange(String(currentQuestion.content.start));
|
||||
setMaxRange(String(max));
|
||||
|
||||
if (currentQuestion.content.chooseRange) {
|
||||
setReversedMinRange(String(currentQuestion.content.start));
|
||||
setReversedMaxRange(String(min));
|
||||
}
|
||||
|
||||
setReversedInputValue(String(currentQuestion.content.start));
|
||||
setInputValue(String(currentQuestion.content.start));
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const onSliderChange = (_: Event, value: number | number[]) => {
|
||||
const range = Array.isArray(value) ? `${value[0]}—${value[1]}` : String(value);
|
||||
|
||||
updateAnswer(currentQuestion.id, range, 0);
|
||||
};
|
||||
|
||||
const onChangeCommitted = async (_: Event | SyntheticEvent<Element, Event>, value: number | number[]) => {
|
||||
if (currentQuestion.content.chooseRange && Array.isArray(value)) {
|
||||
if (reversed) {
|
||||
const newMinReversedValue = String(max + min - value[0]);
|
||||
const newMaxReversedValue = String(max + min - value[1]);
|
||||
|
||||
setMinRange(String(value[0]));
|
||||
setMaxRange(String(value[1]));
|
||||
setReversedMinRange(newMinReversedValue);
|
||||
setReversedMaxRange(newMaxReversedValue);
|
||||
await sendAnswerToBackend(`${newMinReversedValue}—${newMaxReversedValue}`, true);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setMinRange(String(value[0]));
|
||||
setMaxRange(String(value[1]));
|
||||
await sendAnswerToBackend(`${value[0]}—${value[1]}`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (reversed) {
|
||||
setReversedInputValue(String(max + min - window.Number(value)));
|
||||
} else {
|
||||
setInputValue(String(value));
|
||||
}
|
||||
|
||||
await sendAnswerToBackend(String(value));
|
||||
};
|
||||
|
||||
const changeValueLabelFormat = (value: number) => {
|
||||
if (!reversed) {
|
||||
return value;
|
||||
}
|
||||
|
||||
const [minSliderBorder, maxSliderBorder] = sliderValue.split("—").map(window.Number);
|
||||
|
||||
if (value === minSliderBorder) {
|
||||
return max + min - minSliderBorder;
|
||||
}
|
||||
|
||||
return max + min - maxSliderBorder;
|
||||
};
|
||||
|
||||
const onInputChange = ({ target }: ChangeEvent<HTMLInputElement>) => {
|
||||
const value = target.value.replace(/\D/g, "");
|
||||
|
||||
if (reversed) {
|
||||
setReversedInputValue(value);
|
||||
} else {
|
||||
setInputValue(value);
|
||||
}
|
||||
|
||||
updateValueDebounced(value);
|
||||
};
|
||||
|
||||
const onMinInputChange = ({ target }: ChangeEvent<HTMLInputElement>) => {
|
||||
const newValue = target.value.replace(/\D/g, "");
|
||||
|
||||
if (reversed) {
|
||||
setReversedMinRange(newValue);
|
||||
|
||||
if (window.Number(newValue) <= window.Number(reversedMaxRange)) {
|
||||
const value = max + min - window.Number(reversedMaxRange);
|
||||
updateMinRangeDebounced(`${value}—${value}`, true);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
updateMinRangeDebounced(`${newValue}—${max + min - window.Number(reversedMaxRange)}`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setMinRange(newValue);
|
||||
|
||||
if (window.Number(newValue) >= window.Number(maxRange)) {
|
||||
updateMinRangeDebounced(`${maxRange}—${maxRange}`, true);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
updateMinRangeDebounced(`${newValue}—${maxRange}`);
|
||||
};
|
||||
|
||||
const onMaxInputChange = ({ target }: ChangeEvent<HTMLInputElement>) => {
|
||||
const newValue = target.value.replace(/\D/g, "");
|
||||
|
||||
if (reversed) {
|
||||
setReversedMaxRange(newValue);
|
||||
|
||||
if (window.Number(newValue) >= window.Number(reversedMinRange)) {
|
||||
const value = max + min - window.Number(reversedMinRange);
|
||||
updateMaxRangeDebounced(`${value}—${value}`, true);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
updateMaxRangeDebounced(`${max + min - window.Number(reversedMinRange)}—${newValue}`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setMaxRange(newValue);
|
||||
|
||||
if (window.Number(newValue) <= window.Number(minRange)) {
|
||||
updateMaxRangeDebounced(`${minRange}—${minRange}`, true);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
updateMaxRangeDebounced(`${minRange}—${newValue}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Typography
|
||||
variant="h5"
|
||||
color={theme.palette.text.primary}
|
||||
sx={{ wordBreak: "break-word" }}
|
||||
>
|
||||
{currentQuestion.title}
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
width: "100%",
|
||||
marginTop: "20px",
|
||||
gap: "30px",
|
||||
padding: "0 30px",
|
||||
}}
|
||||
>
|
||||
<CustomSlider
|
||||
value={
|
||||
currentQuestion.content.chooseRange
|
||||
? sliderValue.split("—").length || 0 > 1
|
||||
? sliderValue.split("—").map((item) => window.Number(item))
|
||||
: [min, min + 1]
|
||||
: window.Number(sliderValue.split("—")[0])
|
||||
}
|
||||
min={min}
|
||||
max={max}
|
||||
step={currentQuestion.content.step || 1}
|
||||
onChange={onSliderChange}
|
||||
onChangeCommitted={onChangeCommitted}
|
||||
valueLabelFormat={changeValueLabelFormat}
|
||||
sx={{
|
||||
color: theme.palette.primary.main,
|
||||
"& .MuiSlider-valueLabel": {
|
||||
background: theme.palette.primary.main,
|
||||
borderRadius: "8px",
|
||||
minWidth: "60px",
|
||||
height: "36px",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
{!currentQuestion.content.chooseRange && (
|
||||
<CustomTextField
|
||||
placeholder="0"
|
||||
value={reversed ? reversedInputValue : inputValue}
|
||||
onChange={onInputChange}
|
||||
sx={{
|
||||
maxWidth: "80px",
|
||||
borderColor: theme.palette.text.primary,
|
||||
"& .MuiOutlinedInput-root": { background: "transparent" },
|
||||
"& .MuiInputBase-input": { textAlign: "center", zIndex: 1 },
|
||||
"& .MuiOutlinedInput-notchedOutline": {
|
||||
backgroundColor: quizThemes[settings.cfg.theme].isLight ? "white" : theme.palette.background.default,
|
||||
borderColor: "#9A9AAF",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{currentQuestion.content.chooseRange && (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
gap: "15px",
|
||||
alignItems: "center",
|
||||
"& .MuiFormControl-root": { width: "auto" },
|
||||
}}
|
||||
>
|
||||
<CustomTextField
|
||||
placeholder="0"
|
||||
value={reversed ? String(reversedMinRange) : minRange}
|
||||
onChange={onMinInputChange}
|
||||
sx={{
|
||||
maxWidth: "80px",
|
||||
borderColor: theme.palette.text.primary,
|
||||
"& .MuiOutlinedInput-root": { background: "transparent" },
|
||||
"& .MuiInputBase-input": { textAlign: "center", zIndex: 1 },
|
||||
"& .MuiOutlinedInput-notchedOutline": {
|
||||
backgroundColor: quizThemes[settings.cfg.theme].isLight ? "white" : theme.palette.background.default,
|
||||
borderColor: "#9A9AAF",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Typography color={theme.palette.text.primary}>до</Typography>
|
||||
<CustomTextField
|
||||
placeholder="0"
|
||||
value={reversed ? String(reversedMaxRange) : maxRange}
|
||||
onChange={onMaxInputChange}
|
||||
sx={{
|
||||
maxWidth: "80px",
|
||||
"& .MuiOutlinedInput-root": { background: "transparent" },
|
||||
"& .MuiInputBase-input": { textAlign: "center", zIndex: 1 },
|
||||
"& .MuiOutlinedInput-notchedOutline": {
|
||||
backgroundColor: quizThemes[settings.cfg.theme].isLight ? "white" : theme.palette.background.default,
|
||||
borderColor: "#9A9AAF",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
76
lib/components/ViewPublicationPage/questions/Page/index.tsx
Normal file
76
lib/components/ViewPublicationPage/questions/Page/index.tsx
Normal file
@ -0,0 +1,76 @@
|
||||
import { Box, Typography, useTheme } from "@mui/material";
|
||||
import type { QuizQuestionPage } from "@model/questionTypes/page";
|
||||
import QuizVideo from "@/ui_kit/VideoIframe/VideoIframe";
|
||||
|
||||
type PageProps = {
|
||||
currentQuestion: QuizQuestionPage;
|
||||
};
|
||||
|
||||
export const Page = ({ currentQuestion }: PageProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Typography
|
||||
variant="h5"
|
||||
sx={{
|
||||
paddingBottom: "25px",
|
||||
color: theme.palette.text.primary,
|
||||
wordBreak: "break-word",
|
||||
}}
|
||||
>
|
||||
{currentQuestion.title}
|
||||
</Typography>
|
||||
<Typography
|
||||
color={theme.palette.text.primary}
|
||||
sx={{ wordBreak: "break-word" }}
|
||||
>
|
||||
{currentQuestion.content.text}
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
width: "100%",
|
||||
marginTop: "20px",
|
||||
}}
|
||||
>
|
||||
{currentQuestion.content.useImage
|
||||
? currentQuestion.content.back && (
|
||||
<Box
|
||||
sx={{
|
||||
borderRadius: "12px",
|
||||
border: "1px solid #9A9AAF",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
onClick={(event) => event.preventDefault()}
|
||||
>
|
||||
<img
|
||||
key={currentQuestion.id}
|
||||
src={currentQuestion.content.back}
|
||||
alt=""
|
||||
style={{
|
||||
display: "block",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "contain",
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
: currentQuestion.content.video && (
|
||||
<QuizVideo
|
||||
containerSX={{
|
||||
width: "100%",
|
||||
height: "calc(100% - 270px)",
|
||||
maxHeight: "80%",
|
||||
objectFit: "contain",
|
||||
aspectRatio: "16 / 9",
|
||||
}}
|
||||
videoUrl={currentQuestion.content.video}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
144
lib/components/ViewPublicationPage/questions/Rating/index.tsx
Normal file
144
lib/components/ViewPublicationPage/questions/Rating/index.tsx
Normal file
@ -0,0 +1,144 @@
|
||||
import { useRootContainerSize } from "@contexts/RootContainerWidthContext";
|
||||
import FlagIcon from "@icons/questionsPage/FlagIcon";
|
||||
import StarIconMini from "@icons/questionsPage/StarIconMini";
|
||||
import HashtagIcon from "@icons/questionsPage/hashtagIcon";
|
||||
import HeartIcon from "@icons/questionsPage/heartIcon";
|
||||
import LightbulbIcon from "@icons/questionsPage/lightbulbIcon";
|
||||
import LikeIcon from "@icons/questionsPage/likeIcon";
|
||||
import TropfyIcon from "@icons/questionsPage/tropfyIcon";
|
||||
import type { QuizQuestionRating } from "@model/questionTypes/rating";
|
||||
import { Box, Rating as RatingComponent, Typography, useTheme } from "@mui/material";
|
||||
import { useQuizViewStore } from "@stores/quizView";
|
||||
|
||||
const RATING_FORM_BUTTONS = [
|
||||
{
|
||||
name: "star",
|
||||
icon: (color: string, width: number) => (
|
||||
<StarIconMini
|
||||
width={width}
|
||||
color={color}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "trophie",
|
||||
icon: (color: string, width: number) => (
|
||||
<TropfyIcon
|
||||
width={width}
|
||||
color={color}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "flag",
|
||||
icon: (color: string, width: number) => (
|
||||
<FlagIcon
|
||||
width={width}
|
||||
color={color}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "heart",
|
||||
icon: (color: string, width: number) => (
|
||||
<HeartIcon
|
||||
width={width}
|
||||
color={color}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "like",
|
||||
icon: (color: string, width: number) => (
|
||||
<LikeIcon
|
||||
width={width}
|
||||
color={color}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "bubble",
|
||||
icon: (color: string, width: number) => (
|
||||
<LightbulbIcon
|
||||
width={width}
|
||||
color={color}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "hashtag",
|
||||
icon: (color: string, width: number) => (
|
||||
<HashtagIcon
|
||||
width={width}
|
||||
color={color}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
type RatingProps = {
|
||||
currentQuestion: QuizQuestionRating;
|
||||
};
|
||||
|
||||
export const Rating = ({ currentQuestion }: RatingProps) => {
|
||||
const { updateAnswer } = useQuizViewStore((state) => state);
|
||||
const answers = useQuizViewStore((state) => state.answers);
|
||||
const theme = useTheme();
|
||||
const isMobile = useRootContainerSize() < 650;
|
||||
const isTablet = useRootContainerSize() < 750;
|
||||
|
||||
const { answer } = answers.find(({ questionId }) => questionId === currentQuestion.id) ?? {};
|
||||
const form = RATING_FORM_BUTTONS.find(({ name }) => name === currentQuestion.content.form);
|
||||
|
||||
const sendRating = async (value: number | null) => {
|
||||
updateAnswer(currentQuestion.id, String(value), 0);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Typography
|
||||
variant="h5"
|
||||
color={theme.palette.text.primary}
|
||||
sx={{ wordBreak: "break-word" }}
|
||||
>
|
||||
{currentQuestion.title}
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: "20px",
|
||||
marginTop: "20px",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: "inline-block", width: "100%" }}>
|
||||
<RatingComponent
|
||||
value={Number(answer || 0)}
|
||||
onChange={(_, value) => sendRating(value)}
|
||||
sx={{
|
||||
height: "50px",
|
||||
opacity: "1!important",
|
||||
"& .MuiRating-root.Mui-disabled": { opacity: "1!important" },
|
||||
"& .MuiRating-icon": { mr: isMobile ? undefined : "15px" },
|
||||
}}
|
||||
max={currentQuestion.content.steps}
|
||||
icon={form?.icon(theme.palette.primary.main, isMobile ? 30 : isTablet ? 40 : 50)}
|
||||
emptyIcon={form?.icon("#9A9AAF", isMobile ? 30 : isTablet ? 40 : 50)}
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
gap: 2,
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<Typography sx={{ color: "#9A9AAF" }}>{currentQuestion.content.ratingNegativeDescription}</Typography>
|
||||
<Typography sx={{ color: "#9A9AAF" }}>{currentQuestion.content.ratingPositiveDescription}</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
@ -0,0 +1,66 @@
|
||||
import { Select as SelectComponent } from "@/components/ViewPublicationPage/tools/Select";
|
||||
import { useQuizStore } from "@/stores/useQuizStore";
|
||||
import type { QuizQuestionSelect } from "@model/questionTypes/select";
|
||||
import { Box, Typography, useTheme } from "@mui/material";
|
||||
import { useQuizViewStore } from "@stores/quizView";
|
||||
import { quizThemes } from "@utils/themes/Publication/themePublication";
|
||||
|
||||
type SelectProps = {
|
||||
currentQuestion: QuizQuestionSelect;
|
||||
};
|
||||
|
||||
export const Select = ({ currentQuestion }: SelectProps) => {
|
||||
const { settings } = useQuizStore();
|
||||
const { updateAnswer, deleteAnswer } = useQuizViewStore((state) => state);
|
||||
const answers = useQuizViewStore((state) => state.answers);
|
||||
const theme = useTheme();
|
||||
const { answer } = answers.find(({ questionId }) => questionId === currentQuestion.id) ?? {};
|
||||
|
||||
const sendSelectedAnswer = async (value: number) => {
|
||||
if (value < 0) {
|
||||
deleteAnswer(currentQuestion.id);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
updateAnswer(currentQuestion.id, String(value), 0);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Typography
|
||||
variant="h5"
|
||||
color={theme.palette.text.primary}
|
||||
sx={{ wordBreak: "break-word" }}
|
||||
>
|
||||
{currentQuestion.title}
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
width: "100%",
|
||||
marginTop: "20px",
|
||||
}}
|
||||
>
|
||||
<SelectComponent
|
||||
placeholder={currentQuestion.content.default}
|
||||
activeItemIndex={answer ? Number(answer) : -1}
|
||||
items={currentQuestion.content.variants.map(({ answer }) => answer)}
|
||||
colorMain={theme.palette.primary.main}
|
||||
sx={{
|
||||
"& .MuiSelect-select.MuiSelect-outlined": { zIndex: 1 },
|
||||
"& .MuiOutlinedInput-notchedOutline": {
|
||||
background: settings.cfg.design
|
||||
? quizThemes[settings.cfg.theme].isLight
|
||||
? "#F2F3F7"
|
||||
: "rgba(255,255,255, 0.3)"
|
||||
: "transparent",
|
||||
},
|
||||
}}
|
||||
onChange={(_, value) => sendSelectedAnswer(value)}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
@ -37,6 +37,7 @@ export const TextNormal = ({ currentQuestion, answer }: TextNormalProps) => {
|
||||
return currentQuestion.content.back;
|
||||
}
|
||||
}, [currentQuestion]);
|
||||
let isCrutch23022025 = window.location.pathname === "/bf8cae3a-e150-479d-befa-7f264087b223";
|
||||
return (
|
||||
<Box>
|
||||
<Typography
|
||||
@ -51,7 +52,7 @@ export const TextNormal = ({ currentQuestion, answer }: TextNormalProps) => {
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
marginTop: "20px",
|
||||
flexDirection: isMobile ? "column-reverse" : undefined,
|
||||
flexDirection: isCrutch23022025 ? "column" : isMobile ? "column-reverse" : undefined,
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
@ -74,9 +75,9 @@ export const TextNormal = ({ currentQuestion, answer }: TextNormalProps) => {
|
||||
{choiceImgUrlQuestion && choiceImgUrlQuestion !== " " && choiceImgUrlQuestion !== null && (
|
||||
<Box
|
||||
sx={{
|
||||
maxWidth: "400px",
|
||||
width: "100%",
|
||||
height: "300px",
|
||||
maxWidth: isCrutch23022025 ? undefined : "400px",
|
||||
width: isCrutch23022025 ? "auto" : "100%",
|
||||
height: isCrutch23022025 ? "auto" : "300px",
|
||||
margin: "15px",
|
||||
}}
|
||||
onClick={(event) => event.preventDefault()}
|
||||
|
@ -0,0 +1,139 @@
|
||||
import { Box, TextField as MuiTextField, TextFieldProps, Typography, useTheme } from "@mui/material";
|
||||
|
||||
import { Answer, useQuizViewStore } from "@stores/quizView";
|
||||
import { useRootContainerSize } from "@contexts/RootContainerWidthContext";
|
||||
|
||||
import { quizThemes } from "@utils/themes/Publication/themePublication";
|
||||
|
||||
import type { ChangeEvent, FC } from "react";
|
||||
import type { QuizQuestionText } from "@model/questionTypes/text";
|
||||
import { useQuizStore } from "@/stores/useQuizStore";
|
||||
|
||||
const TextField = MuiTextField as unknown as FC<TextFieldProps>; // temporary fix ts(2590)
|
||||
|
||||
const ORIENTATION = [
|
||||
{ horizontal: true },
|
||||
{ horizontal: false },
|
||||
{ horizontal: true },
|
||||
{ horizontal: true },
|
||||
{ horizontal: false },
|
||||
{ horizontal: true },
|
||||
{ horizontal: true },
|
||||
{ horizontal: true },
|
||||
{ horizontal: true },
|
||||
{ horizontal: true },
|
||||
{ horizontal: true },
|
||||
{ horizontal: false },
|
||||
{ horizontal: true },
|
||||
{ horizontal: false },
|
||||
{ horizontal: true },
|
||||
{ horizontal: true },
|
||||
{ horizontal: true },
|
||||
{ horizontal: true },
|
||||
{ horizontal: false },
|
||||
{ horizontal: false },
|
||||
{ horizontal: true },
|
||||
{ horizontal: true },
|
||||
{ horizontal: true },
|
||||
{ horizontal: true },
|
||||
];
|
||||
|
||||
interface TextSpecialProps {
|
||||
currentQuestion: QuizQuestionText;
|
||||
answer?: Answer;
|
||||
stepNumber?: number | null;
|
||||
}
|
||||
|
||||
export const TextSpecial = ({ currentQuestion, answer, stepNumber }: TextSpecialProps) => {
|
||||
const { settings } = useQuizStore();
|
||||
const { updateAnswer } = useQuizViewStore((state) => state);
|
||||
const isHorizontal = ORIENTATION[Number(stepNumber) - 1].horizontal;
|
||||
const theme = useTheme();
|
||||
const isMobile = useRootContainerSize() < 650;
|
||||
|
||||
const onInputChange = async ({ target }: ChangeEvent<HTMLInputElement>) => {
|
||||
updateAnswer(currentQuestion.id, target.value, 0);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: isMobile ? "column" : undefined,
|
||||
alignItems: isMobile ? "center" : undefined,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
marginTop: "20px",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
gap: "20px",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="h5"
|
||||
color={theme.palette.text.primary}
|
||||
sx={{ wordBreak: "break-word" }}
|
||||
>
|
||||
{currentQuestion.title}
|
||||
</Typography>
|
||||
{isHorizontal && currentQuestion.content.back && currentQuestion.content.back !== " " && (
|
||||
<Box
|
||||
sx={{ margin: "30px", width: "50vw", maxHeight: "550px" }}
|
||||
onClick={(event) => event.preventDefault()}
|
||||
>
|
||||
<img
|
||||
key={currentQuestion.id}
|
||||
src={currentQuestion.content.back}
|
||||
style={{ width: "100%", height: "100%", objectFit: "cover" }}
|
||||
alt=""
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
{
|
||||
<TextField
|
||||
autoFocus={true}
|
||||
multiline
|
||||
maxRows={4}
|
||||
placeholder={currentQuestion.content.placeholder}
|
||||
value={answer || ""}
|
||||
onChange={onInputChange}
|
||||
inputProps={{
|
||||
maxLength: 400,
|
||||
background: settings.cfg.design
|
||||
? quizThemes[settings.cfg.theme].isLight
|
||||
? "#F2F3F7"
|
||||
: "rgba(154,154,175, 0.2)"
|
||||
: "transparent",
|
||||
}}
|
||||
sx={{
|
||||
width: "100%",
|
||||
"& .MuiOutlinedInput-root": {
|
||||
backgroundColor: settings.cfg.design ? "rgba(154,154,175, 0.2)" : "#FFFFFF",
|
||||
},
|
||||
"&:focus-visible": {
|
||||
borderColor: theme.palette.primary.main,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
}
|
||||
</Box>
|
||||
{!isHorizontal && currentQuestion.content.back && currentQuestion.content.back !== " " && (
|
||||
<Box
|
||||
sx={{ margin: "15px", width: "40vw" }}
|
||||
onClick={(event) => event.preventDefault()}
|
||||
>
|
||||
<img
|
||||
key={currentQuestion.id}
|
||||
src={currentQuestion.content.back}
|
||||
style={{ width: "100%", height: "100%", objectFit: "contain" }}
|
||||
alt=""
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
@ -0,0 +1,112 @@
|
||||
import { Box, TextField as MuiTextField, TextFieldProps, Typography, useTheme } from "@mui/material";
|
||||
|
||||
import { Answer, useQuizViewStore } from "@stores/quizView";
|
||||
import { useRootContainerSize } from "@contexts/RootContainerWidthContext";
|
||||
|
||||
import { quizThemes } from "@utils/themes/Publication/themePublication";
|
||||
|
||||
import type { ChangeEvent, FC } from "react";
|
||||
import type { QuizQuestionText } from "@model/questionTypes/text";
|
||||
import { useQuizStore } from "@/stores/useQuizStore";
|
||||
|
||||
const TextField = MuiTextField as unknown as FC<TextFieldProps>; // temporary fix ts(2590)
|
||||
|
||||
interface TextSpecialProps {
|
||||
currentQuestion: QuizQuestionText;
|
||||
answer?: Answer;
|
||||
stepNumber?: number | null;
|
||||
}
|
||||
|
||||
export const TextSpecialHorisontal = ({ currentQuestion, answer, stepNumber }: TextSpecialProps) => {
|
||||
const { settings } = useQuizStore();
|
||||
const { updateAnswer } = useQuizViewStore((state) => state);
|
||||
const isHorizontal = true;
|
||||
const theme = useTheme();
|
||||
const isMobile = useRootContainerSize() < 650;
|
||||
|
||||
const onInputChange = async ({ target }: ChangeEvent<HTMLInputElement>) => {
|
||||
updateAnswer(currentQuestion.id, target.value, 0);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: isMobile ? "column" : undefined,
|
||||
alignItems: isMobile ? "center" : undefined,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
marginTop: "20px",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
gap: "20px",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="h5"
|
||||
color={theme.palette.text.primary}
|
||||
sx={{ wordBreak: "break-word" }}
|
||||
>
|
||||
{currentQuestion.title}
|
||||
</Typography>
|
||||
{isHorizontal && currentQuestion.content.back && currentQuestion.content.back !== " " && (
|
||||
<Box
|
||||
sx={{ margin: "30px", width: "50vw", maxHeight: "550px" }}
|
||||
onClick={(event) => event.preventDefault()}
|
||||
>
|
||||
<img
|
||||
key={currentQuestion.id}
|
||||
src={currentQuestion.content.back}
|
||||
style={{ width: "100%", height: "100%", objectFit: "contain" }}
|
||||
alt=""
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
{
|
||||
<TextField
|
||||
autoFocus={true}
|
||||
multiline
|
||||
maxRows={4}
|
||||
placeholder={currentQuestion.content.placeholder}
|
||||
value={answer || ""}
|
||||
onChange={onInputChange}
|
||||
inputProps={{
|
||||
maxLength: 400,
|
||||
background: settings.cfg.design
|
||||
? quizThemes[settings.cfg.theme].isLight
|
||||
? "#F2F3F7"
|
||||
: "rgba(154,154,175, 0.2)"
|
||||
: "transparent",
|
||||
}}
|
||||
sx={{
|
||||
width: "100%",
|
||||
"& .MuiOutlinedInput-root": {
|
||||
backgroundColor: settings.cfg.design ? "rgba(154,154,175, 0.2)" : "#FFFFFF",
|
||||
},
|
||||
"&:focus-visible": {
|
||||
borderColor: theme.palette.primary.main,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
}
|
||||
</Box>
|
||||
{!isHorizontal && currentQuestion.content.back && currentQuestion.content.back !== " " && (
|
||||
<Box
|
||||
sx={{ margin: "15px", width: "40vw" }}
|
||||
onClick={(event) => event.preventDefault()}
|
||||
>
|
||||
<img
|
||||
key={currentQuestion.id}
|
||||
src={currentQuestion.content.back}
|
||||
style={{ width: "100%", height: "100%", objectFit: "contain" }}
|
||||
alt=""
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
@ -1,21 +1,55 @@
|
||||
import { useQuizViewStore } from "@stores/quizView";
|
||||
import { TextNormal } from "./TextNormal";
|
||||
import { TextSpecial } from "./TextSpecial";
|
||||
import { TextSpecialHorisontal } from "./TextSpecialHorisontal";
|
||||
|
||||
import type { QuizQuestionText } from "@model/questionTypes/text";
|
||||
import { useQuizStore } from "@/stores/useQuizStore";
|
||||
|
||||
type TextProps = {
|
||||
currentQuestion: QuizQuestionText;
|
||||
stepNumber: number | null;
|
||||
};
|
||||
|
||||
const pathOnly = window.location.pathname;
|
||||
|
||||
export const Text = ({ currentQuestion, stepNumber }: TextProps) => {
|
||||
const { settings } = useQuizStore();
|
||||
const answers = useQuizViewStore((state) => state.answers);
|
||||
const { answer } = answers.find(({ questionId }) => questionId === currentQuestion.id) ?? {};
|
||||
|
||||
if (pathOnly === "/92ed5e3e-8e6a-491e-87d0-d3197682d0e3" || pathOnly === "/cc006b40-ccbd-4600-a1d3-f902f85aa0a0")
|
||||
return (
|
||||
<TextSpecialHorisontal
|
||||
currentQuestion={currentQuestion}
|
||||
answer={answer}
|
||||
stepNumber={stepNumber}
|
||||
/>
|
||||
);
|
||||
switch (settings.cfg.spec) {
|
||||
case true:
|
||||
return (
|
||||
<TextSpecial
|
||||
currentQuestion={currentQuestion}
|
||||
answer={answer}
|
||||
stepNumber={stepNumber}
|
||||
/>
|
||||
);
|
||||
|
||||
case undefined:
|
||||
return (
|
||||
<TextNormal
|
||||
currentQuestion={currentQuestion}
|
||||
answer={answer}
|
||||
/>
|
||||
);
|
||||
|
||||
default:
|
||||
return (
|
||||
<TextNormal
|
||||
currentQuestion={currentQuestion}
|
||||
answer={answer}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -0,0 +1,216 @@
|
||||
import { useQuizStore } from "@/stores/useQuizStore";
|
||||
import type { QuestionVariant } from "@model/questionTypes/shared";
|
||||
import {
|
||||
Checkbox,
|
||||
FormControlLabel,
|
||||
Input,
|
||||
TextField as MuiTextField,
|
||||
Radio,
|
||||
TextFieldProps,
|
||||
TextareaAutosize,
|
||||
Typography,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import { useQuizViewStore } from "@stores/quizView";
|
||||
import RadioCheck from "@ui_kit/RadioCheck";
|
||||
import RadioIcon from "@ui_kit/RadioIcon";
|
||||
import { quizThemes } from "@utils/themes/Publication/themePublication";
|
||||
import type { FC, MouseEvent } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const TextField = MuiTextField as unknown as FC<TextFieldProps>;
|
||||
|
||||
interface OwnInputProps {
|
||||
questionId: string;
|
||||
variant: QuestionVariant;
|
||||
largeCheck: boolean;
|
||||
ownPlaceholder: string;
|
||||
}
|
||||
const OwnInput = ({ questionId, variant, largeCheck, ownPlaceholder }: OwnInputProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const ownVariants = useQuizViewStore((state) => state.ownVariants);
|
||||
const { updateOwnVariant } = useQuizViewStore((state) => state);
|
||||
|
||||
const ownAnswer = ownVariants[ownVariants.findIndex((v) => v.id === variant.id)]?.variant.answer || "";
|
||||
|
||||
return largeCheck ? (
|
||||
<TextareaAutosize
|
||||
placeholder={ownPlaceholder || "|"}
|
||||
style={{
|
||||
resize: "none",
|
||||
width: "100%",
|
||||
fontSize: "16px",
|
||||
color: ownAnswer.length === 0 ? "ownPlaceholder" : theme.palette.text.primary,
|
||||
letterSpacing: "-0.4px",
|
||||
wordSpacing: "-3px",
|
||||
outline: "0px none",
|
||||
backgroundColor: "inherit",
|
||||
border: "none",
|
||||
//@ts-ignore
|
||||
"&::-webkit-scrollbar": {
|
||||
width: "4px",
|
||||
},
|
||||
"&::-webkit-scrollbar-thumb": {
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
},
|
||||
scrollbarColor: theme.palette.primary.main,
|
||||
}}
|
||||
value={ownAnswer}
|
||||
onClick={(e: React.MouseEvent<HTMLTextAreaElement>) => e.stopPropagation()}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
updateOwnVariant(variant.id, e.target.value);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
placeholder={ownPlaceholder || "|"}
|
||||
sx={{
|
||||
backgroundColor: "inherit",
|
||||
width: "100%",
|
||||
fontSize: "18px",
|
||||
color: ownAnswer.length === 0 ? "ownPlaceholder" : theme.palette.text.primary,
|
||||
}}
|
||||
value={ownAnswer}
|
||||
disableUnderline
|
||||
onClick={(e: React.MouseEvent<HTMLElement>) => e.stopPropagation()}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
updateOwnVariant(variant.id, e.target.value);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const VariantItem = ({
|
||||
questionId,
|
||||
isMulti,
|
||||
variant,
|
||||
answer,
|
||||
index,
|
||||
own = false,
|
||||
questionLargeCheck,
|
||||
ownPlaceholder,
|
||||
}: {
|
||||
isMulti: boolean;
|
||||
questionId: string;
|
||||
variant: QuestionVariant;
|
||||
answer: string | string[] | undefined;
|
||||
index: number;
|
||||
own: boolean;
|
||||
questionLargeCheck: boolean;
|
||||
ownPlaceholder: string;
|
||||
}) => {
|
||||
const { settings } = useQuizStore();
|
||||
const theme = useTheme();
|
||||
const { updateAnswer, deleteAnswer } = useQuizViewStore((state) => state);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const sendVariant = async (event: MouseEvent<HTMLLabelElement>) => {
|
||||
event.preventDefault();
|
||||
|
||||
const variantId = variant.id;
|
||||
|
||||
if (isMulti) {
|
||||
const currentAnswer = typeof answer !== "string" ? answer || [] : [];
|
||||
|
||||
return updateAnswer(
|
||||
questionId,
|
||||
currentAnswer.includes(variantId)
|
||||
? currentAnswer?.filter((item) => item !== variantId)
|
||||
: [...currentAnswer, variantId],
|
||||
variant.points || 0
|
||||
);
|
||||
}
|
||||
|
||||
updateAnswer(questionId, variantId, answer === variantId ? 0 : variant.points || 0);
|
||||
|
||||
if (answer === variantId) {
|
||||
deleteAnswer(questionId);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<FormControlLabel
|
||||
key={variant.id}
|
||||
sx={{
|
||||
position: "relative",
|
||||
margin: "0",
|
||||
mt: own ? "10px" : "0",
|
||||
borderRadius: "12px",
|
||||
color: theme.palette.text.primary,
|
||||
padding: "15px",
|
||||
border: `1px solid`,
|
||||
borderColor: answer === variant.id ? theme.palette.primary.main : "#9A9AAF",
|
||||
backgroundColor: settings.cfg.design
|
||||
? quizThemes[settings.cfg.theme].isLight
|
||||
? "#FFFFFF"
|
||||
: "rgba(255,255,255, 0.3)"
|
||||
: quizThemes[settings.cfg.theme].isLight
|
||||
? "white"
|
||||
: theme.palette.background.default,
|
||||
display: "flex",
|
||||
maxWidth: "685px",
|
||||
maxHeight: "85px",
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
"&:hover": { borderColor: theme.palette.primary.main },
|
||||
"&.MuiFormControl-root": { width: "100%" },
|
||||
"& .MuiFormControlLabel-label": {
|
||||
width: "100%",
|
||||
maxHeight: "100%",
|
||||
wordBreak: "break-word",
|
||||
height: variant.answer.length <= 60 ? undefined : "60px",
|
||||
overflow: "auto",
|
||||
lineHeight: "normal",
|
||||
"&::-webkit-scrollbar": { width: "4px" },
|
||||
"&::-webkit-scrollbar-thumb": { backgroundColor: theme.palette.primary.main },
|
||||
scrollbarColor: theme.palette.primary.main,
|
||||
},
|
||||
"& .MuiFormControlLabel-label.Mui-disabled": {
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
}}
|
||||
value={index}
|
||||
labelPlacement="start"
|
||||
control={
|
||||
isMulti ? (
|
||||
<Radio
|
||||
checked={!!answer?.includes(variant.id)}
|
||||
checkedIcon={<RadioCheck color={theme.palette.primary.main} />}
|
||||
icon={<RadioIcon />}
|
||||
/>
|
||||
) : (
|
||||
<Radio
|
||||
checkedIcon={<RadioCheck color={theme.palette.primary.main} />}
|
||||
icon={<RadioIcon />}
|
||||
/>
|
||||
)
|
||||
}
|
||||
label={
|
||||
own ? (
|
||||
<>
|
||||
<Typography
|
||||
sx={{
|
||||
color: theme.palette.text.primary,
|
||||
fontSize: "14px",
|
||||
position: "absolute",
|
||||
top: "-23px",
|
||||
}}
|
||||
>
|
||||
{t("Enter your answer")}
|
||||
</Typography>
|
||||
<OwnInput
|
||||
questionId={questionId}
|
||||
variant={variant}
|
||||
largeCheck={questionLargeCheck}
|
||||
ownPlaceholder={ownPlaceholder || "|"}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
variant.answer
|
||||
)
|
||||
}
|
||||
onClick={sendVariant}
|
||||
/>
|
||||
);
|
||||
};
|
159
lib/components/ViewPublicationPage/questions/Variant/index.tsx
Normal file
159
lib/components/ViewPublicationPage/questions/Variant/index.tsx
Normal file
@ -0,0 +1,159 @@
|
||||
import { Box, FormGroup, RadioGroup, Typography, useTheme } from "@mui/material";
|
||||
import { useEffect, useMemo } from "react";
|
||||
|
||||
import { VariantItem } from "./VariantItem";
|
||||
|
||||
import { useRootContainerSize } from "@contexts/RootContainerWidthContext";
|
||||
import { useQuizViewStore } from "@stores/quizView";
|
||||
|
||||
import type { QuizQuestionVariant } from "@model/questionTypes/variant";
|
||||
import moment from "moment";
|
||||
|
||||
type VariantProps = {
|
||||
currentQuestion: QuizQuestionVariant;
|
||||
};
|
||||
|
||||
// 23.02.2025
|
||||
const crutchlist = {
|
||||
115048: { x: 629, y: 491 },
|
||||
115101: { x: 979, y: 980 },
|
||||
115109: { x: 746, y: 745 },
|
||||
115122: { x: 959, y: 960 },
|
||||
115132: { x: 541, y: 541 },
|
||||
115142: { x: 834, y: 544 },
|
||||
115178: { x: 1127, y: 1127 },
|
||||
115191: { x: 1106, y: 1106 },
|
||||
115207: { x: 905, y: 906 },
|
||||
115254: { x: 637, y: 637 },
|
||||
115270: { x: 702, y: 703 },
|
||||
115287: { x: 714, y: 715 },
|
||||
115329: { x: 915, y: 916 },
|
||||
115348: { x: 700, y: 701 },
|
||||
115368: { x: 400, y: 300 },
|
||||
115389: { x: 839, y: 840 },
|
||||
115411: { x: 612, y: 610 },
|
||||
115434: { x: 474, y: 473 },
|
||||
115462: { x: 385, y: 385 },
|
||||
115487: { x: 676, y: 677 },
|
||||
115515: { x: 341, y: 341 },
|
||||
115547: { x: 402, y: 403 },
|
||||
115575: { x: 502, y: 503 },
|
||||
115612: { x: 400, y: 300 },
|
||||
115642: { x: 603, y: 603 },
|
||||
};
|
||||
|
||||
export const Variant = ({ currentQuestion }: VariantProps) => {
|
||||
const theme = useTheme();
|
||||
const isMobile = useRootContainerSize() < 650;
|
||||
const isTablet = useRootContainerSize() < 850;
|
||||
const answers = useQuizViewStore((state) => state.answers);
|
||||
const ownVariants = useQuizViewStore((state) => state.ownVariants);
|
||||
const updateOwnVariant = useQuizViewStore((state) => state.updateOwnVariant);
|
||||
|
||||
const answer = answers.find(({ questionId }) => questionId === currentQuestion.id)?.answer;
|
||||
const ownVariant = ownVariants.find((variant) => variant.id === currentQuestion.id);
|
||||
|
||||
const Group = currentQuestion.content.multi ? FormGroup : RadioGroup;
|
||||
|
||||
//let isCrutch23022025Question = isCrutch23022025 && crutchlist.hasOwnProperty(currentQuestion.id)
|
||||
|
||||
useEffect(() => {
|
||||
if (!ownVariant) {
|
||||
updateOwnVariant(currentQuestion.id, "");
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const choiceImgUrlQuestion = useMemo(() => {
|
||||
if (
|
||||
currentQuestion.content.editedUrlImagesList !== undefined &&
|
||||
currentQuestion.content.editedUrlImagesList !== null
|
||||
) {
|
||||
return currentQuestion.content.editedUrlImagesList[isMobile ? "mobile" : isTablet ? "tablet" : "desktop"];
|
||||
} else {
|
||||
return currentQuestion.content.back;
|
||||
}
|
||||
}, [currentQuestion]);
|
||||
if (moment.isMoment(answer)) throw new Error("Answer is Moment in Variant question");
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Typography
|
||||
variant="h5"
|
||||
color={theme.palette.text.primary}
|
||||
sx={{ wordBreak: "break-word" }}
|
||||
>
|
||||
{currentQuestion.title}
|
||||
</Typography>
|
||||
<Box
|
||||
id="batya"
|
||||
sx={{
|
||||
display: "flex",
|
||||
gap: "20px",
|
||||
flexDirection: isMobile ? "column-reverse" : undefined,
|
||||
alignItems: isMobile ? "center" : undefined,
|
||||
}}
|
||||
>
|
||||
<Group
|
||||
name={currentQuestion.id.toString()}
|
||||
value={currentQuestion.content.variants.findIndex(({ id }) => answer === id)}
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
flexBasis: "100%",
|
||||
marginTop: "20px",
|
||||
width: isMobile ? "100%" : undefined,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
flexWrap: "wrap",
|
||||
width: "100%",
|
||||
gap: "20px",
|
||||
}}
|
||||
>
|
||||
{currentQuestion.content.variants
|
||||
.filter((v) => {
|
||||
if (!v.isOwn) return true;
|
||||
return v.isOwn && currentQuestion.content.own;
|
||||
})
|
||||
.map((variant, index) => (
|
||||
<VariantItem
|
||||
key={variant.id}
|
||||
questionId={currentQuestion.id}
|
||||
isMulti={currentQuestion.content.multi}
|
||||
variant={variant}
|
||||
answer={answer}
|
||||
index={index}
|
||||
own={Boolean(variant.isOwn)}
|
||||
questionLargeCheck={currentQuestion.content.largeCheck}
|
||||
ownPlaceholder={currentQuestion.content?.ownPlaceholder || ""}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</Group>
|
||||
{choiceImgUrlQuestion && choiceImgUrlQuestion !== " " && choiceImgUrlQuestion !== null && (
|
||||
<Box
|
||||
sx={{
|
||||
maxWidth: "400px",
|
||||
width: "100%",
|
||||
height: "300px",
|
||||
}}
|
||||
onClick={(event) => event.preventDefault()}
|
||||
>
|
||||
<img
|
||||
key={currentQuestion.id}
|
||||
src={choiceImgUrlQuestion}
|
||||
style={{ width: "100%", height: "100%", objectFit: "cover" }}
|
||||
alt=""
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
@ -0,0 +1,83 @@
|
||||
import React, { forwardRef, useState } from "react";
|
||||
import { useQuizViewStore } from "@stores/quizView";
|
||||
import { useQuizStore } from "@/stores/useQuizStore";
|
||||
import { useSnackbar } from "notistack";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { sendFile } from "@/api/quizRelase";
|
||||
import { ACCEPT_SEND_FILE_TYPES_MAP, MAX_FILE_SIZE } from "../../tools/fileUpload";
|
||||
|
||||
interface OwnVarimgImageProps {
|
||||
questionId: string;
|
||||
variantId: string;
|
||||
}
|
||||
|
||||
export const OwnVarimgImage = forwardRef<HTMLInputElement, OwnVarimgImageProps>(({ questionId, variantId }, ref) => {
|
||||
const { updateAnswer, updateOwnVariant } = useQuizViewStore((state) => state);
|
||||
const { quizId, preview } = useQuizStore();
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
|
||||
const uploadImage = async (file: File) => {
|
||||
if (isUploading) return;
|
||||
if (!file) return;
|
||||
|
||||
// Валидация размера файла
|
||||
if (file.size > MAX_FILE_SIZE) {
|
||||
enqueueSnackbar(t("file is too big"), { variant: "warning" });
|
||||
return;
|
||||
}
|
||||
|
||||
// Валидация типа файла
|
||||
const isFileTypeAccepted = ACCEPT_SEND_FILE_TYPES_MAP.picture.some((fileType) =>
|
||||
file.name.toLowerCase().endsWith(fileType)
|
||||
);
|
||||
if (!isFileTypeAccepted) {
|
||||
enqueueSnackbar(t("file type is not supported"), { variant: "warning" });
|
||||
return;
|
||||
}
|
||||
|
||||
setIsUploading(true);
|
||||
try {
|
||||
const data = await sendFile({
|
||||
questionId,
|
||||
body: { file, name: file.name, preview },
|
||||
qid: quizId,
|
||||
});
|
||||
|
||||
const fileId = data?.data.fileIDMap[questionId];
|
||||
const localImageUrl = URL.createObjectURL(file);
|
||||
|
||||
updateOwnVariant(variantId, "", "", fileId, localImageUrl);
|
||||
// Убираем автоматический выбор own варианта - загрузка возможна только при выбранном own варианте
|
||||
// updateAnswer(questionId, variantId, 0);
|
||||
} catch (error) {
|
||||
console.error("Error uploading image:", error);
|
||||
enqueueSnackbar(t("The answer was not counted"));
|
||||
} finally {
|
||||
setIsUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (file) {
|
||||
uploadImage(file);
|
||||
event.target.value = "";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<input
|
||||
type="file"
|
||||
ref={ref}
|
||||
style={{ display: "none" }}
|
||||
accept={ACCEPT_SEND_FILE_TYPES_MAP.picture.join(",")}
|
||||
onChange={handleFileChange}
|
||||
disabled={isUploading}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
OwnVarimgImage.displayName = "OwnVarimgImage";
|
@ -0,0 +1,237 @@
|
||||
import type { QuestionVariant, QuestionVariantWithEditedImages } from "@/model/questionTypes/shared";
|
||||
import { useQuizStore } from "@/stores/useQuizStore";
|
||||
import { FormControlLabel, TextareaAutosize, Radio, useTheme, Box, Input, Typography } from "@mui/material";
|
||||
import { useQuizViewStore } from "@stores/quizView";
|
||||
import RadioCheck from "@ui_kit/RadioCheck";
|
||||
import RadioIcon from "@ui_kit/RadioIcon";
|
||||
import { quizThemes } from "@utils/themes/Publication/themePublication";
|
||||
import { type MouseEvent } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type VarimgVariantProps = {
|
||||
questionId: string;
|
||||
variant: QuestionVariantWithEditedImages;
|
||||
index: number;
|
||||
isSending: boolean;
|
||||
setIsSending: (isSending: boolean) => void;
|
||||
questionLargeCheck: boolean;
|
||||
isMulti: boolean;
|
||||
answer: string | string[] | undefined;
|
||||
ownPlaceholder: string;
|
||||
};
|
||||
|
||||
interface OwnInputProps {
|
||||
questionId: string;
|
||||
variant: QuestionVariant;
|
||||
largeCheck: boolean;
|
||||
ownPlaceholder: string;
|
||||
}
|
||||
const OwnInput = ({ questionId, variant, largeCheck, ownPlaceholder }: OwnInputProps) => {
|
||||
const theme = useTheme();
|
||||
const ownVariants = useQuizViewStore((state) => state.ownVariants);
|
||||
const { updateOwnVariant } = useQuizViewStore((state) => state);
|
||||
|
||||
const ownAnswer = ownVariants[ownVariants.findIndex((v) => v.id === variant.id)]?.variant.answer || "";
|
||||
|
||||
return largeCheck ? (
|
||||
<TextareaAutosize
|
||||
placeholder={ownPlaceholder || "|"}
|
||||
style={{
|
||||
resize: "none",
|
||||
width: "100%",
|
||||
fontSize: "16px",
|
||||
color: ownAnswer.length === 0 ? "ownPlaceholder" : theme.palette.text.primary,
|
||||
letterSpacing: "-0.4px",
|
||||
wordSpacing: "-3px",
|
||||
outline: "0px none",
|
||||
backgroundColor: "inherit",
|
||||
border: "none",
|
||||
//@ts-ignore
|
||||
"&::-webkit-scrollbar": {
|
||||
width: "4px",
|
||||
},
|
||||
"&::-webkit-scrollbar-thumb": {
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
},
|
||||
scrollbarColor: theme.palette.primary.main,
|
||||
maxHeight: "44px",
|
||||
overflow: "auto",
|
||||
}}
|
||||
value={ownAnswer}
|
||||
onClick={(e: React.MouseEvent<HTMLTextAreaElement>) => e.stopPropagation()}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
updateOwnVariant(variant.id, e.target.value);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
placeholder={ownPlaceholder || "|"}
|
||||
sx={{
|
||||
backgroundColor: "inherit",
|
||||
width: "100%",
|
||||
fontSize: "18px",
|
||||
color: ownAnswer.length === 0 ? "ownPlaceholder" : theme.palette.text.primary,
|
||||
}}
|
||||
value={ownAnswer}
|
||||
disableUnderline
|
||||
onClick={(e: React.MouseEvent<HTMLElement>) => e.stopPropagation()}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
updateOwnVariant(variant.id, e.target.value);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const VarimgVariant = ({
|
||||
questionId,
|
||||
variant,
|
||||
index,
|
||||
isSending,
|
||||
setIsSending,
|
||||
questionLargeCheck,
|
||||
ownPlaceholder,
|
||||
answer,
|
||||
}: VarimgVariantProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const { settings } = useQuizStore();
|
||||
const { t } = useTranslation();
|
||||
const { updateAnswer, deleteAnswer } = useQuizViewStore((state) => state);
|
||||
|
||||
const sendVariant = async (event: MouseEvent<HTMLLabelElement>) => {
|
||||
event.preventDefault();
|
||||
|
||||
updateAnswer(questionId, variant.id, variant.points || 0);
|
||||
|
||||
if (answer === variant.id) {
|
||||
deleteAnswer(questionId);
|
||||
}
|
||||
};
|
||||
|
||||
if (variant?.isOwn) {
|
||||
return (
|
||||
<Box>
|
||||
<Typography
|
||||
sx={{
|
||||
color: theme.palette.text.primary,
|
||||
fontSize: "14px",
|
||||
pl: "15px",
|
||||
}}
|
||||
>
|
||||
{t("Enter your answer")}
|
||||
</Typography>
|
||||
|
||||
<FormControlLabel
|
||||
key={variant.id}
|
||||
disabled={isSending}
|
||||
sx={{
|
||||
marginBottom: "15px",
|
||||
borderRadius: "12px",
|
||||
padding: "20px",
|
||||
color: theme.palette.text.primary,
|
||||
backgroundColor: settings.cfg.design
|
||||
? quizThemes[settings.cfg.theme].isLight
|
||||
? "#FFFFFF"
|
||||
: "rgba(255,255,255, 0.3)"
|
||||
: quizThemes[settings.cfg.theme].isLight
|
||||
? "white"
|
||||
: theme.palette.background.default,
|
||||
border: `1px solid`,
|
||||
borderColor: answer === variant.id ? theme.palette.primary.main : "#9A9AAF",
|
||||
display: "flex",
|
||||
margin: 0,
|
||||
justifyContent: "space-between",
|
||||
"&:hover": { borderColor: theme.palette.primary.main },
|
||||
"& .MuiFormControlLabel-label": {
|
||||
wordBreak: "break-word",
|
||||
height: variant.answer.length <= 60 ? undefined : "60px",
|
||||
overflow: "auto",
|
||||
lineHeight: "normal",
|
||||
width: "100%",
|
||||
"&::-webkit-scrollbar": {
|
||||
width: "4px",
|
||||
},
|
||||
"&::-webkit-scrollbar-thumb": {
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
},
|
||||
scrollbarColor: theme.palette.primary.main,
|
||||
},
|
||||
"& .MuiFormControlLabel-label.Mui-disabled": {
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
}}
|
||||
labelPlacement="start"
|
||||
value={index}
|
||||
onClick={sendVariant}
|
||||
label={
|
||||
<OwnInput
|
||||
questionId={questionId}
|
||||
variant={variant}
|
||||
largeCheck={questionLargeCheck}
|
||||
ownPlaceholder={ownPlaceholder || "|"}
|
||||
/>
|
||||
}
|
||||
control={
|
||||
<Radio
|
||||
checkedIcon={<RadioCheck color={theme.palette.primary.main} />}
|
||||
icon={<RadioIcon />}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<FormControlLabel
|
||||
key={variant.id}
|
||||
disabled={isSending}
|
||||
sx={{
|
||||
marginBottom: "15px",
|
||||
borderRadius: "12px",
|
||||
padding: "20px",
|
||||
color: theme.palette.text.primary,
|
||||
backgroundColor: settings.cfg.design
|
||||
? quizThemes[settings.cfg.theme].isLight
|
||||
? "#FFFFFF"
|
||||
: "rgba(255,255,255, 0.3)"
|
||||
: quizThemes[settings.cfg.theme].isLight
|
||||
? "white"
|
||||
: theme.palette.background.default,
|
||||
border: `1px solid`,
|
||||
borderColor: answer === variant.id ? theme.palette.primary.main : "#9A9AAF",
|
||||
display: "flex",
|
||||
margin: 0,
|
||||
justifyContent: "space-between",
|
||||
"&:hover": { borderColor: theme.palette.primary.main },
|
||||
"& .MuiFormControlLabel-label": {
|
||||
wordBreak: "break-word",
|
||||
height: variant.answer.length <= 60 ? undefined : "60px",
|
||||
overflow: "auto",
|
||||
lineHeight: "normal",
|
||||
width: "100%",
|
||||
"&::-webkit-scrollbar": {
|
||||
width: "4px",
|
||||
},
|
||||
"&::-webkit-scrollbar-thumb": {
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
},
|
||||
scrollbarColor: theme.palette.primary.main,
|
||||
},
|
||||
"& .MuiFormControlLabel-label.Mui-disabled": {
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
}}
|
||||
labelPlacement="start"
|
||||
value={index}
|
||||
onClick={sendVariant}
|
||||
label={variant.answer}
|
||||
control={
|
||||
<Radio
|
||||
checkedIcon={<RadioCheck color={theme.palette.primary.main} />}
|
||||
icon={<RadioIcon />}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
267
lib/components/ViewPublicationPage/questions/Varimg/index.tsx
Normal file
267
lib/components/ViewPublicationPage/questions/Varimg/index.tsx
Normal file
@ -0,0 +1,267 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Box, ButtonBase, RadioGroup, Typography, useTheme, IconButton } from "@mui/material";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
|
||||
import { VarimgVariant } from "./VarimgVariant";
|
||||
import { OwnVarimgImage } from "./OwnVarimgImage";
|
||||
|
||||
import { useQuizViewStore } from "@stores/quizView";
|
||||
import { useRootContainerSize } from "@contexts/RootContainerWidthContext";
|
||||
|
||||
import BlankImage from "@icons/BlankImage";
|
||||
|
||||
import type { QuizQuestionVarImg } from "@model/questionTypes/varimg";
|
||||
import moment from "moment";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type VarimgProps = {
|
||||
currentQuestion: QuizQuestionVarImg;
|
||||
};
|
||||
|
||||
export const Varimg = ({ currentQuestion }: VarimgProps) => {
|
||||
const [isSending, setIsSending] = useState<boolean>(false);
|
||||
const answers = useQuizViewStore((state) => state.answers);
|
||||
const ownVariants = useQuizViewStore((state) => state.ownVariants);
|
||||
const updateOwnVariant = useQuizViewStore((state) => state.updateOwnVariant);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const theme = useTheme();
|
||||
const isMobile = useRootContainerSize() < 650;
|
||||
const isTablet = useRootContainerSize() < 850;
|
||||
|
||||
const { answer } = answers.find(({ questionId }) => questionId === currentQuestion.id) ?? {};
|
||||
const ownVariant = ownVariants.find((variant) => variant.id === currentQuestion.id);
|
||||
const variant = currentQuestion.content.variants.find(({ id }) => answer === id);
|
||||
const ownVariantInQuestion = useMemo(
|
||||
() => currentQuestion.content.variants.find((v) => v.isOwn),
|
||||
[currentQuestion.content.variants]
|
||||
);
|
||||
const ownVariantData = ownVariants.find((v) => v.id === answer);
|
||||
const ownImageUrl = useMemo(() => {
|
||||
return ownVariantData?.variant.localImageUrl;
|
||||
}, [ownVariantData]);
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ownVariant) {
|
||||
updateOwnVariant(currentQuestion.id, "");
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const choiceImgUrlAnswer = useMemo(() => {
|
||||
if (variant !== undefined) {
|
||||
if (variant.editedUrlImagesList !== undefined && variant.editedUrlImagesList !== null) {
|
||||
return variant.editedUrlImagesList[isMobile ? "mobile" : isTablet ? "tablet" : "desktop"];
|
||||
} else {
|
||||
return variant.extendedText;
|
||||
}
|
||||
}
|
||||
}, [variant]);
|
||||
|
||||
const choiceImgUrlQuestion = useMemo(() => {
|
||||
if (
|
||||
currentQuestion.content.editedUrlImagesList !== undefined &&
|
||||
currentQuestion.content.editedUrlImagesList !== null
|
||||
) {
|
||||
return currentQuestion.content.editedUrlImagesList[isMobile ? "mobile" : isTablet ? "tablet" : "desktop"];
|
||||
} else {
|
||||
return currentQuestion.content.back;
|
||||
}
|
||||
}, [variant]);
|
||||
|
||||
const handlePreviewAreaClick = () => {
|
||||
// Загрузка возможна только если own вариант выбран
|
||||
if (ownVariantInQuestion && answer === ownVariantInQuestion.id) {
|
||||
inputRef.current?.click();
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveImage = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
if (ownVariantData) {
|
||||
// Сохраняем текущий answer, очищаем только изображения
|
||||
const currentAnswer = ownVariantData.variant.answer || "";
|
||||
updateOwnVariant(ownVariantData.id, currentAnswer, "", "", "");
|
||||
}
|
||||
};
|
||||
|
||||
if (moment.isMoment(answer)) throw new Error("Answer is Moment in Variant question");
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Typography
|
||||
variant="h5"
|
||||
color={theme.palette.text.primary}
|
||||
sx={{ wordBreak: "break-word" }}
|
||||
>
|
||||
{currentQuestion.title}
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
marginTop: "20px",
|
||||
flexDirection: isMobile ? "column-reverse" : undefined,
|
||||
gap: "30px",
|
||||
alignItems: isMobile ? "center" : undefined,
|
||||
}}
|
||||
>
|
||||
<RadioGroup
|
||||
name={currentQuestion.id}
|
||||
value={currentQuestion.content.variants.findIndex(({ id }) => answer === id)}
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
flexBasis: "100%",
|
||||
width: isMobile ? "100%" : undefined,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
width: "100%",
|
||||
gap: "20px",
|
||||
"&:focus": { color: theme.palette.text.primary },
|
||||
"&:active": { color: theme.palette.text.primary },
|
||||
}}
|
||||
>
|
||||
{currentQuestion.content.variants
|
||||
.filter((v) => {
|
||||
if (!v.isOwn) return true;
|
||||
return v.isOwn && currentQuestion.content.own;
|
||||
})
|
||||
.map((variant, index) => (
|
||||
<VarimgVariant
|
||||
key={variant.id}
|
||||
questionId={currentQuestion.id}
|
||||
variant={variant}
|
||||
isSending={isSending}
|
||||
setIsSending={setIsSending}
|
||||
index={index}
|
||||
questionLargeCheck={currentQuestion.content.largeCheck}
|
||||
ownPlaceholder={currentQuestion.content?.ownPlaceholder || ""}
|
||||
isMulti={Boolean(currentQuestion.content?.multi)}
|
||||
answer={answer}
|
||||
/>
|
||||
))}
|
||||
{ownVariantInQuestion && (
|
||||
<OwnVarimgImage
|
||||
ref={inputRef}
|
||||
questionId={currentQuestion.id}
|
||||
variantId={ownVariantInQuestion.id}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</RadioGroup>
|
||||
<ButtonBase
|
||||
onClick={handlePreviewAreaClick}
|
||||
disabled={!ownVariantInQuestion || answer !== ownVariantInQuestion.id}
|
||||
sx={{
|
||||
maxWidth: "450px",
|
||||
width: "100%",
|
||||
height: "450px",
|
||||
border: "1px solid #9A9AAF",
|
||||
borderRadius: "12px",
|
||||
overflow: "hidden",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
backgroundColor: "#9A9AAF30",
|
||||
color: theme.palette.text.primary,
|
||||
textAlign: "center",
|
||||
position: "relative",
|
||||
"&:hover": {
|
||||
backgroundColor:
|
||||
ownVariantInQuestion && answer === ownVariantInQuestion.id ? "rgba(0,0,0,0.04)" : "transparent",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{(() => {
|
||||
if (answer) {
|
||||
const imageUrl = variant?.isOwn && ownImageUrl ? ownImageUrl : choiceImgUrlAnswer;
|
||||
if (imageUrl) {
|
||||
return (
|
||||
<>
|
||||
<img
|
||||
key={imageUrl}
|
||||
src={imageUrl}
|
||||
style={{ width: "100%", height: "100%", objectFit: "cover" }}
|
||||
alt=""
|
||||
/>
|
||||
{variant?.isOwn && ownImageUrl && (
|
||||
<IconButton
|
||||
onClick={handleRemoveImage}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: 8,
|
||||
left: 8,
|
||||
zIndex: 1,
|
||||
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
||||
color: "white",
|
||||
height: "25px",
|
||||
width: "25px",
|
||||
"&:hover": {
|
||||
backgroundColor: "rgba(0, 0, 0, 0.7)",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
position: "relative",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<BlankImage />
|
||||
{variant?.isOwn && (
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
{t("Add your image")}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (choiceImgUrlQuestion && choiceImgUrlQuestion.trim().length > 0) {
|
||||
return (
|
||||
<img
|
||||
src={choiceImgUrlQuestion}
|
||||
style={{ width: "100%", height: "100%", objectFit: "cover" }}
|
||||
alt=""
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (currentQuestion.content.replText && currentQuestion.content.replText.trim().length > 0) {
|
||||
return currentQuestion.content.replText;
|
||||
}
|
||||
|
||||
return isMobile ? t("Select an answer option below") : t("Select an answer option on the left");
|
||||
})()}
|
||||
</ButtonBase>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
@ -10,7 +10,7 @@ interface Props {
|
||||
|
||||
export default function NextButton({ isNextButtonEnabled, moveToNextQuestion }: Props) {
|
||||
const { settings, nextLoading } = useQuizStore();
|
||||
const { t } = useTranslation();
|
||||
const { t, i18n } = useTranslation();
|
||||
|
||||
return nextLoading ? (
|
||||
<Skeleton
|
||||
|
@ -13,7 +13,8 @@ export default function PrevButton({ isPreviousButtonEnabled, moveToPrevQuestion
|
||||
const theme = useTheme();
|
||||
const { settings } = useQuizStore();
|
||||
const isMobileMini = useRootContainerSize() < 382;
|
||||
const { t } = useTranslation();
|
||||
const { t, i18n } = useTranslation();
|
||||
|
||||
return (
|
||||
<Button
|
||||
disabled={!isPreviousButtonEnabled}
|
||||
|
136
lib/components/ViewPublicationPage/tools/Select.tsx
Normal file
136
lib/components/ViewPublicationPage/tools/Select.tsx
Normal file
@ -0,0 +1,136 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Select as MuiSelect, MenuItem, FormControl, Typography, useTheme } from "@mui/material";
|
||||
|
||||
import ArrowDown from "@icons/ArrowDownIcon";
|
||||
|
||||
import type { SelectChangeEvent, SxProps } from "@mui/material";
|
||||
|
||||
type SelectProps = {
|
||||
items: string[];
|
||||
activeItemIndex?: number;
|
||||
empty?: boolean;
|
||||
onChange?: (item: string, num: number) => void;
|
||||
sx?: SxProps;
|
||||
colorMain?: string;
|
||||
colorPlaceholder?: string;
|
||||
placeholder?: string;
|
||||
};
|
||||
|
||||
export const Select = ({
|
||||
items,
|
||||
activeItemIndex = 0,
|
||||
empty,
|
||||
onChange,
|
||||
sx,
|
||||
placeholder = "",
|
||||
colorMain = "#7E2AEA",
|
||||
colorPlaceholder = "#9A9AAF",
|
||||
}: SelectProps) => {
|
||||
const [activeItem, setActiveItem] = useState<number>(empty ? -1 : activeItemIndex);
|
||||
const theme = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
setActiveItem(activeItemIndex);
|
||||
}, [activeItemIndex]);
|
||||
|
||||
const handleChange = (event: SelectChangeEvent) => {
|
||||
const newItemIndex = Number(event.target.value);
|
||||
|
||||
if (newItemIndex === activeItem) {
|
||||
setActiveItem(-1);
|
||||
onChange?.("", -1);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setActiveItem(newItemIndex);
|
||||
onChange?.(items[newItemIndex], newItemIndex);
|
||||
};
|
||||
|
||||
return (
|
||||
<FormControl
|
||||
fullWidth
|
||||
size="small"
|
||||
sx={{ width: "100%", height: "48px", ...sx }}
|
||||
>
|
||||
<MuiSelect
|
||||
displayEmpty
|
||||
renderValue={(value) =>
|
||||
value ? items[Number(value)] : <Typography sx={{ color: colorPlaceholder }}>{placeholder}</Typography>
|
||||
}
|
||||
id="display-select"
|
||||
variant="outlined"
|
||||
value={activeItem === -1 ? "" : String(activeItem)}
|
||||
onChange={handleChange}
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: "48px",
|
||||
borderRadius: "8px",
|
||||
"& .MuiOutlinedInput-notchedOutline": {
|
||||
border: `1px solid ${colorMain} !important`,
|
||||
borderRadius: "10px",
|
||||
},
|
||||
"& .MuiSelect-icon": {
|
||||
color: theme.palette.primary.main,
|
||||
},
|
||||
}}
|
||||
MenuProps={{
|
||||
PaperProps: {
|
||||
sx: {
|
||||
mt: "8px",
|
||||
p: "4px",
|
||||
borderRadius: "8px",
|
||||
border: "1px solid #EEE4FC",
|
||||
boxShadow: "0px 8px 24px rgba(210, 208, 225, 0.4)",
|
||||
},
|
||||
},
|
||||
MenuListProps: {
|
||||
sx: {
|
||||
py: 0,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "8px",
|
||||
maxWidth: "1380px",
|
||||
"& .Mui-selected": {
|
||||
backgroundColor: "#F2F3F7",
|
||||
color: colorMain,
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
inputProps={{
|
||||
sx: {
|
||||
color: theme.palette.text.primary,
|
||||
display: "block",
|
||||
px: "9px",
|
||||
gap: "20px",
|
||||
"& .MuiTypography-root": {
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
},
|
||||
},
|
||||
}}
|
||||
IconComponent={(props) => <ArrowDown {...props} />}
|
||||
>
|
||||
{items.map((item, index) => (
|
||||
<MenuItem
|
||||
key={item + index}
|
||||
value={index}
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "20px",
|
||||
padding: "10px",
|
||||
borderRadius: "5px",
|
||||
color: colorPlaceholder,
|
||||
whiteSpace: "normal",
|
||||
wordBreak: "break-word",
|
||||
}}
|
||||
>
|
||||
{item}
|
||||
</MenuItem>
|
||||
))}
|
||||
</MuiSelect>
|
||||
</FormControl>
|
||||
);
|
||||
};
|
@ -50,7 +50,8 @@ export function parseQuizData(quizDataResponse: GetQuizDataResponse): Omit<QuizS
|
||||
answerType: "single",
|
||||
onlyNumbers: false,
|
||||
};
|
||||
if (item.c) content.id = Math.floor(Math.random() * 9999999999) + 1;
|
||||
// Убираю замену ID - оставляю оригинальный с бэкенда
|
||||
// if (item.c) content.id = Math.floor(Math.random() * 9999999999) + 1;
|
||||
return {
|
||||
description: item.desc,
|
||||
id: item.id,
|
||||
|
@ -98,9 +98,21 @@ export interface QuizQuestionBase {
|
||||
};
|
||||
}
|
||||
|
||||
export type AnyTypedQuizQuestion = QuizQuestionText | QuizQuestionResult;
|
||||
export type AnyTypedQuizQuestion =
|
||||
| QuizQuestionVariant
|
||||
| QuizQuestionImages
|
||||
| QuizQuestionVarImg
|
||||
| QuizQuestionEmoji
|
||||
| QuizQuestionText
|
||||
| QuizQuestionSelect
|
||||
| QuizQuestionDate
|
||||
| QuizQuestionNumber
|
||||
| QuizQuestionFile
|
||||
| QuizQuestionPage
|
||||
| QuizQuestionRating
|
||||
| QuizQuestionResult;
|
||||
|
||||
export type RealTypedQuizQuestion = Exclude<QuizQuestionText, QuizQuestionResult>;
|
||||
export type RealTypedQuizQuestion = Exclude<AnyTypedQuizQuestion, QuizQuestionResult>;
|
||||
|
||||
type FilterQuestionsWithVariants<T> = T extends {
|
||||
content: { variants: QuestionVariant[] };
|
||||
|
@ -119,6 +119,7 @@ export interface QuizConfig {
|
||||
showfc?: boolean;
|
||||
yandexMetricsNumber?: number;
|
||||
vkMetricsNumber?: number;
|
||||
backBlocked?: boolean;
|
||||
}
|
||||
|
||||
export type FormContactFieldName = "name" | "email" | "phone" | "text" | "address";
|
||||
|
@ -13,5 +13,6 @@ const isProduction = !(
|
||||
|
||||
//туризм больше не в исключениях
|
||||
if (!isProduction) domain = "https://s.hbpn.link";
|
||||
// domain = "https://hbpn.link";
|
||||
|
||||
export { domain, isProduction };
|
||||
|
@ -37,6 +37,6 @@ async function sendErrorsToServer() {
|
||||
// body: errorsQueue,
|
||||
// useToken: true,
|
||||
// });
|
||||
console.log(`Fake-sending ${errorsQueue.length} errors to server`, errorsQueue);
|
||||
console.info(`Fake-sending ${errorsQueue.length} errors to server`, errorsQueue);
|
||||
errorsQueue = [];
|
||||
}
|
||||
|
@ -14,8 +14,6 @@ export function useAIQuiz() {
|
||||
//Получаем инфо о квизе и список вопросов.
|
||||
const { settings, questions, quizId, cnt, quizStep } = useQuizStore();
|
||||
|
||||
useEffect(() => {}, [questions]);
|
||||
|
||||
//Список ответов на вопрос. Мы записываем ответы локально, параллельно отправляя на бек информацию о ответах
|
||||
const answers = useQuizViewStore((state) => state.answers);
|
||||
|
||||
@ -78,7 +76,7 @@ export function useAIQuiz() {
|
||||
const setQuestion = useCallback((_: string) => {}, []);
|
||||
|
||||
//Анализ дисаблить ли кнопки навигации
|
||||
const isPreviousButtonEnabled = quizStep > 0;
|
||||
const isPreviousButtonEnabled = settings.cfg?.backBlocked ? false : quizStep > 0;
|
||||
|
||||
//Анализ дисаблить ли кнопки навигации
|
||||
const isNextButtonEnabled = useMemo(() => {
|
||||
|
@ -221,7 +221,7 @@ export function useBranchingQuiz() {
|
||||
);
|
||||
|
||||
//Анализ дисаблить ли кнопки навигации
|
||||
const isPreviousButtonEnabled = Boolean(prevQuestion);
|
||||
const isPreviousButtonEnabled = settings.cfg?.backBlocked ? false : Boolean(prevQuestion);
|
||||
|
||||
//Анализ дисаблить ли кнопки навигации
|
||||
const isNextButtonEnabled = useMemo(() => {
|
||||
@ -230,6 +230,7 @@ export function useBranchingQuiz() {
|
||||
if ("required" in currentQuestion.content && currentQuestion.content.required) {
|
||||
return hasAnswer;
|
||||
}
|
||||
|
||||
if (linearQuestionIndex !== null && questions.length < cnt) return true;
|
||||
return Boolean(nextQuestion);
|
||||
}, [answers, currentQuestion, nextQuestion]);
|
||||
|
@ -221,7 +221,7 @@ export function useLinearQuiz() {
|
||||
);
|
||||
|
||||
//Анализ дисаблить ли кнопки навигации
|
||||
const isPreviousButtonEnabled = Boolean(prevQuestion);
|
||||
const isPreviousButtonEnabled = settings.cfg?.backBlocked ? false : Boolean(prevQuestion);
|
||||
|
||||
//Анализ дисаблить ли кнопки навигации
|
||||
const isNextButtonEnabled = useMemo(() => {
|
||||
@ -230,6 +230,7 @@ export function useLinearQuiz() {
|
||||
if ("required" in currentQuestion.content && currentQuestion.content.required) {
|
||||
return hasAnswer;
|
||||
}
|
||||
|
||||
if (linearQuestionIndex !== null && questions.length < cnt) return true;
|
||||
return Boolean(nextQuestion);
|
||||
}, [answers, currentQuestion, nextQuestion]);
|
||||
|
@ -2,6 +2,7 @@ import { sendAnswer } from "@/api/quizRelase";
|
||||
import { RealTypedQuizQuestion } from "@/model/questionTypes/shared";
|
||||
import { OwnVariant, QuestionAnswer, createQuizViewStore } from "@/stores/quizView";
|
||||
import moment from "moment";
|
||||
import { notReachable } from "./notReachable";
|
||||
|
||||
export async function sendQuestionAnswer(
|
||||
quizId: string,
|
||||
@ -16,8 +17,202 @@ export async function sendQuestionAnswer(
|
||||
qid: quizId,
|
||||
});
|
||||
}
|
||||
switch (question.type) {
|
||||
case "date": {
|
||||
let answer = "";
|
||||
|
||||
if (question.type === "text") {
|
||||
if (question.content.isRange) {
|
||||
if (!Array.isArray(questionAnswer.answer)) throw new Error("Cannot send answer in range question");
|
||||
|
||||
let from = Number(questionAnswer.answer[0]);
|
||||
let to = Number(questionAnswer.answer[1]);
|
||||
|
||||
if (
|
||||
from !== 0 &&
|
||||
to !== 0 &&
|
||||
from !== Math.min(Number(questionAnswer.answer[0]), Number(questionAnswer.answer[1]))
|
||||
) {
|
||||
from = Math.min(Number(questionAnswer.answer[0]), Number(questionAnswer.answer[1]));
|
||||
to = Math.max(Number(questionAnswer.answer[0]), Number(questionAnswer.answer[1]));
|
||||
}
|
||||
|
||||
answer = `${!from ? "_" : moment(from).format("YYYY.MM.DD")} - ${!to ? "_" : moment(to).format("YYYY.MM.DD")}`;
|
||||
} else {
|
||||
if (!moment.isMoment(questionAnswer.answer)) throw new Error("Cannot send answer in date question");
|
||||
|
||||
answer = moment(questionAnswer.answer).format("YYYY.MM.DD");
|
||||
}
|
||||
return sendAnswer({
|
||||
questionId: question.id,
|
||||
body: answer,
|
||||
qid: quizId,
|
||||
});
|
||||
}
|
||||
case "emoji": {
|
||||
if (question.content.multi) {
|
||||
const answer = questionAnswer.answer as string[];
|
||||
let answerString = ``;
|
||||
|
||||
const selectedVariants = question.content.variants.filter((v) => answer.includes(v.id));
|
||||
|
||||
selectedVariants.forEach((variant) => {
|
||||
const ownVariantData = ownVariants.find((v) => v.id === variant.id)?.variant;
|
||||
const customEmoji = ownVariantData?.extendedText || "";
|
||||
const emojiToSend = customEmoji || variant.extendedText;
|
||||
const textToSend = variant.isOwn ? ownVariantData?.answer || "" : variant.answer;
|
||||
answerString += `\`${emojiToSend} ${textToSend}\`,`;
|
||||
});
|
||||
|
||||
answerString = answerString.slice(0, -1);
|
||||
|
||||
return sendAnswer({
|
||||
questionId: question.id,
|
||||
body: answerString,
|
||||
qid: quizId,
|
||||
});
|
||||
}
|
||||
|
||||
// Fallback for old string format for single choice
|
||||
const answer = questionAnswer.answer as string;
|
||||
const variant = question.content.variants.find((v) => v.id === answer);
|
||||
if (!variant) {
|
||||
// This can happen if the answer is not set, so we don't throw an error, just send empty
|
||||
return sendAnswer({
|
||||
questionId: question.id,
|
||||
body: "",
|
||||
qid: quizId,
|
||||
});
|
||||
}
|
||||
|
||||
const ownVariantData = ownVariants.find((v) => v.id === variant.id)?.variant;
|
||||
const customEmoji = ownVariantData?.extendedText || "";
|
||||
const emojiToSend = customEmoji || variant.extendedText;
|
||||
const textToSend = variant.isOwn ? ownVariantData?.answer || "" : variant.answer;
|
||||
const body = `${emojiToSend} ${textToSend}`.trim();
|
||||
|
||||
return sendAnswer({
|
||||
questionId: question.id,
|
||||
body: body,
|
||||
qid: quizId,
|
||||
});
|
||||
}
|
||||
case "file": {
|
||||
return;
|
||||
}
|
||||
case "images": {
|
||||
if (question.content.multi) {
|
||||
const answer = questionAnswer.answer;
|
||||
const ownAnswer = Array.isArray(answer)
|
||||
? ownVariants[ownVariants.findIndex((variant) => answer.some((a: string) => a === variant.id))]?.variant
|
||||
?.answer || ""
|
||||
: ownVariants[ownVariants.findIndex((variant) => variant.id === questionAnswer.answer)]?.variant?.answer ||
|
||||
"";
|
||||
|
||||
if (moment.isMoment(answer)) throw new Error("Answer is Moment in Variant question");
|
||||
|
||||
//Оставляем только выбранные варианты
|
||||
const selectedVariants = question.content.variants.filter((v) => answer.includes(v.id));
|
||||
|
||||
let answerString = ``;
|
||||
selectedVariants.forEach((e) => {
|
||||
if (!e.isOwn || (e.isOwn && question.content.own)) {
|
||||
let imageValue = e.extendedText;
|
||||
if (e.isOwn) {
|
||||
// Берем fileId из ownVariants для own вариантов
|
||||
const ownVariantData = ownVariants.find((v) => v.id === e.id)?.variant;
|
||||
if (ownVariantData?.originalImageUrl) {
|
||||
// Конструируем полный URL для own вариантов
|
||||
const baseUrl =
|
||||
"https://s3.timeweb.cloud/3c580be9-cf31f296-d055-49cf-b39e-30c7959dc17b/squizimages/55c25eb9-4533-4d51-9da5-54e63e8aeace/";
|
||||
// Убираем расширение файла из fileId
|
||||
const fileIdWithoutExtension = ownVariantData.originalImageUrl.replace(
|
||||
/\.(jpg|jpeg|png|gif|webp)$/i,
|
||||
""
|
||||
);
|
||||
imageValue = baseUrl + fileIdWithoutExtension;
|
||||
}
|
||||
}
|
||||
|
||||
const body = {
|
||||
Image: imageValue,
|
||||
Description: e.isOwn ? ownAnswer : e.answer,
|
||||
};
|
||||
answerString += `\`${JSON.stringify(body)}\`,`;
|
||||
}
|
||||
});
|
||||
answerString = answerString.slice(0, -1);
|
||||
|
||||
return sendAnswer({
|
||||
questionId: question.id,
|
||||
body: answerString,
|
||||
qid: quizId,
|
||||
});
|
||||
}
|
||||
|
||||
const variant = question.content.variants.find((v) => v.id === questionAnswer.answer);
|
||||
|
||||
if (!variant) throw new Error(`Cannot find variant with id ${questionAnswer.answer} in question ${question.id}`);
|
||||
|
||||
let imageValue = variant.extendedText;
|
||||
if (variant.isOwn) {
|
||||
// Берем fileId из ownVariants для own вариантов
|
||||
const ownVariantData = ownVariants.find((v) => v.id === variant.id)?.variant;
|
||||
if (ownVariantData?.originalImageUrl) {
|
||||
// Конструируем полный URL для own вариантов
|
||||
const baseUrl =
|
||||
"https://s3.timeweb.cloud/3c580be9-cf31f296-d055-49cf-b39e-30c7959dc17b/squizimages/55c25eb9-4533-4d51-9da5-54e63e8aeace/";
|
||||
// Убираем расширение файла из fileId
|
||||
const fileIdWithoutExtension = ownVariantData.originalImageUrl.replace(/\.(jpg|jpeg|png|gif|webp)$/i, "");
|
||||
imageValue = baseUrl + fileIdWithoutExtension;
|
||||
}
|
||||
}
|
||||
|
||||
const body = {
|
||||
Image: imageValue,
|
||||
Description: variant.answer,
|
||||
};
|
||||
if (!body) throw new Error(`Body of answer in question ${question.id} is undefined`);
|
||||
|
||||
return sendAnswer({
|
||||
questionId: question.id,
|
||||
body: `\`${JSON.stringify(body)}\``,
|
||||
qid: quizId,
|
||||
});
|
||||
}
|
||||
case "number": {
|
||||
if (typeof questionAnswer.answer !== "string") throw new Error("Cannot send answer in select question");
|
||||
|
||||
return sendAnswer({
|
||||
questionId: question.id,
|
||||
body: questionAnswer.answer,
|
||||
qid: quizId,
|
||||
});
|
||||
}
|
||||
case "page": {
|
||||
return;
|
||||
}
|
||||
case "rating": {
|
||||
if (typeof questionAnswer.answer !== "string") throw new Error("Cannot send answer in select question");
|
||||
|
||||
return sendAnswer({
|
||||
questionId: question.id,
|
||||
body: String(questionAnswer.answer) + " из " + question.content.steps,
|
||||
qid: quizId,
|
||||
});
|
||||
}
|
||||
case "select": {
|
||||
if (typeof questionAnswer.answer !== "string") throw new Error("Cannot send answer in select question");
|
||||
|
||||
const variant = question.content.variants[Number(questionAnswer.answer)];
|
||||
if (!variant) throw new Error(`Cannot find variant with id ${questionAnswer.answer} in question ${question.id}`);
|
||||
|
||||
return sendAnswer({
|
||||
questionId: question.id,
|
||||
body: variant.answer,
|
||||
qid: quizId,
|
||||
});
|
||||
}
|
||||
case "text": {
|
||||
if (moment.isMoment(questionAnswer.answer)) throw new Error("Cannot send Moment in text question");
|
||||
|
||||
return sendAnswer({
|
||||
@ -25,5 +220,81 @@ export async function sendQuestionAnswer(
|
||||
body: questionAnswer.answer,
|
||||
qid: quizId,
|
||||
});
|
||||
} else throw new Error("Inappropriate question type");
|
||||
}
|
||||
case "variant": {
|
||||
if (question.content.multi) {
|
||||
const answer = questionAnswer.answer;
|
||||
if (moment.isMoment(answer)) throw new Error("Answer is Moment in Variant question");
|
||||
|
||||
const ownAnswer = Array.isArray(answer)
|
||||
? ownVariants[ownVariants.findIndex((variant) => answer.some((a: string) => a === variant.id))]?.variant
|
||||
?.answer || ""
|
||||
: ownVariants[ownVariants.findIndex((variant) => variant.id === questionAnswer.answer)]?.variant?.answer ||
|
||||
"";
|
||||
|
||||
//Оставляем только выбранные варианты
|
||||
const selectedVariants = question.content.variants.filter((v) => answer.includes(v.id));
|
||||
|
||||
let answerString = ``;
|
||||
selectedVariants.forEach((e) => {
|
||||
if (!e.isOwn) answerString += `\`${e.answer}\`,`;
|
||||
});
|
||||
|
||||
if (question.content.own && selectedVariants.some((v) => v.isOwn)) {
|
||||
answerString += `\`${ownAnswer}\`,`;
|
||||
}
|
||||
answerString = answerString.slice(0, -1);
|
||||
|
||||
return sendAnswer({
|
||||
questionId: question.id,
|
||||
body: answerString,
|
||||
qid: quizId,
|
||||
});
|
||||
}
|
||||
|
||||
const variant = question.content.variants.find((v) => v.id === questionAnswer.answer);
|
||||
if (!variant) throw new Error(`Cannot find variant with id ${questionAnswer.answer} in question ${question.id}`);
|
||||
|
||||
return sendAnswer({
|
||||
questionId: question.id,
|
||||
body: variant.answer,
|
||||
qid: quizId,
|
||||
});
|
||||
}
|
||||
case "varimg": {
|
||||
const variant = question.content.variants.find((v) => v.id === questionAnswer.answer);
|
||||
const ownAnswer =
|
||||
ownVariants[ownVariants.findIndex((variant) => variant.id === questionAnswer.answer)]?.variant?.answer || "";
|
||||
|
||||
if (!variant) throw new Error(`Cannot find variant with id ${questionAnswer.answer} in question ${question.id}`);
|
||||
|
||||
let imageValue = variant.extendedText;
|
||||
if (variant.isOwn) {
|
||||
// Берем fileId из ownVariants для own вариантов
|
||||
const ownVariantData = ownVariants.find((v) => v.id === variant.id)?.variant;
|
||||
if (ownVariantData?.originalImageUrl) {
|
||||
// Конструируем полный URL для own вариантов
|
||||
const baseUrl =
|
||||
"https://s3.timeweb.cloud/3c580be9-cf31f296-d055-49cf-b39e-30c7959dc17b/squizimages/55c25eb9-4533-4d51-9da5-54e63e8aeace/";
|
||||
// Убираем расширение файла из fileId
|
||||
const fileIdWithoutExtension = ownVariantData.originalImageUrl.replace(/\.(jpg|jpeg|png|gif|webp)$/i, "");
|
||||
imageValue = baseUrl + fileIdWithoutExtension;
|
||||
}
|
||||
}
|
||||
|
||||
const body = {
|
||||
Image: imageValue,
|
||||
Description: variant.isOwn ? ownAnswer : variant.answer,
|
||||
};
|
||||
if (!body) throw new Error(`Body of answer in question ${question.id} is undefined`);
|
||||
|
||||
return sendAnswer({
|
||||
questionId: question.id,
|
||||
body: `\`${JSON.stringify(body)}\``,
|
||||
qid: quizId,
|
||||
});
|
||||
}
|
||||
default:
|
||||
notReachable(question);
|
||||
}
|
||||
}
|
||||
|
@ -26,6 +26,7 @@
|
||||
"preview": "vite preview",
|
||||
"cypress:open": "cypress open",
|
||||
"prepublishOnly": "npm run build:package",
|
||||
"deploy": "docker login gitea.pena && docker build -t gitea.pena/squiz/frontanswerer/$(git branch --show-current):latest . && docker push gitea.pena/squiz/frontanswerer/$(git branch --show-current):latest",
|
||||
"prepare": "husky"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -56,5 +56,8 @@
|
||||
"Step": "Шаг",
|
||||
"questions are not ready yet": "Вопросы для аудитории ещё не созданы. Пожалуйста, подождите",
|
||||
"Add your image": "Добавьте своё изображение",
|
||||
"select emoji": "выберите смайлик"
|
||||
"select emoji": "выберите смайлик",
|
||||
"Please complete the phone number": "Пожалуйста, завершите номер телефона",
|
||||
"Please enter a valid email": "Пожалуйста, введите корректную почту",
|
||||
"Please enter a valid phone number": "Пожалуйста, введите корректный номер телефона"
|
||||
}
|
||||
|
@ -10,11 +10,10 @@ const getLanguageFromURL = (): string => {
|
||||
const langMatch = path.match(/^\/(en|ru|uz)(\/|$)/i);
|
||||
|
||||
if (langMatch) {
|
||||
//console.log("Язык из URL:", langMatch[1]);
|
||||
return langMatch[1].toLowerCase();
|
||||
const detectedLang = langMatch[1].toLowerCase();
|
||||
return detectedLang;
|
||||
}
|
||||
|
||||
//console.log('Язык не указан в URL, используем "ru"');
|
||||
return "ru"; // Жёсткий фолбэк
|
||||
};
|
||||
|
||||
@ -33,6 +32,9 @@ i18n
|
||||
backend: {
|
||||
loadPath: "/locales/{{lng}}.json",
|
||||
allowMultiLoading: false,
|
||||
requestOptions: {
|
||||
cache: "no-store",
|
||||
},
|
||||
},
|
||||
react: {
|
||||
useSuspense: false, // Отключаем для совместимости с React 18
|
||||
@ -43,11 +45,11 @@ i18n
|
||||
caches: [], // Не использовать localStorage
|
||||
},
|
||||
parseMissingKeyHandler: (key) => {
|
||||
console.warn("Missing translation:", key);
|
||||
console.warn("⚠️ Main i18n: Missing translation:", key);
|
||||
return key; // Вернёт ключ вместо ошибки
|
||||
},
|
||||
missingKeyHandler: (lngs, ns, key) => {
|
||||
console.error("🚨 Missing i18n key:", {
|
||||
console.error("🚨 Main i18n: Missing i18n key:", {
|
||||
key,
|
||||
languages: lngs,
|
||||
namespace: ns,
|
||||
@ -55,19 +57,35 @@ i18n
|
||||
});
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
//console.log("i18n инициализирован! Текущий язык:", i18n.language);
|
||||
//console.log("Загруженные переводы:", i18n.store.data);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Ошибка i18n:", err);
|
||||
console.error("❌ Main i18n: Initialization failed:", err);
|
||||
});
|
||||
|
||||
// 3. Логирование всех событий
|
||||
i18n.on("languageChanged", (lng) => {});
|
||||
i18n.on("languageChanged", (lng) => {
|
||||
console.log("🔄 Main i18n: Language changed to:", lng);
|
||||
});
|
||||
|
||||
i18n.on("initialized", (options) => {
|
||||
console.log("🎯 Main i18n: Initialized event fired with options:", options);
|
||||
});
|
||||
|
||||
i18n.on("failedLoading", (lng, ns, msg) => {
|
||||
console.error(`Ошибка загрузки ${lng}.json:`, msg);
|
||||
console.error(`💥 Main i18n: Failed loading ${lng}.json:`, msg);
|
||||
|
||||
// Если не удалось загрузить русский, пробуем английский
|
||||
if (lng === "ru") {
|
||||
console.log("🔄 Main i18n: Trying to load English as fallback");
|
||||
i18n.changeLanguage("en");
|
||||
}
|
||||
});
|
||||
|
||||
i18n.on("loaded", (loaded) => {
|
||||
console.log("📦 Main i18n: Translations loaded:", loaded);
|
||||
});
|
||||
|
||||
i18n.on("missingKey", (lngs, namespace, key, res) => {
|
||||
console.warn("⚠️ Main i18n: Missing key event:", { lngs, namespace, key, res });
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
|
@ -5,13 +5,15 @@ import { initReactI18next } from "react-i18next";
|
||||
const getLanguageFromURL = (): string => {
|
||||
const path = window.location.pathname;
|
||||
const langMatch = path.match(/^\/(en|ru|uz)(\/|$)/i);
|
||||
return langMatch ? langMatch[1].toLowerCase() : "ru"; // Фолбэк на 'ru'
|
||||
const detectedLang = langMatch ? langMatch[1].toLowerCase() : "ru"; // Фолбэк на 'ru'
|
||||
return detectedLang;
|
||||
};
|
||||
|
||||
// 2. Локали, встроенные прямо в конфиг
|
||||
|
||||
const r = {
|
||||
ru: {
|
||||
translation: {
|
||||
"quiz is inactive": "Квиз не активирован",
|
||||
"no questions found": "Нет созданных вопросов",
|
||||
"quiz is empty": "Квиз пуст",
|
||||
@ -67,12 +69,15 @@ const r = {
|
||||
"Get results": "Получить результаты",
|
||||
"Data sent successfully": "Данные успешно отправлены",
|
||||
Step: "Шаг",
|
||||
"questions are not ready yet": "Вопросы для аудитории ещё не созданы. Пожалуйста, подождите",
|
||||
"questions are not ready yet": "Вопросы для аудитории пока не готовы. Подождите",
|
||||
"Add your image": "Добавьте своё изображение",
|
||||
"select emoji": "выберите смайлик",
|
||||
"Please complete the phone number": "Пожалуйста, заполните номер телефона до конца",
|
||||
"": "", // Пустой ключ для fallback
|
||||
},
|
||||
},
|
||||
en: {
|
||||
translation: {
|
||||
"quiz is inactive": "Quiz is inactive",
|
||||
"no questions found": "No questions found",
|
||||
"quiz is empty": "Quiz is empty",
|
||||
@ -131,9 +136,12 @@ const r = {
|
||||
"questions are not ready yet": "There are no questions for the audience yet. Please wait",
|
||||
"Add your image": "Add your image",
|
||||
"select emoji": "select emoji",
|
||||
"Please complete the phone number": "Please complete the phone number",
|
||||
"": "", // Пустой ключ для fallback
|
||||
},
|
||||
},
|
||||
uz: {
|
||||
translation: {
|
||||
"quiz is inactive": "Test faol emas",
|
||||
"no questions found": "Savollar topilmadi",
|
||||
"quiz is empty": "Test boʻsh",
|
||||
@ -192,12 +200,35 @@ const r = {
|
||||
"questions are not ready yet": "Tomoshabinlar uchun hozircha savollar yo'q. Iltimos kuting",
|
||||
"Add your image": "Rasmingizni qo'shing",
|
||||
"select emoji": "emoji tanlang",
|
||||
"Please complete the phone number": "Iltimos, telefon raqamini to'liq kiriting",
|
||||
"": "", // Пустой ключ для fallback
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// 3. Конфигурация i18n без Backend
|
||||
i18n.use(initReactI18next).init({
|
||||
// Проверяем, не инициализирован ли уже i18n
|
||||
if (i18n.isInitialized) {
|
||||
// Добавляем ресурсы к существующему экземпляру
|
||||
(Object.keys(r) as Array<"ru" | "en" | "uz">).forEach((lng) => {
|
||||
if (i18n.store.data[lng] && i18n.store.data[lng].translation) {
|
||||
// Объединяем с существующими переводами
|
||||
i18n.store.data[lng].translation = {
|
||||
...(i18n.store.data[lng].translation as Record<string, string>),
|
||||
...r[lng].translation,
|
||||
};
|
||||
} else {
|
||||
// Добавляем новые переводы
|
||||
i18n.store.data[lng] = {
|
||||
...(i18n.store.data[lng] as Record<string, any>),
|
||||
translation: r[lng].translation,
|
||||
};
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 3. Конфигурация i18n без Backend
|
||||
i18n
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
resources: r, // Используем встроенные переводы
|
||||
lng: getLanguageFromURL(),
|
||||
fallbackLng: "ru",
|
||||
@ -215,17 +246,42 @@ i18n.use(initReactI18next).init({
|
||||
caches: [],
|
||||
},
|
||||
parseMissingKeyHandler: (key) => {
|
||||
console.warn("Missing translation:", key);
|
||||
console.warn("⚠️ Widget i18n: Missing translation key:", key);
|
||||
return key;
|
||||
},
|
||||
missingKeyHandler: (lngs, ns, key) => {
|
||||
console.error("🚨 Missing i18n key:", {
|
||||
console.error("🚨 Widget i18n: Missing i18n key:", {
|
||||
key,
|
||||
languages: lngs,
|
||||
namespace: ns,
|
||||
stack: new Error().stack,
|
||||
});
|
||||
},
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("❌ Widget i18n: Initialization failed:", error);
|
||||
});
|
||||
}
|
||||
|
||||
// 4. Логирование событий
|
||||
i18n.on("languageChanged", (lng) => {
|
||||
console.log("🔄 Widget i18n: Language changed to:", lng);
|
||||
});
|
||||
|
||||
i18n.on("initialized", (options) => {
|
||||
console.log("🎯 Widget i18n: Initialized event fired with options:", options);
|
||||
});
|
||||
|
||||
i18n.on("loaded", (loaded) => {
|
||||
console.log("📦 Widget i18n: Loaded event fired:", loaded);
|
||||
});
|
||||
|
||||
i18n.on("failedLoading", (lng, ns, msg) => {
|
||||
console.error("💥 Widget i18n: Failed loading:", { lng, ns, msg });
|
||||
});
|
||||
|
||||
i18n.on("missingKey", (lngs, namespace, key, res) => {
|
||||
console.warn("⚠️ Widget i18n: Missing key event:", { lngs, namespace, key, res });
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
|
@ -2,6 +2,7 @@ import { createRoot } from "react-dom/client";
|
||||
import { RouteObject, RouterProvider, createBrowserRouter } from "react-router-dom";
|
||||
import App from "./App";
|
||||
import { StrictMode, lazy } from "react";
|
||||
|
||||
import "./i18n/i18n";
|
||||
|
||||
const routes: RouteObject[] = [
|
||||
|
@ -2,6 +2,7 @@ import QuizAnswerer from "@/components/QuizAnswerer";
|
||||
import { createRoot } from "react-dom/client";
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export * from "./widgets";
|
||||
|
||||
import "./i18n/i18nWidget";
|
||||
|
||||
// old widget
|
||||
@ -16,7 +17,10 @@ const widget = {
|
||||
changeFaviconAndTitle: boolean;
|
||||
}) {
|
||||
const element = document.getElementById(selector);
|
||||
if (!element) throw new Error("Element for widget doesn't exist");
|
||||
if (!element) {
|
||||
console.error("❌ Widget: Element for widget doesn't exist:", selector);
|
||||
throw new Error("Element for widget doesn't exist");
|
||||
}
|
||||
|
||||
const root = createRoot(element);
|
||||
|
||||
|
@ -56,7 +56,7 @@ export default function QuizPopup({
|
||||
|
||||
if (!quizData) return null;
|
||||
|
||||
const isQuizCompleted = quizData.settings.cfg.antifraud ? quizData.recentlyCompleted : false;
|
||||
const isQuizCompleted = quizData.settings?.cfg?.antifraud ? quizData.recentlyCompleted : false;
|
||||
if (isQuizCompleted) return null;
|
||||
if (hideOnMobile && isMobile) return null;
|
||||
|
||||
|
@ -56,7 +56,7 @@ export default function QuizSideButton({
|
||||
if (hideOnMobile && isMobile) return null;
|
||||
if (!quizData) return null;
|
||||
|
||||
const isQuizCompleted = quizData.settings.cfg.antifraud ? quizData.recentlyCompleted : false;
|
||||
const isQuizCompleted = quizData.settings?.cfg?.antifraud ? quizData.recentlyCompleted : false;
|
||||
const showButtonFlash = !isQuizCompleted && isFlashEnabled;
|
||||
|
||||
return createPortal(
|
||||
|
47250
widget_en.js
Normal file
47250
widget_en.js
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user