Browse Source

Make a search and upload track in playlist

surstrommed 3 năm trước cách đây
mục cha
commit
115a54382c

+ 113 - 9
src/App.js

@@ -8,15 +8,24 @@ import {
   promiseReducer,
   authReducer,
   localStoredReducer,
+  searchReducer,
   playerReducer,
   routeReducer,
 } from "./reducers/index";
 import { Sidebar } from "./components/Sidebar";
 import "./App.scss";
 import createSagaMiddleware from "redux-saga";
-import { all, takeEvery, put, call, select } from "redux-saga/effects";
+import {
+  all,
+  takeEvery,
+  takeLatest,
+  put,
+  call,
+  select,
+} from "redux-saga/effects";
 import * as actions from "./actions";
-import { gql } from "./helpers";
+import { delay, gql } from "./helpers";
+import { CAudioController } from "./components/AudioController";
 
 export const history = createBrowserHistory();
 
@@ -28,6 +37,7 @@ export const store = createStore(
     auth: localStoredReducer(authReducer, "auth"),
     player: localStoredReducer(playerReducer, "player"),
     route: localStoredReducer(routeReducer, "route"),
+    search: searchReducer,
     // изменить условия на страницах на отображения по роутам
   }),
   applyMiddleware(sagaMiddleware)
@@ -46,12 +56,16 @@ function* rootSaga() {
     setNicknameWatcher(),
     setEmailWatcher(),
     setNewPasswordWatcher(),
+    searchWatcher(),
     audioPlayWatcher(),
     audioPauseWatcher(),
     audioVolumeWatcher(),
     audioLoadWatcher(),
     findPlaylistByOwnerWatcher(),
     createPlaylistWatcher(),
+    findPlaylistTracksWatcher(),
+    loadTracksToPlaylistWatcher(),
+    uploadTracksToPlaylistWatcher(),
   ]);
 }
 
@@ -202,6 +216,47 @@ function* setNewPasswordWatcher() {
   yield takeEvery("SET_NEW_PASSWORD", setNewPasswordWorker);
 }
 
+export function* searchWorker({ text }) {
+  yield put(actions.actionSearchResult({ payload: null }));
+  yield delay(2000);
+  let payload = yield gql(
+    `
+          query searchTracks($query: String){
+            TrackFind(query: $query) {
+              _id url originalFileName
+              id3 {
+                  title, artist
+              }
+              playlists {
+                  _id, name
+              }
+              owner {
+                _id login nick
+              }
+          }
+          }`,
+    {
+      query: JSON.stringify([
+        {
+          $or: [
+            { originalFileName: `/${text}/` },
+            { owner: { login: `/${text}/` } },
+          ],
+        },
+        {
+          sort: [{ name: 1 }],
+        },
+      ]),
+    }
+  );
+  yield put(actions.actionSearchResult({ payload }));
+  console.log("search end", text);
+}
+
+function* searchWatcher() {
+  yield takeLatest("SEARCH", searchWorker);
+}
+
 function* audioLoadWorker({ track, duration, playlist, playlistIndex }) {
   yield put(actions.actionLoadAudio(track, duration, playlist, playlistIndex));
 }
@@ -282,15 +337,58 @@ function* findUserTracksWatcher() {
   yield takeEvery("FIND_USER_TRACKS", findUserTracksWorker);
 }
 
