Prechádzať zdrojové kódy

finished: uploading cropped avatar

yankevych0210 1 rok pred
rodič
commit
cf55096774

+ 41 - 30
src/components/CropperPopup/CropperPopup.jsx

@@ -1,32 +1,37 @@
-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';
+import { useRef } from 'react';
 
-export const CropperPopup = ({ isOpen, close, imageSrc, onCropComplete }) => {
+export const CropperPopup = ({
+  isOpen,
+  close,
+  closeParent,
+  imageUrl,
+  onCropComplete,
+}) => {
   const dispatch = useDispatch();
   const user = useSelector((state) => state.user);
+  const formRef = useRef();
   const {
     crop,
     zoom,
     setZoom,
-    croppedArea,
     onCropChange,
     onZoomChange,
     onCropCompleted,
     getResult,
-  } = useCrop(imageSrc);
+  } = useCrop(imageUrl);
 
-  const handleSave = async () => {
-    const avatar = await getResult();
+  const handleSave = async (e) => {
+    e.preventDefault();
+    const avatarFile = await getResult();
 
-    const formData = new FormData();
-    formData.append('url', avatar);
-    console.log(formData);
-    dispatch(changeAvatar({ user, formData }));
+    dispatch(changeAvatar({ user, file: avatarFile }));
+    close();
+    closeParent();
   };
 
   if (!isOpen) return null;
@@ -39,7 +44,7 @@ export const CropperPopup = ({ isOpen, close, imageSrc, onCropComplete }) => {
 
       <div className={style.cropContainer}>
         <Cropper
-          image={imageSrc}
+          image={imageUrl}
           crop={crop}
           zoom={zoom}
           aspect={1}
@@ -49,25 +54,31 @@ export const CropperPopup = ({ isOpen, close, imageSrc, onCropComplete }) => {
         />
       </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>
+      <form
+        action="/upload"
+        method="post"
+        encType="multipart/form-data"
+        ref={formRef}
+      >
+        <div className={style.control}>
+          <label>
+            zoom
+            <input
+              type="range"
+              value={zoom}
+              min={1}
+              max={3}
+              step={0.1}
+              onChange={(e) => {
+                setZoom(e.target.value);
+              }}
+              className="zoom-range"
+            />
+          </label>
 
-        <button onClick={handleSave}>Save</button>
-      </div>
+          <button onClick={handleSave}>Save</button>
+        </div>
+      </form>
     </div>
   );
 };

+ 21 - 23
src/components/DragAndDropPopup/DragAndDropPopup.jsx

@@ -1,20 +1,14 @@
-import React, { useRef, useState } from 'react';
+import React, { 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 [imageUrl, setImageUrl] = useState('');
   const cropper = usePopup();
-  const formRef = useRef();
-  const dispatch = useDispatch();
-  const user = useSelector((state) => state.user);
 
   const handleDragEnter = (e) => {
     e.preventDefault();
@@ -25,19 +19,21 @@ export const DragAndDropPopup = ({ isOpen, close }) => {
     setIsDragging(false);
   };
 
-  const handleDrop = async (event) => {
+  const getUncroppedAvatar = 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];
+    let file;
+    if (event._reactName === 'onChange') {
+      file = event.target.files[0];
+    }
+    if (event._reactName === 'onDrop') {
+      file = event.dataTransfer.files[0];
+    }
 
-    dispatch(uploadAvatar(currentFile));
-    // cropper.open();
+    const image = await getImageUrlFromFile(file);
+    setImageUrl(image);
+    cropper.open();
   };
 
   const handleClose = () => {
@@ -53,16 +49,16 @@ export const DragAndDropPopup = ({ isOpen, close }) => {
       <CropperPopup
         isOpen={cropper.isPopupVisible}
         close={cropper.close}
-        imageSrc={imageSrc}
+        closeParent={close}
+        imageUrl={imageUrl}
       />
 
-      <form
-        ref={formRef}
+      <div
         className={style.container}
         onDragEnter={handleDragEnter}
         onDragOver={(e) => e.preventDefault()}
         onDragLeave={handleDragLeave}
-        onDrop={handleDrop}
+        onDrop={getUncroppedAvatar}
       >
         <div className={style.header}>
           <img
@@ -71,19 +67,21 @@ export const DragAndDropPopup = ({ isOpen, close }) => {
           />
           <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}
+            onChange={getUncroppedAvatar}
             hidden
             accept="image/*,.png,.jpg.,.web"
           />
         </label>
-      </form>
+      </div>
     </>
   );
 };

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

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

+ 5 - 5
src/components/ImageUploader/ImageUploader.jsx

@@ -5,16 +5,16 @@ import { DragAndDropPopup } from '../DragAndDropPopup/DragAndDropPopup';
 import { useSelector } from 'react-redux';
 
 export const ImageUploader = () => {
-  const uploader = usePopup();
+  const dndPopup = usePopup();
   const { avatar } = useSelector((state) => state.user);
 
   return (
     <>
       <div className={style.imageUploader}>
-        <img src={avatar.url ? avatar.url : initialUserImage} alt="userImage" />
+        <img src={avatar ? avatar : initialUserImage} alt="userImage" />
         <div className={style.info}>
           <span>Upload a New Profile Image</span>
-          <button onClick={uploader.open}>Choose File</button>
+          <button onClick={dndPopup.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>
@@ -22,8 +22,8 @@ export const ImageUploader = () => {
       </div>
 
       <DragAndDropPopup
-        isOpen={uploader.isPopupVisible}
-        close={uploader.close}
+        isOpen={dndPopup.isPopupVisible}
+        close={dndPopup.close}
       />
     </>
   );

+ 10 - 2
src/hooks/useCrop.js

@@ -25,7 +25,8 @@ export const useCrop = (imageSrc) => {
   const getResult = async () => {
     try {
       const croppedImage = await getCroppedImg(imageSrc, croppedArea);
-      return croppedImage;
+      const imageFile = await getImageFile(croppedImage);
+      return imageFile;
     } catch (error) {
       console.error(error);
     }
@@ -45,6 +46,13 @@ export const useCrop = (imageSrc) => {
   };
 };
 
+const getImageFile = async (url) => {
+  const response = await fetch(url);
+  const blob = await response.blob();
+  const file = new File([blob], 'image.jpg', { type: blob.type });
+  return file;
+};
+
 export const createImage = (url) =>
   new Promise((resolve, reject) => {
     const image = new Image();
@@ -75,7 +83,7 @@ export function rotateSize(width, height, rotation) {
 /**
  * This function was adapted from the one in the ReadMe of https://github.com/DominicTobias/react-image-crop
  */
-export default async function getCroppedImg(
+async function getCroppedImg(
   imageSrc,
   pixelCrop,
   rotation = 0,

+ 24 - 62
src/store/user/actions/changeAvatar.js

@@ -1,81 +1,43 @@
 import { createAsyncThunk } from '@reduxjs/toolkit';
 import { getGql } from '../../../services/api';
+import { uploadImage } from './uploadImage';
 
 export const changeAvatar = createAsyncThunk(
   'user/changeAvatar',
 
-  async ({ user, formData }, { rejectWithValue }) => {
+  async ({ user, file }, { rejectWithValue, dispatch }) => {
     const gql = getGql();
 
+    console.log({ user, file });
+
     try {
+      const imageAction = await dispatch(uploadImage(file));
+
+      if (!imageAction.payload._id) {
+        return rejectWithValue('Something went wrong please try again.');
+      }
+
       const response = await gql.request(
         `
-        mutation uploadImage($image: ImageInput!) {
-          ImageUpsert(image: $image) {
-            _id
-            text
-            url
+            mutation setAvatar{
+              UserUpsert(user:{_id: "${user.id}", avatar: {_id: "${imageAction.payload._id}"}}){
+                  _id,
+                  avatar{
+                      _id
+                      url
+                      text
+                      userAvatar {
+                        login
+                      }
+                  }
+              }
           }
-        }
-        
-        `,
-        {
-          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);
-      // }
+      return response.UserUpsert.avatar.url;
     } 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.');
-    // }
   }
 );

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

@@ -1,45 +0,0 @@
-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.');
-    }
-  }
-);

+ 24 - 0
src/store/user/actions/uploadImage.js

@@ -0,0 +1,24 @@
+import { createAsyncThunk } from '@reduxjs/toolkit';
+
+export const uploadImage = createAsyncThunk(
+  'user/uploadAvatar',
+  async (file, thunkAPI) => {
+    const formData = new FormData();
+    formData.append('photo', file);
+
+    try {
+      const response = await fetch('/upload', {
+        method: 'POST',
+        headers: {
+          Authorization: `Bearer ${localStorage.authToken}`,
+        },
+        body: formData,
+      });
+      const data = await response.json();
+
+      return data;
+    } catch (error) {
+      return thunkAPI.rejectWithValue(`failed to load image`);
+    }
+  }
+);

+ 27 - 14
src/store/user/userSlice.js

@@ -1,34 +1,32 @@
 import { createSlice } from '@reduxjs/toolkit';
 import { changeAvatar } from './actions/changeAvatar';
 import { fetchUser } from './actions/fetchUser';
-import { uploadAvatar } from './actions/uploadAvatar';
+import { uploadImage } from './actions/uploadImage';
 
 const initialState = {
   id: '',
   login: '',
   nick: '',
-  avatar: {},
+  avatar: '',
   isLoading: false,
 };
 
+const proxy = 'http://snippet.node.ed.asmer.org.ua/';
+
 export const userSlice = createSlice({
   name: 'user',
   initialState,
-  reducers: {
-    setAvatar(state, action) {
-      state.avatar = action.payload;
-    },
-  },
+  reducers: {},
   extraReducers: (builder) => {
     builder.addCase(fetchUser.pending, (state) => {
       state.isLoading = true;
     });
-
+    // fetch user
     builder.addCase(fetchUser.fulfilled, (state, action) => {
       state.isLoading = false;
       state.id = action.payload._id;
       state.login = action.payload.login;
-      state.avatar = action.payload.avatar;
+      state.avatar = `${proxy}${action.payload.avatar.url}`;
     });
 
     builder.addCase(fetchUser.rejected, (state, action) => {
@@ -36,14 +34,29 @@ export const userSlice = createSlice({
       state.error = action.error.message;
     });
 
-    builder.addCase(uploadAvatar.fulfilled, (state, action) => {
-      console.log(action.payload);
+    // upload image
+    builder.addCase(uploadImage.pending, (state) => {
+      state.isLoading = true;
+    });
+
+    builder.addCase(uploadImage.fulfilled, (state) => {
+      state.isLoading = false;
+    });
+
+    builder.addCase(uploadImage.rejected, (state, action) => {
+      state.isLoading = false;
+      state.error = action.error.message;
     });
 
+    // changeAvatar
+    builder.addCase(changeAvatar.pending, (state) => {
+      state.isLoading = true;
+    });
+    builder.addCase(changeAvatar.rejected, (state) => {
+      state.isLoading = false;
+    });
     builder.addCase(changeAvatar.fulfilled, (state, action) => {
-      state.avatar = action.payload.avatar;
+      state.avatar = `${proxy}${action.payload}`;
     });
   },
 });
-
-export const { setAvatar } = userSlice.actions;