| @@ -13,6 +13,9 @@ | |||
| "react/jsx-filename-extension": "off", | |||
| "react/jsx-props-no-spreading": "off", | |||
| "react/button-has-type": "off", | |||
| "react/max-len": ["error", { | |||
| "code": 100 | |||
| }], | |||
| "react/require-default-props": "off", | |||
| "import/no-extraneous-dependencies": "off", | |||
| "import/prefer-default-export": "off", | |||
| @@ -1,22 +1,18 @@ | |||
| { | |||
| "env": { | |||
| "browser": true, | |||
| "es2021": true | |||
| "env": { | |||
| "browser": true, | |||
| "es2021": true | |||
| }, | |||
| "extends": ["eslint:recommended", "plugin:react/recommended"], | |||
| "parserOptions": { | |||
| "ecmaFeatures": { | |||
| "jsx": true | |||
| }, | |||
| "extends": [ | |||
| "eslint:recommended", | |||
| "plugin:react/recommended" | |||
| ], | |||
| "parserOptions": { | |||
| "ecmaFeatures": { | |||
| "jsx": true | |||
| }, | |||
| "ecmaVersion": 12, | |||
| "sourceType": "module" | |||
| }, | |||
| "plugins": [ | |||
| "react" | |||
| ], | |||
| "rules": { | |||
| } | |||
| "ecmaVersion": 12, | |||
| "sourceType": "module" | |||
| }, | |||
| "plugins": ["react"], | |||
| "rules": { | |||
| "max-lines": ["warn", 100] | |||
| } | |||
| } | |||
| @@ -1,41 +1,25 @@ | |||
| import React, { useMemo } from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { | |||
| CheckButton, | |||
| OfferImage, | |||
| OfferTitle, | |||
| ChatOffer, | |||
| Commands, | |||
| ChatInfo, | |||
| OfferText, | |||
| ChatCardContainer, | |||
| Col, | |||
| UserImage, | |||
| OfferCardContainer, | |||
| UserImgWrapper, | |||
| OfferImgWrapper, | |||
| UserName, | |||
| LastMessage, | |||
| Line, | |||
| LocationContainer, | |||
| XSText, | |||
| LocationIcon, | |||
| OfferCardContainerMobile, | |||
| OfferTextMobile, | |||
| OfferTitleMobile, | |||
| PhoneIconContainer, | |||
| PhoneIcon, | |||
| LocationIconContainer, | |||
| } from "./ChatCard.styled"; | |||
| import selectedTheme from "../../../themes"; | |||
| import { useHistory } from "react-router-dom"; | |||
| import useScreenDimensions from "../../../hooks/useScreenDimensions"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import LittleOfferDetails from "./LittleOfferDetails/LittleOfferDetails"; | |||
| import MobileOfferDetails from "./MobileOfferDetails/MobileOfferDetails"; | |||
| import OfferLocation from "./OfferLocation/OfferLocation"; | |||
| import ChatCommands from "./ChatCommands/ChatCommands"; | |||
| const ChatCard = (props) => { | |||
| const history = useHistory(); | |||
| const dimensions = useScreenDimensions(); | |||
| const { t } = useTranslation(); | |||
| const chat = useMemo(() => { | |||
| return props.chat; | |||
| @@ -47,7 +31,6 @@ const ChatCard = (props) => { | |||
| } | |||
| return ""; | |||
| }, [chat]); | |||
| const routeToItem = () => { | |||
| history.push(`/messages/${chat?.chat?._id}`); | |||
| }; | |||
| @@ -66,51 +49,20 @@ const ChatCard = (props) => { | |||
| <UserName>{chat?.interlocutorData?.name}</UserName> | |||
| {/* Only shows on Mobile */} | |||
| <OfferCardContainerMobile> | |||
| <OfferTextMobile>{t("messages.cardProduct")}</OfferTextMobile> | |||
| <OfferTitleMobile>{chat?.offerData?.name}</OfferTitleMobile> | |||
| </OfferCardContainerMobile> | |||
| <MobileOfferDetails chat={chat} /> | |||
| {/* ^^^^^ */} | |||
| <LastMessage>{lastMessage}</LastMessage> | |||
| <LocationContainer> | |||
| <LocationIconContainer> | |||
| <LocationIcon /> | |||
| </LocationIconContainer> | |||
| <XSText>{chat?.interlocutorData?.location}</XSText> | |||
| </LocationContainer> | |||
| <OfferLocation chat={chat} /> | |||
| </ChatInfo> | |||
| </Col> | |||
| <Line /> | |||
| {/* Only shows on Desktop */} | |||
| <Col mobileDisappear> | |||
| <ChatOffer> | |||
| <OfferImgWrapper> | |||
| <OfferImage src={chat?.offerData?.firstImage} /> | |||
| </OfferImgWrapper> | |||
| <OfferCardContainer> | |||
| <OfferText>{t("messages.cardProduct")}</OfferText> | |||
| <OfferTitle>{chat?.offerData?.name}</OfferTitle> | |||
| </OfferCardContainer> | |||
| </ChatOffer> | |||
| </Col> | |||
| <LittleOfferDetails chat={chat} /> | |||
| {/* ^^^^^^^ */} | |||
| <Commands> | |||
| <PhoneIconContainer> | |||
| <PhoneIcon /> | |||
| </PhoneIconContainer> | |||
| <CheckButton | |||
| buttoncolor={selectedTheme.primaryPurple} | |||
| textcolor={selectedTheme.primaryPurple} | |||
| variant={"outlined"} | |||
| style={{ fontWeight: "600" }} | |||
| onClick={routeToItem} | |||
| > | |||
| {t("messages.seeChats")} | |||
| </CheckButton> | |||
| </Commands> | |||
| <ChatCommands routeToItem={() => routeToItem(chat?.chat?._id)} /> | |||
| </ChatCardContainer> | |||
| ); | |||
| }; | |||
| @@ -1,10 +1,6 @@ | |||
| import { Box, Container, Typography } from "@mui/material"; | |||
| import styled from "styled-components"; | |||
| import selectedTheme from "../../../themes"; | |||
| import { IconButton } from "../../Buttons/IconButton/IconButton"; | |||
| import { PrimaryButton } from "../../Buttons/PrimaryButton/PrimaryButton"; | |||
| import { ReactComponent as Phone } from "../../../assets/images/svg/phone.svg"; | |||
| import { ReactComponent as Location } from "../../../assets/images/svg/location.svg"; | |||
| export const ChatCardContainer = styled(Container)` | |||
| display: flex; | |||
| @@ -14,13 +10,10 @@ export const ChatCardContainer = styled(Container)` | |||
| box-sizing: border-box; | |||
| margin: 10px 0; | |||
| background-color: ${(props) => | |||
| props.sponsored === "true" | |||
| ? selectedTheme.backgroundSponsoredColor | |||
| : "white"}; | |||
| props.sponsored === "true" ? selectedTheme.backgroundSponsoredColor : "white"}; | |||
| border-radius: 4px; | |||
| ${(props) => | |||
| props.sponsored === "true" && | |||
| `border: 1px solid ${selectedTheme.borderSponsoredColor};`} | |||
| props.sponsored === "true" && `border: 1px solid ${selectedTheme.borderSponsoredColor};`} | |||
| padding: 16px; | |||
| max-width: 2000px; | |||
| height: 180px; | |||
| @@ -31,11 +24,9 @@ export const ChatCardContainer = styled(Container)` | |||
| margin: 0; | |||
| ${(props) => | |||
| props.vertical && | |||
| ` | |||
| height: 330px; | |||
| `height: 330px; | |||
| width: 180px; | |||
| margin: 0 18px; | |||
| `} | |||
| margin: 0 18px;`} | |||
| } | |||
| `; | |||
| export const UserImage = styled.img` | |||
| @@ -47,7 +38,6 @@ export const UserImage = styled.img` | |||
| height: 72px; | |||
| } | |||
| `; | |||
| export const UserImgWrapper = styled(Box)` | |||
| overflow: hidden; | |||
| border-radius: 50%; | |||
| @@ -59,191 +49,19 @@ export const UserImgWrapper = styled(Box)` | |||
| min-width: 80px; | |||
| } | |||
| `; | |||
| export const OfferImgWrapper = styled(Box)` | |||
| overflow: hidden; | |||
| border-radius: 4px; | |||
| width: 72px; | |||
| height: 72px; | |||
| min-width: 72px; | |||
| max-width: 72px; | |||
| `; | |||
| export const LocationIcon = styled(Location)` | |||
| height: 12px; | |||
| width: 12px; | |||
| `; | |||
| export const OfferCardContainer = styled(Container)` | |||
| display: flex; | |||
| flex-direction: column; | |||
| box-sizing: border-box; | |||
| padding: 16px; | |||
| max-width: 2000px; | |||
| position: relative; | |||
| @media (max-width: 550px) { | |||
| } | |||
| `; | |||
| export const OfferCardContainerMobile = styled(Box)` | |||
| display: none; | |||
| @media (max-width: 550px) { | |||
| position: relative; | |||
| display: flex; | |||
| flex-direction: column; | |||
| box-sizing: border-box; | |||
| } | |||
| `; | |||
| export const OfferTitle = styled(Typography)` | |||
| font-family: "Open Sans"; | |||
| flex: 1; | |||
| color: ${selectedTheme.primaryPurple}; | |||
| font-weight: 700; | |||
| font-size: 18px; | |||
| cursor: pointer; | |||
| @media (max-width: 550px) { | |||
| font-size: 14px; | |||
| display: none; | |||
| ${(props) => | |||
| props.vertical && | |||
| ` | |||
| display: flex; | |||
| flex: none; | |||
| position: relative; | |||
| line-height: 22px; | |||
| margin-top: 5px; | |||
| font-size: 18px; | |||
| `} | |||
| } | |||
| `; | |||
| export const OfferTitleMobile = styled(Typography)` | |||
| font-family: "Open Sans"; | |||
| display: none; | |||
| flex: 1; | |||
| color: ${selectedTheme.primaryPurple}; | |||
| font-weight: 700; | |||
| font-size: 12px; | |||
| cursor: pointer; | |||
| @media (max-width: 550px) { | |||
| display: block; | |||
| ${(props) => | |||
| props.vertical && | |||
| ` | |||
| display: flex; | |||
| flex: none; | |||
| position: relative; | |||
| line-height: 22px; | |||
| margin-top: 5px; | |||
| font-size: 18px; | |||
| `} | |||
| } | |||
| `; | |||
| export const CheckButton = styled(PrimaryButton)` | |||
| width: 180px; | |||
| height: 48px; | |||
| &:hover { | |||
| background-color: ${selectedTheme.primaryPurple}; | |||
| color: white !important; | |||
| border-radius: 4px; | |||
| } | |||
| @media (max-width: 1024px) { | |||
| width: 150px; | |||
| height: 40px; | |||
| margin-left: 2vw; | |||
| & button { | |||
| padding: 0; | |||
| font-size: 11px; | |||
| } | |||
| } | |||
| @media (max-width: 600px) { | |||
| display: none; | |||
| } | |||
| transition: 0.2s all; | |||
| `; | |||
| export const PhoneIconContainer = styled(IconButton)` | |||
| width: 40px; | |||
| height: 40px; | |||
| background-color: ${selectedTheme.primaryIconBackgroundColor}; | |||
| border-radius: 100%; | |||
| padding-top: 2px; | |||
| text-align: center; | |||
| @media (max-width: 600px) { | |||
| width: 32px; | |||
| height: 32px; | |||
| top: 16px; | |||
| right: 16px; | |||
| padding: 0; | |||
| ${(props) => | |||
| props.vertical && | |||
| ` | |||
| display: none; | |||
| `} | |||
| & button svg { | |||
| width: 16px; | |||
| height: 16px; | |||
| position: relative; | |||
| top: -3px; | |||
| left: -2.4px; | |||
| } | |||
| } | |||
| `; | |||
| export const ChatOffer = styled(Box)` | |||
| display: flex; | |||
| flex-direction: row; | |||
| align-items: center; | |||
| padding-left: 36px; | |||
| @media (max-width: 600px) { | |||
| display: none; | |||
| } | |||
| `; | |||
| export const OfferText = styled(Box)` | |||
| font-family: "Open Sans"; | |||
| font-size: "12px"; | |||
| color: ${selectedTheme.primaryText}; | |||
| `; | |||
| export const OfferTextMobile = styled(Box)` | |||
| font-family: "Open Sans"; | |||
| font-size: 9px; | |||
| color: ${selectedTheme.primaryText}; | |||
| `; | |||
| export const Commands = styled(Box)` | |||
| display: flex; | |||
| flex-direction: column; | |||
| justify-content: space-between; | |||
| align-items: flex-end; | |||
| @media (max-width: 600px) { | |||
| align-items: flex-start; | |||
| } | |||
| `; | |||
| export const ChatInfo = styled(Box)` | |||
| height: 100%; | |||
| display: flex; | |||
| flex-direction: column; | |||
| justify-content: space-between; | |||
| `; | |||
| export const Col = styled(Box)` | |||
| display: flex; | |||
| align-items: center; | |||
| flex-direction: row; | |||
| gap: 18px; | |||
| flex: 1; | |||
| @media (max-width: 1024px) { | |||
| ${(props) => props.mobileDisappear && `display: none;`} | |||
| } | |||
| @media (max-width: 600px) { | |||
| ${(props) => props.mobileDisappear && `display: none;`} | |||
| } | |||
| `; | |||
| export const UserName = styled(Typography)` | |||
| margin-bottom: 12px; | |||
| font-family: "Open Sans"; | |||
| @@ -255,7 +73,6 @@ export const UserName = styled(Typography)` | |||
| font-size: 18px; | |||
| } | |||
| `; | |||
| export const LastMessage = styled(Typography)` | |||
| font-family: "Open Sans"; | |||
| color: ${selectedTheme.primaryDarkTextThird}; | |||
| @@ -273,39 +90,6 @@ export const LastMessage = styled(Typography)` | |||
| display: none; | |||
| } | |||
| `; | |||
| export const LocationContainer = styled(Box)` | |||
| display: flex; | |||
| flex-direction: row; | |||
| gap: 2px; | |||
| @media (max-width: 600px) { | |||
| display: none; | |||
| } | |||
| `; | |||
| export const LocationIconContainer = styled(Box)` | |||
| height: 12px; | |||
| width: auto; | |||
| position: relative; | |||
| `; | |||
| export const XSText = styled(Typography)` | |||
| font-family: "Open Sans"; | |||
| color: ${selectedTheme.primaryText}; | |||
| line-height: 16px; | |||
| font-size: 14px; | |||
| position: relative; | |||
| `; | |||
| export const OfferImage = styled.img` | |||
| max-width: 72px; | |||
| max-height: 72px; | |||
| min-width: 72px; | |||
| width: 72px; | |||
| height: 72px; | |||
| border-radius: 4px; | |||
| `; | |||
| export const Line = styled(Box)` | |||
| border-left: 1px solid rgba(0, 0, 0, 0.15); | |||
| height: 100px; | |||
| @@ -313,13 +97,4 @@ export const Line = styled(Box)` | |||
| margin: auto 0; | |||
| @media (max-width: 600px) { | |||
| display: none; | |||
| } | |||
| `; | |||
| export const PhoneIcon = styled(Phone)` | |||
| @media (max-width: 600px) { | |||
| width: 14px; | |||
| height: 14px; | |||
| position: relative; | |||
| top: 1px; | |||
| } | |||
| `; | |||
| }`; | |||
| @@ -0,0 +1,37 @@ | |||
| import React from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { | |||
| CheckButton, | |||
| Commands, | |||
| PhoneIcon, | |||
| PhoneIconContainer, | |||
| } from "./ChatCommands.styled"; | |||
| import selectedTheme from "../../../../themes"; | |||
| import { useTranslation } from "react-i18next"; | |||
| const ChatCommands = (props) => { | |||
| const { t } = useTranslation(); | |||
| return ( | |||
| <Commands> | |||
| <PhoneIconContainer> | |||
| <PhoneIcon /> | |||
| </PhoneIconContainer> | |||
| <CheckButton | |||
| buttoncolor={selectedTheme.primaryPurple} | |||
| textcolor={selectedTheme.primaryPurple} | |||
| variant={"outlined"} | |||
| style={{ fontWeight: "600" }} | |||
| onClick={props.routeToItem} | |||
| > | |||
| {t("messages.seeChats")} | |||
| </CheckButton> | |||
| </Commands> | |||
| ); | |||
| }; | |||
| ChatCommands.propTypes = { | |||
| routeToItem: PropTypes.func, | |||
| }; | |||
| export default ChatCommands; | |||
| @@ -0,0 +1,75 @@ | |||
| import { Box } from "@mui/material"; | |||
| import styled from "styled-components"; | |||
| import { ReactComponent as Phone } from "../../../../assets/images/svg/phone.svg"; | |||
| import selectedTheme from "../../../../themes"; | |||
| import { PrimaryButton } from "../../../Buttons/PrimaryButton/PrimaryButton"; | |||
| import IconButton from "../../../IconButton/IconButton"; | |||
| export const Commands = styled(Box)` | |||
| display: flex; | |||
| flex-direction: column; | |||
| justify-content: space-between; | |||
| align-items: flex-end; | |||
| @media (max-width: 600px) { | |||
| align-items: flex-start; | |||
| } | |||
| `; | |||
| export const PhoneIcon = styled(Phone)` | |||
| @media (max-width: 600px) { | |||
| width: 14px; | |||
| height: 14px; | |||
| position: relative; | |||
| top: 1px; | |||
| } | |||
| `; | |||
| export const PhoneIconContainer = styled(IconButton)` | |||
| width: 40px; | |||
| height: 40px; | |||
| background-color: ${selectedTheme.primaryIconBackgroundColor}; | |||
| border-radius: 100%; | |||
| padding-top: 2px; | |||
| text-align: center; | |||
| @media (max-width: 600px) { | |||
| width: 32px; | |||
| height: 32px; | |||
| top: 16px; | |||
| right: 16px; | |||
| padding: 0; | |||
| ${(props) => | |||
| props.vertical && | |||
| ` | |||
| display: none; | |||
| `} | |||
| & button svg { | |||
| width: 16px; | |||
| height: 16px; | |||
| position: relative; | |||
| top: -3px; | |||
| left: -2.4px; | |||
| } | |||
| } | |||
| `; | |||
| export const CheckButton = styled(PrimaryButton)` | |||
| width: 180px; | |||
| height: 48px; | |||
| &:hover { | |||
| background-color: ${selectedTheme.primaryPurple}; | |||
| color: white !important; | |||
| border-radius: 4px; | |||
| } | |||
| @media (max-width: 1024px) { | |||
| width: 150px; | |||
| height: 40px; | |||
| margin-left: 2vw; | |||
| & button { | |||
| padding: 0; | |||
| font-size: 11px; | |||
| } | |||
| } | |||
| @media (max-width: 600px) { | |||
| display: none; | |||
| } | |||
| transition: 0.2s all; | |||
| `; | |||
| @@ -0,0 +1,36 @@ | |||
| import React from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { | |||
| ChatOffer, | |||
| OfferCardContainer, | |||
| OfferImage, | |||
| OfferImgWrapper, | |||
| OfferText, | |||
| OfferTitle, | |||
| } from "./LittleOfferDetails.styled"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import { Col } from "../ChatCard.styled"; | |||
| const LittleOfferDetails = (props) => { | |||
| const chat = props.chat; | |||
| const { t } = useTranslation(); | |||
| return ( | |||
| <Col mobileDisappear> | |||
| <ChatOffer> | |||
| <OfferImgWrapper> | |||
| <OfferImage src={chat?.offerData?.firstImage} /> | |||
| </OfferImgWrapper> | |||
| <OfferCardContainer> | |||
| <OfferText>{t("messages.cardProduct")}</OfferText> | |||
| <OfferTitle>{chat?.offerData?.name}</OfferTitle> | |||
| </OfferCardContainer> | |||
| </ChatOffer> | |||
| </Col> | |||
| ); | |||
| }; | |||
| LittleOfferDetails.propTypes = { | |||
| chat: PropTypes.any, | |||
| }; | |||
| export default LittleOfferDetails; | |||
| @@ -0,0 +1,85 @@ | |||
| import { Box, Container, Typography } from "@mui/material"; | |||
| import styled from "styled-components"; | |||
| import selectedTheme from "../../../../themes"; | |||
| export const ChatOffer = styled(Box)` | |||
| display: flex; | |||
| flex-direction: row; | |||
| align-items: center; | |||
| padding-left: 36px; | |||
| @media (max-width: 600px) { | |||
| display: none; | |||
| } | |||
| `; | |||
| export const OfferText = styled(Box)` | |||
| font-family: "Open Sans"; | |||
| font-size: "12px"; | |||
| color: ${selectedTheme.primaryText}; | |||
| `; | |||
| export const OfferTitle = styled(Typography)` | |||
| font-family: "Open Sans"; | |||
| flex: 1; | |||
| color: ${selectedTheme.primaryPurple}; | |||
| font-weight: 700; | |||
| font-size: 18px; | |||
| cursor: pointer; | |||
| @media (max-width: 550px) { | |||
| font-size: 14px; | |||
| display: none; | |||
| ${(props) => | |||
| props.vertical && | |||
| ` | |||
| display: flex; | |||
| flex: none; | |||
| position: relative; | |||
| line-height: 22px; | |||
| margin-top: 5px; | |||
| font-size: 18px; | |||
| `} | |||
| } | |||
| `; | |||
| export const OfferCardContainer = styled(Container)` | |||
| display: flex; | |||
| flex-direction: column; | |||
| box-sizing: border-box; | |||
| padding: 16px; | |||
| max-width: 2000px; | |||
| position: relative; | |||
| @media (max-width: 550px) { | |||
| } | |||
| `; | |||
| export const OfferImgWrapper = styled(Box)` | |||
| overflow: hidden; | |||
| border-radius: 4px; | |||
| width: 72px; | |||
| height: 72px; | |||
| min-width: 72px; | |||
| max-width: 72px; | |||
| `; | |||
| export const OfferImage = styled.img` | |||
| max-width: 72px; | |||
| max-height: 72px; | |||
| min-width: 72px; | |||
| width: 72px; | |||
| height: 72px; | |||
| border-radius: 4px; | |||
| `; | |||
| export const Col = styled(Box)` | |||
| display: flex; | |||
| align-items: center; | |||
| flex-direction: row; | |||
| gap: 18px; | |||
| flex: 1; | |||
| @media (max-width: 1024px) { | |||
| ${(props) => props.mobileDisappear && `display: none;`} | |||
| } | |||
| @media (max-width: 600px) { | |||
| ${(props) => props.mobileDisappear && `display: none;`} | |||
| } | |||
| `; | |||
| @@ -0,0 +1,25 @@ | |||
| import React from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import { | |||
| OfferCardContainerMobile, | |||
| OfferTextMobile, | |||
| OfferTitleMobile, | |||
| } from "./MobileOfferDetails.styled"; | |||
| const MobileOfferDetails = (props) => { | |||
| const chat = props.chat; | |||
| const { t } = useTranslation(); | |||
| return ( | |||
| <OfferCardContainerMobile> | |||
| <OfferTextMobile>{t("messages.cardProduct")}</OfferTextMobile> | |||
| <OfferTitleMobile>{chat?.offerData?.name}</OfferTitleMobile> | |||
| </OfferCardContainerMobile> | |||
| ); | |||
| }; | |||
| MobileOfferDetails.propTypes = { | |||
| chat: PropTypes.any, | |||
| }; | |||
| export default MobileOfferDetails; | |||
| @@ -0,0 +1,43 @@ | |||
| import { Box, Typography } from "@mui/material"; | |||
| import styled from "styled-components"; | |||
| import selectedTheme from "../../../../themes"; | |||
| export const OfferCardContainerMobile = styled(Box)` | |||
| display: none; | |||
| @media (max-width: 550px) { | |||
| position: relative; | |||
| display: flex; | |||
| flex-direction: column; | |||
| box-sizing: border-box; | |||
| } | |||
| `; | |||
| export const OfferTitleMobile = styled(Typography)` | |||
| font-family: "Open Sans"; | |||
| display: none; | |||
| flex: 1; | |||
| color: ${selectedTheme.primaryPurple}; | |||
| font-weight: 700; | |||
| font-size: 12px; | |||
| cursor: pointer; | |||
| @media (max-width: 550px) { | |||
| display: block; | |||
| ${(props) => | |||
| props.vertical && | |||
| ` | |||
| display: flex; | |||
| flex: none; | |||
| position: relative; | |||
| line-height: 22px; | |||
| margin-top: 5px; | |||
| font-size: 18px; | |||
| `} | |||
| } | |||
| `; | |||
| export const OfferTextMobile = styled(Box)` | |||
| font-family: "Open Sans"; | |||
| font-size: 9px; | |||
| color: ${selectedTheme.primaryText}; | |||
| `; | |||
| @@ -0,0 +1,21 @@ | |||
| import React from 'react' | |||
| import PropTypes from 'prop-types' | |||
| import { LocationContainer, LocationIcon, LocationIconContainer, XSText } from './OfferLocation.styled'; | |||
| const OfferLocation = (props) => { | |||
| const chat = props.chat; | |||
| return ( | |||
| <LocationContainer> | |||
| <LocationIconContainer> | |||
| <LocationIcon /> | |||
| </LocationIconContainer> | |||
| <XSText>{chat?.interlocutorData?.location}</XSText> | |||
| </LocationContainer> | |||
| ) | |||
| } | |||
| OfferLocation.propTypes = { | |||
| chat: PropTypes.any, | |||
| } | |||
| export default OfferLocation | |||
| @@ -0,0 +1,33 @@ | |||
| import { Box, Typography } from "@mui/material"; | |||
| import styled from "styled-components"; | |||
| import selectedTheme from "../../../../themes"; | |||
| import { ReactComponent as Location } from "../../../../assets/images/svg/location.svg"; | |||
| export const LocationContainer = styled(Box)` | |||
| display: flex; | |||
| flex-direction: row; | |||
| gap: 2px; | |||
| @media (max-width: 600px) { | |||
| display: none; | |||
| } | |||
| `; | |||
| export const LocationIconContainer = styled(Box)` | |||
| height: 12px; | |||
| width: auto; | |||
| position: relative; | |||
| `; | |||
| export const XSText = styled(Typography)` | |||
| font-family: "Open Sans"; | |||
| color: ${selectedTheme.primaryText}; | |||
| line-height: 16px; | |||
| font-size: 14px; | |||
| position: relative; | |||
| `; | |||
| export const LocationIcon = styled(Location)` | |||
| height: 12px; | |||
| width: 12px; | |||
| `; | |||
| @@ -0,0 +1,32 @@ | |||
| import React from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { CheckBox as CheckboxButton } from "../../../../../CheckBox/CheckBox"; | |||
| const Checkbox = (props) => { | |||
| const item = props.item; | |||
| return ( | |||
| <CheckboxButton | |||
| leftText={item.city} | |||
| rightText={item.offerCount} | |||
| value={item} | |||
| checked={ | |||
| props.filters.find( | |||
| (itemInList) => | |||
| itemInList?.city?.toString() === item?.city?.toString() | |||
| ) | |||
| ? true | |||
| : false | |||
| } | |||
| onChange={props.onChange} | |||
| fullWidth | |||
| /> | |||
| ); | |||
| }; | |||
| Checkbox.propTypes = { | |||
| item: PropTypes.any, | |||
| filters: PropTypes.any, | |||
| onChange: PropTypes.func, | |||
| }; | |||
| export default Checkbox; | |||
| @@ -0,0 +1,84 @@ | |||
| import React from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import DropdownList from "../../../../../Dropdown/DropdownList/DropdownList"; | |||
| import selectedTheme from "../../../../../../themes"; | |||
| import IconWithNumber from "../../../../../Icon/IconWithNumber/IconWithNumber"; | |||
| import { ReactComponent as DropdownDown } from "../../../../../../assets/images/svg/down-arrow.svg"; | |||
| import { ReactComponent as DropdownUp } from "../../../../../../assets/images/svg/up-arrow.svg"; | |||
| import { ReactComponent as Close } from "../../../../../../assets/images/svg/close-white.svg"; | |||
| import { | |||
| SelectedItem, | |||
| SelectedItemsContainer, | |||
| } from "./CheckboxDropdownList.styled"; | |||
| import SearchField from "./SearchField/SearchField"; | |||
| const CheckboxDropdownList = (props) => { | |||
| const data = props.data; | |||
| const handleDelete = (item) => { | |||
| props.setItemsSelected([...props.filters.filter((p) => p !== item)]); | |||
| }; | |||
| return ( | |||
| <DropdownList | |||
| title={props.title} | |||
| textcolor={ | |||
| props.filters.length > 0 | |||
| ? selectedTheme.primaryPurple | |||
| : selectedTheme.primaryText | |||
| } | |||
| dropdownIcon={ | |||
| <IconWithNumber number={props.filters.length}> | |||
| {props.icon} | |||
| </IconWithNumber> | |||
| } | |||
| toggleIconClosed={<DropdownDown />} | |||
| toggleIconOpened={<DropdownUp />} | |||
| fullWidth | |||
| open={props.isOpened} | |||
| setIsOpened={props.setIsOpened} | |||
| toggleIconStyles={{ | |||
| backgroundColor: props.isOpened | |||
| ? "white" | |||
| : selectedTheme.primaryIconBackgroundColor, | |||
| }} | |||
| headerOptions={ | |||
| <React.Fragment> | |||
| <SelectedItemsContainer> | |||
| {props.filters.map((item) => ( | |||
| <SelectedItem key={item.city} onClick={() => handleDelete(item)}> | |||
| { | |||
| data.find( | |||
| (p) => p?.city?.toString() === item?.city?.toString() | |||
| )?.city | |||
| } | |||
| <Close style={{ position: "relative", top: "3px" }} /> | |||
| </SelectedItem> | |||
| ))} | |||
| </SelectedItemsContainer> | |||
| <SearchField | |||
| placeholder={props.searchPlaceholder} | |||
| value={props.toSearch} | |||
| onChange={(event) => props.setToSearch(event.target.value)} | |||
| /> | |||
| </React.Fragment> | |||
| } | |||
| > | |||
| {props.children} | |||
| </DropdownList> | |||
| ); | |||
| }; | |||
| CheckboxDropdownList.propTypes = { | |||
| children: PropTypes.node, | |||
| title: PropTypes.string, | |||
| filters: PropTypes.any, | |||
| icon: PropTypes.node, | |||
| setToSearch: PropTypes.func, | |||
| setItemsSelected: PropTypes.func, | |||
| data: PropTypes.any, | |||
| searchPlaceholder: PropTypes.string, | |||
| toSearch: PropTypes.string, | |||
| isOpened: PropTypes.bool, | |||
| setIsOpened: PropTypes.func, | |||
| }; | |||
| export default CheckboxDropdownList; | |||
| @@ -0,0 +1,25 @@ | |||
| import { Box } from "@mui/material"; | |||
| import styled from "styled-components"; | |||
| import selectedTheme from "../../../../../../themes"; | |||
| export const SelectedItemsContainer = styled(Box)` | |||
| display: flex; | |||
| flex-direction: row; | |||
| flex-wrap: wrap; | |||
| margin-top: 5px; | |||
| `; | |||
| export const SelectedItem = styled(Box)` | |||
| margin-top: 2px; | |||
| background-color: ${selectedTheme.primaryPurple}; | |||
| border-radius: 8px; | |||
| color: white; | |||
| padding-left: 8px; | |||
| padding-right: 6px; | |||
| line-height: 12px; | |||
| letter-spacing: 0.02em; | |||
| font-family: "Open Sans"; | |||
| font-size: 12px; | |||
| cursor: pointer; | |||
| margin-right: 3px; | |||
| height: 22px; | |||
| `; | |||
| @@ -0,0 +1,42 @@ | |||
| import React from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { TextField } from "../../../../../../TextFields/TextField/TextField"; | |||
| import { ReactComponent as CloseBlack } from "../../../../../../../assets/images/svg/close-black.svg"; | |||
| import { ClearText } from "./SearchField.styled"; | |||
| const SearchField = (props) => { | |||
| const handleClear = () => { | |||
| props.onChange(""); | |||
| }; | |||
| return ( | |||
| <TextField | |||
| placeholder={props.placeholder} | |||
| italicPlaceholder | |||
| value={props.value} | |||
| onChange={props.onChange} | |||
| textsize={"12px"} | |||
| font={"Open Sans"} | |||
| fullWidth | |||
| height={"40px"} | |||
| containerStyle={{ marginTop: "6px" }} | |||
| InputProps={{ | |||
| endAdornment: | |||
| props.value.length > 0 ? ( | |||
| <ClearText onClick={handleClear}> | |||
| <CloseBlack /> | |||
| </ClearText> | |||
| ) : ( | |||
| <React.Fragment /> | |||
| ), | |||
| }} | |||
| /> | |||
| ); | |||
| }; | |||
| SearchField.propTypes = { | |||
| value: PropTypes.string, | |||
| placeholder: PropTypes.string, | |||
| onChange: PropTypes.func, | |||
| }; | |||
| export default SearchField; | |||
| @@ -0,0 +1,17 @@ | |||
| import { Box } from "@mui/material"; | |||
| import styled from "styled-components"; | |||
| import selectedTheme from "../../../../../../../themes"; | |||
| export const ClearText = styled(Box)` | |||
| padding-top: 1px; | |||
| border-radius: 100%; | |||
| cursor: pointer; | |||
| padding-right: 2px; | |||
| position: relative; | |||
| left: 6px; | |||
| width: 21px; | |||
| height: 21px; | |||
| &:hover { | |||
| background-color: ${selectedTheme.primaryIconBackgroundColor}; | |||
| } | |||
| `; | |||
| @@ -1,30 +1,19 @@ | |||
| import React, { useEffect, useState } from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import DropdownList from "../../../../Dropdown/DropdownList/DropdownList"; | |||
| import selectedTheme from "../../../../../themes"; | |||
| import IconWithNumber from "../../../../Icon/IconWithNumber/IconWithNumber"; | |||
| import { ReactComponent as DropdownDown } from "../../../../../assets/images/svg/down-arrow.svg"; | |||
| import { ReactComponent as DropdownUp } from "../../../../../assets/images/svg/up-arrow.svg"; | |||
| import { ReactComponent as Close } from "../../../../../assets/images/svg/close-white.svg"; | |||
| import { ReactComponent as CloseBlack } from "../../../../../assets/images/svg/close-black.svg"; | |||
| import { | |||
| ClearText, | |||
| SelectedItem, | |||
| SelectedItemsContainer, | |||
| } from "./FilterCheckboxDropdown.styled"; | |||
| import { TextField } from "../../../../TextFields/TextField/TextField"; | |||
| import DropdownItem from "../../../../Dropdown/DropdownItem/DropdownItem"; | |||
| import { CheckBox } from "../../../../CheckBox/CheckBox"; | |||
| import CheckboxDropdownList from "./CheckboxDropdownList/CheckboxDropdownList"; | |||
| import Checkbox from "./Checkbox/Checkbox"; | |||
| const FilterCheckboxDropdown = (props) => { | |||
| const [toSearch, setToSearch] = useState(""); | |||
| const [dataToShow, setDataToShow] = useState([]); | |||
| const [isOpened, setIsOpened] = useState(false); | |||
| const [toSearch, setToSearch] = useState(""); | |||
| const { data } = props; | |||
| useEffect(() => { | |||
| setDataToShow([...data]); | |||
| }, [data]); | |||
| useEffect(() => { | |||
| if (toSearch.length > 0) { | |||
| setDataToShow( | |||
| @@ -37,110 +26,57 @@ const FilterCheckboxDropdown = (props) => { | |||
| } | |||
| }, [toSearch]); | |||
| useEffect(() => { | |||
| if (props.filters?.length > 0) { | |||
| setIsOpened(true) | |||
| setIsOpened(true); | |||
| } | |||
| }, [props.filters]) | |||
| }, [props.filters]); | |||
| const handleChange = (item) => { | |||
| if (props.oneValueAllowed) { | |||
| props.setItemsSelected([item]); | |||
| } else { | |||
| if (props.filters.find(itemInList => itemInList?.city?.toString() === item?.city?.toString())) { | |||
| props.setItemsSelected([...props.filters.filter((p) => p?.city?.toString() !== item?.city?.toString())]); | |||
| if ( | |||
| props.filters.find( | |||
| (itemInList) => | |||
| itemInList?.city?.toString() === item?.city?.toString() | |||
| ) | |||
| ) { | |||
| props.setItemsSelected([ | |||
| ...props.filters.filter( | |||
| (p) => p?.city?.toString() !== item?.city?.toString() | |||
| ), | |||
| ]); | |||
| } else { | |||
| props.setItemsSelected([...props.filters, item]); | |||
| } | |||
| } | |||
| }; | |||
| const handleDelete = (item) => { | |||
| props.setItemsSelected([...props.filters.filter((p) => p !== item)]); | |||
| }; | |||
| const handleClear = () => { | |||
| setToSearch(""); | |||
| }; | |||
| return ( | |||
| <DropdownList | |||
| <CheckboxDropdownList | |||
| toSearch={toSearch} | |||
| setToSearch={setToSearch} | |||
| title={props.title} | |||
| textcolor={ | |||
| props.filters.length > 0 | |||
| ? selectedTheme.primaryPurple | |||
| : selectedTheme.primaryText | |||
| } | |||
| dropdownIcon={ | |||
| <IconWithNumber number={props.filters.length}> | |||
| {props.icon} | |||
| </IconWithNumber> | |||
| } | |||
| toggleIconClosed={<DropdownDown />} | |||
| toggleIconOpened={<DropdownUp />} | |||
| fullWidth | |||
| open={isOpened} | |||
| filters={props.filters} | |||
| icon={props.icon} | |||
| data={data} | |||
| searchPlaceholder={props.searchPlaceholder} | |||
| isOpened={isOpened} | |||
| setIsOpened={setIsOpened} | |||
| toggleIconStyles={{ | |||
| backgroundColor: isOpened | |||
| ? "white" | |||
| : selectedTheme.primaryIconBackgroundColor, | |||
| }} | |||
| headerOptions={ | |||
| <React.Fragment> | |||
| <SelectedItemsContainer> | |||
| {props.filters.map((item) => ( | |||
| <SelectedItem key={item.city} onClick={() => handleDelete(item)}> | |||
| { | |||
| data.find((p) => p?.city?.toString() === item?.city?.toString()) | |||
| ?.city | |||
| } | |||
| <Close style={{ position: "relative", top: "3px" }} /> | |||
| </SelectedItem> | |||
| ))} | |||
| </SelectedItemsContainer> | |||
| <TextField | |||
| placeholder={props.searchPlaceholder} | |||
| italicPlaceholder | |||
| value={toSearch} | |||
| onChange={(event) => setToSearch(event.target.value)} | |||
| textsize={"12px"} | |||
| font={"Open Sans"} | |||
| fullWidth | |||
| height={"40px"} | |||
| containerStyle={{ marginTop: "6px" }} | |||
| InputProps={{ | |||
| endAdornment: | |||
| toSearch.length > 0 ? ( | |||
| <ClearText onClick={handleClear}> | |||
| <CloseBlack /> | |||
| </ClearText> | |||
| ) : ( | |||
| <React.Fragment /> | |||
| ), | |||
| }} | |||
| /> | |||
| </React.Fragment> | |||
| } | |||
| > | |||
| {dataToShow.map((item) => { | |||
| return ( | |||
| <DropdownItem key={item.city}> | |||
| <CheckBox | |||
| leftText={item.city} | |||
| rightText={item.offerCount} | |||
| value={item} | |||
| checked={props.filters.find( | |||
| (itemInList) => | |||
| itemInList?.city?.toString() === item?.city?.toString() | |||
| ) ? true : false} | |||
| <Checkbox | |||
| item={item} | |||
| filters={props.filters} | |||
| onChange={() => handleChange(item)} | |||
| fullWidth | |||
| /> | |||
| </DropdownItem> | |||
| ); | |||
| })} | |||
| </DropdownList> | |||
| </CheckboxDropdownList> | |||
| ); | |||
| }; | |||
| @@ -157,5 +93,4 @@ FilterCheckboxDropdown.propTypes = { | |||
| FilterCheckboxDropdown.defaultProps = { | |||
| oneValueAllowed: false, | |||
| }; | |||
| export default FilterCheckboxDropdown; | |||
| @@ -1,38 +1,5 @@ | |||
| import { Box } from "@mui/material"; | |||
| import styled from "styled-components"; | |||
| import selectedTheme from "../../../../../themes"; | |||
| // import { Box } from "@mui/material"; | |||
| // import styled from "styled-components"; | |||
| // import selectedTheme from "../../../../../themes"; | |||
| export const SelectedItemsContainer = styled(Box)` | |||
| display: flex; | |||
| flex-direction: row; | |||
| flex-wrap: wrap; | |||
| margin-top: 5px; | |||
| `; | |||
| export const SelectedItem = styled(Box)` | |||
| margin-top: 2px; | |||
| background-color: ${selectedTheme.primaryPurple}; | |||
| border-radius: 8px; | |||
| color: white; | |||
| padding-left: 8px; | |||
| padding-right: 6px; | |||
| line-height: 12px; | |||
| letter-spacing: 0.02em; | |||
| font-family: "Open Sans"; | |||
| font-size: 12px; | |||
| cursor: pointer; | |||
| margin-right: 3px; | |||
| height: 22px; | |||
| `; | |||
| export const ClearText = styled(Box)` | |||
| padding-top: 1px; | |||
| border-radius: 100%; | |||
| cursor: pointer; | |||
| padding-right: 2px; | |||
| position: relative; | |||
| left: 6px; | |||
| width: 21px; | |||
| height: 21px; | |||
| &:hover { | |||
| background-color: ${selectedTheme.primaryIconBackgroundColor}; | |||
| } | |||
| `; | |||
| @@ -6,13 +6,6 @@ import { | |||
| OfferInfo, | |||
| Info, | |||
| PostDate, | |||
| OfferTitle, | |||
| OfferDescriptionText, | |||
| OfferDescriptionTitle, | |||
| Details, | |||
| OfferDetails, | |||
| OfferImage, | |||
| Scroller, | |||
| CategoryIcon, | |||
| SubcategoryIcon, | |||
| QuantityIcon, | |||
| @@ -28,6 +21,7 @@ import { formatDateLocale } from "../../../util/helpers/dateHelpers"; | |||
| import { startChat } from "../../../util/helpers/chatHelper"; | |||
| import Information from "./Information/Information"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import OfferDetails from "./OfferDetails/OfferDetails"; | |||
| const ItemDetailsCard = (props) => { | |||
| const offer = props.offer; | |||
| @@ -51,7 +45,7 @@ const ItemDetailsCard = (props) => { | |||
| const date = formatDateLocale(new Date(offer?.offer?._created)); | |||
| const startExchange = () => { | |||
| startChat(chats, offer, userId); | |||
| startChat(chats, offer?.offer, userId); | |||
| }; | |||
| return ( | |||
| <ItemDetailsCardContainer | |||
| @@ -77,25 +71,9 @@ const ItemDetailsCard = (props) => { | |||
| </Info> | |||
| <PostDate>{date}</PostDate> | |||
| </OfferInfo> | |||
| <Details | |||
| hasScrollBar={!props.showPublishButton} | |||
| exchange={props.showExchangeButton} | |||
| > | |||
| <OfferTitle>{offer?.offer?.name}</OfferTitle> | |||
| <Scroller> | |||
| {offer?.offer?.images?.map((item) => { | |||
| return <OfferImage src={item} key={item} />; | |||
| })} | |||
| </Scroller> | |||
| <OfferDetails> | |||
| <OfferDescriptionTitle> | |||
| {t("itemDetailsCard.description")} | |||
| </OfferDescriptionTitle> | |||
| <OfferDescriptionText showBarterButton={props.showExchangeButton}> | |||
| {offer?.offer?.description} | |||
| </OfferDescriptionText> | |||
| </OfferDetails> | |||
| </Details> | |||
| <OfferDetails offer={offer}/> | |||
| {!props.halfwidth && props.showExchangeButton && ( | |||
| <CheckButton | |||
| variant={props.sponsored ? "contained" : "outlined"} | |||
| @@ -4,7 +4,6 @@ import selectedTheme from "../../../themes"; | |||
| //import { IconButton } from "../../Buttons/IconButton/IconButton"; | |||
| import { PrimaryButton } from "../../Buttons/PrimaryButton/PrimaryButton"; | |||
| import { Icon } from "../../Icon/Icon"; | |||
| import HorizontalScroller from "../../Scroller/HorizontalScroller"; | |||
| import { ReactComponent as Category } from "../../../assets/images/svg/category.svg"; | |||
| import { ReactComponent as Subcategory } from "../../../assets/images/svg/subcategory.svg"; | |||
| import { ReactComponent as Quantity } from "../../../assets/images/svg/quantity.svg"; | |||
| @@ -74,31 +73,6 @@ export const Info = styled(Box)` | |||
| left: 5px; | |||
| } | |||
| `; | |||
| export const OfferTitle = styled(Typography)` | |||
| font-family: "Open Sans"; | |||
| flex: 1; | |||
| color: ${selectedTheme.primaryPurple}; | |||
| font-weight: 700; | |||
| font-size: 24px; | |||
| padding: 0 60px; | |||
| @media screen and (max-width: 600px) { | |||
| padding: 0; | |||
| font-size: 18px; | |||
| } | |||
| `; | |||
| export const OfferImage = styled.img` | |||
| min-width: 144px; | |||
| min-height: 144px; | |||
| width: 144px; | |||
| height: 144px; | |||
| margin-right: 20px; | |||
| object-fit: cover; | |||
| @media screen and (max-width: 600px) { | |||
| min-width: 144px; | |||
| margin-right: 13px; | |||
| } | |||
| `; | |||
| export const OfferAuthor = styled(Box)` | |||
| display: flex; | |||
| flex: 1; | |||
| @@ -116,16 +90,7 @@ export const OfferLocation = styled(Typography)` | |||
| line-height: 16px; | |||
| font-size: 12px; | |||
| `; | |||
| export const OfferDetails = styled(Box)` | |||
| display: flex; | |||
| flex-direction: column; | |||
| flex-wrap: ${(props) => (!props.halfwidth ? "no-wrap" : "wrap")}; | |||
| justify-content: space-between; | |||
| padding: 0 60px; | |||
| @media (max-width: 600px) { | |||
| padding: 0; | |||
| } | |||
| `; | |||
| export const OfferCategory = styled(Box)` | |||
| font-family: "Open Sans"; | |||
| color: ${selectedTheme.primaryText}; | |||
| @@ -147,34 +112,6 @@ export const OfferViews = styled(Box)` | |||
| font-size: 12px; | |||
| width: 34%; | |||
| `; | |||
| export const OfferDescriptionTitle = styled(Box)` | |||
| font-family: "Open Sans"; | |||
| font-size: 12px; | |||
| color: ${selectedTheme.primaryDarkText}; | |||
| line-height: 16px; | |||
| @media (max-width: 600px) { | |||
| font-size: 9px; | |||
| line-height: 13px; | |||
| } | |||
| `; | |||
| export const OfferDescriptionText = styled(Box)` | |||
| font-family: "Open Sans"; | |||
| font-size: 16px; | |||
| color: ${selectedTheme.primaryDarkText}; | |||
| line-height: 22px; | |||
| padding-bottom: 20px; | |||
| max-width: ${(props) => props.showBarterButton ? "calc(100% - 230px)" : "100%"}; | |||
| @media (max-width: 600px) { | |||
| font-size: 14px; | |||
| max-width: 100%; | |||
| max-height: 100px; | |||
| } | |||
| /* max-width: calc(100% - 230px); */ | |||
| /* overflow: hidden; */ | |||
| /* display: -webkit-box; | |||
| -webkit-line-clamp: 5; | |||
| -webkit-box-orient: vertical; */ | |||
| `; | |||
| export const OfferDescription = styled(Box)` | |||
| flex: 3; | |||
| `; | |||
| @@ -215,40 +152,8 @@ export const CheckButton = styled(PrimaryButton)` | |||
| height: 44px; | |||
| } | |||
| `; | |||
| export const Details = styled(Box)` | |||
| display: flex; | |||
| flex-direction: column; | |||
| gap: 12px; | |||
| ${(props) => props.hasScrollBar && !props.exchange && `height: 300px;`} | |||
| overflow-y: auto; | |||
| overflow-x: hidden; | |||
| ::-webkit-scrollbar { | |||
| width: 5px; | |||
| } | |||
| ::-webkit-scrollbar-thumb { | |||
| background: #c4c4c4; | |||
| border-radius: 3px; | |||
| } | |||
| @media screen and (max-width: 600px) { | |||
| margin-top: 15px; | |||
| ${(props) => | |||
| !props.hasScrollBar && props.exchange && | |||
| ` | |||
| overflow: hidden; | |||
| max-height: none;`} | |||
| } | |||
| `; | |||
| // export const OfferImage = styled.img` | |||
| // ` | |||
| export const Scroller = styled(HorizontalScroller)` | |||
| min-height: 144px; | |||
| min-width: 144px; | |||
| max-width: 100%; | |||
| /* & div { | |||
| margin: 0 9px; | |||
| } */ | |||
| `; | |||
| export const PublishButtonContainer = styled(Box)` | |||
| display: flex; | |||
| @@ -1,36 +0,0 @@ | |||
| import React from "react" | |||
| import {ReactComponent as DummyImage1 } from "../../../assets/images/svg/dummyImages/offer-1.svg" | |||
| export const Offer = { | |||
| id: 0, | |||
| title: "Vino", | |||
| category: "Hrana i pice", | |||
| subcategory:"Farbe", | |||
| status:"novo", | |||
| quantity:150, | |||
| numberOfViews:45, | |||
| description: "Vinarija Aleksić osnovana je u Vranju 2006. godine, otvorivši time put oživljavanju vinogradarstva na jugu Srbije.Vinarija Aleksić osnovana je u Vranju 2006. godine, otvorivši time put oživljavanju vinogradarstva na jugu Srbije.Vinarija Aleksić osnovana je u Vranju 2006. godine, otvorivši time put oživljavanju vinogradarstva na jugu Srbije.Vinarija Aleksić osnovana je u Vranju 2006. godine, otvorivši time put oživljavanju vinogradarstva na jugu Srbije.", | |||
| images: [ | |||
| { | |||
| id:0, | |||
| image: <DummyImage1 /> | |||
| }, | |||
| { | |||
| id:1, | |||
| image: <DummyImage1 /> | |||
| }, | |||
| { | |||
| id:2, | |||
| image: <DummyImage1 /> | |||
| }, | |||
| { | |||
| id:3, | |||
| image: <DummyImage1 /> | |||
| }, | |||
| { | |||
| id:4, | |||
| image: <DummyImage1 /> | |||
| }, | |||
| ], | |||
| postDate: "12.04.2022", | |||
| } | |||
| @@ -1,14 +1,46 @@ | |||
| import React from 'react' | |||
| import PropTypes from 'prop-types' | |||
| import React from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { | |||
| Details, | |||
| OfferDescriptionText, | |||
| OfferDescriptionTitle, | |||
| OfferImage, | |||
| OfferLittleDetails, | |||
| OfferTitle, | |||
| Scroller, | |||
| } from "./OfferDetails.styled"; | |||
| import { useTranslation } from "react-i18next"; | |||
| const OfferDetails = () => { | |||
| const OfferDetails = (props) => { | |||
| const offer = props.offer; | |||
| const { t } = useTranslation(); | |||
| return ( | |||
| <div>OfferDetails</div> | |||
| ) | |||
| } | |||
| <Details | |||
| hasScrollBar={!props.showPublishButton} | |||
| exchange={props.showExchangeButton} | |||
| > | |||
| <OfferTitle>{offer?.offer?.name}</OfferTitle> | |||
| <Scroller> | |||
| {offer?.offer?.images?.map((item) => { | |||
| return <OfferImage src={item} key={item} />; | |||
| })} | |||
| </Scroller> | |||
| <OfferLittleDetails> | |||
| <OfferDescriptionTitle> | |||
| {t("itemDetailsCard.description")} | |||
| </OfferDescriptionTitle> | |||
| <OfferDescriptionText showBarterButton={props.showExchangeButton}> | |||
| {offer?.offer?.description} | |||
| </OfferDescriptionText> | |||
| </OfferLittleDetails> | |||
| </Details> | |||
| ); | |||
| }; | |||
| OfferDetails.propTypes = { | |||
| offer: PropTypes.any, | |||
| } | |||
| offer: PropTypes.any, | |||
| showExchangeButton: PropTypes.bool, | |||
| showPublishButton: PropTypes.bool, | |||
| }; | |||
| export default OfferDetails | |||
| export default OfferDetails; | |||
| @@ -0,0 +1,101 @@ | |||
| import { Box, Typography } from "@mui/material"; | |||
| import styled from "styled-components"; | |||
| import selectedTheme from "../../../../themes"; | |||
| import HorizontalScroller from "../../../Scroller/HorizontalScroller"; | |||
| export const Details = styled(Box)` | |||
| display: flex; | |||
| flex-direction: column; | |||
| gap: 12px; | |||
| ${(props) => props.hasScrollBar && !props.exchange && `height: 300px;`} | |||
| overflow-y: auto; | |||
| overflow-x: hidden; | |||
| ::-webkit-scrollbar { | |||
| width: 5px; | |||
| } | |||
| ::-webkit-scrollbar-thumb { | |||
| background: #c4c4c4; | |||
| border-radius: 3px; | |||
| } | |||
| @media screen and (max-width: 600px) { | |||
| margin-top: 15px; | |||
| ${(props) => | |||
| !props.hasScrollBar && props.exchange && | |||
| ` | |||
| overflow: hidden; | |||
| max-height: none;`} | |||
| } | |||
| `; | |||
| export const OfferTitle = styled(Typography)` | |||
| font-family: "Open Sans"; | |||
| flex: 1; | |||
| color: ${selectedTheme.primaryPurple}; | |||
| font-weight: 700; | |||
| font-size: 24px; | |||
| padding: 0 60px; | |||
| @media screen and (max-width: 600px) { | |||
| padding: 0; | |||
| font-size: 18px; | |||
| } | |||
| `; | |||
| export const OfferLittleDetails = styled(Box)` | |||
| display: flex; | |||
| flex-direction: column; | |||
| flex-wrap: ${(props) => (!props.halfwidth ? "no-wrap" : "wrap")}; | |||
| justify-content: space-between; | |||
| padding: 0 60px; | |||
| @media (max-width: 600px) { | |||
| padding: 0; | |||
| } | |||
| `; | |||
| export const Scroller = styled(HorizontalScroller)` | |||
| min-height: 144px; | |||
| min-width: 144px; | |||
| max-width: 100%; | |||
| /* & div { | |||
| margin: 0 9px; | |||
| } */ | |||
| `; | |||
| export const OfferDescriptionTitle = styled(Box)` | |||
| font-family: "Open Sans"; | |||
| font-size: 12px; | |||
| color: ${selectedTheme.primaryDarkText}; | |||
| line-height: 16px; | |||
| @media (max-width: 600px) { | |||
| font-size: 9px; | |||
| line-height: 13px; | |||
| } | |||
| `; | |||
| export const OfferDescriptionText = styled(Box)` | |||
| font-family: "Open Sans"; | |||
| font-size: 16px; | |||
| color: ${selectedTheme.primaryDarkText}; | |||
| line-height: 22px; | |||
| padding-bottom: 20px; | |||
| max-width: ${(props) => props.showBarterButton ? "calc(100% - 230px)" : "100%"}; | |||
| @media (max-width: 600px) { | |||
| font-size: 14px; | |||
| max-width: 100%; | |||
| max-height: 100px; | |||
| } | |||
| /* max-width: calc(100% - 230px); */ | |||
| /* overflow: hidden; */ | |||
| /* display: -webkit-box; | |||
| -webkit-line-clamp: 5; | |||
| -webkit-box-orient: vertical; */ | |||
| `; | |||
| export const OfferImage = styled.img` | |||
| min-width: 144px; | |||
| min-height: 144px; | |||
| width: 144px; | |||
| height: 144px; | |||
| margin-right: 20px; | |||
| object-fit: cover; | |||
| @media screen and (max-width: 600px) { | |||
| min-width: 144px; | |||
| margin-right: 13px; | |||
| } | |||
| `; | |||
| @@ -11,14 +11,16 @@ import { formatDateTime } from "../../../util/helpers/dateHelpers"; | |||
| const MessageCard = (props) => { | |||
| const message = props.message; | |||
| const dateString = formatDateTime(new Date(message._created)) | |||
| const dateString = formatDateTime(new Date(message._created)); | |||
| return ( | |||
| <MessageCardContainer isMyMessage={props.isMyMessage}> | |||
| <ProfileImage src={props.image} /> | |||
| <MessageContent isMyMessage={props.isMyMessage}> | |||
| <MessageText isMyMessage={props.isMyMessage} >{props.message.text}</MessageText> | |||
| <MessageDate isMyMessage={props.isMyMessage} >{dateString}</MessageDate> | |||
| <MessageText isMyMessage={props.isMyMessage}> | |||
| {props.message.text} | |||
| </MessageText> | |||
| <MessageDate isMyMessage={props.isMyMessage}>{dateString}</MessageDate> | |||
| </MessageContent> | |||
| </MessageCardContainer> | |||
| ); | |||
| @@ -1,24 +0,0 @@ | |||
| export default [{ | |||
| id: 0, | |||
| name: "Coca-Cola", | |||
| quote: "Odlična saradnja. Sve preporuke za kompaniju", | |||
| isGood: true, | |||
| isGoodCommunication: "DA", | |||
| isSuccessfulSwap: "DA" | |||
| } | |||
| ,{ | |||
| id: 1, | |||
| name: "Voda Vrnjci", | |||
| quote: "Sasvim korektna saradnja, rado bih ponovio poslovanje sa Vama.", | |||
| isGood: true, | |||
| isGoodCommunication: "DA", | |||
| isSuccessfulSwap: "DA" | |||
| } | |||
| ,{ | |||
| id: 2, | |||
| name: "Women's Beauty House", | |||
| quote: "Nismo se najbolje razumeli, ali generalno ok", | |||
| isGood: false, | |||
| isGoodCommunication: "NE", | |||
| isSuccessfulSwap: "NE" | |||
| }]; | |||
| @@ -15,56 +15,35 @@ import { | |||
| ThumbDown, | |||
| ThumbUp, | |||
| } from "./UserReviewsCard.styled"; | |||
| import { ListItem } from "@mui/material"; | |||
| import selectedTheme from "../../../themes"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import { reviewEnum } from "../../../enums/reviewEnum"; | |||
| // import { useDispatch } from "react-redux"; | |||
| // import { fetchProfile } from "../../../store/actions/profile/profileActions"; | |||
| const UserReviewsCard = (props) => { | |||
| const { t } = useTranslation(); | |||
| // const dispatch = useDispatch(); | |||
| // useEffect(() => { | |||
| // if (props.review?.userId) { | |||
| // dispatch(fetchProfile(props.review.userId)); | |||
| // } | |||
| // }, [props.review?.userId]) | |||
| console.log(props); | |||
| const review = useMemo(() => { | |||
| if (props.givingReview) { | |||
| return { | |||
| ...props.review | |||
| } | |||
| ...props.review, | |||
| }; | |||
| } | |||
| let isSuccessfulSwap = "DA"; | |||
| if (props.review.succeeded === "failed") isSuccessfulSwap = "NE"; | |||
| let isGoodCommunication = "DA"; | |||
| if (props.review.communication === "could be better") isGoodCommunication = "MOŽE BOLJE"; | |||
| if (props.review.communication === "could be better") | |||
| isGoodCommunication = "MOŽE BOLJE"; | |||
| if (props.review.communication === "no") isGoodCommunication = "NE"; | |||
| return { | |||
| name: props.review.companyName, | |||
| image: props.review.image, | |||
| isGoodCommunication, | |||
| isSuccessfulSwap, | |||
| quote: props?.review?.message | |||
| } | |||
| quote: props?.review?.message, | |||
| }; | |||
| }, [props.review]); | |||
| const isGood = useMemo(() => { | |||
| if ( | |||
| review?.isGoodCommunication === reviewEnum.NO.mainText || | |||
| review?.isSuccessfulSwap === reviewEnum.NO.mainText | |||
| ) { | |||
| return false; | |||
| } | |||
| return true; | |||
| }, [review]); | |||
| return ( | |||
| <ReviewContainer key={review?.image}> | |||
| <ListItem alignItems="flex-start" sx={{ alignItems: "center", mt: 2 }}> | |||
| @@ -84,17 +63,14 @@ const UserReviewsCard = (props) => { | |||
| sx={{ pl: 2, py: 2 }} | |||
| > | |||
| <ThumbBox item> | |||
| {isGood ? <ThumbUp color="success" /> : <ThumbDown color="error" />} | |||
| {review.isSuccessfulSwap ? ( | |||
| <ThumbUp color="success" /> | |||
| ) : ( | |||
| <ThumbDown color="error" /> | |||
| )} | |||
| </ThumbBox> | |||
| <ReviewQuoteBox item> | |||
| <ReviewQuoteText | |||
| sx={{ display: "inline" }} | |||
| component="span" | |||
| variant="body2" | |||
| color="text.primary" | |||
| > | |||
| "{review?.quote}" | |||
| </ReviewQuoteText> | |||
| <ReviewQuoteText>"{review?.quote}"</ReviewQuoteText> | |||
| </ReviewQuoteBox> | |||
| </ReviewQuote> | |||
| <ReviewDetails sx={{ pl: 2, pb: 2 }}> | |||
| @@ -1,4 +1,4 @@ | |||
| import React, { useState, useEffect } from "react"; | |||
| import React, { useState, useEffect } from "react"; | |||
| import ChatCard from "../Cards/ChatCard/ChatCard"; | |||
| import { | |||
| ChatColumnContainer, | |||
| @@ -36,7 +36,7 @@ export const ChatColumn = () => { | |||
| useEffect(() => { | |||
| dispatch(fetchChats()); | |||
| }, []) | |||
| }, []); | |||
| useEffect(() => { | |||
| setSortOption(sorting.selectedSortOption); | |||
| @@ -82,7 +82,7 @@ export const ChatColumn = () => { | |||
| })} | |||
| </HeaderSelect> | |||
| </TitleSortContainer> | |||
| <ListHeader enableSort={true}></ListHeader> | |||
| <ListHeader enableSort={true} /> | |||
| <ListContainer> | |||
| {chats.map((item, index) => ( | |||
| <ChatCard key={index} chat={item} /> | |||
| @@ -9,15 +9,14 @@ import { | |||
| } from "./CreateReview.styled"; | |||
| import FirstStepCreateReview from "./FirstStep/FirstStepCreateReview"; | |||
| import SecondStepCreateReview from "./SecondStep/SecondStepCreateReview"; | |||
| import ThirdStepCreateReview from "./ThirdStep/ThirdStepCreateReview"; | |||
| import { useDispatch, useSelector } from "react-redux"; | |||
| import { giveReview } from "../../store/actions/review/reviewActions"; | |||
| import { selectUserId } from "../../store/selectors/loginSelectors"; | |||
| import { reviewEnum } from "../../enums/reviewEnum"; | |||
| import ThirdStepCreateReview from "./ThirdStep/ThirdStepCreateReview"; | |||
| const CreateReview = (props) => { | |||
| const offer = props.offer; | |||
| console.log("props aaa: ", props); | |||
| const [informations, setInformations] = useState({}); | |||
| const [currentStep, setCurrentStep] = useState(1); | |||
| const dispatch = useDispatch(); | |||
| @@ -29,15 +28,12 @@ const CreateReview = (props) => { | |||
| props.handleGiveReviewSuccess(); | |||
| }; | |||
| const submitForm = () => { | |||
| let communication; | |||
| if (informations.correctCommunication === reviewEnum.YES.mainText) | |||
| communication = "yes"; | |||
| let communication = "yes"; | |||
| if (informations.correctCommunication === reviewEnum.NO.mainText) | |||
| communication = "no"; | |||
| if (informations.correctCommunication === reviewEnum.NOT_BAD.mainText) | |||
| communication = "could be better"; | |||
| let succeeded; | |||
| succeeded = "failed"; | |||
| let succeeded = "failed"; | |||
| if (informations.exchangeSucceed === reviewEnum.YES.mainText) | |||
| succeeded = "succeeded"; | |||
| dispatch( | |||
| @@ -55,10 +51,6 @@ const CreateReview = (props) => { | |||
| }; | |||
| const goToNextStep = (newInformations) => { | |||
| setInformations((prevInformations) => { | |||
| console.log({ | |||
| ...prevInformations, | |||
| ...newInformations, | |||
| }); | |||
| return { | |||
| ...prevInformations, | |||
| ...newInformations, | |||
| @@ -87,12 +79,10 @@ const CreateReview = (props) => { | |||
| <CloseButton onClick={closeModal}> | |||
| <CloseIcon /> | |||
| </CloseButton> | |||
| {currentStep === 2 ? ( | |||
| {currentStep === 2 && ( | |||
| <BackIcon onClick={goToPrevStep}> | |||
| <ArrowBackIcon /> | |||
| </BackIcon> | |||
| ) : ( | |||
| "" | |||
| )} | |||
| {currentStep === 1 && ( | |||
| <FirstStepCreateReview | |||
| @@ -4,11 +4,9 @@ import { DirectChatContainer } from "./DirectChat.styled"; | |||
| import DirectChatHeaderTitle from "./DirectChatHeaderTitle/DirectChatHeaderTitle"; | |||
| import DirectChatHeader from "./DirectChatHeader/DirectChatHeader"; | |||
| import { useDispatch, useSelector } from "react-redux"; | |||
| // import { fetchOneOffer } from "../../store/actions/offers/offersActions"; | |||
| import { useLocation, useRouteMatch } from "react-router-dom"; | |||
| import { fetchOneChat } from "../../store/actions/chat/chatActions"; | |||
| import { fetchOneChat, setOneChat } from "../../store/actions/chat/chatActions"; | |||
| import { | |||
| // selectLatestChats, | |||
| selectSelectedChat, | |||
| } from "../../store/selectors/chatSelectors"; | |||
| import DirectChatContent from "./DirectChatContent/DirectChatContent"; | |||
| @@ -20,46 +18,43 @@ const DirectChat = () => { | |||
| const offer = useSelector(selectOffer); | |||
| const routeMatch = useRouteMatch(); | |||
| const location = useLocation(); | |||
| // const allChats = useSelector(selectLatestChats); | |||
| // const foundChat = useMemo( | |||
| // () => allChats.find((item) => item?.chat?._id === chat?.chat?._id), | |||
| // [chat, allChats] | |||
| // ); | |||
| const dispatch = useDispatch(); | |||
| const offerObject = useMemo(() => { | |||
| if (location?.state?.offerId) { | |||
| return offer?.offer; | |||
| } | |||
| return chat?.offer?.offer; | |||
| }, [chat, location.state, offer]); | |||
| const chatObject = useMemo(() => { | |||
| if (location?.state?.offerId) { | |||
| return {}; | |||
| } | |||
| return chat?.chat; | |||
| }, [chat, location.state]); | |||
| const interlocutorObject = useMemo(() => { | |||
| if (location?.state?.offerId) { | |||
| return { | |||
| image: offer?.companyData?.image, | |||
| name: offer?.companyData?.company?.name, | |||
| location: offer?.companyData?.company?.contacts?.location | |||
| } | |||
| location: offer?.companyData?.company?.contacts?.location, | |||
| }; | |||
| } | |||
| return chat?.interlocutor; | |||
| }, [chat,location.state, offer]); | |||
| console.log("offerObject: ", offerObject); | |||
| console.log("chatObject: ", chatObject); | |||
| console.log("interlucatorObject: ", interlocutorObject); | |||
| const dispatch = useDispatch(); | |||
| }, [chat, location.state, offer]); | |||
| useEffect(() => { | |||
| console.log(location.state) | |||
| if (routeMatch.params.idChat && location.state?.offerId) { | |||
| if (routeMatch.params.idChat) { | |||
| refreshChat(); | |||
| } | |||
| }, [routeMatch.params.idChat, location.state?.offerId]); | |||
| const refreshChat = () => { | |||
| if (routeMatch.params.idChat === "newMessage") { | |||
| dispatch(fetchOneOffer(location.state.offerId)) | |||
| dispatch(fetchOneOffer(location.state.offerId)); | |||
| dispatch(setOneChat({})); | |||
| } else { | |||
| dispatch(fetchOneChat(routeMatch.params.idChat)); | |||
| } | |||
| @@ -32,15 +32,15 @@ const DirectChatHeader = (props) => { | |||
| } | |||
| return false; | |||
| }, [exchange, userId]) | |||
| const refetchExchange = () => { | |||
| dispatch(fetchExchange(chat.chat.exchangeId)); | |||
| } | |||
| const makeReview = () => { | |||
| setShowReviewModal(true); | |||
| }; | |||
| const handleGiveReviewSuccess = () => { | |||
| refetchExchange(); | |||
| } | |||
| const refetchExchange = () => { | |||
| dispatch(fetchExchange(chat.chat.exchangeId)); | |||
| } | |||
| return ( | |||
| <DirectChatHeaderContainer> | |||
| {showReviewModal && ( | |||
| @@ -1,20 +1,24 @@ | |||
| import React from 'react' | |||
| import PropTypes from 'prop-types' | |||
| import { DirectChatHeaderTitleContainer, HeaderTitleContent, MessageIcon } from './DirectChatHeaderTitle.styled' | |||
| import { useTranslation } from 'react-i18next' | |||
| import React from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { | |||
| DirectChatHeaderTitleContainer, | |||
| HeaderTitleContent, | |||
| MessageIcon, | |||
| } from "./DirectChatHeaderTitle.styled"; | |||
| import { useTranslation } from "react-i18next"; | |||
| const DirectChatHeaderTitle = () => { | |||
| const {t} = useTranslation(); | |||
| const { t } = useTranslation(); | |||
| return ( | |||
| <DirectChatHeaderTitleContainer> | |||
| <MessageIcon /> | |||
| <HeaderTitleContent>{t("messages.headerTitle")}</HeaderTitleContent> | |||
| <MessageIcon /> | |||
| <HeaderTitleContent>{t("messages.headerTitle")}</HeaderTitleContent> | |||
| </DirectChatHeaderTitleContainer> | |||
| ) | |||
| } | |||
| ); | |||
| }; | |||
| DirectChatHeaderTitle.propTypes = { | |||
| children: PropTypes.node, | |||
| } | |||
| children: PropTypes.node, | |||
| }; | |||
| export default DirectChatHeaderTitle | |||
| export default DirectChatHeaderTitle; | |||
| @@ -8,7 +8,10 @@ import { | |||
| import { useTranslation } from "react-i18next"; | |||
| import selectedTheme from "../../../themes"; | |||
| import { useDispatch } from "react-redux"; | |||
| import { sendMessage, startNewChat } from "../../../store/actions/chat/chatActions"; | |||
| import { | |||
| sendMessage, | |||
| startNewChat, | |||
| } from "../../../store/actions/chat/chatActions"; | |||
| import { useHistory, useLocation } from "react-router-dom"; | |||
| const DirectChatNewMessage = (props) => { | |||
| @@ -35,13 +38,14 @@ const DirectChatNewMessage = (props) => { | |||
| setTypedValue(""); | |||
| }; | |||
| const handleMessageSendSuccess = (newChatId) => { | |||
| console.log("NEW CHAT ID: ", newChatId); | |||
| history.replace(`${newChatId}`); | |||
| } | |||
| }; | |||
| const initiateNewChat = (typedValue) => { | |||
| const offerId = location.state.offerId; | |||
| dispatch(startNewChat({offerId, message: typedValue, handleMessageSendSuccess})) | |||
| } | |||
| dispatch( | |||
| startNewChat({ offerId, message: typedValue, handleMessageSendSuccess }) | |||
| ); | |||
| }; | |||
| return ( | |||
| <DirectChatNewMessageContainer> | |||
| <NewMessageField | |||
| @@ -1,4 +1,4 @@ | |||
| import React, { useEffect, useMemo, useState } from "react"; | |||
| import React, { useEffect, useMemo } from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { MiniChatColumnContainer } from "./MiniChatColumn.styled"; | |||
| import MiniChatCard from "../../Cards/MiniChatCard/MiniChatCard"; | |||
| @@ -14,9 +14,7 @@ import { selectOffer } from "../../../store/selectors/offersSelectors"; | |||
| const MiniChatColumn = () => { | |||
| const chats = useSelector(selectLatestChats); | |||
| const [chatsToShow, setChatsToShow] = useState([]); | |||
| const selectedChat = useSelector(selectSelectedChat); | |||
| const [isThereNewChat, setIsThereNewChat] = useState(false); | |||
| const offer = useSelector(selectOffer); | |||
| const location = useLocation(); | |||
| const dispatch = useDispatch(); | |||
| @@ -25,29 +23,15 @@ const MiniChatColumn = () => { | |||
| return { | |||
| interlocutorData: { | |||
| image: offer?.companyData?.image, | |||
| name: offer?.companyData?.company?.name | |||
| name: offer?.companyData?.company?.name, | |||
| }, | |||
| offerData: { | |||
| name: offer?.offer?.name | |||
| } | |||
| } | |||
| } | |||
| return {} | |||
| }, [offer, location.state]) | |||
| useEffect(() => { | |||
| if (location.state?.offerId) { | |||
| setIsThereNewChat(true); | |||
| } else { | |||
| if (isThereNewChat !== false) { | |||
| dispatch(fetchChats()); | |||
| setIsThereNewChat(false); | |||
| } | |||
| name: offer?.offer?.name, | |||
| }, | |||
| }; | |||
| } | |||
| }, [location.state]) | |||
| useEffect(() => { | |||
| setChatsToShow([...chats]); | |||
| }, [chats]) | |||
| return {}; | |||
| }, [offer, location.state]); | |||
| useEffect(() => { | |||
| dispatch(fetchChats()); | |||
| @@ -55,20 +39,18 @@ const MiniChatColumn = () => { | |||
| return ( | |||
| <MiniChatColumnContainer> | |||
| <MiniChatColumnHeader /> | |||
| {isThereNewChat && ( | |||
| <MiniChatCard | |||
| chat={newChat} | |||
| selected | |||
| /> | |||
| )} | |||
| {chatsToShow.map((item) => { | |||
| {location.state?.offerId && <MiniChatCard chat={newChat} selected />} | |||
| {chats.map((item) => { | |||
| return ( | |||
| <MiniChatCard | |||
| key={Date.now() * Math.random()} | |||
| chat={item} | |||
| selected={item?.chat?._id === selectedChat?.chat?._id && !isThereNewChat} | |||
| /> | |||
| )})} | |||
| <MiniChatCard | |||
| key={Date.now() * Math.random()} | |||
| chat={item} | |||
| selected={ | |||
| item?.chat?._id === selectedChat?.chat?._id | |||
| } | |||
| /> | |||
| ); | |||
| })} | |||
| </MiniChatColumnContainer> | |||
| ); | |||
| }; | |||
| @@ -1,20 +1,26 @@ | |||
| import React from 'react' | |||
| import PropTypes from 'prop-types' | |||
| import { HeaderTitleContent, MailIcon, MiniChatColumnHeaderContainer } from './MiniChatColumnHeaderTitle.styled' | |||
| import { useTranslation } from 'react-i18next' | |||
| import React from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { | |||
| HeaderTitleContent, | |||
| MailIcon, | |||
| MiniChatColumnHeaderContainer, | |||
| } from "./MiniChatColumnHeaderTitle.styled"; | |||
| import { useTranslation } from "react-i18next"; | |||
| const MiniChatColumnHeader = () => { | |||
| const {t} = useTranslation(); | |||
| const { t } = useTranslation(); | |||
| return ( | |||
| <MiniChatColumnHeaderContainer> | |||
| <MailIcon/> | |||
| <HeaderTitleContent>{t("messages.miniChatHeaderTitle")}</HeaderTitleContent> | |||
| <MailIcon /> | |||
| <HeaderTitleContent> | |||
| {t("messages.miniChatHeaderTitle")} | |||
| </HeaderTitleContent> | |||
| </MiniChatColumnHeaderContainer> | |||
| ) | |||
| } | |||
| ); | |||
| }; | |||
| MiniChatColumnHeader.propTypes = { | |||
| children: PropTypes.node, | |||
| } | |||
| children: PropTypes.node, | |||
| }; | |||
| export default MiniChatColumnHeader | |||
| export default MiniChatColumnHeader; | |||
| @@ -15,20 +15,16 @@ import PropTypes from "prop-types"; | |||
| const DropdownList = (props) => { | |||
| const [listShown, setListShown] = useState(props.defaultOpen); | |||
| useEffect(() => { | |||
| if (props.open !== null || props.open !== undefined) { | |||
| if (props.open !== null || props.open !== undefined) | |||
| setListShown(props.open); | |||
| } | |||
| }, [props.open]); | |||
| const handleShow = () => { | |||
| if (props.setIsOpened) { | |||
| if (props.setIsOpened) | |||
| props.setIsOpened(!listShown); | |||
| } | |||
| if (!props.disabled) { | |||
| if (!props.disabled) | |||
| setListShown((prevState) => !prevState); | |||
| if (!(props.open !== null || props.open !== undefined)) | |||
| setListShown((prevState) => !prevState); | |||
| } | |||
| if (!(props.open !== null || props.open !== undefined)) { | |||
| setListShown(prevState => !prevState) | |||
| } | |||
| }; | |||
| return ( | |||
| <DropdownListContainer fullwidth={props.fullWidth ? 1 : 0}> | |||
| @@ -48,7 +44,11 @@ const DropdownList = (props) => { | |||
| > | |||
| {props.title} | |||
| </DropdownTitle> | |||
| {(props.open !== null && props.open !== undefined ? props.open : listShown) ? ( | |||
| {( | |||
| props.open !== null && props.open !== undefined | |||
| ? props.open | |||
| : listShown | |||
| ) ? ( | |||
| <ToggleIconOpened | |||
| style={props.toggleIconStyles} | |||
| onClick={!props.disabled ? () => handleShow() : () => {}} | |||
| @@ -65,16 +65,20 @@ const DropdownList = (props) => { | |||
| </ToggleIconClosed> | |||
| )} | |||
| </DropdownHeader> | |||
| <ToggleContainer shouldShow={props.open !== null && props.open !== undefined ? props.open : listShown}> | |||
| <ToggleContainer | |||
| shouldShow={ | |||
| props.open !== null && props.open !== undefined | |||
| ? props.open | |||
| : listShown | |||
| } | |||
| > | |||
| <DropdownOptions>{props.headerOptions}</DropdownOptions> | |||
| <ListContainer>{props.children}</ListContainer> | |||
| </ToggleContainer> | |||
| </DropdownListContainer> | |||
| ); | |||
| }; | |||
| export default DropdownList; | |||
| DropdownList.propTypes = { | |||
| title: PropTypes.string, | |||
| dropdownIcon: PropTypes.node, | |||
| @@ -90,7 +94,6 @@ DropdownList.propTypes = { | |||
| open: PropTypes.bool, | |||
| disabled: PropTypes.bool, | |||
| }; | |||
| DropdownList.defaultProps = { | |||
| fullWidth: false, | |||
| defaultOpen: false, | |||
| @@ -21,7 +21,7 @@ import { | |||
| ToolsContainer, | |||
| UserIcon, | |||
| } from "./Drawer.styled"; | |||
| import { useSelector } from "react-redux"; | |||
| import { useDispatch, useSelector } from "react-redux"; | |||
| import { selectUserId } from "../../../store/selectors/loginSelectors"; | |||
| import selectedTheme from "../../../themes"; | |||
| import { IconButton } from "../../Buttons/IconButton/IconButton"; | |||
| @@ -29,12 +29,14 @@ import { useTranslation } from "react-i18next"; | |||
| import { useHistory } from "react-router-dom"; | |||
| import { CHAT_PAGE, LOGIN_PAGE, MY_OFFERS_PAGE, REGISTER_PAGE } from "../../../constants/pages"; | |||
| import { selectProfileName } from "../../../store/selectors/profileSelectors"; | |||
| import { logoutUser } from "../../../store/actions/login/loginActions"; | |||
| export const Drawer = (props) => { | |||
| const user = useSelector(selectUserId); | |||
| const { t } = useTranslation(); | |||
| const history = useHistory(); | |||
| const name = useSelector(selectProfileName); | |||
| const dispatch = useDispatch(); | |||
| const goToMyPosts = () => { | |||
| props.toggleDrawer(); | |||
| @@ -60,7 +62,10 @@ export const Drawer = (props) => { | |||
| props.toggleDrawer(); | |||
| props.addOffer(); | |||
| } | |||
| const logoutUser = () => {}; | |||
| const logout = () => { | |||
| props.toggleDrawer(); | |||
| dispatch(logoutUser()); | |||
| }; | |||
| return ( | |||
| <DrawerContainer> | |||
| <CloseButton onClick={props.toggleDrawer}> | |||
| @@ -101,7 +106,7 @@ export const Drawer = (props) => { | |||
| {t("header.addOffer")} | |||
| </AddOfferButton> | |||
| <LogoutButton> | |||
| <IconButton onClick={logoutUser}> | |||
| <IconButton onClick={logout}> | |||
| <LogoutIcon /> | |||
| </IconButton> | |||
| <LogoutText>{t("common.logout")}</LogoutText> | |||
| @@ -1,4 +1,4 @@ | |||
| import React, { useEffect, useRef, useState } from "react"; | |||
| import React, { useCallback, useEffect, useRef, useState } from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { | |||
| AddFile, | |||
| @@ -12,8 +12,6 @@ import { IconButton } from "../Buttons/IconButton/IconButton"; | |||
| import { ReactComponent as EditIcon } from "../../assets/images/svg/edit.svg"; | |||
| import { ReactComponent as TrashIcon } from "../../assets/images/svg/trash.svg"; | |||
| // import { Input } from "@mui/material"; | |||
| const ImagePicker = (props) => { | |||
| const fileInputRef = useRef(null); | |||
| const imageRef = useRef(null); | |||
| @@ -21,27 +19,23 @@ const ImagePicker = (props) => { | |||
| const [isEditing, setIsEditing] = useState(false); | |||
| useEffect(() => { | |||
| console.log("image", props); | |||
| if (props.image) { | |||
| if (props.image) | |||
| setImage(props.image); | |||
| } | |||
| }, [props.image]); | |||
| let listener; | |||
| useEffect(() => { | |||
| listener = (event) => { | |||
| if (imageRef.current) { | |||
| if (imageRef.current.contains(event.target)) { | |||
| setIsEditing(true); | |||
| } else { | |||
| setIsEditing(false); | |||
| } | |||
| let listener = useCallback((event) => { | |||
| if (imageRef.current) { | |||
| if (imageRef.current.contains(event.target)) { | |||
| setIsEditing(true); | |||
| } else { | |||
| setIsEditing(false); | |||
| } | |||
| }; | |||
| } | |||
| }, [imageRef.current]) | |||
| useEffect(() => { | |||
| window.addEventListener("click", listener); | |||
| return () => window.removeEventListener("click", listener); | |||
| }, [imageRef]); | |||
| }, []); | |||
| const handleChange = () => { | |||
| fileInputRef.current.value = ""; | |||
| fileInputRef.current.click(); | |||
| @@ -57,7 +51,6 @@ const ImagePicker = (props) => { | |||
| console.log(error); | |||
| }; | |||
| }; | |||
| const handleDelete = () => { | |||
| if (props.deleteImage) props.deleteImage(); | |||
| setImage(""); | |||
| @@ -96,7 +89,6 @@ const ImagePicker = (props) => { | |||
| </ImagePickerContainer> | |||
| ); | |||
| }; | |||
| ImagePicker.propTypes = { | |||
| children: PropTypes.node, | |||
| className: PropTypes.string, | |||
| @@ -105,5 +97,4 @@ ImagePicker.propTypes = { | |||
| deleteImage: PropTypes.func, | |||
| showDeleteIcon: PropTypes.bool, | |||
| }; | |||
| export default ImagePicker; | |||
| @@ -1,187 +0,0 @@ | |||
| import React, { useEffect, useState, useRef } from 'react'; | |||
| import PropTypes from 'prop-types'; | |||
| import { ErrorMessage } from 'formik'; | |||
| import IconButton from '../IconButton/IconButton'; | |||
| import { ReactComponent as Search } from '../../assets/images/svg/search.svg'; | |||
| import { ReactComponent as EyeOn } from '../../assets/images/svg/eye-on.svg'; | |||
| import { ReactComponent as EyeOff } from '../../assets/images/svg/eye-off.svg'; | |||
| import { ReactComponent as CapsLock } from '../../assets/images/svg/caps-lock.svg'; | |||
| const BaseInputField = ({ | |||
| type, | |||
| label, | |||
| field, | |||
| form, | |||
| placeholder, | |||
| clearPlaceholderOnFocus = true, | |||
| isSearch, | |||
| className, | |||
| disabled, | |||
| centerText, | |||
| link, | |||
| errorMessage, | |||
| autoFocus, | |||
| isCapsLockOn, | |||
| ...props | |||
| }) => { | |||
| const [inputPlaceholder, setPlaceholder] = useState(placeholder); | |||
| const inputField = useRef(null); | |||
| useEffect(() => { | |||
| if (autoFocus) { | |||
| inputField.current.focus(); | |||
| } | |||
| }, [autoFocus, inputField]); | |||
| useEffect(() => { | |||
| if (errorMessage) { | |||
| form.setFieldError(field.name, errorMessage); | |||
| } | |||
| }, [errorMessage]); // eslint-disable-line | |||
| useEffect(() => { | |||
| setPlaceholder(placeholder); | |||
| }, [placeholder]); | |||
| const [inputType, setInputType] = useState('password'); | |||
| const passwordInput = type === 'password' ? ' c-input--password' : ''; | |||
| const showPassword = () => { | |||
| if (inputType === 'password') { | |||
| setInputType('text'); | |||
| } else { | |||
| setInputType('password'); | |||
| } | |||
| }; | |||
| // Nester Formik Field Names get bugged because of Undefined values, so i had to fix it like this | |||
| // If you ask why 0 and 1? I dont see a need for forms to be nested more then 2 levels? | |||
| const fieldName = field.name.split('.'); | |||
| const formError = | |||
| fieldName[0] && fieldName[1] | |||
| ? form.errors[fieldName[0]] && form.errors[fieldName[0]][fieldName[1]] | |||
| : form.errors[fieldName[0]]; | |||
| const formTouched = | |||
| fieldName[0] && fieldName[1] | |||
| ? form.touched[fieldName[0]] && form.touched[fieldName[0]][fieldName[1]] | |||
| : form.touched[fieldName[0]]; | |||
| function styles() { | |||
| let style = 'c-input'; | |||
| if (formError && formTouched) { | |||
| style += ` c-input--error`; | |||
| } | |||
| if (type === 'password') { | |||
| style += ` c-input--password`; | |||
| } | |||
| if (isSearch) { | |||
| style += ` c-input--search`; | |||
| } | |||
| if (centerText) { | |||
| style += ` c-input--center-text`; | |||
| } | |||
| if (type === 'number') { | |||
| style += ` c-input--demi-bold`; | |||
| } | |||
| if (className) { | |||
| style += ` ${className}`; | |||
| } | |||
| return style; | |||
| } | |||
| const additionalActions = () => { | |||
| if (!clearPlaceholderOnFocus) { | |||
| return null; | |||
| } | |||
| return { | |||
| onFocus: () => { | |||
| setPlaceholder(''); | |||
| }, | |||
| onBlur: (e) => { | |||
| setPlaceholder(placeholder); | |||
| field.onBlur(e); | |||
| }, | |||
| }; | |||
| }; | |||
| return ( | |||
| <div className={styles()}> | |||
| {!!label && ( | |||
| <label className="c-input__label" htmlFor={field.name}> | |||
| {label} | |||
| </label> | |||
| )} | |||
| {link && <div className="c-input__link">{link}</div>} | |||
| <div className="c-input__field-wrap"> | |||
| <input | |||
| ref={inputField} | |||
| type={type === 'password' ? inputType : type} | |||
| placeholder={inputPlaceholder} | |||
| disabled={disabled} | |||
| {...field} | |||
| {...props} | |||
| {...additionalActions()} | |||
| className="c-input__field" | |||
| /> | |||
| {!!isSearch && <Search className="c-input__icon" />} | |||
| {!!passwordInput && ( | |||
| <> | |||
| {isCapsLockOn && <CapsLock className="c-input__caps-lock" />} | |||
| <IconButton | |||
| onClick={() => { | |||
| showPassword(); | |||
| }} | |||
| className="c-input__icon" | |||
| > | |||
| {inputType === 'password' ? <EyeOff /> : <EyeOn />} | |||
| </IconButton> | |||
| </> | |||
| )} | |||
| </div> | |||
| <ErrorMessage name={field.name}> | |||
| {(errorMessage) => ( | |||
| <span className="c-input__error">{errorMessage}</span> | |||
| )} | |||
| </ErrorMessage> | |||
| </div> | |||
| ); | |||
| }; | |||
| BaseInputField.propTypes = { | |||
| type: PropTypes.string, | |||
| field: PropTypes.shape({ | |||
| name: PropTypes.string, | |||
| onFocus: PropTypes.func, | |||
| onBlur: PropTypes.func, | |||
| }), | |||
| form: PropTypes.shape({ | |||
| errors: PropTypes.shape({}), | |||
| setFieldError: PropTypes.func, | |||
| touched: PropTypes.shape({}), | |||
| }), | |||
| label: PropTypes.oneOfType([PropTypes.string, PropTypes.shape({})]), | |||
| placeholder: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), | |||
| disabled: PropTypes.bool, | |||
| isSearch: PropTypes.bool, | |||
| className: PropTypes.string, | |||
| link: PropTypes.node, | |||
| errorMessage: PropTypes.string, | |||
| centerText: PropTypes.bool, | |||
| clearPlaceholderOnFocus: PropTypes.bool, | |||
| demiBold: PropTypes.bool, | |||
| touched: PropTypes.bool, | |||
| autoFocus: PropTypes.bool, | |||
| isCapsLockOn: PropTypes.bool, | |||
| }; | |||
| export default BaseInputField; | |||
| @@ -1,40 +0,0 @@ | |||
| import React from 'react'; | |||
| import PropTypes from 'prop-types'; | |||
| import { ReactComponent as Checked } from '../../assets/images/svg/checked.svg'; | |||
| import { ReactComponent as Unchecked } from '../../assets/images/svg/unchecked.svg'; | |||
| const Checkbox = ({ className, children, name, onChange, checked, field }) => ( | |||
| <label htmlFor={name} className={`c-checkbox ${className || ''}`}> | |||
| <input | |||
| name={name} | |||
| id={name} | |||
| className="c-checkbox__field" | |||
| type="checkbox" | |||
| checked={checked} | |||
| {...field} | |||
| onChange={onChange || field.onChange} | |||
| /> | |||
| <div className="c-checkbox__indicator"> | |||
| {checked ? ( | |||
| <Checked className="c-checkbox__icon" /> | |||
| ) : ( | |||
| <Unchecked className="c-checkbox__icon" /> | |||
| )} | |||
| </div> | |||
| <div className="c-checkbox__text">{children}</div> | |||
| </label> | |||
| ); | |||
| Checkbox.propTypes = { | |||
| children: PropTypes.node, | |||
| onChange: PropTypes.func, | |||
| checked: PropTypes.bool, | |||
| name: PropTypes.string, | |||
| field: PropTypes.shape({ | |||
| onChange: PropTypes.func, | |||
| }), | |||
| className: PropTypes.string, | |||
| }; | |||
| export default Checkbox; | |||
| @@ -1,123 +0,0 @@ | |||
| import React, { useEffect, useRef } from 'react'; | |||
| import PropTypes from 'prop-types'; | |||
| import { ErrorMessage, useField } from 'formik'; | |||
| import CurrencyInput from 'react-currency-input-field'; | |||
| import { formatMoneyNumeral } from '../../util/helpers/numeralHelpers'; | |||
| import { | |||
| PLUS_SYMBOL, | |||
| MINUS_SYMBOL, | |||
| NUMPAD_MINUS_SYMBOL, | |||
| NUMPAD_PLUS_SYMBOL, | |||
| K_KEYCODE, | |||
| } from '../../constants/keyCodeConstants'; | |||
| const CurrencyField = ({ | |||
| autoFocus, | |||
| notCentered, | |||
| notBold, | |||
| label, | |||
| onChange, | |||
| value, | |||
| ...props | |||
| }) => { | |||
| const [field, meta] = useField(props); | |||
| const inputField = useRef(null); | |||
| function styles() { | |||
| let style = 'c-currency-field'; | |||
| if (meta.error && meta.touched) { | |||
| style += ` c-currency-field--error`; | |||
| } | |||
| if (notCentered) { | |||
| style += ` c-currency-field--not-centered`; | |||
| } | |||
| if (notBold) { | |||
| style += ` c-currency-field--not-bold`; | |||
| } | |||
| return style; | |||
| } | |||
| useEffect(() => { | |||
| if (autoFocus) { | |||
| inputField.current.focus(); | |||
| } | |||
| }, [autoFocus, inputField]); | |||
| const onKeydownHandler = (event) => { | |||
| if ( | |||
| event.keyCode === MINUS_SYMBOL || | |||
| event.keyCode === PLUS_SYMBOL || | |||
| event.keyCode === NUMPAD_MINUS_SYMBOL || | |||
| event.keyCode === NUMPAD_PLUS_SYMBOL || | |||
| event.keyCode === K_KEYCODE | |||
| ) { | |||
| event.preventDefault(); | |||
| } | |||
| }; | |||
| const prefix = formatMoneyNumeral(0); | |||
| const prefixSymbol = () => { | |||
| if (prefix.includes('CAD')) { | |||
| return 'CAD '; | |||
| } | |||
| return '$'; | |||
| }; | |||
| return ( | |||
| <div className={styles()}> | |||
| {!!label && ( | |||
| <label className="c-currency-field__label" htmlFor={field.name}> | |||
| {label} | |||
| </label> | |||
| )} | |||
| {value ? ( | |||
| <CurrencyInput | |||
| {...props} | |||
| prefix={prefixSymbol()} | |||
| onValueChange={(value) => { | |||
| onChange(value ? Number(value) : ''); | |||
| }} | |||
| onKeyDown={(event) => onKeydownHandler(event)} | |||
| ref={inputField} | |||
| defaultValue={0} | |||
| value={value} | |||
| /> | |||
| ) : ( | |||
| <CurrencyInput | |||
| {...props} | |||
| prefix={prefixSymbol()} | |||
| onValueChange={(value) => { | |||
| onChange(value ? Number(value) : ''); | |||
| }} | |||
| onKeyDown={(event) => onKeydownHandler(event)} | |||
| ref={inputField} | |||
| /> | |||
| )} | |||
| <ErrorMessage name={field.name}> | |||
| {(errorMessage) => ( | |||
| <span className="c-currency-field__error">{errorMessage}</span> | |||
| )} | |||
| </ErrorMessage> | |||
| </div> | |||
| ); | |||
| }; | |||
| CurrencyField.propTypes = { | |||
| field: PropTypes.shape({ | |||
| name: PropTypes.string, | |||
| }), | |||
| form: PropTypes.shape({}), | |||
| label: PropTypes.oneOfType([PropTypes.string, PropTypes.shape({})]), | |||
| disabled: PropTypes.bool, | |||
| onChange: PropTypes.func, | |||
| autoFocus: PropTypes.bool, | |||
| notCentered: PropTypes.bool, | |||
| notBold: PropTypes.bool, | |||
| value: PropTypes.number, | |||
| }; | |||
| export default CurrencyField; | |||
| @@ -1,33 +0,0 @@ | |||
| import React from 'react'; | |||
| import PropTypes from 'prop-types'; | |||
| import BaseInputField from './BaseInputField'; | |||
| const EmailField = ({ | |||
| field, | |||
| form, | |||
| label, | |||
| placeholder, | |||
| disabled, | |||
| ...props | |||
| }) => ( | |||
| <BaseInputField | |||
| type="email" | |||
| label={label} | |||
| placeholder={placeholder} | |||
| disabled={disabled} | |||
| form={form} | |||
| field={field} | |||
| {...props} | |||
| /> | |||
| ); | |||
| EmailField.propTypes = { | |||
| field: PropTypes.shape({}), | |||
| form: PropTypes.shape({}), | |||
| label: PropTypes.oneOfType([PropTypes.string, PropTypes.shape({})]), | |||
| placeholder: PropTypes.string, | |||
| disabled: PropTypes.bool, | |||
| }; | |||
| export default EmailField; | |||
| @@ -1,74 +0,0 @@ | |||
| import React from 'react'; | |||
| import PropTypes from 'prop-types'; | |||
| import BaseInputField from './BaseInputField'; | |||
| import { | |||
| PERIOD_SYMBOL, | |||
| COMMA_SYMBOL, | |||
| PLUS_SYMBOL, | |||
| MINUS_SYMBOL, | |||
| NUMPAD_PERIOD_SYMBOL, | |||
| NUMPAD_MINUS_SYMBOL, | |||
| NUMPAD_PLUS_SYMBOL, | |||
| DOWN_ARROW_KEYCODE, | |||
| UP_ARROW_KEYCODE, | |||
| } from '../../constants/keyCodeConstants'; | |||
| const NumberField = ({ | |||
| field, | |||
| form, | |||
| label, | |||
| placeholder, | |||
| disabled, | |||
| preventAllExceptNumbers, | |||
| ...props | |||
| }) => { | |||
| const onKeydownHandler = (event) => { | |||
| if (preventAllExceptNumbers) { | |||
| if ( | |||
| event.keyCode === PERIOD_SYMBOL || | |||
| event.keyCode === COMMA_SYMBOL || | |||
| event.keyCode === NUMPAD_PERIOD_SYMBOL || | |||
| event.keyCode === DOWN_ARROW_KEYCODE || | |||
| event.keyCode === UP_ARROW_KEYCODE | |||
| ) { | |||
| event.preventDefault(); | |||
| } | |||
| } | |||
| if ( | |||
| event.keyCode === PLUS_SYMBOL || | |||
| event.keyCode === MINUS_SYMBOL || | |||
| event.keyCode === NUMPAD_MINUS_SYMBOL || | |||
| event.keyCode === NUMPAD_PLUS_SYMBOL || | |||
| event.keyCode === DOWN_ARROW_KEYCODE || | |||
| event.keyCode === UP_ARROW_KEYCODE | |||
| ) { | |||
| event.preventDefault(); | |||
| } | |||
| }; | |||
| return ( | |||
| <BaseInputField | |||
| type="number" | |||
| label={label} | |||
| placeholder={placeholder} | |||
| disabled={disabled} | |||
| form={form} | |||
| field={field} | |||
| {...props} | |||
| onKeyDown={(event) => onKeydownHandler(event)} | |||
| /> | |||
| ); | |||
| }; | |||
| NumberField.propTypes = { | |||
| field: PropTypes.shape({}), | |||
| form: PropTypes.shape({}), | |||
| label: PropTypes.oneOfType([PropTypes.string, PropTypes.shape({})]), | |||
| placeholder: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), | |||
| disabled: PropTypes.bool, | |||
| preventAllExceptNumbers: PropTypes.bool, | |||
| }; | |||
| export default NumberField; | |||
| @@ -1,74 +0,0 @@ | |||
| import React, { useState } from 'react'; | |||
| import PropTypes from 'prop-types'; | |||
| import BaseInputField from './BaseInputField'; | |||
| import PasswordStrength from './PasswordStrength'; | |||
| const PasswordField = ({ | |||
| field, | |||
| form, | |||
| label, | |||
| placeholder, | |||
| disabled, | |||
| shouldTestPasswordStrength, | |||
| autoFocus, | |||
| ...props | |||
| }) => { | |||
| const [passwordValue, setPasswordValue] = useState(''); | |||
| const [isCapsLockOn, setIsCapsLockOn] = useState(false); | |||
| const onChange = (e) => { | |||
| if (shouldTestPasswordStrength) { | |||
| const { value } = e.target; | |||
| setPasswordValue(value); | |||
| } | |||
| field.onChange(e); | |||
| }; | |||
| const onKeyDown = (keyEvent) => { | |||
| if (keyEvent.getModifierState('CapsLock')) { | |||
| setIsCapsLockOn(true); | |||
| } else { | |||
| setIsCapsLockOn(false); | |||
| } | |||
| }; | |||
| return ( | |||
| <div className="c-password"> | |||
| <BaseInputField | |||
| type="password" | |||
| label={label} | |||
| placeholder={placeholder} | |||
| disabled={disabled} | |||
| form={form} | |||
| field={field} | |||
| {...props} | |||
| onChange={onChange} | |||
| autoFocus={autoFocus} | |||
| onKeyDown={onKeyDown} | |||
| isCapsLockOn={isCapsLockOn} | |||
| /> | |||
| {shouldTestPasswordStrength && ( | |||
| <PasswordStrength | |||
| passwordValue={passwordValue} | |||
| shouldTestPasswordStrength | |||
| /> | |||
| )} | |||
| </div> | |||
| ); | |||
| }; | |||
| PasswordField.propTypes = { | |||
| field: PropTypes.shape({ | |||
| onChange: PropTypes.func, | |||
| }), | |||
| form: PropTypes.shape({}), | |||
| label: PropTypes.oneOfType([PropTypes.string, PropTypes.shape({})]), | |||
| placeholder: PropTypes.string, | |||
| disabled: PropTypes.bool, | |||
| shouldTestPasswordStrength: PropTypes.bool, | |||
| autoFocus: PropTypes.bool, | |||
| }; | |||
| export default PasswordField; | |||
| @@ -1,130 +0,0 @@ | |||
| import React, { useEffect, useRef, useState } from 'react'; | |||
| import PropTypes from 'prop-types'; | |||
| import owasp from 'owasp-password-strength-test'; | |||
| import i18next from 'i18next'; | |||
| owasp.config({ | |||
| minOptionalTestsToPass: 3, | |||
| }); | |||
| const passwordStrengthOptions = [ | |||
| { | |||
| strength: 'weak', | |||
| color: '#FF5028', | |||
| }, | |||
| { | |||
| strength: 'average', | |||
| color: '#FDB942', | |||
| }, | |||
| { | |||
| strength: 'good', | |||
| color: '#06BEE7', | |||
| }, | |||
| { | |||
| strength: 'strong', | |||
| color: '#00876A', | |||
| }, | |||
| ]; | |||
| /** | |||
| * User must pass a required test and at least 3 optional. | |||
| * @param result - owasp result | |||
| * @returns {number} - index of password strength 0-3 | |||
| */ | |||
| function getPasswordStrengthIndex(result) { | |||
| // requirement for strong password is required test passed and at least 3 optional tests | |||
| if (result.strong) { | |||
| return 3; | |||
| } | |||
| if (!result.strong && result.optionalTestsPassed >= 3) { | |||
| return 2; | |||
| } | |||
| if (result.optionalTestsPassed <= 0) { | |||
| return 0; | |||
| } | |||
| return result.optionalTestsPassed - 1; | |||
| } | |||
| const PasswordStrength = ({ | |||
| shouldTestPasswordStrength, | |||
| passwordValue, | |||
| passwordStrengthTestsRequired, | |||
| }) => { | |||
| const strengthContainer = useRef(null); | |||
| const [passwordStrength, setPasswordStrength] = useState({ | |||
| width: 0, | |||
| color: 'red', | |||
| }); | |||
| const [error, setError] = useState(''); | |||
| useEffect(() => { | |||
| if (shouldTestPasswordStrength && passwordValue) { | |||
| const bBox = strengthContainer.current.getBoundingClientRect(); | |||
| const result = owasp.test(passwordValue); | |||
| const passwordStrengthIndex = getPasswordStrengthIndex(result); | |||
| const passwordOption = passwordStrengthOptions[passwordStrengthIndex]; | |||
| const width = !passwordValue | |||
| ? 0 | |||
| : (bBox.width * (passwordStrengthIndex + 1)) / | |||
| passwordStrengthTestsRequired; | |||
| setPasswordStrength({ width, color: passwordOption.color }); | |||
| const strength = i18next.t(`password.${passwordOption.strength}`); | |||
| setError(i18next.t('login.passwordStrength', { strength })); | |||
| } | |||
| }, [ | |||
| passwordValue, | |||
| shouldTestPasswordStrength, | |||
| passwordStrengthTestsRequired, | |||
| ]); | |||
| if (!shouldTestPasswordStrength || !passwordValue) { | |||
| return null; | |||
| } | |||
| const renderError = () => { | |||
| if (!error) { | |||
| return null; | |||
| } | |||
| return ( | |||
| <div | |||
| className="c-input--error" | |||
| style={{ | |||
| color: passwordStrength.color, | |||
| }} | |||
| > | |||
| {error} | |||
| </div> | |||
| ); | |||
| }; | |||
| return ( | |||
| <div ref={strengthContainer} className="c-password-strength__container"> | |||
| <div className="c-password-strength__line--wrapper"> | |||
| <div | |||
| className="c-password-strength__line" | |||
| style={{ | |||
| backgroundColor: passwordStrength.color, | |||
| width: passwordStrength.width, | |||
| }} | |||
| /> | |||
| </div> | |||
| {renderError()} | |||
| </div> | |||
| ); | |||
| }; | |||
| PasswordStrength.propTypes = { | |||
| shouldTestPasswordStrength: PropTypes.bool, | |||
| passwordValue: PropTypes.string, | |||
| passwordStrengthTestsRequired: PropTypes.number, | |||
| }; | |||
| PasswordStrength.defaultProps = { | |||
| passwordStrengthTestsRequired: 4, | |||
| }; | |||
| export default PasswordStrength; | |||
| @@ -1,45 +0,0 @@ | |||
| import React from 'react'; | |||
| import PropTypes from 'prop-types'; | |||
| import NumberFormat from 'react-number-format'; | |||
| import TextField from './TextField'; | |||
| const PercentageField = ({ field, ...props }) => { | |||
| const handleOnChange = (percentageField) => { | |||
| const { floatValue } = percentageField; | |||
| if (!props.onChange) { | |||
| throw Error('Provide an onChange handler'); | |||
| } | |||
| if (floatValue > 100) { | |||
| return props.onChange('100'); | |||
| } | |||
| if (floatValue <= 0 || !floatValue) { | |||
| return props.onChange('0'); | |||
| } | |||
| return props.onChange(floatValue.toString()); | |||
| }; | |||
| return ( | |||
| <NumberFormat | |||
| format="###%" | |||
| value={field.value} | |||
| customInput={TextField} | |||
| field={field} | |||
| {...props} | |||
| onValueChange={handleOnChange} | |||
| onChange={() => {}} | |||
| /> | |||
| ); | |||
| }; | |||
| PercentageField.propTypes = { | |||
| onChange: PropTypes.func, | |||
| field: PropTypes.shape({ | |||
| value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), | |||
| }), | |||
| }; | |||
| export default PercentageField; | |||
| @@ -1,49 +0,0 @@ | |||
| import React from 'react'; | |||
| import PropTypes from 'prop-types'; | |||
| import { ErrorMessage, useField } from 'formik'; | |||
| import PhoneInput from 'react-phone-number-input'; | |||
| import 'react-phone-number-input/style.css'; | |||
| const PhoneNumberField = ({ label, ...props }) => { | |||
| const [field, meta] = useField(props); | |||
| const inputErrorClassName = | |||
| meta.error && meta.touched ? 'c-input--error' : ''; | |||
| return ( | |||
| <div className={`c-input c-phone-number ${inputErrorClassName}`}> | |||
| {!!label && ( | |||
| <label className="c-input__label" htmlFor={field.name}> | |||
| {label} | |||
| </label> | |||
| )} | |||
| <PhoneInput | |||
| international | |||
| defaultCountry="US" | |||
| {...field} | |||
| {...props} | |||
| onChange={(value) => { | |||
| props.onPhoneChange(value); | |||
| }} | |||
| countryOptionsOrder={['US']} | |||
| /> | |||
| <ErrorMessage name={field.name}> | |||
| {(errorMessage) => ( | |||
| <span className="c-input__error">{errorMessage}</span> | |||
| )} | |||
| </ErrorMessage> | |||
| </div> | |||
| ); | |||
| }; | |||
| PhoneNumberField.propTypes = { | |||
| field: PropTypes.shape({ | |||
| name: PropTypes.string, | |||
| }), | |||
| form: PropTypes.shape({}), | |||
| label: PropTypes.oneOfType([PropTypes.string, PropTypes.shape({})]), | |||
| disabled: PropTypes.bool, | |||
| onChange: PropTypes.func, | |||
| onPhoneChange: PropTypes.func, | |||
| }; | |||
| export default PhoneNumberField; | |||
| @@ -1,54 +0,0 @@ | |||
| import React from 'react'; | |||
| import PropTypes from 'prop-types'; | |||
| import { ReactComponent as RadioOn } from '../../assets/images/svg/radio-on.svg'; | |||
| import { ReactComponent as RadioOff } from '../../assets/images/svg/radio-off.svg'; | |||
| const Checkbox = ({ | |||
| className, | |||
| children, | |||
| name, | |||
| checked, | |||
| field, | |||
| value, | |||
| selected, | |||
| id, | |||
| }) => ( | |||
| <label | |||
| htmlFor={name} | |||
| className={`c-radio ${selected ? 'c-radio--selected' : ''} ${ | |||
| className || '' | |||
| }`} | |||
| > | |||
| <input | |||
| name={name} | |||
| id={id} | |||
| className="c-radio__field" | |||
| type="radio" | |||
| checked={checked} | |||
| value={value} | |||
| {...field} | |||
| /> | |||
| <div className="c-radio__indicator"> | |||
| {selected ? ( | |||
| <RadioOn className="c-radio__icon" /> | |||
| ) : ( | |||
| <RadioOff className="c-radio__icon" /> | |||
| )} | |||
| </div> | |||
| <div className="c-radio__text">{children}</div> | |||
| </label> | |||
| ); | |||
| Checkbox.propTypes = { | |||
| children: PropTypes.node, | |||
| checked: PropTypes.bool, | |||
| name: PropTypes.string, | |||
| field: PropTypes.shape({}), | |||
| form: PropTypes.shape({}), | |||
| className: PropTypes.string, | |||
| value: PropTypes.string, | |||
| selected: PropTypes.bool, | |||
| id: PropTypes.string, | |||
| }; | |||
| export default Checkbox; | |||
| @@ -1,37 +0,0 @@ | |||
| import React from 'react'; | |||
| import PropTypes from 'prop-types'; | |||
| import BaseInputField from './BaseInputField'; | |||
| const Search = ({ | |||
| field, | |||
| form, | |||
| label, | |||
| placeholder, | |||
| disabled, | |||
| className, | |||
| ...props | |||
| }) => ( | |||
| <BaseInputField | |||
| type="text" | |||
| label="" | |||
| placeholder={placeholder} | |||
| disabled={disabled} | |||
| form={form} | |||
| field={field} | |||
| isSearch | |||
| className={className} | |||
| {...props} | |||
| /> | |||
| ); | |||
| Search.propTypes = { | |||
| field: PropTypes.shape({}), | |||
| form: PropTypes.shape({}), | |||
| label: PropTypes.oneOfType([PropTypes.string, PropTypes.shape({})]), | |||
| placeholder: PropTypes.string, | |||
| disabled: PropTypes.bool, | |||
| className: PropTypes.string, | |||
| }; | |||
| export default Search; | |||
| @@ -1,122 +0,0 @@ | |||
| import React, { useEffect } from 'react'; | |||
| import PropTypes from 'prop-types'; | |||
| import Select, { components, createFilter } from 'react-select'; | |||
| import { ErrorMessage, useField } from 'formik'; | |||
| import { ReactComponent as FilledChevronDown } from '../../assets/images/svg/filled-chevron-down.svg'; | |||
| const SelectField = ({ | |||
| label, | |||
| disabled, | |||
| options, | |||
| link, | |||
| defaultSelected = null, | |||
| dropdownFullHeight, | |||
| selectOption, | |||
| ...props | |||
| }) => { | |||
| const [field, meta, helpers] = useField(props); | |||
| const filterConfig = { | |||
| ignoreCase: true, | |||
| ignoreAccents: true, | |||
| trim: true, | |||
| matchFrom: 'start', | |||
| }; | |||
| useEffect(() => { | |||
| if (defaultSelected) { | |||
| helpers.setValue(defaultSelected); | |||
| } | |||
| }, [defaultSelected]); // eslint-disable-line | |||
| const DropdownIndicator = (props) => | |||
| components.DropdownIndicator && ( | |||
| <components.DropdownIndicator {...props}> | |||
| <FilledChevronDown /> | |||
| </components.DropdownIndicator> | |||
| ); | |||
| function styles() { | |||
| let style = 'c-input'; | |||
| if (meta.error && meta.touched) { | |||
| style += ` c-input--error`; | |||
| } | |||
| if (dropdownFullHeight) { | |||
| style += ` c-input--dropdown-full-height`; | |||
| } | |||
| return style; | |||
| } | |||
| return ( | |||
| <div className={styles()}> | |||
| {!!label && ( | |||
| <label className="c-input__label" htmlFor={field.name}> | |||
| {label} | |||
| </label> | |||
| )} | |||
| {!!link && <div className="c-input__link">{link}</div>} | |||
| <Select | |||
| defaultValue={defaultSelected || options[0]} | |||
| components={{ DropdownIndicator }} | |||
| isSearchable={false} | |||
| classNamePrefix="c-select" | |||
| options={options} | |||
| isDisabled={disabled} | |||
| {...field} | |||
| {...props} | |||
| onBlur={(e) => { | |||
| helpers.setTouched(true); | |||
| field.onBlur(e); | |||
| }} | |||
| onChange={(selectedOption) => { | |||
| helpers.setValue(selectedOption); | |||
| if (props.onChange) { | |||
| props.onChange(); | |||
| } | |||
| if (selectOption) { | |||
| selectOption(selectedOption); | |||
| } | |||
| }} | |||
| filterOption={createFilter(filterConfig)} | |||
| /> | |||
| <ErrorMessage name={field.name}> | |||
| {(errorMessage) => { | |||
| if (typeof errorMessage === 'string') { | |||
| return <span className="c-input__error">{errorMessage}</span>; | |||
| } | |||
| return <span className="c-input__error">{errorMessage.value}</span>; | |||
| }} | |||
| </ErrorMessage> | |||
| </div> | |||
| ); | |||
| }; | |||
| SelectField.propTypes = { | |||
| field: PropTypes.shape({ | |||
| name: PropTypes.string, | |||
| }), | |||
| form: PropTypes.shape({}), | |||
| label: PropTypes.oneOfType([PropTypes.string, PropTypes.shape({})]), | |||
| disabled: PropTypes.bool, | |||
| options: PropTypes.arrayOf( | |||
| PropTypes.shape({ | |||
| label: PropTypes.string, | |||
| value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), | |||
| }), | |||
| ), | |||
| onChange: PropTypes.func, | |||
| link: PropTypes.node, | |||
| defaultSelected: PropTypes.shape({ | |||
| label: PropTypes.string, | |||
| value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), | |||
| }), | |||
| dropdownFullHeight: PropTypes.bool, | |||
| selectOption: PropTypes.func, | |||
| }; | |||
| export default SelectField; | |||
| @@ -1,72 +0,0 @@ | |||
| import React from 'react'; | |||
| import PropTypes from 'prop-types'; | |||
| import BaseInputField from './BaseInputField'; | |||
| import { | |||
| BACKSPACE_KEYCODE, | |||
| TAB_KEYCODE, | |||
| RIGHT_ARROW_KEYCODE, | |||
| LEFT_ARROW_KEYCODE, | |||
| } from '../../constants/keyCodeConstants'; | |||
| const TextField = ({ | |||
| field, | |||
| form, | |||
| label, | |||
| placeholder, | |||
| disabled, | |||
| centerText, | |||
| autoFocus, | |||
| preventAllExceptNumbers, | |||
| ...props | |||
| }) => { | |||
| const onKeydownHandler = (event) => { | |||
| if (preventAllExceptNumbers) { | |||
| if ( | |||
| event.keyCode === BACKSPACE_KEYCODE || | |||
| event.keyCode === TAB_KEYCODE || | |||
| event.keyCode === RIGHT_ARROW_KEYCODE || | |||
| event.keyCode === LEFT_ARROW_KEYCODE | |||
| ) { | |||
| return; | |||
| } | |||
| if ( | |||
| (event.keyCode < 58 && event.keyCode > 47) || | |||
| (event.keyCode < 106 && event.keyCode > 95) | |||
| ) { | |||
| return; | |||
| } | |||
| event.preventDefault(); | |||
| } | |||
| }; | |||
| return ( | |||
| <BaseInputField | |||
| autoFocus={autoFocus} | |||
| type="text" | |||
| label={label} | |||
| placeholder={placeholder} | |||
| disabled={disabled} | |||
| form={form} | |||
| field={field} | |||
| centerText={centerText} | |||
| {...props} | |||
| onKeyDown={(event) => onKeydownHandler(event)} | |||
| /> | |||
| ); | |||
| }; | |||
| TextField.propTypes = { | |||
| field: PropTypes.shape({}), | |||
| form: PropTypes.shape({}), | |||
| label: PropTypes.string, | |||
| placeholder: PropTypes.string, | |||
| disabled: PropTypes.bool, | |||
| centerText: PropTypes.bool, | |||
| autoFocus: PropTypes.bool, | |||
| preventAllExceptNumbers: PropTypes.bool, | |||
| }; | |||
| export default TextField; | |||
| @@ -1,28 +1,27 @@ | |||
| import React from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { useHistory } from "react-router-dom"; | |||
| //import { IconButton } from "../../Buttons/IconButton/IconButton"; | |||
| import { HeaderContainer, HeaderText, ButtonContainer } from "./Header.styled"; | |||
| import { ArrowButton } from "../../Buttons/ArrowButton/ArrowButton"; | |||
| // const DownArrow = (props) => ( | |||
| // <IconStyled {...props}> | |||
| // <Down /> | |||
| // </IconStyled> | |||
| // ); | |||
| import { useTranslation } from "react-i18next"; | |||
| const Header = (props) => { | |||
| const history = useHistory(); | |||
| const {t} = useTranslation(); | |||
| const handleBackButton = () => { | |||
| history.goBack(); | |||
| }; | |||
| return ( | |||
| <HeaderContainer onClick={handleBackButton} component="header" className={props.className}> | |||
| <HeaderContainer | |||
| onClick={handleBackButton} | |||
| component="header" | |||
| className={props.className} | |||
| > | |||
| <ButtonContainer> | |||
| <ArrowButton side={"left"}></ArrowButton> | |||
| <HeaderText>Nazad na objave</HeaderText> | |||
| <HeaderText>{t("itemDetailsCard.headerTitle")}</HeaderText> | |||
| </ButtonContainer> | |||
| </HeaderContainer> | |||
| ); | |||
| @@ -1,31 +1,29 @@ | |||
| import React, { useMemo } from 'react'; | |||
| import React, { useMemo } from "react"; | |||
| import Header from "./Header/Header"; | |||
| import { useSelector } from "react-redux"; | |||
| import { ItemDetailsContainer } from "./ItemDetails.styled"; | |||
| import ItemDetailsCard from "../Cards/ItemDetailsCard/ItemDetailsCard"; | |||
| import ItemDetailsHeaderCard from "./ItemDetailsHeaderCard/ItemDetailsHeaderCard"; | |||
| import { selectOffer } from '../../store/selectors/offersSelectors'; | |||
| import { selectUserId } from '../../store/selectors/loginSelectors'; | |||
| import { selectOffer } from "../../store/selectors/offersSelectors"; | |||
| import { selectUserId } from "../../store/selectors/loginSelectors"; | |||
| // import { useHistory } from 'react-router-dom'; | |||
| const ItemDetails = () => { | |||
| const offer = useSelector(selectOffer); | |||
| const userId = useSelector(selectUserId); | |||
| let isMyProfile = useMemo(() => { | |||
| if (offer?.offer?.userId?.toString() === userId.toString()) { | |||
| return true; | |||
| } | |||
| return false; | |||
| }, [offer, userId]) | |||
| return ( | |||
| <ItemDetailsContainer> | |||
| <Header/> | |||
| <ItemDetailsHeaderCard offer={offer} isMyProfile={isMyProfile} /> | |||
| <ItemDetailsCard offer={offer} isMyOffer={isMyProfile}/> | |||
| </ItemDetailsContainer> | |||
| ) | |||
| } | |||
| const offer = useSelector(selectOffer); | |||
| const userId = useSelector(selectUserId); | |||
| let isMyProfile = useMemo(() => { | |||
| if (offer?.offer?.userId?.toString() === userId.toString()) { | |||
| return true; | |||
| } | |||
| return false; | |||
| }, [offer, userId]); | |||
| return ( | |||
| <ItemDetailsContainer> | |||
| <Header /> | |||
| <ItemDetailsHeaderCard offer={offer} isMyProfile={isMyProfile} /> | |||
| <ItemDetailsCard offer={offer} isMyOffer={isMyProfile} /> | |||
| </ItemDetailsContainer> | |||
| ); | |||
| }; | |||
| export default ItemDetails; | |||
| @@ -1,49 +1,30 @@ | |||
| import React from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { | |||
| DetailIcon, | |||
| DetailText, | |||
| MessageIcon, | |||
| OfferDetails, | |||
| OfferImage, | |||
| OfferTitle, | |||
| DetailContainer, | |||
| HeaderTop, | |||
| HeaderDetails, | |||
| BottomDetails, | |||
| StatusText, | |||
| PIBIcon, | |||
| UserIcon, | |||
| UserIconContainer, | |||
| } from "./ItemDetailsHeaderCard.styled"; | |||
| import { ItemDetailsHeaderContainer } from "./ItemDetailsHeaderCard.styled"; | |||
| import { ReactComponent as Category } from "../../../assets/images/svg/category.svg"; | |||
| import { ReactComponent as PIB } from "../../../assets/images/svg/pib.svg"; | |||
| import { ReactComponent as MessageColor } from "../../../assets/images/svg/mailColor.svg"; | |||
| import selectedTheme from "../../../themes"; | |||
| import { useHistory } from "react-router-dom"; | |||
| import { useSelector } from "react-redux"; | |||
| import { selectLatestChats } from "../../../store/selectors/chatSelectors"; | |||
| import { selectUserId } from "../../../store/selectors/loginSelectors"; | |||
| import StatisticDetails from "./StatisticDetails/StatisticDetails"; | |||
| import PIBDetail from "./OfferDetail/PIB/PIBDetail"; | |||
| import CategoryDetail from "./OfferDetail/Category/CategoryDetail"; | |||
| const ItemDetailsHeaderCard = (props) => { | |||
| const history = useHistory(); | |||
| const chats = useSelector(selectLatestChats); | |||
| const offer = props.offer; | |||
| const userId = useSelector(selectUserId); | |||
| if (!props.offer) { | |||
| return <div>Loading...</div>; | |||
| } | |||
| let percentOfSucceededExchanges; | |||
| if (offer?.companyData?.statistics?.exchanges?.succeeded === 0) { | |||
| percentOfSucceededExchanges = 0; | |||
| } else { | |||
| percentOfSucceededExchanges = Math.ceil( | |||
| (offer?.companyData?.statistics?.exchanges?.total / | |||
| offer?.companyData?.statistics?.exchanges?.succeeded) * | |||
| 100 | |||
| ); | |||
| } | |||
| const handleGoProfile = () => { | |||
| history.push(`/profile/${offer?.offer?.userId}`); | |||
| }; | |||
| @@ -70,26 +51,8 @@ const ItemDetailsHeaderCard = (props) => { | |||
| <OfferTitle isMyProfile={props.isMyProfile} onClick={handleGoProfile}> | |||
| {offer?.companyData?.company?.name} | |||
| </OfferTitle> | |||
| <DetailContainer> | |||
| <PIBIcon color={selectedTheme.iconStrokeColor} component="span"> | |||
| <PIB /> | |||
| </PIBIcon> | |||
| <DetailText isMyProfile={props.isMyProfile}> | |||
| PIB - {offer?.companyData?.company?.PIB} | |||
| </DetailText> | |||
| </DetailContainer> | |||
| <DetailContainer shouldHideResponsive> | |||
| <DetailIcon | |||
| color={selectedTheme.iconStrokeColor} | |||
| component="span" | |||
| size="22px" | |||
| > | |||
| <Category width={"22px"} /> | |||
| </DetailIcon> | |||
| <DetailText isMyProfile={props.isMyProfile}> | |||
| {offer?.companyData?.company?.contacts?.location} | |||
| </DetailText> | |||
| </DetailContainer> | |||
| <PIBDetail offer={props.offer}/> | |||
| <CategoryDetail offer={props.offer}/> | |||
| </OfferDetails> | |||
| {props.isMyProfile ? ( | |||
| <UserIconContainer onClick={handleGoProfile}> | |||
| @@ -101,23 +64,8 @@ const ItemDetailsHeaderCard = (props) => { | |||
| </MessageIcon> | |||
| )} | |||
| </HeaderTop> | |||
| <HeaderDetails> | |||
| <BottomDetails> | |||
| <StatusText> | |||
| <b>{offer?.companyData?.statistics?.publishes?.count}</b> objava | |||
| </StatusText> | |||
| <StatusText> | |||
| <b>{offer?.companyData?.statistics?.views?.count}</b> ukupnih | |||
| pregleda | |||
| </StatusText> | |||
| <StatusText> | |||
| <b>{percentOfSucceededExchanges}</b> % uspesnih trampi | |||
| </StatusText> | |||
| <StatusText> | |||
| <b>{percentOfSucceededExchanges}</b> % korektna komunikacija | |||
| </StatusText> | |||
| </BottomDetails> | |||
| </HeaderDetails> | |||
| <StatisticDetails offer={offer} /> | |||
| </ItemDetailsHeaderContainer> | |||
| ); | |||
| }; | |||
| @@ -138,17 +86,6 @@ ItemDetailsHeaderCard.propTypes = { | |||
| sponsored: PropTypes.bool, | |||
| offer: PropTypes.any, | |||
| isMyProfile: PropTypes.bool, | |||
| // offer: PropTypes.shape({ | |||
| // images: PropTypes.any, | |||
| // name:PropTypes.string, | |||
| // description:PropTypes.string, | |||
| // category:PropTypes.shape({ | |||
| // name:PropTypes.string | |||
| // }), | |||
| // location:PropTypes.shape({ | |||
| // city:PropTypes.string | |||
| // }) | |||
| // }) | |||
| }; | |||
| ItemDetailsHeaderCard.defaultProps = { | |||
| halfwidth: false, | |||
| @@ -1,9 +1,8 @@ | |||
| import { Box, Grid, Typography } from "@mui/material"; | |||
| import { Box, Typography } from "@mui/material"; | |||
| import styled from "styled-components"; | |||
| import selectedTheme from "../../../themes"; | |||
| import { IconButton } from "../../Buttons/IconButton/IconButton"; | |||
| import { PrimaryButton } from "../../Buttons/PrimaryButton/PrimaryButton"; | |||
| import { Icon } from "../../Icon/Icon"; | |||
| import { ReactComponent as User} from "../../../assets/images/svg/user.svg"; | |||
| @@ -22,42 +21,12 @@ export const ItemDetailsHeaderContainer = styled(Box)` | |||
| max-width: 2000px; | |||
| position: relative; | |||
| `; | |||
| export const DetailContainer = styled(Box)` | |||
| display: flex; | |||
| flex-direction: row; | |||
| align-items: center; | |||
| gap: 7px; | |||
| font-family: "Open Sans"; | |||
| color: ${selectedTheme.primaryText}; | |||
| line-height: 16px; | |||
| margin-bottom: 7px; | |||
| font-size: 12px; | |||
| @media (max-width: 600px) { | |||
| ${(props) => props.shouldHideResponsive && `display: none;`} | |||
| } | |||
| `; | |||
| export const HeaderTop = styled(Box)` | |||
| display: flex; | |||
| flex-direction: row; | |||
| padding: 18px; | |||
| gap: 18px; | |||
| `; | |||
| export const HeaderDetails = styled(Box)` | |||
| background-color: ${selectedTheme.primaryIconBackgroundColor}; | |||
| `; | |||
| export const BottomDetails = styled(Box)` | |||
| max-width: fit-content; | |||
| display: grid; | |||
| grid-template-columns: repeat(2, 1fr); | |||
| grid-template-rows: repeat(2, 1fr); | |||
| grid-column-gap: 12px; | |||
| grid-row-gap: 12px; | |||
| padding: 18px; | |||
| @media (max-width: 600px) { | |||
| display: flex; | |||
| flex-direction: column; | |||
| } | |||
| `; | |||
| export const OfferImage = styled.img` | |||
| border-radius: 50%; | |||
| width: 144px; | |||
| @@ -116,13 +85,6 @@ export const OfferDetails = styled(Box)` | |||
| `; | |||
| export const StatusText = styled(Grid)` | |||
| font-family: "Open Sans"; | |||
| color: ${selectedTheme.primaryText}; | |||
| @media (max-width: 600px) { | |||
| font-size: 12px; | |||
| } | |||
| `; | |||
| export const OfferCategory = styled(Box)` | |||
| font-family: "Open Sans"; | |||
| color: ${selectedTheme.primaryText}; | |||
| @@ -173,24 +135,7 @@ export const Line = styled(Box)` | |||
| width: 0; | |||
| margin: auto 0; | |||
| `; | |||
| export const DetailIcon = styled(Icon)` | |||
| display: flex; | |||
| align-items: center; | |||
| & svg { | |||
| width: 22px; | |||
| position: relative; | |||
| } | |||
| `; | |||
| export const DetailText = styled(Typography)` | |||
| font-family: "Open Sans"; | |||
| color: ${props => props.isMyProfile ? "white" : selectedTheme.primaryText}; | |||
| line-height: 16px; | |||
| font-size: 16px; | |||
| position: relative; | |||
| @media (max-width: 600px) { | |||
| font-size: 14px; | |||
| } | |||
| `; | |||
| export const CheckButton = styled(PrimaryButton)` | |||
| width: 180px; | |||
| height: 48px; | |||
| @@ -224,18 +169,6 @@ export const MessageIcon = styled(IconButton)` | |||
| } | |||
| } | |||
| `; | |||
| export const PIBIcon = styled(DetailIcon)` | |||
| position: relative; | |||
| top: 1px; | |||
| & span svg { | |||
| width: 22px; | |||
| height: 22px; | |||
| @media (max-width: 600px) { | |||
| width: 14px; | |||
| height: 14px; | |||
| } | |||
| } | |||
| `; | |||
| export const UserIconContainer = styled(MessageIcon)` | |||
| background-color: ${selectedTheme.primaryIconBackgroundColor}; | |||
| ` | |||
| @@ -0,0 +1,30 @@ | |||
| import React from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { | |||
| DetailContainer, | |||
| DetailIcon, | |||
| DetailText, | |||
| } from "./CategoryDetail.styled"; | |||
| import selectedTheme from "../../../../../themes"; | |||
| import { ReactComponent as Category } from "../../../../../assets/images/svg/category.svg"; | |||
| const CategoryDetail = (props) => { | |||
| const offer = props.offer; | |||
| return ( | |||
| <DetailContainer shouldHideResponsive> | |||
| <DetailIcon color={selectedTheme.iconStrokeColor} size="22px"> | |||
| <Category width={"22px"} /> | |||
| </DetailIcon> | |||
| <DetailText isMyProfile={props.isMyProfile}> | |||
| {offer?.companyData?.company?.contacts?.location} | |||
| </DetailText> | |||
| </DetailContainer> | |||
| ); | |||
| }; | |||
| CategoryDetail.propTypes = { | |||
| offer: PropTypes.any, | |||
| isMyProfile: PropTypes.bool, | |||
| }; | |||
| export default CategoryDetail; | |||
| @@ -0,0 +1,37 @@ | |||
| import { Box, Typography } from "@mui/material"; | |||
| import styled from "styled-components"; | |||
| import selectedTheme from "../../../../../themes"; | |||
| import { Icon } from "../../../../Icon/Icon"; | |||
| export const DetailContainer = styled(Box)` | |||
| display: flex; | |||
| flex-direction: row; | |||
| align-items: center; | |||
| gap: 7px; | |||
| font-family: "Open Sans"; | |||
| color: ${selectedTheme.primaryText}; | |||
| line-height: 16px; | |||
| margin-bottom: 7px; | |||
| font-size: 12px; | |||
| @media (max-width: 600px) { | |||
| ${(props) => props.shouldHideResponsive && `display: none;`} | |||
| } | |||
| `; | |||
| export const DetailIcon = styled(Icon)` | |||
| display: flex; | |||
| align-items: center; | |||
| & svg { | |||
| width: 22px; | |||
| position: relative; | |||
| } | |||
| `; | |||
| export const DetailText = styled(Typography)` | |||
| font-family: "Open Sans"; | |||
| color: ${props => props.isMyProfile ? "white" : selectedTheme.primaryText}; | |||
| line-height: 16px; | |||
| font-size: 16px; | |||
| position: relative; | |||
| @media (max-width: 600px) { | |||
| font-size: 14px; | |||
| } | |||
| `; | |||
| @@ -0,0 +1,30 @@ | |||
| import React from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { DetailContainer, DetailText, PIBIcon } from "./PIBDetail.styled"; | |||
| import selectedTheme from "../../../../../themes"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import { ReactComponent as PIB } from "../../../../../assets/images/svg/pib.svg"; | |||
| const PIBDetail = (props) => { | |||
| const { t } = useTranslation(); | |||
| const offer = props.offer; | |||
| return ( | |||
| <DetailContainer> | |||
| <PIBIcon color={selectedTheme.iconStrokeColor} component="span"> | |||
| <PIB /> | |||
| </PIBIcon> | |||
| <DetailText isMyProfile={props.isMyProfile}> | |||
| {`${t("itemDetailsCard.PIB")}${offer?.companyData?.company?.PIB}`} | |||
| </DetailText> | |||
| </DetailContainer> | |||
| ); | |||
| }; | |||
| PIBDetail.propTypes = { | |||
| isMyProfile: PropTypes.bool, | |||
| offer: PropTypes.any, | |||
| icon: PropTypes.node, | |||
| }; | |||
| export default PIBDetail; | |||
| @@ -0,0 +1,49 @@ | |||
| import { Box, Typography } from "@mui/material"; | |||
| import styled from "styled-components"; | |||
| import selectedTheme from "../../../../../themes"; | |||
| import { Icon } from "../../../../Icon/Icon"; | |||
| export const DetailContainer = styled(Box)` | |||
| display: flex; | |||
| flex-direction: row; | |||
| align-items: center; | |||
| gap: 7px; | |||
| font-family: "Open Sans"; | |||
| color: ${selectedTheme.primaryText}; | |||
| line-height: 16px; | |||
| margin-bottom: 7px; | |||
| font-size: 12px; | |||
| @media (max-width: 600px) { | |||
| ${(props) => props.shouldHideResponsive && `display: none;`} | |||
| } | |||
| `; | |||
| export const DetailIcon = styled(Icon)` | |||
| display: flex; | |||
| align-items: center; | |||
| & svg { | |||
| width: 22px; | |||
| position: relative; | |||
| } | |||
| `; | |||
| export const DetailText = styled(Typography)` | |||
| font-family: "Open Sans"; | |||
| color: ${props => props.isMyProfile ? "white" : selectedTheme.primaryText}; | |||
| line-height: 16px; | |||
| font-size: 16px; | |||
| position: relative; | |||
| @media (max-width: 600px) { | |||
| font-size: 14px; | |||
| } | |||
| `; | |||
| export const PIBIcon = styled(DetailIcon)` | |||
| position: relative; | |||
| top: 1px; | |||
| & span svg { | |||
| width: 22px; | |||
| height: 22px; | |||
| @media (max-width: 600px) { | |||
| width: 14px; | |||
| height: 14px; | |||
| } | |||
| } | |||
| `; | |||
| @@ -0,0 +1,60 @@ | |||
| import React, { useMemo } from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { | |||
| BottomDetails, | |||
| HeaderDetails, | |||
| StatusText, | |||
| StatusValue, | |||
| } from "./StatisticDetails.styled"; | |||
| import { useTranslation } from "react-i18next"; | |||
| const StatisticDetails = (props) => { | |||
| const { t } = useTranslation(); | |||
| const offer = props.offer; | |||
| const percentOfSucceededExchanges = useMemo(() => { | |||
| if (offer?.companyData?.statistics?.exchanges?.succeeded === 0) { | |||
| return 0 + "%"; | |||
| } else { | |||
| return ( | |||
| Math.ceil( | |||
| (offer?.companyData?.statistics?.exchanges?.total / | |||
| offer?.companyData?.statistics?.exchanges?.succeeded) * | |||
| 100 | |||
| ) + "%" | |||
| ); | |||
| } | |||
| }); | |||
| return ( | |||
| <HeaderDetails> | |||
| <BottomDetails> | |||
| <StatusText> | |||
| <StatusValue> | |||
| {offer?.companyData?.statistics?.publishes?.count} | |||
| </StatusValue> | |||
| {t("itemDetailsCard.offers")} | |||
| </StatusText> | |||
| <StatusText> | |||
| <StatusValue> | |||
| {offer?.companyData?.statistics?.views?.count} | |||
| </StatusValue> | |||
| {t("itemDetailsCard.totalViews")} | |||
| </StatusText> | |||
| <StatusText> | |||
| <StatusValue>{percentOfSucceededExchanges}</StatusValue> | |||
| {t("itemDetailsCard.successfulExchanges")} | |||
| </StatusText> | |||
| <StatusText> | |||
| <StatusValue>{percentOfSucceededExchanges}</StatusValue> | |||
| {t("itemDetailsCard.correctCommunications")} | |||
| </StatusText> | |||
| </BottomDetails> | |||
| </HeaderDetails> | |||
| ); | |||
| }; | |||
| StatisticDetails.propTypes = { | |||
| offer: PropTypes.any, | |||
| }; | |||
| export default StatisticDetails; | |||
| @@ -0,0 +1,30 @@ | |||
| import { Box, Grid } from "@mui/material"; | |||
| import styled from "styled-components"; | |||
| import selectedTheme from "../../../../themes"; | |||
| export const HeaderDetails = styled(Box)` | |||
| background-color: ${selectedTheme.primaryIconBackgroundColor}; | |||
| `; | |||
| export const BottomDetails = styled(Box)` | |||
| max-width: fit-content; | |||
| display: grid; | |||
| grid-template-columns: repeat(2, 1fr); | |||
| grid-template-rows: repeat(2, 1fr); | |||
| grid-column-gap: 12px; | |||
| grid-row-gap: 12px; | |||
| padding: 18px; | |||
| @media (max-width: 600px) { | |||
| display: flex; | |||
| flex-direction: column; | |||
| } | |||
| `; | |||
| export const StatusText = styled(Grid)` | |||
| font-family: "Open Sans"; | |||
| color: ${selectedTheme.primaryText}; | |||
| @media (max-width: 600px) { | |||
| font-size: 12px; | |||
| } | |||
| `; | |||
| export const StatusValue = styled.b` | |||
| font-weight: bold; | |||
| ` | |||
| @@ -1,59 +0,0 @@ | |||
| import React from 'react' | |||
| import {ReactComponent as DummyImage1 } from "../../assets/images/svg/dummyImages/offer-1.svg" | |||
| import {ReactComponent as DummyAuthorImage1} from "../../assets/images/svg/dummyImages/DummyAuthorImage1.svg" | |||
| // import {ReactComponent as DummyImage2 } from "../../assets/images/svg/dummyImages/offer-2.svg" | |||
| // import {ReactComponent as DummyImage3 } from "../../assets/images/svg/dummyImages/offer-3.svg" | |||
| // import {ReactComponent as DummyImage4 } from "../../assets/images/svg/dummyImages/offer-4.svg" | |||
| export const packageEnum = { | |||
| package: "PACKAGE", | |||
| palette: "PALETTE", | |||
| piece: "PIECE" | |||
| } | |||
| export const Author = { | |||
| id: 0, | |||
| image: <DummyAuthorImage1 />, | |||
| title: "Women's Beauty House", | |||
| pib: 123456789, | |||
| location: "Nis, Serbia", | |||
| numberOfOffers: 9, | |||
| numberOfViews: 1200, | |||
| successSwapsProcent: "75%", | |||
| goodCommunicationProcent: "90%", | |||
| } | |||
| export const Offer = { | |||
| id: 0, | |||
| title: "Vino", | |||
| category: "Hrana i pice", | |||
| subcategory:"Farbe", | |||
| status:"novo", | |||
| quantity:150, | |||
| numberOfViews:45, | |||
| description: "Vinarija Aleksić osnovana je u Vranju 2006. godine, otvorivši time put oživljavanju vinogradarstva na jugu Srbije.Vinarija Aleksić osnovana je u Vranju 2006. godine, otvorivši time put oživljavanju vinogradarstva na jugu Srbije.Vinarija Aleksić osnovana je u Vranju 2006. godine, otvorivši time put oživljavanju vinogradarstva na jugu Srbije.Vinarija Aleksić osnovana je u Vranju 2006. godine, otvorivši time put oživljavanju vinogradarstva na jugu Srbije.", | |||
| images: [ | |||
| { | |||
| id:0, | |||
| image: <DummyImage1 /> | |||
| }, | |||
| { | |||
| id:1, | |||
| image: <DummyImage1 /> | |||
| }, | |||
| { | |||
| id:2, | |||
| image: <DummyImage1 /> | |||
| }, | |||
| { | |||
| id:3, | |||
| image: <DummyImage1 /> | |||
| }, | |||
| { | |||
| id:4, | |||
| image: <DummyImage1 /> | |||
| }, | |||
| ], | |||
| package: packageEnum.package, | |||
| postDate: "12.04.2022", | |||
| } | |||
| @@ -1,26 +0,0 @@ | |||
| import React from 'react'; | |||
| import PropTypes from 'prop-types'; | |||
| const BlockSectionLoader = ({ children, isLoading, fullHeight, noShadow }) => ( | |||
| <div | |||
| className={`c-loader__wrapper c-loader__wrapper--block ${ | |||
| fullHeight ? 'c-loader__wrapper--full-height' : '' | |||
| } ${noShadow ? 'c-loader__wrapper--no-shadow' : ''}`} | |||
| > | |||
| {children} | |||
| {isLoading && ( | |||
| <div className="c-loader"> | |||
| <div className="c-loader__icon" /> | |||
| </div> | |||
| )} | |||
| </div> | |||
| ); | |||
| BlockSectionLoader.propTypes = { | |||
| children: PropTypes.node, | |||
| isLoading: PropTypes.bool, | |||
| fullHeight: PropTypes.bool, | |||
| noShadow: PropTypes.bool, | |||
| }; | |||
| export default BlockSectionLoader; | |||
| @@ -1,13 +0,0 @@ | |||
| import React from "react"; | |||
| import { ReactComponent as Logo } from "../../assets/images/svg/big-logo-vertical.svg"; | |||
| import { FullPageLoaderContainer } from "./FullPageLoader.styled"; | |||
| const FullPageLoader = () => { | |||
| return ( | |||
| <FullPageLoaderContainer> | |||
| <Logo /> | |||
| </FullPageLoaderContainer> | |||
| ); | |||
| }; | |||
| export default FullPageLoader; | |||
| @@ -1,10 +0,0 @@ | |||
| import { Container } from "@mui/material"; | |||
| import styled from "styled-components"; | |||
| export const FullPageLoaderContainer = styled(Container)` | |||
| height: 100%; | |||
| width: 100vw; | |||
| padding-top: 250px; | |||
| text-align: center; | |||
| ` | |||
| @@ -1,20 +0,0 @@ | |||
| import React from 'react'; | |||
| import PropTypes from 'prop-types'; | |||
| const SectionLoader = ({ children, isLoading }) => ( | |||
| <div className="c-loader__wrapper"> | |||
| {children} | |||
| {isLoading && ( | |||
| <div className="c-loader"> | |||
| <div className="c-loader__icon" /> | |||
| </div> | |||
| )} | |||
| </div> | |||
| ); | |||
| SectionLoader.propTypes = { | |||
| children: PropTypes.node, | |||
| isLoading: PropTypes.bool, | |||
| }; | |||
| export default SectionLoader; | |||
| @@ -0,0 +1,19 @@ | |||
| import React from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { LoginDescription as Description } from "./LoginDescription.styled"; | |||
| import { useTranslation } from "react-i18next"; | |||
| const LoginDescription = () => { | |||
| const { t } = useTranslation(); | |||
| return ( | |||
| <Description component="h1" variant="h6"> | |||
| {t("login.welcomeText")} | |||
| </Description> | |||
| ); | |||
| }; | |||
| LoginDescription.propTypes = { | |||
| children: PropTypes.node, | |||
| }; | |||
| export default LoginDescription; | |||
| @@ -0,0 +1,18 @@ | |||
| import { Typography } from "@mui/material"; | |||
| import styled from "styled-components"; | |||
| import selectedTheme from "../../../themes"; | |||
| export const LoginDescription = styled(Typography)` | |||
| font-family: "Open Sans"; | |||
| margin-top: 9px; | |||
| width: 221px; | |||
| font-style: normal; | |||
| font-weight: 400; | |||
| font-size: 16px; | |||
| line-height: 22px; | |||
| display: flex; | |||
| align-items: center; | |||
| text-align: center; | |||
| color: ${selectedTheme.primaryGrayText}; | |||
| margin-bottom: 20px; | |||
| `; | |||
| @@ -0,0 +1,26 @@ | |||
| import React from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { useSelector } from "react-redux"; | |||
| import { selectLoginError } from "../../../store/selectors/loginSelectors"; | |||
| import { ErrorText } from "./ErrorMessage.styled"; | |||
| const ErrorMessage = (props) => { | |||
| const formik = props.formik; | |||
| const error = useSelector(selectLoginError); | |||
| return ( | |||
| <> | |||
| {formik.errors.password && formik.touched.password && ( | |||
| <ErrorText>{formik.errors.password}</ErrorText> | |||
| )} | |||
| {error.length > 0 && !formik.errors.password && ( | |||
| <ErrorText>{error}</ErrorText> | |||
| )} | |||
| </> | |||
| ); | |||
| }; | |||
| ErrorMessage.propTypes = { | |||
| formik: PropTypes.any, | |||
| }; | |||
| export default ErrorMessage; | |||
| @@ -0,0 +1,11 @@ | |||
| import { Typography } from "@mui/material"; | |||
| import styled from "styled-components"; | |||
| export const ErrorText = styled(Typography)` | |||
| color: red; | |||
| font-family: "Open Sans"; | |||
| position: relative; | |||
| top: -12px; | |||
| height: 20px; | |||
| font-size: 14px; | |||
| `; | |||
| @@ -0,0 +1,30 @@ | |||
| import React from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { selectLoginError } from "../../../../store/selectors/loginSelectors"; | |||
| import { useSelector } from "react-redux"; | |||
| import { TextField } from "../../../TextFields/TextField/TextField"; | |||
| import { useTranslation } from "react-i18next"; | |||
| const EmailField = (props) => { | |||
| const { t } = useTranslation(); | |||
| const error = useSelector(selectLoginError); | |||
| const formik = props.formik; | |||
| return ( | |||
| <TextField | |||
| name="email" | |||
| placeholder={t("common.labelEmail")} | |||
| value={formik.values.email} | |||
| onChange={formik.handleChange} | |||
| error={(formik.touched.email && formik.errors.email) || error.length > 0} | |||
| helperText={formik.touched.email && formik.errors.email} | |||
| autoFocus | |||
| fullWidth | |||
| /> | |||
| ); | |||
| }; | |||
| EmailField.propTypes = { | |||
| formik: PropTypes.any, | |||
| }; | |||
| export default EmailField; | |||
| @@ -0,0 +1,50 @@ | |||
| import React, { useState } from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import IconButton from "../../../IconButton/IconButton"; | |||
| import { ReactComponent as VisibilityOn } from "../../../../assets/images/svg/eye-striked.svg"; | |||
| import { ReactComponent as VisibilityOff } from "../../../../assets/images/svg/eye.svg"; | |||
| import { useSelector } from "react-redux"; | |||
| import { selectLoginError } from "../../../../store/selectors/loginSelectors"; | |||
| import { TextField } from "../../../TextFields/TextField/TextField"; | |||
| import { useTranslation } from "react-i18next"; | |||
| const PasswordField = (props) => { | |||
| const formik = props.formik; | |||
| const [showPassword, setShowPassword] = useState(false); | |||
| const handleClickShowPassword = () => setShowPassword(!showPassword); | |||
| const handleMouseDownPassword = () => setShowPassword(!showPassword); | |||
| const error = useSelector(selectLoginError); | |||
| const {t} = useTranslation(); | |||
| return ( | |||
| <TextField | |||
| name="password" | |||
| placeholder={t("common.labelPassword")} | |||
| margin="normal" | |||
| type={showPassword ? "text" : "password"} | |||
| value={formik.values.password} | |||
| onChange={formik.handleChange} | |||
| error={ | |||
| (formik.touched.password && formik.errors.password) || error.length > 0 | |||
| } | |||
| helperText={formik.touched.password && formik.errors.password} | |||
| fullWidth | |||
| InputProps={{ | |||
| endAdornment: ( | |||
| <IconButton | |||
| onClick={handleClickShowPassword} | |||
| onMouseDown={handleMouseDownPassword} | |||
| > | |||
| {showPassword ? <VisibilityOn /> : <VisibilityOff />} | |||
| </IconButton> | |||
| ), | |||
| }} | |||
| /> | |||
| ); | |||
| }; | |||
| PasswordField.propTypes = { | |||
| formik: PropTypes.any, | |||
| }; | |||
| export default PasswordField; | |||
| @@ -0,0 +1,34 @@ | |||
| import React from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { useSelector } from "react-redux"; | |||
| import { selectLoginError } from "../../../store/selectors/loginSelectors"; | |||
| import { FORGOT_PASSWORD_PAGE } from "../../../constants/pages"; | |||
| import Link from "../../Link/Link"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import { NavLink } from "react-router-dom"; | |||
| const ForgotPasswordLink = () => { | |||
| const error = useSelector(selectLoginError); | |||
| const { t } = useTranslation(); | |||
| return ( | |||
| <Link | |||
| to={FORGOT_PASSWORD_PAGE} | |||
| textsize="12px" | |||
| component={NavLink} | |||
| underline="hover" | |||
| align="right" | |||
| style={{ | |||
| marginTop: error.length > 0 ? "0" : "18px", | |||
| marginBottom: "18px", | |||
| }} | |||
| > | |||
| {t("login.forgotYourPassword")} | |||
| </Link> | |||
| ); | |||
| }; | |||
| ForgotPasswordLink.propTypes = { | |||
| children: PropTypes.node, | |||
| }; | |||
| export default ForgotPasswordLink; | |||
| @@ -0,0 +1,98 @@ | |||
| import React, { useEffect } from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { useFormik } from "formik"; | |||
| import { useDispatch, useSelector } from "react-redux"; | |||
| import { useHistory } from "react-router-dom"; | |||
| import { | |||
| clearLoginErrors, | |||
| fetchLogin, | |||
| } from "../../store/actions/login/loginActions"; | |||
| import { selectLoginError } from "../../store/selectors/loginSelectors"; | |||
| import { HOME_PAGE } from "../../constants/pages"; | |||
| import { ReactComponent as Logo } from "../../assets/images/svg/logo-vertical.svg"; | |||
| import { LoginPageContainer, LoginFormContainer } from "./Login.styled"; | |||
| import loginValidation from "../../validations/loginValidation"; | |||
| import loginInitialValues from "../../initialValues/loginInitialValues"; | |||
| import LoginTitle from "./Title/LoginTitle"; | |||
| import LoginDescription from "./Description/LoginDescription"; | |||
| import EmailField from "./Fields/Email/EmailField"; | |||
| import PasswordField from "./Fields/Password/PasswordField"; | |||
| import ErrorMessage from "./ErrorMessage/ErrorMessage"; | |||
| import ForgotPasswordLink from "./ForgotPasswordLink/ForgotPasswordLink"; | |||
| import LoginButton from "./LoginButton/LoginButton"; | |||
| import RegisterLink from "./RegisterLink/RegisterLink"; | |||
| const Login = () => { | |||
| const dispatch = useDispatch(); | |||
| const error = useSelector(selectLoginError); | |||
| const history = useHistory(); | |||
| useEffect(() => { | |||
| dispatch(clearLoginErrors()); | |||
| }, []); | |||
| const handleApiResponseSuccess = () => { | |||
| history.push({ | |||
| pathname: HOME_PAGE, | |||
| state: { | |||
| from: history.location.pathname, | |||
| }, | |||
| }); | |||
| }; | |||
| const handleSubmit = (values) => { | |||
| const { email, password } = values; | |||
| console.log(values); | |||
| dispatch(clearLoginErrors()); | |||
| dispatch( | |||
| fetchLogin({ | |||
| email, | |||
| password, | |||
| handleApiResponseSuccess, | |||
| }) | |||
| ); | |||
| }; | |||
| const formik = useFormik({ | |||
| initialValues: loginInitialValues, | |||
| validationSchema: loginValidation, | |||
| onSubmit: handleSubmit, | |||
| validateOnBlur: true, | |||
| enableReinitialize: true, | |||
| }); | |||
| useEffect(() => { | |||
| if (error) { | |||
| if (formik.errors.email || formik.errors.password) { | |||
| dispatch(clearLoginErrors()); | |||
| } | |||
| } | |||
| }, [formik.errors.email, formik.errors.password]); | |||
| return ( | |||
| <LoginPageContainer> | |||
| <Logo /> | |||
| <LoginTitle /> | |||
| <LoginDescription /> | |||
| <LoginFormContainer component="form" onSubmit={formik.handleSubmit}> | |||
| <EmailField formik={formik} /> | |||
| <PasswordField formik={formik} /> | |||
| <ErrorMessage formik={formik} /> | |||
| <ForgotPasswordLink /> | |||
| <LoginButton formik={formik} /> | |||
| <RegisterLink /> | |||
| </LoginFormContainer> | |||
| </LoginPageContainer> | |||
| ); | |||
| }; | |||
| Login.propTypes = { | |||
| history: PropTypes.shape({ | |||
| replace: PropTypes.func, | |||
| push: PropTypes.func, | |||
| location: PropTypes.shape({ | |||
| pathname: PropTypes.string, | |||
| }), | |||
| }), | |||
| }; | |||
| export default Login; | |||
| @@ -0,0 +1,19 @@ | |||
| import { Box, Container } from "@mui/material"; | |||
| import styled from "styled-components"; | |||
| export const LoginPageContainer = styled(Container)` | |||
| margin-top: 150px; | |||
| display: flex; | |||
| flex-direction: column; | |||
| align-items: center; | |||
| @media (max-height: 900px) { | |||
| margin-top: 110px; | |||
| } | |||
| @media (max-height: 800px) { | |||
| margin-top: 70px; | |||
| } | |||
| `; | |||
| export const LoginFormContainer = styled(Box)` | |||
| width: 335px; | |||
| height: 216px; | |||
| `; | |||
| @@ -0,0 +1,31 @@ | |||
| import React from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import selectedTheme from "../../../themes"; | |||
| import { PrimaryButton } from "../../Buttons/PrimaryButton/PrimaryButton"; | |||
| import { useTranslation } from "react-i18next"; | |||
| const LoginButton = (props) => { | |||
| const { t } = useTranslation(); | |||
| const formik = props.formik; | |||
| return ( | |||
| <PrimaryButton | |||
| type="submit" | |||
| variant="contained" | |||
| height="48px" | |||
| fullWidth | |||
| buttoncolor={selectedTheme.primaryPurple} | |||
| textcolor="white" | |||
| disabled={ | |||
| formik.values.email.length === 0 || formik.values.password.length === 0 | |||
| } | |||
| > | |||
| {t("login.logIn")} | |||
| </PrimaryButton> | |||
| ); | |||
| }; | |||
| LoginButton.propTypes = { | |||
| formik: PropTypes.any, | |||
| }; | |||
| export default LoginButton; | |||
| @@ -0,0 +1,27 @@ | |||
| import React from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import { RegisterAltText, RegisterTextContainer } from "./RegisterLink.styled"; | |||
| import Link from "../../Link/Link"; | |||
| import { NavLink } from "react-router-dom"; | |||
| const RegisterLink = () => { | |||
| const { t } = useTranslation(); | |||
| return ( | |||
| <RegisterTextContainer> | |||
| <RegisterAltText> | |||
| {t("login.dontHaveAccount").padEnd(2, " ")} | |||
| </RegisterAltText> | |||
| <Link to="/register" component={NavLink} underline="hover" align="center"> | |||
| {t("login.signUp")} | |||
| </Link> | |||
| </RegisterTextContainer> | |||
| ); | |||
| }; | |||
| RegisterLink.propTypes = { | |||
| children: PropTypes.any, | |||
| }; | |||
| export default RegisterLink; | |||
| @@ -0,0 +1,20 @@ | |||
| import { Box, Typography } from "@mui/material"; | |||
| import styled from "styled-components"; | |||
| import selectedTheme from "../../../themes"; | |||
| export const RegisterAltText = styled(Typography)` | |||
| font-family: "Poppins"; | |||
| color: ${selectedTheme.primaryText}; | |||
| font-size: 14px; | |||
| padding-right: 6px; | |||
| line-height: 14px; | |||
| `; | |||
| export const RegisterTextContainer = styled(Box)` | |||
| display: flex; | |||
| flex-direction: row; | |||
| margin-top: 36px; | |||
| justify-content: center; | |||
| @media (max-width: 600px) { | |||
| padding-bottom: 36px; | |||
| } | |||
| `; | |||
| @@ -0,0 +1,19 @@ | |||
| import React from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import { LoginTitle as Title } from "./LoginTitle.styled"; | |||
| const LoginTitle = () => { | |||
| const { t } = useTranslation(); | |||
| return ( | |||
| <Title component="h1" variant="h5"> | |||
| {t("login.logInTitle")} | |||
| </Title> | |||
| ); | |||
| }; | |||
| LoginTitle.propTypes = { | |||
| children: PropTypes.node, | |||
| }; | |||
| export default LoginTitle; | |||
| @@ -0,0 +1,17 @@ | |||
| import { Typography } from "@mui/material"; | |||
| import styled from "styled-components"; | |||
| import selectedTheme from "../../../themes"; | |||
| export const LoginTitle = styled(Typography)` | |||
| font-family: "Open Sans"; | |||
| width: 328px; | |||
| height: 33px; | |||
| text-align: center; | |||
| flex: 1; | |||
| font-style: normal; | |||
| font-weight: 400; | |||
| font-size: 24px; | |||
| line-height: 33px; | |||
| color: ${selectedTheme.primaryPurple}; | |||
| margin-top: 36px; | |||
| `; | |||
| @@ -1,57 +0,0 @@ | |||
| import React from 'react'; | |||
| import PropTypes from 'prop-types'; | |||
| import { | |||
| Dialog, | |||
| DialogContent, | |||
| DialogTitle, | |||
| DialogActions, | |||
| Button, | |||
| useMediaQuery, | |||
| useTheme, | |||
| } from '@mui/material'; | |||
| const DialogComponent = ({ | |||
| title, | |||
| content, | |||
| onClose, | |||
| open, | |||
| maxWidth, | |||
| fullWidth, | |||
| responsive, | |||
| }) => { | |||
| const theme = useTheme(); | |||
| const fullScreen = useMediaQuery(theme.breakpoints.down('md')); | |||
| const handleClose = () => { | |||
| onClose(); | |||
| }; | |||
| return ( | |||
| <Dialog | |||
| maxWidth={maxWidth} | |||
| fullWidth={fullWidth} | |||
| fullScreen={responsive && fullScreen} | |||
| onClose={handleClose} | |||
| open={open} | |||
| > | |||
| <DialogTitle>{title}</DialogTitle> | |||
| {content && <DialogContent>{content}</DialogContent>} | |||
| <DialogActions> | |||
| <Button onClick={handleClose}>OK</Button> | |||
| <Button onClick={handleClose}>Cancel</Button> | |||
| </DialogActions> | |||
| </Dialog> | |||
| ); | |||
| }; | |||
| DialogComponent.propTypes = { | |||
| title: PropTypes.string, | |||
| open: PropTypes.bool.isRequired, | |||
| content: PropTypes.any, | |||
| onClose: PropTypes.func.isRequired, | |||
| maxWidth: PropTypes.any, | |||
| fullWidth: PropTypes.bool, | |||
| responsive: PropTypes.bool, | |||
| }; | |||
| export default DialogComponent; | |||
| @@ -1,28 +1,28 @@ | |||
| import React from 'react'; | |||
| import PropTypes from 'prop-types'; | |||
| import { Drawer } from '@mui/material'; | |||
| import React from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { Drawer } from "@mui/material"; | |||
| const DrawerComponent = ({ open, toggleOpen, content, anchor = 'right' }) => ( | |||
| <Drawer | |||
| sx={{ | |||
| minWidth: 250, | |||
| '& .MuiDrawer-paper': { | |||
| minWidth: 250, | |||
| }, | |||
| }} | |||
| anchor={anchor} | |||
| open={open} | |||
| onClose={toggleOpen} | |||
| > | |||
| {content ? content : null} | |||
| </Drawer> | |||
| const DrawerComponent = ({ open, toggleOpen, content, anchor = "right" }) => ( | |||
| <Drawer | |||
| sx={{ | |||
| minWidth: 250, | |||
| "& .MuiDrawer-paper": { | |||
| minWidth: 250, | |||
| }, | |||
| }} | |||
| anchor={anchor} | |||
| open={open} | |||
| onClose={toggleOpen} | |||
| > | |||
| {content ? content : null} | |||
| </Drawer> | |||
| ); | |||
| DrawerComponent.propTypes = { | |||
| open: PropTypes.bool, | |||
| toggleOpen: PropTypes.func, | |||
| content: PropTypes.any, | |||
| anchor: PropTypes.oneOf(['top', 'right', 'left', 'bottom']), | |||
| open: PropTypes.bool, | |||
| toggleOpen: PropTypes.func, | |||
| content: PropTypes.any, | |||
| anchor: PropTypes.oneOf(["top", "right", "left", "bottom"]), | |||
| }; | |||
| export default DrawerComponent; | |||
| @@ -1,15 +0,0 @@ | |||
| import React from 'react'; | |||
| import PropTypes from 'prop-types'; | |||
| import { Typography } from '@mui/material'; | |||
| const ErrorMessageComponent = ({ error }) => ( | |||
| <Typography variant="body1" color="error" my={2}> | |||
| {error} | |||
| </Typography> | |||
| ); | |||
| ErrorMessageComponent.propTypes = { | |||
| error: PropTypes.string.isRequired, | |||
| }; | |||
| export default ErrorMessageComponent; | |||
| @@ -1,29 +0,0 @@ | |||
| import React from 'react'; | |||
| import { Paper, Typography } from '@mui/material'; | |||
| import { DataGrid } from '@mui/x-data-grid'; | |||
| // Use these values from REDUX? | |||
| const rows = [ | |||
| { id: 1, col1: 'Example', col2: 'Row', col3: '1' }, | |||
| { id: 2, col1: 'Row', col2: 'Example', col3: '2' }, | |||
| { id: 3, col1: '3', col2: 'Row', col3: 'Example' }, | |||
| ]; | |||
| const columns = [ | |||
| { field: 'col1', headerName: 'Column 1', flex: 1 }, | |||
| { field: 'col2', headerName: 'Column 2', flex: 1 }, | |||
| { field: 'col3', headerName: 'Column 2', flex: 1 }, | |||
| ]; | |||
| const DataGridExample = () => { | |||
| return ( | |||
| <Paper sx={{ p: 2 }} elevation={5}> | |||
| <Typography variant="h4" gutterBottom align="center"> | |||
| DataGrid Example | |||
| </Typography> | |||
| <DataGrid autoHeight rows={rows} columns={columns} /> | |||
| </Paper> | |||
| ); | |||
| }; | |||
| export default DataGridExample; | |||
| @@ -1,64 +0,0 @@ | |||
| import React from 'react'; | |||
| // import { Button, Divider, Paper, Typography } from '@mui/material'; | |||
| // import DialogComponent from '../DialogComponent'; | |||
| // import DrawerComponent from '../DrawerComponent'; | |||
| // import PopoverComponent from '../PopoverComponent'; | |||
| const Modals = () => { | |||
| // const [dialogOpen, setDialogOpen] = useState(false); | |||
| // const [drawerOpen, setDrawerOpen] = useState(false); | |||
| // const [popoverOpen, setPopoverOpen] = useState(false); | |||
| // const [anchorEl, setAnchorEl] = useState(null); | |||
| return (<></> | |||
| // <Paper | |||
| // sx={{ | |||
| // p: 2, | |||
| // display: 'flex', | |||
| // flexDirection: 'column', | |||
| // }} | |||
| // elevation={5} | |||
| // > | |||
| // <Typography variant="h4" gutterBottom align="center"> | |||
| // Modals Example | |||
| // </Typography> | |||
| // <Divider /> | |||
| // <Button onClick={() => setDialogOpen(true)}>Open Dialog</Button> | |||
| // <Button onClick={() => setDrawerOpen(true)}>Open Drawer</Button> | |||
| // <Button | |||
| // onClick={(e) => { | |||
| // setPopoverOpen(true); | |||
| // setAnchorEl(e.currentTarget); | |||
| // }} | |||
| // > | |||
| // Open Popover | |||
| // </Button> | |||
| // <DialogComponent | |||
| // title="Dialog Title" | |||
| // content={<Typography>Dialog Content</Typography>} | |||
| // open={dialogOpen} | |||
| // onClose={() => setDialogOpen(false)} | |||
| // maxWidth="md" | |||
| // fullWidth | |||
| // responsive | |||
| // /> | |||
| // <DrawerComponent | |||
| // anchor="left" | |||
| // content={<Typography sx={{ p: 2 }}>Drawer Content</Typography>} | |||
| // open={drawerOpen} | |||
| // toggleOpen={() => setDrawerOpen(!drawerOpen)} | |||
| // /> | |||
| // <PopoverComponent | |||
| // anchorEl={anchorEl} | |||
| // open={popoverOpen} | |||
| // onClose={() => { | |||
| // setPopoverOpen(false); | |||
| // setAnchorEl(null); | |||
| // }} | |||
| // content={<Typography sx={{ p: 2 }}>Popover Content</Typography>} | |||
| // /> | |||
| // </Paper> | |||
| ); | |||
| }; | |||
| export default Modals; | |||
| @@ -1,183 +0,0 @@ | |||
| import React, { useEffect, useState } from 'react'; | |||
| import { | |||
| Paper, | |||
| Box, | |||
| Grid, | |||
| Typography, | |||
| Divider, | |||
| TablePagination, | |||
| TextField, | |||
| FormControl, | |||
| InputLabel, | |||
| Select, | |||
| MenuItem, | |||
| } from '@mui/material'; | |||
| // import { useTranslation } from 'react-i18next'; | |||
| import { useDispatch, useSelector, batch } from 'react-redux'; | |||
| import useDebounce from '../../../hooks/useDebounceHook'; | |||
| import { | |||
| itemsSelector, | |||
| pageSelector, | |||
| itemsPerPageSelector, | |||
| countSelector, | |||
| sortSelector, | |||
| } from '../../../store/selectors/randomDataSelectors'; | |||
| import { | |||
| loadData, | |||
| updatePage, | |||
| updateItemsPerPage, | |||
| updateFilter, | |||
| updateSort, | |||
| } from '../../../store/actions/randomData/randomDataActions'; | |||
| const PagingSortingFilteringExample = () => { | |||
| const [filterText, setFilterText] = useState(''); | |||
| const dispatch = useDispatch(); | |||
| // const { t } = useTranslation(); | |||
| const items = useSelector(itemsSelector); | |||
| const currentPage = useSelector(pageSelector); | |||
| const itemsPerPage = useSelector(itemsPerPageSelector); | |||
| const totalCount = useSelector(countSelector); | |||
| const sort = useSelector(sortSelector) || 'name-asc'; | |||
| // Use debounce to prevent too many rerenders | |||
| const debouncedFilterText = useDebounce(filterText, 500); | |||
| useEffect(() => { | |||
| dispatch(loadData(30)); | |||
| dispatch(updateSort(sort)); | |||
| }, []); | |||
| useEffect(() => { | |||
| batch(() => { | |||
| dispatch(updateFilter(filterText)); | |||
| currentPage > 0 && dispatch(updatePage(0)); | |||
| }); | |||
| }, [debouncedFilterText]); | |||
| const handleFilterTextChange = (event) => { | |||
| const filterText = event.target.value; | |||
| setFilterText(filterText); | |||
| }; | |||
| const handleSortChange = (event) => { | |||
| const sort = event.target.value; | |||
| dispatch(updateSort(sort)); | |||
| }; | |||
| const handlePageChange = (event, newPage) => { | |||
| dispatch(updatePage(newPage)); | |||
| }; | |||
| const handleItemsPerPageChange = (event) => { | |||
| const itemsPerPage = parseInt(event.target.value); | |||
| batch(() => { | |||
| dispatch(updateItemsPerPage(itemsPerPage)); | |||
| dispatch(updatePage(0)); | |||
| }); | |||
| }; | |||
| return ( | |||
| <Paper | |||
| sx={{ | |||
| display: 'flex', | |||
| flexDirection: 'column', | |||
| justifyContent: 'start', | |||
| py: 2, | |||
| minHeight: 500, | |||
| }} | |||
| elevation={5} | |||
| > | |||
| <Typography sx={{ my: 4 }} variant="h4" gutterBottom align="center"> | |||
| Pagination, Filtering and Sorting Example Client Side | |||
| </Typography> | |||
| <Box | |||
| sx={{ | |||
| display: 'flex', | |||
| justifyContent: 'space-between', | |||
| flexWrap: 'wrap', | |||
| mx: 2, | |||
| }} | |||
| > | |||
| <Box | |||
| sx={{ | |||
| display: 'flex', | |||
| justifyContent: 'space-between', | |||
| width: '100%', | |||
| }} | |||
| > | |||
| {/* TODO Separate into SelectComponent */} | |||
| <FormControl sx={{ flexGrow: 1 }}> | |||
| <InputLabel id="sort-label">Sort</InputLabel> | |||
| <Select | |||
| label="Sort" | |||
| labelId="sort-label" | |||
| id="sort-select-helper" | |||
| value={sort} | |||
| onChange={handleSortChange} | |||
| > | |||
| <MenuItem value="name-asc">Name - A-Z</MenuItem> | |||
| <MenuItem value="name-desc">Name - Z-A</MenuItem> | |||
| <MenuItem value="price-asc">Price - Lowest to Highest</MenuItem> | |||
| <MenuItem value="price-desc">Price - Highest to Lowest</MenuItem> | |||
| </Select> | |||
| </FormControl> | |||
| <TextField | |||
| sx={{ flexGrow: 1 }} | |||
| variant="outlined" | |||
| label="Filter" | |||
| placeholder="Filter" | |||
| value={filterText} | |||
| onChange={handleFilterTextChange} | |||
| /> | |||
| </Box> | |||
| </Box> | |||
| <Grid container> | |||
| {items && | |||
| items.length > 0 && | |||
| items | |||
| .slice( | |||
| currentPage * itemsPerPage, | |||
| currentPage * itemsPerPage + itemsPerPage | |||
| ) | |||
| .map((product, index) => ( | |||
| // ! DON'T USE index for key, this is for example only | |||
| <Grid item sx={{ p: 2 }} xs={12} sm={6} md={4} lg={3} key={index}> | |||
| {/* TODO separate into component */} | |||
| <Paper sx={{ p: 3, height: '100%' }} elevation={3}> | |||
| <Typography sx={{ fontWeight: 600 }}>Name: </Typography> | |||
| <Typography display="inline"> {product.name}</Typography> | |||
| <Divider /> | |||
| <Typography sx={{ fontWeight: 600 }}>Designer: </Typography> | |||
| <Typography display="inline"> {product.designer}</Typography> | |||
| <Divider /> | |||
| <Typography sx={{ fontWeight: 600 }}>Type: </Typography> | |||
| <Typography display="inline"> {product.type}</Typography> | |||
| <Divider /> | |||
| <Typography sx={{ fontWeight: 600 }}>Price: </Typography> | |||
| <Typography display="inline"> ${product.price}</Typography> | |||
| </Paper> | |||
| </Grid> | |||
| ))} | |||
| </Grid> | |||
| <Box sx={{ width: '100%' }}> | |||
| <TablePagination | |||
| component="div" | |||
| count={totalCount} | |||
| page={currentPage} | |||
| onPageChange={handlePageChange} | |||
| rowsPerPage={itemsPerPage} | |||
| onRowsPerPageChange={handleItemsPerPageChange} | |||
| rowsPerPageOptions={[12, 24, 48, 96]} | |||
| labelRowsPerPage="Items per page" | |||
| showFirstButton | |||
| showLastButton | |||
| /> | |||
| </Box> | |||
| </Paper> | |||
| ); | |||
| }; | |||
| export default PagingSortingFilteringExample; | |||
| @@ -1,159 +0,0 @@ | |||
| import React, { useEffect, useState } from 'react'; | |||
| import { | |||
| Paper, | |||
| Box, | |||
| Grid, | |||
| Typography, | |||
| Divider, | |||
| TablePagination, | |||
| TextField, | |||
| FormControl, | |||
| InputLabel, | |||
| Select, | |||
| MenuItem, | |||
| } from '@mui/material'; | |||
| // import { useTranslation } from 'react-i18next'; | |||
| import Backdrop from '../BackdropComponent'; | |||
| import useDebounce from '../../../hooks/useDebounceHook'; | |||
| import { useRandomData } from '../../../context/RandomDataContext'; | |||
| const PagingSortingFilteringExampleServerSide = () => { | |||
| const [filterText, setFilterText] = useState(''); | |||
| const { state, data } = useRandomData(); | |||
| const { items, loading, totalCount, currentPage, itemsPerPage, sort } = data; | |||
| const { setPage, setItemsPerPage, setSort, setFilter } = state; | |||
| // const { t } = useTranslation(); | |||
| // Use debounce to prevent too many rerenders | |||
| const debouncedFilterText = useDebounce(filterText, 500); | |||
| useEffect(() => { | |||
| setFilter(filterText); | |||
| }, [debouncedFilterText]); | |||
| const handleFilterTextChange = (event) => { | |||
| const filterText = event.target.value; | |||
| setFilterText(filterText); | |||
| }; | |||
| const handleSortChange = (event) => { | |||
| const sort = event.target.value; | |||
| setSort(sort); | |||
| }; | |||
| const handlePageChange = (event, newPage) => { | |||
| setPage(newPage); | |||
| }; | |||
| const handleItemsPerPageChange = (event) => { | |||
| const itemsPerPage = parseInt(event.target.value); | |||
| setItemsPerPage(itemsPerPage); | |||
| setPage(0); | |||
| }; | |||
| return ( | |||
| <Paper | |||
| sx={{ | |||
| display: 'flex', | |||
| flexDirection: 'column', | |||
| justifyContent: 'start', | |||
| py: 2, | |||
| minHeight: 500, | |||
| position: 'relative', | |||
| }} | |||
| elevation={5} | |||
| > | |||
| {loading && <Backdrop isLoading position="absolute" />} | |||
| <Typography sx={{ my: 4 }} variant="h4" gutterBottom align="center"> | |||
| Pagination, Filtering and Sorting Example Server Side | |||
| </Typography> | |||
| <Box | |||
| sx={{ | |||
| display: 'flex', | |||
| justifyContent: 'space-between', | |||
| flexWrap: 'wrap', | |||
| mx: 2, | |||
| }} | |||
| > | |||
| <Box | |||
| sx={{ | |||
| display: 'flex', | |||
| justifyContent: 'space-between', | |||
| width: '100%', | |||
| }} | |||
| > | |||
| <FormControl sx={{ flexGrow: 1 }}> | |||
| <InputLabel id="sort-label">Sort</InputLabel> | |||
| <Select | |||
| label="Sort" | |||
| labelId="sort-label" | |||
| id="sort-select-helper" | |||
| value={sort || ''} | |||
| onChange={handleSortChange} | |||
| > | |||
| <MenuItem value="">None</MenuItem> | |||
| <MenuItem value="name-asc">Name - A-Z</MenuItem> | |||
| <MenuItem value="name-desc">Name - Z-A</MenuItem> | |||
| <MenuItem value="price-asc">Price - Lowest to Highest</MenuItem> | |||
| <MenuItem value="price-desc">Price - Highest to Lowest</MenuItem> | |||
| </Select> | |||
| </FormControl> | |||
| <TextField | |||
| sx={{ flexGrow: 1 }} | |||
| // variant="outlined" | |||
| label="Filter" | |||
| placeholder="Filter" | |||
| value={filterText} | |||
| onChange={handleFilterTextChange} | |||
| /> | |||
| </Box> | |||
| <Grid container sx={{ position: 'relative' }}> | |||
| {items && | |||
| items.length > 0 && | |||
| items.map((item) => ( | |||
| <Grid | |||
| item | |||
| sx={{ p: 2 }} | |||
| xs={12} | |||
| sm={6} | |||
| md={4} | |||
| lg={3} | |||
| key={item.id} | |||
| > | |||
| {/* TODO separate into component */} | |||
| <Paper sx={{ p: 3, height: '100%' }} elevation={3}> | |||
| <Typography sx={{ fontWeight: 600 }}>Name: </Typography> | |||
| <Typography display="inline"> {item.name}</Typography> | |||
| <Divider /> | |||
| <Typography sx={{ fontWeight: 600 }}>Company: </Typography> | |||
| <Typography display="inline"> {item.company}</Typography> | |||
| <Divider /> | |||
| <Typography sx={{ fontWeight: 600 }}>Color: </Typography> | |||
| <Typography display="inline"> {item.color}</Typography> | |||
| <Divider /> | |||
| <Typography sx={{ fontWeight: 600 }}>Price: </Typography> | |||
| <Typography display="inline"> {item.price}</Typography> | |||
| </Paper> | |||
| </Grid> | |||
| ))} | |||
| </Grid> | |||
| <Box sx={{ width: '100%' }}> | |||
| <TablePagination | |||
| component="div" | |||
| count={totalCount} | |||
| page={currentPage} | |||
| onPageChange={handlePageChange} | |||
| rowsPerPage={itemsPerPage} | |||
| onRowsPerPageChange={handleItemsPerPageChange} | |||
| rowsPerPageOptions={[12, 24, 48, 96]} | |||
| labelRowsPerPage="Items per page" | |||
| showFirstButton | |||
| showLastButton | |||
| /> | |||
| </Box> | |||
| </Box> | |||
| </Paper> | |||
| ); | |||
| }; | |||
| export default PagingSortingFilteringExampleServerSide; | |||
| @@ -1,26 +0,0 @@ | |||
| import React, { useState } from 'react'; | |||
| import { Button, Menu, MenuItem } from '@mui/material'; | |||
| const MenuListComponent = () => { | |||
| const [anchorEl, setAnchorEl] = useState(null); | |||
| const open = Boolean(anchorEl); | |||
| const handleClick = (event) => { | |||
| setAnchorEl(event.currentTarget); | |||
| }; | |||
| const handleClose = () => { | |||
| setAnchorEl(null); | |||
| }; | |||
| return ( | |||
| <div> | |||
| <Button onClick={handleClick}>Menu List</Button> | |||
| <Menu id="menu-list" anchorEl={anchorEl} open={open} onClose={handleClose}> | |||
| <MenuItem onClick={handleClose}>Menu Item 1</MenuItem> | |||
| <MenuItem onClick={handleClose}>Menu Item 2</MenuItem> | |||
| <MenuItem onClick={handleClose}>Menu Item 3</MenuItem> | |||
| </Menu> | |||
| </div> | |||
| ); | |||
| }; | |||
| export default MenuListComponent; | |||
| @@ -1,160 +0,0 @@ | |||
| import React, { useState, useMemo, useContext } from "react"; | |||
| import { | |||
| AppBar, | |||
| Badge, | |||
| Box, | |||
| IconButton, | |||
| Toolbar, | |||
| Typography, | |||
| List, | |||
| ListItem, | |||
| ListItemButton, | |||
| ListItemIcon, | |||
| ListItemText, | |||
| useMediaQuery, | |||
| } from "@mui/material"; | |||
| import { useTheme } from "@mui/system"; | |||
| import MenuOutlinedIcon from "@mui/icons-material/MenuOutlined"; | |||
| import ShoppingBasketIcon from "@mui/icons-material/ShoppingBasket"; | |||
| import Brightness4Icon from "@mui/icons-material/Brightness4"; | |||
| import Brightness7Icon from "@mui/icons-material/Brightness7"; | |||
| import MenuList from "./MenuListComponent"; | |||
| import Drawer from "./DrawerComponent"; | |||
| import { ColorModeContext } from "../../context/ColorModeContext"; | |||
| const NavbarComponent = () => { | |||
| const [openDrawer, setOpenDrawer] = useState(false); | |||
| const theme = useTheme(); | |||
| const matches = useMediaQuery(theme.breakpoints.down("sm")); | |||
| const toggleColorMode = useContext(ColorModeContext); | |||
| const handleToggleDrawer = () => { | |||
| setOpenDrawer(!openDrawer); | |||
| }; | |||
| const drawerContent = useMemo( | |||
| () => ( | |||
| <List> | |||
| <ListItemButton divider onClick={handleToggleDrawer}> | |||
| <ListItemIcon> | |||
| <ListItemText>Link 1</ListItemText> | |||
| </ListItemIcon> | |||
| </ListItemButton> | |||
| <ListItem divider onClick={handleToggleDrawer}> | |||
| <ListItemIcon> | |||
| <ListItemText>Link 2</ListItemText> | |||
| </ListItemIcon> | |||
| </ListItem> | |||
| <ListItem divider onClick={handleToggleDrawer}> | |||
| <ListItemText>Link 3</ListItemText> | |||
| </ListItem> | |||
| <ListItem divider> | |||
| <IconButton onClick={toggleColorMode}> | |||
| <ListItemText>Toggle {theme.palette.mode} mode</ListItemText> | |||
| {theme.palette.mode === "dark" ? ( | |||
| <Brightness7Icon /> | |||
| ) : ( | |||
| <Brightness4Icon /> | |||
| )} | |||
| </IconButton> | |||
| </ListItem> | |||
| </List> | |||
| ), | |||
| [handleToggleDrawer] | |||
| ); | |||
| return ( | |||
| <AppBar | |||
| elevation={2} | |||
| sx={{ backgroundColor: "background.default", position: "relative" }} | |||
| > | |||
| <Toolbar> | |||
| <Box | |||
| component="div" | |||
| sx={{ | |||
| display: "flex", | |||
| justifyContent: "space-between", | |||
| alignItems: "center", | |||
| width: "100%", | |||
| }} | |||
| > | |||
| {matches ? ( | |||
| <Drawer | |||
| open={openDrawer} | |||
| toggleOpen={handleToggleDrawer} | |||
| content={drawerContent} | |||
| /> | |||
| ) : ( | |||
| <Box sx={{ display: "flex" }}> | |||
| <Typography | |||
| variant="h6" | |||
| sx={{ | |||
| marginRight: 3, | |||
| cursor: "pointer", | |||
| color: "text.primary", | |||
| }} | |||
| > | |||
| Link 1 | |||
| </Typography> | |||
| <Typography | |||
| variant="body1" | |||
| sx={{ | |||
| marginRight: 3, | |||
| cursor: "pointer", | |||
| color: "text.primary", | |||
| }} | |||
| > | |||
| Link 2 | |||
| </Typography> | |||
| <Typography | |||
| variant="subtitle1" | |||
| sx={{ | |||
| marginRight: 3, | |||
| cursor: "pointer", | |||
| color: "text.primary", | |||
| }} | |||
| > | |||
| Link 3 | |||
| </Typography> | |||
| </Box> | |||
| )} | |||
| <Box> | |||
| <MenuList /> | |||
| </Box> | |||
| <Box | |||
| sx={{ | |||
| display: "flex", | |||
| justifyContent: "center", | |||
| alignItems: "center", | |||
| }} | |||
| > | |||
| {matches ? ( | |||
| <Box> | |||
| <IconButton onClick={handleToggleDrawer}> | |||
| <MenuOutlinedIcon /> | |||
| </IconButton> | |||
| </Box> | |||
| ) : ( | |||
| <Box> | |||
| <IconButton> | |||
| <Badge badgeContent={3} color="primary"> | |||
| <ShoppingBasketIcon color="action" /> | |||
| </Badge> | |||
| </IconButton> | |||
| <IconButton sx={{ ml: 1 }} onClick={toggleColorMode}> | |||
| {theme.palette.mode === "dark" ? ( | |||
| <Brightness7Icon /> | |||
| ) : ( | |||
| <Brightness4Icon /> | |||
| )} | |||
| </IconButton> | |||
| </Box> | |||
| )} | |||
| </Box> | |||
| </Box> | |||
| </Toolbar> | |||
| </AppBar> | |||
| ); | |||
| }; | |||
| export default NavbarComponent; | |||
| @@ -22,6 +22,11 @@ import useFilters from "../../../hooks/useFilters"; | |||
| import useSorting from "../../../hooks/useSorting"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import { Tooltip } from "@mui/material"; | |||
| import { | |||
| ALL_CATEGORIES, | |||
| COMMA, | |||
| SPREAD, | |||
| } from "../../../constants/marketplaceHeaderTitle"; | |||
| const DownArrow = (props) => ( | |||
| <IconStyled {...props}> | |||
| @@ -34,38 +39,45 @@ const Header = (props) => { | |||
| const sorting = useSorting(); | |||
| const { t } = useTranslation(); | |||
| const [sortOption, setSortOption] = useState(sortEnum.INITIAL); | |||
| const [headerString, setHeaderString] = useState("SVE KATEGORIJE"); | |||
| const [headerString, setHeaderString] = useState(ALL_CATEGORIES); | |||
| //Changing shown sort option on select menu | |||
| useEffect(() => { | |||
| setSortOption(sorting.selectedSortOption); | |||
| }, [sorting.selectedSortOption]); | |||
| // Changing header string on refresh or on load | |||
| useEffect(() => { | |||
| let headerStringLocal = ALL_CATEGORIES; | |||
| if (filters.isApplied) { | |||
| let headerStringLocal = "Sve kategorije"; | |||
| // Adding category to header string | |||
| if (filters.selectedCategory?.name) { | |||
| headerStringLocal = filters.selectedCategory.name; | |||
| // Adding subcategories to header string | |||
| if (filters.selectedSubcategory?.name) { | |||
| headerStringLocal += ` | ${filters.selectedSubcategory.name}`; | |||
| headerStringLocal += `${SPREAD}${filters.selectedSubcategory.name}`; | |||
| } | |||
| } | |||
| // Adding locations to header string | |||
| if (filters.selectedLocations && filters.selectedLocations?.length > 0) { | |||
| headerStringLocal += " | "; | |||
| headerStringLocal += SPREAD; | |||
| filters.selectedLocations.forEach((location, index) => { | |||
| // Checking if item is last | |||
| if (index + 1 === filters.selectedLocations.length) { | |||
| headerStringLocal += location.city; | |||
| } else { | |||
| headerStringLocal += location.city + ", "; | |||
| headerStringLocal += location.city + COMMA; | |||
| } | |||
| }); | |||
| } | |||
| setHeaderString(headerStringLocal); | |||
| } | |||
| setHeaderString(headerStringLocal); | |||
| }, [ | |||
| filters.isApplied, | |||
| filters.selectedCategory, | |||
| filters.selectedLocations, | |||
| filters.selectedSubcategory, | |||
| filters.isApplied, | |||
| filters.selectedLocations, | |||
| ]); | |||
| const handleChangeSelect = (event) => { | |||
| @@ -80,8 +92,9 @@ const Header = (props) => { | |||
| return ( | |||
| <HeaderContainer> | |||
| {/* Setting appropriate header title if page is market place or my offers */} | |||
| <Tooltip title={headerString}> | |||
| {props.myOffers !== true ? ( | |||
| {!props.myOffers ? ( | |||
| headerString === "Sve kategorije" && | |||
| (sorting.selectedSortOption === sortEnum.INITIAL || | |||
| sorting.selectedSortOption === sortEnum.NEW) ? ( | |||
| @@ -93,11 +106,16 @@ const Header = (props) => { | |||
| <HeaderLocation>{headerString}</HeaderLocation> | |||
| ) | |||
| ) : ( | |||
| <MySwapsTitle> <RefreshIcon /> {t("header.myOffers")}</MySwapsTitle> | |||
| <MySwapsTitle> | |||
| <RefreshIcon /> {t("header.myOffers")} | |||
| </MySwapsTitle> | |||
| )} | |||
| </Tooltip> | |||
| {/* ^^^^^^ */} | |||
| <HeaderOptions> | |||
| <HeaderButtons> | |||
| {/* Setting display of offer cards to full width */} | |||
| <HeaderButton | |||
| iconColor={ | |||
| props.isGrid | |||
| @@ -108,6 +126,9 @@ const Header = (props) => { | |||
| > | |||
| <GridLine /> | |||
| </HeaderButton> | |||
| {/* ^^^^^^ */} | |||
| {/* Setting display of offer cards to half width (Grid) */} | |||
| <HeaderButton | |||
| iconColor={ | |||
| props.isGrid | |||
| @@ -118,7 +139,10 @@ const Header = (props) => { | |||
| > | |||
| <GridSquare /> | |||
| </HeaderButton> | |||
| {/* ^^^^^^ */} | |||
| </HeaderButtons> | |||
| {/* Select option to choose sorting */} | |||
| <HeaderSelect | |||
| value={sortOption?.value ? sortOption.value : sortEnum.INITIAL.value} | |||
| IconComponent={DownArrow} | |||
| @@ -135,6 +159,7 @@ const Header = (props) => { | |||
| ); | |||
| })} | |||
| </HeaderSelect> | |||
| {/* ^^^^^^ */} | |||
| </HeaderOptions> | |||
| </HeaderContainer> | |||
| ); | |||
| @@ -19,8 +19,5 @@ MarketPlace.propTypes = { | |||
| children: PropTypes.node, | |||
| myOffers: PropTypes.bool, | |||
| }; | |||
| // MarketPlace.defaultProps = { | |||
| // myOffers: false, | |||
| // } | |||
| export default MarketPlace; | |||
| @@ -1,247 +1,42 @@ | |||
| import React, { useEffect, useMemo, useRef, useState } from "react"; | |||
| import React, { useRef } from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { OffersContainer } from "./Offers.styled"; | |||
| import OfferCard from "../../Cards/OfferCard/OfferCard"; | |||
| import { | |||
| fetchMineOffers, | |||
| fetchOffers, | |||
| } from "../../../store/actions/offers/offersActions"; | |||
| import { useDispatch, useSelector } from "react-redux"; | |||
| import { | |||
| selectMineOffers, | |||
| selectOffers, | |||
| selectPinnedOffers, | |||
| selectTotalOffers, | |||
| } from "../../../store/selectors/offersSelectors"; | |||
| import { useSelector } from "react-redux"; | |||
| import Paging from "../../Paging/Paging"; | |||
| import { HOME_PAGE } from "../../../constants/pages"; | |||
| import { useHistory } from "react-router-dom"; | |||
| import { useQueryString } from "../../../hooks/useQueryString"; | |||
| import { | |||
| selectSelectedCategory, | |||
| selectSelectedLocations, | |||
| selectSelectedSortOption, | |||
| selectSelectedSubcategory, | |||
| } from "../../../store/selectors/filtersSelectors"; | |||
| import { sortEnum } from "../../../enums/sortEnum"; | |||
| import { selectLatestChats } from "../../../store/selectors/chatSelectors"; | |||
| import { fetchChats } from "../../../store/actions/chat/chatActions"; | |||
| import { selectUserId } from "../../../store/selectors/loginSelectors"; | |||
| import { startChat } from "../../../util/helpers/chatHelper"; | |||
| import useOffers from "../../../hooks/useOffers"; | |||
| const Offers = (props) => { | |||
| const [page, setPage] = useState(1); | |||
| // const [pinnedLength, setPinnedLength] = useState(0); | |||
| const pinnedOffers = useSelector(selectPinnedOffers); | |||
| const selectedCategory = useSelector(selectSelectedCategory); | |||
| const selectedSubcategory = useSelector(selectSelectedSubcategory); | |||
| const selectedLocations = useSelector(selectSelectedLocations); | |||
| const selectedSortOption = useSelector(selectSelectedSortOption); | |||
| const offers = useSelector(selectOffers); | |||
| const mineOffers = useSelector(selectMineOffers); | |||
| const chats = useSelector(selectLatestChats); | |||
| const total = useSelector(selectTotalOffers); | |||
| const history = useHistory(); | |||
| const dispatch = useDispatch(); | |||
| const offersRef = useRef(null); | |||
| const queryStringHook = useQueryString(props.myOffers); | |||
| const userId = useSelector(selectUserId); | |||
| useEffect(() => { | |||
| dispatch(fetchChats()); | |||
| }, []) | |||
| useEffect(() => { | |||
| let queryObject = queryStringHook.getQueryObject(); | |||
| if (queryObject.page && queryObject.page !== 1) { | |||
| setPage(parseInt(queryObject.page)); | |||
| } | |||
| }, [history.location.search]); | |||
| useEffect(() => { | |||
| if (history?.location?.state?.logo || history?.location?.state?.refetch) { | |||
| dispatch(fetchOffers({ queryString: "" })); | |||
| setPage(1); | |||
| history.location.state = undefined; | |||
| } | |||
| }, [history.location.state]); | |||
| useEffect(() => { | |||
| if (queryStringHook.loadedFromURL) { | |||
| refetch(); | |||
| } else { | |||
| queryStringHook.appendMultipleToQueryString([ | |||
| { key: "size", value: "10" }, | |||
| { key: "page", value: "1" }, | |||
| ]); | |||
| } | |||
| }, [queryStringHook.loadedFromURL, queryStringHook.queryString]); | |||
| useEffect(() => { | |||
| const queryObject = new URLSearchParams(queryStringHook.queryString); | |||
| if (queryObject.has("page")) { | |||
| if (queryObject.get("page") !== page.toString()) { | |||
| queryStringHook.appendToQueryString("page", page); | |||
| } else { | |||
| refetch(); | |||
| } | |||
| } else { | |||
| queryStringHook.appendToQueryString("page", page); | |||
| } | |||
| }, [page]); | |||
| const pinnedOffersToShow = useMemo(() => { | |||
| if (props.myOffers) { | |||
| return mineOffers.filter((item) => item.pinned === true); | |||
| } | |||
| return pinnedOffers; | |||
| }, [pinnedOffers, mineOffers, page, props.myOffers]); | |||
| const offersToShow = useMemo(() => { | |||
| if (props.myOffers) { | |||
| return mineOffers.filter((item) => item.pinned === false); | |||
| } | |||
| return offers; | |||
| }, [offers, mineOffers, page, props.myOffers]); | |||
| const allOffersToShow = useMemo(() => { | |||
| let newOffers = [...pinnedOffersToShow, ...offersToShow]; | |||
| if (props.myOffers) { | |||
| if (selectedCategory && selectedCategory?._id !== 0) { | |||
| newOffers = newOffers.filter( | |||
| (item) => item.category.name === selectedCategory.name | |||
| ); | |||
| } | |||
| if (selectedSubcategory && selectedSubcategory?._id !== 0) { | |||
| newOffers = newOffers.filter( | |||
| (item) => item.subcategory === selectedSubcategory.name | |||
| ); | |||
| } | |||
| if (selectedLocations && selectedLocations?.length > 0) { | |||
| newOffers = newOffers.filter((item) => { | |||
| let isInOneOfLocations = false; | |||
| selectedLocations?.forEach((location) => { | |||
| if (item.location.city === location.city) { | |||
| isInOneOfLocations = true; | |||
| } | |||
| }); | |||
| return isInOneOfLocations; | |||
| }); | |||
| } | |||
| let oldOffers = [...offersToShow]; | |||
| let oldPinnedOffers = [...pinnedOffersToShow]; | |||
| if ( | |||
| selectedSortOption && | |||
| selectedSortOption.value === sortEnum.NEW.value | |||
| ) { | |||
| newOffers = [ | |||
| ...oldPinnedOffers.sort( | |||
| (itemA, itemB) => | |||
| new Date(itemB._created) - new Date(itemA._created) | |||
| ), | |||
| ...oldOffers.sort( | |||
| (itemA, itemB) => | |||
| new Date(itemB._created) - new Date(itemA._created) | |||
| ), | |||
| ]; | |||
| } | |||
| if ( | |||
| selectedSortOption && | |||
| selectedSortOption.value === sortEnum.OLD.value | |||
| ) { | |||
| newOffers = newOffers.sort( | |||
| (itemA, itemB) => new Date(itemA._created) - new Date(itemB._created) | |||
| ); | |||
| newOffers = [ | |||
| ...oldPinnedOffers.sort( | |||
| (itemA, itemB) => | |||
| new Date(itemA._created) - new Date(itemB._created) | |||
| ), | |||
| ...oldOffers.sort( | |||
| (itemA, itemB) => | |||
| new Date(itemA._created) - new Date(itemB._created) | |||
| ), | |||
| ]; | |||
| } | |||
| if ( | |||
| selectedSortOption && | |||
| selectedSortOption.value === sortEnum.POPULAR.value | |||
| ) { | |||
| newOffers = [ | |||
| ...oldPinnedOffers.sort( | |||
| (itemA, itemB) => itemB.views.count - itemA.views.count | |||
| ), | |||
| ...oldOffers.sort( | |||
| (itemA, itemB) => itemB.views.count - itemA.views.count | |||
| ), | |||
| ]; | |||
| } | |||
| newOffers = newOffers.slice((page - 1) * 10, page * 10); | |||
| } | |||
| return newOffers; | |||
| }, [pinnedOffersToShow, offersToShow, props.myOffers, page]); | |||
| const totalOffers = useMemo(() => { | |||
| if (props.myOffers) { | |||
| return mineOffers?.length; | |||
| } | |||
| return total; | |||
| }, [mineOffers, total]); | |||
| const handleDifferentPage = (pageNum) => { | |||
| setPage(pageNum); | |||
| }; | |||
| const refetch = () => { | |||
| if (!props.myOffers) { | |||
| dispatch(fetchOffers({ queryString: "?" + queryStringHook.queryString })); | |||
| history.replace({ | |||
| pathname: HOME_PAGE, | |||
| search: queryStringHook.getGlobalQueryString(), | |||
| }); | |||
| } else { | |||
| dispatch(fetchMineOffers()); | |||
| } | |||
| window.scrollTo({ | |||
| top: 0, | |||
| behavior: "smooth", | |||
| }); | |||
| const queryObject = new URLSearchParams(queryStringHook.queryString); | |||
| if (queryObject.has("page")) { | |||
| if (queryObject.get("page") !== page.toString()) | |||
| setPage(parseInt(queryObject.get("page"))); | |||
| } else { | |||
| setPage(1); | |||
| } | |||
| }; | |||
| const offers = useOffers(props.myOffers); | |||
| const messageOneUser = (offer) => { | |||
| const chatItem = chats.find(item => item.chat.offerId === offer?._id); | |||
| if (chatItem !== undefined) { | |||
| history.push(`/messages/${chatItem.chat._id}`) | |||
| } else { | |||
| if (offer?.userId !== userId) { | |||
| history.push(`/messages/newMessage`, { | |||
| offerId: offer?._id | |||
| }) | |||
| } | |||
| } | |||
| } | |||
| startChat(chats, offer, userId); | |||
| }; | |||
| return ( | |||
| <OffersContainer ref={offersRef}> | |||
| {allOffersToShow.map((item) => { | |||
| {offers.allOffersToShow.map((item) => { | |||
| return ( | |||
| <OfferCard key={item._id} offer={item} halfwidth={props.isGrid} messageUser={messageOneUser} /> | |||
| <OfferCard | |||
| key={item._id} | |||
| offer={item} | |||
| halfwidth={props.isGrid} | |||
| messageUser={messageOneUser} | |||
| /> | |||
| ); | |||
| })} | |||
| {allOffersToShow?.length === 0 && ( | |||
| <>akjshdkjhadsjkasjhkd</> | |||
| )} | |||
| {offers.allOffersToShow?.length === 0 && <>akjshdkjhadsjkasjhkd</>} | |||
| <Paging | |||
| totalElements={totalOffers} | |||
| totalElements={offers.totalOffers} | |||
| elementsPerPage={10} | |||
| current={page} | |||
| changePage={handleDifferentPage} | |||
| current={offers.page} | |||
| changePage={offers.handleDifferentPage} | |||
| /> | |||
| </OffersContainer> | |||
| ); | |||
| @@ -9,32 +9,42 @@ import { | |||
| } from "./Paging.styled"; | |||
| const Paging = (props) => { | |||
| // Determining total pages | |||
| const pages = props.pages | |||
| ? props.pages | |||
| : props.totalElements | |||
| ? Math.ceil(props.totalElements / props.elementsPerPage) | |||
| : 1; | |||
| let moving = 0; | |||
| // Making array of pages which contains 2 pages before and after current page | |||
| const pagesAsArray = Array.apply(null, Array(5)).map(() => {}); | |||
| // Showing 3 dots if current page is away more than 3 of starting or ending page | |||
| const threeDotsBefore = props.current - 2 > 1; | |||
| const threeDotsAfter = props.current + 2 < pages; | |||
| return ( | |||
| <PagingContainer> | |||
| {/* Left arrow */} | |||
| <Arrow | |||
| onClick={() => props.changePage(props.current - 1)} | |||
| disabled={props.current - 1 < 1} | |||
| > | |||
| <ArrowIcon side="left" /> | |||
| </Arrow> | |||
| {threeDotsBefore && ( | |||
| <React.Fragment> | |||
| <PageNumber onClick={() => props.changePage(1)}>1</PageNumber> | |||
| {props.current - 3 !== 1 && <ThreeDots>...</ThreeDots>} | |||
| </React.Fragment> | |||
| )} | |||
| {/* Pages */} | |||
| {pagesAsArray.map((item, index) => { | |||
| const pageNum = props.current - 2 + moving++; | |||
| if (pageNum > pages ) return; | |||
| if (pageNum > pages) return; | |||
| if (pageNum < 1) return; | |||
| return ( | |||
| <PageNumber | |||
| @@ -46,6 +56,7 @@ const Paging = (props) => { | |||
| </PageNumber> | |||
| ); | |||
| })} | |||
| {threeDotsAfter && ( | |||
| <React.Fragment> | |||
| {props.current + 3 !== pages && <ThreeDots>...</ThreeDots>} | |||
| @@ -54,6 +65,8 @@ const Paging = (props) => { | |||
| </PageNumber> | |||
| </React.Fragment> | |||
| )} | |||
| {/* Right arrow */} | |||
| <Arrow | |||
| onClick={() => props.changePage(props.current + 1)} | |||
| disabled={props.current + 1 > pages} | |||
| @@ -13,8 +13,10 @@ import { | |||
| PopoverNoItemsText, | |||
| PopoverTitle, | |||
| } from "./HeaderPopover.styled"; | |||
| import { useTranslation } from "react-i18next"; | |||
| const HeaderPopover = (props) => { | |||
| const { t } = useTranslation(); | |||
| return ( | |||
| <HeaderPopoverContainer> | |||
| <PopoverTitle p={2}>{props.title}</PopoverTitle> | |||
| @@ -24,14 +26,22 @@ const HeaderPopover = (props) => { | |||
| <PopoverListItem key={index}> | |||
| <PopoverListItemAvatarContainer> | |||
| {props.isProfile ? ( | |||
| <PopoverListItemProfileAvatar alt={item.alt} src={item.src} onClick={item?.onClick} /> | |||
| <PopoverListItemProfileAvatar | |||
| alt={item.alt} | |||
| src={item.src} | |||
| onClick={item?.onClick} | |||
| /> | |||
| ) : ( | |||
| <PopoverListItemAvatar alt={item.alt} src={item.src} onClick={item?.onClick} /> | |||
| <PopoverListItemAvatar | |||
| alt={item.alt} | |||
| src={item.src} | |||
| onClick={item?.onClick} | |||
| /> | |||
| )} | |||
| </PopoverListItemAvatarContainer> | |||
| <PopoverListItemTextContainer | |||
| primaryTypographyProps={{ | |||
| onClick: item.onClick | |||
| onClick: item.onClick, | |||
| }} | |||
| primary={item.title} | |||
| secondary={item.text} | |||
| @@ -39,7 +49,7 @@ const HeaderPopover = (props) => { | |||
| </PopoverListItem> | |||
| )) | |||
| ) : ( | |||
| <PopoverNoItemsText>No items at the moment...</PopoverNoItemsText> | |||
| <PopoverNoItemsText>{t("header.noItems")}</PopoverNoItemsText> | |||
| )} | |||
| </PopoverList> | |||
| <PopoverButtonsContainer> | |||