Selaa lähdekoodia

added profile page with editing avatar and nick name, add validation for login and register

Alyona Brytvina 2 vuotta sitten
vanhempi
commit
d4c8025026

+ 1 - 1
.gitignore

@@ -1,7 +1,7 @@
 # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
 
 # dependencies
-.idea
+/.idea
 /node_modules
 /.pnp
 .pnp.js

+ 16 - 0
src/api/auth.js

@@ -44,3 +44,19 @@ export const findUserById = (_id) => {
   }
      `, {id: JSON.stringify([{_id}])});
 };
+
+export const setNick = ({id, nick}) => {
+  const gql = getGql(`${BACKEND_URL}/graphql`);
+  console.log(id, nick);
+  return gql(`
+      mutation setNick{
+          UserUpsert(user: {
+             _id: "${id}", nick: "${nick}",
+             }){
+          _id, login, nick, createdAt, avatar {
+            _id, url
+            }
+          }
+      }
+  `);
+};

+ 11 - 0
src/api/tracks.js

@@ -24,3 +24,14 @@ export const getTracksWithPage = (page = 1) => {
       url: `${BACKEND_URL}/${track.url}`,
     })));
 };
+
+export const getMyTracks = (userId) => {
+  const gql = getGql(`${BACKEND_URL}/graphql`);
+  return gql(`
+      query findMyTracks($query: String){
+            TrackFind(query: $query){
+                 _id url originalFileName
+            }
+        }
+  `, {query: JSON.stringify([{ ___owner: userId }])});
+};

+ 2 - 0
src/components/App/App.jsx

@@ -16,6 +16,7 @@ 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';
 
 export const App = () => (
   <ThemeProvider theme={theme}>
@@ -26,6 +27,7 @@ export const App = () => (
           <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="/uploadTracks" component={UploadTracks}/>
           <PrivateRoute exact path="/profile" component={ProfilePage}/>
           <Route exact path="/login" component={LoginPage}/>

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

@@ -121,9 +121,9 @@ export const Player = () => {
         <IconButton onClick={onMuted}>
           {muted
             ? (<VolumeOffIcon fontSize="large"/>)
-            : volume <= 0.5 && volume !== 0
+            : volume >= 0.1 && volume <= 0.5
               ? (<VolumeDown fontSize="large"/>)
-              : volume <= 0
+              : volume === 0
                 ? (<VolumeOffIcon fontSize="large"/>)
                 : (<VolumeUp fontSize="large"/>)}
         </IconButton>

+ 27 - 45
src/components/TrackList/TrackList.jsx

@@ -1,4 +1,4 @@
-import React, { useEffect, useState } from 'react';
+import React, { useEffect } from 'react';
 import {
   IconButton, List, ListItem, Typography,
   Box, CircularProgress,
@@ -7,8 +7,6 @@ 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';
@@ -16,7 +14,6 @@ import {
 export const TrackList = ({tracks, isLoading}) => {
   const dispatch = useDispatch();
   const playerState = useSelector(state => state.player);
-  const [currentTracks, setCurrentTracks] = useState([]);
 
   useEffect(() => {
     if (playerState.trackList.length === 0) {
@@ -35,37 +32,6 @@ export const TrackList = ({tracks, isLoading}) => {
     }
   }, [playerState.isPlaying]);
 
-  useEffect(() => {
-    setCurrentTracks(tracks);
-  }, [tracks]);
-
-  const SortableItem = SortableElement(({track}) => (
-    <ListItem key={track._id}>
-      <IconButton
-        onClick={() => togglePlayPause(track._id)}
-      >
-        {
-          playerState.isPlaying && track._id === playerState.currentPlayingTrackId
-            ? (<PauseRounded fontSize="large" color="primary"/>)
-            : (<PlayArrowRounded fontSize="large" color="primary"/>)
-        }
-      </IconButton>
-      <Typography>{track?.originalFileName}</Typography>
-    </ListItem>
-  ));
-
-  const SortableList = SortableContainer(() => (
-    <List>
-      {currentTracks.map((track, index) => (
-        <SortableItem key={`item-${track._id}`} index={index} track={track}/>
-      ))}
-    </List>
-  ));
-
-  const onSortEnd = ({oldIndex, newIndex}) => {
-    setCurrentTracks(arrayMoveImmutable(currentTracks, oldIndex, newIndex));
-  };
-
   const togglePlayPause = (id) => {
     if (playerState.isPlaying) {
       playerState.audio.pause();
@@ -79,14 +45,30 @@ export const TrackList = ({tracks, isLoading}) => {
     }
   };
 
-  return isLoading ? (
-    <CircularProgress/>
-  ) : (
-    <Box sx={{
-      minHeight: '70vh',
-    }}
-    >
-      <SortableList onSortEnd={onSortEnd}/>
-    </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>
+    );
 };

+ 100 - 82
src/pages/LoginPage/LoginPage.jsx

@@ -16,16 +16,25 @@ import { Link, useHistory } from 'react-router-dom';
 import { useDispatch, useSelector } from 'react-redux';
 import VisibilityIcon from '@mui/icons-material/Visibility';
 import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
+import PriorityHighIcon from '@mui/icons-material/PriorityHigh';
 import { actionLogin } from '../../store/types/authTypes';
 
 export const LoginPage = () => {
   const dispatch = useDispatch();
   const authToken = useSelector(state => state.auth.authToken);
+
   const [login, setLogin] = useState(null);
   const [password, setPassword] = useState(null);
+
   const [visiblePsw, setVisiblePsw] = useState(false);
   const [openSnackBar, setOpenSnackBar] = useState(false);
 
+  const [loginDirty, setLoginDirty] = useState(false);
+  const [pswDirty, setPswDirty] = useState(false);
+
+  const isPswValid = password?.length >= 4 && password?.length <= 20;
+  const isLoginValid = login?.length >= 2 && login?.length <= 10;
+
   const onChangeLogin = (e) => {
     setLogin(e.target.value);
   };
@@ -49,93 +58,102 @@ export const LoginPage = () => {
   };
 
   return (
-    <Paper elevation={10} className="paperStyle">
-      <Grid
-        alignItems="center"
-        justifyContent="center"
-        flexDirection="column"
-        container
-      >
-        <Grid item>
-          <Avatar sx={{mb: '15px', backgroundColor: '#9c27b0'}}>
-            <LockOutlinedIcon color="white"/>
-          </Avatar>
-        </Grid>
-        <Grid item>
-          <Typography
-            variant="h4"
-          >
-            Sign in
-          </Typography>
+    <Box position="relative">
+      <Paper elevation={10} className="paperStyle">
+        <Grid
+          alignItems="center"
+          justifyContent="center"
+          flexDirection="column"
+          container
+        >
+          <Grid item>
+            <Avatar sx={{mb: '15px', backgroundColor: '#9c27b0'}}>
+              <LockOutlinedIcon color="white"/>
+            </Avatar>
+          </Grid>
+          <Grid item>
+            <Typography
+              variant="h4"
+            >
+              Sign in
+            </Typography>
+          </Grid>
         </Grid>
-      </Grid>
-      <FormControl className="formControl" sx={{margin: '15px 0'}}>
-        <InputLabel>Username</InputLabel>
-        <Input
-          variant="standard"
-          placeholder="Enter username"
-          onChange={onChangeLogin}
-          fullWidth
-          required
-        />
-      </FormControl>
-      <FormControl className="formControl" sx={{margin: '15px 0'}}>
-        <InputLabel>Password</InputLabel>
-        <Input
-          variant="standard"
-          placeholder="Enter password"
-          onChange={onChangePassword}
-          sx={{margin: '10px 0'}}
-          type={visiblePsw ? 'text' : 'password'}
-          endAdornment={(
-            <InputAdornment position="end">
-              <IconButton onClick={() => setVisiblePsw(!visiblePsw)}>
-                {visiblePsw
-                  ? (<VisibilityIcon/>)
-                  : (<VisibilityOffIcon/>)}
-              </IconButton>
-            </InputAdornment>
+        <FormControl className="formControl" sx={{margin: '15px 0'}}>
+          <InputLabel error={!!loginDirty && !isLoginValid}>Username</InputLabel>
+          <Input
+            name="userName"
+            variant="standard"
+            placeholder="Enter username"
+            onChange={onChangeLogin}
+            error={!!loginDirty && !isLoginValid}
+            onBlur={() => setLoginDirty(true)}
+            fullWidth
+            required
+          />
+        </FormControl>
+        <FormControl className="formControl" sx={{margin: '15px 0'}}>
+          <InputLabel error={!!pswDirty && !isPswValid}>Password</InputLabel>
+          <Input
+            name="password"
+            variant="standard"
+            placeholder="Enter password"
+            onChange={onChangePassword}
+            onBlur={() => setPswDirty(true)}
+            error={!!pswDirty && !isPswValid}
+            sx={{margin: '10px 0'}}
+            type={visiblePsw ? 'text' : 'password'}
+            endAdornment={(
+              <InputAdornment position="end">
+                <IconButton onClick={() => setVisiblePsw(!visiblePsw)}>
+                  {visiblePsw
+                    ? (<VisibilityIcon/>)
+                    : (<VisibilityOffIcon/>)}
+                </IconButton>
+              </InputAdornment>
           )}
+            fullWidth
+            required
+          />
+        </FormControl>
+        <Button
+          type="submit"
+          color="primary"
+          variant="contained"
+          sx={{
+            margin: '20px 0',
+          }}
+          onClick={signIn}
+          disabled={!(isLoginValid && isPswValid)}
           fullWidth
-          required
-        />
-      </FormControl>
-      <Button
-        type="submit"
-        color="primary"
-        variant="contained"
-        sx={{
-          margin: '20px 0',
-        }}
-        onClick={signIn}
-        fullWidth
-      >
-        Sign in
-      </Button>
-      <Typography>
-        {/* eslint-disable-next-line react/no-unescaped-entities */}
-        Don't you have an account?
-        <Link
-          to="/register"
         >
-          <Button>
-            Sign up
-          </Button>
-        </Link>
-      </Typography>
-      <Snackbar
-        open={openSnackBar}
-        autoHideDuration={2000}
-        onClose={handleClose}
-        anchorOrigin={{vertical: 'bottom', horizontal: 'right'}}
-      >
-        <Alert
-          severity="success"
+          Sign in
+        </Button>
+        <Typography>
+          {/* eslint-disable-next-line react/no-unescaped-entities */}
+          Don't you have an account?
+          <Link
+            to="/register"
+          >
+            <Button>
+              Sign up
+            </Button>
+          </Link>
+        </Typography>
+        <Snackbar
+          open={openSnackBar}
+          autoHideDuration={2000}
           onClose={handleClose}
+          anchorOrigin={{vertical: 'bottom', horizontal: 'right'}}
         >
-          Success sign in!
-        </Alert>
-      </Snackbar>
-    </Paper>
+          <Alert
+            severity="success"
+            onClose={handleClose}
+          >
+            Success sign in!
+          </Alert>
+        </Snackbar>
+      </Paper>
+    </Box>
   );
 };

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

@@ -7,4 +7,8 @@
 
 .formControl{
   width: 100%;
+}
+
+.errorLabel{
+  color: red;
 }

+ 136 - 80
src/pages/ProfilePage/ProfilePage.jsx

@@ -1,13 +1,13 @@
-import React, { useCallback, useEffect } from 'react';
+import React, { useCallback, useEffect, useState } from 'react';
 import {
-  Avatar, Box, Button, Grid, Paper, Typography, useTheme,
+  Avatar, Box, Button, Grid, Input, InputAdornment, Paper, TextField, Typography, useTheme,
 } from '@mui/material';
 import { useDispatch, useSelector } from 'react-redux';
 import EditIcon from '@mui/icons-material/Edit';
-import { AddCircleOutline } from '@mui/icons-material';
 import { useDropzone } from 'react-dropzone';
 import AddAPhotoIcon from '@mui/icons-material/AddAPhoto';
-import { actionFindUserById } from '../../store/types/authTypes';
+import CheckBoxIcon from '@mui/icons-material/CheckBox';
+import { actionFindUserById, actionSetNick } from '../../store/types/authTypes';
 import { history } from '../../createHistory';
 import { jwtDecode } from '../../utils/jwtDecode';
 import { actionSetUploadFile } from '../../store/types/uploadTypes';
@@ -17,6 +17,9 @@ import './ProfilePage.scss';
 export const ProfilePage = () => {
   const dispatch = useDispatch();
   const user = useSelector(state => state.auth.user);
+  const [isHovered, setIsHovered] = useState(false);
+  const [openNick, setOpenNick] = useState(false);
+  const [currentNick, setCurrentNick] = useState(null);
 
   const onDrop = useCallback(acceptedFiles => {
     dispatch(actionSetUploadFile(acceptedFiles[0]));
@@ -24,19 +27,22 @@ export const ProfilePage = () => {
 
   const {getRootProps, getInputProps} = useDropzone({onDrop});
 
-  console.log(user?.avatar !== null, user);
-
   useEffect(() => {
     if (user === null && localStorage.getItem('authToken') !== null) {
       const token = jwtDecode(localStorage.getItem('authToken'));
       const {id} = token.sub;
       if (id.length !== 0) {
-        console.log(id, 'i tut');
         dispatch(actionFindUserById(id));
       }
     }
   }, []);
 
+  console.log(user?.nick);
+
+  useEffect(() => {
+    setCurrentNick(user?.nick);
+  }, [user?.nick]);
+
   const logOut = () => {
     localStorage.removeItem('authToken');
     if (localStorage.getItem('authToken') === null) {
@@ -44,89 +50,139 @@ export const ProfilePage = () => {
     }
   };
 
+  const onChangeNick = (e) => {
+    setCurrentNick(e.target.value);
+  };
+  console.log(user);
+  const closeAndGetChangedNick = () => {
+    setOpenNick(!openNick);
+    console.log(currentNick, user);
+    dispatch(actionSetNick(currentNick));
+  };
   return (
-    <Paper
-      elevation={10}
+    <Box
       sx={{
-        margin: '0 25vh',
+        m: '10px 0',
+        display: 'flex',
+        alignItems: 'center',
+        justifyContent: 'center',
+        flexDirection: 'column',
+        height: '65vh',
       }}
     >
-      <Grid
-        direction="column"
-        alignItems="center"
-        justifyContent="center"
-        rowSpacing="10px"
-        marginTop="10px"
-        container
+      <Paper
+        elevation={10}
+        sx={{
+          height: '100%',
+          width: '80vh',
+        }}
       >
         <Grid
-          sx={{m: '10px'}}
-          item
-        >
-          <Typography variant="subtitle">
-            Profile
-          </Typography>
-        </Grid>
-        <Grid
-          // onClick={changeAvatar}
-          item
-        >
-          <Box {...getRootProps()}>
-            <input {...getInputProps()} />
-            {
-              user?.avatar !== null
-                ? (
-                  <Avatar
-                    className="avatar"
-                    sx={{width: 100, height: 100}}
-                    src={
-                      user === null
-                        ? null
-                        : (`${BACKEND_URL}/${user?.avatar?.url}`)
-                      }
-                  />
-                )
-                : (
-                  <Avatar
-                    className="avatar"
-                    sx={{width: 100, height: 100}}
-                  >
-                    <AddCircleOutline fontSize="large" color="white"/>
-                  </Avatar>
-                )
-            }
-          </Box>
-        </Grid>
-        <Grid
+          direction="column"
+          alignItems="center"
+          justifyContent="center"
           sx={{
-            mb: '10px',
-            display: 'flex',
-            flexDirection: 'row',
+            height: '100%',
           }}
-          item
+          container
         >
-          <Typography
-            variant="subtitle2"
-            sx={{
-              mr: '5px',
-            }}
+          <Grid
+            sx={{mb: '10px'}}
+            item
           >
-            {user?.nick}
-          </Typography>
-          <EditIcon
-            fontSize="small"
-            color="primary"
-          />
-        </Grid>
-        <Grid
-          sx={{mb: '10px'}}
-          item
-        >
-          <Button onClick={logOut}>
-            Log out
-          </Button>
+            <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>
+                <Avatar
+                  className="avatar"
+                  onMouseEnter={() => {
+                    setIsHovered(!isHovered);
+                    console.log(isHovered);
+                  }}
+                  sx={{width: 100, height: 100}}
+                  src={
+                    user !== null
+                      ? (`${BACKEND_URL}/${user?.avatar?.url}`)
+                      : null
+                  }
+                />
+              </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"
+                      />
+                    </InputAdornment>
+                  )}
+                />
+              )
+              : (
+                <Grid
+                  sx={{
+                    mb: '10px',
+                    display: 'flex',
+                    flexDirection: 'row',
+                  }}
+                  item
+                >
+                  <Typography
+                    variant="subtitle1"
+                    sx={{
+                      mr: '5px',
+                    }}
+                  >
+                    {user?.nick}
+                  </Typography>
+                  <EditIcon
+                    fontSize="medium"
+                    color="primary"
+                    onClick={() => setOpenNick(!openNick)}
+                  />
+                </Grid>
+              )}
+          </Grid>
+          <Grid
+            sx={{mt: '10px'}}
+            item
+          >
+            <Button onClick={logOut}>
+              Log out
+            </Button>
+          </Grid>
         </Grid>
-      </Grid>
-    </Paper>
+      </Paper>
+    </Box>
   );
 };

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

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

+ 30 - 7
src/pages/RegisterPage/RegisterPage.jsx

@@ -1,7 +1,7 @@
 import React, { useState } from 'react';
 import {
   Alert,
-  Avatar,
+  Avatar, Box,
   Button,
   FormControl,
   Grid,
@@ -10,20 +10,30 @@ import {
   Paper, Snackbar,
   Typography,
 } from '@mui/material';
-import { AddCircleOutline } from '@mui/icons-material';
 import VisibilityIcon from '@mui/icons-material/Visibility';
 import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
 import { useDispatch, useSelector } from 'react-redux';
 import { Link, useHistory } from 'react-router-dom';
 import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
-import { actionLogin, actionRegister } from '../../store/types/authTypes';
+import { actionRegister } from '../../store/types/authTypes';
 
 export const RegisterPage = () => {
   const [login, setLogin] = useState(null);
   const [password, setPassword] = useState(null);
   const [confirmPsw, setConfirmPsw] = useState(null);
+
   const [visiblePsw, setVisiblePsw] = useState(false);
   const [openSnackBar, setOpenSnackBar] = useState(false);
+
+  const [loginDirty, setLoginDirty] = useState(false);
+  const [pswDirty, setPswDirty] = useState(false);
+
+  const [pswConfirmDirty, setConfirmPswDirty] = useState(false);
+
+  const isPswValid = password?.length >= 4 && password?.length <= 20;
+  const isConfirmPswValid = password?.length >= 4 && password?.length <= 20;
+  const isLoginValid = login?.length >= 2 && login?.length <= 10;
+
   const dispatch = useDispatch();
   const auth = useSelector(state => state.auth);
   const history = useHistory();
@@ -43,7 +53,6 @@ export const RegisterPage = () => {
   const signUp = () => {
     if (password === confirmPsw) {
       dispatch(actionRegister({login, password}));
-      console.log(auth);
 
       if (auth.login.length !== 0 && localStorage.getItem('authToken') !== null) {
         setOpenSnackBar(!openSnackBar);
@@ -86,23 +95,27 @@ export const RegisterPage = () => {
         </Grid>
       </Grid>
       <FormControl className="formControl" sx={{mb: '10px'}}>
-        <InputLabel>Login</InputLabel>
+        <InputLabel error={!!loginDirty && !isLoginValid}>Login</InputLabel>
         <Input
           label="Login"
           variant="standard"
           placeholder="Enter login"
           sx={{margin: '10px 0'}}
           onChange={onChangeName}
+          onBlur={() => setLoginDirty(true)}
+          error={!!loginDirty && !isLoginValid}
           fullWidth
           required
         />
       </FormControl>
       <FormControl className="formControl" sx={{mb: '10px'}}>
-        <InputLabel>Password</InputLabel>
+        <InputLabel error={!!pswDirty && !isPswValid}>Password</InputLabel>
         <Input
           variant="standard"
           placeholder="Enter password"
           onChange={onChangePassword}
+          onBlur={() => setPswDirty(true)}
+          error={!!pswDirty && !isPswValid}
           sx={{margin: '10px 0'}}
           type={visiblePsw ? 'text' : 'password'}
           endAdornment={(
@@ -119,12 +132,14 @@ export const RegisterPage = () => {
         />
       </FormControl>
       <FormControl className="formControl" sx={{mb: '10px'}}>
-        <InputLabel>Confirm password</InputLabel>
+        <InputLabel error={!!pswConfirmDirty && !isConfirmPswValid}>Confirm password</InputLabel>
         <Input
           label="Confirm password"
           variant="standard"
           placeholder="Confirm password"
           onChange={confirmPassword}
+          onBlur={() => setConfirmPswDirty(true)}
+          error={!!pswConfirmDirty && !isConfirmPswValid}
           type={visiblePsw ? 'text' : 'password'}
           endAdornment={(
             <InputAdornment position="end">
@@ -138,6 +153,13 @@ export const RegisterPage = () => {
           fullWidth
           required
         />
+        {
+          (password !== null && confirmPsw !== null)
+            ? (password !== confirmPsw) || (!isPswValid && !isConfirmPswValid)
+              ? (<Box>The password confirmation does not match</Box>)
+              : ''
+            : ''
+        }
       </FormControl>
       <Button
         type="submit"
@@ -147,6 +169,7 @@ export const RegisterPage = () => {
           margin: '20px 0',
         }}
         onClick={signUp}
+        disabled={!(isLoginValid && isPswValid && isConfirmPswValid)}
         fullWidth
       >
         Sign up

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

@@ -9,7 +9,6 @@ export const SelectedPlaylistPage = () => {
   const playlists = useSelector(state => state.playlists);
   const dispatch = useDispatch();
   const params = useParams();
-  console.log(params.id, playlists.selectedPlaylist);
 
   useEffect(() => {
     dispatch(actionFetchOnePlaylist(params.id));

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

@@ -0,0 +1,96 @@
+// 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>
+//     );
+// };

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

@@ -1,10 +1,11 @@
 import React, { useCallback } from 'react';
 import {
-  Box, Grid, Paper, Typography,
+  Box, Button, Grid, Paper, Typography,
 } from '@mui/material';
-import { useDispatch } from 'react-redux';
+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 { actionSetUploadTrack } from '../../store/types/uploadTypes';
 
 export const UploadTracks = () => {
@@ -16,38 +17,59 @@ export const UploadTracks = () => {
   const {getRootProps, getInputProps} = useDropzone({onDrop});
 
   return (
-    <Paper elevation={10} className="paper">
-      <Grid
-        alignItems="center"
-        justifyContent="center"
-        flexDirection="column"
-        container
+    <Box
+      sx={{
+        m: '10px 0',
+        display: 'flex',
+        alignItems: 'center',
+        justifyContent: 'center',
+        flexDirection: 'column',
+        height: '65vh',
+      }}
+    >
+      <Paper
+        elevation={10}
+        sx={{
+          height: '100%',
+          width: '100vh',
+        }}
       >
-        <Box
-          sx={{
-            width: '100%',
-            height: '50vh',
-            display: 'flex',
-            alignItems: 'center',
-            justifyContent: 'center',
-            flexDirection: 'column',
-          }}
-          {...getRootProps()}
+        <Grid
+          container
         >
-          <UploadIcon
-            color="primary"
+          <Box
             sx={{
-              height: '40px',
-              width: '40px',
-              mb: '20px',
+              width: '100%',
+              height: '50vh',
+              display: 'flex',
+              alignItems: 'center',
+              justifyContent: 'center',
+              flexDirection: 'column',
             }}
-          />
-          <input {...getInputProps()} />
-          <Typography>
-            Drop file to upload
-          </Typography>
-        </Box>
-      </Grid>
-    </Paper>
+            {...getRootProps()}
+          >
+            <UploadIcon
+              color="primary"
+              sx={{
+                height: '40px',
+                width: '40px',
+                mb: '20px',
+              }}
+            />
+            <input {...getInputProps()} />
+            <Typography>
+              Drop file to upload
+            </Typography>
+          </Box>
+        </Grid>
+      </Paper>
+      <Link
+        to="/userTracks"
+      >
+        <Button>
+          My uploaded tracks
+        </Button>
+      </Link>
+    </Box>
   );
 };

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

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

+ 11 - 1
src/store/reducers/authReducer.js

@@ -35,7 +35,6 @@ export function authReducer(state = initialState, action) {
         ...state,
       };
     case types.FETCH_FIND_USER_BY_ID_SUCCESS:
-      console.log(action.payload);
       return {
         ...state,
         user: action.payload,
@@ -50,6 +49,17 @@ export function authReducer(state = initialState, action) {
         ...state,
         user: action.payload,
       };
+    case types.SET_NICK:
+      console.log(action.payload);
+      return {
+        ...state,
+      };
+    case types.SET_NICK_SUCCESS:
+      console.log(action.payload);
+      return {
+        ...state,
+        user: action.payload,
+      };
     default:
       return state;
   }

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

@@ -32,6 +32,7 @@ export function playlistsReducer(state = initialState, action) {
         isLoading: true,
       };
     case types.FETCH_ONE_PLAYLIST_SUCCESS:
+      console.log(action.payload);
       return {
         ...state,
         isLoading: false,

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

@@ -4,6 +4,7 @@ const initialState = {
   trackList: [],
   totalCount: 0,
   isLoading: false,
+  userTracks: [],
 };
 
 export function tracksReducer(state = initialState, action) {
@@ -20,6 +21,15 @@ 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,
+        myTracks: action.payload,
+      };
     default:
       return state;
   }

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

@@ -8,9 +8,11 @@ import types, {
   actionRegisterFail,
   actionLogin,
   actionFindUserByIdSuccess,
-  actionFindUserByIdFail, actionSetUser,
+  actionFindUserByIdFail, actionSetUser, actionSetNickSuccess,
 } from '../types/authTypes';
-import { findUserById, login, registration } from '../../api/auth';
+import {
+  findUserById, login, registration, setNick,
+} from '../../api/auth';
 import { forwardToMainPage } from '../../utils/history';
 import { jwtDecode } from '../../utils/jwtDecode';
 
@@ -49,19 +51,30 @@ function* registerWorker(action) {
 }
 
 function* findUserWorker(action) {
-  console.log(action.payload);
-  const auth = yield select(state => state.auth);
   try {
     const user = yield call(findUserById, action.payload);
-    console.log(action.payload, auth);
     yield put(actionFindUserByIdSuccess(user));
   } catch (e) {
     yield put(actionFindUserByIdFail());
   }
 }
 
+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) {
+    e.message;
+  }
+}
+
 export function* authSaga() {
   yield takeLatest(types.FETCH_LOGIN, loginWorker);
   yield takeLatest(types.FETCH_REGISTER, registerWorker);
   yield takeLatest(types.FETCH_FIND_USER_BY_ID, findUserWorker);
+  yield takeLatest(types.SET_NICK, setNickWorker);
 }

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

@@ -11,6 +11,8 @@ import types, {
   actionFetchPlaylistsSuccess,
   actionFetchOnePlaylistFail,
 } from '../types/playlistTypes';
+import { getTracksCount } from '../../api/tracks';
+import { actionFetchTracksSuccess } from '../types/trackTypes';
 
 function* getAllPlaylists(action) {
   try {
@@ -32,7 +34,13 @@ function* getOnePlaylist(action) {
   }
 }
 
+function* setQueTracksWorker(action) {
+  console.log(action.payload);
+  yield put(actionFetchOnePlaylistSuccess(action.payload));
+}
+
 export function* playlistsSaga() {
   yield takeLatest(types.FETCH_PLAYLISTS, getAllPlaylists);
   yield takeLatest(types.FETCH_ONE_PLAYLIST, getOnePlaylist);
+  yield takeLatest(types.SET_QUE_TRACKS, setQueTracksWorker);
 }

+ 15 - 2
src/store/sagas/tracksSaga.js

@@ -1,8 +1,12 @@
 import {
   call, put, takeLatest, select,
 } from 'redux-saga/effects';
-import types, { actionFetchTracksFail, actionFetchTracksSuccess } from '../types/trackTypes';
-import { getTracksCount, getTracksWithPage } from '../../api/tracks';
+import types, {
+  actionFetchUserTracksSuccess,
+  actionFetchTracksFail,
+  actionFetchTracksSuccess,
+} from '../types/trackTypes';
+import { getMyTracks, getTracksCount, getTracksWithPage } from '../../api/tracks';
 
 function* fetchTracksWorker(action) {
   try {
@@ -15,6 +19,15 @@ 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));
+}
+
 export function* tracksSaga() {
   yield takeLatest(types.FETCH_TRACKS, fetchTracksWorker);
+  yield takeLatest(types.FETCH_USER_TRACKS, fetchMyTracksWorker);
 }

+ 2 - 2
src/store/sagas/uploadSaga.js

@@ -5,6 +5,7 @@ import types, { actionSetUploadFileSuccess } from '../types/uploadTypes';
 import { getGqlForUpload, getGqlForUploadTracks } from '../../utils/getGqlForUpload';
 import { jwtDecode } from '../../utils/jwtDecode';
 import { setAvatar, uploadTracks } from '../../api/upload';
+import { actionSetUser } from '../types/authTypes';
 
 function* uploadFileWorker(action) {
   const auth = yield select(state => state.auth.authToken);
@@ -17,8 +18,7 @@ function* uploadFileWorker(action) {
     const userId = token.sub.id;
 
     const result = yield call(setAvatar, {userId, avatarId});
-
-    // yield put(actionSetUploadFileSuccess());
+    yield put(actionSetUser(result));
   } catch (e) {
     e.message;
   }

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

@@ -9,6 +9,8 @@ const types = {
   FETCH_FIND_USER_BY_ID_SUCCESS: 'FETCH_FIND_USER_BY_ID_SUCCESS',
   FETCH_FIND_USER_BY_ID_FAIL: 'FETCH_FIND_USER_BY_ID_FAIL',
   SET_USER: 'SET_USER',
+  SET_NICK: 'SET_NICK',
+  SET_NICK_SUCCESS: 'SET_NICK_SUCCESS',
 };
 
 export const actionLogin = (payload) => ({type: types.FETCH_LOGIN, payload});
@@ -25,4 +27,7 @@ export const actionFindUserByIdFail = (payload) => ({type: types.FETCH_FIND_USER
 
 export const actionSetUser = (payload) => ({type: types.SET_USER, payload});
 
+export const actionSetNick = (payload) => ({type: types.SET_NICK, payload});
+export const actionSetNickSuccess = (payload) => ({type: types.SET_NICK_SUCCESS, payload});
+
 export default types;

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

@@ -5,6 +5,7 @@ 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',
 };
 
 export const actionFetchPlaylists = (payload) => ({type: types.FETCH_PLAYLISTS, payload});
@@ -15,4 +16,6 @@ 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 default types;

+ 5 - 0
src/store/types/trackTypes.js

@@ -2,10 +2,15 @@ const types = {
   FETCH_TRACKS: 'FETCH_TRACKS',
   FETCH_TRACKS_SUCCESS: 'FETCH_TRACKS_SUCCESS',
   FETCH_TRACKS_FAIL: 'FETCH_TRACKS_FAIL',
+  FETCH_USER_TRACKS: 'FETCH_USER_TRACKS',
+  FETCH_USER_TRACKS_SUCCESS: 'FETCH_USER_TRACKS_SUCCESS',
 };
 
 export const actionFetchTracks = (payload) => ({type: types.FETCH_TRACKS, payload});
 export const actionFetchTracksSuccess = (payload) => ({type: types.FETCH_TRACKS_SUCCESS, payload});
 export const actionFetchTracksFail = (payload) => ({type: types.FETCH_TRACKS_FAIL, payload});
 
+export const actionFetchUserTracks = (payload) => ({type: types.FETCH_USER_TRACKS, payload});
+export const actionFetchUserTracksSuccess = (payload) => ({type: types.FETCH_USER_TRACKS_SUCCESS, payload});
+
 export default types;