Преглед изворни кода

Merge branch 'sortable-playlist-impement' of Sergei-Levshnia/player-project into dev

Sergei-Levshnia пре 2 година
родитељ
комит
e41da2917c
6 измењених фајлова са 167 додато и 80 уклоњено
  1. 59 0
      package-lock.json
  2. 2 0
      package.json
  3. 5 4
      src/App.js
  4. 16 11
      src/actions/index.js
  5. 82 62
      src/components/Page/index.js
  6. 3 3
      src/components/Sidebar/index.js

+ 59 - 0
package-lock.json

@@ -11,6 +11,7 @@
         "@testing-library/jest-dom": "^5.16.1",
         "@testing-library/react": "^12.1.2",
         "@testing-library/user-event": "^13.5.0",
+        "array-move": "^4.0.0",
         "node-sass": "^7.0.1",
         "react": "^17.0.2",
         "react-dom": "^17.0.2",
@@ -18,6 +19,7 @@
         "react-redux": "^7.2.6",
         "react-router-dom": "^5.3.0",
         "react-scripts": "5.0.0",
+        "react-sortable-hoc": "^2.0.0",
         "redux": "^4.1.2",
         "redux-thunk": "^2.4.1",
         "web-vitals": "^2.1.3"
@@ -4282,6 +4284,17 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/array-move": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/array-move/-/array-move-4.0.0.tgz",
+      "integrity": "sha512-+RY54S8OuVvg94THpneQvFRmqWdAHeqtMzgMW6JNurHxe8rsS07cHQdfGkXnTUXiBcyZ0j3SiDIxxj0RPiqCkQ==",
+      "engines": {
+        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "node_modules/array-union": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
@@ -8743,6 +8756,14 @@
         "node": ">= 0.4"
       }
     },
+    "node_modules/invariant": {
+      "version": "2.2.4",
+      "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
+      "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
+      "dependencies": {
+        "loose-envify": "^1.0.0"
+      }
+    },
     "node_modules/ip": {
       "version": "1.1.5",
       "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz",
@@ -14180,6 +14201,21 @@
         }
       }
     },
+    "node_modules/react-sortable-hoc": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/react-sortable-hoc/-/react-sortable-hoc-2.0.0.tgz",
+      "integrity": "sha512-JZUw7hBsAHXK7PTyErJyI7SopSBFRcFHDjWW5SWjcugY0i6iH7f+eJkY8cJmGMlZ1C9xz1J3Vjz0plFpavVeRg==",
+      "dependencies": {
+        "@babel/runtime": "^7.2.0",
+        "invariant": "^2.2.4",
+        "prop-types": "^15.5.7"
+      },
+      "peerDependencies": {
+        "prop-types": "^15.5.7",
+        "react": "^16.3.0 || ^17.0.0",
+        "react-dom": "^16.3.0 || ^17.0.0"
+      }
+    },
     "node_modules/read-pkg": {
       "version": "5.2.0",
       "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz",
@@ -20528,6 +20564,11 @@
         "is-string": "^1.0.7"
       }
     },
+    "array-move": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/array-move/-/array-move-4.0.0.tgz",
+      "integrity": "sha512-+RY54S8OuVvg94THpneQvFRmqWdAHeqtMzgMW6JNurHxe8rsS07cHQdfGkXnTUXiBcyZ0j3SiDIxxj0RPiqCkQ=="
+    },
     "array-union": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
@@ -23830,6 +23871,14 @@
         "side-channel": "^1.0.4"
       }
     },
+    "invariant": {
+      "version": "2.2.4",
+      "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
+      "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
+      "requires": {
+        "loose-envify": "^1.0.0"
+      }
+    },
     "ip": {
       "version": "1.1.5",
       "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz",
@@ -27661,6 +27710,16 @@
         "workbox-webpack-plugin": "^6.4.1"
       }
     },
