Преглед на файлове

Partly finished feature 645

feature/645
jovan.cirkovic преди 3 години
родител
ревизия
461c8a04db
променени са 34 файла, в които са добавени 790 реда и са изтрити 149 реда
  1. 1
    2
      src/AppRoutes.js
  2. 52
    0
      src/components/Cards/CategoryCard/CategoryCard.js
  3. 39
    0
      src/components/Cards/CategoryCard/CategoryCard.styled.js
  4. 21
    0
      src/components/Cards/CategoryCard/CategoryCardName/CategoryCardName.js
  5. 21
    0
      src/components/Cards/CategoryCard/CategoryCardName/CategoryCardName.styled.js
  6. 25
    0
      src/components/Cards/CategoryCard/CategoryCheckButton/CategoryCheckButton.js
  7. 17
    0
      src/components/Cards/CategoryCard/CategoryCheckButton/CategoryCheckButton.styled.js
  8. 23
    0
      src/components/Cards/CategoryCard/CategoryDetail/CategoryDetail.js
  9. 53
    0
      src/components/Cards/CategoryCard/CategoryDetail/CategoryDetail.styled.js
  10. 20
    0
      src/components/Cards/CategoryCard/CategoryEditButton/CategoryEditButton.js
  11. 39
    0
      src/components/Cards/CategoryCard/CategoryEditButton/CategoryEditButton.styled.js
  12. 20
    0
      src/components/Cards/CategoryCard/CategoryRemoveButton/CategoryRemoveButton.js
  13. 38
    0
      src/components/Cards/CategoryCard/CategoryRemoveButton/CategoryRemoveButton.styled.js
  14. 3
    1
      src/components/Cards/ProfileCard/BigProfileCard/BigProfileCard.js
  15. 2
    2
      src/components/Cards/ProfileCard/BigProfileCard/BigProfileCard.styled.js
  16. 18
    10
      src/components/Header/Header.js
  17. 63
    34
      src/components/MarketPlace/Header/Header.js
  18. 13
    0
      src/components/MarketPlace/Header/Header.styled.js
  19. 17
    4
      src/components/MarketPlace/Offers/Offers.js
  20. 16
    14
      src/components/TextFields/SearchField/SearchField.js
  21. 5
    6
      src/components/TextFields/SearchField/SearchField.styled.js
  22. 27
    5
      src/constants/adminNavigation.js
  23. 3
    0
      src/constants/pages.js
  24. 15
    4
      src/hooks/useOffers/useSearch.js
  25. 11
    0
      src/i18n/resources/rs.js
  26. 71
    0
      src/pages/AdminCategoriesPage/AdminCategoriesPage.js
  27. 32
    0
      src/pages/AdminCategoriesPage/AdminCategoriesPage.styled.js
  28. 16
    7
      src/pages/AdminHomePage/AdminHomePage.js
  29. 5
    2
      src/store/actions/filters/filtersActionConstants.js
  30. 46
    30
      src/store/actions/filters/filtersActions.js
  31. 29
    16
      src/store/reducers/filters/filtersReducer.js
  32. 15
    11
      src/store/selectors/filtersSelectors.js
  33. 11
    0
      src/util/helpers/colorHelper.js
  34. 3
    1
      src/util/helpers/routeHelpers.js

+ 1
- 2
src/AppRoutes.js Целия файл

@@ -24,6 +24,7 @@ import {
ABOUT_PAGE,
ADMIN_HOME_PAGE,
ADMIN_USERS_PAGE,
ADMIN_CATEGORIES_PAGE,
// POLICY_PRIVACY_PAGE,
} from "./constants/pages";
import LoginPage from "./pages/LoginPage/LoginPage";
@@ -48,7 +49,6 @@ import AboutPage from "./pages/About/AboutPage";
import AuthRoute from "./components/Router/AuthRoute";
import AdminRoute from "./components/Router/AdminRoute";
import AdminHomePage from "./pages/AdminHomePage/AdminHomePage";
// import AdminUsersPage from "./pages/AdminUsersPage/AdminUsersPage";
// import PrivacyPolicyPage from "./pages/PrivacyPolicy/PrivacyPolicyPage";

