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

Transition to saga and some improvements

surstrommed пре 3 година
родитељ
комит
d4d57a28f2
14 измењених фајлова са 18289 додато и 197 уклоњено
  1. 17792 40
      package-lock.json
  2. 2 0
      package.json
  3. 162 7
      src/App.js
  4. 11 7
      src/App.scss
  5. 138 75
      src/actions/index.js
  6. 0 10
      src/components/Audio.js
  7. 11 14
      src/components/PlayerHeader.js
  8. 50 0
      src/components/Track.js
  9. 4 4
      src/helpers/index.js
  10. 0 3
      src/pages/Auth.js
  11. 16 2
      src/pages/Library.js
  12. 25 29
      src/pages/Profile.js
  13. 12 6
      src/pages/Search.js
  14. 66 0
      src/reducers/index.js

Разлика између датотеке није приказан због своје велике величине
+ 17792 - 40
package-lock.json


+ 2 - 0
package.json

@@ -15,12 +15,14 @@
     "react": "^17.0.2",
     "react-bootstrap": "^2.1.0",
     "react-dom": "^17.0.2",
+    "react-double-marquee": "^1.1.0",
     "react-dropzone": "^11.5.1",
     "react-redux": "^7.2.6",
     "react-router": "^5.2.0",
     "react-router-dom": "^5.2.0",
     "react-scripts": "5.0.0",
     "redux": "^4.1.2",
+    "redux-saga": "^1.1.3",
     "redux-thunk": "^2.4.1",
     "sass": "^1.45.2",
     "web-vitals": "^2.1.2"

+ 162 - 7
src/App.js

@@ -3,28 +3,183 @@ import { Header } from "./components/Header";
 import { Router } from "react-router-dom";
 import { Provider } from "react-redux";
 import { createStore, combineReducers, applyMiddleware } from "redux";
-import thunk from "redux-thunk";
 import { createBrowserHistory } from "history";
-import { promiseReducer, authReducer } from "./reducers/index";
+import {
+  promiseReducer,
+  authReducer,
+  localStoredReducer,
+} from "./reducers/index";
 import { Sidebar } from "./components/Sidebar";
 import "./App.scss";
-import { actionAboutMe } from "./actions";
+import createSagaMiddleware from "redux-saga";
+import { all, takeEvery, put, call, select } from "redux-saga/effects";
+import * as actions from "./actions";
 export const history = createBrowserHistory();
 
+const sagaMiddleware = createSagaMiddleware();
+
 export const store = createStore(
   combineReducers({
-    promise: promiseReducer,
-    auth: authReducer,
+    promise: localStoredReducer(promiseReducer, "promise"),
+    auth: localStoredReducer(authReducer, "auth"),
   }),
-  applyMiddleware(thunk)
+  applyMiddleware(sagaMiddleware)
 );
 
