| "react/jsx-filename-extension": "off", | "react/jsx-filename-extension": "off", | ||||
| "react/jsx-props-no-spreading": "off", | "react/jsx-props-no-spreading": "off", | ||||
| "react/button-has-type": "off", | "react/button-has-type": "off", | ||||
| "react/max-len": ["error", { | |||||
| "code": 100 | |||||
| }], | |||||
| "react/require-default-props": "off", | "react/require-default-props": "off", | ||||
| "import/no-extraneous-dependencies": "off", | "import/no-extraneous-dependencies": "off", | ||||
| "import/prefer-default-export": "off", | "import/prefer-default-export": "off", |
| { | { | ||||
| "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] | |||||
| } | |||||
| } | } |
| import React, { useMemo } from "react"; | import React, { useMemo } from "react"; | ||||
| import PropTypes from "prop-types"; | import PropTypes from "prop-types"; | ||||
| import { | import { | ||||
| CheckButton, | |||||
| OfferImage, | |||||
| OfferTitle, | |||||
| ChatOffer, | |||||
| Commands, | |||||
| ChatInfo, | ChatInfo, | ||||
| OfferText, | |||||
| ChatCardContainer, | ChatCardContainer, | ||||
| Col, | Col, | ||||
| UserImage, | UserImage, | ||||
| OfferCardContainer, | |||||
| UserImgWrapper, | UserImgWrapper, | ||||
| OfferImgWrapper, | |||||
| UserName, | UserName, | ||||
| LastMessage, | LastMessage, | ||||
| Line, | Line, | ||||
| LocationContainer, | |||||
| XSText, | |||||
| LocationIcon, | |||||
| OfferCardContainerMobile, | |||||
| OfferTextMobile, | |||||
| OfferTitleMobile, | |||||
| PhoneIconContainer, | |||||
| PhoneIcon, | |||||
| LocationIconContainer, | |||||
| } from "./ChatCard.styled"; | } from "./ChatCard.styled"; | ||||
| import selectedTheme from "../../../themes"; | |||||
| import { useHistory } from "react-router-dom"; | import { useHistory } from "react-router-dom"; | ||||
| import useScreenDimensions from "../../../hooks/useScreenDimensions"; | 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 ChatCard = (props) => { | ||||
| const history = useHistory(); | const history = useHistory(); | ||||
| const dimensions = useScreenDimensions(); | const dimensions = useScreenDimensions(); | ||||
| const { t } = useTranslation(); | |||||
| const chat = useMemo(() => { | const chat = useMemo(() => { | ||||
| return props.chat; | return props.chat; | ||||
| } | } | ||||
| return ""; | return ""; | ||||
| }, [chat]); | }, [chat]); | ||||
| const routeToItem = () => { | const routeToItem = () => { | ||||
| history.push(`/messages/${chat?.chat?._id}`); | history.push(`/messages/${chat?.chat?._id}`); | ||||
| }; | }; | ||||
| <UserName>{chat?.interlocutorData?.name}</UserName> | <UserName>{chat?.interlocutorData?.name}</UserName> | ||||
| {/* Only shows on Mobile */} | {/* Only shows on Mobile */} | ||||
| <OfferCardContainerMobile> | |||||
| <OfferTextMobile>{t("messages.cardProduct")}</OfferTextMobile> | |||||
| <OfferTitleMobile>{chat?.offerData?.name}</OfferTitleMobile> | |||||
| </OfferCardContainerMobile> | |||||
| <MobileOfferDetails chat={chat} /> | |||||
| {/* ^^^^^ */} | {/* ^^^^^ */} | ||||
| <LastMessage>{lastMessage}</LastMessage> | <LastMessage>{lastMessage}</LastMessage> | ||||
| <LocationContainer> | |||||
| <LocationIconContainer> | |||||
| <LocationIcon /> | |||||
| </LocationIconContainer> | |||||
| <XSText>{chat?.interlocutorData?.location}</XSText> | |||||
| </LocationContainer> | |||||
| <OfferLocation chat={chat} /> | |||||
| </ChatInfo> | </ChatInfo> | ||||
| </Col> | </Col> | ||||
| <Line /> | <Line /> | ||||
| {/* Only shows on Desktop */} | {/* 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> | </ChatCardContainer> | ||||
| ); | ); | ||||
| }; | }; |
| import { Box, Container, Typography } from "@mui/material"; | import { Box, Container, Typography } from "@mui/material"; | ||||
| import styled from "styled-components"; | import styled from "styled-components"; | ||||
| import selectedTheme from "../../../themes"; | 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)` | export const ChatCardContainer = styled(Container)` | ||||
| display: flex; | display: flex; | ||||
| box-sizing: border-box; | box-sizing: border-box; | ||||
| margin: 10px 0; | margin: 10px 0; | ||||
| background-color: ${(props) => | background-color: ${(props) => | ||||
| props.sponsored === "true" | |||||
| ? selectedTheme.backgroundSponsoredColor | |||||
| : "white"}; | |||||
| props.sponsored === "true" ? selectedTheme.backgroundSponsoredColor : "white"}; | |||||
| border-radius: 4px; | border-radius: 4px; | ||||
| ${(props) => | ${(props) => | ||||
| props.sponsored === "true" && | |||||
| `border: 1px solid ${selectedTheme.borderSponsoredColor};`} | |||||
| props.sponsored === "true" && `border: 1px solid ${selectedTheme.borderSponsoredColor};`} | |||||
| padding: 16px; | padding: 16px; | ||||
| max-width: 2000px; | max-width: 2000px; | ||||
| height: 180px; | height: 180px; | ||||
| margin: 0; | margin: 0; | ||||
| ${(props) => | ${(props) => | ||||
| props.vertical && | props.vertical && | ||||
| ` | |||||
| height: 330px; | |||||
| `height: 330px; | |||||
| width: 180px; | width: 180px; | ||||
| margin: 0 18px; | |||||
| `} | |||||
| margin: 0 18px;`} | |||||
| } | } | ||||
| `; | `; | ||||
| export const UserImage = styled.img` | export const UserImage = styled.img` | ||||
| height: 72px; | height: 72px; | ||||
| } | } | ||||
| `; | `; | ||||
| export const UserImgWrapper = styled(Box)` | export const UserImgWrapper = styled(Box)` | ||||
| overflow: hidden; | overflow: hidden; | ||||
| border-radius: 50%; | border-radius: 50%; | ||||
| min-width: 80px; | 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)` | export const ChatInfo = styled(Box)` | ||||
| height: 100%; | height: 100%; | ||||
| display: flex; | display: flex; | ||||
| flex-direction: column; | flex-direction: column; | ||||
| justify-content: space-between; | justify-content: space-between; | ||||
| `; | `; | ||||
| export const Col = styled(Box)` | export const Col = styled(Box)` | ||||
| display: flex; | display: flex; | ||||
| align-items: center; | align-items: center; | ||||
| flex-direction: row; | flex-direction: row; | ||||
| gap: 18px; | gap: 18px; | ||||
| flex: 1; | 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)` | export const UserName = styled(Typography)` | ||||
| margin-bottom: 12px; | margin-bottom: 12px; | ||||
| font-family: "Open Sans"; | font-family: "Open Sans"; | ||||
| font-size: 18px; | font-size: 18px; | ||||
| } | } | ||||
| `; | `; | ||||
| export const LastMessage = styled(Typography)` | export const LastMessage = styled(Typography)` | ||||
| font-family: "Open Sans"; | font-family: "Open Sans"; | ||||
| color: ${selectedTheme.primaryDarkTextThird}; | color: ${selectedTheme.primaryDarkTextThird}; | ||||
| display: none; | 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)` | export const Line = styled(Box)` | ||||
| border-left: 1px solid rgba(0, 0, 0, 0.15); | border-left: 1px solid rgba(0, 0, 0, 0.15); | ||||
| height: 100px; | height: 100px; | ||||
| margin: auto 0; | margin: auto 0; | ||||
| @media (max-width: 600px) { | @media (max-width: 600px) { | ||||
| display: none; | display: none; | ||||
| } | |||||
| `; | |||||
| export const PhoneIcon = styled(Phone)` | |||||
| @media (max-width: 600px) { | |||||
| width: 14px; | |||||
| height: 14px; | |||||
| position: relative; | |||||
| top: 1px; | |||||
| } | |||||
| `; | |||||
| }`; |
| 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; |
| 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; | |||||
| `; |
| 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; |
| 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;`} | |||||
| } | |||||
| `; |
| 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; |
| 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}; | |||||
| `; |
| 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 |
| 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; | |||||
| `; |
| 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; |
| 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; |
| 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; | |||||
| `; |
| 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; |
| 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}; | |||||
| } | |||||
| `; |
| import React, { useEffect, useState } from "react"; | import React, { useEffect, useState } from "react"; | ||||
| import PropTypes from "prop-types"; | 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 DropdownItem from "../../../../Dropdown/DropdownItem/DropdownItem"; | ||||
| import { CheckBox } from "../../../../CheckBox/CheckBox"; | |||||
| import CheckboxDropdownList from "./CheckboxDropdownList/CheckboxDropdownList"; | |||||
| import Checkbox from "./Checkbox/Checkbox"; | |||||
| const FilterCheckboxDropdown = (props) => { | const FilterCheckboxDropdown = (props) => { | ||||
| const [toSearch, setToSearch] = useState(""); | |||||
| const [dataToShow, setDataToShow] = useState([]); | const [dataToShow, setDataToShow] = useState([]); | ||||
| const [isOpened, setIsOpened] = useState(false); | const [isOpened, setIsOpened] = useState(false); | ||||
| const [toSearch, setToSearch] = useState(""); | |||||
| const { data } = props; | const { data } = props; | ||||
| useEffect(() => { | useEffect(() => { | ||||
| setDataToShow([...data]); | setDataToShow([...data]); | ||||
| }, [data]); | }, [data]); | ||||
| useEffect(() => { | useEffect(() => { | ||||
| if (toSearch.length > 0) { | if (toSearch.length > 0) { | ||||
| setDataToShow( | setDataToShow( | ||||
| } | } | ||||
| }, [toSearch]); | }, [toSearch]); | ||||
| useEffect(() => { | useEffect(() => { | ||||
| if (props.filters?.length > 0) { | if (props.filters?.length > 0) { | ||||
| setIsOpened(true) | |||||
| setIsOpened(true); | |||||
| } | } | ||||
| }, [props.filters]) | |||||
| }, [props.filters]); | |||||
| const handleChange = (item) => { | const handleChange = (item) => { | ||||
| if (props.oneValueAllowed) { | if (props.oneValueAllowed) { | ||||
| props.setItemsSelected([item]); | props.setItemsSelected([item]); | ||||
| } else { | } 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 { | } else { | ||||
| props.setItemsSelected([...props.filters, item]); | props.setItemsSelected([...props.filters, item]); | ||||
| } | } | ||||
| } | } | ||||
| }; | }; | ||||
| const handleDelete = (item) => { | |||||
| props.setItemsSelected([...props.filters.filter((p) => p !== item)]); | |||||
| }; | |||||
| const handleClear = () => { | |||||
| setToSearch(""); | |||||
| }; | |||||
| return ( | return ( | ||||
| <DropdownList | |||||
| <CheckboxDropdownList | |||||
| toSearch={toSearch} | |||||
| setToSearch={setToSearch} | |||||
| title={props.title} | 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} | 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) => { | {dataToShow.map((item) => { | ||||
| return ( | return ( | ||||
| <DropdownItem key={item.city}> | <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)} | onChange={() => handleChange(item)} | ||||
| fullWidth | |||||
| /> | /> | ||||
| </DropdownItem> | </DropdownItem> | ||||
| ); | ); | ||||
| })} | })} | ||||
| </DropdownList> | |||||
| </CheckboxDropdownList> | |||||
| ); | ); | ||||
| }; | }; | ||||
| FilterCheckboxDropdown.defaultProps = { | FilterCheckboxDropdown.defaultProps = { | ||||
| oneValueAllowed: false, | oneValueAllowed: false, | ||||
| }; | }; | ||||
| export default FilterCheckboxDropdown; | export default FilterCheckboxDropdown; |
| 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}; | |||||
| } | |||||
| `; |
| OfferInfo, | OfferInfo, | ||||
| Info, | Info, | ||||
| PostDate, | PostDate, | ||||
| OfferTitle, | |||||
| OfferDescriptionText, | |||||
| OfferDescriptionTitle, | |||||
| Details, | |||||
| OfferDetails, | |||||
| OfferImage, | |||||
| Scroller, | |||||
| CategoryIcon, | CategoryIcon, | ||||
| SubcategoryIcon, | SubcategoryIcon, | ||||
| QuantityIcon, | QuantityIcon, | ||||
| import { startChat } from "../../../util/helpers/chatHelper"; | import { startChat } from "../../../util/helpers/chatHelper"; | ||||
| import Information from "./Information/Information"; | import Information from "./Information/Information"; | ||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import OfferDetails from "./OfferDetails/OfferDetails"; | |||||
| const ItemDetailsCard = (props) => { | const ItemDetailsCard = (props) => { | ||||
| const offer = props.offer; | const offer = props.offer; | ||||
| const date = formatDateLocale(new Date(offer?.offer?._created)); | const date = formatDateLocale(new Date(offer?.offer?._created)); | ||||
| const startExchange = () => { | const startExchange = () => { | ||||
| startChat(chats, offer, userId); | |||||
| startChat(chats, offer?.offer, userId); | |||||
| }; | }; | ||||
| return ( | return ( | ||||
| <ItemDetailsCardContainer | <ItemDetailsCardContainer | ||||
| </Info> | </Info> | ||||
| <PostDate>{date}</PostDate> | <PostDate>{date}</PostDate> | ||||
| </OfferInfo> | </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 && ( | {!props.halfwidth && props.showExchangeButton && ( | ||||
| <CheckButton | <CheckButton | ||||
| variant={props.sponsored ? "contained" : "outlined"} | variant={props.sponsored ? "contained" : "outlined"} |
| //import { IconButton } from "../../Buttons/IconButton/IconButton"; | //import { IconButton } from "../../Buttons/IconButton/IconButton"; | ||||
| import { PrimaryButton } from "../../Buttons/PrimaryButton/PrimaryButton"; | import { PrimaryButton } from "../../Buttons/PrimaryButton/PrimaryButton"; | ||||
| import { Icon } from "../../Icon/Icon"; | import { Icon } from "../../Icon/Icon"; | ||||
| import HorizontalScroller from "../../Scroller/HorizontalScroller"; | |||||
| import { ReactComponent as Category } from "../../../assets/images/svg/category.svg"; | import { ReactComponent as Category } from "../../../assets/images/svg/category.svg"; | ||||
| import { ReactComponent as Subcategory } from "../../../assets/images/svg/subcategory.svg"; | import { ReactComponent as Subcategory } from "../../../assets/images/svg/subcategory.svg"; | ||||
| import { ReactComponent as Quantity } from "../../../assets/images/svg/quantity.svg"; | import { ReactComponent as Quantity } from "../../../assets/images/svg/quantity.svg"; | ||||
| left: 5px; | 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)` | export const OfferAuthor = styled(Box)` | ||||
| display: flex; | display: flex; | ||||
| flex: 1; | flex: 1; | ||||
| line-height: 16px; | line-height: 16px; | ||||
| font-size: 12px; | 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)` | export const OfferCategory = styled(Box)` | ||||
| font-family: "Open Sans"; | font-family: "Open Sans"; | ||||
| color: ${selectedTheme.primaryText}; | color: ${selectedTheme.primaryText}; | ||||
| font-size: 12px; | font-size: 12px; | ||||
| width: 34%; | 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)` | export const OfferDescription = styled(Box)` | ||||
| flex: 3; | flex: 3; | ||||
| `; | `; | ||||
| height: 44px; | 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 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)` | export const PublishButtonContainer = styled(Box)` | ||||
| display: flex; | display: flex; |
| 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", | |||||
| } |
| 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 ( | 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 = { | OfferDetails.propTypes = { | ||||
| offer: PropTypes.any, | |||||
| } | |||||
| offer: PropTypes.any, | |||||
| showExchangeButton: PropTypes.bool, | |||||
| showPublishButton: PropTypes.bool, | |||||
| }; | |||||
| export default OfferDetails | |||||
| export default OfferDetails; |
| 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; | |||||
| } | |||||
| `; |
| const MessageCard = (props) => { | const MessageCard = (props) => { | ||||
| const message = props.message; | const message = props.message; | ||||
| const dateString = formatDateTime(new Date(message._created)) | |||||
| const dateString = formatDateTime(new Date(message._created)); | |||||
| return ( | return ( | ||||
| <MessageCardContainer isMyMessage={props.isMyMessage}> | <MessageCardContainer isMyMessage={props.isMyMessage}> | ||||
| <ProfileImage src={props.image} /> | <ProfileImage src={props.image} /> | ||||
| <MessageContent isMyMessage={props.isMyMessage}> | <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> | </MessageContent> | ||||
| </MessageCardContainer> | </MessageCardContainer> | ||||
| ); | ); |
| 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" | |||||
| }]; |
| ThumbDown, | ThumbDown, | ||||
| ThumbUp, | ThumbUp, | ||||
| } from "./UserReviewsCard.styled"; | } from "./UserReviewsCard.styled"; | ||||
| import { ListItem } from "@mui/material"; | import { ListItem } from "@mui/material"; | ||||
| import selectedTheme from "../../../themes"; | import selectedTheme from "../../../themes"; | ||||
| import { useTranslation } from "react-i18next"; | 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 UserReviewsCard = (props) => { | ||||
| const { t } = useTranslation(); | const { t } = useTranslation(); | ||||
| // const dispatch = useDispatch(); | |||||
| // useEffect(() => { | |||||
| // if (props.review?.userId) { | |||||
| // dispatch(fetchProfile(props.review.userId)); | |||||
| // } | |||||
| // }, [props.review?.userId]) | |||||
| console.log(props); | console.log(props); | ||||
| const review = useMemo(() => { | const review = useMemo(() => { | ||||
| if (props.givingReview) { | if (props.givingReview) { | ||||
| return { | return { | ||||
| ...props.review | |||||
| } | |||||
| ...props.review, | |||||
| }; | |||||
| } | } | ||||
| let isSuccessfulSwap = "DA"; | let isSuccessfulSwap = "DA"; | ||||
| if (props.review.succeeded === "failed") isSuccessfulSwap = "NE"; | if (props.review.succeeded === "failed") isSuccessfulSwap = "NE"; | ||||
| let isGoodCommunication = "DA"; | 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"; | if (props.review.communication === "no") isGoodCommunication = "NE"; | ||||
| return { | return { | ||||
| name: props.review.companyName, | name: props.review.companyName, | ||||
| image: props.review.image, | image: props.review.image, | ||||
| isGoodCommunication, | isGoodCommunication, | ||||
| isSuccessfulSwap, | isSuccessfulSwap, | ||||
| quote: props?.review?.message | |||||
| } | |||||
| quote: props?.review?.message, | |||||
| }; | |||||
| }, [props.review]); | }, [props.review]); | ||||
| const isGood = useMemo(() => { | |||||
| if ( | |||||
| review?.isGoodCommunication === reviewEnum.NO.mainText || | |||||
| review?.isSuccessfulSwap === reviewEnum.NO.mainText | |||||
| ) { | |||||
| return false; | |||||
| } | |||||
| return true; | |||||
| }, [review]); | |||||
| return ( | return ( | ||||
| <ReviewContainer key={review?.image}> | <ReviewContainer key={review?.image}> | ||||
| <ListItem alignItems="flex-start" sx={{ alignItems: "center", mt: 2 }}> | <ListItem alignItems="flex-start" sx={{ alignItems: "center", mt: 2 }}> | ||||
| sx={{ pl: 2, py: 2 }} | sx={{ pl: 2, py: 2 }} | ||||
| > | > | ||||
| <ThumbBox item> | <ThumbBox item> | ||||
| {isGood ? <ThumbUp color="success" /> : <ThumbDown color="error" />} | |||||
| {review.isSuccessfulSwap ? ( | |||||
| <ThumbUp color="success" /> | |||||
| ) : ( | |||||
| <ThumbDown color="error" /> | |||||
| )} | |||||
| </ThumbBox> | </ThumbBox> | ||||
| <ReviewQuoteBox item> | <ReviewQuoteBox item> | ||||
| <ReviewQuoteText | |||||
| sx={{ display: "inline" }} | |||||
| component="span" | |||||
| variant="body2" | |||||
| color="text.primary" | |||||
| > | |||||
| "{review?.quote}" | |||||
| </ReviewQuoteText> | |||||
| <ReviewQuoteText>"{review?.quote}"</ReviewQuoteText> | |||||
| </ReviewQuoteBox> | </ReviewQuoteBox> | ||||
| </ReviewQuote> | </ReviewQuote> | ||||
| <ReviewDetails sx={{ pl: 2, pb: 2 }}> | <ReviewDetails sx={{ pl: 2, pb: 2 }}> |
| import React, { useState, useEffect } from "react"; | |||||
| import React, { useState, useEffect } from "react"; | |||||
| import ChatCard from "../Cards/ChatCard/ChatCard"; | import ChatCard from "../Cards/ChatCard/ChatCard"; | ||||
| import { | import { | ||||
| ChatColumnContainer, | ChatColumnContainer, | ||||
| useEffect(() => { | useEffect(() => { | ||||
| dispatch(fetchChats()); | dispatch(fetchChats()); | ||||
| }, []) | |||||
| }, []); | |||||
| useEffect(() => { | useEffect(() => { | ||||
| setSortOption(sorting.selectedSortOption); | setSortOption(sorting.selectedSortOption); | ||||
| })} | })} | ||||
| </HeaderSelect> | </HeaderSelect> | ||||
| </TitleSortContainer> | </TitleSortContainer> | ||||
| <ListHeader enableSort={true}></ListHeader> | |||||
| <ListHeader enableSort={true} /> | |||||
| <ListContainer> | <ListContainer> | ||||
| {chats.map((item, index) => ( | {chats.map((item, index) => ( | ||||
| <ChatCard key={index} chat={item} /> | <ChatCard key={index} chat={item} /> |
| } from "./CreateReview.styled"; | } from "./CreateReview.styled"; | ||||
| import FirstStepCreateReview from "./FirstStep/FirstStepCreateReview"; | import FirstStepCreateReview from "./FirstStep/FirstStepCreateReview"; | ||||
| import SecondStepCreateReview from "./SecondStep/SecondStepCreateReview"; | import SecondStepCreateReview from "./SecondStep/SecondStepCreateReview"; | ||||
| import ThirdStepCreateReview from "./ThirdStep/ThirdStepCreateReview"; | |||||
| import { useDispatch, useSelector } from "react-redux"; | import { useDispatch, useSelector } from "react-redux"; | ||||
| import { giveReview } from "../../store/actions/review/reviewActions"; | import { giveReview } from "../../store/actions/review/reviewActions"; | ||||
| import { selectUserId } from "../../store/selectors/loginSelectors"; | import { selectUserId } from "../../store/selectors/loginSelectors"; | ||||
| import { reviewEnum } from "../../enums/reviewEnum"; | import { reviewEnum } from "../../enums/reviewEnum"; | ||||
| import ThirdStepCreateReview from "./ThirdStep/ThirdStepCreateReview"; | |||||
| const CreateReview = (props) => { | const CreateReview = (props) => { | ||||
| const offer = props.offer; | const offer = props.offer; | ||||
| console.log("props aaa: ", props); | |||||
| const [informations, setInformations] = useState({}); | const [informations, setInformations] = useState({}); | ||||
| const [currentStep, setCurrentStep] = useState(1); | const [currentStep, setCurrentStep] = useState(1); | ||||
| const dispatch = useDispatch(); | const dispatch = useDispatch(); | ||||
| props.handleGiveReviewSuccess(); | props.handleGiveReviewSuccess(); | ||||
| }; | }; | ||||
| const submitForm = () => { | const submitForm = () => { | ||||
| let communication; | |||||
| if (informations.correctCommunication === reviewEnum.YES.mainText) | |||||
| communication = "yes"; | |||||
| let communication = "yes"; | |||||
| if (informations.correctCommunication === reviewEnum.NO.mainText) | if (informations.correctCommunication === reviewEnum.NO.mainText) | ||||
| communication = "no"; | communication = "no"; | ||||
| if (informations.correctCommunication === reviewEnum.NOT_BAD.mainText) | if (informations.correctCommunication === reviewEnum.NOT_BAD.mainText) | ||||
| communication = "could be better"; | communication = "could be better"; | ||||
| let succeeded; | |||||
| succeeded = "failed"; | |||||
| let succeeded = "failed"; | |||||
| if (informations.exchangeSucceed === reviewEnum.YES.mainText) | if (informations.exchangeSucceed === reviewEnum.YES.mainText) | ||||
| succeeded = "succeeded"; | succeeded = "succeeded"; | ||||
| dispatch( | dispatch( | ||||
| }; | }; | ||||
| const goToNextStep = (newInformations) => { | const goToNextStep = (newInformations) => { | ||||
| setInformations((prevInformations) => { | setInformations((prevInformations) => { | ||||
| console.log({ | |||||
| ...prevInformations, | |||||
| ...newInformations, | |||||
| }); | |||||
| return { | return { | ||||
| ...prevInformations, | ...prevInformations, | ||||
| ...newInformations, | ...newInformations, | ||||
| <CloseButton onClick={closeModal}> | <CloseButton onClick={closeModal}> | ||||
| <CloseIcon /> | <CloseIcon /> | ||||
| </CloseButton> | </CloseButton> | ||||
| {currentStep === 2 ? ( | |||||
| {currentStep === 2 && ( | |||||
| <BackIcon onClick={goToPrevStep}> | <BackIcon onClick={goToPrevStep}> | ||||
| <ArrowBackIcon /> | <ArrowBackIcon /> | ||||
| </BackIcon> | </BackIcon> | ||||
| ) : ( | |||||
| "" | |||||
| )} | )} | ||||
| {currentStep === 1 && ( | {currentStep === 1 && ( | ||||
| <FirstStepCreateReview | <FirstStepCreateReview |
| import DirectChatHeaderTitle from "./DirectChatHeaderTitle/DirectChatHeaderTitle"; | import DirectChatHeaderTitle from "./DirectChatHeaderTitle/DirectChatHeaderTitle"; | ||||
| import DirectChatHeader from "./DirectChatHeader/DirectChatHeader"; | import DirectChatHeader from "./DirectChatHeader/DirectChatHeader"; | ||||
| import { useDispatch, useSelector } from "react-redux"; | import { useDispatch, useSelector } from "react-redux"; | ||||
| // import { fetchOneOffer } from "../../store/actions/offers/offersActions"; | |||||
| import { useLocation, useRouteMatch } from "react-router-dom"; | import { useLocation, useRouteMatch } from "react-router-dom"; | ||||
| import { fetchOneChat } from "../../store/actions/chat/chatActions"; | |||||
| import { fetchOneChat, setOneChat } from "../../store/actions/chat/chatActions"; | |||||
| import { | import { | ||||
| // selectLatestChats, | |||||
| selectSelectedChat, | selectSelectedChat, | ||||
| } from "../../store/selectors/chatSelectors"; | } from "../../store/selectors/chatSelectors"; | ||||
| import DirectChatContent from "./DirectChatContent/DirectChatContent"; | import DirectChatContent from "./DirectChatContent/DirectChatContent"; | ||||
| const offer = useSelector(selectOffer); | const offer = useSelector(selectOffer); | ||||
| const routeMatch = useRouteMatch(); | const routeMatch = useRouteMatch(); | ||||
| const location = useLocation(); | 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(() => { | const offerObject = useMemo(() => { | ||||
| if (location?.state?.offerId) { | if (location?.state?.offerId) { | ||||
| return offer?.offer; | return offer?.offer; | ||||
| } | } | ||||
| return chat?.offer?.offer; | return chat?.offer?.offer; | ||||
| }, [chat, location.state, offer]); | }, [chat, location.state, offer]); | ||||
| const chatObject = useMemo(() => { | const chatObject = useMemo(() => { | ||||
| if (location?.state?.offerId) { | if (location?.state?.offerId) { | ||||
| return {}; | return {}; | ||||
| } | } | ||||
| return chat?.chat; | return chat?.chat; | ||||
| }, [chat, location.state]); | }, [chat, location.state]); | ||||
| const interlocutorObject = useMemo(() => { | const interlocutorObject = useMemo(() => { | ||||
| if (location?.state?.offerId) { | if (location?.state?.offerId) { | ||||
| return { | return { | ||||
| image: offer?.companyData?.image, | image: offer?.companyData?.image, | ||||
| name: offer?.companyData?.company?.name, | name: offer?.companyData?.company?.name, | ||||
| location: offer?.companyData?.company?.contacts?.location | |||||
| } | |||||
| location: offer?.companyData?.company?.contacts?.location, | |||||
| }; | |||||
| } | } | ||||
| return chat?.interlocutor; | 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(() => { | useEffect(() => { | ||||
| console.log(location.state) | |||||
| if (routeMatch.params.idChat && location.state?.offerId) { | |||||
| if (routeMatch.params.idChat) { | |||||
| refreshChat(); | refreshChat(); | ||||
| } | } | ||||
| }, [routeMatch.params.idChat, location.state?.offerId]); | }, [routeMatch.params.idChat, location.state?.offerId]); | ||||
| const refreshChat = () => { | const refreshChat = () => { | ||||
| if (routeMatch.params.idChat === "newMessage") { | if (routeMatch.params.idChat === "newMessage") { | ||||
| dispatch(fetchOneOffer(location.state.offerId)) | |||||
| dispatch(fetchOneOffer(location.state.offerId)); | |||||
| dispatch(setOneChat({})); | |||||
| } else { | } else { | ||||
| dispatch(fetchOneChat(routeMatch.params.idChat)); | dispatch(fetchOneChat(routeMatch.params.idChat)); | ||||
| } | } |
| } | } | ||||
| return false; | return false; | ||||
| }, [exchange, userId]) | }, [exchange, userId]) | ||||
| const refetchExchange = () => { | |||||
| dispatch(fetchExchange(chat.chat.exchangeId)); | |||||
| } | |||||
| const makeReview = () => { | const makeReview = () => { | ||||
| setShowReviewModal(true); | setShowReviewModal(true); | ||||
| }; | }; | ||||
| const handleGiveReviewSuccess = () => { | const handleGiveReviewSuccess = () => { | ||||
| refetchExchange(); | refetchExchange(); | ||||
| } | } | ||||
| const refetchExchange = () => { | |||||
| dispatch(fetchExchange(chat.chat.exchangeId)); | |||||
| } | |||||
| return ( | return ( | ||||
| <DirectChatHeaderContainer> | <DirectChatHeaderContainer> | ||||
| {showReviewModal && ( | {showReviewModal && ( |
| 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 DirectChatHeaderTitle = () => { | ||||
| const {t} = useTranslation(); | |||||
| const { t } = useTranslation(); | |||||
| return ( | return ( | ||||
| <DirectChatHeaderTitleContainer> | <DirectChatHeaderTitleContainer> | ||||
| <MessageIcon /> | |||||
| <HeaderTitleContent>{t("messages.headerTitle")}</HeaderTitleContent> | |||||
| <MessageIcon /> | |||||
| <HeaderTitleContent>{t("messages.headerTitle")}</HeaderTitleContent> | |||||
| </DirectChatHeaderTitleContainer> | </DirectChatHeaderTitleContainer> | ||||
| ) | |||||
| } | |||||
| ); | |||||
| }; | |||||
| DirectChatHeaderTitle.propTypes = { | DirectChatHeaderTitle.propTypes = { | ||||
| children: PropTypes.node, | |||||
| } | |||||
| children: PropTypes.node, | |||||
| }; | |||||
| export default DirectChatHeaderTitle | |||||
| export default DirectChatHeaderTitle; |
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import selectedTheme from "../../../themes"; | import selectedTheme from "../../../themes"; | ||||
| import { useDispatch } from "react-redux"; | 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"; | import { useHistory, useLocation } from "react-router-dom"; | ||||
| const DirectChatNewMessage = (props) => { | const DirectChatNewMessage = (props) => { | ||||
| setTypedValue(""); | setTypedValue(""); | ||||
| }; | }; | ||||
| const handleMessageSendSuccess = (newChatId) => { | const handleMessageSendSuccess = (newChatId) => { | ||||
| console.log("NEW CHAT ID: ", newChatId); | |||||
| history.replace(`${newChatId}`); | history.replace(`${newChatId}`); | ||||
| } | |||||
| }; | |||||
| const initiateNewChat = (typedValue) => { | const initiateNewChat = (typedValue) => { | ||||
| const offerId = location.state.offerId; | const offerId = location.state.offerId; | ||||
| dispatch(startNewChat({offerId, message: typedValue, handleMessageSendSuccess})) | |||||
| } | |||||
| dispatch( | |||||
| startNewChat({ offerId, message: typedValue, handleMessageSendSuccess }) | |||||
| ); | |||||
| }; | |||||
| return ( | return ( | ||||
| <DirectChatNewMessageContainer> | <DirectChatNewMessageContainer> | ||||
| <NewMessageField | <NewMessageField |
| import React, { useEffect, useMemo, useState } from "react"; | |||||
| import React, { useEffect, useMemo } from "react"; | |||||
| import PropTypes from "prop-types"; | import PropTypes from "prop-types"; | ||||
| import { MiniChatColumnContainer } from "./MiniChatColumn.styled"; | import { MiniChatColumnContainer } from "./MiniChatColumn.styled"; | ||||
| import MiniChatCard from "../../Cards/MiniChatCard/MiniChatCard"; | import MiniChatCard from "../../Cards/MiniChatCard/MiniChatCard"; | ||||
| const MiniChatColumn = () => { | const MiniChatColumn = () => { | ||||
| const chats = useSelector(selectLatestChats); | const chats = useSelector(selectLatestChats); | ||||
| const [chatsToShow, setChatsToShow] = useState([]); | |||||
| const selectedChat = useSelector(selectSelectedChat); | const selectedChat = useSelector(selectSelectedChat); | ||||
| const [isThereNewChat, setIsThereNewChat] = useState(false); | |||||
| const offer = useSelector(selectOffer); | const offer = useSelector(selectOffer); | ||||
| const location = useLocation(); | const location = useLocation(); | ||||
| const dispatch = useDispatch(); | const dispatch = useDispatch(); | ||||
| return { | return { | ||||
| interlocutorData: { | interlocutorData: { | ||||
| image: offer?.companyData?.image, | image: offer?.companyData?.image, | ||||
| name: offer?.companyData?.company?.name | |||||
| name: offer?.companyData?.company?.name, | |||||
| }, | }, | ||||
| offerData: { | 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(() => { | useEffect(() => { | ||||
| dispatch(fetchChats()); | dispatch(fetchChats()); | ||||
| return ( | return ( | ||||
| <MiniChatColumnContainer> | <MiniChatColumnContainer> | ||||
| <MiniChatColumnHeader /> | <MiniChatColumnHeader /> | ||||
| {isThereNewChat && ( | |||||
| <MiniChatCard | |||||
| chat={newChat} | |||||
| selected | |||||
| /> | |||||
| )} | |||||
| {chatsToShow.map((item) => { | |||||
| {location.state?.offerId && <MiniChatCard chat={newChat} selected />} | |||||
| {chats.map((item) => { | |||||
| return ( | 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> | </MiniChatColumnContainer> | ||||
| ); | ); | ||||
| }; | }; |
| 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 MiniChatColumnHeader = () => { | ||||
| const {t} = useTranslation(); | |||||
| const { t } = useTranslation(); | |||||
| return ( | return ( | ||||
| <MiniChatColumnHeaderContainer> | <MiniChatColumnHeaderContainer> | ||||
| <MailIcon/> | |||||
| <HeaderTitleContent>{t("messages.miniChatHeaderTitle")}</HeaderTitleContent> | |||||
| <MailIcon /> | |||||
| <HeaderTitleContent> | |||||
| {t("messages.miniChatHeaderTitle")} | |||||
| </HeaderTitleContent> | |||||
| </MiniChatColumnHeaderContainer> | </MiniChatColumnHeaderContainer> | ||||
| ) | |||||
| } | |||||
| ); | |||||
| }; | |||||
| MiniChatColumnHeader.propTypes = { | MiniChatColumnHeader.propTypes = { | ||||
| children: PropTypes.node, | |||||
| } | |||||
| children: PropTypes.node, | |||||
| }; | |||||
| export default MiniChatColumnHeader | |||||
| export default MiniChatColumnHeader; |
| const DropdownList = (props) => { | const DropdownList = (props) => { | ||||
| const [listShown, setListShown] = useState(props.defaultOpen); | const [listShown, setListShown] = useState(props.defaultOpen); | ||||
| useEffect(() => { | useEffect(() => { | ||||
| if (props.open !== null || props.open !== undefined) { | |||||
| if (props.open !== null || props.open !== undefined) | |||||
| setListShown(props.open); | setListShown(props.open); | ||||
| } | |||||
| }, [props.open]); | }, [props.open]); | ||||
| const handleShow = () => { | const handleShow = () => { | ||||
| if (props.setIsOpened) { | |||||
| if (props.setIsOpened) | |||||
| props.setIsOpened(!listShown); | props.setIsOpened(!listShown); | ||||
| } | |||||
| if (!props.disabled) { | |||||
| if (!props.disabled) | |||||
| setListShown((prevState) => !prevState); | |||||
| if (!(props.open !== null || props.open !== undefined)) | |||||
| setListShown((prevState) => !prevState); | setListShown((prevState) => !prevState); | ||||
| } | |||||
| if (!(props.open !== null || props.open !== undefined)) { | |||||
| setListShown(prevState => !prevState) | |||||
| } | |||||
| }; | }; | ||||
| return ( | return ( | ||||
| <DropdownListContainer fullwidth={props.fullWidth ? 1 : 0}> | <DropdownListContainer fullwidth={props.fullWidth ? 1 : 0}> | ||||
| > | > | ||||
| {props.title} | {props.title} | ||||
| </DropdownTitle> | </DropdownTitle> | ||||
| {(props.open !== null && props.open !== undefined ? props.open : listShown) ? ( | |||||
| {( | |||||
| props.open !== null && props.open !== undefined | |||||
| ? props.open | |||||
| : listShown | |||||
| ) ? ( | |||||
| <ToggleIconOpened | <ToggleIconOpened | ||||
| style={props.toggleIconStyles} | style={props.toggleIconStyles} | ||||
| onClick={!props.disabled ? () => handleShow() : () => {}} | onClick={!props.disabled ? () => handleShow() : () => {}} | ||||
| </ToggleIconClosed> | </ToggleIconClosed> | ||||
| )} | )} | ||||
| </DropdownHeader> | </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> | <DropdownOptions>{props.headerOptions}</DropdownOptions> | ||||
| <ListContainer>{props.children}</ListContainer> | <ListContainer>{props.children}</ListContainer> | ||||
| </ToggleContainer> | </ToggleContainer> | ||||
| </DropdownListContainer> | </DropdownListContainer> | ||||
| ); | ); | ||||
| }; | }; | ||||
| export default DropdownList; | export default DropdownList; | ||||
| DropdownList.propTypes = { | DropdownList.propTypes = { | ||||
| title: PropTypes.string, | title: PropTypes.string, | ||||
| dropdownIcon: PropTypes.node, | dropdownIcon: PropTypes.node, | ||||
| open: PropTypes.bool, | open: PropTypes.bool, | ||||
| disabled: PropTypes.bool, | disabled: PropTypes.bool, | ||||
| }; | }; | ||||
| DropdownList.defaultProps = { | DropdownList.defaultProps = { | ||||
| fullWidth: false, | fullWidth: false, | ||||
| defaultOpen: false, | defaultOpen: false, |
| ToolsContainer, | ToolsContainer, | ||||
| UserIcon, | UserIcon, | ||||
| } from "./Drawer.styled"; | } from "./Drawer.styled"; | ||||
| import { useSelector } from "react-redux"; | |||||
| import { useDispatch, useSelector } from "react-redux"; | |||||
| import { selectUserId } from "../../../store/selectors/loginSelectors"; | import { selectUserId } from "../../../store/selectors/loginSelectors"; | ||||
| import selectedTheme from "../../../themes"; | import selectedTheme from "../../../themes"; | ||||
| import { IconButton } from "../../Buttons/IconButton/IconButton"; | import { IconButton } from "../../Buttons/IconButton/IconButton"; | ||||
| import { useHistory } from "react-router-dom"; | import { useHistory } from "react-router-dom"; | ||||
| import { CHAT_PAGE, LOGIN_PAGE, MY_OFFERS_PAGE, REGISTER_PAGE } from "../../../constants/pages"; | import { CHAT_PAGE, LOGIN_PAGE, MY_OFFERS_PAGE, REGISTER_PAGE } from "../../../constants/pages"; | ||||
| import { selectProfileName } from "../../../store/selectors/profileSelectors"; | import { selectProfileName } from "../../../store/selectors/profileSelectors"; | ||||
| import { logoutUser } from "../../../store/actions/login/loginActions"; | |||||
| export const Drawer = (props) => { | export const Drawer = (props) => { | ||||
| const user = useSelector(selectUserId); | const user = useSelector(selectUserId); | ||||
| const { t } = useTranslation(); | const { t } = useTranslation(); | ||||
| const history = useHistory(); | const history = useHistory(); | ||||
| const name = useSelector(selectProfileName); | const name = useSelector(selectProfileName); | ||||
| const dispatch = useDispatch(); | |||||
| const goToMyPosts = () => { | const goToMyPosts = () => { | ||||
| props.toggleDrawer(); | props.toggleDrawer(); | ||||
| props.toggleDrawer(); | props.toggleDrawer(); | ||||
| props.addOffer(); | props.addOffer(); | ||||
| } | } | ||||
| const logoutUser = () => {}; | |||||
| const logout = () => { | |||||
| props.toggleDrawer(); | |||||
| dispatch(logoutUser()); | |||||
| }; | |||||
| return ( | return ( | ||||
| <DrawerContainer> | <DrawerContainer> | ||||
| <CloseButton onClick={props.toggleDrawer}> | <CloseButton onClick={props.toggleDrawer}> | ||||
| {t("header.addOffer")} | {t("header.addOffer")} | ||||
| </AddOfferButton> | </AddOfferButton> | ||||
| <LogoutButton> | <LogoutButton> | ||||
| <IconButton onClick={logoutUser}> | |||||
| <IconButton onClick={logout}> | |||||
| <LogoutIcon /> | <LogoutIcon /> | ||||
| </IconButton> | </IconButton> | ||||
| <LogoutText>{t("common.logout")}</LogoutText> | <LogoutText>{t("common.logout")}</LogoutText> |
| import React, { useEffect, useRef, useState } from "react"; | |||||
| import React, { useCallback, useEffect, useRef, useState } from "react"; | |||||
| import PropTypes from "prop-types"; | import PropTypes from "prop-types"; | ||||
| import { | import { | ||||
| AddFile, | AddFile, | ||||
| import { ReactComponent as EditIcon } from "../../assets/images/svg/edit.svg"; | import { ReactComponent as EditIcon } from "../../assets/images/svg/edit.svg"; | ||||
| import { ReactComponent as TrashIcon } from "../../assets/images/svg/trash.svg"; | import { ReactComponent as TrashIcon } from "../../assets/images/svg/trash.svg"; | ||||
| // import { Input } from "@mui/material"; | |||||
| const ImagePicker = (props) => { | const ImagePicker = (props) => { | ||||
| const fileInputRef = useRef(null); | const fileInputRef = useRef(null); | ||||
| const imageRef = useRef(null); | const imageRef = useRef(null); | ||||
| const [isEditing, setIsEditing] = useState(false); | const [isEditing, setIsEditing] = useState(false); | ||||
| useEffect(() => { | useEffect(() => { | ||||
| console.log("image", props); | |||||
| if (props.image) { | |||||
| if (props.image) | |||||
| setImage(props.image); | setImage(props.image); | ||||
| } | |||||
| }, [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); | window.addEventListener("click", listener); | ||||
| return () => window.removeEventListener("click", listener); | return () => window.removeEventListener("click", listener); | ||||
| }, [imageRef]); | |||||
| }, []); | |||||
| const handleChange = () => { | const handleChange = () => { | ||||
| fileInputRef.current.value = ""; | fileInputRef.current.value = ""; | ||||
| fileInputRef.current.click(); | fileInputRef.current.click(); | ||||
| console.log(error); | console.log(error); | ||||
| }; | }; | ||||
| }; | }; | ||||
| const handleDelete = () => { | const handleDelete = () => { | ||||
| if (props.deleteImage) props.deleteImage(); | if (props.deleteImage) props.deleteImage(); | ||||
| setImage(""); | setImage(""); | ||||
| </ImagePickerContainer> | </ImagePickerContainer> | ||||
| ); | ); | ||||
| }; | }; | ||||
| ImagePicker.propTypes = { | ImagePicker.propTypes = { | ||||
| children: PropTypes.node, | children: PropTypes.node, | ||||
| className: PropTypes.string, | className: PropTypes.string, | ||||
| deleteImage: PropTypes.func, | deleteImage: PropTypes.func, | ||||
| showDeleteIcon: PropTypes.bool, | showDeleteIcon: PropTypes.bool, | ||||
| }; | }; | ||||
| export default ImagePicker; | export default ImagePicker; |
| 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; |
| 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; |
| 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; |
| 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; |
| 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; |
| 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; |
| 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; |
| 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; |
| 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; |
| 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; |
| 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; |
| 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; |
| 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; |
| import React from "react"; | import React from "react"; | ||||
| import PropTypes from "prop-types"; | import PropTypes from "prop-types"; | ||||
| import { useHistory } from "react-router-dom"; | import { useHistory } from "react-router-dom"; | ||||
| //import { IconButton } from "../../Buttons/IconButton/IconButton"; | |||||
| import { HeaderContainer, HeaderText, ButtonContainer } from "./Header.styled"; | import { HeaderContainer, HeaderText, ButtonContainer } from "./Header.styled"; | ||||
| import { ArrowButton } from "../../Buttons/ArrowButton/ArrowButton"; | import { ArrowButton } from "../../Buttons/ArrowButton/ArrowButton"; | ||||
| // const DownArrow = (props) => ( | |||||
| // <IconStyled {...props}> | |||||
| // <Down /> | |||||
| // </IconStyled> | |||||
| // ); | |||||
| import { useTranslation } from "react-i18next"; | |||||
| const Header = (props) => { | const Header = (props) => { | ||||
| const history = useHistory(); | const history = useHistory(); | ||||
| const {t} = useTranslation(); | |||||
| const handleBackButton = () => { | const handleBackButton = () => { | ||||
| history.goBack(); | history.goBack(); | ||||
| }; | }; | ||||
| return ( | return ( | ||||
| <HeaderContainer onClick={handleBackButton} component="header" className={props.className}> | |||||
| <HeaderContainer | |||||
| onClick={handleBackButton} | |||||
| component="header" | |||||
| className={props.className} | |||||
| > | |||||
| <ButtonContainer> | <ButtonContainer> | ||||
| <ArrowButton side={"left"}></ArrowButton> | <ArrowButton side={"left"}></ArrowButton> | ||||
| <HeaderText>Nazad na objave</HeaderText> | |||||
| <HeaderText>{t("itemDetailsCard.headerTitle")}</HeaderText> | |||||
| </ButtonContainer> | </ButtonContainer> | ||||
| </HeaderContainer> | </HeaderContainer> | ||||
| ); | ); |
| import React, { useMemo } from 'react'; | |||||
| import React, { useMemo } from "react"; | |||||
| import Header from "./Header/Header"; | import Header from "./Header/Header"; | ||||
| import { useSelector } from "react-redux"; | import { useSelector } from "react-redux"; | ||||
| import { ItemDetailsContainer } from "./ItemDetails.styled"; | import { ItemDetailsContainer } from "./ItemDetails.styled"; | ||||
| import ItemDetailsCard from "../Cards/ItemDetailsCard/ItemDetailsCard"; | import ItemDetailsCard from "../Cards/ItemDetailsCard/ItemDetailsCard"; | ||||
| import ItemDetailsHeaderCard from "./ItemDetailsHeaderCard/ItemDetailsHeaderCard"; | 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'; | // import { useHistory } from 'react-router-dom'; | ||||
| const ItemDetails = () => { | 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; | export default ItemDetails; |
| import React from "react"; | import React from "react"; | ||||
| import PropTypes from "prop-types"; | import PropTypes from "prop-types"; | ||||
| import { | import { | ||||
| DetailIcon, | |||||
| DetailText, | |||||
| MessageIcon, | MessageIcon, | ||||
| OfferDetails, | OfferDetails, | ||||
| OfferImage, | OfferImage, | ||||
| OfferTitle, | OfferTitle, | ||||
| DetailContainer, | |||||
| HeaderTop, | HeaderTop, | ||||
| HeaderDetails, | |||||
| BottomDetails, | |||||
| StatusText, | |||||
| PIBIcon, | |||||
| UserIcon, | UserIcon, | ||||
| UserIconContainer, | UserIconContainer, | ||||
| } from "./ItemDetailsHeaderCard.styled"; | } from "./ItemDetailsHeaderCard.styled"; | ||||
| import { ItemDetailsHeaderContainer } 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 { ReactComponent as MessageColor } from "../../../assets/images/svg/mailColor.svg"; | ||||
| import selectedTheme from "../../../themes"; | |||||
| import { useHistory } from "react-router-dom"; | import { useHistory } from "react-router-dom"; | ||||
| import { useSelector } from "react-redux"; | import { useSelector } from "react-redux"; | ||||
| import { selectLatestChats } from "../../../store/selectors/chatSelectors"; | import { selectLatestChats } from "../../../store/selectors/chatSelectors"; | ||||
| import { selectUserId } from "../../../store/selectors/loginSelectors"; | 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 ItemDetailsHeaderCard = (props) => { | ||||
| const history = useHistory(); | const history = useHistory(); | ||||
| const chats = useSelector(selectLatestChats); | const chats = useSelector(selectLatestChats); | ||||
| const offer = props.offer; | const offer = props.offer; | ||||
| const userId = useSelector(selectUserId); | 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 = () => { | const handleGoProfile = () => { | ||||
| history.push(`/profile/${offer?.offer?.userId}`); | history.push(`/profile/${offer?.offer?.userId}`); | ||||
| }; | }; | ||||
| <OfferTitle isMyProfile={props.isMyProfile} onClick={handleGoProfile}> | <OfferTitle isMyProfile={props.isMyProfile} onClick={handleGoProfile}> | ||||
| {offer?.companyData?.company?.name} | {offer?.companyData?.company?.name} | ||||
| </OfferTitle> | </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> | </OfferDetails> | ||||
| {props.isMyProfile ? ( | {props.isMyProfile ? ( | ||||
| <UserIconContainer onClick={handleGoProfile}> | <UserIconContainer onClick={handleGoProfile}> | ||||
| </MessageIcon> | </MessageIcon> | ||||
| )} | )} | ||||
| </HeaderTop> | </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> | </ItemDetailsHeaderContainer> | ||||
| ); | ); | ||||
| }; | }; | ||||
| sponsored: PropTypes.bool, | sponsored: PropTypes.bool, | ||||
| offer: PropTypes.any, | offer: PropTypes.any, | ||||
| isMyProfile: PropTypes.bool, | 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 = { | ItemDetailsHeaderCard.defaultProps = { | ||||
| halfwidth: false, | halfwidth: false, |
| import { Box, Grid, Typography } from "@mui/material"; | |||||
| import { Box, Typography } from "@mui/material"; | |||||
| import styled from "styled-components"; | import styled from "styled-components"; | ||||
| import selectedTheme from "../../../themes"; | import selectedTheme from "../../../themes"; | ||||
| import { IconButton } from "../../Buttons/IconButton/IconButton"; | import { IconButton } from "../../Buttons/IconButton/IconButton"; | ||||
| import { PrimaryButton } from "../../Buttons/PrimaryButton/PrimaryButton"; | import { PrimaryButton } from "../../Buttons/PrimaryButton/PrimaryButton"; | ||||
| import { Icon } from "../../Icon/Icon"; | |||||
| import { ReactComponent as User} from "../../../assets/images/svg/user.svg"; | import { ReactComponent as User} from "../../../assets/images/svg/user.svg"; | ||||
| max-width: 2000px; | max-width: 2000px; | ||||
| position: relative; | 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)` | export const HeaderTop = styled(Box)` | ||||
| display: flex; | display: flex; | ||||
| flex-direction: row; | flex-direction: row; | ||||
| padding: 18px; | padding: 18px; | ||||
| gap: 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` | export const OfferImage = styled.img` | ||||
| border-radius: 50%; | border-radius: 50%; | ||||
| width: 144px; | width: 144px; | ||||
| `; | `; | ||||
| export const StatusText = styled(Grid)` | |||||
| font-family: "Open Sans"; | |||||
| color: ${selectedTheme.primaryText}; | |||||
| @media (max-width: 600px) { | |||||
| font-size: 12px; | |||||
| } | |||||
| `; | |||||
| export const OfferCategory = styled(Box)` | export const OfferCategory = styled(Box)` | ||||
| font-family: "Open Sans"; | font-family: "Open Sans"; | ||||
| color: ${selectedTheme.primaryText}; | color: ${selectedTheme.primaryText}; | ||||
| width: 0; | width: 0; | ||||
| margin: auto 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)` | export const CheckButton = styled(PrimaryButton)` | ||||
| width: 180px; | width: 180px; | ||||
| height: 48px; | height: 48px; | ||||
| } | } | ||||
| } | } | ||||
| `; | `; | ||||
| 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)` | export const UserIconContainer = styled(MessageIcon)` | ||||
| background-color: ${selectedTheme.primaryIconBackgroundColor}; | background-color: ${selectedTheme.primaryIconBackgroundColor}; | ||||
| ` | ` |
| 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; |
| 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; | |||||
| } | |||||
| `; |
| 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; |
| 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; | |||||
| } | |||||
| } | |||||
| `; |
| 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; |
| 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; | |||||
| ` |
| 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", | |||||
| } |
| 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; |
| 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; |
| 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; | |||||
| ` |
| 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; |
| 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; |
| 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; | |||||
| `; |
| 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; |
| 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; | |||||
| `; |
| 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; |
| 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; |
| 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; |
| 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; |
| 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; | |||||
| `; |
| 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; |
| 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; |
| 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; | |||||
| } | |||||
| `; |
| 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; |
| 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; | |||||
| `; |
| 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; |
| 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 = { | 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; | export default DrawerComponent; |
| 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; |
| 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; |
| 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; |
| 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; |
| 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; |
| 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; |
| 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; |
| import useSorting from "../../../hooks/useSorting"; | import useSorting from "../../../hooks/useSorting"; | ||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import { Tooltip } from "@mui/material"; | import { Tooltip } from "@mui/material"; | ||||
| import { | |||||
| ALL_CATEGORIES, | |||||
| COMMA, | |||||
| SPREAD, | |||||
| } from "../../../constants/marketplaceHeaderTitle"; | |||||
| const DownArrow = (props) => ( | const DownArrow = (props) => ( | ||||
| <IconStyled {...props}> | <IconStyled {...props}> | ||||
| const sorting = useSorting(); | const sorting = useSorting(); | ||||
| const { t } = useTranslation(); | const { t } = useTranslation(); | ||||
| const [sortOption, setSortOption] = useState(sortEnum.INITIAL); | 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(() => { | useEffect(() => { | ||||
| setSortOption(sorting.selectedSortOption); | setSortOption(sorting.selectedSortOption); | ||||
| }, [sorting.selectedSortOption]); | }, [sorting.selectedSortOption]); | ||||
| // Changing header string on refresh or on load | |||||
| useEffect(() => { | useEffect(() => { | ||||
| let headerStringLocal = ALL_CATEGORIES; | |||||
| if (filters.isApplied) { | if (filters.isApplied) { | ||||
| let headerStringLocal = "Sve kategorije"; | |||||
| // Adding category to header string | |||||
| if (filters.selectedCategory?.name) { | if (filters.selectedCategory?.name) { | ||||
| headerStringLocal = filters.selectedCategory.name; | headerStringLocal = filters.selectedCategory.name; | ||||
| // Adding subcategories to header string | |||||
| if (filters.selectedSubcategory?.name) { | 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) { | if (filters.selectedLocations && filters.selectedLocations?.length > 0) { | ||||
| headerStringLocal += " | "; | |||||
| headerStringLocal += SPREAD; | |||||
| filters.selectedLocations.forEach((location, index) => { | filters.selectedLocations.forEach((location, index) => { | ||||
| // Checking if item is last | |||||
| if (index + 1 === filters.selectedLocations.length) { | if (index + 1 === filters.selectedLocations.length) { | ||||
| headerStringLocal += location.city; | headerStringLocal += location.city; | ||||
| } else { | } else { | ||||
| headerStringLocal += location.city + ", "; | |||||
| headerStringLocal += location.city + COMMA; | |||||
| } | } | ||||
| }); | }); | ||||
| } | } | ||||
| setHeaderString(headerStringLocal); | |||||
| } | } | ||||
| setHeaderString(headerStringLocal); | |||||
| }, [ | }, [ | ||||
| filters.isApplied, | |||||
| filters.selectedCategory, | filters.selectedCategory, | ||||
| filters.selectedLocations, | |||||
| filters.selectedSubcategory, | filters.selectedSubcategory, | ||||
| filters.isApplied, | |||||
| filters.selectedLocations, | |||||
| ]); | ]); | ||||
| const handleChangeSelect = (event) => { | const handleChangeSelect = (event) => { | ||||
| return ( | return ( | ||||
| <HeaderContainer> | <HeaderContainer> | ||||
| {/* Setting appropriate header title if page is market place or my offers */} | |||||
| <Tooltip title={headerString}> | <Tooltip title={headerString}> | ||||
| {props.myOffers !== true ? ( | |||||
| {!props.myOffers ? ( | |||||
| headerString === "Sve kategorije" && | headerString === "Sve kategorije" && | ||||
| (sorting.selectedSortOption === sortEnum.INITIAL || | (sorting.selectedSortOption === sortEnum.INITIAL || | ||||
| sorting.selectedSortOption === sortEnum.NEW) ? ( | sorting.selectedSortOption === sortEnum.NEW) ? ( | ||||
| <HeaderLocation>{headerString}</HeaderLocation> | <HeaderLocation>{headerString}</HeaderLocation> | ||||
| ) | ) | ||||
| ) : ( | ) : ( | ||||
| <MySwapsTitle> <RefreshIcon /> {t("header.myOffers")}</MySwapsTitle> | |||||
| <MySwapsTitle> | |||||
| <RefreshIcon /> {t("header.myOffers")} | |||||
| </MySwapsTitle> | |||||
| )} | )} | ||||
| </Tooltip> | </Tooltip> | ||||
| {/* ^^^^^^ */} | |||||
| <HeaderOptions> | <HeaderOptions> | ||||
| <HeaderButtons> | <HeaderButtons> | ||||
| {/* Setting display of offer cards to full width */} | |||||
| <HeaderButton | <HeaderButton | ||||
| iconColor={ | iconColor={ | ||||
| props.isGrid | props.isGrid | ||||
| > | > | ||||
| <GridLine /> | <GridLine /> | ||||
| </HeaderButton> | </HeaderButton> | ||||
| {/* ^^^^^^ */} | |||||
| {/* Setting display of offer cards to half width (Grid) */} | |||||
| <HeaderButton | <HeaderButton | ||||
| iconColor={ | iconColor={ | ||||
| props.isGrid | props.isGrid | ||||
| > | > | ||||
| <GridSquare /> | <GridSquare /> | ||||
| </HeaderButton> | </HeaderButton> | ||||
| {/* ^^^^^^ */} | |||||
| </HeaderButtons> | </HeaderButtons> | ||||
| {/* Select option to choose sorting */} | |||||
| <HeaderSelect | <HeaderSelect | ||||
| value={sortOption?.value ? sortOption.value : sortEnum.INITIAL.value} | value={sortOption?.value ? sortOption.value : sortEnum.INITIAL.value} | ||||
| IconComponent={DownArrow} | IconComponent={DownArrow} | ||||
| ); | ); | ||||
| })} | })} | ||||
| </HeaderSelect> | </HeaderSelect> | ||||
| {/* ^^^^^^ */} | |||||
| </HeaderOptions> | </HeaderOptions> | ||||
| </HeaderContainer> | </HeaderContainer> | ||||
| ); | ); |
| children: PropTypes.node, | children: PropTypes.node, | ||||
| myOffers: PropTypes.bool, | myOffers: PropTypes.bool, | ||||
| }; | }; | ||||
| // MarketPlace.defaultProps = { | |||||
| // myOffers: false, | |||||
| // } | |||||
| export default MarketPlace; | export default MarketPlace; |
| import React, { useEffect, useMemo, useRef, useState } from "react"; | |||||
| import React, { useRef } from "react"; | |||||
| import PropTypes from "prop-types"; | import PropTypes from "prop-types"; | ||||
| import { OffersContainer } from "./Offers.styled"; | import { OffersContainer } from "./Offers.styled"; | ||||
| import OfferCard from "../../Cards/OfferCard/OfferCard"; | 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 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 { selectLatestChats } from "../../../store/selectors/chatSelectors"; | ||||
| import { fetchChats } from "../../../store/actions/chat/chatActions"; | |||||
| import { selectUserId } from "../../../store/selectors/loginSelectors"; | import { selectUserId } from "../../../store/selectors/loginSelectors"; | ||||
| import { startChat } from "../../../util/helpers/chatHelper"; | |||||
| import useOffers from "../../../hooks/useOffers"; | |||||
| const Offers = (props) => { | 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 chats = useSelector(selectLatestChats); | ||||
| const total = useSelector(selectTotalOffers); | |||||
| const history = useHistory(); | |||||
| const dispatch = useDispatch(); | |||||
| const offersRef = useRef(null); | const offersRef = useRef(null); | ||||
| const queryStringHook = useQueryString(props.myOffers); | |||||
| const userId = useSelector(selectUserId); | 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 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 ( | return ( | ||||
| <OffersContainer ref={offersRef}> | <OffersContainer ref={offersRef}> | ||||
| {allOffersToShow.map((item) => { | |||||
| {offers.allOffersToShow.map((item) => { | |||||
| return ( | 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 | <Paging | ||||
| totalElements={totalOffers} | |||||
| totalElements={offers.totalOffers} | |||||
| elementsPerPage={10} | elementsPerPage={10} | ||||
| current={page} | |||||
| changePage={handleDifferentPage} | |||||
| current={offers.page} | |||||
| changePage={offers.handleDifferentPage} | |||||
| /> | /> | ||||
| </OffersContainer> | </OffersContainer> | ||||
| ); | ); |
| } from "./Paging.styled"; | } from "./Paging.styled"; | ||||
| const Paging = (props) => { | const Paging = (props) => { | ||||
| // Determining total pages | |||||
| const pages = props.pages | const pages = props.pages | ||||
| ? props.pages | ? props.pages | ||||
| : props.totalElements | : props.totalElements | ||||
| ? Math.ceil(props.totalElements / props.elementsPerPage) | ? Math.ceil(props.totalElements / props.elementsPerPage) | ||||
| : 1; | : 1; | ||||
| let moving = 0; | let moving = 0; | ||||
| // Making array of pages which contains 2 pages before and after current page | |||||
| const pagesAsArray = Array.apply(null, Array(5)).map(() => {}); | 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 threeDotsBefore = props.current - 2 > 1; | ||||
| const threeDotsAfter = props.current + 2 < pages; | const threeDotsAfter = props.current + 2 < pages; | ||||
| return ( | return ( | ||||
| <PagingContainer> | <PagingContainer> | ||||
| {/* Left arrow */} | |||||
| <Arrow | <Arrow | ||||
| onClick={() => props.changePage(props.current - 1)} | onClick={() => props.changePage(props.current - 1)} | ||||
| disabled={props.current - 1 < 1} | disabled={props.current - 1 < 1} | ||||
| > | > | ||||
| <ArrowIcon side="left" /> | <ArrowIcon side="left" /> | ||||
| </Arrow> | </Arrow> | ||||
| {threeDotsBefore && ( | {threeDotsBefore && ( | ||||
| <React.Fragment> | <React.Fragment> | ||||
| <PageNumber onClick={() => props.changePage(1)}>1</PageNumber> | <PageNumber onClick={() => props.changePage(1)}>1</PageNumber> | ||||
| {props.current - 3 !== 1 && <ThreeDots>...</ThreeDots>} | {props.current - 3 !== 1 && <ThreeDots>...</ThreeDots>} | ||||
| </React.Fragment> | </React.Fragment> | ||||
| )} | )} | ||||
| {/* Pages */} | |||||
| {pagesAsArray.map((item, index) => { | {pagesAsArray.map((item, index) => { | ||||
| const pageNum = props.current - 2 + moving++; | const pageNum = props.current - 2 + moving++; | ||||
| if (pageNum > pages ) return; | |||||
| if (pageNum > pages) return; | |||||
| if (pageNum < 1) return; | if (pageNum < 1) return; | ||||
| return ( | return ( | ||||
| <PageNumber | <PageNumber | ||||
| </PageNumber> | </PageNumber> | ||||
| ); | ); | ||||
| })} | })} | ||||
| {threeDotsAfter && ( | {threeDotsAfter && ( | ||||
| <React.Fragment> | <React.Fragment> | ||||
| {props.current + 3 !== pages && <ThreeDots>...</ThreeDots>} | {props.current + 3 !== pages && <ThreeDots>...</ThreeDots>} | ||||
| </PageNumber> | </PageNumber> | ||||
| </React.Fragment> | </React.Fragment> | ||||
| )} | )} | ||||
| {/* Right arrow */} | |||||
| <Arrow | <Arrow | ||||
| onClick={() => props.changePage(props.current + 1)} | onClick={() => props.changePage(props.current + 1)} | ||||
| disabled={props.current + 1 > pages} | disabled={props.current + 1 > pages} |
| PopoverNoItemsText, | PopoverNoItemsText, | ||||
| PopoverTitle, | PopoverTitle, | ||||
| } from "./HeaderPopover.styled"; | } from "./HeaderPopover.styled"; | ||||
| import { useTranslation } from "react-i18next"; | |||||
| const HeaderPopover = (props) => { | const HeaderPopover = (props) => { | ||||
| const { t } = useTranslation(); | |||||
| return ( | return ( | ||||
| <HeaderPopoverContainer> | <HeaderPopoverContainer> | ||||
| <PopoverTitle p={2}>{props.title}</PopoverTitle> | <PopoverTitle p={2}>{props.title}</PopoverTitle> | ||||
| <PopoverListItem key={index}> | <PopoverListItem key={index}> | ||||
| <PopoverListItemAvatarContainer> | <PopoverListItemAvatarContainer> | ||||
| {props.isProfile ? ( | {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> | </PopoverListItemAvatarContainer> | ||||
| <PopoverListItemTextContainer | <PopoverListItemTextContainer | ||||
| primaryTypographyProps={{ | primaryTypographyProps={{ | ||||
| onClick: item.onClick | |||||
| onClick: item.onClick, | |||||
| }} | }} | ||||
| primary={item.title} | primary={item.title} | ||||
| secondary={item.text} | secondary={item.text} | ||||
| </PopoverListItem> | </PopoverListItem> | ||||
| )) | )) | ||||
| ) : ( | ) : ( | ||||
| <PopoverNoItemsText>No items at the moment...</PopoverNoItemsText> | |||||
| <PopoverNoItemsText>{t("header.noItems")}</PopoverNoItemsText> | |||||
| )} | )} | ||||
| </PopoverList> | </PopoverList> | ||||
| <PopoverButtonsContainer> | <PopoverButtonsContainer> |