ТП
This commit is contained in:
parent
c9475e497c
commit
5cf876067d
1
.yarnrc
Normal file
1
.yarnrc
Normal file
@ -0,0 +1 @@
|
||||
"@frontend:registry" "https://penahub.gitlab.yandexcloud.net/api/v4/packages/npm/"
|
17
craco.config.js
Normal file
17
craco.config.js
Normal file
@ -0,0 +1,17 @@
|
||||
const CracoAlias = require("craco-alias");
|
||||
|
||||
module.exports = {
|
||||
plugins: [
|
||||
{
|
||||
plugin: CracoAlias,
|
||||
options: {
|
||||
source: "options",
|
||||
baseUrl: "./src",
|
||||
aliases: {
|
||||
"@root": "./",
|
||||
"@stores": "./stores"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
@ -3,9 +3,11 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@craco/craco": "^7.1.0",
|
||||
"@emotion/react": "^11.10.4",
|
||||
"@emotion/styled": "^11.10.4",
|
||||
"@fontsource/roboto": "^4.5.8",
|
||||
"@frontend/kitui": "^1.0.17",
|
||||
"@mui/icons-material": "^5.10.9",
|
||||
"@mui/material": "^5.10.10",
|
||||
"@mui/styled-engine-sc": "^5.10.6",
|
||||
@ -14,6 +16,7 @@
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"axios": "^1.4.0",
|
||||
"craco-alias": "^3.0.1",
|
||||
"notistack": "^3.0.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
|
@ -9,6 +9,7 @@ import Effect from './effect.js';
|
||||
import Widget from './widget.js';
|
||||
import Footer from './footer';
|
||||
import { Outlet } from "react-router-dom";
|
||||
import FloatingSupportChat from "./floatingSupportChat/FloatingSupportChat";
|
||||
|
||||
export default function Component() {
|
||||
return(
|
||||
@ -22,7 +23,7 @@ export default function Component() {
|
||||
<Effect/>
|
||||
<Widget/>
|
||||
<Footer />
|
||||
<Outlet />
|
||||
<FloatingSupportChat/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
259
src/floatingSupportChat/Chat.js
Normal file
259
src/floatingSupportChat/Chat.js
Normal file
@ -0,0 +1,259 @@
|
||||
import { Box, FormControl, IconButton, InputAdornment, InputBase, SxProps, Theme, Typography, useMediaQuery, useTheme } from "@mui/material";
|
||||
import { TicketMessage } from "@frontend/kitui";
|
||||
import { addOrUpdateUnauthMessages, useUnauthTicketStore, incrementUnauthMessageApiPage, setUnauthIsPreventAutoscroll, setUnauthSessionData, setIsMessageSending } from "../stores/unauthTicket";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import ChatMessage from "./ChatMessage";
|
||||
import SendIcon from "./SendIcon";
|
||||
import UserCircleIcon from "./UserCircleIcon";
|
||||
import { throttle } from "@frontend/kitui";
|
||||
import { makeRequest } from "@frontend/kitui";
|
||||
import { useTicketMessages, getMessageFromFetchError, useSSESubscription, useEventListener, createTicket } from "@frontend/kitui";
|
||||
|
||||
|
||||
export default function Chat({ sx }) {
|
||||
const theme = useTheme();
|
||||
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||
const [messageField, setMessageField] = useState("");
|
||||
const sessionData = useUnauthTicketStore(state => state.sessionData);
|
||||
const messages = useUnauthTicketStore(state => state.messages);
|
||||
const messageApiPage = useUnauthTicketStore(state => state.apiPage);
|
||||
const messagesPerPage = useUnauthTicketStore(state => state.messagesPerPage);
|
||||
const isMessageSending = useUnauthTicketStore(state => state.isMessageSending);
|
||||
const isPreventAutoscroll = useUnauthTicketStore(state => state.isPreventAutoscroll);
|
||||
const lastMessageId = useUnauthTicketStore(state => state.lastMessageId);
|
||||
const chatBoxRef = useRef(null);
|
||||
|
||||
const fetchState = useTicketMessages({
|
||||
url: "https://admin.pena.digital/heruvym/getMessages",
|
||||
isUnauth: true,
|
||||
ticketId: sessionData?.ticketId,
|
||||
messagesPerPage,
|
||||
messageApiPage,
|
||||
onNewMessages: useCallback(messages => {
|
||||
if (chatBoxRef.current && chatBoxRef.current.scrollTop < 1) chatBoxRef.current.scrollTop = 1;
|
||||
addOrUpdateUnauthMessages(messages);
|
||||
}, []),
|
||||
onError: useCallback((error) => {
|
||||
const message = getMessageFromFetchError(error);
|
||||
if (message) enqueueSnackbar(message);
|
||||
}, []),
|
||||
});
|
||||
|
||||
useSSESubscription({
|
||||
enabled: Boolean(sessionData),
|
||||
url: `https://hub.pena.digital/heruvym/ticket?ticket=${sessionData?.ticketId}&s=${sessionData?.sessionId}`,
|
||||
onNewData: addOrUpdateUnauthMessages,
|
||||
onDisconnect: useCallback(() => {
|
||||
setUnauthIsPreventAutoscroll(false);
|
||||
}, []),
|
||||
marker: "ticket"
|
||||
});
|
||||
|
||||
const throttledScrollHandler = useMemo(() => throttle(() => {
|
||||
const chatBox = chatBoxRef.current;
|
||||
if (!chatBox) return;
|
||||
|
||||
const scrollBottom = chatBox.scrollHeight - chatBox.scrollTop - chatBox.clientHeight;
|
||||
const isPreventAutoscroll = scrollBottom > chatBox.clientHeight;
|
||||
setUnauthIsPreventAutoscroll(isPreventAutoscroll);
|
||||
|
||||
if (fetchState !== "idle") return;
|
||||
|
||||
if (chatBox.scrollTop < chatBox.clientHeight) {
|
||||
incrementUnauthMessageApiPage();
|
||||
}
|
||||
}, 200), [fetchState]);
|
||||
|
||||
useEventListener("scroll", throttledScrollHandler, chatBoxRef);
|
||||
|
||||
useEffect(function scrollOnNewMessage() {
|
||||
if (!chatBoxRef.current) return;
|
||||
|
||||
if (!isPreventAutoscroll) {
|
||||
setTimeout(() => {
|
||||
scrollToBottom();
|
||||
}, 50);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [lastMessageId]);
|
||||
|
||||
async function handleSendMessage() {
|
||||
if (!messageField || isMessageSending) return;
|
||||
|
||||
if (!sessionData) {
|
||||
setIsMessageSending(true);
|
||||
createTicket({
|
||||
url: "https://hub.pena.digital/heruvym/create",
|
||||
body: {
|
||||
Title: "Unauth title",
|
||||
Message: messageField,
|
||||
},
|
||||
useToken: false,
|
||||
}).then(response => {
|
||||
setUnauthSessionData({
|
||||
ticketId: response.Ticket,
|
||||
sessionId: response.sess,
|
||||
});
|
||||
}).catch(error => {
|
||||
const errorMessage = getMessageFromFetchError(error);
|
||||
if (errorMessage) enqueueSnackbar(errorMessage);
|
||||
}).finally(() => {
|
||||
setMessageField("");
|
||||
setIsMessageSending(false);
|
||||
});
|
||||
} else {
|
||||
setIsMessageSending(true);
|
||||
makeRequest({
|
||||
url: "https://hub.pena.digital/heruvym/send",
|
||||
method: "POST",
|
||||
useToken: false,
|
||||
body: {
|
||||
ticket: sessionData.ticketId,
|
||||
message: messageField,
|
||||
lang: "ru",
|
||||
files: [],
|
||||
},
|
||||
withCredentials: true,
|
||||
}).catch(error => {
|
||||
const errorMessage = getMessageFromFetchError(error);
|
||||
if (errorMessage) enqueueSnackbar(errorMessage);
|
||||
}).finally(() => {
|
||||
setMessageField("");
|
||||
setIsMessageSending(false);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function scrollToBottom(behavior) {
|
||||
if (!chatBoxRef.current) return;
|
||||
|
||||
const chatBox = chatBoxRef.current;
|
||||
chatBox.scroll({
|
||||
left: 0,
|
||||
top: chatBox.scrollHeight,
|
||||
behavior,
|
||||
});
|
||||
}
|
||||
|
||||
const handleTextfieldKeyPress = (e) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSendMessage();
|
||||
}
|
||||
};
|
||||
|
||||
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 />
|
||||
<Box sx={{
|
||||
mt: "5px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "3px",
|
||||
}}>
|
||||
<Typography>Мария</Typography>
|
||||
<Typography sx={{
|
||||
fontSize: "16px",
|
||||
lineHeight: "19px",
|
||||
}}>онлайн-консультант</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}
|
||||
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>
|
||||
);
|
||||
}
|
83
src/floatingSupportChat/ChatMessage.js
Normal file
83
src/floatingSupportChat/ChatMessage.js
Normal file
@ -0,0 +1,83 @@
|
||||
import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
|
||||
import { isDateToday } from "./date";
|
||||
|
||||
|
||||
export default function ChatMessage({ unAuthenticated = false, isSelf, text, createdAt }) {
|
||||
const theme = useTheme();
|
||||
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||
|
||||
const messageBackgroundColor = isSelf ? "white" : unAuthenticated ? "#EFF0F5" : theme.palette.grey2.main;
|
||||
|
||||
const date = new Date(createdAt);
|
||||
const time = date.toLocaleString([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
...(!isDateToday(date) && { 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,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
alignSelf: "end",
|
||||
fontWeight: 400,
|
||||
fontSize: "14px",
|
||||
lineHeight: "17px",
|
||||
order: isSelf ? 1 : 2,
|
||||
color: theme.palette.grey2.main,
|
||||
mb: "-4px",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>{time}</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: messageBackgroundColor,
|
||||
border: unAuthenticated ? `1px solid #E3E3E3` : `1px solid ${theme.palette.grey2.main}`,
|
||||
order: isSelf ? 2 : 1,
|
||||
p: upMd ? "18px" : "12px",
|
||||
borderRadius: "8px",
|
||||
maxWidth: "464px",
|
||||
color: (isSelf || unAuthenticated) ? theme.palette.grey3.main : "white",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "-1px",
|
||||
right: isSelf ? "-8px" : undefined,
|
||||
left: isSelf ? undefined : "-8px",
|
||||
transform: isSelf ? undefined : "scale(-1, 1)",
|
||||
}}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="8"
|
||||
viewBox="0 0 16 8"
|
||||
fill="none"
|
||||
>
|
||||
<path d="M0.5 0.5L15.5 0.500007
|
||||
C10 0.500006 7.5 8 7.5 7.5H7.5H0.5V0.5Z"
|
||||
fill={messageBackgroundColor}
|
||||
stroke={unAuthenticated ? "#E3E3E3" : theme.palette.grey2.main}
|
||||
/>
|
||||
<rect y="1" width="8" height="8" fill={messageBackgroundColor} />
|
||||
</svg>
|
||||
<Typography
|
||||
sx={{
|
||||
fontWeight: 400,
|
||||
fontSize: "16px",
|
||||
lineHeight: "19px",
|
||||
}}
|
||||
>{text}</Typography>
|
||||
</Box>
|
||||
</Box >
|
||||
);
|
||||
}
|
22
src/floatingSupportChat/CircleDoubleDownIcon.js
Normal file
22
src/floatingSupportChat/CircleDoubleDownIcon.js
Normal file
@ -0,0 +1,22 @@
|
||||
import { Box } from "@mui/material";
|
||||
|
||||
export default function CircleDoubleDown({ isUp = false }) {
|
||||
|
||||
return (
|
||||
<Box sx={{
|
||||
width: "32px",
|
||||
height: "32px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
flexShrink: 0,
|
||||
transform: isUp ? "scale(1, -1)" : undefined,
|
||||
}}>
|
||||
<svg width="33" height="32" viewBox="0 0 33 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16.9004 4C10.273 4 4.90039 9.37258 4.90039 16C4.90039 22.6274 10.273 28 16.9004 28C23.5278 28 28.9004 22.6274 28.9004 16C28.9004 9.37258 23.5278 4 16.9004 4Z" stroke="#252734" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M12.9004 21L16.9004 17L20.9004 21" stroke="#252734" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M12.9004 14L16.9004 10L20.9004 14" stroke="#252734" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</Box>
|
||||
);
|
||||
}
|
94
src/floatingSupportChat/FloatingSupportChat.js
Normal file
94
src/floatingSupportChat/FloatingSupportChat.js
Normal file
@ -0,0 +1,94 @@
|
||||
import { Box, Fab, Typography } from "@mui/material";
|
||||
import { useState } from "react";
|
||||
import CircleDoubleDown from "./CircleDoubleDownIcon";
|
||||
import Chat from "./Chat";
|
||||
|
||||
export default function FloatingSupportChat() {
|
||||
const [isChatOpened, setIsChatOpened] = useState(false);
|
||||
|
||||
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,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<CircleDoubleDown isUp={isChatOpened} />
|
||||
{!isChatOpened && <Typography sx={{ zIndex: "10000" }}>Задайте нам вопрос</Typography>}
|
||||
</Fab>
|
||||
</Box>
|
||||
);
|
||||
}
|
10
src/floatingSupportChat/SendIcon.js
Normal file
10
src/floatingSupportChat/SendIcon.js
Normal file
@ -0,0 +1,10 @@
|
||||
export default function SendIcon({ style }) {
|
||||
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="45" height="45" viewBox="0 0 45 45" fill="none" style={style}>
|
||||
<circle cx="22.5" cy="22.5" r="22.5" fill="#944FEE" />
|
||||
<path d="M33.8489 22.1816L15.9232 12.1415C15.7722 12.0581 15.5994 12.0227 15.4277 12.0399C15.2561 12.0571 15.0938 12.1263 14.9624 12.2381C14.831 12.3498 14.7368 12.499 14.6923 12.6657C14.6478 12.8323 14.6551 13.0086 14.7133 13.171L18.0883 22.638C18.1627 22.8218 18.1627 23.0273 18.0883 23.2111L14.7133 32.6781C14.6551 32.8405 14.6478 33.0167 14.6923 33.1834C14.7368 33.3501 14.831 33.4992 14.9624 33.611C15.0938 33.7228 15.2561 33.7919 15.4277 33.8092C15.5994 33.8264 15.7722 33.791 15.9232 33.7076L33.8489 23.6675C33.9816 23.594 34.0922 23.4864 34.1693 23.3558C34.2463 23.2251 34.2869 23.0762 34.2869 22.9245C34.2869 22.7729 34.2463 22.624 34.1693 22.4933C34.0922 22.3627 33.9816 22.255 33.8489 22.1816V22.1816Z" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M18.1943 22.9248H24.9868" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
);
|
||||
}
|
22
src/floatingSupportChat/UserCircleIcon.js
Normal file
22
src/floatingSupportChat/UserCircleIcon.js
Normal file
@ -0,0 +1,22 @@
|
||||
import { Box } from "@mui/material";
|
||||
|
||||
|
||||
export default function UserCircleIcon() {
|
||||
|
||||
return (
|
||||
<Box sx={{
|
||||
width: "32px",
|
||||
height: "32px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
<svg width="32" height="33" viewBox="0 0 32 33" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16 28.5C22.6274 28.5 28 23.1274 28 16.5C28 9.87258 22.6274 4.5 16 4.5C9.37258 4.5 4 9.87258 4 16.5C4 23.1274 9.37258 28.5 16 28.5Z" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M16 20.5C18.7614 20.5 21 18.2614 21 15.5C21 12.7386 18.7614 10.5 16 10.5C13.2386 10.5 11 12.7386 11 15.5C11 18.2614 13.2386 20.5 16 20.5Z" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M7.97461 25.425C8.727 23.943 9.87506 22.6983 11.2915 21.8289C12.708 20.9595 14.3376 20.4992 15.9996 20.4992C17.6616 20.4992 19.2912 20.9595 20.7077 21.8289C22.1242 22.6983 23.2722 23.943 24.0246 25.425" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</Box>
|
||||
);
|
||||
}
|
25
src/floatingSupportChat/date.js
Normal file
25
src/floatingSupportChat/date.js
Normal file
@ -0,0 +1,25 @@
|
||||
import { getDeclension } from "./declension.js";
|
||||
|
||||
|
||||
export function isDateToday(date) {
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
return date.getTime() > today.getTime();
|
||||
}
|
||||
|
||||
const avgDaysInMonth = 30.43692;
|
||||
const avgDaysInYear = 365.242199;
|
||||
|
||||
export function formatDateWithDeclention(numberOfDays) {
|
||||
if (numberOfDays === 0) return "0 дней";
|
||||
|
||||
const years = Math.floor(numberOfDays / avgDaysInYear);
|
||||
const months = Math.floor(numberOfDays % avgDaysInYear / avgDaysInMonth);
|
||||
const days = Math.floor(numberOfDays % avgDaysInYear % avgDaysInMonth);
|
||||
|
||||
const yearsDisplay = years > 0 ? `${years} ${getDeclension(years, "год")}` : "";
|
||||
const monthsDisplay = months > 0 ? `${months} ${getDeclension(months, "месяц")}` : "";
|
||||
const daysDisplay = days > 0 ? `${days} ${getDeclension(days, "день")}` : "";
|
||||
|
||||
return `${yearsDisplay} ${monthsDisplay} ${daysDisplay}`;
|
||||
}
|
24
src/floatingSupportChat/declension.js
Normal file
24
src/floatingSupportChat/declension.js
Normal file
@ -0,0 +1,24 @@
|
||||
|
||||
|
||||
function declension(number, declensions, cases = [2, 0, 1, 1, 1, 2]) {
|
||||
return declensions[
|
||||
number % 100 > 4 && number % 100 < 20
|
||||
? 2
|
||||
: cases[number % 10 < 5 ? number % 10 : 5]
|
||||
];
|
||||
}
|
||||
|
||||
export function getDeclension(number, word) {
|
||||
switch (word) {
|
||||
case "шаблон":
|
||||
return declension(number, ["шаблон", "шаблона", "шаблонов"]);
|
||||
case "день":
|
||||
return declension(number, ["день", "дня", "дней"]);
|
||||
case "месяц":
|
||||
return declension(number, ["месяц", "месяца", "месяцев"]);
|
||||
case "год":
|
||||
return declension(number, ["год", "года", "лет"]);
|
||||
case "МБ":
|
||||
return "МБ";
|
||||
}
|
||||
};
|
@ -12,7 +12,7 @@ import theme from "./theme"
|
||||
import {ContactFormModal} from "./kit/ContactForm";
|
||||
import Button from "@mui/material/Button";
|
||||
import { SnackbarProvider, useSnackbar } from 'notistack';
|
||||
import { setIsContactFormOpen } from "./stores/contactForm";
|
||||
import FloatingSupportChat from "./floatingSupportChat/FloatingSupportChat";
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
root.render(
|
||||
@ -22,12 +22,7 @@ root.render(
|
||||
<SnackbarProvider>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/" element={ <App /> } >
|
||||
<Route
|
||||
path="support"
|
||||
element={<ContactFormModal/>}
|
||||
/>
|
||||
</Route>
|
||||
<Route index path="/" element={ <App /> } />
|
||||
<Route path="/docs" element={ <Docs /> } />
|
||||
<Route path="/offers" element={ <Offer /> } />
|
||||
<Route path="/tour" element={ <Tour /> } />
|
||||
|
61
src/stores/unauthTicket.js
Normal file
61
src/stores/unauthTicket.js
Normal file
@ -0,0 +1,61 @@
|
||||
import { TicketMessage } from "@frontend/kitui";
|
||||
import { create } from "zustand";
|
||||
import { createJSONStorage, devtools, persist } from "zustand/middleware";
|
||||
|
||||
export const useUnauthTicketStore = create()(
|
||||
persist(
|
||||
devtools(
|
||||
(set, get) => ({
|
||||
sessionData: null,
|
||||
isMessageSending: false,
|
||||
messages: [],
|
||||
apiPage: 0,
|
||||
messagesPerPage: 10,
|
||||
lastMessageId: undefined,
|
||||
isPreventAutoscroll: false,
|
||||
}),
|
||||
{
|
||||
name: "Unauth tickets"
|
||||
}
|
||||
),
|
||||
{
|
||||
version: 0,
|
||||
name: "unauth-ticket",
|
||||
storage: createJSONStorage(() => localStorage),
|
||||
partialize: state => ({
|
||||
sessionData: state.sessionData,
|
||||
})
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
export const setUnauthSessionData = (sessionData) => useUnauthTicketStore.setState({ sessionData });
|
||||
export const setIsMessageSending = (isMessageSending) => useUnauthTicketStore.setState({ isMessageSending });
|
||||
|
||||
export const addOrUpdateUnauthMessages = (receivedMessages) => {
|
||||
const state = useUnauthTicketStore.getState();
|
||||
const messageIdToMessageMap = {};
|
||||
|
||||
[...state.messages, ...receivedMessages].forEach(message => messageIdToMessageMap[message.id] = message);
|
||||
|
||||
const sortedMessages = Object.values(messageIdToMessageMap).sort(sortMessagesByTime);
|
||||
|
||||
useUnauthTicketStore.setState({
|
||||
messages: sortedMessages,
|
||||
lastMessageId: sortedMessages.at(-1)?.id,
|
||||
});
|
||||
};
|
||||
|
||||
export const incrementUnauthMessageApiPage = () => {
|
||||
const state = useUnauthTicketStore.getState();
|
||||
|
||||
useUnauthTicketStore.setState({ apiPage: state.apiPage + 1 });
|
||||
};
|
||||
|
||||
export const setUnauthIsPreventAutoscroll = (isPreventAutoscroll) => useUnauthTicketStore.setState({ isPreventAutoscroll });
|
||||
|
||||
function sortMessagesByTime(ticket1, ticket2) {
|
||||
const date1 = new Date(ticket1.created_at).getTime();
|
||||
const date2 = new Date(ticket2.created_at).getTime();
|
||||
return date1 - date2;
|
||||
}
|
37
src/theme.js
37
src/theme.js
@ -17,6 +17,43 @@ let theme = createTheme({
|
||||
primary: {
|
||||
main: "#7E2AEA"
|
||||
},
|
||||
secondary: {
|
||||
main: "#252734"
|
||||
},
|
||||
text: {
|
||||
primary: "#000000",
|
||||
secondary: "#7E2AEA",
|
||||
},
|
||||
background: {
|
||||
default: "#F2F3F7",
|
||||
},
|
||||
lightPurple: {
|
||||
main: "#333647",
|
||||
},
|
||||
darkPurple: {
|
||||
main: "#252734",
|
||||
},
|
||||
brightPurple: {
|
||||
main: "#7E2AEA",
|
||||
},
|
||||
fadePurple: {
|
||||
main: "#C19AF5",
|
||||
},
|
||||
grey1: {
|
||||
main: "#434657",
|
||||
},
|
||||
grey2: {
|
||||
main: "#9A9AAF",
|
||||
},
|
||||
grey3: {
|
||||
main: "#4D4D4D",
|
||||
},
|
||||
orange: {
|
||||
main: "#FB5607",
|
||||
},
|
||||
navbarbg: {
|
||||
main: "#FFFFFF",
|
||||
},
|
||||
},
|
||||
components: {
|
||||
MuiContainer:{
|
||||
|
Loading…
Reference in New Issue
Block a user