Przeglądaj źródła

Finished feature 657 and merged with master

feature/657
Djordje Mitrovic 3 lat temu
rodzic
commit
be44196c7c
33 zmienionych plików z 564 dodań i 91 usunięć
  1. 2
    14
      src/AppRoutes.js
  2. 4
    0
      src/assets/images/svg/dollar-sign.svg
  3. 94
    0
      src/components/Admin/Sidebar/Sidebar.js
  4. 125
    0
      src/components/Admin/Sidebar/Sidebar.styled.js
  5. 2
    1
      src/components/Cards/ProfileCard/ProfileStats/ProfileStats.js
  6. 1
    1
      src/components/ItemDetails/ItemDetails.styled.js
  7. 8
    0
      src/components/ProfileMini/ProfileMini.js
  8. 14
    0
      src/components/ProfileMini/ProfileMini.styled.js
  9. 17
    0
      src/components/Router/AdminRoute.js
  10. 1
    1
      src/components/UserReviews/UserReviews.styled.js
  11. 35
    0
      src/constants/adminNavigation.js
  12. 13
    11
      src/constants/pages.js
  13. 9
    0
      src/i18n/resources/rs.js
  14. 4
    3
      src/layouts/MainLayout/MainLayout.js
  15. 0
    0
      src/pages/AdminHomePage/AdminCategoriesPage/AdminCategoriesPage.js
  16. 0
    0
      src/pages/AdminHomePage/AdminCategoriesPage/AdminCategoriesPage.styled.js
  17. 66
    9
      src/pages/AdminHomePage/AdminHomePage.js
  18. 6
    0
      src/pages/AdminHomePage/AdminHomePage.styled.js
  19. 15
    0
      src/pages/AdminHomePage/AdminLocationsPage/AdminLocationsPage.js
  20. 6
    0
      src/pages/AdminHomePage/AdminLocationsPage/AdminLocationsPage.styled.js
  21. 15
    0
      src/pages/AdminHomePage/AdminPaymentPage/AdminPaymentPage.js
  22. 6
    0
      src/pages/AdminHomePage/AdminPaymentPage/AdminPaymentPage.styled.js
  23. 11
    7
      src/pages/AdminHomePage/AdminSubcategoriesPage/AdminSubcategoriesPage.js
  24. 5
    5
      src/pages/AdminHomePage/AdminSubcategoriesPage/AdminSubcategoriesPage.styled.js
  25. 0
    0
      src/pages/AdminHomePage/AdminUsersPage/AdminUsersPage.js
  26. 2
    2
      src/request/index.js
  27. 2
    1
      src/request/loginRequest.js
  28. 10
    11
      src/store/actions/login/loginActionConstants.js
  29. 8
    3
      src/store/actions/login/loginActions.js
  30. 2
    2
      src/store/middleware/accessTokensMiddleware.js
  31. 51
    13
      src/store/saga/loginSaga.js
  32. 4
    0
      src/store/selectors/loginSelectors.js
  33. 26
    7
      src/util/helpers/routeHelpers.js

+ 2
- 14
src/AppRoutes.js Wyświetl plik

// import PricesPage from "./pages/Prices/PricesPage"; // import PricesPage from "./pages/Prices/PricesPage";
import AboutPage from "./pages/About/AboutPage"; import AboutPage from "./pages/About/AboutPage";
import AuthRoute from "./components/Router/AuthRoute"; import AuthRoute from "./components/Router/AuthRoute";
import AdminRoute from "./components/Router/AdminRoute";
import AdminHomePage from "./pages/AdminHomePage/AdminHomePage"; import AdminHomePage from "./pages/AdminHomePage/AdminHomePage";
import AdminUsersPage from "./pages/AdminUsersPage/AdminUsersPage";
import AdminCategoriesPage from "./pages/AdminCategoriesPage/AdminCategoriesPage";
import AdminSubcategoriesPage from "./pages/AdminSubcategoriesPage/AdminSubcategoriesPage";
// import PrivacyPolicyPage from "./pages/PrivacyPolicy/PrivacyPolicyPage"; // import PrivacyPolicyPage from "./pages/PrivacyPolicy/PrivacyPolicyPage";


