#4 mergin dev into master to save current progress

Merged
Sergei-Levshnia merged 14 commits from Sergei-Levshnia/dev into Sergei-Levshnia/master 2 years ago
7 changed files with 629 additions and 181 deletions
  1. 150 29
      package-lock.json
  2. 3 0
      package.json
  3. 62 144
      src/App.js
  4. 97 8
      src/actions/index.js
  5. 67 0
      src/components/Login/index.js
  6. 120 0
      src/components/Page/index.js
  7. 130 0
      src/components/Sidebar/index.js

+ 150 - 29
package-lock.json

@@ -11,12 +11,15 @@
         "@testing-library/jest-dom": "^5.16.1",
         "@testing-library/react": "^12.1.2",
         "@testing-library/user-event": "^13.5.0",
+        "array-move": "^4.0.0",
         "node-sass": "^7.0.1",
         "react": "^17.0.2",
         "react-dom": "^17.0.2",
+        "react-dropzone": "^11.5.1",
         "react-redux": "^7.2.6",
         "react-router-dom": "^5.3.0",
         "react-scripts": "5.0.0",
+        "react-sortable-hoc": "^2.0.0",
         "redux": "^4.1.2",
         "redux-thunk": "^2.4.1",
         "web-vitals": "^2.1.3"
@@ -4281,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",
@@ -4395,6 +4409,14 @@
         "node": ">= 4.5.0"
       }
     },
+    "node_modules/attr-accept": {
+      "version": "2.2.2",
+      "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz",
+      "integrity": "sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==",
+      "engines": {
+        "node": ">=4"
+      }
+    },
     "node_modules/autoprefixer": {
       "version": "10.4.2",
       "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.2.tgz",
@@ -7542,6 +7564,17 @@
         "webpack": "^4.0.0 || ^5.0.0"
       }
     },
+    "node_modules/file-selector": {
+      "version": "0.2.4",
+      "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.2.4.tgz",
+      "integrity": "sha512-ZDsQNbrv6qRi1YTDOEWzf5J2KjZ9KMI1Q2SGeTkCJmNNW25Jg4TW4UMcmoqcg4WrAyKRcpBXdbWRxkfrOzVRbA==",
+      "dependencies": {
+        "tslib": "^2.0.3"
+      },
+      "engines": {
+        "node": ">= 10"
+      }
+    },
     "node_modules/filelist": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.2.tgz",
@@ -7648,9 +7681,9 @@
       "integrity": "sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw=="
     },
     "node_modules/follow-redirects": {
-      "version": "1.14.6",
-      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.6.tgz",
-      "integrity": "sha512-fhUl5EwSJbbl8AR+uYL2KQDxLkdSjZGR36xy46AO7cOMTrCMON6Sa28FmAnC2tRTDbd/Uuzz3aJBv7EBN7JH8A==",
+      "version": "1.14.7",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.7.tgz",
+      "integrity": "sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ==",
       "funding": [
         {
           "type": "individual",
@@ -8723,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",
@@ -11726,11 +11767,11 @@
       }
     },
     "node_modules/node-forge": {
-      "version": "0.10.0",
-      "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz",
-      "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==",
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.2.1.tgz",
+      "integrity": "sha512-Fcvtbb+zBcZXbTTVwqGA5W+MKBj56UjVRevvchv5XrcyXbmNdesfZL37nlcWOfpgHhgmxApw3tQbTr4CqNmX4w==",
       "engines": {
-        "node": ">= 6.0.0"
+        "node": ">= 6.13.0"
       }
     },
     "node_modules/node-gyp": {
@@ -13975,6 +14016,22 @@
         "react": "17.0.2"
       }
     },
