обратная связь с плохой вёрсткой
This commit is contained in:
parent
a739bf37f2
commit
89945bae9e
56
package-lock.json
generated
56
package-lock.json
generated
@ -22,6 +22,7 @@
|
|||||||
"@types/react": "^18.0.0",
|
"@types/react": "^18.0.0",
|
||||||
"@types/react-dnd": "^3.0.2",
|
"@types/react-dnd": "^3.0.2",
|
||||||
"@types/react-dom": "^18.0.0",
|
"@types/react-dom": "^18.0.0",
|
||||||
|
"axios": "^1.5.1",
|
||||||
"emoji-mart": "^5.5.2",
|
"emoji-mart": "^5.5.2",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"html-to-image": "^1.11.11",
|
"html-to-image": "^1.11.11",
|
||||||
@ -5391,6 +5392,29 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/axios": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.5.1.tgz",
|
||||||
|
"integrity": "sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A==",
|
||||||
|
"dependencies": {
|
||||||
|
"follow-redirects": "^1.15.0",
|
||||||
|
"form-data": "^4.0.0",
|
||||||
|
"proxy-from-env": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/axios/node_modules/form-data": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||||
|
"dependencies": {
|
||||||
|
"asynckit": "^0.4.0",
|
||||||
|
"combined-stream": "^1.0.8",
|
||||||
|
"mime-types": "^2.1.12"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/axobject-query": {
|
"node_modules/axobject-query": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz",
|
||||||
@ -14593,6 +14617,11 @@
|
|||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/proxy-from-env": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
|
||||||
|
},
|
||||||
"node_modules/psl": {
|
"node_modules/psl": {
|
||||||
"version": "1.9.0",
|
"version": "1.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
|
||||||
@ -21789,6 +21818,28 @@
|
|||||||
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.5.2.tgz",
|
||||||
"integrity": "sha512-u2MVsXfew5HBvjsczCv+xlwdNnB1oQR9HlAcsejZttNjKKSkeDNVwB1vMThIUIFI9GoT57Vtk8iQLwqOfAkboA=="
|
"integrity": "sha512-u2MVsXfew5HBvjsczCv+xlwdNnB1oQR9HlAcsejZttNjKKSkeDNVwB1vMThIUIFI9GoT57Vtk8iQLwqOfAkboA=="
|
||||||
},
|
},
|
||||||
|
"axios": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.5.1.tgz",
|
||||||
|
"integrity": "sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A==",
|
||||||
|
"requires": {
|
||||||
|
"follow-redirects": "^1.15.0",
|
||||||
|
"form-data": "^4.0.0",
|
||||||
|
"proxy-from-env": "^1.1.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"form-data": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||||
|
"requires": {
|
||||||
|
"asynckit": "^0.4.0",
|
||||||
|
"combined-stream": "^1.0.8",
|
||||||
|
"mime-types": "^2.1.12"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"axobject-query": {
|
"axobject-query": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz",
|
||||||
@ -28318,6 +28369,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"proxy-from-env": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
|
||||||
|
},
|
||||||
"psl": {
|
"psl": {
|
||||||
"version": "1.9.0",
|
"version": "1.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
"@types/react": "^18.0.0",
|
"@types/react": "^18.0.0",
|
||||||
"@types/react-dnd": "^3.0.2",
|
"@types/react-dnd": "^3.0.2",
|
||||||
"@types/react-dom": "^18.0.0",
|
"@types/react-dom": "^18.0.0",
|
||||||
|
"axios": "^1.5.1",
|
||||||
"emoji-mart": "^5.5.2",
|
"emoji-mart": "^5.5.2",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"html-to-image": "^1.11.11",
|
"html-to-image": "^1.11.11",
|
||||||
|
18
src/api/contactForm.ts
Normal file
18
src/api/contactForm.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
|
||||||
|
const apiUrl = "https://admin.pena.digital/feedback";
|
||||||
|
|
||||||
|
export function sendContactFormRequest(body: {
|
||||||
|
|
||||||
|
contact: string;
|
||||||
|
whoami: string;
|
||||||
|
}) {
|
||||||
|
return axios(apiUrl + "/callme", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
data: body,
|
||||||
|
});
|
||||||
|
}
|
15
src/assets/icons/x.tsx
Normal file
15
src/assets/icons/x.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { useTheme } from "@mui/material";
|
||||||
|
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
width?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PenaLogo({ width }: Props) {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M2.39844 2.39999L21.5984 21.6M2.39844 21.6L21.5984 2.39999" stroke="black"/>
|
||||||
|
</svg>
|
||||||
|
)}
|
@ -15,6 +15,7 @@ import InstallQuiz from "./pages/InstallQuiz/InstallQuiz";
|
|||||||
import { Result } from "./pages/Result/Result";
|
import { Result } from "./pages/Result/Result";
|
||||||
import { Setting } from "./pages/Result/Setting";
|
import { Setting } from "./pages/Result/Setting";
|
||||||
import MyQuizzesFull from "./pages/createQuize/MyQuizzesFull";
|
import MyQuizzesFull from "./pages/createQuize/MyQuizzesFull";
|
||||||
|
import ContactFormModal from "@ui_kit/ContactForm";
|
||||||
import ImageCrop from "@ui_kit/Modal/ImageCrop";
|
import ImageCrop from "@ui_kit/Modal/ImageCrop";
|
||||||
import Landing from "./pages/Landing/Landing";
|
import Landing from "./pages/Landing/Landing";
|
||||||
|
|
||||||
@ -37,6 +38,7 @@ const root = createRoot(document.getElementById("root")!);
|
|||||||
root.render(
|
root.render(
|
||||||
<DndProvider backend={HTML5Backend}>
|
<DndProvider backend={HTML5Backend}>
|
||||||
<ThemeProvider theme={lightTheme}>
|
<ThemeProvider theme={lightTheme}>
|
||||||
|
<ContactFormModal />
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Routes>
|
<Routes>
|
||||||
{routeslink.map((e, i) => (
|
{routeslink.map((e, i) => (
|
||||||
|
@ -7,7 +7,7 @@ import abstraction from '../../assets/Quiz-main.png'
|
|||||||
import SectionStyled from './SectionStyled';
|
import SectionStyled from './SectionStyled';
|
||||||
import {styled} from "@mui/material/styles";
|
import {styled} from "@mui/material/styles";
|
||||||
import { Link, redirect } from 'react-router-dom';
|
import { Link, redirect } from 'react-router-dom';
|
||||||
|
import { setIsContactFormOpen } from "../../stores/contactForm";
|
||||||
|
|
||||||
export default function Component() {
|
export default function Component() {
|
||||||
return(
|
return(
|
||||||
@ -48,6 +48,7 @@ export default function Component() {
|
|||||||
</Box>
|
</Box>
|
||||||
<Link to="/list" style={{textDecoration: "none"}}>
|
<Link to="/list" style={{textDecoration: "none"}}>
|
||||||
<Button variant="contained"
|
<Button variant="contained"
|
||||||
|
onClick={() => setIsContactFormOpen(true)}
|
||||||
>
|
>
|
||||||
Попробуйте бесплатно
|
Попробуйте бесплатно
|
||||||
|
|
||||||
|
63
src/stores/contactForm.ts
Normal file
63
src/stores/contactForm.ts
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import { create } from "zustand";
|
||||||
|
import { sendContactFormRequest } from "../api/contactForm";
|
||||||
|
import { getMessageFromFetchError } from "../utils/backendMessageHandler";
|
||||||
|
|
||||||
|
|
||||||
|
interface ContactFormStore {
|
||||||
|
isModalOpen: boolean;
|
||||||
|
isSubmitDisabled: boolean;
|
||||||
|
mail: string;
|
||||||
|
phoneNumberField: string;
|
||||||
|
telegramField: string;
|
||||||
|
whatsappField: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: ContactFormStore = {
|
||||||
|
isModalOpen: false,
|
||||||
|
isSubmitDisabled: false,
|
||||||
|
mail: "",
|
||||||
|
phoneNumberField: "",
|
||||||
|
telegramField: "",
|
||||||
|
whatsappField: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useContactFormStore = create<ContactFormStore>()(
|
||||||
|
(set, get) => initialState
|
||||||
|
);
|
||||||
|
|
||||||
|
export const setIsContactFormOpen = (isModalOpen: ContactFormStore["isModalOpen"]) => useContactFormStore.setState({ isModalOpen });
|
||||||
|
export const setContactFormMailField = (mail: ContactFormStore["mail"]) => useContactFormStore.setState({ mail });
|
||||||
|
export const setContactFormTelegramField = (telegramField: ContactFormStore["telegramField"]) => useContactFormStore.setState({ telegramField });
|
||||||
|
export const setIsSubmitDisabled = (isSubmitDisabled: ContactFormStore["isSubmitDisabled"]) => useContactFormStore.setState({ isSubmitDisabled });
|
||||||
|
export const setContactFormPhoneNumberField = (phoneNumberField: ContactFormStore["phoneNumberField"]) => {
|
||||||
|
if (/^\+?\d*$/.test(phoneNumberField)) useContactFormStore.setState({ phoneNumberField });
|
||||||
|
};
|
||||||
|
export const setContactFormWhatsappField = (whatsappField: ContactFormStore["whatsappField"]) => {
|
||||||
|
if (/^\+?\d*$/.test(whatsappField)) useContactFormStore.setState({ whatsappField });
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sendContactForm = async (): Promise<string | null> => {
|
||||||
|
const { mail, phoneNumberField, telegramField, whatsappField } = useContactFormStore.getState();
|
||||||
|
|
||||||
|
if (!mail) return "Почта не указана";
|
||||||
|
let contact = `phonenumber:${phoneNumberField}`;
|
||||||
|
if (telegramField) contact += `\ntelegram:${telegramField}`;
|
||||||
|
if (whatsappField) contact += `\nwhatsapp:${whatsappField}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
useContactFormStore.setState({ isSubmitDisabled: true });
|
||||||
|
const response = await sendContactFormRequest({
|
||||||
|
contact,
|
||||||
|
whoami: mail,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status !== 200) throw new Error(response.statusText);
|
||||||
|
|
||||||
|
useContactFormStore.setState({ ...initialState });
|
||||||
|
|
||||||
|
return "Данные отправлены";
|
||||||
|
} catch (error: any) {
|
||||||
|
useContactFormStore.setState({ isSubmitDisabled: false });
|
||||||
|
return getMessageFromFetchError(error);
|
||||||
|
}
|
||||||
|
};
|
132
src/ui_kit/ContactForm.tsx
Normal file
132
src/ui_kit/ContactForm.tsx
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
import { Box, Button, IconButton, TextField, Typography, useMediaQuery, useTheme, Modal } from "@mui/material";
|
||||||
|
import {
|
||||||
|
sendContactForm,
|
||||||
|
setContactFormMailField,
|
||||||
|
setIsContactFormOpen,
|
||||||
|
useContactFormStore,
|
||||||
|
} from "../stores/contactForm";
|
||||||
|
import { enqueueSnackbar } from "notistack";
|
||||||
|
import X from "../assets/icons/x";
|
||||||
|
import PenaLogo from "../ui_kit/PenaLogo";
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const isMobile = useMediaQuery(theme.breakpoints.down("md"));
|
||||||
|
|
||||||
|
const isModalOpen = useContactFormStore((state) => state.isModalOpen);
|
||||||
|
const isSubmitDisabled = useContactFormStore((state) => state.isSubmitDisabled);
|
||||||
|
const mail = useContactFormStore((state) => state.mail);
|
||||||
|
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||||
|
|
||||||
|
async function handleSendForm() {
|
||||||
|
const message = await sendContactForm();
|
||||||
|
|
||||||
|
if (message) enqueueSnackbar(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
disableScrollLock
|
||||||
|
open={isModalOpen}
|
||||||
|
onClose={() => setIsContactFormOpen(false)}
|
||||||
|
aria-labelledby="modal-modal-title"
|
||||||
|
aria-describedby="modal-modal-description"
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
component="form"
|
||||||
|
noValidate
|
||||||
|
sx={{
|
||||||
|
width: "600px",
|
||||||
|
height: "474px",
|
||||||
|
margin: "200px auto 0",
|
||||||
|
position: "relative",
|
||||||
|
backgroundColor: "white",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
flexDirection: "column",
|
||||||
|
p: upMd ? "50px" : "18px",
|
||||||
|
pb: upMd ? "40px" : "30px",
|
||||||
|
gap: "15px",
|
||||||
|
borderRadius: "12px",
|
||||||
|
"& .MuiFormHelperText-root.Mui-error, & .MuiFormHelperText-root.Mui-error.MuiFormHelperText-filled":
|
||||||
|
{
|
||||||
|
position: "absolute",
|
||||||
|
top: "46px",
|
||||||
|
margin: "0",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
onClick={() => setIsContactFormOpen(false)}
|
||||||
|
sx={{
|
||||||
|
position: "absolute",
|
||||||
|
right: "7px",
|
||||||
|
top: "7px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<X/>
|
||||||
|
</IconButton>
|
||||||
|
<Box>
|
||||||
|
<PenaLogo width={upMd ? 233 : 196} />
|
||||||
|
</Box>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
mt: "5px",
|
||||||
|
mb: upMd ? "10px" : "33px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Предрегистрация
|
||||||
|
</Typography>
|
||||||
|
<TextField
|
||||||
|
value={mail}
|
||||||
|
onChange={(e) => setContactFormMailField(e.target.value)}
|
||||||
|
placeholder="Имя"
|
||||||
|
name="name"
|
||||||
|
fullWidth
|
||||||
|
sx={{
|
||||||
|
width: isMobile ? "266px" : "300px",
|
||||||
|
mt: "25px",
|
||||||
|
mb: "10px",
|
||||||
|
"& .MuiInputBase-root": {
|
||||||
|
backgroundColor: "#F2F3F7",
|
||||||
|
height: "45px",
|
||||||
|
borderRadius: "10px",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
inputProps={{
|
||||||
|
sx: {
|
||||||
|
borderRadius: "10px",
|
||||||
|
fontSize: "18px",
|
||||||
|
lineHeight: "21px",
|
||||||
|
py: 0,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
onClick={handleSendForm}
|
||||||
|
sx={{
|
||||||
|
py: "12px",
|
||||||
|
"&:hover": {
|
||||||
|
},
|
||||||
|
"&:active": {
|
||||||
|
color: "white",
|
||||||
|
backgroundColor: "black",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Отправить
|
||||||
|
</Button>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
mt: "5px",
|
||||||
|
mb: upMd ? "10px" : "33px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
После запуска продукта вам придет сообщение
|
||||||
|
на указанную электронную почту
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
35
src/utils/backendMessageHandler.ts
Normal file
35
src/utils/backendMessageHandler.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { isAxiosError } from "axios";
|
||||||
|
|
||||||
|
const backendErrorMessage: Record<string, string> = {
|
||||||
|
"user not found": "Пользователь не найден",
|
||||||
|
"invalid password": "Неправильный пароль",
|
||||||
|
"field <password> is empty": 'Поле "Пароль" не заполнено',
|
||||||
|
"field <login> is empty": 'Поле "Логин" не заполнено',
|
||||||
|
"field <email> is empty": 'Поле "E-mail" не заполнено',
|
||||||
|
"field <phoneNumber> is empty": 'Поле "Номер телефона" не заполнено',
|
||||||
|
"user with this email or login is exist": "Пользователь уже существует",
|
||||||
|
};
|
||||||
|
|
||||||
|
const unknownErrorMessage = "Что-то пошло не так. Повторите попытку позже";
|
||||||
|
|
||||||
|
export function getMessageFromFetchError(error: any): string | null {
|
||||||
|
if (process.env.NODE_ENV !== "production") console.log(error);
|
||||||
|
|
||||||
|
const message = backendErrorMessage[error.response?.data?.message];
|
||||||
|
if (message) return message;
|
||||||
|
|
||||||
|
if (error.message === "Failed to fetch") return "Ошибка сети";
|
||||||
|
|
||||||
|
if (isAxiosError(error)) {
|
||||||
|
switch (error.code) {
|
||||||
|
case "ERR_NETWORK":
|
||||||
|
return "Ошибка сети";
|
||||||
|
case "ERR_CANCELED":
|
||||||
|
return null;
|
||||||
|
case "ERR_BAD_REQUEST":
|
||||||
|
return "Слишком много запросов";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return unknownErrorMessage;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user