Pārlūkot izejas kodu

added Skeleton, used regex for tracks name, fixed markup

Alyona Brytvina 2 gadi atpakaļ
vecāks
revīzija
1cdd3912ac
46 mainītis faili ar 853 papildinājumiem un 726 dzēšanām
  1. 3 0
      .eslintrc.js
  2. 5 0
      package-lock.json
  3. 1 0
      package.json
  4. BIN
      public/favicon.ico
  5. 1 1
      public/index.html
  6. 3 2
      src/api/auth.js
  7. 25 18
      src/api/playlists.js
  8. 5 1
      src/api/tracks.js
  9. 13 9
      src/components/App/App.jsx
  10. 72 23
      src/components/Dropzone/Dropzone.js
  11. 0 30
      src/components/Dropzone/SortableList.js
  12. 59 0
      src/components/Filter/Filter.jsx
  13. 4 4
      src/components/Header/Header.jsx
  14. 26 69
      src/components/Player/Player.jsx
  15. 1 2
      src/components/PlayerBar/PlayerBar.jsx
  16. 41 0
      src/components/Skeleton/SkeletonProduct.jsx
  17. 33 0
      src/components/SnackBar/SnackBar.jsx
  18. 20 16
      src/components/TrackList/TrackList.jsx
  19. 64 0
      src/components/VolumeControl/VolumeControl.jsx
  20. 24 0
      src/constants/index.js
  21. 1 5
      src/helpers/index.jsx
  22. 19 0
      src/hooks/usePagination.jsx
  23. 5 32
      src/pages/LoginPage/LoginPage.jsx
  24. 1 1
      src/pages/LoginPage/LoginPage.scss
  25. 21 61
      src/pages/MainPage/MainPage.jsx
  26. 80 119
      src/pages/PlaylistsPage/PlaylistsPage.jsx
  27. 112 96
      src/pages/ProfilePage/ProfilePage.jsx
  28. 0 3
      src/pages/ProfilePage/ProfilePage.scss
  29. 3 26
      src/pages/RegisterPage/RegisterPage.jsx
  30. 22 95
      src/pages/UploadPlaylist/UploadPlaylist.jsx
  31. 31 42
      src/pages/UploadTracks/UploadTracks.jsx
  32. 0 2
      src/store/reducers/authReducer.js
  33. 33 0
      src/store/reducers/snackBarReducer.js
  34. 0 5
      src/store/reducers/tracksReducer.js
  35. 5 0
      src/store/reducers/uploadReducer.js
  36. 11 4
      src/store/sagas/authSaga.js
  37. 10 2
      src/store/sagas/playerSaga.js
  38. 41 19
      src/store/sagas/playlistsSaga.js
  39. 21 18
      src/store/sagas/uploadSaga.js
  40. 9 5
      src/store/store.js
  41. 3 0
      src/store/types/playerTypes.js
  42. 9 0
      src/store/types/snackBarTypes.js
  43. 2 0
      src/store/types/uploadTypes.js
  44. 0 1
      src/utils/getGql.js
  45. 8 15
      src/utils/getGqlForUpload.js
  46. 6 0
      src/utils/regex.js

+ 3 - 0
.eslintrc.js

@@ -32,6 +32,7 @@ module.exports = {
     'object-curly-spacing': 'off',
     'no-underscore-dangle': 'off',
     'no-case-declarations': 'off',
+    'import/no-cycle': 'off',
     'no-unused-expressions': 'off',
     'eslint-disable-next-line': 'off',
     'no-restricted-globals ': 'off',
@@ -47,6 +48,8 @@ module.exports = {
     'max-len': 'off',
     'quote-props': 'off',
     'eslint quote-props': 'off',
+    'react/no-unescaped-entities': 'off',
+    'react/no-array-index-key': 'off',
     'import/extensions': 'off',
     'import/no-unresolved': 'off',
     'import/prefer-default-export': 'off',

+ 5 - 0
package-lock.json

@@ -10655,6 +10655,11 @@
         "@babel/runtime": "^7.9.2"
       }
     },
+    "redux-devtools-extension": {
+      "version": "2.13.9",
+      "resolved": "https://registry.npmjs.org/redux-devtools-extension/-/redux-devtools-extension-2.13.9.tgz",
+      "integrity": "sha512-cNJ8Q/EtjhQaZ71c8I9+BPySIBVEKssbPpskBfsXqb8HJ002A3KRVHfeRzwRo6mGPqsm7XuHTqNSNeS1Khig0A=="
+    },
     "redux-saga": {
       "version": "1.1.3",
       "resolved": "https://registry.npmjs.org/redux-saga/-/redux-saga-1.1.3.tgz",

+ 1 - 0
package.json

@@ -20,6 +20,7 @@
     "react-scripts": "5.0.0",
     "react-sortable-hoc": "^2.0.0",
     "redux": "^4.1.2",
+    "redux-devtools-extension": "^2.13.9",
     "redux-saga": "^1.1.3",
     "surge": "^0.23.1",
     "web-vitals": "^2.1.2"

BIN
public/favicon.ico


+ 1 - 1
public/index.html

@@ -24,7 +24,7 @@
       work correctly both with client-side routing and a non-root public URL.
       Learn how to configure a non-root public URL by running `npm run build`.
     -->
-    <title>React App</title>
+    <title>Bonita Player</title>
   </head>
   <body>
     <noscript>You need to enable JavaScript to run this app.</noscript>

+ 3 - 2
src/api/auth.js

@@ -34,7 +34,7 @@ export const registration = (payload) => {
   );
 };
 