+    "node_modules/react-dropzone": {
+      "version": "11.5.1",
+      "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-11.5.1.tgz",
+      "integrity": "sha512-eNhttdq4ZDe3eKbXAe54Opt+sbtqmNK5NWTHf/l5d+1TdZqShJ8gMjBrya00qx5zkI//TYxRhu1d9pemTgaWwg==",
+      "dependencies": {
+        "attr-accept": "^2.2.1",
+        "file-selector": "^0.2.2",
+        "prop-types": "^15.7.2"
+      },
+      "engines": {
+        "node": ">= 10"
+      },
+      "peerDependencies": {
+        "react": ">= 16.8"
+      }
+    },
     "node_modules/react-error-overlay": {
       "version": "6.0.10",
       "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.10.tgz",
@@ -14144,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",
@@ -14919,11 +14991,14 @@
       "integrity": "sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo="
     },
     "node_modules/selfsigned": {
-      "version": "1.10.11",
-      "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.11.tgz",
-      "integrity": "sha512-aVmbPOfViZqOZPgRBT0+3u4yZFHpmnIghLMlAcb5/xhp5ZtB/RVnKhz5vl2M32CLXAqR4kha9zfhNg0Lf/sxKA==",
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.0.0.tgz",
+      "integrity": "sha512-cUdFiCbKoa1mZ6osuJs2uDHrs0k0oprsKveFiiaBKCNq3SYyb5gs2HxhQyDNLCmL51ZZThqi4YNDpCK6GOP1iQ==",
       "dependencies": {
-        "node-forge": "^0.10.0"
+        "node-forge": "^1.2.0"
+      },
+      "engines": {
+        "node": ">=10"
       }
     },
     "node_modules/semver": {
@@ -16680,9 +16755,9 @@
       }
     },
     "node_modules/webpack-dev-server": {
-      "version": "4.7.2",
-      "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.7.2.tgz",
-      "integrity": "sha512-s6yEOSfPpB6g1T2+C5ZOUt5cQOMhjI98IVmmvMNb5cdiqHoxSUfACISHqU/wZy+q4ar/A9jW0pbNj7sa50XRVA==",
+      "version": "4.7.3",
+      "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.7.3.tgz",
+      "integrity": "sha512-mlxq2AsIw2ag016nixkzUkdyOE8ST2GTy34uKSABp1c4nhjZvH90D5ZRR+UOLSsG4Z3TFahAi72a3ymRtfRm+Q==",
       "dependencies": {
         "@types/bonjour": "^3.5.9",
         "@types/connect-history-api-fallback": "^1.3.5",
@@ -16706,7 +16781,7 @@
         "p-retry": "^4.5.0",
         "portfinder": "^1.0.28",
         "schema-utils": "^4.0.0",
-        "selfsigned": "^1.10.11",
+        "selfsigned": "^2.0.0",
         "serve-index": "^1.9.1",
         "sockjs": "^0.3.21",
         "spdy": "^4.0.2",
@@ -20489,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",
@@ -20570,6 +20650,11 @@
       "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
       "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg=="
     },
+    "attr-accept": {
+      "version": "2.2.2",
+      "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz",
+      "integrity": "sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg=="
+    },
     "autoprefixer": {
       "version": "10.4.2",
       "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.2.tgz",
@@ -22917,6 +23002,14 @@
         "schema-utils": "^3.0.0"
       }
     },
+    "file-selector": {
+      "version": "0.2.4",
+      "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.2.4.tgz",
+      "integrity": "sha512-ZDsQNbrv6qRi1YTDOEWzf5J2KjZ9KMI1Q2SGeTkCJmNNW25Jg4TW4UMcmoqcg4WrAyKRcpBXdbWRxkfrOzVRbA==",
+      "requires": {
+        "tslib": "^2.0.3"
+      }
+    },
     "filelist": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.2.tgz",
@@ -23001,9 +23094,9 @@
       "integrity": "sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw=="
     },
     "follow-redirects": {
-      "version": "1.14.6",
-      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.6.tgz",
-      "integrity": "sha512-fhUl5EwSJbbl8AR+uYL2KQDxLkdSjZGR36xy46AO7cOMTrCMON6Sa28FmAnC2tRTDbd/Uuzz3aJBv7EBN7JH8A=="
+      "version": "1.14.7",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.7.tgz",
+      "integrity": "sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ=="
     },
     "forever-agent": {
       "version": "0.6.1",
@@ -23778,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",
@@ -25954,9 +26055,9 @@
       }
     },
     "node-forge": {
-      "version": "0.10.0",
-      "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz",
-      "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA=="
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.2.1.tgz",
+      "integrity": "sha512-Fcvtbb+zBcZXbTTVwqGA5W+MKBj56UjVRevvchv5XrcyXbmNdesfZL37nlcWOfpgHhgmxApw3tQbTr4CqNmX4w=="
     },
     "node-gyp": {
       "version": "8.4.1",
@@ -27465,6 +27566,16 @@
         "scheduler": "^0.20.2"
       }
     },
