yankevych0210 преди 1 година
родител
ревизия
d27492de42

+ 45 - 0
package-lock.json

@@ -18,6 +18,7 @@
         "react": "^18.2.0",
         "react-codemirror2": "^7.2.1",
         "react-dom": "^18.2.0",
+        "react-easy-crop": "^4.7.4",
         "react-redux": "^8.0.5",
         "react-router-dom": "^6.8.1",
         "react-scripts": "5.0.1",
@@ -13594,6 +13595,11 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/normalize-wheel": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/normalize-wheel/-/normalize-wheel-1.0.1.tgz",
+      "integrity": "sha512-1OnlAPZ3zgrk8B91HyRj+eVv+kS5u+Z0SCsak6Xil/kmgEia50ga7zfkumayonZrImffAxPU/5WcyGhzetHNPA=="
+    },
     "node_modules/npm-run-path": {
       "version": "4.0.1",
       "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
@@ -15821,6 +15827,24 @@
         "react": "^18.2.0"
       }
     },
+    "node_modules/react-easy-crop": {
+      "version": "4.7.4",
+      "resolved": "https://registry.npmjs.org/react-easy-crop/-/react-easy-crop-4.7.4.tgz",
+      "integrity": "sha512-oDi1375Jo/zuPUvo3oauxnNbfy8L4wsbmHD1KB2vT55fdgu+q8/K0w/rDWzy9jz4jfQ94Q9+3Yu366sDDFVmiA==",
+      "dependencies": {
+        "normalize-wheel": "^1.0.1",
+        "tslib": "2.0.1"
+      },
+      "peerDependencies": {
+        "react": ">=16.4.0",
+        "react-dom": ">=16.4.0"
+      }
+    },
+    "node_modules/react-easy-crop/node_modules/tslib": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz",
+      "integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ=="
+    },
     "node_modules/react-error-overlay": {
       "version": "6.0.11",
       "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz",
@@ -29326,6 +29350,11 @@
       "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
       "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A=="
     },
+    "normalize-wheel": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/normalize-wheel/-/normalize-wheel-1.0.1.tgz",
+      "integrity": "sha512-1OnlAPZ3zgrk8B91HyRj+eVv+kS5u+Z0SCsak6Xil/kmgEia50ga7zfkumayonZrImffAxPU/5WcyGhzetHNPA=="
+    },
     "npm-run-path": {
       "version": "4.0.1",
       "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
@@ -30741,6 +30770,22 @@
         "scheduler": "^0.23.0"
       }
     },
