Quellcode durchsuchen

use saga for player and async requests, add sign in, add private route, fixed player

Alyona Brytvina vor 2 Jahren
Ursprung
Commit
c163b4f594

+ 2 - 0
.eslintrc.js

@@ -33,6 +33,8 @@ module.exports = {
     'no-underscore-dangle': 'off',
     'no-case-declarations': 'off',
     'no-unused-expressions': 'off',
+    'eslint-disable-next-line': 'off',
+    'no-restricted-globals ': 'off',
     'no-confusing-arrow': 'off',
     'no-use-before-define': 'off',
     'no-return-await': 'off',

+ 1 - 1
.gitignore

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

Datei-Diff unterdrückt, da er zu groß ist
+ 678 - 0
package-lock.json


+ 3 - 0
package.json

@@ -12,10 +12,13 @@
     "@testing-library/user-event": "^13.5.0",
     "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",
     "redux": "^4.1.2",
+    "redux-saga": "^1.1.3",
+    "surge": "^0.23.1",
     "web-vitals": "^2.1.2"
   },
   "scripts": {

+ 14 - 0
src/api/auth.js

@@ -0,0 +1,14 @@
+import { getGql } from '../utils/getGql';
+import { BACKEND_URL } from '../constants';
+
+export const login = (payload) => {
+  const {login, password} = payload;
+  const gql = getGql(`${BACKEND_URL}/graphql`);
+  return gql(`
+      query ($login:String!, $password:String!) {
+       login(login:$login, password:$password)
+       }
+     `, {
+    login, password,
+  });
+};

+ 26 - 0
src/api/tracks.js

@@ -0,0 +1,26 @@
+import { getGql } from '../utils/getGql';
+import { BACKEND_URL } from '../constants';
+
+export const getTracksCount = () => {
+  const gql = getGql(`${BACKEND_URL}/graphql`);
+  return gql(`
+      query getCount {
+       TrackCount(query:"[{}]")
+      } 
+  `);
+};
+
+export const getTracksWithPage = (page = 1) => {
+  const gql = getGql(`${BACKEND_URL}/graphql`);
+  return gql(`
+      query skipTracks($query: String) {
+        TrackFind(query: $query) {
+       _id url originalFileName
+        }
+      }
+  `, {query: JSON.stringify([{}, {skip: [(page - 1) * 100]}])})
+    .then(data => data.map(track => ({
+      ...track,
+      url: `${BACKEND_URL}/${track.url}`,
+    })));
+};

+ 5 - 3
src/components/App/App.jsx

@@ -10,7 +10,8 @@ import { LoginPage } from '../../pages/LoginPage/LoginPage';
 import { PlaylistsPage } from '../../pages/PlaylistsPage/PlaylistsPage';
 import store from '../../store/store';
 import { theme } from '../../assets/theme';
-import { PaginationControlled } from '../PaginationControlled/PaginationControlled';
+import { RegisterPage } from '../../pages/RegisterPage/RegisterPage';
+import { PrivateRoute } from '../PrivateRoute/PrivateRoute';
 
 export const App = () => (
   <ThemeProvider theme={theme}>
@@ -18,9 +19,10 @@ export const App = () => (
       <BrowserRouter>
         <Header/>
         <Switch>
-          <Route exact path="/" component={MainPage}/>
-          <Route exact path="/playlists" component={PlaylistsPage}/>
+          <PrivateRoute exact path="/" component={MainPage}/>
+          <PrivateRoute exact path="/playlists" component={PlaylistsPage}/>
           <Route exact path="/login" component={LoginPage}/>
+          <Route exact path="/register" component={RegisterPage}/>
         </Switch>
         <Player/>
       </BrowserRouter>

+ 0 - 1
src/components/Dropzone/Dropzone.jsx

@@ -7,7 +7,6 @@ export const Dropzone = () => {
   const dispatch = useDispatch();
   const onDrop = useCallback(acceptedFiles => {
     dispatch(actionUploadFile(acceptedFiles[0]));
-    console.log(acceptedFiles);
   }, []);
   const {getRootProps, getInputProps, isDragActive} = useDropzone({onDrop});
 

+ 0 - 1
src/components/PaginationControlled/PaginationControlled.jsx

@@ -25,7 +25,6 @@ export const PaginationControlled = (tracks) => {
   }, []);
 
   const handleChange = (e) => {
-    console.log(page, e);
     setPage(page + 1);
   };
 

+ 21 - 59
src/components/Player/Player.jsx

@@ -9,15 +9,16 @@ import {
 import { useDispatch, useSelector } from 'react-redux';
 import { PlayerBar } from '../PlayerBar/PlayerBar';
 import {
-  actionPause, actionPlay, actionSetAudio, actionSetCurrentTrackId,
+  actionNextTrack,
+  actionPause, actionPlay, actionPreviousTrack,
 } from '../../store/types/playerTypes';
 
+const DEFAULT_VOLUME = 1;
 export const Player = () => {
   const playerState = useSelector(state => state.player);
   const dispatch = useDispatch();
-  const [volume, setVolume] = useState(1);
+  const [volume, setVolume] = useState(DEFAULT_VOLUME);
   const [muted, setMuted] = useState(false);
-  console.log(playerState);
 
   const onVolumeChange = (e) => {
     const volume = e.target.value / 100;
@@ -31,74 +32,35 @@ export const Player = () => {
   };
 
   const onBackward = () => {
-    const {trackList} = playerState;
-    const trackIndex = playerState.trackList.findIndex(track => track._id === playerState.currentPlayingTrackId);
-
-    if (trackIndex === 0) {
-      const findFirstTrack = playerState.trackList.find((track, index) => index === playerState.trackList.length - 1);
-      playerState.audio.pause();
-
-      const {url, _id} = findFirstTrack;
-      const audio = new Audio(url);
-      const id = _id;
-
-      dispatch(actionSetAudio(audio));
-      dispatch(actionPlay({trackList, id}));
-    } else {
-      const findPrevTrack = playerState.trackList.find((track, index) => index === trackIndex - 1);
-      playerState.audio.pause();
-
-      const {url, _id} = findPrevTrack;
-      const audio = new Audio(url);
-      const id = _id;
-
-      dispatch(actionSetAudio(audio));
-      dispatch(actionPlay({trackList, id}));
-    }
+    dispatch(actionPreviousTrack());
   };
 
   const onForward = () => {
-    const {trackList} = playerState;
-    console.log(trackList);
-    const trackIndex = trackList.findIndex(track => track._id === playerState.currentPlayingTrackId);
-
-    if (trackIndex === trackList.length - 1) {
-      const findFirstTrack = trackList.find((track, index) => index === 0);
-      playerState.audio.pause();
-
-      const {url, _id} = findFirstTrack;
-
-      const audio = new Audio(url);
-      const id = _id;
-
-      dispatch(actionSetAudio(audio));
-      dispatch(actionPlay({trackList, id}));
-    } else {
-      const findNextTrack = playerState.trackList.find((track, index) => index === trackIndex + 1);
-      playerState.audio.pause();
-
-      const {url, _id} = findNextTrack;
-
-      const audio = new Audio(url);
-      const id = _id;
-
-      dispatch(actionSetAudio(audio));
-      dispatch(actionPlay({trackList, id}));
-    }
+    dispatch(actionNextTrack());
   };
 
   const togglePlayPause = () => {
     const {trackList, currentPlayingTrackId} = playerState;
-    const id = currentPlayingTrackId;
+
     if (playerState.isPlaying) {
       dispatch(actionPause());
-      playerState.audio.pause();
-    } else if (playerState.isPlaying === false) {
-      dispatch(actionPlay({trackList, id}));
-      playerState.audio.play();
+    } else {
+      dispatch(actionPlay({trackList, id: currentPlayingTrackId}));
     }
   };
 
+  useEffect(() => {
+    playerState.audio?.addEventListener('ended', onForward);
+
+    return () => {
+      playerState.audio?.removeEventListener('ended', onForward);
+    };
+  }, [playerState.audio]);
+
+  useEffect(() => {
+    setVolume(DEFAULT_VOLUME);
+  }, [playerState.currentPlayingTrackId]);
+
   return (
     <Box sx={{
       width: '100%',

+ 4 - 3
src/components/PlayerBar/PlayerBar.jsx

@@ -2,15 +2,16 @@ import React, { useEffect, useState } from 'react';
 import {
   Box, Slider, styled, Typography,
 } from '@mui/material';
-import { useSelector } from 'react-redux';
+import { useDispatch, useSelector } from 'react-redux';
+import { actionChangeTime } from '../../store/types/playerTypes';
 
 export const PlayerBar = () => {
   const [position, setPosition] = useState(0);
   const playerState = useSelector(state => state.player);
-  console.log('render');
+  const dispatch = useDispatch();
 
   const onChange = e => {
-    playerState.audio.currentTime = e.target.value;
+    dispatch(actionChangeTime(e.target.value));
   };
 
   const formatDuration = value => {

+ 19 - 0
src/components/PrivateRoute/PrivateRoute.jsx

@@ -0,0 +1,19 @@
+import React from 'react';
+import { Redirect, Route } from 'react-router-dom';
+import { useSelector } from 'react-redux';
+
+export const PrivateRoute = ({component: Component, ...rest}) => {
+  const authToken = useSelector(state => state.auth.authToken) ?? null;
+  const isAuthenticated = authToken !== null;
+
+  return (
+    <Route
+      {...rest}
+      render={props => isAuthenticated ? (
+        <Component {...props}/>
+      ) : (
+        <Redirect to="/login"/>
+      )}
+    />
+  );
+};

+ 19 - 62
src/components/TrackList/TrackList.jsx

@@ -1,24 +1,22 @@
 import React, { useEffect, useState } from 'react';
 import {
   IconButton, List, ListItem, Typography,
-  Slider, Stack, Box, Pagination, PaginationItem,
+  Stack, Box, Pagination, CircularProgress,
 } from '@mui/material';
 import {
-  PauseRounded, PlayArrowRounded, PlayCircle, VolumeDown, VolumeUp,
+  PauseRounded, PlayArrowRounded,
 } from '@mui/icons-material';
 import { useDispatch, useSelector } from 'react-redux';
 import {
-  actionPause, actionPlay, actionSetAudio, actionSetCurrentTrackId,
+  actionPause, actionPlay,
 } from '../../store/types/playerTypes';
-import { getGql } from '../../utils/getGql';
-import { BACKEND_URL } from '../../constants';
+import { actionFetchTracks } from '../../store/types/trackTypes';
 
-export const TrackList = ({tracks, trackCount}) => {
+export const TrackList = ({tracks, trackCount, isLoading}) => {
   const dispatch = useDispatch();
   const playerState = useSelector(state => state.player);
-  const [trackList, setTrackList] = useState(tracks);
+
   const [page, setPage] = useState(1);
-  console.log(trackList, page);
 
   useEffect(() => {
     if (playerState.trackList.length === 0) {
@@ -26,10 +24,9 @@ export const TrackList = ({tracks, trackCount}) => {
     }
     if (playerState.isPlaying) {
       if (playerState.audio === null) {
-        const {url} = trackList.find(track => track._id === playerState.currentPlayingTrackId);
+        const {url} = playerState.trackList.find(track => track._id === playerState.currentPlayingTrackId);
         const audio = new Audio(url);
         audio.play();
-        dispatch(actionSetAudio(audio));
       } else {
         playerState.audio.play();
       }
@@ -38,72 +35,32 @@ export const TrackList = ({tracks, trackCount}) => {
     }
   }, [playerState.isPlaying]);
 
-  useEffect(() => {
-    console.log('here', 1);
-    if (playerState.audio !== null) {
-      console.log('here', 2);
-      if (playerState.audio.duration === playerState.audio.currentTime) {
-        console.log('here', 3);
-
-        playerState.audio.addEventListener('onended', togglePlayPause);
-
-        return () => {
-          playerState.audio.removeEventListener('onended', togglePlayPause);
-        };
-      }
-      ;
-    }
-  }, [playerState.audio?.currentTime]);
-
-  useEffect(() => {
-    if (playerState.audio !== null) {
-      console.log(trackList, playerState.currentPlayingTrackId);
-      const {url} = trackList.find(track => track._id === playerState.currentPlayingTrackId);
-      const audio = new Audio(url);
-      audio.play();
-      dispatch(actionSetAudio(audio));
-      const id = playerState.currentPlayingTrackId;
-      dispatch(actionPlay({
-        trackList, id,
-      }));
-    }
-  }, [playerState.currentPlayingTrackId]);
-
   const togglePlayPause = (id) => {
-    if (playerState.currentPlayingTrackId !== null && playerState.currentPlayingTrackId !== id) {
+    if (playerState.isPlaying) {
       playerState.audio.pause();
-      dispatch(actionSetCurrentTrackId(id));
-    } else if (playerState.isPlaying === false) {
-      dispatch(actionPlay({
-        trackList, id,
-      }));
+      if (playerState.currentPlayingTrackId !== id) {
+        dispatch(actionPlay({trackList: tracks, id}));
+      } else {
+        dispatch(actionPause());
+      }
     } else {
-      dispatch(actionPause());
+      dispatch(actionPlay({trackList: tracks, id}));
     }
   };
   useEffect(() => {
-    const gql = getGql(`${BACKEND_URL}/graphql`);
-    gql(`
-      query skipTracks($query: String) {
-        TrackFind(query: $query) {
-       _id url originalFileName
-        }
-      }
-  `, {query: JSON.stringify([{}, {skip: [(page - 1) * 100]}])})
-      .then(data => setTrackList(data.map(track => ({
-        ...track,
-        url: `${BACKEND_URL}/${track.url}`,
-      }))));
+    dispatch(actionFetchTracks(page));
   }, [page]);
 
   const handleChange = (e, value) => {
     setPage(value);
   };
 
-  return (
+  return isLoading ? (
+    <CircularProgress/>
+  ) : (
     <Box>
       <List>
-        {trackList.map(track => (
+        {tracks.map(track => (
           <ListItem key={track._id}>
             <IconButton
               onClick={() => togglePlayPause(track._id)}

+ 0 - 2
src/index.js

@@ -2,8 +2,6 @@ import React from 'react';
 import ReactDOM from 'react-dom';
 import { App } from './components/App/App';
 
-localStorage.authToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOnsiaWQiOiI2MWM2MWYwZWU5NDcyOTMzYTY3ODVlZmEiLCJsb2dpbiI6Imxlc2hhIiwiYWNsIjpbIjYxYzYxZjBlZTk0NzI5MzNhNjc4NWVmYSIsInVzZXIiXX0sImlhdCI6MTY0MDM3NDY3Mn0.51jdHqISRb19LqMYoEmlnLKWXi76r8b9nYwl2j4YLfg';
-
 ReactDOM.render(
   <App />,
   document.getElementById('root'),

+ 94 - 59
src/pages/LoginPage/LoginPage.jsx

@@ -1,65 +1,100 @@
-import React from 'react';
+import React, { useState } from 'react';
 import {
-  Avatar, Button, Checkbox, FormControlLabel, Grid, Paper, TextField, Typography,
+  Avatar, Button, Checkbox, FormControlLabel, Grid, OutlinedInput, Paper, TextField, Typography,
 } from '@mui/material';
 import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
 import './LoginPage.scss';
+import { Link } from 'react-router-dom';
+import { useDispatch } from 'react-redux';
+import { RegisterPage } from '../RegisterPage/RegisterPage';
+import { actionLogin, actionLoginSuccess } from '../../store/types/authTypes';
 
-export const LoginPage = () => (
-  <Paper elevation={10} className="paperStyle">
-    <Grid
-      alignItems="center"
-      justifyContent="center"
-      flexDirection="column"
-      container
-    >
-      <Grid item>
-        <Avatar>
-          <LockOutlinedIcon/>
-        </Avatar>
-      </Grid>
-      <Grid item>
-        <Typography variant="h4">
-          Sign in
-        </Typography>
+export const LoginPage = () => {
+  const dispatch = useDispatch();
+  const [login, setLogin] = useState(null);
+  const [password, setPassword] = useState(null);
+
+  const onChangeLogin = (e) => {
+    setLogin(e.target.value);
+  };
+
+  const onChangePassword = (e) => {
+    setPassword(e.target.value);
+  };
+
+  const signIn = () => {
+    dispatch(actionLogin({login, password}));
+  };
+
+  return (
+    <Paper elevation={10} className="paperStyle">
+      <Grid
+        alignItems="center"
+        justifyContent="center"
+        flexDirection="column"
+        container
+      >
+        <Grid item>
+          <Avatar>
+            <LockOutlinedIcon/>
+          </Avatar>
+        </Grid>
+        <Grid item>
+          <Typography
+            variant="h4"
+            margin="0 20px 0 10px"
+          >
+            Sign in
+          </Typography>
+        </Grid>
       </Grid>
-    </Grid>
-    <TextField
-      label="Username"
-      variant="standard"
-      placeholder="Enter username"
-      sx={{margin: '10px 0'}}
-      fullWidth
-      required
-    />
-    <TextField
-      label="Password"
-      variant="standard"
-      placeholder="Enter password"
-      fullWidth
-      required
-    />
-    <FormControlLabel
-      control={(
-        <Checkbox
-          color="primary"
-        />
-      )}
-      label="Remember me"
-    />
-    <Button
-      type="submit"
-      color="primary"
-      variant="contained"
-      sx={{
-        margin: '20px 0',
-      }}
-      fullWidth
-    >
-      Sign in
-    </Button>
-    <Typography>
-      Do you have an account?
-    </Typography>
-  </Paper>
-);
+      <TextField
+        label="Username"
+        variant="standard"
+        placeholder="Enter username"
+        sx={{margin: '10px 0'}}
+        onChange={onChangeLogin}
+        fullWidth
+        required
+      />
+      <TextField
+        label="Password"
+        variant="standard"
+        placeholder="Enter password"
+        onChange={onChangePassword}
+        fullWidth
+        required
+      />
+      <FormControlLabel
+        control={(
+          <Checkbox
+            color="primary"
+          />
+        )}
+        label="Remember me"
+      />
+      <Button
+        type="submit"
+        color="primary"
+        variant="contained"
+        sx={{
+          margin: '20px 0',
+        }}
+        onClick={signIn}
+        fullWidth
+      >
+        Sign in
+      </Button>
+      <Typography>
+        Do you have an account?
+        <Link
+          to="/register"
+        >
+          <Button>
+            Sign up
+          </Button>
+        </Link>
+      </Typography>
+    </Paper>
+  );
+};

+ 10 - 21
src/pages/MainPage/MainPage.jsx

@@ -1,34 +1,23 @@
 import React, { useEffect, useState } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
 import { TrackList } from '../../components/TrackList/TrackList';
-import { BACKEND_URL } from '../../constants';
-import { getGql } from '../../utils/getGql';
+import { actionFetchTracks } from '../../store/types/trackTypes';
 
 export const MainPage = () => {
-  const [tracks, setTracks] = useState([]);
-  const [trackCount, setTrackCount] = useState([]);
+  const dispatch = useDispatch();
+  const tracksState = useSelector(state => state.tracks);
 
   useEffect(() => {
-    const gql = getGql(`${BACKEND_URL}/graphql`);
-    gql(`
-      query allTracks {
-        TrackFind(query: "[{}]") {
-         _id url originalFileName
-        }
-      }
-  `).then(data => setTracks(data.map(track => ({...track, url: `${BACKEND_URL}/${track.url}`}))));
-  }, []);
-
-  useEffect(() => {
-    const gql = getGql(`${BACKEND_URL}/graphql`);
-    gql(`
-      query getCount{
-           TrackCount(query:"[{}]")
-             } `).then(response => setTrackCount(response));
+    dispatch(actionFetchTracks(1));
   }, []);
 
   return (
     <div>
-      <TrackList tracks={tracks} trackCount={trackCount}/>
+      <TrackList
+        tracks={tracksState.trackList}
+        trackCount={tracksState.totalCount}
+        isLoading={tracksState.isLoading}
+      />
     </div>
   );
 };

+ 73 - 0
src/pages/RegisterPage/RegisterPage.jsx

@@ -0,0 +1,73 @@
+import React from 'react';
+import {
+  Avatar, Button, Checkbox, FormControlLabel, Grid, Paper, TextField, Typography,
+} from '@mui/material';
+import { AddCircleOutline } from '@mui/icons-material';
+
+export const RegisterPage = () => (
+  <Paper elevation={10} className="paperStyle">
+    <Grid
+      alignItems="center"
+      justifyContent="center"
+      flexDirection="column"
+      container
+    >
+      <Grid item>
+        <Avatar>
+          <AddCircleOutline/>
+        </Avatar>
+      </Grid>
+      <Grid align="center" item>
+        <Typography
+          variant="h4"
+          margin="20px 0"
+        >
+          Sign up
+        </Typography>
+        <Typography variant="caption">
+          Please fill this form to create an account!
+        </Typography>
+      </Grid>
+    </Grid>
+    <TextField
+      label="Name"
+      variant="standard"
+      placeholder="Enter name"
+      sx={{margin: '10px 0'}}
+      fullWidth
+      required
+    />
+    <TextField
+      label="Password"
+      variant="standard"
+      placeholder="Enter password"
+      fullWidth
+      required
+    />
+    <TextField
+      label="Confirm password"
+      variant="standard"
+      placeholder="Confirm password"
+      fullWidth
+      required
+    />
+    <FormControlLabel
+      sx={{
+        marginRight: '0',
+      }}
+      control={<Checkbox name="Checked"/>}
+      label="I accept the terms and conditions"
+    />
+    <Button
+      type="submit"
+      color="primary"
+      variant="contained"
+      sx={{
+        margin: '20px 0',
+      }}
+      fullWidth
+    >
+      Sign up
+    </Button>
+  </Paper>
+);

+ 35 - 0
src/store/reducers/authReducer.js

@@ -0,0 +1,35 @@
+import types from '../types/authTypes';
+
+const initialState = {
+  login: '',
+  authToken: localStorage.getItem('authToken') ?? null,
+};
+
+export function authReducer(state = initialState, action) {
+  switch (action.type) {
+    case types.FETCH_LOGIN_SUCCESS:
+      return {
+        ...state,
+        login: action.payload.login,
+        authToken: action.payload.authToken,
+      };
+    case types.FETCH_LOGIN_FAIL:
+      return {
+        ...state,
+        errorMessage: action.payload,
+      };
+    case types.FETCH_REGISTER_SUCCESS:
+      return {
+        ...state,
+        login: action.payload.login,
+        authToken: action.payload.authToken,
+      };
+    case types.FETCH_REGISTER_FAIL:
+      return {
+        ...state,
+        errorMessage: action.payload,
+      };
+    default:
+      return state;
+  }
+}

+ 2 - 34
src/store/reducers/playerReducer.js

@@ -5,48 +5,16 @@ const initialState = {
   trackList: [],
   isPlaying: false,
   duration: 0,
-  currentTime: 0,
   audio: null,
   currentPlayingTrackId: null,
 };
 
 export function playerReducer(state = initialState, action) {
   switch (action.type) {
-    case types.PLAY:
-      const trackPlay = action.payload.trackList.find(track => track._id === action.payload.id);
-      console.log(action.payload.trackList.find(track => track._id === action.payload.id), action.payload.id);
+    case types.SET_PLAYER_STATE:
       return {
         ...state,
-        isPlaying: true,
-        trackList: action.payload.trackList,
-        currentPlayingTrackId: trackPlay._id,
-      };
-    case types.PAUSE:
-      return {
-        ...state,
-        isPlaying: false,
-      };
-    case types.SET_AUDIO:
-      return {
-        ...state,
-        audio: action.payload,
-      };
-    case types.SET_CURRENT_TRACK_ID:
-      return {
-        ...state,
-        currentPlayingTrackId: action.payload,
-      };
-    case types.SET_UPLOAD_FILE:
-      const fd = new FormData();
-      fd.append('track', action.payload);
-      fetch(`${BACKEND_URL}/track`, {
-        method: 'POST',
-        headers: localStorage.authToken ? {Authorization: `Bearer ${localStorage.authToken}`} : {},
-        body: fd,
-      }).then(response => console.log(response));
-      return {
-        ...state,
-        currentPlayingTrackId: action.payload,
+        ...action.payload,
       };
     default:
       return state;

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

@@ -0,0 +1,26 @@
+import types from '../types/trackTypes';
+
+const initialState = {
+  trackList: [],
+  totalCount: 0,
+  isLoading: false,
+};
+
+export function tracksReducer(state = initialState, action) {
+  switch (action.type) {
+    case types.FETCH_TRACKS:
+      return {
+        ...state,
+        isLoading: true,
+      };
+    case types.FETCH_TRACKS_SUCCESS:
+      return {
+        ...state,
+        trackList: action.payload.trackList,
+        totalCount: action.payload.totalCount,
+        isLoading: false,
+      };
+    default:
+      return state;
+  }
+}

+ 24 - 0
src/store/sagas/authSaga.js

@@ -0,0 +1,24 @@
+import {
+  call, put, takeLatest, select,
+} from 'redux-saga/effects';
+import types, {
+  actionLoginSuccess,
+  actionRegisterSuccess,
+  actionLoginFail,
+  actionRegisterFail,
+} from '../types/authTypes';
+import { login } from '../../api/auth';
+
+function* loginWorker(action) {
+  try {
+    const authToken = yield call(login, action.payload);
+    localStorage.setItem('authToken', authToken);
+    yield put(actionLoginSuccess({authToken, login: action.payload.login}));
+  } catch (e) {
+    yield put(actionLoginFail(e.message));
+  }
+}
+
+export function* authSaga() {
+  yield takeLatest(types.FETCH_LOGIN, loginWorker);
+}

+ 85 - 0
src/store/sagas/playerSaga.js

@@ -0,0 +1,85 @@
+import {
+  call, put, takeLatest, select,
+} from 'redux-saga/effects';
+import types, { actionPause, actionPlay, actionSetPlayerState } from '../types/playerTypes';
+
+function* playWorker(action) {
+  const {trackList, id} = action.payload;
+  const {url} = trackList.find(track => track._id === id);
+  const state = yield select(state => state.player);
+
+  if (id !== state.currentPlayingTrackId) {
+    if (state.audio !== null) {
+      state.audio.pause();
+    }
+  } else {
+    state.audio.play();
+    yield put(actionSetPlayerState({
+      isPlaying: true,
+    }));
+    return;
+  }
+
+  const audio = new Audio(url);
+  audio.play();
+
+  yield put(actionSetPlayerState({
+    trackList,
+    currentPlayingTrackId: id,
+    audio,
+    isPlaying: true,
+  }));
+}
+
+function* pauseWorker(action) {
+  const audio = yield select(state => state.player.audio);
+  audio.pause();
+  yield put(actionSetPlayerState({
+    isPlaying: false,
+  }));
+}
+
+function* previousTrackWorker(action) {
+  const state = yield select(state => state.player);
+  const {audio, trackList, currentPlayingTrackId} = state;
+  const trackIndex = trackList.findIndex(track => track._id === currentPlayingTrackId);
+
+  const previousTrack = trackIndex === 0
+    ? trackList.find((track, index) => index === trackList.length - 1)
+    : trackList.find((track, index) => index === trackIndex - 1);
+
+  audio.pause();
+
+  yield put(actionPlay({
+    trackList, id: previousTrack._id,
+  }));
+}
+
+function* nextTrackWorker(action) {
+  const state = yield select(state => state.player);
+  const {audio, trackList, currentPlayingTrackId} = state;
+  const trackIndex = trackList.findIndex(track => track._id === currentPlayingTrackId);
+
+  const nextTrack = trackIndex === trackList.length - 1
+    ? trackList.find((track, index) => index === 0)
+    : trackList.find((track, index) => index === trackIndex + 1);
+
+  audio.pause();
+
+  yield put(actionPlay({
+    trackList, id: nextTrack._id,
+  }));
+}
+
+function* changeTrackWorker(action) {
+  const audio = yield select(state => state.player.audio);
+  audio.currentTime = action.payload;
+}
+
+export function* playerSaga() {
+  yield takeLatest(types.PLAY, playWorker);
+  yield takeLatest(types.PAUSE, pauseWorker);
+  yield takeLatest(types.PREVIOUS_TRACK, previousTrackWorker);
+  yield takeLatest(types.NEXT_TRACK, nextTrackWorker);
+  yield takeLatest(types.CHANGE_TIME, changeTrackWorker);
+}

+ 20 - 0
src/store/sagas/tracksSaga.js

@@ -0,0 +1,20 @@
+import {
+  call, put, takeLatest, select,
+} from 'redux-saga/effects';
+import types, { actionFetchTracksFail, actionFetchTracksSuccess } from '../types/trackTypes';
+import { getTracksCount, getTracksWithPage } from '../../api/tracks';
+
+function* fetchTracksWorker(action) {
+  try {
+    const page = action.payload;
+    const totalCount = yield call(getTracksCount);
+    const trackList = yield call(getTracksWithPage, page);
+    yield put(actionFetchTracksSuccess({totalCount, trackList}));
+  } catch (e) {
+    yield put(actionFetchTracksFail());
+  }
+}
+
+export function* tracksSaga() {
+  yield takeLatest(types.FETCH_TRACKS, fetchTracksWorker);
+}

+ 31 - 2
src/store/store.js

@@ -1,10 +1,39 @@
-import { combineReducers, createStore } from 'redux';
+import {
+  combineReducers, createStore, compose, applyMiddleware,
+} from 'redux';
+import createSagaMiddleware from 'redux-saga';
+import { all } from 'redux-saga/effects';
 import { playerReducer } from './reducers/playerReducer';
+import { tracksSaga } from './sagas/tracksSaga';
+import { tracksReducer } from './reducers/tracksReducer';
+import { playerSaga } from './sagas/playerSaga';
+import { authReducer } from './reducers/authReducer';
+import { authSaga } from './sagas/authSaga';
+
+const sagaMiddleware = createSagaMiddleware();
 
 const rootReducer = combineReducers({
   player: playerReducer,
+  tracks: tracksReducer,
+  auth: authReducer,
 });
 
-const store = createStore(rootReducer);
+function* rootSaga() {
+  yield all([
+    tracksSaga(),
+    playerSaga(),
+    authSaga(),
+  ]);
+}
+
+const store = createStore(
+  rootReducer,
+  compose(
+    applyMiddleware(sagaMiddleware),
+    window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__(),
+  ),
+);
+
+sagaMiddleware.run(rootSaga);
 
 export default store;

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

@@ -0,0 +1,17 @@
+const types = {
+  FETCH_LOGIN_SUCCESS: 'FETCH_LOGIN_SUCCESS',
+  FETCH_LOGIN_FAIL: 'FETCH_REGISTER_FAIL',
+  FETCH_REGISTER_SUCCESS: 'FETCH_LOGIN_SUCCESS',
+  FETCH_REGISTER_FAIL: 'FETCH_REGISTER_FAIL',
+  FETCH_LOGIN: 'FETCH_LOGIN',
+  FETCH_REGISTER: 'FETCH_REGISTER',
+};
+
+export const actionLogin = (payload) => ({type: types.FETCH_LOGIN, payload});
+export const actionLoginSuccess = (payload) => ({type: types.FETCH_LOGIN_SUCCESS, payload});
+export const actionLoginFail = (payload) => ({type: types.FETCH_LOGIN_FAIL, payload});
+export const actionRegister = (payload) => ({type: types.FETCH_REGISTER, payload});
+export const actionRegisterSuccess = (payload) => ({type: types.FETCH_REGISTER_SUCCESS, payload});
+export const actionRegisterFail = (payload) => ({type: types.FETCH_REGISTER_FAIL, payload});
+
+export default types;

+ 10 - 2
src/store/types/playerTypes.js

@@ -4,12 +4,20 @@ const types = {
   SET_AUDIO: 'SET_AUDIO',
   SET_CURRENT_TRACK_ID: 'SET_CURRENT_TRACK_ID',
   SET_UPLOAD_FILE: 'SET_UPLOAD_FILE',
+  SET_PLAYER_STATE: 'SET_PLAYER_STATE',
+  PREVIOUS_TRACK: 'PREVIOUS_TRACK',
+  NEXT_TRACK: 'NEXT_TRACK',
+  CHANGE_TIME: 'CHANGE_TIME',
 };
 
 export const actionPause = (payload) => ({type: types.PAUSE, payload});
 export const actionPlay = (payload) => ({type: types.PLAY, payload});
-export const actionSetAudio = (payload) => ({type: types.SET_AUDIO, payload});
-export const actionSetCurrentTrackId = (payload) => ({type: types.SET_CURRENT_TRACK_ID, payload});
+// export const actionSetAudio = (payload) => ({type: types.SET_AUDIO, payload});
+// export const actionSetCurrentTrackId = (payload) => ({type: types.SET_CURRENT_TRACK_ID, payload});
 export const actionUploadFile = (payload) => ({type: types.SET_UPLOAD_FILE, payload});
+export const actionSetPlayerState = (payload) => ({type: types.SET_PLAYER_STATE, payload});
+export const actionPreviousTrack = (payload) => ({type: types.PREVIOUS_TRACK, payload});
+export const actionNextTrack = (payload) => ({type: types.NEXT_TRACK, payload});
+export const actionChangeTime = (payload) => ({type: types.CHANGE_TIME, payload});
 
 export default types;

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

@@ -0,0 +1,11 @@
+const types = {
+  FETCH_TRACKS: 'FETCH_TRACKS',
+  FETCH_TRACKS_SUCCESS: 'FETCH_TRACKS_SUCCESS',
+  FETCH_TRACKS_FAIL: 'FETCH_TRACKS_FAIL',
+};
+
+export const actionFetchTracks = (payload) => ({type: types.FETCH_TRACKS, payload});
+export const actionFetchTracksSuccess = (payload) => ({type: types.FETCH_TRACKS_SUCCESS, payload});
+export const actionFetchTracksFail = (payload) => ({type: types.FETCH_TRACKS_FAIL, payload});
+
+export default types;