-export const findUserById = (_id) => getGql(`
+export const findUserById = (_id) => (getGql(`
     query findUserById($id: String){
        UserFindOne(query: $id) {
            _id, login, nick, createdAt, avatar {
@@ -44,7 +44,8 @@ export const findUserById = (_id) => getGql(`
     }
      `, {
   id: JSON.stringify([{_id}]),
-});
+})
+);
 
 export const setNick = ({id, nick}) => getGql(`
       mutation setNick{

+ 25 - 18
src/api/playlists.js

@@ -56,27 +56,34 @@ export const getPlaylistsCount = () => getGql(`
 });
 
 export const createPlaylist = (playlistName) => getGql(`
-      mutation createPlaylist{
-          PlaylistUpsert(playlist: {name: "${playlistName}"}){
-              _id
-          }
-      }
-  `);
+     mutation createPlaylist ($playlistName: String!){
+                PlaylistUpsert(playlist: {name: $playlistName}) {
+                    _id
+                }
+            }
+  `, {playlistName});
 
-export const addTracksToPlaylist = ({playlistId, arrayOfTracks}) => getGql(`
-      mutation createPlaylist{
-          PlaylistUpsert(playlist: {
-            _id: "${playlistId}"
-             tracks:{
-                 _id: "${arrayOfTracks._id}"
-             }
-          }){
-           _id tracks{
-              _id 
-           }
+export const addTracksToPlaylist = ({playlistId, arrayOfTracks}) => (getGql(`
+      mutation p($playlist:PlaylistInput){
+         PlaylistUpsert(playlist:$playlist){
+         _id name tracks{
+               _id originalFileName url
+            }
          }
       }
-`);
+`, {playlist: {_id: playlistId, tracks: arrayOfTracks}}));
+
+export const getUserPlaylistsCount = (userId) => getGql(`
+      query getCount($query: String){
+       PlaylistCount(query:$query)
+      } 
+  `, {
+  query: JSON.stringify([{
+    ___owner: userId,
+    name: {$exists: true, $ne: ''},
+    tracks: {$exists: true, $ne: []},
+  }]),
+});
 
 export const getUserPlaylist = ({userId, page = 1}) => getGql(`
       query findUserPlaylists($query: String){

+ 5 - 1
src/api/tracks.js

@@ -13,7 +13,11 @@ export const getTracksWithPage = (page = 1) => getGql(`
             _id url originalFileName
         }
       }
-  `, {query: JSON.stringify([{}, {skip: [(page - 1) * 100]}])})
+  `, {
+  query: JSON.stringify([
+    {originalFileName: {$exists: true, $ne: ''}},
+    {skip: [(page - 1) * 100]}]),
+})
   .then(data => data.map(track => ({
     ...track,
     url: `${BACKEND_URL}/${track.url}`,

+ 13 - 9
src/components/App/App.jsx

@@ -17,6 +17,8 @@ import { history } from '../../createHistory';
 import { ProfilePage } from '../../pages/ProfilePage/ProfilePage';
 import { UploadPlaylist } from '../../pages/UploadPlaylist/UploadPlaylist';
 import { UploadTracks } from '../../pages/UploadTracks/UploadTracks';
+import { ROUTES } from '../../constants';
+import { SnackBar } from '../SnackBar/SnackBar';
 
 export const App = () => (
   <ThemeProvider theme={theme}>
@@ -24,17 +26,19 @@ export const App = () => (
       <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="/uploadPlaylist" component={UploadPlaylist}/>
-          <PrivateRoute exact path="/uploadTracks" component={UploadTracks}/>
-          <PrivateRoute exact path="/uploadPlaylist" component={UploadPlaylist}/>
-          <PrivateRoute exact path="/profile" component={ProfilePage}/>
-          <Route exact path="/login" component={LoginPage}/>
-          <Route exact path="/register" component={RegisterPage}/>
+          <PrivateRoute exact path={ROUTES.MAIN_PAGE} component={MainPage}/>
+          <PrivateRoute exact path={ROUTES.USER_MAIN_PAGE} component={MainPage}/>
+          <PrivateRoute exact path={ROUTES.PLAYLISTS_PAGE} component={PlaylistsPage}/>
+          <PrivateRoute exact path={ROUTES.USER_PLAYLISTS_PAGE} component={PlaylistsPage}/>
+          <PrivateRoute exact path={ROUTES.SELECTED_PLAYLIST_PAGE} component={SelectedPlaylistPage}/>
+          <PrivateRoute exact path={ROUTES.UPLOAD_PLAYLIST_PAGE} component={UploadPlaylist}/>
+          <PrivateRoute exact path={ROUTES.UPLOAD_TRACKS_PAGE} component={UploadTracks}/>
+          <PrivateRoute exact path={ROUTES.PROFILE_PAGE} component={ProfilePage}/>
+          <Route exact path={ROUTES.LOGIN_PAGE} component={LoginPage}/>
+          <Route exact path={ROUTES.REGISTER_PAGE} component={RegisterPage}/>
         </Switch>
         <Player/>
+        <SnackBar/>
       </Router>
     </Provider>
   </ThemeProvider>

+ 72 - 23
src/components/Dropzone/Dropzone.js

@@ -1,18 +1,60 @@
-import { Box, Typography } from '@mui/material';
+import {
+  Box, List, ListItem, ListItemButton, ListItemText, Typography,
+} from '@mui/material';
 import UploadIcon from '@mui/icons-material/Upload';
-import React from 'react';
+import React, { useState } from 'react';
 import { useDropzone } from 'react-dropzone';
+import { SortableContainer, SortableElement } from 'react-sortable-hoc';
 
-export const Dropzone = ({ onDrop, accept }) => {
-  const { getRootProps, getInputProps, isDragActive } = useDropzone({
+const SortableItem = SortableElement(({file, i}) => {
+  const [selectedIndex, setSelectedIndex] = useState(i);
+
+  const handleListItemClick = (event, index) => {
+    setSelectedIndex(index);
+  };
+
+  return (
+    <ListItem
+      sx={{
+        padding: '0',
+      }}
+    >
+      <ListItemButton
+        selected={selectedIndex === i}
+        onClick={(event) => handleListItemClick(event, 0)}
+      >
+        <ListItemText primary={`${i + 1} - ${file?.name}`}/>
+      </ListItemButton>
+    </ListItem>
+  );
+});
+
+export const SortableList = SortableContainer(({files}) => (
+  <List sx={{width: '400px', mt: '20px'}}>
+    {files.map((file, index) => (
+      <SortableItem
+        key={`${file.name}item-${+index}`}
+        i={index}
+        index={index}
+        file={file}
+      />
+    ))}
+  </List>
+));
+
+export const Dropzone = ({
+  onDrop, onSortEnd, files, multiple,
+}) => {
+  const {getRootProps, getInputProps, isDragActive} = useDropzone({
     onDrop,
-    // accept,
+    multiple,
+    accept: 'audio/*',
   });
 
   return (
     <Box
       sx={{
-        mt: '50px',
+        m: '30px 0',
         height: '50vh',
         display: 'flex',
         alignItems: 'center',
@@ -22,24 +64,31 @@ export const Dropzone = ({ onDrop, accept }) => {
       }}
       {...getRootProps()}
     >
-      <UploadIcon
-        color="primary"
-        sx={{
-          height: '40px',
-          width: '40px',
-          mb: '20px',
-        }}
-      />
       <input {...getInputProps()} />
-      <Typography>
-        {isDragActive ? (
-          <Box component="span">Release to drop the files here</Box>
-        ) : (
-          <Box component="span">
-            Drag some files here, or click to select files
-          </Box>
-        )}
-      </Typography>
+      {files.length > 0 ? (
+        <SortableList files={files} onSortEnd={onSortEnd}/>
+      ) : (
+        <>
+          <UploadIcon
+            color="primary"
+            sx={{
+              height: '40px',
+              width: '40px',
+              mb: '20px',
+            }}
+          />
+          <Typography>
+            {isDragActive ? (
+              <Box component="span">Release to drop the files here</Box>
+            ) : (
+              <Box component="span">
+                Drag some files here, or click to select files
+              </Box>
+            )}
+          </Typography>
+        </>
+      )}
+
     </Box>
   );
 };

+ 0 - 30
src/components/Dropzone/SortableList.js

@@ -1,30 +0,0 @@
-import React from 'react';
-import {
-  Box, List, ListItem, Typography,
-} from '@mui/material';
-import { SortableContainer, SortableElement } from 'react-sortable-hoc';
-
-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 component="span" variant="caption">{track?.originalFileName}</Typography>
-  </ListItem>
-));
-
-export const SortableList = SortableContainer(({tracks}) => (
-  <List sx={{width: '400px', margin: '20px 0'}}>
-    {tracks.map((track, index) => (
-      <SortableItem key={`${track._id}item-${+index}`} index={index} track={track}/>
-    ))}
-  </List>
-));

+ 59 - 0
src/components/Filter/Filter.jsx

@@ -0,0 +1,59 @@
+import {
+  Box, Button, ButtonGroup, Typography,
+} from '@mui/material';
+import FileUploadIcon from '@mui/icons-material/FileUpload';
+import React from 'react';
+import { Link } from 'react-router-dom';
+import { ROUTES } from '../../constants';
+import { forwardToPage } from '../../utils/history';
+
+export const Filter = ({
+  onClickUpload, linkToUser, linkToAll,
+  selectedFilter, categoryName, count, onSelect,
+}) => (
+  <Box
+    sx={{
+      m: '10px 30px',
+      display: 'flex',
+      justifyContent: 'space-between',
+      alignItems: 'center',
+    }}
+  >
+    <Box>
+      <Button
+        onClick={onClickUpload}
+        variant="outlined"
+      >
+        <FileUploadIcon fontSize="small"/>
+        <Typography marginLeft="5px" variant="button">
+          {`Upload ${categoryName}`}
+        </Typography>
+      </Button>
+    </Box>
+    <ButtonGroup variant="outlined">
+      <Button
+        variant={selectedFilter === 'all' ? 'contained' : 'outlined'}
+        onClick={() => {
+          onSelect('all');
+        }}
+      >
+        {`All ${categoryName}`}
+      </Button>
+      <Button
+        variant={selectedFilter === 'my' ? 'contained' : 'outlined'}
+        onClick={() => {
+          onSelect('my');
+        }}
+      >
+        {`My ${categoryName}`}
+      </Button>
+    </ButtonGroup>
+    <Typography
+      variant="button"
+      color="primary"
+      cursor="default"
+    >
+      {`Total - ${count}`}
+    </Typography>
+  </Box>
+);

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

@@ -10,10 +10,10 @@ import AudiotrackIcon from '@mui/icons-material/Audiotrack';
 import FormatListBulletedIcon from '@mui/icons-material/FormatListBulleted';
 import { ReactComponent as Vector } from '../../assets/svgs/Vector.svg';
 import { buildUrl } from '../../utils/buildUrl';
+import { ROUTES } from '../../constants';
 
 export const Header = () => {
   const user = useSelector(state => state.auth.user);
-  console.log(user);
 
   return (
     <AppBar position="static">
@@ -28,13 +28,13 @@ export const Header = () => {
           alignItems: 'center',
         }}
         >
-          <Link to="/">
+          <Link to={ROUTES.MAIN_PAGE}>
             <Button variant="secondary">
               <AudiotrackIcon/>
               <Typography variant="button" >Tracks</Typography>
             </Button>
           </Link>
-          <Link to="/playlists">
+          <Link to={ROUTES.PLAYLISTS_PAGE}>
             <Button variant="secondary">
               <FormatListBulletedIcon/>
               <Typography variant="button" marginLeft="5px">Playlists</Typography>
@@ -42,7 +42,7 @@ export const Header = () => {
           </Link>
         </Box>
         <Link
-          to={user !== null ? '/profile' : '/login'}
+          to={user !== null ? ROUTES.PROFILE_PAGE : ROUTES.LOGIN_PAGE}
         >
           <IconButton
             size="large"

+ 26 - 69
src/components/Player/Player.jsx

@@ -1,41 +1,35 @@
-import React, { useEffect, useState } from 'react';
+import React, { useEffect } from 'react';
 import {
-  Box, IconButton, Slider, Stack, Typography,
+  Box, CircularProgress, IconButton, Typography,
 } from '@mui/material';
-import VolumeOffIcon from '@mui/icons-material/VolumeOff';
+import { useDispatch, useSelector } from 'react-redux';
 import {
-  FastForwardRounded, FastRewindRounded, PauseRounded, PlayArrowRounded, VolumeDown, VolumeUp,
+  FastForwardRounded,
+  FastRewindRounded,
+  PauseRounded,
+  PlayArrowRounded,
 } from '@mui/icons-material';
-import { useDispatch, useSelector } from 'react-redux';
 import { PlayerBar } from '../PlayerBar/PlayerBar';
+import { regex } from '../../utils/regex';
 import {
   actionNextTrack,
-  actionPause, actionPlay, actionPreviousTrack,
+  actionPause,
+  actionPlay,
+  actionPreviousTrack,
 } from '../../store/types/playerTypes';
-
-const DEFAULT_VOLUME = 1;
+import { VolumeControl } from '../VolumeControl/VolumeControl';
 
 export const Player = () => {
-  const playerState = useSelector(state => state.player);
   const dispatch = useDispatch();
+  const playerState = useSelector(state => state.player);
 
-  const [volume, setVolume] = useState(DEFAULT_VOLUME);
-
-  const trackIndex = playerState.trackList.findIndex(track => track._id === playerState.currentPlayingTrackId);
-
-  const onVolumeChange = (e) => {
-    const volume = e.target.value / 100;
-    setVolume(volume);
-    playerState.audio.volume = volume;
-  };
+  const togglePlayPause = () => {
+    const {trackList, currentPlayingTrackId} = playerState;
 
-  const onMuted = () => {
-    if (volume === 0) {
-      playerState.audio.volume = DEFAULT_VOLUME;
-      setVolume(DEFAULT_VOLUME);
+    if (playerState.isPlaying) {
+      dispatch(actionPause());
     } else {
-      playerState.audio.volume = 0;
-      setVolume(playerState.audio.volume);
+      dispatch(actionPlay({trackList, id: currentPlayingTrackId}));
     }
   };
 
@@ -47,16 +41,6 @@ export const Player = () => {
     dispatch(actionNextTrack());
   };
 
-  const togglePlayPause = () => {
-    const {trackList, currentPlayingTrackId} = playerState;
-
-    if (playerState.isPlaying) {
-      dispatch(actionPause());
-    } else {
-      dispatch(actionPlay({trackList, id: currentPlayingTrackId}));
-    }
-  };
-
   useEffect(() => {
     playerState.audio?.addEventListener('ended', onForward);
 
@@ -65,9 +49,7 @@ export const Player = () => {
     };
   }, [playerState.audio]);
 
-  useEffect(() => {
-    setVolume(DEFAULT_VOLUME);
-  }, [playerState.currentPlayingTrackId]);
+  const trackIndex = playerState.trackList.findIndex(track => track._id === playerState.currentPlayingTrackId);
 
   return (
     <Box sx={{
@@ -83,13 +65,10 @@ export const Player = () => {
     >
       <PlayerBar/>
       <Typography variant="caption">
-        {playerState.trackList?.[trackIndex]?.originalFileName ?? ''}
+        {regex(playerState.trackList?.[trackIndex]?.originalFileName ?? '')}
       </Typography>
       <Box
         sx={{
-          display: 'flex',
-          alignItems: 'center',
-          flexDirection: 'row',
           marginTop: '10px',
         }}
       >
@@ -103,9 +82,11 @@ export const Player = () => {
           onClick={togglePlayPause}
           color="primary"
         >
-          {!playerState.isPlaying
-            ? (<PlayArrowRounded fontSize="large"/>)
-            : (<PauseRounded fontSize="large"/>)}
+          {playerState.duration === 0
+            ? <CircularProgress/>
+            : playerState.isPlaying
+              ? <PauseRounded fontSize="large"/>
+              : <PlayArrowRounded fontSize="large"/>}
         </IconButton>
         <IconButton
           color="primary"
@@ -116,31 +97,7 @@ export const Player = () => {
           />
         </IconButton>
       </Box>
-      <Stack
-        spacing={2}
-        direction="row"
-        alignItems="center"
-        minWidth="20%"
-        sx={
-          {ml: 2, width: 200}
-        }
-      >
-        <IconButton onClick={onMuted}>
-          {volume === 0
-            ? (<VolumeOffIcon fontSize="large"/>)
-            : volume >= 0.01 && volume <= 0.5
-              ? (<VolumeDown fontSize="large"/>)
-              : volume === 0
-                ? (<VolumeOffIcon fontSize="large"/>)
-                : (<VolumeUp fontSize="large"/>)}
-        </IconButton>
-        <Slider
-          arial-label="Volume"
-          max={100}
-          value={volume * 100}
-          onChange={onVolumeChange}
-        />
-      </Stack>
+      <VolumeControl />
     </Box>
   );
 };

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

@@ -11,7 +11,6 @@ export const PlayerBar = () => {
   const dispatch = useDispatch();
 
   const onChange = e => {
-    console.log(e);
     dispatch(actionChangeTime(e.target.value));
   };
 
@@ -58,6 +57,7 @@ export const PlayerBar = () => {
         step={1}
         value={position}
         onChange={onChange}
+        sx={{padding: '0'}}
         max={
           playerState.audio?.duration === undefined ? 0
             : Number.isNaN(playerState.audio?.duration)
@@ -70,7 +70,6 @@ export const PlayerBar = () => {
           display: 'flex',
           alignItems: 'center',
           justifyContent: 'space-between',
-          mt: -2,
         }}
       >
         <TinyText>{formatDuration(playerState.audio?.currentTime)}</TinyText>

+ 41 - 0
src/components/Skeleton/SkeletonProduct.jsx

@@ -0,0 +1,41 @@
+import React from 'react';
+import {
+  Box, Grid, Paper, Skeleton,
+} from '@mui/material';
+
+export const SkeletonProduct = () => (
+  <Box sx={{width: '600px', height: '100%', ml: '20px'}}>
+    {
+      Array(7).fill(undefined).map((item, index) => (
+        <Skeleton key={index} width="100%" height={70} animation="wave"/>
+      ))
+    }
+  </Box>
+);
+
+export const SkeletonProductPlaylists = () => (
+  <Grid
+    spacing={3}
+    container
+  >
+    {
+      Array(12).fill(undefined).map((item, index) => (
+        <Grid
+          key={index}
+          item
+        >
+          <Skeleton
+            animation="wave"
+            variant="rectangular"
+            width={155}
+            height={190}
+            sx={{
+              margin: '24px 0 0 20px',
+              borderRadius: '5%',
+            }}
+          />
+        </Grid>
+      ))
+    }
+  </Grid>
+);

+ 33 - 0
src/components/SnackBar/SnackBar.jsx

@@ -0,0 +1,33 @@
+import React from 'react';
+import { Alert, Snackbar } from '@mui/material';
+import { useDispatch, useSelector } from 'react-redux';
+import { actionResetSnackBar } from '../../store/types/snackBarTypes';
+
+export const SnackBar = () => {
+  const dispatch = useDispatch();
+  const state = useSelector(state => state?.snackBar);
+  const {message, isOpen} = state;
+
+  const handleClose = (event, reason) => {
+    if (reason === 'clickaway') {
+      return;
+    }
+    dispatch(actionResetSnackBar());
+  };
+
+  return (
+    <Snackbar
+      open={isOpen}
+      autoHideDuration={3000}
+      onClose={handleClose}
+      anchorOrigin={{vertical: 'bottom', horizontal: 'right'}}
+    >
+      <Alert
+        severity="success"
+        onClose={handleClose}
+      >
+        {message}
+      </Alert>
+    </Snackbar>
+  );
+};

+ 20 - 16
src/components/TrackList/TrackList.jsx

@@ -1,11 +1,13 @@
 import React from 'react';
 import {
   IconButton, List, ListItem, Typography,
-  Box, CircularProgress,
+  Box,
 } from '@mui/material';
 import { PauseRounded, PlayArrowRounded } from '@mui/icons-material';
 import { useDispatch, useSelector } from 'react-redux';
 import { actionPause, actionPlay } from '../../store/types/playerTypes';
+import { SkeletonProduct } from '../Skeleton/SkeletonProduct';
+import { regex } from '../../utils/regex';
 
 export const TrackList = ({tracks, isLoading}) => {
   const dispatch = useDispatch();
@@ -24,25 +26,27 @@ export const TrackList = ({tracks, isLoading}) => {
     }
   };
 
-  return isLoading ? (
-    <CircularProgress size="large"/>
-  ) : (
+  return (
     <Box sx={{
       minHeight: '70vh',
     }}
     >
-      <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>
+      {isLoading ? (
+        <SkeletonProduct />
+      ) : (
+        <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>{regex(track?.originalFileName)}</Typography>
+            </ListItem>
+          ))}
+        </List>
+      )}
     </Box>
   );
 };

+ 64 - 0
src/components/VolumeControl/VolumeControl.jsx

@@ -0,0 +1,64 @@
+import React, { useEffect, useState } from 'react';
+import {
+  IconButton, Slider, Stack,
+} from '@mui/material';
+import {
+  VolumeDown,
+  VolumeUp,
+} from '@mui/icons-material';
+import VolumeOffIcon from '@mui/icons-material/VolumeOff';
+import { useSelector } from 'react-redux';
+import { DEFAULT_VOLUME } from '../../constants';
+
+export const VolumeControl = () => {
+  const playerState = useSelector(state => state.player);
+  const [volume, setVolume] = useState(DEFAULT_VOLUME);
+
+  useEffect(() => {
+    setVolume(DEFAULT_VOLUME);
+  }, [playerState.currentPlayingTrackId]);
+
+  const onVolumeChange = (e) => {
+    const volume = e.target.value / 100;
+    setVolume(volume);
+    playerState.audio.volume = volume;
+  };
+
+  const onMuted = () => {
+    if (volume === 0) {
+      playerState.audio.volume = DEFAULT_VOLUME;
+      setVolume(DEFAULT_VOLUME);
+    } else {
+      playerState.audio.volume = 0;
+      setVolume(playerState.audio.volume);
+    }
+  };
+
+  return (
+    <Stack
+      spacing={2}
+      direction="row"
+      alignItems="center"
+      minWidth="20%"
+      sx={
+        {ml: 2, width: 200}
+      }
+    >
+      <IconButton onClick={onMuted}>
+        {volume === 0
+          ? (<VolumeOffIcon fontSize="large"/>)
+          : volume >= 0.01 && volume <= 0.5
+            ? (<VolumeDown fontSize="large"/>)
+            : volume === 0
+              ? (<VolumeOffIcon fontSize="large"/>)
+              : (<VolumeUp fontSize="large"/>)}
+      </IconButton>
+      <Slider
+        arial-label="Volume"
+        max={100}
+        value={volume * 100}
+        onChange={onVolumeChange}
+      />
+    </Stack>
+  );
+};

+ 24 - 0
src/constants/index.js

@@ -1 +1,25 @@
+import React from 'react';
+
 export const BACKEND_URL = 'http://player.asmer.fs.a-level.com.ua';
+
+export const ALERT_TYPES = {
+  SUCCESS: 'success',
+  ERROR: 'error',
+  WARNING: 'warning',
+  INFO: 'info',
+};
+
+export const ROUTES = {
+  MAIN_PAGE: '/',
+  USER_MAIN_PAGE: '/userTracks',
+  PLAYLISTS_PAGE: '/playlists',
+  USER_PLAYLISTS_PAGE: '/userPlaylists',
+  SELECTED_PLAYLIST_PAGE: '/selectedPlaylist/:id',
+  UPLOAD_PLAYLIST_PAGE: '/uploadPlaylist',
+  UPLOAD_TRACKS_PAGE: '/uploadTracks',
+  PROFILE_PAGE: '/profile',
+  LOGIN_PAGE: '/login',
+  REGISTER_PAGE: '/register',
+};
+
+export const DEFAULT_VOLUME = 1;

+ 1 - 5
src/helpers/index.jsx

@@ -1,11 +1,7 @@
 import React from 'react';
 
 export const saveState = state => {
-  try {
-    localStorage.setItem('state', JSON.stringify(state));
-  } catch (error) {
-    console.error('Can\'t save state to localStorage!');
-  }
+  localStorage.setItem('state', JSON.stringify(state));
 };
 
 export const stateToStorageSelector = state => ({

+ 19 - 0
src/hooks/usePagination.jsx

@@ -0,0 +1,19 @@
+import { useEffect, useState } from 'react';
+import { useDispatch } from 'react-redux';
+
+export const usePagination = ({
+  page, selectedFilter, actionFetchMy, actionFetchAll,
+}) => {
+  const dispatch = useDispatch();
+  const [currentPage, setCurrentPage] = useState(page);
+
+  useEffect(() => {
+    if (selectedFilter === 'all') {
+      dispatch(actionFetchAll(currentPage));
+    } else if (selectedFilter === 'my') {
+      dispatch(actionFetchMy(currentPage));
+    }
+  }, [currentPage, selectedFilter]);
+
+  return [currentPage, setCurrentPage];
+};

+ 5 - 32
src/pages/LoginPage/LoginPage.jsx

@@ -1,32 +1,29 @@
 import React, { useState } from 'react';
 import {
-  Alert,
   Avatar, Box,
   Button,
   FormControl,
   Grid,
   IconButton, Input,
   InputAdornment, InputLabel,
-  Paper, Snackbar,
+  Paper,
   Typography,
 } from '@mui/material';
 import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
 import './LoginPage.scss';
 import { Link } from 'react-router-dom';
-import { useDispatch, useSelector } from 'react-redux';
+import { useDispatch } from 'react-redux';
 import VisibilityIcon from '@mui/icons-material/Visibility';
 import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
 import { actionLogin } from '../../store/types/authTypes';
+import { ROUTES } from '../../constants';
 
 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 [loginDirty, setLoginDirty] = useState(false);
   const [pswDirty, setPswDirty] = useState(false);
@@ -44,16 +41,6 @@ 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 (
@@ -110,7 +97,7 @@ export const LoginPage = () => {
                     : (<VisibilityOffIcon/>)}
                 </IconButton>
               </InputAdornment>
-          )}
+            )}
             fullWidth
             required
           />
@@ -129,29 +116,15 @@ export const LoginPage = () => {
           Sign in
         </Button>
         <Typography>
-          {/* eslint-disable-next-line react/no-unescaped-entities */}
           Don't you have an account?
           <Link
-            to="/register"
+            to={ROUTES.REGISTER_PAGE}
           >
             <Button>
               Sign up
             </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>
     </Box>
   );

+ 1 - 1
src/pages/LoginPage/LoginPage.scss

@@ -1,5 +1,5 @@
 .paperStyle{
-  height: 70vh;
+  height: 450px;
   padding: 20px;
   width: 280px;
   margin: 20px auto;

+ 21 - 61
src/pages/MainPage/MainPage.jsx

@@ -1,85 +1,45 @@
-import React, { useEffect, useState } from 'react';
-import { useDispatch, useSelector } from 'react-redux';
+import React, { useState } from 'react';
+import { useSelector } from 'react-redux';
 import {
-  Box, Button, ButtonGroup, Pagination, Stack, Typography,
+  Box, Pagination, Stack,
 } from '@mui/material';
-import FileUploadIcon from '@mui/icons-material/FileUpload';
 import { TrackList } from '../../components/TrackList/TrackList';
 import { actionFetchTracks, actionFetchUserTracks } from '../../store/types/trackTypes';
+import { Filter } from '../../components/Filter/Filter';
+import { usePagination } from '../../hooks/usePagination';
+import { ROUTES } from '../../constants';
 import { forwardToPage } from '../../utils/history';
 
 export const MainPage = () => {
-  const dispatch = useDispatch();
   const tracksState = useSelector(state => state.tracks);
 
-  const [page, setPage] = useState(1);
   const [selectedFilter, setSelectedFilter] = useState('all');
 
-  useEffect(() => {
-    if (selectedFilter === 'all') {
-      dispatch(actionFetchTracks(page));
-    } else if (selectedFilter === 'my') {
-      dispatch(actionFetchUserTracks(page));
-    }
-  }, [page]);
+  const [page, setPage] = usePagination({
+    page: 1,
+    selectedFilter,
+    actionFetchMy: actionFetchUserTracks,
+    actionFetchAll: actionFetchTracks,
+  });
 
   const handleChange = (e, value) => {
     setPage(value);
   };
 
-  const showUserTracks = () => {
-    setSelectedFilter('my');
+  const onSelectFilter = filter => {
     setPage(1);
-    dispatch(actionFetchUserTracks(page));
-  };
-
-  const showAllTracks = () => {
-    setSelectedFilter('all');
-    setPage(1);
-    dispatch(actionFetchTracks(page));
+    setSelectedFilter(filter);
   };
 
   return (
     <Box>
-      <ButtonGroup
-        sx={{
-          m: '10px 30px',
-          display: 'flex',
-          justifyContent: 'space-between',
-          alignItems: 'center',
-        }}
-      >
-        <Box>
-          <Button
-            onClick={() => forwardToPage('/uploadTracks')}
-            variant="outlined"
-          >
-            <FileUploadIcon fontSize="small"/>
-            <Typography marginLeft="5px" variant="button">Upload tracks</Typography>
-          </Button>
-        </Box>
-        <Box>
-          <Button
-            variant={selectedFilter === 'all' ? 'contained' : 'outlined'}
-            onClick={showAllTracks}
-          >
-            Tracks
-          </Button>
-          <Button
-            variant={selectedFilter === 'my' ? 'contained' : 'outlined'}
-            onClick={showUserTracks}
-          >
-            My tracks
-          </Button>
-        </Box>
-        <Typography
-          variant="button"
-          color="primary"
-          cursor="default"
-        >
-          {`Total - ${tracksState.totalCount}`}
-        </Typography>
-      </ButtonGroup>
+      <Filter
+        onClickUpload={() => forwardToPage(ROUTES.UPLOAD_TRACKS_PAGE)}
+        onSelect={onSelectFilter}
+        categoryName="tracks"
+        selectedFilter={selectedFilter}
+        count={tracksState.totalCount}
+      />
       <TrackList
         tracks={tracksState.trackList}
         isLoading={tracksState.isLoading}

+ 80 - 119
src/pages/PlaylistsPage/PlaylistsPage.jsx

@@ -1,154 +1,115 @@
-import React, { useEffect, useState } from 'react';
+import React, { useState } from 'react';
 import {
-  Box, Button, ButtonGroup,
+  Box,
   Grid, Pagination, Paper, Stack, Typography,
 } from '@mui/material';
-import { useDispatch, useSelector } from 'react-redux';
+import { useSelector } from 'react-redux';
 import { Link } from 'react-router-dom';
-
-import AddBoxIcon from '@mui/icons-material/AddBox';
 import { actionFetchPlaylists, actionFetchUserPlaylists } from '../../store/types/playlistTypes';
 import { GenerateGradient } from '../../components/GenerateGradient/GenerateGradient';
 import { forwardToPage } from '../../utils/history';
+import { Filter } from '../../components/Filter/Filter';
+import { usePagination } from '../../hooks/usePagination';
+import { ROUTES } from '../../constants';
+import { SkeletonProductPlaylists } from '../../components/Skeleton/SkeletonProduct';
 
 export const PlaylistsPage = () => {
-  const dispatch = useDispatch();
   const state = useSelector(state => state.playlists);
-  const {playlists, totalCount} = state;
+  const {playlists, totalCount, isLoading} = state;
 
-  const [page, setPage] = useState(1);
   const [selectedFilter, setSelectedFilter] = useState('all');
 
-  useEffect(() => {
-    dispatch(actionFetchPlaylists(page));
-  }, []);
-
-  useEffect(() => {
-    if (selectedFilter === 'all') {
-      dispatch(actionFetchPlaylists(page));
-    } else if (selectedFilter === 'my') {
-      dispatch(actionFetchUserPlaylists(page));
-    }
-  }, [page]);
+  const [page, setPage] = usePagination({
+    page: 1,
+    selectedFilter,
+    actionFetchMy: actionFetchUserPlaylists,
+    actionFetchAll: actionFetchPlaylists,
+  });
 
   const handleChange = (e, value) => {
     setPage(value);
   };
 
-  const showUserPlaylists = () => {
-    setSelectedFilter('my');
-    setPage(1);
-    dispatch(actionFetchUserPlaylists(page));
-  };
-
-  const showAllPlaylists = () => {
-    setSelectedFilter('all');
+  const onSelectFilter = filter => {
     setPage(1);
-    dispatch(actionFetchPlaylists(page));
+    setSelectedFilter(filter);
   };
 
   return (
     <Box>
-      <ButtonGroup
-        sx={{
-          display: 'flex',
-          justifyContent: 'space-between',
-          alignItems: 'center',
-          m: '10px 50px',
-        }}
-      >
-        <Box>
-          <Button variant="outlined" onClick={() => forwardToPage('/uploadPlaylist')}>
-            <AddBoxIcon fontSize="small"/>
-            <Typography marginLeft="5px" variant="button">Create playlist</Typography>
-          </Button>
-        </Box>
-        <Box>
-          <Button
-            variant={selectedFilter === 'all' ? 'contained' : 'outlined'}
-            onClick={showAllPlaylists}
-          >
-            Playlists
-          </Button>
-          <Button
-            variant={selectedFilter === 'my' ? 'contained' : 'outlined'}
-            onClick={showUserPlaylists}
-          >
-            My playlists
-          </Button>
-        </Box>
-        <Box>
-          <Typography
-            variant="button"
-            color="primary"
-            cursor="default"
-          >
-            {`Total - ${state.totalCount}`}
-          </Typography>
-        </Box>
-      </ButtonGroup>
-      <Grid
-        spacing={3}
-        columns={10}
-        sx={{
-          margin: '0',
-          width: '100%',
-          display: 'flex',
-          justifyContent: 'center',
-        }}
-        container
-      >
-        {playlists.map(playlist => (
-          <Grid
-            key={playlist._id}
-            item
-          >
-            <Link to={`/selectedPlaylist/${playlist._id}`}>
-              <Paper
-                sx={{
-                  minHeight: '30vh',
-                  width: '25vh',
-                  display: 'flex',
-                  alignItems: 'center',
-                  flexDirection: 'column',
-                  justifyContent: 'space-around',
-                }}
-                elevation={10}
-              >
-                <Box
+      <Filter
+        onClickUpload={() => forwardToPage(ROUTES.UPLOAD_PLAYLIST_PAGE)}
+        onSelect={onSelectFilter}
+        categoryName="playlists"
+        selectedFilter={selectedFilter}
+        count={totalCount}
+      />
+      {isLoading ? (
+        <SkeletonProductPlaylists />
+      ) : (
+        <Grid
+          spacing={3}
+          columns={10}
+          style={{
+            margin: '0',
+            width: '100%',
+            display: 'flex',
+            minHeight: 'calc(640px - 35vh)',
+          }}
+          container
+        >
+          {playlists.map(playlist => (
+            <Grid
+              key={playlist._id}
+              item
+            >
+              <Link to={`${ROUTES.SELECTED_PLAYLIST_PAGE.replace(':id', playlist._id)}`}>
+                <Paper
                   sx={{
+                    minHeight: '30vh',
+                    width: '25vh',
                     display: 'flex',
                     alignItems: 'center',
                     flexDirection: 'column',
-                    justifyContent: 'center',
-                    textAlign: 'center',
+                    justifyContent: 'space-around',
                   }}
+                  elevation={10}
                 >
-                  <GenerateGradient/>
-                </Box>
-                <Box
-                  sx={{
-                    display: 'flex',
-                    alignItems: 'center',
-                    flexDirection: 'column',
-                    justifyContent: 'center',
-                    textAlign: 'center',
-                  }}
-                >
-                  <Typography
-                    variant="caption"
+                  <Box
+                    sx={{
+                      display: 'flex',
+                      alignItems: 'center',
+                      flexDirection: 'column',
+                      justifyContent: 'center',
+                      textAlign: 'center',
+                    }}
+                  >
+                    <GenerateGradient/>
+                  </Box>
+                  <Box
                     sx={{
-                      whiteSpace: 'pre-wrap',
+                      display: 'flex',
+                      alignItems: 'center',
+                      flexDirection: 'column',
+                      justifyContent: 'center',
+                      textAlign: 'center',
                     }}
                   >
-                    {playlist.name}
-                  </Typography>
-                </Box>
-              </Paper>
-            </Link>
-          </Grid>
-        ))}
-      </Grid>
+                    <Typography
+                      variant="caption"
+                      sx={{
+                        whiteSpace: 'pre-wrap',
+                      }}
+                    >
+                      {playlist.name}
+                    </Typography>
+                  </Box>
+                </Paper>
+              </Link>
+            </Grid>
+          ))}
+        </Grid>
+      )}
       <Stack
         spacing={2}
         position="static"

+ 112 - 96
src/pages/ProfilePage/ProfilePage.jsx

@@ -1,6 +1,6 @@
 import React, { useCallback, useEffect, useState } from 'react';
 import {
-  Avatar, Box, Button, Grid, Input, InputAdornment, Paper, Typography,
+  Avatar, Box, Button, Grid, Input, InputAdornment, InputLabel, Paper, Typography,
 } from '@mui/material';
 import { useDispatch, useSelector } from 'react-redux';
 import EditIcon from '@mui/icons-material/Edit';
@@ -13,7 +13,7 @@ import {
 import { history } from '../../createHistory';
 import { jwtDecode } from '../../utils/jwtDecode';
 import { actionSetUploadFile } from '../../store/types/uploadTypes';
-import './ProfilePage.scss';
+
 import { buildUrl } from '../../utils/buildUrl';
 
 export const ProfilePage = () => {
@@ -27,13 +27,13 @@ export const ProfilePage = () => {
     dispatch(actionSetUploadFile(acceptedFiles[0]));
   }, []);
 
-  const {getRootProps, getInputProps} = useDropzone({onDrop});
+  const {getRootProps, getInputProps} = useDropzone({onDrop, accept: 'image/*'});
 
   useEffect(() => {
-    // findUser();
     if (user === null && localStorage.getItem('authToken') !== null) {
       const token = jwtDecode(localStorage.getItem('authToken'));
       const {id} = token.sub;
+
       if (id.length !== 0) {
         dispatch(actionFindUserById(id));
       }
@@ -56,7 +56,6 @@ export const ProfilePage = () => {
 
   const closeAndGetChangedNick = () => {
     setOpenNick(!openNick);
-    console.log(currentNick, user);
     dispatch(actionSetNick(currentNick));
   };
   return (
@@ -66,7 +65,7 @@ export const ProfilePage = () => {
         display: 'flex',
         alignItems: 'center',
         justifyContent: 'center',
-        flexDirection: 'column',
+        textAlign: 'center',
         height: '65vh',
       }}
     >
@@ -74,111 +73,128 @@ export const ProfilePage = () => {
         elevation={10}
         sx={{
           height: '100%',
-          width: '80vh',
+          width: '70vh',
         }}
       >
         <Grid
           direction="column"
           alignItems="center"
-          justifyContent="center"
+          justifyContent="space-around"
           sx={{
             height: '100%',
           }}
           container
         >
-          <Grid
-            sx={{mb: '10px'}}
-            item
-          >
-            <Typography variant="subtitle2">
-              Profile
-            </Typography>
-          </Grid>
-          <Grid item position="relative">
-            <Box {...getRootProps()} >
-              <input {...getInputProps()} />
-              <Box position="absolute" zIndex={isHovered ? '1' : '0'}>
-                <Avatar
-                  className="avatar"
-                  onMouseLeave={() => setIsHovered(!isHovered)}
-                  sx={{width: 100, height: 100}}
-                >
-                  <AddAPhotoIcon fontSize="large" color="primary"/>
-                </Avatar>
-              </Box>
-              <Box>
-                {user?.avatar?.url !== null
-                  ? (
-                    <Avatar
-                      className="avatar"
-                      onMouseEnter={() => {
-                        setIsHovered(!isHovered);
-                      }}
-                      sx={{width: 100, height: 100}}
-                      src={buildUrl(user?.avatar?.url ?? '')}
-                    />
-                  ) : (
-                    <Avatar
-                      className="avatar"
-                      onMouseLeave={() => setIsHovered(!isHovered)}
-                      sx={{width: 100, height: 100}}
-                    >
-                      <AddAPhotoIcon fontSize="large" color="primary"/>
-                    </Avatar>
-                  )}
-              </Box>
-            </Box>
-          </Grid>
-          <Grid
-            onMouseLeave={() => setOpenNick(!openNick)}
-            sx={{m: '20px'}}
-            item
-          >
-            {openNick
-              ? (
-                <Input
-                  sx={{
-                    mr: '5px',
-                  }}
-                  value={currentNick}
-                  onChange={onChangeNick}
-                  endAdornment={(
-                    <InputAdornment position="end">
-                      <CheckBoxIcon
-                        onClick={closeAndGetChangedNick}
-                        cursor="pointer"
-                        color="primary"
-                        fontSize="medium"
+          <Grid item>
+            <Grid
+              sx={{mb: '10px'}}
+              item
+            >
+              <Typography variant="h5">
+                Profile
+              </Typography>
+            </Grid>
+            <Grid item position="relative" sx={{display: 'flex', justifyContent: 'center', alignItems: 'center'}}>
+              <Box sx={{cursor: 'pointer', margin: '30px 0'}} {...getRootProps()} >
+                <input {...getInputProps()} />
+                <Box position="absolute" zIndex={isHovered ? '1' : '0'}>
+                  <Avatar
+                    onMouseLeave={() => setIsHovered(!isHovered)}
+                    sx={{
+                      width: 100, height: 100, backgroundColor: 'rgba(0,0,0,0.5)',
+                    }}
+                  >
+                    <AddAPhotoIcon fontSize="large"/>
+                  </Avatar>
+                </Box>
+                <Box>
+                  {user?.avatar?.url !== null
+                    ? (
+                      <Avatar
+                        className="avatar"
+                        onMouseEnter={() => {
+                          setIsHovered(!isHovered);
+                        }}
+                        sx={{width: 100, height: 100, backgroundColor: '#9c27b0'}}
+                        src={buildUrl(user?.avatar?.url ?? '')}
                       />
-                    </InputAdornment>
-                  )}
-                />
-              )
-              : (
-                <Grid
-                  sx={{
-                    mb: '10px',
-                    display: 'flex',
-                    flexDirection: 'row',
-                  }}
-                  item
-                >
-                  <Typography
-                    variant="subtitle1"
+                    ) : (
+                      <Avatar
+                        onMouseLeave={() => setIsHovered(!isHovered)}
+                        sx={{width: 100, height: 100, backgroundColor: '#9c27b0'}}
+                      >
+                        <AddAPhotoIcon fontSize="large" color="primary"/>
+                      </Avatar>
+                    )}
+                </Box>
+              </Box>
+            </Grid>
+            <Grid
+              sx={{m: '20px'}}
+              item
+            >
+              {openNick
+                ? (
+                  <Input
                     sx={{
                       mr: '5px',
                     }}
-                  >
-                    {user?.nick}
-                  </Typography>
-                  <EditIcon
-                    fontSize="medium"
-                    color="primary"
-                    cursor="pointer"
-                    onClick={() => setOpenNick(!openNick)}
+                    value={currentNick}
+                    onChange={onChangeNick}
+                    endAdornment={(
+                      <InputAdornment position="end">
+                        <CheckBoxIcon
+                          onClick={closeAndGetChangedNick}
+                          cursor="pointer"
+                          color="primary"
+                          fontSize="medium"
+                        />
+                      </InputAdornment>
+                    )}
                   />
-                </Grid>
-              )}
+                )
+                : (
+                  <Grid
+                    sx={{
+                      mb: '10px',
+                      display: 'flex',
+                      flexDirection: 'row',
+                    }}
+                    item
+                  >
+                    {user.nick === null || user?.nick?.length === 0
+                      ? (
+                        <Box sx={{display: 'flex'}}>
+                          <InputLabel>Enter nick</InputLabel>
+                          <EditIcon
+                            fontSize="medium"
+                            color="primary"
+                            cursor="pointer"
+                            onClick={() => setOpenNick(!openNick)}
+                          />
+                        </Box>
+                      ) : (
+                        <Box sx={{display: 'flex'}}>
+                          <Typography
+                            variant="subtitle1"
+                            sx={{
+                              mr: '5px',
+                            }}
+                          >
+                            {user?.nick}
+                          </Typography>
+                          <EditIcon
+                            fontSize="medium"
+                            color="primary"
+                            cursor="pointer"
+                            onClick={() => setOpenNick(!openNick)}
+                          />
+                        </Box>
+
+                      )}
+                  </Grid>
+                )}
+            </Grid>
           </Grid>
           <Grid
             sx={{mt: '10px'}}

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

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

+ 3 - 26
src/pages/RegisterPage/RegisterPage.jsx

@@ -1,19 +1,18 @@
 import React, { useState } from 'react';
 import {
-  Alert,
   Avatar, Box,
   Button,
   FormControl,
   Grid,
   IconButton, Input,
   InputAdornment, InputLabel,
-  Paper, Snackbar,
+  Paper,
   Typography,
 } from '@mui/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 { Link } from 'react-router-dom';
 import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
 import { actionRegister } from '../../store/types/authTypes';
 
@@ -36,7 +35,6 @@ export const RegisterPage = () => {
 
   const dispatch = useDispatch();
   const auth = useSelector(state => state.auth);
-  const history = useHistory();
 
   const onChangeName = (e) => {
     setLogin(e.target.value);
@@ -54,20 +52,12 @@ export const RegisterPage = () => {
     if (password === confirmPsw) {
       dispatch(actionRegister({login, password}));
 
-      if (auth.login.length !== 0 && localStorage.getItem('authToken') !== null) {
+      if (auth.login.length !== 0 && auth.authToken?.length !== 0) {
         setOpenSnackBar(!openSnackBar);
       }
     }
   };
 
-  const handleClose = (event, reason) => {
-    if (reason === 'clickaway') {
-      return;
-    }
-    setOpenSnackBar(false);
-    history.push('/');
-  };
-
   return (
     <Paper elevation={10} className="paperStyle">
       <Grid
@@ -182,19 +172,6 @@ export const RegisterPage = () => {
           </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>
   );
 };

+ 22 - 95
src/pages/UploadPlaylist/UploadPlaylist.jsx

@@ -1,46 +1,31 @@
-import React, { useCallback, useEffect, useState } from 'react';
+import React, { useCallback, useState } from 'react';
 import {
   Typography,
-  Box, Paper, Grid, Button, Avatar, FormControl, InputLabel, Input, InputAdornment, Alert, Snackbar,
+  Box, Paper, Grid, Button, Avatar, InputLabel, Input,
 } from '@mui/material';
 import { useDispatch, useSelector } from 'react-redux';
 import { arrayMoveImmutable } from 'array-move';
 import AudioFileIcon from '@mui/icons-material/AudioFile';
-import CheckBoxIcon from '@mui/icons-material/CheckBox';
-import EditIcon from '@mui/icons-material/Edit';
-import { SortableList } from '../../components/Dropzone/SortableList';
 import { Dropzone } from '../../components/Dropzone/Dropzone';
-import { actionSetUploadTrack } from '../../store/types/uploadTypes';
 import './UploadPlaylist.scss';
 import { actionCreatePlaylist } from '../../store/types/playlistTypes';
 
 export const UploadPlaylist = () => {
   const dispatch = useDispatch();
-  const upload = useSelector(state => state?.upload.tracks);
+  const [files, setFiles] = useState([]);
 
-  const [uploadTracks, setUploadTracks] = useState([]);
   const [playlistName, setPlaylistName] = useState(null);
-
   const [isNameDirty, setIsNameDirty] = useState(false);
-  const [openChangeName, setOpenChangeName] = useState(true);
-
-  const [openSnackBar, setOpenSnackBar] = useState(false);
-
-  useEffect(() => {
-    if (upload?.length !== 0) {
-      setUploadTracks(prevState => [
-        ...prevState,
-        {...upload},
-      ]);
-    }
-  }, [upload]);
 
   const onDrop = useCallback(acceptedFiles => {
-    dispatch(actionSetUploadTrack(acceptedFiles[0]));
+    setFiles(oldFiles => ([
+      ...oldFiles,
+      ...acceptedFiles,
+    ]));
   }, []);
 
   const onSortEnd = ({oldIndex, newIndex}) => {
-    setUploadTracks(arrayMoveImmutable(uploadTracks, oldIndex, newIndex));
+    setFiles(arrayMoveImmutable(files, oldIndex, newIndex));
   };
 
   const onChangePlaylistName = (e) => {
@@ -50,15 +35,7 @@ export const UploadPlaylist = () => {
   const isNameValid = playlistName?.length >= 5 && playlistName?.length < 20;
 
   const createPlaylist = () => {
-    console.log(playlistName, uploadTracks);
-    dispatch(actionCreatePlaylist({playlistName, uploadTracks}));
-  };
-
-  const handleClose = (event, reason) => {
-    if (reason === 'clickaway') {
-      return;
-    }
-    setOpenSnackBar(false);
+    dispatch(actionCreatePlaylist({playlistName, files}));
   };
 
   return (
@@ -88,57 +65,20 @@ export const UploadPlaylist = () => {
             </Typography>
           </Grid>
         </Grid>
-        {openChangeName
-          ? (
-            <Box>
-              <InputLabel error={isNameDirty && !isNameValid}>Playlist name</InputLabel>
-              <Input
-                sx={{
-                  mr: '5px',
-                }}
-                onBlur={() => setIsNameDirty(true)}
-                // value={currentNick}
-                error={isNameDirty && !isNameValid}
-                onChange={onChangePlaylistName}
-                endAdornment={(
-                  <InputAdornment position="end">
-                    <CheckBoxIcon
-                      onClick={() => {
-                        isNameValid ? setOpenChangeName(!openChangeName) : setIsNameDirty(true);
-                      }}
-                      cursor="pointer"
-                      color="primary"
-                      fontSize="medium"
-                    />
-                  </InputAdornment>
-                )}
-              />
-            </Box>
-          ) : (
-            <Box sx={{
-              display: 'flex',
+        <Box>
+          <InputLabel error={isNameDirty && !isNameValid}>Playlist name</InputLabel>
+          <Input
+            sx={{
+              mr: '5px',
             }}
-            >
-              <Typography
-                variant="subtitle1"
-                sx={{
-                  mr: '5px',
-                }}
-              >
-                {playlistName}
-              </Typography>
-              <EditIcon
-                fontSize="medium"
-                color="primary"
-                cursor="pointer"
-                onClick={() => setOpenChangeName(!openChangeName)}
-              />
-            </Box>
-          )}
+            onBlur={() => setIsNameDirty(true)}
+            error={isNameDirty && !isNameValid}
+            onChange={onChangePlaylistName}
+          />
+        </Box>
         <Box>
-          <Dropzone onDrop={onDrop}/>
-          <SortableList tracks={uploadTracks} onSortEnd={onSortEnd}/>
-          {upload.length === 0
+          <Dropzone onSortEnd={onSortEnd} multiple onDrop={onDrop} files={files}/>
+          {files.length === 0
             ? (
               <Typography>If you want to create a playlist drag and drop an audio file</Typography>
             ) : (
@@ -150,7 +90,7 @@ export const UploadPlaylist = () => {
                   margin: '20px 0',
                 }}
                 onClick={createPlaylist}
-                disabled={!(isNameValid) && openChangeName}
+                disabled={!(isNameValid)}
                 fullWidth
               >
                 Create playlist
@@ -158,19 +98,6 @@ export const UploadPlaylist = () => {
             )}
         </Box>
       </Paper>
-      <Snackbar
-        open={openSnackBar}
-        autoHideDuration={2000}
-        onClose={handleClose}
-        anchorOrigin={{vertical: 'bottom', horizontal: 'right'}}
-      >
-        <Alert
-          severity="success"
-          onClose={handleClose}
-        >
-          Success! Playlist was created!
-        </Alert>
-      </Snackbar>
     </Box>
   );
 };

+ 31 - 42
src/pages/UploadTracks/UploadTracks.jsx

@@ -1,45 +1,32 @@
-import React, { useCallback, useEffect, useState } from 'react';
+import React, { useCallback, useState } from 'react';
 import {
   Typography,
-  Box, Paper, Grid, Avatar, Alert, Snackbar,
+  Box, Paper, Grid, Avatar, Button,
 } from '@mui/material';
 import { useDispatch, useSelector } from 'react-redux';
 import { arrayMoveImmutable } from 'array-move';
 import AudioFileIcon from '@mui/icons-material/AudioFile';
-import { SortableList } from '../../components/Dropzone/SortableList';
 import { Dropzone } from '../../components/Dropzone/Dropzone';
-import { actionSetUploadTrack } from '../../store/types/uploadTypes';
 import './UploadTracks.scss';
+import { actionSetUploadTrack } from '../../store/types/uploadTypes';
 
 export const UploadTracks = () => {
   const dispatch = useDispatch();
-  const upload = useSelector(state => state?.upload.tracks);
+  const [files, setFiles] = useState([]);
 
-  const [uploadTracks, setUploadTracks] = useState([]);
-  const [openSnackBar, setOpenSnackBar] = useState(false);
-
-  useEffect(() => {
-    if (upload?.length !== 0) {
-      setUploadTracks(prevState => [
-        ...prevState,
-        {...upload},
-      ]);
-    }
-  }, [upload]);
+  const addTrack = () => {
+    dispatch(actionSetUploadTrack(files));
+  };
 
   const onDrop = useCallback(acceptedFiles => {
-    dispatch(actionSetUploadTrack(acceptedFiles[0]));
+    setFiles(oldFiles => ([
+      ...oldFiles,
+      ...acceptedFiles,
+    ]));
   }, []);
 
   const onSortEnd = ({oldIndex, newIndex}) => {
-    setUploadTracks(arrayMoveImmutable(uploadTracks, oldIndex, newIndex));
-  };
-
-  const handleClose = (event, reason) => {
-    if (reason === 'clickaway') {
-      return;
-    }
-    setOpenSnackBar(false);
+    setFiles(arrayMoveImmutable(files, oldIndex, newIndex));
   };
 
   return (
@@ -65,29 +52,31 @@ export const UploadTracks = () => {
             <Typography
               variant="h4"
             >
-              Add track
+              Add tracks
             </Typography>
           </Grid>
         </Grid>
         <Box>
-          <Dropzone onDrop={onDrop}/>
-          <SortableList tracks={uploadTracks} onSortEnd={onSortEnd}/>
-          <Typography>If you want to add track drag and drop an audio file</Typography>
+          <Dropzone onSortEnd={onSortEnd} multiple onDrop={onDrop} files={files}/>
+          {files.length === 0
+            ? (
+              <Typography>If you want to add tracks drag and drop an audio file</Typography>
+            ) : (
+              <Button
+                type="submit"
+                color="primary"
+                variant="contained"
+                sx={{
+                  margin: '20px 0',
+                }}
+                onClick={addTrack}
+                fullWidth
+              >
+                Upload tracks
+              </Button>
+            )}
         </Box>
       </Paper>
-      <Snackbar
-        open={openSnackBar}
-        autoHideDuration={2000}
-        onClose={handleClose}
-        anchorOrigin={{vertical: 'bottom', horizontal: 'right'}}
-      >
-        <Alert
-          severity="success"
-          onClose={handleClose}
-        >
-          Success! Track was added!
-        </Alert>
-      </Snackbar>
     </Box>
   );
 };

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

@@ -38,8 +38,6 @@ export function authReducer(state = initialState, action) {
     case types.SET_USER:
       return {
         ...state,
-        login: action.payload,
-        authToken: action.payload,
         user: action.payload,
       };
     case types.SET_NICK_SUCCESS:

+ 33 - 0
src/store/reducers/snackBarReducer.js

@@ -0,0 +1,33 @@
+import types from '../types/snackBarTypes';
+
+export const ALERT_TYPES = {
+  SUCCESS: 'success',
+  ERROR: 'error',
+  WARNING: 'warning',
+  INFO: 'info',
+};
+
+const initialState = {
+  isOpen: false,
+  type: '',
+  message: '',
+};
+
+export function snackBarReducer(state = initialState, action) {
+  switch (action.type) {
+    case types.SET_SNACK_BAR:
+      return {
+        ...state,
+        isOpen: true,
+        type: action.payload.type,
+        message: action.payload.message,
+      };
+    case types.RESET_SNACK_BAR:
+      return {
+        ...state,
+        isOpen: false,
+      };
+    default:
+      return state;
+  }
+}

+ 0 - 5
src/store/reducers/tracksReducer.js

@@ -1,5 +1,4 @@
 import types from '../types/trackTypes';
-import { BACKEND_URL } from '../../constants';
 
 const initialState = {
   trackList: [],
@@ -21,10 +20,6 @@ export function tracksReducer(state = initialState, action) {
         totalCount: action.payload.totalCount,
         isLoading: false,
       };
-    case types.FETCH_USER_TRACKS:
-      return {
-        ...state,
-      };
     case types.FETCH_USER_TRACKS_SUCCESS:
       return {
         ...state,

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

@@ -25,6 +25,11 @@ export function uploadReducer(state = initialState, action) {
         ...state,
         tracks: action.payload,
       };
+    case types.UPSERT_USER_TRACKS_SUCCESS:
+      return {
+        ...state,
+        tracks: action.payload,
+      };
     default:
       return state;
   }

+ 11 - 4
src/store/sagas/authSaga.js

@@ -1,5 +1,5 @@
 import {
-  call, put, takeLatest, select,
+  call, put, takeLatest, select, all,
 } from 'redux-saga/effects';
 import types, {
   actionLoginSuccess,
@@ -15,9 +15,11 @@ import {
 } from '../../api/auth';
 import { forwardToPage } from '../../utils/history';
 import { jwtDecode } from '../../utils/jwtDecode';
+import { actionSetSnackBar } from '../types/snackBarTypes';
+import { ALERT_TYPES } from '../reducers/snackBarReducer';
+import { ROUTES } from '../../constants';
 
 function* loginWorker(action) {
-  // const auth = yield select(state => state.auth.authToken);
   try {
     localStorage.removeItem('authToken');
     const authToken = yield call(login, action.payload);
@@ -32,7 +34,11 @@ function* loginWorker(action) {
     yield put(actionFindUserByIdSuccess(user));
     yield put(actionSetUser(user));
 
-    yield call(forwardToPage, '/');
+    yield call(forwardToPage, ROUTES.MAIN_PAGE);
+
+    if (user._id.length !== 0) {
+      yield put(actionSetSnackBar({type: ALERT_TYPES.SUCCESS, message: 'Success!'}));
+    }
   } catch (e) {
     yield put(actionLoginFail(e.message));
   }
@@ -43,8 +49,9 @@ function* registerWorker(action) {
     localStorage.removeItem('authToken');
     const userData = yield call(registration, action.payload);
     yield put(actionRegisterSuccess({authToken: null, login: userData.login}));
+
     if (userData._id.length !== 0) {
-      yield put(actionLogin({login: action.payload.login, password: action.payload.password}));
+      yield put(actionLogin({login: userData.login, password: action.payload.password}));
     }
   } catch (e) {
     yield put(actionRegisterFail(e.message));

+ 10 - 2
src/store/sagas/playerSaga.js

@@ -1,7 +1,8 @@
 import {
-  call, put, takeLatest, select,
+  put, takeLatest, select,
 } from 'redux-saga/effects';
-import types, { actionPause, actionPlay, actionSetPlayerState } from '../types/playerTypes';
+import types, { actionPlay, actionSetPlayerState } from '../types/playerTypes';
+import store from '../store';
 
 function* playWorker(action) {
   const {trackList, id} = action.payload;
@@ -11,6 +12,9 @@ function* playWorker(action) {
   if (id !== state.currentPlayingTrackId) {
     if (state.audio !== null) {
       state.audio.pause();
+      yield put(actionSetPlayerState({
+        duration: 0,
+      }));
     }
   } else {
     state.audio.play();
@@ -21,6 +25,10 @@ function* playWorker(action) {
   }
 
   const audio = new Audio(url);
+  audio.addEventListener('durationchange', (e) => {
+    store.dispatch(actionSetPlayerState({duration: e.target.duration}));
+  });
+
   audio.play();
 
   yield put(actionSetPlayerState({

+ 41 - 19
src/store/sagas/playlistsSaga.js

@@ -1,18 +1,29 @@
 import {
+  all,
   call, put, select, takeLatest,
 } from 'redux-saga/effects';
 
 import {
-  getSelectedPlaylist, getPlaylistsWithPage, getPlaylistsCount, createPlaylist, addTracksToPlaylist, getUserPlaylist,
+  getSelectedPlaylist,
+  getPlaylistsWithPage,
+  getPlaylistsCount,
+  createPlaylist,
+  addTracksToPlaylist,
+  getUserPlaylist,
+  getUserPlaylistsCount,
 } from '../../api/playlists';
 import types, {
   actionFetchOnePlaylistSuccess,
   actionFetchPlaylistsFail,
   actionFetchPlaylistsSuccess,
-  actionFetchOnePlaylistFail, actionCreatePlaylistByIdSuccess, actionFetchUserPlaylistsSuccess,
+  actionFetchOnePlaylistFail, actionFetchUserPlaylistsSuccess,
 } from '../types/playlistTypes';
-import { getUserTracks } from '../../api/tracks';
-import { actionFetchUserTracksSuccess } from '../types/trackTypes';
+import { getGqlForUpload } from '../../utils/getGqlForUpload';
+import { uploadTracks } from '../../api/upload';
+import { actionSetSnackBar } from '../types/snackBarTypes';
+import { ALERT_TYPES } from '../reducers/snackBarReducer';
+import { forwardToPage } from '../../utils/history';
+import { ROUTES } from '../../constants';
 
 function* getAllPlaylists(action) {
   try {
@@ -35,31 +46,42 @@ function* getOnePlaylist(action) {
 }
 
 function* createUserPlaylistWorker(action) {
-  console.log(action.payload);
-  const {playlistName} = action.payload;
-  const uploadTracks = yield select(state => state.upload.tracks);
+  const {playlistName, files} = action.payload;
+  const arrayId = [];
 
-  try {
-    const playlistId = yield call(createPlaylist, playlistName);
+  const playlistId = yield call(createPlaylist, playlistName);
 
-    console.log(playlistId._id, action.payload.uploadTracks);
-    const getPlaylist = yield call(addTracksToPlaylist, {playlistId: playlistId._id, arrayOfTracks: uploadTracks});
-    console.log(getPlaylist);
+  const tracks = yield all(files.map(file => {
+    const formData = new FormData();
+    formData.append('track', file);
+    return call(getGqlForUpload, {formData, fetchPart: 'track'});
+  }));
 
-    const userPlaylist = yield call(getSelectedPlaylist, playlistId._id);
-    yield put(actionCreatePlaylistByIdSuccess(userPlaylist));
-    console.log(userPlaylist);
-  } catch (e) {
-    e.message;
+  const allTracks = yield all(tracks.map(track => call(uploadTracks, track._id)));
+
+  allTracks.map(track => arrayId.push({ _id: track._id }));
+
+  const getPlaylistWithTracks = yield call(addTracksToPlaylist, {
+    playlistId: playlistId._id,
+    arrayOfTracks: arrayId,
+  });
+
+  if (Object.keys(getPlaylistWithTracks).length !== 0) {
+    yield put(actionSetSnackBar({type: ALERT_TYPES.SUCCESS, message: 'Success!'}));
+    yield call(forwardToPage, ROUTES.PLAYLISTS_PAGE);
   }
 }
 
 function* fetchUserPlaylistsWorker(action) {
   const page = action.payload;
   const userId = yield select(state => state?.auth?.user?._id);
-  const userPlaylists = yield call(getUserPlaylist, {userId, page});
 
-  yield put(actionFetchUserPlaylistsSuccess({userPlaylists, totalCount: userPlaylists.length}));
+  const playlistsCount = yield call(getUserPlaylistsCount, userId);
+
+  if (userId.length !== 0) {
+    const userPlaylists = yield call(getUserPlaylist, {userId, page});
+    yield put(actionFetchUserPlaylistsSuccess({userPlaylists, totalCount: playlistsCount}));
+  }
 }
 
 export function* playlistsSaga() {

+ 21 - 18
src/store/sagas/uploadSaga.js

@@ -1,39 +1,42 @@
 import {
+  all,
   call, put, select, takeLatest,
 } from 'redux-saga/effects';
 import types, {
-  actionSetUploadTrackSuccess,
 } from '../types/uploadTypes';
 import { getGqlForUpload } from '../../utils/getGqlForUpload';
 import { jwtDecode } from '../../utils/jwtDecode';
-import { setAvatar } from '../../api/upload';
+import { setAvatar, uploadTracks } from '../../api/upload';
 import { actionSetUser } from '../types/authTypes';
+import { actionSetSnackBar } from '../types/snackBarTypes';
+import { ALERT_TYPES } from '../reducers/snackBarReducer';
+import { forwardToPage } from '../../utils/history';
+import { ROUTES } from '../../constants';
 
 function* uploadFileWorker(action) {
   const auth = yield select(state => state.auth.authToken);
 
-  try {
-    const response = yield call(getGqlForUpload, {data: action.payload, formName: 'photo', fetchPart: 'upload'});
-    const avatarId = response._id;
+  const response = yield call(getGqlForUpload, {data: action.payload, formName: 'photo', fetchPart: 'upload'});
+  const avatarId = response._id;
 
-    const token = yield call(jwtDecode, auth);
-    const userId = token.sub.id;
+  const token = yield call(jwtDecode, auth);
+  const userId = token.sub.id;
+  const result = yield call(setAvatar, {userId, avatarId});
 
-    const result = yield call(setAvatar, {userId, avatarId});
-    console.log(result);
-    yield put(actionSetUser(result));
-  } catch (e) {
-    e.message;
-  }
+  yield put(actionSetUser(result));
 }
 
 function* uploadTrackWorker(action) {
-  try {
-    const response = yield call(getGqlForUpload, {data: action.payload, formName: 'track', fetchPart: 'track'});
+  const tracks = yield all(action.payload.map(file => {
+    const formData = new FormData();
+    formData.append('track', file);
+    return call(getGqlForUpload, {formData, fetchPart: 'track'});
+  }));
 
-    yield put(actionSetUploadTrackSuccess({...response, originalFileName: action.payload.name}));
-  } catch (e) {
-    e.message;
+  const getTracks = yield all(tracks.map(track => call(uploadTracks, track._id)));
+  if (getTracks.length !== 0) {
+    yield call(forwardToPage, ROUTES.MAIN_PAGE);
+    yield put(actionSetSnackBar({type: ALERT_TYPES.SUCCESS, message: 'Success!'}));
   }
 }
 

+ 9 - 5
src/store/store.js

@@ -1,5 +1,5 @@
 import {
-  combineReducers, createStore, compose, applyMiddleware,
+  combineReducers, createStore, applyMiddleware, compose,
 } from 'redux';
 import createSagaMiddleware from 'redux-saga';
 import { all } from 'redux-saga/effects';
@@ -14,6 +14,7 @@ import { playlistsSaga } from './sagas/playlistsSaga';
 import { uploadSaga } from './sagas/uploadSaga';
 import { uploadReducer } from './reducers/uploadReducer';
 import { loadState, saveState, stateToStorageSelector } from '../helpers';
+import { snackBarReducer } from './reducers/snackBarReducer';
 
 const sagaMiddleware = createSagaMiddleware();
 
@@ -23,6 +24,7 @@ const rootReducer = combineReducers({
   auth: authReducer,
   playlists: playlistsReducer,
   upload: uploadReducer,
+  snackBar: snackBarReducer,
 });
 
 function* rootSaga() {
@@ -35,13 +37,15 @@ function* rootSaga() {
   ]);
 }
 
+const middleware = [
+  applyMiddleware(sagaMiddleware),
+  ...(window.__REDUX_DEVTOOLS_EXTENSION__ ? [window.__REDUX_DEVTOOLS_EXTENSION__()] : []),
+];
+
 const store = createStore(
   rootReducer,
   loadState(),
-  compose(
-    applyMiddleware(sagaMiddleware),
-    window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__(),
-  ),
+  compose(...middleware),
 );
 
 store.subscribe(() => {

+ 3 - 0
src/store/types/playerTypes.js

@@ -7,6 +7,7 @@ const types = {
   PREVIOUS_TRACK: 'PREVIOUS_TRACK',
   NEXT_TRACK: 'NEXT_TRACK',
   CHANGE_TIME: 'CHANGE_TIME',
+  SET_DURATION: 'SET_DURATION',
 };
 
 export const actionPause = (payload) => ({type: types.PAUSE, payload});
@@ -15,6 +16,8 @@ export const actionSetPlayerState = (payload) => ({type: types.SET_PLAYER_STATE,
 
 export const actionPreviousTrack = (payload) => ({type: types.PREVIOUS_TRACK, payload});
 export const actionNextTrack = (payload) => ({type: types.NEXT_TRACK, payload});
+
 export const actionChangeTime = (payload) => ({type: types.CHANGE_TIME, payload});
+export const actionSetDuration = (payload) => ({type: types.SET_DURATION, payload});
 
 export default types;

+ 9 - 0
src/store/types/snackBarTypes.js

@@ -0,0 +1,9 @@
+const types = {
+  SET_SNACK_BAR: 'SET_SNACK_BAR',
+  RESET_SNACK_BAR: 'RESET_SNACK_BAR',
+};
+
+export const actionSetSnackBar = (payload) => ({type: types.SET_SNACK_BAR, payload});
+export const actionResetSnackBar = (payload) => ({type: types.RESET_SNACK_BAR, payload});
+
+export default types;

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

@@ -3,6 +3,8 @@ const types = {
   SET_UPLOAD_FILE_SUCCESS: 'SET_UPLOAD_FILE_SUCCESS',
   SET_UPLOAD_TRACK: 'SET_UPLOAD_TRACK',
   SET_UPLOAD_TRACK_SUCCESS: 'SET_UPLOAD_TRACK_SUCCESS',
+  UPSERT_USER_TRACKS: 'FETCH_USER_TRACKS',
+  UPSERT_USER_TRACKS_SUCCESS: 'UPSERT_USER_TRACKS_SUCCESS',
 };
 
 export const actionSetUploadFile = (payload) => ({type: types.SET_UPLOAD_FILE, payload});

+ 0 - 1
src/utils/getGql.js

@@ -1,4 +1,3 @@
-import { BACKEND_URL } from '../constants';
 import { buildUrl } from './buildUrl';
 
 export const getGql = (query, variables = {}) => fetch(buildUrl('graphql'), {

+ 8 - 15
src/utils/getGqlForUpload.js

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

+ 6 - 0
src/utils/regex.js

@@ -0,0 +1,6 @@
+import React from 'react';
+
+export const regex = (name) => {
+  const result = name.replace(/\.(mp3|mp4|music-2021.com)$/i, '');
+  return result.replace(/(\(.*?\))/g, '');
+};