+function* promiseWorker(action) {
+  const { name, promise } = action;
+  yield put(actions.actionPending(name));
+  try {
+    let data = yield promise;
+    yield put(actions.actionResolved(name, data));
+    return data;
+  } catch (error) {
+    yield put(actions.actionRejected(name, error));
+  }
+}
+
+function* promiseWatcher() {
+  yield takeEvery("PROMISE_START", promiseWorker);
+}
+
+function* aboutMeWorker() {
+  // let { id } = getState().auth.payload.sub;
+  // await dispatch(actionFindUser(id));
+  let { id } = yield select().auth.payload.sub;
+  yield call(promiseWatcher, actions.actionFindUser(id));
+}
+
+function* aboutMeWatcher() {
+  yield takeEvery("ABOUT_ME", aboutMeWorker);
+}
+
+function* loginWorker({ login, password }) {
+  let token = yield call(promiseWorker, actions.actionLogin(login, password));
+  if (token) {
+    yield put(actions.actionAuthLogin(token));
+    yield put(actions.actionAboutMe());
+  }
+}
+
+function* loginWatcher() {
+  yield takeEvery("FULL_LOGIN", loginWorker);
+}
+
+function* registerWorker({ login, password }) {
+  let { _id } = yield call(
+    promiseWorker,
+    actions.actionRegister(login, password)
+  );
+  if (_id) {
+    let nick = login;
+    if (nick.includes("@")) {
+      nick = nick.substring(0, nick.indexOf("@"));
+      if (nick.length > 8) {
+        nick = nick.substring(0, 8);
+      }
+    }
+    yield call(promiseWorker, actions.actionLogin(login, password));
+    yield call(promiseWorker, actions.actionUserUpdate({ _id, nick }));
+  }
+}
+
+function* registerWatcher() {
+  yield takeEvery("FULL_REGISTER", registerWorker);
+}
+
+function* findTracksWorker() {
+  yield call(promiseWatcher, actions.actionFindTracks());
+}
+
+function* findTracksWatcher() {
+  yield takeEvery("FIND_TRACKS", findTracksWorker);
+}
+
+function* setAvatarWorker({ file }) {
+  // async (dispatch, getState) => {
+  //   let { _id } = await dispatch(actionUploadPhoto(file));
+  //   let { id } = getState().auth.payload.sub;
+  //   await dispatch(actionUserUpdate({ _id: id, avatar: { _id } }));
+  //   await dispatch(actionAboutMe());
+  // };
+  let { _id } = yield call(promiseWatcher, actions.actionSetAvatar(file));
+  let { id } = yield select().auth.payload.sub;
+  yield call(
+    promiseWatcher,
+    actions.actionUserUpdate({ _id: id, avatar: { _id } })
+  );
+}
+
+function* setAvatarWatcher() {
+  yield takeEvery("SET_AVATAR", setAvatarWorker);
+}
+
+function* setNicknameWorker({ _id, nick }) {
+  //   await dispatch(actionUserUpdate({ _id, nick }));
+  //   await dispatch(actionAboutMe());
+  yield call(promiseWatcher, actions.actionUserUpdate({ _id, nick }));
+  yield put(actions.actionAboutMe());
+}
+
+function* setNicknameWatcher() {
+  yield takeEvery("SET_NICKNAME", setNicknameWorker);
+}
+
+function* setEmailWorker({ _id, login }) {
+  //   await dispatch(actionUserUpdate({ _id, nick }));
+  //   await dispatch(actionAboutMe());
+  yield call(promiseWatcher, actions.actionUserUpdate({ _id, login }));
+  yield put(actions.actionAboutMe());
+}
+
+function* setEmailWatcher() {
+  yield takeEvery("SET_EMAIL", setEmailWorker);
+}
+
+function* setNewPasswordWorker({ login, password, newPassword }) {
+  //   await dispatch(actionChangePassword(login, password, newPassword));
+  //   await dispatch(actionAboutMe());
+  yield call(
+    promiseWatcher,
+    actions.actionChangePassword(login, password, newPassword)
+  );
+  yield put(actions.actionAboutMe());
+}
+
+function* setNewPasswordWatcher() {
+  yield takeEvery("SET_NEW_PASSWORD", setNewPasswordWorker);
+}
+
 if (localStorage.authToken) {
-  store.dispatch(actionAboutMe());
+  store.dispatch(actions.actionAboutMe());
+}
+
+if (
+  localStorage.authToken &&
+  (history.location.pathname === "/search" ||
+    history.location.pathname === "/library")
+) {
+  store.dispatch(actions.actionFindTracks());
 }
 
 store.subscribe(() => console.log(store.getState()));
 
