Forráskód Böngészése

add a possibility to move tracks, create a user profile, add a dropzone for upload an avatar and tracks

Alyona Brytvina 2 éve
szülő
commit
2ae919f884
38 módosított fájl, 1264 hozzáadás és 220 törlés
  1. 51 0
      package-lock.json
  2. 3 0
      package.json
  3. 34 2
      src/api/auth.js
  4. 69 0
      src/api/playlists.js
  5. 30 0
      src/api/upload.js
  6. BIN
      src/assets/jpg/vinylPlate.jpg
  7. 10 3
      src/components/App/App.jsx
  8. 0 23
      src/components/Dropzone/Dropzone.jsx
  9. 26 0
      src/components/GenerateGradient/GenerateGradient.jsx
  10. 28 0
      src/components/GenerateGradient/GenerateGradient.scss
  11. 39 25
      src/components/Header/Header.jsx
  12. 7 2
      src/components/Player/Player.jsx
  13. 2 1
      src/components/PlayerBar/PlayerBar.jsx
  14. 41 46
      src/components/TrackList/TrackList.jsx
  15. 3 0
      src/createHistory.js
  16. 75 34
      src/pages/LoginPage/LoginPage.jsx
  17. 4 0
      src/pages/LoginPage/LoginPage.scss
  18. 27 5
      src/pages/MainPage/MainPage.jsx
  19. 104 9
      src/pages/PlaylistsPage/PlaylistsPage.jsx
  20. 132 0
      src/pages/ProfilePage/ProfilePage.jsx
  21. 3 0
      src/pages/ProfilePage/ProfilePage.scss
  22. 171 67
      src/pages/RegisterPage/RegisterPage.jsx
  23. 25 0
      src/pages/SelectedPlaylistPage/SelectedPlaylistPage.jsx
  24. 53 0
      src/pages/UploadTracks/UploadTracks.jsx
  25. 21 0
      src/store/reducers/authReducer.js
  26. 47 0
      src/store/reducers/playlistsReducer.js
  27. 31 0
      src/store/reducers/uploadReducer.js
  28. 44 1
      src/store/sagas/authSaga.js
  29. 38 0
      src/store/sagas/playlistsSaga.js
  30. 42 0
      src/store/sagas/uploadSaga.js
  31. 8 0
      src/store/store.js
  32. 11 0
      src/store/types/authTypes.js
  33. 2 2
      src/store/types/playerTypes.js
  34. 18 0
      src/store/types/playlistTypes.js
  35. 11 0
      src/store/types/uploadTypes.js
  36. 33 0
      src/utils/getGqlForUpload.js
  37. 7 0
      src/utils/history.js
  38. 14 0
      src/utils/jwtDecode.js

+ 51 - 0
package-lock.json

@@ -3301,6 +3301,11 @@
         "is-string": "^1.0.7"
       }
     },
+    "array-move": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/array-move/-/array-move-4.0.0.tgz",
+      "integrity": "sha512-+RY54S8OuVvg94THpneQvFRmqWdAHeqtMzgMW6JNurHxe8rsS07cHQdfGkXnTUXiBcyZ0j3SiDIxxj0RPiqCkQ=="
+    },
     "array-union": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
@@ -6763,6 +6768,14 @@
         "side-channel": "^1.0.4"
       }
     },
+    "invariant": {
+      "version": "2.2.4",
+      "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
+      "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
+      "requires": {
+        "loose-envify": "^1.0.0"
+      }
+    },
     "ip": {
       "version": "1.1.5",
       "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz",
@@ -10297,6 +10310,19 @@
         "tiny-warning": "^1.0.0"
       },
       "dependencies": {
+        "history": {
+          "version": "4.10.1",
+          "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
+          "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==",
+          "requires": {
+            "@babel/runtime": "^7.1.2",
+            "loose-envify": "^1.2.0",
+            "resolve-pathname": "^3.0.0",
+            "tiny-invariant": "^1.0.2",
+            "tiny-warning": "^1.0.0",
+            "value-equal": "^1.0.1"
+          }
+        },
         "isarray": {
           "version": "0.0.1",
           "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
@@ -10324,6 +10350,21 @@
         "react-router": "5.2.1",
         "tiny-invariant": "^1.0.2",
         "tiny-warning": "^1.0.0"
+      },
+      "dependencies": {
+        "history": {
+          "version": "4.10.1",
+          "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
+          "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==",
+          "requires": {
+            "@babel/runtime": "^7.1.2",
+            "loose-envify": "^1.2.0",
+            "resolve-pathname": "^3.0.0",
+            "tiny-invariant": "^1.0.2",
+            "tiny-warning": "^1.0.0",
+            "value-equal": "^1.0.1"
+          }
+        }
       }
     },
     "react-scripts": {
@@ -10542,6 +10583,16 @@
         }
       }
     },
