Sfoglia il codice sorgente

added dropzone, added find by owner, fixed registration page, added buttons for changing pages

Alyona Brytvina 2 anni fa
parent
commit
b28d915c50

+ 49 - 0
src/api/playlists.js

@@ -67,3 +67,52 @@ export const getPlaylistsCount = () => {
     }]),
   });
 };
+
+export const createPlaylist = (playlistName) => {
+  const gql = getGql(`${BACKEND_URL}/graphql`);
+
+  return gql(`
+      mutation createPlaylist{
+          PlaylistUpsert(playlist: {name: "${playlistName}"}){
+              _id
+          }
+      }
+  `);
+};
+
+export const addTracksToPlaylist = ({playlistId, arrayOfTracks}) => {
+  const gql = getGql(`${BACKEND_URL}/graphql`);
+
+  return gql(`
+      mutation createPlaylist{
+          PlaylistUpsert(playlist: {
+            _id: "${playlistId}"
+             tracks:{
+                 _id: "${arrayOfTracks._id}"
+             }
+          }){
+           _id tracks{
+              _id 
+           }
+         }
+      }
+`);
+};
+
+export const getUserPlaylist = (userId) => {
+  const gql = getGql(`${BACKEND_URL}/graphql`);
+
+  return gql(`
+      query findUserPlaylists($query: String){
+          PlaylistFind(query:$query){
+          _id name description
+      }
+    }
+  `, {
+    query: JSON.stringify([{
+      ___owner: userId,
+      name: {$exists: true, $ne: ''},
+      tracks: {$exists: true, $ne: []},
+    }]),
+  });
+};

+ 6 - 2
src/api/tracks.js

@@ -25,7 +25,7 @@ export const getTracksWithPage = (page = 1) => {
     })));
 };
 
