Browse Source

+ ProfileForm | +AdminUserList | +AdminUserForm

ilya_shyian 1 year ago
parent
commit
1d163da6df

+ 2 - 2
src/actions/actionAboutMe.js

@@ -5,7 +5,7 @@ export const actionAboutMe = () => async (dispatch, getState) => {
     const {
         auth: {
             payload: {
-                sub: { id },
+                sub: { _id },
             },
         },
     } = getState();
@@ -21,7 +21,7 @@ export const actionAboutMe = () => async (dispatch, getState) => {
                         }
                     }`,
                 {
-                    q: JSON.stringify([{ _id: id }]),
+                    q: JSON.stringify([{ _id }]),
                 }
             )
         )

+ 19 - 0
src/actions/actionUserById.js

@@ -0,0 +1,19 @@
+import { gql } from "../helpers";
+
+import { actionPromise } from "../reducers";
+
+export const actionUserById = ({ _id, promiseName = "adminUserById" }) =>
+    actionPromise(
+        promiseName,
+        gql(
+            `query UsersById($q:String){
+                UserFindOne(query: $q){
+                    _id username is_active acl name nick
+                    avatar{
+                        _id url
+                    }
+                }
+            }`,
+            { q: JSON.stringify([{ _id }]) }
+        )
+    );

+ 4 - 0
src/actions/actionUserUpsert.js

@@ -2,6 +2,10 @@ import { gql } from "../helpers";
 import { actionPromise } from "../reducers";
 
 export const actionUserUpsert = (user) => async (dispatch, getState) => {
+    if (!user?.password?.length) {
+        delete user.password;
+    }
+
     await dispatch(
         actionPromise(
             "userUpsert",

+ 5 - 2
src/actions/actionUsersAll.js

@@ -8,9 +8,12 @@ export const actionUsersAll =
             actionPromise(
                 promiseName,
                 gql(
-                    `query OUsersAll($query:String){
+                    `query UsersAll($query:String){
                         UserFind(query: $query){
-                            _id username 
+                            _id username is_active acl
+                            avatar{
+                                _id url
+                            }
                         }
                     }`,
                     {

+ 32 - 0
src/actions/actionUsersFind.js

@@ -0,0 +1,32 @@
+import { actionPromise } from "../reducers";
+import { gql } from "../helpers";
+
+export const actionUsersFind =
+    ({ text = "", limit = 0, skip = 0, promiseName = "adminUsersFind", orderBy = "_id" } = {}) =>
+    async (dispatch, getState) => {
+        dispatch(
+            actionPromise(
+                promiseName,
+                gql(
+                    `query UsersFind($query:String){
+                        UserFind(query: $query){
+                            _id username 
+                        }
+                    }`,
+                    {
+                        query: JSON.stringify([
+                            {
+                                username__contains: text,
+                                _id__contains: text,
+                            },
+                            {
+                                limit: !!limit ? limit : 100,
+                                skip: skip,
+                                orderBy,
+                            },
+                        ]),
+                    }
+                )
+            )
+        );
+    };

+ 6 - 15
src/components/DashboardPage/ProfileForm/index.js

@@ -4,15 +4,16 @@ import { useContext, useEffect, useState } from "react";
 import { connect } from "react-redux";
 import { actionUpdateAvatar } from "../../../actions/actionUpdateAvatar";
 import { actionUserUpdate } from "../../../actions/actionUserUpdate";
-import { Ava } from "../../common/Ava";
 import { DropZone } from "../../common/DropZone";
-import { CProfileImage } from "./ProfileImage";
 import { useFormik } from "formik";
 import * as Yup from "yup";
 import { UIContext } from "../../UIContext";
 import { MdVisibility, MdVisibilityOff } from "react-icons/md";
+import { ProfileImageEditor } from "../../common/ProfileImageEditor";
 
-const CDropZone = connect(null, { onFileDrop: (acceptedFiles) => actionUpdateAvatar(acceptedFiles[0]) })(DropZone);
+const CProfileImageEditor = connect((state) => ({ avatar: state.promise?.aboutMe?.payload?.avatar || null }), {
+    onFileDrop: (acceptedFiles) => actionUpdateAvatar(acceptedFiles[0]),
+})(ProfileImageEditor);
 
 const profileSchema = Yup.object().shape({
     name: Yup.string(),
@@ -22,7 +23,6 @@ const profileSchema = Yup.object().shape({
 });
 
 export const ProfileForm = ({ profile = {}, promiseStatus, onProfileSave, serverErrors = [] } = {}) => {
-    const [isLetterShown, setIsLetterShown] = useState(false);
     const [editMod, setEditMod] = useState(false);
     const [showPassword, setShowPassword] = useState(false);
     const { setAlert } = useContext(UIContext);
@@ -86,19 +86,10 @@ export const ProfileForm = ({ profile = {}, promiseStatus, onProfileSave, server
         <Box component="form" className="ProfileForm" onSubmit={formik.handleSubmit}>
             <Grid container spacing={3}>
                 <Grid xs={4} item>
-                    <CDropZone>
-                        <Box
-                            className="profileImageWrapper"
-                            onMouseEnter={() => setIsLetterShown(true)}
-                            onMouseLeave={() => setIsLetterShown(false)}
-                        >
-                            <CProfileImage />
-                            <Box className={`letter ${isLetterShown && "show"}`}>Drop file or click to update</Box>
-                        </Box>
-                    </CDropZone>
+                    <CProfileImageEditor />
                 </Grid>
                 <Grid xs={8} item>
-                    <Table justify>
+                    <Table>
                         <TableBody>
                             <TableRow>
                                 <TableCell>Username</TableCell>

+ 2 - 2
src/components/LayoutPage/index.js

@@ -165,7 +165,7 @@ export const LayoutPage = () => {
                                 path="/cart"
                                 exact
                                 element={
-                                    <CProtectedRoute roles={["user"]} fallback="/auth">
+                                    <CProtectedRoute roles={["active"]} fallback="/auth">
                                         <CCartPage />
                                     </CProtectedRoute>
                                 }
@@ -188,7 +188,7 @@ export const LayoutPage = () => {
                                 path="/dashboard/"
                                 exact
                                 element={
-                                    <CProtectedRoute roles={["user"]} fallback="/">
+                                    <CProtectedRoute roles={["active"]} fallback="/">
                                         <DashboardPageContainer />
                                     </CProtectedRoute>
                                 }

+ 83 - 2
src/components/admin/AdminLayoutPage/index.js

@@ -12,7 +12,7 @@ import {
     actionFeedOrdersFind,
     actionFeedCatsFind,
 } from "../../../reducers";
-import { actionFeedAdd, actionFeedClear, actionFeedGoods, actionFeedOrders } from "../../../reducers/feedReducer";
+import { actionFeedAdd, actionFeedClear, actionFeedGoods, actionFeedOrders, actionFeedUsers } from "../../../reducers/feedReducer";
 import { CAdminGoodPage } from "../AdminGoodPage";
 import { AdminGoodsPage } from "../AdminGoodsPage";
 import { AdminCategoriesPage } from "../AdminCategoriesPage";
@@ -24,6 +24,22 @@ import { actionCatAll } from "../../../actions/actionCatAll";
 import { actionGoodsAll } from "../../../actions/actionGoodsAll";
 import { CAdminCategoryTree } from "../AdminCategoryTree";
 import { actionUsersAll } from "../../../actions/actionUsersAll";
+import { AdminUsersPage } from "../AdminUsersPage";
+import { CAdminUserPage } from "../AdminUserPage.js";
+import { actionUserById } from "../../../actions/actionUserById";
+
+const AdminCategoryTreePageContainer = ({}) => {
+    const dispatch = useDispatch();
+
+    useEffect(() => {
+        dispatch(actionCatAll());
+        return () => {
+            dispatch(actionPromiseClear("catAll"));
+        };
+    }, []);
+
+    return <CAdminCategoryTree />;
+};
 
 const AdminCategoryPageContainer = ({}) => {
     const dispatch = useDispatch();
@@ -339,6 +355,66 @@ const AdminOrderPageContainer = () => {
     return <CAdminOrderPage />;
 };
 
+const AdminUsersPageContainer = ({ users }) => {
+    const dispatch = useDispatch();
+    const [searchParams] = useSearchParams();
+    const orderBy = searchParams.get("orderBy") || "_id";
+
+    useEffect(() => {
+        dispatch(actionFeedClear());
+        dispatch(actionPromiseClear("feedUsersAll"));
+        dispatch(actionPromiseClear("userUpsert"));
+        dispatch(actionFeedUsers({ skip: 0, orderBy }));
+    }, [orderBy]);
+
+    useEffect(() => {
+        dispatch(actionFeedUsers({ skip: users?.length || 0, orderBy }));
+        window.onscroll = (e) => {
+            if (window.innerHeight + window.scrollY >= document.body.offsetHeight) {
+                const {
+                    feed,
+                    promise: { feedUsersAll },
+                } = store.getState();
+
+                if (feedUsersAll.status !== "PENDING") {
+                    dispatch(actionFeedUsers({ skip: feed.payload?.length || 0, orderBy }));
+                }
+            }
+        };
+        return () => {
+            dispatch(actionFeedClear());
+            dispatch(actionPromiseClear("feedUsersAll"));
+            dispatch(actionPromiseClear("userUpsert"));
+            window.onscroll = null;
+        };
+    }, []);
+
+    useEffect(() => {
+        if (users?.length) store.dispatch(actionFeedAdd(users));
+    }, [users]);
+    return <AdminUsersPage orderBy={orderBy} />;
+};
+
+const AdminUserPageContainer = () => {
+    const params = useParams();
+    const dispatch = useDispatch();
+
+    useEffect(() => {
+        dispatch(actionPromiseClear("adminUserById"));
+        dispatch(actionPromiseClear("uploadFile"));
+        return () => {
+            dispatch(actionPromiseClear("adminUserById"));
+            dispatch(actionPromiseClear("uploadFile"));
+        };
+    }, []);
+    useEffect(() => {
+        if (params._id) {
+            dispatch(actionUserById({ _id: params._id, promiseName: "adminUserById" }));
+        }
+    }, [params._id]);
+    return <CAdminUserPage />;
+};
+
 const CAdminGoodsPageContainer = connect((state) => ({ goods: state.promise?.feedGoodsAll?.payload || [] }))(AdminGoodsPageContainer);
 
 const CAdminOrdersPageContainer = connect((state) => ({ orders: state.promise?.feedOrdersAll?.payload || [] }))(AdminOrdersPageContainer);
@@ -347,12 +423,14 @@ const CAdminCategoriesPageContainer = connect((state) => ({ cats: state.promise?
     AdminCategoriesPageContainer
 );
 
+const CAdminUsersPageContainer = connect((state) => ({ users: state.promise?.feedUsersAll?.payload || [] }))(AdminUsersPageContainer);
+
 const AdminLayoutPage = () => {
     return (
         <Box className="AdminLayoutPage">
             <Routes>
                 <Route path="/" element={<Navigate to={"/admin/goods/"} />} />
-                <Route path="/tree/" element={<CAdminCategoryTree />} />
+                <Route path="/tree/" element={<AdminCategoryTreePageContainer />} />
                 <Route path="/goods/" element={<CAdminGoodsPageContainer />} />
                 <Route path="/goods/search" element={<AdminGoodsSearchPageContainer />} />
                 <Route path="/good/" element={<AdminGoodPageContainer />} />
@@ -365,6 +443,9 @@ const AdminLayoutPage = () => {
                 <Route path="/orders/search" element={<AdminOrdersSearchPageContainer />} />
                 <Route path="/order/" element={<AdminOrderPageContainer />} />
                 <Route path="/order/:_id" element={<AdminOrderPageContainer />} />
+                <Route path="/users/" element={<CAdminUsersPageContainer />} />
+                <Route path="/user/" element={<AdminUserPageContainer />} />
+                <Route path="/user/:_id" element={<AdminUserPageContainer />} />
                 <Route path="*" element={<Navigate to="/404" />} />
             </Routes>
         </Box>

+ 246 - 0
src/components/admin/AdminUserPage.js/UserForm.js

@@ -0,0 +1,246 @@
+import { connect, useDispatch } from "react-redux";
+import { useState, useEffect, useContext } from "react";
+import { actionPromiseClear } from "../../../reducers";
+import { actionUserUpdate } from "../../../actions/actionUserUpdate";
+import { UIContext } from "../../UIContext";
+import Select from "react-select";
+import { Box, Button, Grid, IconButton, InputLabel, Stack, TextField } from "@mui/material";
+import { useFormik } from "formik";
+import * as Yup from "yup";
+import { useNavigate } from "react-router-dom";
+import { MdVisibility, MdVisibilityOff } from "react-icons/md";
+import { aclList } from "../../../helpers";
+import { actionUploadFile } from "../../../actions/actionUploadFile";
+import { ProfileImageEditor } from "../../common/ProfileImageEditor";
+
+const CProfileImageEditor = connect(null, {
+    onFileDrop: (acceptedFiles) => actionUploadFile(acceptedFiles[0]),
+})(ProfileImageEditor);
+
+const userSchema = Yup.object().shape({
+    name: Yup.string(),
+    username: Yup.string().min(3, "Too Short!").max(15, "Too Long!").required("Required"),
+    password: Yup.string().min(3, "Too Short!").max(15, "Too Long!"),
+    nick: Yup.string(),
+});
+
+export const UserForm = ({
+    serverErrors = [],
+    onSaveClick,
+    onSave,
+    onClose,
+    onDelete,
+    promiseStatus,
+    deletePromiseStatus,
+    avatar = null,
+    user = {},
+} = {}) => {
+    const { setAlert } = useContext(UIContext);
+    const [promiseTimeOut, setPromiseTimeOut] = useState(null);
+    const [showPassword, setShowPassword] = useState(false);
+
+    const [acl, setAcl] = useState([]);
+    const navigate = useNavigate();
+    const dispatch = useDispatch();
+
+    const formik = useFormik({
+        initialValues: {
+            name: "",
+            username: "",
+            nick: "",
+            password: "",
+        },
+        validationSchema: userSchema,
+        validateOnChange: true,
+        onSubmit: () => {
+            let userToSave = {};
+            userToSave = formik.values;
+            user?._id && (userToSave._id = user._id);
+            userToSave.acl = acl;
+            userToSave.avatar = avatar;
+            onSaveClick && onSaveClick();
+            onSave(userToSave);
+            setPromiseTimeOut(setTimeout(() => formik.setSubmitting(false), 3000));
+        },
+    });
+
+    useEffect(() => {
+        return () => {
+            promiseTimeOut && clearTimeout(promiseTimeOut);
+            setPromiseTimeOut(null);
+        };
+    }, []);
+
+    useEffect(() => {
+        if (promiseStatus === "FULFILLED") {
+            formik.setSubmitting(false);
+            promiseTimeOut && clearTimeout(promiseTimeOut);
+            setPromiseTimeOut(null);
+            setAlert({
+                show: true,
+                severity: "success",
+                message: "Готово",
+            });
+        }
+        if (promiseStatus === "REJECTED") {
+            const errorMessage = serverErrors.reduce((prev, curr) => prev + "\n" + curr.message, "");
+            formik.setSubmitting(false);
+            promiseTimeOut && clearTimeout(promiseTimeOut);
+            setPromiseTimeOut(null);
+            setAlert({
+                show: true,
+                severity: "error",
+                message: errorMessage,
+            });
+        }
+    }, [promiseStatus]);
+
+    useEffect(() => {
+        if (deletePromiseStatus === "FULFILLED") {
+            promiseTimeOut && clearTimeout(promiseTimeOut);
+            setPromiseTimeOut(null);
+            navigate("/admin/users/");
+        }
+        if (deletePromiseStatus === "REJECTED") {
+            promiseTimeOut && clearTimeout(promiseTimeOut);
+            setPromiseTimeOut(null);
+            setAlert({
+                show: true,
+                severity: "error",
+                message: "Помилка",
+            });
+        }
+        return () => {
+            dispatch(actionPromiseClear("userDelete"));
+        };
+    }, [deletePromiseStatus]);
+
+    useEffect(() => {
+        setAcl(user?.acl || []);
+        formik.setFieldValue("name", user.name || "");
+        formik.setFieldValue("username", user.username || "");
+        formik.setFieldValue("nick", user.nick || "");
+        formik.setFieldValue("password", user.password || "");
+        formik.setFieldValue();
+        formik.validateForm();
+    }, [user]);
+
+    useEffect(() => {
+        return () => {
+            onClose && onClose();
+        };
+    }, []);
+
+    return (
+        <Box className="UserForm" component="form" onSubmit={formik.handleSubmit}>
+            <Grid container>
+                <Grid item xs={5}>
+                    <CProfileImageEditor avatar={avatar} />
+                </Grid>
+                <Grid item xs={7}>
+                    <TextField
+                        id="name"
+                        name="name"
+                        variant="outlined"
+                        label="Ім'я"
+                        size="small"
+                        error={formik.touched.name && Boolean(formik.errors.name)}
+                        value={formik.values.name}
+                        onBlur={formik.handleBlur}
+                        onChange={formik.handleChange}
+                        helperText={formik.touched.name && formik.errors.name}
+                        multiline
+                        fullWidth
+                        sx={{ mt: 2 }}
+                    />
+
+                    <TextField
+                        variant="outlined"
+                        id="username"
+                        name="username"
+                        label="Username"
+                        size="small"
+                        error={formik.touched.username && Boolean(formik.errors.username)}
+                        value={formik.values.username}
+                        onBlur={formik.handleBlur}
+                        onChange={formik.handleChange}
+                        helperText={formik.touched.username && formik.errors.username}
+                        multiline
+                        fullWidth
+                        sx={{ mt: 2 }}
+                    />
+
+                    <TextField
+                        variant="outlined"
+                        id="nick"
+                        name="nick"
+                        label="Nick"
+                        size="small"
+                        error={formik.touched.nick && Boolean(formik.errors.nick)}
+                        value={formik.values.nick}
+                        onBlur={formik.handleBlur}
+                        onChange={formik.handleChange}
+                        helperText={formik.touched.nick && formik.errors.nick}
+                        multiline
+                        fullWidth
+                        sx={{ mt: 2 }}
+                    />
+
+                    <TextField
+                        id="password"
+                        name="password"
+                        variant="outlined"
+                        size="small"
+                        label="Новий пароль"
+                        type={showPassword ? "text" : "password"}
+                        error={formik.touched.password && Boolean(formik.errors.password)}
+                        value={formik.values.password}
+                        onBlur={formik.handleBlur}
+                        onChange={formik.handleChange}
+                        helperText={formik.touched.password && formik.errors.password}
+                        InputProps={{
+                            endAdornment: (
+                                <IconButton onClick={() => setShowPassword((prev) => !prev)} edge="end">
+                                    {showPassword ? <MdVisibilityOff /> : <MdVisibility />}
+                                </IconButton>
+                            ),
+                        }}
+                        fullWidth
+                        sx={{ mt: 2 }}
+                    />
+                    <Box sx={{ mt: 3 }}>
+                        <InputLabel>Категорії</InputLabel>
+                        <Select
+                            placeholder="Обрати категорії"
+                            value={acl.map((value) => ({ value, label: value }))}
+                            closeMenuOnSelect={false}
+                            onChange={(e) => setAcl(e.map(({ value }) => value))}
+                            options={(aclList || [])?.map((value) => ({ value, label: value }))}
+                            isMulti={true}
+                        />
+                    </Box>
+                </Grid>
+            </Grid>
+
+            <Stack direction="row" sx={{ mt: 3 }} justifyContent="flex-end" spacing={1}>
+                <Button variant="contained" disabled={!formik.isValid || formik.isSubmitting} type="submit">
+                    Зберегти
+                </Button>
+            </Stack>
+        </Box>
+    );
+};
+
+export const CUserForm = connect(
+    (state) => ({
+        promiseStatus: state.promise.userUpsert?.status || null,
+        deletePromiseStatus: state.promise.userDelete?.status || null,
+        user: state.promise?.adminUserById?.payload || {},
+        avatar: state.promise?.uploadFile?.payload || state.promise?.adminUserById?.payload?.avatar || null,
+        serverErrors: state.promise?.userUpsert?.error || [],
+    }),
+    {
+        onSave: (user) => actionUserUpdate(user),
+        onClose: () => actionPromiseClear("userUpsert"),
+    }
+)(UserForm);

+ 10 - 0
src/components/admin/AdminUserPage.js/index.js

@@ -0,0 +1,10 @@
+import { Box } from "@mui/material";
+import { CUserForm } from "./UserForm";
+import { connect } from "react-redux";
+
+export const AdminUserPage = ({ good }) => (
+    <Box className="AdminUserPage">
+        <CUserForm good={good} />
+    </Box>
+);
+export const CAdminUserPage = connect((state) => ({ good: state.promise?.adminUserById?.payload || {} }))(AdminUserPage);

+ 36 - 0
src/components/admin/AdminUsersPage/AdminUserItem.js

@@ -0,0 +1,36 @@
+import { Link } from "react-router-dom";
+
+import { Box, Button, TableCell, TableRow } from "@mui/material";
+import { AiFillPlusCircle, AiOutlineMinusCircle } from "react-icons/ai";
+import { backendURL, mediaURL } from "../../../helpers";
+import defaultAvatarImage from "../../../images/default-avatar-image.png";
+
+const AdminUserItem = ({ user }) => (
+    <TableRow className="AdminUserItem">
+        <TableCell scope="row">{user._id}</TableCell>
+        <TableCell scope="row">
+            {
+                <Box
+                    component="img"
+                    src={user?.avatar ? `${backendURL}${mediaURL}${user.avatar?.url}` : defaultAvatarImage}
+                    onError={({ currentTarget }) => {
+                        currentTarget.onerror = null;
+                        currentTarget.src = defaultAvatarImage;
+                    }}
+                />
+            }
+        </TableCell>
+        <TableCell>{user.username ? user.username : "-"}</TableCell>
+        <TableCell>
+            {typeof user.is_active === "boolean" ? user.is_active ? <AiFillPlusCircle /> : <AiOutlineMinusCircle /> : "-"}
+        </TableCell>
+        <TableCell>{user.acl ? user.acl.includes("admin") ? <AiFillPlusCircle /> : <AiOutlineMinusCircle /> : "-"}</TableCell>
+        <TableCell className="edit">
+            <Button component={Link} className="Link" to={`/admin/user/${user._id}/`} variant="contained">
+                Редагувати
+            </Button>
+        </TableCell>
+    </TableRow>
+);
+
+export { AdminUserItem };

+ 58 - 0
src/components/admin/AdminUsersPage/AdminUserList.js

@@ -0,0 +1,58 @@
+import { AdminUserListHeader } from "./AdminUserListHeader";
+import { connect } from "react-redux";
+
+import { SearchBar, SearchResults } from "../../common/SearchBar";
+import { actionUsersFind } from "../../../actions/actionUsersFind";
+import { actionPromiseClear } from "../../../reducers";
+import { Box, Table, TableBody, TableHead } from "@mui/material";
+import { AdminUserItem } from "./AdminUserItem";
+import { createSearchParams, useNavigate, useSearchParams } from "react-router-dom";
+
+const CSearchBar = connect(null, {
+    onSearch: (text) => actionUsersFind({ promiseName: "adminUsersFind", text, limit: 5 }),
+    onSearchEnd: () => actionPromiseClear("adminUsersFind"),
+})(SearchBar);
+
+const CSearchResults = connect((state) => ({ items: state.promise.adminUsersFind?.payload || [] }))(SearchResults);
+
+const AdminUserList = ({ users, orderBy = "_id" }) => {
+    const [searchParams, setSearchParams] = useSearchParams();
+    const navigate = useNavigate();
+
+    return (
+        <Box className="AdminUserList">
+            <Box className="searchBarWrapper">
+                <CSearchBar
+                    render={CSearchResults}
+                    searchLink="/admin/users/search"
+                    renderParams={{ itemLink: "/admin/user/" }}
+                    onSearchButtonClick={(text) => {
+                        searchParams.set("text", text);
+                        setSearchParams(searchParams);
+                        navigate({ pathname: "/admin/users/search", search: createSearchParams(searchParams).toString() });
+                    }}
+                />
+            </Box>
+            <Table>
+                <TableHead>
+                    <AdminUserListHeader
+                        sort={orderBy}
+                        onSortChange={(orderBy) => {
+                            searchParams.set("orderBy", orderBy);
+                            setSearchParams(searchParams);
+                        }}
+                    />
+                </TableHead>
+                <TableBody>
+                    {(users || []).map((user) => (
+                        <AdminUserItem user={user} key={user._id} />
+                    ))}
+                </TableBody>
+            </Table>
+        </Box>
+    );
+};
+
+const CAdminUserList = connect((state) => ({ users: state.feed?.payload || [] }))(AdminUserList);
+
+export { AdminUserList, CAdminUserList };

+ 57 - 0
src/components/admin/AdminUsersPage/AdminUserListHeader.js

@@ -0,0 +1,57 @@
+import { AddButton } from "../../common/AddButton";
+import { TableCell, TableRow, TableSortLabel } from "@mui/material";
+import { useNavigate } from "react-router-dom";
+
+const AdminUserListHeader = ({ onSortChange, sort }) => {
+    const navigate = useNavigate();
+    return (
+        <TableRow className="AdminUserListHeader">
+            <TableCell scope="col">
+                <TableSortLabel
+                    active={sort === "_id" || sort === "-_id"}
+                    direction={sort === "_id" ? "asc" : "desc"}
+                    onClick={() => onSortChange(sort === "_id" ? "-_id" : "_id")}
+                >
+                    #
+                </TableSortLabel>
+            </TableCell>
+            <TableCell scope="col">Аватар</TableCell>
+            <TableCell scope="col">
+                <TableSortLabel
+                    active={sort === "username" || sort === "-username"}
+                    direction={sort === "username" ? "asc" : "desc"}
+                    onClick={() => onSortChange(sort === "username" ? "-username" : "username")}
+                >
+                    Username
+                </TableSortLabel>
+            </TableCell>
+            <TableCell scope="col">
+                <TableSortLabel
+                    active={sort === "is_active" || sort === "-is_active"}
+                    direction={sort === "is_active" ? "asc" : "desc"}
+                    onClick={() => onSortChange(sort === "is_active" ? "-is_active" : "is_active")}
+                >
+                    Активний
+                </TableSortLabel>
+            </TableCell>
+            <TableCell scope="col">
+                <TableSortLabel
+                    active={sort === "is_superuser" || sort === "-is_superuser"}
+                    direction={sort === "is_superuser" ? "asc" : "desc"}
+                    onClick={() => onSortChange(sort === "is_superuser" ? "-is_superuser" : "is_superuser")}
+                >
+                    Адміністратор
+                </TableSortLabel>
+            </TableCell>
+            <TableCell scope="col">
+                <AddButton
+                    onClick={() => {
+                        navigate("/admin/order/");
+                    }}
+                />
+            </TableCell>
+        </TableRow>
+    );
+};
+
+export { AdminUserListHeader };

+ 13 - 0
src/components/admin/AdminUsersPage/index.js

@@ -0,0 +1,13 @@
+import { Box, Typography } from "@mui/material";
+import { CAdminUserList } from "./AdminUserList";
+
+export const AdminUsersPage = ({ orderBy }) => {
+    return (
+        <Box className="AdminUsersPage">
+            <Typography variant="h5" sx={{ marginBottom: "10px", marginTop: "10px" }}>
+                Користувачі
+            </Typography>
+            <CAdminUserList orderBy={orderBy} />
+        </Box>
+    );
+};

+ 0 - 4
src/components/DashboardPage/ProfileForm/ProfileImage.js

@@ -1,10 +1,6 @@
-import { connect } from "react-redux";
-
 import { backendURL, mediaURL } from "../../../helpers";
 import defaultAvatarImage from "../../../images/default-avatar-image.png";
 
 export const ProfileImage = ({ avatar }) => {
     return <img src={avatar?.url ? `${backendURL}${mediaURL}${avatar?.url}` : defaultAvatarImage} className="ProfileImage" />;
 };
-
-export const CProfileImage = connect((state) => ({ avatar: state.promise?.aboutMe?.payload?.avatar || {} }))(ProfileImage);

+ 22 - 0
src/components/common/ProfileImageEditor/index.js

@@ -0,0 +1,22 @@
+import { Box } from "@mui/material";
+import { useState } from "react";
+import { DropZone } from "../DropZone";
+import { ProfileImage } from "./ProfileImage";
+
+export const ProfileImageEditor = ({ onFileDrop, avatar }) => {
+    const [isLetterShown, setIsLetterShown] = useState(false);
+    return (
+        <Box className="ProfileImageEditor">
+            <DropZone onFileDrop={onFileDrop}>
+                <Box
+                    className="profileImageWrapper"
+                    onMouseEnter={() => setIsLetterShown(true)}
+                    onMouseLeave={() => setIsLetterShown(false)}
+                >
+                    <ProfileImage avatar={avatar} />
+                    <Box className={`letter ${isLetterShown && "show"}`}>Drop file or click to update</Box>
+                </Box>
+            </DropZone>
+        </Box>
+    );
+};

+ 4 - 0
src/components/layout/Aside/AdminCategories.js

@@ -17,6 +17,10 @@ const adminCategories = [
         _id: "tree/",
         name: "Дерево категорій",
     },
+    {
+        _id: "users/",
+        name: "Користувачі",
+    },
 ];
 
 export const AdminCategories = () => <Categories categories={adminCategories} url="/admin/" />;

+ 1 - 0
src/helpers/aclList.js

@@ -0,0 +1 @@
+export const aclList = ["anon", "admin", "active"];

+ 3 - 1
src/helpers/index.js

@@ -2,8 +2,10 @@ import { jwtDecode } from "./jwtDecode";
 
 import { getGQL } from "./GraphQL";
 import { statusNumber, statusOptions } from "./orderStatus";
+import { aclList } from "./aclList";
 
 export const backendURL = "http://188.72.209.29/api";
 export const gql = getGQL(backendURL + "/graphql/");
+
 export const mediaURL = "";
-export { jwtDecode, statusNumber, statusOptions };
+export { jwtDecode, statusNumber, statusOptions, aclList };

+ 327 - 374
src/index.scss

@@ -1,442 +1,395 @@
-*{
-  padding: 0;
-  margin:0;
+* {
+    padding: 0;
+    margin: 0;
 }
 
-.Link{
-  color:inherit;
-  text-decoration: none;
+.Link {
+    color: inherit;
+    text-decoration: none;
 }
 
+.ProfileImageEditor {
+    & .profileImageWrapper {
+        position: relative;
+        overflow: hidden;
+        & .ProfileImage {
+            max-width: 100%;
+        }
+        & .letter {
+            z-index: 2;
+            display: block;
+            position: absolute;
+            width: 100%;
+            transition: 0.2s;
+            text-align: center;
+            background: #cecece;
+            padding: 10px;
+            opacity: 0.9;
 
-.Modal{
-  position: fixed;
-  z-index: 1;
-  left: 0;
-  top: 0;
-  width: 100%;
-  height: 100vw;
-  overflow: auto;
-  background-color: rgba(0,0,0,0.1);
-
-
-
-  & .modalContent {
-    background-color: #fefefe;
-    margin: 15% auto;
-    padding: 20px;
-    border-radius: 7px;
-    width:50%;
-
-  }
-
-
+            &.show {
+                margin-top: -43px;
+            }
+        }
+    }
 }
 
+.Modal {
+    position: fixed;
+    z-index: 1;
+    left: 0;
+    top: 0;
+    width: 100%;
+    height: 100vw;
+    overflow: auto;
+    background-color: rgba(0, 0, 0, 0.1);
 
-
-
-
-
-
-.AuthPage{
-
-  display: flex;
-  justify-content: center;
-  min-width: 100%;
-  height: 100vh;
-  align-items: center;
-  & .LoginForm{
-    min-width: 400px;
-  }
+    & .modalContent {
+        background-color: #fefefe;
+        margin: 15% auto;
+        padding: 20px;
+        border-radius: 7px;
+        width: 50%;
+    }
 }
 
+.AuthPage {
+    display: flex;
+    justify-content: center;
+    min-width: 100%;
+    height: 100vh;
+    align-items: center;
+    & .LoginForm {
+        min-width: 400px;
+    }
+}
 
 .carousel {
-  &   .thumb {
-
-    border: 1px solid rgba(0,0,0,0)!important;
-    cursor:pointer;
-    &:hover{
-      border:1px solid #C9C5CA!important;
+    & .thumb {
+        border: 1px solid rgba(0, 0, 0, 0) !important;
+        cursor: pointer;
+        &:hover {
+            border: 1px solid #c9c5ca !important;
+        }
     }
-  }
-
 }
 
-.GoodCard{
-
-  & .BuyButton{
-    margin-left: auto;
-    white-space: nowrap;
-  }
+.GoodCard {
+    & .BuyButton {
+        margin-left: auto;
+        white-space: nowrap;
+    }
 }
 
+.DrawerCart {
+    width: 350px;
+    & .header {
+        justify-content: space-between;
+        width: 100%;
+    }
+    & .header {
+        padding: 10px;
+    }
 
-.DrawerCart{
-  width:350px;
-  & .header{
-    justify-content: space-between;
-    width: 100%;
-  }
-  & .header{
-    padding: 10px;
-  }
-
-
-  & .DrawerCartItem{
-    width:100%;
-    display: flex;
+    & .DrawerCartItem {
+        width: 100%;
+        display: flex;
 
+        & .content {
+            flex: 1;
+        }
 
-    & .content{
-      flex:1;
+        & .buttons {
+            padding: 10px;
+            display: flex;
+            align-items: center;
+        }
     }
-
-    & .buttons{
-      padding: 10px;
-      display: flex;
-      align-items: center;
+    & .list {
     }
-  }
-  & .list{
-
-  }
 }
 
+.App {
+    & .Error404 {
+        height: 99vh;
+    }
 
+    & .Header {
+        margin-bottom: 30px;
 
-.App{
-  & .Error404{
-    height: 99vh;
-  }
-
-
-  & .Header{
-    margin-bottom: 30px;
-
-
-    & .AppBar{
-      background: white;
-      color:#6750A4;
+        & .AppBar {
+            background: white;
+            color: #6750a4;
 
-      & .ToolBar{
-        padding-left: 50px;
-        padding-right: 50px;
+            & .ToolBar {
+                padding-left: 50px;
+                padding-right: 50px;
 
-        & .AvatarButton{
-          height: 46px;
-          width: 46px;
-        }
+                & .AvatarButton {
+                    height: 46px;
+                    width: 46px;
+                }
 
-        & .Logo{
-          width:50px;
-          height:50px;
-          margin-right: 10px;
-        }
-        & .SearchBarWrapper{
-          flex-grow: 1;
-          margin-left: 10px;
-
-          & .SearchBar{
-            position: relative;
-            justify-content: center;
-            padding-left: 20%;
-            & .SearchBarInput{
-              width: 100%;
-              max-width: 500px;
-            }
-            & .SearchResults{
-              position: absolute;
-              width: 100%;
-              max-width: 480px;
-              background: white;
-              padding: 10px;
-              z-index: 2;
-
-              & .SearchGoodResultItem{
-                text-align: left;
-                & img{
-                  width:100%;
-                  max-height: 100px;
+                & .Logo {
+                    width: 50px;
+                    height: 50px;
+                    margin-right: 10px;
                 }
-                &:hover{
-                  background: #F1F2F4;
+                & .SearchBarWrapper {
+                    flex-grow: 1;
+                    margin-left: 10px;
+
+                    & .SearchBar {
+                        position: relative;
+                        justify-content: center;
+                        padding-left: 20%;
+                        & .SearchBarInput {
+                            width: 100%;
+                            max-width: 500px;
+                        }
+                        & .SearchResults {
+                            position: absolute;
+                            width: 100%;
+                            max-width: 480px;
+                            background: white;
+                            padding: 10px;
+                            z-index: 2;
+
+                            & .SearchGoodResultItem {
+                                text-align: left;
+                                & img {
+                                    width: 100%;
+                                    max-height: 100px;
+                                }
+                                &:hover {
+                                    background: #f1f2f4;
+                                }
+                            }
+                        }
+                    }
+                }
+                & .CartIcon {
+                    & .MuiBadge-badge {
+                        right: 4px;
+                        top: 35px;
+                        padding: 0 4px;
+                    }
+
+                    & .CartLogo {
+                        color: #6750a4;
+                        width: 30px;
+                        height: 30px;
+                    }
                 }
-              }
-            }
-
-          }
-        }
-        & .CartIcon{
-          & .MuiBadge-badge{
-            right: 4px;
-            top: 35px;
-            padding: 0 4px;
-          }
-
-          & .CartLogo{
-            color:#6750A4;
-            width:30px;
-            height:30px;
-          }
-        }
-
-        & .LogoutIcon{
 
-          & .LogoutLogo{
-            color:#6750A4;
-            width:30px;
-            height:30px;
-          }
+                & .LogoutIcon {
+                    & .LogoutLogo {
+                        color: #6750a4;
+                        width: 30px;
+                        height: 30px;
+                    }
+                }
+            }
         }
-      }
-
     }
+    & .Aside {
+        margin-left: 50px;
 
-  }
-  & .Aside{
-    margin-left: 50px;
-
-
-    & .body{
-      padding: 10px 0px 100px 0px;
-      border-radius: 5px;
-      border:1px solid #C9C5CA ;
-      & .Categories{
-
-      }
+        & .body {
+            padding: 10px 0px 100px 0px;
+            border-radius: 5px;
+            border: 1px solid #c9c5ca;
+            & .Categories {
+            }
+        }
     }
 
-  }
+    & .Content {
+        margin-left: 50px;
+        margin-right: 50px;
+        min-height: 600px;
+        border-radius: 5px;
+        flex: 1;
+        border: 1px solid #c9c5ca;
 
-  & .Content{
-    margin-left: 50px;
-    margin-right: 50px;
-    min-height: 600px;
-    border-radius: 5px;
-    flex:1;
-    border:1px solid #C9C5CA ;
+        & .DashboardPage {
+            padding: 20px;
 
-    & .DashboardPage{
-      padding: 20px;
-
-      & .Paper{
-        padding: 20px;
-      }
-
-      & .ProfileForm{
-        & .profileImageWrapper{
-          position: relative;
-          overflow: hidden;
-          & .ProfileImage{
-            max-width: 100%;
+            & .Paper {
+                padding: 20px;
+            }
 
-          }
-          & .letter{
-            z-index: 2;
-            display: block;
-            position: absolute;
+            & .ProfileForm {
+                MuiTableCell-root {
+                    border-right: 1px solid grey;
+                }
+            }
 
-            width: 100%;
-            transition:.2s;
-            text-align: center;
-            background: #CECECE;
-            padding: 10px;
-            opacity: .9;
+            & .DashboardOrder {
+                padding: 20px;
 
-            &.show{
-              margin-top:-43px;
+                & .DashboardOrderGood {
+                    & img {
+                        width: 100%;
+                    }
+                }
             }
-          }
-
         }
 
+        & .AdminLayoutPage {
+            padding: 10px;
+            padding-bottom: 400px;
 
+            & .AdminOrderPage {
+                text-align: left;
+            }
 
-        MuiTableCell-root{border-right: 1px solid grey}
-
-
-      }
-
-      & .DashboardOrder{
-        padding:20px;
-
-        & .DashboardOrderGood{
-          & img{
-           width:100%;
-          }
-        }
-      }
-    }
+            & .AdminUserList {
+                & .AdminUserItem {
+                    & img {
+                        width: 100px;
+                    }
+                }
+            }
 
-    & .AdminLayoutPage{
-      padding: 10px;
-      padding-bottom: 400px;
+            & .AdminGoodPage {
+                & .GoodForm {
+                    width: 40%;
+                    text-align: left;
+                }
+                & .EntityEditor {
+                    & .DropZoneImage {
+                        width: 100%;
+                    }
+                    & .Dropzone {
+                        background: #f1f2f4;
+                        width: 100%;
+                        padding: 70px 0;
+                        border: 1px dashed #e9eaec;
+                        border-radius: 5px;
+                        text-align: center;
+                    }
+                }
+            }
+            & .AdminGoodList {
+                & .AdminGoodItem {
+                    & img {
+                        width: 100px;
+                    }
+                }
+            }
 
-      & .AdminOrderPage{
-        text-align: left;
-      }
+            & .AdminCategoryPage {
+                & .CategoryForm {
+                    width: 40%;
+                    text-align: left;
+                }
 
-      & .AdminGoodPage{
-        & .GoodForm{
-          width:40%;
-          text-align: left;
-        }
-        & .EntityEditor{
-          & .DropZoneImage{
-            width: 100%;
-          }
-          & .Dropzone{
-            background: #F1F2F4;
-            width: 100%;
-            padding: 70px 0;
-            border: 1px dashed #E9EAEC;
-            border-radius: 5px;
-            text-align: center;
+                & .AdminCategoryList {
+                    width: 90%;
+                    display: flex;
+                    justify-content: center;
+                    flex-wrap: wrap;
+                    margin-left: 5%;
+                }
+            }
 
-          }
+            & .searchBarWrapper {
+                display: flex;
+                justify-content: center;
+                margin-bottom: 10px;
+
+                & .SearchBar {
+                    position: relative;
+                    & .SearchBarInput {
+                        width: 100%;
+                        max-width: 500px;
+                        min-width: 500px;
+                    }
+                    & .SearchResults {
+                        position: absolute;
+                        width: 100%;
+                        max-width: 480px;
+                        background: white;
+                        padding: 10px;
+                        z-index: 2;
+
+                        & .SearchGoodResultItem {
+                            text-align: left;
+                            & img {
+                                width: 100%;
+                                max-height: 100px;
+                            }
+                            &:hover {
+                                background: #f1f2f4;
+                            }
+                        }
+                        & .SearchOrderResultItem {
+                            text-align: left;
+                            &:hover {
+                                background: #f1f2f4;
+                            }
+                        }
+                    }
+                }
+            }
         }
-      }
-      & .AdminGoodList{
-        width:90%;
-        display: flex;
-        justify-content: center;
-        flex-wrap: wrap;
-        margin-left: 5%;
-
 
-
-        & .AdminGoodItem{
-          & img{
-            width: 100px;
-          }
+        & .MainPage {
+            padding: 10px;
+            & .MainPageImage {
+                width: 100%;
+                border-radius: 10px;
+            }
         }
-      }
 
-      & .AdminCategoryPage{
-        & .CategoryForm{
-          width:40%;
-          text-align: left;
+        & .CartPage {
+            & .OrderForm {
+                padding: 10px 15px;
+                text-align: left;
+            }
         }
 
-        & .AdminCategoryList{
-          width:90%;
-          display: flex;
-          justify-content: center;
-          flex-wrap: wrap;
-          margin-left: 5%;
-
-
+        & .GoodPage {
+            padding: 10px;
+            & .content {
+                text-align: left;
+            }
         }
-      }
 
-      & .searchBarWrapper{
-        display: flex;
-        justify-content: center;
-        margin-bottom:10px ;
-
-        & .SearchBar{
-
-          position: relative;
-          & .SearchBarInput{
-            width: 100%;
-            max-width: 500px;
-            min-width: 500px;
-          }
-          & .SearchResults{
-            position: absolute;
-            width: 100%;
-            max-width: 480px;
-            background: white;
+        & .GoodsPage {
             padding: 10px;
-            z-index: 2;
-
-            & .SearchGoodResultItem{
-              text-align: left;
-              & img{
-                width:100%;
-                max-height: 100px;
-              }
-              &:hover{
-                background: #F1F2F4;
-              }
+            & .sortOptionsWrapper {
+                display: flex;
+                justify-content: right;
+                padding-right: 15px;
             }
-            & .SearchOrderResultItem{
-              text-align:left;
-              &:hover{
-                background: #F1F2F4;
-              }
+            & .Divider {
+                margin-top: 10px;
+                margin-bottom: 10px;
+            }
+            & .SubCategories {
+                padding-top: 10px;
+                padding-bottom: 10px;
+                display: flex;
+                & .SubCategory {
+                    width: 250px;
+                    padding: 15px 7px;
+                    margin-right: 15px;
+                    margin-bottom: 15px;
+                    cursor: pointer;
+                }
             }
-          }
-
         }
-      }
-    }
-
-
-    & .MainPage{
-      padding: 10px;
-      & .MainPageImage{
-        width: 100%;
-        border-radius: 10px;
-      }
-
-    }
-
-    & .CartPage{
-      & .OrderForm{
-        padding: 10px 15px;
-        text-align: left;
-      }
     }
 
-    & .GoodPage{
-      padding:10px;
-      & .content{
-        text-align: left;
-      }
-
-    }
-
-    & .GoodsPage{
-      padding:10px;
-      & .sortOptionsWrapper{
-        display: flex;
-        justify-content: right;
-        padding-right: 15px;
-
-      }
-      & .Divider{
-        margin-top: 10px;
-        margin-bottom: 10px;
-      }
-      & .SubCategories{
-        padding-top: 10px;
-        padding-bottom: 10px;
-        display: flex;
-        & .SubCategory{
-          width: 250px;
-          padding: 15px 7px;
-          margin-right: 15px;
-          margin-bottom: 15px;
-          cursor: pointer;
+    & .Footer {
+        margin-top: 70px;
+        background-color: #f4eff4;
+        padding: 25px 0px;
+        & .TableCell {
+            border-bottom: none;
+            padding: 0;
+            padding-top: 5px;
         }
-
-      }
-    }
-  }
-
-  & .Footer{
-    margin-top:70px;
-    background-color:  #F4EFF4;
-    padding: 25px 0px;
-    & .TableCell{
-      border-bottom: none;
-      padding: 0;
-      padding-top: 5px;
     }
-
-  }
 }
-

+ 16 - 0
src/reducers/feedReducer.js

@@ -5,6 +5,8 @@ import { actionGoodsAll } from "../actions/actionGoodsAll";
 import { actionOrdersAll } from "../actions/actionOrdersAll";
 import { actionOrdersFind } from "../actions/actionOrdersFind";
 import { actionCategoryGoods } from "../actions/actionCategoryGoods";
+import { actionUsersFind } from "../actions/actionUsersFind";
+import { actionUsersAll } from "../actions/actionUsersAll";
 
 function feedReducer(state = { payload: [] }, { type, payload = [] }) {
     if (type === "FEED_ADD") {
@@ -64,6 +66,18 @@ const actionFeedOrdersFind =
         await dispatch(actionOrdersFind({ skip, limit: 5, promiseName: "feedOrdersFind", text, orderBy }));
     };
 
+const actionFeedUsersFind =
+    ({ skip = 0, text = "", orderBy = "_id" }) =>
+    async (dispatch, getState) => {
+        await dispatch(actionUsersFind({ skip, promiseName: "feedUsersFind", text, limit: 7, orderBy }));
+    };
+
+const actionFeedUsers =
+    ({ skip = 0, orderBy = "_id" }) =>
+    async (dispatch, getState) => {
+        await dispatch(actionUsersAll({ promiseName: "feedUsersAll", skip, limit: 15, orderBy }));
+    };
+
 export {
     actionFeedCats,
     actionFeedCatsFind,
@@ -75,4 +89,6 @@ export {
     actionFeedOrders,
     actionFeedOrdersFind,
     actionFeedCategoryGoods,
+    actionFeedUsers,
+    actionFeedUsersFind,
 };

+ 4 - 0
src/reducers/index.js

@@ -14,6 +14,8 @@ import {
     actionFeedOrdersFind,
     actionFeedOrders,
     feedReducer,
+    actionFeedUsers,
+    actionFeedUsersFind,
 } from "./feedReducer";
 
 export { cartReducer, actionCartAdd, actionCartChange, actionCartDelete, actionCartClear };
@@ -28,6 +30,8 @@ export {
     actionFeedAdd,
     actionFeedOrdersFind,
     actionFeedOrders,
+    actionFeedUsers,
+    actionFeedUsersFind,
     feedReducer,
 };
 export const store = createStore(