+function* findPlaylistTracksWorker({ _id }) {
+  yield call(promiseWorker, actions.actionFindPlaylistTracks(_id));
+}
+
+function* findPlaylistTracksWatcher() {
+  yield takeEvery("PLAYLIST_TRACKS", findPlaylistTracksWorker);
+}
+
+function* loadTracksToPlaylistWorker({ idTrack, idPlaylist }) {
+  yield call(
+    promiseWorker,
+    actions.actionLoadTracksToPlaylist(idTrack, idPlaylist)
+  );
+}
+
+function* loadTracksToPlaylistWatcher() {
+  yield takeEvery("LOAD_TRACKS_PLAYLIST", loadTracksToPlaylistWorker);
+}
+
+export function* uploadTracksToPlaylistWorker({ file }) {
+  let idPlaylist = history.location.pathname.substring(
+    history.location.pathname.lastIndexOf("/")
+  );
+  if (file) {
+    let track = yield call(
+      promiseWorker,
+      actions.actionUploadFile(file, "track")
+    );
+    let idTrack = track?._id;
+    console.log(idTrack, idPlaylist);
+    yield put(actions.actionTracksToPlaylist(idTrack, idPlaylist));
+  }
+  yield put(actions.actionAboutMe());
+}
+
+export function* uploadTracksToPlaylistWatcher() {
+  yield takeEvery("UPLOAD_TRACKS", uploadTracksToPlaylistWorker);
+}
+
+if (
+  localStorage.authToken &&
+  history.location.pathname.includes("/myplaylist")
+) {
+  let { auth } = store.getState();
+  if (auth) {
+    let { id } = auth?.payload?.sub;
+    store.dispatch(actions.actionPlaylistTracks(id));
+  }
+}
+
 if (localStorage.authToken && history.location.pathname === "/library") {
-  console.log("Library");
   store.dispatch(actions.actionFindUserPlaylists());
-  // store.dispatch(actions.actionAboutMe());
-  // let { auth } = store.getState();
-  // if (auth) {
-  // let { id } = auth?.payload?.sub;
-  // store.dispatch(actions.actionTracksUser("5fe35e5be926687ee86b0a49"));
-  // }
 }
 
 if (localStorage.authToken && history.location.pathname === "/") {
@@ -306,6 +404,12 @@ function App() {
           <Header />
           <Sidebar />
           <Main />
+          <CAudioController
+            name="name"
+            currentTime="currentTime"
+            duration="duration"
+            volume="volume"
+          />
         </div>
       </Provider>
     </Router>

+ 3 - 0
src/App.scss

@@ -6,6 +6,7 @@ body {
   background-color: #34393d;
   color: white;
   font-family: "Catamaran", sans-serif;
+  height: 140vh;
 }
 
 .spoilerText {
@@ -36,6 +37,7 @@ body {
 }
 
 .customBorder {
+  width: 50%;
   border-width: 1px;
   border-style: dashed !important;
 }
@@ -77,6 +79,7 @@ body {
 .sticky {
   position: fixed;
   display: inline-block;
+  top: 0;
   width: 50%;
   font-size: 12px;
 }

+ 64 - 15
src/actions/index.js

@@ -95,21 +95,6 @@ export const actionFindUsers = () =>
     )
   );
 
-export const actionUploadFile = (file, type = "photo") => {
-  let fd = new FormData();
-  fd.append(type, file);
-  return actionPromise(
-    "uploadFile",
-    fetch(`${backURL}/upload`, {
-      method: "POST",
-      headers: localStorage.authToken
-        ? { Authorization: "Bearer " + localStorage.authToken }
-        : {},
-      body: fd,
-    }).then((res) => res.json())
-  );
-};
-
 export const actionFindTracks = () =>
   actionPromise(
     "tracks",
@@ -186,6 +171,47 @@ export const actionCreatePlaylist = (name) =>
     )
   );
 
+export const actionFindPlaylistTracks = (_id) =>
+  actionPromise(
+    "playlistTracks",
+    gql(
+      `query playlistTracks($q:String) {
+ PlaylistFindOne(query:$q) {
+ _id name tracks {_id url originalFileName} owner {_id login}
+ }
+}`,
+      { q: JSON.stringify([{ _id }]) }
+    )
+  );
+
+export const actionLoadTracksToPlaylist = (idTrack, idPlaylist) =>
+  actionPromise(
+    "loadTracksToPlaylist",
+    gql(
+      `mutation loadTracksToPlaylist($playlist:PlaylistInput) {
+ PlaylistUpsert(playlist:$playlist) {
+ _id name tracks {_id url originalFileName} owner {_id login}
+ }
+}`,
+      { playlist: { _id: idPlaylist, tracks: { _id: idTrack } } }
+    )
+  );
+
+export const actionUploadFile = (file, type = "photo") => {
+  let fd = new FormData();
+  fd.append(type, file);
+  return actionPromise(
+    `${type === "photo" ? "uploadPhoto" : "uploadTrack"}`,
+    fetch(`${backURL}/${type === "photo" ? "upload" : "track"}`, {
+      method: "POST",
+      headers: localStorage.authToken
+        ? { Authorization: "Bearer " + localStorage.authToken }
+        : {},
+      body: fd,
+    }).then((res) => res.json())
+  );
+};
+
 // ================================================
 
 export const actionPending = (name) => ({
@@ -312,6 +338,13 @@ export const actionSetNewPassword = (login, password, newPassword) => ({
 //   await dispatch(actionAboutMe());
 // };
 
+export const actionSearch = (text) => ({ type: "SEARCH", text });
+
+export const actionSearchResult = (payload) => ({
+  type: "SEARCH_RESULT",
+  payload,
+});
+
 export const actionLoadAudio = (track, duration, playlist, playlistIndex) => ({
   type: "LOAD_TRACK",
   track,
@@ -350,3 +383,19 @@ export const actionCreateNewPlaylist = (name) => ({
   type: "CREATE_PLAYLIST",
   name,
 });
+
+export const actionPlaylistTracks = (_id) => ({
+  type: "PLAYLIST_TRACKS",
+  _id,
+});
+
+export const actionTracksToPlaylist = (idTrack, idPlaylist) => ({
+  type: "LOAD_TRACKS_PLAYLIST",
+  idTrack,
+  idPlaylist,
+});
+
+export const actionUploadTracks = (file) => ({
+  type: "UPLOAD_TRACKS",
+  file,
+});

+ 11 - 6
src/components/Dropzone.js

@@ -1,19 +1,23 @@
 import React, { useCallback } from "react";
 import { useDropzone } from "react-dropzone";
 import { connect } from "react-redux";
-import { actionSetAvatar } from "../actions";
+import { actionSetAvatar, actionUploadTracks } from "../actions";
 
-const MyDropzone = ({ onload }) => {
+const MyDropzone = ({ onloadAvatar, onloadMusic }) => {
   const onDrop = useCallback(
     (acceptedFiles) => {
-      onload(acceptedFiles[0]);
+      if (acceptedFiles[0].type.includes("audio")) {
+        onloadMusic(acceptedFiles[0]);
+      } else {
+        onloadAvatar(acceptedFiles[0]);
+      }
     },
-    [onload]
+    [onloadAvatar, onloadMusic]
   );
   const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop });
 
   return (
-    <div className="mt-2 text-center customBorder" {...getRootProps()}>
+    <div className="mt-2 text-center customBorder mx-auto" {...getRootProps()}>
       <input {...getInputProps()} />
       {isDragActive ? (
         <p>Поместите файлы сюда...</p>
@@ -28,5 +32,6 @@ const MyDropzone = ({ onload }) => {
 };
 
 export const CMyDropzone = connect(null, {
-  onload: actionSetAvatar,
+  onloadAvatar: actionSetAvatar,
+  onloadMusic: actionUploadTracks,
 })(MyDropzone);

+ 21 - 20
src/components/Playlist.js

@@ -7,25 +7,25 @@ import { PlayerHeader } from "./PlayerHeader";
 import { Loader } from "./Loader";
 import { CTrack } from "./Track";
 import { Button } from "react-bootstrap";
+import { backURL } from "./../helpers/index";
 
-// const Track = ({ track: { _id, url, originalFileName } = {} }) => (
-//   <div className="Tracks">
-//     <audio controls src={backURL + "/" + url}></audio>{" "}
-//     <strong>{originalFileName}</strong>
-//   </div>
-// );
+const PlaylistTracks = ({ promise, tracks: { _id, url } = {} }) => (
+  <div>
+    {promise?.uploadTrack?.payload?.length !== 0 ? (
+      <div className="d-block mx-auto mt-2 container w-50">
+        <PlayerHeader personal />
+      </div>
+    ) : null}
+  </div>
+);
+
+// <CTrack audio={} index={1} key={Math.random()} />
 
-// const MyTracks = ({ tracks } = {}) => (
-//   <div>
-//     {(tracks || []).map((track) => (
-//       <Track track={track} />
-//     ))}
-//   </div>
-// );
+const CPlaylistTracks = connect((state) => ({
+  promise: state.promise,
+  tracks: state.promise.uploadTrack?.payload || [],
+}))(PlaylistTracks);
 
-// const CMyTracks = connect((state) => ({
-//   tracks: state.promise.trackFindByPlaylist?.payload || [],
-// }))(MyTracks);
 // {promise?.userTracks?.payload?.length !== 0 ? (
 //   <PlayerHeader personal />
 // ) : null}
@@ -45,6 +45,7 @@ import { Button } from "react-bootstrap";
 const MyPlaylistTracks = ({ promise }) => (
   <>
     <CMyDropzone />
+    <CPlaylistTracks />
   </>
 );
 
@@ -95,7 +96,7 @@ const MyPlaylists = ({ promise, onPlaylistCreate }) => {
       <hr />
       <form>
         <div className="mb-3">
-          <label forHtml="playlistCreate" className="form-label">
+          <label forhtml="playlistCreate" className="form-label">
             Создание нового плейлиста:
           </label>
           <input
@@ -111,7 +112,7 @@ const MyPlaylists = ({ promise, onPlaylistCreate }) => {
         <button
           type="submit"
           className="btn btn-primary"
-          disabled={playlist.length < 2 || playlist.length > 10 ? true : false}
+          disabled={playlist.length > 1 && playlist.length < 11 ? false : true}
           onClick={() => {
             onPlaylistCreate(playlist);
           }}
@@ -122,8 +123,8 @@ const MyPlaylists = ({ promise, onPlaylistCreate }) => {
       <hr />
       <div className="Playlists">
         <ul>
-          {promise?.userPlaylists?.payload.map((playlist) => (
-            <Playlist playlist={playlist} />
+          {(promise?.userPlaylists?.payload || []).map((playlist) => (
+            <Playlist playlist={playlist} key={Math.random()} />
           ))}
         </ul>
       </div>

+ 0 - 21
src/components/SearchField.js

@@ -1,21 +0,0 @@
-import { Button } from "react-bootstrap";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { faSearch } from "@fortawesome/free-solid-svg-icons";
-
-export const SearchField = () => {
-  return (
-    <div className="input-group rounded">
-      <input
-        type="search"
-        className="form-control rounded"
-        placeholder="Поиск музыки"
-        aria-label="Поиск"
-        aria-describedby="search-addon"
-      />
-    </div>
-  );
-};
-
-// <Button variant="primary" id="search-addon">
-// <FontAwesomeIcon icon={faSearch} />
-// </Button>

+ 13 - 0
src/components/SearchResult.js

@@ -0,0 +1,13 @@
+import { connect } from "react-redux";
+import { CTrack } from "./Track";
+
+const SearchResult = ({ search }) => {
+  return (search?.searchResult?.payload?.payload || []).map((track, index) => (
+    <CTrack audio={track} index={index} key={Math.random()} />
+  ));
+};
+
+export const CSearchResult = connect(
+  (state) => ({ search: state.search || [] }),
+  null
+)(SearchResult);

+ 1 - 8
src/components/Track.js

@@ -69,7 +69,7 @@ const Track = ({
           ></i>
         </Button>
         <Button>
-        <i class="fas fa-download"></i>
+          <i className="fas fa-download"></i>
         </Button>
         <div className="ml-5">
           Загрузил:{" "}
@@ -78,13 +78,6 @@ const Track = ({
           </Link>
         </div>
       </div>
-
-      <CAudioController
-        name={audio?.originalFileName}
-        currentTime={currentTime}
-        duration={duration}
-        volume={volume}
-      />
     </>
   );
 };

+ 2 - 0
src/helpers/index.js

@@ -46,6 +46,8 @@ export function validateNickname(nick) {
   return /^[a-z0-9_-]{3,8}$/.test(nick);
 }
 
+export const delay = (ms) => new Promise((ok) => setTimeout(() => ok(ms), ms));
+
 export const useLocalStoredState = (defaultState, localStorageName) => {
   let payload;
   try {

+ 34 - 4
src/pages/Search.js

@@ -1,12 +1,36 @@
 import { connect } from "react-redux";
-import { SearchField } from "./../components/SearchField";
+import { CSearchResult } from "../components/SearchResult";
 import { AuthCheck } from "./../components/AuthCheck";
 import { history } from "./../App";
 import { CTrack } from "../components/Track";
 import { PlayerHeader } from "./../components/PlayerHeader";
 import { Loader } from "./../components/Loader";
+import { useState } from "react";
+import { actionSearch } from "./../actions/index";
 
-const Search = ({ auth, promise }) => {
+const SearchField = connect(null, { onChangeSearch: actionSearch })(
+  ({ onChangeSearch }) => {
+    const [text, setText] = useState("");
+    return (
+      <div className="input-group rounded">
+        <input
+          type="search"
+          className="form-control rounded"
+          placeholder="Поиск музыки"
+          aria-label="Поиск"
+          aria-describedby="search-addon"
+          value={text}
+          onChange={(e) => {
+            setText(e.target.value);
+            onChangeSearch(text);
+          }}
+        />
+      </div>
+    );
+  }
+);
+
+const Search = ({ search, auth, promise }) => {
   return (
     <div className="SearchPage">
       {auth.token && history.location.pathname === "/search" ? (
@@ -14,7 +38,9 @@ const Search = ({ auth, promise }) => {
           <h1 className="text-center">Поиск по сайту</h1>
           <SearchField />
           {promise?.tracks?.payload?.length !== 0 ? <PlayerHeader /> : null}
-          {promise.tracks.status === "PENDING" ? (
+          {search?.searchResult?.payload?.payload ? (
+            <CSearchResult />
+          ) : promise.tracks.status === "PENDING" ? (
             <Loader />
           ) : promise?.tracks?.payload &&
             promise?.tracks?.payload?.length !== 0 ? (
@@ -40,6 +66,10 @@ const Search = ({ auth, promise }) => {
 };
 
 export const CSearch = connect(
-  (state) => ({ auth: state.auth, promise: state.promise }),
+  (state) => ({
+    search: state.search,
+    auth: state.auth,
+    promise: state.promise,
+  }),
   null
 )(Search);

+ 7 - 0
src/reducers/index.js

@@ -52,6 +52,13 @@ export const localStoredReducer =
 // track: {_id, url, originalFileName}
 // playlist: {_id, name, tracks: [{_id}, {_id}, ...tracks]}
 
+export const searchReducer = (state={}, {type, ...params}) => {
+  if (type === 'SEARCH_RESULT'){
+      return {searchResult: {...params}}
+  }
+  return state//, в таком случае вызываются все редьюсеры, но далеко не всегда action.type будет относится к этому редьюсеру. Тогда редьюсер должен вернуть state как есть. 
+}
+
 export const playerReducer = (
   state = {},
   {