+    "react-sortable-hoc": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/react-sortable-hoc/-/react-sortable-hoc-2.0.0.tgz",
+      "integrity": "sha512-JZUw7hBsAHXK7PTyErJyI7SopSBFRcFHDjWW5SWjcugY0i6iH7f+eJkY8cJmGMlZ1C9xz1J3Vjz0plFpavVeRg==",
+      "requires": {
+        "@babel/runtime": "^7.2.0",
+        "invariant": "^2.2.4",
+        "prop-types": "^15.5.7"
+      }
+    },
     "read-pkg": {
       "version": "5.2.0",
       "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz",

+ 2 - 0
package.json

@@ -6,6 +6,7 @@
     "@testing-library/jest-dom": "^5.16.1",
     "@testing-library/react": "^12.1.2",
     "@testing-library/user-event": "^13.5.0",
+    "array-move": "^4.0.0",
     "node-sass": "^7.0.1",
     "react": "^17.0.2",
     "react-dom": "^17.0.2",
@@ -13,6 +14,7 @@
     "react-redux": "^7.2.6",
     "react-router-dom": "^5.3.0",
     "react-scripts": "5.0.0",
+    "react-sortable-hoc": "^2.0.0",
     "redux": "^4.1.2",
     "redux-thunk": "^2.4.1",
     "web-vitals": "^2.1.3"

+ 5 - 4
src/App.js

@@ -81,9 +81,10 @@ const Player = () =>
             <h2>Welcome to online Player!</h2>
             <div style={{ width: '50%', margin: '0 auto' }}>
               <ul style={{ textAlign: 'start' }}>
-                <li><strong>To create playlist: </strong>click "NEW PLAYLIST"</li>
-                <li><strong>To upload track: </strong>drag and drop it to playlist area</li>
-                <li><strong>To see list of all your tracks: </strong>click "My tracks"</li>
+                <li><strong>Click "NEW PLAYLIST" - </strong><small>To create new playlist.</small></li>
+                <li><strong>Drag 'n' drop track to playlist area - </strong><small>To upload the track and add it to current playlist.</small></li>
+                <li><strong>Drag a track within playlist - </strong><small>To chage the order of playlist tracks.</small></li>
+                <li><strong>Click "MY TRACKS" - </strong><small>To see all of your uploaded tracks.</small></li>
               </ul>
             </div>
           </>
@@ -101,7 +102,7 @@ function App() {
           <Switch>
             <Route path="/login" component={Logcomp.LoginFormConnect} exact />
             <Route path="/registration" component={Logcomp.RegisterFormConnect} exact />
-            <Route path='/player' component={Player} />
+            <Route path="/player" component={Player} />
           </Switch>
         </div>
       </Provider>

+ 16 - 11
src/actions/index.js

@@ -135,9 +135,23 @@ export const actionLoadFile = (file, type) => {
     )
 }
 
+export const actionUpdatePlaylist = (playlistId, updPlaylist) => {
+    console.log('UPDATING', playlistId, updPlaylist)
+    return (
+        actionPromise('trackToPlaylist', gql(`
+            mutation($playlistId: ID, $newTracks: [TrackInput]) {
+                PlaylistUpsert(playlist:{ _id: $playlistId, tracks: $newTracks}) {
+                _id, name, tracks { _id, originalFileName, }
+                }
+            }
+            `, { playlistId: playlistId ,  newTracks: updPlaylist }))
+
+    )
+}
 export const actionUploadUserTrack = (file, playlistId) =>
     async (dispatch, getState) => {
         await dispatch(actionLoadFile(file, 'track'))
+
         if(!playlistId) {
             dispatch(actionGetUserTracks())
         } else {
@@ -146,19 +160,13 @@ export const actionUploadUserTrack = (file, playlistId) =>
             let oldPlaylist = getState().promise.playlistTracks.payload[0].tracks
 
             if(oldPlaylist) {
-                console.log('id pashet', oldPlaylist)
+                //console.log('id pashet', oldPlaylist)
                 oldPlaylist.forEach(track => updPlaylist.push({_id: track._id}))
             }
             updPlaylist.push({_id: getState().promise.loadFile.payload?._id})
             console.log('UPDATED PLST', updPlaylist)
 
-            await dispatch(actionPromise('trackToPlaylist', gql(`
-            mutation($playlistId: ID, $newTracks: [TrackInput]) {
-                PlaylistUpsert(playlist:{ _id: $playlistId, tracks: $newTracks}) {
-                _id, name, tracks { _id, originalFileName, }
-                }
-            }
-            `, { playlistId: playlistId ,  newTracks: updPlaylist.reverse() })))
+            await dispatch(actionUpdatePlaylist(playlistId, updPlaylist.reverse()))
             dispatch(actionGetPlaylistById(playlistId))
         }
     }
@@ -166,9 +174,6 @@ export const actionUploadUserTrack = (file, playlistId) =>
 export const actionUploadAvatar = (file) =>
     async (dispatch, getState) => {
       await dispatch(actionLoadFile(file, 'upload'))
-      //let picId = getState().promise?.loadFile?.payload?._id
-      //let userId = jwtDecode(localStorage.authToken).sub.id
-  
       await dispatch(actionPromise('setAvatar', gql(`
         mutation {
           UserUpsert(user:{_id: "${jwtDecode(localStorage.authToken).sub.id}", avatar: {_id: "${getState().promise?.loadFile?.payload?._id}"}}){

+ 82 - 62
src/components/Page/index.js

@@ -1,102 +1,122 @@
 import * as action from '../../actions'
 
-import { useEffect, useState} from 'react';
+import { useEffect, useState, useCallback } from 'react';
 import { connect } from 'react-redux';
 import { useDropzone } from 'react-dropzone'
-import React, {useCallback} from 'react'
-
-const Track = ({track:{url, originalFileName, id3:{title, artist, album}}}) =>
-    <li style={{border: '1px solid black', display:'flex', alignItems:'center'}}>
-      <div style={{marginRight:'2%'}}>
-        <button style={{padding: '10px', margin:'2px'}}> {`>`} </button>
-        <button style={{padding: '10px', margin:'2px'}}>| |</button>
-        <br/>
-        <button style={{padding: '10px', margin:'2px'}}>Add to...</button>
-      </div>
-      <div style={{textAlign: 'left'}}>
-        <h5>{artist || 'Artist: unknown'}</h5> 
-        <h6>{album || 'Album: unknown'}</h6>
-        <h5>{title || originalFileName}</h5>
-        <p>{url}</p>
-      </div>
-    </li>
-//----------------------------------------------------
-const Playlist = ({playlist}) => {
-  return(
+import { sortableContainer, sortableElement } from 'react-sortable-hoc';
+import { arrayMoveImmutable } from 'array-move';
+
+
+const Track = ({ track: { _id, url, originalFileName, id3: { title, artist, album } } }) =>
+  <li style={{ border: '1px solid black', display: 'flex', alignItems: 'center' }}>
+    <div style={{ marginRight: '2%' }}>
+      <button style={{ padding: '10px', margin: '2px' }}> {`>`} </button>
+      <button style={{ padding: '10px', margin: '2px' }}>| |</button>
+      <br />
+      <button style={{ padding: '10px', margin: '2px' }}>Add to...</button>
+    </div>
+    <div style={{ textAlign: 'left' }}>
+      <h5>{artist || 'Artist: unknown'}</h5>
+      <h6>{album || 'Album: unknown'}</h6>
+      <h5>{title || originalFileName}</h5>
+      <p>{_id}</p>
+      <p>{url}</p>
+    </div>
+  </li>
+
+const SortableItem = sortableElement(Track);
+
+const SortableContainer = sortableContainer(({ children }) => {
+  return <ul>{children}</ul>;
+});
+
+const Playlist = ({ playlist, updPlaylist }) => {
+  let [_tracks, setTracks] = useState()
+
+  useEffect(() => setTracks(playlist[0]?.tracks), [playlist])
+
+  const onSortEnd = ({ oldIndex, newIndex }) => {
+    setTracks(arrayMoveImmutable(_tracks, oldIndex, newIndex))
+    updPlaylist(playlist[0]._id, arrayMoveImmutable(_tracks, oldIndex, newIndex).map(track => ({ _id: track._id })))
+  };
+
+  return (
     <>
       <h2>{playlist[0]?.name || 'Playlist'}</h2>
-      <ul>
-        {(playlist[0]?.tracks || []).map(track => <Track track={track}/>)}
-      </ul>
+      <SortableContainer onSortEnd={onSortEnd}>
+        {(_tracks || []).map((track, index) => <SortableItem index={index} track={track} />)}
+      </SortableContainer>
     </>
   )
 }
-export const PlaylistConnect = connect(state => ({playlist: state.promise.playlistTracks?.payload || []}))(Playlist)
+export const PlaylistConnect = connect(
+  state => ({ playlist: state.promise.playlistTracks?.payload || [] }),
+  { updPlaylist: action.actionUpdatePlaylist }
+)(Playlist)
 
-const PlaylistTrackDropzone = ({playlist, uploadTrack}) => {
+const PlaylistTrackDropzone = ({ playlist, uploadTrack }) => {
   let [playlistId, setPlaylistId] = useState()
 
-  useEffect(()=> {
+  useEffect(() => {
     setPlaylistId(playlist[0]?._id)
   }, [playlist])
 
   const onDrop = useCallback(acceptedFiles => {
     uploadTrack(acceptedFiles[0], playlistId)
   }, [uploadTrack, playlistId])
-  const {getRootProps, getInputProps, isDragActive} = useDropzone({onDrop})
+  const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop })
 
   return (
-      <div {...getRootProps()} style={isDragActive? {border:'1px solid mediumseagreen'} : {border:'1px solid black'}}>
-        {isDragActive? <small>drag here...</small> : <small>to upload drag track here</small>}
-        <PlaylistConnect />
-      </div>
+    <div {...getRootProps()} style={isDragActive ? { border: '1px solid mediumseagreen' } : { border: '1px solid black' }}>
+      {isDragActive ? <small>drag here...</small> : <small>to upload drag track here</small>}
+      <PlaylistConnect />
+    </div>
   )
 }
 const PlaylistTrackDropzoneConnect = connect(
-    state => ({playlist: state.promise.playlistTracks?.payload || []}),
-    {uploadTrack: action.actionUploadUserTrack}
-  )(PlaylistTrackDropzone)
+  state => ({ playlist: state.promise.playlistTracks?.payload || [] }),
+  { uploadTrack: action.actionUploadUserTrack }
+)(PlaylistTrackDropzone)
 
-const PlaylistPage = ({match: {params: {_id}}, getTracks}) => {
+const PlaylistPage = ({ match: { params: { _id } }, getTracks }) => {
   useEffect(() => { getTracks(_id.substring(1)) }, [_id, getTracks])
-  //return(<PlaylistConnect />)
-  return(<PlaylistTrackDropzoneConnect />)
+  return (<PlaylistTrackDropzoneConnect />)
 }
-export const PlaylistPageConnect = connect(null, {getTracks: action.actionGetPlaylistById})(PlaylistPage)
-//----------------------------------------------------
+export const PlaylistPageConnect = connect(null, { getTracks: action.actionGetPlaylistById })(PlaylistPage)
+
 
-const UserTracks = ({user, tracks}) =>
-  <>
-    <h2>{ user.login || 'My' } tracks:</h2>
-    <ul>
-      {(tracks || []).map(track => <Track track={track}/>).reverse()}
-    </ul>
-  </>
+const UserTracks = ({ user, tracks }) => {
+  return (
+    <>
+      <h2>{user.login || 'My'} tracks:</h2>
+      <ul>{(tracks || []).map(track => <Track track={track} />).reverse()}</ul>
+    </>
+  )
+}
 
 const UserTracksConnect = connect(state => ({
-    tracks: state.promise.userTracks?.payload || [], 
-    user: state.promise.userData?.payload || {}
-  })
+  tracks: state.promise.userTracks?.payload || [],
+  user: state.promise.userData?.payload || {}
+})
 )(UserTracks)
 
-const UserTracksDropzone = ({onLoad}) => {
+const UserTracksDropzone = ({ onLoad }) => {
   const onDrop = useCallback(acceptedFiles => {
     onLoad(acceptedFiles[0])
   }, [onLoad])
-  const {getRootProps, getInputProps, isDragActive} = useDropzone({onDrop})
+  const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop })
 
   return (
-      <div {...getRootProps()} style={isDragActive? {border:'1px solid mediumseagreen'} : {border:'1px solid black'}}>
-        {isDragActive? <small>drag here...</small> : <small>to upload drag track here</small>}
-        <UserTracksConnect />
-      </div>
+    <div {...getRootProps()} style={isDragActive ? { border: '1px solid mediumseagreen' } : { border: '1px solid black' }}>
+      {isDragActive ? <small>drag here...</small> : <small>to upload drag track here</small>}
+      <UserTracksConnect />
+    </div>
   )
 }
-export const UserTrackDropzoneConnect = connect(null, {onLoad: action.actionUploadUserTrack})(UserTracksDropzone)
+export const UserTrackDropzoneConnect = connect(null, { onLoad: action.actionUploadUserTrack })(UserTracksDropzone)
 
-const UserTracksPage = ({match: {params: {_id}}, getUserTracks}) => {
-  useEffect(() => { getUserTracks() },[_id, getUserTracks])
-  //return(<UserTracksConnect/>)
-  return(<UserTrackDropzoneConnect/>)
+const UserTracksPage = ({ match: { params: { _id } }, getUserTracks }) => {
+  useEffect(() => { getUserTracks() }, [_id, getUserTracks])
+  return (<UserTrackDropzoneConnect />)
 }
-export const UserTracksPageConnect = connect(null, {getUserTracks: action.actionGetUserTracks})(UserTracksPage)
+export const UserTracksPageConnect = connect(null, { getUserTracks: action.actionGetUserTracks })(UserTracksPage)

+ 3 - 3
src/components/Sidebar/index.js

@@ -69,7 +69,7 @@ const UserTracksBtn = ({userId}) => {
       margin: '5px', 
       padding:'5px'
     }} 
-    >My tracks</Link>
+    >MY TRACKS</Link>
   )
 }
 export const UserTracksBtnConnect = connect(state => ({userId: state.promise.userData?.payload?._id || ''}))(UserTracksBtn)
@@ -82,7 +82,7 @@ const PlaylistAdd = ({addPlaylist}) => {
       {
         !clicked?
           <button 
-            style={{ border: '1px solid black', backgroundColor: 'mediumseagreen',width : '95%',padding: '5px', margin: '5px' }}
+            style={{backgroundColor: 'mediumseagreen',width : '95%',padding: '5px', margin: '5px', fontSize:'21px' }}
             onClick={() => setClicked(true)}
           >NEW PLAYLIST</button>
           :
@@ -120,7 +120,7 @@ const Playlists = ({playlists}) => {
           return (
             <Link 
               style={{display:'block', backgroundColor: 'darkcyan', color: 'cyan', margin: '5px', padding:'5px'}} 
-              to={`${history.location.pathname}/playlist/:${item._id}`}
+              to={`/player/playlist/:${item._id}`}
             >{item.name}</Link>
           )
         }) : ''