@@ -0,0 +1,6 @@ + + + + \ No newline at end of file @@ -150,7 +150,7 @@ background-color: rgba(0, 0, 0, 0.7); border-radius: 15px; padding: 20px; - z-index: 10000; + z-index: 1000; &:hover { outline: 4px solid #8280ff66; outline-offset: -3px; @@ -148,7 +148,7 @@ background-color: rgba(0, 0, 0, 0.7); border-radius: 15px; padding: 20px; - z-index: 10000; + z-index: 1000; &:hover { outline: 4px solid #8280ff66; outline-offset: -3px; @@ -0,0 +1,17 @@ +import { api } from '#/shared/api' + +interface SendReport { + errorMessage: string + email: string + file?: File +} + +export async function sendReport({errorMessage, email, file}: SendReport) { + let formData = new FormData() + + formData.append('report_text', `${email}: ${errorMessage}`) + if (file) { + formData.append('images', file) + } + return await api.post('/reports/', formData) +} @@ -0,0 +1 @@ +export * from './error-report.api' \ No newline at end of file @@ -0,0 +1,64 @@ +import { useAppSelector } from '#/app/store/store' +import { API_URL } from '#/shared/lib/constants' +import axios from 'axios' +import { useSession } from 'next-auth/react' +import React, { ChangeEvent } from 'react' +import { sendReport } from '../api' +import { makePrivateRequest } from '#/shared/api' +import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' + +export const useErrorReport = () => { + + const { showMessage } = useShowDataStore() + const [errorMessage, setErrorMessage] = React.useState('') + + const [error, setError] = React.useState(false) + + const [isSend, setIsSend] = React.useState(false) + + const [file, setFile] = React.useState() + + const { data: session } = useSession() + + const email = useAppSelector((state) => state.user.email) + + const handleFileChange = (e: ChangeEvent) => { + if (e.target.files) { + if (4500000 > e.target.files[0].size) setFile(e.target.files[0]) + } + } + + const sendError = makePrivateRequest(async () => { + if (errorMessage.trim() === '') { + showMessage("Поле не должно быть пустым.") + setError(true) + setTimeout(() => setError(false), 3000) + return + } + + try { + const { status } = await sendReport({ errorMessage, email, file }) + + if (status === 201) { + setErrorMessage('') + setIsSend(true) + setTimeout(() => { + setIsSend(false) + }, 4000) + } + } catch (err) { + setError(true) + setTimeout(() => setError(false), 3000) + } + }) + + return { + isSend, + setErrorMessage, + handleFileChange, + file, + setFile, + sendError, + error, + } +} @@ -0,0 +1,78 @@ +.container { + width: 589px; + height: auto; + background-color: var(--background-color-additional); + padding: 32px; + border-radius: 20px; + + box-shadow: rgba(0, 0, 0, 0.2) 0px 8px 10px -5px, rgba(0, 0, 0, 0.14) 0px 16px 24px 2px, rgba(0, 0, 0, 0.12) 0px 6px 30px 5px; + + &_sended { + display: flex; + gap: 16px; + flex-direction: column; + width: 300px; + text-align: center; + } + + &__buttons { + display: flex; + justify-content: flex-end; + flex-direction: row; + align-items: flex-end; + margin-top: 12px; + gap: 8px; + } + + &__close { + transform: rotate(45deg); + margin-left: 8px; + color: var(--new-ui-gray-color); + } +} + +.text { + color: #868686; + font-size: 17px; + line-height: 1.5; + + &_accent { + color: #22b47f; + font-size: 23px; + } + + &_heading { + font-size: 21px; + font-weight: 500; + } + + &_file { + font-size: 15px; + } +} + +.textarea { + position: relative; + margin: 20px 0px 9.6px 0px; + + &__button { + position: absolute; + padding: 0; + bottom: 16px; + left: 8px; + background-color: transparent; + box-shadow: none; + margin: 0; + rotate: 30deg; + + &:hover { + background-color: transparent; + box-shadow: none; + } + } + + &__file { + display: flex; + align-items: center; + } +} @@ -0,0 +1,92 @@ +import '@testing-library/jest-dom' +import { act, fireEvent, screen, waitFor } from '@testing-library/react' +import { jestRender } from '#/../jest/utils/render' +import { windowMock } from '#/../jest/utils/window-mock' +import { ERROR_REPORT, PLATE_CHANGE_PASSWORD } from '#/features/modals' +import { getModalTest } from '#/../jest/utils/modal-tester' +import { api } from '#/shared/api/instance' +import { ErrorReportPlate } from './error-report' +import axios from 'axios' +import { sendReport } from '../api' + +jest.mock('next/router') +jest.mock('@sentry/nextjs') + +jest.mock('../api') + +const showMessageMock = jest.fn() +const useSessionMock = jest.fn() + +jest.mock('next-auth/react', () => { + return { + useSession: () => useSessionMock(), + } +}) + +jest.mock('#/shared/lib/hooks/use-show-data', () => { + return { + useShowDataStore: jest.fn(() => { + return { + showMessage: showMessageMock, + } + }), + } +}) + +useSessionMock.mockReturnValue({ + data: { + access: 'something access', + }, +}) + +windowMock() + +describe('error-report', () => { + it('successfully show modal', async () => { + jestRender() + + const modal = await getModalTest(ERROR_REPORT) + + act(() => { + modal.setState(true) + }) + ;(sendReport as jest.Mock).mockResolvedValue({ + status: 201, + data: { detail: 'error report sent' }, + }) + + const textarea = screen.getByPlaceholderText('Опишите детально возникшую проблему') + const button = screen.getByText('Отправить') + + fireEvent.change(textarea, { target: { value: 'О нет все пропало' } }) + + act(() => { + fireEvent.click(button) + }) + + await waitFor(() => { + expect(sendReport).toHaveBeenCalledWith({ errorMessage: 'О нет все пропало', email: '' }) + }) + }) + + it('empty textarea', async () => { + jestRender() + + const modal = await getModalTest(ERROR_REPORT) + + act(() => { + modal.setState(true) + }) + + const textarea = screen.getByPlaceholderText('Опишите детально возникшую проблему') + const button = screen.getByText('Отправить') + + act(() => { + fireEvent.click(button) + }) + + await waitFor(() => { + expect(showMessageMock).toHaveBeenCalledWith("Поле не должно быть пустым.") + }) + }) +}) @@ -0,0 +1,73 @@ +import * as React from 'react' +import AttachSvg from '#/assets/svg/attach.svg?react' +import PlusSvg from '#/assets/svg/plus.svg?react' +import { ERROR_REPORT, getModalById, PlateTemplate } from '#/features/modals' + +import styles from './error-report.module.scss' +import { c } from '#/shared' +import { CommonTextArea } from '#/shared/ui/common-textarea' +import { CommonButton } from '#/shared/ui/button' +import { useErrorReport } from '../model/use-error-report' + +export const ErrorReportPlate = () => { + const modal = getModalById(ERROR_REPORT) + + const { error, isSend, setErrorMessage, handleFileChange, file, setFile, sendError } = useErrorReport() + + return ( + + {isSend ? ( + + Спасибо! + Ваше сообщение направлено нашим специалистам. Ответ придет на почту, указанную при регистрации + + ) : ( + e.stopPropagation()}> + Сообщить об ошибке + setErrorMessage(evt.target.value)} + buttomSlot={ + <> + + + + + + + > + } + /> + {file && ( + + Добавлено 1 изображение: {file.name} + + setFile(undefined)}> + + + + )} + + + modal.setState(false)}> + Отмена + + + Отправить + + + + )} + + ) +} @@ -0,0 +1 @@ +export * from './error-report' \ No newline at end of file @@ -0,0 +1 @@ +export * from './ui' \ No newline at end of file @@ -1,2 +1,3 @@ export const PLATE_CHANGE_PASSWORD = 'plate-change-password' export const RESEND_INVATION_PASSWORD = 'resend-invation-password' +export const ERROR_REPORT = 'error-report' @@ -12,7 +12,6 @@ export interface CommonInputProps extends React.InputHTMLAttributes } export const CommonInput = forwardRef( @@ -0,0 +1,120 @@ +.box { + display: flex; + flex-direction: column; +} + +.header { + display: flex; + align-items: center; + justify-content: space-between; + + &__error { + transform: translateY(16px); + color: #ff2372; + font-size: 13px; + opacity: 0; + transition: all 300ms; + margin-top: 8px; + + &_visible { + opacity: 1; + transform: translateY(0); + } + } +} + +.label { + color: #a6a5a5; + font-size: 14px; + margin-bottom: 8px; + width: 100%; + + &_error { + color: #97989f; + font-size: 14px; + margin-bottom: 8px; + } +} + +.textarea { + line-height: 28px; + padding: 0; + resize: none; + background-color: transparent; + border: none; + outline: none; + font-size: 16px; + width: 100%; + font-family: '__Raleway_9d395e' !important; + + &:focus { + border: none; + outline: none; + } + + &_default { + font-size: 19px; + color: var(--new-ui-text-color) !important; + } + + &_error { + color: #ff2372 !important; + &::placeholder { + color: #ff2372 !important; + } + } +} + +.wrapper { + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: center; + transition: all 0.3 ease-in; + + &_default { + color: var(--new-ui-text-color) !important; + padding: 16.5px 14px; + background-color: rgb(61, 61, 61); + border: 1px solid var(--border-color2); + border-radius: 8px; + + &:hover { + border-color: var(--border-color); + } + + &::placeholder { + color: var(--border-color2) !important; + } + } + + &_error { + border-color: #ff2372; + + &::placeholder { + color: #ff2372 !important; + } + + &:focus { + border: 2px solid #ff2372; + } + } +} + +html[data-theme='light'] { + .wrapper { + &_error { + border-color: #ff76a7 !important; + &::placeholder { + color: #ff76a7 !important; + } + &:focus { + border: 2px solid #ff76a7 !important; + } + } + + &_default { + background-color: white; + } + } +} @@ -0,0 +1,55 @@ +import { c } from '#/shared/lib/helpers' +import { FieldError } from 'react-hook-form' +import styles from './common-textarea.module.scss' +import React, { ForwardedRef, forwardRef, useEffect } from 'react' + +export type TextAreaVariant = 'default' + +export interface CommonTextAreaProps extends React.TextareaHTMLAttributes { + children?: React.ReactNode + variant?: TextAreaVariant + label?: string | React.ReactNode + error?: FieldError + buttomSlot?: React.ReactNode + trim?: boolean + showError?: boolean +} + +export const CommonTextArea = forwardRef( + ({ showError = true, buttomSlot, label, error, className, disabled, trim = false, variant = 'default', ...props }, ref) => { + const onChange = (event: React.ChangeEvent) => { + event.target.value = trim ? event.target.value.trim() : event.target.value + } + + return ( + + + {label && {label}} + + + + {buttomSlot} + + {showError && {error ? error?.message : ''}} + + ) + } +) @@ -0,0 +1 @@ +export * from './common-textarea' \ No newline at end of file @@ -0,0 +1 @@ +export * from './ui' @@ -1,300 +0,0 @@ -import * as React from 'react' -import { ChangeEvent, useState } from 'react' -import AttachFileIcon from '@mui/icons-material/AttachFile' -import CloseIcon from '@mui/icons-material/Close' -import { Box, Button, Modal, Stack, TextField, Typography } from '@mui/material' -import axios from 'axios' -import { useSession } from 'next-auth/react' - -import { useAppSelector } from '#/app/store/store' -import { API_URL } from '#/shared/lib/constants' - -const style = { - position: 'absolute' as 'absolute', - top: '50%', - left: '50%', - transform: 'translate(-50%, -50%)', - width: 589, - height: 'auto', - bgcolor: 'background.paper', - borderRadius: 5, - boxShadow: 16, - p: 4, -} -const styleSend = { - position: 'absolute' as 'absolute', - top: '50%', - left: '50%', - transform: 'translate(-50%, -65%)', - width: 300, - height: 'auto', - bgcolor: 'background.paper', - borderRadius: 5, - boxShadow: 16, - p: 4, - textAlign: 'center', -} - -const styleSendDark = { - position: 'absolute' as 'absolute', - top: '50%', - left: '50%', - transform: 'translate(-50%, -65%)', - width: 300, - height: 'auto', - bgcolor: '#2B2828', - borderRadius: 5, - boxShadow: 16, - p: 4, - textAlign: 'center', -} - -const styleMobile = { - position: 'absolute' as 'absolute', - top: '50%', - left: '50%', - transform: 'translate(-50%, -50%)', - bgcolor: 'background.paper', - p: 4, - zIndex: 99, -} - -interface IErrorModal { - open: boolean - handleClose: () => void - device: 'mobile' | 'desktop' -} - -const ErrorModal: React.FC = ({ open, handleClose, device }) => { - const [errorMessage, setErrorMessage] = React.useState('') - - const [error, setError] = React.useState(false) - - const [isSend, setIsSend] = React.useState(false) - - const [file, setFile] = React.useState() - - const { data: session } = useSession() - - const email = useAppSelector((state) => state.user.email) - - const theme = useAppSelector((state) => state.theme.theme) - - const handleFileChange = (e: ChangeEvent) => { - if (e.target.files) { - if (4500000 > e.target.files[0].size) setFile(e.target.files[0]) - } - } - const sendError = async () => { - if (errorMessage.trim() === '') { - setError(true) - setTimeout(() => setError(false), 3000) - return - } - - let formData: any = new FormData() - - formData.append('report_text', `${email}: ${errorMessage}`) //append the values with key, value pair - if (file) { - formData.append('images', file) - } - - try { - const { status } = await axios.post(API_URL + '/reports/', formData, { - headers: { - 'content-type': 'multipart/form-data ', - 'content-length': file ? `${file.size}` : '', - - Authorization: `Bearer ${session?.access}`, - }, - }) - - if (status === 201) { - setErrorMessage('') - setIsSend(true) - setTimeout(() => { - setIsSend(false) - handleClose() - }, 4000) - } - } catch (err) { - setError(true) - setTimeout(() => setError(false), 3000) - } - } - - return ( - - {isSend ? ( - - Спасибо! - - Ваше сообщение направлено нашим специалистам. Ответ придет на почту, указанную при - регистрации - - - ) : ( - - - Сообщить об ошибке - - setErrorMessage(evt.target.value)} - InputProps={{ - startAdornment: ( - <> - - - - - > - ), - }} - sx={{ - marginTop: 2.5, - marginBottom: 1.2, - fontSize: 13, - '& label': { color: '#8853FA' }, - backgroundColor: theme === 'light' ? 'transparent' : '#3D3D3D', - '& .MuiOutlinedInput-input': { - color: theme === 'light' ? '#272727' : '#E1E1E1', - }, - }} - /> - {file && ( - - - Добавлено 1 изображение: {file.name} - - setFile(undefined)} - sx={{ - width: '18px', - height: '18px', - cursor: 'pointer', - marginLeft: '3px', - marginTop: '2px', - }} - /> - - )} - - - handleClose()} - sx={{ - color: theme === 'light' ? '#5A5A5A' : '#D0D0D0', - backgroundColor: theme === 'light' ? '#E8E8E8' : '#5D5A5A', - fontWeight: 500, - fontSize: '16px', - textTransform: 'none', - padding: '8px 12px', - borderRadius: '13px', - border: 'none', - cursor: 'pointer', - marginTop: device === 'desktop' ? 0 : 1, - }} - > - Отмена - - - Отправить - - - - )} - - ) -} - -export default ErrorModal @@ -111,9 +111,6 @@ } } -.icon { -} - .blocked { display: flex; align-items: center; @@ -23,6 +23,7 @@ import { LayoutProps } from '../types' import { change } from '#/features/pending' import { ChangePasswordPlate } from '#/features/change-password-corp' import { ResendInvationPlate } from '#/features/resend-invation-corp ' +import { ErrorReportPlate } from '#/features/error-report' const freeRoutes = ['/login', '/register', '/reset', '/change-password'] @@ -152,6 +153,7 @@ export const Layout: React.FC = ({ children, titlePage, title = tit + > ) @@ -13,6 +13,7 @@ import { useAppDispatch, useAppSelector } from '#/app/store/store' import { AccountMenu } from '#/shared' import { NotificationMenu } from '#/shared' import { TooltipFreeTokens } from '#/shared' +import { ERROR_REPORT, getModalById } from '#/features/modals' function a11yProps(index: number) { return { @@ -73,9 +74,10 @@ export const MainMenu: React.FC = memo(() => { const email = useAppSelector((state) => state.user.email) - const [openErrorModal, setOpenErrorModal] = React.useState(false) - const handleOpenErrorModal = () => setOpenErrorModal(true) - const handleCloseErrorModal = () => setOpenErrorModal(false) + // const [openErrorModal, setOpenErrorModal] = React.useState(false) + // const handleOpenErrorModal = () => setOpenErrorModal(true) + // const handleCloseErrorModal = () => setOpenErrorModal(false) + const errorReport = getModalById(ERROR_REPORT) const selectPage = pathname === '/' ? 0 : pathname === '/service-keys' ? 1 : pathname === '/admin' ? 2 : 3 @@ -173,7 +175,9 @@ export const MainMenu: React.FC = memo(() => { {status === 'authenticated' && ( { + errorReport.setState(true) + }} priority style={{ marginTop: 12, @@ -186,13 +190,6 @@ export const MainMenu: React.FC = memo(() => { alt='Error' /> )} - {openErrorModal && ( - - )} import('#/shared/ui/error-modal'), { ssr: false }) - - const [openErrorModal, setOpenErrorModal] = React.useState(false) - - const handleOpenErrorModal = () => setOpenErrorModal(true) - - const handleCloseErrorModal = () => setOpenErrorModal(false) + const error_report = getModalById(ERROR_REPORT) const match = useMediaQuery('(min-height:800px)') @@ -272,7 +267,9 @@ export const SideMenu = ({ device, sidemenuDefaultOpen }: { device: Device; side - + { + error_report.setState(true) + }}> > )} - {openErrorModal && ( - - )} {open && (
Спасибо!
Ваше сообщение направлено нашим специалистам. Ответ придет на почту, указанную при регистрации
Сообщить об ошибке
Добавлено 1 изображение: {file.name}
{error ? error?.message : ''}