+    "react-easy-crop": {
+      "version": "4.7.4",
+      "resolved": "https://registry.npmjs.org/react-easy-crop/-/react-easy-crop-4.7.4.tgz",
+      "integrity": "sha512-oDi1375Jo/zuPUvo3oauxnNbfy8L4wsbmHD1KB2vT55fdgu+q8/K0w/rDWzy9jz4jfQ94Q9+3Yu366sDDFVmiA==",
+      "requires": {
+        "normalize-wheel": "^1.0.1",
+        "tslib": "2.0.1"
+      },
+      "dependencies": {
+        "tslib": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz",
+          "integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ=="
+        }
+      }
+    },
     "react-error-overlay": {
       "version": "6.0.11",
       "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz",

+ 1 - 0
package.json

@@ -14,6 +14,7 @@
     "react": "^18.2.0",
     "react-codemirror2": "^7.2.1",
     "react-dom": "^18.2.0",
+    "react-easy-crop": "^4.7.4",
     "react-redux": "^8.0.5",
     "react-router-dom": "^6.8.1",
     "react-scripts": "5.0.1",

+ 1 - 0
src/assets/img/iconAddFile.svg

@@ -0,0 +1 @@
+<svg height="58" viewBox="0 0 44 58" width="44" xmlns="http://www.w3.org/2000/svg"><path d="m22 32v-6h-2v6h-6v2h6v6h2v-6h6v-2zm12.997-32h-31.99a2.998 2.998 0 0 0 -3.007 2.996v52.008a3.006 3.006 0 0 0 3.007 2.996h37.986a2.998 2.998 0 0 0 3.007-2.996v-46.006 6.002l-15-15zm-5.997 5 10 10h-9.005c-.55 0-.995-.456-.995-.995z" fill="#bdbdbd" fill-rule="evenodd"/></svg>

+ 73 - 0
src/components/CropperPopup/CropperPopup.jsx

@@ -0,0 +1,73 @@
+import { useState, useCallback, useEffect } from 'react';
+import Cropper from 'react-easy-crop';
+import { useDispatch, useSelector } from 'react-redux';
+import { useCrop } from '../../hooks/useCrop';
+import { setAvatar } from '../../store/user/userSlice';
+import { changeAvatar } from '../../store/user/actions/changeAvatar';
+import style from './CropperPopup.module.scss';
+
+export const CropperPopup = ({ isOpen, close, imageSrc, onCropComplete }) => {
+  const dispatch = useDispatch();
+  const user = useSelector((state) => state.user);
+  const {
+    crop,
+    zoom,
+    setZoom,
+    croppedArea,
+    onCropChange,
+    onZoomChange,
+    onCropCompleted,
+    getResult,
+  } = useCrop(imageSrc);
+
+  const handleSave = async () => {
+    const avatar = await getResult();
+
+    const formData = new FormData();
+    formData.append('url', avatar);
+    console.log(formData);
+    dispatch(changeAvatar({ user, formData }));
+  };
+
+  if (!isOpen) return null;
+  return (
+    <div className={style.container}>
+      <div className={style.header}>
+        <span> goBack </span>
+        <span> close </span>
+      </div>
+
+      <div className={style.cropContainer}>
+        <Cropper
+          image={imageSrc}
+          crop={crop}
+          zoom={zoom}
+          aspect={1}
+          onCropChange={onCropChange}
+          onZoomChange={onZoomChange}
+          onCropComplete={onCropCompleted}
+        />
+      </div>
+
+      <div className={style.control}>
+        <label>
+          zoom
+          <input
+            type="range"
+            value={zoom}
+            min={1}
+            max={3}
+            step={0.1}
+            aria-labelledby="Zoom"
+            onChange={(e) => {
+              setZoom(e.target.value);
+            }}
+            className="zoom-range"
+          />
+        </label>
+
+        <button onClick={handleSave}>Save</button>
+      </div>
+    </div>
+  );
+};

+ 56 - 0
src/components/CropperPopup/CropperPopup.module.scss

@@ -0,0 +1,56 @@
+@import '../../scss/index.scss';
+
+.container {
+    position: absolute;
+    top:18%;
+    left: 50%;
+    transform: translate(-50%);
+    width: 800px;
+    height: 600px;
+    background-color: #1e1f26;
+    z-index: 6;
+    border-radius: 6px;
+
+    .header {
+        padding: 15px;
+        display: flex;
+        justify-content: space-between;
+        border-bottom: 2px solid #0EBEFF;
+    }
+  
+
+    .cropContainer {
+        position: absolute;
+        top: 52px;
+        left: 0;
+        right: 0;
+        bottom: 80px; 
+    }
+
+    .control {
+        padding: 13px 18px;
+        position: relative;
+        bottom: -473px;
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+
+        label {
+            display: flex;
+            align-items: center;
+            font-size: 18px;
+            
+            input {
+                margin: 2px 0 0 10px;
+                background-color: red;
+            }
+        }
+
+        button {
+            @extend %buttonGreen;
+            background-color: #0EBEFF;
+        }
+    }
+
+
+}

+ 89 - 0
src/components/DragAndDropPopup/DragAndDropPopup.jsx

@@ -0,0 +1,89 @@
+import React, { useRef, useState } from 'react';
+import style from './DragAndDropPopup.module.scss';
+import { ReactComponent as IconAddFile } from '../../assets/img/iconAddFile.svg';
+import { usePopup } from '../../hooks/usePopup';
+import { CropperPopup } from '../CropperPopup/CropperPopup';
+import { getImageUrlFromFile } from '../../utils/getImageUrlFromFile';
+import { useDispatch, useSelector } from 'react-redux';
+import { changeAvatar } from '../../store/user/actions/changeAvatar';
+import { uploadAvatar } from '../../store/user/actions/uploadAvatar';
+
+export const DragAndDropPopup = ({ isOpen, close }) => {
+  const [isDragging, setIsDragging] = useState(false);
+  const [imageSrc, setImageSrc] = useState('');
+  const cropper = usePopup();
+  const formRef = useRef();
+  const dispatch = useDispatch();
+  const user = useSelector((state) => state.user);
+
+  const handleDragEnter = (e) => {
+    e.preventDefault();
+    setIsDragging(true);
+  };
+
+  const handleDragLeave = () => {
+    setIsDragging(false);
+  };
+
+  const handleDrop = async (event) => {
+    event.preventDefault();
+    setIsDragging(false);
+    const image = await getImageUrlFromFile(event.dataTransfer.files[0]);
+    setImageSrc(image);
+    cropper.open();
+  };
+
+  const handleUpload = async (event) => {
+    const currentFile = event.target.files[0];
+
+    dispatch(uploadAvatar(currentFile));
+    // cropper.open();
+  };
+
+  const handleClose = () => {
+    close();
+    cropper.close();
+  };
+
+  if (!isOpen) return null;
+  return (
+    <>
+      <div className={style.overlay} onClick={handleClose} />
+
+      <CropperPopup
+        isOpen={cropper.isPopupVisible}
+        close={cropper.close}
+        imageSrc={imageSrc}
+      />
+
+      <form
+        ref={formRef}
+        className={style.container}
+        onDragEnter={handleDragEnter}
+        onDragOver={(e) => e.preventDefault()}
+        onDragLeave={handleDragLeave}
+        onDrop={handleDrop}
+      >
+        <div className={style.header}>
+          <img
+            src="https://static.filestackapi.com/picker/1.23.0/assets/images/navbar-local_file_system.svg"
+            alt=""
+          />
+          <span onClick={close}>x</span>
+        </div>
+        <label htmlFor="upload" className={style.dropArea}>
+          <IconAddFile />
+          <span>Select Files to Upload</span>
+          <span>or Drag and Drop, Copy and Paste Files</span>
+          <input
+            type="file"
+            id="upload"
+            onChange={handleUpload}
+            hidden
+            accept="image/*,.png,.jpg.,.web"
+          />
+        </label>
+      </form>
+    </>
+  );
+};

+ 60 - 0
src/components/DragAndDropPopup/DragAndDropPopup.module.scss

@@ -0,0 +1,60 @@
+@import '../../scss/index.scss';
+
+.overlay {
+    position: fixed;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    background-color: rgba(0, 0, 0, 0.5); 
+    z-index: 2; 
+  }
+
+.container {
+    position: absolute;
+    top:18%;
+    left: 50%;
+    transform: translate(-50%);
+    width: 750px;
+    height: 500px;
+    background-color: #1e1f26;
+    z-index: 4;
+    border-radius: 6px;
+   
+
+    .header {
+        height: 50px;
+        @include flex( $align: center);
+        padding: 10px;
+        border-bottom: 2px solid #0EBEFF;
+        img {
+            margin: 0 auto;
+        }       
+        margin-bottom: 40px;
+    }
+
+    .dropArea {
+        margin:4%;
+        width: 92%;
+        height: 380px;
+        background-color: lighten($color: #1e1f26, $amount: 3%);
+        @include flex($direction: column, $align: center, $justify: center);
+        border: 1px dashed #BDBDBD;
+        transition: all 0.1s ease-in-out;
+        svg {
+            margin-bottom: 20px;
+           path {
+            transition: all 0.1s ease-in-out;
+           }
+        }
+       
+       &:hover {
+        border: 1px dashed lighten($color: #BDBDBD, $amount: 20%);
+        box-shadow: 0px 2px 10px 0px #000000af;
+        path {
+            fill: #0EBEFF;
+        }
+       }
+       
+    }
+}

+ 1 - 1
src/components/Header/Header.jsx

@@ -32,7 +32,7 @@ export const Header = () => {
       {isAuth ? (
         <nav>
           <img
-            src={avatar ? avatar : userInitImage}
+            src={avatar.url ? avatar.url : userInitImage}
             onClick={userPopup.open}
             alt="userImage"
           />

+ 22 - 9
src/components/ImageUploader/ImageUploader.jsx

@@ -1,17 +1,30 @@
-import React from 'react';
 import style from './ImageUploader.module.scss';
 import initialUserImage from '../../assets/img/initialUserImage.jpeg';
+import { usePopup } from '../../hooks/usePopup';
+import { DragAndDropPopup } from '../DragAndDropPopup/DragAndDropPopup';
+import { useSelector } from 'react-redux';
 
 export const ImageUploader = () => {
+  const uploader = usePopup();
+  const { avatar } = useSelector((state) => state.user);
+
   return (
-    <div className={style.imageUploader}>
-      <img src={initialUserImage} alt="userImage" />
-      <div className={style.info}>
-        <span>Upload a New Profile Image</span>
-        <button>Choose File</button>
-        <span>Ideal dimensions are 500px x 500px.</span>
-        <span>Maximum file size is 5mb.</span>
+    <>
+      <div className={style.imageUploader}>
+        <img src={avatar.url ? avatar.url : initialUserImage} alt="userImage" />
+        <div className={style.info}>
+          <span>Upload a New Profile Image</span>
+          <button onClick={uploader.open}>Choose File</button>
+          <span>or drag and drop an image here</span>
+          <span>Ideal dimensions are 500px x 500px.</span>
+          <span>Maximum file size is 5mb.</span>
+        </div>
       </div>
-    </div>
+
+      <DragAndDropPopup
+        isOpen={uploader.isPopupVisible}
+        close={uploader.close}
+      />
+    </>
   );
 };

+ 1 - 1
src/components/ImageUploader/ImageUploader.module.scss

@@ -23,7 +23,7 @@
         span:first-of-type {
             font-weight: 700;
             font-size: 18px;
-           margin: 15px 0;
+           margin: 8px 0;
         }
 
         button {

+ 139 - 0
src/hooks/useCrop.js

@@ -0,0 +1,139 @@
+import { useCallback, useState } from 'react';
+
+export const useCrop = (imageSrc) => {
+  const [crop, setCrop] = useState({ x: 0, y: 0 });
+  const [zoom, setZoom] = useState(1);
+  const [croppedArea, setCroppedArea] = useState({
+    x: 0,
+    y: 0,
+    width: 0,
+    height: 0,
+  });
+
+  const onCropChange = useCallback((crop) => {
+    setCrop(crop);
+  }, []);
+
+  const onZoomChange = useCallback((zoom) => {
+    setZoom(zoom);
+  }, []);
+
+  const onCropCompleted = useCallback((_, croppedAreaPixels) => {
+    setCroppedArea(croppedAreaPixels);
+  }, []);
+
+  const getResult = async () => {
+    try {
+      const croppedImage = await getCroppedImg(imageSrc, croppedArea);
+      return croppedImage;
+    } catch (error) {
+      console.error(error);
+    }
+  };
+
+  return {
+    crop,
+    setCrop,
+    zoom,
+    setZoom,
+    onCropChange,
+    onZoomChange,
+    onCropCompleted,
+    croppedArea,
+    setCroppedArea,
+    getResult,
+  };
+};
+
+export const createImage = (url) =>
+  new Promise((resolve, reject) => {
+    const image = new Image();
+    image.addEventListener('load', () => resolve(image));
+    image.addEventListener('error', (error) => reject(error));
+    image.setAttribute('crossOrigin', 'anonymous'); // needed to avoid cross-origin issues on CodeSandbox
+    image.src = url;
+  });
+
+export function getRadianAngle(degreeValue) {
+  return (degreeValue * Math.PI) / 180;
+}
+
+/**
+ * Returns the new bounding area of a rotated rectangle.
+ */
+export function rotateSize(width, height, rotation) {
+  const rotRad = getRadianAngle(rotation);
+
+  return {
+    width:
+      Math.abs(Math.cos(rotRad) * width) + Math.abs(Math.sin(rotRad) * height),
+    height:
+      Math.abs(Math.sin(rotRad) * width) + Math.abs(Math.cos(rotRad) * height),
+  };
+}
+
+/**
+ * This function was adapted from the one in the ReadMe of https://github.com/DominicTobias/react-image-crop
+ */
+export default async function getCroppedImg(
+  imageSrc,
+  pixelCrop,
+  rotation = 0,
+  flip = { horizontal: false, vertical: false }
+) {
+  const image = await createImage(imageSrc);
+  const canvas = document.createElement('canvas');
+  const ctx = canvas.getContext('2d');
+
+  if (!ctx) {
+    return null;
+  }
+
+  const rotRad = getRadianAngle(rotation);
+
+  // calculate bounding box of the rotated image
+  const { width: bBoxWidth, height: bBoxHeight } = rotateSize(
+    image.width,
+    image.height,
+    rotation
+  );
+
+  // set canvas size to match the bounding box
+  canvas.width = bBoxWidth;
+  canvas.height = bBoxHeight;
+
+  // translate canvas context to a central location to allow rotating and flipping around the center
+  ctx.translate(bBoxWidth / 2, bBoxHeight / 2);
+  ctx.rotate(rotRad);
+  ctx.scale(flip.horizontal ? -1 : 1, flip.vertical ? -1 : 1);
+  ctx.translate(-image.width / 2, -image.height / 2);
+
+  // draw rotated image
+  ctx.drawImage(image, 0, 0);
+
+  // croppedAreaPixels values are bounding box relative
+  // extract the cropped image using these values
+  const data = ctx.getImageData(
+    pixelCrop.x,
+    pixelCrop.y,
+    pixelCrop.width,
+    pixelCrop.height
+  );
+
+  // set canvas width to final desired crop size - this will clear existing context
+  canvas.width = pixelCrop.width;
+  canvas.height = pixelCrop.height;
+
+  // paste generated rotate image at the top left corner
+  ctx.putImageData(data, 0, 0);
+
+  // As Base64 string
+  // return canvas.toDataURL('image/jpeg');
+
+  // As a blob
+  return new Promise((resolve, reject) => {
+    canvas.toBlob((file) => {
+      resolve(URL.createObjectURL(file));
+    }, 'image/jpeg');
+  });
+}

+ 4 - 4
src/services/api.js

@@ -1,13 +1,13 @@
 import { GraphQLClient } from 'graphql-request';
 
-const endpoint = '/graphql';
-
-export const getGql = () => {
+export const getGql = (endpoint = '/graphql') => {
   return new GraphQLClient(endpoint, {
     headers: {
       'Content-Type': 'application/json;charset=utf-8',
       Accept: 'application/json',
-      Authorization: localStorage.authToken ? `Bearer ${localStorage.authToken}` : '',
+      Authorization: localStorage.authToken
+        ? `Bearer ${localStorage.authToken}`
+        : '',
     },
   });
 };

+ 0 - 29
src/services/queries.js

@@ -1,29 +0,0 @@
-export const queries = {
-  updateWorkInfo(id, title, description) {
-    return `
-      mutation updateWork {
-        SnippetUpsert(snippet:{ _id: "${id}" title:"${title}",description: "${description}"}) {
-          _id
-          title
-          description
-          createdAt
-        }
-      }`;
-  },
-
-  getWork(id) {
-    return `query Snippet {
-      SnippetFindOne(query: "[{\\"_id\\":\\"${id}\\"}]") {
-        _id
-        createdAt
-        title
-        description
-        files {
-          _id
-          text
-          type
-        }
-      }
-    }`;
-  },
-};

+ 81 - 0
src/store/user/actions/changeAvatar.js

@@ -0,0 +1,81 @@
+import { createAsyncThunk } from '@reduxjs/toolkit';
+import { getGql } from '../../../services/api';
+
+export const changeAvatar = createAsyncThunk(
+  'user/changeAvatar',
+
+  async ({ user, formData }, { rejectWithValue }) => {
+    const gql = getGql();
+
+    try {
+      const response = await gql.request(
+        `
+        mutation uploadImage($image: ImageInput!) {
+          ImageUpsert(image: $image) {
+            _id
+            text
+            url
+          }
+        }
+        
+        `,
+        {
+          image: formData,
+        }
+      );
+
+      console.log(response.ImageUpsert);
+
+      const imageId = response.ImageUpsert._id;
+
+      // if (imageId) {
+      //   const response = await gql.request(
+      //     `
+      //       mutation setAvatar{
+      //         UserUpsert(user:{_id: "${user.id}", avatar: {_id: "${imageId}"}}){
+      //             _id, avatar{
+      //                 _id
+      //             }
+      //         }
+      //     }
+      //     `
+      //   );
+
+      //   console.log(response.UserUpsert);
+      // }
+    } catch (error) {
+      console.log(error);
+      return rejectWithValue('Something went wrong please try again.');
+    }
+
+    // try {
+    //   const response = await gql.request(
+    //     `
+    //     mutation ChangeAvatar($user: UserInput!) {
+    //       UserUpsert(user: $user) {
+    //         _id
+    //         login
+    //         nick
+    //         avatar {
+    //           url
+    //         }
+    //       }
+    //     }
+    //     `,
+    //     {
+    //       user: {
+    //         _id: user.id,
+    //         avatar,
+    //       },
+    //     }
+    //   );
+
+    //   if (response.UserFindOne) {
+    //     return response.UserFindOne;
+    //   }
+    // } catch (error) {
+    //   console.log(error);
+    //   return rejectWithValue('Something went wrong please try again.');
+    // }
+  }
+);

+ 5 - 1
src/store/user/actions/fetchUser.js

@@ -15,7 +15,12 @@ export const fetchUser = createAsyncThunk(
               login
               nick
               avatar {
+                _id
                 url
+                text
+                userAvatar {
+                  _id
+                }
               }
             }
           }
@@ -28,7 +33,6 @@ export const fetchUser = createAsyncThunk(
           ]),
         }
       );
-      console.log(response.UserFindOne);
 
       if (response.UserFindOne) {
         return response.UserFindOne;

+ 45 - 0
src/store/user/actions/uploadAvatar.js

@@ -0,0 +1,45 @@
+import { createAsyncThunk } from '@reduxjs/toolkit';
+import { getGql } from '../../../services/api';
+
+export const uploadAvatar = createAsyncThunk(
+  'user/uploadAvatar',
+
+  async (file, { rejectWithValue }) => {
+    const gql = getGql('/upload');
+
+    try {
+      const fromData = new FormData();
+      fromData.append('image', file);
+
+      fetch('/upload', {
+        method: 'POST',
+        headers: localStorage.authToken
+          ? { Authorization: 'Bearer ' + localStorage.authToken }
+          : {},
+        body: fromData,
+      })
+        .then((res) => res.json())
+        .then((res) => console.log('UPLOAD RESULT', res));
+
+      //   const response = await gql.request(
+      //     `
+      //     mutation uploadImage($image: ImageInput!) {
+      //       ImageUpsert(image: $image) {
+      //         _id
+      //         text
+      //         url
+      //       }
+      //     }
+      //     `,
+      //     {
+      //       fromData,
+      //     }
+      //   );
+
+      //   console.log(response.ImageUpsert);
+    } catch (error) {
+      console.log(error);
+      return rejectWithValue('Something went wrong please try again.');
+    }
+  }
+);

+ 18 - 2
src/store/user/userSlice.js

@@ -1,18 +1,24 @@
 import { createSlice } from '@reduxjs/toolkit';
+import { changeAvatar } from './actions/changeAvatar';
 import { fetchUser } from './actions/fetchUser';
+import { uploadAvatar } from './actions/uploadAvatar';
 
 const initialState = {
   id: '',
   login: '',
   nick: '',
-  avatar: '',
+  avatar: {},
   isLoading: false,
 };
 
 export const userSlice = createSlice({
   name: 'user',
   initialState,
-  reducers: {},
+  reducers: {
+    setAvatar(state, action) {
+      state.avatar = action.payload;
+    },
+  },
   extraReducers: (builder) => {
     builder.addCase(fetchUser.pending, (state) => {
       state.isLoading = true;
@@ -29,5 +35,15 @@ export const userSlice = createSlice({
       state.isLoading = false;
       state.error = action.error.message;
     });
+
+    builder.addCase(uploadAvatar.fulfilled, (state, action) => {
+      console.log(action.payload);
+    });
+
+    builder.addCase(changeAvatar.fulfilled, (state, action) => {
+      state.avatar = action.payload.avatar;
+    });
   },
 });
+
+export const { setAvatar } = userSlice.actions;

+ 7 - 0
src/utils/getImageUrlFromFile.js

@@ -0,0 +1,7 @@
+export function getImageUrlFromFile(file) {
+  return new Promise((resolve) => {
+    const reader = new FileReader();
+    reader.addEventListener('load', () => resolve(reader.result), false);
+    reader.readAsDataURL(file);
+  });
+}