const AppRoutes = () => {
@@ -58,7 +58,6 @@ const AppRoutes = () => {
<AuthRoute exact path={LOGIN_PAGE} component={LoginPage} />
<AuthRoute exact path={ADMIN_LOGIN_PAGE} component={AdminLoginPage} />
<AdminRoute path={ADMIN_HOME_PAGE} component={AdminHomePage} />
{/* <AdminRoute path={ADMIN_USERS_PAGE} component={AdminUsersPage} /> */}
<Route path={NOT_FOUND_PAGE} component={NotFoundPage} />
<Route path={ERROR_PAGE} component={ErrorPage} />
<AuthRoute

+ 52
- 0
src/components/Cards/CategoryCard/CategoryCard.js Целия файл

@@ -0,0 +1,52 @@
import React from "react";
import PropTypes from "prop-types";
import {
CategoryCardContainer,
CategoryCardDetailsContainer,
CategoryCardLeftContainer,
CategoryCardRightContainer,
} from "./CategoryCard.styled";
import CategoryCardName from "./CategoryCardName/CategoryCardName";
import CategoryDetail from "./CategoryDetail/CategoryDetail";
import CategoryCheckButton from "./CategoryCheckButton/CategoryCheckButton";
import CategoryEditButton from "./CategoryEditButton/CategoryEditButton";
import CategoryRemoveButton from "./CategoryRemoveButton/CategoryRemoveButton";
import { useTranslation } from "react-i18next";

const CategoryCard = (props) => {
const { t } = useTranslation();
return (
<CategoryCardContainer>
<CategoryCardLeftContainer>
<CategoryCardName categoryName={props?.category?.name} />
<CategoryCardDetailsContainer>
<CategoryDetail
label={t("admin.categories.noOfOffers")}
value={props?.category?.offerCount}
/>
{!props.hideSecondLabel && (
<CategoryDetail
label={props?.secondLabel}
value={props?.category?.subcategories?.length}
/>
)}
</CategoryCardDetailsContainer>
</CategoryCardLeftContainer>
<CategoryCardRightContainer>
<CategoryRemoveButton />
<CategoryEditButton />
{!props.hideCheckButton && <CategoryCheckButton />}
</CategoryCardRightContainer>
</CategoryCardContainer>
);
};

CategoryCard.propTypes = {
children: PropTypes.node,
category: PropTypes.object,
hideCheckButton: PropTypes.bool,
secondLabel: PropTypes.string,
hideSecondLabel: PropTypes.bool,
};

export default CategoryCard;

+ 39
- 0
src/components/Cards/CategoryCard/CategoryCard.styled.js Целия файл

@@ -0,0 +1,39 @@
import { Box } from "@mui/material";
import styled from "styled-components";
import selectedTheme from "../../../themes";

export const CategoryCardContainer = styled(Box)`
background: white;
height: 84px;
width: calc(100% - 10px);
margin: 5px;
margin-top: 13px;
margin-bottom: 13px;
border: 1px solid ${selectedTheme.colors.borderNormal};
border-radius: 4px;
display: flex;
flex-direction: row;
justify-content: space-between;
position: relative;
@media (max-width: 600px) {
height: 102px;
}
`;
export const CategoryCardLeftContainer = styled(Box)`
display: flex;
flex-direction: row;
`;
export const CategoryCardRightContainer = styled(Box)`
display: flex;
flex-direction: row;
`;
export const CategoryCardDetailsContainer = styled(Box)`
display: flex;
flex-direction: row;
@media (max-width: 600px) {
position: absolute;
bottom: 18px;
left: 0;
max-height: 16px;
}
`;

+ 21
- 0
src/components/Cards/CategoryCard/CategoryCardName/CategoryCardName.js Целия файл

@@ -0,0 +1,21 @@
import React from "react";
import PropTypes from "prop-types";
import {
CategoryCardNameContainer,
CategoryCardNameText,
} from "./CategoryCardName.styled";

const CategoryCardName = (props) => {
return (
<CategoryCardNameContainer>
<CategoryCardNameText>{props.categoryName}</CategoryCardNameText>
</CategoryCardNameContainer>
);
};

CategoryCardName.propTypes = {
children: PropTypes.node,
categoryName: PropTypes.string,
};

export default CategoryCardName;

+ 21
- 0
src/components/Cards/CategoryCard/CategoryCardName/CategoryCardName.styled.js Целия файл

@@ -0,0 +1,21 @@
import { Box, Typography } from "@mui/material";
import styled from "styled-components";
import selectedTheme from "../../../../themes";

export const CategoryCardNameContainer = styled(Box)`
padding: 18px;
min-width: 234px;
`;
export const CategoryCardNameText = styled(Typography)`
font-family: ${selectedTheme.fonts.textFont};
font-weight: 700;
font-size: 16px;
line-height: 21px;
padding-top: 12px;
color: ${selectedTheme.colors.primaryPurple};
cursor: pointer;
@media (max-width: 600px) {
font-size: 18px;
padding: 0;
}
`;

+ 25
- 0
src/components/Cards/CategoryCard/CategoryCheckButton/CategoryCheckButton.js Целия файл

@@ -0,0 +1,25 @@
import React from "react";
import PropTypes from "prop-types";
import selectedTheme from "../../../../themes";
import { CheckButton } from "./CategoryCheckButton.styled";
import { useTranslation } from "react-i18next";

const CategoryCheckButton = () => {
const { t } = useTranslation();
return (
<CheckButton
variant={"outlined"}
buttoncolor={selectedTheme.colors.primaryPurple}
textcolor={selectedTheme.colors.primaryPurple}
style={{ fontWeight: "600" }}
>
{t("admin.categories.checkCategory")}
</CheckButton>
);
};

CategoryCheckButton.propTypes = {
category: PropTypes.any,
};

export default CategoryCheckButton;

+ 17
- 0
src/components/Cards/CategoryCard/CategoryCheckButton/CategoryCheckButton.styled.js Целия файл

@@ -0,0 +1,17 @@
import styled from "styled-components";
import selectedTheme from "../../../../themes";
import { PrimaryButton } from "../../../Buttons/PrimaryButton/PrimaryButton";

export const CheckButton = styled(PrimaryButton)`
width: 224px;
height: 48px;
margin-top: 9px;
margin-right: 9px;
& button:hover {
background-color: ${selectedTheme.colors.primaryPurple} !important;
color: white !important;
}
@media (max-width: 850px) {
display: none;
}
`;

+ 23
- 0
src/components/Cards/CategoryCard/CategoryDetail/CategoryDetail.js Целия файл

@@ -0,0 +1,23 @@
import React from "react";
import PropTypes from "prop-types";
import {
CategoryDetailContainer,
CategoryDetailLabel,
CategoryDetailValue,
} from "./CategoryDetail.styled";

const CategoryDetail = (props) => {
return (
<CategoryDetailContainer>
<CategoryDetailLabel>{props.label}</CategoryDetailLabel>
<CategoryDetailValue>{props.value}</CategoryDetailValue>
</CategoryDetailContainer>
);
};

CategoryDetail.propTypes = {
label: PropTypes.string,
value: PropTypes.string,
};

export default CategoryDetail;

+ 53
- 0
src/components/Cards/CategoryCard/CategoryDetail/CategoryDetail.styled.js Целия файл

@@ -0,0 +1,53 @@
import { Box, Typography } from "@mui/material";
import styled from "styled-components";
import selectedTheme from "../../../../themes";
import { hexToRGB } from "../../../../util/helpers/colorHelper";

export const CategoryDetailContainer = styled(Box)`
/* display: flex;
flex-direction: row; */
margin-top: 20px;
margin-bottom: 20px;
min-width: 150px;
text-align: center;
border-left: 1px solid ${hexToRGB(selectedTheme.colors.primaryText, 0.13)};
@media (max-width: 600px) {
margin: 0;
min-width: 123px;
&:nth-child(1) {
border-left: 0;
}
&:nth-child(2) {
padding-left: 18px;
}
}
`;
export const CategoryDetailLabel = styled(Typography)`
font-family: ${selectedTheme.fonts.textFont};
color: ${selectedTheme.colors.primaryText};
font-size: 12px;
letter-spacing: 0.01rem;
padding-top: 14px;
padding-right: 3px;
@media (max-width: 600px) {
position: relative;
bottom: 2.5px;
padding-top: 0;
}
`;
export const CategoryDetailValue = styled(Typography)`
font-family: ${selectedTheme.fonts.textFont};
font-weight: 700;
font-size: 16px;
line-height: 21px;
padding-top: 11px;
color: ${selectedTheme.colors.primaryPurple};
@media (max-width: 600px) {
position: relative;
bottom: 2.5px;
padding: 0;
font-size: 12px;
font-weight: 600;
}
`;

+ 20
- 0
src/components/Cards/CategoryCard/CategoryEditButton/CategoryEditButton.js Целия файл

@@ -0,0 +1,20 @@
import React from "react";
import PropTypes from "prop-types";
import {
CategoryEditButtonContainer,
EditIcon,
} from "./CategoryEditButton.styled";

const CategoryEditButton = () => {
return (
<CategoryEditButtonContainer>
<EditIcon />
</CategoryEditButtonContainer>
);
};

CategoryEditButton.propTypes = {
category: PropTypes.any,
};

export default CategoryEditButton;

+ 39
- 0
src/components/Cards/CategoryCard/CategoryEditButton/CategoryEditButton.styled.js Целия файл

@@ -0,0 +1,39 @@
import styled from "styled-components";
import selectedTheme from "../../../../themes";
import { IconButton } from "../../../Buttons/IconButton/IconButton";
import { ReactComponent as Edit } from "../../../../assets/images/svg/edit.svg";

export const CategoryEditButtonContainer = styled(IconButton)`
width: 40px;
height: 40px;
background-color: ${selectedTheme.colors.primaryIconBackgroundColor};
border-radius: 100%;
position: relative;
top: 22px;
margin-right: 18px;
padding-top: 2px;
text-align: center;
@media (max-width: 600px) {
width: 32px;
height: 32px;
top: 18px;
right: 18px;
margin-right: 0;
padding: 0;
${(props) =>
props.vertical &&
`
display: none;
`}
& button svg {
width: 16px;
height: 16px;
position: relative;
top: -3px;
left: -1.5px;
}
}
`;
export const EditIcon = styled(Edit)`
`

+ 20
- 0
src/components/Cards/CategoryCard/CategoryRemoveButton/CategoryRemoveButton.js Целия файл

@@ -0,0 +1,20 @@
import React from "react";
import PropTypes from "prop-types";
import {
CategoryRemoveButtonContainer,
RemoveIcon,
} from "./CategoryRemoveButton.styled";

const CategoryRemoveButton = () => {
return (
<CategoryRemoveButtonContainer>
<RemoveIcon />
</CategoryRemoveButtonContainer>
);
};

CategoryRemoveButton.propTypes = {
category: PropTypes.any,
};

export default CategoryRemoveButton;

+ 38
- 0
src/components/Cards/CategoryCard/CategoryRemoveButton/CategoryRemoveButton.styled.js Целия файл

@@ -0,0 +1,38 @@
import styled from "styled-components";
import selectedTheme from "../../../../themes";
import { IconButton } from "../../../Buttons/IconButton/IconButton";
import { ReactComponent as Remove } from "../../../../assets/images/svg/trash.svg";

export const CategoryRemoveButtonContainer = styled(IconButton)`
width: 40px;
height: 40px;
background-color: ${selectedTheme.colors.primaryIconBackgroundColor};
border-radius: 100%;
position: relative;
top: 22px;
margin-right: 18px;
padding-top: 2px;
text-align: center;
@media (max-width: 600px) {
width: 32px;
height: 32px;
top: 18px;
right: 12px;
padding: 0;
${(props) =>
props.vertical &&
`
display: none;
`}
& button svg {
width: 16px;
height: 16px;
position: relative;
top: -3px;
left: -1.5px;
}
}
`;
export const RemoveIcon = styled(Remove)`
`

+ 3
- 1
src/components/Cards/ProfileCard/BigProfileCard/BigProfileCard.js Целия файл

@@ -28,7 +28,7 @@ const BigProfileCard = (props) => {
const blockUser = () => {};
return (
<>
<ProfileCardContainer>
<ProfileCardContainer halfwidth={props.halfwidth}>
<ProfileCardWrapper variant="outlined">
<EditButton onClick={() => setEditProfileModal(true)}>
<EditIcon />
@@ -46,6 +46,7 @@ const BigProfileCard = (props) => {
<ProfileContact profile={props.profile} isAdmin />
</ProfileInfoContainer>
<CheckButton
halfwidth={props.halfwidth}
variant={"outlined"}
buttoncolor={selectedTheme.colors.primaryPurple}
textcolor={selectedTheme.colors.primaryPurple}
@@ -70,6 +71,7 @@ const BigProfileCard = (props) => {

BigProfileCard.propTypes = {
profile: PropTypes.any,
halfwidth: PropTypes.bool,
};

export default BigProfileCard;

+ 2
- 2
src/components/Cards/ProfileCard/BigProfileCard/BigProfileCard.styled.js Целия файл

@@ -12,7 +12,7 @@ import { PrimaryButton } from "../../../Buttons/PrimaryButton/PrimaryButton";
// import { ReactComponent as Location } from "../../../assets/images/svg/location.svg";

export const ProfileCardContainer = styled(Box)`
width: 100%;
width: ${(props) => (props.halfwidth ? `49%` : `100%`)};
box-sizing: border-box;
max-height: 184px;
margin-top: 34px;
@@ -30,7 +30,6 @@ export const EditIcon = styled(Edit)`
}
`;


export const MessageButton = styled(IconButton)`
width: 40px;
height: 40px;
@@ -92,6 +91,7 @@ export const CheckButton = styled(PrimaryButton)`
position: absolute;
bottom: 25px;
right: 9px;
display: ${(props) => (props.halfwidth ? `none` : `block`)};
& button:hover {
background-color: ${selectedTheme.colors.primaryPurple} !important;
color: white !important;

+ 18
- 10
src/components/Header/Header.js Целия файл

@@ -33,6 +33,7 @@ import MyMessagesButton from "./MyMessagesButton/MyMessagesButton";
import UserButton from "./UserButton/UserButton";
import LoginButton from "./LoginButton/LoginButton";
import RegisterButton from "./RegisterButton/RegisterButton";
import useIsMobile from "../../hooks/useIsMobile";

const Header = () => {
const [showCreateOfferModal, setShowCreateOfferModal] = useState(false);
@@ -47,12 +48,13 @@ const Header = () => {
const drawerRef = useRef(null);
const [shouldShow, setShouldShow] = useState(true);
const routeMatch = useRouteMatch();
const { isMobile } = useIsMobile();

// Dont show header on auth routes(login, register, etc.) and admin routes
useEffect(() => {
if (isAuthRoute() || isAdminRoute()) setShouldShow(false);
if (isAuthRoute() || (isAdminRoute() && !isMobile)) setShouldShow(false);
else setShouldShow(true);
}, [routeMatch]);
}, [routeMatch, isMobile]);

// Fetch mine profile on loading home page
useEffect(() => {
@@ -76,14 +78,19 @@ const Header = () => {
}
// Handling search when user is on marketplace and when he is not
const handleSearch = (value) => {
if (!routeMatches(HOME_PAGE) && !routeMatches(BASE_PAGE)) {
const newQueryString = new URLSearchParams({ search: value });
history.push({
pathname: HOME_PAGE,
search: newQueryString.toString(),
});
if (isAdminRoute()) {
console.log("admin");
search.setSearchStringManually(value);
} else {
search.searchOffersImmediately(value);
if (!routeMatches(HOME_PAGE) && !routeMatches(BASE_PAGE)) {
const newQueryString = new URLSearchParams({ search: value });
history.push({
pathname: HOME_PAGE,
search: newQueryString.toString(),
});
} else {
search.searchOffersImmediately(value);
}
}
};

@@ -105,7 +112,7 @@ const Header = () => {
};

if (!shouldShow) {
return (<></>)
return <></>;
}

return (
@@ -180,6 +187,7 @@ const Header = () => {
Header.propTypes = {
isGrid: PropTypes.bool,
showModalHandler: PropTypes.func,
manualSearch: PropTypes.func,
};

export default Header;

+ 63
- 34
src/components/MarketPlace/Header/Header.js Целия файл

@@ -4,6 +4,7 @@ import {
ButtonContainer,
// ButtonContainer,
CategoryHeaderIcon,
CategoryIcon,
HeaderAltLocation,
HeaderButton,
HeaderButtons,
@@ -89,6 +90,7 @@ const Header = (props) => {
<>
<SkeletonHeader skeleton={props?.skeleton} myOffers={props?.myOffers} />
<HeaderWrapperContainer
className={props.className}
skeleton={props?.skeleton}
isAdmin={props?.isAdmin}
>
@@ -129,18 +131,28 @@ const Header = (props) => {
<>
{!isMobile ? (
<HeaderTitleContainer>
{props.users ? <UserIcon /> : <SwapsHeaderIcon />}
{props.users ? (
<UserIcon />
) : props.categories ? (
<CategoryIcon />
) : (
<SwapsHeaderIcon />
)}
<HeaderTitleText>
{props.users
? t("admin.users.headerTitle")
: props.categories
? t("admin.categories.headerTitle")
: t("header.myOffers")}
</HeaderTitleText>
</HeaderTitleContainer>
) : (
<ButtonContainer onClick={goBack}>
<ArrowButton side={"left"}></ArrowButton>
<HeaderText>{t("messages.goBack")}</HeaderText>
</ButtonContainer>
!props.hideBackButton && (
<ButtonContainer onClick={goBack}>
<ArrowButton side={"left"}></ArrowButton>
<HeaderText>{t("messages.goBack")}</HeaderText>
</ButtonContainer>
)
)}
</>
)}
@@ -149,33 +161,35 @@ const Header = (props) => {
{/* ^^^^^^ */}

<HeaderOptions>
<HeaderButtons>
{/* Setting display of offer cards to full width */}
<HeaderButton
iconColor={
props?.isGrid
? selectedTheme.colors.iconStrokeColor
: selectedTheme.colors.primaryPurple
}
onClick={() => props?.setIsGrid(false)}
>
<GridLine />
</HeaderButton>
{/* ^^^^^^ */}
{!props.hideGrid && (
<HeaderButtons>
{/* Setting display of offer cards to full width */}
<HeaderButton
iconColor={
props?.isGrid
? selectedTheme.colors.iconStrokeColor
: selectedTheme.colors.primaryPurple
}
onClick={() => props?.setIsGrid(false)}
>
<GridLine />
</HeaderButton>
{/* ^^^^^^ */}

{/* Setting display of offer cards to half width (Grid) */}
<HeaderButton
iconColor={
props?.isGrid
? selectedTheme.colors.primaryPurple
: selectedTheme.colors.iconStrokeColor
}
onClick={() => props?.setIsGrid(true)}
>
<GridSquare />
</HeaderButton>
{/* ^^^^^^ */}
</HeaderButtons>
{/* Setting display of offer cards to half width (Grid) */}
<HeaderButton
iconColor={
props?.isGrid
? selectedTheme.colors.primaryPurple
: selectedTheme.colors.iconStrokeColor
}
onClick={() => props?.setIsGrid(true)}
>
<GridSquare />
</HeaderButton>
{/* ^^^^^^ */}
</HeaderButtons>
)}

{/* Select option to choose sorting */}
<HeaderSelect
@@ -208,9 +222,21 @@ const Header = (props) => {
</HeaderContainer>
{isMobile && (
<PageTitleContainer>
<SwapsIcon />
{props.users ? (
<UserIcon />
) : props.categories ? (
<CategoryIcon />
) : (
<SwapsIcon />
)}
<SwapsTitle>
{props?.myOffers ? t("header.myOffers") : t("offer.offers")}
{props.users
? t("admin.users.headerTitle")
: props.categories
? t("admin.categories.headerTitle")
: props?.myOffers
? t("header.myOffers")
: t("offer.offers")}
</SwapsTitle>
</PageTitleContainer>
)}
@@ -224,12 +250,15 @@ Header.propTypes = {
setIsGrid: PropTypes.func,
isGrid: PropTypes.bool,
offers: PropTypes.any,
category: PropTypes.string,
myOffers: PropTypes.bool,
skeleton: PropTypes.bool,
sorting: PropTypes.any,
isAdmin: PropTypes.bool,
users: PropTypes.bool,
categories: PropTypes.bool,
hideGrid: PropTypes.bool,
className: PropTypes.string,
hideBackButton: PropTypes.bool,
};
Header.defaultProps = {
isGrid: false,

+ 13
- 0
src/components/MarketPlace/Header/Header.styled.js Целия файл

@@ -7,6 +7,7 @@ import Select from "../../Select/Select";
import { ReactComponent as Swaps } from "../../../assets/images/svg/swaps.svg";
import { ReactComponent as CategoryHeader } from "../../../assets/images/svg/category-header.svg";
import { ReactComponent as User } from "../../../assets/images/svg/user.svg";
import { ReactComponent as Category } from "../../../assets/images/svg/category.svg";

export const HeaderWrapperContainer = styled(Box)`
display: ${(props) => (props.skeleton ? "none" : "block")};
@@ -194,11 +195,23 @@ export const CategoryHeaderIcon = styled(CategoryHeader)`
top: 1px;
}
`;
export const CategoryIcon = styled(Category)`
position: relative;
top: 4px;
right: 2px;
`;
export const PageTitleContainer = styled(Box)`
position: relative;
left: 6px;
margin-top: 36px;
width: 100px;
@media (max-width: 600px) {
& svg {
width: 12px;
height: 12px;
top: 2px;
}
}
`;
export const SwapsIcon = styled(RefreshIcon)`
width: 12px;

+ 17
- 4
src/components/MarketPlace/Offers/Offers.js Целия файл

@@ -8,14 +8,17 @@ import { selectLatestChats } from "../../../store/selectors/chatSelectors";
import { selectUserId } from "../../../store/selectors/loginSelectors";
import { startChat } from "../../../util/helpers/chatHelper";
import OffersNotFound from "./OffersNotFound";
import HeadersMyOffers from "./HeaderMyOffers.js/HeadersMyOffers";
// import HeadersMyOffers from "./SearchBar/SearchBar";
import SkeletonOfferCard from "../../Cards/OfferCard/SkeletonOfferCard/SkeletonOfferCard";
import BigProfileCard from "../../Cards/ProfileCard/BigProfileCard/BigProfileCard";
import SearchField from "../../TextFields/SearchField/SearchField";
import { useTranslation } from "react-i18next";

const Offers = (props) => {
const chats = useSelector(selectLatestChats);
const offersRef = useRef(null);
const userId = useSelector(selectUserId);
const { t } = useTranslation();
const offers = props?.offers;
// For skeleton screen
const arrayForMapping = Array.apply(null, Array(4)).map(() => {});
@@ -40,12 +43,18 @@ const Offers = (props) => {
<FilterIcon />
</FilterContainer>
{(props?.myOffers || props?.isAdmin) && (
<HeadersMyOffers
// <HeadersMyOffers
<SearchField
searchMyOffers={offers?.search?.searchOffers}
handleSearch={offers?.apply}
isAdmin={props?.isAdmin}
offers={offers}
isUsers={props.isUsers}
placeholder={
props.isUsers
? t("admin.users.searchPlaceholder")
: t("header.searchOffers")
}
/>
)}
{offers?.allOffersToShow?.length === 0 ? (
@@ -54,7 +63,11 @@ const Offers = (props) => {
<OffersContainer ref={offersRef}>
{props.isUsers
? props.users?.map((item) => (
<BigProfileCard key={item._id} profile={item} />
<BigProfileCard
key={item._id}
profile={item}
halfwidth={props?.isGrid}
/>
))
: offers?.allOffersToShow?.map((item) => {
return (
@@ -102,7 +115,7 @@ Offers.propTypes = {

Offers.defaultProps = {
myOffers: false,
users: []
users: [],
};

export default Offers;

src/components/MarketPlace/Offers/HeaderMyOffers.js/HeadersMyOffers.js → src/components/TextFields/SearchField/SearchField.js Целия файл

@@ -1,13 +1,11 @@
import React, { useCallback, useEffect, useRef } from "react";
import PropTypes from "prop-types";
import { EndIcon, SearchIcon, SearchInput } from "./HeadersMyOffers.styled";
import { useTranslation } from "react-i18next";
import useSearch from "../../../../hooks/useOffers/useSearch";
import { EndIcon, SearchIcon, SearchInput } from "./SearchField.styled";
import useSearch from "../../../hooks/useOffers/useSearch";

const HeadersMyOffers = (props) => {
const SearchField = (props) => {
const searchRef = useRef(null);
const search = useSearch(() => {});
const { t } = useTranslation();
let listener = useCallback(
(event) => {
// Event keycode 13 = ENTER keycode
@@ -30,7 +28,8 @@ const HeadersMyOffers = (props) => {
const handleSearch = () => {
if (props.isAdmin) {
console.log(searchRef.current.value);
search.searchOffersImmediately(searchRef.current.value);
if (props.handleSearch) props.handleSearch(searchRef.current.value);
else search.searchOffersImmediately(searchRef.current.value);
} else {
props.searchMyOffers(searchRef.current.value);
props.handleSearch();
@@ -39,7 +38,7 @@ const HeadersMyOffers = (props) => {
return (
<SearchInput
isAdmin={props.isAdmin}
fullWidth
fullWidth={props.fullWidth}
InputProps={{
endAdornment: (
<EndIcon size="36px">
@@ -47,25 +46,28 @@ const HeadersMyOffers = (props) => {
</EndIcon>
),
}}
placeholder={
props.isUsers
? t("admin.users.searchPlaceholder")
: t("header.searchOffers")
}
placeholder={props.placeholder}
onFocus={handleFocusSearch}
onBlur={handleBlurSearch}
ref={searchRef}
className={props.className}
/>
);
};

HeadersMyOffers.propTypes = {
SearchField.propTypes = {
children: PropTypes.node,
searchMyOffers: PropTypes.func,
handleSearch: PropTypes.func,
isAdmin: PropTypes.bool,
offers: PropTypes.any,
isUsers: PropTypes.bool,
fullWidth: PropTypes.bool,
className: PropTypes.string,
placeholder: PropTypes.string,
};
SearchField.defaultProps = {
fullWidth: true,
};

export default HeadersMyOffers;
export default SearchField;

src/components/MarketPlace/Offers/HeaderMyOffers.js/HeadersMyOffers.styled.js → src/components/TextFields/SearchField/SearchField.styled.js Целия файл

@@ -1,11 +1,10 @@
import { Box } from "@mui/material";
import styled from "styled-components";
import { Icon } from "../../../Icon/Icon";
import { ReactComponent as Search } from "../../../../assets/images/svg/magnifying-glass.svg";
import selectedTheme from "../../../../themes";
import { TextField } from "../../../TextFields/TextField/TextField";
import selectedTheme from "../../../themes";
import { Icon } from "../../Icon/Icon";
import { TextField } from "../TextField/TextField";
import { ReactComponent as Search } from "../../../assets/images/svg/magnifying-glass.svg";


export const HeadersMyOffersContainer = styled(Box)``;
export const EndIcon = styled(Icon)``;
export const SearchIcon = styled(Search)`
position: relative;

+ 27
- 5
src/constants/adminNavigation.js Целия файл

@@ -3,11 +3,33 @@ import { ReactComponent as UserIcon } from "../assets/images/svg/user-gray.svg";
import { ReactComponent as CategoryIcon } from "../assets/images/svg/category.svg";
import { ReactComponent as LocationIcon } from "../assets/images/svg/location.svg";
import { ReactComponent as DollarIcon } from "../assets/images/svg/dollar-sign.svg";
import { ADMIN_USERS_PAGE } from "./pages";
import {
ADMIN_CATEGORIES_PAGE,
ADMIN_LOCATIONS_PAGE,
ADMIN_PAYMENT_PAGE,
ADMIN_USERS_PAGE,
} from "./pages";
import i18n from "../i18n";

export const ADMIN_NAVIGATION = [
{ text: "Korisnici", icon: <UserIcon />, route: `${ADMIN_USERS_PAGE}` },
{ text: "Kategorije", icon: <CategoryIcon />, route: "/admin/categoires" },
{ text: "Lokacije", icon: <LocationIcon />, route: "/admin/locations" },
{ text: "Uplate", icon: <DollarIcon />, route: "/admin/payment" },
{
text: i18n.t("admin.navigation.users"),
icon: <UserIcon />,
route: `${ADMIN_USERS_PAGE}`,
},
{
text: i18n.t("admin.navigation.categories"),
icon: <CategoryIcon />,
route: `${ADMIN_CATEGORIES_PAGE}`,
},
{
text: i18n.t("admin.navigation.locations"),
icon: <LocationIcon />,
route: `${ADMIN_LOCATIONS_PAGE}`,
},
{
text: i18n.t("admin.navigation.payment"),
icon: <DollarIcon />,
route: `${ADMIN_PAYMENT_PAGE}`,
},
];

+ 3
- 0
src/constants/pages.js Целия файл

@@ -20,3 +20,6 @@ export const PRICES_PAGE = "/prices";
export const POLICY_PRIVACY_PAGE = "/policy";
export const ADMIN_HOME_PAGE = "/admin";
export const ADMIN_USERS_PAGE = "/admin/users";
export const ADMIN_CATEGORIES_PAGE = "/admin/categories";
export const ADMIN_LOCATIONS_PAGE = "/admin/locations";
export const ADMIN_PAYMENT_PAGE = "/admin/payment";

+ 15
- 4
src/hooks/useOffers/useSearch.js Целия файл

@@ -3,8 +3,14 @@ import { useDispatch, useSelector } from "react-redux";
import { useHistory } from "react-router-dom";
import { BASE_PAGE, HOME_PAGE } from "../../constants/pages";
import { KEY_SEARCH } from "../../constants/queryStringConstants";
import { setSearchString } from "../../store/actions/filters/filtersActions";
import { selectSearchString } from "../../store/selectors/filtersSelectors";
import {
setManualSearchString,
setSearchString,
} from "../../store/actions/filters/filtersActions";
import {
selectManualSearchString,
selectSearchString,
} from "../../store/selectors/filtersSelectors";
import { routeMatches } from "../../util/helpers/routeHelpers";

const useSearch = (applyAllFilters) => {
@@ -13,6 +19,7 @@ const useSearch = (applyAllFilters) => {
const dispatch = useDispatch();
const searchString = useSelector(selectSearchString);
const history = useHistory();
const manualSearchString = useSelector(selectManualSearchString);

// On every global change of search string, new request to backend should be sent
useEffect(() => {
@@ -48,18 +55,20 @@ const useSearch = (applyAllFilters) => {
}, [searchStringLocally]);

const searchOffers = (searchValue) => {
console.log('searchoff')
setIsInitiallyLoaded(true);
setSearchStringLocally(searchValue);
};

const searchOffersImmediately = (searchValue) => {
console.log('searchoffersimm');
setIsInitiallyLoaded(true);
searchOffers(searchValue);
applyAllFilters();
};

const setSearchStringManually = (searchValue) => {
dispatch(setManualSearchString(searchValue));
};

const clear = () => {
setSearchStringLocally("");
};
@@ -67,9 +76,11 @@ const useSearch = (applyAllFilters) => {
return {
searchOffers,
setSearchStringLocally,
setSearchStringManually,
searchOffersImmediately,
searchStringLocally,
searchString,
manualSearchString,
clear,
};
};

+ 11
- 0
src/i18n/resources/rs.js Целия файл

@@ -432,7 +432,18 @@ export default {
navigation: {
role: "Administrator",
menu: "Meni",
users: "Korisnici",
categories: "Kategorije",
locations: "Lokacije",
payment: "Uplate",
logout: "Odjavi se",
},
categories: {
checkCategory: "Pogledaj podkategorije",
noOfOffers: "Broj objava: ",
noOfSubcategories: "Broj potkategorija: ",
headerTitle: "Kategorije",
placeholder: "Pretražite kategorije...",
},
},
};

+ 71
- 0
src/pages/AdminCategoriesPage/AdminCategoriesPage.js Целия файл

@@ -0,0 +1,71 @@
import React from "react";
import PropTypes from "prop-types";
import CategoryCard from "../../components/Cards/CategoryCard/CategoryCard";
import { useDispatch, useSelector } from "react-redux";
import { useEffect } from "react";
import { fetchCategories } from "../../store/actions/categories/categoriesActions";
import { selectCategories } from "../../store/selectors/categoriesSelectors";
import { useTranslation } from "react-i18next";
import {
AdminCategoriesHeader,
AdminCategoriesPageContainer,
AdminCategoriesSearchField,
} from "./AdminCategoriesPage.styled";
import { selectManualSearchString } from "../../store/selectors/filtersSelectors";
import { useMemo } from "react";
import { setManualSearchString } from "../../store/actions/filters/filtersActions";

const AdminCategoriesPage = () => {
const { t } = useTranslation();
const dispatch = useDispatch();
const categories = useSelector(selectCategories);
const manualSearchString = useSelector(selectManualSearchString);
useEffect(() => {
dispatch(fetchCategories());
}, []);
const handleSearch = (value) => {
console.log(value);
dispatch(setManualSearchString(value));
};
const categoriesToShow = useMemo(() => {
if (categories) {
console.log(categories);
console.log(manualSearchString);
if (manualSearchString)
return categories.filter((item) =>
item.name.toLowerCase().includes(manualSearchString.toLowerCase())
);
return categories;
}
return [];
}, [categories, manualSearchString]);
return (
<AdminCategoriesPageContainer>
<AdminCategoriesSearchField
isAdmin
handleSearch={handleSearch}
placeholder={t("admin.categories.placeholder")}
/>
<AdminCategoriesHeader
myOffers
categories
hideGrid
isAdmin
hideBackButton
/>
{categoriesToShow.map((category) => (
<CategoryCard
key={category._id}
category={category}
secondLabel={t("admin.categories.noOfSubcategories")}
/>
))}
</AdminCategoriesPageContainer>
);
};

AdminCategoriesPage.propTypes = {
children: PropTypes.node,
};

export default AdminCategoriesPage;

+ 32
- 0
src/pages/AdminCategoriesPage/AdminCategoriesPage.styled.js Целия файл

@@ -0,0 +1,32 @@
import { Box } from "@mui/material";
import styled from "styled-components";
import Header from "../../components/MarketPlace/Header/Header";
import SearchField from "../../components/TextFields/SearchField/SearchField";

export const AdminCategoriesPageContainer = styled(Box)`
padding: 15px 60px 60px 60px;
@media (max-width: 600px) {
padding: 18px;
position: relative;
top: 65px;
}
`;
export const AdminCategoriesHeader = styled(Header)`
/* top: 40px; */
top: 0;
@media (max-width: 600px) {
top: -10px;
& div {
margin-top: 0;
}
& div div:nth-child(1) {
top: 22px;
}
}
`;
export const AdminCategoriesSearchField = styled(SearchField)`
top: -15px;
@media (max-width: 600px) {
display: none;
}
`;

+ 16
- 7
src/pages/AdminHomePage/AdminHomePage.js Целия файл

@@ -9,10 +9,15 @@ import { useSelector } from "react-redux";
// import { OFFERS_SCOPE } from "../../store/actions/offers/offersActionConstants";
import AdminUsersPage from "../AdminUsersPage/AdminUsersPage";
import { selectMineProfile } from "../../store/selectors/profileSelectors";
import { Route, useHistory } from "react-router-dom";
import { ADMIN_USERS_PAGE, HOME_PAGE } from "../../constants/pages";
import { Switch, useHistory } from "react-router-dom";
import {
ADMIN_CATEGORIES_PAGE,
ADMIN_USERS_PAGE,
HOME_PAGE,
} from "../../constants/pages";
import { selectUserId } from "../../store/selectors/loginSelectors";
// import AdminRoute from "../../components/Router/AdminRoute";
import AdminCategoriesPage from "../AdminCategoriesPage/AdminCategoriesPage";
import AdminRoute from "../../components/Router/AdminRoute";

const AdminHomePage = () => {
const profile = useSelector(selectMineProfile);
@@ -34,10 +39,14 @@ const AdminHomePage = () => {
leftCard={<Sidebar />}
content={
// <MarketPlace offers={offers} isAdmin skeleton={isLoadingOffers} />
<>
<Route path={ADMIN_USERS_PAGE} component={AdminUsersPage} />
<Route component={AdminUsersPage} />
</>
<Switch>
<AdminRoute path={ADMIN_USERS_PAGE} component={AdminUsersPage} />
<AdminRoute
path={ADMIN_CATEGORIES_PAGE}
component={AdminCategoriesPage}
/>
<AdminRoute component={AdminUsersPage} />
</Switch>
}
/>
);

+ 5
- 2
src/store/actions/filters/filtersActionConstants.js Целия файл

@@ -1,6 +1,6 @@
import { createClearType, createSetType } from "../actionHelpers"
import { createClearType, createSetType } from "../actionHelpers";

const FILTERS_SCOPE = "FILTERS"
const FILTERS_SCOPE = "FILTERS";
export const SET_FILTERS = createSetType(FILTERS_SCOPE);
export const CLEAR_FILTERS = createClearType(FILTERS_SCOPE);
export const SET_CATEGORY = createSetType("FILTERS_SET_CATEGORY");
@@ -11,3 +11,6 @@ export const SET_IS_APPLIED = createSetType("FILTERS_SET_IS_APPLIED");
export const SET_QUERY_STRING = createSetType("FILTERS_SET_QUERY_STRING");
export const SET_HEADER_STRING = createSetType("FILTERS_SET_HEADER_STRING");
export const SET_SEARCH_STRING = createSetType("FILTERS_SET_SEARCH");
export const SET_MANUAL_SEARCH_STRING = createSetType(
"FILTERS_SET_MANUAL_SEARCH_STRING"
);

+ 46
- 30
src/store/actions/filters/filtersActions.js Целия файл

@@ -1,41 +1,57 @@
import { CLEAR_FILTERS, SET_CATEGORY, SET_FILTERS, SET_HEADER_STRING, SET_IS_APPLIED, SET_LOCATIONS, SET_QUERY_STRING, SET_SEARCH_STRING, SET_SORT_OPTION, SET_SUBCATEGORY } from "./filtersActionConstants";
import {
CLEAR_FILTERS,
SET_CATEGORY,
SET_FILTERS,
SET_HEADER_STRING,
SET_IS_APPLIED,
SET_LOCATIONS,
SET_MANUAL_SEARCH_STRING,
SET_QUERY_STRING,
SET_SEARCH_STRING,
SET_SORT_OPTION,
SET_SUBCATEGORY,
} from "./filtersActionConstants";

export const setFilters = (payload) => ({
type: SET_FILTERS,
payload,
})
type: SET_FILTERS,
payload,
});
export const clearFilters = () => ({
type: CLEAR_FILTERS
})
type: CLEAR_FILTERS,
});
export const setFilteredCategory = (payload) => ({
type: SET_CATEGORY,
payload
})
type: SET_CATEGORY,
payload,
});
export const setFilteredSubcategory = (payload) => ({
type: SET_SUBCATEGORY,
payload
})
type: SET_SUBCATEGORY,
payload,
});
export const setManualSearchString = (payload) => ({
type: SET_MANUAL_SEARCH_STRING,
payload,
});
export const setFilteredLocations = (payload) => ({
type: SET_LOCATIONS,
payload
})
type: SET_LOCATIONS,
payload,
});
export const setFilteredSortOption = (payload) => ({
type: SET_SORT_OPTION,
payload
})
type: SET_SORT_OPTION,
payload,
});
export const setIsAppliedStatus = (payload) => ({
type: SET_IS_APPLIED,
payload,
})
type: SET_IS_APPLIED,
payload,
});
export const setQueryString = (payload) => ({
type: SET_QUERY_STRING,
payload,
})
type: SET_QUERY_STRING,
payload,
});
export const setHeaderString = (payload) => ({
type: SET_HEADER_STRING,
payload
})
type: SET_HEADER_STRING,
payload,
});
export const setSearchString = (payload) => ({
type: SET_SEARCH_STRING,
payload
})
type: SET_SEARCH_STRING,
payload,
});

