fix: conflicts resolved
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 15 KiB |
BIN
public/favicon.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
71
public/favicon.svg
Normal file
@ -0,0 +1,71 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="43.585045"
|
||||
height="39.566273"
|
||||
viewBox="0 0 63.268614 57.700814"
|
||||
fill="none"
|
||||
version="1.1"
|
||||
id="svg8"
|
||||
sodipodi:docname="favicon.svg"
|
||||
inkscape:export-filename="favicon.png"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview8"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
showguides="true" />
|
||||
<g
|
||||
clip-path="url(#clip0_316_1239)"
|
||||
id="g8"
|
||||
transform="translate(-1.6513393,-3.2563442)">
|
||||
<g
|
||||
id="g9"
|
||||
transform="translate(0.27803262,-0.00871564)">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M 25.9138,3.31956 C 18.594,2.47167 13.5439,10.3345 8.84663,16.0182 4.72431,21.0062 1.6549,26.6402 1.29838,33.1042 0.919075,39.9813 2.16658,47.1435 6.85174,52.1873 11.6777,57.3827 18.9068,60.6653 25.9138,59.604 32.3391,58.6308 35.1822,51.5749 39.9658,47.1716 45.16,42.3905 54.837,40.1668 54.7027,33.1042 54.5683,26.0308 44.3552,24.6463 39.441,19.5621 34.3509,14.2959 33.1853,4.16185 25.9138,3.31956 Z"
|
||||
fill="#7e2aea"
|
||||
id="path1" />
|
||||
<circle
|
||||
cx="44.125999"
|
||||
cy="56.918098"
|
||||
r="4.0390601"
|
||||
fill="#7e2aea"
|
||||
id="circle1" />
|
||||
<circle
|
||||
cx="40.086498"
|
||||
cy="12.1038"
|
||||
r="1.53869"
|
||||
fill="#7e2aea"
|
||||
id="circle2" />
|
||||
<path
|
||||
d="m 64.699,31.4509 c -0.4487,-4.3618 -2.5007,-8.4017 -5.7585,-11.3366 -3.2577,-2.9349 -7.4891,-4.5558 -11.8739,-4.5485 -0.6225,3e-4 -1.2446,0.0329 -1.8638,0.0976 -4.3599,0.4578 -8.3958,2.5137 -11.3293,5.7713 -2.9336,3.2577 -4.557,7.4861 -4.5571,11.8699 v 0 25.3412 h 7.6024 v -10.77 c 2.9724,2.0679 6.5079,3.1735 10.1288,3.1676 0.6226,-2e-4 1.2447,-0.0327 1.8639,-0.0975 2.3167,-0.2435 4.5629,-0.9409 6.6101,-2.0525 2.0472,-1.1116 3.8555,-2.6155 5.3215,-4.4259 1.466,-1.8104 2.5611,-3.8918 3.2227,-6.1254 0.6616,-2.2336 0.8767,-4.5757 0.6332,-6.8924 z m -9.764,8.2359 c -0.8351,1.0374 -1.8677,1.8988 -3.038,2.5343 -1.1704,0.6355 -2.4552,1.0325 -3.78,1.168 -0.3553,0.0369 -0.7122,0.0555 -1.0694,0.0558 -2.2991,-0.0021 -4.5293,-0.7858 -6.3243,-2.2224 -1.7951,-1.4366 -3.0484,-3.4408 -3.5544,-5.6836 -0.5059,-2.2428 -0.2343,-4.5909 0.7702,-6.6591 1.0045,-2.0681 2.6822,-3.7333 4.7578,-4.7222 2.0756,-0.9889 4.4257,-1.2429 6.6647,-0.7202 2.2389,0.5228 4.2336,1.7911 5.6567,3.5969 1.4231,1.8058 2.19,4.0417 2.1749,6.3408 -0.0151,2.2991 -0.8114,4.5248 -2.2582,6.3117 z"
|
||||
fill="#000000"
|
||||
id="path2" />
|
||||
</g>
|
||||
</g>
|
||||
<defs
|
||||
id="defs8">
|
||||
<clipPath
|
||||
id="clip0_316_1239">
|
||||
<rect
|
||||
width="179.509"
|
||||
height="69.487198"
|
||||
fill="#ffffff"
|
||||
id="rect8"
|
||||
x="0"
|
||||
y="0" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 3.1 KiB |
BIN
public/favicon192.png
Normal file
After Width: | Height: | Size: 7.6 KiB |
BIN
public/favicon512.png
Normal file
After Width: | Height: | Size: 20 KiB |
@ -1,47 +1,28 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="ru">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta name="description" content="Web site created using create-react-app" />
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
|
||||
<title>Pena Hub</title>
|
||||
<meta name="description" content=" Заказная IT-разработка: создание уникальных и эффективных решений для вашего бизнеса. Проектирование, разработка и интеграция IT-систем под индивидуальные требования клиентов. "/>
|
||||
<meta name="keywords" content=" Заказная IT-разработка, заказная разработка, индивидуальное решение, бизнес-решение, программирование, программное обеспечение, веб-дизайн, мобильные приложения, IT-интеграция, технологические решения "/>
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Rubik:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<title>React App</title>
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" sizes="any"/><!-- 32×32 -->
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.svg" type="image/svg+xml"/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/favicon.png"/><!-- 180×180 -->
|
||||
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Rubik:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
<!-- <script src="https://markknol.github.io/console-log-viewer/console-log-viewer.js"></script> -->
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
|
||||
</html>
|
Before Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 9.4 KiB |
@ -1,23 +1,10 @@
|
||||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"short_name": "PenaHub",
|
||||
"name": "Pena Hub",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
{ "src": "/favicon192.png", "type": "image/png", "sizes": "192x192" },
|
||||
{ "src": "/favicon512.png", "type": "image/png", "sizes": "512x512" }
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
|
@ -8,6 +8,7 @@ import type {
|
||||
SendDocumentsArgs,
|
||||
UpdateDocumentsArgs,
|
||||
} from "@root/model/auth"
|
||||
import { AxiosError } from "axios"
|
||||
|
||||
const apiUrl = process.env.REACT_APP_DOMAIN + "/verification"
|
||||
|
||||
@ -22,8 +23,19 @@ export async function verification(
|
||||
withCredentials: true,
|
||||
})
|
||||
|
||||
verificationResponse.files = verificationResponse.files.map((obj) => {
|
||||
obj.url = obj.url.replace("https://hub.pena.digital", process.env.REACT_APP_DOMAIN?.toString() || "").replace("https://shub.pena.digital", process.env.REACT_APP_DOMAIN?.toString() || "")
|
||||
return obj
|
||||
})
|
||||
console.log(verificationResponse)
|
||||
|
||||
return [verificationResponse]
|
||||
} catch (nativeError) {
|
||||
const err = nativeError as AxiosError
|
||||
if (err.response?.status === 404) {
|
||||
return [null, `нет данных`]
|
||||
}
|
||||
console.log(nativeError)
|
||||
const [error] = parseAxiosError(nativeError)
|
||||
|
||||
return [null, `Ошибка запроса верификации. ${error}`]
|
||||
|
@ -1,46 +1,67 @@
|
||||
import { makeRequest } from "@frontend/kitui"
|
||||
import { SendPaymentRequest, SendPaymentResponse } from "@root/model/wallet"
|
||||
import { parseAxiosError } from "@root/utils/parse-error"
|
||||
import { makeRequest } from "@frontend/kitui";
|
||||
import { SendPaymentRequest, SendPaymentResponse } from "@root/model/wallet";
|
||||
import { parseAxiosError } from "@root/utils/parse-error";
|
||||
|
||||
const apiUrl = process.env.REACT_APP_DOMAIN + "/customer"
|
||||
const apiUrl = process.env.REACT_APP_DOMAIN + "/customer";
|
||||
|
||||
const testPaymentBody: SendPaymentRequest = {
|
||||
type: "bankCard",
|
||||
amount: 15020,
|
||||
currency: "RUB",
|
||||
bankCard: {
|
||||
number: "RUB",
|
||||
expiryYear: "2021",
|
||||
expiryMonth: "05",
|
||||
csc: "05",
|
||||
cardholder: "IVAN IVANOV",
|
||||
},
|
||||
phoneNumber: "79000000000",
|
||||
login: "login_test",
|
||||
returnUrl: window.location.origin + "/wallet",
|
||||
}
|
||||
type: "bankCard",
|
||||
amount: 15020,
|
||||
currency: "RUB",
|
||||
bankCard: {
|
||||
number: "RUB",
|
||||
expiryYear: "2021",
|
||||
expiryMonth: "05",
|
||||
csc: "05",
|
||||
cardholder: "IVAN IVANOV",
|
||||
},
|
||||
phoneNumber: "79000000000",
|
||||
login: "login_test",
|
||||
returnUrl: window.location.origin + "/wallet",
|
||||
};
|
||||
|
||||
export async function sendPayment(
|
||||
{body = testPaymentBody, fromSquiz = false}: {body?: SendPaymentRequest, fromSquiz:boolean}
|
||||
): Promise<[SendPaymentResponse | null, string?]> {
|
||||
if (fromSquiz) body.returnUrl = "squiz.pena.digital/list?action=fromhub"
|
||||
try {
|
||||
const sendPaymentResponse = await makeRequest<
|
||||
export async function sendPayment({
|
||||
body = testPaymentBody,
|
||||
fromSquiz = false,
|
||||
}: {
|
||||
body?: SendPaymentRequest;
|
||||
fromSquiz: boolean;
|
||||
}): Promise<[SendPaymentResponse | null, string?]> {
|
||||
if (fromSquiz) body.returnUrl = "squiz.pena.digital/list?action=fromhub";
|
||||
try {
|
||||
const sendPaymentResponse = await makeRequest<
|
||||
SendPaymentRequest,
|
||||
SendPaymentResponse
|
||||
>({
|
||||
url: apiUrl + "/wallet",
|
||||
contentType: true,
|
||||
method: "POST",
|
||||
useToken: true,
|
||||
withCredentials: false,
|
||||
body,
|
||||
})
|
||||
url: apiUrl + "/wallet",
|
||||
contentType: true,
|
||||
method: "POST",
|
||||
useToken: true,
|
||||
withCredentials: false,
|
||||
body,
|
||||
});
|
||||
|
||||
return [sendPaymentResponse]
|
||||
} catch (nativeError) {
|
||||
const [error] = parseAxiosError(nativeError)
|
||||
return [sendPaymentResponse];
|
||||
} catch (nativeError) {
|
||||
const [error] = parseAxiosError(nativeError);
|
||||
|
||||
return [null, `Ошибка оплаты. ${error}`]
|
||||
}
|
||||
return [null, `Ошибка оплаты. ${error}`];
|
||||
}
|
||||
}
|
||||
|
||||
export const sendRSPayment = async (): Promise<string | null> => {
|
||||
try {
|
||||
await makeRequest<never, string>({
|
||||
url: apiUrl + "/wallet/rspay",
|
||||
method: "POST",
|
||||
useToken: true,
|
||||
withCredentials: false,
|
||||
});
|
||||
|
||||
return null;
|
||||
} catch (nativeError) {
|
||||
const [error] = parseAxiosError(nativeError);
|
||||
|
||||
return `Ошибка оплаты. ${error}`;
|
||||
}
|
||||
};
|
||||
|
BIN
src/assets/bank-logo/rs-pay.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
@ -15,21 +15,20 @@ export default function ChatMessage({ unAuthenticated = false, isSelf, text, cre
|
||||
|
||||
const messageBackgroundColor = isSelf ? "white" : unAuthenticated ? "#EFF0F5" : theme.palette.gray.main
|
||||
|
||||
const date = new Date(createdAt)
|
||||
const date = new Date(createdAt);
|
||||
const today = isDateToday(date);
|
||||
const time = date.toLocaleString([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
...(!isDateToday(date) && { year: "2-digit", month: "2-digit", day: "2-digit" })
|
||||
...(!today && { year: "2-digit", month: "2-digit", day: "2-digit" })
|
||||
})
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignSelf: isSelf ? "end" : "start",
|
||||
gap: "9px",
|
||||
pl: isSelf ? undefined : "8px",
|
||||
pr: isSelf ? "8px" : undefined,
|
||||
padding: isSelf ? "0 8px 0 0" : "0 0 0 8px",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
@ -39,6 +38,7 @@ export default function ChatMessage({ unAuthenticated = false, isSelf, text, cre
|
||||
fontSize: "14px",
|
||||
lineHeight: "17px",
|
||||
order: isSelf ? 1 : 2,
|
||||
margin: isSelf ? "0 0 0 auto" : "0 auto 0 0",
|
||||
color: theme.palette.gray.main,
|
||||
mb: "-4px",
|
||||
whiteSpace: "nowrap",
|
||||
@ -51,9 +51,10 @@ export default function ChatMessage({ unAuthenticated = false, isSelf, text, cre
|
||||
order: isSelf ? 2 : 1,
|
||||
p: upMd ? "18px" : "12px",
|
||||
borderRadius: "8px",
|
||||
maxWidth: "464px",
|
||||
color: (isSelf || unAuthenticated) ? theme.palette.gray.dark : "white",
|
||||
position: "relative",
|
||||
maxWidth: `calc(100% - ${today ? 45 : 110}px)`,
|
||||
overflowWrap: "break-word",
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
|
@ -6,6 +6,7 @@ type CustomSliderProps = {
|
||||
min: number;
|
||||
max: number;
|
||||
onChange: (value: number | number[]) => void;
|
||||
firstStep: number;
|
||||
};
|
||||
|
||||
export const CustomSlider = ({
|
||||
@ -13,11 +14,16 @@ export const CustomSlider = ({
|
||||
min = 0,
|
||||
max = 100,
|
||||
onChange,
|
||||
firstStep
|
||||
}: CustomSliderProps) => {
|
||||
const theme = useTheme()
|
||||
const [step, setStep] = useState<number>(1)
|
||||
|
||||
useEffect(() => {
|
||||
if (value <= firstStep) {
|
||||
return setStep(firstStep)
|
||||
}
|
||||
|
||||
if (value < 100) {
|
||||
return setStep(10)
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ const name: Record<string, string> = {
|
||||
templategen: "Шаблонизатор",
|
||||
squiz: "Опросник",
|
||||
reducer: "Скоращатель ссылок",
|
||||
custom: "Кастомные тарифы",
|
||||
custom: "Мои тарифы",
|
||||
};
|
||||
|
||||
interface Props {
|
||||
|
@ -33,14 +33,15 @@ import {
|
||||
useEventListener,
|
||||
createTicket,
|
||||
} from "@frontend/kitui";
|
||||
import { sendTicketMessage } from "@root/api/ticket";
|
||||
import { sendTicketMessage, shownMessage } from "@root/api/ticket";
|
||||
import { useSSETab } from "@root/utils/hooks/useSSETab";
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
sx?: SxProps<Theme>;
|
||||
}
|
||||
|
||||
export default function Chat({ sx }: Props) {
|
||||
export default function Chat({ open = false, sx }: Props) {
|
||||
const theme = useTheme();
|
||||
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||
const [messageField, setMessageField] = useState<string>("");
|
||||
@ -135,6 +136,16 @@ export default function Chat({ sx }: Props) {
|
||||
[lastMessageId]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
const newMessages = messages.filter(({ shown }) => shown.me !== 1);
|
||||
|
||||
newMessages.map(async ({ id }) => {
|
||||
await shownMessage(id);
|
||||
});
|
||||
}
|
||||
}, [open, messages]);
|
||||
|
||||
async function handleSendMessage() {
|
||||
if (!messageField || isMessageSending) return;
|
||||
|
||||
@ -200,130 +211,134 @@ export default function Chat({ sx }: Props) {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
height: "clamp(250px, calc(100vh - 90px), 600px)",
|
||||
backgroundColor: "#944FEE",
|
||||
borderRadius: "8px",
|
||||
...sx,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
gap: "9px",
|
||||
pl: "22px",
|
||||
pt: "12px",
|
||||
pb: "20px",
|
||||
filter: "drop-shadow(0px 3px 12px rgba(37, 39, 52, 0.3))",
|
||||
}}
|
||||
>
|
||||
<UserCircleIcon />
|
||||
<>
|
||||
{open && (
|
||||
<Box
|
||||
sx={{
|
||||
mt: "5px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "3px",
|
||||
height: "clamp(250px, calc(100vh - 90px), 600px)",
|
||||
backgroundColor: "#944FEE",
|
||||
borderRadius: "8px",
|
||||
...sx,
|
||||
}}
|
||||
>
|
||||
<Typography>Мария</Typography>
|
||||
<Typography
|
||||
<Box
|
||||
sx={{
|
||||
fontSize: "16px",
|
||||
lineHeight: "19px",
|
||||
display: "flex",
|
||||
gap: "9px",
|
||||
pl: "22px",
|
||||
pt: "12px",
|
||||
pb: "20px",
|
||||
filter: "drop-shadow(0px 3px 12px rgba(37, 39, 52, 0.3))",
|
||||
}}
|
||||
>
|
||||
онлайн-консультант
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
backgroundColor: "white",
|
||||
borderRadius: "8px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
ref={chatBoxRef}
|
||||
sx={{
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
flexBasis: 0,
|
||||
flexDirection: "column",
|
||||
gap: upMd ? "20px" : "16px",
|
||||
px: upMd ? "20px" : "5px",
|
||||
py: upMd ? "20px" : "13px",
|
||||
overflowY: "auto",
|
||||
flexGrow: 1,
|
||||
}}
|
||||
>
|
||||
{sessionData &&
|
||||
messages.map((message) => (
|
||||
<ChatMessage
|
||||
unAuthenticated
|
||||
key={message.id}
|
||||
text={message.message}
|
||||
createdAt={message.created_at}
|
||||
isSelf={sessionData.sessionId === message.user_id}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
<FormControl fullWidth sx={{ borderTop: "1px solid black" }}>
|
||||
<InputBase
|
||||
value={messageField}
|
||||
fullWidth
|
||||
placeholder="Введите сообщение..."
|
||||
id="message"
|
||||
multiline
|
||||
onKeyDown={handleTextfieldKeyPress}
|
||||
<UserCircleIcon />
|
||||
<Box
|
||||
sx={{
|
||||
mt: "5px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "3px",
|
||||
}}
|
||||
>
|
||||
<Typography>Мария</Typography>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "16px",
|
||||
lineHeight: "19px",
|
||||
}}
|
||||
>
|
||||
онлайн-консультант
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
p: 0,
|
||||
flexGrow: 1,
|
||||
backgroundColor: "white",
|
||||
borderRadius: "8px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
inputProps={{
|
||||
sx: {
|
||||
fontWeight: 400,
|
||||
fontSize: "16px",
|
||||
lineHeight: "19px",
|
||||
pt: upMd ? "30px" : "28px",
|
||||
pb: upMd ? "30px" : "24px",
|
||||
px: "19px",
|
||||
maxHeight: "calc(19px * 5)",
|
||||
color: "black",
|
||||
},
|
||||
}}
|
||||
onChange={(e) => setMessageField(e.target.value)}
|
||||
endAdornment={
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
disabled={isMessageSending}
|
||||
onClick={handleSendMessage}
|
||||
sx={{
|
||||
height: "53px",
|
||||
width: "53px",
|
||||
mr: "13px",
|
||||
p: 0,
|
||||
opacity: isMessageSending ? 0.3 : 1,
|
||||
}}
|
||||
>
|
||||
<SendIcon
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
ref={chatBoxRef}
|
||||
sx={{
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
flexBasis: 0,
|
||||
flexDirection: "column",
|
||||
gap: upMd ? "20px" : "16px",
|
||||
px: upMd ? "20px" : "5px",
|
||||
py: upMd ? "20px" : "13px",
|
||||
overflowY: "auto",
|
||||
flexGrow: 1,
|
||||
}}
|
||||
>
|
||||
{sessionData &&
|
||||
messages.map((message) => (
|
||||
<ChatMessage
|
||||
unAuthenticated
|
||||
key={message.id}
|
||||
text={message.message}
|
||||
createdAt={message.created_at}
|
||||
isSelf={sessionData.sessionId === message.user_id}
|
||||
/>
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
<FormControl fullWidth sx={{ borderTop: "1px solid black" }}>
|
||||
<InputBase
|
||||
value={messageField}
|
||||
fullWidth
|
||||
placeholder="Введите сообщение..."
|
||||
id="message"
|
||||
multiline
|
||||
onKeyDown={handleTextfieldKeyPress}
|
||||
sx={{
|
||||
width: "100%",
|
||||
p: 0,
|
||||
}}
|
||||
inputProps={{
|
||||
sx: {
|
||||
fontWeight: 400,
|
||||
fontSize: "16px",
|
||||
lineHeight: "19px",
|
||||
pt: upMd ? "30px" : "28px",
|
||||
pb: upMd ? "30px" : "24px",
|
||||
px: "19px",
|
||||
maxHeight: "calc(19px * 5)",
|
||||
color: "black",
|
||||
},
|
||||
}}
|
||||
onChange={(e) => setMessageField(e.target.value)}
|
||||
endAdornment={
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
disabled={isMessageSending}
|
||||
onClick={handleSendMessage}
|
||||
sx={{
|
||||
height: "53px",
|
||||
width: "53px",
|
||||
mr: "13px",
|
||||
p: 0,
|
||||
opacity: isMessageSending ? 0.3 : 1,
|
||||
}}
|
||||
>
|
||||
<SendIcon
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -1,94 +1,110 @@
|
||||
import { Box, Fab, Typography } from "@mui/material"
|
||||
import { useState } from "react"
|
||||
import CircleDoubleDown from "./CircleDoubleDownIcon"
|
||||
import Chat from "./Chat"
|
||||
import { useState } from "react";
|
||||
import { Box, Fab, Typography, Badge, useTheme } from "@mui/material";
|
||||
|
||||
import CircleDoubleDown from "./CircleDoubleDownIcon";
|
||||
import Chat from "./Chat";
|
||||
|
||||
import { useUnauthTicketStore } from "@root/stores/unauthTicket";
|
||||
|
||||
export default function FloatingSupportChat() {
|
||||
const [isChatOpened, setIsChatOpened] = useState<boolean>(false)
|
||||
const [isChatOpened, setIsChatOpened] = useState<boolean>(false);
|
||||
const theme = useTheme();
|
||||
const { messages } = useUnauthTicketStore((state) => state);
|
||||
|
||||
const animation = {
|
||||
"@keyframes runningStripe": {
|
||||
"0%": {
|
||||
left: "10%",
|
||||
backgroundColor: "transparent",
|
||||
},
|
||||
"10%": {
|
||||
backgroundColor: "#ffffff",
|
||||
},
|
||||
"50%": {
|
||||
backgroundColor: "#ffffff",
|
||||
transform: "translate(400px, 0)",
|
||||
},
|
||||
"80%": {
|
||||
backgroundColor: "#ffffff",
|
||||
},
|
||||
const animation = {
|
||||
"@keyframes runningStripe": {
|
||||
"0%": {
|
||||
left: "10%",
|
||||
backgroundColor: "transparent",
|
||||
},
|
||||
"10%": {
|
||||
backgroundColor: "#ffffff",
|
||||
},
|
||||
"50%": {
|
||||
backgroundColor: "#ffffff",
|
||||
transform: "translate(400px, 0)",
|
||||
},
|
||||
"80%": {
|
||||
backgroundColor: "#ffffff",
|
||||
},
|
||||
|
||||
"100%": {
|
||||
backgroundColor: "transparent",
|
||||
boxShadow: "none",
|
||||
left: "100%",
|
||||
},
|
||||
},
|
||||
}
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
position: "fixed",
|
||||
right: "20px",
|
||||
bottom: "10px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "8px",
|
||||
width: "clamp(200px, 100% - 40px, 454px)",
|
||||
zIndex: 10,
|
||||
}}
|
||||
>
|
||||
{isChatOpened && (
|
||||
<Chat
|
||||
sx={{
|
||||
alignSelf: "start",
|
||||
width: "clamp(200px, 100%, 400px)",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Fab
|
||||
disableRipple
|
||||
sx={{
|
||||
position: "relative",
|
||||
backgroundColor: "rgba(255, 255, 255, 0.7)",
|
||||
pl: "11px",
|
||||
pr: !isChatOpened ? "15px" : "11px",
|
||||
gap: "11px",
|
||||
height: "54px",
|
||||
borderRadius: "27px",
|
||||
alignSelf: "end",
|
||||
overflow: "hidden",
|
||||
"&:hover": {
|
||||
background: "rgba(255, 255, 255, 0.7)",
|
||||
},
|
||||
}}
|
||||
variant={"extended"}
|
||||
onClick={() => setIsChatOpened((prev) => !prev)}
|
||||
>
|
||||
{!isChatOpened && (
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
bgcolor: "#FFFFFF",
|
||||
height: "100px",
|
||||
width: "25px",
|
||||
animation: "runningStripe linear 3s infinite",
|
||||
transform: " skew(-10deg) rotate(70deg) skewX(20deg) skewY(10deg)",
|
||||
boxShadow: "0px 3px 12px rgba(126, 42, 234, 0.1)",
|
||||
opacity: "0.4",
|
||||
...animation,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
"100%": {
|
||||
backgroundColor: "transparent",
|
||||
boxShadow: "none",
|
||||
left: "100%",
|
||||
},
|
||||
},
|
||||
};
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
position: "fixed",
|
||||
right: "20px",
|
||||
bottom: "10px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "8px",
|
||||
width: "clamp(200px, 100% - 40px, 454px)",
|
||||
zIndex: 10,
|
||||
}}
|
||||
>
|
||||
<Chat
|
||||
open={isChatOpened}
|
||||
sx={{ alignSelf: "start", width: "clamp(200px, 100%, 400px)" }}
|
||||
/>
|
||||
|
||||
<CircleDoubleDown isUp={isChatOpened} />
|
||||
{!isChatOpened && <Typography sx={{ zIndex: "10000" }}>Задайте нам вопрос</Typography>}
|
||||
</Fab>
|
||||
</Box>
|
||||
)
|
||||
<Fab
|
||||
disableRipple
|
||||
sx={{
|
||||
position: "relative",
|
||||
backgroundColor: "rgba(255, 255, 255, 0.7)",
|
||||
pl: "11px",
|
||||
pr: !isChatOpened ? "15px" : "11px",
|
||||
gap: "11px",
|
||||
height: "54px",
|
||||
borderRadius: "27px",
|
||||
alignSelf: "end",
|
||||
overflow: "hidden",
|
||||
"&:hover": {
|
||||
background: "rgba(255, 255, 255, 0.7)",
|
||||
},
|
||||
}}
|
||||
variant={"extended"}
|
||||
onClick={() => setIsChatOpened((prev) => !prev)}
|
||||
>
|
||||
{!isChatOpened && (
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
bgcolor: "#FFFFFF",
|
||||
height: "100px",
|
||||
width: "25px",
|
||||
animation: "runningStripe linear 3s infinite",
|
||||
transform:
|
||||
" skew(-10deg) rotate(70deg) skewX(20deg) skewY(10deg)",
|
||||
boxShadow: "0px 3px 12px rgba(126, 42, 234, 0.1)",
|
||||
opacity: "0.4",
|
||||
...animation,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Badge
|
||||
badgeContent={messages.filter(({ shown }) => shown.me !== 1).length}
|
||||
sx={{
|
||||
"& .MuiBadge-badge": {
|
||||
display: isChatOpened ? "none" : "flex",
|
||||
color: "#FFFFFF",
|
||||
background: theme.palette.purple.main,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<CircleDoubleDown isUp={isChatOpened} />
|
||||
</Badge>
|
||||
|
||||
{!isChatOpened && (
|
||||
<Typography sx={{ zIndex: "10000" }}>Задайте нам вопрос</Typography>
|
||||
)}
|
||||
</Fab>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
@ -2,15 +2,24 @@ import { useState } from "react"
|
||||
import { InputAdornment, TextField, Typography, useTheme } from "@mui/material"
|
||||
|
||||
import type { ChangeEvent } from "react"
|
||||
import {Privilege} from "@frontend/kitui"
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
value: number;
|
||||
adornmentText: string;
|
||||
privilege: Privilege;
|
||||
onChange: (value: number) => void;
|
||||
}
|
||||
|
||||
export default function NumberInputWithUnitAdornment({ id, value, adornmentText, onChange }: Props) {
|
||||
const sliderSettingsByType = {
|
||||
день: { max: 365, min: 0 },
|
||||
шаблон: { max: 5000, min: 0 },
|
||||
МБ: { max: 5000, min: 0 },
|
||||
заявка: { max: 5000, min: 0 }
|
||||
}
|
||||
|
||||
export default function NumberInputWithUnitAdornment({ id, value, adornmentText, privilege, onChange }: Props) {
|
||||
const theme = useTheme()
|
||||
const [changed, setChanged] = useState<boolean>(false)
|
||||
|
||||
@ -20,7 +29,10 @@ export default function NumberInputWithUnitAdornment({ id, value, adornmentText,
|
||||
size="small"
|
||||
placeholder="Введите вручную"
|
||||
id={id}
|
||||
value={changed ? (value !== 0 ? value : "") : ""}
|
||||
onBlur={(e) => {e.target.value = String(Number(String(e.target.value).replace(/^0+(?=\d\.)/, '')))
|
||||
console.log("сработало", e.target.value)
|
||||
}}
|
||||
value={changed ? (value !== sliderSettingsByType[privilege.value]?.min ? parseInt(String(value), 10) : sliderSettingsByType[privilege.value]?.min) : ""}
|
||||
onChange={({ target }: ChangeEvent<HTMLInputElement>) => {
|
||||
if (!changed) {
|
||||
setChanged(true)
|
||||
@ -34,7 +46,7 @@ export default function NumberInputWithUnitAdornment({ id, value, adornmentText,
|
||||
|
||||
|
||||
if (!isFinite(newNumber) || newNumber < 0) {
|
||||
onChange(0)
|
||||
onChange(sliderSettingsByType[privilege.value]?.min)
|
||||
|
||||
return
|
||||
}
|
||||
|
@ -5,9 +5,10 @@ interface Props {
|
||||
name?: string;
|
||||
desc?: string;
|
||||
image?: string;
|
||||
href?: string;
|
||||
}
|
||||
|
||||
export default function TemplCardPhoneLight({name="PenaDoc", desc="Самый удобный сервис для автоматизации документооборота и заполнения однотипных документов", image = card1Image }: Props) {
|
||||
export default function TemplCardPhoneLight({name="PenaDoc", desc="Самый удобный сервис для автоматизации документооборота и заполнения однотипных документов", image = card1Image, href = "#" }: Props) {
|
||||
const theme = useTheme()
|
||||
const upMd = useMediaQuery(theme.breakpoints.up("md"))
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down(600))
|
||||
@ -54,7 +55,11 @@ export default function TemplCardPhoneLight({name="PenaDoc", desc="Самый у
|
||||
{desc}
|
||||
</Typography>
|
||||
|
||||
<Button variant="pena-contained-light">Подробнее</Button>
|
||||
<Button
|
||||
variant="pena-contained-light"
|
||||
target={"_blank"}
|
||||
href={href}>
|
||||
Подробнее</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
|
@ -9,9 +9,10 @@ interface Props {
|
||||
name?: string;
|
||||
desc?: string;
|
||||
image?: string;
|
||||
href?: string;
|
||||
}
|
||||
|
||||
export default function WideTemplCard({ light = true, sx, name="PenaDoc", desc="Самый удобный сервис для автоматизации документооборота и заполнения однотипных документов", image = cardImageBig }: Props) {
|
||||
export default function WideTemplCard({ light = true, sx, name="PenaDoc", desc="Самый удобный сервис для автоматизации документооборота и заполнения однотипных документов", image = cardImageBig, href="#" }: Props) {
|
||||
const theme = useTheme()
|
||||
const isTablet = useMediaQuery(theme.breakpoints.down(1000))
|
||||
|
||||
@ -21,8 +22,7 @@ export default function WideTemplCard({ light = true, sx, name="PenaDoc", desc="
|
||||
position: "relative",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
py: "40px",
|
||||
px: "20px",
|
||||
p: "40px 20px 20px 20px",
|
||||
backgroundColor: light ? "#E6E6EB" : "#434657",
|
||||
borderRadius: "12px",
|
||||
...sx,
|
||||
@ -33,12 +33,19 @@ export default function WideTemplCard({ light = true, sx, name="PenaDoc", desc="
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "start",
|
||||
justifyContent: "space-between"
|
||||
}}
|
||||
>
|
||||
<Typography variant="h5">{name}</Typography>
|
||||
<Typography sx={{ marginTop: isTablet ? "10px" : "20px" }} maxWidth="552px">
|
||||
<Typography maxWidth="552px">
|
||||
{desc}
|
||||
</Typography>
|
||||
<Button sx={{ width: "180px", height: "44px", p: 0 }}
|
||||
variant="pena-contained-light"
|
||||
target={"_blank"}
|
||||
href={href}>
|
||||
Подробнее
|
||||
</Button>
|
||||
</Box>
|
||||
<img
|
||||
src={image}
|
||||
|
@ -5,6 +5,7 @@ import { CssBaseline, ThemeProvider } from "@mui/material"
|
||||
import Faq from "./pages/Faq/Faq"
|
||||
import Wallet from "./pages/Wallet"
|
||||
import Payment from "./pages/Payment/Payment"
|
||||
import QuizPayment from "./pages/QuizPayment/QuizPayment"
|
||||
import Support from "./pages/Support/Support"
|
||||
import AccountSettings from "./pages/AccountSettings/AccountSettings"
|
||||
import Landing from "./pages/Landing/Landing"
|
||||
@ -70,6 +71,7 @@ const App = () => {
|
||||
},
|
||||
})
|
||||
|
||||
console.log(location)
|
||||
if (location.state?.redirectTo)
|
||||
return <Navigate to={location.state.redirectTo} replace state={{ backgroundLocation: location }} />
|
||||
|
||||
@ -82,7 +84,6 @@ const App = () => {
|
||||
<Route path="/recover" element={<RecoverDialog />} />
|
||||
<Route path="/changepwd" element={<RecoverPassword />} />
|
||||
<Route path="/changepwd/expired" element={<OutdatedLink />} />
|
||||
|
||||
</Routes>
|
||||
)}
|
||||
<Routes location={location.state?.backgroundLocation || location}>
|
||||
@ -92,6 +93,7 @@ const App = () => {
|
||||
<Route path="/recover" element={<Navigate to="/" replace state={{ redirectTo: "/recover" }} />} />
|
||||
<Route path="/changepwd" element={<Navigate to="/" replace state={{ redirectTo: window.location.pathname + window.location.search }} />} />
|
||||
<Route path="/changepwd/expired" element={<Navigate to="/" replace state={{ redirectTo: "/changepwd/expired" }} />} />
|
||||
|
||||
<Route element={<PrivateRoute />}>
|
||||
<Route element={<ProtectedLayout />}>
|
||||
<Route path="/tariffs" element={<Tariffs />} />
|
||||
@ -110,6 +112,7 @@ const App = () => {
|
||||
</Route>
|
||||
</Route>
|
||||
<Route path="/ppdd" element={<PPofData/>}/>
|
||||
<Route path="/quizpayment" element={<QuizPayment/>} />
|
||||
<Route element={<Docs />}>
|
||||
<Route path={"/docs/oferta"} element={<Oferta />}/>
|
||||
<Route path={"/docs/privacy"} element={<PrivacyPolicy />}/>
|
||||
|
@ -28,6 +28,7 @@ export default function JuridicalDocumentsDialog() {
|
||||
const isOpen = useUserStore((state) => state.isDocumentsDialogOpen)
|
||||
const verificationStatus = useUserStore((state) => state.verificationStatus)
|
||||
const documents = useUserStore((state) => state.documents)//загруженные юзером файлы
|
||||
console.log(documents)
|
||||
const documentsUrl = useUserStore((state) => state.documentsUrl)//ссылки с бекенда
|
||||
const userId = useUserStore((state) => state.userId) ?? ""
|
||||
|
||||
|
@ -48,6 +48,9 @@ export const verify = async (id: string) => {
|
||||
const [verificationResult, verificationError] = await verification(id)
|
||||
|
||||
if (verificationError) {
|
||||
|
||||
if (verificationError === "нет данных") return
|
||||
|
||||
setVerificationStatus(VerificationStatus.NOT_VERIFICATED)
|
||||
|
||||
devlog("Error fetching user", verificationError)
|
||||
|
22
src/pages/ApologyPage.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import { Box, Typography } from "@mui/material";
|
||||
|
||||
export const ApologyPage = ({ message }: { message: string }) => {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
height: "100vh",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
{message || "что-то пошло не так"}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
};
|
@ -14,7 +14,7 @@ const name: Record<string, string> = {
|
||||
templategen: "Шаблонизатор",
|
||||
squiz: "Опросник",
|
||||
reducer: "Сокращатель ссылок",
|
||||
custom: "Кастомные тарифы",
|
||||
custom: "Мои тарифы",
|
||||
}
|
||||
|
||||
interface Props {
|
||||
|
@ -18,7 +18,8 @@ import EmailIcon from '@mui/icons-material/Email';
|
||||
import {enqueueSnackbar} from "notistack"
|
||||
import { makeRequest } from "@frontend/kitui"
|
||||
|
||||
const subPages = ["Платежи", "Покупки тарифов", "Окончания тарифов"]
|
||||
const subPages = ["Платежи"]
|
||||
// const subPages = ["Платежи", "Покупки тарифов", "Окончания тарифов"]
|
||||
|
||||
export default function History() {
|
||||
const [selectedItem, setSelectedItem] = useState<number>(0)
|
||||
@ -93,6 +94,7 @@ export default function History() {
|
||||
}
|
||||
onError={handleComponentError}
|
||||
>
|
||||
{historyData?.length === 0 && <Typography textAlign="center" >Нет данных</Typography>}
|
||||
{historyData?.filter((e) => {
|
||||
e.createdAt = extractDateFromString(e.createdAt)
|
||||
return(!e.isDeleted && e.key === "payCart" && Array.isArray(e.rawDetails[0].Value)
|
||||
|
@ -47,7 +47,10 @@ export default function Section5() {
|
||||
gap: upMd ? "24px" : "20px",
|
||||
}}
|
||||
>
|
||||
<Button sx={{ width: "180px", height: "44px", p: 0 }} variant="pena-contained-light">
|
||||
<Button sx={{ width: "180px", height: "44px", p: 0 }}
|
||||
variant="pena-contained-light"
|
||||
target={"_blank"}
|
||||
href={"https://pena.digital/"}>
|
||||
Наши услуги
|
||||
</Button>
|
||||
</Box>
|
||||
|
@ -1,241 +1,292 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
IconButton,
|
||||
Typography,
|
||||
useMediaQuery,
|
||||
useTheme,
|
||||
} from "@mui/material"
|
||||
import ArrowBackIcon from "@mui/icons-material/ArrowBack"
|
||||
import SectionWrapper from "@components/SectionWrapper"
|
||||
import PaymentMethodCard from "./PaymentMethodCard"
|
||||
import mastercardLogo from "../../assets/bank-logo/logo-mastercard.png"
|
||||
import visaLogo from "../../assets/bank-logo/logo-visa.png"
|
||||
import qiwiLogo from "../../assets/bank-logo/logo-qiwi.png"
|
||||
import mirLogo from "../../assets/bank-logo/logo-mir.png"
|
||||
import tinkoffLogo from "../../assets/bank-logo/logo-tinkoff.png"
|
||||
import { cardShadow } from "@root/utils/theme"
|
||||
import { useEffect, useLayoutEffect, useState } from "react"
|
||||
import InputTextfield from "@root/components/InputTextfield"
|
||||
import { sendPayment } from "@root/api/wallet"
|
||||
import { getMessageFromFetchError } from "@frontend/kitui"
|
||||
import { enqueueSnackbar } from "notistack"
|
||||
import { currencyFormatter } from "@root/utils/currencyFormatter"
|
||||
import { useLocation, useNavigate } from "react-router-dom"
|
||||
import { useHistoryTracker } from "@root/utils/hooks/useHistoryTracker"
|
||||
Box,
|
||||
Button,
|
||||
IconButton,
|
||||
Typography,
|
||||
useMediaQuery,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
|
||||
import SectionWrapper from "@components/SectionWrapper";
|
||||
import PaymentMethodCard from "./PaymentMethodCard";
|
||||
import mastercardLogo from "@root/assets/bank-logo/logo-mastercard.png";
|
||||
import visaLogo from "@root/assets/bank-logo/logo-visa.png";
|
||||
import qiwiLogo from "@root/assets/bank-logo/logo-qiwi.png";
|
||||
import mirLogo from "@root/assets/bank-logo/logo-mir.png";
|
||||
import tinkoffLogo from "@root/assets/bank-logo/logo-tinkoff.png";
|
||||
import rsPayLogo from "@root/assets/bank-logo/rs-pay.png";
|
||||
import { cardShadow } from "@root/utils/theme";
|
||||
import { useEffect, useLayoutEffect, useState } from "react";
|
||||
import InputTextfield from "@root/components/InputTextfield";
|
||||
import { sendPayment, sendRSPayment } from "@root/api/wallet";
|
||||
import { getMessageFromFetchError } from "@frontend/kitui";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
import { currencyFormatter } from "@root/utils/currencyFormatter";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { useHistoryTracker } from "@root/utils/hooks/useHistoryTracker";
|
||||
import { useUserStore } from "@root/stores/user";
|
||||
import { VerificationStatus } from "@root/model/account";
|
||||
import { WarnModal } from "./WarnModal";
|
||||
|
||||
const paymentMethods = [
|
||||
{ name: "Mastercard", image: mastercardLogo },
|
||||
{ name: "Visa", image: visaLogo },
|
||||
{ name: "QIWI Кошелек", image: qiwiLogo },
|
||||
{ name: "Мир", image: mirLogo },
|
||||
{ name: "Тинькофф", image: tinkoffLogo },
|
||||
] as const
|
||||
type PaymentMethod = {
|
||||
label: string;
|
||||
name: string;
|
||||
image: string;
|
||||
unpopular?: boolean;
|
||||
};
|
||||
|
||||
type PaymentMethod = (typeof paymentMethods)[number]["name"];
|
||||
const paymentMethods: PaymentMethod[] = [
|
||||
{ label: "Mastercard", name: "mastercard", image: mastercardLogo },
|
||||
{ label: "Visa", name: "visa", image: visaLogo },
|
||||
{ label: "QIWI Кошелек", name: "qiwi", image: qiwiLogo },
|
||||
{ label: "Мир", name: "mir", image: mirLogo },
|
||||
{ label: "Тинькофф", name: "tinkoff", image: tinkoffLogo },
|
||||
];
|
||||
|
||||
type PaymentMethodType = (typeof paymentMethods)[number]["name"];
|
||||
|
||||
export default function Payment() {
|
||||
const theme = useTheme()
|
||||
const upMd = useMediaQuery(theme.breakpoints.up("md"))
|
||||
const upSm = useMediaQuery(theme.breakpoints.up("sm"))
|
||||
const isTablet = useMediaQuery(theme.breakpoints.down(1000))
|
||||
const theme = useTheme();
|
||||
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||
const upSm = useMediaQuery(theme.breakpoints.up("sm"));
|
||||
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
|
||||
|
||||
const [selectedPaymentMethod, setSelectedPaymentMethod] =
|
||||
useState<PaymentMethod | null>(null)
|
||||
const [paymentValueField, setPaymentValueField] = useState<string>("0")
|
||||
const [paymentLink, setPaymentLink] = useState<string>("")
|
||||
const [fromSquiz, setIsFromSquiz] = useState<boolean>(false)
|
||||
const location = useLocation()
|
||||
const [selectedPaymentMethod, setSelectedPaymentMethod] =
|
||||
useState<PaymentMethodType | null>(null);
|
||||
const [warnModalOpen, setWarnModalOpen] = useState<boolean>(false);
|
||||
const [paymentValueField, setPaymentValueField] = useState<string>("0");
|
||||
const [paymentLink, setPaymentLink] = useState<string>("");
|
||||
const [fromSquiz, setIsFromSquiz] = useState<boolean>(false);
|
||||
const location = useLocation();
|
||||
const verificationStatus = useUserStore((state) => state.verificationStatus);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const notEnoughMoneyAmount =
|
||||
(location.state?.notEnoughMoneyAmount as number) ?? 0
|
||||
const notEnoughMoneyAmount =
|
||||
(location.state?.notEnoughMoneyAmount as number) ?? 0;
|
||||
|
||||
const paymentValue = parseFloat(paymentValueField) * 100
|
||||
|
||||
useLayoutEffect(() => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
setPaymentValueField((notEnoughMoneyAmount / 100).toString())
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
const fromSquiz = params.get("action")
|
||||
if (fromSquiz === "squizpay") {
|
||||
setIsFromSquiz(true)
|
||||
setPaymentValueField(params.get("dif") || "0")
|
||||
}
|
||||
console.log(fromSquiz)
|
||||
}, [])
|
||||
const paymentValue = parseFloat(paymentValueField) * 100;
|
||||
|
||||
useEffect(() => {
|
||||
setPaymentLink("")
|
||||
}, [selectedPaymentMethod])
|
||||
useLayoutEffect(() => {
|
||||
setPaymentValueField((notEnoughMoneyAmount / 100).toString());
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const fromSquiz = params.get("action");
|
||||
if (fromSquiz === "squizpay") {
|
||||
setIsFromSquiz(true);
|
||||
setPaymentValueField((Number(params.get("dif") || "0") / 100).toString());
|
||||
}
|
||||
history.pushState(null, document.title, "/payment");
|
||||
console.log(fromSquiz);
|
||||
}, []);
|
||||
|
||||
async function handleChoosePaymentClick() {
|
||||
if (Number(paymentValueField) !== 0) {
|
||||
const [sendPaymentResponse, sendPaymentError] = await sendPayment({fromSquiz})
|
||||
useEffect(() => {
|
||||
setPaymentLink("");
|
||||
}, [selectedPaymentMethod]);
|
||||
|
||||
if (sendPaymentError) {
|
||||
return enqueueSnackbar(sendPaymentError)
|
||||
}
|
||||
async function handleChoosePaymentClick() {
|
||||
if (Number(paymentValueField) === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sendPaymentResponse) {
|
||||
setPaymentLink(sendPaymentResponse.link)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (selectedPaymentMethod !== "rspay") {
|
||||
const [sendPaymentResponse, sendPaymentError] = await sendPayment({
|
||||
fromSquiz,
|
||||
});
|
||||
|
||||
const handleCustomBackNavigation = useHistoryTracker()
|
||||
if (sendPaymentError) {
|
||||
return enqueueSnackbar(sendPaymentError);
|
||||
}
|
||||
|
||||
return (
|
||||
<SectionWrapper
|
||||
maxWidth="lg"
|
||||
sx={{
|
||||
mt: "25px",
|
||||
mb: "70px",
|
||||
px: isTablet ? (upMd ? "40px" : "18px") : "20px",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
mt: "20px",
|
||||
mb: "40px",
|
||||
display: "flex",
|
||||
gap: "10px",
|
||||
}}
|
||||
>
|
||||
{!upMd && (
|
||||
<IconButton
|
||||
onClick={handleCustomBackNavigation}
|
||||
sx={{ p: 0, height: "28px", width: "28px", color: "black" }}
|
||||
>
|
||||
<ArrowBackIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
<Typography variant="h4">Способ оплаты</Typography>
|
||||
</Box>
|
||||
{!upMd && (
|
||||
<Typography variant="body2" mb="30px">
|
||||
if (sendPaymentResponse) {
|
||||
setPaymentLink(sendPaymentResponse.link);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const handleCustomBackNavigation = useHistoryTracker();
|
||||
|
||||
return (
|
||||
<SectionWrapper
|
||||
maxWidth="lg"
|
||||
sx={{
|
||||
mt: "25px",
|
||||
mb: "70px",
|
||||
px: isTablet ? (upMd ? "40px" : "18px") : "20px",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
mt: "20px",
|
||||
mb: "40px",
|
||||
display: "flex",
|
||||
gap: "10px",
|
||||
}}
|
||||
>
|
||||
{!upMd && (
|
||||
<IconButton
|
||||
onClick={handleCustomBackNavigation}
|
||||
sx={{ p: 0, height: "28px", width: "28px", color: "black" }}
|
||||
>
|
||||
<ArrowBackIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
<Typography variant="h4">Способ оплаты</Typography>
|
||||
</Box>
|
||||
{!upMd && (
|
||||
<Typography variant="body2" mb="30px">
|
||||
Выберите способ оплаты
|
||||
</Typography>
|
||||
)}
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: upMd ? "white" : undefined,
|
||||
display: "flex",
|
||||
flexDirection: upMd ? "row" : "column",
|
||||
borderRadius: "12px",
|
||||
boxShadow: upMd ? cardShadow : undefined,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: upMd ? "68.5%" : undefined,
|
||||
p: upMd ? "20px" : undefined,
|
||||
display: "flex",
|
||||
flexDirection: upSm ? "row" : "column",
|
||||
flexWrap: "wrap",
|
||||
gap: upMd ? "14px" : "20px",
|
||||
alignContent: "start",
|
||||
}}
|
||||
>
|
||||
{paymentMethods.map((method) => (
|
||||
<PaymentMethodCard
|
||||
isSelected={selectedPaymentMethod === method.name}
|
||||
key={method.name}
|
||||
name={method.name}
|
||||
image={method.image}
|
||||
onClick={() => setSelectedPaymentMethod(method.name)}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "start",
|
||||
color: theme.palette.gray.dark,
|
||||
width: upMd ? "31.5%" : undefined,
|
||||
p: upMd ? "20px" : undefined,
|
||||
pl: upMd ? "33px" : undefined,
|
||||
mt: upMd ? undefined : "30px",
|
||||
borderLeft: upMd
|
||||
? `1px solid ${theme.palette.gray.main}`
|
||||
: undefined,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
maxWidth: "85%",
|
||||
}}
|
||||
>
|
||||
{upMd && <Typography mb="56px">Выберите способ оплаты</Typography>}
|
||||
<Typography mb="20px">К оплате</Typography>
|
||||
{paymentLink ? (
|
||||
<Typography
|
||||
sx={{
|
||||
fontWeight: 500,
|
||||
fontSize: "20px",
|
||||
lineHeight: "48px",
|
||||
mb: "28px",
|
||||
}}
|
||||
>
|
||||
{currencyFormatter.format(paymentValue / 100)}
|
||||
</Typography>
|
||||
) : (
|
||||
<InputTextfield
|
||||
TextfieldProps={{
|
||||
placeholder: "К оплате",
|
||||
value: paymentValueField,
|
||||
type: "number",
|
||||
}}
|
||||
onChange={(e) => setPaymentValueField(e.target.value)}
|
||||
id="payment-amount"
|
||||
gap={upMd ? "16px" : "10px"}
|
||||
color={"#F2F3F7"}
|
||||
FormInputSx={{ mb: "28px" }}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
{paymentLink ? (
|
||||
<Button
|
||||
variant="pena-outlined-light"
|
||||
component="a"
|
||||
href={paymentLink}
|
||||
sx={{
|
||||
mt: "auto",
|
||||
color: "black",
|
||||
border: `1px solid ${theme.palette.purple.main}`,
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.purple.dark,
|
||||
border: `1px solid ${theme.palette.purple.dark}`,
|
||||
},
|
||||
}}
|
||||
>
|
||||
</Typography>
|
||||
)}
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: upMd ? "white" : undefined,
|
||||
display: "flex",
|
||||
flexDirection: upMd ? "row" : "column",
|
||||
borderRadius: "12px",
|
||||
boxShadow: upMd ? cardShadow : undefined,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: upMd ? "68.5%" : undefined,
|
||||
p: upMd ? "20px" : undefined,
|
||||
display: "flex",
|
||||
flexDirection: upSm ? "row" : "column",
|
||||
flexWrap: "wrap",
|
||||
gap: upMd ? "14px" : "20px",
|
||||
alignContent: "start",
|
||||
}}
|
||||
>
|
||||
{paymentMethods.map(({ name, label, image, unpopular = false }) => (
|
||||
<PaymentMethodCard
|
||||
isSelected={selectedPaymentMethod === name}
|
||||
key={name}
|
||||
label={label}
|
||||
image={image}
|
||||
onClick={() => setSelectedPaymentMethod(name)}
|
||||
unpopular={unpopular}
|
||||
/>
|
||||
))}
|
||||
<PaymentMethodCard
|
||||
isSelected={false}
|
||||
label={"Расчётный счёт"}
|
||||
image={rsPayLogo}
|
||||
onClick={async() => {
|
||||
|
||||
if (verificationStatus !== VerificationStatus.VERIFICATED) {
|
||||
setWarnModalOpen(true);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const sendRSPaymentError = await sendRSPayment();
|
||||
|
||||
if (sendRSPaymentError) {
|
||||
return enqueueSnackbar(sendRSPaymentError);
|
||||
}
|
||||
|
||||
enqueueSnackbar(
|
||||
"Cпасибо за заявку, в течении 24 часов вам будет выставлен счёт для оплаты услуг."
|
||||
);
|
||||
|
||||
navigate("/settings");
|
||||
}}
|
||||
unpopular={true}
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "start",
|
||||
color: theme.palette.gray.dark,
|
||||
width: upMd ? "31.5%" : undefined,
|
||||
p: upMd ? "20px" : undefined,
|
||||
pl: upMd ? "33px" : undefined,
|
||||
mt: upMd ? undefined : "30px",
|
||||
borderLeft: upMd
|
||||
? `1px solid ${theme.palette.gray.main}`
|
||||
: undefined,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
maxWidth: "85%",
|
||||
}}
|
||||
>
|
||||
{upMd && <Typography mb="56px">Выберите способ оплаты</Typography>}
|
||||
<Typography mb="20px">К оплате</Typography>
|
||||
{paymentLink ? (
|
||||
<Typography
|
||||
sx={{
|
||||
fontWeight: 500,
|
||||
fontSize: "20px",
|
||||
lineHeight: "48px",
|
||||
mb: "28px",
|
||||
}}
|
||||
>
|
||||
{currencyFormatter.format(paymentValue / 100)}
|
||||
</Typography>
|
||||
) : (
|
||||
<InputTextfield
|
||||
TextfieldProps={{
|
||||
placeholder: "К оплате",
|
||||
value: paymentValueField,
|
||||
type: "number",
|
||||
}}
|
||||
onChange={(e) => setPaymentValueField(e.target.value)}
|
||||
id="payment-amount"
|
||||
gap={upMd ? "16px" : "10px"}
|
||||
color={"#F2F3F7"}
|
||||
FormInputSx={{ mb: "28px" }}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
{paymentLink ? (
|
||||
<Button
|
||||
variant="pena-outlined-light"
|
||||
component="a"
|
||||
href={paymentLink}
|
||||
sx={{
|
||||
mt: "auto",
|
||||
color: "black",
|
||||
border: `1px solid ${theme.palette.purple.main}`,
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.purple.dark,
|
||||
border: `1px solid ${theme.palette.purple.dark}`,
|
||||
},
|
||||
}}
|
||||
>
|
||||
Оплатить
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant="pena-outlined-light"
|
||||
disabled={!isFinite(paymentValue)}
|
||||
onClick={handleChoosePaymentClick}
|
||||
sx={{
|
||||
mt: "auto",
|
||||
color: "black",
|
||||
border: `1px solid ${theme.palette.purple.main}`,
|
||||
"&:hover": {
|
||||
color: "white",
|
||||
},
|
||||
"&:active": {
|
||||
color: "white",
|
||||
},
|
||||
}}
|
||||
>
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant="pena-outlined-light"
|
||||
disabled={!isFinite(paymentValue)}
|
||||
onClick={handleChoosePaymentClick}
|
||||
sx={{
|
||||
mt: "auto",
|
||||
color: "black",
|
||||
border: `1px solid ${theme.palette.purple.main}`,
|
||||
"&:hover": {
|
||||
color: "white",
|
||||
},
|
||||
"&:active": {
|
||||
color: "white",
|
||||
},
|
||||
}}
|
||||
>
|
||||
Выбрать
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</SectionWrapper>
|
||||
)
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
<WarnModal open={warnModalOpen} setOpen={setWarnModalOpen} />
|
||||
</SectionWrapper>
|
||||
);
|
||||
}
|
||||
|
@ -1,43 +1,55 @@
|
||||
import { Button, Typography, useMediaQuery, useTheme } from "@mui/material"
|
||||
import { Button, Typography, useMediaQuery, useTheme } from "@mui/material";
|
||||
|
||||
interface Props {
|
||||
name: string;
|
||||
image: string;
|
||||
isSelected?: boolean;
|
||||
onClick: () => void;
|
||||
label: string;
|
||||
image: string;
|
||||
isSelected?: boolean;
|
||||
unpopular?: boolean;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export default function PaymentMethodCard({ name, image, isSelected, onClick }: Props) {
|
||||
const theme = useTheme()
|
||||
const upSm = useMediaQuery(theme.breakpoints.up("sm"))
|
||||
export default function PaymentMethodCard({
|
||||
label,
|
||||
image,
|
||||
isSelected,
|
||||
unpopular,
|
||||
onClick,
|
||||
}: Props) {
|
||||
const theme = useTheme();
|
||||
const upSm = useMediaQuery(theme.breakpoints.up("sm"));
|
||||
|
||||
return (
|
||||
<Button
|
||||
sx={{
|
||||
width: upSm ? "237px" : "100%",
|
||||
p: "20px",
|
||||
pr: "10px",
|
||||
display: "flex",
|
||||
justifyContent: "start",
|
||||
borderRadius: "8px",
|
||||
backgroundColor: theme.palette.background.default,
|
||||
border: isSelected ? `1px solid ${theme.palette.purple.main}` : `1px solid ${theme.palette.gray.main}`,
|
||||
gap: "20px",
|
||||
alignItems: "center",
|
||||
flexWrap: "wrap",
|
||||
boxShadow: isSelected ? `0 0 0 1.5px ${theme.palette.purple.main};` : "none",
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.purple.main,
|
||||
border: `1px solid ${theme.palette.purple.main}`,
|
||||
"& > p": {
|
||||
color: "white",
|
||||
}
|
||||
},
|
||||
}}
|
||||
onClick={onClick}
|
||||
>
|
||||
<img src={image} alt="payment method" />
|
||||
<Typography sx={{ color: theme.palette.gray.dark }}>{name}</Typography>
|
||||
</Button>
|
||||
)
|
||||
return (
|
||||
<Button
|
||||
sx={{
|
||||
width: upSm ? "237px" : "100%",
|
||||
p: "20px",
|
||||
pr: "10px",
|
||||
display: "flex",
|
||||
justifyContent: "start",
|
||||
borderRadius: "8px",
|
||||
filter: unpopular ? "saturate(0.6) brightness(0.85)" : null,
|
||||
backgroundColor: theme.palette.background.default,
|
||||
border: isSelected
|
||||
? `1px solid ${theme.palette.purple.main}`
|
||||
: `1px solid ${theme.palette.gray.main}`,
|
||||
gap: "15px",
|
||||
alignItems: "center",
|
||||
flexWrap: "wrap",
|
||||
boxShadow: isSelected
|
||||
? `0 0 0 1.5px ${theme.palette.purple.main};`
|
||||
: "none",
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.purple.main,
|
||||
border: `1px solid ${theme.palette.purple.main}`,
|
||||
"& > p": {
|
||||
color: "white",
|
||||
},
|
||||
},
|
||||
}}
|
||||
onClick={onClick}
|
||||
>
|
||||
<img src={image} alt="payment method" />
|
||||
<Typography sx={{ color: theme.palette.gray.dark }}>{label}</Typography>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
61
src/pages/Payment/WarnModal.tsx
Normal file
@ -0,0 +1,61 @@
|
||||
import { Modal, Box, Typography, Button, useTheme } from "@mui/material";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
type WarnModalProps = {
|
||||
open: boolean;
|
||||
setOpen: (isOpen: boolean) => void;
|
||||
};
|
||||
|
||||
export const WarnModal = ({ open, setOpen }: WarnModalProps) => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
margin: "10px",
|
||||
padding: "25px",
|
||||
maxWidth: "600px",
|
||||
borderRadius: "5px",
|
||||
textAlign: "center",
|
||||
background: theme.palette.background.default,
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<Typography id="modal-modal-title" variant="h6" component="h2">
|
||||
Верификация не пройдена.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
flexWrap: "wrap",
|
||||
gap: "20px",
|
||||
marginTop: "15px",
|
||||
}}
|
||||
>
|
||||
<Button variant="pena-outlined-purple" onClick={() => setOpen(false)}>
|
||||
Отмена
|
||||
</Button>
|
||||
<Button
|
||||
variant="pena-outlined-purple"
|
||||
onClick={() => navigate("/settings")}
|
||||
>
|
||||
Пройти верификацию
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
};
|
72
src/pages/QuizPayment/QuizPayment.tsx
Normal file
@ -0,0 +1,72 @@
|
||||
import { useEffect, useState } from "react"
|
||||
import axios, { AxiosResponse } from "axios"
|
||||
import { ApologyPage } from "../ApologyPage"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import { clearAuthToken, getMessageFromFetchError, setAuthToken, useUserAccountFetcher, useUserFetcher } from "@frontend/kitui";
|
||||
import { clearUserData, setUser, setUserAccount, useUserStore } from "@root/stores/user";
|
||||
|
||||
|
||||
function refresh(token: string) {
|
||||
return axios<never, AxiosResponse<{ accessToken: string; }>>(process.env.REACT_APP_DOMAIN + "/auth/refresh", {
|
||||
headers: {
|
||||
"Authorization": "Bearer " + token,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: "POST"
|
||||
});
|
||||
}
|
||||
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
const action = params.get("action")
|
||||
const dif = params.get("dif")
|
||||
const token = params.get("data")
|
||||
const userId = params.get("userid")
|
||||
|
||||
|
||||
let first = true
|
||||
|
||||
export default function QuizPayment() {
|
||||
const navigate = useNavigate()
|
||||
const [message, setMessage] = useState("Идёт загрузка")
|
||||
|
||||
console.log("Я начал работать")
|
||||
|
||||
if (first) {
|
||||
history.pushState(null, document.title, "/quizpayment");
|
||||
try {
|
||||
first = false
|
||||
if (action && dif && token) {
|
||||
(async () => {
|
||||
// const data = await refresh(token)
|
||||
console.log(token)
|
||||
setAuthToken(token)
|
||||
// setAuthToken(data.data.accessToken)
|
||||
console.log("делаем юзера")
|
||||
|
||||
useUserFetcher({
|
||||
url: process.env.REACT_APP_DOMAIN + `/user/${userId}`,
|
||||
userId,
|
||||
onNewUser: (user) => {
|
||||
setUser(user)
|
||||
navigate(`/payment?action=${action}&dif=${dif}`, { replace: true })
|
||||
|
||||
},
|
||||
onError: () => { },
|
||||
})
|
||||
return
|
||||
})()
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
setMessage("Произошла ошибка")
|
||||
var link = document.createElement("a");
|
||||
link.href = "https://quiz.pena.digital/tariffs";
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ApologyPage message={message} />
|
||||
)
|
||||
};
|
@ -44,6 +44,7 @@ function TariffConstructor() {
|
||||
>
|
||||
{Object.entries(customTariffs).filter(([serviceKey]) => serviceKey === "squiz").map(([serviceKey, privileges], index) => {
|
||||
console.log("serviceKey ",serviceKey)
|
||||
console.log(Object.entries(customTariffs))
|
||||
return <Box key={index}>
|
||||
<Box
|
||||
sx={{
|
||||
@ -65,7 +66,7 @@ function TariffConstructor() {
|
||||
<ArrowBackIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
<ComplexHeader text1="Мой тариф " text2={serviceNameByKey[serviceKey]} />
|
||||
<ComplexHeader text1="Мой тариф " text2={serviceNameByKey[serviceKey] === "Опросник" ? "PenaQuiz" : serviceNameByKey[serviceKey]} />
|
||||
</Box>
|
||||
<CustomTariffCard serviceKey={serviceKey} privileges={privileges} />
|
||||
</Box>
|
||||
|
@ -13,11 +13,12 @@ import { useEffect, useState } from "react"
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
|
||||
const sliderSettingsByType = {
|
||||
день: { max: 365, min: 30 },
|
||||
шаблон: { max: 5000, min: 100 },
|
||||
МБ: { max: 5000, min: 100 },
|
||||
день: { max: 365, min: 0 },
|
||||
шаблон: { max: 5000, min: 0 },
|
||||
МБ: { max: 5000, min: 0 },
|
||||
заявка: { max: 5000, min: 0 }
|
||||
}
|
||||
type PrivilegeName = "день" | "шаблон" | "МБ"
|
||||
type PrivilegeName = "день" | "шаблон" | "МБ" | "заявка"
|
||||
|
||||
interface Props {
|
||||
privilege: Privilege;
|
||||
@ -26,10 +27,10 @@ interface Props {
|
||||
export default function TariffPrivilegeSlider({ privilege }: Props) {
|
||||
const theme = useTheme()
|
||||
const upMd = useMediaQuery(theme.breakpoints.up("md"))
|
||||
const userValue = useCustomTariffsStore((state) => state.userValuesMap[privilege.serviceKey]?.[privilege._id]) ?? 0
|
||||
const userValue = useCustomTariffsStore((state) => state.userValuesMap[privilege.serviceKey]?.[privilege._id]) ?? sliderSettingsByType[privilege.value]?.min
|
||||
const discounts = useDiscountStore((state) => state.discounts)
|
||||
const currentCartTotal = useCartStore((state) => state.cart.priceAfterDiscounts)
|
||||
const purchasesAmount = useUserStore((state) => state.userAccount?.wallet.spent) ?? 0
|
||||
const purchasesAmount = useUserStore((state) => state.userAccount?.wallet.spent) ?? sliderSettingsByType[privilege.value]?.min
|
||||
const isUserNko = useUserStore(state => state.userAccount?.status) === "nko"
|
||||
const [value, setValue] = useState<number>(userValue)
|
||||
const throttledValue = useThrottle(value, 200)
|
||||
@ -67,10 +68,17 @@ export default function TariffPrivilegeSlider({ privilege }: Props) {
|
||||
|
||||
|
||||
const setNotSmallNumber = useDebouncedCallback(() => {
|
||||
if (value === 0) return
|
||||
if (value === sliderSettingsByType[privilege.value]?.min) return
|
||||
if (Number(value) < Number(sliderSettingsByType[privilege.value]?.min)) {
|
||||
setValue(sliderSettingsByType[privilege.value]?.min)
|
||||
}
|
||||
if (privilege.value === "день" && Number(value) < 30 && Number(value) !== 0) {
|
||||
setValue(30)
|
||||
}
|
||||
|
||||
if (privilege.value !== "день" && Number(value) < 100 && Number(value) !== 0) {
|
||||
setValue(100)
|
||||
}
|
||||
}, 600)
|
||||
|
||||
const quantityElement = (
|
||||
@ -99,6 +107,7 @@ export default function TariffPrivilegeSlider({ privilege }: Props) {
|
||||
<NumberInputWithUnitAdornment
|
||||
id={"privilege_input_" + privilege._id}
|
||||
value={value}
|
||||
privilege={privilege}
|
||||
adornmentText={getDeclension(0, privilege.value)}
|
||||
onChange={(value) => {
|
||||
setValue(value)
|
||||
@ -152,9 +161,10 @@ export default function TariffPrivilegeSlider({ privilege }: Props) {
|
||||
</Box>
|
||||
<CustomSlider
|
||||
value={value}
|
||||
min={0}
|
||||
min={sliderSettingsByType[privilege.value]?.min }
|
||||
max={sliderSettingsByType[privilege.value]?.max || 100}
|
||||
onChange={handleSliderChange(privilege.value)}
|
||||
firstStep={privilege.value === "день" ? 30 : 100}
|
||||
/>
|
||||
{!upMd && quantityElement}
|
||||
</Box>
|
||||
|
@ -85,7 +85,22 @@ export default function Tariffs() {
|
||||
</Typography>
|
||||
|
||||
{/*{upMd ? <WideTemplCard sx={{ marginTop: "55px" }} /> : <TemplCardPhoneLight />}*/}
|
||||
{upMd ? <WideTemplCard sx={{ marginTop: "55px" }} name={"PenaQuiz"} desc={"Конструктор quiz опросов, для любых видов исследований и quiz маркетинга, арбитража трафика"} image={CardImage2}/> : <TemplCardPhoneLight name={"PenaQuiz"} desc={"Конструктор quiz опросов, для любых видов исследований и quiz маркетинга, арбитража трафика"} image={CardImage2}/>}
|
||||
{upMd ?
|
||||
<WideTemplCard
|
||||
sx={{ marginTop: "55px" }}
|
||||
name={"PenaQuiz"}
|
||||
desc={"Конструктор quiz опросов, для любых видов исследований и quiz маркетинга, арбитража трафика"}
|
||||
image={CardImage2}
|
||||
href={"https://quiz.pena.digital"}
|
||||
/>
|
||||
:
|
||||
<TemplCardPhoneLight
|
||||
name={"PenaQuiz"}
|
||||
desc={"Конструктор quiz опросов, для любых видов исследований и quiz маркетинга, арбитража трафика"}
|
||||
image={CardImage2}
|
||||
href={"https://quiz.pena.digital"}
|
||||
/>
|
||||
}
|
||||
</SectionWrapper>
|
||||
)
|
||||
}
|
||||
|
@ -178,11 +178,11 @@ function TariffPage() {
|
||||
{StepperText[unit]}
|
||||
</Typography>
|
||||
</Box>
|
||||
{isMobile ? (
|
||||
{/* {isMobile ? (
|
||||
<Select items={subPages} selectedItem={selectedItem} setSelectedItem={setSelectedItem} />
|
||||
) : (
|
||||
<Tabs items={subPages} selectedItem={selectedItem} setSelectedItem={setSelectedItem} />
|
||||
)}
|
||||
)} */}
|
||||
<Box
|
||||
sx={{
|
||||
justifyContent: "left",
|
||||
@ -195,20 +195,20 @@ function TariffPage() {
|
||||
>
|
||||
{createTariffElements(filteredTariffs, true)}
|
||||
</Box>
|
||||
{recentlyPurchased.length > 0 && (
|
||||
<>
|
||||
<Typography
|
||||
sx={{
|
||||
mt: "40px",
|
||||
fontSize: isMobile ? "24px" : "36px",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
Ранее вы
|
||||
</Typography>
|
||||
<Slider items={createTariffElements(recentlyPurchased)} />
|
||||
</>
|
||||
)}
|
||||
{/*{recentlyPurchased.length > 0 && (*/}
|
||||
{/* <>*/}
|
||||
{/* <Typography*/}
|
||||
{/* sx={{*/}
|
||||
{/* mt: "40px",*/}
|
||||
{/* fontSize: isMobile ? "24px" : "36px",*/}
|
||||
{/* fontWeight: "500",*/}
|
||||
{/* }}*/}
|
||||
{/* >*/}
|
||||
{/* Ранее вы*/}
|
||||
{/* </Typography>*/}
|
||||
{/* <Slider items={createTariffElements(recentlyPurchased)} />*/}
|
||||
{/* </>*/}
|
||||
{/*)}*/}
|
||||
</SectionWrapper>
|
||||
);
|
||||
}
|
||||
|
@ -41,7 +41,7 @@ const validationSchema = object({
|
||||
.email("Введите корректный email"),
|
||||
password: string()
|
||||
.min(8, "Минимум 8 символов")
|
||||
.matches(/^[.,:;-_+\d\w]+$/, "Некорректные символы")
|
||||
.matches(/^[.,:;\-_+!&()<>\[\]\{\}`@"#$\%\^\=?\d\w]+$/, "Некорректные символы")
|
||||
.required("Поле обязательно"),
|
||||
repeatPassword: string()
|
||||
.oneOf([ref("password"), undefined], "Пароли не совпадают")
|
||||
|
@ -2,5 +2,5 @@ export const serviceNameByKey: Record<string, string | undefined> = {
|
||||
templategen: "Шаблонизатор",
|
||||
squiz: "Опросник",
|
||||
reducer: "Сокращатель ссылок",
|
||||
custom: "Кастомные тарифы",
|
||||
custom: "Мои тарифы",
|
||||
}
|
||||
|