+function* rootSaga() {
+  yield all([
+    promiseWatcher(),
+    loginWatcher(),
+    registerWatcher(),
+    aboutMeWatcher(),
+    findTracksWatcher(),
+    setAvatarWatcher(),
+    setNicknameWatcher(),
+    setEmailWatcher(),
+    setNewPasswordWatcher(),
+  ]);
+}
+
+sagaMiddleware.run(rootSaga);
+
 function App() {
   return (
     <Router history={history}>

+ 11 - 7
src/App.scss

@@ -67,24 +67,28 @@ body {
   padding: 25px 0;
   transition: box-shadow 400ms;
   display: flex;
-  justify-content: space-around;
 }
 
-.player-container > div {
+.player-container > span {
   text-decoration: underline $mainColor;
 }
 
 .sticky {
   position: fixed;
-  width: 70%;
-  left: 20%;
+  width: 10%;
   top: 0;
-  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.4);
 }
 
-.sticky > div {
+.sticky > span {
   text-decoration: none;
-  opacity: 0.7;
+}
+
+.header-section > div {
+  margin: 1vh;
+}
+
+.customAudio {
+  width: 70vh;
 }
 
 .btn-circle {

+ 138 - 75
src/actions/index.js

@@ -1,40 +1,5 @@
 import { backURL, gql } from "../helpers";
 
-export const actionPending = (name) => ({
-  type: "PROMISE",
-  status: "PENDING",
-  name,
-});
-
-export const actionResolved = (name, payload) => ({
-  type: "PROMISE",
-  status: "RESOLVED",
-  name,
-  payload,
-});
-
-export const actionRejected = (name, error) => ({
-  type: "PROMISE",
-  status: "REJECTED",
-  name,
-  error,
-});
-
-export const actionAuthLogin = (token) => ({ type: "AUTH_LOGIN", token });
-
-export const actionAuthLogout = () => ({ type: "AUTH_LOGOUT" });
-
-export const actionPromise = (name, promise) => async (dispatch) => {
-  dispatch(actionPending(name));
-  try {
-    let data = await promise;
-    dispatch(actionResolved(name, data));
-    return data;
-  } catch (error) {
-    dispatch(actionRejected(name, error));
-  }
-};
-
 export const actionUserUpdate = ({ _id, login, password, nick, avatar }) =>
   actionPromise(
     "userUpdate",
@@ -63,14 +28,15 @@ export const actionChangePassword = (login, password, newPassword) =>
   actionPromise(
     "changePassword",
     gql(
-      `query changePass($login:String!, $password:String!, $newPassword:String!){
+      `mutation changePass($login:String!, $password:String!, $newPassword:String!){
         changePassword(login:$login, password: $password, newPassword: $newPassword)
       }`,
-      { login, password, newPassword }
+      { login, password, newPassword },
+      true
     )
   );
 
-const actionLogin = (login, password) =>
+export const actionLogin = (login, password) =>
   actionPromise(
     "login",
     gql(
@@ -81,7 +47,7 @@ const actionLogin = (login, password) =>
     )
   );
 
-const actionRegister = (login, password) =>
+export const actionRegister = (login, password) =>
   actionPromise(
     "registration",
     gql(
@@ -111,41 +77,13 @@ export const actionFindUser = (_id) =>
     )
   );
 
-export const actionAboutMe = () => async (dispatch, getState) => {
-  let { id } = getState().auth.payload.sub;
-  await dispatch(actionFindUser(id));
-};
-
-export const actionFullLogin = (l, p) => async (dispatch) => {
-  let token = await dispatch(actionLogin(l, p));
-  if (token) {
-    await dispatch(actionAuthLogin(token));
-    await dispatch(actionAboutMe());
-  }
-};
-
-export const actionFullRegister = (l, p) => async (dispatch) => {
-  let { _id } = await dispatch(actionRegister(l, p));
-  if (_id) {
-    let nick = l;
-    if (nick.includes("@")) {
-      nick = nick.substring(0, nick.indexOf("@"));
-      if (nick.length > 8) {
-        nick = nick.substring(0, 8);
-      }
-    }
-    await dispatch(actionUserUpdate({ _id, nick }));
-    await dispatch(actionFullLogin(l, p));
-  }
-};
-
 export const actionFindTracks = () =>
   actionPromise(
     "tracks",
     gql(
       `query findTracks($q:String){
         TrackFind(query:$q){
-          _id url owner {
+          _id url originalFileName owner {
             _id login nick
           }
         }
@@ -171,7 +109,7 @@ export const actionFindUsers = () =>
     )
   );
 
-const actionUploadPhoto = (file) => {
+export const actionUploadPhoto = (file) => {
   let fd = new FormData();
   fd.append("photo", file);
   return actionPromise(
@@ -186,9 +124,134 @@ const actionUploadPhoto = (file) => {
   );
 };
 
-export const actionSetAvatar = (file) => async (dispatch, getState) => {
-  let { _id } = await dispatch(actionUploadPhoto(file));
-  let { id } = getState().auth.payload.sub;
-  await dispatch(actionUserUpdate({ _id: id, avatar: { _id } }));
-  await dispatch(actionAboutMe());
-};
+// ================================================
+
+export const actionPending = (name) => ({
+  type: "PROMISE",
+  status: "PENDING",
+  name,
+});
+
+export const actionResolved = (name, payload) => ({
+  type: "PROMISE",
+  status: "RESOLVED",
+  name,
+  payload,
+});
+
+export const actionRejected = (name, error) => ({
+  type: "PROMISE",
+  status: "REJECTED",
+  name,
+  error,
+});
+
+export const actionAuthLogin = (token) => ({ type: "AUTH_LOGIN", token });
+
+export const actionAuthLogout = () => ({ type: "AUTH_LOGOUT" });
+
+export const actionPromise = (name, promise) => ({
+  type: "PROMISE_START",
+  name,
+  promise,
+});
+// async (dispatch) => {
+//   dispatch(actionPending(name));
+//   try {
+//     let data = await promise;
+//     dispatch(actionResolved(name, data));
+//     return data;
+//   } catch (error) {
+//     dispatch(actionRejected(name, error));
+//   }
+// };
+
+export const actionAboutMe = () => ({
+  type: "ABOUT_ME",
+});
+// => async (dispatch, getState) => {
+//   let { id } = getState().auth.payload.sub; //select()
+//   await dispatch(actionFindUser(id));       //call(promiseWatcher, actionFindUser())
+// };
+
+export const actionFullLogin = (login, password) => ({
+  type: "FULL_LOGIN",
+  login,
+  password,
+});
+
+// async (dispatch) => {
+//   let token = await dispatch(actionLogin(l, p));
+//   if (token) {
+//     await dispatch(actionAuthLogin(token));
+//     await dispatch(actionAboutMe());
+//   }
+// };
+
+export const actionFullRegister = (login, password) => ({
+  type: "FULL_REGISTER",
+  login,
+  password,
+});
+// async (dispatch) => {
+//   let { _id } = await dispatch(actionRegister(l, p));
+//   if (_id) {
+//     let nick = l;
+//     if (nick.includes("@")) {
+//       nick = nick.substring(0, nick.indexOf("@"));
+//       if (nick.length > 8) {
+//         nick = nick.substring(0, 8);
+//       }
+//     }
+//     await dispatch(actionFullLogin(l, p));
+//     await dispatch(actionUserUpdate({ _id, nick }));
+//   }
+// };
+
+export const actionAllTracks = () => ({
+  type: "FIND_TRACKS",
+  // name,
+  // promise,
+});
+
+export const actionSetAvatar = (file) => ({
+  type: "SET_AVATAR",
+  file,
+});
+// async (dispatch, getState) => {
+//   let { _id } = await dispatch(actionUploadPhoto(file));
+//   let { id } = getState().auth.payload.sub;
+//   await dispatch(actionUserUpdate({ _id: id, avatar: { _id } }));
+//   await dispatch(actionAboutMe());
+// };
+
+export const actionSetNickname = ({ _id, nick }) => ({
+  type: "SET_NICKNAME",
+  _id,
+  nick,
+});
+// async (dispatch) => {
+//   await dispatch(actionUserUpdate({ _id, nick }));
+//   await dispatch(actionAboutMe());
+// };
+
+export const actionSetEmail = ({ _id, login }) => ({
+  type: "SET_EMAIL",
+  _id,
+  login,
+});
+// async (dispatch) => {
+//   await dispatch(actionUserUpdate({ _id, login }));
+//   await dispatch(actionAboutMe());
+// };
+
+export const actionSetNewPassword = (login, password, newPassword) => ({
+  type: "SET_NEW_PASSWORD",
+  login,
+  password,
+  newPassword,
+});
+// async (dispatch) => {
+//   await dispatch(actionChangePassword(login, password, newPassword));
+//   await dispatch(actionAboutMe());
+// };

+ 0 - 10
src/components/Audio.js

@@ -1,10 +0,0 @@
-import { Link } from "react-router-dom";
-import { PlayerHeader } from "./PlayerHeader";
-
-export const Audio = ({ personal }) => {
-  return (
-    <div>
-      <PlayerHeader personal={personal} />
-    </div>
-  );
-};

+ 11 - 14
src/components/PlayerHeader.js

@@ -1,7 +1,7 @@
 import React, { useState, useEffect } from "react";
 
 export const PlayerHeader = ({ personal }) => {
- const [offset, setOffset] = useState(0);
+  const [offset, setOffset] = useState(0);
   useEffect(() => {
     window.onscroll = () => {
       setOffset(window.pageYOffset);
@@ -9,18 +9,15 @@ export const PlayerHeader = ({ personal }) => {
   }, []);
 
   return (
-    <>
-      <header
-        className={`mt-5 header-section player-container ${
-          offset > 50 ? "sticky" : ""
-        }`}
-      >
-        <div>#</div>
-        <div>Название</div>
-        <div>{personal ? "Плейлист" : "Владелец"}</div>
-        <div>Дата добавления</div>
-        <div>Длительность</div>
-      </header>
-    </>
+    <nav
+      className={`mt-5 navbar ${offset > 50 ? "sticky" : ""}`}
+      style={{ width: "110%" }}
+    >
+      <div className="container-fluid player-container">
+        <span>#</span>
+        <span>Название</span>
+        <span>{personal ? "Плейлист" : "Владелец"}</span>
+      </div>
+    </nav>
   );
 };

+ 50 - 0
src/components/Track.js

@@ -0,0 +1,50 @@
+import { Button } from "react-bootstrap";
+import { connect } from "react-redux";
+import { backURL } from "../helpers/index";
+import { useState, useEffect, useRef } from "react";
+
+export const Track = ({ audio, index }) => {
+  const [reproduction, setReproduction] = useState(false);
+  let track = audio?.url ? `${backURL}/${audio.url}` : undefined;
+  let trackRef = useRef(new Audio(track));
+
+  useEffect(() => {
+    reproduction ? trackRef.current.play() : trackRef.current.pause();
+  }, [reproduction]);
+
+  function truncText(text) {
+    if (text.length > 40) {
+      return text.substring(0, 40) + "...";
+    } else return text;
+  }
+
+  return (
+    <div className="d-flex mt-5">
+      <div className="customAudio p-2 bg-dark text-white">
+        <span className="ml-3 d-inline-block">
+          {index + 1} |{" "}
+          <span>
+            {audio?.originalFileName
+              ? truncText(audio.originalFileName)
+              : undefined}
+          </span>
+        </span>
+      </div>
+      <Button onClick={() => setReproduction(!reproduction)}>
+        <i
+          className={`fas ${
+            reproduction ? "fa-pause-circle" : "fa-play-circle"
+          }`}
+        ></i>
+      </Button>
+      <Button>
+        <i className="fas fa-plus"></i>
+      </Button>
+    </div>
+  );
+};
+
+export const CAudio = connect(
+  (state) => ({ auth: state.auth, promise: state.promise }),
+  null
+)(Audio);

+ 4 - 4
src/helpers/index.js

@@ -10,14 +10,14 @@ export const jwtDecode = (token) => {
 
 export const getGQL =
   (url) =>
-  async (query, variables = {}) => {
+  async (query, variables = {}, skip = false) => {
     let obj = await fetch(url, {
       method: "POST",
       headers: {
         "Content-Type": "application/json",
-        Authorization: localStorage.authToken
-          ? "Bearer " + localStorage.authToken
-          : {},
+        ...(localStorage.authToken && !skip
+          ? { Authorization: "Bearer " + localStorage.authToken }
+          : {}),
       },
       body: JSON.stringify({ query, variables }),
     });

+ 0 - 3
src/pages/Auth.js

@@ -13,9 +13,6 @@ const Auth = ({ auth, promise, actionLogOut }) => {
   ) {
     history.push("/");
   }
-  if (auth.token) {
-    localStorage.authToken = auth.token;
-  }
   return (
     <>
       {auth.payload ? (

+ 16 - 2
src/pages/Library.js

@@ -3,7 +3,9 @@ import { AuthCheck } from "./../components/AuthCheck";
 import { history } from "./../App";
 import { actionFindTracks, actionFindUser } from "./../actions/index";
 import { CMyDropzone } from "../components/Dropzone";
-import { Audio } from "./../components/Audio";
+import { Track } from "../components/Track";
+import { PlayerHeader } from "./../components/PlayerHeader";
+import { Loader } from "./../components/Loader";
 
 const Library = ({ auth, promise, actionTracks, actionUser }) => {
   return (
@@ -14,7 +16,19 @@ const Library = ({ auth, promise, actionTracks, actionUser }) => {
             Ваша библиотека с музыкой, {promise?.user?.payload?.nick}
           </h1>
           <CMyDropzone />
-          <Audio personal />
+          <PlayerHeader personal />
+          <Track />
+          {promise?.tracks?.payload ? (
+            promise.tracks.payload.map((track, index) =>
+              track.owner._id === auth.payload.sub.id ? (
+                <Track audio={track} index={index} key={Math.random()} />
+              ) : (
+                <h2>В вашей библиотеке нет треков.</h2>
+              )
+            )
+          ) : (
+            <Loader />
+          )}
         </div>
       ) : (
         <div className="d-block mx-auto mt-2 container w-50">

+ 25 - 29
src/pages/Profile.js

@@ -11,25 +11,27 @@ import {
   validateNickname,
 } from "./../helpers/index";
 import {
-  actionUserUpdate,
-  actionAboutMe,
-  actionChangePassword,
+  actionSetNickname,
+  actionSetEmail,
+  actionSetNewPassword,
 } from "./../actions/index";
 import { Loader } from "../components/Loader";
 import { CMyDropzone } from "../components/Dropzone";
 import { Spoiler } from "../components/Spoiler";
 
-const Profile = ({ auth, promise, actionUpdate, changePassword, aboutMe }) => {
+const Profile = ({
+  auth,
+  promise,
+  nickUpdate,
+  emailUpdate,
+  changePassword,
+}) => {
   const [login, setLogin] = useState("");
   const [nick, setNickname] = useState("");
   const [password, setPassword] = useState("");
   const [newPassword, setNewPassword] = useState("");
   const [passwordShown, setPasswordShown] = useState(false);
 
-  const togglePassword = () => {
-    setPasswordShown(!passwordShown);
-  };
-
   return promise?.user?.status === "PENDING" ? (
     <Loader />
   ) : (
@@ -49,7 +51,7 @@ const Profile = ({ auth, promise, actionUpdate, changePassword, aboutMe }) => {
                 <form
                   action="/upload"
                   method="post"
-                  enctype="multipart/form-data"
+                  encType="multipart/form-data"
                   id="form"
                 >
                   <img
@@ -130,13 +132,9 @@ const Profile = ({ auth, promise, actionUpdate, changePassword, aboutMe }) => {
                             ? false
                             : true
                         }
-                        onClick={() => {
-                          actionUpdate({
-                            _id: promise?.user?.payload?._id,
-                            nick,
-                          });
-                          aboutMe();
-                        }}
+                        onClick={() =>
+                          nickUpdate({ _id: promise?.user?.payload?._id, nick })
+                        }
                       >
                         Сохранить
                       </Button>
@@ -202,13 +200,12 @@ const Profile = ({ auth, promise, actionUpdate, changePassword, aboutMe }) => {
                             ? false
                             : true
                         }
-                        onClick={() => {
-                          actionUpdate({
+                        onClick={() =>
+                          emailUpdate({
                             _id: promise?.user?.payload?._id,
                             login,
-                          });
-                          aboutMe();
-                        }}
+                          })
+                        }
                       >
                         Сохранить
                       </Button>
@@ -253,7 +250,7 @@ const Profile = ({ auth, promise, actionUpdate, changePassword, aboutMe }) => {
                       <Button
                         className="mt-2"
                         variant="secondary"
-                        onClick={togglePassword}
+                        onClick={() => setPasswordShown(!passwordShown)}
                       >
                         {`${passwordShown ? "Hide" : "Show"} passwords`}
                       </Button>
@@ -280,14 +277,13 @@ const Profile = ({ auth, promise, actionUpdate, changePassword, aboutMe }) => {
                       <Button
                         variant="success"
                         disabled={validatePassword(newPassword) ? false : true}
-                        onClick={() => {
+                        onClick={() =>
                           changePassword(
                             promise?.user?.payload?.login,
                             password,
                             newPassword
-                          );
-                          aboutMe();
-                        }}
+                          )
+                        }
                       >
                         Сохранить
                       </Button>
@@ -311,8 +307,8 @@ const Profile = ({ auth, promise, actionUpdate, changePassword, aboutMe }) => {
 export const CProfile = connect(
   (state) => ({ auth: state.auth, promise: state.promise }),
   {
-    actionUpdate: actionUserUpdate,
-    changePassword: actionChangePassword,
-    aboutMe: actionAboutMe,
+    emailUpdate: actionSetEmail,
+    nickUpdate: actionSetNickname,
+    changePassword: actionSetNewPassword,
   }
 )(Profile);

+ 12 - 6
src/pages/Search.js

@@ -2,20 +2,26 @@ import { connect } from "react-redux";
 import { SearchField } from "./../components/SearchField";
 import { AuthCheck } from "./../components/AuthCheck";
 import { history } from "./../App";
-import { Button } from "react-bootstrap";
 import { actionFindTracks, actionFindUser } from "./../actions/index";
-import { backURL } from "./../helpers/index";
-import { Audio } from "./../components/Audio";
+import { Track } from "../components/Track";
+import { PlayerHeader } from "./../components/PlayerHeader";
+import { Loader } from "./../components/Loader";
 
-const Search = ({ auth, actionTracks, actionUser }) => {
+const Search = ({ auth, promise }) => {
   return (
     <div className="SearchPage">
       {auth.token && history.location.pathname === "/search" ? (
         <div className="d-block mx-auto mt-2 container w-50">
           <h1>Поиск по сайту</h1>
           <SearchField />
-          <Button onClick={() => actionTracks()}>Tracks</Button>
-          <Audio />
+          <PlayerHeader />
+          {promise?.tracks?.payload ? (
+            promise.tracks.payload.map((track, index) => (
+              <Track audio={track} index={index} key={Math.random()} />
+            ))
+          ) : (
+            <Loader />
+          )}
         </div>
       ) : (
         <div className="d-block mx-auto mt-2 container w-50">

+ 66 - 0
src/reducers/index.js

@@ -37,3 +37,69 @@ export function authReducer(state, { type, token }) {
   }
   return state;
 }
+
+export const localStoredReducer =
+  (reducer, localStorageName) => (state, action) => {
+    if (!state && localStorage[localStorageName]) {
+      return JSON.parse(localStorage[localStorageName]);
+    } else {
+      let newState = reducer(state, action);
+      localStorage.setItem(localStorageName, JSON.stringify(newState));
+      return newState;
+    }
+  };
+
+// track: {_id, url, originalFileName}
+// playlist: {_id, name, tracks: [{_id}, {_id}, ...tracks]}
+
+export const playerReducer = (
+  state = {},
+  {
+    type,
+    track,
+    isPlaying,
+    isStopped,
+    duration,
+    playlist,
+    playlistIndex,
+    currentTime,
+    volume,
+  }
+) => {
+  if (type === "LOAD_TRACK") {
+    return {
+      ...state,
+      track,
+      duration,
+      playlist,
+      playlistIndex,
+    };
+  }
+  if (type === "PLAY_TRACK") {
+    return {
+      ...state,
+      isPlaying,
+      isStopped: !isPlaying,
+    };
+  }
+  if (type === "STOP_TRACK") {
+    return {
+      ...state,
+      isStopped,
+      isPlaying: !isStopped,
+    };
+  }
+  if (type === "VOLUME_TRACK") {
+    return {
+      ...state,
+      volume,
+    };
+  }
+  if (type === "TIME_TRACK") {
+    return {
+      ...state,
+      currentTime,
+    };
+  }
+  return state;
+};