const AppRoutes = () => { const AppRoutes = () => {
<Route exact path={BASE_PAGE} component={HomePage} /> <Route exact path={BASE_PAGE} component={HomePage} />
<AuthRoute exact path={LOGIN_PAGE} component={LoginPage} /> <AuthRoute exact path={LOGIN_PAGE} component={LoginPage} />
<AuthRoute exact path={ADMIN_LOGIN_PAGE} component={AdminLoginPage} /> <AuthRoute exact path={ADMIN_LOGIN_PAGE} component={AdminLoginPage} />
<Route path={ADMIN_HOME_PAGE} component={AdminHomePage} />
<Route path={ADMIN_USERS_PAGE} component={AdminUsersPage} />
<Route
path={ADMIN_SUBCATEGORIES_PAGE}
component={AdminSubcategoriesPage}
/>
<Route
exact
path={ADMIN_CATEGORIES_PAGE}
component={AdminCategoriesPage}
/>
<AdminRoute path={ADMIN_HOME_PAGE} component={AdminHomePage} />
<Route path={NOT_FOUND_PAGE} component={NotFoundPage} /> <Route path={NOT_FOUND_PAGE} component={NotFoundPage} />
<Route path={ERROR_PAGE} component={ErrorPage} /> <Route path={ERROR_PAGE} component={ErrorPage} />
<AuthRoute <AuthRoute

+ 4
- 0
src/assets/images/svg/dollar-sign.svg Wyświetl plik

<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11 0.916748V21.0834" stroke="#C4C4C4" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M15.5833 4.58325H8.70833C7.85743 4.58325 7.04138 4.92127 6.4397 5.52295C5.83802 6.12463 5.5 6.94068 5.5 7.79159C5.5 8.64249 5.83802 9.45854 6.4397 10.0602C7.04138 10.6619 7.85743 10.9999 8.70833 10.9999H13.2917C14.1426 10.9999 14.9586 11.3379 15.5603 11.9396C16.162 12.5413 16.5 13.3573 16.5 14.2083C16.5 15.0592 16.162 15.8752 15.5603 16.4769C14.9586 17.0786 14.1426 17.4166 13.2917 17.4166H5.5" stroke="#C4C4C4" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

+ 94
- 0
src/components/Admin/Sidebar/Sidebar.js Wyświetl plik

import React from "react";
import {
SidebarContainer,
SidebarContent,
SidebarHeader,
SidebarProfileImageContainer,
SidebarProfileImage,
SidebarProfileName,
SidebarNavigation,
SidebarNavigationMeni,
SidebarNavigationMeniItemUl,
SidebarNavigationMeniItem,
SidebarNavigationMeniItemIcon,
SidebarProfileRole,
SidebarNavigationMeniLogout,
} from "./Sidebar.styled";
import { ReactComponent as LogoHorizontal } from "../../../assets/images/svg/logo-horizontal.svg";
import { useHistory } from "react-router-dom";
import { useSelector } from "react-redux";
import { selectMineProfile } from "../../../store/selectors/profileSelectors";
import { getImageUrl, variants } from "../../../util/helpers/imageUrlGetter";
import useIsMobile from "../../../hooks/useIsMobile";
import { ADMIN_NAVIGATION } from "../../../constants/adminNavigation";
import { ReactComponent as Logout } from "../../../assets/images/svg/log-out.svg";
import { useTranslation } from "react-i18next";
import { useDispatch } from "react-redux";
import { logoutAdmin } from "../../../store/actions/login/loginActions";
import { isInRoute, routeMatches } from "../../../util/helpers/routeHelpers";
import { ADMIN_HOME_PAGE, ADMIN_USERS_PAGE } from "../../../constants/pages";

const Sidebar = () => {
const history = useHistory();
const profile = useSelector(selectMineProfile);
const dispatch = useDispatch();
const { isMobile } = useIsMobile();
const { t } = useTranslation();
console.log(profile);
const routeToItem = (route) => {
console.log(route);
history.push(route);
};
const logoutHandler = () => {
dispatch(logoutAdmin());
};

return (
<SidebarContainer>
<SidebarHeader>
<LogoHorizontal />
</SidebarHeader>
<SidebarContent>
<SidebarProfileImageContainer>
<SidebarProfileImage
src={getImageUrl(profile.image, variants.profileImage, isMobile)}
/>
<SidebarProfileName>{profile.company.name}</SidebarProfileName>
<SidebarProfileRole>{t("admin.navigation.role")}</SidebarProfileRole>
</SidebarProfileImageContainer>
<SidebarNavigation>
<SidebarNavigationMeni>
{t("admin.navigation.menu")}
</SidebarNavigationMeni>
<SidebarNavigationMeniItemUl>
{ADMIN_NAVIGATION.map((value) => {
let isRouteActive = isInRoute(value.route);
if (
routeMatches(ADMIN_HOME_PAGE) &&
routeMatches(ADMIN_USERS_PAGE, value.route)
)
isRouteActive = true;
return (
<SidebarNavigationMeniItem
active={isRouteActive}
key={value.text}
onClick={() => routeToItem(value.route)}
>
<SidebarNavigationMeniItemIcon active={isRouteActive}>
{value.icon}
</SidebarNavigationMeniItemIcon>
{value.text}
</SidebarNavigationMeniItem>
);
})}
</SidebarNavigationMeniItemUl>
<SidebarNavigationMeniLogout onClick={logoutHandler}>
<Logout /> {t("admin.navigation.logout")}
</SidebarNavigationMeniLogout>
</SidebarNavigation>
</SidebarContent>
</SidebarContainer>
);
};

export default Sidebar;

+ 125
- 0
src/components/Admin/Sidebar/Sidebar.styled.js Wyświetl plik

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

export const SidebarContainer = styled(Box)`
margin-top: -30px;
position: fixed;
bottom: 0;
left: 0;
min-width: 20%;
overflow-y: auto;
height: 100vh;
`;

export const SidebarHeader = styled(Box)`
background-color: #f5edff;
padding-top: 36px;
padding-left: 36px;
padding-bottom: 36px;
`;

export const SidebarContent = styled(Box)`
background-color: #fff;
`;

export const SidebarProfileImageContainer = styled(Box)`
display: flex;
flex-direction: column;
align-items: center;
padding: 36px 0 120px 0;
`;

export const SidebarProfileImage = styled.img`
width: 108px;
height: 108px;
border-radius: 100%;
`;

export const SidebarProfileName = styled(Typography)`
font-family: "DM Sans";
font-size: 16px;
font-weight: 700;
margin-top: 19px;
color: ${selectedTheme.colors.primaryPurple};
`;

export const SidebarProfileRole = styled(Typography)`
font-family: "DM Sans";
font-size: 12px;
color: ${selectedTheme.colors.primaryPurple};
`;

export const SidebarNavigation = styled(Box)`
margin-left: 18px;
display: flex;
flex-direction: column;
`;

export const SidebarNavigationMeni = styled(Typography)`
font-family: "DM Sans";
font-size: 24px;
font-weight: 700;
margin-bottom: 56px;
margin-left: 18px;
`;

export const SidebarNavigationMeniItemUl = styled.ul``;
export const SidebarNavigationMeniItem = styled.li`
padding-top: 19px;
padding-bottom: 19px;
padding-left: 18px;
font-family: "DM Sans";
font-size: 16px;
font-weight: 400;
display: flex;
align-items: center;
color: ${selectedTheme.colors.primaryPurple};
cursor: pointer;
${(props) =>
props.active &&
`
background-color: #f5edff;
font-weight: 700;
`}
path {
stroke: #c4c4c4;
}

&:hover {
background-color: #f5edff;
font-weight: 700;

path {
stroke: #feb005;
}
}
`;

export const SidebarNavigationMeniItemIcon = styled(Box)`
margin-right: 9px;

${(props) =>
props.active &&
`
path {
stroke: #feb005;
}
`}
`;

export const SidebarNavigationMeniLogout = styled(Box)`
font-family: "DM Sans";
font-size: 16px;
padding-top: 19px;
padding-bottom: 19px;
padding-left: 18px;
margin-top: 100px;
color: ${selectedTheme.colors.primaryPurple};
cursor: pointer;

&:hover {
background-color: #f5edff;
font-weight: 700;
}
`;

+ 2
- 1
src/components/Cards/ProfileCard/ProfileStats/ProfileStats.js Wyświetl plik

const ProfileStats = (props) => { const ProfileStats = (props) => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<ProfileStatsContainer>
<ProfileStatsContainer className={props.className}>
<ProfileStatsGrid> <ProfileStatsGrid>
<StatsItem variant="subtitle2"> <StatsItem variant="subtitle2">
<b>{props.profile?.statistics?.publishes?.count}</b> <b>{props.profile?.statistics?.publishes?.count}</b>
ProfileStats.propTypes = { ProfileStats.propTypes = {
profile: PropTypes.object, profile: PropTypes.object,
percentOfSucceededExchanges: PropTypes.number, percentOfSucceededExchanges: PropTypes.number,
className: PropTypes.string,
}; };


export default ProfileStats; export default ProfileStats;

+ 1
- 1
src/components/ItemDetails/ItemDetails.styled.js Wyświetl plik



@media screen and (max-width: 600px) { @media screen and (max-width: 600px) {
margin-left: 0; margin-left: 0;
margin-top: 228px;
margin-top: 310px;


svg { svg {
width: 14px; width: 14px;

+ 8
- 0
src/components/ProfileMini/ProfileMini.js Wyświetl plik

ProfileHeader, ProfileHeader,
ProfileHeaderIconContainer, ProfileHeaderIconContainer,
ProfileHeaderText, ProfileHeaderText,
ProfileMiniStats,
} from "./ProfileMini.styled"; } from "./ProfileMini.styled";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { selectOffer } from "../../store/selectors/offersSelectors"; import { selectOffer } from "../../store/selectors/offersSelectors";
if (offer?.offer?.userId?.toString() === userId?.toString()) return true; if (offer?.offer?.userId?.toString() === userId?.toString()) return true;
return false; return false;
}, [offer, userId]); }, [offer, userId]);
console.log(offer);
return ( return (
<> <>
{isLoadingOfferContent || isLoadingOfferContent === undefined ? ( {isLoadingOfferContent || isLoadingOfferContent === undefined ? (
isMyProfile={isMyProfile} isMyProfile={isMyProfile}
singleOffer singleOffer
/> />
<ProfileMiniStats
profile={offer.companyData}
percentOfSucceededExchanges={
offer.companyData?.statistics?.exchanges?.total
}
/>
</ProfileHeader> </ProfileHeader>
)} )}
</> </>

+ 14
- 0
src/components/ProfileMini/ProfileMini.styled.js Wyświetl plik

import { Box } from "@mui/system"; import { Box } from "@mui/system";
import { Typography } from "@mui/material"; import { Typography } from "@mui/material";
import selectedTheme from "../../themes"; import selectedTheme from "../../themes";
import ProfileStats from "../Cards/ProfileCard/ProfileStats/ProfileStats";


export const ProfileHeader = styled(Box)` export const ProfileHeader = styled(Box)`
margin-top: 60px; margin-top: 60px;
font-size: 12px; font-size: 12px;
} }
`; `;
export const ProfileMiniStats = styled(ProfileStats)`
position: relative;
width: 100%;
margin-left: 0;
margin-top: -1rem;
margin-bottom: 36px;
border-radius: 0 0 4px 4px;
border: 1px solid ${selectedTheme.colors.primaryPurple};

@media (max-width: 600px) {
width: 92%;
}
`;

+ 17
- 0
src/components/Router/AdminRoute.js Wyświetl plik

import React, { useMemo } from "react";
import { Redirect, Route } from "react-router";
import { useSelector } from "react-redux";
import { HOME_PAGE } from "../../constants/pages";
import { selectRoles } from "../../store/selectors/loginSelectors";

const AdminRoute = ({ ...props }) => {
const role = useSelector(selectRoles);
const isUserAdmin = useMemo(() => {
if (!role?.includes("Admin")) return false;
return true;
}, [role]);

return isUserAdmin ? <Route {...props} /> : <Redirect to={HOME_PAGE} />;
};

export default AdminRoute;

+ 1
- 1
src/components/UserReviews/UserReviews.styled.js Wyświetl plik

border: 1px solid ${selectedTheme.colors.borderNormal}; border: 1px solid ${selectedTheme.colors.borderNormal};
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
max-height: ${(props) => (props.isProfileReviews ? "70vh" : "43vh")};
max-height: ${(props) => (props.isProfileReviews ? "70vh" : "39vh")};


/* overflow-y: auto; */ /* overflow-y: auto; */
&::-webkit-scrollbar { &::-webkit-scrollbar {

+ 35
- 0
src/constants/adminNavigation.js Wyświetl plik

import React from "react";
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_CATEGORIES_PAGE,
ADMIN_LOCATIONS_PAGE,
ADMIN_PAYMENT_PAGE,
ADMIN_USERS_PAGE,
} from "./pages";
import i18n from "../i18n";

export const ADMIN_NAVIGATION = [
{
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}`,
},
];

+ 13
- 11
src/constants/pages.js Wyświetl plik

export const BASE_PAGE = '/';
export const LOGIN_PAGE = '/login';
export const FORGOT_PASSWORD_PAGE = '/forgot-password';
export const ADMIN_LOGIN_PAGE = '/admin/login';
export const HOME_PAGE = '/home';
export const ERROR_PAGE = '/error-page';
export const NOT_FOUND_PAGE = '/not-found';
export const FORGOT_PASSWORD_MAIL_SENT = '/forgot-password/mail-sent';
export const BASE_PAGE = "/";
export const LOGIN_PAGE = "/login";
export const FORGOT_PASSWORD_PAGE = "/forgot-password";
export const ADMIN_LOGIN_PAGE = "/admin/login";
export const HOME_PAGE = "/home";
export const ERROR_PAGE = "/error-page";
export const NOT_FOUND_PAGE = "/not-found";
export const FORGOT_PASSWORD_MAIL_SENT = "/forgot-password/mail-sent";
export const REGISTER_PAGE = "/register"; export const REGISTER_PAGE = "/register";
export const REGISTER_SUCCESSFUL_PAGE = "/register/success"; export const REGISTER_SUCCESSFUL_PAGE = "/register/success";
export const RESET_PASSWORD_PAGE = "/reset-password/:token"; export const RESET_PASSWORD_PAGE = "/reset-password/:token";
export const CREATE_OFFER_PAGE = "/create-offer"; export const CREATE_OFFER_PAGE = "/create-offer";
export const ITEM_DETAILS_PAGE = "/proizvodi/:idProizvod"; export const ITEM_DETAILS_PAGE = "/proizvodi/:idProizvod";
export const PROFILE_PAGE = "/profile/:idProfile"
export const PROFILE_PAGE = "/profile/:idProfile";
export const CHAT_PAGE = "/messages"; export const CHAT_PAGE = "/messages";
export const CHAT_MESSAGE_PAGE = "/messages/:idChat"; export const CHAT_MESSAGE_PAGE = "/messages/:idChat";
export const MY_OFFERS_PAGE = "/myoffers"
export const MY_OFFERS_PAGE = "/myoffers";
export const ABOUT_PAGE = "/about"; export const ABOUT_PAGE = "/about";
export const PRICES_PAGE = "/prices"; export const PRICES_PAGE = "/prices";
export const POLICY_PRIVACY_PAGE = "/policy"; export const POLICY_PRIVACY_PAGE = "/policy";
export const ADMIN_HOME_PAGE = "/admin/home";
export const ADMIN_HOME_PAGE = "/admin";
export const ADMIN_USERS_PAGE = "/admin/users"; export const ADMIN_USERS_PAGE = "/admin/users";
export const ADMIN_CATEGORIES_PAGE = "/admin/categories"; export const ADMIN_CATEGORIES_PAGE = "/admin/categories";
export const ADMIN_LOCATIONS_PAGE = "/admin/locations";
export const ADMIN_PAYMENT_PAGE = "/admin/payment";
export const ADMIN_SUBCATEGORIES_PAGE = "/admin/categories/:categoryId"; export const ADMIN_SUBCATEGORIES_PAGE = "/admin/categories/:categoryId";

+ 9
- 0
src/i18n/resources/rs.js Wyświetl plik

searchPlaceholder: "Pretražite korisnike....", searchPlaceholder: "Pretražite korisnike....",
checkProfile: "Pogledaj profil", checkProfile: "Pogledaj profil",
}, },
navigation: {
role: "Administrator",
menu: "Meni",
users: "Korisnici",
categories: "Kategorije",
locations: "Lokacije",
payment: "Uplate",
logout: "Odjavi se",
},
categories: { categories: {
checkCategory: "Pogledaj podkategorije", checkCategory: "Pogledaj podkategorije",
noOfOffers: "Broj objava: ", noOfOffers: "Broj objava: ",

+ 4
- 3
src/layouts/MainLayout/MainLayout.js Wyświetl plik



const MainLayout = (props) => { const MainLayout = (props) => {
return ( return (
<MainLayoutContainer>
<MainLayoutContainer className={props.className}>
{props.children} {props.children}
<Grid container maxHeight="xl"> <Grid container maxHeight="xl">
<LeftCard item xs={0} sm={0} md={3} lg={3} xl={2.4} >
<LeftCard item xs={0} sm={0} md={3} lg={3} xl={2.4}>
{props.leftCard} {props.leftCard}
</LeftCard> </LeftCard>
<Content item xs={12} sm={12} md={9} lg={9} xl={9.6} >
<Content item xs={12} sm={12} md={9} lg={9} xl={9.6}>
{props.content} {props.content}
</Content> </Content>
</Grid> </Grid>
leftCard: PropTypes.node, leftCard: PropTypes.node,
content: PropTypes.node, content: PropTypes.node,
rightCard: PropTypes.node, rightCard: PropTypes.node,
className: PropTypes.string,
}; };


export default MainLayout; export default MainLayout;

src/pages/AdminCategoriesPage/AdminCategoriesPage.js → src/pages/AdminHomePage/AdminCategoriesPage/AdminCategoriesPage.js Wyświetl plik


src/pages/AdminCategoriesPage/AdminCategoriesPage.styled.js → src/pages/AdminHomePage/AdminCategoriesPage/AdminCategoriesPage.styled.js Wyświetl plik


+ 66
- 9
src/pages/AdminHomePage/AdminHomePage.js Wyświetl plik

import React from "react";
import React, { useMemo } from "react";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import MarketPlace from "../../components/MarketPlace/MarketPlace";
import useOffers from "../../hooks/useOffers/useOffers";
// import MarketPlace from "../../components/MarketPlace/MarketPlace";
// import useOffers from "../../hooks/useOffers/useOffers";
import Sidebar from "../../components/Admin/Sidebar/Sidebar";
import { MainLayoutAdminHomePage } from "./AdminHomePage.styled";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { selectIsLoadingByActionType } from "../../store/selectors/loadingSelectors";
import { OFFERS_SCOPE } from "../../store/actions/offers/offersActionConstants";
// import { selectIsLoadingByActionType } from "../../store/selectors/loadingSelectors";
// import { OFFERS_SCOPE } from "../../store/actions/offers/offersActionConstants";
import AdminUsersPage from "../AdminUsersPage/AdminUsersPage";
import { selectMineProfile } from "../../store/selectors/profileSelectors";
import { Switch, useHistory } from "react-router-dom";
import {
ADMIN_CATEGORIES_PAGE,
ADMIN_LOCATIONS_PAGE,
ADMIN_PAYMENT_PAGE,
ADMIN_SUBCATEGORIES_PAGE,
ADMIN_USERS_PAGE,
HOME_PAGE,
} from "../../constants/pages";
import { selectUserId } from "../../store/selectors/loginSelectors";
import AdminCategoriesPage from "../AdminCategoriesPage/AdminCategoriesPage";
import AdminRoute from "../../components/Router/AdminRoute";
import AdminSubcategoriesPage from "./AdminSubcategoriesPage/AdminSubcategoriesPage";
import AdminLocationsPage from "./AdminLocationsPage/AdminLocationsPage";
import AdminPaymentPage from "./AdminPaymentPage/AdminPaymentPage";


const AdminHomePage = () => { const AdminHomePage = () => {
const offers = useOffers();
const isLoadingOffers = useSelector(
selectIsLoadingByActionType(OFFERS_SCOPE)
const profile = useSelector(selectMineProfile);
const userId = useSelector(selectUserId);
const history = useHistory();
const isUserLogin = useMemo(() => {
if (userId?.length === 0) return false;
return true;
}, [userId]);
if (!profile.roles.includes("Admin") || !isUserLogin) {
history.push(HOME_PAGE);
}
return (
<MainLayoutAdminHomePage
leftCard={<Sidebar />}
content={
<Switch>
<AdminRoute path={ADMIN_USERS_PAGE} component={AdminUsersPage} />
<AdminRoute
exact
path={ADMIN_CATEGORIES_PAGE}
component={AdminCategoriesPage}
/>
<AdminRoute
path={ADMIN_SUBCATEGORIES_PAGE}
component={AdminSubcategoriesPage}
/>
<AdminRoute
path={ADMIN_SUBCATEGORIES_PAGE}
component={AdminSubcategoriesPage}
/>
<AdminRoute
path={ADMIN_LOCATIONS_PAGE}
component={AdminLocationsPage}
/>
<AdminRoute path={ADMIN_PAYMENT_PAGE} component={AdminPaymentPage} />
<AdminRoute
path={ADMIN_SUBCATEGORIES_PAGE}
component={AdminSubcategoriesPage}
/>
<AdminRoute component={AdminUsersPage} />
</Switch>
}
/>
); );
return <MarketPlace offers={offers} isAdmin skeleton={isLoadingOffers} />;
}; };


AdminHomePage.propTypes = { AdminHomePage.propTypes = {

+ 6
- 0
src/pages/AdminHomePage/AdminHomePage.styled.js Wyświetl plik

import styled from "styled-components";
import MainLayout from "../../layouts/MainLayout/MainLayout";

export const MainLayoutAdminHomePage = styled(MainLayout)`
margin-top: 0;
`;

+ 15
- 0
src/pages/AdminHomePage/AdminLocationsPage/AdminLocationsPage.js Wyświetl plik

import React from "react";
import PropTypes from "prop-types";
import { AdminLocationsPageContainer } from "./AdminLocationsPage.styled";

const AdminLocationsPage = () => {
return (
<AdminLocationsPageContainer>Admin locations</AdminLocationsPageContainer>
);
};

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

export default AdminLocationsPage;

+ 6
- 0
src/pages/AdminHomePage/AdminLocationsPage/AdminLocationsPage.styled.js Wyświetl plik

import { Box } from "@mui/material";
import styled from "styled-components";

export const AdminLocationsPageContainer = styled(Box)`
`

+ 15
- 0
src/pages/AdminHomePage/AdminPaymentPage/AdminPaymentPage.js Wyświetl plik

import React from 'react'
import PropTypes from 'prop-types'
import { AdminPaymentPageContainer } from './AdminPaymentPage.styled'

const AdminPaymentPage = () => {
return (
<AdminPaymentPageContainer>Admin payment</AdminPaymentPageContainer>
)
}

AdminPaymentPage.propTypes = {
children: PropTypes.node,
}

export default AdminPaymentPage

+ 6
- 0
src/pages/AdminHomePage/AdminPaymentPage/AdminPaymentPage.styled.js Wyświetl plik

import { Box } from "@mui/material";
import styled from "styled-components";

export const AdminPaymentPageContainer = styled(Box)`
`

src/pages/AdminSubcategoriesPage/AdminSubcategoriesPage.js → src/pages/AdminHomePage/AdminSubcategoriesPage/AdminSubcategoriesPage.js Wyświetl plik

import PropTypes from "prop-types"; import PropTypes from "prop-types";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { useEffect } from "react"; import { useEffect } from "react";
import { fetchCategories } from "../../store/actions/categories/categoriesActions";
import { selectCategories } from "../../store/selectors/categoriesSelectors";
import { fetchCategories } from "../../../store/actions/categories/categoriesActions";
import { selectCategories } from "../../../store/selectors/categoriesSelectors";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { import {
AdminSubcategoriesHeader, AdminSubcategoriesHeader,
NewSubcategoryButton, NewSubcategoryButton,
SponsoredCategoryCard, SponsoredCategoryCard,
} from "./AdminSubcategoriesPage.styled"; } from "./AdminSubcategoriesPage.styled";
import { selectManualSearchString } from "../../store/selectors/filtersSelectors";
import { selectManualSearchString } from "../../../store/selectors/filtersSelectors";
import { useMemo } from "react"; import { useMemo } from "react";
import { setManualSearchString } from "../../store/actions/filters/filtersActions";
import { setManualSearchString } from "../../../store/actions/filters/filtersActions";
import { useRouteMatch } from "react-router-dom"; import { useRouteMatch } from "react-router-dom";
import CategoryCard from "../../components/Cards/CategoryCard/CategoryCard";
import selectedTheme from "../../themes";
import CategoryCard from "../../../components/Cards/CategoryCard/CategoryCard";
import selectedTheme from "../../../themes";
import { useState } from "react"; import { useState } from "react";
import EditCategory from "../../components/Modals/EditCategory/EditCategory";
import EditCategory from "../../../components/Modals/EditCategory/EditCategory";
import { isInRoute } from "../../../util/helpers/routeHelpers";
import { ADMIN_SUBCATEGORIES_PAGE } from "../../../constants/pages";


const AdminSubcategoriesPage = () => { const AdminSubcategoriesPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const manualSearchString = useSelector(selectManualSearchString); const manualSearchString = useSelector(selectManualSearchString);
const [openedAddModal, setOpenedAddModal] = useState(false); const [openedAddModal, setOpenedAddModal] = useState(false);


console.log(isInRoute(ADMIN_SUBCATEGORIES_PAGE));

useEffect(() => { useEffect(() => {
dispatch(fetchCategories()); dispatch(fetchCategories());
}, []); }, []);

src/pages/AdminSubcategoriesPage/AdminSubcategoriesPage.styled.js → src/pages/AdminHomePage/AdminSubcategoriesPage/AdminSubcategoriesPage.styled.js Wyświetl plik

import { Box } from "@mui/material"; import { Box } from "@mui/material";
import styled from "styled-components"; import styled from "styled-components";
import { PrimaryButton } from "../../components/Buttons/PrimaryButton/PrimaryButton";
import CategoryCard from "../../components/Cards/CategoryCard/CategoryCard";
import Header from "../../components/MarketPlace/Header/Header";
import SearchField from "../../components/TextFields/SearchField/SearchField";
import selectedTheme from "../../themes";
import { PrimaryButton } from "../../../components/Buttons/PrimaryButton/PrimaryButton";
import CategoryCard from "../../../components/Cards/CategoryCard/CategoryCard";
import Header from "../../../components/MarketPlace/Header/Header";
import SearchField from "../../../components/TextFields/SearchField/SearchField";
import selectedTheme from "../../../themes";


export const AdminSubcategoriesPageContainer = styled(Box)` export const AdminSubcategoriesPageContainer = styled(Box)`
padding: 60px; padding: 60px;

src/pages/AdminUsersPage/AdminUsersPage.js → src/pages/AdminHomePage/AdminUsersPage/AdminUsersPage.js Wyświetl plik


+ 2
- 2
src/request/index.js Wyświetl plik

// baseURL: "http://192.168.88.150:3001/", // DJOLE // baseURL: "http://192.168.88.150:3001/", // DJOLE
// baseURL: "http://192.168.88.175:3005/", // baseURL: "http://192.168.88.175:3005/",
// baseURL: "http://192.168.88.143:3001/", // DULE // baseURL: "http://192.168.88.143:3001/", // DULE
// baseURL: "https://trampa-api-test.dilig.net/",
baseURL: "https://trampa-api-test.dilig.net/",
// baseURL: "http://localhost:3001/", // baseURL: "http://localhost:3001/",
baseURL: process.env.REACT_APP_BASE_API_URL,
// baseURL: process.env.REACT_APP_BASE_API_URL,
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },

+ 2
- 1
src/request/loginRequest.js Wyświetl plik

export const attemptLogin = (payload) => export const attemptLogin = (payload) =>
postRequest(apiEndpoints.authentications.login, payload); postRequest(apiEndpoints.authentications.login, payload);



export const logoutUserRequest = (payload) => export const logoutUserRequest = (payload) =>
postRequest(apiEndpoints.authentications.logout, payload); postRequest(apiEndpoints.authentications.logout, payload);


export const logoutAdminRequest = (payload) =>
postRequest(apiEndpoints.authentications.logout, payload);

+ 10
- 11
src/store/actions/login/loginActionConstants.js Wyświetl plik

createSuccessType, createSuccessType,
createSubmitType, createSubmitType,
createUpdateType, createUpdateType,
} from '../actionHelpers';
} from "../actionHelpers";



const LOGIN_USER_SCOPE = 'LOGIN_USER';
const LOGIN_USER_SCOPE = "LOGIN_USER";
export const LOGIN_USER_FETCH = createFetchType(LOGIN_USER_SCOPE); export const LOGIN_USER_FETCH = createFetchType(LOGIN_USER_SCOPE);
export const LOGIN_USER_SUCCESS = createSuccessType(LOGIN_USER_SCOPE); export const LOGIN_USER_SUCCESS = createSuccessType(LOGIN_USER_SCOPE);
export const LOGIN_USER_ERROR = createErrorType(LOGIN_USER_SCOPE); export const LOGIN_USER_ERROR = createErrorType(LOGIN_USER_SCOPE);
export const CLEAR_LOGIN_USER_ERROR = createClearType( export const CLEAR_LOGIN_USER_ERROR = createClearType(
`${LOGIN_USER_SCOPE}_ERROR`,
`${LOGIN_USER_SCOPE}_ERROR`
); );
export const LOGIN_USER_LOADING = createLoadingType(LOGIN_USER_SCOPE); export const LOGIN_USER_LOADING = createLoadingType(LOGIN_USER_SCOPE);


export const UPDATE_USER_JWT_TOKEN = createUpdateType("UPDATE_USER_JWT_TOKEN");
export const RESET_LOGIN_STATE = createClearType("UPDATE_USER_JWT_TOKEN");
export const AUTHENTICATE_USER = createUpdateType("AUTHENTICATE_USER");
export const LOGOUT_USER = createUpdateType("LOGOUT_USER");
export const LOGOUT_ADMIN = createUpdateType("LOGOUT_ADMIN");
export const REFRESH_TOKEN = createUpdateType("REFRESH_TOKEN");


export const UPDATE_USER_JWT_TOKEN = createUpdateType('UPDATE_USER_JWT_TOKEN');
export const RESET_LOGIN_STATE = createClearType('UPDATE_USER_JWT_TOKEN');
export const AUTHENTICATE_USER = createUpdateType('AUTHENTICATE_USER');
export const LOGOUT_USER = createUpdateType('LOGOUT_USER');
export const REFRESH_TOKEN = createUpdateType('REFRESH_TOKEN');

const GENERATE_TOKEN_SCOPE = 'GENERATE_TOKEN';
const GENERATE_TOKEN_SCOPE = "GENERATE_TOKEN";
export const GENERATE_TOKEN = createSubmitType(GENERATE_TOKEN_SCOPE); export const GENERATE_TOKEN = createSubmitType(GENERATE_TOKEN_SCOPE);
export const GENERATE_TOKEN_SUCCESS = createSuccessType(GENERATE_TOKEN_SCOPE); export const GENERATE_TOKEN_SUCCESS = createSuccessType(GENERATE_TOKEN_SCOPE);
export const GENERATE_TOKEN_ERROR = createErrorType(GENERATE_TOKEN_SCOPE); export const GENERATE_TOKEN_ERROR = createErrorType(GENERATE_TOKEN_SCOPE);

+ 8
- 3
src/store/actions/login/loginActions.js Wyświetl plik

GENERATE_TOKEN, GENERATE_TOKEN,
GENERATE_TOKEN_SUCCESS, GENERATE_TOKEN_SUCCESS,
GENERATE_TOKEN_ERROR, GENERATE_TOKEN_ERROR,
} from './loginActionConstants';
LOGOUT_ADMIN,
} from "./loginActionConstants";


export const fetchLogin = (payload) => ({ export const fetchLogin = (payload) => ({
type: LOGIN_USER_FETCH, type: LOGIN_USER_FETCH,
payload, payload,
}); });


export const logoutAdmin = (payload) => ({
type: LOGOUT_ADMIN,
payload,
});

export const refreshUserToken = (payload) => ({ export const refreshUserToken = (payload) => ({
type: REFRESH_TOKEN, type: REFRESH_TOKEN,
payload
payload,
}); });


export const generateToken = (payload) => ({ export const generateToken = (payload) => ({

+ 2
- 2
src/store/middleware/accessTokensMiddleware.js Wyświetl plik

//Change URL with .env //Change URL with .env
// const baseURL = "http://192.168.88.143:3001/"; // DULE // const baseURL = "http://192.168.88.143:3001/"; // DULE
// const baseURL = "http://192.168.88.175:3005/"; // const baseURL = "http://192.168.88.175:3005/";
// const baseURL = "https://trampa-api-test.dilig.net/";
const baseURL = "https://trampa-api-test.dilig.net/";
// const baseURL = "http://192.168.88.150:3001/"; // DJOLE // const baseURL = "http://192.168.88.150:3001/"; // DJOLE
// const baseURL = "http://localhost:3001/"; // const baseURL = "http://localhost:3001/";
const baseURL = process.env.REACT_APP_BASE_API_URL
// const baseURL = process.env.REACT_APP_BASE_API_URL


//Interceptor unique name //Interceptor unique name
export const accessTokensMiddlewareInterceptorName = "ACCESS_TOKEN_INTERCEPTOR"; export const accessTokensMiddlewareInterceptorName = "ACCESS_TOKEN_INTERCEPTOR";

+ 51
- 13
src/store/saga/loginSaga.js Wyświetl plik

import { import {
AUTHENTICATE_USER, AUTHENTICATE_USER,
LOGIN_USER_FETCH, LOGIN_USER_FETCH,
LOGOUT_ADMIN,
LOGOUT_USER, LOGOUT_USER,
REFRESH_TOKEN, REFRESH_TOKEN,
} from "../actions/login/loginActionConstants"; } from "../actions/login/loginActionConstants";
import { import {
attemptLogin, attemptLogin,
logoutAdminRequest,
logoutUserRequest, logoutUserRequest,
} from "../../request/loginRequest"; } from "../../request/loginRequest";
import { import {
resetLoginState, resetLoginState,
setUserJwtToken, setUserJwtToken,
} from "../actions/login/loginActions"; } from "../actions/login/loginActions";
import { LOGIN_PAGE } from "../../constants/pages";
import { ADMIN_LOGIN_PAGE, LOGIN_PAGE } from "../../constants/pages";
import { addHeaderToken, removeHeaderToken } from "../../request"; import { addHeaderToken, removeHeaderToken } from "../../request";
import { import {
JWT_REFRESH_TOKEN, JWT_REFRESH_TOKEN,
import { rejectErrorCodeHelper } from "../../util/helpers/rejectErrorCodeHelper"; import { rejectErrorCodeHelper } from "../../util/helpers/rejectErrorCodeHelper";
import i18next from "i18next"; import i18next from "i18next";
import { attemptFetchProfile } from "../../request/profileRequest"; import { attemptFetchProfile } from "../../request/profileRequest";
import { clearProfile, setMineProfile } from "../actions/profile/profileActions";
import {
clearProfile,
setMineProfile,
} from "../actions/profile/profileActions";
import { clearOffers } from "../actions/offers/offersActions"; import { clearOffers } from "../actions/offers/offersActions";
import { clearChat } from "../actions/chat/chatActions"; import { clearChat } from "../actions/chat/chatActions";


const refresh = data.refresh; const refresh = data.refresh;
const tokenDecoded = jwt.decode(token); const tokenDecoded = jwt.decode(token);
const refreshDecoded = jwt.decode(refresh); const refreshDecoded = jwt.decode(refresh);
if(isAdmin && !tokenDecoded.roles.includes("Admin")){
if (isAdmin && !tokenDecoded.roles.includes("Admin")) {
throw Error("Not an admin login on /login"); throw Error("Not an admin login on /login");
} }
const accessToken = { const accessToken = {
yield call(addHeaderToken, token); yield call(addHeaderToken, token);
const profileData = yield call(attemptFetchProfile, userId); const profileData = yield call(attemptFetchProfile, userId);
if (profileData) yield put(setMineProfile(profileData.data)); if (profileData) yield put(setMineProfile(profileData.data));
yield put(fetchUserSuccess({jwtToken: accessToken, refreshToken: refreshToken, userId}));
yield put(
fetchUserSuccess({
jwtToken: accessToken,
refreshToken: refreshToken,
userId,
})
);
if (payload.handleApiResponseSuccess) { if (payload.handleApiResponseSuccess) {
yield call(payload.handleApiResponseSuccess); yield call(payload.handleApiResponseSuccess);
} }
} }
} catch (e) { } catch (e) {
if(e.message){
if (e.message) {
yield put(fetchUserError(e.message)); yield put(fetchUserError(e.message));
} }
if (e.response && e.response.data) { if (e.response && e.response.data) {
let errorMessage = yield call(rejectErrorCodeHelper, e.response.status); let errorMessage = yield call(rejectErrorCodeHelper, e.response.status);
if (e.response.status === 401 || e.response.status === 404) { if (e.response.status === 401 || e.response.status === 404) {
errorMessage = i18next.t("login.wrongCredentials", { errorMessage = i18next.t("login.wrongCredentials", {
lng: "rs"
lng: "rs",
}); });
} }
yield put(fetchUserError(errorMessage)); yield put(fetchUserError(errorMessage));
const JwtToken = yield call(authScopeStringGetHelper, JWT_TOKEN); const JwtToken = yield call(authScopeStringGetHelper, JWT_TOKEN);
const user = yield call(jwt.decode, JwtToken); const user = yield call(jwt.decode, JwtToken);
if (user) { if (user) {
const requestBody = {token: JwtToken}
const requestBody = { token: JwtToken };
yield call(logoutUserRequest, requestBody); yield call(logoutUserRequest, requestBody);
yield call(payload.payload)
yield call(payload.payload);
} }
} catch (error) { } catch (error) {
console.dir(error); // eslint-disable-line console.dir(error); // eslint-disable-line
yield put(clearChat()); yield put(clearChat());
} }
} }
function* refreshUserToken({payload}) {
function* logoutAdmin(payload) {
try {
const JwtToken = yield call(authScopeStringGetHelper, JWT_TOKEN);
const user = yield call(jwt.decode, JwtToken);
if (user) {
const requestBody = { token: JwtToken };
yield call(logoutAdminRequest, requestBody);
yield call(payload.payload);
}
} catch (error) {
console.dir(error); // eslint-disable-line
} finally {
yield call(authScopeClearHelper);
yield call(removeHeaderToken);
yield put(resetLoginState());
yield call(history.replace, ADMIN_LOGIN_PAGE);
yield put(clearProfile());
yield put(clearOffers());
yield put(clearChat());
}
}
function* refreshUserToken({ payload }) {
try { try {
const newTokenDecoded = jwt.decode(payload); const newTokenDecoded = jwt.decode(payload);


yield put(setUserJwtToken({token: payload, exp: newTokenDecoded.exp}));
yield put(
setUserJwtToken({
token: payload,
exp: newTokenDecoded.exp,
roles: newTokenDecoded.roles,
})
);
yield call(addHeaderToken, payload); yield call(addHeaderToken, payload);
yield call(authScopeSetHelper, JWT_TOKEN, payload); yield call(authScopeSetHelper, JWT_TOKEN, payload);
return true; return true;
}
catch(e){
} catch (e) {
console.dir(e); console.dir(e);
return false; return false;
} }
takeLatest(LOGIN_USER_FETCH, fetchLogin), takeLatest(LOGIN_USER_FETCH, fetchLogin),
takeLatest(AUTHENTICATE_USER, authenticateUser), takeLatest(AUTHENTICATE_USER, authenticateUser),
takeLatest(LOGOUT_USER, logout), takeLatest(LOGOUT_USER, logout),
takeLatest(REFRESH_TOKEN, refreshUserToken)
takeLatest(REFRESH_TOKEN, refreshUserToken),
takeLatest(LOGOUT_ADMIN, logoutAdmin),
]); ]);
} }

+ 4
- 0
src/store/selectors/loginSelectors.js Wyświetl plik

loginSelector, loginSelector,
(state) => state.errorMessage (state) => state.errorMessage
); );
export const selectRoles = createSelector(
loginSelector,
(state) => state.jwtToken.roles
);

+ 26
- 7
src/util/helpers/routeHelpers.js Wyświetl plik

import { import {
ADMIN_CATEGORIES_PAGE, ADMIN_CATEGORIES_PAGE,
ADMIN_HOME_PAGE, ADMIN_HOME_PAGE,
ADMIN_LOCATIONS_PAGE,
ADMIN_LOGIN_PAGE, ADMIN_LOGIN_PAGE,
ADMIN_PAYMENT_PAGE,
ADMIN_SUBCATEGORIES_PAGE, ADMIN_SUBCATEGORIES_PAGE,
ADMIN_USERS_PAGE, ADMIN_USERS_PAGE,
FORGOT_PASSWORD_MAIL_SENT, FORGOT_PASSWORD_MAIL_SENT,
} from "../../constants/pages"; } from "../../constants/pages";
import history from "../../store/utils/history"; import history from "../../store/utils/history";


export const routeMatches = (route) => {
export const routeMatches = (route, secondRoute = null) => {
let routeToCheck = secondRoute || history.location.pathname;
if ( if (
history.location.pathname === route ||
history.location.pathname + "/" === route ||
history.location.pathname.slice(0, -1) === route
routeToCheck === route ||
routeToCheck + "/" === route ||
routeToCheck.slice(0, -1) === route
) )
return true; return true;
return false; return false;
routeMatches(ADMIN_HOME_PAGE) || routeMatches(ADMIN_HOME_PAGE) ||
routeMatches(ADMIN_USERS_PAGE) || routeMatches(ADMIN_USERS_PAGE) ||
routeMatches(ADMIN_CATEGORIES_PAGE) || routeMatches(ADMIN_CATEGORIES_PAGE) ||
dynamicRouteMatches(ADMIN_SUBCATEGORIES_PAGE)
dynamicRouteMatches(ADMIN_SUBCATEGORIES_PAGE) ||
routeMatches(ADMIN_LOCATIONS_PAGE) ||
routeMatches(ADMIN_PAYMENT_PAGE)
) )
return true; return true;
return false; return false;
}; };
export const dynamicRouteMatches = (dynamicRoute) => { export const dynamicRouteMatches = (dynamicRoute) => {
const charactersToDelete =
(dynamicRoute.length - dynamicRoute.indexOf(":")) * -1;
let indexOfDynamicChar = dynamicRoute.indexOf(":");
if (indexOfDynamicChar === -1) return false;
const charactersToDelete = (dynamicRoute.length - indexOfDynamicChar) * -1;
const newDynamicRoute = dynamicRoute.slice(0, charactersToDelete); const newDynamicRoute = dynamicRoute.slice(0, charactersToDelete);
return history.location.pathname.includes(newDynamicRoute); return history.location.pathname.includes(newDynamicRoute);
}; };

export const isInRoute = (routeToCheck) => {
console.log("routeToCheck", routeToCheck);
console.log(
"first Expression",
history.location.pathname.includes(routeToCheck)
);
console.log("second expression", dynamicRouteMatches(routeToCheck));
return (
history.location.pathname.includes(routeToCheck) ||
dynamicRouteMatches(routeToCheck)
);
};

Ładowanie…
Anuluj
Zapisz