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* *.ntvs*
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?

@ -1,38 +1,32 @@
include: include:
- project: "devops/pena-continuous-integration" - project: "devops/pena-continuous-integration"
file: "/templates/docker/build-template.gitlab-ci.yml" 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" - project: "devops/pena-continuous-integration"
file: "/templates/docker/deploy-template.gitlab-ci.yml" file: "/templates/docker/deploy-template.gitlab-ci.yml"
stages: stages:
- clean
- build - build
- deploy - 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: build-app:
tags:
- frontbuild
extends: .build_template extends: .build_template
variables: variables:
DOCKER_BUILD_PATH: "./Dockerfile" DOCKER_BUILD_PATH: "./Dockerfile"
STAGING_BRANCH: "main" STAGING_BRANCH: "staging"
PRODUCTION_BRANCH: "main" PRODUCTION_BRANCH: "main"
deploy-to-staging: deploy-to-staging:
extends: .deploy_template extends: .deploy_template
variables: rules:
DEPLOY_TO: "staging" - if: "$CI_COMMIT_BRANCH == $STAGING_BRANCH"
BRANCH: "main" 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 FROM nginx:latest as result
WORKDIR /usr/share/nginx/html 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 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: respondent:
container_name: respondent container_name: respondent
restart: unless-stopped restart: unless-stopped
image: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG.$CI_PIPELINE_ID image: $CI_REGISTRY_IMAGE/staging:$CI_COMMIT_REF_SLUG.$CI_PIPELINE_ID
networks:
- marketplace_penahub_frontend
hostname: respondent hostname: respondent
tty: true tty: true
networks:
marketplace_penahub_frontend:
external: true

@ -2,9 +2,12 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <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" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title> <title>Quiz</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