+ 29
- 16
src/store/reducers/filters/filtersReducer.js Целия файл

@@ -5,6 +5,7 @@ import {
SET_HEADER_STRING,
SET_IS_APPLIED,
SET_LOCATIONS,
SET_MANUAL_SEARCH_STRING,
SET_SEARCH_STRING,
SET_SORT_OPTION,
SET_SUBCATEGORY,
@@ -23,9 +24,10 @@ const initialState = {
categoryString: "",
subcategoryString: "",
locationsString: "",
text: ""
text: "",
},
searchString: ""
searchString: "",
manualSearchString: "",
},
};

@@ -40,10 +42,21 @@ export default createReducer(
[SET_IS_APPLIED]: setIsAppliedStatus,
[SET_HEADER_STRING]: setHeaderString,
[SET_SEARCH_STRING]: setSearchString,
[SET_MANUAL_SEARCH_STRING]: setManualSearchString,
},
initialState
);

function setManualSearchString(state, { payload }) {
return {
...state,
filters: {
...state.filters,
manualSearchString: payload,
},
};
}

function setFilters(state, { payload }) {
return {
...state,
@@ -84,39 +97,39 @@ function setFilteredLocations(state, { payload }) {
},
};
}
function setFilteredSortOption(state, {payload}) {
function setFilteredSortOption(state, { payload }) {
return {
...state,
filters: {
...state.filters,
sortOption: payload,
}
}
},
};
}
function setIsAppliedStatus(state, {payload}) {
function setIsAppliedStatus(state, { payload }) {
return {
...state,
filters: {
...state.filters,
isApplied: payload,
}
}
},
};
}
function setHeaderString(state, {payload}) {
function setHeaderString(state, { payload }) {
return {
...state,
filters: {
...state.filters,
headerString: payload
}
}
headerString: payload,
},
};
}
function setSearchString(state, {payload}) {
function setSearchString(state, { payload }) {
return {
...state,
filters: {
...state.filters,
searchString: payload
}
}
searchString: payload,
},
};
}

