| @@ -0,0 +1,4 @@ | |||
| <svg width="72" height="72" viewBox="0 0 72 72" fill="none" xmlns="http://www.w3.org/2000/svg"> | |||
| <path d="M36 15V57" stroke="#5A3984" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> | |||
| <path d="M15 36H57" stroke="#5A3984" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> | |||
| </svg> | |||
| @@ -50,13 +50,13 @@ export const RegisterAltText = styled(Typography)` | |||
| 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; | |||
| ` | |||
| `; | |||
| export const FieldLabel = styled(Label)` | |||
| position: relative; | |||
| bottom: -14px; | |||
| @@ -68,9 +68,29 @@ export const FieldLabel = styled(Label)` | |||
| cursor: auto; | |||
| letter-spacing: 0.2px; | |||
| } | |||
| ` | |||
| `; | |||
| export const SelectText = styled(Typography)` | |||
| font-size: 16px; | |||
| font-family: "Open Sans"; | |||
| font-weight: 400; | |||
| `; | |||
| export const SelectField = styled(Select)` | |||
| position: relative; | |||
| top: 15px; | |||
| margin-bottom: 18px; | |||
| ` | |||
| & div { | |||
| ${SelectText} { | |||
| font-weight: 600; | |||
| } | |||
| } | |||
| `; | |||
| export const SelectAltText = styled(Typography)` | |||
| font-family: "Open Sans"; | |||
| font-style: italic; | |||
| white-space: pre; | |||
| font-size: 12px; | |||
| position: relative; | |||
| bottom: -1px; | |||
| `; | |||
| @@ -1,9 +1,71 @@ | |||
| import React from "react"; | |||
| import React, { useState } from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { CreateOfferFormContainer } from "./SecondPartCreateOffer.styled"; | |||
| import { | |||
| CreateOfferFormContainer, | |||
| FieldLabel, | |||
| Scroller, | |||
| // ImageListStyled, | |||
| } from "./SecondPartCreateOffer.styled"; | |||
| import ImagePicker from "../../../ImagePicker/ImagePicker"; | |||
| // import Select from "../../../Select/Select"; | |||
| import Option from "../../../Select/Option/Option"; | |||
| import { SelectAltText, SelectField, SelectText } from "../CreateOffer.styled"; | |||
| import { NextButton } from "../FirstPart/FirstPartCreateOffer.styled"; | |||
| import selectedTheme from "../../../../themes"; | |||
| import { conditionSelectEnum } from "../../../../enums/conditionEnum"; | |||
| const SecondPartCreateOffer = () => { | |||
| return <CreateOfferFormContainer>Aaaa</CreateOfferFormContainer>; | |||
| const [images, setImages] = useState([null, null, null]); // 3 images | |||
| const setImage = (index, image) => { | |||
| console.log(images); | |||
| setImages((prevState) => { | |||
| let newState = [...prevState]; | |||
| newState[index] = image; | |||
| return [...newState]; | |||
| }); | |||
| }; | |||
| return ( | |||
| <CreateOfferFormContainer> | |||
| <Scroller> | |||
| {images.map((item, index) => ( | |||
| <ImagePicker | |||
| key={index} | |||
| image={item} | |||
| setImage={(image) => setImage(index, image)} | |||
| deleteImage={() => setImage(index, null)} | |||
| /> | |||
| ))} | |||
| </Scroller> | |||
| <FieldLabel leftText="STANJE" /> | |||
| <SelectField defaultValue={conditionSelectEnum.NEW.value}> | |||
| {Object.keys(conditionSelectEnum).map((key) => { | |||
| var item = conditionSelectEnum[key]; | |||
| return ( | |||
| <Option value={item.value} key={item.value}> | |||
| <SelectText>{item.mainText}</SelectText> | |||
| <SelectAltText>{item.altText}</SelectAltText> | |||
| </Option> | |||
| ); | |||
| })} | |||
| </SelectField> | |||
| <NextButton | |||
| type="submit" | |||
| variant="contained" | |||
| height="48px" | |||
| fullWidth | |||
| buttoncolor={selectedTheme.primaryPurple} | |||
| textcolor="white" | |||
| // disabled={ | |||
| // formik.values.username.length === 0 || | |||
| // formik.values.password.length === 0 | |||
| // } | |||
| > | |||
| NASTAVI | |||
| </NextButton> | |||
| </CreateOfferFormContainer> | |||
| ); | |||
| }; | |||
| SecondPartCreateOffer.propTypes = { | |||
| @@ -1,8 +1,54 @@ | |||
| import { Box } from "@mui/material"; | |||
| import { Box, ImageList } from "@mui/material"; | |||
| import styled from "styled-components"; | |||
| import selectedTheme from "../../../../themes"; | |||
| import { Label } from "../../../CheckBox/Label"; | |||
| import HorizontalScroller from "../../../Scroller/HorizontalScroller"; | |||
| import { ReactComponent as TrashIcon } from "../../../../assets/images/svg/trash.svg"; | |||
| export const CreateOfferFormContainer = styled(Box)` | |||
| width: 335px; | |||
| width: 350px; | |||
| height: 700px; | |||
| padding-top: 20px; | |||
| `; | |||
| `; | |||
| export const ImageCard = styled.img` | |||
| width: 216px; | |||
| height: 144px; | |||
| display: flex; | |||
| flex-direction: row; | |||
| flex-wrap: nowrap; | |||
| `; | |||
| export const ImageListStyled = styled(ImageList)` | |||
| display: flex; | |||
| flex: 1; | |||
| overflow: hidden; | |||
| cursor: grab; | |||
| `; | |||
| export const FieldLabel = styled(Label)` | |||
| position: relative; | |||
| top: 12px; | |||
| & label { | |||
| font-size: 12px; | |||
| font-weight: 600; | |||
| line-height: 20px; | |||
| color: ${selectedTheme.primaryGrayText}; | |||
| cursor: auto; | |||
| letter-spacing: 0.2px; | |||
| } | |||
| `; | |||
| export const Scroller = styled(HorizontalScroller)` | |||
| min-width: 640px; | |||
| position: relative; | |||
| left: calc(-320px + 50%); | |||
| margin-bottom: 36px; | |||
| `; | |||
| export const Trash = styled(TrashIcon)` | |||
| cursor: pointer; | |||
| margin: auto; | |||
| width: 22px; | |||
| height: 22px; | |||
| & path { | |||
| stroke: white; | |||
| } | |||
| `; | |||
| @@ -1,4 +1,4 @@ | |||
| import React, { useEffect } from "react"; | |||
| import React, { useState } from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { | |||
| ContentContainer, | |||
| @@ -16,73 +16,91 @@ import Link from "../../Link/Link"; | |||
| import { PrimaryButton } from "../../Buttons/PrimaryButton/PrimaryButton"; | |||
| import FilterCheckboxDropdown from "./FilterDropdown/Checkbox/FilterCheckboxDropdown"; | |||
| import Mockupdata from "./Mockupdata"; | |||
| import { useState } from "react"; | |||
| // import { useState } from "react"; | |||
| import FilterRadioDropdown from "./FilterDropdown/Radio/FilterRadioDropdown"; | |||
| import { useHistory } from "react-router-dom"; | |||
| import { HOME_PAGE } from "../../../constants/pages"; | |||
| import qs from "query-string"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import selectedTheme from "../../../themes"; | |||
| import useFilters from "../../../hooks/useFilters"; | |||
| const FilterCard = () => { | |||
| const [appliedFilters, setAppliedFilters] = useState([]); | |||
| const [selectedCategory, setSelectedCategory] = useState(0); | |||
| const [selectedSubcategory, setSelectedSubcategory] = useState(0); | |||
| const history = useHistory(); | |||
| const { t } = useTranslation(); | |||
| const [isOpened, setIsOpened] = useState(false); | |||
| const [isDisabled, setIsDisabled] = useState(true); | |||
| useEffect(() => { | |||
| const queryString = history.location.search.substring(1); | |||
| const queryObject = qs.parse(queryString); | |||
| if (queryObject.category) { | |||
| setSelectedCategory( | |||
| Mockupdata[1].find( | |||
| (item) => item.string === queryObject.category.toString() | |||
| ).id | |||
| ); | |||
| } | |||
| if (queryObject.subcategory) { | |||
| setSelectedSubcategory( | |||
| Mockupdata[1].find( | |||
| (item) => item.string === queryObject.subcategory.toString() | |||
| ).id | |||
| ); | |||
| } | |||
| if (queryObject.city) { | |||
| let filters = []; | |||
| if (Array.isArray(queryObject.city)) { | |||
| queryObject.city.forEach((item) => { | |||
| filters.push(Mockupdata[0].find((p) => p.string === item).id); | |||
| }); | |||
| } else { | |||
| filters.push( | |||
| Mockupdata[0].find((p) => p.string === queryObject.city).id | |||
| ); | |||
| } | |||
| setAppliedFilters([...filters]); | |||
| const filters = useFilters(); | |||
| // useEffect(() => { | |||
| // const queryString = history.location.search.substring(1); | |||
| // const queryObject = qs.parse(queryString); | |||
| // if (queryObject.category) { | |||
| // setSelectedCategory( | |||
| // Mockupdata[1].find( | |||
| // (item) => item.string === queryObject.category.toString() | |||
| // ).id | |||
| // ); | |||
| // } | |||
| // if (queryObject.subcategory) { | |||
| // setSelectedSubcategory( | |||
| // Mockupdata[1].find( | |||
| // (item) => item.string === queryObject.subcategory.toString() | |||
| // ).id | |||
| // ); | |||
| // } | |||
| // if (queryObject.city) { | |||
| // let filters = []; | |||
| // if (Array.isArray(queryObject.city)) { | |||
| // queryObject.city.forEach((item) => { | |||
| // filters.push(Mockupdata[0].find((p) => p.string === item).id); | |||
| // }); | |||
| // } else { | |||
| // filters.push( | |||
| // Mockupdata[0].find((p) => p.string === queryObject.city).id | |||
| // ); | |||
| // } | |||
| // setfilters.selectedLocations([...filters]); | |||
| // } | |||
| // }, []); | |||
| const handleSelectCategory = (category) => { | |||
| filters.setSelectedCategory(category); | |||
| filters.setSelectedSubcategory(); | |||
| if (category?._id === 0) { | |||
| setIsOpened(false); | |||
| setIsDisabled(true); | |||
| } else { | |||
| setIsDisabled(false); | |||
| } | |||
| }, []); | |||
| } | |||
| const handleOpen = () => { | |||
| setIsOpened(prevState => !prevState); | |||
| } | |||
| const handleFilters = () => { | |||
| let queryObject = {}; | |||
| if (selectedCategory !== 0) { | |||
| if (filters.selectedCategory !== 0) { | |||
| queryObject = { | |||
| category: Mockupdata[1].find( | |||
| (item) => item.id.toString() === selectedCategory.toString() | |||
| (item) => item.id.toString() === filters.selectedCategory.toString() | |||
| ).string, | |||
| }; | |||
| if (selectedSubcategory !== 0) { | |||
| if (filters.selectedSubcategory !== 0) { | |||
| queryObject = { | |||
| ...queryObject, | |||
| subcategory: Mockupdata[1].find( | |||
| (item) => item.id.toString() === selectedSubcategory.toString() | |||
| (item) => | |||
| item.id.toString() === filters.selectedSubcategory.toString() | |||
| ).string, | |||
| }; | |||
| } | |||
| } | |||
| if (appliedFilters.length > 0) { | |||
| if (filters.selectedLocations.length > 0) { | |||
| let arrayObject = []; | |||
| appliedFilters.forEach((item) => { | |||
| filters.selectedLocations.forEach((item) => { | |||
| arrayObject.push( | |||
| Mockupdata[0].find((p) => p.id.toString() === item.toString()).string | |||
| ); | |||
| @@ -100,9 +118,9 @@ const FilterCard = () => { | |||
| }); | |||
| }; | |||
| const clearFilters = () => { | |||
| setAppliedFilters([]); | |||
| setSelectedCategory(0); | |||
| setSelectedSubcategory(0); | |||
| filters.setSelectedLocations([]); | |||
| filters.setSelectedCategory(0); | |||
| filters.setSelectedSubcategory(0); | |||
| history.push({ | |||
| pathname: HOME_PAGE, | |||
| search: "", | |||
| @@ -117,48 +135,61 @@ const FilterCard = () => { | |||
| <FilterCardContainer> | |||
| <Header> | |||
| <Title>{t("filters.title")}</Title> | |||
| <Link to="#" textsize={"12px"} font={"Open Sans"} onClick={clearFilters}> | |||
| <Link | |||
| to="#" | |||
| textsize={"12px"} | |||
| font={"Open Sans"} | |||
| onClick={clearFilters} | |||
| > | |||
| {t("filters.cancel")} | |||
| </Link> | |||
| </Header> | |||
| <ContentContainer> | |||
| {/* Categories */} | |||
| <FilterRadioDropdown | |||
| data={[...Mockupdata[1]]} | |||
| data={[...filters.categories]} | |||
| icon={ | |||
| selectedCategory && selectedCategory !== 0 ? ( | |||
| <CategoryChosen /> | |||
| ) : ( | |||
| <Category /> | |||
| ) | |||
| filters.selectedCategory?.name ? <CategoryChosen /> : <Category /> | |||
| } | |||
| title={ | |||
| selectedCategory && selectedCategory !== 0 | |||
| ? Mockupdata[1].find( | |||
| (item) => item.id.toString() === selectedCategory.toString() | |||
| ).string | |||
| filters.selectedCategory?.name | |||
| ? filters.selectedCategory?.name | |||
| : t("filters.categories.title") | |||
| } | |||
| searchPlaceholder={t("filters.categories.placeholder")} | |||
| setSelected={setSelectedCategory} | |||
| selected={selectedCategory} | |||
| setSelected={handleSelectCategory} | |||
| selected={filters.selectedCategory} | |||
| firstOption={{ | |||
| label: "SVE KATEGORIJE", | |||
| value: { _id: 0 }, | |||
| }} | |||
| /> | |||
| {/* Subcategories */} | |||
| <FilterRadioDropdown | |||
| data={[...Mockupdata[1]]} | |||
| data={filters.subcategories ? [...filters.subcategories] : []} | |||
| icon={<Subcategory />} | |||
| title={t("filters.subcategories.title")} | |||
| searchPlaceholder={t("filters.subcategories.placeholder")} | |||
| setSelected={setSelectedSubcategory} | |||
| selected={selectedSubcategory} | |||
| setSelected={filters.setSelectedSubcategory} | |||
| selected={filters.selectedSubcategory} | |||
| open={isOpened} | |||
| disabled={isDisabled} | |||
| handleOpen={handleOpen} | |||
| firstOption={{ | |||
| label: "SVE PODKATEGORIJE", | |||
| value: { _id: 0 }, | |||
| }} | |||
| /> | |||
| {/* Locations */} | |||
| <FilterCheckboxDropdown | |||
| searchPlaceholder={t("filters.location.placeholder")} | |||
| data={[...Mockupdata[0]]} | |||
| filters={appliedFilters} | |||
| data={[...filters.locations]} | |||
| filters={filters.selectedLocations} | |||
| icon={<Location />} | |||
| title={t("filters.location.title")} | |||
| setItemsSelected={setAppliedFilters} | |||
| setItemsSelected={filters.setSelectedLocations} | |||
| /> | |||
| </ContentContainer> | |||
| @@ -33,7 +33,7 @@ const FilterCheckboxDropdown = (props) => { | |||
| if (toSearch.length > 0) { | |||
| setDataToShow( | |||
| data.filter((item) => | |||
| item.string.toLowerCase().includes(toSearch.toLowerCase()) | |||
| item.city.toLowerCase().includes(toSearch.toLowerCase()) | |||
| ) | |||
| ); | |||
| } else { | |||
| @@ -16,16 +16,17 @@ const FilterRadioDropdown = (props) => { | |||
| const [dataToShow, setDataToShow] = useState([]); | |||
| const [isOpened, setIsOpened] = useState(false); | |||
| const { data } = props; | |||
| useEffect(() => { | |||
| setDataToShow([...data]); | |||
| }, []); | |||
| }, [data]); | |||
| useEffect(() => { | |||
| if (toSearch.length > 0) { | |||
| setDataToShow( | |||
| data.filter((item) => | |||
| item.string.toLowerCase().includes(toSearch.toLowerCase()) | |||
| item.name | |||
| ? item.name.toLowerCase().includes(toSearch.toLowerCase()) | |||
| : item.toLowerCase().includes(toSearch.toLowerCase()) | |||
| ) | |||
| ); | |||
| } else { | |||
| @@ -36,27 +37,37 @@ const FilterRadioDropdown = (props) => { | |||
| const handleClear = () => { | |||
| setToSearch(""); | |||
| }; | |||
| const handleChange = (value) => { | |||
| props.setSelected(value); | |||
| const handleOpen = () => { | |||
| setIsOpened((prevState) => !prevState); | |||
| if (props.handleOpen) props.handleOpen(); | |||
| }; | |||
| return ( | |||
| <DropdownList | |||
| title={props.title} | |||
| textcolor={ | |||
| props.selected !== 0 | |||
| ? selectedTheme.primaryPurple | |||
| : selectedTheme.primaryText | |||
| !props.selected || props.selected?._id === 0 | |||
| ? selectedTheme.primaryText | |||
| : selectedTheme.primaryPurple | |||
| } | |||
| dropdownIcon={props.icon} | |||
| toggleIconClosed={<DropdownDown />} | |||
| toggleIconOpened={<DropdownUp />} | |||
| fullWidth | |||
| setIsOpened={setIsOpened} | |||
| open={props.open} | |||
| disabled={props.disabled} | |||
| setIsOpened={handleOpen} | |||
| toggleIconStyles={{ | |||
| backgroundColor: isOpened ? "white" : selectedTheme.primaryIconBackgroundColor, | |||
| backgroundColor: ( | |||
| props.open !== undefined || props.open !== null | |||
| ? props.open | |||
| : isOpened | |||
| ) | |||
| ? "white" | |||
| : selectedTheme.primaryIconBackgroundColor, | |||
| }} | |||
| headerOptions={ | |||
| // SearchBar | |||
| <React.Fragment> | |||
| <TextField | |||
| placeholder={props.searchPlaceholder} | |||
| @@ -82,19 +93,36 @@ const FilterRadioDropdown = (props) => { | |||
| </React.Fragment> | |||
| } | |||
| > | |||
| <RadioGroup onChange={(event) => handleChange(event.target.value)}> | |||
| {dataToShow.map((item) => ( | |||
| <DropdownItem key={item.id}> | |||
| <RadioGroup> | |||
| {props.firstOption && ( | |||
| <DropdownItem key={0}> | |||
| <RadioButton | |||
| value={item.id} | |||
| label={item.string} | |||
| number={item.numberOfProducts} | |||
| value={props.firstOption.value} | |||
| label={props.firstOption.label} | |||
| // number={item.numberOfProducts} | |||
| fullWidth | |||
| checked={props.selected.toString() === item.id.toString()} | |||
| onChange={handleChange} | |||
| checked={!props.selected || props.selected._id === 0} | |||
| onChange={props.setSelected} | |||
| /> | |||
| </DropdownItem> | |||
| ))} | |||
| )} | |||
| {dataToShow.map((item) => { | |||
| console.log("item: ", item); | |||
| return ( | |||
| <DropdownItem key={item.name} onClick={() => props.setSelected(item)}> | |||
| <RadioButton | |||
| value={item} | |||
| label={item.name ? item.name : item} | |||
| // number={item.numberOfProducts} | |||
| fullWidth | |||
| checked={ | |||
| JSON.stringify(props.selected) === JSON.stringify(item) | |||
| } | |||
| onChange={props.setSelected} | |||
| /> | |||
| </DropdownItem> | |||
| ); | |||
| })} | |||
| </RadioGroup> | |||
| </DropdownList> | |||
| ); | |||
| @@ -110,6 +138,10 @@ FilterRadioDropdown.propTypes = { | |||
| searchPlaceholder: PropTypes.string, | |||
| setSelected: PropTypes.func, | |||
| selected: PropTypes.number, | |||
| firstOption: PropTypes.any, | |||
| disabled: PropTypes.bool, | |||
| open: PropTypes.bool, | |||
| handleOpen: PropTypes.func, | |||
| }; | |||
| FilterRadioDropdown.defaultProps = { | |||
| oneValueAllowed: false, | |||
| @@ -22,7 +22,10 @@ export const OfferCardContainer = styled(Container)` | |||
| height: 180px; | |||
| position: relative; | |||
| `; | |||
| export const OfferImage = styled.img``; | |||
| export const OfferImage = styled.img` | |||
| max-width: 144px; | |||
| max-height: 144px; | |||
| `; | |||
| export const OfferInfo = styled(Box)` | |||
| display: flex; | |||
| flex: 2; | |||
| @@ -1,4 +1,4 @@ | |||
| import React, { useState } from "react"; | |||
| import React, { useEffect, useState } from "react"; | |||
| import { | |||
| DropdownListContainer, | |||
| DropdownHeader, | |||
| @@ -14,40 +14,58 @@ import PropTypes from "prop-types"; | |||
| const DropdownList = (props) => { | |||
| const [listShown, setListShown] = useState(props.defaultOpen); | |||
| useEffect(() => { | |||
| if (props.open !== null || props.open !== undefined) { | |||
| setListShown(props.open); | |||
| } | |||
| }, [props.open]); | |||
| const handleShow = () => { | |||
| if (props.setIsOpened) { | |||
| props.setIsOpened(!listShown); | |||
| } | |||
| setListShown((prevState) => !prevState); | |||
| if (!props.disabled) { | |||
| setListShown((prevState) => !prevState); | |||
| } | |||
| if (!(props.open !== null || props.open !== undefined)) { | |||
| setListShown(prevState => !prevState) | |||
| } | |||
| }; | |||
| return ( | |||
| <DropdownListContainer fullwidth={props.fullWidth ? 1 : 0}> | |||
| <DropdownHeader> | |||
| {props.dropdownIcon && ( | |||
| <DropdownIcon onClick={() => handleShow()}> | |||
| <DropdownIcon | |||
| onClick={!props.disabled ? () => handleShow() : () => {}} | |||
| disabled={props.disabled} | |||
| > | |||
| {props.dropdownIcon} | |||
| </DropdownIcon> | |||
| )} | |||
| <DropdownTitle onClick={() => handleShow()} textcolor={props.textcolor}> | |||
| <DropdownTitle | |||
| onClick={!props.disabled ? () => handleShow() : () => {}} | |||
| textcolor={props.textcolor} | |||
| disabled={props.disabled} | |||
| > | |||
| {props.title} | |||
| </DropdownTitle> | |||
| {listShown ? ( | |||
| {(props.open !== null && props.open !== undefined ? props.open : listShown) ? ( | |||
| <ToggleIconOpened | |||
| style={props.toggleIconStyles} | |||
| onClick={() => handleShow()} | |||
| onClick={!props.disabled ? () => handleShow() : () => {}} | |||
| > | |||
| {props.toggleIconOpened} | |||
| </ToggleIconOpened> | |||
| ) : ( | |||
| <ToggleIconClosed | |||
| style={props.toggleIconStyles} | |||
| onClick={() => handleShow()} | |||
| onClick={!props.disabled ? () => handleShow() : () => {}} | |||
| disabled={props.disabled} | |||
| > | |||
| {props.toggleIconClosed} | |||
| </ToggleIconClosed> | |||
| )} | |||
| </DropdownHeader> | |||
| <ToggleContainer shouldShow={listShown}> | |||
| <ToggleContainer shouldShow={props.open !== null && props.open !== undefined ? props.open : listShown}> | |||
| <DropdownOptions>{props.headerOptions}</DropdownOptions> | |||
| <ListContainer>{props.children}</ListContainer> | |||
| </ToggleContainer> | |||
| @@ -69,6 +87,8 @@ DropdownList.propTypes = { | |||
| headerOptions: PropTypes.node, | |||
| textcolor: PropTypes.string, | |||
| setIsOpened: PropTypes.func, | |||
| open: PropTypes.bool, | |||
| disabled: PropTypes.bool, | |||
| }; | |||
| DropdownList.defaultProps = { | |||
| @@ -20,32 +20,57 @@ export const DropdownTitle = styled(Typography)` | |||
| padding-top: 5px; | |||
| padding-right: 0.9rem; | |||
| font-family: "Open Sans"; | |||
| color: ${props => props.textcolor ? props.textcolor : selectedTheme.primaryText}; | |||
| color: ${(props) => | |||
| props.disabled | |||
| ? selectedTheme.iconStrokeDisabledColor | |||
| : props.textcolor | |||
| ? props.textcolor | |||
| : selectedTheme.primaryText}; | |||
| `; | |||
| export const ToggleIconOpened = styled(IconButton)` | |||
| cursor: pointer; | |||
| color: ${selectedTheme.primaryPurple}; | |||
| & span { | |||
| ${props => props.backgroundColor ? `background-color: ${props.backgroundColor}` : ``} | |||
| ${(props) => | |||
| props.backgroundColor ? `background-color: ${props.backgroundColor}` : ``} | |||
| } | |||
| `; | |||
| export const ToggleIconClosed = styled(IconButton)` | |||
| cursor: pointer; | |||
| color: ${selectedTheme.primaryPurple}; | |||
| &:hover button { | |||
| ${(props) => | |||
| props.disabled && | |||
| `background-color: ${selectedTheme.primaryIconBackgroundColor}`} | |||
| } | |||
| & span { | |||
| ${props => props.backgroundColor ? `background-color: ${props.backgroundColor}` : ``} | |||
| ${(props) => | |||
| props.backgroundColor || !props.disabled | |||
| ? `background-color: ${props.backgroundColor}` | |||
| : ``} | |||
| } | |||
| & svg path { | |||
| ${(props) => | |||
| props.disabled && | |||
| ` | |||
| stroke: ${selectedTheme.iconStrokeDisabledColor}; | |||
| `} | |||
| } | |||
| `; | |||
| export const DropdownIcon = styled(IconButton)` | |||
| font-size: 22px !important; | |||
| & span { | |||
| & svg { | |||
| font-size: 22px; | |||
| & svg { | |||
| font-size: 22px; | |||
| & path { | |||
| ${(props) => | |||
| props.disabled && | |||
| ` | |||
| stroke: ${selectedTheme.iconStrokeDisabledColor}; | |||
| `} | |||
| } | |||
| } | |||
| `; | |||
| @@ -59,8 +84,7 @@ export const DropdownHeader = styled(Box)` | |||
| display: flex; | |||
| flex-direction: row; | |||
| `; | |||
| export const DropdownOptions = styled(Box)` | |||
| `; | |||
| export const DropdownOptions = styled(Box)``; | |||
| export const ToggleContainer = styled(Box)` | |||
| display: ${(props) => (props.shouldShow ? "block" : "none")}; | |||
| `; | |||
| @@ -0,0 +1,109 @@ | |||
| import React, { useRef, useState } from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { | |||
| AddFile, | |||
| AddIcon, | |||
| ImagePickerContainer, | |||
| ImageUploaded, | |||
| } from "./ImagePicker.styled"; | |||
| import { Tooltip } from "@mui/material"; | |||
| import { Trash } from "../Cards/CreateOfferCard/SecondPart/SecondPartCreateOffer.styled"; | |||
| import { Icon } from "../Icon/Icon"; | |||
| // import { Input } from "@mui/material"; | |||
| const ImagePicker = (props) => { | |||
| const fileInputRef = useRef(null); | |||
| const [isOpened, setIsOpened] = useState(false); | |||
| const handleChange = () => { | |||
| fileInputRef.current.click(); | |||
| console.log(fileInputRef.current.click); | |||
| }; | |||
| const handleImage = (event) => { | |||
| console.log("fileEvent: ", event.target.files[0]); | |||
| let reader = new FileReader(); | |||
| reader.readAsDataURL(event.target.files[0]); | |||
| reader.onload = () => { | |||
| console.log(reader.result.toString()); | |||
| props.setImage(reader.result); | |||
| }; | |||
| reader.onerror = () => {}; | |||
| }; | |||
| // let timeoutObject = { | |||
| // timeoutFunctionSet: false, | |||
| // timeoutFunction: () => {}, | |||
| // }; | |||
| // const showMessage = (event) => { | |||
| // console.log(event); | |||
| // }; | |||
| // const handleMouseEnter = (event) => { | |||
| // timeoutObject.timeoutFunctionSet = true; | |||
| // timeoutObject.timeoutFunction = setTimeout(() => { | |||
| // showMessage(event); | |||
| // }, 1000); | |||
| // }; | |||
| // const handleMouseMove = (event) => { | |||
| // if (timeoutObject.timeoutFunctionSet) { | |||
| // clearTimeout(timeoutObject.timeoutFunction); | |||
| // timeoutObject.timeoutFunction = setTimeout(() => { | |||
| // showMessage(event); | |||
| // }, 1000); | |||
| // } | |||
| // }; | |||
| // const handleMouseLeave = () => { | |||
| // if (timeoutObject.timeoutFunctionSet) { | |||
| // clearTimeout(timeoutObject.timeoutFunction); | |||
| // timeoutObject.timeoutFunctionSet = false; | |||
| // } | |||
| // }; | |||
| const handleOpen = () => { | |||
| setIsOpened(true); | |||
| if (!props.image) setIsOpened(false); | |||
| }; | |||
| const handleClose = () => { | |||
| setIsOpened(false); | |||
| }; | |||
| const handleDelete = () => { | |||
| props.deleteImage(); | |||
| setIsOpened(false) | |||
| } | |||
| return ( | |||
| <Tooltip | |||
| open={isOpened} | |||
| onOpen={handleOpen} | |||
| onClose={handleClose} | |||
| enterDelay={500} | |||
| enterNextDelay={500} | |||
| arrow | |||
| title={ | |||
| <Icon style={{width: "50px", height: "42px", paddingTop: "10px"}}> | |||
| <Trash onClick={handleDelete} /> | |||
| </Icon> | |||
| } | |||
| d | |||
| > | |||
| <ImagePickerContainer | |||
| className={props.className} | |||
| onClick={!props.image ? handleChange : () => {}} | |||
| hasImage={props.image} | |||
| > | |||
| <AddFile type="file" ref={fileInputRef} onInput={handleImage} /> | |||
| {props.image ? ( | |||
| <ImageUploaded src={props.image} draggable={false}></ImageUploaded> | |||
| ) : ( | |||
| <AddIcon /> | |||
| )} | |||
| </ImagePickerContainer> | |||
| </Tooltip> | |||
| ); | |||
| }; | |||
| ImagePicker.propTypes = { | |||
| children: PropTypes.node, | |||
| className: PropTypes.string, | |||
| setImage: PropTypes.func, | |||
| image: PropTypes.func, | |||
| deleteImage: PropTypes.func | |||
| }; | |||
| export default ImagePicker; | |||
| @@ -0,0 +1,67 @@ | |||
| import { Box } from "@mui/material"; | |||
| import styled from "styled-components"; | |||
| import selectedTheme from "../../themes"; | |||
| import {ReactComponent as Plus} from "../../assets/images/svg/plus.svg"; | |||
| export const ImagePickerContainer = styled(Box)` | |||
| flex: 1; | |||
| display: flex; | |||
| flex-basis: 216px; | |||
| flex-grow: 0; | |||
| flex-shrink: 0; | |||
| height: 144px; | |||
| margin: 0 9px; | |||
| border-radius: 4px; | |||
| cursor: pointer; | |||
| background-color: ${selectedTheme.imagePickerBackground}; | |||
| background-image: linear-gradient( | |||
| to right, | |||
| ${selectedTheme.primaryPurple} 50%, | |||
| rgba(255, 255, 255, 0) 0% | |||
| ), | |||
| linear-gradient( | |||
| ${selectedTheme.primaryPurple} 50%, | |||
| rgba(255, 255, 255, 0) 0% | |||
| ), | |||
| linear-gradient( | |||
| to right, | |||
| ${selectedTheme.primaryPurple} 50%, | |||
| rgba(255, 255, 255, 0) 0% | |||
| ), | |||
| linear-gradient( | |||
| ${selectedTheme.primaryPurple} 50%, | |||
| rgba(255, 255, 255, 0) 0% | |||
| ); | |||
| background-position: bottom, right, top, left; | |||
| background-size: 20px 1px, 1px 20px, 20px 1px, 1px 20px; | |||
| background-repeat: repeat-x, repeat-y, repeat-x, repeat-y; | |||
| &:first-of-type { | |||
| margin-left: 0; | |||
| } | |||
| &:last-of-type { | |||
| margin-right: 0; | |||
| } | |||
| ${props => props.hasImage && ` | |||
| background-image: none; | |||
| border: 1px solid ${selectedTheme.primaryPurple}; | |||
| `} | |||
| `; | |||
| export const AddIcon = styled(Plus)` | |||
| margin: auto; | |||
| ` | |||
| export const AddFile = styled.input` | |||
| display: none; | |||
| ` | |||
| export const ImageUploaded = styled.img` | |||
| width: 216px; | |||
| height: 144px; | |||
| object-fit: scale-down; | |||
| ` | |||
| export const Tooltip = styled(Box)` | |||
| background-color: rgba(255, 255, 255, 0.5); | |||
| width: 100px; | |||
| height: 100px; | |||
| position: absolute; | |||
| left: 0; | |||
| top: 0; | |||
| ` | |||
| @@ -18,6 +18,7 @@ import { selectFilters } from "../../../store/selectors/filtersSelectors"; | |||
| import Mockupdata from "../../Cards/FilterCard/Mockupdata"; | |||
| import Option from "../../Select/Option/Option"; | |||
| import { useHistory, useLocation, useRouteMatch } from "react-router-dom"; | |||
| import { sortEnum } from "../../../enums/sortEnum"; | |||
| const DownArrow = (props) => ( | |||
| <IconStyled {...props}> | |||
| @@ -25,25 +26,6 @@ const DownArrow = (props) => ( | |||
| </IconStyled> | |||
| ); | |||
| const MockupdataForSelect = [ | |||
| { | |||
| id: 0, | |||
| string: "Sortiraj po", | |||
| }, | |||
| { | |||
| id: 1, | |||
| string: "Najpopularnije", | |||
| }, | |||
| { | |||
| id: 2, | |||
| string: "Najnovije", | |||
| }, | |||
| { | |||
| id: 3, | |||
| string: "Najstarije", | |||
| }, | |||
| ]; | |||
| const Header = (props) => { | |||
| const [categoryString, setCategoryString] = useState(""); | |||
| const [filtersString, setFiltersString] = useState(""); | |||
| @@ -77,7 +59,14 @@ const Header = (props) => { | |||
| }, [category, cities]); | |||
| const handleChangeSelect = (value) => { | |||
| let chosenOption = MockupdataForSelect.find(item => item.id === value); | |||
| let chosenOption; | |||
| for (const sortOption in sortEnum) { | |||
| console.log(sortEnum[sortOption]) | |||
| if (sortEnum[sortOption].value === value) { | |||
| chosenOption = sortEnum[sortOption]; | |||
| } | |||
| } | |||
| console.log(location); | |||
| console.log(history); | |||
| console.log(chosenOption); | |||
| @@ -117,15 +106,17 @@ const Header = (props) => { | |||
| height="34px" | |||
| onChange={handleChangeSelect} | |||
| > | |||
| {MockupdataForSelect.map((item) => ( | |||
| {Object.keys(sortEnum).map((property) => { | |||
| console.log(sortEnum[property]) | |||
| return ( | |||
| <Option | |||
| value={item.id} | |||
| key={item.id} | |||
| style={{ display: item.id === 0 ? "none" : "flex" }} | |||
| value={sortEnum[property].value} | |||
| key={sortEnum[property].value} | |||
| style={{ display: sortEnum[property].value === 0 ? "none" : "flex" }} | |||
| > | |||
| {item.string} | |||
| {sortEnum[property].mainText} | |||
| </Option> | |||
| ))} | |||
| )})} | |||
| </HeaderSelect> | |||
| </HeaderOptions> | |||
| </HeaderContainer> | |||
| @@ -38,7 +38,7 @@ export const HeaderSelect = styled(Select)` | |||
| font-weight: 400; | |||
| position: relative; | |||
| left: -5px; | |||
| & div { | |||
| & div:first-child { | |||
| padding-left: 8px; | |||
| } | |||
| ` | |||
| @@ -1,11 +1,35 @@ | |||
| import React, { useState } from "react"; | |||
| import React, { useEffect, useState } from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { MarketPlaceContainer } from "./MarketPlace.styled"; | |||
| import Header from "./Header/Header"; | |||
| import Offers from "./Offers/Offers"; | |||
| import { useDispatch, useSelector } from "react-redux"; | |||
| import { fetchCategories } from "../../store/actions/categories/categoriesActions"; | |||
| import { fetchLocations } from "../../store/actions/locations/locationsActions"; | |||
| import { selectSubcategories } from "../../store/selectors/categoriesSelectors"; | |||
| const MarketPlace = () => { | |||
| const [isGrid, setIsGrid] = useState(false); | |||
| const dispatch = useDispatch(); | |||
| const categories = useSelector(selectSubcategories("Automobili")); | |||
| useEffect(() => { | |||
| dispatch(fetchCategories()); | |||
| dispatch(fetchLocations()); | |||
| }, []); | |||
| useEffect(() => { | |||
| let newSubcategories = []; | |||
| if (categories) { | |||
| for (let i = 0; i < categories.length; i++) { | |||
| let subcategoryString = ""; | |||
| Object.keys(categories[i]).forEach(item => { | |||
| subcategoryString += categories[i][item] | |||
| }) | |||
| newSubcategories.push(subcategoryString) | |||
| } | |||
| } | |||
| console.log("selektor: ", newSubcategories); | |||
| }, [categories]); | |||
| return ( | |||
| <MarketPlaceContainer> | |||
| @@ -55,7 +55,7 @@ const HorizontalScroller = (props) => { | |||
| scrollRef.current.scrollBy({ left: -50, behaviour: "smooth" }); | |||
| }; | |||
| return ( | |||
| <HorizontalScrollerContainer style={props.containerStyle}> | |||
| <HorizontalScrollerContainer style={props.containerStyle} className={props.className}> | |||
| <Arrow onClick={handleLeft} disabled={isDisabledLeftButton}> | |||
| <ArrowIcon side={"left"} /> | |||
| </Arrow> | |||
| @@ -46,7 +46,6 @@ export const ListContainer = styled(ScrollContainer)` | |||
| flex: 1; | |||
| flex-direction: row; | |||
| flex-wrap: nowrap; | |||
| cursor: grab; | |||
| scroll-behavior: smooth; | |||
| margin: 0 18px; | |||
| user-select: none; | |||
| @@ -1,15 +1,21 @@ | |||
| import React from "react"; | |||
| import React, { useState } from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { SelectIcon, SelectStyled } from "./Select.styled"; | |||
| import { SelectStyled, SelectIcon } from "./Select.styled"; | |||
| import { ReactComponent as Down } from "../../assets/images/svg/down-arrow.svg"; | |||
| // import {Select as SelectMUI} from "@mui/material"; | |||
| const Select = (props) => { | |||
| const [isOpened, setIsOpened] = useState(false); | |||
| const handleOpen = () => { | |||
| setIsOpened(prevState => !prevState); | |||
| } | |||
| return ( | |||
| <SelectStyled | |||
| defaultValue={props.defaultValue} | |||
| defaultValue={props.defaultValue} | |||
| fullWidth={props.fullwidth} | |||
| open={isOpened} | |||
| onClick={() => handleOpen()} | |||
| width={props.width} | |||
| height={props.height} | |||
| className={props.className} | |||
| @@ -36,7 +42,7 @@ Select.propTypes = { | |||
| }; | |||
| Select.defaultProps = { | |||
| fullwidth: true, | |||
| height: "48px" | |||
| height: "48px", | |||
| }; | |||
| export default Select; | |||
| @@ -8,9 +8,11 @@ export const SelectStyled = styled(Select)` | |||
| font-size: 16px; | |||
| font-weight: 600; | |||
| font-family: "Open Sans"; | |||
| cursor: pointer; | |||
| ` | |||
| export const SelectIcon = styled(Box)` | |||
| position: relative; | |||
| top: 0px; | |||
| left: -15px; | |||
| top: -1px; | |||
| left: -8px; | |||
| cursor: pointer; | |||
| ` | |||
| @@ -0,0 +1,17 @@ | |||
| export const conditionSelectEnum = { | |||
| NEW: { | |||
| value: 1, | |||
| mainText: "Novo", | |||
| altText: "" | |||
| }, | |||
| USED: { | |||
| value: 2, | |||
| mainText: "Polovno ", | |||
| altText: " (korišćeno)" | |||
| }, | |||
| LIKE_NEW: { | |||
| value: 3, | |||
| mainText: "Kao novo", | |||
| altText: " (nekorišćeno)" | |||
| } | |||
| } | |||
| @@ -0,0 +1,18 @@ | |||
| export const sortEnum = { | |||
| INITIAL: { | |||
| value: 0, | |||
| mainText: "Sortiraj po" | |||
| }, | |||
| POPULAR: { | |||
| value: 1, | |||
| mainText: "Najpopularnije" | |||
| }, | |||
| NEW: { | |||
| value: 2, | |||
| mainText: "Najnovije" | |||
| }, | |||
| OLD: { | |||
| value: 3, | |||
| mainText: "Najstarije" | |||
| } | |||
| } | |||
| @@ -0,0 +1,82 @@ | |||
| import { useEffect, useState } from "react"; | |||
| import { useDispatch } from "react-redux"; | |||
| import { useSelector } from "react-redux"; | |||
| import { fetchCategories } from "../store/actions/categories/categoriesActions"; | |||
| import { fetchLocations } from "../store/actions/locations/locationsActions"; | |||
| import { | |||
| selectCategories, | |||
| selectSubcategories, | |||
| } from "../store/selectors/categoriesSelectors"; | |||
| // import qs from "query-string"; | |||
| import { selectLocations } from "../store/selectors/locationsSelectors"; | |||
| const useFilters = () => { | |||
| const [selectedCategory, setSelectedCategory] = useState(); | |||
| const [selectedSubcategory, setSelectedSubcategory] = useState(); | |||
| const [selectedLocations, setSelectedLocations] = useState([]); | |||
| const [selectedSortOption, setSelectedSortOption] = useState({}); | |||
| const categories = useSelector(selectCategories); | |||
| const subcategories = useSelector( | |||
| selectSubcategories(selectedCategory?.name) | |||
| ); | |||
| const locations = useSelector(selectLocations); | |||
| const dispatch = useDispatch(); | |||
| useEffect(() => { | |||
| dispatch(fetchCategories()); | |||
| dispatch(fetchLocations()); | |||
| }, []); | |||
| useEffect(() => { | |||
| console.log("selectedCategory changed: ", selectedCategory); | |||
| }, [selectedCategory]) | |||
| // useEffect(() => { | |||
| // if (categories && locations) { | |||
| // const queryString = history.location.search.substring(1); | |||
| // const queryObject = qs.parse(queryString); | |||
| // let category; | |||
| // if (queryObject.category) { | |||
| // category = categories.find( | |||
| // (item) => item.mainText === queryObject.category.toString() | |||
| // ); | |||
| // setSelectedCategory(category); | |||
| // } | |||
| // if (queryObject.subcategory) { | |||
| // setSelectedSubcategory( | |||
| // category.subcategories.find( | |||
| // (item) => item.mainText === queryObject.subcategory.toString() | |||
| // ).id | |||
| // ); | |||
| // } | |||
| // if (queryObject.city) { | |||
| // let filters = []; | |||
| // if (Array.isArray(queryObject.city)) { | |||
| // queryObject.city.forEach((item) => { | |||
| // filters.push(Mockupdata[0].find((p) => p.string === item).id); | |||
| // }); | |||
| // } else { | |||
| // filters.push( | |||
| // Mockupdata[0].find((p) => p.string === queryObject.city).id | |||
| // ); | |||
| // } | |||
| // setAppliedFilters([...filters]); | |||
| // } | |||
| // } | |||
| // }, [categories, locations]); | |||
| useEffect(() => { | |||
| console.log("subcategories: ", subcategories); | |||
| }, [subcategories]); | |||
| return { | |||
| selectedCategory, | |||
| setSelectedCategory, | |||
| selectedSubcategory, | |||
| setSelectedSubcategory, | |||
| selectedLocations, | |||
| setSelectedLocations, | |||
| selectedSortOption, | |||
| setSelectedSortOption, | |||
| categories, | |||
| subcategories, | |||
| locations, | |||
| }; | |||
| }; | |||
| export default useFilters; | |||
| @@ -160,5 +160,7 @@ export default { | |||
| offers: { | |||
| getOffers: 'offers', | |||
| addOffer: 'offers', | |||
| categories: 'categories', | |||
| locations: 'locations' | |||
| } | |||
| }; | |||
| @@ -0,0 +1,5 @@ | |||
| import { getRequest } from "."; | |||
| import apiEndpoints from "./apiEndpoints"; | |||
| export const attemptFetchCategories = () => | |||
| getRequest(apiEndpoints.offers.categories); | |||
| @@ -2,7 +2,8 @@ import axios from "axios"; | |||
| import queryString from "qs"; | |||
| const request = axios.create({ | |||
| baseURL: "http://192.168.88.150:3001/", | |||
| // baseURL: "http://192.168.88.150:3001/", | |||
| baseURL: "http://192.168.88.175:3005/", | |||
| headers: { | |||
| "Content-Type": "application/json", | |||
| }, | |||
| @@ -11,8 +12,10 @@ const request = axios.create({ | |||
| queryString.stringify(params, { arrayFormat: "comma" }), | |||
| }); | |||
| export const getRequest = (url, params = null, options = null) => | |||
| request.get(url, { params, ...options }); | |||
| export const getRequest = (url, params = null, options = null) => { | |||
| console.log('poziv') | |||
| return request.get(url, { params, ...options }); | |||
| } | |||
| export const postRequest = (url, data, params = null, options = null) => | |||
| request.post(url, data, { params, ...options }); | |||
| @@ -0,0 +1,5 @@ | |||
| import { getRequest } from "."; | |||
| import apiEndpoints from "./apiEndpoints"; | |||
| export const attemptFetchLocations = () => | |||
| getRequest(apiEndpoints.offers.locations); | |||
| @@ -0,0 +1,6 @@ | |||
| import { createFetchType } from "../actionHelpers"; | |||
| const CATEGORIES_SCOPE = "CATEGORIES"; | |||
| export const CATEGORIES_FETCH = createFetchType(CATEGORIES_SCOPE); | |||
| export const CATEGORIES_SET = "CATEGORIES_SET"; | |||
| @@ -0,0 +1,10 @@ | |||
| import { CATEGORIES_FETCH, CATEGORIES_SET } from "./categoriesActionConstants"; | |||
| export const fetchCategories = () => ({ | |||
| type: CATEGORIES_FETCH | |||
| }) | |||
| export const setCategories = (payload) => ({ | |||
| type: CATEGORIES_SET, | |||
| payload | |||
| }) | |||
| @@ -0,0 +1,6 @@ | |||
| import { createFetchType } from "../actionHelpers"; | |||
| const LOCATIONS_SCOPE = "LOCATIONS_SCOPE"; | |||
| export const LOCATIONS_FETCH = createFetchType(LOCATIONS_SCOPE); | |||
| export const LOCATIONS_SET = "LOCATIONS_SET"; | |||
| @@ -0,0 +1,10 @@ | |||
| import { LOCATIONS_FETCH, LOCATIONS_SET } from "./locationsActionConstants"; | |||
| export const fetchLocations = () => ({ | |||
| type: LOCATIONS_FETCH, | |||
| }); | |||
| export const setLocations = (payload) => ({ | |||
| type: LOCATIONS_SET, | |||
| payload | |||
| }) | |||
| @@ -13,7 +13,8 @@ import { logoutUser, refreshUserToken } from "../actions/login/loginActions"; | |||
| // import { setUserAccessToken } from "../actions/user/userActions"; | |||
| //Change URL with .env | |||
| const baseURL = "http://192.168.88.150:3001/"; | |||
| //const baseURL = "http://192.168.88.150:3001/"; | |||
| const baseURL = "http://192.168.88.175:3005/"; | |||
| //Interceptor unique name | |||
| export const accessTokensMiddlewareInterceptorName = "ACCESS_TOKEN_INTERCEPTOR"; | |||
| @@ -25,16 +26,17 @@ export default ({ dispatch }) => | |||
| const jwtToken = authScopeStringGetHelper(JWT_TOKEN); | |||
| const refresh = authScopeStringGetHelper(JWT_REFRESH_TOKEN); | |||
| if (!jwtToken || !refresh) return Promise.resolve(response); | |||
| const jwtTokenDecoded = jwt.decode(jwtToken); | |||
| const refreshTokenDecoded = jwt.decode(refresh); | |||
| if (!response.headers?.Authorization) { | |||
| response.headers.Authorization = `Bearer ${jwtToken}`; | |||
| } | |||
| const jwtTokenDecoded = jwt.decode(jwtToken); | |||
| const refreshTokenDecoded = jwt.decode(refresh); | |||
| // If refresh token is expired, log out user | |||
| if (new Date() > new Date(refreshTokenDecoded?.exp * 1000)) { | |||
| dispatch(logoutUser()); | |||
| } | |||
| // If access token is expired, refresh access token | |||
| if (new Date() > new Date(jwtTokenDecoded.exp * 1000)) { | |||
| const axiosResponse = await axios.post(`${baseURL}auth/refresh`, { | |||
| @@ -43,6 +45,7 @@ export default ({ dispatch }) => | |||
| const newToken = axiosResponse.data.token; | |||
| dispatch(refreshUserToken(newToken)); | |||
| } | |||
| return Promise.resolve(response); | |||
| }, accessTokensMiddlewareInterceptorName); | |||
| @@ -0,0 +1,20 @@ | |||
| import { CATEGORIES_SET } from "../../actions/categories/categoriesActionConstants"; | |||
| import createReducer from "../../utils/createReducer"; | |||
| const initialState = { | |||
| categories: [], | |||
| }; | |||
| export default createReducer( | |||
| { | |||
| [CATEGORIES_SET]: setCategories | |||
| }, | |||
| initialState | |||
| ); | |||
| function setCategories(state, action) { | |||
| return { | |||
| ...state, | |||
| categories: action.payload | |||
| } | |||
| } | |||
| @@ -8,6 +8,8 @@ import createFilter from "redux-persist-transform-filter"; | |||
| import persistReducer from "redux-persist/es/persistReducer"; | |||
| import filtersReducer from "./filters/filtersReducer"; | |||
| import offersReducer from "./offers/offersReducer"; | |||
| import categoriesReducer from "./categories/categoriesReducer"; | |||
| import locationsReducer from "./locations/locationsReducer"; | |||
| const loginPersistConfig = { | |||
| key: "login", | |||
| @@ -37,11 +39,24 @@ const randomDataPersistConfig = { | |||
| ], | |||
| }; | |||
| const categoriesPersistConfig = { | |||
| key: "categories", | |||
| storage: storage, | |||
| transform: [createFilter("categories", ["_id", "name", "subcategories"])], | |||
| }; | |||
| const locationsPersistConfig = { | |||
| key: "locations", | |||
| storage: storage, | |||
| transform: [createFilter("locations", ["_id", "city"])], | |||
| }; | |||
| export default combineReducers({ | |||
| login: persistReducer(loginPersistConfig, loginReducer), | |||
| user: persistReducer(userPersistConfig, userReducer), | |||
| loading: loadingReducer, | |||
| filters: filtersReducer, | |||
| randomData: persistReducer(randomDataPersistConfig, randomDataReducer), | |||
| offers: offersReducer | |||
| offers: offersReducer, | |||
| categories: persistReducer(categoriesPersistConfig, categoriesReducer), | |||
| locations: persistReducer(locationsPersistConfig, locationsReducer), | |||
| }); | |||
| @@ -0,0 +1,20 @@ | |||
| import { LOCATIONS_SET } from "../../actions/locations/locationsActionConstants"; | |||
| import createReducer from "../../utils/createReducer"; | |||
| const initialState = { | |||
| locations: [], | |||
| }; | |||
| export default createReducer( | |||
| { | |||
| [LOCATIONS_SET]: setLocations, | |||
| }, | |||
| initialState | |||
| ); | |||
| function setLocations(state, action) { | |||
| return { | |||
| ...state, | |||
| locations: action.payload, | |||
| }; | |||
| } | |||
| @@ -0,0 +1,13 @@ | |||
| import { all, call, put, takeLatest } from "@redux-saga/core/effects"; | |||
| import { attemptFetchCategories } from "../../request/categoriesRequest"; | |||
| import { CATEGORIES_FETCH } from "../actions/categories/categoriesActionConstants"; | |||
| import { setCategories } from "../actions/categories/categoriesActions"; | |||
| function* fetchCategories() { | |||
| const { data } = yield call(attemptFetchCategories); | |||
| yield put(setCategories(data)); | |||
| } | |||
| export default function* categoriesSaga() { | |||
| yield all([takeLatest(CATEGORIES_FETCH, fetchCategories)]); | |||
| } | |||
| @@ -1,5 +1,7 @@ | |||
| import { all } from 'redux-saga/effects'; | |||
| import categoriesSaga from './categoriesSaga'; | |||
| import forgotPasswordSaga from './forgotPasswordSaga'; | |||
| import locationsSaga from './locationsSaga'; | |||
| import loginSaga from './loginSaga'; | |||
| import offersSaga from './offersSaga'; | |||
| import registerSaga from './registerSaga'; | |||
| @@ -9,6 +11,8 @@ export default function* rootSaga() { | |||
| loginSaga(), | |||
| registerSaga(), | |||
| forgotPasswordSaga(), | |||
| offersSaga() | |||
| offersSaga(), | |||
| categoriesSaga(), | |||
| locationsSaga() | |||
| ]); | |||
| } | |||
| @@ -0,0 +1,16 @@ | |||
| import { all, call, put, takeLatest } from "@redux-saga/core/effects"; | |||
| import { attemptFetchLocations } from "../../request/locationsRequest"; | |||
| import { LOCATIONS_FETCH } from "../actions/locations/locationsActionConstants"; | |||
| import { setLocations } from "../actions/locations/locationsActions"; | |||
| function* fetchLocations() { | |||
| const {data} = yield call(attemptFetchLocations) | |||
| console.log(data); | |||
| yield put(setLocations(data)); | |||
| } | |||
| export default function* locationsSaga() { | |||
| yield all([ | |||
| takeLatest(LOCATIONS_FETCH, fetchLocations) | |||
| ]) | |||
| } | |||
| @@ -0,0 +1,14 @@ | |||
| import { createSelector } from "reselect"; | |||
| const categoriesSelector = (state) => state.categories; | |||
| export const selectCategories = createSelector( | |||
| categoriesSelector, | |||
| (state) => state.categories | |||
| ); | |||
| export const selectSubcategories = (category) => | |||
| createSelector(categoriesSelector, (state) => | |||
| state.categories?.find( | |||
| (item) => item?.name?.toString() === category?.toString() | |||
| )?.subcategories | |||
| ); | |||
| @@ -0,0 +1,8 @@ | |||
| import { createSelector } from "reselect"; | |||
| const locationSelector = (state) => state.locations; | |||
| export const selectLocations = createSelector( | |||
| locationSelector, | |||
| (state) => state.locations | |||
| ); | |||
| @@ -13,5 +13,7 @@ export const primaryThemeColors = { | |||
| offerBackgroundColor: "#F5F5F5", | |||
| selectOptionTextColor: "#1D1D1D", | |||
| primaryDarkText: "#505050", | |||
| iconStrokeColor: "#8C8C8C" | |||
| iconStrokeColor: "#8C8C8C", | |||
| iconStrokeDisabledColor: '#C4C4C4', | |||
| imagePickerBackground: "#E4E4E4" | |||
| } | |||