@ -20,9 +20,9 @@
"@mui/x-date-pickers": "^6.16.1", "@mui/x-date-pickers": "^6.16.1",
"@types/node": "^16.7.13", "@types/node": "^16.7.13",
"axios": "^1.5.1", "axios": "^1.5.1",
"dayjs": "^1.11.10",
"emoji-mart": "^5.5.2", "emoji-mart": "^5.5.2",
"immer": "^10.0.3", "immer": "^10.0.3",
"moment": "^2.30.1",
"nanoid": "^5.0.3", "nanoid": "^5.0.3",
"notistack": "^3.0.1", "notistack": "^3.0.1",
"react": "^18.2.0", "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 { Box, CssBaseline, ThemeProvider } from "@mui/material";
import { LocalizationProvider } from "@mui/x-date-pickers"; 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 { ruRU } from '@mui/x-date-pickers/locales';
import dayjs from "dayjs"; import moment from "moment";
import "dayjs/locale/ru";
import { SnackbarProvider } from 'notistack'; import { SnackbarProvider } from 'notistack';
import { SWRConfig } from "swr"; import { SWRConfig } from "swr";
import { ViewPage } from "./pages/ViewPublicationPage"; import { ViewPage } from "./pages/ViewPublicationPage";
import lightTheme from "./utils/themes/light"; import lightTheme from "./utils/themes/light";
dayjs.locale("ru"); moment.locale("ru");
const localeText = ruRU.components.MuiLocalizationProvider.defaultProps.localeText; const localeText = ruRU.components.MuiLocalizationProvider.defaultProps.localeText;
interface Props { interface Props {
@ -24,7 +23,7 @@ export default function App({ widget = false }: Props) {
revalidateOnFocus: false, revalidateOnFocus: false,
shouldRetryOnError: false, shouldRetryOnError: false,
}}> }}>
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale="ru" localeText={localeText}> <LocalizationProvider dateAdapter={AdapterMoment} adapterLocale="ru" localeText={localeText}>
<ThemeProvider theme={lightTheme}> <ThemeProvider theme={lightTheme}>
<SnackbarProvider <SnackbarProvider
preventDuplicate={true} 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; height?: number;
sx?: SxProps; sx?: SxProps;
onClick?: any; 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 ( return (
<IconButton <IconButton
sx={sx} sx={sx}
@ -24,21 +25,21 @@ export default function Info({ width = 20, height = 20, sx, onClick, className }
> >
<path <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" 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" strokeWidth="1.5"
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
/> />
<path <path
d="M9.25 9.25H10V14.5H10.75" d="M9.25 9.25H10V14.5H10.75"
stroke="#7E2AEA" stroke={color}
strokeWidth="1.5" strokeWidth="1.5"
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
/> />
<path <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" 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> </svg>
</IconButton> </IconButton>

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

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

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

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

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

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

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

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

@ -10,7 +10,7 @@ import { useQuizViewStore, updateAnswer } from "@stores/quizView/store";
import type { QuizQuestionNumber } from "../../../model/questionTypes/number"; import type { QuizQuestionNumber } from "../../../model/questionTypes/number";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import { sendAnswer } from "@api/quizRelase"; import { sendAnswer } from "@api/quizRelase";
import { quizThemes } from "@utils/themes/Publication/themePublication"; import { quizThemes } from "@utils/themes/Publication/themePublication";
import { useQuestionsStore } from "@stores/quizData/store"; import { useQuestionsStore } from "@stores/quizData/store";
@ -19,74 +19,89 @@ type NumberProps = {
}; };
export const Number = ({ currentQuestion }: 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 [minRange, setMinRange] = useState<string>("0");
const [maxRange, setMaxRange] = useState<string>("100000000000"); const [maxRange, setMaxRange] = useState<string>("100000000000");
const theme = useTheme(); const theme = useTheme();
const { answers } = useQuizViewStore(); const { answers } = useQuizViewStore();
const isMobile = useMediaQuery(theme.breakpoints.down(650)); 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 min = window.Number(currentQuestion.content.range.split("—")[0]);
const max = window.Number(currentQuestion.content.range.split("—")[1]); 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; const sliderValue = answer || currentQuestion.content.start + "—" + max;
useEffect(() => { useEffect(() => {
if (answer) { if (answer) {
setMinRange(answer.split("—")[0]); if (answer.includes("—")) {
setMaxRange(answer.split("—")[1]); setMinRange(answer.split("—")[0]);
setMaxRange(answer.split("—")[1]);
} else {
setInputValue(answer);
}
} }
if (!answer) { if (!answer) {
setMinRange(String(currentQuestion.content.start)); setMinRange(String(currentQuestion.content.start));
setMaxRange(String(max)); setMaxRange(String(max));
setInputValue(String(currentQuestion.content.start));
} }
}, []); }, []);
@ -94,7 +109,9 @@ export const Number = ({ currentQuestion }: NumberProps) => {
return ( return (
<Box> <Box>
<Typography variant="h5" color={theme.palette.text.primary}>{currentQuestion.title}</Typography> <Typography variant="h5" color={theme.palette.text.primary}>
{currentQuestion.title}
</Typography>
<Box <Box
sx={{ sx={{
display: "flex", display: "flex",
@ -102,7 +119,7 @@ export const Number = ({ currentQuestion }: NumberProps) => {
width: "100%", width: "100%",
marginTop: "20px", marginTop: "20px",
gap: "30px", gap: "30px",
paddingRight: isMobile ? "10px" : undefined paddingRight: isMobile ? "10px" : undefined,
}} }}
> >
<CustomSlider <CustomSlider
@ -116,50 +133,51 @@ export const Number = ({ currentQuestion }: NumberProps) => {
min={min} min={min}
max={max} max={max}
step={currentQuestion.content.step || 1} step={currentQuestion.content.step || 1}
onChange={async (_, value) => { onChange={(_, value) => {
const range = Array.isArray(value)
? `${value[0]}${value[1]}`
: String(value);
const range = String(value).replace(",", "—").replace (/\D/, '');
updateAnswer(currentQuestion.id, range); updateAnswer(currentQuestion.id, range);
updateMinRangeDebounced(range, true);
}} }}
onChangeCommitted={(_, value) => { onChangeCommitted={async (_, value) => {
if (currentQuestion.content.chooseRange) { if (currentQuestion.content.chooseRange && Array.isArray(value)) {
const range = value as number[]; setMinRange(String(value[0]));
setMaxRange(String(value[1]));
await sendAnswerToBackend(`${value[0]}${value[1]}`);
setMinRange(String(range[0])); return;
setMaxRange(String(range[1]));
} }
setInputValue(String(value));
await sendAnswerToBackend(String(value));
}} }}
//@ts-ignore
sx={{ sx={{
color: theme.palette.primary.main, color: theme.palette.primary.main,
"& .MuiSlider-valueLabel": { "& .MuiSlider-valueLabel": {
background: theme.palette.primary.main, background: theme.palette.primary.main,
} },
}} }}
/> />
{!currentQuestion.content.chooseRange && ( {!currentQuestion.content.chooseRange && (
<CustomTextField <CustomTextField
placeholder="0" placeholder="0"
value={answer} value={inputValue}
onChange={async ({ target }) => { onChange={({ target }) => {
updateMinRangeDebounced(window.Number(target.value.replace (/\D/, '')) > max const value = target.value.replace(/\D/g, "");
? String(max) setInputValue(value);
: window.Number(target.value) < min updateValueDebounced(value);
? String(min)
: target.value, true);
}} }}
sx={{ sx={{
maxWidth: "80px", maxWidth: "80px",
borderColor: theme.palette.text.primary, borderColor: theme.palette.text.primary,
"& .MuiInputBase-input": { "& .MuiInputBase-input": {
textAlign: "center", 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" placeholder="0"
value={minRange} value={minRange}
onChange={({ target }) => { 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); updateMinRangeDebounced(`${maxRange}${maxRange}`, true);
return; return;
} }
updateMinRangeDebounced(`${target.value}${maxRange}`); updateMinRangeDebounced(`${newValue}${maxRange}`);
}} }}
sx={{ sx={{
maxWidth: "80px", maxWidth: "80px",
borderColor: theme.palette.text.primary, borderColor: theme.palette.text.primary,
"& .MuiInputBase-input": { "& .MuiInputBase-input": {
textAlign: "center", 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" placeholder="0"
value={maxRange} value={maxRange}
onChange={({ target }) => { 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); updateMaxRangeDebounced(`${minRange}${minRange}`, true);
return; return;
} }
updateMaxRangeDebounced(`${minRange}${target.value}`); updateMaxRangeDebounced(`${minRange}${newValue}`);
}} }}
sx={{ sx={{
maxWidth: "80px", maxWidth: "80px",
borderColor: theme.palette.text.primary, borderColor: theme.palette.text.primary,
"& .MuiInputBase-input": { "& .MuiInputBase-input": {
textAlign: "center", 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>
</Box> </Box>
); );
}; };

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

@ -92,7 +92,7 @@ export const Varimg = ({ currentQuestion }: VarimgProps) => {
await sendAnswer({ await sendAnswer({
questionId: currentQuestion.id, 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 qid: settings.qid
}) })

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

@ -3,10 +3,11 @@ import { produce } from "immer";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { create } from "zustand"; import { create } from "zustand";
import { devtools } from "zustand/middleware"; import { devtools } from "zustand/middleware";
import type { Moment } from "moment";
type Answer = { type Answer = {
questionId: string; questionId: string;
answer: string | string[]; answer: string | string[] | Moment;
}; };
type OwnVariant = { type OwnVariant = {
@ -40,7 +41,10 @@ function setProducedState<A extends string | { type: string; }>(
useQuizViewStore.setState(state => produce(state, recipe), false, action); 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); const index = state.answers.findIndex(answer => questionId === answer.questionId);
if (index < 0) { 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: dependencies:
assert-plus "^1.0.0" assert-plus "^1.0.0"
dayjs@^1.10.4, dayjs@^1.11.10: dayjs@^1.10.4:
version "1.11.10" version "1.11.10"
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.10.tgz#68acea85317a6e164457d6d6947564029a6a16a0" resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.10.tgz#68acea85317a6e164457d6d6947564029a6a16a0"
integrity sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ== 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" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== 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: ms@2.1.2:
version "2.1.2" version "2.1.2"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"