fix: conflicts resolved

This commit is contained in:
IlyaDoronin 2024-02-15 11:33:11 +03:00
commit 2d6affab57
37 changed files with 992 additions and 602 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 15 KiB

BIN
public/favicon.png Normal file

Binary file not shown.

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

BIN
public/favicon512.png Normal file

Binary file not shown.

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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

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

Binary file not shown.

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

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

@ -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: "Мои тарифы",
}