+    "react-dropzone": {
+      "version": "11.5.1",
+      "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-11.5.1.tgz",
+      "integrity": "sha512-eNhttdq4ZDe3eKbXAe54Opt+sbtqmNK5NWTHf/l5d+1TdZqShJ8gMjBrya00qx5zkI//TYxRhu1d9pemTgaWwg==",
+      "requires": {
+        "attr-accept": "^2.2.1",
+        "file-selector": "^0.2.2",
+        "prop-types": "^15.7.2"
+      }
+    },
     "react-error-overlay": {
       "version": "6.0.10",
       "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.10.tgz",
@@ -27599,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",
@@ -28159,11 +28280,11 @@
       "integrity": "sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo="
     },
     "selfsigned": {
-      "version": "1.10.11",
-      "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.11.tgz",
-      "integrity": "sha512-aVmbPOfViZqOZPgRBT0+3u4yZFHpmnIghLMlAcb5/xhp5ZtB/RVnKhz5vl2M32CLXAqR4kha9zfhNg0Lf/sxKA==",
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.0.0.tgz",
+      "integrity": "sha512-cUdFiCbKoa1mZ6osuJs2uDHrs0k0oprsKveFiiaBKCNq3SYyb5gs2HxhQyDNLCmL51ZZThqi4YNDpCK6GOP1iQ==",
       "requires": {
-        "node-forge": "^0.10.0"
+        "node-forge": "^1.2.0"
       }
     },
     "semver": {
@@ -29534,9 +29655,9 @@
       }
     },
     "webpack-dev-server": {
-      "version": "4.7.2",
-      "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.7.2.tgz",
-      "integrity": "sha512-s6yEOSfPpB6g1T2+C5ZOUt5cQOMhjI98IVmmvMNb5cdiqHoxSUfACISHqU/wZy+q4ar/A9jW0pbNj7sa50XRVA==",
+      "version": "4.7.3",
+      "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.7.3.tgz",
+      "integrity": "sha512-mlxq2AsIw2ag016nixkzUkdyOE8ST2GTy34uKSABp1c4nhjZvH90D5ZRR+UOLSsG4Z3TFahAi72a3ymRtfRm+Q==",
       "requires": {
         "@types/bonjour": "^3.5.9",
         "@types/connect-history-api-fallback": "^1.3.5",
@@ -29560,7 +29681,7 @@
         "p-retry": "^4.5.0",
         "portfinder": "^1.0.28",
         "schema-utils": "^4.0.0",
-        "selfsigned": "^1.10.11",
+        "selfsigned": "^2.0.0",
         "serve-index": "^1.9.1",
         "sockjs": "^0.3.21",
         "spdy": "^4.0.2",

+ 3 - 0
package.json

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

+ 62 - 144
src/App.js

@@ -1,14 +1,29 @@
 import './App.css';
 import * as action from './actions'
 import * as reducer from './reducers'
+import * as Logcomp from './components/Login'
+import * as Sidebar from './components/Sidebar'
+import * as Page from './components/Page'
 
 import thunk from 'redux-thunk';
-import { useEffect, useState } from 'react';
 import { createStore, combineReducers, applyMiddleware } from 'redux';
-import { Provider, connect } from 'react-redux';
-import { Link, Route, Router, Switch, Redirect } from 'react-router-dom';
+import { Provider } from 'react-redux';
+import { Link, Route, Router, Switch } from 'react-router-dom';
 import createHistory from 'history/createBrowserHistory'
 
+export function jwtDecode(token) {
+  try {
+    let decoded = token.split('.')
+    decoded = decoded[1]
+    decoded = atob(decoded)
+    decoded = JSON.parse(decoded)
+    return decoded
+  }
+  catch (e) {
+    return;
+  }
+}
+
 export const getGQL = url =>
   (query, variables = {}) =>
     fetch(url, {
@@ -25,8 +40,8 @@ export const getGQL = url =>
         return data.data[Object.keys(data.data)[0]]
       })
 
-const history = createHistory()
-const backendURL = "http://player.asmer.fs.a-level.com.ua"
+export const history = createHistory()
+export const backendURL = "http://player.asmer.fs.a-level.com.ua"
 export const gql = getGQL(backendURL + '/graphql')
 
 const store = createStore(
@@ -38,11 +53,9 @@ const store = createStore(
     }
   ), applyMiddleware(thunk)
 )
-
 store.subscribe(() => console.log(store.getState()))
-
 //works only once on start of page
-if(store.getState().auth?.token) {
+if (store.getState().auth?.token) {
   history.push('/player')
   store.dispatch(action.actionGetUserData())
   store.dispatch(action.actionGetUserPlaylists())
@@ -50,146 +63,51 @@ if(store.getState().auth?.token) {
   history.push('/login')
 }
 
-export function jwtDecode(token) {
-  try {
-    let decoded = token.split('.')
-    decoded = decoded[1]
-    decoded = atob(decoded)
-    decoded = JSON.parse(decoded)
-    return decoded
-
-  } catch (e) {
-    return;
-  }
-}
-
-const LoginForm = ({ loged, onLogin }) => {
-  let [login, setLogin] = useState()
-  let [password, setPassword] = useState()
-  let [log, setLog] = useState()
-
-  useEffect(() => {
-    setLog(loged)
-    if (log?.payload && localStorage.authToken) history.push('/player')
-  }, [loged, log])
-
+const PlayerBar = () => {
   return (
-    <>
-      <h1>Web-player</h1>
+    <footer>
       <div>
-        <h2>Log-in</h2>
-        <input type="text" placeholder='Login' onChange={(e) => setLogin(e.target.value)} />
-        <br />
-        <input type="password" placeholder='Password' onChange={(e) => setPassword(e.target.value)} />
-        <br />
-        <small style={{ color: 'red' }}>{loged.status === 'REJECTED' || (loged.status === 'RESOLVED' && !loged.payload) ? 'invalid login or password' : ''}</small>
-        <br />
-        <button
-          disabled={!password || !login}
-          onClick={() => { onLogin(login, password) }}
-        >Login</button>
-        <p>- OR -</p>
-        <Link to="/registration">Register new user</Link>
+        <button>{`<<`}</button>
+        <button>{`> / ||`}</button>
+        <button>{`>>`}</button>
+        <input type="range" />
       </div>
-    </>
+      <small>ARTIST - TRACK NAME</small>
+    </footer>
   )
 }
 
-const LoginFormConnect = connect(state => ({ loged: state.promise.login || {} }), { onLogin: action.actionFullLogin })(LoginForm)
-
-const RegisterForm = ({ onRegister }) => {
-  let [login, setLogin] = useState()
-  let [password, setPassword] = useState()
-  let [password2, setPassword2] = useState()
-
-  return (
-    <>
-      <h1>Web-player</h1>
-      <div>
-        <h2>Registration</h2>
-        <input type="text" placeholder='Login' onChange={(e) => setLogin(e.target.value)} />
-        <br />
-        <input type="password" placeholder='Password' onChange={(e) => setPassword(e.target.value)} />
-        <br />
-        <input disabled={!password} type="password" placeholder='Repeat Password' onChange={(e) => setPassword2(e.target.value)} />
-        <br />
-        <small style={{ color: 'red' }}>{password2 && password2 !== password ? 'Passwords do not match' : ''}</small>
-        <br />
-        <button disabled={!password || !login || password2 !== password} onClick={() => onRegister(login, password)}>Register</button>
-        <br />
-        <Link to="/login">Back to log-in page</Link>
-      </div>
-    </>
-  )
-}
-const RegisterFormConnect = connect(null, { onRegister: action.actionRegister })(RegisterForm)
-
-const Player = ({ user, playlists, onLogout }) => {
-  let [userInfo, setUserInfo] = useState(user.payload)
-  let [userPlaylists, setPlaylists] = useState(user.payload)
-
-
-  useEffect(()=> {
-    setUserInfo(user.payload)
-    setPlaylists(playlists.payload)
-    console.log(userPlaylists)
-  },[user, playlists, userInfo, userPlaylists])
-  
-  return (
-    <>
-      <header>Player</header>
-      <div style={{ display: 'flex' }}>
-        <aside style={{ border: '1px solid black', width: '20%' }}>
-          <div
-            style={{ border: '1px solid black', backgroundColor: 'red', color: 'white' }}
-            onClick={() => { onLogout(); history.push('/login') }}
-          >
-            log-out[X]
-          </div>
-          {/* profile window */}
-          <div style={{border:'1px solid chartreuse'}}>
-            <img 
-              width={100} 
-              height={100}  
-              style={{ border: '1px solid black', display:'block', margin:'5% auto'}}
-              src={ userInfo?.avatar?.url ? backendURL + '/' + userInfo?.avatar?.url : ''}
-              alt='avatar' 
-            />
-            <p>{userInfo?.login || 'user'}</p>
-          </div>
-
-          <div style={{ border: '1px solid black' }}>
-            <div style={{ border: '1px solid black', backgroundColor: 'mediumaquamarine' }}>My tracks(link)</div>
-            <div style={{backgroundColor: 'lightcyan'}}>
-              User playlists:
-              <div style={{ border: '1px solid black', backgroundColor: 'mediumseagreen' }}>Add plylist [+](button)</div>
-              {userPlaylists? userPlaylists.map(item => <div style={{backgroundColor: 'lightsteelblue', width:'100%', margin:'2% auto'}}>{item.name}</div>) : ''}
+const Player = () =>
+  <div>
+    <header><Link to="/player">Player</Link></header>
+    <div style={{ border:'1px solid blue',display: 'flex', height: '89vh', minHeight: '60vh', overflow:'none'}}>
+      <aside style={{width: '30%', overflow:'auto'}}>
+        <Sidebar.LogoutBtnConnect />
+        <Sidebar.ProfileWindowDropzoneConnect />
+        <Sidebar.UserTracksBtnConnect />
+        <Sidebar.PlaylistAddConnect />
+        <Sidebar.PlaylistsConnect />
+      </aside>
+      <main style={{ border: '1px solid red', width: '80%', height:'100%', overflow:'auto'}}>
+        <Switch>
+          <Route path="/player/playlist/:_id" component={Page.PlaylistPageConnect} exact />
+          <Route path="/player/tracks/:_id" component={Page.UserTracksPageConnect} exact />
+          <>
+            <h2>Welcome to online Player!</h2>
+            <div style={{ width: '50%', margin: '0 auto' }}>
+              <ul style={{ textAlign: 'start' }}>
+                <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>
-          </div>
-        </aside>
-        <main style={{ border: '1px solid black', width: '80%' }}>
-          MAIN
-          рут патх='путь' компонет=`компогнент отривоски`
-        </main>
-      </div>
-      <footer> back stop forw</footer>
-    </>
-  )
-}
-
-const PlayerConnect = connect(
-  state => ({
-    user: state.promise.userData || {},
-    playlists: state.promise.userPlaylists || {}
-    //playlists: state.promise.getPlaylists || {}
-  }),
-  { 
-    onLogout: action.actionAuthLogout,
-    //getUserData: action.actionGetUserData,
-    //getPlaylists: actionGetPlaylists
-  }
-)(Player)
-
+          </>
+        </Switch>
+      </main>
+    </div>
+    <PlayerBar />
+  </div>
 
 function App() {
   return (
@@ -197,9 +115,9 @@ function App() {
       <Provider store={store}>
         <div className="App">
           <Switch>
-            <Route path="/login" component={LoginFormConnect} exact />
-            <Route path="/registration" component={RegisterFormConnect} exact />
-            <Route path="/player" component={PlayerConnect} exact />
+            <Route path="/login" component={Logcomp.LoginFormConnect} exact />
+            <Route path="/registration" component={Logcomp.RegisterFormConnect} exact />
+            <Route path="/player" component={Player} />
           </Switch>
         </div>
       </Provider>

+ 97 - 8
src/actions/index.js

@@ -1,4 +1,4 @@
-import { jwtDecode, gql } from '../App'
+import { jwtDecode, gql, backendURL } from '../App'
 
 const actionPending = name => ({ type: 'PROMISE', status: 'PENDING', name })
 const actionResolved = (name, payload) => ({ type: 'PROMISE', status: 'RESOLVED', name, payload })
@@ -77,22 +77,111 @@ export const actionGetUserPlaylists = () => {
     )
 }
 
+export const actionGetPlaylistById = (_id/*='5fe35e5ce926687ee86b0a4f'*/) =>
+    actionPromise('playlistTracks', gql(`
+        query playlistById($playlistId: String!) {
+            PlaylistFind(query: $playlistId) {
+                _id, 
+                name,
+                tracks {
+
+                    _id, url, originalFileName,
+                    id3{ title, artist, album },
+                }
+            }
+        }
+    `, { playlistId: JSON.stringify([{ _id }]) }))
+
 export const actionGetUserTracks = () => {
     let _id = jwtDecode(localStorage.authToken).sub.id
+    //let _id = '5fe35e1ce926687ee86b0a3f' //newUserId
     return(
         actionPromise('userTracks', gql(`
             query getUserTracks($ownerId: String!) {
                 TrackFind(query: $ownerId) {
-                    _id,
-                    id3 {
-                        title, artist
-                    }
-                    playlists {
-                        _id, name
-                    }
+                    _id, originalFileName, url,
+                    id3 { title, artist, album }
                 }
             }
         `, { ownerId: JSON.stringify([{ ___owner: _id }]) } ))
     )
 }
 
+export const actionAddPlaylist = playlistName => 
+    async dispatch => {
+        await dispatch(actionPromise('addPlaylist', gql(`
+            mutation addPlaylist ($playlistName: String!){
+                PlaylistUpsert(playlist: {name: $playlistName}) {
+                    _id, name
+                }
+            }
+        `, {playlistName: playlistName})))
+        dispatch(actionGetUserPlaylists())
+    }
+
+export const actionLoadFile = (file, type) => {
+    let fd = new FormData()
+    console.log('TYPE', type)
+    fd.append(type === 'upload'? 'photo' : type, file)
+    
+    return (
+        actionPromise('loadFile', fetch(backendURL + `/${type}`,{
+        method: "POST",
+        headers: localStorage.authToken ? {Authorization: 'Bearer ' + localStorage.authToken} : {},
+        body: fd
+        })
+        .then(res => res.json())
+        )
+    )
+}
+
+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 {
+            console.log('UPLOADING TO PLAYLIS')
+            let updPlaylist = []
+            let oldPlaylist = getState().promise.playlistTracks.payload[0].tracks
+
+            if(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(actionUpdatePlaylist(playlistId, updPlaylist.reverse()))
+            dispatch(actionGetPlaylistById(playlistId))
+        }
+    }
+
+export const actionUploadAvatar = (file) =>
+    async (dispatch, getState) => {
+      await dispatch(actionLoadFile(file, 'upload'))
+      await dispatch(actionPromise('setAvatar', gql(`
+        mutation {
+          UserUpsert(user:{_id: "${jwtDecode(localStorage.authToken).sub.id}", avatar: {_id: "${getState().promise?.loadFile?.payload?._id}"}}){
+            _id, login, avatar{
+                _id, url
+            }
+          }
+        }
+      `)))
+      dispatch(actionGetUserData())
+    }

+ 67 - 0
src/components/Login/index.js

@@ -0,0 +1,67 @@
+import * as action from '../../actions'
+
+import { useEffect, useState } from 'react';
+import { connect } from 'react-redux';
+import { Link } from 'react-router-dom';
+import { history } from '../../App';
+
+const RegisterForm = ({ onRegister }) => {
+  let [login, setLogin] = useState()
+  let [password, setPassword] = useState()
+  let [password2, setPassword2] = useState()
+  return (
+    <>
+      <h1>Web-player</h1>
+      <div>
+        <h2>Registration</h2>
+        <input type="text" placeholder='Login' onChange={(e) => setLogin(e.target.value)} />
+        <br />
+        <input type="password" placeholder='Password' onChange={(e) => setPassword(e.target.value)} />
+        <br />
+        <input disabled={!password} type="password" placeholder='Repeat Password' onChange={(e) => setPassword2(e.target.value)} />
+        <br />
+        <small style={{ color: 'red' }}>{password2 && password2 !== password ? 'Passwords do not match' : ''}</small>
+        <br />
+        <button disabled={!password || !login || password2 !== password} onClick={() => onRegister(login, password)}>Register</button>
+        <br />
+        <Link to="/login">Back to log-in page</Link>
+      </div>
+    </>
+  )
+}
+export const RegisterFormConnect = connect(null, { onRegister: action.actionRegister })(RegisterForm)
+
+const LoginForm = ({ loged, onLogin }) => {
+  let [login, setLogin] = useState()
+  let [password, setPassword] = useState()
+  let [log, setLog] = useState()
+
+  useEffect(() => {
+    setLog(loged)
+    if (log?.payload && localStorage.authToken) history.push('/player')
+  }, [loged, log])
+
+  return (
+    <>
+      <h1>Web-player</h1>
+      <div>
+        <h2>Log-in</h2>
+        <input type="text" placeholder='Login' onChange={(e) => setLogin(e.target.value)} />
+        <br />
+        <input type="password" placeholder='Password' onChange={(e) => setPassword(e.target.value)} />
+        <br />
+        <small style={{ color: 'red' }}>
+          {loged.status === 'REJECTED' || (loged.status === 'RESOLVED' && !loged.payload)? 'invalid login or password' : ''}
+        </small>
+        <br />
+        <button
+          disabled={!password || !login}
+          onClick={() => { onLogin(login, password) }}
+        >Login</button>
+        <p>- OR -</p>
+        <Link to="/registration">Register new user</Link>
+      </div>
+    </>
+  )
+}
+export const LoginFormConnect = connect(state => ({ loged: state.promise.login || {} }), { onLogin: action.actionFullLogin })(LoginForm)

+ 120 - 0
src/components/Page/index.js

@@ -0,0 +1,120 @@
+import * as action from '../../actions'
+
+import { useEffect, useState, useCallback } from 'react';
+import { connect } from 'react-redux';
+import { useDropzone } from 'react-dropzone'
+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>
+    </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>
+      <SortableContainer onSortEnd={onSortEnd}>
+        {(_tracks || []).map((track, index) => <SortableItem index={index} track={track} />)}
+      </SortableContainer>
+    </>
+  )
+}
+export const PlaylistConnect = connect(
+  state => ({ playlist: state.promise.playlistTracks?.payload || [] }),
+  { updPlaylist: action.actionUpdatePlaylist }
+)(Playlist)
+
+const PlaylistTrackDropzone = ({ playlist, uploadTrack }) => {
+  let [playlistId, setPlaylistId] = useState()
+
+  useEffect(() => {
+    setPlaylistId(playlist[0]?._id)
+  }, [playlist])
+
+  const onDrop = useCallback(acceptedFiles => {
+    uploadTrack(acceptedFiles[0], playlistId)
+  }, [uploadTrack, playlistId])
+  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>
+  )
+}
+const PlaylistTrackDropzoneConnect = connect(
+  state => ({ playlist: state.promise.playlistTracks?.payload || [] }),
+  { uploadTrack: action.actionUploadUserTrack }
+)(PlaylistTrackDropzone)
+
+const PlaylistPage = ({ match: { params: { _id } }, getTracks }) => {
+  useEffect(() => { getTracks(_id.substring(1)) }, [_id, getTracks])
+  return (<PlaylistTrackDropzoneConnect />)
+}
+export const PlaylistPageConnect = connect(null, { getTracks: action.actionGetPlaylistById })(PlaylistPage)
+
+
+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 || {}
+})
+)(UserTracks)
+
+const UserTracksDropzone = ({ onLoad }) => {
+  const onDrop = useCallback(acceptedFiles => {
+    onLoad(acceptedFiles[0])
+  }, [onLoad])
+  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>
+  )
+}
+export const UserTrackDropzoneConnect = connect(null, { onLoad: action.actionUploadUserTrack })(UserTracksDropzone)
+
+const UserTracksPage = ({ match: { params: { _id } }, getUserTracks }) => {
+  useEffect(() => { getUserTracks() }, [_id, getUserTracks])
+  return (<UserTrackDropzoneConnect />)
+}
+export const UserTracksPageConnect = connect(null, { getUserTracks: action.actionGetUserTracks })(UserTracksPage)

