Merge branch 'staging'
This commit is contained in:
commit
9108148a6e
@ -1,4 +1,5 @@
|
||||
/dist
|
||||
/dist-package
|
||||
/widget
|
||||
Makefile
|
||||
README.md
|
||||
|
@ -26,8 +26,10 @@ module.exports = {
|
||||
"@typescript-eslint/no-unsafe-assignment": "off",
|
||||
"@typescript-eslint/no-unsafe-call": "off",
|
||||
"@typescript-eslint/no-unsafe-member-access": "off",
|
||||
"@typescript-eslint/no-unused-vars": "warn",
|
||||
"@typescript-eslint/require-await": "warn",
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"warn",
|
||||
{ "vars": "all", "args": "none" }
|
||||
],
|
||||
"@typescript-eslint/restrict-template-expressions": "off",
|
||||
"no-debugger": "off",
|
||||
"no-empty-function": "off",
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -9,6 +9,7 @@ lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-package
|
||||
dist-ssr
|
||||
widget
|
||||
*.local
|
||||
@ -22,4 +23,4 @@ widget
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
*.sw?
|
||||
|
52
README.md
Normal file
52
README.md
Normal file
@ -0,0 +1,52 @@
|
||||
## Виджет
|
||||
### Сборка
|
||||
```bash
|
||||
yarn build:widget
|
||||
```
|
||||
### Использование
|
||||
```html
|
||||
<script type="module">
|
||||
import widget from "https://s.hbpn.link/export/pub.js";
|
||||
|
||||
widget.create({
|
||||
selector: "widget-container",
|
||||
quizId: "...",
|
||||
})
|
||||
</script>
|
||||
```
|
||||
## Npm-пакет
|
||||
### Перед использованием и публикацией
|
||||
```bash
|
||||
npm config set //penahub.gitlab.yandexcloud.net/api/v4/packages/npm/:_authToken=INSTANCE_TOKEN
|
||||
npm config set //penahub.gitlab.yandexcloud.net/api/v4/projects/43/packages/npm/:_authToken=PROJECT_TOKEN
|
||||
```
|
||||
### Публикация
|
||||
1. Инкрементировать версию в package.json
|
||||
2.
|
||||
```bash
|
||||
yarn publish
|
||||
```
|
||||
3. Нажать enter при запросе версии
|
||||
### Установка
|
||||
Добавить в корень проекта файл .yarnrc с содержимым
|
||||
```
|
||||
"@frontend:registry" "https://penahub.gitlab.yandexcloud.net/api/v4/packages/npm/"
|
||||
```
|
||||
```bash
|
||||
yarn add @frontend/squzanswerer
|
||||
```
|
||||
Peer dependencies:
|
||||
```bash
|
||||
yarn add @emoji-mart/data @emoji-mart/react @emotion/react @emotion/styled @mui/icons-material @mui/material @mui/x-date-pickers axios emoji-mart immer moment nanoid notistack react-dom react-error-boundary react-router-dom react swr use-debounce zustand
|
||||
```
|
||||
### Использование
|
||||
```ts
|
||||
import { QuizView } from "@frontend/squzanswerer";
|
||||
|
||||
export default function Component() {
|
||||
// ...
|
||||
return (
|
||||
<QuizView quizId={quizId} />
|
||||
}
|
||||
}
|
||||
```
|
152
lib/api/quizRelase.ts
Normal file
152
lib/api/quizRelase.ts
Normal file
@ -0,0 +1,152 @@
|
||||
import { GetQuizDataResponse, parseQuizData } from "@model/api/getQuizData";
|
||||
import axios from "axios";
|
||||
|
||||
import type { AxiosError } from "axios";
|
||||
import { replaceSpacesToEmptyLines } from "../components/ViewPublicationPage/tools/replaceSpacesToEmptyLines";
|
||||
import { QuizSettings } from "@model/settingsData";
|
||||
|
||||
let SESSIONS = "";
|
||||
|
||||
const domain = location.hostname ==="localhost" ? "https://s.hbpn.link" : ""
|
||||
|
||||
export const publicationMakeRequest = ({ url, body }: any) => {
|
||||
return axios(url, {
|
||||
data: body,
|
||||
headers: {
|
||||
"X-Sessionkey": SESSIONS,
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
method: "POST",
|
||||
});
|
||||
};
|
||||
|
||||
export async function getData(quizId: string): Promise<{
|
||||
data: GetQuizDataResponse | null;
|
||||
isRecentlyCompleted: boolean;
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const { data, headers } = await axios<GetQuizDataResponse>(
|
||||
domain + `/answer/settings`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"X-Sessionkey": SESSIONS,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
data: {
|
||||
quiz_id: quizId,
|
||||
limit: 100,
|
||||
page: 0,
|
||||
need_config: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
const sessions = JSON.parse(localStorage.getItem("sessions") || "{}");
|
||||
|
||||
if (typeof sessions[quizId] === "number") {
|
||||
// unix время. Если меньше суток прошло - выводить ошибку, иначе пустить дальше
|
||||
if (Date.now() - sessions[quizId] < 86400000) {
|
||||
return { data, isRecentlyCompleted: true };
|
||||
}
|
||||
}
|
||||
|
||||
SESSIONS = headers["x-sessionkey"] ? headers["x-sessionkey"] : SESSIONS;
|
||||
|
||||
return { data, isRecentlyCompleted: false };
|
||||
} catch (nativeError) {
|
||||
const error = nativeError as AxiosError;
|
||||
|
||||
return { data: null, isRecentlyCompleted: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
export async function getQuizData(quizId: string) {
|
||||
const response = await getData(quizId);
|
||||
const quizDataResponse = response.data;
|
||||
|
||||
if (response.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
if (!quizDataResponse) {
|
||||
throw new Error("Quiz not found");
|
||||
}
|
||||
|
||||
const quizSettings = replaceSpacesToEmptyLines(parseQuizData(quizDataResponse));
|
||||
|
||||
const res = JSON.parse(JSON.stringify({ data: quizSettings }).replaceAll(/\\" \\"/g, '""').replaceAll(/" "/g, '""')).data as QuizSettings;
|
||||
res.recentlyCompleted = response.isRecentlyCompleted;
|
||||
return res;
|
||||
}
|
||||
|
||||
export function sendAnswer({ questionId, body, qid }: any) {
|
||||
const formData = new FormData();
|
||||
|
||||
const answers = [
|
||||
{
|
||||
question_id: questionId,
|
||||
content: body, //тут массив с ответом
|
||||
},
|
||||
];
|
||||
formData.append("answers", JSON.stringify(answers));
|
||||
console.log("QID", qid)
|
||||
formData.append("qid", qid);
|
||||
|
||||
return publicationMakeRequest({
|
||||
url: domain + `/answer/answer`,
|
||||
body: formData,
|
||||
method: "POST",
|
||||
});
|
||||
}
|
||||
|
||||
//body ={file, filename}
|
||||
export function sendFile({ questionId, body, qid }: any) {
|
||||
const formData = new FormData();
|
||||
|
||||
const answers: any = [
|
||||
{
|
||||
question_id: questionId,
|
||||
content: "file:" + body.name,
|
||||
},
|
||||
];
|
||||
|
||||
formData.append("answers", JSON.stringify(answers));
|
||||
formData.append(body.name, body.file);
|
||||
console.log("QID", qid)
|
||||
formData.append("qid", qid);
|
||||
|
||||
return publicationMakeRequest({
|
||||
url: domain + `/answer/answer`,
|
||||
body: formData,
|
||||
method: "POST",
|
||||
});
|
||||
}
|
||||
|
||||
//форма контактов
|
||||
export function sendFC({ questionId, body, qid }: any) {
|
||||
const formData = new FormData();
|
||||
|
||||
// const keysBody = Object.keys(body)
|
||||
// const content:any = {}
|
||||
// fields.forEach((key) => {
|
||||
// if (keysBody.includes(key)) content[key] = body.key
|
||||
// })
|
||||
|
||||
const answers = [
|
||||
{
|
||||
question_id: questionId,
|
||||
content: JSON.stringify(body),
|
||||
result: true,
|
||||
qid,
|
||||
},
|
||||
];
|
||||
|
||||
formData.append("answers", JSON.stringify(answers));
|
||||
formData.append("qid", qid);
|
||||
|
||||
return publicationMakeRequest({
|
||||
url: domain + `/answer/answer`,
|
||||
body: formData,
|
||||
method: "POST",
|
||||
});
|
||||
}
|
0
src/assets/icons/ArrowDownIcon.tsx → lib/assets/icons/ArrowDownIcon.tsx
Executable file → Normal file
0
src/assets/icons/ArrowDownIcon.tsx → lib/assets/icons/ArrowDownIcon.tsx
Executable file → Normal file
10
lib/assets/icons/BlankImage.tsx
Normal file
10
lib/assets/icons/BlankImage.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
export default function BlankImage() {
|
||||
|
||||
return (
|
||||
<svg width="100%" height="100%" viewBox="0 -70 800 535" fill="none" display="block" preserveAspectRatio="xMidYMax meet" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="#F0F0F0" d="M555 47a47.003 47.003 0 0 1 29.014-43.422 46.999 46.999 0 0 1 61.408 61.408 46.997 46.997 0 0 1-76.656 15.248A47 47 0 0 1 555 47Z" />
|
||||
<path fill="#F3F3F3" d="M641.874 240.665c7.74-7.74 20.263-7.82 28.102-.181L1051 611.837 779.035 883.805 383.869 498.67l258.005-258.005Z" />
|
||||
<path fill="#EDEDED" d="M183.393 61.546c7.692-7.037 19.499-6.985 27.129.12l677.42 630.746-690.929 382.738L-397 592.531 183.393 61.546Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
0
src/assets/icons/ContactFormIcon/AddressIcon.tsx → lib/assets/icons/ContactFormIcon/AddressIcon.tsx
0
src/assets/icons/ContactFormIcon/AddressIcon.tsx → lib/assets/icons/ContactFormIcon/AddressIcon.tsx
@ -21,4 +21,4 @@ export const NameplateLogoFQDark: FC<SVGProps<SVGSVGElement>> = (props) => (
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
);
|
||||
);
|
0
src/assets/icons/UploadIcon.tsx → lib/assets/icons/UploadIcon.tsx
Executable file → Normal file
0
src/assets/icons/UploadIcon.tsx → lib/assets/icons/UploadIcon.tsx
Executable file → Normal file
0
src/assets/icons/questionsPage/lightbulbIcon.tsx → lib/assets/icons/questionsPage/lightbulbIcon.tsx
0
src/assets/icons/questionsPage/lightbulbIcon.tsx → lib/assets/icons/questionsPage/lightbulbIcon.tsx
78
lib/components/QuizAnswerer.tsx
Normal file
78
lib/components/QuizAnswerer.tsx
Normal file
@ -0,0 +1,78 @@
|
||||
import { QuizDataContext } from "@contexts/QuizDataContext";
|
||||
import { QuizSettings } from "@model/settingsData";
|
||||
import { Box, CssBaseline, ThemeProvider } from "@mui/material";
|
||||
import { LocalizationProvider } from "@mui/x-date-pickers";
|
||||
import { AdapterMoment } from "@mui/x-date-pickers/AdapterMoment";
|
||||
import { ruRU } from '@mui/x-date-pickers/locales';
|
||||
import { handleComponentError } from "@utils/handleComponentError";
|
||||
import lightTheme from "@utils/themes/light";
|
||||
import moment from "moment";
|
||||
import { SnackbarProvider } from 'notistack';
|
||||
import { ErrorBoundary } from "react-error-boundary";
|
||||
import { ApologyPage } from "./ViewPublicationPage/ApologyPage";
|
||||
import ViewPublicationPage from "./ViewPublicationPage/ViewPublicationPage";
|
||||
import { RootContainerWidthContext } from "@contexts/RootContainerWidthContext";
|
||||
import { startTransition, useEffect, useLayoutEffect, useRef, useState } from "react";
|
||||
|
||||
|
||||
moment.locale("ru");
|
||||
const localeText = ruRU.components.MuiLocalizationProvider.defaultProps.localeText;
|
||||
|
||||
type Props = {
|
||||
quizSettings: QuizSettings;
|
||||
quizId: string;
|
||||
preview?: boolean;
|
||||
};
|
||||
|
||||
export default function QuizAnswerer({ quizSettings, quizId, preview = false }: Props) {
|
||||
const [rootContainerWidth, setRootContainerWidth] = useState<number>(() => window.innerWidth);
|
||||
const rootContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (rootContainerRef.current) setRootContainerWidth(rootContainerRef.current.clientWidth);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handleWindowResize = () => {
|
||||
startTransition(() => {
|
||||
if (rootContainerRef.current) setRootContainerWidth(rootContainerRef.current.clientWidth);
|
||||
});
|
||||
};
|
||||
window.addEventListener("resize", handleWindowResize);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", handleWindowResize);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<RootContainerWidthContext.Provider value={rootContainerWidth}>
|
||||
<QuizDataContext.Provider value={{ ...quizSettings, quizId, preview }}>
|
||||
<LocalizationProvider dateAdapter={AdapterMoment} adapterLocale="ru" localeText={localeText}>
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<SnackbarProvider
|
||||
preventDuplicate={true}
|
||||
style={{ backgroundColor: lightTheme.palette.brightPurple.main }}
|
||||
>
|
||||
<CssBaseline />
|
||||
<Box
|
||||
ref={rootContainerRef}
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<ErrorBoundary
|
||||
FallbackComponent={ApologyPage}
|
||||
onError={handleComponentError}
|
||||
>
|
||||
<ViewPublicationPage />
|
||||
</ErrorBoundary>
|
||||
</Box>
|
||||
</SnackbarProvider>
|
||||
</ThemeProvider>
|
||||
</LocalizationProvider>
|
||||
</QuizDataContext.Provider>
|
||||
</RootContainerWidthContext.Provider>
|
||||
);
|
||||
}
|
28
lib/components/ViewPublicationPage/ApologyPage.tsx
Normal file
28
lib/components/ViewPublicationPage/ApologyPage.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
import { Box, Typography } from "@mui/material";
|
||||
import { FallbackProps } from "react-error-boundary";
|
||||
|
||||
type Props = Partial<FallbackProps>;
|
||||
|
||||
export const ApologyPage = ({ error }: Props) => {
|
||||
let message = "Что-то пошло не так";
|
||||
|
||||
if (error.message === "No questions found") message = "Нет созданных вопросов";
|
||||
if (error.message === "Quiz already completed") message = "Вы уже прошли этот опрос";
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
textAlign: "center",
|
||||
}}
|
||||
>{message}</Typography>
|
||||
</Box>
|
||||
);
|
||||
};
|
436
lib/components/ViewPublicationPage/ContactForm.tsx
Normal file
436
lib/components/ViewPublicationPage/ContactForm.tsx
Normal file
@ -0,0 +1,436 @@
|
||||
import AddressIcon from "@icons/ContactFormIcon/AddressIcon";
|
||||
import EmailIcon from "@icons/ContactFormIcon/EmailIcon";
|
||||
import NameIcon from "@icons/ContactFormIcon/NameIcon";
|
||||
import PhoneIcon from "@icons/ContactFormIcon/PhoneIcon";
|
||||
import TextIcon from "@icons/ContactFormIcon/TextIcon";
|
||||
import { Box, Button, InputAdornment, Link, TextField as MuiTextField, TextFieldProps, Typography, useTheme } from "@mui/material";
|
||||
|
||||
import CustomCheckbox from "@ui_kit/CustomCheckbox";
|
||||
import { FC, useRef, useState } from "react";
|
||||
|
||||
import { sendFC } from "@api/quizRelase";
|
||||
import { NameplateLogo } from "@icons/NameplateLogo";
|
||||
import { QuizQuestionResult } from "@model/questionTypes/result";
|
||||
import { AnyTypedQuizQuestion } from "@model/questionTypes/shared";
|
||||
import { quizThemes } from "@utils/themes/Publication/themePublication";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
import { useRootContainerSize } from "../../contexts/RootContainerWidthContext";
|
||||
import { useQuizData } from "@contexts/QuizDataContext";
|
||||
|
||||
|
||||
const TextField = MuiTextField as unknown as FC<TextFieldProps>; // temporary fix ts(2590)
|
||||
const EMAIL_REGEXP = /^(([^<>()[\].,:\s@"]+(\.[^<>()[\].,:\s@"]+)*)|(".+"))@(([^<>()[\].,:\s@"]+\.)+[^<>()[\].,:\s@"]{2,})$/iu;
|
||||
|
||||
type Props = {
|
||||
currentQuestion: AnyTypedQuizQuestion;
|
||||
onShowResult: () => void;
|
||||
};
|
||||
|
||||
export const ContactForm = ({ currentQuestion, onShowResult }: Props) => {
|
||||
const theme = useTheme();
|
||||
const { settings, questions, quizId } = useQuizData();
|
||||
|
||||
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 fireOnce = useRef(true);
|
||||
const [fire, setFire] = useState(false);
|
||||
const isMobile = useRootContainerSize() < 850;
|
||||
|
||||
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 = {} as any;
|
||||
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 || "Фамилия"]: text };
|
||||
|
||||
if (Object.keys(body).length > 0) {
|
||||
try {
|
||||
await sendFC({
|
||||
questionId: currentQuestion.id,
|
||||
body: body,
|
||||
qid: quizId,
|
||||
});
|
||||
|
||||
const sessions = JSON.parse(localStorage.getItem("sessions") || "{}");
|
||||
localStorage.setItem(
|
||||
"sessions",
|
||||
JSON.stringify({ ...sessions, [quizId]: new Date().getTime() })
|
||||
);
|
||||
} catch (e) {
|
||||
enqueueSnackbar("ответ не был засчитан");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const FCcopy: any = settings.cfg.formContact.fields || settings.cfg.formContact;
|
||||
|
||||
const filteredFC: any = {};
|
||||
for (const i in FCcopy) {
|
||||
const field = FCcopy[i];
|
||||
if (field.used) {
|
||||
filteredFC[i] = field;
|
||||
}
|
||||
}
|
||||
const isWide = Object.keys(filteredFC).length > 2;
|
||||
|
||||
async function handleShowResultsClick() {
|
||||
//@ts-ignore
|
||||
const FC: any = settings.cfg.formContact.fields || settings.cfg.formContact;
|
||||
if (FC["email"].used !== EMAIL_REGEXP.test(email)) {
|
||||
return enqueueSnackbar("введена некорректная почта");
|
||||
}
|
||||
|
||||
|
||||
if (fireOnce.current) {
|
||||
if (
|
||||
name.length === 0
|
||||
&& email.length === 0
|
||||
&& phone.length === 0
|
||||
&& text.length === 0
|
||||
&& adress.length === 0
|
||||
) return enqueueSnackbar("Пожалуйста, заполните поля");
|
||||
|
||||
//почта валидна, хоть одно поле заполнено
|
||||
setFire(true);
|
||||
try {
|
||||
await inputHC();
|
||||
fireOnce.current = false;
|
||||
const sessions: any = JSON.parse(
|
||||
localStorage.getItem("sessions") || "{}"
|
||||
);
|
||||
sessions[quizId] = Date.now();
|
||||
localStorage.setItem(
|
||||
"sessions",
|
||||
JSON.stringify(sessions)
|
||||
);
|
||||
enqueueSnackbar("Данные успешно отправлены");
|
||||
} catch (e) {
|
||||
enqueueSnackbar("повторите попытку позже");
|
||||
}
|
||||
|
||||
onShowResult();
|
||||
}
|
||||
|
||||
setFire(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
backgroundColor: theme.palette.background.default,
|
||||
height: "100%",
|
||||
overflow: "auto",
|
||||
"&::-webkit-scrollbar": {
|
||||
width: "0",
|
||||
display: "none",
|
||||
msOverflowStyle: "none",
|
||||
},
|
||||
scrollbarWidth: "none",
|
||||
msOverflowStyle: "none",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: isWide && !isMobile ? "100%" : isMobile ? undefined : "530px",
|
||||
borderRadius: "4px",
|
||||
height: "90vh",
|
||||
display: isWide && !isMobile ? "flex" : undefined,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: isWide && !isMobile ? "100%" : undefined,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderRight: isWide && !isMobile ? "1px solid gray" : undefined,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
textAlign: "center",
|
||||
m: "20px 0",
|
||||
fontSize: "28px",
|
||||
color: theme.palette.text.primary,
|
||||
wordBreak: "break-word"
|
||||
}}
|
||||
>
|
||||
{settings.cfg.formContact.title ||
|
||||
"Заполните форму, чтобы получить результаты теста"}
|
||||
</Typography>
|
||||
{settings.cfg.formContact.desc && (
|
||||
<Typography
|
||||
sx={{
|
||||
color: theme.palette.text.primary,
|
||||
textAlign: "center",
|
||||
m: "20px 0",
|
||||
fontSize: "18px",
|
||||
wordBreak: "break-word"
|
||||
}}
|
||||
>
|
||||
{settings.cfg.formContact.desc}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
flexDirection: "column",
|
||||
backgroundColor: theme.palette.background.default,
|
||||
p: "30px",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
my: "20px",
|
||||
}}
|
||||
>
|
||||
<Inputs
|
||||
name={name}
|
||||
setName={setName}
|
||||
email={email}
|
||||
setEmail={setEmail}
|
||||
phone={phone}
|
||||
setPhone={setPhone}
|
||||
text={text}
|
||||
setText={setText}
|
||||
adress={adress}
|
||||
setAdress={setAdress}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{
|
||||
// resultQuestion &&
|
||||
// settings.cfg.resultInfo.when === "after" &&
|
||||
<Button
|
||||
disabled={!(ready && !fire)}
|
||||
variant="contained"
|
||||
onClick={handleShowResultsClick}
|
||||
>
|
||||
{settings.cfg.formContact?.button || "Получить результаты"}
|
||||
</Button>
|
||||
}
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
mt: "20px",
|
||||
width: isMobile ? "300px" : "450px",
|
||||
}}
|
||||
>
|
||||
<CustomCheckbox
|
||||
label=""
|
||||
handleChange={({ target }) => {
|
||||
setReady(target.checked);
|
||||
}}
|
||||
checked={ready}
|
||||
colorIcon={theme.palette.primary.main}
|
||||
/>
|
||||
<Typography sx={{ color: theme.palette.text.primary }}>
|
||||
С 
|
||||
<Link href={"https://shub.pena.digital/ppdd"} target="_blank">
|
||||
Положением об обработке персональных данных{" "}
|
||||
</Link>
|
||||
 и 
|
||||
<Link
|
||||
href={"https://shub.pena.digital/docs/privacy"}
|
||||
target="_blank"
|
||||
>
|
||||
{" "}
|
||||
Политикой конфиденциальности{" "}
|
||||
</Link>
|
||||
 ознакомлен
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
component={Link}
|
||||
target={"_blank"}
|
||||
href={"https://quiz.pena.digital"}
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
mt: "20px",
|
||||
gap: "15px",
|
||||
textDecoration: "none",
|
||||
}}
|
||||
>
|
||||
<NameplateLogo
|
||||
style={{
|
||||
fontSize: "34px",
|
||||
color: quizThemes[settings.cfg.theme].isLight ? "#151515" : "#FFFFFF",
|
||||
}}
|
||||
/>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "20px",
|
||||
color: quizThemes[settings.cfg.theme].isLight ? "#4D4D4D" : "#F5F7FF",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
Сделано на PenaQuiz
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const Inputs = ({
|
||||
name,
|
||||
setName,
|
||||
email,
|
||||
setEmail,
|
||||
phone,
|
||||
setPhone,
|
||||
text,
|
||||
setText,
|
||||
adress,
|
||||
setAdress,
|
||||
}: any) => {
|
||||
const { settings } = useQuizData();
|
||||
|
||||
// @ts-ignore
|
||||
const FC = settings.cfg.formContact.fields || settings.cfg.formContact;
|
||||
|
||||
if (!FC) return null;
|
||||
|
||||
//@ts-ignore
|
||||
const Name = (
|
||||
<CustomInput
|
||||
//@ts-ignore
|
||||
onChange={({ target }) => setName(target.value)}
|
||||
id={name}
|
||||
title={FC["name"].innerText || "Введите имя"}
|
||||
desc={FC["name"].text || "имя"}
|
||||
Icon={NameIcon}
|
||||
/>
|
||||
);
|
||||
//@ts-ignore
|
||||
const Email = (
|
||||
<CustomInput
|
||||
error={!EMAIL_REGEXP.test(email)}
|
||||
label={!EMAIL_REGEXP.test(email) ? "" : "Некорректная почта"}
|
||||
//@ts-ignore
|
||||
onChange={({ target }) => setEmail(target.value)}
|
||||
id={email}
|
||||
title={FC["email"].innerText || "Введите Email"}
|
||||
desc={FC["email"].text || "Email"}
|
||||
Icon={EmailIcon}
|
||||
/>
|
||||
);
|
||||
const Phone = (
|
||||
<CustomInput
|
||||
//@ts-ignore
|
||||
onChange={({ target }) => setPhone(target.value)}
|
||||
id={phone}
|
||||
title={FC["phone"].innerText || "Введите номер телефона"}
|
||||
desc={FC["phone"].text || "номер телефона"}
|
||||
Icon={PhoneIcon}
|
||||
/>
|
||||
);
|
||||
//@ts-ignore
|
||||
const Text = (
|
||||
<CustomInput
|
||||
//@ts-ignore
|
||||
onChange={({ target }) => setText(target.value)}
|
||||
id={text}
|
||||
title={FC["text"].text || "Введите фамилию"}
|
||||
desc={FC["text"].innerText || "фамилию"}
|
||||
Icon={TextIcon}
|
||||
/>
|
||||
);
|
||||
//@ts-ignore
|
||||
const Adress = (
|
||||
<CustomInput
|
||||
//@ts-ignore
|
||||
onChange={({ target }) => setAdress(target.value)}
|
||||
id={adress}
|
||||
title={FC["address"].innerText || "Введите адрес"}
|
||||
desc={FC["address"].text || "адрес"}
|
||||
Icon={AddressIcon}
|
||||
/>
|
||||
);
|
||||
|
||||
//@ts-ignore
|
||||
if (Object.values(FC).some((data) => data.used)) {
|
||||
return (
|
||||
<>
|
||||
{FC["name"].used ? Name : <></>}
|
||||
{FC["email"].used ? Email : <></>}
|
||||
{FC["phone"].used ? Phone : <></>}
|
||||
{FC["text"].used ? Text : <></>}
|
||||
{FC["address"].used ? Adress : <></>}
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
{Name}
|
||||
{Email}
|
||||
{Phone}
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const CustomInput = ({ title, desc, Icon, onChange }: any) => {
|
||||
const theme = useTheme();
|
||||
const isMobile = useRootContainerSize() < 600;
|
||||
//@ts-ignore
|
||||
return (
|
||||
<Box m="15px 0">
|
||||
<Typography mb="7px" color={theme.palette.text.primary}>
|
||||
{title}
|
||||
</Typography>
|
||||
|
||||
<TextField
|
||||
onChange={onChange}
|
||||
sx={{
|
||||
width: isMobile ? "300px" : "350px",
|
||||
}}
|
||||
placeholder={desc}
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<Icon color="gray" />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
111
lib/components/ViewPublicationPage/Footer.tsx
Normal file
111
lib/components/ViewPublicationPage/Footer.tsx
Normal file
@ -0,0 +1,111 @@
|
||||
import { useQuizData } from "@contexts/QuizDataContext";
|
||||
import { Box, Typography, useTheme } from "@mui/material";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
|
||||
type FooterProps = {
|
||||
stepNumber: number | null;
|
||||
nextButton: ReactNode;
|
||||
prevButton: ReactNode;
|
||||
};
|
||||
|
||||
export const Footer = ({ stepNumber, nextButton, prevButton }: FooterProps) => {
|
||||
const theme = useTheme();
|
||||
const { questions } = useQuizData();
|
||||
console.log(questions)
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
position: "relative",
|
||||
padding: "15px 0",
|
||||
borderTop: `1px solid ${theme.palette.grey[400]}`,
|
||||
height: '75px',
|
||||
display: "flex",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
maxWidth: "1000px",
|
||||
padding: "0 10px",
|
||||
margin: "0 auto",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "10px",
|
||||
}}
|
||||
>
|
||||
{/*{mode[settings.cfg.theme] ? (*/}
|
||||
{/* <NameplateLogoFQ style={{ fontSize: "34px", width:"200px", height:"auto" }} />*/}
|
||||
{/*):(*/}
|
||||
{/* <NameplateLogoFQDark style={{ fontSize: "34px", width:"200px", height:"auto" }} />*/}
|
||||
{/*)}*/}
|
||||
{stepNumber !== null &&
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "10px",
|
||||
marginRight: "auto",
|
||||
color: theme.palette.text.primary,
|
||||
}}
|
||||
>
|
||||
<Typography>Шаг</Typography>
|
||||
<Typography
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
fontWeight: "bold",
|
||||
borderRadius: "50%",
|
||||
width: "30px",
|
||||
height: "30px",
|
||||
color: "#FFF",
|
||||
background: theme.palette.primary.main,
|
||||
}}
|
||||
>
|
||||
{stepNumber}
|
||||
</Typography>
|
||||
<Typography>Из</Typography>
|
||||
<Typography sx={{ fontWeight: "bold" }}>
|
||||
{questions.filter(q => q.type !== "result").length}
|
||||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "10px",
|
||||
marginRight: "auto",
|
||||
// color: theme.palette.grey1.main,
|
||||
}}
|
||||
>
|
||||
{/* <Typography>Шаг</Typography>
|
||||
<Typography
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
fontWeight: "bold",
|
||||
borderRadius: "50%",
|
||||
width: "30px",
|
||||
height: "30px",
|
||||
color: "#FFF",
|
||||
background: theme.palette.brightPurple.main,
|
||||
}}
|
||||
>
|
||||
{stepNumber} */}
|
||||
{/* </Typography> */}
|
||||
{/* <Typography>Из</Typography>
|
||||
<Typography sx={{ fontWeight: "bold" }}>
|
||||
{questions.length}
|
||||
</Typography> */}
|
||||
</Box>
|
||||
{prevButton}
|
||||
{nextButton}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
97
lib/components/ViewPublicationPage/Question.tsx
Normal file
97
lib/components/ViewPublicationPage/Question.tsx
Normal file
@ -0,0 +1,97 @@
|
||||
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 { useQuizData } from "@contexts/QuizDataContext";
|
||||
import { notReachable } from "@utils/notReachable";
|
||||
import { quizThemes } from "@utils/themes/Publication/themePublication";
|
||||
import { ReactNode } from "react";
|
||||
import { useRootContainerSize } from "../../contexts/RootContainerWidthContext";
|
||||
|
||||
type Props = {
|
||||
currentQuestion: RealTypedQuizQuestion;
|
||||
currentQuestionStepNumber: number | null;
|
||||
nextButton: ReactNode;
|
||||
prevButton: ReactNode;
|
||||
};
|
||||
|
||||
export const Question = ({
|
||||
currentQuestion,
|
||||
currentQuestionStepNumber,
|
||||
nextButton,
|
||||
prevButton,
|
||||
}: Props) => {
|
||||
const theme = useTheme();
|
||||
const { settings } = useQuizData();
|
||||
const isMobile = useRootContainerSize() < 650;
|
||||
console.log(settings)
|
||||
return (
|
||||
<Box sx={{
|
||||
backgroundColor: theme.palette.background.default,
|
||||
height: isMobile ? "100%" : "100vh"
|
||||
}}>
|
||||
<Box sx={{
|
||||
height: "calc(100% - 75px)",
|
||||
width: "100%",
|
||||
maxWidth: "1440px",
|
||||
padding: "40px 25px 20px",
|
||||
margin: "0 auto",
|
||||
overflow: "auto",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-between"
|
||||
}}>
|
||||
<QuestionByType key={currentQuestion.id} question={currentQuestion} />
|
||||
{quizThemes[settings.cfg.theme].isLight ? (
|
||||
<Link target={"_blank"} href={"https://quiz.pena.digital"}>
|
||||
<NameplateLogoFQ style={{ fontSize: "34px", width: "200px", height: "auto" }} />
|
||||
</Link>
|
||||
) : (
|
||||
<Link target={"_blank"} href={"https://quiz.pena.digital"}>
|
||||
<NameplateLogoFQDark style={{ fontSize: "34px", width: "200px", height: "auto" }} />
|
||||
</Link>
|
||||
|
||||
)}
|
||||
</Box>
|
||||
<Footer
|
||||
stepNumber={currentQuestionStepNumber}
|
||||
prevButton={prevButton}
|
||||
nextButton={nextButton}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
function QuestionByType({ question }: {
|
||||
question: RealTypedQuizQuestion;
|
||||
}) {
|
||||
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} />;
|
||||
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);
|
||||
}
|
||||
}
|
203
lib/components/ViewPublicationPage/ResultForm.tsx
Normal file
203
lib/components/ViewPublicationPage/ResultForm.tsx
Normal file
@ -0,0 +1,203 @@
|
||||
import {
|
||||
Box,
|
||||
Button, Link,
|
||||
Typography,
|
||||
useTheme
|
||||
} from "@mui/material";
|
||||
|
||||
import { NameplateLogo } from "@icons/NameplateLogo";
|
||||
import YoutubeEmbedIframe from "./tools/YoutubeEmbedIframe";
|
||||
|
||||
import { useQuizData } from "@contexts/QuizDataContext";
|
||||
import { quizThemes } from "@utils/themes/Publication/themePublication";
|
||||
import { useRootContainerSize } from "../../contexts/RootContainerWidthContext";
|
||||
import type { QuizQuestionResult } from "../../model/questionTypes/result";
|
||||
import { setCurrentQuizStep } from "@stores/quizView";
|
||||
|
||||
|
||||
type ResultFormProps = {
|
||||
resultQuestion: QuizQuestionResult;
|
||||
};
|
||||
|
||||
export const ResultForm = ({ resultQuestion }: ResultFormProps) => {
|
||||
const theme = useTheme();
|
||||
const isMobile = useRootContainerSize() < 650;
|
||||
const { settings } = useQuizData();
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
height: "100%",
|
||||
width: "100vw",
|
||||
pt: "28px",
|
||||
overflow: "auto",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "start",
|
||||
width: isMobile ? "100%" : "490px",
|
||||
padding: isMobile ? "0 16px" : undefined,
|
||||
}}
|
||||
>
|
||||
{
|
||||
!resultQuestion?.content.useImage && resultQuestion.content.video && (
|
||||
<YoutubeEmbedIframe
|
||||
videoUrl={resultQuestion.content.video}
|
||||
containerSX={{
|
||||
width: isMobile ? "100%" : "490px",
|
||||
height: isMobile ? "100%" : "280px",
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
resultQuestion?.content.useImage && resultQuestion.content.back && (
|
||||
<Box
|
||||
component="img"
|
||||
src={resultQuestion.content.back}
|
||||
sx={{
|
||||
width: isMobile ? "100%" : "490px",
|
||||
height: isMobile ? "100%" : "280px",
|
||||
}}
|
||||
></Box>
|
||||
)
|
||||
}
|
||||
{resultQuestion.description !== "" &&
|
||||
resultQuestion.description !== " " && (
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "23px",
|
||||
fontWeight: 700,
|
||||
m: "20px 0",
|
||||
color: theme.palette.text.primary,
|
||||
wordBreak: "break-word"
|
||||
}}
|
||||
>
|
||||
{resultQuestion.description}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
<Typography
|
||||
sx={{
|
||||
m: "20px 0",
|
||||
color: theme.palette.text.primary,
|
||||
wordBreak: "break-word"
|
||||
}}
|
||||
>
|
||||
{resultQuestion.title}
|
||||
</Typography>
|
||||
|
||||
{
|
||||
resultQuestion.content.text !== "" &&
|
||||
resultQuestion.content.text !== " " && (
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "18px",
|
||||
m: "20px 0",
|
||||
wordBreak: "break-word",
|
||||
color: theme.palette.text.primary,
|
||||
}}
|
||||
>
|
||||
{
|
||||
resultQuestion.content.text
|
||||
}
|
||||
</Typography>
|
||||
)
|
||||
}
|
||||
</Box>
|
||||
|
||||
<Box width="100%">
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
justifyContent: "end",
|
||||
px: "20px",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
component={Link}
|
||||
target={"_blank"}
|
||||
href={"https://quiz.pena.digital"}
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
mt: "15px",
|
||||
gap: "10px",
|
||||
textDecoration: "none",
|
||||
mb: "5px"
|
||||
}}
|
||||
>
|
||||
<NameplateLogo
|
||||
style={{
|
||||
fontSize: "34px",
|
||||
color: quizThemes[settings.cfg.theme].isLight ? "#000000" : "#F5F7FF",
|
||||
}}
|
||||
/>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "20px",
|
||||
color: quizThemes[settings.cfg.theme].isLight ? "#4D4D4D" : "#F5F7FF",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
Сделано на PenaQuiz
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
boxShadow: "0 0 15px 0 rgba(0,0,0,.08)",
|
||||
width: "100%",
|
||||
flexDirection: "column",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
p:
|
||||
(
|
||||
settings.cfg.resultInfo.showResultForm === "before" &&
|
||||
!Boolean(settings.cfg.score)
|
||||
) ||
|
||||
(
|
||||
settings.cfg.resultInfo.showResultForm === "after" &&
|
||||
resultQuestion.content.redirect
|
||||
)
|
||||
? "20px" : "0",
|
||||
}}
|
||||
>
|
||||
{settings.cfg.resultInfo.showResultForm === "before" && !Boolean(settings.cfg.score) && (
|
||||
<Button
|
||||
onClick={() => setCurrentQuizStep("contactform")}
|
||||
variant="contained"
|
||||
sx={{
|
||||
p: "10px 20px",
|
||||
width: "auto",
|
||||
height: "50px",
|
||||
}}
|
||||
>
|
||||
{resultQuestion.content.hint.text || "Узнать подробнее"}
|
||||
</Button>
|
||||
)}
|
||||
{settings.cfg.resultInfo.showResultForm === "after" &&
|
||||
resultQuestion.content.redirect && (
|
||||
<Button
|
||||
href={resultQuestion.content.redirect}
|
||||
variant="contained"
|
||||
sx={{ p: "10px 20px", width: "auto", height: "50px" }}
|
||||
>
|
||||
{resultQuestion.content.hint.text || "Перейти на сайт"}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
490
lib/components/ViewPublicationPage/StartPageViewPublication.tsx
Normal file
490
lib/components/ViewPublicationPage/StartPageViewPublication.tsx
Normal file
@ -0,0 +1,490 @@
|
||||
import { Box, Button, ButtonBase, Link, Paper, Typography, useTheme } from "@mui/material";
|
||||
import { useUADevice } from "../../utils/hooks/useUADevice";
|
||||
import { notReachable } from "../../utils/notReachable";
|
||||
import YoutubeEmbedIframe from "./tools/YoutubeEmbedIframe";
|
||||
|
||||
import { NameplateLogo } from "@icons/NameplateLogo";
|
||||
import { QuizStartpageAlignType, QuizStartpageType } from "@model/settingsData";
|
||||
import { useQuizData } from "@contexts/QuizDataContext";
|
||||
import { quizThemes } from "@utils/themes/Publication/themePublication";
|
||||
import { useRootContainerSize } from "../../contexts/RootContainerWidthContext";
|
||||
import { setCurrentQuizStep } from "@stores/quizView";
|
||||
|
||||
|
||||
export const StartPageViewPublication = () => {
|
||||
const theme = useTheme();
|
||||
const { settings } = useQuizData();
|
||||
const { isMobileDevice } = useUADevice();
|
||||
const isMobile = useRootContainerSize() < 650;
|
||||
const isTablet = useRootContainerSize() < 800;
|
||||
|
||||
const handleCopyNumber = () => {
|
||||
navigator.clipboard.writeText(settings.cfg.info.phonenumber);
|
||||
};
|
||||
console.log(settings.cfg.startpage.background.type)
|
||||
|
||||
const background =
|
||||
settings.cfg.startpage.background.type === "image" ? (
|
||||
settings.cfg.startpage.background.desktop ? (
|
||||
<img
|
||||
src={settings.cfg.startpage.background.desktop}
|
||||
alt=""
|
||||
style={{
|
||||
width: (isMobile || settings.cfg.startpageType === "expanded") ? "100%" : undefined,
|
||||
height: "100%",
|
||||
objectFit: "cover",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
/>
|
||||
) : null
|
||||
) : settings.cfg.startpage.background.type === "video" ? (
|
||||
settings.cfg.startpage.background.video ? (
|
||||
<YoutubeEmbedIframe
|
||||
videoUrl={settings.cfg.startpage.background.video}
|
||||
containerSX={{
|
||||
width:
|
||||
settings.cfg.startpageType === "centered"
|
||||
? "550px"
|
||||
: settings.cfg.startpageType === "expanded"
|
||||
? "100vw"
|
||||
: "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;
|
||||
|
||||
return (
|
||||
<Paper
|
||||
className="settings-preview-draghandle"
|
||||
sx={{
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
background:
|
||||
settings.cfg.startpageType === "expanded" && !isMobile
|
||||
? settings.cfg.startpage.position === "left"
|
||||
? "linear-gradient(90deg,#272626,transparent)"
|
||||
: settings.cfg.startpage.position === "center"
|
||||
? "linear-gradient(180deg,transparent,#272626)"
|
||||
: "linear-gradient(270deg,#272626,transparent)"
|
||||
: theme.palette.background.default,
|
||||
|
||||
color: settings.cfg.startpageType === "expanded" ? "white" : "black",
|
||||
}}
|
||||
>
|
||||
<QuizPreviewLayoutByType
|
||||
quizHeaderBlock={
|
||||
<Box p={settings.cfg.startpageType === "standard" ? "" : "16px"}>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "20px",
|
||||
mb: "7px",
|
||||
}}
|
||||
>
|
||||
{settings.cfg.startpage.logo && (
|
||||
<img
|
||||
src={settings.cfg.startpage.logo}
|
||||
style={{
|
||||
height: "37px",
|
||||
maxWidth: "43px",
|
||||
objectFit: "cover",
|
||||
}}
|
||||
alt=""
|
||||
/>
|
||||
)}
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "14px",
|
||||
color: settings.cfg.startpageType === "expanded"
|
||||
&& !isMobile ? "white" : theme.palette.text.primary,
|
||||
wordBreak: "break-word"
|
||||
}}
|
||||
>{settings.cfg.info.orgname}</Typography>
|
||||
</Box>
|
||||
<Link mb="16px" href={settings.cfg.info.site}>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "16px",
|
||||
color: theme.palette.primary.main,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
maxWidth: isTablet ? "200px" : "300px"
|
||||
}}>
|
||||
{settings.cfg.info.site}
|
||||
</Typography>
|
||||
</Link>
|
||||
</Box>
|
||||
}
|
||||
quizMainBlock={
|
||||
<>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems:
|
||||
settings.cfg.startpageType === "centered"
|
||||
? "center"
|
||||
: settings.cfg.startpageType === "expanded"
|
||||
? settings.cfg.startpage.position === "center"
|
||||
? "center"
|
||||
: "start"
|
||||
: "start",
|
||||
mt: "28px",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
fontWeight: "bold",
|
||||
fontSize: "26px",
|
||||
fontStyle: "normal",
|
||||
fontStretch: "normal",
|
||||
lineHeight: "1.2",
|
||||
overflowWrap: "break-word",
|
||||
width: "100%",
|
||||
textAlign: settings.cfg.startpageType === "centered" || settings.cfg.startpage.position === "center" ? "center" : "-moz-initial",
|
||||
color: settings.cfg.startpageType === "expanded" && !isMobile ? "white" : theme.palette.text.primary
|
||||
}}
|
||||
>
|
||||
{settings.name}
|
||||
</Typography>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "16px",
|
||||
m: "16px 0",
|
||||
overflowWrap: "break-word",
|
||||
width: "100%",
|
||||
textAlign: settings.cfg.startpageType === "centered" || settings.cfg.startpage.position === "center" ? "center" : "-moz-initial",
|
||||
color: settings.cfg.startpageType === "expanded" && !isMobile ? "white" : theme.palette.text.primary
|
||||
}}
|
||||
>
|
||||
{settings.cfg.startpage.description}
|
||||
</Typography>
|
||||
<Box width={settings.cfg.startpageType === "standard" ? "100%" : "auto"}>
|
||||
<Button
|
||||
variant="contained"
|
||||
sx={{
|
||||
fontSize: "16px",
|
||||
padding: "10px 15px",
|
||||
width: settings.cfg.startpageType === "standard" ? "100%" : "auto",
|
||||
}}
|
||||
onClick={() => setCurrentQuizStep("question")}
|
||||
>
|
||||
{settings.cfg.startpage.button.trim() ? settings.cfg.startpage.button : "Пройти тест"}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
mt: "46px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
flexDirection: "column"
|
||||
}}
|
||||
>
|
||||
<Box sx={{ maxWidth: isTablet ? "240px" : "300px" }}>
|
||||
{settings.cfg.info.clickable ? (
|
||||
isMobileDevice ? (
|
||||
<Link href={`tel:${settings.cfg.info.phonenumber}`}>
|
||||
<Typography sx={{ fontSize: "16px", color: theme.palette.primary.main }}>
|
||||
{settings.cfg.info.phonenumber}
|
||||
</Typography>
|
||||
</Link>
|
||||
) : (
|
||||
<ButtonBase onClick={handleCopyNumber}>
|
||||
<Typography sx={{ fontSize: "16px", color: theme.palette.primary.main }}>
|
||||
{settings.cfg.info.phonenumber}
|
||||
</Typography>
|
||||
</ButtonBase>
|
||||
)
|
||||
) : (
|
||||
<Typography sx={{ fontSize: "16px", color: theme.palette.primary.main }}>
|
||||
{settings.cfg.info.phonenumber}
|
||||
</Typography>
|
||||
)}
|
||||
<Typography sx={{
|
||||
width: "100%",
|
||||
overflowWrap: "break-word",
|
||||
fontSize: "12px",
|
||||
textAlign: "end",
|
||||
maxHeight: "120px",
|
||||
overflow: "auto",
|
||||
color:
|
||||
settings.cfg.startpageType === "expanded" && !isMobile
|
||||
? "white"
|
||||
: theme.palette.text.primary,
|
||||
}}>
|
||||
{settings.cfg.info.law}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
component={Link}
|
||||
target={"_blank"}
|
||||
href={"https://quiz.pena.digital"}
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "15px",
|
||||
textDecoration: "none"
|
||||
}}
|
||||
>
|
||||
<NameplateLogo style={{ fontSize: "34px", color: settings.cfg.startpageType === "expanded" && !isMobile ? "#FFFFFF" : (quizThemes[settings.cfg.theme].isLight ? "#151515" : "#FFFFFF") }} />
|
||||
<Typography sx={{ fontSize: "20px", color: settings.cfg.startpageType === "expanded" && !isMobile ? "#F5F7FF" : (quizThemes[settings.cfg.theme].isLight ? "#4D4D4D" : "#F5F7FF"), whiteSpace: "nowrap", }}>
|
||||
Сделано на PenaQuiz
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
}
|
||||
backgroundBlock={background}
|
||||
startpageType={settings.cfg.startpageType}
|
||||
alignType={settings.cfg.startpage.position}
|
||||
/>
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
function QuizPreviewLayoutByType({
|
||||
quizHeaderBlock,
|
||||
quizMainBlock,
|
||||
backgroundBlock,
|
||||
startpageType,
|
||||
alignType,
|
||||
}: {
|
||||
quizHeaderBlock: JSX.Element;
|
||||
quizMainBlock: JSX.Element;
|
||||
backgroundBlock: JSX.Element | null;
|
||||
startpageType: QuizStartpageType;
|
||||
alignType: QuizStartpageAlignType;
|
||||
}) {
|
||||
const isMobile = useRootContainerSize() < 650;
|
||||
|
||||
function StartPageMobile() {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column-reverse",
|
||||
flexGrow: 1,
|
||||
justifyContent: "flex-end",
|
||||
height: "100%",
|
||||
"&::-webkit-scrollbar": { width: 0 },
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "flex-start",
|
||||
p: "25px",
|
||||
height: "80%",
|
||||
overflowY: "auto",
|
||||
overflowX: "hidden"
|
||||
}}
|
||||
>
|
||||
{quizHeaderBlock}
|
||||
<Box
|
||||
sx={{
|
||||
height: "80%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-between",
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
{quizMainBlock}
|
||||
</Box>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{backgroundBlock}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
switch (startpageType) {
|
||||
case null:
|
||||
case "standard": {
|
||||
return (
|
||||
<>
|
||||
{isMobile ? (
|
||||
<StartPageMobile />
|
||||
) : (
|
||||
<Box
|
||||
id="pain"
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: alignType === "left" ? (isMobile ? "column-reverse" : "row") : "row-reverse",
|
||||
flexGrow: 1,
|
||||
justifyContent: isMobile ? "flex-end" : undefined,
|
||||
height: "100%",
|
||||
"&::-webkit-scrollbar": { width: 0 },
|
||||
overflow: "auto"
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: isMobile ? "100%" : "40%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "flex-start",
|
||||
p: "25px",
|
||||
height: isMobile ? "80%" : undefined
|
||||
}}
|
||||
>
|
||||
{quizHeaderBlock}
|
||||
{quizMainBlock}
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
width: isMobile ? "100%" : "60%",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{backgroundBlock}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
</>
|
||||
|
||||
);
|
||||
}
|
||||
case "expanded": {
|
||||
return (
|
||||
<>
|
||||
{isMobile ? (
|
||||
<StartPageMobile />
|
||||
) : (
|
||||
<Box
|
||||
sx={{
|
||||
overflow: "auto",
|
||||
position: "relative",
|
||||
display: "flex",
|
||||
justifyContent: startpageAlignTypeToJustifyContent[alignType],
|
||||
flexGrow: 1,
|
||||
height: "100%",
|
||||
"&::-webkit-scrollbar": { width: 0 },
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: "40%",
|
||||
position: "relative",
|
||||
padding: "16px",
|
||||
zIndex: 3,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-between",
|
||||
alignItems: alignType === "center" ? "center" : "start",
|
||||
}}
|
||||
>
|
||||
{quizHeaderBlock}
|
||||
{quizMainBlock}
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
zIndex: -1,
|
||||
left: 0,
|
||||
top: 0,
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{backgroundBlock}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
</>
|
||||
|
||||
);
|
||||
}
|
||||
case "centered": {
|
||||
return (
|
||||
<>
|
||||
{isMobile ? (
|
||||
<StartPageMobile />
|
||||
) : (
|
||||
<Box
|
||||
sx={{
|
||||
overflow: "auto",
|
||||
padding: "16px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
height: "100%",
|
||||
"&::-webkit-scrollbar": { width: 0 },
|
||||
}}
|
||||
>
|
||||
{quizHeaderBlock}
|
||||
{backgroundBlock && (
|
||||
<Box
|
||||
sx={{
|
||||
width: "60%",
|
||||
height: "275px",
|
||||
// overflow: "hidden",
|
||||
display: "flex",
|
||||
justifyContent: "center"
|
||||
}}
|
||||
>
|
||||
{backgroundBlock}
|
||||
</Box>
|
||||
)}
|
||||
{quizMainBlock}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
</>
|
||||
|
||||
);
|
||||
}
|
||||
default:
|
||||
notReachable(startpageType);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const startpageAlignTypeToJustifyContent: Record<QuizStartpageAlignType, "start" | "center" | "end"> = {
|
||||
left: "start",
|
||||
center: "center",
|
||||
right: "end",
|
||||
};
|
98
lib/components/ViewPublicationPage/ViewPublicationPage.tsx
Normal file
98
lib/components/ViewPublicationPage/ViewPublicationPage.tsx
Normal file
@ -0,0 +1,98 @@
|
||||
import { Button, ThemeProvider } from "@mui/material";
|
||||
import { useQuizViewStore } from "@stores/quizView";
|
||||
import { useQuestionFlowControl } from "@utils/hooks/useQuestionFlowControl";
|
||||
import { useQuizData } from "@contexts/QuizDataContext";
|
||||
import { notReachable } from "@utils/notReachable";
|
||||
import { quizThemes } from "@utils/themes/Publication/themePublication";
|
||||
import { ReactElement, useEffect } from "react";
|
||||
import { useRootContainerSize } from "../../contexts/RootContainerWidthContext";
|
||||
import { ContactForm } from "./ContactForm";
|
||||
import { Question } from "./Question";
|
||||
import { ResultForm } from "./ResultForm";
|
||||
import { StartPageViewPublication } from "./StartPageViewPublication";
|
||||
|
||||
|
||||
export default function ViewPublicationPage() {
|
||||
const { settings, recentlyCompleted } = useQuizData();
|
||||
let currentQuizStep = useQuizViewStore(state => state.currentQuizStep);
|
||||
const isMobileMini = useRootContainerSize() < 382;
|
||||
const {
|
||||
currentQuestion,
|
||||
currentQuestionStepNumber,
|
||||
isNextButtonEnabled,
|
||||
isPreviousButtonEnabled,
|
||||
moveToPrevQuestion,
|
||||
moveToNextQuestion,
|
||||
showResultAfterContactForm,
|
||||
} = useQuestionFlowControl();
|
||||
|
||||
useEffect(function setFaviconAndTitle() {
|
||||
const link = document.querySelector('link[rel="icon"]');
|
||||
if (link && settings.cfg.startpage.favIcon) {
|
||||
link.setAttribute("href", settings.cfg.startpage.favIcon);
|
||||
}
|
||||
|
||||
document.title = settings.name;
|
||||
}, [settings]);
|
||||
|
||||
if (recentlyCompleted) throw new Error("Quiz already completed");
|
||||
if (currentQuizStep === "startpage" && settings.cfg.noStartPage) currentQuizStep = "question";
|
||||
|
||||
let quizStepElement: ReactElement;
|
||||
switch (currentQuizStep) {
|
||||
case "startpage": {
|
||||
quizStepElement = <StartPageViewPublication />;
|
||||
break;
|
||||
}
|
||||
case "question": {
|
||||
if (currentQuestion.type === "result") {
|
||||
quizStepElement = <ResultForm resultQuestion={currentQuestion} />;
|
||||
break;
|
||||
}
|
||||
|
||||
quizStepElement = (
|
||||
<Question
|
||||
currentQuestion={currentQuestion}
|
||||
currentQuestionStepNumber={currentQuestionStepNumber}
|
||||
prevButton={
|
||||
<Button
|
||||
disabled={!isPreviousButtonEnabled}
|
||||
variant="contained"
|
||||
sx={{ fontSize: "16px", padding: "10px 15px" }}
|
||||
onClick={moveToPrevQuestion}
|
||||
>
|
||||
{isMobileMini ? "←" : "← Назад"}
|
||||
</Button>
|
||||
}
|
||||
nextButton={
|
||||
<Button
|
||||
disabled={!isNextButtonEnabled}
|
||||
variant="contained"
|
||||
sx={{ fontSize: "16px", padding: "10px 15px" }}
|
||||
onClick={moveToNextQuestion}
|
||||
>
|
||||
Далее →
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "contactform": {
|
||||
quizStepElement = (
|
||||
<ContactForm
|
||||
currentQuestion={currentQuestion}
|
||||
onShowResult={showResultAfterContactForm}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
}
|
||||
default: notReachable(currentQuizStep);
|
||||
}
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={quizThemes[settings.cfg.theme || "StandardTheme"].theme}>
|
||||
{quizStepElement}
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
113
lib/components/ViewPublicationPage/questions/Date.tsx
Normal file
113
lib/components/ViewPublicationPage/questions/Date.tsx
Normal file
@ -0,0 +1,113 @@
|
||||
import moment from "moment";
|
||||
import { DatePicker } from "@mui/x-date-pickers";
|
||||
import { Box, Typography, useTheme } from "@mui/material";
|
||||
|
||||
import { useQuizViewStore, updateAnswer } from "@stores/quizView";
|
||||
|
||||
import type { QuizQuestionDate } from "../../../model/questionTypes/date";
|
||||
import CalendarIcon from "@icons/CalendarIcon";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
import { sendAnswer } from "@api/quizRelase";
|
||||
|
||||
import { quizThemes } from "@utils/themes/Publication/themePublication";
|
||||
import { useQuizData } from "@contexts/QuizDataContext";
|
||||
import { useState } from "react";
|
||||
|
||||
type DateProps = {
|
||||
currentQuestion: QuizQuestionDate;
|
||||
};
|
||||
|
||||
export const Date = ({ currentQuestion }: DateProps) => {
|
||||
const theme = useTheme();
|
||||
const { settings, quizId } = useQuizData();
|
||||
const { answers } = useQuizViewStore();
|
||||
const answer = answers.find(
|
||||
({ questionId }) => questionId === currentQuestion.id
|
||||
)?.answer as string;
|
||||
const currentAnswer = moment(answer) || moment();
|
||||
const [readySend, setReadySend] = useState(true)
|
||||
|
||||
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",
|
||||
}}
|
||||
>
|
||||
<DatePicker
|
||||
slots={{
|
||||
openPickerIcon: () => (
|
||||
<CalendarIcon
|
||||
sx={{
|
||||
"& path": { stroke: theme.palette.primary.main },
|
||||
"& rect": { stroke: theme.palette.primary.main },
|
||||
}}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
value={currentAnswer}
|
||||
onChange={async (date) => {
|
||||
if (readySend) {
|
||||
setReadySend(false)
|
||||
if (!date) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await sendAnswer({
|
||||
questionId: currentQuestion.id,
|
||||
body: moment(date).format("YYYY.MM.DD"),
|
||||
qid: quizId,
|
||||
});
|
||||
|
||||
updateAnswer(
|
||||
currentQuestion.id,
|
||||
date,
|
||||
0
|
||||
);
|
||||
} catch (e) {
|
||||
enqueueSnackbar("ответ не был засчитан");
|
||||
}
|
||||
setReadySend(true)
|
||||
}
|
||||
}}
|
||||
slotProps={{
|
||||
openPickerButton: {
|
||||
sx: {
|
||||
p: 0,
|
||||
},
|
||||
"data-cy": "open-datepicker",
|
||||
},
|
||||
layout: {
|
||||
sx: { backgroundColor: theme.palette.background.default },
|
||||
},
|
||||
}}
|
||||
sx={{
|
||||
"& .MuiInputBase-root": {
|
||||
backgroundColor: quizThemes[settings.cfg.theme].isLight
|
||||
? "white"
|
||||
: theme.palette.background.default,
|
||||
borderRadius: "10px",
|
||||
maxWidth: "250px",
|
||||
pr: "22px",
|
||||
"& input": {
|
||||
py: "11px",
|
||||
pl: "20px",
|
||||
lineHeight: "19px",
|
||||
},
|
||||
"& fieldset": {
|
||||
borderColor: "#9A9AAF",
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
192
lib/components/ViewPublicationPage/questions/Emoji.tsx
Normal file
192
lib/components/ViewPublicationPage/questions/Emoji.tsx
Normal file
@ -0,0 +1,192 @@
|
||||
import {
|
||||
Box,
|
||||
FormControl,
|
||||
FormControlLabel,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
Typography,
|
||||
useTheme
|
||||
} from "@mui/material";
|
||||
|
||||
import { deleteAnswer, updateAnswer, useQuizViewStore } from "@stores/quizView";
|
||||
|
||||
import RadioCheck from "@ui_kit/RadioCheck";
|
||||
import RadioIcon from "@ui_kit/RadioIcon";
|
||||
|
||||
import { sendAnswer } from "@api/quizRelase";
|
||||
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
import type { QuizQuestionEmoji } from "../../../model/questionTypes/emoji";
|
||||
import { useQuizData } from "@contexts/QuizDataContext";
|
||||
import { polyfillCountryFlagEmojis } from "country-flag-emoji-polyfill";
|
||||
|
||||
|
||||
polyfillCountryFlagEmojis();
|
||||
import { useState } from "react";
|
||||
|
||||
type EmojiProps = {
|
||||
currentQuestion: QuizQuestionEmoji;
|
||||
};
|
||||
|
||||
export const Emoji = ({ currentQuestion }: EmojiProps) => {
|
||||
const theme = useTheme();
|
||||
const { quizId } = useQuizData();
|
||||
const { answers } = useQuizViewStore();
|
||||
const { answer } =
|
||||
answers.find(
|
||||
({ questionId }) => questionId === currentQuestion.id
|
||||
) ?? {};
|
||||
const [readySend, setReadySend] = useState(true)
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Typography
|
||||
variant="h5"
|
||||
color={theme.palette.text.primary}
|
||||
sx={{ wordBreak: "break-word" }}
|
||||
>{currentQuestion.title}</Typography>
|
||||
<RadioGroup
|
||||
name={currentQuestion.id}
|
||||
value={currentQuestion.content.variants.findIndex(
|
||||
({ id }) => answer === id
|
||||
)}
|
||||
onChange={({ target }) => {
|
||||
updateAnswer(
|
||||
currentQuestion.id,
|
||||
currentQuestion.content.variants[Number(target.value)].answer,
|
||||
currentQuestion.content.variants[Number(target.value)].points || 0
|
||||
);
|
||||
}
|
||||
}
|
||||
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.map((variant, index) => (
|
||||
<FormControl
|
||||
key={variant.id}
|
||||
sx={{
|
||||
borderRadius: "12px",
|
||||
border: `1px solid`,
|
||||
borderColor: answer === variant.id ? theme.palette.primary.main : "#9A9AAF",
|
||||
overflow: "hidden",
|
||||
maxWidth: "317px",
|
||||
width: "100%",
|
||||
height: "255px",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
height: "193px",
|
||||
background: "#ffffff",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
{variant.extendedText && (
|
||||
<Typography fontSize={"100px"}>
|
||||
{variant.extendedText}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
<FormControlLabel
|
||||
key={variant.id}
|
||||
sx={{
|
||||
margin: 0,
|
||||
padding: "15px",
|
||||
color: theme.palette.text.primary,
|
||||
display: "flex",
|
||||
gap: "10px",
|
||||
alignItems:
|
||||
variant.answer.length <= 60 ? "center" : "flex-start",
|
||||
position: "relative",
|
||||
height: "80px",
|
||||
"& .MuiFormControlLabel-label": {
|
||||
wordBreak: "break-word",
|
||||
height: variant.answer.length <= 60 ? undefined : "60px",
|
||||
overflow: "auto",
|
||||
paddingLeft: "45px",
|
||||
"&::-webkit-scrollbar": {
|
||||
width: "4px",
|
||||
},
|
||||
"&::-webkit-scrollbar-thumb": {
|
||||
backgroundColor: "#b8babf",
|
||||
}
|
||||
},
|
||||
}}
|
||||
value={index}
|
||||
onClick={async (event) => {
|
||||
event.preventDefault();
|
||||
if (readySend) {
|
||||
setReadySend(false)
|
||||
try {
|
||||
|
||||
await sendAnswer({
|
||||
questionId: currentQuestion.id,
|
||||
body: currentQuestion.content.variants[index].extendedText + " " + currentQuestion.content.variants[index].answer,
|
||||
qid: quizId,
|
||||
});
|
||||
|
||||
updateAnswer(
|
||||
currentQuestion.id,
|
||||
currentQuestion.content.variants[index].id,
|
||||
currentQuestion.content.variants[index].points || 0
|
||||
);
|
||||
|
||||
} catch (e) {
|
||||
enqueueSnackbar("ответ не был засчитан");
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (answer === currentQuestion.content.variants[index].id) {
|
||||
deleteAnswer(currentQuestion.id);
|
||||
try {
|
||||
|
||||
await sendAnswer({
|
||||
questionId: currentQuestion.id,
|
||||
body: "",
|
||||
qid: quizId,
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
enqueueSnackbar("ответ не был засчитан");
|
||||
}
|
||||
}
|
||||
setReadySend(true)
|
||||
}
|
||||
|
||||
}}
|
||||
|
||||
control={
|
||||
<Radio checkedIcon={<RadioCheck color={theme.palette.primary.main} />} icon={<RadioIcon />} />
|
||||
}
|
||||
label={
|
||||
<Box sx={{ display: "flex", gap: "10px" }}>
|
||||
<Typography sx={{
|
||||
wordBreak: "break-word",
|
||||
lineHeight: "normal",
|
||||
}}>{variant.answer}</Typography>
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
))}
|
||||
</Box>
|
||||
</RadioGroup>
|
||||
</Box>
|
||||
);
|
||||
};
|
288
lib/components/ViewPublicationPage/questions/File.tsx
Normal file
288
lib/components/ViewPublicationPage/questions/File.tsx
Normal file
@ -0,0 +1,288 @@
|
||||
import {
|
||||
Box,
|
||||
ButtonBase,
|
||||
IconButton,
|
||||
Modal,
|
||||
Skeleton,
|
||||
Typography,
|
||||
useTheme
|
||||
} from "@mui/material";
|
||||
import { updateAnswer, useQuizViewStore } from "@stores/quizView";
|
||||
|
||||
import CloseBold from "@icons/CloseBold";
|
||||
import UploadIcon from "@icons/UploadIcon";
|
||||
|
||||
import { sendAnswer, sendFile } from "@api/quizRelase";
|
||||
import { useQuizData } from "@contexts/QuizDataContext";
|
||||
import Info from "@icons/Info";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
import { useState } from "react";
|
||||
import { useRootContainerSize } from "../../../contexts/RootContainerWidthContext";
|
||||
import type { QuizQuestionFile } from "../../../model/questionTypes/file";
|
||||
import { ACCEPT_SEND_FILE_TYPES_MAP, MAX_FILE_SIZE, UPLOAD_FILE_DESCRIPTIONS_MAP } from "../tools/fileUpload";
|
||||
|
||||
type ModalWarningType = "errorType" | "errorSize" | "picture" | "video" | "audio" | "document" | null;
|
||||
|
||||
type FileProps = {
|
||||
currentQuestion: QuizQuestionFile;
|
||||
};
|
||||
|
||||
export const File = ({ currentQuestion }: FileProps) => {
|
||||
const theme = useTheme();
|
||||
const { answers } = useQuizViewStore();
|
||||
const { quizId } = useQuizData();
|
||||
const [modalWarningType, setModalWarningType] = useState<ModalWarningType>(null);
|
||||
const [isSending, setIsSending] = useState<boolean>(false);
|
||||
const [isDropzoneHighlighted, setIsDropzoneHighlighted] = useState<boolean>(false);
|
||||
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;
|
||||
console.log(file.size)
|
||||
console.log(MAX_FILE_SIZE)
|
||||
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
|
||||
},
|
||||
qid: quizId,
|
||||
});
|
||||
console.log(data);
|
||||
|
||||
await sendAnswer({
|
||||
questionId: currentQuestion.id,
|
||||
body: `https://storage.yandexcloud.net/squizanswer/${quizId}/${currentQuestion.id}/${data.data.fileIDMap[currentQuestion.id]}`,
|
||||
qid: quizId,
|
||||
});
|
||||
|
||||
updateAnswer(currentQuestion.id, `${file.name}|${URL.createObjectURL(file)}`, 0);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
enqueueSnackbar("ответ не был засчитан");
|
||||
}
|
||||
|
||||
setIsSending(false);
|
||||
};
|
||||
|
||||
const onDrop = (event: React.DragEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
setIsDropzoneHighlighted(false);
|
||||
|
||||
const file = event.dataTransfer.files[0];
|
||||
|
||||
uploadFile(file);
|
||||
};
|
||||
|
||||
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] ? (
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: "15px" }}>
|
||||
<Typography color={theme.palette.text.primary}>Вы загрузили:</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={async () => {
|
||||
if (answer.length > 0) {
|
||||
setIsSending(true);
|
||||
await sendAnswer({
|
||||
questionId: currentQuestion.id,
|
||||
body: "",
|
||||
qid: quizId,
|
||||
});
|
||||
}
|
||||
console.log(answer);
|
||||
updateAnswer(currentQuestion.id, "", 0);
|
||||
setIsSending(false);
|
||||
}}
|
||||
>
|
||||
<CloseBold />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
) : (
|
||||
<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={e => uploadFile(e.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={(e) => e.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: theme.palette.background.default,
|
||||
border: `1px solid ${isDropzoneHighlighted ? "red" : "#9A9AAF"}`,
|
||||
borderRadius: "8px",
|
||||
}}
|
||||
>
|
||||
<UploadIcon />
|
||||
<Box>
|
||||
<Typography
|
||||
sx={{
|
||||
color: "#9A9AAF",
|
||||
// color: theme.palette.grey2.main,
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
{UPLOAD_FILE_DESCRIPTIONS_MAP[currentQuestion.content.type].title}
|
||||
</Typography>
|
||||
<Typography
|
||||
sx={{
|
||||
color: "#9A9AAF",
|
||||
// color: theme.palette.grey2.main,
|
||||
fontSize: "16px",
|
||||
lineHeight: "19px",
|
||||
}}
|
||||
>
|
||||
{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>
|
||||
)}
|
||||
{answer && currentQuestion.content.type === "picture" && (
|
||||
<img
|
||||
src={answer.split("|")[1]}
|
||||
alt=""
|
||||
style={{
|
||||
marginTop: "15px",
|
||||
maxWidth: "300px",
|
||||
maxHeight: "300px",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{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; }) => {
|
||||
|
||||
switch (status) {
|
||||
case null: return null;
|
||||
case 'errorType': return <Typography>Выбран некорректный тип файла</Typography>;
|
||||
case 'errorSize': return <Typography>Файл слишком большой. Максимальный размер 50 МБ</Typography>;
|
||||
default: return (
|
||||
<>
|
||||
<Typography>Допустимые расширения файлов:</Typography>
|
||||
<Typography>{
|
||||
ACCEPT_SEND_FILE_TYPES_MAP[status].join(" ")}</Typography>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
163
lib/components/ViewPublicationPage/questions/Images.tsx
Normal file
163
lib/components/ViewPublicationPage/questions/Images.tsx
Normal file
@ -0,0 +1,163 @@
|
||||
import {
|
||||
Box,
|
||||
FormControlLabel,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
Typography,
|
||||
useTheme
|
||||
} from "@mui/material";
|
||||
|
||||
import { deleteAnswer, updateAnswer, useQuizViewStore } from "@stores/quizView";
|
||||
import RadioCheck from "@ui_kit/RadioCheck";
|
||||
import RadioIcon from "@ui_kit/RadioIcon";
|
||||
|
||||
import { sendAnswer } from "@api/quizRelase";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
import { useRootContainerSize } from "../../../contexts/RootContainerWidthContext";
|
||||
import type { QuizQuestionImages } from "../../../model/questionTypes/images";
|
||||
import { useQuizData } from "@contexts/QuizDataContext";
|
||||
|
||||
type ImagesProps = {
|
||||
currentQuestion: QuizQuestionImages;
|
||||
};
|
||||
|
||||
export const Images = ({ currentQuestion }: ImagesProps) => {
|
||||
const { quizId } = useQuizData();
|
||||
const { answers } = useQuizViewStore();
|
||||
const theme = useTheme();
|
||||
const answer = answers.find(({ questionId }) => questionId === currentQuestion.id)?.answer;
|
||||
const isTablet = useRootContainerSize() < 1000;
|
||||
const isMobile = useRootContainerSize() < 500;
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Typography variant="h5" color={theme.palette.text.primary} sx={{ wordBreak: "break-word" }}>{currentQuestion.title}</Typography>
|
||||
<RadioGroup
|
||||
name={currentQuestion.id}
|
||||
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.map((variant, index) => (
|
||||
<Box
|
||||
key={index}
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
borderRadius: "5px",
|
||||
border: `1px solid`,
|
||||
borderColor: answer === variant.id ? theme.palette.primary.main : "#9A9AAF",
|
||||
}}
|
||||
onClick={async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
try {
|
||||
|
||||
await sendAnswer({
|
||||
questionId: currentQuestion.id,
|
||||
body: `${currentQuestion.content.variants[index].answer} <img style="width:100%; max-width:250px; max-height:250px" src="${currentQuestion.content.variants[index].extendedText}"/>`,
|
||||
qid: quizId,
|
||||
});
|
||||
|
||||
updateAnswer(
|
||||
currentQuestion.id,
|
||||
currentQuestion.content.variants[index].id,
|
||||
currentQuestion.content.variants[index].points || 0
|
||||
);
|
||||
|
||||
} catch (e) {
|
||||
enqueueSnackbar("ответ не был засчитан");
|
||||
}
|
||||
|
||||
|
||||
if (answer === currentQuestion.content.variants[index].id) {
|
||||
deleteAnswer(currentQuestion.id);
|
||||
try {
|
||||
|
||||
await sendAnswer({
|
||||
questionId: currentQuestion.id,
|
||||
body: "",
|
||||
qid: quizId,
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
enqueueSnackbar("ответ не был засчитан");
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: "10px" }}>
|
||||
<Box sx={{ width: "100%", height: "300px" }}>
|
||||
{variant.extendedText && (
|
||||
<img
|
||||
src={variant.extendedText}
|
||||
alt=""
|
||||
style={{
|
||||
display: "block",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "cover",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
<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",
|
||||
position: "relative",
|
||||
height: "80px",
|
||||
"& .MuiFormControlLabel-label": {
|
||||
wordBreak: "break-word",
|
||||
height: variant.answer.length <= 60 ? undefined : "60px",
|
||||
overflow: "auto",
|
||||
lineHeight: "normal",
|
||||
paddingLeft: "45px",
|
||||
"&::-webkit-scrollbar": {
|
||||
width: "4px",
|
||||
},
|
||||
"&::-webkit-scrollbar-thumb": {
|
||||
backgroundColor: "#b8babf",
|
||||
}
|
||||
},
|
||||
}}
|
||||
value={index}
|
||||
control={
|
||||
<Radio checkedIcon={<RadioCheck color={theme.palette.primary.main} />} icon={<RadioIcon />} />
|
||||
}
|
||||
label={variant.answer}
|
||||
/>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</RadioGroup>
|
||||
</Box>
|
||||
);
|
||||
|
||||
};
|
454
lib/components/ViewPublicationPage/questions/Number.tsx
Normal file
454
lib/components/ViewPublicationPage/questions/Number.tsx
Normal file
@ -0,0 +1,454 @@
|
||||
import { Box, Typography, useTheme } from "@mui/material";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
|
||||
import { CustomSlider } from "@ui_kit/CustomSlider";
|
||||
import CustomTextField from "@ui_kit/CustomTextField";
|
||||
|
||||
import { updateAnswer, useQuizViewStore } from "@stores/quizView";
|
||||
|
||||
import { sendAnswer } from "@api/quizRelase";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
import type { QuizQuestionNumber } from "@model/questionTypes/number";
|
||||
|
||||
import { useQuizData } from "@contexts/QuizDataContext";
|
||||
import { quizThemes } from "@utils/themes/Publication/themePublication";
|
||||
import { useRootContainerSize } from "@contexts/RootContainerWidthContext";
|
||||
|
||||
import type { ChangeEvent, SyntheticEvent } from "react";
|
||||
|
||||
type NumberProps = {
|
||||
currentQuestion: QuizQuestionNumber;
|
||||
};
|
||||
|
||||
export const Number = ({ currentQuestion }: NumberProps) => {
|
||||
const { settings, quizId } = useQuizData();
|
||||
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 theme = useTheme();
|
||||
const { answers } = useQuizViewStore();
|
||||
|
||||
const isMobile = useRootContainerSize() < 650;
|
||||
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 sendAnswerToBackend = async (value: string, noUpdate = false) => {
|
||||
try {
|
||||
await sendAnswer({
|
||||
questionId: currentQuestion.id,
|
||||
body: value,
|
||||
qid: quizId,
|
||||
});
|
||||
|
||||
if (!noUpdate) {
|
||||
updateAnswer(currentQuestion.id, value, 0);
|
||||
}
|
||||
} catch (e) {
|
||||
enqueueSnackbar("ответ не был засчитан");
|
||||
}
|
||||
};
|
||||
|
||||
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
|
||||
);
|
||||
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);
|
||||
|
||||
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));
|
||||
}
|
||||
}, []);
|
||||
|
||||
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",
|
||||
paddingRight: isMobile ? "10px" : undefined,
|
||||
}}
|
||||
>
|
||||
<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,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
{!currentQuestion.content.chooseRange && (
|
||||
<CustomTextField
|
||||
placeholder="0"
|
||||
value={reversed ? reversedInputValue : inputValue}
|
||||
onChange={onInputChange}
|
||||
sx={{
|
||||
maxWidth: "80px",
|
||||
borderColor: theme.palette.text.primary,
|
||||
"& .MuiInputBase-input": {
|
||||
textAlign: "center",
|
||||
backgroundColor: quizThemes[settings.cfg.theme].isLight
|
||||
? "white"
|
||||
: theme.palette.background.default,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{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,
|
||||
"& .MuiInputBase-input": {
|
||||
textAlign: "center",
|
||||
backgroundColor: quizThemes[settings.cfg.theme].isLight
|
||||
? "white"
|
||||
: theme.palette.background.default,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Typography color={theme.palette.text.primary}>до</Typography>
|
||||
<CustomTextField
|
||||
placeholder="0"
|
||||
value={reversed ? String(reversedMaxRange) : maxRange}
|
||||
onChange={onMaxInputChange}
|
||||
sx={{
|
||||
maxWidth: "80px",
|
||||
borderColor: theme.palette.text.primary,
|
||||
"& .MuiInputBase-input": {
|
||||
textAlign: "center",
|
||||
backgroundColor: quizThemes[settings.cfg.theme].isLight
|
||||
? "white"
|
||||
: theme.palette.background.default,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
@ -1,7 +1,5 @@
|
||||
import { Box, Typography, useTheme } from "@mui/material";
|
||||
|
||||
import { useQuizViewStore, updateAnswer } from "@stores/quizView/store";
|
||||
|
||||
import type { QuizQuestionPage } from "../../../model/questionTypes/page";
|
||||
import YoutubeEmbedIframe from "../tools/YoutubeEmbedIframe";
|
||||
|
||||
@ -11,13 +9,11 @@ type PageProps = {
|
||||
|
||||
export const Page = ({ currentQuestion }: PageProps) => {
|
||||
const theme = useTheme();
|
||||
const { answers } = useQuizViewStore();
|
||||
const { answer } = answers.find(({ questionId }) => questionId === currentQuestion.id) ?? {};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Typography variant="h5" sx={{ paddingBottom: "25px", color: theme.palette.text.primary }}>{currentQuestion.title}</Typography>
|
||||
<Typography color={theme.palette.text.primary}>{currentQuestion.content.text}</Typography>
|
||||
<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",
|
||||
@ -46,7 +42,7 @@ export const Page = ({ currentQuestion }: PageProps) => {
|
||||
<YoutubeEmbedIframe
|
||||
containerSX={{
|
||||
width: "100%",
|
||||
height: "calc( 100vh - 270px)",
|
||||
height: "calc(100% - 270px)",
|
||||
maxHeight: "80vh",
|
||||
objectFit: "contain",
|
||||
}}
|
143
lib/components/ViewPublicationPage/questions/Rating.tsx
Normal file
143
lib/components/ViewPublicationPage/questions/Rating.tsx
Normal file
@ -0,0 +1,143 @@
|
||||
import {
|
||||
Box,
|
||||
Rating as RatingComponent,
|
||||
Typography,
|
||||
useTheme
|
||||
} from "@mui/material";
|
||||
|
||||
import { updateAnswer, useQuizViewStore } from "@stores/quizView";
|
||||
|
||||
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 { sendAnswer } from "@api/quizRelase";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
import { useRootContainerSize } from "../../../contexts/RootContainerWidthContext";
|
||||
import type { QuizQuestionRating } from "../../../model/questionTypes/rating";
|
||||
import { useQuizData } from "@contexts/QuizDataContext";
|
||||
|
||||
type RatingProps = {
|
||||
currentQuestion: QuizQuestionRating;
|
||||
};
|
||||
|
||||
const buttonRatingForm = [
|
||||
{
|
||||
name: "star",
|
||||
icon: (color: string) => <StarIconMini width={50} color={color} />,
|
||||
},
|
||||
{
|
||||
name: "trophie",
|
||||
icon: (color: string) => <TropfyIcon color={color} />,
|
||||
},
|
||||
{
|
||||
name: "flag",
|
||||
icon: (color: string) => <FlagIcon color={color} />,
|
||||
},
|
||||
{
|
||||
name: "heart",
|
||||
icon: (color: string) => <HeartIcon color={color} />,
|
||||
},
|
||||
{
|
||||
name: "like",
|
||||
icon: (color: string) => <LikeIcon color={color} />,
|
||||
},
|
||||
{
|
||||
name: "bubble",
|
||||
icon: (color: string) => <LightbulbIcon color={color} />,
|
||||
},
|
||||
{
|
||||
name: "hashtag",
|
||||
icon: (color: string) => <HashtagIcon color={color} />,
|
||||
},
|
||||
];
|
||||
|
||||
export const Rating = ({ currentQuestion }: RatingProps) => {
|
||||
const { quizId } = useQuizData();
|
||||
const { answers } = useQuizViewStore();
|
||||
const theme = useTheme();
|
||||
const isMobile = useRootContainerSize() < 650;
|
||||
const { answer } =
|
||||
answers.find(
|
||||
({ questionId }) => questionId === currentQuestion.id
|
||||
) ?? {};
|
||||
const form = buttonRatingForm.find(
|
||||
({ name }) => name === currentQuestion.content.form
|
||||
);
|
||||
|
||||
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",
|
||||
width: isMobile ? "100%" : undefined,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "inline-block",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<RatingComponent
|
||||
value={Number(answer || 0)}
|
||||
onChange={async (_, value) => {
|
||||
|
||||
|
||||
try {
|
||||
|
||||
await sendAnswer({
|
||||
questionId: currentQuestion.id,
|
||||
body: String(value) + " из " + currentQuestion.content.steps,
|
||||
qid: quizId,
|
||||
});
|
||||
|
||||
updateAnswer(currentQuestion.id, String(value), 0);
|
||||
|
||||
} catch (e) {
|
||||
enqueueSnackbar("ответ не был засчитан");
|
||||
}
|
||||
}}
|
||||
sx={{
|
||||
height: "50px",
|
||||
gap: isMobile ? undefined : "15px",
|
||||
justifyContent: isMobile ? "space-between" : undefined,
|
||||
width: isMobile ? "100%" : undefined
|
||||
}}
|
||||
max={currentQuestion.content.steps}
|
||||
icon={form?.icon(theme.palette.primary.main)}
|
||||
emptyIcon={form?.icon("#9A9AAF")}
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
gap: 2,
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<Typography sx={{
|
||||
color: "#9A9AAF"
|
||||
// color: theme.palette.grey2.main
|
||||
}}>
|
||||
{currentQuestion.content.ratingNegativeDescription}
|
||||
</Typography>
|
||||
<Typography sx={{ color: "#9A9AAF" }}>
|
||||
{currentQuestion.content.ratingPositiveDescription}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
||||
};
|
78
lib/components/ViewPublicationPage/questions/Select.tsx
Normal file
78
lib/components/ViewPublicationPage/questions/Select.tsx
Normal file
@ -0,0 +1,78 @@
|
||||
import { Box, Typography, useTheme } from "@mui/material";
|
||||
|
||||
import { Select as SelectComponent } from "../tools//Select";
|
||||
|
||||
import { deleteAnswer, updateAnswer, useQuizViewStore } from "@stores/quizView";
|
||||
|
||||
import { sendAnswer } from "@api/quizRelase";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
import type { QuizQuestionSelect } from "../../../model/questionTypes/select";
|
||||
import { useQuizData } from "@contexts/QuizDataContext";
|
||||
|
||||
type SelectProps = {
|
||||
currentQuestion: QuizQuestionSelect;
|
||||
};
|
||||
|
||||
export const Select = ({ currentQuestion }: SelectProps) => {
|
||||
const theme = useTheme();
|
||||
const { quizId } = useQuizData();
|
||||
const { answers } = useQuizViewStore();
|
||||
const { answer } =
|
||||
answers.find(
|
||||
({ questionId }) => questionId === currentQuestion.id
|
||||
) ?? {};
|
||||
|
||||
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}
|
||||
onChange={async (_, value) => {
|
||||
if (value < 0) {
|
||||
deleteAnswer(currentQuestion.id);
|
||||
try {
|
||||
|
||||
await sendAnswer({
|
||||
questionId: currentQuestion.id,
|
||||
body: "",
|
||||
qid: quizId,
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
enqueueSnackbar("ответ не был засчитан");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
await sendAnswer({
|
||||
questionId: currentQuestion.id,
|
||||
body: String(currentQuestion.content.variants[Number(value)].answer),
|
||||
qid: quizId,
|
||||
});
|
||||
|
||||
updateAnswer(currentQuestion.id, String(value), 0);
|
||||
|
||||
} catch (e) {
|
||||
enqueueSnackbar("ответ не был засчитан");
|
||||
}
|
||||
|
||||
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
78
lib/components/ViewPublicationPage/questions/Text.tsx
Normal file
78
lib/components/ViewPublicationPage/questions/Text.tsx
Normal file
@ -0,0 +1,78 @@
|
||||
import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
|
||||
|
||||
import CustomTextField from "@ui_kit/CustomTextField";
|
||||
|
||||
import { updateAnswer, useQuizViewStore } from "@stores/quizView";
|
||||
|
||||
import { sendAnswer } from "@api/quizRelase";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
import type { QuizQuestionText } from "../../../model/questionTypes/text";
|
||||
import { useQuizData } from "@contexts/QuizDataContext";
|
||||
|
||||
type TextProps = {
|
||||
currentQuestion: QuizQuestionText;
|
||||
};
|
||||
|
||||
export const Text = ({ currentQuestion }: TextProps) => {
|
||||
const theme = useTheme();
|
||||
const { quizId } = useQuizData();
|
||||
const { answers } = useQuizViewStore();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down(650));
|
||||
const { answer } = answers.find(({ questionId }) => questionId === currentQuestion.id) ?? {};
|
||||
|
||||
const inputHC = useDebouncedCallback(async (text) => {
|
||||
try {
|
||||
|
||||
await sendAnswer({
|
||||
questionId: currentQuestion.id,
|
||||
body: text,
|
||||
qid: quizId,
|
||||
});
|
||||
|
||||
|
||||
} catch (e) {
|
||||
enqueueSnackbar("ответ не был засчитан");
|
||||
}
|
||||
}, 400);
|
||||
return (
|
||||
<Box>
|
||||
<Typography variant="h5" color={theme.palette.text.primary} sx={{wordBreak: "break-word"}}>{currentQuestion.title}</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
marginTop: "20px",
|
||||
flexDirection: isMobile ? "column-reverse" : undefined,
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<CustomTextField
|
||||
placeholder={currentQuestion.content.placeholder}
|
||||
//@ts-ignore
|
||||
value={answer || ""}
|
||||
onChange={async ({ target }) => {
|
||||
updateAnswer(currentQuestion.id, target.value, 0);
|
||||
inputHC(target.value);
|
||||
}
|
||||
}
|
||||
sx={{
|
||||
"&:focus-visible": {
|
||||
borderColor: theme.palette.primary.main
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{currentQuestion.content.back && currentQuestion.content.back !== " " && (
|
||||
<Box sx={{ maxWidth: "400px", width: "100%", height: "300px", margin: "15px" }}>
|
||||
<img
|
||||
key={currentQuestion.id}
|
||||
src={currentQuestion.content.back}
|
||||
style={{ width: "100%", height: "100%", objectFit: "cover" }}
|
||||
alt=""
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
279
lib/components/ViewPublicationPage/questions/Variant.tsx
Normal file
279
lib/components/ViewPublicationPage/questions/Variant.tsx
Normal file
@ -0,0 +1,279 @@
|
||||
import {
|
||||
Box,
|
||||
Checkbox,
|
||||
FormControlLabel,
|
||||
FormGroup,
|
||||
TextField as MuiTextField,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
TextFieldProps,
|
||||
Typography,
|
||||
useTheme
|
||||
} from "@mui/material";
|
||||
import { FC, useEffect, useState } from "react";
|
||||
|
||||
import {
|
||||
deleteAnswer,
|
||||
updateAnswer,
|
||||
updateOwnVariant,
|
||||
useQuizViewStore
|
||||
} from "@stores/quizView";
|
||||
|
||||
import { CheckboxIcon } from "@icons/Checkbox";
|
||||
import RadioCheck from "@ui_kit/RadioCheck";
|
||||
import RadioIcon from "@ui_kit/RadioIcon";
|
||||
|
||||
import { sendAnswer } from "@api/quizRelase";
|
||||
|
||||
import { useQuizData } from "@contexts/QuizDataContext";
|
||||
import { useRootContainerSize } from "@contexts/RootContainerWidthContext";
|
||||
import { quizThemes } from "@utils/themes/Publication/themePublication";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
import type { QuestionVariant } from "../../../model/questionTypes/shared";
|
||||
import type { QuizQuestionVariant } from "../../../model/questionTypes/variant";
|
||||
|
||||
const TextField = MuiTextField as unknown as FC<TextFieldProps>;
|
||||
|
||||
type VariantProps = {
|
||||
currentQuestion: QuizQuestionVariant;
|
||||
};
|
||||
|
||||
type VariantItemProps = {
|
||||
currentQuestion: QuizQuestionVariant;
|
||||
variant: QuestionVariant;
|
||||
answer: string | string[] | undefined;
|
||||
index: number;
|
||||
own?: boolean;
|
||||
readySend: boolean;
|
||||
setReadySend: (a: boolean) => void
|
||||
};
|
||||
|
||||
export const Variant = ({ currentQuestion }: VariantProps) => {
|
||||
const theme = useTheme();
|
||||
const isMobile = useRootContainerSize() < 650;
|
||||
const { answers, ownVariants } = useQuizViewStore();
|
||||
const { answer } =
|
||||
answers.find(
|
||||
({ questionId }) => questionId === currentQuestion.id
|
||||
) ?? {};
|
||||
const ownVariant = ownVariants.find(
|
||||
(variant) => variant.id === currentQuestion.id
|
||||
);
|
||||
|
||||
const [readySend, setReadySend] = useState(true)
|
||||
|
||||
const Group = currentQuestion.content.multi ? FormGroup : RadioGroup;
|
||||
|
||||
useEffect(() => {
|
||||
if (!ownVariant) {
|
||||
updateOwnVariant(currentQuestion.id, "");
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Typography variant="h5" color={theme.palette.text.primary} sx={{ wordBreak: "break-word" }}>{currentQuestion.title}</Typography>
|
||||
<Box 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.map((variant, index) => (
|
||||
<VariantItem
|
||||
key={variant.id}
|
||||
currentQuestion={currentQuestion}
|
||||
variant={variant}
|
||||
// @ts-ignore
|
||||
answer={answer}
|
||||
index={index}
|
||||
readySend={readySend}
|
||||
setReadySend={setReadySend}
|
||||
/>
|
||||
))}
|
||||
{currentQuestion.content.own && ownVariant && (
|
||||
<VariantItem
|
||||
own
|
||||
currentQuestion={currentQuestion}
|
||||
variant={ownVariant.variant}
|
||||
// @ts-ignore
|
||||
answer={answer}
|
||||
index={currentQuestion.content.variants.length + 2}
|
||||
readySend={readySend}
|
||||
setReadySend={setReadySend}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Group>
|
||||
{currentQuestion.content.back && currentQuestion.content.back !== " " && (
|
||||
<Box sx={{ maxWidth: "400px", width: "100%", height: "300px" }}>
|
||||
<img
|
||||
key={currentQuestion.id}
|
||||
src={currentQuestion.content.back}
|
||||
style={{ width: "100%", height: "100%", objectFit: "cover" }}
|
||||
alt=""
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const VariantItem = ({
|
||||
currentQuestion,
|
||||
variant,
|
||||
answer,
|
||||
index,
|
||||
own = false,
|
||||
readySend,
|
||||
setReadySend
|
||||
}: VariantItemProps) => {
|
||||
const theme = useTheme();
|
||||
const { settings, quizId } = useQuizData();
|
||||
|
||||
return (
|
||||
<FormControlLabel
|
||||
key={variant.id}
|
||||
sx={{
|
||||
margin: "0",
|
||||
borderRadius: "12px",
|
||||
color: theme.palette.text.primary,
|
||||
padding: "15px",
|
||||
border: `1px solid`,
|
||||
borderColor: answer === variant.id
|
||||
? theme.palette.primary.main
|
||||
: "#9A9AAF",
|
||||
backgroundColor: quizThemes[settings.cfg.theme].isLight
|
||||
? "white"
|
||||
: theme.palette.background.default,
|
||||
display: "flex",
|
||||
maxWidth: "685px",
|
||||
maxHeight: "85px",
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
"&.MuiFormControl-root": {
|
||||
width: "100%",
|
||||
},
|
||||
"& .MuiFormControlLabel-label": {
|
||||
wordBreak: "break-word",
|
||||
height: variant.answer.length <= 60 ? undefined : "60px",
|
||||
overflow: "auto",
|
||||
lineHeight: "normal",
|
||||
"&::-webkit-scrollbar": {
|
||||
width: "4px",
|
||||
},
|
||||
"&::-webkit-scrollbar-thumb": {
|
||||
backgroundColor: "#b8babf",
|
||||
}
|
||||
}
|
||||
}}
|
||||
value={index}
|
||||
labelPlacement="start"
|
||||
control={
|
||||
currentQuestion.content.multi ?
|
||||
<Checkbox
|
||||
checked={!!answer?.includes(variant.id)}
|
||||
checkedIcon={<CheckboxIcon checked color={theme.palette.primary.main} />}
|
||||
icon={<CheckboxIcon />}
|
||||
/>
|
||||
:
|
||||
<Radio checkedIcon={<RadioCheck color={theme.palette.primary.main} />} icon={<RadioIcon />} />
|
||||
}
|
||||
label={own ? <TextField label="Другое..." /> : variant.answer}
|
||||
onClick={async (event) => {
|
||||
event.preventDefault();
|
||||
if (readySend) {
|
||||
setReadySend(false)
|
||||
const variantId = currentQuestion.content.variants[index].id;
|
||||
console.log(answer);
|
||||
|
||||
if (currentQuestion.content.multi) {
|
||||
const currentAnswer = typeof answer !== "string" ? answer || [] : [];
|
||||
|
||||
try {
|
||||
await sendAnswer({
|
||||
questionId: currentQuestion.id,
|
||||
body: currentAnswer.includes(variantId)
|
||||
? currentAnswer?.filter((item) => item !== variantId)
|
||||
: [...currentAnswer, variantId],
|
||||
qid: quizId,
|
||||
});
|
||||
|
||||
updateAnswer(
|
||||
currentQuestion.id,
|
||||
currentAnswer.includes(variantId)
|
||||
? currentAnswer?.filter((item) => item !== variantId)
|
||||
: [...currentAnswer, variantId],
|
||||
currentQuestion.content.variants[index].points || 0
|
||||
);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
enqueueSnackbar("ответ не был засчитан");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await sendAnswer({
|
||||
questionId: currentQuestion.id,
|
||||
body: currentQuestion.content.variants[index].answer,
|
||||
qid: quizId,
|
||||
});
|
||||
|
||||
updateAnswer(currentQuestion.id, variantId,
|
||||
answer === variantId ? 0
|
||||
:
|
||||
currentQuestion.content.variants[index].points || 0
|
||||
);
|
||||
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
enqueueSnackbar("ответ не был засчитан");
|
||||
}
|
||||
|
||||
if (answer === variantId) {
|
||||
try {
|
||||
|
||||
await sendAnswer({
|
||||
questionId: currentQuestion.id,
|
||||
body: "",
|
||||
qid: quizId,
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
enqueueSnackbar("ответ не был засчитан");
|
||||
}
|
||||
deleteAnswer(currentQuestion.id);
|
||||
}
|
||||
setReadySend(true)
|
||||
}
|
||||
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
189
lib/components/ViewPublicationPage/questions/Varimg.tsx
Normal file
189
lib/components/ViewPublicationPage/questions/Varimg.tsx
Normal file
@ -0,0 +1,189 @@
|
||||
import {
|
||||
Box,
|
||||
FormControlLabel,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
Typography,
|
||||
useTheme
|
||||
} from "@mui/material";
|
||||
import { deleteAnswer, updateAnswer, useQuizViewStore } from "@stores/quizView";
|
||||
|
||||
import RadioCheck from "@ui_kit/RadioCheck";
|
||||
import RadioIcon from "@ui_kit/RadioIcon";
|
||||
|
||||
import { sendAnswer } from "@api/quizRelase";
|
||||
import BlankImage from "@icons/BlankImage";
|
||||
import { useQuizData } from "@contexts/QuizDataContext";
|
||||
import { quizThemes } from "@utils/themes/Publication/themePublication";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
import { useRootContainerSize } from "../../../contexts/RootContainerWidthContext";
|
||||
import type { QuizQuestionVarImg } from "../../../model/questionTypes/varimg";
|
||||
|
||||
type VarimgProps = {
|
||||
currentQuestion: QuizQuestionVarImg;
|
||||
};
|
||||
|
||||
export const Varimg = ({ currentQuestion }: VarimgProps) => {
|
||||
const { settings, quizId } = useQuizData();
|
||||
const { answers } = useQuizViewStore();
|
||||
const theme = useTheme();
|
||||
const isMobile = useRootContainerSize() < 650;
|
||||
|
||||
const { answer } =
|
||||
answers.find(
|
||||
({ questionId }) => questionId === currentQuestion.id
|
||||
) ?? {};
|
||||
const variant = currentQuestion.content.variants.find(
|
||||
({ id }) => answer === id
|
||||
);
|
||||
|
||||
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: isMobile ? "30px" : undefined,
|
||||
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: isMobile ? "20px" : undefined }}>
|
||||
{currentQuestion.content.variants.map((variant, index) => (
|
||||
<FormControlLabel
|
||||
key={variant.id}
|
||||
sx={{
|
||||
marginBottom: "15px",
|
||||
borderRadius: "5px",
|
||||
padding: "15px",
|
||||
color: theme.palette.text.primary,
|
||||
backgroundColor: 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: isMobile ? 0 : undefined,
|
||||
"& .MuiFormControlLabel-label": {
|
||||
wordBreak: "break-word",
|
||||
height: variant.answer.length <= 60 ? undefined : "60px",
|
||||
overflow: "auto",
|
||||
lineHeight: "normal",
|
||||
paddingLeft: "45px",
|
||||
"&::-webkit-scrollbar": {
|
||||
width: "4px",
|
||||
},
|
||||
"&::-webkit-scrollbar-thumb": {
|
||||
backgroundColor: "#b8babf",
|
||||
}
|
||||
}
|
||||
}}
|
||||
value={index}
|
||||
onClick={async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
|
||||
|
||||
try {
|
||||
|
||||
await sendAnswer({
|
||||
questionId: currentQuestion.id,
|
||||
body: `${currentQuestion.content.variants[index].answer} <img style="width:100%; max-width:250px; max-height:250px" src="${currentQuestion.content.variants[index].extendedText}"/>`,
|
||||
qid: quizId,
|
||||
});
|
||||
|
||||
updateAnswer(
|
||||
currentQuestion.id,
|
||||
currentQuestion.content.variants[index].id,
|
||||
currentQuestion.content.variants[index].points || 0
|
||||
);
|
||||
|
||||
} catch (e) {
|
||||
enqueueSnackbar("ответ не был засчитан");
|
||||
}
|
||||
|
||||
|
||||
if (answer === currentQuestion.content.variants[index].id) {
|
||||
try {
|
||||
|
||||
await sendAnswer({
|
||||
questionId: currentQuestion.id,
|
||||
body: "",
|
||||
qid: quizId,
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
enqueueSnackbar("ответ не был засчитан");
|
||||
}
|
||||
deleteAnswer(currentQuestion.id);
|
||||
}
|
||||
}}
|
||||
control={
|
||||
<Radio checkedIcon={<RadioCheck color={theme.palette.primary.main} />} icon={<RadioIcon />} />
|
||||
}
|
||||
label={variant.answer}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</RadioGroup>
|
||||
{/* {(variant?.extendedText || currentQuestion.content.back) && ( */}
|
||||
<Box
|
||||
sx={{
|
||||
maxWidth: "450px",
|
||||
width: "100%",
|
||||
height: "450px",
|
||||
border: "1px solid #9A9AAF",
|
||||
borderRadius: "12px",
|
||||
overflow: "hidden",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
backgroundColor: "#9A9AAF12",
|
||||
color: "#9A9AAF",
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
{answer ? (
|
||||
variant?.extendedText ? (
|
||||
<img
|
||||
src={variant?.extendedText}
|
||||
style={{ width: "100%", height: "100%", objectFit: "cover" }}
|
||||
alt=""
|
||||
/>
|
||||
) : (
|
||||
<BlankImage />
|
||||
)
|
||||
) : currentQuestion.content.back !== " "
|
||||
&& currentQuestion.content.back !== null
|
||||
&& currentQuestion.content.back.length > 0
|
||||
? (
|
||||
<img
|
||||
src={currentQuestion.content.back}
|
||||
style={{ width: "100%", height: "100%", objectFit: "cover" }}
|
||||
alt=""
|
||||
/>
|
||||
) : (currentQuestion.content.replText !== " " && currentQuestion.content.replText.length > 0) ? currentQuestion.content.replText : variant?.extendedText || isMobile ? (
|
||||
"Выберите вариант ответа ниже"
|
||||
) : (
|
||||
"Выберите вариант ответа слева"
|
||||
)}
|
||||
|
||||
</Box>
|
||||
{/* )} */}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
||||
};
|
3
src/pages/ViewPublicationPage/tools/Select.tsx → lib/components/ViewPublicationPage/tools/Select.tsx
3
src/pages/ViewPublicationPage/tools/Select.tsx → lib/components/ViewPublicationPage/tools/Select.tsx
@ -135,7 +135,8 @@ export const Select = ({
|
||||
padding: "10px",
|
||||
borderRadius: "5px",
|
||||
color: colorPlaceholder,
|
||||
whiteSpace: "normal"
|
||||
whiteSpace: "normal",
|
||||
wordBreak: "break-word"
|
||||
}}
|
||||
>
|
||||
{item}
|
16
lib/components/ViewPublicationPage/tools/checkEmptyData.ts
Normal file
16
lib/components/ViewPublicationPage/tools/checkEmptyData.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { QuizQuestionResult } from "@model/questionTypes/result";
|
||||
|
||||
export const isResultQuestionEmpty = (resultQuestion: QuizQuestionResult) => {
|
||||
if (
|
||||
(resultQuestion.title.length > 0 && resultQuestion.title !== " ")
|
||||
|| (resultQuestion.description.length > 0 && resultQuestion.description !== " ")
|
||||
|| (resultQuestion.content.back.length > 0 && resultQuestion.content.back !== " ")
|
||||
|| (resultQuestion.content.originalBack.length > 0 && resultQuestion.content.originalBack !== " ")
|
||||
|| (resultQuestion.content.innerName.length > 0 && resultQuestion.content.innerName !== " ")
|
||||
|| (resultQuestion.content.text.length > 0 && resultQuestion.content.text !== " ")
|
||||
|| (resultQuestion.content.video.length > 0 && resultQuestion.content.video !== " ")
|
||||
|| (resultQuestion.content.hint.text.length > 0 && resultQuestion.content.hint.text !== " ")
|
||||
) return false;
|
||||
|
||||
return true;
|
||||
};
|
69
lib/components/ViewPublicationPage/tools/fileUpload.ts
Normal file
69
lib/components/ViewPublicationPage/tools/fileUpload.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import { UploadFileType } from "@model/questionTypes/file";
|
||||
|
||||
|
||||
export const MAX_FILE_SIZE = 419430400;
|
||||
|
||||
export const UPLOAD_FILE_DESCRIPTIONS_MAP = {
|
||||
picture: {
|
||||
title: "Добавить изображение",
|
||||
description: "Принимает изображения",
|
||||
},
|
||||
video: {
|
||||
title: "Добавить видео",
|
||||
description: "Принимает .mp4 и .mov формат — максимум 50мб",
|
||||
},
|
||||
audio: { title: "Добавить аудиофайл", description: "Принимает аудиофайлы" },
|
||||
document: { title: "Добавить документ", description: "Принимает документы" },
|
||||
} as const satisfies Record<UploadFileType, { title: string; description: string; }>;
|
||||
|
||||
export const ACCEPT_SEND_FILE_TYPES_MAP = {
|
||||
picture: [
|
||||
".jpeg",
|
||||
".jpg",
|
||||
".png",
|
||||
".ico",
|
||||
".gif",
|
||||
".tiff",
|
||||
".webp",
|
||||
".eps",
|
||||
".svg"
|
||||
],
|
||||
video: [
|
||||
".mp4",
|
||||
".mov",
|
||||
".wmv",
|
||||
".avi",
|
||||
".avchd",
|
||||
".flv",
|
||||
".f4v",
|
||||
".swf",
|
||||
".mkv",
|
||||
".webm",
|
||||
".mpeg-2"
|
||||
],
|
||||
audio: [
|
||||
".aac",
|
||||
".aiff",
|
||||
".dsd",
|
||||
".flac",
|
||||
".mp3",
|
||||
".mqa",
|
||||
".ogg",
|
||||
".wav",
|
||||
".wma"
|
||||
],
|
||||
document: [
|
||||
".doc",
|
||||
".docx",
|
||||
".dotx",
|
||||
".rtf",
|
||||
".odt",
|
||||
".pdf",
|
||||
".txt",
|
||||
".xls",
|
||||
".ppt",
|
||||
".xlsx",
|
||||
".pptx",
|
||||
".pages",
|
||||
],
|
||||
} as const;
|
13
lib/contexts/QuizDataContext.ts
Normal file
13
lib/contexts/QuizDataContext.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { QuizSettings } from "@model/settingsData";
|
||||
import { createContext, useContext } from "react";
|
||||
|
||||
type QuizData = QuizSettings & { quizId: string; preview: boolean; };
|
||||
|
||||
export const QuizDataContext = createContext<QuizData | null>(null);
|
||||
|
||||
export const useQuizData = () => {
|
||||
const quizData = useContext(QuizDataContext);
|
||||
if (quizData === null) throw new Error("QuizData context is null");
|
||||
|
||||
return quizData;
|
||||
};
|
11
lib/contexts/RootContainerWidthContext.ts
Normal file
11
lib/contexts/RootContainerWidthContext.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { createContext, useContext } from "react";
|
||||
|
||||
|
||||
export const RootContainerWidthContext = createContext<number | null>(null);
|
||||
|
||||
export const useRootContainerSize = () => {
|
||||
const rootContainerSize = useContext(RootContainerWidthContext);
|
||||
if (rootContainerSize === null) throw new Error("rootContainerSize context is null");
|
||||
|
||||
return rootContainerSize;
|
||||
};
|
5
lib/index.ts
Normal file
5
lib/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import QuizAnswerer from "./components/QuizAnswerer";
|
||||
import type { QuizSettings } from "@model/settingsData";
|
||||
|
||||
export { QuizAnswerer };
|
||||
export type { QuizSettings };
|
54
lib/model/api/getQuizData.ts
Normal file
54
lib/model/api/getQuizData.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import { AnyTypedQuizQuestion } from "@model/questionTypes/shared";
|
||||
import { QuizSettings } from "@model/settingsData";
|
||||
|
||||
export interface GetQuizDataResponse {
|
||||
cnt: number;
|
||||
settings: {
|
||||
fp: boolean;
|
||||
rep: boolean;
|
||||
name: string;
|
||||
cfg: string;
|
||||
lim: number;
|
||||
due: number;
|
||||
delay: number;
|
||||
pausable: boolean;
|
||||
};
|
||||
items: {
|
||||
id: number;
|
||||
title: string;
|
||||
desc: string;
|
||||
typ: string;
|
||||
req: boolean;
|
||||
p: number;
|
||||
c: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export function parseQuizData(quizDataResponse: GetQuizDataResponse): Omit<QuizSettings, "recentlyCompleted"> {
|
||||
const items: QuizSettings["questions"] = quizDataResponse.items.map((item) => {
|
||||
const content = JSON.parse(item.c);
|
||||
|
||||
return {
|
||||
description: item.desc,
|
||||
id: item.id,
|
||||
page: item.p,
|
||||
required: item.req,
|
||||
title: item.title,
|
||||
type: item.typ,
|
||||
content
|
||||
} as unknown as AnyTypedQuizQuestion;
|
||||
});
|
||||
|
||||
const settings: QuizSettings["settings"] = {
|
||||
fp: quizDataResponse.settings.fp,
|
||||
rep: quizDataResponse.settings.rep,
|
||||
name: quizDataResponse.settings.name,
|
||||
cfg: JSON.parse(quizDataResponse?.settings.cfg),
|
||||
lim: quizDataResponse.settings.lim,
|
||||
due: quizDataResponse.settings.due,
|
||||
delay: quizDataResponse.settings.delay,
|
||||
pausable: quizDataResponse.settings.pausable
|
||||
};
|
||||
|
||||
return { cnt: quizDataResponse.cnt, settings, questions: items };
|
||||
}
|
@ -7,6 +7,7 @@ import type {
|
||||
export interface QuizQuestionDate extends QuizQuestionBase {
|
||||
type: "date";
|
||||
content: {
|
||||
id: string;
|
||||
/** Чекбокс "Необязательный вопрос" */
|
||||
required: boolean;
|
||||
/** Чекбокс "Внутреннее название вопроса" */
|
@ -8,6 +8,7 @@ import type {
|
||||
export interface QuizQuestionEmoji extends QuizQuestionBase {
|
||||
type: "emoji";
|
||||
content: {
|
||||
id: string;
|
||||
/** Чекбокс "Можно несколько" */
|
||||
multi: boolean;
|
||||
/** Чекбокс "Вариант "свой ответ"" */
|
@ -16,6 +16,7 @@ export type UploadFileType = keyof typeof UPLOAD_FILE_TYPES_MAP;
|
||||
export interface QuizQuestionFile extends QuizQuestionBase {
|
||||
type: "file";
|
||||
content: {
|
||||
id: string;
|
||||
/** Чекбокс "Необязательный вопрос" */
|
||||
required: boolean;
|
||||
/** Чекбокс "Внутреннее название вопроса" */
|
@ -8,6 +8,7 @@ import type {
|
||||
export interface QuizQuestionImages extends QuizQuestionBase {
|
||||
type: "images";
|
||||
content: {
|
||||
id: string;
|
||||
/** Чекбокс "Вариант "свой ответ"" */
|
||||
own: boolean;
|
||||
/** Чекбокс "Можно несколько" */
|
||||
@ -28,8 +29,8 @@ export interface QuizQuestionImages extends QuizQuestionBase {
|
||||
variants: QuestionVariant[];
|
||||
hint: QuestionHint;
|
||||
rule: QuestionBranchingRule;
|
||||
back: string;
|
||||
originalBack: string;
|
||||
back: string | null;
|
||||
originalBack: string | null;
|
||||
autofill: boolean;
|
||||
largeCheck: boolean;
|
||||
};
|
@ -7,6 +7,7 @@ import type {
|
||||
export interface QuizQuestionNumber extends QuizQuestionBase {
|
||||
type: "number";
|
||||
content: {
|
||||
id: string;
|
||||
/** Чекбокс "Необязательный вопрос" */
|
||||
required: boolean;
|
||||
/** Чекбокс "Внутреннее название вопроса" */
|
@ -7,6 +7,7 @@ import type {
|
||||
export interface QuizQuestionPage extends QuizQuestionBase {
|
||||
type: "page";
|
||||
content: {
|
||||
id: string;
|
||||
/** Чекбокс "Внутреннее название вопроса" */
|
||||
innerNameCheck: boolean;
|
||||
/** Поле "Внутреннее название вопроса" */
|
@ -7,6 +7,7 @@ import type {
|
||||
export interface QuizQuestionRating extends QuizQuestionBase {
|
||||
type: "rating";
|
||||
content: {
|
||||
id: string;
|
||||
/** Чекбокс "Необязательный вопрос" */
|
||||
required: boolean;
|
||||
/** Чекбокс "Внутреннее название вопроса" */
|
@ -4,9 +4,13 @@ import type {
|
||||
QuestionHint,
|
||||
} from "./shared";
|
||||
|
||||
interface ResultQuestionBranchingRule extends QuestionBranchingRule {
|
||||
minScore?: number
|
||||
}
|
||||
export interface QuizQuestionResult extends QuizQuestionBase {
|
||||
type: "result";
|
||||
content: {
|
||||
id: string;
|
||||
back: string;
|
||||
originalBack: string;
|
||||
video: string;
|
||||
@ -14,7 +18,7 @@ export interface QuizQuestionResult extends QuizQuestionBase {
|
||||
text: string;
|
||||
price: [number] | [number, number];
|
||||
useImage: boolean;
|
||||
rule: QuestionBranchingRule,
|
||||
rule: ResultQuestionBranchingRule,
|
||||
hint: QuestionHint;
|
||||
autofill: boolean;
|
||||
redirect: string
|
@ -8,6 +8,7 @@ import type {
|
||||
export interface QuizQuestionSelect extends QuizQuestionBase {
|
||||
type: "select";
|
||||
content: {
|
||||
id: string;
|
||||
/** Чекбокс "Можно несколько" */
|
||||
multi: boolean;
|
||||
/** Чекбокс "Необязательный вопрос" */
|
@ -1,3 +1,4 @@
|
||||
import { nanoid } from "nanoid";
|
||||
import type { QuizQuestionDate } from "./date";
|
||||
import type { QuizQuestionEmoji } from "./emoji";
|
||||
import type { QuizQuestionFile } from "./file";
|
||||
@ -5,24 +6,23 @@ import type { QuizQuestionImages } from "./images";
|
||||
import type { QuizQuestionNumber } from "./number";
|
||||
import type { QuizQuestionPage } from "./page";
|
||||
import type { QuizQuestionRating } from "./rating";
|
||||
import type { QuizQuestionResult } from "./result";
|
||||
import type { QuizQuestionSelect } from "./select";
|
||||
import type { QuizQuestionText } from "./text";
|
||||
import type { QuizQuestionVariant } from "./variant";
|
||||
import type { QuizQuestionVarImg } from "./varimg";
|
||||
import type { QuizQuestionResult } from "./result";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
export interface QuestionBranchingRuleMain {
|
||||
next: string;
|
||||
or: boolean;
|
||||
rules: {
|
||||
question: string; //id родителя (пока что)
|
||||
answers: string[]
|
||||
}[]
|
||||
next: string;
|
||||
or: boolean;
|
||||
rules: {
|
||||
question: string; //id родителя (пока что)
|
||||
answers: string[];
|
||||
}[];
|
||||
}
|
||||
export interface QuestionBranchingRule {
|
||||
|
||||
children: string[],
|
||||
export interface QuestionBranchingRule {
|
||||
children: string[];
|
||||
//список условий
|
||||
main: QuestionBranchingRuleMain[];
|
||||
parentId: string | null | "root";
|
||||
@ -46,6 +46,7 @@ export type QuestionVariant = {
|
||||
extendedText: string;
|
||||
/** Оригинал изображения (до кропа) */
|
||||
originalImageUrl: string;
|
||||
points?: number;
|
||||
};
|
||||
|
||||
export type QuestionType =
|
||||
@ -76,15 +77,15 @@ export interface QuizQuestionBase {
|
||||
required: boolean;
|
||||
deleteTimeoutId: number;
|
||||
content: {
|
||||
id: string;
|
||||
hint: QuestionHint;
|
||||
rule: QuestionBranchingRule;
|
||||
back: string;
|
||||
originalBack: string;
|
||||
back: string | null;
|
||||
originalBack: string | null;
|
||||
autofill: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export type AnyTypedQuizQuestion =
|
||||
| QuizQuestionVariant
|
||||
| QuizQuestionImages
|
||||
@ -99,7 +100,7 @@ export type AnyTypedQuizQuestion =
|
||||
| QuizQuestionRating
|
||||
| QuizQuestionResult;
|
||||
|
||||
|
||||
export type RealTypedQuizQuestion = Exclude<AnyTypedQuizQuestion, QuizQuestionResult>;
|
||||
|
||||
type FilterQuestionsWithVariants<T> = T extends {
|
||||
content: { variants: QuestionVariant[]; };
|
||||
@ -108,18 +109,19 @@ type FilterQuestionsWithVariants<T> = T extends {
|
||||
export type QuizQuestionsWithVariants = FilterQuestionsWithVariants<AnyTypedQuizQuestion>;
|
||||
|
||||
|
||||
export const createBranchingRuleMain: (targetId:string, parentId:string) => QuestionBranchingRuleMain = (targetId, parentId) => ({
|
||||
export const createBranchingRuleMain: (targetId: string, parentId: string) => QuestionBranchingRuleMain = (targetId, parentId) => ({
|
||||
next: targetId,
|
||||
or: false,
|
||||
rules: [{
|
||||
question: parentId,
|
||||
answers: [] as string[],
|
||||
}]
|
||||
})
|
||||
});
|
||||
|
||||
export const createQuestionVariant: () => QuestionVariant = () => ({
|
||||
id: nanoid(),
|
||||
answer: "",
|
||||
extendedText: "",
|
||||
hints: "",
|
||||
originalImageUrl: "",
|
||||
});
|
||||
});
|
@ -7,7 +7,7 @@ import type {
|
||||
export interface QuizQuestionText extends QuizQuestionBase {
|
||||
type: "text";
|
||||
content: {
|
||||
id: number;
|
||||
id: string;
|
||||
placeholder: string;
|
||||
/** Чекбокс "Внутреннее название вопроса" */
|
||||
innerNameCheck: boolean;
|
@ -8,6 +8,7 @@ import type {
|
||||
export interface QuizQuestionVariant extends QuizQuestionBase {
|
||||
type: "variant";
|
||||
content: {
|
||||
id: string;
|
||||
/** Чекбокс "Длинный текстовый ответ" */
|
||||
largeCheck: boolean;
|
||||
/** Чекбокс "Можно несколько" */
|
@ -8,6 +8,7 @@ import type {
|
||||
export interface QuizQuestionVarImg extends QuizQuestionBase {
|
||||
type: "varimg";
|
||||
content: {
|
||||
id: string;
|
||||
/** Чекбокс "Вариант "свой ответ"" */
|
||||
own: boolean;
|
||||
/** Чекбокс "Внутреннее название вопроса" */
|
123
lib/model/settingsData.ts
Normal file
123
lib/model/settingsData.ts
Normal file
@ -0,0 +1,123 @@
|
||||
import { AnyTypedQuizQuestion } from "./questionTypes/shared";
|
||||
|
||||
export type QuizStartpageType = "standard" | "expanded" | "centered" | null;
|
||||
|
||||
export type QuizStartpageAlignType = "left" | "right" | "center";
|
||||
|
||||
export type QuizType = "quiz" | "form" | null;
|
||||
|
||||
export type QuizResultsType = true | null;
|
||||
|
||||
export type QuizStep = "startpage" | "question" | "contactform";
|
||||
|
||||
export type QuizTheme =
|
||||
| "StandardTheme"
|
||||
| "StandardDarkTheme"
|
||||
| "PinkTheme"
|
||||
| "PinkDarkTheme"
|
||||
| "BlackWhiteTheme"
|
||||
| "OliveTheme"
|
||||
| "YellowTheme"
|
||||
| "GoldDarkTheme"
|
||||
| "PurpleTheme"
|
||||
| "BlueTheme"
|
||||
| "BlueDarkTheme";
|
||||
|
||||
export type FCField = {
|
||||
text: string;
|
||||
innerText: string;
|
||||
key: string;
|
||||
required: boolean;
|
||||
used: boolean;
|
||||
};
|
||||
|
||||
export type QuizSettings = {
|
||||
questions: AnyTypedQuizQuestion[];
|
||||
settings: {
|
||||
fp: boolean;
|
||||
rep: boolean;
|
||||
name: string;
|
||||
lim: number;
|
||||
due: number;
|
||||
delay: number;
|
||||
pausable: boolean;
|
||||
cfg: QuizConfig;
|
||||
};
|
||||
cnt: number;
|
||||
recentlyCompleted: boolean;
|
||||
};
|
||||
|
||||
export interface QuizConfig {
|
||||
type: QuizType;
|
||||
noStartPage: boolean;
|
||||
startpageType: QuizStartpageType;
|
||||
score?: boolean;
|
||||
results: QuizResultsType;
|
||||
haveRoot: string | null;
|
||||
theme: QuizTheme;
|
||||
resultInfo: {
|
||||
when: "email" | "";
|
||||
share: boolean;
|
||||
replay: boolean;
|
||||
theme: string;
|
||||
reply: string;
|
||||
replname: string;
|
||||
showResultForm: "before" | "after";
|
||||
};
|
||||
startpage: {
|
||||
description: string;
|
||||
button: string;
|
||||
position: QuizStartpageAlignType;
|
||||
favIcon: string | null;
|
||||
logo: string | null;
|
||||
originalLogo: string | null;
|
||||
background: {
|
||||
type: null | "image" | "video";
|
||||
desktop: string | null;
|
||||
originalDesktop: string | null;
|
||||
mobile: string | null;
|
||||
originalMobile: string | null;
|
||||
video: string | null;
|
||||
cycle: boolean;
|
||||
};
|
||||
};
|
||||
formContact: {
|
||||
title: string;
|
||||
desc: string;
|
||||
fields: Record<FormContactFieldName, FormContactFieldData>;
|
||||
button: string;
|
||||
};
|
||||
info: {
|
||||
phonenumber: string;
|
||||
clickable: boolean;
|
||||
orgname: string;
|
||||
site: string;
|
||||
law?: string;
|
||||
};
|
||||
meta: string;
|
||||
}
|
||||
|
||||
export type FormContactFieldName =
|
||||
| "name"
|
||||
| "email"
|
||||
| "phone"
|
||||
| "text"
|
||||
| "address";
|
||||
|
||||
type FormContactFieldData = {
|
||||
text: string;
|
||||
innerText: string;
|
||||
key: string;
|
||||
required: boolean;
|
||||
used: boolean;
|
||||
};
|
||||
|
||||
export interface QuizItems {
|
||||
description: string;
|
||||
id: number;
|
||||
page: number;
|
||||
required: boolean;
|
||||
title: string;
|
||||
type: string;
|
||||
content: unknown;
|
||||
}
|
123
lib/stores/quizView.ts
Normal file
123
lib/stores/quizView.ts
Normal file
@ -0,0 +1,123 @@
|
||||
import { QuestionVariant } from "@model/questionTypes/shared";
|
||||
import { produce } from "immer";
|
||||
import { nanoid } from "nanoid";
|
||||
import { create } from "zustand";
|
||||
import { devtools } from "zustand/middleware";
|
||||
import type { Moment } from "moment";
|
||||
import { QuizStep } from "@model/settingsData";
|
||||
|
||||
type QuestionAnswer = {
|
||||
questionId: string;
|
||||
answer: string | string[] | Moment;
|
||||
};
|
||||
|
||||
type OwnVariant = {
|
||||
id: string;
|
||||
variant: QuestionVariant;
|
||||
};
|
||||
|
||||
interface QuizViewStore {
|
||||
answers: QuestionAnswer[];
|
||||
ownVariants: OwnVariant[];
|
||||
pointsSum: number;
|
||||
points: Record<string, number>;
|
||||
currentQuizStep: QuizStep;
|
||||
}
|
||||
|
||||
export const useQuizViewStore = create<QuizViewStore>()(
|
||||
devtools(
|
||||
(set, get) => ({
|
||||
answers: [],
|
||||
ownVariants: [],
|
||||
points: {},
|
||||
pointsSum: 0,
|
||||
currentQuizStep: "startpage",
|
||||
}),
|
||||
{
|
||||
name: "quizView",
|
||||
enabled: import.meta.env.DEV,
|
||||
trace: import.meta.env.DEV,
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
function setProducedState<A extends string | { type: string; }>(
|
||||
recipe: (state: QuizViewStore) => void,
|
||||
action?: A,
|
||||
) {
|
||||
useQuizViewStore.setState(state => produce(state, recipe), false, action);
|
||||
}
|
||||
|
||||
const calcPoints = () => {
|
||||
const storePoints = useQuizViewStore.getState().points;
|
||||
let sum = Object.values(storePoints).reduce((accumulator, currentValue) => accumulator + currentValue)
|
||||
console.log("сумма ", sum)
|
||||
useQuizViewStore.setState({ pointsSum: sum })
|
||||
}
|
||||
|
||||
export const updateAnswer = (
|
||||
questionId: string,
|
||||
answer: string | string[] | Moment,
|
||||
points: number
|
||||
) => {
|
||||
setProducedState(state => {
|
||||
const index = state.answers.findIndex(answer => questionId === answer.questionId);
|
||||
|
||||
if (index < 0) {
|
||||
state.answers.push({ questionId, answer });
|
||||
} else {
|
||||
state.answers[index] = { questionId, answer };
|
||||
}
|
||||
}, {
|
||||
type: "updateAnswer",
|
||||
questionId,
|
||||
answer
|
||||
})
|
||||
const storePoints = useQuizViewStore.getState().points;
|
||||
useQuizViewStore.setState({ points: { ...storePoints, ...{ [questionId]: points } } })
|
||||
calcPoints()
|
||||
};
|
||||
|
||||
export const deleteAnswer = (questionId: string) => useQuizViewStore.setState(state => ({
|
||||
answers: state.answers.filter(answer => questionId !== answer.questionId)
|
||||
}), false, {
|
||||
type: "deleteAnswer",
|
||||
questionId
|
||||
});
|
||||
|
||||
export const updateOwnVariant = (id: string, answer: string) => setProducedState(state => {
|
||||
const index = state.ownVariants.findIndex((variant) => variant.id === id);
|
||||
|
||||
if (index < 0) {
|
||||
state.ownVariants.push({
|
||||
id,
|
||||
variant: {
|
||||
id: nanoid(),
|
||||
answer,
|
||||
extendedText: "",
|
||||
hints: "",
|
||||
originalImageUrl: "",
|
||||
},
|
||||
});
|
||||
} else {
|
||||
state.ownVariants[index].variant.answer = answer;
|
||||
}
|
||||
}, {
|
||||
type: "updateOwnVariant",
|
||||
id,
|
||||
answer
|
||||
});
|
||||
|
||||
export const deleteOwnVariant = (id: string) => useQuizViewStore.setState(state => ({
|
||||
ownVariants: state.ownVariants.filter((variant) => variant.id !== id)
|
||||
}), false, {
|
||||
type: "deleteOwnVariant",
|
||||
id
|
||||
});
|
||||
|
||||
export const setCurrentQuizStep = (currentQuizStep: QuizStep) => useQuizViewStore.setState({
|
||||
currentQuizStep
|
||||
}, false, {
|
||||
type: "setCurrentQuizStep",
|
||||
currentQuizStep
|
||||
});
|
5
src/ui_kit/CustomCheckbox.tsx → lib/ui_kit/CustomCheckbox.tsx
Executable file → Normal file
5
src/ui_kit/CustomCheckbox.tsx → lib/ui_kit/CustomCheckbox.tsx
Executable file → Normal file
@ -1,4 +1,4 @@
|
||||
import { FormControlLabel, Checkbox, useTheme, Box, useMediaQuery } from "@mui/material";
|
||||
import { Checkbox, FormControlLabel } from "@mui/material";
|
||||
import React from "react";
|
||||
|
||||
import { CheckboxIcon } from "@icons/Checkbox";
|
||||
@ -15,8 +15,6 @@ interface Props {
|
||||
}
|
||||
|
||||
export default function CustomCheckbox({ label, handleChange, checked, sx, dataCy, colorIcon }: Props) {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down(790));
|
||||
|
||||
return (
|
||||
<FormControlLabel
|
||||
@ -25,7 +23,6 @@ export default function CustomCheckbox({ label, handleChange, checked, sx, dataC
|
||||
sx={{ padding: "0px 13px 1px 11px" }}
|
||||
disableRipple
|
||||
icon={<CheckboxIcon />}
|
||||
//@ts-ignore
|
||||
checkedIcon={<CheckboxIcon checked color={colorIcon} />}
|
||||
onChange={handleChange}
|
||||
checked={checked}
|
@ -1,5 +1,7 @@
|
||||
import { Slider, SxProps, Theme, useTheme } from "@mui/material";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
type CustomSliderProps = {
|
||||
defaultValue?: number;
|
||||
value?: number | number[];
|
||||
@ -8,7 +10,11 @@ type CustomSliderProps = {
|
||||
step?: number;
|
||||
sx?: SxProps<Theme>;
|
||||
onChange?: (_: Event, value: number | number[]) => void;
|
||||
onChangeCommitted?: (_: React.SyntheticEvent | Event, value: number | number[]) => void;
|
||||
onChangeCommitted?: (
|
||||
_: React.SyntheticEvent | Event,
|
||||
value: number | number[]
|
||||
) => void;
|
||||
valueLabelFormat?: (value: number, index: number) => ReactNode;
|
||||
};
|
||||
|
||||
export const CustomSlider = ({
|
||||
@ -19,6 +25,7 @@ export const CustomSlider = ({
|
||||
step,
|
||||
onChange,
|
||||
onChangeCommitted,
|
||||
valueLabelFormat,
|
||||
sx,
|
||||
}: CustomSliderProps) => {
|
||||
// const handleChange = ({ type }: Event, newValue: number | number[]) => {
|
||||
@ -39,6 +46,7 @@ export const CustomSlider = ({
|
||||
onChange={onChange}
|
||||
valueLabelDisplay="on"
|
||||
onChangeCommitted={onChangeCommitted}
|
||||
valueLabelFormat={valueLabelFormat}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
data-cy="slider"
|
||||
sx={{
|
||||
@ -73,7 +81,7 @@ export const CustomSlider = ({
|
||||
"& .MuiSlider-track": {
|
||||
height: "12px",
|
||||
},
|
||||
...sx
|
||||
...sx,
|
||||
}}
|
||||
/>
|
||||
);
|
71
lib/ui_kit/CustomTextField.tsx
Normal file
71
lib/ui_kit/CustomTextField.tsx
Normal file
@ -0,0 +1,71 @@
|
||||
import { FormControl, TextField as MuiTextField, SxProps, Theme, useTheme } from "@mui/material";
|
||||
|
||||
import type { InputProps, TextFieldProps } from "@mui/material";
|
||||
import type { ChangeEvent, FC, FocusEvent, KeyboardEvent } from "react";
|
||||
|
||||
|
||||
const TextField = MuiTextField as unknown as FC<TextFieldProps>; // temporary fix ts(2590)
|
||||
|
||||
interface CustomTextFieldProps {
|
||||
placeholder: string;
|
||||
value?: string;
|
||||
error?: string;
|
||||
onChange?: (event: ChangeEvent<HTMLInputElement>) => void;
|
||||
onKeyDown?: (event: KeyboardEvent<HTMLInputElement>) => void;
|
||||
onBlur?: (event: FocusEvent<HTMLInputElement>) => void;
|
||||
text?: string;
|
||||
sx?: SxProps<Theme>;
|
||||
InputProps?: Partial<InputProps>;
|
||||
}
|
||||
|
||||
export default function CustomTextField({
|
||||
placeholder,
|
||||
value,
|
||||
text,
|
||||
sx,
|
||||
error,
|
||||
onChange,
|
||||
onKeyDown,
|
||||
onBlur,
|
||||
InputProps,
|
||||
}: CustomTextFieldProps) {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<FormControl fullWidth variant="standard" sx={{ p: 0 }}>
|
||||
<TextField
|
||||
defaultValue={text}
|
||||
fullWidth
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
error={!!error}
|
||||
label={error}
|
||||
onChange={onChange}
|
||||
onKeyDown={onKeyDown}
|
||||
onBlur={onBlur}
|
||||
sx={{
|
||||
"& .MuiInputBase-root": {
|
||||
backgroundColor: theme.palette.background.default,
|
||||
height: "48px",
|
||||
borderRadius: "10px",
|
||||
},
|
||||
"& .MuiInputLabel-root": {
|
||||
fontSize: "13.5px",
|
||||
marginTop: "3px",
|
||||
},
|
||||
...sx,
|
||||
}}
|
||||
InputProps={InputProps}
|
||||
inputProps={{
|
||||
sx: {
|
||||
borderRadius: "10px",
|
||||
fontSize: "18px",
|
||||
lineHeight: "21px",
|
||||
py: 0,
|
||||
},
|
||||
}}
|
||||
data-cy="textfield"
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}
|
70
lib/ui_kit/LabeledDatePicker.tsx
Normal file
70
lib/ui_kit/LabeledDatePicker.tsx
Normal file
@ -0,0 +1,70 @@
|
||||
import CalendarIcon from "@icons/CalendarIcon";
|
||||
import { Box, SxProps, Theme, Typography, useTheme } from "@mui/material";
|
||||
import { DatePicker } from "@mui/x-date-pickers";
|
||||
import moment from "moment";
|
||||
import { useRootContainerSize } from "../contexts/RootContainerWidthContext";
|
||||
|
||||
interface Props {
|
||||
label?: string;
|
||||
sx?: SxProps<Theme>;
|
||||
value?: moment.Moment;
|
||||
onChange?: (value: string | null) => void;
|
||||
}
|
||||
|
||||
export default function LabeledDatePicker({ label, value = moment(), onChange, sx }: Props) {
|
||||
const theme = useTheme();
|
||||
const upLg = useRootContainerSize() > theme.breakpoints.values.md;
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
...sx,
|
||||
}}
|
||||
>
|
||||
{label && (
|
||||
<Typography
|
||||
sx={{
|
||||
fontWeight: 500,
|
||||
fontSize: "16px",
|
||||
lineHeight: "20px",
|
||||
color: "#4D4D4D",
|
||||
mb: "10px",
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Typography>
|
||||
)}
|
||||
<DatePicker
|
||||
//@ts-ignore
|
||||
value={value._d}
|
||||
onChange={onChange}
|
||||
slots={{
|
||||
openPickerIcon: () => <CalendarIcon />,
|
||||
}}
|
||||
slotProps={{
|
||||
openPickerButton: {
|
||||
sx: {
|
||||
p: 0,
|
||||
},
|
||||
"data-cy": "open-datepicker",
|
||||
},
|
||||
}}
|
||||
sx={{
|
||||
"& .MuiInputBase-root": {
|
||||
backgroundColor: "#F2F3F7",
|
||||
borderRadius: "10px",
|
||||
pr: "22px",
|
||||
"& input": {
|
||||
py: "11px",
|
||||
pl: upLg ? "20px" : "13px",
|
||||
lineHeight: "19px",
|
||||
},
|
||||
"& fieldset": {
|
||||
borderColor: "#9A9AAF",
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
17
lib/ui_kit/LoadingSkeleton.tsx
Normal file
17
lib/ui_kit/LoadingSkeleton.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import { Skeleton } from "@mui/material";
|
||||
|
||||
|
||||
export default function LoadingSkeleton() {
|
||||
|
||||
return (
|
||||
<Skeleton
|
||||
component="div"
|
||||
variant="rectangular"
|
||||
sx={{
|
||||
bgcolor: "grey",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
43
lib/utils/handleComponentError.ts
Normal file
43
lib/utils/handleComponentError.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { ErrorInfo } from "react";
|
||||
|
||||
|
||||
interface ComponentError {
|
||||
timestamp: number;
|
||||
message: string;
|
||||
callStack: string | undefined;
|
||||
componentStack: string | null | undefined;
|
||||
}
|
||||
|
||||
export function handleComponentError(error: Error, info: ErrorInfo) {
|
||||
const componentError: ComponentError = {
|
||||
timestamp: Math.floor(Date.now() / 1000),
|
||||
message: error.message,
|
||||
callStack: error.stack,
|
||||
componentStack: info.componentStack,
|
||||
};
|
||||
|
||||
queueErrorRequest(componentError);
|
||||
}
|
||||
|
||||
let errorsQueue: ComponentError[] = [];
|
||||
let timeoutId: ReturnType<typeof setTimeout>;
|
||||
|
||||
function queueErrorRequest(error: ComponentError) {
|
||||
errorsQueue.push(error);
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = setTimeout(() => {
|
||||
sendErrorsToServer();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
async function sendErrorsToServer() {
|
||||
// makeRequest({
|
||||
// url: "",
|
||||
// method: "POST",
|
||||
// body: errorsQueue,
|
||||
// useToken: true,
|
||||
// });
|
||||
console.log(`Fake-sending ${errorsQueue.length} errors to server`, errorsQueue);
|
||||
errorsQueue = [];
|
||||
}
|
205
lib/utils/hooks/useQuestionFlowControl.ts
Normal file
205
lib/utils/hooks/useQuestionFlowControl.ts
Normal file
@ -0,0 +1,205 @@
|
||||
import { AnyTypedQuizQuestion } from "@model/questionTypes/shared";
|
||||
import { setCurrentQuizStep, useQuizViewStore } from "@stores/quizView";
|
||||
import { useCallback, useDebugValue, useMemo, useState } from "react";
|
||||
import { isResultQuestionEmpty } from "../../components/ViewPublicationPage/tools/checkEmptyData";
|
||||
import moment from "moment";
|
||||
import { useQuizData } from "@contexts/QuizDataContext";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
|
||||
|
||||
export function useQuestionFlowControl() {
|
||||
const { settings, questions } = useQuizData();
|
||||
const [currentQuestion, setCurrentQuestion] = useState<AnyTypedQuizQuestion>(getFirstQuestion);
|
||||
const { answers, pointsSum } = useQuizViewStore(state => state);
|
||||
|
||||
|
||||
const linearQuestionIndex = questions.every(({ content }) => content.rule.parentId !== "root") // null when branching enabled
|
||||
? questions.indexOf(currentQuestion)
|
||||
: null;
|
||||
|
||||
function getFirstQuestion() {
|
||||
if (questions.length === 0) throw new Error("No questions found");
|
||||
|
||||
if (settings.cfg.haveRoot) {
|
||||
const nextQuestion = questions.find(
|
||||
question => question.id === settings.cfg.haveRoot || question.content.id === settings.cfg.haveRoot
|
||||
);
|
||||
if (!nextQuestion) throw new Error("Root question not found");
|
||||
|
||||
return nextQuestion;
|
||||
}
|
||||
|
||||
return questions[0];
|
||||
}
|
||||
|
||||
|
||||
const nextQuestionIdPointsLogic = () => {
|
||||
return questions.find(question =>
|
||||
question.type === "result" && question.content.rule.parentId === "line"
|
||||
);
|
||||
};
|
||||
|
||||
const nextQuestionIdMainLogic = () => {
|
||||
const questionAnswer = answers.find(({ questionId }) => questionId === currentQuestion.id);
|
||||
|
||||
//Если ответ существует и не является объектом момента
|
||||
if (questionAnswer && !moment.isMoment(questionAnswer.answer)) {
|
||||
//Ответы должны храниться в массиве
|
||||
const userAnswers = Array.isArray(questionAnswer.answer) ? questionAnswer.answer : [questionAnswer.answer];
|
||||
|
||||
//Сравниваем список условий ветвления и выбираем подходящее
|
||||
for (const branchingRule of currentQuestion.content.rule.main) {
|
||||
if (userAnswers.some(answer => branchingRule.rules[0].answers.includes(answer))) {
|
||||
return branchingRule.next;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!currentQuestion.required) {//вопрос не обязателен и не нашли совпадений между ответами и условиями ветвления
|
||||
const defaultNextQuestionId = currentQuestion.content.rule.default;
|
||||
if (defaultNextQuestionId.length > 1 && defaultNextQuestionId !== " ") return defaultNextQuestionId;
|
||||
//Вопросы типа страница, ползунок, своё поле для ввода и дата не могут иметь больше 1 ребёнка. Пользователь не может настроить там дефолт
|
||||
//Кинуть на ребёнка надо даже если там нет дефолта
|
||||
if (
|
||||
["date", "page", "text", "number"].includes(currentQuestion.type)
|
||||
&& currentQuestion.content.rule.children.length === 1
|
||||
) return currentQuestion.content.rule.children[0];
|
||||
}
|
||||
|
||||
//ничё не нашли, ищем резулт
|
||||
return questions.find(q => {
|
||||
return q.type === "result" && q.content.rule.parentId === currentQuestion.content.id;
|
||||
})?.id;
|
||||
};
|
||||
const calculateNextQuestionId = useMemo(() => {
|
||||
if (Boolean(settings.cfg.score)) {
|
||||
return nextQuestionIdPointsLogic();
|
||||
}
|
||||
return nextQuestionIdMainLogic();
|
||||
}, [answers, currentQuestion, questions]);
|
||||
|
||||
const prevQuestion = linearQuestionIndex !== null
|
||||
? questions[linearQuestionIndex - 1]
|
||||
: questions.find(q =>
|
||||
q.id === currentQuestion.content.rule.parentId
|
||||
|| q.content.id === currentQuestion.content.rule.parentId
|
||||
);
|
||||
|
||||
const findResultPointsLogic = () => {
|
||||
const results = questions
|
||||
.filter(e => e.type === "result" && e.content.rule.minScore !== undefined && e.content.rule.minScore <= pointsSum);
|
||||
const numbers = results.map(e => e.type === "result" && e.content.rule.minScore !== undefined ? e.content.rule.minScore : 0);
|
||||
const indexOfNext = Math.max(...numbers);
|
||||
console.log(results);
|
||||
console.log(numbers);
|
||||
console.log(indexOfNext);
|
||||
return results[numbers.indexOf(indexOfNext)];
|
||||
};
|
||||
|
||||
const getNextQuestion = () => {
|
||||
let next;
|
||||
console.log(11111111111);
|
||||
//Искать можно двумя логиками. Основной и балловой
|
||||
if (Boolean(settings.cfg.score)) {
|
||||
//Балловая
|
||||
console.log(222222222);
|
||||
|
||||
|
||||
//Ищем линейно
|
||||
if (linearQuestionIndex !== null) {
|
||||
console.log(33333333333);
|
||||
|
||||
|
||||
next = questions[linearQuestionIndex + 1];
|
||||
console.log(4444444);
|
||||
console.log("перед ифом", next);
|
||||
if (next?.type === "result" || next == undefined) next = findResultPointsLogic();
|
||||
console.log(5555555555);
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
} else {
|
||||
console.log(6666666);
|
||||
//Основная
|
||||
if (linearQuestionIndex !== null) {
|
||||
console.log(777777777);
|
||||
//Ищем линейно
|
||||
next = questions[linearQuestionIndex + 1] ?? questions.find(question =>
|
||||
question.type === "result" && question.content.rule.parentId === "line"
|
||||
);
|
||||
} else {
|
||||
console.log(88888888888888);
|
||||
//Ищем ветвлением
|
||||
next = questions.find(q => q.id === calculateNextQuestionId || q.content.id === calculateNextQuestionId);
|
||||
}
|
||||
}
|
||||
console.log("next", next);
|
||||
if (!next && currentQuestion.type !== "result") throw new Error("Не найден следующий вопрос");
|
||||
return next;
|
||||
};
|
||||
const nextQuestion = getNextQuestion();
|
||||
const showResult = useCallback(() => {
|
||||
if (nextQuestion?.type !== "result") throw new Error("Current question is not result");
|
||||
|
||||
setCurrentQuestion(nextQuestion);
|
||||
if (
|
||||
settings.cfg.resultInfo.showResultForm === "after"
|
||||
|| isResultQuestionEmpty(nextQuestion)
|
||||
) setCurrentQuizStep("contactform");
|
||||
}, [nextQuestion, settings.cfg.resultInfo.showResultForm]);
|
||||
|
||||
const showResultAfterContactForm = useCallback(() => {
|
||||
if (currentQuestion.type !== "result") throw new Error("Current question is not result");
|
||||
if (isResultQuestionEmpty(currentQuestion)) {
|
||||
enqueueSnackbar("Данные отправлены");
|
||||
return;
|
||||
}
|
||||
|
||||
setCurrentQuizStep("question");
|
||||
}, [currentQuestion]);
|
||||
|
||||
const moveToPrevQuestion = useCallback(() => {
|
||||
if (!prevQuestion) throw new Error("Previous question not found");
|
||||
|
||||
setCurrentQuestion(prevQuestion);
|
||||
}, [prevQuestion]);
|
||||
|
||||
const moveToNextQuestion = useCallback(() => {
|
||||
if (!nextQuestion) throw new Error("Next question not found");
|
||||
|
||||
if (nextQuestion.type === "result") return showResult();
|
||||
|
||||
setCurrentQuestion(nextQuestion);
|
||||
}, [nextQuestion, showResult]);
|
||||
|
||||
const isPreviousButtonEnabled = Boolean(prevQuestion);
|
||||
|
||||
const isNextButtonEnabled = useMemo(() => {
|
||||
const hasAnswer = answers.some(({ questionId }) => questionId === currentQuestion.id);
|
||||
|
||||
if ("required" in currentQuestion.content && currentQuestion.content.required) {
|
||||
return hasAnswer;
|
||||
}
|
||||
|
||||
return Boolean(nextQuestion);
|
||||
}, [answers, currentQuestion.content, currentQuestion.id, nextQuestion]);
|
||||
|
||||
useDebugValue({
|
||||
linearQuestionIndex,
|
||||
currentQuestion: currentQuestion,
|
||||
prevQuestion: prevQuestion,
|
||||
nextQuestion: nextQuestion,
|
||||
});
|
||||
|
||||
return {
|
||||
currentQuestion,
|
||||
currentQuestionStepNumber: linearQuestionIndex === null ? null : linearQuestionIndex + 1,
|
||||
isNextButtonEnabled,
|
||||
isPreviousButtonEnabled,
|
||||
moveToPrevQuestion,
|
||||
moveToNextQuestion,
|
||||
showResultAfterContactForm,
|
||||
};
|
||||
}
|
45
src/utils/themes/Publication/themePublication.ts → lib/utils/themes/Publication/themePublication.ts
45
src/utils/themes/Publication/themePublication.ts → lib/utils/themes/Publication/themePublication.ts
@ -1,7 +1,6 @@
|
||||
import { createTheme } from "@mui/material";
|
||||
import { QuizTheme } from "@model/settingsData";
|
||||
import { Theme, createTheme } from "@mui/material";
|
||||
import themePublic from "./genericPublication";
|
||||
import theme from "../generic";
|
||||
|
||||
|
||||
|
||||
const StandardTheme = createTheme({
|
||||
@ -224,30 +223,16 @@ const BlueDarkTheme = createTheme({
|
||||
}
|
||||
})
|
||||
|
||||
export const modes = {
|
||||
StandardTheme: true,
|
||||
StandardDarkTheme: false,
|
||||
PinkTheme: true,
|
||||
PinkDarkTheme: false,
|
||||
BlackWhiteTheme: true,
|
||||
OliveTheme: true,
|
||||
YellowTheme: true,
|
||||
GoldDarkTheme: false,
|
||||
PurpleTheme: true,
|
||||
BlueTheme: true,
|
||||
BlueDarkTheme: false
|
||||
}
|
||||
|
||||
export const themesPublication = {
|
||||
StandardTheme,
|
||||
StandardDarkTheme,
|
||||
PinkTheme,
|
||||
PinkDarkTheme,
|
||||
BlackWhiteTheme,
|
||||
OliveTheme,
|
||||
YellowTheme,
|
||||
GoldDarkTheme,
|
||||
PurpleTheme,
|
||||
BlueTheme,
|
||||
BlueDarkTheme,
|
||||
}
|
||||
export const quizThemes: Record<QuizTheme, { theme: Theme; isLight: boolean; }> = {
|
||||
StandardTheme: { theme: StandardTheme, isLight: true },
|
||||
StandardDarkTheme: { theme: StandardDarkTheme, isLight: false },
|
||||
PinkTheme: { theme: PinkTheme, isLight: true },
|
||||
PinkDarkTheme: { theme: PinkDarkTheme, isLight: false },
|
||||
BlackWhiteTheme: { theme: BlackWhiteTheme, isLight: true },
|
||||
OliveTheme: { theme: OliveTheme, isLight: true },
|
||||
YellowTheme: { theme: YellowTheme, isLight: true },
|
||||
GoldDarkTheme: { theme: GoldDarkTheme, isLight: false },
|
||||
PurpleTheme: { theme: PurpleTheme, isLight: true },
|
||||
BlueTheme: { theme: BlueTheme, isLight: true },
|
||||
BlueDarkTheme: { theme: BlueDarkTheme, isLight: false },
|
||||
};
|
0
src/utils/themes/dark.ts → lib/utils/themes/dark.ts
Executable file → Normal file
0
src/utils/themes/dark.ts → lib/utils/themes/dark.ts
Executable file → Normal file
3
src/utils/themes/generic.ts → lib/utils/themes/generic.ts
Executable file → Normal file
3
src/utils/themes/generic.ts → lib/utils/themes/generic.ts
Executable file → Normal file
@ -89,6 +89,7 @@ const theme = createTheme({
|
||||
fontWeight: 500,
|
||||
},
|
||||
fontFamily: [
|
||||
"Twemoji Country Flags",
|
||||
"Rubik",
|
||||
"-apple-system",
|
||||
"BlinkMacSystemFont",
|
||||
@ -134,4 +135,4 @@ theme.typography.infographic = {
|
||||
}
|
||||
};
|
||||
|
||||
export default theme;
|
||||
export default theme;
|
0
src/utils/themes/light.ts → lib/utils/themes/light.ts
Executable file → Normal file
0
src/utils/themes/light.ts → lib/utils/themes/light.ts
Executable file → Normal file
83
package.json
83
package.json
@ -1,24 +1,75 @@
|
||||
{
|
||||
"name": "squzanswerer",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"name": "@frontend/squzanswerer",
|
||||
"version": "1.0.5",
|
||||
"type": "module",
|
||||
"main": "./dist-package/index.js",
|
||||
"module": "./dist-package/index.js",
|
||||
"types": "./dist-package/index.d.ts",
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"dist-package"
|
||||
],
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist-package/index.js"
|
||||
}
|
||||
},
|
||||
"publishConfig": {
|
||||
"registry": "https://penahub.gitlab.yandexcloud.net/api/v4/projects/43/packages/npm/"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"build:widget": "tsc && vite build --config vite.config.widget.ts",
|
||||
"build:package": "tsc && vite build --config vite.config.package.ts",
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview",
|
||||
"cypress:open": "cypress open"
|
||||
"cypress:open": "cypress open",
|
||||
"prepublishOnly": "npm run build:package"
|
||||
},
|
||||
"dependencies": {
|
||||
"devDependencies": {
|
||||
"@emoji-mart/data": "^1.1.2",
|
||||
"@emoji-mart/react": "^1.1.1",
|
||||
"@emotion/react": "^11.10.5",
|
||||
"@emotion/styled": "^11.10.5",
|
||||
"@frontend/kitui": "^1.0.54",
|
||||
"@mui/icons-material": "^5.10.14",
|
||||
"@mui/material": "^5.10.14",
|
||||
"@mui/x-date-pickers": "^6.16.1",
|
||||
"@types/node": "^16.7.13",
|
||||
"@types/react": "^18.2.43",
|
||||
"@types/react-dom": "^18.2.17",
|
||||
"@typescript-eslint/eslint-plugin": "^6.14.0",
|
||||
"@typescript-eslint/parser": "^6.14.0",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"axios": "^1.5.1",
|
||||
"cypress": "^13.6.1",
|
||||
"emoji-mart": "^5.5.2",
|
||||
"eslint": "^8.55.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.5",
|
||||
"immer": "^10.0.3",
|
||||
"moment": "^2.30.1",
|
||||
"nanoid": "^5.0.3",
|
||||
"notistack": "^3.0.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-error-boundary": "^4.0.12",
|
||||
"react-router-dom": "^6.21.3",
|
||||
"swr": "^2.2.4",
|
||||
"typescript": "^5.2.2",
|
||||
"use-debounce": "^9.0.4",
|
||||
"vite": "^5.0.8",
|
||||
"vite-plugin-dts": "^3.7.2",
|
||||
"zustand": "^4.3.8"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emoji-mart/data": "^1.1.2",
|
||||
"@emoji-mart/react": "^1.1.1",
|
||||
"@emotion/react": "^11.10.5",
|
||||
"@emotion/styled": "^11.10.5",
|
||||
"@mui/icons-material": "^5.10.14",
|
||||
"@mui/material": "^5.10.14",
|
||||
"@mui/x-date-pickers": "^6.16.1",
|
||||
"axios": "^1.5.1",
|
||||
"emoji-mart": "^5.5.2",
|
||||
"immer": "^10.0.3",
|
||||
@ -27,25 +78,13 @@
|
||||
"notistack": "^3.0.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.6.2",
|
||||
"react-error-boundary": "^4.0.12",
|
||||
"react-router-dom": "^6.21.3",
|
||||
"swr": "^2.2.4",
|
||||
"typescript": "^5.2.2",
|
||||
"use-debounce": "^9.0.4",
|
||||
"zustand": "^4.3.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@emoji-mart/data": "^1.1.2",
|
||||
"@emoji-mart/react": "^1.1.1",
|
||||
"@types/react": "^18.2.43",
|
||||
"@types/react-dom": "^18.2.17",
|
||||
"@typescript-eslint/eslint-plugin": "^6.14.0",
|
||||
"@typescript-eslint/parser": "^6.14.0",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"cypress": "^13.6.1",
|
||||
"eslint": "^8.55.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.5",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^5.0.8"
|
||||
"dependencies": {
|
||||
"country-flag-emoji-polyfill": "^0.1.8"
|
||||
}
|
||||
}
|
||||
|
223
pub.js
223
pub.js
File diff suppressed because one or more lines are too long
58
src/App.tsx
58
src/App.tsx
@ -1,38 +1,36 @@
|
||||
import { CssBaseline, ThemeProvider } from "@mui/material";
|
||||
import { LocalizationProvider } from "@mui/x-date-pickers";
|
||||
import { AdapterMoment } from "@mui/x-date-pickers/AdapterMoment";
|
||||
import { ruRU } from '@mui/x-date-pickers/locales';
|
||||
import moment from "moment";
|
||||
import { SnackbarProvider } from 'notistack';
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
import { SWRConfig } from "swr";
|
||||
import { ViewPage } from "./pages/ViewPublicationPage";
|
||||
import lightTheme from "./utils/themes/light";
|
||||
import { getQuizData } from "@api/quizRelase";
|
||||
import { Box } from "@mui/material";
|
||||
import LoadingSkeleton from "@ui_kit/LoadingSkeleton";
|
||||
import { useParams } from "react-router-dom";
|
||||
import useSWR from "swr";
|
||||
import QuizAnswerer from "../lib/components/QuizAnswerer";
|
||||
import { ApologyPage } from "../lib/components/ViewPublicationPage/ApologyPage";
|
||||
|
||||
|
||||
moment.locale("ru");
|
||||
const localeText = ruRU.components.MuiLocalizationProvider.defaultProps.localeText;
|
||||
// const defaultQuizId = "45ef7f9c-784d-4e58-badb-f6b337f08ba0"; // branching
|
||||
const defaultQuizId = "cde381db-8ccb-402c-b55f-2c814be9bf25"; //looooong header
|
||||
// const defaultQuizId = "ad7f5a87-b833-4f5b-854e-453706ed655c"; // linear
|
||||
|
||||
export default function App() {
|
||||
const quizId = useParams().quizId ?? defaultQuizId;
|
||||
const { data, error, isLoading } = useSWR(["quizData", quizId], params => getQuizData(params[1]), {
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnReconnect: false,
|
||||
shouldRetryOnError: false,
|
||||
refreshInterval: 0,
|
||||
});
|
||||
|
||||
if (isLoading) return <LoadingSkeleton />;
|
||||
if (error) return <ApologyPage error={error} />;
|
||||
if (!data) throw new Error("Quiz data is null");
|
||||
|
||||
return (
|
||||
<SWRConfig value={{
|
||||
revalidateOnFocus: false,
|
||||
shouldRetryOnError: false,
|
||||
<Box sx={{
|
||||
height: "100dvh",
|
||||
}}>
|
||||
<LocalizationProvider dateAdapter={AdapterMoment} adapterLocale="ru" localeText={localeText}>
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<BrowserRouter>
|
||||
<SnackbarProvider
|
||||
preventDuplicate={true}
|
||||
style={{ backgroundColor: lightTheme.palette.brightPurple.main }}
|
||||
>
|
||||
<CssBaseline />
|
||||
<ViewPage />
|
||||
</SnackbarProvider>
|
||||
</BrowserRouter>
|
||||
</ThemeProvider>
|
||||
</LocalizationProvider>
|
||||
</SWRConfig>
|
||||
<QuizAnswerer
|
||||
quizSettings={data}
|
||||
quizId={quizId}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
30
src/WidgetApp.tsx
Normal file
30
src/WidgetApp.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import { getQuizData } from "@api/quizRelase";
|
||||
import LoadingSkeleton from "@ui_kit/LoadingSkeleton";
|
||||
import useSWR from "swr";
|
||||
import QuizAnswerer from "../lib/components/QuizAnswerer";
|
||||
import { ApologyPage } from "../lib/components/ViewPublicationPage/ApologyPage";
|
||||
|
||||
|
||||
interface Props {
|
||||
quizId: string;
|
||||
}
|
||||
|
||||
export default function WidgetApp({ quizId }: Props) {
|
||||
const { data, error, isLoading } = useSWR(["quizData", quizId], params => getQuizData(params[1]), {
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnReconnect: false,
|
||||
shouldRetryOnError: false,
|
||||
refreshInterval: 0,
|
||||
});
|
||||
|
||||
if (isLoading) return <LoadingSkeleton />;
|
||||
if (error) return <ApologyPage error={error} />;
|
||||
if (!data) throw new Error("Quiz data is null");
|
||||
|
||||
return (
|
||||
<QuizAnswerer
|
||||
quizSettings={data}
|
||||
quizId={quizId}
|
||||
/>
|
||||
);
|
||||
}
|
@ -1,148 +0,0 @@
|
||||
import axios from "axios";
|
||||
|
||||
import type { AxiosError } from "axios";
|
||||
import type { GetDataResponse } from "../model/settingsData";
|
||||
|
||||
let SESSIONS = "";
|
||||
|
||||
export const publicationMakeRequest = ({ url, body }: any) => {
|
||||
console.log(body);
|
||||
return axios(url, {
|
||||
//@ts-ignore
|
||||
data: body,
|
||||
headers: {
|
||||
"X-Sessionkey": SESSIONS,
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
method: "POST",
|
||||
});
|
||||
};
|
||||
|
||||
export async function getData(quizId: string): Promise<{
|
||||
data: GetDataResponse | null;
|
||||
isRecentlyCompleted: boolean;
|
||||
error?: string;
|
||||
}> {
|
||||
const QID =
|
||||
process.env.NODE_ENV === "production"
|
||||
? window.location.pathname.replace(/\//g, "")
|
||||
: "ef836ff8-35b1-4031-9acf-af5766bac2b2";
|
||||
|
||||
try {
|
||||
const { data, headers } = await axios<GetDataResponse>(
|
||||
`/answer/settings`,
|
||||
{
|
||||
method: "POST",
|
||||
// headers,
|
||||
data: JSON.stringify({
|
||||
quiz_id: quizId,
|
||||
limit: 100,
|
||||
page: 0,
|
||||
need_config: true,
|
||||
}),
|
||||
}
|
||||
);
|
||||
const sessions = JSON.parse(localStorage.getItem("sessions") || "{}");
|
||||
|
||||
if (typeof sessions[QID] === "number") {
|
||||
// unix время. Если меньше суток прошло - выводить ошибку, иначе пустить дальше
|
||||
if (Date.now() - sessions[QID] < 86400000) {
|
||||
return { data, isRecentlyCompleted: true };
|
||||
}
|
||||
}
|
||||
|
||||
SESSIONS = headers["x-sessionkey"];
|
||||
|
||||
return { data, isRecentlyCompleted: false };
|
||||
} catch (nativeError) {
|
||||
const error = nativeError as AxiosError;
|
||||
|
||||
return { data: null, isRecentlyCompleted: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
export function sendAnswer({ questionId, body, qid }: any) {
|
||||
const formData = new FormData();
|
||||
console.log(qid);
|
||||
const answers = [
|
||||
{
|
||||
question_id: questionId,
|
||||
content: body, //тут массив с ответом
|
||||
},
|
||||
];
|
||||
formData.append("answers", JSON.stringify(answers));
|
||||
formData.append("qid", qid);
|
||||
|
||||
return publicationMakeRequest({
|
||||
url: `/answer/answer`,
|
||||
body: formData,
|
||||
method: "POST",
|
||||
});
|
||||
}
|
||||
|
||||
//body ={file, filename}
|
||||
export function sendFile({ questionId, body, qid }: any) {
|
||||
console.log(body);
|
||||
const formData = new FormData();
|
||||
|
||||
const answers: any = [
|
||||
{
|
||||
question_id: questionId,
|
||||
content: "file:" + body.name,
|
||||
},
|
||||
];
|
||||
|
||||
formData.append("answers", JSON.stringify(answers));
|
||||
formData.append(body.name, body.file);
|
||||
formData.append("qid", qid);
|
||||
|
||||
return publicationMakeRequest({
|
||||
url: `/answer/answer`,
|
||||
body: formData,
|
||||
method: "POST",
|
||||
});
|
||||
}
|
||||
|
||||
const fields = [
|
||||
"name",
|
||||
"email",
|
||||
"phone",
|
||||
"adress",
|
||||
"telegram",
|
||||
"wechat",
|
||||
"viber",
|
||||
"vk",
|
||||
"skype",
|
||||
"whatsup",
|
||||
"messenger",
|
||||
"text",
|
||||
];
|
||||
|
||||
//форма контактов
|
||||
export function sendFC({ questionId, body, qid }: any) {
|
||||
const formData = new FormData();
|
||||
|
||||
// const keysBody = Object.keys(body)
|
||||
// const content:any = {}
|
||||
// fields.forEach((key) => {
|
||||
// if (keysBody.includes(key)) content[key] = body.key
|
||||
// })
|
||||
|
||||
const answers = [
|
||||
{
|
||||
question_id: questionId,
|
||||
content: JSON.stringify(body),
|
||||
result: true,
|
||||
qid,
|
||||
},
|
||||
];
|
||||
|
||||
formData.append("answers", JSON.stringify(answers));
|
||||
formData.append("qid", qid);
|
||||
|
||||
return publicationMakeRequest({
|
||||
url: `/answer/answer`,
|
||||
body: formData,
|
||||
method: "POST",
|
||||
});
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 1.3 KiB |
Binary file not shown.
Before Width: | Height: | Size: 47 KiB |
Binary file not shown.
Before Width: | Height: | Size: 1.1 KiB |
Binary file not shown.
Before Width: | Height: | Size: 1.2 KiB |
@ -1,8 +0,0 @@
|
||||
<svg width="204" height="134" viewBox="0 0 204 134" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="1.25" y="1.25" width="201.5" height="131.5" rx="6.75" stroke="#7E2AEA" strokeWidth="1.5"/>
|
||||
<rect x="7.5" y="62.5" width="49" height="64" rx="3.5" fill="#7E2AEA" stroke="#7E2AEA"/>
|
||||
<path d="M202 14.75H202.75V14V8C202.75 4.27208 199.728 1.25 196 1.25H8C4.27208 1.25 1.25 4.27208 1.25 8V14V14.75H2H202Z" fill="#F2F3F7" stroke="#7E2AEA" strokeWidth="1.5"/>
|
||||
<circle cx="169.5" cy="8" r="2.5" fill="#7E2AEA"/>
|
||||
<circle cx="177.5" cy="8" r="2.5" fill="#7E2AEA"/>
|
||||
<circle cx="185.5" cy="8" r="2.5" fill="#7E2AEA"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 622 B |
Binary file not shown.
Before Width: | Height: | Size: 24 KiB |
@ -1,81 +0,0 @@
|
||||
import { Box } from "@mui/material";
|
||||
|
||||
interface Props {
|
||||
color?: string;
|
||||
}
|
||||
|
||||
export default function CalendarIcon({ color }: Props) {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
height: "36px",
|
||||
width: "36px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width="36"
|
||||
height="36"
|
||||
viewBox="0 0 36 36"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g id="Group 22">
|
||||
<rect id="Border" width="36" height="36" rx="6" fill="#7E2AEA" />
|
||||
<g id="Group 21">
|
||||
<path
|
||||
id="Vector"
|
||||
d="M25.5 9.75H10.5C10.0858 9.75 9.75 10.0858 9.75 10.5V25.5C9.75 25.9142 10.0858 26.25 10.5 26.25H25.5C25.9142 26.25 26.25 25.9142 26.25 25.5V10.5C26.25 10.0858 25.9142 9.75 25.5 9.75Z"
|
||||
stroke="white"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
id="Vector_2"
|
||||
d="M22.5 8.25V11.25"
|
||||
stroke="white"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
id="Vector_3"
|
||||
d="M13.5 8.25V11.25"
|
||||
stroke="white"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
id="Vector_4"
|
||||
d="M9.75 14.25H26.25"
|
||||
stroke="white"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
id="Vector_5"
|
||||
d="M14.625 18H17.25L15.75 19.875C15.9969 19.8746 16.24 19.9351 16.4579 20.0512C16.6757 20.1672 16.8616 20.3353 16.999 20.5404C17.1363 20.7455 17.2209 20.9814 17.2453 21.2271C17.2696 21.4727 17.2329 21.7206 17.1385 21.9487C17.0441 22.1768 16.8949 22.378 16.704 22.5346C16.5132 22.6912 16.2866 22.7983 16.0445 22.8463C15.8024 22.8944 15.5521 22.8819 15.3159 22.81C15.0798 22.7382 14.865 22.6091 14.6906 22.4344"
|
||||
stroke="white"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
id="Vector_6"
|
||||
d="M19.5 19.125L21 18V22.875"
|
||||
stroke="white"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</Box>
|
||||
);
|
||||
}
|
@ -1,535 +0,0 @@
|
||||
import { Box } from "@mui/material";
|
||||
|
||||
interface Props {
|
||||
color?: string;
|
||||
}
|
||||
|
||||
export default function Notebook({ color }: Props) {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
height: "171px",
|
||||
width: "279px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width="279"
|
||||
height="171"
|
||||
viewBox="0 0 279 171"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M123.717 1.39258V3.94582L126.959 3.94582C127.348 3.94582 127.663 4.26075 127.663 4.64924V6.96329C127.663 7.60426 128.182 8.12386 128.823 8.12386L150.642 8.12386C151.283 8.12386 151.802 7.60426 151.802 6.96329V4.64924C151.802 4.26075 152.117 3.94582 152.506 3.94582L155.284 3.94582V1.39258L123.717 1.39258Z"
|
||||
fill="black"
|
||||
/>
|
||||
<circle
|
||||
cx="139.409"
|
||||
cy="4.55077"
|
||||
r="1.06935"
|
||||
transform="rotate(-180 139.409 4.55077)"
|
||||
fill="url(#paint0_linear_3_590)"
|
||||
/>
|
||||
<circle
|
||||
cx="139.385"
|
||||
cy="4.52608"
|
||||
r="0.580283"
|
||||
fill="url(#paint1_radial_3_590)"
|
||||
/>
|
||||
<g filter="url(#filter0_f_3_590)">
|
||||
<path
|
||||
d="M139.383 4.17799C139.291 4.17799 139.202 4.2269 139.137 4.31396C139.072 4.40102 139.035 4.5191 139.035 4.64222C139.035 4.76534 139.072 4.88342 139.137 4.97048C139.202 5.05754 139.291 5.10645 139.383 5.10645L139.383 4.64222L139.383 4.17799Z"
|
||||
fill="url(#paint2_linear_3_590)"
|
||||
/>
|
||||
</g>
|
||||
<g filter="url(#filter1_f_3_590)">
|
||||
<circle cx="139.384" cy="4.29403" r="0.116057" fill="#50326D" />
|
||||
</g>
|
||||
<g filter="url(#filter2_f_3_590)">
|
||||
<path
|
||||
d="M139.501 5.10643C139.624 5.10643 139.742 5.05752 139.829 4.97046C139.916 4.8834 139.965 4.76533 139.965 4.6422C139.965 4.51908 139.916 4.40101 139.829 4.31395C139.742 4.22689 139.624 4.17798 139.501 4.17798L139.501 4.6422L139.501 5.10643Z"
|
||||
fill="url(#paint3_linear_3_590)"
|
||||
/>
|
||||
</g>
|
||||
<g filter="url(#filter3_f_3_590)">
|
||||
<ellipse
|
||||
cx="139.5"
|
||||
cy="4.75815"
|
||||
rx="0.232113"
|
||||
ry="0.116057"
|
||||
fill="url(#paint4_linear_3_590)"
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
d="M117.914 159.462H139.501V162.711H121.164C119.369 162.711 117.914 161.257 117.914 159.462Z"
|
||||
fill="url(#paint5_radial_3_590)"
|
||||
/>
|
||||
<path
|
||||
d="M117.914 159.462H139.501V162.711H121.164C119.369 162.711 117.914 161.257 117.914 159.462Z"
|
||||
fill="url(#paint6_linear_3_590)"
|
||||
/>
|
||||
<path
|
||||
d="M161.088 159.462H139.501V162.712H157.838C159.633 162.712 161.088 161.257 161.088 159.462Z"
|
||||
fill="url(#paint7_radial_3_590)"
|
||||
/>
|
||||
<path
|
||||
d="M161.088 159.462H139.501V162.712H157.838C159.633 162.712 161.088 161.257 161.088 159.462Z"
|
||||
fill="url(#paint8_linear_3_590)"
|
||||
/>
|
||||
<path
|
||||
d="M249.523 169.443L248.826 168.747H264.958L264.262 169.443H249.523Z"
|
||||
fill="url(#paint9_linear_3_590)"
|
||||
/>
|
||||
<rect
|
||||
x="249.521"
|
||||
y="169.443"
|
||||
width="14.8552"
|
||||
height="0.232113"
|
||||
rx="0.116057"
|
||||
fill="#2A2A2A"
|
||||
/>
|
||||
<path
|
||||
d="M249.754 169.675H264.145L263.813 170.165C263.725 170.294 263.58 170.371 263.424 170.371H250.474C250.319 170.371 250.173 170.294 250.086 170.165L249.754 169.675Z"
|
||||
fill="url(#paint10_linear_3_590)"
|
||||
/>
|
||||
<path
|
||||
d="M14.6241 169.443L13.9277 168.747H30.0596L29.3633 169.443H14.6241Z"
|
||||
fill="url(#paint11_linear_3_590)"
|
||||
/>
|
||||
<rect
|
||||
x="14.625"
|
||||
y="169.443"
|
||||
width="14.6231"
|
||||
height="0.232113"
|
||||
rx="0.116057"
|
||||
fill="#2A2A2A"
|
||||
/>
|
||||
<path
|
||||
d="M14.8555 169.675H29.0144L28.6894 170.162C28.6024 170.293 28.456 170.371 28.2992 170.371H15.5707C15.4139 170.371 15.2675 170.293 15.1805 170.162L14.8555 169.675Z"
|
||||
fill="url(#paint12_linear_3_590)"
|
||||
/>
|
||||
<path
|
||||
d="M21.9376 6.79972C21.9376 3.23858 24.8245 0.35171 28.3857 0.35171L250.613 0.35171C254.174 0.35171 257.061 3.23858 257.061 6.79973V159.11L21.9376 159.11L21.9376 6.79972Z"
|
||||
stroke="url(#paint13_linear_3_590)"
|
||||
strokeWidth="0.70342"
|
||||
/>
|
||||
<path
|
||||
d="M22.633 6.79261C22.633 3.61996 25.2049 1.048 28.3776 1.048L250.619 1.048C253.792 1.048 256.364 3.61994 256.364 6.7926V159.11L22.633 159.11L22.633 6.79261Z"
|
||||
fill="black"
|
||||
stroke="#2D2E31"
|
||||
strokeWidth="0.70342"
|
||||
/>
|
||||
<rect
|
||||
x="22.9805"
|
||||
y="153.891"
|
||||
width="233.042"
|
||||
height="5.57072"
|
||||
fill="url(#paint14_linear_3_590)"
|
||||
/>
|
||||
<g filter="url(#filter4_iii_3_590)">
|
||||
<path
|
||||
d="M0 159.462L279 159.462V164.057C279 166.647 276.9 168.746 274.311 168.746L4.68946 168.746C2.09954 168.746 0 166.647 0 164.057L0 159.462Z"
|
||||
fill="#D1D2D4"
|
||||
/>
|
||||
<path
|
||||
d="M0 159.462L279 159.462V164.057C279 166.647 276.9 168.746 274.311 168.746L4.68946 168.746C2.09954 168.746 0 166.647 0 164.057L0 159.462Z"
|
||||
fill="url(#paint15_linear_3_590)"
|
||||
/>
|
||||
</g>
|
||||
<mask
|
||||
id="mask0_3_590"
|
||||
// style="mask-type:alpha"
|
||||
maskUnits="userSpaceOnUse"
|
||||
x="26"
|
||||
y="10"
|
||||
width="227"
|
||||
height="144"
|
||||
>
|
||||
<rect
|
||||
x="26.0039"
|
||||
y="10.0527"
|
||||
width="226.184"
|
||||
height="143.839"
|
||||
fill="#D9D9D9"
|
||||
/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_3_590)">
|
||||
<rect
|
||||
x="26.0039"
|
||||
y="10.0525"
|
||||
width="226.184"
|
||||
height="143.839"
|
||||
fill="#F0F1F6"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter
|
||||
id="filter0_f_3_590"
|
||||
x="138.709"
|
||||
y="3.85232"
|
||||
width="0.998971"
|
||||
height="1.57978"
|
||||
filterUnits="userSpaceOnUse"
|
||||
colorInterpolationFilters="sRGB"
|
||||
>
|
||||
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="BackgroundImageFix"
|
||||
result="shape"
|
||||
/>
|
||||
<feGaussianBlur
|
||||
stdDeviation="0.162829"
|
||||
result="effect1_foregroundBlur_3_590"
|
||||
/>
|
||||
</filter>
|
||||
<filter
|
||||
id="filter1_f_3_590"
|
||||
x="138.125"
|
||||
y="3.0356"
|
||||
width="2.51719"
|
||||
height="2.51694"
|
||||
filterUnits="userSpaceOnUse"
|
||||
colorInterpolationFilters="sRGB"
|
||||
>
|
||||
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="BackgroundImageFix"
|
||||
result="shape"
|
||||
/>
|
||||
<feGaussianBlur
|
||||
stdDeviation="0.571191"
|
||||
result="effect1_foregroundBlur_3_590"
|
||||
/>
|
||||
</filter>
|
||||
<filter
|
||||
id="filter2_f_3_590"
|
||||
x="139.174"
|
||||
y="3.85232"
|
||||
width="1.11616"
|
||||
height="1.57978"
|
||||
filterUnits="userSpaceOnUse"
|
||||
colorInterpolationFilters="sRGB"
|
||||
>
|
||||
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="BackgroundImageFix"
|
||||
result="shape"
|
||||
/>
|
||||
<feGaussianBlur
|
||||
stdDeviation="0.162829"
|
||||
result="effect1_foregroundBlur_3_590"
|
||||
/>
|
||||
</filter>
|
||||
<filter
|
||||
id="filter3_f_3_590"
|
||||
x="138.887"
|
||||
y="4.2613"
|
||||
width="1.22643"
|
||||
height="0.993766"
|
||||
filterUnits="userSpaceOnUse"
|
||||
colorInterpolationFilters="sRGB"
|
||||
>
|
||||
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="BackgroundImageFix"
|
||||
result="shape"
|
||||
/>
|
||||
<feGaussianBlur
|
||||
stdDeviation="0.190397"
|
||||
result="effect1_foregroundBlur_3_590"
|
||||
/>
|
||||
</filter>
|
||||
<filter
|
||||
id="filter4_iii_3_590"
|
||||
x="0"
|
||||
y="156.648"
|
||||
width="279"
|
||||
height="12.0981"
|
||||
filterUnits="userSpaceOnUse"
|
||||
colorInterpolationFilters="sRGB"
|
||||
>
|
||||
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="BackgroundImageFix"
|
||||
result="shape"
|
||||
/>
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset dy="-3.98605" />
|
||||
<feGaussianBlur stdDeviation="1.40684" />
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 0.502066 0 0 0 0 0.502766 0 0 0 0 0.504167 0 0 0 1 0"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in2="shape"
|
||||
result="effect1_innerShadow_3_590"
|
||||
/>
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset dy="-1.40684" />
|
||||
<feGaussianBlur stdDeviation="0.586183" />
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 0.900764 0 0 0 0 0.904549 0 0 0 0 0.908333 0 0 0 1 0"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in2="effect1_innerShadow_3_590"
|
||||
result="effect2_innerShadow_3_590"
|
||||
/>
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset dy="-0.234473" />
|
||||
<feGaussianBlur stdDeviation="0.35171" />
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 0.616667 0 0 0 0 0.616667 0 0 0 0 0.616667 0 0 0 1 0"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in2="effect2_innerShadow_3_590"
|
||||
result="effect3_innerShadow_3_590"
|
||||
/>
|
||||
</filter>
|
||||
<linearGradient
|
||||
id="paint0_linear_3_590"
|
||||
x1="138.529"
|
||||
y1="3.98464"
|
||||
x2="140.227"
|
||||
y2="4.99109"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#222222" />
|
||||
<stop offset="1" stop-color="#0B0B0B" />
|
||||
</linearGradient>
|
||||
<radialGradient
|
||||
id="paint1_radial_3_590"
|
||||
cx="0"
|
||||
cy="0"
|
||||
r="1"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(139.385 4.47333) rotate(37.875) scale(0.601477 0.605921)"
|
||||
>
|
||||
<stop stop-color="#152457" />
|
||||
<stop offset="1" />
|
||||
</radialGradient>
|
||||
<linearGradient
|
||||
id="paint2_linear_3_590"
|
||||
x1="139.122"
|
||||
y1="4.28115"
|
||||
x2="139.417"
|
||||
y2="5.07777"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#20569B" />
|
||||
<stop offset="0.677083" stop-color="#061127" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint3_linear_3_590"
|
||||
x1="139.849"
|
||||
y1="5.00327"
|
||||
x2="139.615"
|
||||
y2="4.16228"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#3D6495" />
|
||||
<stop offset="0.71875" stop-color="#061127" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint4_linear_3_590"
|
||||
x1="139.809"
|
||||
y1="4.83552"
|
||||
x2="139.7"
|
||||
y2="4.56205"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#3291AF" />
|
||||
<stop offset="1" stop-color="#3291AF" stop-opacity="0" />
|
||||
</linearGradient>
|
||||
<radialGradient
|
||||
id="paint5_radial_3_590"
|
||||
cx="0"
|
||||
cy="0"
|
||||
r="1"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(128.707 159.462) rotate(90) scale(4.52621 30.0669)"
|
||||
>
|
||||
<stop stop-color="white" />
|
||||
<stop offset="1" stop-color="#D9D9D9" />
|
||||
</radialGradient>
|
||||
<linearGradient
|
||||
id="paint6_linear_3_590"
|
||||
x1="118.32"
|
||||
y1="161.551"
|
||||
x2="122.556"
|
||||
y2="159.462"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset="0.00209588" stop-color="#242424" />
|
||||
<stop offset="0.34936" stop-color="#EFEFEF" />
|
||||
</linearGradient>
|
||||
<radialGradient
|
||||
id="paint7_radial_3_590"
|
||||
cx="0"
|
||||
cy="0"
|
||||
r="1"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(150.295 159.462) rotate(90) scale(4.52621 30.0669)"
|
||||
>
|
||||
<stop stop-color="white" />
|
||||
<stop offset="1" stop-color="#D9D9D9" />
|
||||
</radialGradient>
|
||||
<linearGradient
|
||||
id="paint8_linear_3_590"
|
||||
x1="160.682"
|
||||
y1="161.551"
|
||||
x2="156.446"
|
||||
y2="159.462"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset="0.00209588" stop-color="#242424" />
|
||||
<stop offset="0.34936" stop-color="#EFEFEF" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint9_linear_3_590"
|
||||
x1="248.826"
|
||||
y1="168.863"
|
||||
x2="264.842"
|
||||
y2="168.863"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#D1D2D4" />
|
||||
<stop offset="0.063237" stop-color="#818181" />
|
||||
<stop offset="0.507008" stop-color="#D0D0D0" />
|
||||
<stop offset="0.864583" stop-color="#818181" />
|
||||
<stop offset="1" stop-color="#D1D2D4" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint10_linear_3_590"
|
||||
x1="249.99"
|
||||
y1="169.907"
|
||||
x2="264.145"
|
||||
y2="169.907"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#4D4D4D" />
|
||||
<stop offset="0.156699" stop-color="#292929" />
|
||||
<stop offset="0.501829" stop-color="#6A6A6A" />
|
||||
<stop offset="0.884758" stop-color="#2E2D2D" />
|
||||
<stop offset="1" stop-color="#4D4D4D" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint11_linear_3_590"
|
||||
x1="13.9277"
|
||||
y1="168.863"
|
||||
x2="29.9435"
|
||||
y2="168.863"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#D1D2D4" />
|
||||
<stop offset="0.063237" stop-color="#818181" />
|
||||
<stop offset="0.507008" stop-color="#D0D0D0" />
|
||||
<stop offset="0.864583" stop-color="#818181" />
|
||||
<stop offset="1" stop-color="#D1D2D4" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint12_linear_3_590"
|
||||
x1="15.0876"
|
||||
y1="169.907"
|
||||
x2="29.0144"
|
||||
y2="169.907"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#4D4D4D" />
|
||||
<stop offset="0.156699" stop-color="#292929" />
|
||||
<stop offset="0.501829" stop-color="#6A6A6A" />
|
||||
<stop offset="0.884758" stop-color="#2E2D2D" />
|
||||
<stop offset="1" stop-color="#4D4D4D" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint13_linear_3_590"
|
||||
x1="16.9437"
|
||||
y1="-2.17555e-06"
|
||||
x2="77.177"
|
||||
y2="59.8852"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#A8A8A8" />
|
||||
<stop offset="1" stop-color="#737475" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint14_linear_3_590"
|
||||
x1="139.501"
|
||||
y1="153.891"
|
||||
x2="139.501"
|
||||
y2="159.462"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#2D2D2D" />
|
||||
<stop offset="1" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint15_linear_3_590"
|
||||
x1="0"
|
||||
y1="164.104"
|
||||
x2="279"
|
||||
y2="164.104"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#A9AAAC" />
|
||||
<stop
|
||||
offset="0.0205416"
|
||||
stop-color="#F2F2F2"
|
||||
stop-opacity="0.921875"
|
||||
/>
|
||||
<stop offset="0.0339099" stop-color="#787879" />
|
||||
<stop
|
||||
offset="0.124814"
|
||||
stop-color="#D6D6D6"
|
||||
stop-opacity="0.606575"
|
||||
/>
|
||||
<stop offset="0.515625" stop-color="#E4E4E4" stop-opacity="0" />
|
||||
<stop
|
||||
offset="0.864583"
|
||||
stop-color="#D7D7D7"
|
||||
stop-opacity="0.666378"
|
||||
/>
|
||||
<stop offset="0.973923" stop-color="#848484" />
|
||||
<stop offset="0.992314" stop-color="#F4F4F4" />
|
||||
<stop offset="1" stop-color="#BFBFBF" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
</Box>
|
||||
);
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user