Merge branch 'dev' into refactor

This commit is contained in:
nflnkr 2024-01-31 15:57:07 +03:00
commit 1e3fc3ae26
28 changed files with 903 additions and 466 deletions

2
.gitignore vendored

@ -22,4 +22,4 @@ widget
*.ntvs*
*.njsproj
*.sln
*.sw?
*.sw?

@ -1,38 +1,32 @@
include:
- project: "devops/pena-continuous-integration"
file: "/templates/docker/build-template.gitlab-ci.yml"
- project: "devops/pena-continuous-integration"
file: "/templates/docker/clean-template.gitlab-ci.yml"
- project: "devops/pena-continuous-integration"
file: "/templates/docker/deploy-template.gitlab-ci.yml"
stages:
- clean
- build
- deploy
clear-old-images:
extends: .clean_template
variables:
STAGING_BRANCH: "main"
PRODUCTION_BRANCH: "main"
image:
name: docker/compose:1.28.0
entrypoint: [""]
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker images
script:
- docker system prune -af
build-app:
tags:
- frontbuild
extends: .build_template
variables:
DOCKER_BUILD_PATH: "./Dockerfile"
STAGING_BRANCH: "main"
STAGING_BRANCH: "staging"
PRODUCTION_BRANCH: "main"
deploy-to-staging:
extends: .deploy_template
variables:
DEPLOY_TO: "staging"
BRANCH: "main"
rules:
- if: "$CI_COMMIT_BRANCH == $STAGING_BRANCH"
tags:
- front
- staging
deploy-to-prod:
extends: .deploy_template
rules:
- if: "$CI_COMMIT_BRANCH == $PRODUCTION_BRANCH"
tags:
- front
- prod

@ -10,5 +10,6 @@ RUN yarn build
FROM nginx:latest as result
WORKDIR /usr/share/nginx/html
COPY --from=build /usr/app/build/ /usr/share/nginx/html
COPY --from=build /usr/app/dist/ /usr/share/nginx/html
COPY pub.js /usr/share/nginx/html/export/pub.js
COPY hub.conf /etc/nginx/conf.d/default.conf

@ -0,0 +1,8 @@
services:
respondent:
container_name: respondent
restart: unless-stopped
image: $CI_REGISTRY_IMAGE/main:$CI_COMMIT_REF_SLUG.$CI_PIPELINE_ID
hostname: respondent
tty: true

@ -2,12 +2,7 @@ services:
respondent:
container_name: respondent
restart: unless-stopped
image: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG.$CI_PIPELINE_ID
networks:
- marketplace_penahub_frontend
image: $CI_REGISTRY_IMAGE/staging:$CI_COMMIT_REF_SLUG.$CI_PIPELINE_ID
hostname: respondent
tty: true
networks:
marketplace_penahub_frontend:
external: true

@ -2,9 +2,12 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
<link rel="apple-touch-icon" href="/logo192.png" />
<link rel="manifest" href="/manifest.json" />
<meta name="theme-color" content="#000000" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
<title>Quiz</title>
</head>
<body>
<div id="root"></div>

@ -20,9 +20,9 @@
"@mui/x-date-pickers": "^6.16.1",
"@types/node": "^16.7.13",
"axios": "^1.5.1",
"dayjs": "^1.11.10",
"emoji-mart": "^5.5.2",
"immer": "^10.0.3",
"moment": "^2.30.1",
"nanoid": "^5.0.3",
"notistack": "^3.0.1",
"react": "^18.2.0",

1
pub.js Normal file

@ -0,0 +1 @@
console.log("PEHA NUB")

@ -1,16 +1,15 @@
import { Box, CssBaseline, ThemeProvider } from "@mui/material";
import { LocalizationProvider } from "@mui/x-date-pickers";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import { AdapterMoment } from "@mui/x-date-pickers/AdapterMoment";
import { ruRU } from '@mui/x-date-pickers/locales';
import dayjs from "dayjs";
import "dayjs/locale/ru";
import moment from "moment";
import { SnackbarProvider } from 'notistack';
import { SWRConfig } from "swr";
import { ViewPage } from "./pages/ViewPublicationPage";
import lightTheme from "./utils/themes/light";
dayjs.locale("ru");
moment.locale("ru");
const localeText = ruRU.components.MuiLocalizationProvider.defaultProps.localeText;
interface Props {
@ -24,7 +23,7 @@ export default function App({ widget = false }: Props) {
revalidateOnFocus: false,
shouldRetryOnError: false,
}}>
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale="ru" localeText={localeText}>
<LocalizationProvider dateAdapter={AdapterMoment} adapterLocale="ru" localeText={localeText}>
<ThemeProvider theme={lightTheme}>
<SnackbarProvider
preventDuplicate={true}

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

