diff --git a/.gitignore b/.gitignore index e698ff5..a302ba2 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,4 @@ widget *.ntvs* *.njsproj *.sln -*.sw? +*.sw? \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 72a3692..2355dbd 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -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 diff --git a/Dockerfile b/Dockerfile index 9a55e55..6426786 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/deployments/main/docker-compose.yaml b/deployments/main/docker-compose.yaml new file mode 100644 index 0000000..8dacb5e --- /dev/null +++ b/deployments/main/docker-compose.yaml @@ -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 + diff --git a/deployments/staging/docker-compose.yaml b/deployments/staging/docker-compose.yaml index ee06d99..d258d2e 100644 --- a/deployments/staging/docker-compose.yaml +++ b/deployments/staging/docker-compose.yaml @@ -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 diff --git a/index.html b/index.html index e4b78ea..779a0a3 100644 --- a/index.html +++ b/index.html @@ -2,9 +2,12 @@ - + + + + - Vite + React + TS + Quiz
diff --git a/package.json b/package.json index bef10bc..c46c491 100755 --- a/package.json +++ b/package.json @@ -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", diff --git a/pub.js b/pub.js new file mode 100644 index 0000000..f192725 --- /dev/null +++ b/pub.js @@ -0,0 +1 @@ +console.log("PEHA NUB") diff --git a/src/App.tsx b/src/App.tsx index 939a658..e31e57d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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, }}> - + - + \ No newline at end of file diff --git a/src/assets/icons/Info.tsx b/src/assets/icons/Info.tsx index d2ef527..b56f67f 100644 --- a/src/assets/icons/Info.tsx +++ b/src/assets/icons/Info.tsx @@ -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 ( diff --git a/src/main.tsx b/src/main.tsx index e929f0d..e7b603b 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,4 +1,3 @@ -import "dayjs/locale/ru"; import { createRoot } from "react-dom/client"; import App from "./App"; diff --git a/src/model/questionTypes/file.ts b/src/model/questionTypes/file.ts index 51a748a..1346635 100644 --- a/src/model/questionTypes/file.ts +++ b/src/model/questionTypes/file.ts @@ -5,7 +5,6 @@ import type { } from "./shared"; export const UPLOAD_FILE_TYPES_MAP = { - all: "Все типы файлов", picture: "Изображения", video: "Видео", audio: "Аудио", diff --git a/src/model/settingsData.ts b/src/model/settingsData.ts index c7009b2..29d7fb1 100644 --- a/src/model/settingsData.ts +++ b/src/model/settingsData.ts @@ -87,6 +87,7 @@ export interface QuizConfig { text: FCField; address: FCField; button: string; + fields: Record; }; 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; diff --git a/src/pages/ViewPublicationPage/ContactForm.tsx b/src/pages/ViewPublicationPage/ContactForm.tsx index ec3e9da..572b991 100644 --- a/src/pages/ViewPublicationPage/ContactForm.tsx +++ b/src/pages/ViewPublicationPage/ContactForm.tsx @@ -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; // 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> = {}; - + //@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 + if (!resultQuestion) + return ( + + ); return ( - {settings?.cfg.formContact.title || "Заполните форму, чтобы получить результаты теста"} - + {settings?.cfg.formContact.title || + "Заполните форму, чтобы получить результаты теста"} - { - settings?.cfg.formContact.desc && + {settings?.cfg.formContact.desc && ( {settings?.cfg.formContact.desc} - } + )} - - + p: "30px", + }} + > - - { // resultQuestion && - // settings?.cfg.resultInfo.when === "after" && - ( - - )} + setFire(false); + } else { + enqueueSnackbar("введена некорректная почта"); + } + }} + > + {settings?.cfg.formContact?.button || "Получить результаты"} + + } - { setReady(target.checked) }} checked={ready} colorIcon={theme.palette.primary.main} /> + { + setReady(target.checked); + }} + checked={ready} + colorIcon={theme.palette.primary.main} + /> С  - Положением об обработке персональных данных + Положением об обработке персональных данных{" "} +  и  - Политикой конфиденциальности + + {" "} + Политикой конфиденциальности{" "} +  ознакомлен - - - + + Сделано на PenaQuiz - - + + ); }; 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 = ( + setName(target.value)} + id={name} + title={FC["name"].innerText || "Введите имя"} + desc={FC["name"].text || "имя"} + Icon={NameIcon} + /> + ); //@ts-ignore - const Name = setName(target.value)} id={name} title={FC["name"].innerText || "Введите имя"} desc={FC["name"].text || "имя"} Icon={NameIcon} /> + const Email = ( + setEmail(target.value)} + id={email} + title={FC["email"].innerText || "Введите Email"} + desc={FC["email"].text || "Email"} + Icon={EmailIcon} + /> + ); + const Phone = ( + setPhone(target.value)} + id={phone} + title={FC["phone"].innerText || "Введите номер телефона"} + desc={FC["phone"].text || "номер телефона"} + Icon={PhoneIcon} + /> + ); //@ts-ignore - const Email = setEmail(target.value)} id={email} title={FC["email"].innerText || "Введите Email"} desc={FC["email"].text || "Email"} Icon={EmailIcon} /> + const Text = ( + setText(target.value)} + id={text} + title={FC["text"].text || "Введите фамилию"} + desc={FC["text"].innerText || "фамилию"} + Icon={TextIcon} + /> + ); //@ts-ignore - const Phone = setPhone(target.value)} id={phone} title={FC["phone"].innerText || "Введите номер телефона"} desc={FC["phone"].text || "номер телефона"} Icon={PhoneIcon} /> - //@ts-ignore - const Text = setText(target.value)} id={text} title={FC["text"].innerText || "Введите фамилию"} desc={FC["text"].text || "фамилию"} Icon={TextIcon} /> - //@ts-ignore - const Adress = setAdress(target.value)} id={adress} title={FC["address"].innerText || "Введите адрес"} desc={FC["address"].text || "адрес"} Icon={AddressIcon} /> + const Adress = ( + 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 ( - {title} + + {title} + { }} placeholder={desc} InputProps={{ - startAdornment: , + startAdornment: ( + + + + ), }} /> - ) -} + ); +}; diff --git a/src/pages/ViewPublicationPage/index.tsx b/src/pages/ViewPublicationPage/index.tsx index 6c9cc9b..47e9727 100644 --- a/src/pages/ViewPublicationPage/index.tsx +++ b/src/pages/ViewPublicationPage/index.tsx @@ -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]); diff --git a/src/pages/ViewPublicationPage/questions/Date.tsx b/src/pages/ViewPublicationPage/questions/Date.tsx index 1d457cd..6f80cd2 100644 --- a/src/pages/ViewPublicationPage/questions/Date.tsx +++ b/src/pages/ViewPublicationPage/questions/Date.tsx @@ -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("ответ не был засчитан"); diff --git a/src/pages/ViewPublicationPage/questions/Emoji.tsx b/src/pages/ViewPublicationPage/questions/Emoji.tsx index ff7ae23..dd8f12b 100644 --- a/src/pages/ViewPublicationPage/questions/Emoji.tsx +++ b/src/pages/ViewPublicationPage/questions/Emoji.tsx @@ -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 }) diff --git a/src/pages/ViewPublicationPage/questions/File.tsx b/src/pages/ViewPublicationPage/questions/File.tsx index 5f059de..cc4a53d 100644 --- a/src/pages/ViewPublicationPage/questions/File.tsx +++ b/src/pages/ViewPublicationPage/questions/File.tsx @@ -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 (<> + Выбран некорректный тип файла + ) + case 'errorSize': + return (<> + Файл слишком большой. Максимальный размер 50 МБ + ) + default: + return (<> + Допустимые расширения файлов: + { + //@ts-ignore + ACCEPT_SEND_FILE_TYPES_MAP[status].join(" ")} + ) + } +} + +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) => { 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 ( - - {currentQuestion.title} - - {answer?.split("|")[0] && ( - - Вы загрузили: - - - {answer?.split("|")[0]} - { - updateAnswer(currentQuestion.id, ""); + <> + + {currentQuestion.title} + + {answer?.split("|")[0] && ( + + Вы загрузили: + - - - - - )} - - {!answer?.split("|")[0] && ( - - - ) => - 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", - }} - > - - - { - UPLOAD_FILE_DESCRIPTIONS_MAP[currentQuestion.content.type] - .title - } - - + { + updateAnswer(currentQuestion.id, ""); }} > - { - UPLOAD_FILE_DESCRIPTIONS_MAP[currentQuestion.content.type] - .description - } - + + - - )} - {answer && currentQuestion.content.type === "picture" && ( - - )} - {answer && currentQuestion.content.type === "video" && ( - - + setStatusModal("")} + > + + + + + ); }; diff --git a/src/pages/ViewPublicationPage/questions/Images.tsx b/src/pages/ViewPublicationPage/questions/Images.tsx index e962955..cb7530a 100644 --- a/src/pages/ViewPublicationPage/questions/Images.tsx +++ b/src/pages/ViewPublicationPage/questions/Images.tsx @@ -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} `, qid: settings.qid }) diff --git a/src/pages/ViewPublicationPage/questions/Number.tsx b/src/pages/ViewPublicationPage/questions/Number.tsx index 5760c03..e8d1025 100644 --- a/src/pages/ViewPublicationPage/questions/Number.tsx +++ b/src/pages/ViewPublicationPage/questions/Number.tsx @@ -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("0"); const [minRange, setMinRange] = useState("0"); const [maxRange, setMaxRange] = useState("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 ( - {currentQuestion.title} + + {currentQuestion.title} + { width: "100%", marginTop: "20px", gap: "30px", - paddingRight: isMobile ? "10px" : undefined + paddingRight: isMobile ? "10px" : undefined, }} > { 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 && ( { - 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) => { ); - }; diff --git a/src/pages/ViewPublicationPage/questions/Rating.tsx b/src/pages/ViewPublicationPage/questions/Rating.tsx index e45c023..b22562b 100644 --- a/src/pages/ViewPublicationPage/questions/Rating.tsx +++ b/src/pages/ViewPublicationPage/questions/Rating.tsx @@ -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 }) diff --git a/src/pages/ViewPublicationPage/questions/Varimg.tsx b/src/pages/ViewPublicationPage/questions/Varimg.tsx index bf5eb1a..57eb2b5 100644 --- a/src/pages/ViewPublicationPage/questions/Varimg.tsx +++ b/src/pages/ViewPublicationPage/questions/Varimg.tsx @@ -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} `, qid: settings.qid }) diff --git a/src/pages/ViewPublicationPage/tools/File.tsx b/src/pages/ViewPublicationPage/tools/File.tsx index e92bede..cc57a0e 100644 --- a/src/pages/ViewPublicationPage/tools/File.tsx +++ b/src/pages/ViewPublicationPage/tools/File.tsx @@ -7,7 +7,6 @@ import type { } from "model/questionTypes/file"; export const UPLOAD_FILE_TYPES_MAP: Record = { - all: "file", picture: "image/*", video: "video/*", audio: "audio/*", @@ -23,7 +22,7 @@ export default function File({ question }: Props) { const fileInputRef = useRef(null); const [file, setFile] = useState(null); const [acceptedType, setAcceptedType] = useState( - UPLOAD_FILE_TYPES_MAP.all + UPLOAD_FILE_TYPES_MAP.picture ); useEffect(() => { diff --git a/src/stores/quizView/store.ts b/src/stores/quizView/store.ts index 919b35e..769f717 100644 --- a/src/stores/quizView/store.ts +++ b/src/stores/quizView/store.ts @@ -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( 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) { diff --git a/src/ui_kit/LabeledDatePicker.tsx b/src/ui_kit/LabeledDatePicker.tsx new file mode 100644 index 0000000..ee8218c --- /dev/null +++ b/src/ui_kit/LabeledDatePicker.tsx @@ -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; + 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 ( + + {label && ( + + {label} + + )} + , + }} + 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", + }, + }, + }} + /> + + ); +} diff --git a/src/utils/hooks/useGetSettings.ts b/src/utils/hooks/useGetSettings.ts new file mode 100644 index 0000000..822cd40 --- /dev/null +++ b/src/utils/hooks/useGetSettings.ts @@ -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 +} diff --git a/yarn.lock b/yarn.lock index 37778bc..bb372ad 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"