+    "react-sortable-hoc": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/react-sortable-hoc/-/react-sortable-hoc-2.0.0.tgz",
+      "integrity": "sha512-JZUw7hBsAHXK7PTyErJyI7SopSBFRcFHDjWW5SWjcugY0i6iH7f+eJkY8cJmGMlZ1C9xz1J3Vjz0plFpavVeRg==",
+      "requires": {
+        "@babel/runtime": "^7.2.0",
+        "invariant": "^2.2.4",
+        "prop-types": "^15.5.7"
+      }
+    },
     "react-transition-group": {
       "version": "4.4.2",
       "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.2.tgz",

+ 3 - 0
package.json

@@ -10,12 +10,15 @@
     "@testing-library/jest-dom": "^5.16.1",
     "@testing-library/react": "^12.1.2",
     "@testing-library/user-event": "^13.5.0",
+    "array-move": "^4.0.0",
+    "history": "^4.10.1",
     "react": "^17.0.2",
     "react-dom": "^17.0.2",
     "react-dropzone": "^11.5.1",
     "react-redux": "^7.2.6",
     "react-router-dom": "^5.3.0",
     "react-scripts": "5.0.0",
+    "react-sortable-hoc": "^2.0.0",
     "redux": "^4.1.2",
     "redux-saga": "^1.1.3",
     "surge": "^0.23.1",

+ 34 - 2
src/api/auth.js

@@ -5,10 +5,42 @@ export const login = (payload) => {
   const {login, password} = payload;
   const gql = getGql(`${BACKEND_URL}/graphql`);
   return gql(`
-      query ($login:String!, $password:String!) {
+      query log ($login:String!, $password:String!) {
        login(login:$login, password:$password)
        }
      `, {
-    login, password,
+    login,
+    password,
   });
 };
+
+export const registration = (payload) => {
+  const {login, password} = payload;
+  const gql = getGql(`${BACKEND_URL}/graphql`);
+  return gql(
+    `mutation reg($login: String!, $password: String!){
+         createUser(login:$login,
+              password: $password){
+         _id login
+         }
+    }
+    `,
+    {
+      login,
+      password,
+    },
+  );
+};
+
+export const findUserById = (_id) => {
+  const gql = getGql(`${BACKEND_URL}/graphql`);
+  return gql(`
+      query findUserById($id: String){
+            UserFindOne(query: $id) {
+                _id, login, nick, createdAt, avatar {
+            _id, url
+            }
+      }
+  }
+     `, {id: JSON.stringify([{_id}])});
+};

+ 69 - 0
src/api/playlists.js

@@ -0,0 +1,69 @@
+import { getGql } from '../utils/getGql';
+import { BACKEND_URL } from '../constants';
+
+export const getPlaylists = () => {
+  const gql = getGql(`${BACKEND_URL}/graphql`);
+  return gql(`query nonEmptyPlaylists($query: String){
+  PlaylistFind(query: $query) {
+   _id name 
+  }
+}
+    `, {
+    query: JSON.stringify([{
+      name: {$exists: true, $ne: ''},
+      tracks: {$exists: true, $ne: []},
+    }]),
+  });
+};
+
+export const getSelectedPlaylist = (_id) => {
+  const gql = getGql(`${BACKEND_URL}/graphql`);
+  console.log(_id);
+  return gql(`query FindOnePlaylist($playlist:String!) {
+         PlaylistFindOne(query:$playlist){
+               _id name description tracks{
+                     _id url originalFileName
+               }
+         }
+    } 
+    `, {playlist: JSON.stringify([{_id}])})
+    .then(data => data.tracks.map(track => ({
+      ...track,
+      url: `${BACKEND_URL}/${track.url}`,
+    })));
+};
+
+export const getPlaylistsWithPage = (page = 1) => {
+  const gql = getGql(`${BACKEND_URL}/graphql`);
+  return gql(`
+      query skipPlaylist($query: String){
+          PlaylistFind(query:$query){
+          _id name 
+           }
+      }
+  `, {
+    query: JSON.stringify([{
+      name: {$exists: true, $ne: ''},
+      tracks: {$exists: true, $ne: []},
+    },
+    {
+      limit: [20],
+      skip: [(page - 1) * 20],
+    }])
+    ,
+  });
+};
+
+export const getPlaylistsCount = () => {
+  const gql = getGql(`${BACKEND_URL}/graphql`);
+  return gql(`
+      query getCount($query: String){
+       PlaylistCount(query:$query)
+      } 
+  `, {
+    query: JSON.stringify([{
+      name: {$exists: true, $ne: ''},
+      tracks: {$exists: true, $ne: []},
+    }]),
+  });
+};

+ 30 - 0
src/api/upload.js

@@ -0,0 +1,30 @@
+import { getGql } from '../utils/getGql';
+import { BACKEND_URL } from '../constants';
+
+export const setAvatar = ({avatarId, userId}) => {
+  const gql = getGql(`${BACKEND_URL}/graphql`);
+  console.log(avatarId, userId);
+
+  return gql(`
+      mutation setAvatar{
+           UserUpsert(user:{_id: "${userId}", avatar: {_id: "${avatarId}"}}){
+                _id, nick, avatar{
+                    _id url
+                }
+           }
+      }
+     `);
+};
+
+export const uploadTracks = (id) => {
+  const gql = getGql(`${BACKEND_URL}/graphql`);
+  console.log(id);
+
+  return gql(`
+      mutation uploadTrack{
+          TrackUpsert(track: {_id: "${id}"}){
+                _id, url, originalFileName
+           }
+      }
+     `);
+};

BIN
src/assets/jpg/vinylPlate.jpg


+ 10 - 3
src/components/App/App.jsx

@@ -1,6 +1,6 @@
 import React from 'react';
 import './App.scss';
-import { BrowserRouter, Route, Switch } from 'react-router-dom';
+import { Router, Route, Switch } from 'react-router-dom';
 import { Provider } from 'react-redux';
 import { ThemeProvider } from '@mui/material/styles';
 import { Header } from '../Header/Header';
@@ -12,20 +12,27 @@ import store from '../../store/store';
 import { theme } from '../../assets/theme';
 import { RegisterPage } from '../../pages/RegisterPage/RegisterPage';
 import { PrivateRoute } from '../PrivateRoute/PrivateRoute';
+import { UploadTracks } from '../../pages/UploadTracks/UploadTracks';
+import { SelectedPlaylistPage } from '../../pages/SelectedPlaylistPage/SelectedPlaylistPage';
+import { history } from '../../createHistory';
+import { ProfilePage } from '../../pages/ProfilePage/ProfilePage';
 
 export const App = () => (
   <ThemeProvider theme={theme}>
     <Provider store={store}>
-      <BrowserRouter>
+      <Router history={history}>
         <Header/>
         <Switch>
           <PrivateRoute exact path="/" component={MainPage}/>
           <PrivateRoute exact path="/playlists" component={PlaylistsPage}/>
+          <PrivateRoute exact path="/selectedPlaylist/:id" component={SelectedPlaylistPage}/>
+          <PrivateRoute exact path="/uploadTracks" component={UploadTracks}/>
+          <PrivateRoute exact path="/profile" component={ProfilePage}/>
           <Route exact path="/login" component={LoginPage}/>
           <Route exact path="/register" component={RegisterPage}/>
         </Switch>
         <Player/>
-      </BrowserRouter>
+      </Router>
     </Provider>
   </ThemeProvider>
 );

+ 0 - 23
src/components/Dropzone/Dropzone.jsx

@@ -1,23 +0,0 @@
-import React, { useCallback } from 'react';
-import { useDropzone } from 'react-dropzone';
-import { useDispatch } from 'react-redux';
-import { actionUploadFile } from '../../store/types/playerTypes';
-
-export const Dropzone = () => {
-  const dispatch = useDispatch();
-  const onDrop = useCallback(acceptedFiles => {
-    dispatch(actionUploadFile(acceptedFiles[0]));
-  }, []);
-  const {getRootProps, getInputProps, isDragActive} = useDropzone({onDrop});
-
-  return (
-    <div {...getRootProps()}>
-      <input {...getInputProps()} />
-      {
-        isDragActive
-          ? <p>Drop the files here ...</p>
-          : <p>Drag drop some files here, or click to select files</p>
-      }
-    </div>
-  );
-};

+ 26 - 0
src/components/GenerateGradient/GenerateGradient.jsx

@@ -0,0 +1,26 @@
+import React from 'react';
+import { Box } from '@mui/material';
+import './GenerateGradient.scss';
+
+const GenGradient = () => {
+  const r = Math.floor(Math.random() * 255);
+  const g = Math.floor(Math.random() * 10);
+  const b = Math.floor(Math.random() * 255);
+  return `rgb(${r},${g},${b})`;
+};
+
+export const GenerateGradient = () => {
+  const deg = Math.floor(Math.random() * 360);
+  return (
+    <Box
+      className="vinylPlate"
+      sx={{
+        background: `linear-gradient(${deg}deg,${GenGradient()},${GenGradient()},${GenGradient()})`,
+      }}
+    >
+      <Box className="vinylPlate__middleCircle">
+        <Box className="vinylPlate__innerCircle"/>
+      </Box>
+    </Box>
+  );
+};

+ 28 - 0
src/components/GenerateGradient/GenerateGradient.scss

@@ -0,0 +1,28 @@
+.vinylPlate {
+  width: 20vh;
+  height: 20vh;
+  border-radius: 50%;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+
+  &__middleCircle {
+    width: 6vh;
+    height: 6vh;
+    border-radius: 50%;
+    border: 1px solid black;
+    display: flex;
+    align-items: center;
+    justify-Content: center;
+    background: linear-gradient(to left, #360930d3, #42093bd3 50%, #f0d3ecd3 100%)
+  }
+
+  &__innerCircle {
+    width: 1vh;
+    height: 1vh;
+    border-radius: 50%;
+    border: 1px solid black;
+    background-color: white;
+  }
+}
+

+ 39 - 25
src/components/Header/Header.jsx

@@ -1,35 +1,49 @@
 import React from 'react';
 import './Header.scss';
 import { AccountCircle } from '@mui/icons-material';
+import UploadIcon from '@mui/icons-material/Upload';
 import {
   AppBar, Toolbar, IconButton, Typography, Box, Button,
 } from '@mui/material';
 import { Link } from 'react-router-dom';
 import { ReactComponent as Vector } from '../../assets/svgs/Vector.svg';
 
-export const Header = () => (
-  <AppBar position="static">
-    <Toolbar sx={{display: 'flex', justifyContent: 'space-between'}}>
-      <Vector className="logo"/>
-      <Box sx={{
-        width: '50%', display: 'flex', justifyContent: 'space-between', alignItems: 'center',
-      }}
-      >
-        <Link to="/">
-          <Button variant="secondary">Main</Button>
-        </Link>
-        <Link to="/playlists">
-          <Button variant="secondary">Playlists</Button>
-        </Link>
-        <Link to="/login">
-          <IconButton
-            size="large"
-            color="inherit"
+export const Header = () => {
+  console.log(localStorage.getItem('authToken') ? './profile' : './login');
+  return (
+    <AppBar position="static">
+      <Toolbar sx={{display: 'flex', justifyContent: 'space-between'}}>
+        <Vector className="logo"/>
+        <Box sx={{
+          width: '50%', display: 'flex', justifyContent: 'space-between', alignItems: 'center',
+        }}
+        >
+          <Link to="/">
+            <Button variant="secondary">Main</Button>
+          </Link>
+          <Link to="/playlists">
+            <Button variant="secondary">Playlists</Button>
+          </Link>
+          <Link to="/uploadTracks">
+            <IconButton
+              size="large"
+              color="inherit"
+            >
+              <UploadIcon/>
+            </IconButton>
+          </Link>
+          <Link
+            to="/profile"
           >
-            <AccountCircle/>
-          </IconButton>
-        </Link>
-      </Box>
-    </Toolbar>
-  </AppBar>
-);
+            <IconButton
+              size="large"
+              color="inherit"
+            >
+              <AccountCircle/>
+            </IconButton>
+          </Link>
+        </Box>
+      </Toolbar>
+    </AppBar>
+  );
+};

+ 7 - 2
src/components/Player/Player.jsx

@@ -1,6 +1,6 @@
 import React, { useEffect, useState } from 'react';
 import {
-  Box, IconButton, Slider, Stack,
+  Box, IconButton, Slider, Stack, Typography,
 } from '@mui/material';
 import VolumeOffIcon from '@mui/icons-material/VolumeOff';
 import {
@@ -19,6 +19,7 @@ export const Player = () => {
   const dispatch = useDispatch();
   const [volume, setVolume] = useState(DEFAULT_VOLUME);
   const [muted, setMuted] = useState(false);
+  const trackIndex = playerState.trackList.findIndex(track => track._id === playerState.currentPlayingTrackId);
 
   const onVolumeChange = (e) => {
     const volume = e.target.value / 100;
@@ -64,7 +65,7 @@ export const Player = () => {
   return (
     <Box sx={{
       width: '100%',
-      height: '20%',
+      height: '30vh',
       position: 'sticky',
       bottom: '0',
       backgroundColor: 'white',
@@ -74,11 +75,15 @@ export const Player = () => {
     }}
     >
       <PlayerBar/>
+      <Typography variant="caption">
+        {playerState.trackList?.[trackIndex]?.originalFileName ?? ''}
+      </Typography>
       <Box
         sx={{
           display: 'flex',
           alignItems: 'center',
           flexDirection: 'row',
+          marginTop: '10px',
         }}
       >
         <IconButton

+ 2 - 1
src/components/PlayerBar/PlayerBar.jsx

@@ -11,6 +11,7 @@ export const PlayerBar = () => {
   const dispatch = useDispatch();
 
   const onChange = e => {
+    console.log(e);
     dispatch(actionChangeTime(e.target.value));
   };
 
@@ -43,7 +44,7 @@ export const PlayerBar = () => {
     <Box sx={{
       width: '100%',
       height: '20%',
-      bottom: '0',
+      marginBottom: '10px',
       backgroundColor: 'white',
       display: playerState.audio === null ? 'none' : 'flex',
       alignItems: 'center',

+ 41 - 46
src/components/TrackList/TrackList.jsx

@@ -1,22 +1,22 @@
 import React, { useEffect, useState } from 'react';
 import {
   IconButton, List, ListItem, Typography,
-  Stack, Box, Pagination, CircularProgress,
+  Box, CircularProgress,
 } from '@mui/material';
 import {
   PauseRounded, PlayArrowRounded,
 } from '@mui/icons-material';
 import { useDispatch, useSelector } from 'react-redux';
+import { SortableContainer, SortableElement } from 'react-sortable-hoc';
+import { arrayMoveImmutable } from 'array-move';
 import {
   actionPause, actionPlay,
 } from '../../store/types/playerTypes';
-import { actionFetchTracks } from '../../store/types/trackTypes';
 
-export const TrackList = ({tracks, trackCount, isLoading}) => {
+export const TrackList = ({tracks, isLoading}) => {
   const dispatch = useDispatch();
   const playerState = useSelector(state => state.player);
-
-  const [page, setPage] = useState(1);
+  const [currentTracks, setCurrentTracks] = useState([]);
 
   useEffect(() => {
     if (playerState.trackList.length === 0) {
@@ -35,6 +35,37 @@ export const TrackList = ({tracks, trackCount, isLoading}) => {
     }
   }, [playerState.isPlaying]);
 
+  useEffect(() => {
+    setCurrentTracks(tracks);
+  }, [tracks]);
+
+  const SortableItem = SortableElement(({track}) => (
+    <ListItem key={track._id}>
+      <IconButton
+        onClick={() => togglePlayPause(track._id)}
+      >
+        {
+          playerState.isPlaying && track._id === playerState.currentPlayingTrackId
+            ? (<PauseRounded fontSize="large" color="primary"/>)
+            : (<PlayArrowRounded fontSize="large" color="primary"/>)
+        }
+      </IconButton>
+      <Typography>{track?.originalFileName}</Typography>
+    </ListItem>
+  ));
+
+  const SortableList = SortableContainer(() => (
+    <List>
+      {currentTracks.map((track, index) => (
+        <SortableItem key={`item-${track._id}`} index={index} track={track}/>
+      ))}
+    </List>
+  ));
+
+  const onSortEnd = ({oldIndex, newIndex}) => {
+    setCurrentTracks(arrayMoveImmutable(currentTracks, oldIndex, newIndex));
+  };
+
   const togglePlayPause = (id) => {
     if (playerState.isPlaying) {
       playerState.audio.pause();
@@ -47,51 +78,15 @@ export const TrackList = ({tracks, trackCount, isLoading}) => {
       dispatch(actionPlay({trackList: tracks, id}));
     }
   };
-  useEffect(() => {
-    dispatch(actionFetchTracks(page));
-  }, [page]);
-
-  const handleChange = (e, value) => {
-    setPage(value);
-  };
 
   return isLoading ? (
     <CircularProgress/>
   ) : (
-    <Box>
-      <List>
-        {tracks.map(track => (
-          <ListItem key={track._id}>
-            <IconButton
-              onClick={() => togglePlayPause(track._id)}
-            >
-              {
-                playerState.isPlaying && track._id === playerState.currentPlayingTrackId
-                  ? (<PauseRounded fontSize="large" color="primary"/>)
-                  : (<PlayArrowRounded fontSize="large" color="primary"/>)
-              }
-            </IconButton>
-            <Typography>{track.originalFileName}</Typography>
-          </ListItem>
-        ))}
-      </List>
-      <Stack
-        spacing={2}
-        position="static"
-        bottom="0"
-        sx={{
-          display: 'flex',
-          alignItems: 'center',
-          mb: '3%',
-        }}
-      >
-        <Pagination
-          page={page}
-          count={Math.ceil(trackCount / 100)}
-          onChange={handleChange}
-          color="primary"
-        />
-      </Stack>
+    <Box sx={{
+      minHeight: '70vh',
+    }}
+    >
+      <SortableList onSortEnd={onSortEnd}/>
     </Box>
   );
 };

+ 3 - 0
src/createHistory.js

@@ -0,0 +1,3 @@
+import { createBrowserHistory } from 'history';
+
+export const history = createBrowserHistory();

+ 75 - 34
src/pages/LoginPage/LoginPage.jsx

@@ -1,18 +1,30 @@
 import React, { useState } from 'react';
 import {
-  Avatar, Button, Checkbox, FormControlLabel, Grid, OutlinedInput, Paper, TextField, Typography,
+  Alert,
+  Avatar, Box,
+  Button,
+  FormControl,
+  Grid,
+  IconButton, Input,
+  InputAdornment, InputLabel,
+  Paper, Snackbar,
+  Typography,
 } from '@mui/material';
 import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
 import './LoginPage.scss';
-import { Link } from 'react-router-dom';
-import { useDispatch } from 'react-redux';
-import { RegisterPage } from '../RegisterPage/RegisterPage';
-import { actionLogin, actionLoginSuccess } from '../../store/types/authTypes';
+import { Link, useHistory } from 'react-router-dom';
+import { useDispatch, useSelector } from 'react-redux';
+import VisibilityIcon from '@mui/icons-material/Visibility';
+import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
+import { actionLogin } from '../../store/types/authTypes';
 
 export const LoginPage = () => {
   const dispatch = useDispatch();
+  const authToken = useSelector(state => state.auth.authToken);
   const [login, setLogin] = useState(null);
   const [password, setPassword] = useState(null);
+  const [visiblePsw, setVisiblePsw] = useState(false);
+  const [openSnackBar, setOpenSnackBar] = useState(false);
 
   const onChangeLogin = (e) => {
     setLogin(e.target.value);
@@ -24,6 +36,16 @@ export const LoginPage = () => {
 
   const signIn = () => {
     dispatch(actionLogin({login, password}));
+    if (authToken !== null) {
+      setOpenSnackBar(true);
+    }
+  };
+
+  const handleClose = (event, reason) => {
+    if (reason === 'clickaway') {
+      return;
+    }
+    setOpenSnackBar(false);
   };
 
   return (
@@ -35,44 +57,49 @@ export const LoginPage = () => {
         container
       >
         <Grid item>
-          <Avatar>
-            <LockOutlinedIcon/>
+          <Avatar sx={{mb: '15px', backgroundColor: '#9c27b0'}}>
+            <LockOutlinedIcon color="white"/>
           </Avatar>
         </Grid>
         <Grid item>
           <Typography
             variant="h4"
-            margin="0 20px 0 10px"
           >
             Sign in
           </Typography>
         </Grid>
       </Grid>
-      <TextField
-        label="Username"
-        variant="standard"
-        placeholder="Enter username"
-        sx={{margin: '10px 0'}}
-        onChange={onChangeLogin}
-        fullWidth
-        required
-      />
-      <TextField
-        label="Password"
-        variant="standard"
-        placeholder="Enter password"
-        onChange={onChangePassword}
-        fullWidth
-        required
-      />
-      <FormControlLabel
-        control={(
-          <Checkbox
-            color="primary"
-          />
-        )}
-        label="Remember me"
-      />
+      <FormControl className="formControl" sx={{margin: '15px 0'}}>
+        <InputLabel>Username</InputLabel>
+        <Input
+          variant="standard"
+          placeholder="Enter username"
+          onChange={onChangeLogin}
+          fullWidth
+          required
+        />
+      </FormControl>
+      <FormControl className="formControl" sx={{margin: '15px 0'}}>
+        <InputLabel>Password</InputLabel>
+        <Input
+          variant="standard"
+          placeholder="Enter password"
+          onChange={onChangePassword}
+          sx={{margin: '10px 0'}}
+          type={visiblePsw ? 'text' : 'password'}
+          endAdornment={(
+            <InputAdornment position="end">
+              <IconButton onClick={() => setVisiblePsw(!visiblePsw)}>
+                {visiblePsw
+                  ? (<VisibilityIcon/>)
+                  : (<VisibilityOffIcon/>)}
+              </IconButton>
+            </InputAdornment>
+          )}
+          fullWidth
+          required
+        />
+      </FormControl>
       <Button
         type="submit"
         color="primary"
@@ -86,7 +113,8 @@ export const LoginPage = () => {
         Sign in
       </Button>
       <Typography>
-        Do you have an account?
+        {/* eslint-disable-next-line react/no-unescaped-entities */}
+        Don't you have an account?
         <Link
           to="/register"
         >
@@ -95,6 +123,19 @@ export const LoginPage = () => {
           </Button>
         </Link>
       </Typography>
+      <Snackbar
+        open={openSnackBar}
+        autoHideDuration={2000}
+        onClose={handleClose}
+        anchorOrigin={{vertical: 'bottom', horizontal: 'right'}}
+      >
+        <Alert
+          severity="success"
+          onClose={handleClose}
+        >
+          Success sign in!
+        </Alert>
+      </Snackbar>
     </Paper>
   );
 };

+ 4 - 0
src/pages/LoginPage/LoginPage.scss

@@ -3,4 +3,8 @@
   padding: 20px;
   width: 280px;
   margin: 20px auto;
+}
+
+.formControl{
+  width: 100%;
 }

+ 27 - 5
src/pages/MainPage/MainPage.jsx

@@ -1,23 +1,45 @@
 import React, { useEffect, useState } from 'react';
 import { useDispatch, useSelector } from 'react-redux';
+import { Box, Pagination, Stack } from '@mui/material';
 import { TrackList } from '../../components/TrackList/TrackList';
 import { actionFetchTracks } from '../../store/types/trackTypes';
 
 export const MainPage = () => {
   const dispatch = useDispatch();
   const tracksState = useSelector(state => state.tracks);
+  const [page, setPage] = useState(1);
 
   useEffect(() => {
-    dispatch(actionFetchTracks(1));
-  }, []);
+    dispatch(actionFetchTracks(page));
+  }, [page]);
+
+  const handleChange = (e, value) => {
+    setPage(value);
+  };
 
   return (
-    <div>
+    <Box>
       <TrackList
         tracks={tracksState.trackList}
-        trackCount={tracksState.totalCount}
         isLoading={tracksState.isLoading}
       />
-    </div>
+      <Stack
+        spacing={2}
+        position="static"
+        bottom="0"
+        sx={{
+          display: 'flex',
+          alignItems: 'center',
+          mb: '3%',
+        }}
+      >
+        <Pagination
+          page={page}
+          count={Math.ceil(tracksState.totalCount / 100)}
+          onChange={handleChange}
+          color="primary"
+        />
+      </Stack>
+    </Box>
   );
 };

+ 104 - 9
src/pages/PlaylistsPage/PlaylistsPage.jsx

@@ -1,9 +1,104 @@
-import React from 'react';
-import { Dropzone } from '../../components/Dropzone/Dropzone';
-
-export const PlaylistsPage = () => (
-  <div>
-    PlaylistsPage
-    <Dropzone/>
-  </div>
-);
+import React, { useEffect, useState } from 'react';
+import {
+  Box,
+  Grid, Pagination, Paper, Stack, Typography,
+} from '@mui/material';
+import { useDispatch, useSelector } from 'react-redux';
+import { Link } from 'react-router-dom';
+import { actionFetchPlaylists } from '../../store/types/playlistTypes';
+import { GenerateGradient } from '../../components/GenerateGradient/GenerateGradient';
+
+export const PlaylistsPage = () => {
+  const dispatch = useDispatch();
+  const state = useSelector(state => state.playlists);
+  const {playlists, totalCount} = state;
+  const [page, setPage] = useState(1);
+
+  useEffect(() => {
+    dispatch(actionFetchPlaylists(page));
+  }, [page]);
+
+  const handleChange = (e, value) => {
+    setPage(value);
+  };
+
+  return (
+    <Box>
+      <Grid
+        spacing={3}
+        columns={12}
+        sx={{margin: '0'}}
+        container
+      >
+        {playlists.map(playlist => (
+          <Grid
+            key={playlist._id}
+            sx={{mb: '5%'}}
+            item
+          >
+            <Link to={`/selectedPlaylist/${playlist._id}`}>
+              <Paper
+                sx={{
+                  minHeight: '30vh',
+                  width: '20vh',
+                  display: 'flex',
+                  alignItems: 'center',
+                  flexDirection: 'column',
+                  justifyContent: 'space-around',
+                }}
+                elevation={10}
+              >
+                <Box
+                  sx={{
+                    display: 'flex',
+                    alignItems: 'center',
+                    flexDirection: 'column',
+                    justifyContent: 'center',
+                    textAlign: 'center',
+                  }}
+                >
+                  <GenerateGradient />
+                </Box>
+                <Box
+                  sx={{
+                    display: 'flex',
+                    alignItems: 'center',
+                    flexDirection: 'column',
+                    justifyContent: 'center',
+                    textAlign: 'center',
+                  }}
+                >
+                  <Typography
+                    variant="caption"
+                    sx={{
+                      whiteSpace: 'pre-wrap',
+                    }}
+                  >
+                    {playlist.name}
+                  </Typography>
+                </Box>
+              </Paper>
+            </Link>
+          </Grid>
+        ))}
+      </Grid>
+      <Stack
+        spacing={2}
+        position="static"
+        bottom="0"
+        sx={{
+          display: 'flex',
+          alignItems: 'center',
+          mb: '3%',
+        }}
+      >
+        <Pagination
+          page={page}
+          count={Math.ceil(totalCount / 20)}
+          onChange={handleChange}
+          color="primary"
+        />
+      </Stack>
+    </Box>
+  );
+};

+ 132 - 0
src/pages/ProfilePage/ProfilePage.jsx

@@ -0,0 +1,132 @@
+import React, { useCallback, useEffect } from 'react';
+import {
+  Avatar, Box, Button, Grid, Paper, Typography, useTheme,
+} from '@mui/material';
+import { useDispatch, useSelector } from 'react-redux';
+import EditIcon from '@mui/icons-material/Edit';
+import { AddCircleOutline } from '@mui/icons-material';
+import { useDropzone } from 'react-dropzone';
+import AddAPhotoIcon from '@mui/icons-material/AddAPhoto';
+import { actionFindUserById } from '../../store/types/authTypes';
+import { history } from '../../createHistory';
+import { jwtDecode } from '../../utils/jwtDecode';
+import { actionSetUploadFile } from '../../store/types/uploadTypes';
+import { BACKEND_URL } from '../../constants';
+import './ProfilePage.scss';
+
+export const ProfilePage = () => {
+  const dispatch = useDispatch();
+  const user = useSelector(state => state.auth.user);
+
+  const onDrop = useCallback(acceptedFiles => {
+    dispatch(actionSetUploadFile(acceptedFiles[0]));
+  }, []);
+
+  const {getRootProps, getInputProps} = useDropzone({onDrop});
+
+  console.log(user?.avatar !== null, user);
+
+  useEffect(() => {
+    if (user === null && localStorage.getItem('authToken') !== null) {
+      const token = jwtDecode(localStorage.getItem('authToken'));
+      const {id} = token.sub;
+      if (id.length !== 0) {
+        console.log(id, 'i tut');
+        dispatch(actionFindUserById(id));
+      }
+    }
+  }, []);
+
+  const logOut = () => {
+    localStorage.removeItem('authToken');
+    if (localStorage.getItem('authToken') === null) {
+      history.push('/login');
+    }
+  };
+
+  return (
+    <Paper
+      elevation={10}
+      sx={{
+        margin: '0 25vh',
+      }}
+    >
+      <Grid
+        direction="column"
+        alignItems="center"
+        justifyContent="center"
+        rowSpacing="10px"
+        marginTop="10px"
+        container
+      >
+        <Grid
+          sx={{m: '10px'}}
+          item
+        >
+          <Typography variant="subtitle">
+            Profile
+          </Typography>
+        </Grid>
+        <Grid
+          // onClick={changeAvatar}
+          item
+        >
+          <Box {...getRootProps()}>
+            <input {...getInputProps()} />
+            {
+              user?.avatar !== null
+                ? (
+                  <Avatar
+                    className="avatar"
+                    sx={{width: 100, height: 100}}
+                    src={
+                      user === null
+                        ? null
+                        : (`${BACKEND_URL}/${user?.avatar?.url}`)
+                      }
+                  />
+                )
+                : (
+                  <Avatar
+                    className="avatar"
+                    sx={{width: 100, height: 100}}
+                  >
+                    <AddCircleOutline fontSize="large" color="white"/>
+                  </Avatar>
+                )
+            }
+          </Box>
+        </Grid>
+        <Grid
+          sx={{
+            mb: '10px',
+            display: 'flex',
+            flexDirection: 'row',
+          }}
+          item
+        >
+          <Typography
+            variant="subtitle2"
+            sx={{
+              mr: '5px',
+            }}
+          >
+            {user?.nick}
+          </Typography>
+          <EditIcon
+            fontSize="small"
+            color="primary"
+          />
+        </Grid>
+        <Grid
+          sx={{mb: '10px'}}
+          item
+        >
+          <Button onClick={logOut}>
+            Log out
+          </Button>
+        </Grid>
+      </Grid>
+    </Paper>
+  );
+};

+ 3 - 0
src/pages/ProfilePage/ProfilePage.scss

@@ -0,0 +1,3 @@
+.avatar{
+  background-color: #9c27b0;
+}

+ 171 - 67
src/pages/RegisterPage/RegisterPage.jsx

@@ -1,73 +1,177 @@
-import React from 'react';
+import React, { useState } from 'react';
 import {
-  Avatar, Button, Checkbox, FormControlLabel, Grid, Paper, TextField, Typography,
+  Alert,
+  Avatar,
+  Button,
+  FormControl,
+  Grid,
+  IconButton, Input,
+  InputAdornment, InputLabel,
+  Paper, Snackbar,
+  Typography,
 } from '@mui/material';
 import { AddCircleOutline } from '@mui/icons-material';
+import VisibilityIcon from '@mui/icons-material/Visibility';
+import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
+import { useDispatch, useSelector } from 'react-redux';
+import { Link, useHistory } from 'react-router-dom';
+import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
+import { actionLogin, actionRegister } from '../../store/types/authTypes';
 
-export const RegisterPage = () => (
-  <Paper elevation={10} className="paperStyle">
-    <Grid
-      alignItems="center"
-      justifyContent="center"
-      flexDirection="column"
-      container
-    >
-      <Grid item>
-        <Avatar>
-          <AddCircleOutline/>
-        </Avatar>
-      </Grid>
-      <Grid align="center" item>
-        <Typography
-          variant="h4"
-          margin="20px 0"
+export const RegisterPage = () => {
+  const [login, setLogin] = useState(null);
+  const [password, setPassword] = useState(null);
+  const [confirmPsw, setConfirmPsw] = useState(null);
+  const [visiblePsw, setVisiblePsw] = useState(false);
+  const [openSnackBar, setOpenSnackBar] = useState(false);
+  const dispatch = useDispatch();
+  const auth = useSelector(state => state.auth);
+  const history = useHistory();
+
+  const onChangeName = (e) => {
+    setLogin(e.target.value);
+  };
+
+  const onChangePassword = (e) => {
+    setPassword(e.target.value);
+  };
+
+  const confirmPassword = (e) => {
+    setConfirmPsw(e.target.value);
+  };
+
+  const signUp = () => {
+    if (password === confirmPsw) {
+      dispatch(actionRegister({login, password}));
+      console.log(auth);
+
+      if (auth.login.length !== 0 && localStorage.getItem('authToken') !== null) {
+        setOpenSnackBar(!openSnackBar);
+      }
+    }
+  };
+
+  const handleClose = (event, reason) => {
+    if (reason === 'clickaway') {
+      return;
+    }
+    setOpenSnackBar(false);
+    history.push('/');
+  };
+
+  return (
+    <Paper elevation={10} className="paperStyle">
+      <Grid
+        alignItems="center"
+        justifyContent="center"
+        flexDirection="column"
+        container
+      >
+        <Grid item>
+          <Avatar sx={{mb: '15px', backgroundColor: '#9c27b0'}}>
+            <LockOutlinedIcon color="white"/>
+          </Avatar>
+        </Grid>
+        <Grid
+          align="center"
+          sx={{mb: '10px'}}
+          item
         >
-          Sign up
-        </Typography>
-        <Typography variant="caption">
-          Please fill this form to create an account!
-        </Typography>
+          <Typography variant="h4">
+            Sign up
+          </Typography>
+          <Typography variant="caption">
+            Create an account!
+          </Typography>
+        </Grid>
       </Grid>
-    </Grid>
-    <TextField
-      label="Name"
-      variant="standard"
-      placeholder="Enter name"
-      sx={{margin: '10px 0'}}
-      fullWidth
-      required
-    />
-    <TextField
-      label="Password"
-      variant="standard"
-      placeholder="Enter password"
-      fullWidth
-      required
-    />
-    <TextField
-      label="Confirm password"
-      variant="standard"
-      placeholder="Confirm password"
-      fullWidth
-      required
-    />
-    <FormControlLabel
-      sx={{
-        marginRight: '0',
-      }}
-      control={<Checkbox name="Checked"/>}
-      label="I accept the terms and conditions"
-    />
-    <Button
-      type="submit"
-      color="primary"
-      variant="contained"
-      sx={{
-        margin: '20px 0',
-      }}
-      fullWidth
-    >
-      Sign up
-    </Button>
-  </Paper>
-);
+      <FormControl className="formControl" sx={{mb: '10px'}}>
+        <InputLabel>Login</InputLabel>
+        <Input
+          label="Login"
+          variant="standard"
+          placeholder="Enter login"
+          sx={{margin: '10px 0'}}
+          onChange={onChangeName}
+          fullWidth
+          required
+        />
+      </FormControl>
+      <FormControl className="formControl" sx={{mb: '10px'}}>
+        <InputLabel>Password</InputLabel>
+        <Input
+          variant="standard"
+          placeholder="Enter password"
+          onChange={onChangePassword}
+          sx={{margin: '10px 0'}}
+          type={visiblePsw ? 'text' : 'password'}
+          endAdornment={(
+            <InputAdornment position="end">
+              <IconButton onClick={() => setVisiblePsw(!visiblePsw)}>
+                {visiblePsw
+                  ? (<VisibilityIcon/>)
+                  : (<VisibilityOffIcon/>)}
+              </IconButton>
+            </InputAdornment>
+          )}
+          fullWidth
+          required
+        />
+      </FormControl>
+      <FormControl className="formControl" sx={{mb: '10px'}}>
+        <InputLabel>Confirm password</InputLabel>
+        <Input
+          label="Confirm password"
+          variant="standard"
+          placeholder="Confirm password"
+          onChange={confirmPassword}
+          type={visiblePsw ? 'text' : 'password'}
+          endAdornment={(
+            <InputAdornment position="end">
+              <IconButton onClick={() => setVisiblePsw(!visiblePsw)}>
+                {visiblePsw
+                  ? (<VisibilityIcon/>)
+                  : (<VisibilityOffIcon/>)}
+              </IconButton>
+            </InputAdornment>
+          )}
+          fullWidth
+          required
+        />
+      </FormControl>
+      <Button
+        type="submit"
+        color="primary"
+        variant="contained"
+        sx={{
+          margin: '20px 0',
+        }}
+        onClick={signUp}
+        fullWidth
+      >
+        Sign up
+      </Button>
+      <Typography sx={{display: 'flex', alignItems: 'center'}}>
+        Already have an account?
+        <Link to="/login">
+          <Button>
+            Sign in
+          </Button>
+        </Link>
+      </Typography>
+      <Snackbar
+        open={openSnackBar}
+        autoHideDuration={2000}
+        onClose={handleClose}
+        anchorOrigin={{vertical: 'bottom', horizontal: 'right'}}
+      >
+        <Alert
+          severity="success"
+          onClose={handleClose}
+        >
+          Success registration and log in!
+        </Alert>
+      </Snackbar>
+    </Paper>
+  );
+};

+ 25 - 0
src/pages/SelectedPlaylistPage/SelectedPlaylistPage.jsx

@@ -0,0 +1,25 @@
+import React, { useEffect } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import { Box } from '@mui/material';
+import { useParams } from 'react-router-dom';
+import { actionFetchOnePlaylist } from '../../store/types/playlistTypes';
+import { TrackList } from '../../components/TrackList/TrackList';
+
+export const SelectedPlaylistPage = () => {
+  const playlists = useSelector(state => state.playlists);
+  const dispatch = useDispatch();
+  const params = useParams();
+  console.log(params.id, playlists.selectedPlaylist);
+
+  useEffect(() => {
+    dispatch(actionFetchOnePlaylist(params.id));
+  }, []);
+
+  return (
+    <Box>
+      <TrackList
+        tracks={playlists?.selectedPlaylist ?? []}
+      />
+    </Box>
+  );
+};

+ 53 - 0
src/pages/UploadTracks/UploadTracks.jsx

@@ -0,0 +1,53 @@
+import React, { useCallback } from 'react';
+import {
+  Box, Grid, Paper, Typography,
+} from '@mui/material';
+import { useDispatch } from 'react-redux';
+import { useDropzone } from 'react-dropzone';
+import UploadIcon from '@mui/icons-material/Upload';
+import { actionSetUploadTrack } from '../../store/types/uploadTypes';
+
+export const UploadTracks = () => {
+  const dispatch = useDispatch();
+  const onDrop = useCallback(acceptedFiles => {
+    console.log(acceptedFiles);
+    dispatch(actionSetUploadTrack(acceptedFiles[0]));
+  }, []);
+  const {getRootProps, getInputProps} = useDropzone({onDrop});
+
+  return (
+    <Paper elevation={10} className="paper">
+      <Grid
+        alignItems="center"
+        justifyContent="center"
+        flexDirection="column"
+        container
+      >
+        <Box
+          sx={{
+            width: '100%',
+            height: '50vh',
+            display: 'flex',
+            alignItems: 'center',
+            justifyContent: 'center',
+            flexDirection: 'column',
+          }}
+          {...getRootProps()}
+        >
+          <UploadIcon
+            color="primary"
+            sx={{
+              height: '40px',
+              width: '40px',
+              mb: '20px',
+            }}
+          />
+          <input {...getInputProps()} />
+          <Typography>
+            Drop file to upload
+          </Typography>
+        </Box>
+      </Grid>
+    </Paper>
+  );
+};

+ 21 - 0
src/store/reducers/authReducer.js

@@ -3,6 +3,7 @@ import types from '../types/authTypes';
 const initialState = {
   login: '',
   authToken: localStorage.getItem('authToken') ?? null,
+  user: null,
 };
 
 export function authReducer(state = initialState, action) {
@@ -29,6 +30,26 @@ export function authReducer(state = initialState, action) {
         ...state,
         errorMessage: action.payload,
       };
+    case types.FETCH_FIND_USER_BY_ID:
+      return {
+        ...state,
+      };
+    case types.FETCH_FIND_USER_BY_ID_SUCCESS:
+      console.log(action.payload);
+      return {
+        ...state,
+        user: action.payload,
+      };
+    case types.FETCH_FIND_USER_BY_ID_FAIL:
+      return {
+        ...state,
+      };
+    case types.SET_USER:
+      console.log(action.payload);
+      return {
+        ...state,
+        user: action.payload,
+      };
     default:
       return state;
   }

+ 47 - 0
src/store/reducers/playlistsReducer.js

@@ -0,0 +1,47 @@
+import types from '../types/playlistTypes';
+
+const initialState = {
+  playlists: [],
+  isLoading: false,
+  totalCount: 0,
+  selectedPlaylist: [],
+};
+
+export function playlistsReducer(state = initialState, action) {
+  switch (action.type) {
+    case types.FETCH_PLAYLISTS:
+      return {
+        ...state,
+        isLoading: true,
+      };
+    case types.FETCH_PLAYLISTS_SUCCESS:
+      return {
+        ...state,
+        isLoading: false,
+        totalCount: action.payload.playlistsCount,
+        playlists: action.payload.playlists,
+      };
+    case
+      types.FETCH_PLAYLISTS_FAIL:
+      return {
+        ...state,
+      };
+    case types.FETCH_ONE_PLAYLIST:
+      return {
+        ...state,
+        isLoading: true,
+      };
+    case types.FETCH_ONE_PLAYLIST_SUCCESS:
+      return {
+        ...state,
+        isLoading: false,
+        selectedPlaylist: action.payload,
+      };
+    case types.FETCH_ONE_PLAYLIST_FAIL:
+      return {
+        ...state,
+      };
+    default:
+      return state;
+  }
+}

+ 31 - 0
src/store/reducers/uploadReducer.js

@@ -0,0 +1,31 @@
+import types from '../types/uploadTypes';
+
+const initialState = {
+  file: null,
+  track: null,
+};
+
+export function uploadReducer(state = initialState, action) {
+  switch (action.type) {
+    case types.SET_UPLOAD_FILE:
+      return {
+        ...state,
+      };
+    case types.SET_UPLOAD_FILE_SUCCESS:
+      return {
+        ...state,
+        file: action.payload,
+      };
+    case types.SET_UPLOAD_TRACK:
+      return {
+        ...state,
+      };
+    case types.SET_UPLOAD_TRACK_SUCCESS:
+      return {
+        ...state,
+        track: action.payload,
+      };
+    default:
+      return state;
+  }
+}

+ 44 - 1
src/store/sagas/authSaga.js

@@ -6,19 +6,62 @@ import types, {
   actionRegisterSuccess,
   actionLoginFail,
   actionRegisterFail,
+  actionLogin,
+  actionFindUserByIdSuccess,
+  actionFindUserByIdFail, actionSetUser,
 } from '../types/authTypes';
-import { login } from '../../api/auth';
+import { findUserById, login, registration } from '../../api/auth';
+import { forwardToMainPage } from '../../utils/history';
+import { jwtDecode } from '../../utils/jwtDecode';
 
 function* loginWorker(action) {
+  const auth = yield select(state => state.auth.authToken);
   try {
+    localStorage.removeItem('authToken');
     const authToken = yield call(login, action.payload);
     localStorage.setItem('authToken', authToken);
     yield put(actionLoginSuccess({authToken, login: action.payload.login}));
+    const token = yield call(jwtDecode, auth);
+    const {id} = token.sub;
+
+    const user = yield call(findUserById, id);
+
+    yield put(actionFindUserByIdSuccess(user));
+    yield put(actionSetUser(user));
+
+    yield call(forwardToMainPage, '/');
   } catch (e) {
     yield put(actionLoginFail(e.message));
   }
 }
 
+function* registerWorker(action) {
+  try {
+    localStorage.removeItem('authToken');
+    const userData = yield call(registration, action.payload);
+    yield put(actionRegisterSuccess({authToken: null, login: userData.login}));
+    if (userData._id.length !== 0) {
+      yield call(loginWorker, actionLogin({login: action.payload.login, password: action.payload.password}));
+    }
+  } catch (e) {
+    yield put(actionRegisterFail(e.message));
+  }
+}
+
+function* findUserWorker(action) {
+  console.log(action.payload);
+  const auth = yield select(state => state.auth);
+  try {
+    const user = yield call(findUserById, action.payload);
+    console.log(action.payload, auth);
+    yield put(actionFindUserByIdSuccess(user));
+  } catch (e) {
+    yield put(actionFindUserByIdFail());
+  }
+}
+
 export function* authSaga() {
   yield takeLatest(types.FETCH_LOGIN, loginWorker);
+  yield takeLatest(types.FETCH_REGISTER, registerWorker);
+  yield takeLatest(types.FETCH_FIND_USER_BY_ID, findUserWorker);
 }

+ 38 - 0
src/store/sagas/playlistsSaga.js

@@ -0,0 +1,38 @@
+import {
+  call, put, takeLatest,
+} from 'redux-saga/effects';
+
+import {
+  getSelectedPlaylist, getPlaylistsWithPage, getPlaylistsCount,
+} from '../../api/playlists';
+import types, {
+  actionFetchOnePlaylistSuccess,
+  actionFetchPlaylistsFail,
+  actionFetchPlaylistsSuccess,
+  actionFetchOnePlaylistFail,
+} from '../types/playlistTypes';
+
+function* getAllPlaylists(action) {
+  try {
+    const playlistsCount = yield call(getPlaylistsCount);
+    const page = action.payload;
+    const playlists = yield call(getPlaylistsWithPage, page);
+    yield put(actionFetchPlaylistsSuccess({playlists, playlistsCount}));
+  } catch (e) {
+    yield put(actionFetchPlaylistsFail());
+  }
+}
+
+function* getOnePlaylist(action) {
+  try {
+    const selectedPlaylist = yield call(getSelectedPlaylist, action.payload);
+    yield put(actionFetchOnePlaylistSuccess(selectedPlaylist));
+  } catch (e) {
+    yield put(actionFetchOnePlaylistFail());
+  }
+}
+
+export function* playlistsSaga() {
+  yield takeLatest(types.FETCH_PLAYLISTS, getAllPlaylists);
+  yield takeLatest(types.FETCH_ONE_PLAYLIST, getOnePlaylist);
+}

+ 42 - 0
src/store/sagas/uploadSaga.js

@@ -0,0 +1,42 @@
+import {
+  call, put, select, takeLatest,
+} from 'redux-saga/effects';
+import types, { actionSetUploadFileSuccess } from '../types/uploadTypes';
+import { getGqlForUpload, getGqlForUploadTracks } from '../../utils/getGqlForUpload';
+import { jwtDecode } from '../../utils/jwtDecode';
+import { setAvatar, uploadTracks } from '../../api/upload';
+
+function* uploadFileWorker(action) {
+  const auth = yield select(state => state.auth.authToken);
+
+  try {
+    const response = yield call(getGqlForUpload, action.payload);
+    const avatarId = response._id;
+
+    const token = yield call(jwtDecode, auth);
+    const userId = token.sub.id;
+
+    const result = yield call(setAvatar, {userId, avatarId});
+
+    // yield put(actionSetUploadFileSuccess());
+  } catch (e) {
+    e.message;
+  }
+}
+
+function* uploadTrackWorker(action) {
+  try {
+    const response = yield call(getGqlForUploadTracks, action.payload);
+    const trackId = response._id;
+
+    const result = yield call(uploadTracks, trackId);
+    yield put(actionSetUploadFileSuccess(result));
+  } catch (e) {
+    e.message;
+  }
+}
+
+export function* uploadSaga() {
+  yield takeLatest(types.SET_UPLOAD_FILE, uploadFileWorker);
+  yield takeLatest(types.SET_UPLOAD_TRACK, uploadTrackWorker);
+}

+ 8 - 0
src/store/store.js

@@ -9,6 +9,10 @@ import { tracksReducer } from './reducers/tracksReducer';
 import { playerSaga } from './sagas/playerSaga';
 import { authReducer } from './reducers/authReducer';
 import { authSaga } from './sagas/authSaga';
+import { playlistsReducer } from './reducers/playlistsReducer';
+import { playlistsSaga } from './sagas/playlistsSaga';
+import { uploadSaga } from './sagas/uploadSaga';
+import { uploadReducer } from './reducers/uploadReducer';
 
 const sagaMiddleware = createSagaMiddleware();
 
@@ -16,6 +20,8 @@ const rootReducer = combineReducers({
   player: playerReducer,
   tracks: tracksReducer,
   auth: authReducer,
+  playlists: playlistsReducer,
+  upload: uploadReducer,
 });
 
 function* rootSaga() {
@@ -23,6 +29,8 @@ function* rootSaga() {
     tracksSaga(),
     playerSaga(),
     authSaga(),
+    playlistsSaga(),
+    uploadSaga(),
   ]);
 }
 

+ 11 - 0
src/store/types/authTypes.js

@@ -5,13 +5,24 @@ const types = {
   FETCH_REGISTER_FAIL: 'FETCH_REGISTER_FAIL',
   FETCH_LOGIN: 'FETCH_LOGIN',
   FETCH_REGISTER: 'FETCH_REGISTER',
+  FETCH_FIND_USER_BY_ID: 'FETCH_FIND_USER_BY_ID',
+  FETCH_FIND_USER_BY_ID_SUCCESS: 'FETCH_FIND_USER_BY_ID_SUCCESS',
+  FETCH_FIND_USER_BY_ID_FAIL: 'FETCH_FIND_USER_BY_ID_FAIL',
+  SET_USER: 'SET_USER',
 };
 
 export const actionLogin = (payload) => ({type: types.FETCH_LOGIN, payload});
 export const actionLoginSuccess = (payload) => ({type: types.FETCH_LOGIN_SUCCESS, payload});
 export const actionLoginFail = (payload) => ({type: types.FETCH_LOGIN_FAIL, payload});
+
 export const actionRegister = (payload) => ({type: types.FETCH_REGISTER, payload});
 export const actionRegisterSuccess = (payload) => ({type: types.FETCH_REGISTER_SUCCESS, payload});
 export const actionRegisterFail = (payload) => ({type: types.FETCH_REGISTER_FAIL, payload});
 
+export const actionFindUserById = (payload) => ({type: types.FETCH_FIND_USER_BY_ID, payload});
+export const actionFindUserByIdSuccess = (payload) => ({type: types.FETCH_FIND_USER_BY_ID_SUCCESS, payload});
+export const actionFindUserByIdFail = (payload) => ({type: types.FETCH_FIND_USER_BY_ID_FAIL, payload});
+
+export const actionSetUser = (payload) => ({type: types.SET_USER, payload});
+
 export default types;

+ 2 - 2
src/store/types/playerTypes.js

@@ -3,7 +3,7 @@ const types = {
   PLAY: 'PLAY',
   SET_AUDIO: 'SET_AUDIO',
   SET_CURRENT_TRACK_ID: 'SET_CURRENT_TRACK_ID',
-  SET_UPLOAD_FILE: 'SET_UPLOAD_FILE',
+  // SET_UPLOAD_FILE: 'SET_UPLOAD_FILE',
   SET_PLAYER_STATE: 'SET_PLAYER_STATE',
   PREVIOUS_TRACK: 'PREVIOUS_TRACK',
   NEXT_TRACK: 'NEXT_TRACK',
@@ -14,7 +14,7 @@ export const actionPause = (payload) => ({type: types.PAUSE, payload});
 export const actionPlay = (payload) => ({type: types.PLAY, payload});
 // export const actionSetAudio = (payload) => ({type: types.SET_AUDIO, payload});
 // export const actionSetCurrentTrackId = (payload) => ({type: types.SET_CURRENT_TRACK_ID, payload});
-export const actionUploadFile = (payload) => ({type: types.SET_UPLOAD_FILE, payload});
+// export const actionUploadFile = (payload) => ({type: types.SET_UPLOAD_FILE, payload});
 export const actionSetPlayerState = (payload) => ({type: types.SET_PLAYER_STATE, payload});
 export const actionPreviousTrack = (payload) => ({type: types.PREVIOUS_TRACK, payload});
 export const actionNextTrack = (payload) => ({type: types.NEXT_TRACK, payload});

+ 18 - 0
src/store/types/playlistTypes.js

@@ -0,0 +1,18 @@
+const types = {
+  FETCH_PLAYLISTS: 'FETCH_PLAYLISTS',
+  FETCH_PLAYLISTS_SUCCESS: 'FETCH_PLAYLISTS_SUCCESS',
+  FETCH_PLAYLISTS_FAIL: 'FETCH_PLAYLISTS_FAIL',
+  FETCH_ONE_PLAYLIST: 'FETCH_ONE_PLAYLIST',
+  FETCH_ONE_PLAYLIST_SUCCESS: 'FETCH_ONE_PLAYLIST_SUCCESS',
+  FETCH_ONE_PLAYLIST_FAIL: 'FETCH_ONE_PLAYLIST_FAIL',
+};
+
+export const actionFetchPlaylists = (payload) => ({type: types.FETCH_PLAYLISTS, payload});
+export const actionFetchPlaylistsSuccess = (payload) => ({type: types.FETCH_PLAYLISTS_SUCCESS, payload});
+export const actionFetchPlaylistsFail = (payload) => ({type: types.FETCH_PLAYLISTS_FAIL, payload});
+
+export const actionFetchOnePlaylist = (payload) => ({type: types.FETCH_ONE_PLAYLIST, payload});
+export const actionFetchOnePlaylistSuccess = (payload) => ({type: types.FETCH_ONE_PLAYLIST_SUCCESS, payload});
+export const actionFetchOnePlaylistFail = (payload) => ({type: types.FETCH_ONE_PLAYLIST_FAIL, payload});
+
+export default types;

+ 11 - 0
src/store/types/uploadTypes.js

@@ -0,0 +1,11 @@
+const types = {
+  SET_UPLOAD_FILE: 'SET_UPLOAD_FILE',
+  SET_UPLOAD_FILE_SUCCESS: 'SET_UPLOAD_FILE_SUCCESS',
+  SET_UPLOAD_TRACK: 'SET_UPLOAD_TRACK',
+};
+
+export const actionSetUploadFile = (payload) => ({type: types.SET_UPLOAD_FILE, payload});
+export const actionSetUploadFileSuccess = (payload) => ({type: types.SET_UPLOAD_FILE_SUCCESS, payload});
+export const actionSetUploadTrack = (payload) => ({type: types.SET_UPLOAD_TRACK, payload});
+
+export default types;

+ 33 - 0
src/utils/getGqlForUpload.js

@@ -0,0 +1,33 @@
+import { BACKEND_URL } from '../constants';
+
+export const getGqlForUpload = (data) => {
+  const formData = new FormData();
+  formData.append('photo', data);
+  console.log(data);
+  return (fetch(`${BACKEND_URL}/upload`, {
+    method: 'POST',
+    headers: {
+      ...(localStorage.authToken ? {'Authorization': `Bearer ${localStorage.authToken}`}
+        : {}),
+    },
+    body: formData,
+  }))
+    .then((response) => response.json());
+};
+
+export const getGqlForUploadTracks = (data) => {
+  console.log(data);
+  const formData = new FormData();
+  formData.append('track', data);
+  console.log(formData, data);
+
+  return (fetch(`${BACKEND_URL}/track`, {
+    method: 'POST',
+    headers: {
+      ...(localStorage.authToken ? {'Authorization': `Bearer ${localStorage.authToken}`}
+        : {}),
+    },
+    body: formData,
+  }))
+    .then((response) => response.json());
+};

+ 7 - 0
src/utils/history.js

@@ -0,0 +1,7 @@
+import { history } from '../createHistory';
+
+export const forwardToMainPage = (location) => {
+  if (localStorage.getItem('authToken')) {
+    history.push(location);
+  }
+};

+ 14 - 0
src/utils/jwtDecode.js

@@ -0,0 +1,14 @@
+// eslint-disable-next-line consistent-return
+export const jwtDecode = (token) => {
+  try {
+    let decoded = token.split('.');
+    // eslint-disable-next-line prefer-destructuring
+    decoded = decoded[1];
+    decoded = atob(decoded);
+    decoded = JSON.parse(decoded);
+    console.log(decoded);
+    return decoded;
+  } catch (e) {
+    e.message;
+  }
+};