@ -5,10 +5,11 @@ type InfoProps = {
height?: number;
sx?: SxProps;
onClick?: any;
className?: string
className?: string;
color?: string
};
export default function Info({ width = 20, height = 20, sx, onClick, className }: InfoProps) {
export default function Info({ width = 20, height = 20, sx, onClick, className, color = "#7e2aea" }: InfoProps) {
return (
<IconButton
sx={sx}
@ -24,21 +25,21 @@ export default function Info({ width = 20, height = 20, sx, onClick, className }
>
<path
d="M10 19C14.9706 19 19 14.9706 19 10C19 5.02944 14.9706 1 10 1C5.02944 1 1 5.02944 1 10C1 14.9706 5.02944 19 10 19Z"
stroke="#7E2AEA"
stroke={color}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M9.25 9.25H10V14.5H10.75"
stroke="#7E2AEA"
stroke={color}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M9.8125 7C10.4338 7 10.9375 6.49632 10.9375 5.875C10.9375 5.25368 10.4338 4.75 9.8125 4.75C9.19118 4.75 8.6875 5.25368 8.6875 5.875C8.6875 6.49632 9.19118 7 9.8125 7Z"
fill="#7E2AEA"
fill={color}
/>
</svg>
</IconButton>

@ -1,4 +1,3 @@
import "dayjs/locale/ru";
import { createRoot } from "react-dom/client";
import App from "./App";

@ -5,7 +5,6 @@ import type {
} from "./shared";
export const UPLOAD_FILE_TYPES_MAP = {
all: "Все типы файлов",
picture: "Изображения",
video: "Видео",
audio: "Аудио",

@ -87,6 +87,7 @@ export interface QuizConfig {
text: FCField;
address: FCField;
button: string;
fields: Record<FormContactFieldName, FormContactFieldData>;
};
info: {
phonenumber: string;
@ -98,6 +99,21 @@ export interface QuizConfig {
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;

@ -1,6 +1,6 @@
import AddressIcon from "@icons/ContactFormIcon/AddressIcon";
import EmailIcon from "@icons/ContactFormIcon/EmailIcon";
import NameIcon from "@icons/ContactFormIcon/NameIcon";
import EmailIcon from "@icons/ContactFormIcon/EmailIcon";
import PhoneIcon from "@icons/ContactFormIcon/PhoneIcon";
import TextIcon from "@icons/ContactFormIcon/TextIcon";
import { Box, Button, InputAdornment, Link, TextField as MuiTextField, TextFieldProps, Typography, useMediaQuery, useTheme } from "@mui/material";
@ -20,16 +20,8 @@ import { useQuestionsStore } from "@stores/quizData/store";
const TextField = MuiTextField as unknown as FC<TextFieldProps>; // temporary fix ts(2590)
const EMAIL_REGEXP = /^(([^<>()[\].,:\s@"]+(\.[^<>()[\].,:\s@"]+)*)|(".+"))@(([^<>()[\].,:\s@"]+\.)+[^<>()[\].,:\s@"]{2,})$/iu;
type ContactType =
| "name"
| "email"
| "phone"
| "text"
| "adress";
type ContactFormProps = {
currentQuestion: any;
showResultForm: boolean;
@ -37,6 +29,44 @@ type ContactFormProps = {
setShowResultForm: (show: boolean) => void;
};
const icons = [
{
type: "name",
icon: NameIcon,
defaultText: "Введите имя",
defaultTitle: "имя",
backendName: "name",
},
{
type: "email",
icon: EmailIcon,
defaultText: "Введите Email",
defaultTitle: "Email",
backendName: "email",
},
{
type: "phone",
icon: PhoneIcon,
defaultText: "Введите номер телефона",
defaultTitle: "номер телефона",
backendName: "phone",
},
{
type: "text",
icon: TextIcon,
defaultText: "Введите фамилию",
defaultTitle: "фамилию",
backendName: "adress",
},
{
type: "address",
icon: AddressIcon,
defaultText: "Введите адрес",
defaultTitle: "адрес",
backendName: "adress",
},
];
export const ContactForm = ({
currentQuestion,
showResultForm,
@ -44,73 +74,94 @@ export const ContactForm = ({
setShowResultForm,
}: ContactFormProps) => {
const theme = useTheme();
const { settings, items } = useQuestionsStore()
const { settings, items } = useQuestionsStore();
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 [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 fireOnce = useRef(true);
const [fire, setFire] = useState(false);
const isMobile = useMediaQuery(theme.breakpoints.down(850));
const resultQuestion: QuizQuestionResult = items.find((question): question is QuizQuestionResult => {
if (settings?.cfg.haveRoot) { //ветвимся
const followNextForm = () => {
setShowContactForm(false);
setShowResultForm(true);
};
//@ts-ignore
const resultQuestion: QuizQuestionResult = items.find((question) => {
if (settings?.cfg.haveRoot) {
//ветвимся
return (
question.type === "result" &&
//@ts-ignore
question.content.rule.parentId === currentQuestion.content.id
);
} else {// не ветвимся
} else {
// не ветвимся
return (
question.type === "result" &&
question.content.rule.parentId === "line"
question.type === "result" && question.content.rule.parentId === "line"
);
}
})!;
});
const inputHC = async () => {
if (!settings) return;
const body: Partial<Record<ContactType, string>> = {};
//@ts-ignore
const FC = settings?.cfg.formContact.fields || settings?.cfg.formContact;
const body = {};
//@ts-ignore
if (name.length > 0) body.name = name;
//@ts-ignore
if (email.length > 0) body.email = email;
//@ts-ignore
if (phone.length > 0) body.phone = phone;
if (text.length > 0) body.text = text;
if (adress.length > 0) body.adress = adress;
//@ts-ignore
if (adress.length > 0) body.address = adress;
//@ts-ignore
if (text.length > 0) body.customs = { [FC.text.text || "Фамилия"]: text };
if (Object.keys(body).length > 0) {
try {
await sendFC({
questionId: resultQuestion?.id,
body: body,
qid: settings.qid
})
qid: settings.qid,
});
const sessions = JSON.parse(localStorage.getItem("sessions") || "{}");
localStorage.setItem(
"sessions",
JSON.stringify({ ...sessions, [settings.qid]: new Date().getTime() })
);
} catch (e) {
enqueueSnackbar("ответ не был засчитан")
enqueueSnackbar("ответ не был засчитан");
}
}
}
};
//@ts-ignore
const FCcopy: any = settings?.cfg.formContact.fields || settings?.cfg.formContact;
let FCcopy: any = settings?.cfg.formContact.fields || settings?.cfg.formContact;
const filteredFC: any = {};
for (const i in FCcopy) {
const field = FCcopy[i];
let filteredFC: any = {};
for (let i in FCcopy) {
let field = FCcopy[i];
console.log(filteredFC);
if (field.used) {
filteredFC[i] = field;
}
}
const isWide = Object.keys(filteredFC).length > 2;
let isWide = Object.keys(filteredFC).length > 2;
if (!settings) throw new Error("settings is null");
if (!resultQuestion) return <ApologyPage message="не получилось найти результат для этой ветки :(" />
if (!resultQuestion)
return (
<ApologyPage message="не получилось найти результат для этой ветки :(" />
);
return (
<Box
@ -121,17 +172,21 @@ export const ContactForm = ({
backgroundColor: theme.palette.background.default,
height: "100vh",
overflow: "auto",
"&::-webkit-scrollbar": { width: "0", display: "none", msOverflowStyle: "none" },
"&::-webkit-scrollbar": {
width: "0",
display: "none",
msOverflowStyle: "none",
},
scrollbarWidth: "none",
msOverflowStyle: "none"
msOverflowStyle: "none",
}}
>
<Box
sx={{
width: isWide && !isMobile ? "100%" : (isMobile ? undefined : "530px"),
width: isWide && !isMobile ? "100%" : isMobile ? undefined : "530px",
borderRadius: "4px",
height: "90vh",
display: isWide && !isMobile ? "flex" : undefined
display: isWide && !isMobile ? "flex" : undefined,
}}
>
<Box
@ -141,7 +196,7 @@ export const ContactForm = ({
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
borderRight: isWide && !isMobile ? "1px solid gray" : undefined
borderRight: isWide && !isMobile ? "1px solid gray" : undefined,
}}
>
<Typography
@ -149,28 +204,26 @@ export const ContactForm = ({
textAlign: "center",
m: "20px 0",
fontSize: "28px",
color: theme.palette.text.primary
color: theme.palette.text.primary,
}}
>
{settings?.cfg.formContact.title || "Заполните форму, чтобы получить результаты теста"}
{settings?.cfg.formContact.title ||
"Заполните форму, чтобы получить результаты теста"}
</Typography>
{
settings?.cfg.formContact.desc &&
{settings?.cfg.formContact.desc && (
<Typography
sx={{
color: theme.palette.text.primary,
textAlign: "center",
m: "20px 0",
fontSize: "18px"
fontSize: "18px",
}}
>
{settings?.cfg.formContact.desc}
</Typography>
}
)}
</Box>
<Box
sx={{
display: "flex",
@ -178,77 +231,92 @@ export const ContactForm = ({
justifyContent: "center",
flexDirection: "column",
backgroundColor: theme.palette.background.default,
p: "30px"
}}>
p: "30px",
}}
>
<Box
sx={{
display: "flex",
flexDirection: "column",
my: "20px"
my: "20px",
}}
>
<Inputs
name={name} setName={setName}
email={email} setEmail={setEmail}
phone={phone} setPhone={setPhone}
text={text} setText={setText}
adress={adress} setAdress={setAdress}
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={async () => {
//@ts-ignore
const FC: any = settings?.cfg.formContact.fields || settings?.cfg.formContact
if (FC["email"].used === EMAIL_REGEXP.test(email)) {//почта валидна
setFire(true)
// settings?.cfg.resultInfo.when === "after" &&
<Button
disabled={!(ready && !fire)}
variant="contained"
onClick={async () => {
//@ts-ignore
const FC: any = settings?.cfg.formContact.fields || settings?.cfg.formContact;
if (FC["email"].used === EMAIL_REGEXP.test(email)) {
//почта валидна
setFire(true);
if (fireOnce.current) {
if (
name.length > 0 ||
email.length > 0 ||
phone.length > 0 ||
text.length > 0 ||
adress.length > 0
) {
try {
await inputHC()
fireOnce.current = false
enqueueSnackbar("Данные успешно отправлены")
} catch (e) {
enqueueSnackbar("повторите попытку позже")
}
if ((settings?.cfg.resultInfo.showResultForm === "after" || settings?.cfg.resultInfo.when === "email") && !checkEmptyData({ resultData: resultQuestion })) {
setShowContactForm(false)
setShowResultForm(true)
}
} else {
enqueueSnackbar("Пожалуйста, заполните поля")
if (fireOnce.current) {
if (
name.length > 0 ||
email.length > 0 ||
phone.length > 0 ||
text.length > 0 ||
adress.length > 0
) {
try {
await inputHC();
fireOnce.current = false;
const QID =
process.env.NODE_ENV === "production"
? window.location.pathname.replace(/\//g, "")
: "ef836ff8-35b1-4031-9acf-af5766bac2b2";
const sessions: any = JSON.parse(
localStorage.getItem("sessions") || "{}"
);
sessions[QID] = Date.now();
localStorage.setItem(
"sessions",
JSON.stringify(sessions)
);
enqueueSnackbar("Данные успешно отправлены");
} catch (e) {
enqueueSnackbar("повторите попытку позже");
}
}
setFire(false)
} else {
enqueueSnackbar("введена некорректная почта")
if (
settings?.cfg.resultInfo.showResultForm === "after" &&
!checkEmptyData({ resultData: resultQuestion })
) {
setShowContactForm(false);
setShowResultForm(true);
}
} else {
enqueueSnackbar("Пожалуйста, заполните поля");
}
}
}}
>
{settings?.cfg.formContact?.button || "Получить результаты"}
</Button>
)}
setFire(false);
} else {
enqueueSnackbar("введена некорректная почта");
}
}}
>
{settings?.cfg.formContact?.button || "Получить результаты"}
</Button>
}
<Box
sx={{
@ -257,96 +325,169 @@ export const ContactForm = ({
width: isMobile ? "300px" : "450px",
}}
>
<CustomCheckbox label="" handleChange={({ target }) => { setReady(target.checked) }} checked={ready} colorIcon={theme.palette.primary.main} />
<CustomCheckbox
label=""
handleChange={({ target }) => {
setReady(target.checked);
}}
checked={ready}
colorIcon={theme.palette.primary.main}
/>
<Typography sx={{ color: theme.palette.text.primary }}>
С&ensp;
<Link href={"https://shub.pena.digital/ppdd"} target="_blank">
Положением об обработке персональных данных </Link>
Положением об обработке персональных данных{" "}
</Link>
&ensp;и&ensp;
<Link href={"https://shub.pena.digital/docs/privacy"} target="_blank"> Политикой конфиденциальности </Link>
<Link
href={"https://shub.pena.digital/docs/privacy"}
target="_blank"
>
{" "}
Политикой конфиденциальности{" "}
</Link>
&ensp;ознакомлен
</Typography>
</Box>
<Box
sx={{
display: "flex",
alignItems: "center",
mt: "20px",
gap: "15px"
gap: "15px",
}}
>
<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"
}}>
<NameplateLogo
style={{
fontSize: "34px",
//@ts-ignore
color: mode[settings.cfg.theme] ? "#151515" : "#FFFFFF",
}}
/>
<Typography
sx={{
fontSize: "20px",
//@ts-ignore
color: mode[settings.cfg.theme] ? "#4D4D4D" : "#F5F7FF",
whiteSpace: "nowrap",
}}
>
Сделано на PenaQuiz
</Typography>
</Box>
</Box>
</Box >
</Box >
</Box>
</Box>
);
};
const Inputs = ({
name, setName,
email, setEmail,
phone, setPhone,
text, setText,
adress, setAdress
name,
setName,
email,
setEmail,
phone,
setPhone,
text,
setText,
adress,
setAdress,
}: any) => {
const { settings } = useQuestionsStore()
const { settings } = useQuestionsStore();
// @ts-ignore
const FC = settings?.cfg.formContact.fields || settings?.cfg.formContact;
if (!FC) return null;
//@ts-ignore
const FC: any = settings?.cfg.formContact.fields || settings?.cfg.formContact
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 Name = <CustomInput onChange={({ target }) => setName(target.value)} id={name} title={FC["name"].innerText || "Введите имя"} desc={FC["name"].text || "имя"} Icon={NameIcon} />
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 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 Text = (
<CustomInput
//@ts-ignore
onChange={({ target }) => setText(target.value)}
id={text}
title={FC["text"].text || "Введите фамилию"}
desc={FC["text"].innerText || "фамилию"}
Icon={TextIcon}
/>
);
//@ts-ignore
const Phone = <CustomInput onChange={({ target }) => setPhone(target.value)} id={phone} title={FC["phone"].innerText || "Введите номер телефона"} desc={FC["phone"].text || "номер телефона"} Icon={PhoneIcon} />
//@ts-ignore
const Text = <CustomInput onChange={({ target }) => setText(target.value)} id={text} title={FC["text"].innerText || "Введите фамилию"} desc={FC["text"].text || "фамилию"} Icon={TextIcon} />
//@ts-ignore
const Adress = <CustomInput onChange={({ target }) => setAdress(target.value)} id={adress} title={FC["address"].innerText || "Введите адрес"} desc={FC["address"].text || "адрес"} Icon={AddressIcon} />
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 : <></>}
</>
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}
</>
return (
<>
{Name}
{Email}
{Phone}
</>
);
}
}
};
const CustomInput = ({ title, desc, Icon, onChange }: any) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(600));
//@ts-ignore
return (
<Box m="15px 0">
<Typography mb="7px" color={theme.palette.text.primary}>{title}</Typography>
<Typography mb="7px" color={theme.palette.text.primary}>
{title}
</Typography>
<TextField
onChange={onChange}
@ -355,9 +496,13 @@ const CustomInput = ({ title, desc, Icon, onChange }: any) => {
}}
placeholder={desc}
InputProps={{
startAdornment: <InputAdornment position="start"><Icon color="gray" /></InputAdornment>,
startAdornment: (
<InputAdornment position="start">
<Icon color="gray" />
</InputAdornment>
),
}}
/>
</Box>
)
}
);
};

@ -11,15 +11,16 @@ import useSWR from "swr";
import { ApologyPage } from "./ApologyPage";
import { Question } from "./Question";
import { StartPageViewPublication } from "./StartPageViewPublication";
import { replaceSpacesToEmptyLines } from "./tools/replaceSpacesToEmptyLines";
import { parseQuizData } from "@model/api/getQuizData";
import { parseQuizData } from "@model/api/getQuizData";
import { replaceSpacesToEmptyLines } from "./tools/replaceSpacesToEmptyLines";
const QID =
import.meta.env.PROD ?
window.location.pathname.replace(/\//g, '')
:
"0bed8483-3016-4bca-b8e0-a72c3146f18b";
"ef836ff8-35b1-4031-9acf-af5766bac2b2";
export const ViewPage = () => {
@ -36,6 +37,8 @@ export const ViewPage = () => {
if (link && settings.cfg.startpage.favIcon) {
link.setAttribute("href", settings?.cfg.startpage.favIcon);
}
//установка заголовка страницы
document.title = settings.name;
setVisualStartPage(!settings.cfg.noStartPage);
}, [settings]);

@ -1,4 +1,4 @@
import dayjs from "dayjs";
import moment from "moment";
import { DatePicker } from "@mui/x-date-pickers";
import { Box, Typography, useTheme } from "@mui/material";
@ -24,8 +24,8 @@ export const Date = ({ currentQuestion }: DateProps) => {
const answer = answers.find(
({ questionId }) => questionId === currentQuestion.id
)?.answer as string;
const [day, month, year] = answer?.split(".") || [];
const currentAnswer = moment(answer) || moment();
if (!settings) throw new Error("settings is null");
return (
@ -52,12 +52,9 @@ export const Date = ({ currentQuestion }: DateProps) => {
/>
),
}}
value={dayjs(
month && day && year
? new window.Date(`${month}.${day}.${year}`)
: new window.Date()
)}
value={ currentAnswer }
onChange={async (date) => {
console.log(date)
if (!date) {
return;
}
@ -65,26 +62,13 @@ export const Date = ({ currentQuestion }: DateProps) => {
try {
await sendAnswer({
questionId: currentQuestion.id,
body: new window.Date(date.toDate()).toLocaleDateString(
"ru-RU",
{
year: "numeric",
month: "2-digit",
day: "2-digit",
}
),
body: moment(date).format("YYYY.MM.DD"),
qid: settings.qid,
});
updateAnswer(
currentQuestion.id,
String(
new window.Date(date.toDate()).toLocaleDateString("ru-RU", {
year: "numeric",
month: "2-digit",
day: "2-digit",
})
)
date
);
} catch (e) {
enqueueSnackbar("ответ не был засчитан");

@ -111,7 +111,7 @@ export const Emoji = ({ currentQuestion }: EmojiProps) => {
await sendAnswer({
questionId: currentQuestion.id,
body: currentQuestion.content.variants[index].answer,
body: currentQuestion.content.variants[index].extendedText + " " + currentQuestion.content.variants[index].answer,
qid: settings.qid
})

@ -1,9 +1,9 @@
import {
Box,
Typography,
ButtonBase,
useTheme,
IconButton, useMediaQuery,
Box,
Typography,
ButtonBase,
useTheme,
IconButton, useMediaQuery, Modal,
} from "@mui/material";
import { useQuizViewStore, updateAnswer } from "@stores/quizView/store";
import { UPLOAD_FILE_TYPES_MAP } from "../tools/File";
@ -11,23 +11,97 @@ import { UPLOAD_FILE_TYPES_MAP } from "../tools/File";
import UploadIcon from "@icons/UploadIcon";
import CloseBold from "@icons/CloseBold";
import type { ChangeEvent } from "react";
import { useState, type ChangeEvent } from "react";
import type { QuizQuestionFile } from "../../../model/questionTypes/file";
import type { DragEvent } from "react";
import type { UploadFileType } from "@model/questionTypes/file";
import { enqueueSnackbar } from "notistack";
import { sendFile } from "@api/quizRelase";
import { sendAnswer, sendFile } from "@api/quizRelase";
import { useQuestionsStore } from "@stores/quizData/store"
import Info from "@icons/Info";
type FileProps = {
currentQuestion: QuizQuestionFile;
};
const CurrentModal = ({ status }: { status: "errorType" | "errorSize" | "picture" | "video" | "audio" | "document" | "" }) => {
switch (status) {
case 'errorType':
return (<>
<Typography>Выбран некорректный тип файла</Typography>
</>)
case 'errorSize':
return (<>
<Typography>Файл слишком большой. Максимальный размер 50 МБ</Typography>
</>)
default:
return (<>
<Typography>Допустимые расширения файлов:</Typography>
<Typography>{
//@ts-ignore
ACCEPT_SEND_FILE_TYPES_MAP[status].join(" ")}</Typography>
</>)
}
}
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",
],
}
const UPLOAD_FILE_DESCRIPTIONS_MAP: Record<
UploadFileType,
{ title: string; description: string }
> = {
all: { title: "Добавить файл", description: "Принимает любые файлы" },
picture: {
title: "Добавить изображение",
description: "Принимает изображения",
@ -41,171 +115,230 @@ const UPLOAD_FILE_DESCRIPTIONS_MAP: Record<
} as const;
export const File = ({ currentQuestion }: FileProps) => {
const theme = useTheme();
const { settings } = useQuestionsStore()
const { answers } = useQuizViewStore();
const [statusModal, setStatusModal] = useState<"errorType" | "errorSize" | "picture" | "video" | "audio" | "document" | "">("")
const answer = answers.find(
({ questionId }) => questionId === currentQuestion.id
)?.answer as string;
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(500));
const uploadFile = async ({ target }: ChangeEvent<HTMLInputElement>) => {
if (!settings) return;
const file = target.files?.[0];
if (file) {
if (file.size <= 52428800) {
//проверяем на соответствие
console.log(file.name.toLowerCase())
if (ACCEPT_SEND_FILE_TYPES_MAP[currentQuestion.content.type].find((ednding => {
console.log(ednding)
console.log(file.name.toLowerCase().endsWith(ednding))
return file.name.toLowerCase().endsWith(ednding)
}))) {
try {
//Нужный формат
console.log(file)
try {
await sendFile({
questionId: currentQuestion.id,
body: {
file: `${file.name}|${URL.createObjectURL(file)}`,
name: file.name
},
qid: settings.qid
})
const data = await sendFile({
questionId: currentQuestion.id,
body: {
file: file,
name: file.name
},
qid: settings.qid
})
console.log(data)
updateAnswer(
currentQuestion.id,
`${file.name}|${URL.createObjectURL(file)}`
);
await sendAnswer({
questionId: currentQuestion.id,
//@ts-ignore
body: `https://storage.yandexcloud.net/squizanswer/${settings.qid}/${currentQuestion.id}/${data.data.fileIDMap[currentQuestion.id]}`,
//@ts-ignore
qid: settings.qid
})
} catch (e) {
enqueueSnackbar("ответ не был засчитан")
}
updateAnswer(
currentQuestion.id,
`${file.name}|${URL.createObjectURL(file)}`
);
} catch (e) {
console.log(e)
enqueueSnackbar("ответ не был засчитан")
}
} else {
//неподходящий формат
setStatusModal("errorType")
}
} else {
setStatusModal("errorSize")
}
}
};
return (
<Box>
<Typography variant="h5" color={theme.palette.text.primary}>{currentQuestion.title}</Typography>
<Box
sx={{
display: "flex",
flexDirection: "column",
width: "100%",
marginTop: "20px",
maxWidth: answer?.split("|")[0] ? "640px" : "550px",
}}
>
{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={() => {
updateAnswer(currentQuestion.id, "");
<>
<Box>
<Typography variant="h5" color={theme.palette.text.primary}>{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",
}}
>
<CloseBold />
</IconButton>
</Box>
</Box>
)}
{!answer?.split("|")[0] && (
<ButtonBase component="label" sx={{ justifyContent: "flex-start" }}>
<input
onChange={uploadFile}
hidden
accept={UPLOAD_FILE_TYPES_MAP[currentQuestion.content.type]}
multiple
type="file"
/>
<Box
onDragOver={(event: DragEvent<HTMLDivElement>) =>
event.preventDefault()
}
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 #9A9AAF`,
// border: `1px solid ${theme.palette.grey2.main}`,
borderRadius: "8px",
}}
>
<UploadIcon />
<Box>
<Typography
sx={{
color: "#9A9AAF",
// color: theme.palette.grey2.main,
fontWeight: 500,
whiteSpace: "nowrap",
textOverflow: "ellipsis",
overflow: "hidden",
}}
>
{
UPLOAD_FILE_DESCRIPTIONS_MAP[currentQuestion.content.type]
.title
}
</Typography>
<Typography
sx={{
color: "#9A9AAF",
// color: theme.palette.grey2.main,
fontSize: "16px",
lineHeight: "19px",
{answer?.split("|")[0]}</Typography>
<IconButton
sx={{ p: 0 }}
onClick={() => {
updateAnswer(currentQuestion.id, "");
}}
>
{
UPLOAD_FILE_DESCRIPTIONS_MAP[currentQuestion.content.type]
.description
}
</Typography>
<CloseBold />
</IconButton>
</Box>
</Box>
</ButtonBase>
)}
{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",
}}
/>
)}
)}
{!answer?.split("|")[0] && (
<Box sx={{
display: "flex",
alignItems: "center"
}}>
<ButtonBase component="label" sx={{ justifyContent: "flex-start", width: "100%" }}>
<input
onChange={uploadFile}
hidden
accept={ACCEPT_SEND_FILE_TYPES_MAP[currentQuestion.content.type].join(",")}
multiple
type="file"
/>
<Box
onDragOver={(event: DragEvent<HTMLDivElement>) =>
event.preventDefault()
}
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 #9A9AAF`,
// border: `1px solid ${theme.palette.grey2.main}`,
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={() => setStatusModal(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>
</Box>
</Box>
<Modal
open={Boolean(statusModal)}
onClose={() => setStatusModal("")}
>
<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={statusModal} />
</Box>
</Modal>
</>
);
};

@ -78,7 +78,7 @@ export const Images = ({ currentQuestion }: ImagesProps) => {
await sendAnswer({
questionId: currentQuestion.id,
body: currentQuestion.content.variants[index].answer,
body: `${currentQuestion.content.variants[index].answer} <img style="width:100%; max-width:250px; max-height:250px" src="${currentQuestion.content.variants[index].extendedText}"/>`,
qid: settings.qid
})

@ -10,7 +10,7 @@ import { useQuizViewStore, updateAnswer } from "@stores/quizView/store";
import type { QuizQuestionNumber } from "../../../model/questionTypes/number";
import { enqueueSnackbar } from "notistack";
import { sendAnswer } from "@api/quizRelase";
import { quizThemes } from "@utils/themes/Publication/themePublication";
import { useQuestionsStore } from "@stores/quizData/store";
@ -19,74 +19,89 @@ type NumberProps = {
};
export const Number = ({ currentQuestion }: NumberProps) => {
const { settings } = useQuestionsStore()
const { settings } = useQuestionsStore();
const [inputValue, setInputValue] = useState<string>("0");
const [minRange, setMinRange] = useState<string>("0");
const [maxRange, setMaxRange] = useState<string>("100000000000");
const theme = useTheme();
const { answers } = useQuizViewStore();
const isMobile = useMediaQuery(theme.breakpoints.down(650));
const updateMinRangeDebounced = useDebouncedCallback(async (value, crowded = false) => {
if (!settings) return null;
if (crowded) {
setMinRange(maxRange);
}
try {
await sendAnswer({
questionId: currentQuestion.id,
body: value,
qid: settings.qid
})
updateAnswer(currentQuestion.id, value);
} catch (e) {
enqueueSnackbar("ответ не был засчитан")
}
}, 1000);
const updateMaxRangeDebounced = useDebouncedCallback(async (value, crowded = false) => {
if (!settings) return null;
if (crowded) {
setMaxRange(minRange);
}
try {
await sendAnswer({
questionId: currentQuestion.id,
body: value,
qid: settings.qid
})
updateAnswer(currentQuestion.id, value);
} catch (e) {
enqueueSnackbar("ответ не был засчитан")
}
}, 1000);
const answer = answers.find(({ questionId }) => questionId === currentQuestion.id)?.answer as string;
const min = window.Number(currentQuestion.content.range.split("—")[0]);
const max = window.Number(currentQuestion.content.range.split("—")[1]);
const sendAnswerToBackend = async (value: string) => {
try {
await sendAnswer({
questionId: currentQuestion.id,
body: value,
//@ts-ignore
qid: settings.qid,
});
updateAnswer(currentQuestion.id, value);
} catch (e) {
enqueueSnackbar("ответ не был засчитан");
}
};
const updateValueDebounced = useDebouncedCallback(async (value: string) => {
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) => {
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) => {
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 || currentQuestion.content.start + "—" + max;
useEffect(() => {
if (answer) {
setMinRange(answer.split("—")[0]);
setMaxRange(answer.split("—")[1]);
if (answer.includes("—")) {
setMinRange(answer.split("—")[0]);
setMaxRange(answer.split("—")[1]);
} else {
setInputValue(answer);
}
}
if (!answer) {
setMinRange(String(currentQuestion.content.start));
setMaxRange(String(max));
setInputValue(String(currentQuestion.content.start));
}
}, []);
@ -94,7 +109,9 @@ export const Number = ({ currentQuestion }: NumberProps) => {
return (
<Box>
<Typography variant="h5" color={theme.palette.text.primary}>{currentQuestion.title}</Typography>
<Typography variant="h5" color={theme.palette.text.primary}>
{currentQuestion.title}
</Typography>
<Box
sx={{
display: "flex",
@ -102,7 +119,7 @@ export const Number = ({ currentQuestion }: NumberProps) => {
width: "100%",
marginTop: "20px",
gap: "30px",
paddingRight: isMobile ? "10px" : undefined
paddingRight: isMobile ? "10px" : undefined,
}}
>
<CustomSlider
@ -116,50 +133,51 @@ export const Number = ({ currentQuestion }: NumberProps) => {
min={min}
max={max}
step={currentQuestion.content.step || 1}
onChange={async (_, value) => {
const range = String(value).replace(",", "—").replace (/\D/, '');
onChange={(_, value) => {
const range = Array.isArray(value)
? `${value[0]}${value[1]}`
: String(value);
updateAnswer(currentQuestion.id, range);
updateMinRangeDebounced(range, true);
}}
onChangeCommitted={(_, value) => {
if (currentQuestion.content.chooseRange) {
const range = value as number[];
onChangeCommitted={async (_, value) => {
if (currentQuestion.content.chooseRange && Array.isArray(value)) {
setMinRange(String(value[0]));
setMaxRange(String(value[1]));
await sendAnswerToBackend(`${value[0]}${value[1]}`);
setMinRange(String(range[0]));
setMaxRange(String(range[1]));
return;
}
setInputValue(String(value));
await sendAnswerToBackend(String(value));
}}
//@ts-ignore
sx={{
color: theme.palette.primary.main,
"& .MuiSlider-valueLabel": {
background: theme.palette.primary.main,
}
},
}}
/>
{!currentQuestion.content.chooseRange && (
<CustomTextField
placeholder="0"
value={answer}
onChange={async ({ target }) => {
updateMinRangeDebounced(window.Number(target.value.replace (/\D/, '')) > max
? String(max)
: window.Number(target.value) < min
? String(min)
: target.value, true);
value={inputValue}
onChange={({ target }) => {
const value = target.value.replace(/\D/g, "");
setInputValue(value);
updateValueDebounced(value);
}}
sx={{
maxWidth: "80px",
borderColor: theme.palette.text.primary,
"& .MuiInputBase-input": {
textAlign: "center",
backgroundColor: quizThemes[settings.cfg.theme].isLight ? "white" : theme.palette.background.default,
backgroundColor: quizThemes[settings.cfg.theme].isLight
? "white"
: theme.palette.background.default,
},
}}
/>
@ -178,22 +196,25 @@ export const Number = ({ currentQuestion }: NumberProps) => {
placeholder="0"
value={minRange}
onChange={({ target }) => {
setMinRange(target.value.replace (/\D/, ''));
const newValue = target.value.replace(/\D/g, "");
setMinRange(newValue);
if (window.Number(target.value) >= window.Number(maxRange)) {
if (window.Number(newValue) >= window.Number(maxRange)) {
updateMinRangeDebounced(`${maxRange}${maxRange}`, true);
return;
}
updateMinRangeDebounced(`${target.value}${maxRange}`);
updateMinRangeDebounced(`${newValue}${maxRange}`);
}}
sx={{
maxWidth: "80px",
borderColor: theme.palette.text.primary,
"& .MuiInputBase-input": {
textAlign: "center",
backgroundColor: quizThemes[settings.cfg.theme].isLight ? "white" : theme.palette.background.default,
backgroundColor: quizThemes[settings.cfg.theme].isLight
? "white"
: theme.palette.background.default,
},
}}
/>
@ -202,22 +223,25 @@ export const Number = ({ currentQuestion }: NumberProps) => {
placeholder="0"
value={maxRange}
onChange={({ target }) => {
setMaxRange(target.value.replace (/\D/, ''));
const newValue = target.value.replace(/\D/g, "");
setMaxRange(newValue);
if (window.Number(target.value) <= window.Number(minRange)) {
if (window.Number(newValue) <= window.Number(minRange)) {
updateMaxRangeDebounced(`${minRange}${minRange}`, true);
return;
}
updateMaxRangeDebounced(`${minRange}${target.value}`);
updateMaxRangeDebounced(`${minRange}${newValue}`);
}}
sx={{
maxWidth: "80px",
borderColor: theme.palette.text.primary,
"& .MuiInputBase-input": {
textAlign: "center",
backgroundColor: quizThemes[settings.cfg.theme].isLight ? "white" : theme.palette.background.default,
backgroundColor: quizThemes[settings.cfg.theme].isLight
? "white"
: theme.palette.background.default,
},
}}
/>
@ -226,5 +250,4 @@ export const Number = ({ currentQuestion }: NumberProps) => {
</Box>
</Box>
);
};

@ -99,7 +99,7 @@ export const Rating = ({ currentQuestion }: RatingProps) => {
await sendAnswer({
questionId: currentQuestion.id,
body: String(value),
body: String(value) + " из " + currentQuestion.content.steps,
qid: settings.qid
})

@ -92,7 +92,7 @@ export const Varimg = ({ currentQuestion }: VarimgProps) => {
await sendAnswer({
questionId: currentQuestion.id,
body: currentQuestion.content.variants[index].answer,
body: `${currentQuestion.content.variants[index].answer} <img style="width:100%; max-width:250px; max-height:250px" src="${currentQuestion.content.variants[index].extendedText}"/>`,
qid: settings.qid
})

@ -7,7 +7,6 @@ import type {
} from "model/questionTypes/file";
export const UPLOAD_FILE_TYPES_MAP: Record<UploadFileType, string> = {
all: "file",
picture: "image/*",
video: "video/*",
audio: "audio/*",
@ -23,7 +22,7 @@ export default function File({ question }: Props) {
const fileInputRef = useRef<HTMLInputElement>(null);
const [file, setFile] = useState<File | null>(null);
const [acceptedType, setAcceptedType] = useState<any>(
UPLOAD_FILE_TYPES_MAP.all
UPLOAD_FILE_TYPES_MAP.picture
);
useEffect(() => {

@ -3,10 +3,11 @@ import { produce } from "immer";
import { nanoid } from "nanoid";
import { create } from "zustand";
import { devtools } from "zustand/middleware";
import type { Moment } from "moment";
type Answer = {
questionId: string;
answer: string | string[];
answer: string | string[] | Moment;
};
type OwnVariant = {
@ -40,7 +41,10 @@ function setProducedState<A extends string | { type: string; }>(
useQuizViewStore.setState(state => produce(state, recipe), false, action);
}
export const updateAnswer = (questionId: string, answer: string | string[]) => setProducedState(state => {
export const updateAnswer = (
questionId: string,
answer: string | string[] | Moment
) => setProducedState(state => {
const index = state.answers.findIndex(answer => questionId === answer.questionId);
if (index < 0) {

@ -0,0 +1,69 @@
import CalendarIcon from "@icons/CalendarIcon";
import { Typography, Box, SxProps, Theme, useMediaQuery, useTheme } from "@mui/material";
import { DatePicker } from "@mui/x-date-pickers";
import moment from "moment";
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 = useMediaQuery(theme.breakpoints.up("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>
);
}

@ -0,0 +1,56 @@
import { useEffect, useLayoutEffect, useRef, useState } from "react"
import { useQuestionsStore } from "@stores/quizData/store";
import { getData } from "@api/quizRelase"
interface SettingsGetter {
quizId: string
}
export function useGetSettings(quizId: string) {
const [fetchState, setFetchState] = useState<"fetching" | "idle" | "all fetched">("idle")
useEffect(() => {
async function get() {
const data = await getData(quizId)
//@ts-ignore
const settings = data.settings
const parseData = {
settings: {
fp: settings.fp,
rep: settings.rep,
name: settings.name,
cfg: JSON.parse(settings?.cfg),
lim: settings.lim,
due: settings.due,
delay: settings.delay,
pausable: settings.pausable
},
//@ts-ignore
items: data.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
}
}),
//@ts-ignore
cnt: data.cnt
}
//@ts-ignore
useQuestionsStore.setState(parseData)
}
get()
// const controller = new AbortController()
}, [])
return
// return
}

@ -1471,7 +1471,7 @@ dashdash@^1.12.0:
dependencies:
assert-plus "^1.0.0"
dayjs@^1.10.4, dayjs@^1.11.10:
dayjs@^1.10.4:
version "1.11.10"
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.10.tgz#68acea85317a6e164457d6d6947564029a6a16a0"
integrity sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==
@ -2444,6 +2444,11 @@ minimist@^1.2.8:
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
moment@^2.30.1:
version "2.30.1"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.30.1.tgz#f8c91c07b7a786e30c59926df530b4eac96974ae"
integrity sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==
ms@2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"