+ 130 - 0
src/components/Sidebar/index.js

@@ -0,0 +1,130 @@
+import * as action from '../../actions'
+
+import { useState, useEffect, useCallback } from 'react';
+import { connect } from 'react-redux';
+import { Link } from 'react-router-dom';
+import { history, backendURL } from '../../App';
+import { useDropzone } from 'react-dropzone'
+
+const LogoutBtn = ({ onLogout }) =>
+  <div
+    style={{ border: '1px solid black', backgroundColor: 'red', color: 'white' }}
+    onClick={() => { onLogout(); history.push('/login') }}
+  >log-out[X]</div>
+
+export const LogoutBtnConnect = connect(null, { onLogout: action.actionAuthLogout })(LogoutBtn)
+
+const ProfileWindow = ({ user }) => {
+  let [userInfo, setUserInfo] = useState(user.payload)
+
+  useEffect(() => {
+    setUserInfo(user.payload)
+  }, [user, userInfo])
+
+  return (
+    <section>
+      <h3>{userInfo?.login || 'user'}</h3>
+      <img
+        width={100}
+        height={100}
+        style={{ border: '1px solid black', display: 'block', margin: '5% auto', marginBottom: '2px' }}
+        src={userInfo?.avatar?.url ? backendURL + '/' + userInfo?.avatar?.url : ''}
+        alt='avatar'
+      />
+    </section>
+  )
+}
+export const ProfileWindowConnect = connect(state => ({ user: state.promise.userData || {} }))(ProfileWindow)
+
+const ProfileWindowDropzone = ({ onLoad }) => {
+  const onDrop = useCallback(acceptedFiles => {
+    onLoad(acceptedFiles[0])
+  }, [onLoad])
+  const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop })
+
+  return (
+    <div {...getRootProps()} style={isDragActive ? { border: '1px solid mediumseagreen' } : { border: '1px solid black' }}>
+      <input {...getInputProps()} />
+      <ProfileWindowConnect />
+      {isDragActive ? <small>drag here...</small> : <small>change avatar</small>}
+    </div>
+  )
+}
+export const ProfileWindowDropzoneConnect = connect(null, { onLoad: action.actionUploadAvatar })(ProfileWindowDropzone)
+
+const UserTracksBtn = ({ userId }) => {
+  let [_id, setId] = useState()
+  useEffect(() => {
+    console.log('CHENG', userId)
+    setId(userId)
+  }, [userId])
+
+  return (
+    <Link
+      to={`/player/tracks/:${_id}`}
+      style={{
+        display: 'block',
+        backgroundColor: 'purple',
+        color: 'white',
+        margin: '5px',
+        padding: '5px'
+      }}
+    >My tracks</Link>
+  )
+}
+export const UserTracksBtnConnect = connect(state => ({ userId: state.promise.userData?.payload?._id || '' }))(UserTracksBtn)
+
+const PlaylistAdd = ({ addPlaylist }) => {
+  let [clicked, setClicked] = useState(false)
+  let [name, setName] = useState()
+  return (
+    <div>
+      {
+        !clicked ?
+          <button
+            style={{ border: '1px solid black', backgroundColor: 'mediumseagreen', width: '95%', padding: '5px', margin: '5px' }}
+            onClick={() => setClicked(true)}
+          >NEW PLAYLIST</button>
+          :
+          <div style={{ width: '95%', margin: '0 auto' }}>
+            <input
+              style={{ width: '72%', padding: '5px' }}
+              placeholder='Playlist name'
+              value={name}
+              onChange={(e) => setName(e.target.value)}
+            />
+
+            <button
+              disabled={!name}
+              style={{ padding: '5px', backgroundColor: 'mediumseagreen' }}
+              onClick={() => { addPlaylist(name); setClicked(false); setName(''); }}
+            >+</button>
+
+            <button
+              style={{ padding: '5px', backgroundColor: 'red' }}
+              onClick={() => { setClicked(false); setName('') }}
+            >X</button>
+          </div>
+      }
+    </div>
+  )
+}
+export const PlaylistAddConnect = connect(null, { addPlaylist: action.actionAddPlaylist })(PlaylistAdd)
+
+const Playlists = ({ playlists }) => {
+  return (
+    <div>
+      {
+        playlists?.payload ? playlists.payload.map(item => {
+          return (
+            <Link
+              style={{ display: 'block', backgroundColor:'darkcyan', color: 'cyan', margin: '5px', padding: '5px' }}
+              to={`${history.location.pathname}/playlist/:${item._id}`}
+            >{item.name}</Link>
+          )
+        }).reverse() : ''
+      }
+    </div>
+  )
+}
+export const PlaylistsConnect = connect(state => ({ playlists: state.promise.userPlaylists || {} }))(Playlists)