обратная связь с плохой вёрсткой

This commit is contained in:
Nastya 2023-10-07 09:10:50 +03:00
parent a739bf37f2
commit 89945bae9e
10 changed files with 733 additions and 197 deletions

56
package-lock.json generated

@ -22,6 +22,7 @@
"@types/react": "^18.0.0",
"@types/react-dnd": "^3.0.2",
"@types/react-dom": "^18.0.0",
"axios": "^1.5.1",
"emoji-mart": "^5.5.2",
"file-saver": "^2.0.5",
"html-to-image": "^1.11.11",
@ -5391,6 +5392,29 @@
"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": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz",
@ -14593,6 +14617,11 @@
"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": {
"version": "1.9.0",
"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",
"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": {
"version": "2.2.0",
"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": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",

@ -17,6 +17,7 @@
"@types/react": "^18.0.0",
"@types/react-dnd": "^3.0.2",
"@types/react-dom": "^18.0.0",
"axios": "^1.5.1",
"emoji-mart": "^5.5.2",
"file-saver": "^2.0.5",
"html-to-image": "^1.11.11",

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

@ -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 { Setting } from "./pages/Result/Setting";
import MyQuizzesFull from "./pages/createQuize/MyQuizzesFull";
import ContactFormModal from "@ui_kit/ContactForm";
import ImageCrop from "@ui_kit/Modal/ImageCrop";
import Landing from "./pages/Landing/Landing";
@ -37,6 +38,7 @@ const root = createRoot(document.getElementById("root")!);
root.render(
<DndProvider backend={HTML5Backend}>
<ThemeProvider theme={lightTheme}>
<ContactFormModal />
<BrowserRouter>
<Routes>
{routeslink.map((e, i) => (

@ -7,7 +7,7 @@ import abstraction from '../../assets/Quiz-main.png'
import SectionStyled from './SectionStyled';
import {styled} from "@mui/material/styles";
import { Link, redirect } from 'react-router-dom';
import { setIsContactFormOpen } from "../../stores/contactForm";
export default function Component() {
return(
@ -48,6 +48,7 @@ export default function Component() {
</Box>
<Link to="/list" style={{textDecoration: "none"}}>
<Button variant="contained"
onClick={() => setIsContactFormOpen(true)}
>
Попробуйте бесплатно

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

@ -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>
);
};

@ -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;
}

605
yarn.lock

File diff suppressed because it is too large Load Diff