-export const getMyTracks = (userId) => {
+export const getUserTracks = (userId) => {
   const gql = getGql(`${BACKEND_URL}/graphql`);
   return gql(`
       query findMyTracks($query: String){
@@ -33,5 +33,9 @@ export const getMyTracks = (userId) => {
                  _id url originalFileName
             }
         }
-  `, {query: JSON.stringify([{ ___owner: userId }])});
+  `, {query: JSON.stringify([{ ___owner: userId }])})
+    .then(data => data.map(track => ({
+      ...track,
+      url: `${BACKEND_URL}/${track.url}`,
+    })));
 };

BIN
src/assets/jpg/vinylPlate.jpg


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

@@ -12,11 +12,11 @@ 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';
-import { UserTracks } from '../../pages/UserTracks/UserTracks';
+import { UploadPlaylist } from '../../pages/UploadPlaylist/UploadPlaylist';
+import { UploadTracks } from '../../pages/UploadTracks/UploadTracks';
 
 export const App = () => (
   <ThemeProvider theme={theme}>
@@ -24,11 +24,18 @@ export const App = () => (
       <Router history={history}>
         <Header/>
         <Switch>
+          {/* TODO: move all routes to constants and reuse it at other places like
+            const ROUTES = {
+              MAIN_PAGE: '/',
+              PLAYLISTS_PAGE: '/playlists',
+            };
+          */}
           <PrivateRoute exact path="/" component={MainPage}/>
           <PrivateRoute exact path="/playlists" component={PlaylistsPage}/>
           <PrivateRoute exact path="/selectedPlaylist/:id" component={SelectedPlaylistPage}/>
-          <PrivateRoute exact path="/userTracks" component={UserTracks}/>
+          <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}/>

+ 45 - 0
src/components/Dropzone/Dropzone.js

@@ -0,0 +1,45 @@
+import { Box, Typography } from '@mui/material';
+import UploadIcon from '@mui/icons-material/Upload';
+import React from 'react';
+import { useDropzone } from 'react-dropzone';
+
+export const Dropzone = ({ onDrop, accept }) => {
+  const { getRootProps, getInputProps, isDragActive } = useDropzone({
+    onDrop,
+    // accept,
+  });
+
+  return (
+    <Box
+      sx={{
+        mt: '50px',
+        height: '50vh',
+        display: 'flex',
+        alignItems: 'center',
+        justifyContent: 'center',
+        flexDirection: 'column',
+        border: '1px dashed #9c27b0',
+      }}
+      {...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>
+    </Box>
+  );
+};

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

@@ -0,0 +1,30 @@
+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>
+));

+ 2 - 2
src/components/GenerateGradient/GenerateGradient.jsx

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

+ 0 - 8
src/components/Header/Header.jsx

@@ -24,14 +24,6 @@ export const Header = () => {
           <Link to="/playlists">
             <Button variant="secondary">Playlists</Button>
           </Link>
-          <Link to="/uploadTracks">
-            <IconButton
-              size="large"
-              color="inherit"
-            >
-              <UploadIcon/>
-            </IconButton>
-          </Link>
           <Link
             to="/profile"
           >

+ 25 - 51
src/components/TrackList/TrackList.jsx

@@ -1,38 +1,17 @@
-import React, { useEffect } from 'react';
+import React from 'react';
 import {
   IconButton, List, ListItem, Typography,
   Box, CircularProgress,
 } from '@mui/material';
-import {
-  PauseRounded, PlayArrowRounded,
-} from '@mui/icons-material';
+import { PauseRounded, PlayArrowRounded } from '@mui/icons-material';
 import { useDispatch, useSelector } from 'react-redux';
-import {
-  actionPause, actionPlay,
-} from '../../store/types/playerTypes';
+import { actionPause, actionPlay } from '../../store/types/playerTypes';
 
 export const TrackList = ({tracks, isLoading}) => {
   const dispatch = useDispatch();
   const playerState = useSelector(state => state.player);
 
-  useEffect(() => {
-    if (playerState.trackList.length === 0) {
-      return;
-    }
-    if (playerState.isPlaying) {
-      if (playerState.audio === null) {
-        const {url} = playerState.trackList.find(track => track._id === playerState.currentPlayingTrackId);
-        const audio = new Audio(url);
-        audio.play();
-      } else {
-        playerState.audio.play();
-      }
-    } else {
-      playerState.audio.pause();
-    }
-  }, [playerState.isPlaying]);
-
-  const togglePlayPause = (id) => {
+  const togglePlayPause = id => {
     if (playerState.isPlaying) {
       playerState.audio.pause();
       if (playerState.currentPlayingTrackId !== id) {
@@ -45,30 +24,25 @@ export const TrackList = ({tracks, isLoading}) => {
     }
   };
 
-  return isLoading
-    ? (
-      <CircularProgress/>
-    ) : (
-      <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>
-      </Box>
-    );
+  return isLoading ? (
+    <CircularProgress/>
+  ) : (
+    <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>
+    </Box>
+  );
 };

+ 1 - 2
src/pages/LoginPage/LoginPage.jsx

@@ -12,11 +12,10 @@ import {
 } from '@mui/material';
 import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
 import './LoginPage.scss';
-import { Link, useHistory } from 'react-router-dom';
+import { Link } 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 PriorityHighIcon from '@mui/icons-material/PriorityHigh';
 import { actionLogin } from '../../store/types/authTypes';
 
 export const LoginPage = () => {

+ 43 - 2
src/pages/MainPage/MainPage.jsx

@@ -1,8 +1,11 @@
 import React, { useEffect, useState } from 'react';
 import { useDispatch, useSelector } from 'react-redux';
-import { Box, Pagination, Stack } from '@mui/material';
+import {
+  Box, Button, ButtonGroup, Pagination, Stack,
+} from '@mui/material';
 import { TrackList } from '../../components/TrackList/TrackList';
-import { actionFetchTracks } from '../../store/types/trackTypes';
+import { actionFetchTracks, actionFetchUserTracks } from '../../store/types/trackTypes';
+import { forwardToUploadPage } from '../../utils/history';
 
 export const MainPage = () => {
   const dispatch = useDispatch();
@@ -17,8 +20,46 @@ export const MainPage = () => {
     setPage(value);
   };
 
+  const showMyTracks = () => {
+    dispatch(actionFetchUserTracks());
+  };
+
+  const showAllTracks = () => {
+    dispatch(actionFetchTracks(page));
+  };
+
   return (
     <Box>
+      <ButtonGroup
+        sx={{
+          mt: '10px',
+          display: 'flex',
+          justifyContent: 'space-evenly',
+          alignItems: 'center',
+        }}
+      >
+        <Box>
+          <Button
+            onClick={() => forwardToUploadPage('/uploadTracks')}
+            variant="outlined"
+          >
+            Upload tracks
+          </Button>
+        </Box>
+        <Box>
+          <Button onClick={showAllTracks}>
+            Tracks
+          </Button>
+          <Button onClick={showMyTracks}>
+            My tracks
+          </Button>
+        </Box>
+        <Button
+          variant="outlined"
+        >
+          {`Total: ${tracksState.totalCount}`}
+        </Button>
+      </ButtonGroup>
       <TrackList
         tracks={tracksState.trackList}
         isLoading={tracksState.isLoading}

+ 42 - 4
src/pages/PlaylistsPage/PlaylistsPage.jsx

@@ -1,12 +1,13 @@
 import React, { useEffect, useState } from 'react';
 import {
-  Box,
+  Box, Button, ButtonGroup, CircularProgress,
   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 { actionFetchPlaylists, actionFetchUserPlaylists } from '../../store/types/playlistTypes';
 import { GenerateGradient } from '../../components/GenerateGradient/GenerateGradient';
+import { forwardToCreatePlaylistPage } from '../../utils/history';
 
 export const PlaylistsPage = () => {
   const dispatch = useDispatch();
@@ -22,12 +23,49 @@ export const PlaylistsPage = () => {
     setPage(value);
   };
 
+  const showUserPlaylists = () => {
+    dispatch(actionFetchUserPlaylists());
+  };
+
+  const showAllPlaylists = () => {
+    dispatch(actionFetchPlaylists(page));
+  };
+
   return (
     <Box>
+      <ButtonGroup
+        sx={{
+          mt: '10px',
+          display: 'flex',
+          justifyContent: 'space-evenly',
+          alignItems: 'center',
+        }}
+      >
+        <Box onClick={() => forwardToCreatePlaylistPage('/uploadPlaylist')}>
+          <Button variant="outlined">
+            Create playlist
+          </Button>
+        </Box>
+        <Box>
+          <Button onClick={showAllPlaylists}>
+            Playlists
+          </Button>
+          <Button onClick={showUserPlaylists}>
+            My playlists
+          </Button>
+        </Box>
+        <Box>
+          <Button
+            variant="outlined"
+          >
+            {`Total:${state.totalCount}`}
+          </Button>
+        </Box>
+      </ButtonGroup>
       <Grid
         spacing={3}
         columns={12}
-        sx={{margin: '0'}}
+        sx={{margin: '0', width: '100%'}}
         container
       >
         {playlists.map(playlist => (
@@ -57,7 +95,7 @@ export const PlaylistsPage = () => {
                     textAlign: 'center',
                   }}
                 >
-                  <GenerateGradient />
+                  <GenerateGradient/>
                 </Box>
                 <Box
                   sx={{

+ 1 - 1
src/pages/ProfilePage/ProfilePage.jsx

@@ -111,7 +111,6 @@ export const ProfilePage = () => {
                   className="avatar"
                   onMouseEnter={() => {
                     setIsHovered(!isHovered);
-                    console.log(isHovered);
                   }}
                   sx={{width: 100, height: 100}}
                   src={
@@ -168,6 +167,7 @@ export const ProfilePage = () => {
                   <EditIcon
                     fontSize="medium"
                     color="primary"
+                    cursor="pointer"
                     onClick={() => setOpenNick(!openNick)}
                   />
                 </Grid>

+ 176 - 96
src/pages/UploadPlaylist/UploadPlaylist.jsx

@@ -1,96 +1,176 @@
-// import React, { useEffect, useState } from 'react';
-// import {
-//   IconButton, List, ListItem, Typography,
-//   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 { setQueTracks } from '../../store/types/playlistTypes';
-//
-// export const TrackList = ({tracks, isLoading}) => {
-//   const dispatch = useDispatch();
-//   const playerState = useSelector(state => state.player);
-//   const [currentTracks, setCurrentTracks] = useState(tracks);
-//   console.log(tracks, isLoading);
-//
-//   useEffect(() => {
-//     if (playerState.trackList.length === 0) {
-//       return;
-//     }
-//     if (playerState.isPlaying) {
-//       if (playerState.audio === null) {
-//         const {url} = playerState.trackList.find(track => track._id === playerState.currentPlayingTrackId);
-//         const audio = new Audio(url);
-//         audio.play();
-//       } else {
-//         playerState.audio.play();
-//       }
-//     } else {
-//       playerState.audio.pause();
-//     }
-//   }, [playerState.isPlaying]);
-//
-//   useEffect(() => {
-//     dispatch(setQueTracks(currentTracks));
-//   }, [currentTracks]);
-//
-//   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>
-//       {tracks.map((track, index) => (
-//         <SortableItem key={`item-${track._id}`} index={index} track={track}/>
-//       ))}
-//     </List>
-//   ));
-//
-//   const onSortEnd = ({oldIndex, newIndex}) => {
-//     setCurrentTracks(arrayMoveImmutable(tracks, oldIndex, newIndex));
-//   };
-//
-//   const togglePlayPause = (id) => {
-//     if (playerState.isPlaying) {
-//       playerState.audio.pause();
-//       if (playerState.currentPlayingTrackId !== id) {
-//         dispatch(actionPlay({trackList: tracks, id}));
-//       } else {
-//         dispatch(actionPause());
-//       }
-//     } else {
-//       dispatch(actionPlay({trackList: tracks, id}));
-//     }
-//   };
-//
-//   return isLoading
-//     ? (
-//       <CircularProgress/>
-//     )
-//     : (
-//       <Box sx={{
-//         minHeight: '70vh',
-//       }}
-//       >
-//         <SortableList onSortEnd={onSortEnd} distance={1}/>
-//       </Box>
-//     );
-// };
+import React, { useCallback, useEffect, useState } from 'react';
+import {
+  Typography,
+  Box, Paper, Grid, Button, Avatar, FormControl, InputLabel, Input, InputAdornment, Alert, Snackbar,
+} 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 [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]));
+  }, []);
+
+  const onSortEnd = ({oldIndex, newIndex}) => {
+    setUploadTracks(arrayMoveImmutable(uploadTracks, oldIndex, newIndex));
+  };
+
+  const onChangePlaylistName = (e) => {
+    setPlaylistName(e.target.value);
+  };
+
+  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);
+  };
+
+  return (
+    <Box>
+      <Paper elevation={10} className="paperStyleForPlaylist">
+        <Grid
+          alignItems="center"
+          justifyContent="center"
+          flexDirection="column"
+          container
+        >
+          <Grid item>
+            <Avatar sx={{mb: '15px', backgroundColor: '#9c27b0'}}>
+              <AudioFileIcon color="white"/>
+            </Avatar>
+          </Grid>
+          <Grid
+            sx={{
+              mb: '20px',
+            }}
+            item
+          >
+            <Typography
+              variant="h4"
+            >
+              Create playlist
+            </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',
+            }}
+            >
+              <Typography
+                variant="subtitle1"
+                sx={{
+                  mr: '5px',
+                }}
+              >
+                {playlistName}
+              </Typography>
+              <EditIcon
+                fontSize="medium"
+                color="primary"
+                cursor="pointer"
+                onClick={() => setOpenChangeName(!openChangeName)}
+              />
+            </Box>
+          )}
+        <Box>
+          <Dropzone onDrop={onDrop}/>
+          <SortableList tracks={uploadTracks} onSortEnd={onSortEnd}/>
+          {upload.length === 0
+            ? (
+              <Typography>If you want to create a playlist drag and drop an audio file</Typography>
+            ) : (
+              <Button
+                type="submit"
+                color="primary"
+                variant="contained"
+                sx={{
+                  margin: '20px 0',
+                }}
+                onClick={createPlaylist}
+                disabled={!(isNameValid) && openChangeName}
+                fullWidth
+              >
+                Create playlist
+              </Button>
+            )}
+        </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>
+  );
+};

+ 10 - 0
src/pages/UploadPlaylist/UploadPlaylist.scss

@@ -0,0 +1,10 @@
+.paperStyleForPlaylist{
+  padding: 20px;
+  width: 400px;
+  margin: 20px auto;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  text-align: center;
+}

+ 68 - 50
src/pages/UploadTracks/UploadTracks.jsx

@@ -1,75 +1,93 @@
-import React, { useCallback } from 'react';
+import React, { useCallback, useEffect, useState } from 'react';
 import {
-  Box, Button, Grid, Paper, Typography,
+  Typography,
+  Box, Paper, Grid, Avatar, Alert, Snackbar,
 } from '@mui/material';
 import { useDispatch, useSelector } from 'react-redux';
-import { useDropzone } from 'react-dropzone';
-import UploadIcon from '@mui/icons-material/Upload';
-import { Link } from 'react-router-dom';
+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';
 
 export const UploadTracks = () => {
   const dispatch = useDispatch();
+  const upload = useSelector(state => state?.upload.tracks);
+
+  const [uploadTracks, setUploadTracks] = useState([]);
+  const [openSnackBar, setOpenSnackBar] = useState(false);
+
+  useEffect(() => {
+    if (upload?.length !== 0) {
+      setUploadTracks(prevState => [
+        ...prevState,
+        {...upload},
+      ]);
+    }
+  }, [upload]);
+
   const onDrop = useCallback(acceptedFiles => {
-    console.log(acceptedFiles);
     dispatch(actionSetUploadTrack(acceptedFiles[0]));
   }, []);
-  const {getRootProps, getInputProps} = useDropzone({onDrop});
+
+  const onSortEnd = ({oldIndex, newIndex}) => {
+    setUploadTracks(arrayMoveImmutable(uploadTracks, oldIndex, newIndex));
+  };
+
+  const handleClose = (event, reason) => {
+    if (reason === 'clickaway') {
+      return;
+    }
+    setOpenSnackBar(false);
+  };
 
   return (
-    <Box
-      sx={{
-        m: '10px 0',
-        display: 'flex',
-        alignItems: 'center',
-        justifyContent: 'center',
-        flexDirection: 'column',
-        height: '65vh',
-      }}
-    >
-      <Paper
-        elevation={10}
-        sx={{
-          height: '100%',
-          width: '100vh',
-        }}
-      >
+    <Box>
+      <Paper elevation={10} className="paperStyleForPlaylist">
         <Grid
+          alignItems="center"
+          justifyContent="center"
+          flexDirection="column"
           container
         >
-          <Box
+          <Grid item>
+            <Avatar sx={{mb: '15px', backgroundColor: '#9c27b0'}}>
+              <AudioFileIcon color="white"/>
+            </Avatar>
+          </Grid>
+          <Grid
             sx={{
-              width: '100%',
-              height: '50vh',
-              display: 'flex',
-              alignItems: 'center',
-              justifyContent: 'center',
-              flexDirection: 'column',
+              mb: '20px',
             }}
-            {...getRootProps()}
+            item
           >
-            <UploadIcon
-              color="primary"
-              sx={{
-                height: '40px',
-                width: '40px',
-                mb: '20px',
-              }}
-            />
-            <input {...getInputProps()} />
-            <Typography>
-              Drop file to upload
+            <Typography
+              variant="h4"
+            >
+              Add track
             </Typography>
-          </Box>
+          </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>
+        </Box>
       </Paper>
-      <Link
-        to="/userTracks"
+      <Snackbar
+        open={openSnackBar}
+        autoHideDuration={2000}
+        onClose={handleClose}
+        anchorOrigin={{vertical: 'bottom', horizontal: 'right'}}
       >
-        <Button>
-          My uploaded tracks
-        </Button>
-      </Link>
+        <Alert
+          severity="success"
+          onClose={handleClose}
+        >
+          Success! Track was added!
+        </Alert>
+      </Snackbar>
     </Box>
   );
 };

+ 10 - 0
src/pages/UploadTracks/UploadTracks.scss

@@ -0,0 +1,10 @@
+.paperStyleForPlaylist{
+  padding: 20px;
+  width: 400px;
+  margin: 20px auto;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  text-align: center;
+}

+ 0 - 24
src/pages/UserTracks/UserTracks.jsx

@@ -1,24 +0,0 @@
-import { Box } from '@mui/material';
-import React, { useEffect } from 'react';
-import { useDispatch, useSelector } from 'react-redux';
-import { TrackList } from '../../components/TrackList/TrackList';
-import { actionFetchUserTracks } from '../../store/types/trackTypes';
-
-export const UserTracks = () => {
-  const dispatch = useDispatch();
-  const tracks = useSelector(state => state?.tracks);
-
-  console.log(tracks);
-
-  useEffect(() => {
-    dispatch(actionFetchUserTracks());
-  }, []);
-
-  return (
-    <Box>
-      <TrackList
-        tracks={tracks ?? []}
-      />
-    </Box>
-  );
-};

+ 21 - 1
src/store/reducers/playlistsReducer.js

@@ -32,7 +32,6 @@ export function playlistsReducer(state = initialState, action) {
         isLoading: true,
       };
     case types.FETCH_ONE_PLAYLIST_SUCCESS:
-      console.log(action.payload);
       return {
         ...state,
         isLoading: false,
@@ -42,6 +41,27 @@ export function playlistsReducer(state = initialState, action) {
       return {
         ...state,
       };
+    case types.CREATE_PLAYLIST:
+      return {
+        ...state,
+      };
+    case types.CREATE_PLAYLIST_BY_ID_SUCCESS:
+      return {
+        ...state,
+        selectedPlaylist: action.payload,
+      };
+    case types.FETCH_USER_PLAYLISTS:
+      return {
+        ...state,
+      };
+    case types.FETCH_USER_PLAYLISTS_SUCCESS:
+      console.log(action.payload);
+      return {
+        ...state,
+        playlists: action.payload.userPlaylists,
+        totalCount: action.payload.totalCount,
+        isLoading: false,
+      };
     default:
       return state;
   }

+ 4 - 2
src/store/reducers/tracksReducer.js

@@ -1,10 +1,10 @@
 import types from '../types/trackTypes';
+import { BACKEND_URL } from '../../constants';
 
 const initialState = {
   trackList: [],
   totalCount: 0,
   isLoading: false,
-  userTracks: [],
 };
 
 export function tracksReducer(state = initialState, action) {
@@ -28,7 +28,9 @@ export function tracksReducer(state = initialState, action) {
     case types.FETCH_USER_TRACKS_SUCCESS:
       return {
         ...state,
-        myTracks: action.payload,
+        trackList: action.payload.userTracks,
+        totalCount: action.payload.totalCount,
+        isLoading: false,
       };
     default:
       return state;

+ 3 - 3
src/store/reducers/uploadReducer.js

@@ -1,8 +1,8 @@
 import types from '../types/uploadTypes';
 
 const initialState = {
-  file: null,
-  track: null,
+  file: [],
+  tracks: [],
 };
 
 export function uploadReducer(state = initialState, action) {
@@ -23,7 +23,7 @@ export function uploadReducer(state = initialState, action) {
     case types.SET_UPLOAD_TRACK_SUCCESS:
       return {
         ...state,
-        track: action.payload,
+        tracks: action.payload,
       };
     default:
       return state;

+ 5 - 5
src/store/sagas/authSaga.js

@@ -17,13 +17,14 @@ import { forwardToMainPage } from '../../utils/history';
 import { jwtDecode } from '../../utils/jwtDecode';
 
 function* loginWorker(action) {
-  const auth = yield select(state => state.auth.authToken);
+  // 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 token = yield call(jwtDecode, authToken);
     const {id} = token.sub;
 
     const user = yield call(findUserById, id);
@@ -43,7 +44,7 @@ function* registerWorker(action) {
     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}));
+      yield put(actionLogin({login: action.payload.login, password: action.payload.password}));
     }
   } catch (e) {
     yield put(actionRegisterFail(e.message));
@@ -61,10 +62,9 @@ function* findUserWorker(action) {
 
 function* setNickWorker(action) {
   const userId = yield select(state => state.auth.user._id);
-  console.log(action.payload, userId);
+
   try {
     const response = yield call(setNick, {id: userId, nick: action.payload });
-    console.log(response);
     yield put(actionSetNickSuccess(response));
     yield put(actionSetUser(response));
   } catch (e) {

+ 31 - 8
src/store/sagas/playlistsSaga.js

@@ -1,18 +1,18 @@
 import {
-  call, put, takeLatest,
+  call, put, select, takeLatest,
 } from 'redux-saga/effects';
 
 import {
-  getSelectedPlaylist, getPlaylistsWithPage, getPlaylistsCount,
+  getSelectedPlaylist, getPlaylistsWithPage, getPlaylistsCount, createPlaylist, addTracksToPlaylist, getUserPlaylist,
 } from '../../api/playlists';
 import types, {
   actionFetchOnePlaylistSuccess,
   actionFetchPlaylistsFail,
   actionFetchPlaylistsSuccess,
-  actionFetchOnePlaylistFail,
+  actionFetchOnePlaylistFail, actionCreatePlaylistByIdSuccess, actionFetchUserPlaylistsSuccess,
 } from '../types/playlistTypes';
-import { getTracksCount } from '../../api/tracks';
-import { actionFetchTracksSuccess } from '../types/trackTypes';
+import { getUserTracks } from '../../api/tracks';
+import { actionFetchUserTracksSuccess } from '../types/trackTypes';
 
 function* getAllPlaylists(action) {
   try {
@@ -34,13 +34,36 @@ function* getOnePlaylist(action) {
   }
 }
 
-function* setQueTracksWorker(action) {
+function* createUserPlaylistWorker(action) {
   console.log(action.payload);
-  yield put(actionFetchOnePlaylistSuccess(action.payload));
+  const {playlistName} = action.payload;
+  const uploadTracks = yield select(state => state.upload.tracks);
+
+  try {
+    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 userPlaylist = yield call(getSelectedPlaylist, playlistId._id);
+    yield put(actionCreatePlaylistByIdSuccess(userPlaylist));
+    console.log(userPlaylist);
+  } catch (e) {
+    e.message;
+  }
+}
+
+function* fetchUserPlaylistsWorker(action) {
+  const userId = yield select(state => state?.auth?.user?._id);
+  const userPlaylists = yield call(getUserPlaylist, userId);
+
+  yield put(actionFetchUserPlaylistsSuccess({userPlaylists, totalCount: userPlaylists.length}));
 }
 
 export function* playlistsSaga() {
   yield takeLatest(types.FETCH_PLAYLISTS, getAllPlaylists);
   yield takeLatest(types.FETCH_ONE_PLAYLIST, getOnePlaylist);
-  yield takeLatest(types.SET_QUE_TRACKS, setQueTracksWorker);
+  yield takeLatest(types.CREATE_PLAYLIST, createUserPlaylistWorker);
+  yield takeLatest(types.FETCH_USER_PLAYLISTS, fetchUserPlaylistsWorker);
 }

+ 9 - 8
src/store/sagas/tracksSaga.js

@@ -6,7 +6,9 @@ import types, {
   actionFetchTracksFail,
   actionFetchTracksSuccess,
 } from '../types/trackTypes';
-import { getMyTracks, getTracksCount, getTracksWithPage } from '../../api/tracks';
+import {
+  getTracksCount, getTracksWithPage, getUserTracks,
+} from '../../api/tracks';
 
 function* fetchTracksWorker(action) {
   try {
@@ -19,15 +21,14 @@ function* fetchTracksWorker(action) {
   }
 }
 
-function* fetchMyTracksWorker(action) {
-  const userId = yield select(state => state.auth.user._id);
-  console.log(action.payload, userId);
-  const myTracks = yield call(getMyTracks, userId);
-  console.log(myTracks);
-  yield put(actionFetchUserTracksSuccess(myTracks));
+function* fetchUserTracksWorker(action) {
+  const userId = yield select(state => state?.auth?.user?._id);
+  const userTracks = yield call(getUserTracks, userId);
+
+  yield put(actionFetchUserTracksSuccess({userTracks, totalCount: userTracks.length}));
 }
 
 export function* tracksSaga() {
   yield takeLatest(types.FETCH_TRACKS, fetchTracksWorker);
-  yield takeLatest(types.FETCH_USER_TRACKS, fetchMyTracksWorker);
+  yield takeLatest(types.FETCH_USER_TRACKS, fetchUserTracksWorker);
 }

+ 9 - 4
src/store/sagas/uploadSaga.js

@@ -1,10 +1,12 @@
 import {
   call, put, select, takeLatest,
 } from 'redux-saga/effects';
-import types, { actionSetUploadFileSuccess } from '../types/uploadTypes';
+import types, {
+  actionSetUploadTrackSuccess,
+} from '../types/uploadTypes';
 import { getGqlForUpload, getGqlForUploadTracks } from '../../utils/getGqlForUpload';
 import { jwtDecode } from '../../utils/jwtDecode';
-import { setAvatar, uploadTracks } from '../../api/upload';
+import { setAvatar } from '../../api/upload';
 import { actionSetUser } from '../types/authTypes';
 
 function* uploadFileWorker(action) {
@@ -28,9 +30,12 @@ function* uploadTrackWorker(action) {
   try {
     const response = yield call(getGqlForUploadTracks, action.payload);
     const trackId = response._id;
+    console.log(response);
 
-    const result = yield call(uploadTracks, trackId);
-    yield put(actionSetUploadFileSuccess(result));
+    yield put(actionSetUploadTrackSuccess({...response, originalFileName: action.payload.name}));
+
+    // const result = yield call(uploadTracks, trackId);
+    // console.log(result);
   } catch (e) {
     e.message;
   }

+ 12 - 2
src/store/types/playlistTypes.js

@@ -5,7 +5,12 @@ const types = {
   FETCH_ONE_PLAYLIST: 'FETCH_ONE_PLAYLIST',
   FETCH_ONE_PLAYLIST_SUCCESS: 'FETCH_ONE_PLAYLIST_SUCCESS',
   FETCH_ONE_PLAYLIST_FAIL: 'FETCH_ONE_PLAYLIST_FAIL',
-  SET_QUE_TRACKS: 'SET_QUE_TRACKS',
+  CREATE_PLAYLIST: 'CREATE_PLAYLIST',
+  ADD_TRACKS_TO_PLAYLIST: 'ADD_TRACKS_TO_PLAYLIST',
+  CREATE_PLAYLIST_BY_ID_SUCCESS: 'CREATE_PLAYLIST_BY_ID_SUCCESS',
+  CREATE_PLAYLIST_WITH_TRACKS_SUCCESS: 'CREATE_PLAYLIST_WITH_TRACKS_SUCCESS',
+  FETCH_USER_PLAYLISTS: 'FETCH_USER_PLAYLISTS',
+  FETCH_USER_PLAYLISTS_SUCCESS: 'FETCH_USER_PLAYLISTS_SUCCESS',
 };
 
 export const actionFetchPlaylists = (payload) => ({type: types.FETCH_PLAYLISTS, payload});
@@ -16,6 +21,11 @@ export const actionFetchOnePlaylist = (payload) => ({type: types.FETCH_ONE_PLAYL
 export const actionFetchOnePlaylistSuccess = (payload) => ({type: types.FETCH_ONE_PLAYLIST_SUCCESS, payload});
 export const actionFetchOnePlaylistFail = (payload) => ({type: types.FETCH_ONE_PLAYLIST_FAIL, payload});
 
-export const setQueTracks = (payload) => ({type: types.SET_QUE_TRACKS, payload});
+export const actionCreatePlaylist = (payload) => ({type: types.CREATE_PLAYLIST, payload});
+export const actionCreatePlaylistByIdSuccess = (payload) => ({type: types.CREATE_PLAYLIST_BY_ID_SUCCESS, payload});
+export const actionCreatePlaylistWithTracksSuccess = (payload) => ({type: types.CREATE_PLAYLIST_WITH_TRACKS_SUCCESS, payload});
+
+export const actionFetchUserPlaylists = (payload) => ({type: types.FETCH_USER_PLAYLISTS, payload});
+export const actionFetchUserPlaylistsSuccess = (payload) => ({type: types.FETCH_USER_PLAYLISTS_SUCCESS, payload});
 
 export default types;

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

@@ -2,10 +2,13 @@ const types = {
   SET_UPLOAD_FILE: 'SET_UPLOAD_FILE',
   SET_UPLOAD_FILE_SUCCESS: 'SET_UPLOAD_FILE_SUCCESS',
   SET_UPLOAD_TRACK: 'SET_UPLOAD_TRACK',
+  SET_UPLOAD_TRACK_SUCCESS: 'SET_UPLOAD_TRACK_SUCCESS',
 };
 
 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 const actionSetUploadTrackSuccess = (payload) => ({type: types.SET_UPLOAD_TRACK_SUCCESS, payload});
 
 export default types;

+ 12 - 0
src/utils/history.js

@@ -5,3 +5,15 @@ export const forwardToMainPage = (location) => {
     history.push(location);
   }
 };
+
+export const forwardToUploadPage = (location) => {
+  if (localStorage.getItem('authToken')) {
+    history.push(location);
+  }
+};
+
+export const forwardToCreatePlaylistPage = (location) => {
+  if (localStorage.getItem('authToken')) {
+    history.push(location);
+  }
+};