viktoriia.kapran пре 1 година
родитељ
комит
01ad6691bc
34 измењених фајлова са 502 додато и 353 уклоњено
  1. 2 6
      js21 react/my-react-app/src/App.js
  2. 1 0
      js21 react/my-react-app/src/components/CartGood.js
  3. 20 7
      js21 react/my-react-app/src/components/CategoriesMenu/CategoryMenu.js
  4. 2 2
      js21 react/my-react-app/src/components/CategoriesSection/CategoriesSection.js
  5. 1 1
      js21 react/my-react-app/src/components/Counter/Counter.js
  6. 4 2
      js21 react/my-react-app/src/components/Counter/Counter.scss
  7. 2 21
      js21 react/my-react-app/src/components/Dnd.js
  8. 0 16
      js21 react/my-react-app/src/components/DrawUserName.js
  9. 12 30
      js21 react/my-react-app/src/components/EditForm.js
  10. 80 10
      js21 react/my-react-app/src/components/Header.js
  11. 36 24
      js21 react/my-react-app/src/components/ImageUploader.js
  12. 0 17
      js21 react/my-react-app/src/components/ItemList.js
  13. 7 11
      js21 react/my-react-app/src/components/LoginForm.js
  14. 15 0
      js21 react/my-react-app/src/components/NotFoundBanner.js
  15. 23 0
      js21 react/my-react-app/src/components/SearchComponent.js
  16. 18 0
      js21 react/my-react-app/src/components/ShowUserName.js
  17. 2 2
      js21 react/my-react-app/src/components/hoc/RequireAdmin.js
  18. 2 2
      js21 react/my-react-app/src/components/hoc/RequireAuth.js
  19. 2 6
      js21 react/my-react-app/src/pages/CartPage.js
  20. 10 5
      js21 react/my-react-app/src/pages/CategoryPage/CategoryPage.js
  21. 127 86
      js21 react/my-react-app/src/pages/EditProfilePage.js
  22. 1 7
      js21 react/my-react-app/src/pages/LoginPage.js
  23. 28 0
      js21 react/my-react-app/src/pages/SearchedGoodsPage.js
  24. 20 13
      js21 react/my-react-app/src/pages/UserPage.js
  25. 0 7
      js21 react/my-react-app/src/pages/admin/AdminPage.js
  26. 12 18
      js21 react/my-react-app/src/pages/admin/CategoriesPage.js
  27. 11 1
      js21 react/my-react-app/src/pages/admin/CreateCategoryPage.js
  28. 12 1
      js21 react/my-react-app/src/pages/admin/CreateGoodPage.js
  29. 13 1
      js21 react/my-react-app/src/pages/admin/EditCategoryPage.js
  30. 12 1
      js21 react/my-react-app/src/pages/admin/EditGoodPage.js
  31. 7 2
      js21 react/my-react-app/src/pages/admin/GoodsPage.js
  32. 1 2
      js21 react/my-react-app/src/pages/admin/OrdersPage.js
  33. 1 1
      js21 react/my-react-app/src/pages/admin/UsersPage.js
  34. 18 51
      js21 react/my-react-app/src/store/api.js

+ 2 - 6
js21 react/my-react-app/src/App.js

@@ -6,7 +6,6 @@ import NotFound from './pages/NotFound';
 import UserPage from './pages/UserPage';
 import RegisterPage from './pages/RegisterPage';
 import LoginPage from './pages/LoginPage';
-import AdminPage from './pages/admin/AdminPage';
 import CartPage from './pages/CartPage';
 import CategoryPage from './pages/CategoryPage/CategoryPage';
 import GoodPage from './pages/GoodPage/GoodPage';
@@ -24,6 +23,7 @@ import OrdersPage from './pages/admin/OrdersPage';
 import CreateGoodPage from './pages/admin/CreateGoodPage';
 import EditGoodPage from './pages/admin/EditGoodPage';
 import EditProfilePage from './pages/EditProfilePage';