+ 15
- 11
src/store/selectors/filtersSelectors.js Целия файл

@@ -1,36 +1,40 @@
import { createSelector } from 'reselect';
import { createSelector } from "reselect";

const filtersSelector = (state) => state.filters;

export const selectFilters = createSelector(
filtersSelector,
(state) => state.filters,
(state) => state.filters
);
export const selectSelectedCategory = createSelector(
filtersSelector,
(state => state.filters.category)
)
(state) => state.filters.category
);
export const selectSelectedSubcategory = createSelector(
filtersSelector,
(state) => state.filters.subcategory
)
);
export const selectSelectedLocations = createSelector(
filtersSelector,
(state) => state.filters.locations
)
);
export const selectSelectedSortOption = createSelector(
filtersSelector,
(state) => state.filters.sortOption
)
);
export const selectAppliedStatus = createSelector(
filtersSelector,
(state) => state.filters.isApplied
)
);
export const selectHeaderString = createSelector(
filtersSelector,
(state) => state.filters.headerString
)
);
export const selectSearchString = createSelector(
filtersSelector,
(state) => state.filters.searchString,
)
(state) => state.filters.searchString
);
export const selectManualSearchString = createSelector(
filtersSelector,
(state) => state.filters.manualSearchString
);

+ 11
- 0
src/util/helpers/colorHelper.js Целия файл

@@ -0,0 +1,11 @@
export const hexToRGB = (hex, opacity) => {
var r = parseInt(hex.slice(1, 3), 16),
g = parseInt(hex.slice(3, 5), 16),
b = parseInt(hex.slice(5, 7), 16);

if (opacity) {
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
} else {
return `rgb(${r}, ${g}, ${b})`;
}
}

+ 3
- 1
src/util/helpers/routeHelpers.js Целия файл

@@ -1,4 +1,5 @@
import {
ADMIN_CATEGORIES_PAGE,
ADMIN_HOME_PAGE,
ADMIN_LOGIN_PAGE,
ADMIN_USERS_PAGE,
@@ -46,7 +47,8 @@ export const isAdminRoute = () => {
if (
routeMatches(ADMIN_LOGIN_PAGE) ||
routeMatches(ADMIN_HOME_PAGE) ||
routeMatches(ADMIN_USERS_PAGE)
routeMatches(ADMIN_USERS_PAGE) ||
routeMatches(ADMIN_CATEGORIES_PAGE)
)
return true;
return false;

Loading…
Отказ
Запис