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" &&
- (
-
+ }
- { 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" && (
-
- )}
+ )}
+
+ {!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
+ }
+
+
+ {
+ UPLOAD_FILE_DESCRIPTIONS_MAP[currentQuestion.content.type]
+ .description
+ }
+
+
+
+
+ setStatusModal(currentQuestion.content.type)} />
+
+ )}
+ {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"