+import SearchedGoodsPage from './pages/SearchedGoodsPage';
 
 
 function App() {
@@ -44,11 +44,6 @@ function App() {
               </RequireAuth>
             } />
             <Route path='/cart' element={<CartPage />} />
-            <Route path='/admin' element={
-              <RequireAdmin>
-                <AdminPage />
-              </RequireAdmin>
-            } />
             <Route path='/admin/users' element={
               <RequireAdmin>
                 <UsersPage />
@@ -93,6 +88,7 @@ function App() {
             <Route path='/login' element={<LoginPage />} />
             <Route path='/:category/:categoryId' element={<CategoryPage />} />
             <Route path='/good/:goodId' element={<GoodPage />} />
+            <Route path='/goods' element={<SearchedGoodsPage />} />
             <Route path='*' element={<NotFound />} />
           </Route>
         </Routes>

+ 1 - 0
js21 react/my-react-app/src/components/CartGood.js

@@ -18,6 +18,7 @@ export default function CartGood({ good }) {
   const [totalSum, setTotalSum] = useState(good.good.price * good.count);
   const dispatch = useDispatch();
   const count = useSelector(state => state.cart.goods.find(findedGood => findedGood.good._id === good.good._id)?.count);
+  
   useEffect(() => {
     setTotalSum(good.good.price * count);
   }, [count]);

+ 20 - 7
js21 react/my-react-app/src/components/CategoriesMenu/CategoryMenu.js

@@ -1,28 +1,41 @@
 import React from 'react';
 import './CategoryMenu.scss'
 import Skeleton from '@mui/material/Skeleton';
-import { useGetCategoriesQuery } from '../../store/api';
+import { useGetRootCategoriesQuery } from '../../store/api';
 import { useSelector } from 'react-redux';
-import ItemList from '../ItemList';
 import List from '@mui/material/List';
+import { Link } from 'react-router-dom';
+import ListItem from '@mui/material/ListItem';
+import ListItemButton from '@mui/material/ListItemButton';
+import ListItemText from '@mui/material/ListItemText';
+
+function LiItem({ url, text }) {
+  return (
+    <ListItem disablePadding>
+      <ListItemButton component={Link} to={url}>
+        <ListItemText primary={text} />
+      </ListItemButton>
+    </ListItem>
+  )
+}
 
 const CategoryMenu = () => {
-  const { data, error, isFetching } = useGetCategoriesQuery();
-  const admin = useSelector(state => state.auth?.payload?.sub?.acl?.includes('admin'));
+  const { data, error, isFetching } = useGetRootCategoriesQuery();
+  const isAdmin = useSelector(state => state.auth?.payload?.sub?.acl?.includes('admin'));
   const adminMenu = ['Categories', 'Goods', 'Orders', 'Users'];
 
   return (
     <aside className='aside'>
-      {admin ?
+      {isAdmin ?
         <List>
           {adminMenu.map((category, index) =>
-            <ItemList key={index} url={`/admin/${category.toLowerCase()}`} text={category} />)}
+            <LiItem key={index} url={`/admin/${category.toLowerCase()}`} text={category} />)}
         </List>
         :
         (isFetching ? Array(10).fill(1).map((_, index) => <Skeleton key={index} className='skeleton' />) :
           <List>
             {data.CategoryFind.map(category =>
-              <ItemList key={category._id} url={`/${category.name}/${category._id}`} text={category.name} />)}
+              <LiItem key={category._id} url={`/${category.name}/${category._id}`} text={category.name} />)}
           </List>)}
 
     </aside>

+ 2 - 2
js21 react/my-react-app/src/components/CategoriesSection/CategoriesSection.js

@@ -3,10 +3,10 @@ import React from "react";
 import { Link } from "react-router-dom";
 import './CategoriesSection.scss';
 
-export function CategoriesSection({ categories, categoryEl }) {
+export function CategoriesSection({ categories, categoryLabel }) {
   return (
     <Box sx={{ flexGrow: 1 }}>
-      {categoryEl}
+      {categoryLabel}
       {categories?.map(category => <Link key={category._id} to={`/${category.name}/${category._id}`} className="link-style">
         {category.name}</Link>)}
     </Box>

+ 1 - 1
js21 react/my-react-app/src/components/Counter/Counter.js

@@ -10,12 +10,12 @@ export default function Counter({ value, onCount }) {
   return (
     <Box sx={{ width: '50px', mx: '6px' }}>
       <TextField
+        className='counter'
         type="number"
         min="1"
         value={countValue}
         size="small"
         onChange={e => {
-          console.log(e.target.value);
           setCountValue(e.target.value);
           if (e.target.value !== '') { // emit only valid values
             onCount(+e.target.value);

+ 4 - 2
js21 react/my-react-app/src/components/Counter/Counter.scss

@@ -1,5 +1,7 @@
-input {
-  text-align: center;
+.counter {
+  * {
+    text-align: center;
+  }
 }
 input[type="number"]::-webkit-outer-spin-button,
 input[type="number"]::-webkit-inner-spin-button {

+ 2 - 21
js21 react/my-react-app/src/components/Dnd.js

@@ -11,8 +11,6 @@ import { sortableKeyboardCoordinates, rectSortingStrategy, SortableContext, useS
 import { CSS } from "@dnd-kit/utilities";
 import { arrayMoveImmutable } from 'array-move';
 
-
-
 const SortableItem = (props) => {
   const {
     attributes,
@@ -25,17 +23,7 @@ const SortableItem = (props) => {
   const itemStyle = {
     transform: CSS.Transform.toString(transform),
     transition,
-    //width: 110,
-    //height: 30,
-    //display: "flex",
-    //alignItems: "center",
-    //paddingLeft: 5,
-    //border: "1px solid gray",
-    //borderRadius: 5,
-    //marginBottom: 5,
-    //userSelect: "none",
     cursor: "grab",
-    //boxSizing: "border-box"
   };
 
   const Render = props.render;
@@ -51,12 +39,7 @@ const SortableItem = (props) => {
 const Droppable = ({ id, items, itemProp, keyField, render }) => {
   const { setNodeRef } = useDroppable({ id });
 
-  const droppableStyle = {
-    //padding: "20px 10px",
-    //border: "1px solid black",
-    //borderRadius: "5px",
-    //minWidth: 110
-  };
+  const droppableStyle = {};
 
   return (
     <SortableContext id={id} items={items} strategy={rectSortingStrategy}>
@@ -70,9 +53,7 @@ const Droppable = ({ id, items, itemProp, keyField, render }) => {
 
 
 function Dnd({ items: startItems, render, itemProp, keyField, onChange, horizontal }) {
-  const [items, setItems] = useState(
-    startItems
-  );
+  const [items, setItems] = useState(startItems);
   useEffect(() => setItems(startItems), [startItems]);
 
   useEffect(() => {

+ 0 - 16
js21 react/my-react-app/src/components/DrawUserName.js

@@ -1,16 +0,0 @@
-import React from "react";
-import { useSelector } from 'react-redux';
-import Box from '@mui/material/Box';
-import store from '../store';
-
-const DrawUserName = () => {
-  const token = useSelector(state => state.auth.token);
-
-  return (
-    <>
-      {token ? <Box>Hello, {store.getState().auth?.payload?.sub?.login}</Box> : <></>}
-    </>
-
-  )
-}
-export default DrawUserName;

+ 12 - 30
js21 react/my-react-app/src/components/EditForm.js

@@ -1,8 +1,7 @@
 import { Box, Button, Checkbox, FormControl, InputLabel, ListItemText, MenuItem, OutlinedInput, Select, Stack, TextField } from '@mui/material';
 import React, { useState } from 'react';
-import { useLocation, useNavigate } from 'react-router-dom';
 import ImageUploader from '../components/ImageUploader';
-import { useCreateCategoryMutation, useCreateGoodMutation, useGetCategoriesWithSubCategoriesQuery } from '../store/api';
+import { useGetAllCategoriesQuery } from '../store/api';
 
 
 const ITEM_HEIGHT = 48;
@@ -16,22 +15,13 @@ const MenuProps = {
   },
 };
 
-function EditForm({ id, name, categories, price, description, goodImages, buttonText }) {
-  const { data } = useGetCategoriesWithSubCategoriesQuery();
+function EditForm({ id, name, categories, price, description, goodImages, buttonText, entity, onSubmit }) {
+  const { data } = useGetAllCategoriesQuery();
   const [itemName, setItemName] = useState(name || '');
   const [selectedCategories, setSelectedCategories] = useState(categories || []);
   const [itemPrice, setItemPrice] = useState(price || 0);
   const [itemDescription, setItemDescription] = useState(description || '');
   const [images, setImages] = useState(goodImages || []);
-  const navigate = useNavigate();
-  const location = useLocation();
-  const goodLocation = location.pathname == '/admin/good' || location.pathname == `/admin/good/${id}`;
-  const categoryLocation = location.pathname == '/admin/category' || location.pathname == `/admin/category/${id}`
-
-  const [createGood, result] = useCreateGoodMutation();
-
-  const [createCategory, resultMut] = useCreateCategoryMutation();
-
 
   const onCategoryClick = (category) => {
     if (selectedCategories.find(selectedCategory => selectedCategory._id == category._id)) {
@@ -47,13 +37,12 @@ function EditForm({ id, name, categories, price, description, goodImages, button
     setItemDescription(event.target.value);
   }
 
-
   const handleSubmit = () => {
 
     const item = {
       name: itemName,
     };
-    if (goodLocation) {
+    if (entity == 'good') {
       item.categories = selectedCategories.map(category => ({
         _id: category._id,
         name: category.name
@@ -62,7 +51,7 @@ function EditForm({ id, name, categories, price, description, goodImages, button
       item.description = itemDescription;
       item.images = images.map(image => ({ _id: image._id }));
     }
-    if (categoryLocation) {
+    if (entity == 'category') {
       item.subCategories = selectedCategories.map(subCategory => ({
         _id: subCategory._id,
         name: subCategory.name,
@@ -72,17 +61,10 @@ function EditForm({ id, name, categories, price, description, goodImages, button
     if (id) {
       item._id = id;
     }
-    if (goodLocation) {
-      createGood(item).then(response => {
-        navigate('/admin/goods');
-      });
-    } else if (categoryLocation) {
-      createCategory(item).then(response => {
-        navigate('/admin/categories');
-      });
+    if (onSubmit) {
+      onSubmit(item);
     }
-
-  }
+  };
 
   return (
     <Box component='form' onSubmit={handleSubmit}>
@@ -95,11 +77,11 @@ function EditForm({ id, name, categories, price, description, goodImages, button
         />
         <FormControl
           sx={{ m: '0 auto 20px', width: '100%' }}>
-          <InputLabel>{goodLocation ? 'Category' : 'Subcategories'}</InputLabel>
+          <InputLabel>{entity == 'good' ? 'Category' : 'Subcategories'}</InputLabel>
           <Select
             multiple
             value={selectedCategories}
-            input={<OutlinedInput label={goodLocation ? 'Category' : 'Subcategories'} />}
+            input={<OutlinedInput label={entity == 'good' ? 'Category' : 'Subcategories'} />}
             renderValue={(selected) => selected.map(item => item.name).join(', ')}
             MenuProps={MenuProps}
           >
@@ -111,7 +93,7 @@ function EditForm({ id, name, categories, price, description, goodImages, button
             ))}
           </Select>
         </FormControl>
-        {goodLocation &&
+        {entity == 'good' &&
           <>
             <TextField
               sx={{ m: '0 auto 20px', width: '100%' }}
@@ -128,7 +110,7 @@ function EditForm({ id, name, categories, price, description, goodImages, button
               value={itemDescription}
               onChange={handleDescriptionChange}
             />
-            <ImageUploader passImages={goodImages} onChange={(images) => setImages(images)} />
+            <ImageUploader previousImages={goodImages} onChange={(images) => setImages(images)} />
           </>
         }
         <Button variant="contained" sx={{ maxWidth: '200px', width: '100%', m: '20px auto 0' }} onClick={() => handleSubmit()}>{buttonText}</Button>

+ 80 - 10
js21 react/my-react-app/src/components/Header.js

@@ -1,17 +1,20 @@
-import React from 'react';
-import { Link } from 'react-router-dom';
+import React, { useState } from 'react';
+import { createSearchParams, Link, useNavigate } from 'react-router-dom';
 import Badge from '@mui/material/Badge';
-import { styled } from '@mui/material/styles';
+import InputBase from '@mui/material/InputBase';
+import { styled, alpha } from '@mui/material/styles';
 import IconButton from '@mui/material/IconButton';
-import { ShoppingCart, AccountCircle } from '@mui/icons-material/';
+import { ShoppingCart, AccountCircle, Search } from '@mui/icons-material/';
 import AppBar from '@mui/material/AppBar';
 import Box from '@mui/material/Box';
 import Toolbar from '@mui/material/Toolbar';
 import { useDispatch, useSelector } from 'react-redux';
 import { logout } from '../store/authSlice';
 import { clearCart } from '../store/cartSlice';
-import DrawUserName from './DrawUserName';
+import ShowUserName from './ShowUserName';
 import LinkButton from './LinkButton';
+import { BASE_URL, useGetUserByIdQuery } from '../store/api';
+import { Avatar } from '@mui/material';
 
 const StyledBadge = styled(Badge)(({ theme }) => ({
   '& .MuiBadge-badge': {
@@ -21,8 +24,42 @@ const StyledBadge = styled(Badge)(({ theme }) => ({
     padding: '0 4px',
   },
 }));
+
+const SearchEl = styled('div')(({ theme }) => ({
+  position: 'relative',
+  borderRadius: theme.shape.borderRadius,
+  backgroundColor: alpha(theme.palette.common.white, 0.15),
+  '&:hover': {
+    backgroundColor: alpha(theme.palette.common.white, 0.25),
+  },
+  marginRight: theme.spacing(2),
+  marginLeft: 0,
+  width: '100%',
+  [theme.breakpoints.up('sm')]: {
+    marginLeft: theme.spacing(3),
+    width: 'auto',
+  },
+}));
+
+const StyledInputBase = styled(InputBase)(({ theme }) => ({
+  color: 'inherit',
+  '& .MuiInputBase-input': {
+    padding: theme.spacing(1, 1, 1, 0),
+    // vertical padding + font size from searchIcon
+    paddingLeft: `calc(1em + ${theme.spacing(1)})`,
+    transition: theme.transitions.create('width'),
+    width: '100%',
+    [theme.breakpoints.up('md')]: {
+      width: '40ch',
+    },
+  },
+}));
+
+
 export default function Header() {
+  let [search, setSearch] = useState('');
   const dispatch = useDispatch();
+  const navigate = useNavigate();
   const goodsInCart = useSelector(state => state.cart.goodsCount);
   const onLogout = () => {
     dispatch(logout());
@@ -30,15 +67,44 @@ export default function Header() {
   }
   const token = useSelector(state => state.auth.token);
   const userId = useSelector(state => state.auth?.payload?.sub?.id);
-  const admin = useSelector(state => state.auth?.payload?.sub?.acl.includes('admin'));
+  const isAdmin = useSelector(state => state.auth?.payload?.sub?.acl.includes('admin'));
+  const {data} = useGetUserByIdQuery(userId);
+  
+  const handleChange = (e) => {
+    setSearch(e.target.value);
+  }
+
+  const searchData = (searchStr) => {
+    navigate({
+      pathname: '/goods',
+      search: '?' + createSearchParams({ text: searchStr }),
+    });
+    setSearch('');
+  }
+
   return (
     <AppBar position="static">
       <Toolbar>
-        <Box sx={{ flexGrow: 1 }}>
+        <Box sx={{ flexGrow: 1, display: 'flex', alignItems: 'center' }}>
           <Link to="/"><img src="/images/logo.jpg" /></Link>
+          <SearchEl>
+            <StyledInputBase
+              placeholder="Search…"
+              inputProps={{
+                'value': search,
+                onChange: (e) => handleChange(e),
+                onKeyPress: (e) => {
+                  if (e.key === 'Enter') {
+                    searchData(e.target.value);
+                  }
+                }
+              }}
+            />
+          </SearchEl>
+          <IconButton onClick={() => searchData(search)}><Search sx={{color: 'white'}}/></IconButton>
         </Box>
         <Box sx={{ display: 'flex', alignItems: 'center' }}>
-          <DrawUserName />
+          <ShowUserName />
           <Box sx={{ mx: 1, my: 2 }}>
             <Link to="cart">
               <IconButton>
@@ -50,8 +116,12 @@ export default function Header() {
           </Box>
           {token ?
             <>
-              {!admin && <Link to={`/user/${userId}`}>
-                <IconButton><AccountCircle sx={{ color: 'white', fontSize: '30px' }} /></IconButton>
+              {!isAdmin && <Link to={`/user/${userId}`}>
+                {data?.UserFindOne?.avatar ? 
+                <Avatar src={BASE_URL + '/' + data.UserFindOne.avatar.url}/>
+                :
+                <IconButton><AccountCircle sx={{ color: 'white', fontSize: '30px' }} /></IconButton>}
+
               </Link>}
               <LinkButton to="/" click={onLogout}>Logout</LinkButton>
             </>

+ 36 - 24
js21 react/my-react-app/src/components/ImageUploader.js

@@ -1,8 +1,8 @@
 import React, { useMemo, useState } from 'react';
-import {  useUploadImageMutation } from '../store/api';
+import { BASE_URL, useUploadImageMutation } from '../store/api';
 import { useDropzone } from 'react-dropzone';
 import { Image } from './Image/Image';
-import { Box } from '@mui/material';
+import { Avatar, Box } from '@mui/material';
 import Dnd from './Dnd';
 
 const baseStyle = {
@@ -21,6 +21,10 @@ const baseStyle = {
   transition: 'border .24s ease-in-out'
 };
 
+const avatarStyle = {
+  cursor: 'pointer',
+};
+
 const focusedStyle = {
   borderColor: '#2196f3'
 };
@@ -38,31 +42,34 @@ const ImageOnDnd = ({ image, onDelete }) =>
     <Image url={image?.url} click={() => onDelete(image)} />
   </Box>
 
-function ImageUploader({ passImages, onChange }) {
-  const [images, setImages] = useState(passImages || []);
+function ImageUploader({ previousImages, onChange, isAvatar }) {
+  const [images, setImages] = useState(previousImages || []);
   const [uploadImage, result] = useUploadImageMutation();
-  console.log('result', result);
-
-  // const onDrop = useCallback(acceptedFiles => {
-  //   filesUpload(acceptedFiles).then(imagesResponse => {
-  //     setImages(images.concat(imagesResponse));
-  //   });
-  // }, [])
 
   const updateImages = images => {
     setImages(images);
-    onChange(images);
+    if (onChange) {
+      onChange(images);
+    }
   }
 
   const onDrop = acceptedFiles => {
-    filesUpload(acceptedFiles).then(imagesResponse => {
-      updateImages(images.concat(imagesResponse));
-    });
+      filesUpload(acceptedFiles).then(imagesResponse => {
+        console.log(imagesResponse);
+        updateImages(isAvatar ? imagesResponse : images.concat(imagesResponse));
+      });
   }
-  const { getRootProps, getInputProps, isDragActive, isFocused, isDragAccept, isDragReject } = useDropzone({ accept: { 'image/*': [] }, onDrop });
+  const {
+    getRootProps,
+    getInputProps,
+    isDragActive,
+    isFocused,
+    isDragAccept,
+    isDragReject
+  } = useDropzone({ accept: { 'image/*': [] }, onDrop, maxFiles: !!isAvatar });
 
   const style = useMemo(() => ({
-    ...baseStyle,
+    ...(isAvatar ? avatarStyle : baseStyle),
     ...(isFocused ? focusedStyle : {}),
     ...(isDragAccept ? acceptStyle : {}),
     ...(isDragReject ? rejectStyle : {})
@@ -72,11 +79,12 @@ function ImageUploader({ passImages, onChange }) {
     isDragReject
   ]);
 
+
+
   const fileUpload = (file) => {
     const formData = new FormData();
     formData.append('photo', file);
     return uploadImage(formData).then(response => {
-      console.log('response', response);
       return Promise.resolve(response.data);
     });
   }
@@ -92,15 +100,19 @@ function ImageUploader({ passImages, onChange }) {
     <>
       <div {...getRootProps({ style })}>
         <input {...getInputProps()} />
-        {
-          isDragActive ?
-            <p>Drop the files here ...</p> :
-            <p>Drag 'n' drop some files here, or click to select files</p>
+        {isAvatar ? <Avatar sx={{width: '250px', height: '250px'}} src={images[0] ? (BASE_URL + '/' + images[0]?.url) : ''}/> :
+          <>
+            {
+              isDragActive ?
+                <p>Drop the files here ...</p> :
+                <p>Drag 'n' drop some files here, or click to select files</p>
+            }
+          </>
         }
       </div>
-      <Box sx={{ display: 'flex', m: '20px auto 0' }}>
+      {!isAvatar && <Box sx={{ display: 'flex', m: '20px auto 0' }}>
         <Dnd items={images} render={localImage} onChange={images => updateImages(images)} itemProp="image" keyField="_id" horizontal />
-      </Box>
+      </Box>}
     </>
   )
 }

+ 0 - 17
js21 react/my-react-app/src/components/ItemList.js

@@ -1,17 +0,0 @@
-import React from 'react';
-import { Link } from 'react-router-dom';
-import ListItem from '@mui/material/ListItem';
-import ListItemButton from '@mui/material/ListItemButton';
-import ListItemText from '@mui/material/ListItemText';
-
-function ItemList({ url, text }) {
-  return (
-    <ListItem disablePadding>
-      <ListItemButton component={Link} to={url}>
-        <ListItemText primary={text} />
-      </ListItemButton>
-    </ListItem>
-  )
-}
-
-export default ItemList;

+ 7 - 11
js21 react/my-react-app/src/components/LoginForm.js

@@ -14,14 +14,10 @@ const LoginForm = ({ submit, onSubmit }) => {
   const [confirmPassword, setConfirmPassword] = useState('');
   const [showPassword, setShowPassword] = useState(false);
   const [showConfirmPassword, setShowConfirmPassword] = useState(false);
-  const handleClickShowPassword = () => setShowPassword((show) => !show);
-  const handleClickShowConfirmPassword = () => setShowConfirmPassword((show) => !show);
+  const onShowPassword = () => setShowPassword((show) => !show);
+  const onShowConfirmPassword = () => setShowConfirmPassword((show) => !show);
 
-  const handleMouseDownConfirmPassword = (event) => {
-    event.preventDefault();
-  };
-
-  const handleMouseDownPassword = (event) => {
+  const onMouseDown = (event) => {
     event.preventDefault();
   };
 
@@ -55,8 +51,8 @@ const LoginForm = ({ submit, onSubmit }) => {
             endAdornment={
               <InputAdornment position="end">
                 <IconButton
-                  onClick={handleClickShowPassword}
-                  onMouseDown={handleMouseDownPassword}
+                  onClick={onShowPassword}
+                  onMouseDown={onMouseDown}
                   edge="end"
                 >
                   {showPassword ? <VisibilityOff /> : <Visibility />}
@@ -75,8 +71,8 @@ const LoginForm = ({ submit, onSubmit }) => {
               endAdornment={
                 <InputAdornment position="end">
                   <IconButton
-                    onClick={handleClickShowConfirmPassword}
-                    onMouseDown={handleMouseDownConfirmPassword}
+                    onClick={onShowConfirmPassword}
+                    onMouseDown={onMouseDown}
                     edge="end"
                   >
                     {showConfirmPassword ? <VisibilityOff /> : <Visibility />}

+ 15 - 0
js21 react/my-react-app/src/components/NotFoundBanner.js

@@ -0,0 +1,15 @@
+import { Box } from '@mui/material';
+import React from 'react';
+
+function NotFoundBanner({text}) {
+  return (
+    <Box sx={{
+      textAlign: 'center', m: '100px auto 0', fontSize: '30px', backgroundColor: '#d5d5d5',
+      color: '#fff', p: '50px 30px', maxWidth: '420px', borderRadius: '10px'
+    }}>
+      {text}
+    </Box>
+  )
+}
+
+export default NotFoundBanner;

+ 23 - 0
js21 react/my-react-app/src/components/SearchComponent.js

@@ -0,0 +1,23 @@
+import { Search } from '@mui/icons-material';
+import { Box, IconButton, TextField } from '@mui/material';
+import React, { useState } from 'react';
+
+function SearchComponent({ onSearch }) {
+  const [searchStr, setSearchStr] = useState('');
+  return (
+    <Box>
+      <TextField
+        variant="standard"
+        placeholder='Search'
+        onChange={e => setSearchStr(e.target.value)}
+        onKeyPress={(e) => {
+          if (e.key === 'Enter') {
+            onSearch(e.target.value);
+          }
+        }} />
+      <IconButton onClick={() => onSearch(searchStr)}><Search /></IconButton>
+    </Box>
+  )
+}
+
+export default SearchComponent;

+ 18 - 0
js21 react/my-react-app/src/components/ShowUserName.js

@@ -0,0 +1,18 @@
+import React from "react";
+import { useSelector } from 'react-redux';
+import Box from '@mui/material/Box';
+import { useGetUserByIdQuery } from "../store/api";
+
+const ShowUserName = () => {
+  const token = useSelector(state => state.auth.token);
+  const id = useSelector(state => state?.auth?.payload?.sub?.id);
+  const {data} = useGetUserByIdQuery(id);
+
+  return (
+    <>
+      {token ? <Box>Hello, {data?.UserFindOne?.nick ? data?.UserFindOne?.nick : data?.UserFindOne?.login}</Box> : <></>}
+    </>
+
+  )
+}
+export default ShowUserName;

+ 2 - 2
js21 react/my-react-app/src/components/hoc/RequireAdmin.js

@@ -4,8 +4,8 @@ import { Navigate, useLocation } from 'react-router-dom';
 
 function RequireAdmin({ children }) {
   const location = useLocation();
-  const admin = useSelector(state => state.auth.payload.sub.acl.includes('admin'));
-  if (!admin) {
+  const isAdmin = useSelector(state => state.auth.payload.sub.acl.includes('admin'));
+  if (!isAdmin) {
     return <Navigate to={'/'} state={{ from: location.pathname }} />
   }
   return children;

+ 2 - 2
js21 react/my-react-app/src/components/hoc/RequireAuth.js

@@ -4,8 +4,8 @@ import { Navigate, useLocation } from 'react-router-dom';
 
 function RequireAuth({ children }) {
   const location = useLocation();
-  const auth = useSelector(state => state.auth.token);
-  if (!auth) {
+  const token = useSelector(state => state.auth.token);
+  if (!token) {
     return <Navigate to={'/login'} state={{ from: location.pathname }} />
   }
 

+ 2 - 6
js21 react/my-react-app/src/pages/CartPage.js

@@ -6,6 +6,7 @@ import { useDispatch, useSelector } from 'react-redux';
 import { Box, Button } from '@mui/material';
 import Price from '../components/Price';
 import { useCreateOrderMutation } from '../store/api';
+import NotFoundBanner from '../components/NotFoundBanner';
 
 export default function CartPage() {
   const goods = useSelector(state => state.cart.goods);
@@ -51,12 +52,7 @@ export default function CartPage() {
           </Box>
         </>
         :
-        <Box sx={{
-          textAlign: 'center', m: '100px auto 0', fontSize: '30px', backgroundColor: '#d5d5d5',
-          color: '#fff', p: '50px 30px', maxWidth: '420px', borderRadius: '10px'
-        }}>
-          Cart is empty!
-        </Box>
+        <NotFoundBanner text='Cart is empty!'/>
       }
     </>
   )

+ 10 - 5
js21 react/my-react-app/src/pages/CategoryPage/CategoryPage.js

@@ -7,6 +7,7 @@ import './Category.scss';
 import Loader from '../../components/Loader';
 import { Box } from "@mui/material";
 import { useGetCategoryByIdQuery } from '../../store/api';
+import NotFoundBanner from '../../components/NotFoundBanner';
 
 const CategoryPage = () => {
   const { categoryId } = useParams();
@@ -16,12 +17,16 @@ const CategoryPage = () => {
       {isFetching ? <Loader /> :
         <>
           <Title>{data.CategoryFindOne?.name}</Title>
-          {data.CategoryFindOne?.subCategories?.length > 0 && <CategoriesSection key={data.CategoryFindOne?._id} categories={data.CategoryFindOne?.subCategories} categoryEl='Subcategories:' />}
-          {data.CategoryFindOne?.parent && <CategoriesSection categories={[data.CategoryFindOne?.parent]} categoryEl='Parent category:' />}
+          {data.CategoryFindOne?.subCategories?.length > 0 && <CategoriesSection key={data.CategoryFindOne?._id} categories={data.CategoryFindOne?.subCategories} categoryLabel='Subcategories:' />}
+          {data.CategoryFindOne?.parent && <CategoriesSection categories={[data.CategoryFindOne?.parent]} categoryLabel='Parent category:' />}
+          {data?.CategoryFindOne?.goods?.length ?
+            <Box className='goods-container'>
+              {data.CategoryFindOne?.goods?.map(good => <GoodCard key={good._id} good={good} />)}
+            </Box>
+            :
+            <NotFoundBanner text='No goods found!' />
+          }
 
-          <Box className='goods-container'>
-            {data.CategoryFindOne?.goods?.map(good => <GoodCard key={good._id} good={good} />)}
-          </Box>
         </>}
 
     </>

+ 127 - 86
js21 react/my-react-app/src/pages/EditProfilePage.js

@@ -1,17 +1,64 @@
-import { Avatar, Box, Button, FormControl, IconButton, InputAdornment, OutlinedInput } from '@mui/material';
-import { Edit, Visibility, VisibilityOff } from '@mui/icons-material';
+import { Box, Button, FormControl, IconButton, InputAdornment, OutlinedInput, Snackbar } from '@mui/material';
+import { Visibility, VisibilityOff, Save } from '@mui/icons-material';
 import React, { useState } from 'react';
 import Title from '../components/Title';
 import { Stack } from '@mui/system';
+import { useGetUserByIdQuery, useUpdateUserMutation } from '../store/api';
+import { useSelector } from 'react-redux';
+import Loader from '../components/Loader';
+import ImageUploader from '../components/ImageUploader';
+import MuiAlert from '@mui/material/Alert';
 
 function EditProfilePage() {
-  const [nick, setNick] = useState('');
+  const userId = useSelector(state => state?.auth?.payload?.sub?.id);
+  const { data, isFetching } = useGetUserByIdQuery(userId);
+  const [avatarId, setAvatarId] = useState('');
+  const [nick, setNick] = useState(data?.UserFindOne?.nick || '');
   const [password, setPassword] = useState('');
   const [confirmPassword, setConfirmPassword] = useState('');
   const [showPassword, setShowPassword] = useState(false);
   const [showConfirmPassword, setShowConfirmPassword] = useState(false);
+  const [open, setOpen] = useState(false);
+  const [snackbarMessage, setSnackbarMessage] = useState('');
+  const [snackbarType, setSnackbarType] = useState('');
   const handleClickShowPassword = () => setShowPassword((show) => !show);
   const handleClickShowConfirmPassword = () => setShowConfirmPassword((show) => !show);
+  const [updateUser] = useUpdateUserMutation();
+
+  const handleClose = (event, reason) => {
+    if (reason === 'clickaway') {
+      return;
+    }
+
+    setOpen(false);
+  };
+
+  const onSubmit = () => {
+    const user = {
+      _id: data.UserFindOne._id,
+      nick,
+      password: password || undefined,
+      avatar: avatarId ? {
+        _id: avatarId
+      } : undefined
+    }
+    if (password !== confirmPassword) {
+      setSnackbarType('error');
+      setSnackbarMessage('Passwords don\'t match!');
+      setOpen(true);
+      return;
+    }
+    updateUser(user).then(response => {
+      setSnackbarType('success');
+      setSnackbarMessage('Saved!');
+      setOpen(true);
+
+    }).catch(e => {
+      setSnackbarType('error');
+      setSnackbarMessage('Ooops! Something went wrong!');
+      setOpen(true);
+    });
+  };
 
   const handleMouseDownConfirmPassword = (event) => {
     event.preventDefault();
@@ -21,90 +68,84 @@ function EditProfilePage() {
     event.preventDefault();
   };
 
-  return (
-    <Box>
-      <Title>Edit profile</Title>
-      <Box sx={{ display: 'flex', alignItems: 'center', mb: '40px' }}>
-        <Avatar sx={{ width: '150px', height: '150px' }} />
-        <Button sx={{ ml: '20px' }}><Edit />  Edit avatar</Button>
-      </Box>
-      <Box component='form' sx={{ display: 'flex', alignItems: 'center', mb: '40px' }}>
-        <FormControl variant="outlined">
-          <OutlinedInput
-            placeholder="New nick"
-            type="text"
-            value={nick}
-            onChange={e => setNick(e.target.value)} />
-        </FormControl>
-        <Button sx={{ ml: '20px' }}>Change nick</Button>
-      </Box>
-      <Box component='form' sx={{ display: 'flex', alignItems: 'center' }}>
-        <Stack>
-          <FormControl variant="outlined" sx={{ mb: '20px' }}>
-            <OutlinedInput
-              placeholder="password"
-              type={showPassword ? 'text' : 'password'}
-              value={password}
-              onChange={e => setPassword(e.target.value)}
-              endAdornment={
-                <InputAdornment position="end">
-                  <IconButton
-                    onClick={handleClickShowPassword}
-                    onMouseDown={handleMouseDownPassword}
-                    edge="end"
-                  >
-                    {showPassword ? <VisibilityOff /> : <Visibility />}
-                  </IconButton>
-                </InputAdornment>
-              } />
-          </FormControl>
-          <FormControl variant="outlined" sx={{ mb: '20px' }}>
-            <OutlinedInput
-              placeholder="Confirm password"
-              type={showConfirmPassword ? 'text' : 'password'}
-              error={password !== confirmPassword}
-              value={confirmPassword}
-              onChange={e => setConfirmPassword(e.target.value)}
-              endAdornment={
-                <InputAdornment position="end">
-                  <IconButton
-                    onClick={handleClickShowConfirmPassword}
-                    onMouseDown={handleMouseDownConfirmPassword}
-                    edge="end"
-                  >
-                    {showConfirmPassword ? <VisibilityOff /> : <Visibility />}
-                  </IconButton>
-                </InputAdornment>
-              } />
-          </FormControl>
-        </Stack>
-        <Button sx={{ ml: '20px' }}>Change password</Button>
-      </Box>
-    </Box>
-  )
-}
+  const Alert = React.forwardRef(function Alert(props, ref) {
+    return <MuiAlert elevation={6} ref={ref} variant="filled" {...props} />;
+  });
 
-export default EditProfilePage;
+  return (
+    <>
+      {isFetching ? <Loader /> :
+        <Box>
+          <Title>Edit profile</Title>
+          <Box sx={{ maxWidth: '250px', width: '100%' }}>
+            <Box sx={{ display: 'flex', alignItems: 'flex-start', mb: '40px', width: '100%' }}>
+              <ImageUploader previousImages={[data.UserFindOne.avatar]} isAvatar={true} onChange={(images) => setAvatarId(images[0]._id)} />
+            </Box>
+            <Box component='form' sx={{ display: 'flex', alignItems: 'flex-start', mb: '40px', width: '100%' }}>
+              <FormControl variant="outlined" sx={{ width: '100%' }}>
+                <OutlinedInput
+                  sx={{ width: '100%' }}
+                  placeholder="Nick"
+                  type="text"
+                  value={nick}
+                  onChange={e => setNick(e.target.value)} />
+              </FormControl>
+            </Box>
+            <Box component='form' sx={{ display: 'flex', alignItems: 'flex-start', width: '100%' }}>
+              <Stack sx={{ width: '100%' }}>
+                <FormControl variant="outlined" sx={{ mb: '20px', width: '100%' }}>
+                  <OutlinedInput
+                    sx={{ width: '100%' }}
+                    placeholder="password"
+                    type={showPassword ? 'text' : 'password'}
+                    value={password}
+                    onChange={e => setPassword(e.target.value)}
+                    endAdornment={
+                      <InputAdornment position="end">
+                        <IconButton
+                          onClick={handleClickShowPassword}
+                          onMouseDown={handleMouseDownPassword}
+                          edge="end"
+                        >
+                          {showPassword ? <VisibilityOff /> : <Visibility />}
+                        </IconButton>
+                      </InputAdornment>
+                    } />
+                </FormControl>
+                <FormControl variant="outlined" sx={{ mb: '20px', width: '100%' }}>
+                  <OutlinedInput
+                    sx={{ width: '100%' }}
+                    placeholder="Confirm password"
+                    type={showConfirmPassword ? 'text' : 'password'}
+                    error={password !== confirmPassword}
+                    value={confirmPassword}
+                    onChange={e => setConfirmPassword(e.target.value)}
+                    endAdornment={
+                      <InputAdornment position="end">
+                        <IconButton
+                          onClick={handleClickShowConfirmPassword}
+                          onMouseDown={handleMouseDownConfirmPassword}
+                          edge="end"
+                        >
+                          {showConfirmPassword ? <VisibilityOff /> : <Visibility />}
+                        </IconButton>
+                      </InputAdornment>
+                    } />
+                </FormControl>
+              </Stack>
+            </Box>
+            <Button onClick={() => onSubmit()} variant="contained" sx={{ width: '100%' }}>Save changes</Button>
 
-{/* 
+            <Snackbar open={open} autoHideDuration={3000} onClose={handleClose}>
+              <Alert onClose={handleClose} severity={snackbarType} sx={{ width: '100%' }}>
+                {snackbarMessage}
+              </Alert>
+            </Snackbar>
+          </Box>
+        </Box>}
+    </>
 
-const ShowNick = () => {
-    const {data, isFetching} = useGetUserByIdQuery('632205aeb74e1f5f2ec1a320')
-    if (isFetching){
-        return <h1>Loading</h1>
-    }
-    console.log(data)
-    return (
-        <h1>NICK: {data && data.UserFindOne && data.UserFindOne.nick}</h1>
-    )
+  )
 }
-    const SetNick = () => {
-    const [nick, setNickState] = useState('')
-    const dispatch = useDispatch()
-    return (
-        <div>
-            <input value={nick} onChange={e => setNickState(e.target.value)}/>
-            <button onClick={() => dispatch(actionSetNick(nick))}>Save</button>
-        </div>
-    )
-} */}
+
+export default EditProfilePage;

+ 1 - 7
js21 react/my-react-app/src/pages/LoginPage.js

@@ -9,9 +9,7 @@ import { useLoginMutation } from '../store/api';
 import { loginAction } from '../store/authSlice';
 const LoginPage = () => {
   const token = useSelector(state => state.auth.token);
-  const userId = useSelector(state => state.auth?.payload?.sub?.id);
   const error = useSelector(state => state.auth.error);
-  const admin = useSelector(state => state.auth?.payload?.sub?.acl.includes('admin'));
   const [fullLogin, result] = useLoginMutation();
   const dispatch = useDispatch();
   const navigate = useNavigate();
@@ -19,11 +17,7 @@ const LoginPage = () => {
 
   useEffect(() => {
     if (token) {
-      if (admin) {
-        navigate('/admin');
-      } else {
-        navigate(`/user/${userId}`);
-      }
+      navigate('/');
     }
   }, [token]);
 

+ 28 - 0
js21 react/my-react-app/src/pages/SearchedGoodsPage.js

@@ -0,0 +1,28 @@
+import { Box } from '@mui/material';
+import React from 'react';
+import { useSearchParams } from 'react-router-dom';
+import GoodCard from '../components/GoodCard/GoodCard';
+import Loader from '../components/Loader';
+import Title from '../components/Title';
+import { useGetGoodsQuery } from '../store/api';
+import './CategoryPage/Category.scss';
+
+function SearchedGoodsPage() {
+  const [searchParams, setSearchParams] = useSearchParams();
+  const searchItem = searchParams.get('text') || '';
+  const {data, isFetching} = useGetGoodsQuery({searchStr: searchItem});
+
+  return (
+    <>
+    {isFetching ? <Loader /> : 
+    <>
+    <Title>Searched Goods</Title>
+    <Box className='goods-container'>
+      {data?.GoodFind?.map(good => <GoodCard key={good._id} good={good} />)}
+    </Box>
+    </>}
+    </>
+  )
+}
+
+export default SearchedGoodsPage;

+ 20 - 13
js21 react/my-react-app/src/pages/UserPage.js

@@ -1,30 +1,37 @@
-import { Box } from '@mui/material';
-import React, { useEffect, useState } from 'react'
-import { useGetOrdersByOwnerIdQuery } from '../store/api';
+import { Box, Pagination } from '@mui/material';
+import React, { useState } from 'react'
+import { useGetOrderCountQuery, useGetOrdersQuery } from '../store/api';
 import Loader from '../components/Loader';
 import Title from '../components/Title';
 import OrderCard from '../components/OrderCard';
 import { useParams } from 'react-router-dom';
 import LinkButton from '../components/LinkButton';
+import NotFoundBanner from '../components/NotFoundBanner';
 
 export default function UserPage() {
   const { userId } = useParams();
-  const { data, error, isFetching } = useGetOrdersByOwnerIdQuery(userId);
-  const [sortedData, setSortedData] = useState();
-  useEffect(() => {
-    if (data) {
-      const dataForSort = [...data?.OrderFind];
-      setSortedData(dataForSort?.sort((a, b) => b.createdAt - a.createdAt));
-    }
-  }, [data]);
+  const [page, setPage] = useState(1);
+  const [limit, setLimit] = useState(5);
+  const { data: orderCount, isFetching: fetchingCount } = useGetOrderCountQuery();
+  const { data, error, isFetching } = useGetOrdersQuery({ skip: (page - 1) * limit, limit, sort: { _id: -1 } });
+  const handleChange = (event, value) => {
+    setPage(value);
+  };
 
   return (
     <>
       <LinkButton to={`/user/${userId}/edit`}>Edit profile</LinkButton>
       <Title>My orders</Title>
-      {isFetching ? <Loader /> :
+      {isFetching && fetchingCount ? <Loader /> :
         <Box>
-          {sortedData?.map(order => <OrderCard key={order._id} order={order} />)}
+          {orderCount?.orderCount ?
+            <>
+              {data?.OrderFind?.map(order => <OrderCard key={order._id} order={order} />)}
+              <Pagination count={Math.ceil(orderCount?.OrderCount / limit) || 0} page={page} onChange={handleChange} />
+            </> :
+            <NotFoundBanner text='You don`t have orders!' />
+          }
+
         </Box>}
     </>
   )

+ 0 - 7
js21 react/my-react-app/src/pages/admin/AdminPage.js

@@ -1,7 +0,0 @@
-import React from 'react';
-
-export default function AdminPage() {
-  return (
-    <div>Admin</div>
-  )
-}

+ 12 - 18
js21 react/my-react-app/src/pages/admin/CategoriesPage.js

@@ -1,24 +1,26 @@
 import React, { useState } from 'react';
-import { useDeleteCategoryMutation, useGetCategoriesWithSubCategoriesQuery, useGetCategoryCountQuery } from '../../store/api';
+import { useDeleteCategoryMutation, useGetAllCategoriesQuery, useGetCategoryCountQuery } from '../../store/api';
 import Loader from '../../components/Loader';
 import {
   Button, IconButton, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Dialog,
-  DialogActions, DialogContent, DialogContentText, TablePagination, Collapse
+  DialogActions, DialogContent, DialogContentText, TablePagination, Collapse, TextField
 } from '@mui/material';
-import { DeleteOutlineOutlined, Edit, KeyboardArrowDown, KeyboardArrowUp } from '@mui/icons-material';
+import { DeleteOutlineOutlined, Edit, KeyboardArrowDown, KeyboardArrowUp, Search } from '@mui/icons-material';
 import { Link } from 'react-router-dom';
 import LinkButton from '../../components/LinkButton';
 import { Box } from '@mui/system';
 import { Image } from '../../components/Image/Image';
+import SearchComponent from '../../components/SearchComponent';
 
 function CategoriesPage() {
+  const [search, setSearch] = useState('');
   const { data: categoryCount } = useGetCategoryCountQuery();
-  const { data, error, isFetching } = useGetCategoriesWithSubCategoriesQuery();
   const [page, setPage] = useState(0);
   const [rowsPerPage, setRowsPerPage] = useState(5);
   const [open, setOpen] = useState(false);
   const [selectedId, setSelectedId] = useState('');
   const [deleteCategory, result] = useDeleteCategoryMutation();
+  const { data, error, isFetching } = useGetAllCategoriesQuery(search);
 
   const handleChangePage = (event, newPage) => {
     setPage(newPage);
@@ -40,8 +42,8 @@ function CategoriesPage() {
   };
 
   const onDeletionSubmit = () => {
-  const category = data?.CategoryFind?.find(category => category._id === selectedId);
-    deleteCategory({_id: category._id, name: category.name}).then(response => {
+    const category = data?.CategoryFind?.find(category => category._id === selectedId);
+    deleteCategory({ _id: category._id, name: category.name }).then(response => {
       setOpen(false);
     });
   }
@@ -112,7 +114,10 @@ function CategoriesPage() {
     <>
       {isFetching ? <Loader /> :
         <>
-          <LinkButton to={'/admin/category'}>Create category</LinkButton>
+          <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
+            <LinkButton to={'/admin/category'}>Create category</LinkButton>
+            <SearchComponent onSearch={(str) => setSearch(str)}/>
+          </Box>
           <Paper sx={{ m: 1 }}>
             <TableContainer>
               <Table>
@@ -135,17 +140,6 @@ function CategoriesPage() {
                       goods={category?.goods}
                       category={category}
                       parent={category?.parent} />
-                    // <TableRow key={category._id}>
-                    //   <TableCell>{category._id}</TableCell>
-                    //   <TableCell>{category.name}</TableCell>
-                    //   <TableCell>{category.goods?.length || 0}</TableCell>
-                    //   <TableCell align='right'>
-                    //     <IconButton component={Link} to={`/admin/category/${category._id}`}><Edit /></IconButton>
-                    //   </TableCell>
-                    //   <TableCell align='right'>
-                    //     <IconButton onClick={() => openModalWindow(category._id)}><DeleteOutlineOutlined /></IconButton>
-                    //   </TableCell>
-                    // </TableRow>
                   )}
                   <Dialog
                     open={open}

+ 11 - 1
js21 react/my-react-app/src/pages/admin/CreateCategoryPage.js

@@ -1,16 +1,26 @@
 import { Box } from '@mui/material';
 import React from 'react';
+import { useNavigate } from 'react-router-dom';
 import EditForm from '../../components/EditForm';
 import Title from '../../components/Title';
+import { useCreateCategoryMutation } from '../../store/api';
 
 
 
 function CreateCategoryPage() {
+const navigate = useNavigate();
+  const [createCategory, resultMut] = useCreateCategoryMutation();
+
+  const create = (category) => {
+    createCategory(category).then(response => {
+      navigate('/admin/categories');
+    });
+  }
 
   return (
     <Box sx={{ maxWidth: '450px', m: '20px auto', width: '100%' }} >
       <Title>Create category</Title>
-      <EditForm buttonText='Add category' />
+      <EditForm entity='category' buttonText='Add category' onSubmit={(category) => create(category)} />
     </Box >
 
   )

+ 12 - 1
js21 react/my-react-app/src/pages/admin/CreateGoodPage.js

@@ -1,13 +1,24 @@
 import { Box } from '@mui/material';
 import React from 'react';
 import EditForm from '../../components/EditForm';
+import { useNavigate } from 'react-router-dom';
+import {  useCreateGoodMutation } from '../../store/api';
 import Title from '../../components/Title';
 
 const CreateGoodPage = () => {
+  const navigate = useNavigate();
+  const [createGood, result] = useCreateGoodMutation();
+
+  const create = (good => {
+    createGood(good).then(response => {
+      navigate('/admin/goods');
+    });
+  });
+
   return (
     <Box sx={{ maxWidth: '450px', m: '20px auto', width: '100%' }} >
       <Title>Create good</Title>
-      <EditForm buttonText='Add'/>
+      <EditForm entity='good' buttonText='Add' onSubmit={(good) => create(good)}/>
     </Box >
   )
 }

+ 13 - 1
js21 react/my-react-app/src/pages/admin/EditCategoryPage.js

@@ -1,7 +1,8 @@
 import { Box } from '@mui/system';
 import React from 'react';
 import { useParams } from 'react-router-dom';
-import { useGetCategoryByIdQuery } from '../../store/api';
+import { useGetCategoryByIdQuery, useCreateCategoryMutation } from '../../store/api';
+import { useNavigate } from 'react-router-dom';
 import Loader from '../../components/Loader';
 import Title from '../../components/Title';
 import EditForm from '../../components/EditForm';
@@ -9,16 +10,27 @@ import EditForm from '../../components/EditForm';
 function EditCategoryPage() {
   const { categoryId } = useParams();
   const { data, isFetching } = useGetCategoryByIdQuery(categoryId);
+  const navigate = useNavigate();
+  const [editCategory, resultMut] = useCreateCategoryMutation();
+
+  const edit = (category) => {
+    editCategory(category).then(response => {
+      navigate('/admin/categories');
+    });
+  }
+
   return (
     <>
       {isFetching ? <Loader /> :
         <Box sx={{ maxWidth: '450px', m: '20px auto', width: '100%' }}>
           <Title>Edit category</Title>
           <EditForm
+            entity='category'
             buttonText='Update'
             id={categoryId}
             name={data?.CategoryFindOne?.name}
             categories={data?.CategoryFindOne?.subCategories}
+            onSubmit={(category) => edit(category)}
           />
         </Box>
       }

+ 12 - 1
js21 react/my-react-app/src/pages/admin/EditGoodPage.js

@@ -2,19 +2,29 @@ import { Box } from '@mui/material';
 import React from 'react';
 import { useParams } from 'react-router-dom';
 import Title from '../../components/Title';
-import { useGetGoodByIdQuery } from '../../store/api';
+import { useGetGoodByIdQuery, useCreateGoodMutation } from '../../store/api';
+import { useNavigate } from 'react-router-dom';
 import Loader from '../../components/Loader';
 import EditForm from '../../components/EditForm';
 
 function EditGoodPage() {
   const { goodId } = useParams();
   const { data, isFetching } = useGetGoodByIdQuery(goodId);
+  const navigate = useNavigate();
+  const [editGood, result] = useCreateGoodMutation();
+
+  const edit = (good => {
+    editGood(good).then(response => {
+      navigate('/admin/goods');
+    });
+  });
   return (
     <>
       {isFetching ? <Loader /> :
         <Box sx={{ maxWidth: '450px', m: '20px auto', width: '100%' }} >
           <Title>Create good</Title>
           <EditForm
+            entity='good'
             buttonText='Edit'
             id={goodId}
             name={data?.GoodFindOne?.name}
@@ -22,6 +32,7 @@ function EditGoodPage() {
             price={data?.GoodFindOne?.price}
             description={data?.GoodFindOne?.description}
             goodImages={data?.GoodFindOne?.images}
+            onSubmit={(good) => edit(good)}
           />
         </Box>
       }

+ 7 - 2
js21 react/my-react-app/src/pages/admin/GoodsPage.js

@@ -10,12 +10,14 @@ import { useDeleteGoodMutation, useGetGoodCountQuery, useGetGoodsQuery } from '.
 import LinkButton from '../../components/LinkButton';
 import Loader from '../../components/Loader';
 import { Image } from '../../components/Image/Image';
+import SearchComponent from '../../components/SearchComponent';
 
 function GoodsPage() {
+  const [search, setSearch] = useState('');
   const [page, setPage] = useState(0);
   const [rowsPerPage, setRowsPerPage] = useState(10);
-  const { data: goodCount } = useGetGoodCountQuery();
-  const { data, isFetching } = useGetGoodsQuery({ skip: page * rowsPerPage, limit: rowsPerPage });
+  const { data: goodCount, refetch } = useGetGoodCountQuery();
+  const { data, isFetching } = useGetGoodsQuery({ searchStr: search, skip: page * rowsPerPage, limit: rowsPerPage });
   const [open, setOpen] = useState(false);
   const [selectedId, setSelectedId] = useState('');
   const [deleteGood, result] = useDeleteGoodMutation();
@@ -50,7 +52,10 @@ function GoodsPage() {
     <>
       {isFetching ? <Loader /> :
         <>
+          <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
           <LinkButton to={'/admin/good'}>Create good</LinkButton>
+            <SearchComponent onSearch={(str) => {setSearch(str); refetch()}} />
+          </Box>
           <Paper sx={{ m: 1 }}>
             <TableContainer>
               <Table>

+ 1 - 2
js21 react/my-react-app/src/pages/admin/OrdersPage.js

@@ -10,7 +10,7 @@ function OrdersPage() {
   const [page, setPage] = useState(0);
   const [rowsPerPage, setRowsPerPage] = useState(50);
   const { data: orderCount } = useGetOrderCountQuery();
-  const { data, isFetching, } = useGetOrdersQuery({ skip: page * rowsPerPage, limit: rowsPerPage });
+  const { data, isFetching } = useGetOrdersQuery({ skip: page * rowsPerPage, limit: rowsPerPage, sort: {_id: -1}});
 
   const handleChangePage = (event, newPage) => {
     setPage(newPage);
@@ -87,7 +87,6 @@ function OrdersPage() {
               <TableBody>
                 {data?.OrderFind?.map(order =>
                   <Row
-                    key={order?._id}
                     id={order?._id}
                     login={order?.owner?.login}
                     total={order?.total}

+ 1 - 1
js21 react/my-react-app/src/pages/admin/UsersPage.js

@@ -8,7 +8,7 @@ function UsersPage() {
   const [page, setPage] = useState(0);
   const [rowsPerPage, setRowsPerPage] = useState(50);
   const { data: userCount } = useGetUserCountQuery();
-  const { data, isFetching } = useGetUsersQuery({ skip: page * rowsPerPage, limit: rowsPerPage });
+  const { data, isFetching } = useGetUsersQuery({ skip: page * rowsPerPage, limit: rowsPerPage, sort: {_id: -1} });
 
   const handleChangePage = (event, newPage) => {
     setPage(newPage);

+ 18 - 51
js21 react/my-react-app/src/store/api.js

@@ -3,7 +3,7 @@ import { gql } from 'graphql-request';
 import { graphqlRequestBaseQuery } from '@rtk-query/graphql-request-base-query'; //npm install
 
 
-const BASE_URL = "http://shop-roles.node.ed.asmer.org.ua";
+export const BASE_URL = "http://shop-roles.node.ed.asmer.org.ua";
 
 const prepareHeaders = (headers, { getState }) => {
   // By default, if we have a token in the store, let's use that for authenticated requests
@@ -41,7 +41,7 @@ export const api = createApi({
   }),
   tagTypes: ['Category', 'Order', 'Good', 'User'],
   endpoints: (builder) => ({
-    getCategories: builder.query({
+    getRootCategories: builder.query({
       query: () => ({
         document: gql`
                   query GetCategories{
@@ -58,8 +58,8 @@ export const api = createApi({
                   `}),
       providesTags: ['Category'],
     }),
-    getCategoriesWithSubCategories: builder.query({
-      query: () => ({
+    getAllCategories: builder.query({
+      query: (searchCategory = '') => ({
         document: gql`
                   query GetCategories($q: String){
                       CategoryFind(query: $q) {
@@ -75,7 +75,7 @@ export const api = createApi({
                       }
                     }
                   `,
-        variables: { q: JSON.stringify([{}]) }
+        variables: { q: JSON.stringify([{name: {$regex: searchCategory, $options : 'i'}}]) }
       }),
       providesTags: ['Category'],
     }),
@@ -165,54 +165,21 @@ export const api = createApi({
       }),
       providesTags: ['User']
     }),
-    setNick: builder.mutation({
-      query: ({ _id, nick }) => ({
+    updateUser: builder.mutation({
+      query: ({ _id, nick, avatar, password }) => ({
         document: gql`
-              mutation SetNick($_id: String, $nick: String) {
-                  UserUpsert(user: {_id: $_id, nick: $nick}) {
+              mutation updateUser($_id: String, $nick: String, $avatar: ImageInput, $password: String) {
+                  UserUpsert(user: {_id: $_id, nick: $nick, avatar: $avatar, password: $password}) {
                       _id, nick
                   }
               }
           `,
-        variables: { _id, nick }
+        variables: { _id, nick, avatar, password }
       }),
       invalidatesTags: ['User']
     }),
-    setPassword: builder.mutation({
-      query: ({ _id, password }) => ({
-        document: gql`
-                  mutation SetPassword($_id:String, $password: String) {
-                    UserUpsert(user: {_id: $_id, password: $password}) {
-                      _id
-                    }
-                  }`,
-        variables: { _id, password }
-      }),
-      invalidatesTags: ['User']
-    }),
-    getOrdersByOwnerId: builder.query({
-      query: () => ({
-        document: gql`
-                      query orders($q: String) {
-                        OrderFind(query: $q) {
-                          _id, total, createdAt, owner{
-                            _id, login
-                          }, orderGoods{
-                            price, count, good{
-                              name, _id, images {
-                                url
-                              }
-                            }
-                          }
-                        }
-                      }
-                      `,
-        variables: { q: JSON.stringify([{}]) }
-      }),
-      providesTags: ['Order'],
-    }),
     getOrders: builder.query({
-      query: ({ limit, skip }) => ({
+      query: ({ limit, skip, sort}) => ({
         document: gql`
                     query orders($q: String) {
                       OrderFind(query: $q) {
@@ -228,7 +195,7 @@ export const api = createApi({
                       }
                     }
                     `,
-        variables: { q: JSON.stringify([{}, { limit: [limit], skip: [skip] }]) }
+        variables: { q: JSON.stringify([{}, { limit: [limit], skip: [skip], sort: [sort] }]) }
       }),
       providesTags: ['Order'],
     }),
@@ -265,7 +232,7 @@ export const api = createApi({
       providesTags: ['Order'],
     }),
     getGoods: builder.query({
-      query: ({ skip, limit }) => ({
+      query: ({ searchStr = '', skip = 0, limit = 0 }) => ({
         document: gql`
                     query GetGoods($q: String) {
                       GoodFind(query: $q) {
@@ -276,7 +243,7 @@ export const api = createApi({
                         }
                       }
                     }`,
-        variables: { q: JSON.stringify([{}, { skip: [skip], limit: [limit] }]) }
+        variables: { q: JSON.stringify([{name: {$regex: searchStr, $options : 'i'}}, { skip: [skip], limit: [limit] }]) }
       }),
       providesTags: ['Good'],
     }),
@@ -300,14 +267,14 @@ export const api = createApi({
       })
     }),
     getUsers: builder.query({
-      query: ({ skip, limit }) => ({
+      query: ({ skip, limit, sort }) => ({
         document: gql`
                     query GetUsers($q: String) {
                       UserFind(query: $q) {
                         _id, login, createdAt
                       }
                     }`,
-        variables: { q: JSON.stringify([{}, { skip: [skip], limit: [limit] }]) }
+        variables: { q: JSON.stringify([{}, { skip: [skip], limit: [limit], sort: [sort] }]) }
       }),
       providesTags: ['User'],
     }),
@@ -402,9 +369,9 @@ export const api = createApi({
   }),
 });
 
-export const { useGetCategoriesQuery, useGetCategoryByIdQuery, useGetGoodByIdQuery, useLoginMutation, useGetOrdersByOwnerIdQuery,
+export const { useGetRootCategoriesQuery, useGetCategoryByIdQuery, useGetGoodByIdQuery, useLoginMutation, useGetOrdersByOwnerIdQuery,
   useRegisterMutation, useGetUserByIdQuery, useCreateCategoryMutation, useDeleteCategoryMutation, useGetGoodsQuery, useGetUsersQuery,
   useGetOrderGoodQuery, useGetUserCountQuery, useGetOrderCountQuery, useGetGoodCountQuery, useGetCategoryCountQuery, useGetOrdersQuery,
-  useCreateGoodMutation, useDeleteGoodMutation, useCreateOrderMutation, useGetCategoriesWithSubCategoriesQuery } = api;
+  useCreateGoodMutation, useDeleteGoodMutation, useCreateOrderMutation, useGetAllCategoriesQuery, useUpdateUserMutation } = api;
 
 export const { useUploadImageMutation } = imageApi;