Browse Source

last updates

Rostyslav Nahornyi 1 year ago
parent
commit
72f597668f
78 changed files with 7534 additions and 348 deletions
  1. 7 0
      craco.config.js
  2. 3593 156
      package-lock.json
  3. 11 4
      package.json
  4. 1 0
      public/index.html
  5. 10 2
      src/App.jsx
  6. 13 0
      src/Components/AddButton/AddButton.jsx
  7. 44 0
      src/Components/AddButton/style.scoped.scss
  8. 60 0
      src/Components/DropdownTracks/DropdownTracks.jsx
  9. 291 0
      src/Components/DropdownTracks/style.scoped.scss
  10. 17 17
      src/Components/LeftBar/LeftBar.jsx
  11. 48 0
      src/Components/LeftBar/style.scoped.scss
  12. 13 0
      src/Components/OrangeButton/OrangeButton.jsx
  13. 38 0
      src/Components/OrangeButton/style.scoped.scss
  14. 52 0
      src/Components/PaginationBar/PaginationBar.jsx
  15. 80 60
      src/Components/Player/Player.jsx
  16. 176 0
      src/Components/Player/style.scoped.scss
  17. 84 0
      src/Components/PlaylistCover/PlaylistCover.jsx
  18. 107 0
      src/Components/PlaylistCover/style.scoped.scss
  19. 67 0
      src/Components/PlaylistInfo/PlaylistInfo.jsx
  20. 113 0
      src/Components/PlaylistInfo/style.scoped.scss
  21. 29 0
      src/Components/PlaylistsTrack/PlaylistsTrack.jsx
  22. 39 0
      src/Components/PlaylistsTrack/style.scoped.scss
  23. 2 2
      src/Components/PrivateRoute/PrivateRoute.jsx
  24. 103 0
      src/Components/ProfileData/ProfileData.jsx
  25. 76 0
      src/Components/ProfileData/style.scoped.scss
  26. 39 0
      src/Components/QueueItem/QueueItem.jsx
  27. 70 0
      src/Components/QueueItem/style.scoped.scss
  28. 70 0
      src/Components/SearchBar/SearchBar.jsx
  29. 10 7
      src/Components/Tab/Tab.jsx
  30. 27 0
      src/Components/Tab/style.scoped.scss
  31. 100 0
      src/Components/TrackItem/TrackItem.jsx
  32. 56 0
      src/Components/TrackItem/style.scoped.scss
  33. 24 4
      src/Components/index.js
  34. 44 7
      src/Pages/Home/Home.jsx
  35. 67 0
      src/Pages/Home/style.scoped.scss
  36. 68 0
      src/Pages/PlaylistItem/PlaylistItem.jsx
  37. 61 0
      src/Pages/PlaylistItem/style.scoped.scss
  38. 66 8
      src/Pages/Playlists/Playlists.jsx
  39. 59 0
      src/Pages/Playlists/style.scoped.scss
  40. 26 9
      src/Pages/Profile/Profile.jsx
  41. 60 0
      src/Pages/Profile/style.scoped.scss
  42. 69 7
      src/Pages/Queue/Queue.jsx
  43. 39 0
      src/Pages/Queue/style.scoped.scss
  44. 107 8
      src/Pages/Tracks/Tracks.jsx
  45. 54 0
      src/Pages/Tracks/style.scoped.scss
  46. 115 0
      src/Pages/Upload/Upload.jsx
  47. 72 0
      src/Pages/Upload/style.scoped.scss
  48. 3 2
      src/Pages/index.js
  49. BIN
      src/assets/6570e93e3a5a4b83a692d229864a.jpg
  50. 22 0
      src/assets/add_track_icon.svg
  51. 1 0
      src/assets/close_icon.svg
  52. 1 0
      src/assets/dark_plus_icon.svg
  53. BIN
      src/assets/dot.png
  54. BIN
      src/assets/drag_icon.png
  55. 15 0
      src/assets/play_icon_2.svg
  56. BIN
      src/assets/play_icon_3.png
  57. 7 0
      src/assets/playlist_icon_2.svg
  58. BIN
      src/assets/song.mp3
  59. BIN
      src/assets/upload.png
  60. 1 14
      src/index.js
  61. 115 24
      src/redux/actions/creators/audio.js
  62. 232 0
      src/redux/actions/creators/playlists.js
  63. 45 0
      src/redux/actions/creators/profile.js
  64. 166 0
      src/redux/actions/creators/tracks.js
  65. 40 0
      src/redux/actions/creators/upload.js
  66. 70 6
      src/redux/actions/types.js
  67. 30 2
      src/redux/reducers/audioReducer.js
  68. 11 3
      src/redux/reducers/index.js
  69. 120 0
      src/redux/reducers/playlistsReducer.js
  70. 20 0
      src/redux/reducers/profileReducer.js
  71. 87 0
      src/redux/reducers/tracksReducer.js
  72. 26 0
      src/redux/reducers/uploadReducer.js
  73. 16 6
      src/utils/constants.js
  74. 10 0
      src/utils/getGQL_Upload.js
  75. 95 0
      src/utils/graphQueries.js
  76. 5 0
      src/utils/index.js
  77. 15 0
      src/utils/jwtDecoder.js
  78. 4 0
      src/utils/regex.js

+ 7 - 0
craco.config.js

@@ -0,0 +1,7 @@
+module.exports = {
+    plugins: [
+        {
+            plugin: require("craco-plugin-scoped-css"),
+        },
+    ],
+};

File diff suppressed because it is too large
+ 3593 - 156
package-lock.json


+ 11 - 4
package.json

@@ -3,6 +3,7 @@
   "version": "0.1.0",
   "private": true,
   "dependencies": {
+    "@craco/craco": "^5.9.0",
     "@emotion/react": "^11.9.0",
     "@emotion/styled": "^11.8.1",
     "@mui/icons-material": "^5.6.2",
@@ -10,21 +11,27 @@
     "@testing-library/jest-dom": "^5.16.4",
     "@testing-library/react": "^13.1.1",
     "@testing-library/user-event": "^13.5.0",
+    "array-move": "^4.0.0",
+    "craco-plugin-scoped-css": "^1.1.1",
+    "node-sass": "^7.0.1",
     "react": "^18.0.0",
     "react-dom": "^18.0.0",
+    "react-dropzone": "^14.2.2",
+    "react-hook-form": "^7.33.1",
     "react-redux": "^8.0.1",
     "react-router-dom": "^6.3.0",
     "react-scripts": "5.0.1",
+    "react-sortable-hoc": "^2.0.0",
     "redux": "^4.2.0",
     "redux-thunk": "^2.4.1",
-    "styled-components": "^5.3.5",
+    "reselect": "^4.1.6",
     "validator": "^13.7.0",
     "web-vitals": "^2.1.4"
   },
   "scripts": {
-    "start": "react-scripts start",
-    "build": "react-scripts build",
-    "test": "react-scripts test",
+    "start": "craco start",
+    "build": "craco build",
+    "test": "craco test",
     "eject": "react-scripts eject"
   },
   "eslintConfig": {

+ 1 - 0
public/index.html

@@ -16,6 +16,7 @@
     <link rel="preconnect" href="https://fonts.googleapis.com">
     <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
     <link href="https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@400;600&display=swap" rel="stylesheet">
+    <script src="https://code.iconify.design/2/2.2.1/iconify.min.js"></script>
     <title>React App</title>
   </head>
   <body>

+ 10 - 2
src/App.jsx

@@ -1,5 +1,5 @@
 import React from "react";
-import PrivateRoute from "./components/PrivateRoute/PrivateRoute";
+import { PrivateRoute } from "./Components";
 import { BrowserRouter, Routes, Route } from "react-router-dom";
 import {
     Home,
@@ -9,8 +9,10 @@ import {
     Tracks,
     Login,
     Register,
-} from "./pages";
+    Upload,
+} from "./Pages";
 import { history } from "./utils/history";
+import PlaylistItem from "./Pages/PlaylistItem/PlaylistItem";
 
 // history.listen(() => console.log(history.location))
 
@@ -33,6 +35,12 @@ const App = () => {
                 <Route element={<PrivateRoute />}>
                     <Route path="playlists" element={<Playlists />} />
                 </Route>
+                <Route element={<PrivateRoute />}>
+                    <Route path="playlists/:id" element={<PlaylistItem />} />
+                </Route>
+                <Route element={<PrivateRoute />}>
+                    <Route path="upload" element={<Upload />} />
+                </Route>
                 <Route path="login" element={<Login />} />
                 <Route path="signup" element={<Register />} />
             </Routes>

+ 13 - 0
src/Components/AddButton/AddButton.jsx

@@ -0,0 +1,13 @@
+import React from "react";
+import "./style.scoped.scss";
+
+const AddButton = ({ icon, text, opacity = 1, clickHandler}) => {
+    return (
+        <div className="add-button" style={{opacity}} onClick={clickHandler}>
+            {icon ? <img src={icon}  className="icon"/> : null}
+            <div className="text">{text}</div>
+        </div>
+    );
+};
+
+export default AddButton;

+ 44 - 0
src/Components/AddButton/style.scoped.scss

@@ -0,0 +1,44 @@
+$whiteFilter: invert(100%) sepia(0%) saturate(7500%) hue-rotate(116deg)
+    brightness(109%) contrast(109%);
+
+$darkFilter: invert(48%) sepia(3%) saturate(4%) hue-rotate(326deg)
+    brightness(110%) contrast(78%);
+
+.add-button {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    height: 30px;
+    padding: 0 15px;
+    margin-left: 15px;
+    border-radius: 8px;
+    border: 1px solid grey;
+    background-color: rgba($color: #fff, $alpha: 0.05);
+    transition: 0.2s;
+
+    .icon {
+        width: 20px;
+        height: 25px;
+        margin-right: 10px;
+
+        filter: $darkFilter;
+    }
+
+    .text {
+        color: rgb(179, 179, 179);
+        font-weight: 600;
+    }
+
+    &:hover {
+        border: 1px solid white;
+        cursor: pointer;
+
+        .text {
+            color: white;
+        }
+
+        .icon {
+            filter: $whiteFilter;
+        }
+    }
+}

+ 60 - 0
src/Components/DropdownTracks/DropdownTracks.jsx

@@ -0,0 +1,60 @@
+import React from "react";
+import { useEffect } from "react";
+import { useState } from "react";
+import { useDispatch } from "react-redux";
+import { actionRemoveTracks, actionSetSortByTracks } from "../../redux/actions/creators/tracks";
+import "./style.scoped.scss";
+
+const defaultValues = [
+    {
+        name: "A-Z",
+        value: 1,
+    },
+    {
+        name: "Z-A",
+        value: -1,
+    },
+];
+
+const DropdownMenu = () => {
+    const [value, setValue] = useState(defaultValues[0].value);
+    const dispatch = useDispatch();
+
+    useEffect(() => {
+        dispatch(actionSetSortByTracks(value));
+        dispatch(actionRemoveTracks());
+    }, [value]);
+
+    return (
+        <div className="dropdown-menu">
+            <input
+                className="dropdown"
+                type="checkbox"
+                id="dropdown"
+                name="dropdown"
+                value={value.value}
+            />
+            <label className="for-dropdown" htmlFor="dropdown">
+                Sort by: {value.name}
+                <i
+                    className="uil uil-arrow-down iconify"
+                    data-icon="uil:arrow-right"
+                />
+            </label>
+            <div className="section-dropdown">
+                {defaultValues.map((value, index) => (
+                    <div
+                        className="option"
+                        key={index}
+                        onClick={() => setValue(value.value)}
+                    >
+                        {value.name}
+                        <i className="uil uil-arrow-right" />
+                    </div>
+                ))}
+            </div>
+        </div>
+    );
+};
+
+export default DropdownMenu;

+ 291 - 0
src/Components/DropdownTracks/style.scoped.scss

@@ -0,0 +1,291 @@
+.dropdown-menu {
+    position: relative;
+    max-width: 100%;
+    text-align: center;
+    user-select: none;
+    z-index: 5;
+
+    [type="checkbox"]:checked,
+    [type="checkbox"]:not(:checked) {
+        position: absolute;
+        left: -9999px;
+        opacity: 0;
+        pointer-events: none;
+    }
+    .dark-light:checked + label,
+    .dark-light:not(:checked) + label {
+        position: fixed;
+        top: 40px;
+        right: 40px;
+        z-index: 20000;
+        display: block;
+        border-radius: 50%;
+        width: 46px;
+        height: 46px;
+        cursor: pointer;
+        transition: all 200ms linear;
+        box-shadow: 0 0 25px rgba(255, 235, 167, 0.45);
+    }
+    .dark-light:checked + label {
+        transform: rotate(360deg);
+    }
+    .dark-light:checked + label:after,
+
+    .dark-light:checked + label:after {
+        opacity: 1;
+    }
+    .dark-light:checked + label:before,
+    .dark-light:not(:checked) + label:before {
+        position: absolute;
+        content: "";
+        top: 0;
+        left: 0;
+        overflow: hidden;
+        z-index: 1;
+        display: block;
+        border-radius: 50%;
+        width: 46px;
+        height: 46px;
+        background-color: #48dbfb;
+        background-image: url("https://assets.codepen.io/1462889/sun.svg");
+        background-size: 25px 25px;
+        background-repeat: no-repeat;
+        background-position: center;
+        transition: all 200ms linear;
+    }
+    .dark-light:checked + label:before {
+        background-color: #000;
+    }
+    .dark-light:checked ~ .light-back {
+        opacity: 1;
+    }
+    .dropdown:checked + label,
+    .dropdown:not(:checked) + label {
+        position: relative;
+        font-size: 15px;
+        line-height: 2;
+        height: 50px;
+        transition: all 200ms linear;
+        border-radius: 4px;
+        letter-spacing: 1px;
+        display: inline-flex;
+        align-items: center;
+        justify-content: center;
+        text-align: center;
+        border: none;
+        cursor: pointer;
+        color: white;
+    }
+    .dark-light:checked ~ .sec-center .for-dropdown {
+        color: #ffeba7;
+    }
+    .dropdown:checked + label:before,
+    .dropdown:not(:checked) + label:before {
+        position: fixed;
+        top: 0;
+        left: 0;
+        content: "";
+        width: 100%;
+        height: 100%;
+        z-index: -1;
+        cursor: auto;
+        pointer-events: none;
+    }
+    .dropdown:checked + label:before {
+        pointer-events: auto;
+    }
+    .dropdown:not(:checked) + label .uil {
+        font-size: 24px;
+        margin-left: 10px;
+        transition: transform 200ms linear;
+    }
+    .dropdown:checked + label .uil {
+        transform: rotate(180deg);
+        font-size: 24px;
+        margin-left: 10px;
+        transition: transform 200ms linear;
+    }
+    .dropdown + label .uil {
+        transform: rotate(90deg);
+    }
+    .section-dropdown {
+        position: absolute;
+        padding: 5px;
+        background-color: #111;
+        left: 0;
+        width: 100%;
+        border-radius: 4px;
+        display: block;
+        box-shadow: 0 14px 35px 0 rgba(9, 9, 12, 0.4);
+        z-index: 2;
+        opacity: 0;
+        pointer-events: none;
+        transform: translateY(20px);
+        transition: all 200ms linear;
+    }
+    .dark-light:checked ~ .sec-center .section-dropdown {
+        background-color: #fff;
+        box-shadow: 0 14px 35px 0 rgba(9, 9, 12, 0.15);
+    }
+    .dropdown:checked ~ .section-dropdown {
+        opacity: 1;
+        pointer-events: auto;
+        transform: translateY(0);
+    }
+    .section-dropdown:before {
+        position: absolute;
+        top: -20px;
+        left: 0;
+        width: 100%;
+        height: 20px;
+        content: "";
+        display: block;
+        z-index: 1;
+    }
+    .section-dropdown:after {
+        position: absolute;
+        top: -7px;
+        left: 30px;
+        width: 0;
+        height: 0;
+        border-left: 8px solid transparent;
+        border-right: 8px solid transparent;
+        border-bottom: 8px solid #111;
+        content: "";
+        display: block;
+        z-index: 2;
+        transition: all 200ms linear;
+    }
+    .dark-light:checked ~ .sec-center .section-dropdown:after {
+        border-bottom: 8px solid #fff;
+    }
+
+    .option {
+        position: relative;
+        color: #fff;
+        transition: all 200ms linear;
+        font-family: "Roboto", sans-serif;
+        font-weight: 500;
+        font-size: 15px;
+        border-radius: 2px;
+        padding: 5px 0;
+        padding-left: 20px;
+        padding-right: 15px;
+        margin: 2px 0;
+        text-align: left;
+        text-decoration: none;
+        display: -ms-flexbox;
+        display: flex;
+        -webkit-align-items: center;
+        -moz-align-items: center;
+        -ms-align-items: center;
+        align-items: center;
+        justify-content: space-between;
+        -ms-flex-pack: distribute;
+
+        &:hover {
+            cursor: pointer;
+            color: #102770;
+            background-color: #ffeba7;
+        }
+    }
+    .dark-light:checked ~ .sec-center .section-dropdown .option {
+        color: #102770;
+    }
+    .dark-light:checked ~ .sec-center .section-dropdown .option {
+        &:hover {
+            color: #ffeba7;
+            background-color: #102770;
+        }
+    }
+    .option .uil {
+        font-size: 22px;
+    }
+    .dropdown-sub:checked + label,
+    .dropdown-sub:not(:checked) + label {
+        position: relative;
+        color: #fff;
+        transition: all 200ms linear;
+        font-family: "Roboto", sans-serif;
+        font-weight: 500;
+        font-size: 15px;
+        border-radius: 2px;
+        padding: 5px 0;
+        padding-left: 20px;
+        padding-right: 15px;
+        text-align: left;
+        text-decoration: none;
+        display: -ms-flexbox;
+        display: flex;
+        -webkit-align-items: center;
+        -moz-align-items: center;
+        -ms-align-items: center;
+        align-items: center;
+        justify-content: space-between;
+        -ms-flex-pack: distribute;
+        cursor: pointer;
+    }
+    .dropdown-sub:checked + label .uil,
+    .dropdown-sub:not(:checked) + label .uil {
+        font-size: 22px;
+    }
+    .dropdown-sub:not(:checked) + label .uil {
+        transition: transform 200ms linear;
+    }
+    .dropdown-sub:checked + label .uil {
+        transform: rotate(135deg);
+        transition: transform 200ms linear;
+    }
+    .dropdown-sub:checked + label:hover,
+    .dropdown-sub:not(:checked) + label:hover {
+        color: #102770;
+        background-color: #ffeba7;
+    }
+    .dark-light:checked ~ .sec-center .section-dropdown .for-dropdown-sub {
+        color: #102770;
+    }
+    .dark-light:checked
+        ~ .sec-center
+        .section-dropdown
+        .for-dropdown-sub:hover {
+        color: #ffeba7;
+        background-color: #102770;
+    }
+
+    .section-dropdown-sub {
+        position: relative;
+        display: block;
+        width: 100%;
+        pointer-events: none;
+        opacity: 0;
+        max-height: 0;
+        padding-left: 10px;
+        padding-right: 3px;
+        overflow: hidden;
+        transition: all 200ms linear;
+    }
+
+    .logo {
+        position: fixed;
+        top: 50px;
+        left: 40px;
+        display: block;
+        z-index: 11000000;
+        background-color: transparent;
+        border-radius: 0;
+        padding: 0;
+        transition: all 250ms linear;
+    }
+    .logo:hover {
+        background-color: transparent;
+    }
+    .logo img {
+        height: 26px;
+        width: auto;
+        display: block;
+        transition: all 200ms linear;
+    }
+    .dark-light:checked ~ .logo img {
+        filter: brightness(10%);
+    }
+}

+ 17 - 17
src/Components/LeftBar/LeftBar.jsx

@@ -1,8 +1,9 @@
-import React, { useState } from "react";
+import React from "react";
 import { Link } from "react-router-dom";
-import { Wrapper, Navbar, Line, BtnBack, Logo, Image, Text } from "./style";
-import { Tab } from "../../components";
+import { Tab } from "../../Components";
 import { ROUTES } from "../../utils/constants";
+import { back, push } from "../../utils/history";
+import './style.scoped.scss';
 
 import logo from "../../assets/logo.png";
 import IconBack from "@mui/icons-material/KeyboardBackspace";
@@ -11,7 +12,6 @@ import ProfileIcon from "../../assets/profile_icon.svg";
 import QueueIcon from "../../assets/queue_icon.svg";
 import TrackIcon from "../../assets/track_icon.svg";
 import PlaylistIcon from "../../assets/playlist_icon.svg";
-import { back, push } from "../../utils/history";
 
 const tabs = [
     {
@@ -25,7 +25,7 @@ const tabs = [
         icon: ProfileIcon,
     },
     {
-        label: "Play queue",
+        label: "Set up queue",
         route: ROUTES.QUEUE,
         icon: QueueIcon,
     },
@@ -43,18 +43,18 @@ const tabs = [
 
 const LeftBar = () => {
     return (
-        <Wrapper>
-            <Navbar>
-                <BtnBack onClick={() => back()}>
-                    <IconBack/>
-                </BtnBack>
+        <div className="leftbar">
+            <aside className="navbar">
+                <div className="button-back" onClick={() => back()}>
+                    <IconBack />
+                </div>
                 <Link to="/" onClick={() => push(ROUTES.HOME)}>
-                    <Logo>
-                        <Image src={logo} alt="logo" />
-                        <Text>Audio Player</Text>
-                    </Logo>
+                    <div className="logo">
+                        <img className="image" src={logo} alt="logo" />
+                        <p className="text">Audio Player</p>
+                    </div>
                 </Link>
-            </Navbar>
+            </aside>
 
             {tabs.map((tab, index) => (
                 <React.Fragment key={index}>
@@ -65,10 +65,10 @@ const LeftBar = () => {
                             url={tab.route}
                         />
                     </Link>
-                    {index === 2 ? <Line /> : null}
+                    {index === 2 ? <div className="line" /> : null}
                 </React.Fragment>
             ))}
-        </Wrapper>
+        </div>
     );
 };
 export default LeftBar;

+ 48 - 0
src/Components/LeftBar/style.scoped.scss

@@ -0,0 +1,48 @@
+.leftbar {
+    width: 15%;
+    display: flex;
+    flex-direction: column;
+    background: rgba(24, 25, 29, 255);
+    padding: 5px 0 0 10px;
+}
+
+.navbar {
+    display: flex;
+    align-items: center;
+    text-decoration: none;
+    margin-bottom: 25px;
+}
+
+.button-back {
+    color: grey;
+    transition: 0.1s;
+
+    &:hover {
+        color: #fff;
+        cursor: pointer;
+    }
+}
+
+.logo {
+    display: flex;
+    align-items: center;
+    margin-left: 30px;
+}
+
+.image {
+    width: 15px;
+    height: 15px;
+}
+
+.text {
+    font-size: 14px;
+    margin-left: 10px;
+}
+
+.line {
+    margin: 10px 0;
+    background: rgb(50, 50, 50);
+    height: 1px;
+    margin-top: 14px;
+    margin-left: -10px;
+}

+ 13 - 0
src/Components/OrangeButton/OrangeButton.jsx

@@ -0,0 +1,13 @@
+import React from "react";
+import "./style.scoped.scss";
+
+const OrangeButton = ({ icon, text, clickHandler }) => {
+    return (
+        <div className="orange-button" onClick={clickHandler}>
+            {icon ? <img src={icon} className="icon" /> : null}
+            <div className="text">{text}</div>
+        </div>
+    );
+};
+
+export default OrangeButton;

+ 38 - 0
src/Components/OrangeButton/style.scoped.scss

@@ -0,0 +1,38 @@
+$whiteFilter: invert(100%) sepia(0%) saturate(7500%) hue-rotate(116deg)
+    brightness(109%) contrast(109%);
+
+$darkFilter: invert(48%) sepia(3%) saturate(4%) hue-rotate(326deg)
+    brightness(110%) contrast(78%);
+
+.orange-button {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    height: 30px;
+    padding: 0 25px;
+    border-radius: 8px;
+    background-color: rgb(254, 127, 1);
+    transition: 0.2s;
+
+    .icon {
+        width: 20px;
+        height: 20px;
+        margin-right: 5px;
+    }
+
+    .text {
+        color: black;
+    }
+
+    &:hover {
+        cursor: pointer;
+
+        .text {
+            color: white;
+        }
+
+        .icon {
+            filter: $whiteFilter;
+        }
+    }
+}

+ 52 - 0
src/Components/PaginationBar/PaginationBar.jsx

@@ -0,0 +1,52 @@
+import React from "react";
+import { createTheme, ThemeProvider } from "@mui/material";
+import Pagination from "@mui/material/Pagination";
+import { useNavigate, useSearchParams } from "react-router-dom";
+import { useEffect } from "react";
+import { useDispatch } from "react-redux";
+import { LIMIT, ROUTES } from "../../utils/constants";
+import {
+    actionGetPlaylistsCount,
+    actionPlaylistPageChange,
+} from "../../redux/actions/creators/playlists";
+
+const theme = createTheme({
+    palette: {
+        mode: "dark",
+    },
+});
+
+const PaginationBar = ({ currentPage, count }) => {
+    const dispatch = useDispatch();
+    const navigate = useNavigate();
+
+    const [searchParams] = useSearchParams();
+
+    useEffect(() => {
+        dispatch(actionGetPlaylistsCount());
+
+        const currPage = +searchParams.get("page") || currentPage;
+        dispatch(actionPlaylistPageChange(currPage));
+    }, []);
+
+    const changeHandler = (e, page) => {
+        navigate(`${ROUTES.PLAYLISTS}?page=${page}`);
+        dispatch(actionPlaylistPageChange(page));
+    };
+
+    return (
+        <ThemeProvider theme={theme}>
+            <div style={{ alignSelf: "center" }}>
+                <Pagination
+                    page={currentPage}
+                    count={Math.ceil(count / LIMIT.PLAYLISTS_ON_PAGE) ?? count}
+                    variant="outlined"
+                    shape="rounded"
+                    onChange={changeHandler}
+                />
+            </div>
+        </ThemeProvider>
+    );
+};
+
+export default PaginationBar;

+ 80 - 60
src/Components/Player/Player.jsx

@@ -1,31 +1,15 @@
 import React, { useState } from "react";
 import { useSelector, useDispatch } from "react-redux";
 import {
-    togglePlay,
-    toggleRepeat,
-    setVolume,
-    setCurrentTime,
+    actionTogglePlay,
+    actionToggleRepeat,
+    actionSetVolume,
+    actionSetCurrentTime,
+    actionPrevTrack,
+    actionNextTrack,
 } from "../../redux/actions/creators/audio";
-import {
-    ButtonCollapse,
-    ButtonVolume,
-    Audio,
-    Duration,
-    Footer,
-    Header,
-    MainButtons,
-    Info,
-    PlaylistName,
-    TrackName,
-    Volume,
-    VolumeSettings,
-    Wrapper,
-    StatusButton,
-    NextButton,
-    RepeatButton,
-    PreviousButton,
-    ShuffleButton,
-} from "./style";
+import "./style.scoped.scss";
+import { removeAudioExtension } from "../../utils/regex";
 
 import CollapseIcon from "../../assets/collapse_icon.svg";
 import ShuffleIcon from "../../assets/shuffle_icon.svg";
@@ -36,80 +20,116 @@ import StopIcon from "../../assets/stop_icon.svg";
 import RepeatIcon from "../../assets/repeat_icon.svg";
 import VolumeUpIcon from "../../assets/volume_up_icon.svg";
 import VolumeStopIcon from "../../assets/volume_stop_icon.svg";
+import { secondsToHMS } from "../../utils";
+
+const whiteFilter = `invert(100%) sepia(0%) saturate(7500%) hue-rotate(116deg)
+brightness(109%) contrast(109%)`;
+const darkFilter = `invert(48%) sepia(3%) saturate(4%) hue-rotate(326deg) brightness(110%) contrast(78%)`;
 
 const Player = () => {
     const dispatch = useDispatch();
-    const state = useSelector((state) => state.audio);
+    const state = useSelector((store) => store.audio);
+
+    const title = state.track
+        ? state.track.id3.title ||
+          removeAudioExtension(state.track.originalFileName)
+        : null;
 
     const [isCollapsed, setIsCollapsed] = useState(false);
 
     return (
-        <Wrapper isCollapsed={isCollapsed}>
-            <Header>
-                <Duration>0:00:00</Duration>
-                <Audio
+        <div
+            className="player"
+            style={{
+                height: isCollapsed ? "30px" : "80px",
+            }}
+        >
+            <div className="header">
+                <p className="duration">{secondsToHMS(state.currentTime)}</p>
+                <input
+                    className="audio"
                     type={"range"}
                     min={0}
                     max={state.duration}
                     value={state.currentTime}
                     onChange={(e) => {
-                        dispatch(setCurrentTime(+e.target.value));
+                        dispatch(actionSetCurrentTime(+e.target.value));
                     }}
                 />
-                <ButtonCollapse
+                <img
+                    className="button-collapse"
                     src={CollapseIcon}
-                    isCollapsed={isCollapsed}
-                    onClick={() => setIsCollapsed(!isCollapsed)} // change to redux
+                    style={{
+                        filter: isCollapsed ? darkFilter : whiteFilter,
+                    }}
+                    onClick={() => setIsCollapsed(!isCollapsed)}
                 />
-            </Header>
+            </div>
             {!isCollapsed ? (
-                <Footer>
-                    <Info>
-                        <PlaylistName>Here is playlist</PlaylistName>
-                        <TrackName>Here is track</TrackName>
-                    </Info>
-                    <MainButtons>
-                        <ShuffleButton src={ShuffleIcon} />
-                        <PreviousButton src={PreviousIcon} />
-                        <StatusButton
+                <div className="footer">
+                    <div className="info">
+                        <p className="track-name">{title ?? "Here is track"}</p>
+                        <p className="playlist-name">Here is playlist</p>
+                    </div>
+                    <div className="main-buttons">
+                        <img className="shuffle-button" src={ShuffleIcon} />
+                        <img
+                            className="previous-button"
+                            src={PreviousIcon}
+                            onClick={() => dispatch(actionPrevTrack())}
+                        />
+                        <img
+                            className="status-button"
                             src={state.isPlaying ? StopIcon : PlayIcon}
-                            isPlaying={state.isPlaying}
                             onClick={() =>
                                 dispatch(
-                                    togglePlay(state.isPlaying ? false : true)
+                                    actionTogglePlay(
+                                        state.isPlaying ? false : true
+                                    )
                                 )
                             }
                         />
-                        <NextButton src={NextIcon} />
-                        <RepeatButton
+                        <img
+                            className="next-button"
+                            src={NextIcon}
+                            onClick={() => dispatch(actionNextTrack())}
+                        />
+                        <img
+                            className="repeat-button"
                             src={RepeatIcon}
-                            isRepeated={state.isRepeated}
+                            style={{
+                                filter: state.isRepeated
+                                    ? whiteFilter
+                                    : darkFilter,
+                            }}
                             onClick={() =>
-                                dispatch(toggleRepeat(!state.isRepeated))
+                                dispatch(actionToggleRepeat(!state.isRepeated))
                             }
                         />
-                    </MainButtons>
-                    <VolumeSettings>
-                        <ButtonVolume
-                            src={state.volume ? VolumeUpIcon : VolumeStopIcon} // redux
+                    </div>
+                    <div className="volume-settings">
+                        <img
+                            className="button-volume"
+                            src={state.volume ? VolumeUpIcon : VolumeStopIcon}
                             onClick={() =>
-                                dispatch(setVolume(state.volume ? 0 : 1))
+                                dispatch(actionSetVolume(state.volume ? 0 : 1))
                             }
                         />
-                        <Volume
+                        <input
+                            className="volume"
                             type={"range"}
                             min={0}
                             max={1}
                             step={0.01}
                             value={state.volume}
                             onChange={(e) => {
-                                dispatch(setVolume(+e.target.value));
+                                dispatch(actionSetVolume(+e.target.value));
                             }}
-                        ></Volume>
-                    </VolumeSettings>
-                </Footer>
+                        />
+                    </div>
+                </div>
             ) : null}
-        </Wrapper>
+        </div>
     );
 };
 

+ 176 - 0
src/Components/Player/style.scoped.scss

@@ -0,0 +1,176 @@
+$whiteFilter: invert(100%) sepia(0%) saturate(7500%) hue-rotate(116deg)
+    brightness(109%) contrast(109%);
+
+$darkFilter: invert(48%) sepia(3%) saturate(4%) hue-rotate(326deg)
+    brightness(110%) contrast(78%);
+
+.player {
+    flex-shrink: 0;
+
+    background: #1a1a1a;
+    border-top: 3px solid #525252;
+    transition: 0.4s;
+
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    padding: 5px 20px 0;
+}
+
+.header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding-bottom: 5px;
+}
+
+.duration {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding-bottom: 2px;
+}
+
+.audio {
+    appearance: none;
+    -webkit-appearance: none;
+    background: #0f0e0e;
+    width: 95%;
+    margin: 0 5px;
+
+    &:focus {
+        outline: none;
+    }
+
+    &::-webkit-slider-thumb {
+        -webkit-appearance: none;
+
+        width: 10px;
+        height: 10px;
+        border-radius: 5px;
+        background: rgb(100, 100, 100);
+        cursor: pointer;
+        margin-top: -2.2px;
+    }
+
+    &::-webkit-slider-runnable-track {
+        width: 100%;
+        height: 5px;
+        cursor: pointer;
+        background: rgb(50, 50, 50);
+        border-radius: 5px;
+    }
+}
+
+.button-collapse {
+    width: 15px;
+    height: 15px;
+
+    &:hover {
+        cursor: pointer;
+    }
+}
+
+.footer {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+}
+
+.info {
+    width: 150px;
+
+    .playlist-name {
+        font-size: 12px;
+    }
+
+    .track-name {
+        font-size: 16px;
+    }
+}
+
+.main-buttons {
+    display: flex;
+    align-items: center;
+}
+
+.shuffle-button {
+    width: 18px;
+    height: 18px;
+    transition: 0.3s;
+    filter: $darkFilter;
+
+    &:hover {
+        cursor: pointer;
+        filter: $whiteFilter;
+    }
+}
+
+.previous-button {
+    width: 22px;
+    height: 22px;
+    transition: 0.3s;
+    filter: $darkFilter;
+
+    &:hover {
+        cursor: pointer;
+        filter: $whiteFilter;
+    }
+}
+
+.status-button {
+    width: 40px;
+    height: 40px;
+    filter: invert(98%) sepia(0%) saturate(303%) hue-rotate(143deg)
+        brightness(88%) contrast(84%);
+    transition: 1s;
+
+    &:hover {
+        cursor: pointer;
+        filter: $whiteFilter;
+    }
+}
+
+.next-button {
+    width: 22px;
+    height: 22px;
+    transition: 0.3s;
+    filter: $darkFilter;
+
+    &:hover {
+        cursor: pointer;
+        filter: $whiteFilter;
+    }
+}
+
+.repeat-button {
+    width: 18px;
+    height: 18px;
+    transition: 0.3s;
+
+    &:hover {
+        cursor: pointer;
+    }
+}
+
+.volume-settings {
+    width: 150px;
+    display: flex;
+    align-items: center;
+}
+
+.button-volume {
+    width: 22px;
+    height: 22px;
+    filter: $darkFilter;
+    transition: 0.3s;
+
+    &:hover {
+        filter: $whiteFilter;
+        cursor: pointer;
+    }
+}
+
+.volume {
+    width: 100%;
+}

+ 84 - 0
src/Components/PlaylistCover/PlaylistCover.jsx

@@ -0,0 +1,84 @@
+import React, { useState, useRef } from "react";
+import { useForm } from "react-hook-form";
+import "./style.scoped.scss";
+
+import { styled, TextField } from "@mui/material";
+import Button from "@mui/material/Button";
+import PlaylistIcon from "../../assets/playlist_icon_2.svg";
+import { useDispatch } from "react-redux";
+import { actionUpsertPlaylistInfo } from "../../redux/actions/creators/playlists";
+
+const CssTextField = styled(TextField)({
+    "& .MuiInput-root": {
+        color: "gray",
+        "&:before": {
+            borderBottom: "1px solid black",
+        },
+        "&:after": {
+            borderBottom: "2px solid rgb(89, 215, 89)",
+        },
+    },
+    "& .MuiInputLabel-root": {
+        color: "gray",
+        fontSize: "15px",
+        "&.Mui-focused": {
+            color: "rgb(89, 215, 89)",
+        },
+    },
+});
+
+const PlaylistCover = ({ _id, name, description }) => {
+    const { register, handleSubmit, reset } = useForm();
+    const dispatch = useDispatch();
+
+    const [isEditingInfo, setIsEditingInfo] = useState(false);
+
+    const submitHandler = (data) => {
+        const { name, description } = data;
+        dispatch(actionUpsertPlaylistInfo({ _id, name, description }));
+        reset();
+    };
+
+    return (
+        <form onSubmit={handleSubmit(submitHandler)}>
+            <div
+                className="playlist-info"
+                onClick={() => setIsEditingInfo(!isEditingInfo)}
+            >
+                <img className="icon" src={PlaylistIcon} alt="playlist_icon" />
+                <p className="name">{name}</p>
+                <p className="description">{description}</p>
+            </div>
+            <div
+                className={`playlist-edit ${
+                    isEditingInfo ? "playlist-edit-visible" : ""
+                }`}
+            >
+                <CssTextField
+                    variant="standard"
+                    label="Name"
+                    id="name-input"
+                    autoComplete="none1"
+                    {...register("name", { required: true })}
+                />
+                <CssTextField
+                    variant="standard"
+                    label="Description"
+                    id="description-input"
+                    autoComplete="none2"
+                    {...register("description", { required: true })}
+                />
+                <Button
+                    style={{ marginTop: "20px" }}
+                    color="success"
+                    variant="outlined"
+                    type="submit"
+                >
+                    Save
+                </Button>
+            </div>
+        </form>
+    );
+};
+
+export default PlaylistCover;

+ 107 - 0
src/Components/PlaylistCover/style.scoped.scss

@@ -0,0 +1,107 @@
+.playlist-info {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    width: 250px;
+    min-height: 250px;
+    margin-bottom: 10px;
+    position: relative;
+    z-index: 2;
+    word-break: break-all;
+    background-color: #131316;
+    border-radius: 20px;
+    transition: 0.5s;
+
+    .icon {
+        position: absolute;
+        display: flex;
+        width: 35%;
+        height: 35%;
+        z-index: 1;
+        transition: 0.35s;
+        filter: invert(100%) sepia(0%) saturate(7500%) hue-rotate(116deg)
+            brightness(109%) contrast(109%);
+    }
+
+    p {
+        transition: 0.5s;
+        user-select: none;
+        opacity: 0;
+    }
+
+    .name {
+        font-size: 36px;
+    }
+
+    &::before {
+        top: 0;
+        content: "Hover here";
+        width: 100%;
+        height: 20px;
+        display: flex;
+        color: #000;
+        justify-content: center;
+        border-top-left-radius: 20px;
+        border-top-right-radius: 20px;
+        opacity: 1;
+        transition: 0.3s;
+
+        background-color: rgb(89, 215, 89);
+        position: absolute;
+    }
+
+    &::after {
+        bottom: 0;
+        content: "Click to edit";
+        width: 100%;
+        height: 20px;
+        display: flex;
+        color: #000;
+        justify-content: center;
+        border-bottom-left-radius: 20px;
+        border-bottom-right-radius: 20px;
+        opacity: 0;
+        transition: 0.3s;
+
+        background-color: rgb(254, 127, 1);
+        position: absolute;
+    }
+
+    &:hover {
+        cursor: pointer;
+
+        p {
+            opacity: 1;
+        }
+
+        .icon {
+            opacity: 0;
+        }
+
+        &::before {
+            opacity: 0;
+        }
+
+        &::after {
+            opacity: 1;
+        }
+    }
+}
+
+.playlist-edit {
+    display: flex;
+    flex-direction: column;
+    height: 100px;
+    width: 100%;
+    justify-content: space-between;
+    position: absolute;
+    bottom: 0;
+    transition: 0.5s;
+    opacity: 0;
+}
+
+.playlist-edit-visible {
+    bottom: -110px;
+    opacity: 1;
+}

+ 67 - 0
src/Components/PlaylistInfo/PlaylistInfo.jsx

@@ -0,0 +1,67 @@
+import React from "react";
+import PlaylistsTrack from "../PlaylistsTrack/PlaylistsTrack";
+import { useNavigate } from "react-router-dom";
+import "./style.scoped.scss";
+
+import PlaylistIcon from "../../assets/playlist_icon_2.svg";
+import PlayIcon from "../../assets/play_icon.svg";
+import { ROUTES } from "../../utils/constants";
+import { useDispatch } from "react-redux";
+import { actionSetQueue } from "../../redux/actions/creators/audio";
+
+const PlaylistInfo = ({ playlist }) => {
+    const navigate = useNavigate();
+    const dispatch = useDispatch();
+
+    const { _id, name, description, tracks } = playlist;
+
+    const editClickHandler = () => {
+        navigate(`${ROUTES.PLAYLISTS}/${_id}`);
+    };
+
+    const playClickHandler = () => {
+        dispatch(actionSetQueue({ _id, name, tracks: tracks ?? [] }));
+    };
+
+    return (
+        <div className="playlist-item">
+            <div className="top">
+                <div className="cover">
+                    <img
+                        className="playlist-icon"
+                        src={PlaylistIcon}
+                        alt="playlist_icon"
+                    />
+                    <img
+                        className="play-icon"
+                        src={PlayIcon}
+                        alt="play_icon"
+                        onClick={playClickHandler}
+                    />
+                </div>
+                <div className="tracks-box">
+                    <div className="tracks">
+                        {tracks?.length
+                            ? tracks.map((track) => (
+                                  <PlaylistsTrack
+                                      key={track._id}
+                                      track={track}
+                                      playlistId={_id}
+                                  />
+                              ))
+                            : "0 TRACKS"}
+                    </div>
+                    <div className="button-edit" onClick={editClickHandler}>
+                        EDIT
+                    </div>
+                </div>
+            </div>
+            <div className="info">
+                <p className="name">{name}</p>
+                <p className="description">{description}</p>
+            </div>
+        </div>
+    );
+};
+
+export default PlaylistInfo;

+ 113 - 0
src/Components/PlaylistInfo/style.scoped.scss

@@ -0,0 +1,113 @@
+.playlist-item {
+    display: flex;
+    flex-direction: column;
+    margin-bottom: 50px;
+
+    .top {
+        $size: 250px;
+
+        display: flex;
+        justify-content: space-between;
+        max-height: $size;
+
+        .cover {
+            height: $size;
+            width: $size;
+            background-color: #131316;
+            position: relative;
+            display: flex;
+            justify-content: center;
+            align-items: center;
+            border-radius: 15px;
+            margin-right: 50px;
+
+            .playlist-icon {
+                width: 75px;
+                height: 75px;
+                filter: invert(100%) sepia(0%) saturate(7500%)
+                    hue-rotate(116deg) brightness(109%) contrast(109%);
+            }
+
+            .play-icon {
+                position: absolute;
+                width: 30px;
+                height: 30px;
+                right: 10px;
+                top: 10px;
+                filter: invert(100%) sepia(0%) saturate(7500%)
+                    hue-rotate(116deg) brightness(109%) contrast(109%);
+                opacity: 0;
+                transition: 0.1s;
+
+                &:hover {
+                    cursor: pointer;
+                }
+            }
+
+            &:hover {
+                .play-icon {
+                    opacity: 1;
+                }
+            }
+        }
+
+        .tracks-box {
+            $border: 2px solid rgb(84, 84, 84);
+
+            width: 70%;
+            max-height: $size;
+            border-radius: 3px;
+            position: relative;
+
+            .tracks {
+                border-left: $border;
+                border-right: $border;
+                border-top: $border;
+                border-top-left-radius: 5px;
+                border-top-right-radius: 5px;
+                font-size: 20px;
+                padding: 15px;
+                height: $size - 20px;
+                overflow-y: scroll;
+            }
+
+            .button-edit {
+                position: absolute;
+                left: 0;
+                bottom: -5px;
+                width: 100%;
+                height: 30px;
+                display: flex;
+                justify-content: center;
+                align-items: center;
+                color: #000;
+                transition: 0.3s;
+
+                background-color: rgb(60, 130, 60);
+                border-bottom-left-radius: 8px;
+                border-bottom-right-radius: 8px;
+                border-left: $border;
+                border-right: $border;
+                border-bottom: $border;
+
+                &:hover {
+                    cursor: pointer;
+                    background-color: rgba(80, 171, 80, 1);
+                }
+            }
+        }
+    }
+
+    .info {
+        margin-top: 5px;
+        margin-left: 10px;
+
+        .name {
+            font-size: 20px;
+        }
+
+        .description {
+            font-size: 14px;
+        }
+    }
+}

+ 29 - 0
src/Components/PlaylistsTrack/PlaylistsTrack.jsx

@@ -0,0 +1,29 @@
+import React from "react";
+import { useDispatch } from "react-redux";
+import IconRemove from "../../assets/close_icon.svg";
+import { actionRemoveTrackFromPlaylist } from "../../redux/actions/creators/playlists";
+import { removeAudioExtension } from "../../utils/regex";
+import "./style.scoped.scss";
+
+const PlaylistsTrack = ({ track, playlistId }) => {
+    const dispatch = useDispatch();
+
+    const title = track
+        ? track.id3.title || removeAudioExtension(track.originalFileName)
+        : null;
+
+    const clickHandler = (e) => {
+        dispatch(actionRemoveTrackFromPlaylist(playlistId, track._id));
+    };
+
+    return (
+        <div className="track-item">
+            <p className="name">{title}</p>
+            <div className="button-remove" onClick={clickHandler}>
+                <img src={IconRemove} alt="icon_remove" />
+            </div>
+        </div>
+    );
+};
+
+export default PlaylistsTrack;

+ 39 - 0
src/Components/PlaylistsTrack/style.scoped.scss

@@ -0,0 +1,39 @@
+.track-item {
+    $size: 40px;
+
+    width: 100%;
+    height: $size;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    background-color: rgba($color: #fff, $alpha: 0.1);
+    border-radius: 8px;
+    margin-bottom: 15px;
+
+    .name {
+        padding-left: 25px;
+        font-size: 18px;
+    }
+
+    .button-remove {
+        height: $size;
+        width: $size;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        background-color: rgba(254, 127, 1, 0.6);
+        border-top-right-radius: 8px;
+        border-bottom-right-radius: 8px;
+        transition: 0.3s;
+
+        img {
+            width: 15px;
+            height: 15px;
+        }
+
+        &:hover {
+            cursor: pointer;
+            background-color: rgba(254, 127, 1, 1);
+        }
+    }
+}

+ 2 - 2
src/Components/PrivateRoute/PrivateRoute.jsx

@@ -2,10 +2,10 @@ import React from "react";
 import { useSelector } from "react-redux";
 import { Navigate, Outlet } from "react-router";
 
-const PrivatedRoute = () => {
+const PrivateRoute = () => {
     const { authToken } = useSelector((state) => state.auth);
 
     return authToken ? <Outlet /> : <Navigate to="/login" replace />;
 };
 
-export default PrivatedRoute;
+export default PrivateRoute;

+ 103 - 0
src/Components/ProfileData/ProfileData.jsx

@@ -0,0 +1,103 @@
+import React from "react";
+import "./style.scoped.scss";
+import Ava from "../../assets/6570e93e3a5a4b83a692d229864a.jpg";
+import OrangeButton from "../OrangeButton/OrangeButton";
+
+import Middot from "../../assets/dot.png";
+import PlayIcon from "../../assets/play_icon.svg";
+import ShuffleIcon from "../../assets/shuffle_icon.svg";
+import UploadIcon from "../../assets/upload.png";
+import { useNavigate } from "react-router-dom";
+import { ROUTES } from "../../utils/constants";
+import { useDispatch, useSelector } from "react-redux";
+import {
+    actionGetTracks,
+    actionGetTracksCount,
+    actionRemoveTracks,
+} from "../../redux/actions/creators/tracks";
+import {
+    actionPlay,
+    actionSetQueue,
+    actionShuffle,
+} from "../../redux/actions/creators/audio";
+import { useEffect } from "react";
+import { actionGetProfileData } from "../../redux/actions/creators/profile";
+import { jwtDecode } from "../../utils/jwtDecoder";
+import { actionGetPlaylistsCount } from "../../redux/actions/creators/playlists";
+
+const ProfileData = ({ avatarChanging, buttonsVisible }) => {
+    const dispatch = useDispatch();
+    const tracks = useSelector((store) => store.tracks);
+    const profile = useSelector((store) => store.profile);
+    const playlists = useSelector((store) => store.playlists);
+    const navigate = useNavigate();
+
+    useEffect(() => {
+        dispatch(actionGetProfileData());
+        dispatch(actionGetTracksCount());
+        dispatch(actionGetPlaylistsCount());
+
+        if (buttonsVisible) {
+            dispatch(actionRemoveTracks());
+            dispatch(actionGetTracks(null, null, true));
+        }
+        return () => dispatch(actionRemoveTracks());
+    }, []);
+
+    const playHandler = () => {
+        dispatch(actionSetQueue({ tracks: tracks.tracks }));
+        dispatch(actionPlay());
+    };
+
+    return (
+        <div className="profile-data">
+            <div className="avatar">
+                <img
+                    className={`avatar-icon ${avatarChanging ? "visible" : ""}`}
+                    src={Ava}
+                    alt="profile_avatar"
+                />
+                {avatarChanging ? (
+                    <img
+                        className="upload-icon"
+                        src={UploadIcon}
+                        alt="upload_icon"
+                    />
+                ) : null}
+            </div>
+            <div className="data">
+                <p className="name">{profile.login}</p>
+                <p>
+                    <span>{tracks.count} tracks</span>
+                    <img className="middot" src={Middot} alt="middot" />
+                    <span>{playlists.count} playlists</span>
+                </p>
+                <p>
+                    <span>created at: {profile.createdAt}</span>
+                    <img className="middot" src={Middot} alt="middot" />
+                    <span>{profile.permission}</span>
+                </p>
+                {buttonsVisible ? (
+                    <div className="buttons">
+                        <div className="btn-play">
+                            <OrangeButton
+                                text={"Play tracks"}
+                                icon={PlayIcon}
+                                clickHandler={playHandler}
+                            />
+                        </div>
+                        <div className="btn-upload">
+                            <OrangeButton
+                                text={"Upload tracks"}
+                                icon={UploadIcon}
+                                clickHandler={() => navigate(ROUTES.UPLOAD)}
+                            />
+                        </div>
+                    </div>
+                ) : null}
+            </div>
+        </div>
+    );
+};
+
+export default ProfileData;

+ 76 - 0
src/Components/ProfileData/style.scoped.scss

@@ -0,0 +1,76 @@
+$filter: invert(100%) sepia(0%) saturate(7500%) hue-rotate(116deg)
+    brightness(109%) contrast(109%);
+
+.profile-data {
+    display: flex;
+}
+
+.avatar {
+    width: 150px;
+    display: flex;
+    position: relative;
+    margin-right: 50px;
+
+    &:hover {
+        .visible {
+            filter: invert(48%) sepia(3%) saturate(4%) hue-rotate(326deg)
+                brightness(110%) contrast(78%);
+            opacity: 0.5;
+            cursor: pointer;
+        }
+
+        .upload-icon {
+            opacity: 1;
+        }
+    }
+
+    .avatar-icon {
+        border-radius: 50%;
+        width: 130px;
+        height: 130px;
+        transition: 0.4s;
+    }
+
+    .upload-icon {
+        display: flex;
+        position: absolute;
+        filter: $filter;
+        height: 50px;
+        width: 50px;
+        left: 40px;
+        top: 40px;
+        opacity: 0;
+        transition: 0.4s;
+        cursor: pointer;
+    }
+}
+
+.data {
+    display: flex;
+    flex-direction: column;
+
+    .name {
+        font-size: 32px;
+        margin-bottom: 20px;
+    }
+
+    .middot {
+        width: 10px;
+        height: 10px;
+        filter: $filter;
+        margin: 0 10px;
+    }
+
+    p {
+        letter-spacing: 3px;
+    }
+
+    .buttons {
+        display: flex;
+        margin-top: 20px;
+
+        div {
+            margin-right: 20px;
+        }
+    }
+}

+ 39 - 0
src/Components/QueueItem/QueueItem.jsx

@@ -0,0 +1,39 @@
+import React from "react";
+import { removeAudioExtension } from "../../utils/regex";
+import "./style.scoped.scss";
+
+import CloseIcon from "../../assets/close_icon.svg";
+import DragIcon from "../../assets/drag_icon.png";
+import { sortableHandle } from "react-sortable-hoc";
+import { useDispatch } from "react-redux";
+import { actionDeleteTrackFromQueue } from "../../redux/actions/creators/audio";
+
+const DragHandle = sortableHandle(() => <img src={DragIcon} alt="drag-icon" />);
+
+const QueueItem = (props) => {
+    const dispatch = useDispatch();
+
+    const { data } = props;
+    const title = data.id3.title || removeAudioExtension(data.originalFileName);
+
+    return (
+        <div className="queue-item">
+            <div className="info">
+                <div className="drag-icon">
+                    <DragHandle />
+                </div>
+                <p className="title">{title}</p>
+            </div>
+            <div className="buttons">
+                <div
+                    className="button-remove"
+                    onClick={() => dispatch(actionDeleteTrackFromQueue(data))}
+                >
+                    <img src={CloseIcon} alt="close_icon" />
+                </div>
+            </div>
+        </div>
+    );
+};
+
+export default QueueItem;

+ 70 - 0
src/Components/QueueItem/style.scoped.scss

@@ -0,0 +1,70 @@
+$whiteFilter: invert(100%) sepia(0%) saturate(7500%) hue-rotate(116deg)
+    brightness(109%) contrast(109%);
+
+.queue-item {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    width: 100%;
+    height: 50px;
+    min-height: 50px;
+    border-radius: 5px;
+    margin-bottom: 10px;
+    user-select: none;
+
+    .info {
+        display: flex;
+        padding-left: 10px;
+
+        .drag-icon {
+            margin-right: 10px;
+
+            img {
+                width: 25px;
+                height: 25px;
+                filter: $whiteFilter;
+                cursor: pointer;
+            }
+        }
+
+        p {
+            font-size: 18px;
+        }
+    }
+
+    .buttons {
+        height: 100%;
+
+        .button-remove {
+            height: 50px;
+            width: 50px;
+            background-color: rgba(254, 127, 1, 0.6);
+            border-top-right-radius: 5px;
+            border-bottom-right-radius: 5px;
+            display: flex;
+            justify-content: center;
+            align-items: center;
+            cursor: pointer;
+            opacity: 0;
+            transition: 0.3s;
+
+            img {
+                height: 20px;
+                width: 20px;
+                filter: $whiteFilter;
+            }
+
+            &:hover {
+                background-color: rgba(254, 127, 1, 1);
+            }
+        }
+    }
+
+    &:hover {
+        background-color: rgba($color: #fff, $alpha: 0.1);
+
+        .button-remove {
+            opacity: 1;
+        }
+    }
+}

+ 70 - 0
src/Components/SearchBar/SearchBar.jsx

@@ -0,0 +1,70 @@
+import { useState, useEffect } from "react";
+import { createTheme, ThemeProvider } from "@mui/material/styles";
+import Autocomplete from "@mui/material/Autocomplete";
+import TextField from "@mui/material/TextField";
+import { useDispatch, useSelector } from "react-redux";
+import {
+    actionGetTracks,
+    actionRemoveTracks,
+} from "../../redux/actions/creators/tracks";
+import { removeAudioExtension } from "../../utils/regex";
+import { actionAddTrackToPlaylist } from "../../redux/actions/creators/playlists";
+
+const theme = createTheme({
+    palette: {
+        mode: "dark",
+    },
+});
+
+const SearchBar = ({ playlistId }) => {
+    const dispatch = useDispatch();
+    const state = useSelector((store) => store.tracks);
+    const [options, setOptions] = useState([]);
+
+    const onOpenHandler = () => {
+        dispatch(actionRemoveTracks());
+        dispatch(actionGetTracks(null, null, true));
+    };
+
+    const onCloseHandler = (e) => {
+        dispatch(actionRemoveTracks());
+        setOptions([]);
+    };
+
+    useEffect(() => {
+        setOptions(state.tracks);
+    }, [state.tracks]);
+
+    const getTitle = (option) => {
+        if (option) {
+            return (
+                option.id3.title ||
+                removeAudioExtension(option.originalFileName)
+            );
+        }
+    };
+
+    return (
+        <ThemeProvider theme={theme}>
+            <Autocomplete
+                onOpen={onOpenHandler}
+                onClose={onCloseHandler}
+                id="free-solo-demo"
+                options={options}
+                getOptionLabel={getTitle}
+                onChange={(_, value) =>
+                    dispatch(actionAddTrackToPlaylist(playlistId, value._id))
+                }
+                renderInput={(params) => (
+                    <TextField
+                        {...params}
+                        label="Search track to add"
+                        color="success"
+                    />
+                )}
+            />
+        </ThemeProvider>
+    );
+};
+
+export default SearchBar;

+ 10 - 7
src/Components/Tab/Tab.jsx

@@ -1,15 +1,18 @@
-import React, {useState} from "react";
-import { Wrapper, Text, Image } from "./style";
+import React, { useState } from "react";
 import { history } from "../../utils/history";
+import './style.scoped.scss';
 
-const Tab = ({label, icon, url}) => {
+const Tab = ({ label, icon, url }) => {
     const [isActive, setIsActive] = useState(history.location.pathname === url);
 
     return (
-        <Wrapper active={isActive}>
-            <Image src={icon} alt="icon" />
-            <Text>{label}</Text>
-        </Wrapper>
+        <div
+            className="tab"
+            style={{ background: isActive ? "rgba(255, 255, 255, 0.1)" : "none" }}
+        >
+            <img className="image" src={icon} alt="icon" />
+            <p className="text">{label}</p>
+        </div>
     );
 };
 

+ 27 - 0
src/Components/Tab/style.scoped.scss

@@ -0,0 +1,27 @@
+.tab {
+    display: flex;
+    width: 99%;
+    margin: 0 0 5px -10px;
+    padding: 5px 0 5px 10px;
+    display: flex;
+    align-items: center;
+    transition: 0.2s;
+    border-radius: 0 3px 3px 0;
+
+    &:hover {
+        cursor: pointer;
+        background: rgba(255, 255, 255, 0.1);
+    }
+}
+
+.image {
+    filter: invert(100%) sepia(0%) saturate(7500%) hue-rotate(116deg)
+        brightness(109%) contrast(109%);
+    width: 20px;
+    height: 20px;
+}
+
+.text {
+    font-size: 14px;
+    margin-left: 10px;
+}

+ 100 - 0
src/Components/TrackItem/TrackItem.jsx

@@ -0,0 +1,100 @@
+import React from "react";
+import "./style.scoped.scss";
+import PlayIcon from "../../assets/play_icon_2.svg";
+import { styled } from "@mui/material/styles";
+import Checkbox from "@mui/material/Checkbox";
+import { useDispatch } from "react-redux";
+import {
+    actionClearQueue,
+    actionSetTrack,
+} from "../../redux/actions/creators/audio";
+import { removeAudioExtension } from "../../utils/regex";
+import { actionTrackToDelete } from "../../redux/actions/creators/tracks";
+
+const BpIcon = styled("span")(({ theme = "dark" }) => ({
+    borderRadius: 3,
+    width: 16,
+    height: 16,
+    backgroundColor: "gray",
+    ".Mui-focusVisible &": {
+        outline: "2px auto rgba(19,124,189,.6)",
+        outlineOffset: 2,
+    },
+    "input:hover ~ &": {
+        backgroundColor: "gray",
+    },
+}));
+
+const BpCheckedIcon = styled(BpIcon)({
+    backgroundColor: "gray",
+    "&:before": {
+        display: "block",
+        width: 16,
+        height: 16,
+        borderRadius: 3,
+        backgroundColor: "#106ba3",
+        backgroundImage:
+            "url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath" +
+            " fill-rule='evenodd' clip-rule='evenodd' d='M12 5c-.28 0-.53.11-.71.29L7 9.59l-2.29-2.3a1.003 " +
+            "1.003 0 00-1.42 1.42l3 3c.18.18.43.29.71.29s.53-.11.71-.29l5-5A1.003 1.003 0 0012 5z' fill='%23fff'/%3E%3C/svg%3E\")",
+        content: '""',
+    },
+    "input:hover ~ &": {
+        backgroundColor: "#106ba3",
+    },
+});
+
+function BpCheckbox(props) {
+    return (
+        <Checkbox
+            sx={{
+                "&:hover": { bgcolor: "transparent" },
+            }}
+            disableRipple
+            color="default"
+            checkedIcon={<BpCheckedIcon />}
+            icon={<BpIcon />}
+            {...props}
+        />
+    );
+}
+
+const TrackItem = (props) => {
+    const dispatch = useDispatch();
+
+    const track = props.data;
+    const title =
+        track.id3.title || removeAudioExtension(track.originalFileName);
+
+    return (
+        <div className="track">
+            <div className="buttons">
+                <div
+                    className="button-delete"
+                    onClick={() => dispatch(actionTrackToDelete(track._id))}
+                >
+                    <BpCheckbox />
+                </div>
+                <div
+                    className="button-play"
+                    onClick={() => {
+                        dispatch(actionSetTrack(track));
+                        dispatch(actionClearQueue());
+                    }}
+                >
+                    <img src={PlayIcon} alt="play_icon" />
+                </div>
+            </div>
+
+            <div className="info">
+                <p className="title">{title ?? "Title"}</p>
+                <p className="artist">{track.id3.artist ?? "artist"}</p>
+                <p className="album">{track.id3.album ?? "album"}</p>
+                <p className="year">{track.id3.year ?? "year"}</p>
+                <p className="genre">{track.id3.genre ?? "genre"}</p>
+            </div>
+        </div>
+    );
+};
+
+export default TrackItem;

+ 56 - 0
src/Components/TrackItem/style.scoped.scss

@@ -0,0 +1,56 @@
+.track {
+    display: flex;
+    align-items: center;
+    width: 100%;
+    min-height: 50px;
+    border-radius: 5px;
+    transition: 0.3s;
+
+    .buttons {
+        display: flex;
+        align-items: center;
+        margin-right: 5px;
+
+        .button-play {
+            display: flex;
+            align-items: center;
+
+            img {
+                width: 30px;
+                height: 30px;
+                transition: 0.2s;
+                opacity: 0;
+
+                &:hover {
+                    opacity: 1;
+                    cursor: pointer;
+                }
+            }
+            filter: invert(42%) sepia(88%) saturate(1050%) hue-rotate(358deg)
+                brightness(98%) contrast(103%);
+        }
+    }
+
+    .info {
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+        width: 100%;
+
+        p {
+            width: 15%;
+            word-break: break-all;
+            // text-align: center;
+        }
+    }
+
+    &:hover {
+        background-color: rgba($color: #fff, $alpha: 0.1);
+
+        .button-play {
+            img {
+                opacity: 1;
+            }
+        }
+    }
+}

+ 24 - 4
src/Components/index.js

@@ -1,9 +1,29 @@
-import LeftBar from './LeftBar/LeftBar';
-import Player from './Player/Player';
-import Tab from './Tab/Tab';
+import LeftBar from "./LeftBar/LeftBar";
+import Player from "./Player/Player";
+import Tab from "./Tab/Tab";
+import PrivateRoute from "./PrivateRoute/PrivateRoute";
+import AddButton from "./AddButton/AddButton";
+import OrangeButton from "./OrangeButton/OrangeButton";
+import DropdownTracks from "./DropdownTracks/DropdownTracks";
+import PlaylistCover from "./PlaylistCover/PlaylistCover";
+import SearchBar from "./SearchBar/SearchBar";
+import PlaylistsTrack from "./PlaylistsTrack/PlaylistsTrack";
+import QueueItem from "./QueueItem/QueueItem";
+import PlaylistInfo from "./PlaylistInfo/PlaylistInfo";
+import PaginationBar from "./PaginationBar/PaginationBar";
 
 export {
     LeftBar,
     Player,
     Tab,
-}
+    PrivateRoute,
+    AddButton,
+    OrangeButton,
+    DropdownTracks,
+    PlaylistCover,
+    SearchBar,
+    PlaylistsTrack,
+    QueueItem,
+    PlaylistInfo,
+    PaginationBar,
+};

+ 44 - 7
src/Pages/Home/Home.jsx

@@ -1,16 +1,53 @@
 import React from "react";
-import { LeftBar, Player } from "../../components";
-import { Content, Main, Wrapper } from "./styles";
+import { useNavigate } from "react-router-dom";
+import { LeftBar, Player } from "../../Components";
+import ProfileData from "../../Components/ProfileData/ProfileData";
+import { ROUTES } from "../../utils/constants";
+import { push } from "../../utils/history";
+import "./style.scoped.scss";
 
 const Home = () => {
+    const navigate = useNavigate();
+
     return (
-        <Wrapper>
-            <Main>
+        <div className="home">
+            <div className="main">
                 <LeftBar />
-                <Content></Content>
-            </Main>
+                <main className="content">
+                    <div className="wrapper">
+                        <div className="header">
+                            <h1 className="page-name">Home</h1>
+                        </div>
+                        <ProfileData
+                            avatarChanging={false}
+                            buttonsVisible={true}
+                        />
+                        <hr className="separator" />
+                        <div className="buttons">
+                            <div
+                                className="button-tracks box"
+                                onClick={() => {
+                                    push(ROUTES.TRACKS);
+                                    navigate(ROUTES.TRACKS);
+                                }}
+                            >
+                                Tracks
+                            </div>
+                            <div
+                                className="button-playlists box"
+                                onClick={() => {
+                                    push(ROUTES.PLAYLISTS);
+                                    navigate(ROUTES.PLAYLISTS);
+                                }}
+                            >
+                                Playlists
+                            </div>
+                        </div>
+                    </div>
+                </main>
+            </div>
             <Player />
-        </Wrapper>
+        </div>
     );
 };
 

+ 67 - 0
src/Pages/Home/style.scoped.scss

@@ -0,0 +1,67 @@
+.home {
+    display: flex;
+    flex-direction: column;
+    min-height: 100vh;
+}
+
+.main {
+    display: flex;
+    flex-grow: 1;
+}
+
+.content {
+    display: flex;
+    justify-content: center;
+    width: 85%;
+
+    .wrapper {
+        width: 90%;
+        margin-top: 10px;
+    }
+
+    .separator {
+        margin: 50px 0;
+    }
+}
+
+.header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 40px;
+
+    .page-name {
+        font-size: 54px;
+    }
+}
+
+.buttons {
+    display: flex;
+    justify-content: space-around;
+    align-items: center;
+    
+
+    .box {
+        width: 250px;
+        height: 250px;
+        border: 1px solid white;
+        border-radius: 10px;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        font-size: 34px;
+        font-weight: 600;
+        cursor: pointer;
+    }
+
+    .button-tracks {
+        background-image: url("../../assets/track_icon.svg");
+        background-size: 25px 25px;
+    }
+
+    .button-playlists {
+        background-image: url("../../assets/playlist_icon_2.svg");
+        background-size: 25px 25px;
+
+    }
+}

+ 68 - 0
src/Pages/PlaylistItem/PlaylistItem.jsx

@@ -0,0 +1,68 @@
+import React, { useEffect } from "react";
+import { useDispatch, useSelector } from "react-redux";
+import { useParams } from "react-router-dom";
+import {
+    LeftBar,
+    Player,
+    PlaylistCover,
+    PlaylistsTrack,
+    SearchBar,
+} from "../../Components";
+import { actionGetPlaylistById } from "../../redux/actions/creators/playlists";
+import "./style.scoped.scss";
+
+const PlaylistItem = () => {
+    const { id } = useParams(); // playlistID
+    const dispatch = useDispatch();
+    const playlist = useSelector((store) => store.playlists.currentPlaylist);
+
+    useEffect(() => {
+        dispatch(actionGetPlaylistById(id));
+    }, []);
+
+    return (
+        <div className="playlist">
+            <div className="main">
+                <LeftBar />
+                <main className="content">
+                    <div className="wrapper">
+                        <div className="header">
+                            <div className="page-name">{playlist?.name}</div>
+                            <div className="playlist-counter">
+                                {playlist?.tracks?.length ?? 0} tracks
+                            </div>
+                        </div>
+                        <main className="main">
+                            <div className="left-bar-info">
+                                <PlaylistCover
+                                    _id={id}
+                                    name={playlist?.name}
+                                    description={playlist?.description}
+                                />
+                            </div>
+                            <div className="right-bar-tracks">
+                                <div className="search-bar">
+                                    <SearchBar playlistId={id} />
+                                </div>
+                                <div className="tracks-list">
+                                    {playlist?.tracks?.length > 0
+                                        ? playlist.tracks.map((track) => (
+                                              <PlaylistsTrack
+                                                  key={track._id}
+                                                  playlistId={id}
+                                                  track={track}
+                                              />
+                                          ))
+                                        : null}
+                                </div>
+                            </div>
+                        </main>
+                    </div>
+                </main>
+            </div>
+            <Player />
+        </div>
+    );
+};
+
+export default PlaylistItem;

+ 61 - 0
src/Pages/PlaylistItem/style.scoped.scss

@@ -0,0 +1,61 @@
+.playlist {
+    display: flex;
+    flex-direction: column;
+    min-height: 100vh;
+}
+
+.main {
+    display: flex;
+    flex-grow: 1;
+}
+
+.content {
+    display: flex;
+    justify-content: center;
+    width: 85%;
+
+    .wrapper {
+        width: 90%;
+        margin-top: 10px;
+    }
+}
+
+.header {
+    display: flex;
+    align-items: center;
+    margin-bottom: 40px;
+
+    .page-name {
+        font-size: 48px;
+        margin-right: 20px;
+    }
+
+    .playlist-counter {
+        align-self: flex-end;
+        font-size: 30px;
+    }
+}
+
+.main {
+    display: flex;
+}
+
+.left-bar-info {
+    display: flex;
+    flex-direction: column;
+    position: relative;
+    margin-right: 50px;
+}
+
+.right-bar-tracks {
+    width: 100%;
+
+    .search-bar {
+        margin-bottom: 30px;
+    }
+
+    .tracks-list {
+        display: flex;
+        flex-direction: column;
+    }
+}

+ 66 - 8
src/Pages/Playlists/Playlists.jsx

@@ -1,16 +1,74 @@
-import React from "react";
-import { LeftBar, Player } from "../../components";
-import { Content, Main, Wrapper } from "./styles";
+import React, { useEffect, useState } from "react";
+import {
+    LeftBar,
+    OrangeButton,
+    PaginationBar,
+    Player,
+    PlaylistInfo,
+} from "../../Components";
+import PlusIcon from "../../assets/dark_plus_icon.svg";
+import "./style.scoped.scss";
+import { useDispatch, useSelector } from "react-redux";
+import {
+    actionCreatePlaylist,
+    actionGetPlaylists,
+} from "../../redux/actions/creators/playlists";
+import { useNavigate, useSearchParams } from "react-router-dom";
 
 const Playlists = () => {
+    const navigate = useNavigate();
+    const dispatch = useDispatch();
+    const state = useSelector((store) => store.playlists);
+
+    const [searchParams] = useSearchParams();
+
+    const createPlaylistHandler = () => {
+        dispatch(actionCreatePlaylist(navigate));
+    };
+
+    useEffect(() => {
+        const currPage = +searchParams.get("page") || state.currentPage;
+        dispatch(actionGetPlaylists(currPage));
+    }, [state.currentPage]);
+
     return (
-        <Wrapper>
-            <Main>
+        <div className="playlists">
+            <div className="main">
                 <LeftBar />
-                <Content></Content>
-            </Main>
+
+                <div className="wrapper">
+                    <main className="content">
+                        <div className="user-playlists">
+                            <div className="header">
+                                <div className="page-name">Playlists</div>
+                            </div>
+                            <div className="controls-bar">
+                                <div className="button-create-playlist">
+                                    <OrangeButton
+                                        icon={PlusIcon}
+                                        text={"Create playlist"}
+                                        clickHandler={createPlaylistHandler}
+                                    />
+                                </div>
+                            </div>
+                            <div className="playlists-list">
+                                {state.playlists.map((playlist) => (
+                                    <PlaylistInfo
+                                        key={playlist._id}
+                                        playlist={playlist}
+                                    />
+                                ))}
+                            </div>
+                        </div>
+                        <PaginationBar
+                            currentPage={state.currentPage}
+                            count={state.count}
+                        />
+                    </main>
+                </div>
+            </div>
             <Player />
-        </Wrapper>
+        </div>
     );
 };
 

+ 59 - 0
src/Pages/Playlists/style.scoped.scss

@@ -0,0 +1,59 @@
+.playlists {
+    display: flex;
+    flex-direction: column;
+    min-height: 100vh;
+}
+
+.main {
+    display: flex;
+    flex-grow: 1;
+    height: 100%;
+}
+
+.wrapper {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    width: 85%;
+    min-height: 100%;
+    margin-top: 10px;
+
+    .content {
+        width: 90%;
+        height: 100%;
+        display: flex;
+        flex-direction: column;
+        justify-content: space-between;
+        margin-bottom: 10px;
+    }
+
+    .user-playlists {
+        height: 100%;
+    }
+}
+
+.header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 10px;
+
+    .page-name {
+        font-size: 54px;
+    }
+}
+
+.controls-bar {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-left: 5px;
+}
+
+.playlists-list {
+    height: 70vh;
+    overflow-y: scroll;
+    display: flex;
+    flex-direction: column;
+    margin-top: 20px;
+}

+ 26 - 9
src/Pages/Profile/Profile.jsx

@@ -1,22 +1,39 @@
 import React from "react";
 import { useDispatch } from "react-redux";
 import { actionLogout } from "../../redux/actions/creators/auth";
-import { LeftBar, Player } from "../../components";
-import { Content, Main, Wrapper } from "./styles";
+import { LeftBar, Player } from "../../Components";
+import "./style.scoped.scss";
+import ProfileData from "../../Components/ProfileData/ProfileData";
+
 
 const Profile = () => {
     const dispatch = useDispatch();
 
     return (
-        <Wrapper>
-            <Main>
+        <div className="profile">
+            <div className="main">
                 <LeftBar />
-                <Content>
-                    <button onClick={() => dispatch(actionLogout())}>LOGOUT</button>
-                </Content>
-            </Main>
+                <main className="content">
+                    <div className="wrapper">
+                        <div className="header">
+                            <h1 className="page-name">Profile</h1>
+                        </div>
+                        <ProfileData
+                            avatarChanging={true}
+                            buttonsVisible={false}
+                        />
+                        <hr className="separator" />
+                        <div
+                            className="button-logout"
+                            onClick={() => dispatch(actionLogout())}
+                        >
+                            Logout
+                        </div>
+                    </div>
+                </main>
+            </div>
             <Player />
-        </Wrapper>
+        </div>
     );
 };
 

+ 60 - 0
src/Pages/Profile/style.scoped.scss

@@ -0,0 +1,60 @@
+.profile {
+    display: flex;
+    flex-direction: column;
+    min-height: 100vh;
+}
+
+.main {
+    display: flex;
+    flex-grow: 1;
+}
+
+.content {
+    display: flex;
+    justify-content: center;
+    width: 85%;
+
+    .wrapper {
+        width: 90%;
+        margin-top: 10px;
+    }
+
+    .separator {
+        margin: 50px 0;
+    }
+}
+
+.header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 40px;
+
+    .page-name {
+        font-size: 54px;
+    }
+}
+
+.buttons-change {
+    display: flex;
+    justify-content: space-around;
+    align-content: center;
+}
+
+.button-logout {
+    margin-top: 50px;
+    border: 1px solid #fff;
+    height: 50px;
+    border-radius: 10px;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    font-size: 20px;
+    font-weight: 600;
+    transition: 0.2s;
+
+    &:hover {
+        cursor: pointer;
+        border: 1px solid #000;
+    }
+}

+ 69 - 7
src/Pages/Queue/Queue.jsx

@@ -1,16 +1,78 @@
 import React from "react";
-import { LeftBar, Player } from "../../components";
-import { Content, Main, Wrapper } from "./styles";
+import { useDispatch, useSelector } from "react-redux";
+import { LeftBar, Player, QueueItem } from "../../Components";
+import {
+    sortableContainer,
+    sortableElement,
+    sortableHandle,
+} from "react-sortable-hoc";
+import { arrayMoveImmutable } from "array-move";
+import "./style.scoped.scss";
+import { createSelector } from "reselect";
+import { actionSetQueue } from "../../redux/actions/creators/audio";
+
+const getQueue = createSelector(
+    (state) => state.audio.queue,
+    (queue) => queue
+);
+
+const DragHandle = sortableHandle(() => DragIcon);
+
+const SortableItem = sortableElement(({ track }) => (
+    <QueueItem key={track._id} data={track} dragHandle={<DragHandle />} />
+));
+
+const SortableContainer = sortableContainer(({ children }) => {
+    return <div>{children}</div>;
+});
 
 const Queue = () => {
+    const queue = useSelector(getQueue);
+    const dispatch = useDispatch();
+
+    const onSortEnd = ({ oldIndex, newIndex }) => {
+        // const
+        dispatch(
+            actionSetQueue({
+                ...queue,
+                tracks: arrayMoveImmutable(queue.tracks, oldIndex, newIndex),
+            })
+        );
+    };
+
     return (
-        <Wrapper>
-            <Main>
+        <div className="queue">
+            <div className="main">
                 <LeftBar />
-                <Content></Content>
-            </Main>
+                <main className="content">
+                    <div className="wrapper">
+                        <div className="header">
+                            <h1 className="page-name">
+                                Queue
+                                {queue?.name ? ` | ${queue.name}` : null}
+                            </h1>
+                        </div>
+                        <div className="track-list">
+                            <SortableContainer
+                                onSortEnd={onSortEnd}
+                                useDragHandle
+                            >
+                                {queue?.tracks?.length > 0
+                                    ? queue.tracks.map((track, index) => (
+                                          <SortableItem
+                                              key={track._id}
+                                              index={index}
+                                              track={track}
+                                          />
+                                      ))
+                                    : null}
+                            </SortableContainer>
+                        </div>
+                    </div>
+                </main>
+            </div>
             <Player />
-        </Wrapper>
+        </div>
     );
 };
 

+ 39 - 0
src/Pages/Queue/style.scoped.scss

@@ -0,0 +1,39 @@
+.queue {
+    display: flex;
+    flex-direction: column;
+    min-height: 100vh;
+}
+
+.main {
+    display: flex;
+    flex-grow: 1;
+}
+
+.content {
+    display: flex;
+    justify-content: center;
+    width: 85%;
+
+    .wrapper {
+        width: 90%;
+        margin-top: 10px;
+    }
+}
+
+.header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 40px;
+
+    .page-name {
+        font-size: 54px;
+    }
+}
+
+.tracks-list {
+    display: flex;
+    flex-direction: column;
+    height: 70vh;
+    overflow-y: scroll;
+}

+ 107 - 8
src/Pages/Tracks/Tracks.jsx

@@ -1,16 +1,115 @@
-import React from "react";
-import { LeftBar, Player } from "../../components";
-import { Content, Main, Wrapper } from "./styles";
+import React, { useEffect, useRef } from "react";
+import {
+    AddButton,
+    LeftBar,
+    Player,
+    OrangeButton,
+    DropdownTracks,
+} from "../../Components";
+import "./style.scoped.scss";
+
+import AddTrackIcon from "../../assets/add_track_icon.svg";
+import PlayIcon from "../../assets/play_icon_3.png";
+
+import { Link } from "react-router-dom";
+import { ROUTES } from "../../utils/constants";
+import TrackItem from "../../Components/TrackItem/TrackItem";
+import { useDispatch, useSelector } from "react-redux";
+import {
+    actionDeleteTracks,
+    actionGetTracks,
+    actionGetTracksCount,
+    actionTracksFetchingOn,
+} from "../../redux/actions/creators/tracks";
+import store from "../../redux/store";
+import { actionPlay, actionSetQueue } from "../../redux/actions/creators/audio";
 
 const Tracks = () => {
+    const list = useRef(null);
+    const dispatch = useDispatch();
+    const state = useSelector((store) => store.tracks);
+
+    useEffect(() => {
+        dispatch(actionGetTracksCount());
+
+        list.current.addEventListener("scroll", scrollHandler);
+        return function() {
+            document.removeEventListener("scroll", scrollHandler);
+        };
+    }, []);
+
+    useEffect(() => {
+        if (state.isFetching) {
+            dispatch(actionGetTracks(state.page, state.sortBy));
+        }
+    }, [state.isFetching]);
+
+    const scrollHandler = (e) => {
+        const { tracks, count } = store.getState().tracks;
+
+        if (
+            e.target.scrollHeight -
+                (e.target.scrollTop + e.target.clientHeight) <
+                100 &&
+            tracks.length < count - 1
+        ) {
+            dispatch(actionTracksFetchingOn());
+        }
+    };
+
     return (
-        <Wrapper>
-            <Main>
+        <div className="tracks">
+            <div className="main">
                 <LeftBar />
-                <Content></Content>
-            </Main>
+
+                <main className="content">
+                    <div className="wrapper">
+                        <div className="header">
+                            <h1 className="page-name">Tracks</h1>
+                            <div className="buttons">
+                                <AddButton
+                                    text={"Delete tracks"}
+                                    opacity={state.toDelete.length ? 1 : 0}
+                                    clickHandler={() =>
+                                        dispatch(
+                                            actionDeleteTracks(state.toDelete)
+                                        )
+                                    }
+                                />
+                                <Link to={ROUTES.UPLOAD}>
+                                    <AddButton
+                                        icon={AddTrackIcon}
+                                        text={"Add tracks"}
+                                    />
+                                </Link>
+                            </div>
+                        </div>
+                        <div className="filter-bar">
+                            <OrangeButton
+                                icon={PlayIcon}
+                                text={`Play all (${state.tracks.length})`}
+                                clickHandler={() => {
+                                    dispatch(
+                                        actionSetQueue({ tracks: state.tracks })
+                                    );
+                                    dispatch(actionPlay());
+                                }}
+                            />
+                            <DropdownTracks />
+                        </div>
+                        <div className="tracks-list" ref={list}>
+                            {state.tracks.length > 0
+                                ? state.tracks.map((track) => (
+                                      <TrackItem key={track._id} data={track} />
+                                  ))
+                                : null}
+                        </div>
+                    </div>
+                </main>
+            </div>
+
             <Player />
-        </Wrapper>
+        </div>
     );
 };
 

+ 54 - 0
src/Pages/Tracks/style.scoped.scss

@@ -0,0 +1,54 @@
+.tracks {
+    display: flex;
+    flex-direction: column;
+    min-height: 100vh;
+}
+
+.main {
+    display: flex;
+    flex-grow: 1;
+}
+
+.content {
+    display: flex;
+    justify-content: center;
+    width: 85%;
+
+    .wrapper {
+        width: 90%;
+        margin-top: 10px;
+    }
+}
+
+.header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 10px;
+
+    .page-name {
+        font-size: 54px;
+    }
+
+    .buttons {
+        display: flex;
+    }
+}
+
+.filter-bar {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 20px;
+
+    .sorting {
+        width: 100px;
+    }
+}
+
+.tracks-list {
+    display: flex;
+    flex-direction: column;
+    height: 70vh;
+    overflow-y: scroll;
+}

+ 115 - 0
src/Pages/Upload/Upload.jsx

@@ -0,0 +1,115 @@
+import { useMemo } from "react";
+import { LeftBar, Player } from "../../Components";
+import { useDropzone } from "react-dropzone";
+import "./style.scoped.scss";
+import { useDispatch, useSelector } from "react-redux";
+import {
+    actionUploadOpen,
+    actionUploadTracks,
+} from "../../redux/actions/creators/upload";
+
+const baseStyle = {
+    flex: 1,
+    display: "flex",
+    flexDirection: "column",
+    alignItems: "center",
+    padding: "100px",
+    borderWidth: 3,
+    borderRadius: 2,
+    borderColor: "#eeeeee",
+    borderStyle: "dashed",
+    backgroundColor: "gray",
+    color: "black",
+    fontWeigt: 600,
+    fontSize: "20px",
+    outline: "none",
+    transition: "border .3s ease-in-out",
+    cursor: "pointer",
+};
+
+const focusedStyle = {
+    borderColor: "#2196f3",
+};
+
+const acceptStyle = {
+    borderColor: "#00e676",
+};
+
+const rejectStyle = {
+    borderColor: "#ff1744",
+};
+
+const Upload = () => {
+    const {
+        acceptedFiles,
+        fileRejections,
+        getRootProps,
+        getInputProps,
+        isFocused,
+        isDragAccept,
+        isDragReject,
+    } = useDropzone({
+        maxFiles: 15,
+        accept: {
+            "audio/*": [],
+        },
+    });
+
+    const files = acceptedFiles.map((file) => (
+        <li key={file.path}>{file.path}</li>
+    ));
+
+    const style = useMemo(
+        () => ({
+            ...baseStyle,
+            ...(isFocused ? focusedStyle : {}),
+            ...(isDragAccept ? acceptStyle : {}),
+            ...(isDragReject ? rejectStyle : {}),
+        }),
+        [isFocused, isDragAccept, isDragReject]
+    );
+
+    const dispatch = useDispatch();
+    const state = useSelector((store) => store.upload);
+
+    return (
+        <div className="upload">
+            <div className="main">
+                <LeftBar />
+                <main className="content">
+                    <div className="wrapper">
+                        <div className="header">
+                            <h1 className="page-name">Upload tracks </h1>
+                        </div>
+
+                        <section
+                            className="container"
+                            onClick={() => dispatch(actionUploadOpen())}
+                        >
+                            <div {...getRootProps({ style })}>
+                                <input {...getInputProps()} />
+                                Open / drag audio files
+                            </div>
+                            <aside className="accepted-files">
+                                <h4>Accepted files</h4>
+                                <ul>{files}</ul>
+                            </aside>
+                        </section>
+
+                        <button
+                            className="button-add_tracks"
+                            onClick={() =>
+                                dispatch(actionUploadTracks(acceptedFiles))
+                            }
+                        >
+                            {state.status ? state.status : "UPLOAD TRACKS"}
+                        </button>
+                    </div>
+                </main>
+            </div>
+            <Player />
+        </div>
+    );
+};
+
+export default Upload;

+ 72 - 0
src/Pages/Upload/style.scoped.scss

@@ -0,0 +1,72 @@
+.upload {
+    display: flex;
+    flex-direction: column;
+    min-height: 100vh;
+}
+
+.main {
+    display: flex;
+    flex-grow: 1;
+}
+
+.content {
+    display: flex;
+    justify-content: center;
+    width: 85%;
+
+    .wrapper {
+        width: 90%;
+        margin-top: 10px;
+    }
+}
+
+.header {
+    display: flex;
+    align-items: center;
+    margin-bottom: 10px;
+
+    .page-name {
+        font-size: 54px;
+    }
+}
+
+.accepted-files {
+    margin-top: 10px;
+}
+
+.button-add_tracks {
+    height: 40px;
+    width: 100%;
+    border: none;
+    border-radius: 5px;
+    margin-top: 20px;
+    background-color: rgb(130, 130, 130);
+
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    font-weight: 600;
+    font-size: 18px;
+
+    background-image: linear-gradient(90deg, blue 50%, transparent 50%),
+        linear-gradient(90deg, blue 50%, transparent 50%),
+        linear-gradient(0, blue 50%, transparent 50%),
+        linear-gradient(0, blue 50%, transparent 50%);
+    background-repeat: repeat-x, repeat-x, repeat-y, repeat-y;
+    background-size: 10px 2px, 10px 2px, 2px 10px, 2px 10px;
+    animation: marching-ants 0.8s infinite linear;
+
+    @keyframes marching-ants {
+        0% {
+            background-position: 0 0, 10px 100%, 0 10px, 100% 0;
+        }
+
+        100% {
+            background-position: 10px 0, 0 100%, 0 0, 100% 10px;
+        }
+    }
+
+    &:hover {
+        cursor: pointer;
+    }
+}

+ 3 - 2
src/Pages/index.js

@@ -5,7 +5,7 @@ import Tracks from './Tracks/Tracks';
 import Playlists from './Playlists/Playlists';
 import Login from './Login/Login';
 import Register from './Register/Register';
-
+import Upload from './Upload/Upload';
 
 export {
     Home, 
@@ -15,4 +15,5 @@ export {
     Playlists,
     Login,
     Register,    
-}
+    Upload
+}

BIN
src/assets/6570e93e3a5a4b83a692d229864a.jpg


+ 22 - 0
src/assets/add_track_icon.svg

@@ -0,0 +1,22 @@
+<?xml version="1.0" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
+ "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
+<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
+ width="50.000000pt" height="50.000000pt" viewBox="0 0 50.000000 50.000000"
+ preserveAspectRatio="xMidYMid meet">
+
+<g transform="translate(0.000000,50.000000) scale(0.100000,-0.100000)"
+fill="#000000" stroke="none">
+<path d="M235 446 l-100 -22 -5 -129 -5 -130 -39 -6 c-81 -13 -73 -99 10 -99
+63 0 74 23 74 159 l0 119 65 16 c35 9 69 16 75 16 7 0 10 -27 8 -77 l-3 -78
+-39 -6 c-56 -9 -74 -49 -38 -82 26 -24 41 -21 48 8 3 14 21 38 40 55 l34 29 0
+126 c0 94 -3 125 -12 124 -7 0 -58 -11 -113 -23z"/>
+<path d="M351 186 c-87 -48 -50 -186 49 -186 51 0 100 49 100 99 0 75 -83 124
+-149 87z m104 -31 c50 -49 15 -135 -55 -135 -41 0 -80 39 -80 80 0 70 86 105
+135 55z"/>
+<path d="M390 130 c0 -13 -7 -20 -20 -20 -11 0 -20 -4 -20 -10 0 -5 9 -10 20
+-10 13 0 20 -7 20 -20 0 -11 5 -20 10 -20 6 0 10 9 10 20 0 13 7 20 20 20 11
+0 20 5 20 10 0 6 -9 10 -20 10 -13 0 -20 7 -20 20 0 11 -4 20 -10 20 -5 0 -10
+-9 -10 -20z"/>
+</g>
+</svg>

File diff suppressed because it is too large
+ 1 - 0
src/assets/close_icon.svg


+ 1 - 0
src/assets/dark_plus_icon.svg

@@ -0,0 +1 @@
+<?xml version="1.0"?><svg fill="#000000" xmlns="http://www.w3.org/2000/svg"  viewBox="0 0 24 24" width="384px" height="384px">    <path d="M12,2C6.477,2,2,6.477,2,12s4.477,10,10,10s10-4.477,10-10S17.523,2,12,2z M16,13h-3v3c0,0.552-0.448,1-1,1h0 c-0.552,0-1-0.448-1-1v-3H8c-0.552,0-1-0.448-1-1v0c0-0.552,0.448-1,1-1h3V8c0-0.552,0.448-1,1-1h0c0.552,0,1,0.448,1,1v3h3 c0.552,0,1,0.448,1,1v0C17,12.552,16.552,13,16,13z"/></svg>

BIN
src/assets/dot.png


BIN
src/assets/drag_icon.png


+ 15 - 0
src/assets/play_icon_2.svg

@@ -0,0 +1,15 @@
+<?xml version="1.0" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
+ "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
+<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
+ width="50.000000pt" height="50.000000pt" viewBox="0 0 50.000000 50.000000"
+ preserveAspectRatio="xMidYMid meet">
+
+<g transform="translate(0.000000,50.000000) scale(0.100000,-0.100000)"
+fill="#000000" stroke="none">
+<path d="M100 250 c0 -104 3 -190 7 -190 10 0 323 184 323 190 0 6 -313 190
+-323 190 -4 0 -7 -85 -7 -190z m187 64 c57 -32 103 -61 103 -64 0 -3 -46 -32
+-103 -64 -56 -33 -117 -68 -134 -79 l-33 -19 0 162 0 162 33 -19 c17 -11 78
+-46 134 -79z"/>
+</g>
+</svg>

BIN
src/assets/play_icon_3.png


File diff suppressed because it is too large
+ 7 - 0
src/assets/playlist_icon_2.svg


BIN
src/assets/song.mp3


BIN
src/assets/upload.png


+ 1 - 14
src/index.js

@@ -12,17 +12,4 @@ root.render(
     </Provider>
 );
 
-import { getGQL } from "./utils/getGQL";
-import { registerQuery } from "./utils/graphQueries";
-
-// getGQL(`{
-//     query {
-//         FindTrack(query: "[{}]") {
-//             owner {
-//                 login
-//             }
-//         }
-//     }
-// }`).then(value => console.log(value))
-
-store.subscribe(() => console.log(store.getState()))
+// store.subscribe(() => console.log(store.getState()));

+ 115 - 24
src/redux/actions/creators/audio.js

@@ -1,72 +1,163 @@
-import song from "../../../assets/song.mp3"; // delete
-import song2 from "../../../assets/little.mp3"; // delete
-
 import store from "../../store";
 import types from "../types";
+import { backendURL } from "../../../utils/constants";
+import { actionGetTracks, actionRemoveTracks } from "./tracks";
 
-const audio = new Audio(song);
+const audio = new Audio();
 
-const actionTogglePlay = (status) => ({
+const togglePlay = (status) => ({
     type: types.TOGGLE_PLAY,
     payload: status,
 });
-const actionSetDuration = (value) => ({
+const setDuration = (value) => ({
     type: types.SET_DURATION,
     payload: value,
 });
-const actionSetCurrentTime = (value) => ({
+const setCurrentTime = (value) => ({
     type: types.SET_CURRENT_TIME,
     payload: value,
 });
-const actionSetVolume = (value) => ({
+const setVolume = (value) => ({
     type: types.SET_VOLUME,
     payload: value,
 });
-const actionToggleRepeat = (status) => ({
+const toggleRepeat = (status) => ({
     type: types.TOGGLE_REPEAT,
     payload: status,
 });
+const setTrack = (track) => ({
+    type: types.SET_TRACK,
+    payload: track,
+});
+const resetPlayer = () => ({
+    type: types.PLAYER_START_POSITION,
+});
+const setCurrentIndex = (index) => ({
+    type: types.PLAYER_SET_CURRENT_INDEX,
+    payload: index,
+});
 
-export const togglePlay = (status) => {
+export const actionTogglePlay = (status) => {
     status ? audio.play() : audio.pause();
 
-    return actionTogglePlay(status);
+    return togglePlay(status);
+};
+
+export const actionPlay = () => {
+    audio.play();
+
+    return { type: types.PLAY };
+};
+
+export const actionPause = () => {
+    audio.pause();
+
+    return { type: types.PAUSE };
 };
 
-export const setDuration = (e) => {
+export const actionSetDuration = (e) => {
     const value = e.target.duration;
 
-    store.dispatch(actionSetDuration(value));
+    store.dispatch(setDuration(value));
 };
 
-export const setCurrentTime = (value) => {
+export const actionSetCurrentTime = (value) => {
     audio.currentTime = value;
 
-    return actionSetCurrentTime(value);
+    return setCurrentTime(value);
 };
 
-export const setVolume = (value) => {
+export const actionSetVolume = (value) => {
     audio.volume = value;
 
-    return actionSetVolume(value);
+    return setVolume(value);
 };
 
-export const toggleRepeat = (status) => {
+export const actionToggleRepeat = (status) => {
     audio.loop = status;
 
-    return actionToggleRepeat(status);
+    return toggleRepeat(status);
+};
+
+export const actionResetPlayer = () => {
+    store.dispatch(actionPause());
+
+    return resetPlayer();
+};
+
+export const actionSetTrack = (track) => {
+    store.dispatch(actionResetPlayer());
+
+    audio.src = `${backendURL}/${track.url}`;
+    store.dispatch(actionPlay());
+
+    return setTrack(track);
+};
+
+export const actionSetQueue = (queue) => {
+    if (queue.tracks?.length) {
+        store.dispatch(actionSetTrack(queue.tracks[0]));
+    }
+
+    return {
+        type: types.PLAYER_SET_QUEUE,
+        payload: queue,
+    };
+};
+
+export const actionNextTrack = () => {
+    const state = store.getState().audio;
+    const currIdx = state.currentQueueIndex;
+
+    if (currIdx !== state.queue.tracks.length - 1) {
+        store.dispatch(actionSetTrack(state.queue.tracks[currIdx + 1]));
+        audio.play();
+
+        return { type: types.NEXT_TRACK };
+    } else {
+        return actionResetPlayer();
+    }
+};
+
+export const actionPrevTrack = () => {
+    const state = store.getState().audio;
+    const currIdx = state.currentQueueIndex;
+
+    if (currIdx !== 0) {
+        store.dispatch(actionSetTrack(state.queue.tracks[currIdx - 1]));
+
+        return { type: types.PREV_TRACK };
+    } else {
+        return actionResetPlayer();
+    }
+};
+
+export const actionClearQueue = () => ({
+    type: types.CLEAR_QUEUE,
+});
+
+export const actionDeleteTrackFromQueue = (track) => {
+    console.log(track);
+    store.dispatch(actionResetPlayer());
+    store.dispatch(setCurrentIndex(0));
+
+    const { _id } = track;
+
+    return { type: types.DELETE_TRACK_FROM_QUEUE, payload: _id };
 };
 
-// audio listeners
 const onEnded = () => {
-    if (!audio.loop) {
-        //next track if exists(if no toggle_play)
+    const state = store.getState().audio;
+
+    if (!state.isRepeated) {
+        if (!!state.queue?.tracks?.length) store.dispatch(actionNextTrack());
+        else store.dispatch(actionResetPlayer());
     }
 };
 const onTimeUpdate = (e) => {
-    store.dispatch(actionSetCurrentTime(e.target.currentTime));
+    store.dispatch(setCurrentTime(e.target.currentTime));
 };
 
 audio.addEventListener("ended", onEnded);
 audio.addEventListener("timeupdate", onTimeUpdate);
-audio.addEventListener("durationchange", setDuration);
+audio.addEventListener("durationchange", actionSetDuration);

+ 232 - 0
src/redux/actions/creators/playlists.js

@@ -0,0 +1,232 @@
+import { LIMIT, ROUTES } from "../../../utils/constants";
+import { getGQL } from "../../../utils/getGQL";
+import {
+    createPlaylistQuery,
+    getIdsInPlaylistQuery,
+    getPlaylistByIdQuery,
+    getPlaylistsCountQuery,
+    getPlaylistsQuery,
+    upsertPlaylistInfoQuery,
+} from "../../../utils/graphQueries";
+import { jwtDecode } from "../../../utils/jwtDecoder";
+import types from "../types";
+
+const createPlaylistPending = () => ({
+    type: types.CREATE_PLAYLIST_PENDING,
+    status: "PENDING",
+});
+const createPlaylistSuccess = (id) => ({
+    type: types.CREATE_PLAYLIST_SUCCESS,
+    payload: id,
+    status: "SUCCESS",
+});
+const createPlaylistFail = () => ({
+    type: types.CREATE_PLAYLIST_FAIL,
+    status: "FAIL",
+});
+const getPlaylistsCountPending = () => ({
+    type: types.GET_PLAYLISTS_COUNT_PENDING,
+    status: "PENDING",
+});
+const getPlaylistsCountSuccess = (value) => ({
+    type: types.GET_PLAYLISTS_COUNT_SUCCESS,
+    payload: value,
+    status: "SUCCESS",
+});
+const getPlaylistsCountFail = () => ({
+    type: types.GET_PLAYLISTS_COUNT_FAIL,
+    status: "FAIL",
+});
+const getPlaylistsPending = () => ({
+    type: types.GET_PLAYLISTS_PENDING,
+    status: "PENDING",
+});
+const getPlaylistsSuccess = (playlists) => ({
+    type: types.GET_PLAYLISTS_SUCCESS,
+    payload: playlists,
+    status: "SUCCESS",
+});
+const getPlaylistsFail = () => ({
+    type: types.GET_PLAYLISTS_FAIL,
+    status: "FAIL",
+});
+const getPlaylistByIdPending = () => ({
+    type: types.GET_PLAYLIST_BY_ID_PENDING,
+    status: "PENDING",
+});
+const getPlaylistByIdSuccess = (playlist) => ({
+    type: types.GET_PLAYLIST_BY_ID_SUCCESS,
+    payload: playlist,
+    status: "SUCCESS",
+});
+const getPlaylistByIdFail = () => ({
+    type: types.GET_PLAYLIST_BY_ID_FAIL,
+    status: "FAIL",
+});
+const upsertPlaylistInfoPending = () => ({
+    type: types.PLAYLIST_UPSERT_INFO_PENDING,
+    status: "PENDING",
+});
+const upsertPlaylistInfoSuccess = (playlist) => ({
+    type: types.PLAYLIST_UPSERT_INFO_SUCCESS,
+    status: "SUCCESS",
+    payload: playlist,
+});
+const upsertPlaylistInfoFail = () => ({
+    type: types.PLAYLIST_UPSERT_INFO_FAIL,
+    status: "FAIL",
+});
+const removeTrackFromPlaylistPending = () => ({
+    type: types.REMOVE_TRACK_FROM_PLAYLIST_PENDING,
+    status: "PENDING",
+});
+const removeTrackFromPlaylistSuccess = (tracks, playlistId) => ({
+    type: types.REMOVE_TRACK_FROM_PLAYLIST_SUCCESS,
+    status: "SUCCESS",
+    payload: { tracks, playlistId },
+});
+const removeTrackFromPlaylistFail = () => ({
+    type: types.REMOVE_TRACK_FROM_PLAYLIST_FAIL,
+    status: "FAIL",
+});
+const addTrackToPlaylistPending = () => ({
+    type: types.ADD_TRACK_TO_PLAYLIST_PENDING,
+    status: "PENDING",
+});
+const addTrackToPlaylistSuccess = (tracks) => ({
+    type: types.ADD_TRACK_TO_PLAYLIST_SUCCESS,
+    payload: tracks,
+    status: "SUCCESS",
+});
+const addTrackToPlaylistFail = () => ({
+    type: types.ADD_TRACK_TO_PLAYLIST_FAIL,
+    status: "FAIL",
+});
+
+export const actionCreatePlaylist = (navigate) => (dispatch) => {
+    dispatch(createPlaylistPending());
+
+    getGQL(createPlaylistQuery)
+        .then((playlist) => {
+            const { _id } = playlist;
+
+            dispatch(createPlaylistSuccess());
+            navigate(`${ROUTES.PLAYLISTS}/${_id}`);
+        })
+        .catch(() => dispatch(createPlaylistFail()));
+};
+
+export const actionGetPlaylistsCount = () => (dispatch) => {
+    dispatch(getPlaylistsCountPending());
+
+    const jwtData = jwtDecode(localStorage.getItem("authToken"));
+
+    getGQL(getPlaylistsCountQuery, {
+        query: JSON.stringify([
+            {
+                ___owner: jwtData.id,
+            },
+        ]),
+    })
+        .then((count) => dispatch(getPlaylistsCountSuccess(count)))
+        .catch(() => dispatch(getPlaylistsCountFail()));
+};
+
+export const actionPlaylistPageChange = (page) => ({
+    type: types.PLAYLIST_PAGE_CHANGE,
+    payload: page,
+});
+
+export const actionGetPlaylists = (page) => (dispatch) => {
+    dispatch(getPlaylistsPending());
+
+    const jwtData = jwtDecode(localStorage.getItem("authToken"));
+
+    getGQL(getPlaylistsQuery, {
+        query: JSON.stringify([
+            {
+                ___owner: jwtData.id,
+            },
+            {
+                limit: [LIMIT.PLAYLISTS_ON_PAGE],
+                skip: [(page - 1) * LIMIT.PLAYLISTS_ON_PAGE],
+            },
+        ]),
+    })
+        .then((playlists) => dispatch(getPlaylistsSuccess(playlists)))
+        .catch(() => dispatch(getPlaylistsFail()));
+};
+
+export const actionGetPlaylistById = (_id) => (dispatch) => {
+    dispatch(getPlaylistByIdPending());
+
+    getGQL(getPlaylistByIdQuery, {
+        query: JSON.stringify([{ _id }]),
+    })
+        .then((playlist) => dispatch(getPlaylistByIdSuccess(playlist)))
+        .catch(() => dispatch(getPlaylistByIdFail()));
+};
+
+export const actionUpsertPlaylistInfo = (data) => (dispatch) => {
+    dispatch(upsertPlaylistInfoPending());
+
+    getGQL(upsertPlaylistInfoQuery, { query: data })
+        .then((data) => dispatch(upsertPlaylistInfoSuccess(data)))
+        .catch(e =>console.log(e));
+};
+
+export const actionRemoveTrackFromPlaylist = (playlistID, trackID) => (
+    dispatch
+) => {
+    dispatch(removeTrackFromPlaylistPending());
+
+    getGQL(getIdsInPlaylistQuery, {
+        query: JSON.stringify([{ _id: playlistID }]),
+    })
+        .then((tracksWithId) => {
+            const ids = tracksWithId.tracks;
+            ids.splice(ids.findIndex((id) => id === trackID), 1);
+
+            getGQL(upsertPlaylistInfoQuery, {
+                query: {
+                    _id: playlistID,
+                    tracks: ids,
+                },
+            })
+                .then(({ tracks }) =>
+                    dispatch(removeTrackFromPlaylistSuccess(tracks, playlistID))
+                )
+                .catch(() => dispatch(removeTrackFromPlaylistFail()));
+        })
+        .catch(() => dispatch(removeTrackFromPlaylistFail()));
+};
+
+export const actionAddTrackToPlaylist = (playlistID, trackID) => (dispatch) => {
+    dispatch(addTrackToPlaylistPending());
+
+    getGQL(getIdsInPlaylistQuery, {
+        query: JSON.stringify([{ _id: playlistID }]),
+    })
+        .then((tracksWithId) => {
+            const ids = tracksWithId.tracks ?? [];
+            
+            const isExist =
+                ids.findIndex((id) => id._id === trackID) === -1 ? false : true;
+
+            if (!isExist) {
+                ids.push({ _id: trackID });
+
+                getGQL(upsertPlaylistInfoQuery, {
+                    query: {
+                        _id: playlistID,
+                        tracks: ids,
+                    },
+                })
+                    .then(({ tracks }) =>
+                        dispatch(addTrackToPlaylistSuccess(tracks))
+                    )
+                    .catch(() => dispatch(addTrackToPlaylistFail()));
+            }
+        })
+        .catch(() => dispatch(addTrackToPlaylistFail()));
+};

+ 45 - 0
src/redux/actions/creators/profile.js

@@ -0,0 +1,45 @@
+import { getGQL } from "../../../utils/getGQL";
+import { getGQL_Upload } from "../../../utils/getGQL_Upload";
+import { changePasswordQuery } from "../../../utils/graphQueries";
+import { jwtDecode } from "../../../utils/jwtDecoder";
+import types from "../types";
+
+const changePasswordPending = () => ({
+    type: types.CHANGE_PASSWORD_PENDING,
+    status: "PENDING",
+});
+const changePasswordSuccess = () => ({
+    type: types.CHANGE_PASSWORD_SUCCESS,
+    status: "SUCCESS",
+});
+const changePasswordFail = () => ({
+    type: types.CHANGE_PASSWORD_FAIL,
+    status: "FAIL",
+});
+
+export const actionGetProfileData = () => {
+    const jwtData = jwtDecode(localStorage.getItem("authToken"));
+    const { createdAt, permission, login } = jwtData;
+
+    const dateCreatedAt = new Date(createdAt * 1000).toLocaleDateString();
+
+    return {
+        type: types.PROFILE_GET_DATA,
+        payload: { createdAt: dateCreatedAt, permission, login },
+    };
+};
+
+export const actionChange = (pastPassword, newPassword) => (
+    dispatch
+) => {
+    dispatch(changePasswordPending());
+
+    const jwtData = jwtDecode(localStorage.getItem("authToken"));
+    getGQL(changePasswordQuery, {
+        login: jwtData.login,
+        password: pastPassword,
+        newPassword,
+    })
+        .then(() => dispatch(changePasswordSuccess()))
+        .catch(() => changePasswordFail());
+};

+ 166 - 0
src/redux/actions/creators/tracks.js

@@ -0,0 +1,166 @@
+import { LIMIT } from "../../../utils/constants";
+import { getGQL } from "../../../utils/getGQL";
+import {
+    deleteTrackById,
+    getTracksCountQuery,
+    getTracksQuery,
+} from "../../../utils/graphQueries";
+import { jwtDecode } from "../../../utils/jwtDecoder";
+import types from "../types";
+
+const actionGetTracksPending = () => ({
+    type: types.GET_TRACKS_PENDING,
+    status: "PENDING",
+});
+const actionGetTracksSuccess = (tracks) => ({
+    type: types.GET_TRACKS_SUCCESS,
+    payload: tracks,
+    status: "SUCCESS",
+});
+const actionGetTracksFail = () => ({
+    type: types.GET_TRACKS_FAIL,
+    status: "FAIL",
+});
+const actionGetTracksCountPending = () => ({
+    type: types.GET_TRACKS_COUNT_PENDING,
+    status: "PENDING",
+});
+const actionGetTracksCountSuccess = (value) => ({
+    type: types.GET_TRACKS_COUNT_SUCCESS,
+    payload: value,
+    status: "SUCCESS",
+});
+const actionGetTracksCountFail = () => ({
+    type: types.GET_TRACKS_COUNT_FAIL,
+    status: "FAIL",
+});
+const actionDeleteTracksPending = () => ({
+    type: types.DELETE_TRACKS_PENDING,
+    status: "PENDING",
+});
+const actionDeleteTracksSuccess = () => ({
+    type: types.DELETE_TRACKS_SUCCESS,
+    status: "SUCCESS",
+});
+const actionDeleteTracksFail = () => ({
+    type: types.DELETE_TRACKS_FAIL,
+    status: "FAIL",
+});
+
+const actionUploadTracksPending = () => ({
+    type: types.UPLOAD_TRACKS_PENDING,
+    status: "PENDING",
+});
+const actionUploadTracksSuccess = () => ({
+    type: types.UPLOAD_TRACKS_SUCCESS,
+    status: "SUCCESS",
+});
+const actionUploadTracksFail = () => ({
+    type: types.UPLOAD_TRACKS_FAIL,
+    status: "FAIL",
+});
+
+export const actionUploadOpen = () => ({
+    type: types.UPLOAD_OPEN,
+});
+
+export const actionUploadTracks = (files) => (dispatch) => {
+    if (files.length === 0) {
+        dispatch(actionUploadTracksFail());
+        return;
+    }
+
+    dispatch(actionUploadTracksPending());
+
+    const tracks = files.map((file) => {
+        const formData = new FormData();
+        formData.append("track", file);
+        return getGQL_Upload({ formData, fetchPart: "track" });
+    });
+
+    Promise.all(tracks)
+        .then(() => {
+            dispatch(actionUploadTracksSuccess());
+        })
+        .catch(() => dispatch(actionUploadTracksFail()));
+};
+
+const actionNextTracksPage = () => ({ type: types.NEXT_TRACKS_PAGE });
+
+export const actionGetTracks = (page, sortBy, findAll = false) => (
+    dispatch
+) => {
+    const jwtData = jwtDecode(localStorage.getItem("authToken"));
+
+    dispatch(actionGetTracksPending());
+
+    const paginationSettings = !findAll
+        ? {
+              limit: [LIMIT.TRACKS_ON_PAGE],
+              skip: [page * LIMIT.TRACKS_ON_PAGE],
+              sort: [{ originalFileName: sortBy }],
+          }
+        : {};
+
+    getGQL(getTracksQuery, {
+        query: JSON.stringify([
+            {
+                ___owner: jwtData.id,
+                url: { $exists: true, $ne: "" },
+                originalFileName: { $exists: true, $ne: "" },
+            },
+            paginationSettings,
+        ]),
+    })
+        .then((tracks) => {
+            dispatch(actionGetTracksSuccess(tracks));
+            dispatch(actionNextTracksPage());
+        })
+        .catch(() => dispatch(actionGetTracksFail()))
+        .finally(() => dispatch(actionTracksFetchingOff()));
+};
+
+export const actionTracksFetchingOn = () => ({
+    type: types.TRACKS_FETCHING_ON,
+});
+export const actionTracksFetchingOff = () => ({
+    type: types.TRACKS_FETCHING_OFF,
+});
+
+export const actionGetTracksCount = () => (dispatch) => {
+    const jwtData = jwtDecode(localStorage.getItem("authToken"));
+
+    dispatch(actionGetTracksCountPending());
+
+    getGQL(getTracksCountQuery, {
+        query: JSON.stringify([
+            {
+                ___owner: jwtData.id,
+            },
+        ]),
+    })
+        .then((value) => dispatch(actionGetTracksCountSuccess(value)))
+        .catch(() => dispatch(actionGetTracksCountFail()));
+};
+
+export const actionSetSortByTracks = (value) => ({
+    type: types.SET_SORT_BY_TRACKS,
+    payload: value,
+});
+
+export const actionRemoveTracks = () => ({ type: types.REMOVE_TRACKS });
+
+export const actionTrackToDelete = (id) => ({
+    type: types.TRACK_TO_DELETE,
+    payload: id,
+});
+
+export const actionDeleteTracks = (ids) => (dispatch) => {
+    dispatch(actionDeleteTracksPending());
+
+    const promises = ids.map((id) => getGQL(deleteTrackById, { id }));
+
+    Promise.all(promises)
+        .then(() => dispatch(actionDeleteTracksSuccess()))
+        .catch(() => dispatch(actionDeleteTracksFail()));
+};

+ 40 - 0
src/redux/actions/creators/upload.js

@@ -0,0 +1,40 @@
+import { getGQL_Upload } from "../../../utils/getGQL_Upload";
+import types from "../types";
+
+const actionUploadTracksPending = () => ({
+    type: types.UPLOAD_TRACKS_PENDING,
+    status: "PENDING",
+});
+const actionUploadTracksSuccess = () => ({
+    type: types.UPLOAD_TRACKS_SUCCESS,
+    status: "SUCCESS",
+});
+const actionUploadTracksFail = () => ({
+    type: types.UPLOAD_TRACKS_FAIL,
+    status: "FAIL",
+});
+
+export const actionUploadOpen = () => ({
+    type: types.UPLOAD_OPEN,
+});
+
+export const actionUploadTracks = (files) => (dispatch) => {
+    if (files.length === 0) {
+        dispatch(actionUploadTracksFail());
+        return;
+    }
+
+    dispatch(actionUploadTracksPending());
+
+    const tracks = files.map((file) => {
+        const formData = new FormData();
+        formData.append("track", file);
+        return getGQL_Upload({ formData, fetchPart: "track" });
+    });
+
+    Promise.all(tracks)
+        .then(() => {
+            dispatch(actionUploadTracksSuccess());
+        })
+        .catch(() => dispatch(actionUploadTracksFail()));
+};

+ 70 - 6
src/redux/actions/types.js

@@ -1,4 +1,3 @@
-
 const AUDIO = {
     PLAY: "PLAY",
     PAUSE: "PAUSE",
@@ -7,12 +6,15 @@ const AUDIO = {
     SET_CURRENT_TIME: "SET_CURRENT_TIME",
     SET_VOLUME: "SET_VOLUME",
     TOGGLE_REPEAT: "TOGGLE_REPEAT",
-    SHUFFLE: "SHUFFLE", // shuffle(all tracks, playlist, queue)
     SET_TRACK: "SET_TRACK",
-    SET_PLAYLIST: "SET_PLAYLIST",
     NEXT_TRACK: "NEXT_TRACK",
     PREV_TRACK: "PREV_TRACK",
-}
+    PLAYER_START_POSITION: "PLAYER_START_POSITION",
+    PLAYER_SET_QUEUE: "PLAYER_SET_QUEUE",
+    CLEAR_QUEUE: "CLEAR_QUEUE",
+    DELETE_TRACK_FROM_QUEUE: "DELETE_TRACK_FROM_QUEUE",
+    PLAYER_SET_CURRENT_INDEX: "PLAYER_SET_CURRENT_INDEX",
+};
 
 const AUTH = {
     LOGIN_PENDING: "LOGIN_PENDING",
@@ -22,9 +24,71 @@ const AUTH = {
     REGISTER_SUCCESS: "REGISTER_SUCCESS",
     REGISTER_FAIL: "REGISTER_FAIL",
     LOGOUT: "LOGOUT",
-}
+};
+
+const UPLOAD = {
+    UPLOAD_OPEN: "UPLOAD_OPEN",
+    UPLOAD_TRACKS_PENDING: "UPLOAD_TRACKS_PENDING",
+    UPLOAD_TRACKS_SUCCESS: "UPLOAD_TRACKS_SUCCESS",
+    UPLOAD_TRACKS_FAIL: "UPLOAD_TRACKS_FAIL",
+};
+
+const TRACKS = {
+    GET_TRACKS_PENDING: "GET_TRACKS_PENDING",
+    GET_TRACKS_SUCCESS: "GET_TRACKS_SUCCESS",
+    GET_TRACKS_FAIL: "GET_TRACKS_FAIL",
+    NEXT_TRACKS_PAGE: "NEXT_TRACKS_PAGE",
+    TRACKS_FETCHING_ON: "TRACKS_FETCHING_ON",
+    TRACKS_FETCHING_OFF: "TRACKS_FETCHING_OFF",
+    GET_TRACKS_COUNT_PENDING: "GET_TRACKS_COUNT_PENDING",
+    GET_TRACKS_COUNT_SUCCESS: "GET_TRACKS_COUNT_SUCCESS",
+    GET_TRACKS_COUNT_FAIL: "GET_TRACKS_COUNT_FAIL",
+    SET_SORT_BY_TRACKS: "SET_SORT_BY_TRACKS",
+    REMOVE_TRACKS: "REMOVE_TRACKS",
+    DELETE_TRACKS: "DELETE_TRACKS",
+    TRACK_TO_DELETE: "TRACK_TO_DELETE",
+    DELETE_TRACKS_PENDING: "DELETE_TRACKS_PENDING",
+    DELETE_TRACKS_SUCCESS: "DELETE_TRACKS_SUCCESS",
+    DELETE_TRACKS_FAIL: "DELETE_TRACKS_FAIL",
+};
+
+const PLAYLISTS = {
+    CREATE_PLAYLIST_PENDING: "CREATE_PLAYLIST_PENDING",
+    CREATE_PLAYLIST_SUCCESS: "CREATE_PLAYLIST_SUCCESS",
+    CREATE_PLAYLIST_FAIL: "CREATE_PLAYLIST_FAIL",
+    GET_PLAYLISTS_COUNT_PENDING: "GET_PLAYLISTS_COUNT_PENDING",
+    GET_PLAYLISTS_COUNT_SUCCESS: "GET_PLAYLISTS_COUNT_SUCCESS",
+    GET_PLAYLISTS_COUNT_FAIL: "GET_PLAYLISTS_COUNT_FAIL",
+    PLAYLIST_PAGE_CHANGE: "PLAYLIST_PAGE_CHANGE",
+    GET_PLAYLISTS_PENDING: "GET_PLAYLISTS_PENDING",
+    GET_PLAYLISTS_SUCCESS: "GET_PLAYLISTS_SUCCESS",
+    GET_PLAYLISTS_FAIL: "GET_PLAYLISTS_FAIL",
+    GET_PLAYLIST_BY_ID_PENDING: "GET_PLAYLIST_BY_ID_PENDING",
+    GET_PLAYLIST_BY_ID_SUCCESS: "GET_PLAYLIST_BY_ID_SUCCESS",
+    GET_PLAYLIST_BY_ID_FAIL: "GET_PLAYLIST_BY_ID_FAIL",
+    PLAYLIST_UPSERT_INFO_PENDING: "PLAYLIST_UPSERT_INFO_PENDING",
+    PLAYLIST_UPSERT_INFO_SUCCESS: "PLAYLIST_UPSERT_INFO_SUCCESS",
+    PLAYLIST_UPSERT_INFO_FAIL: "PLAYLIST_UPSERT_INFO_FAIL",
+    REMOVE_TRACK_FROM_PLAYLIST_PENDING: "REMOVE_TRACK_FROM_PLAYLIST_PENDING",
+    REMOVE_TRACK_FROM_PLAYLIST_SUCCESS: "REMOVE_TRACK_FROM_PLAYLIST_SUCCESS",
+    REMOVE_TRACK_FROM_PLAYLIST_FAIL: "REMOVE_TRACK_FROM_PLAYLIST_FAIL",
+    ADD_TRACK_TO_PLAYLIST_PENDING: "ADD_TRACK_TO_PLAYLIST_PENDING",
+    ADD_TRACK_TO_PLAYLIST_SUCCESS: "ADD_TRACK_TO_PLAYLIST_SUCCESS",
+    ADD_TRACK_TO_PLAYLIST_FAIL: "ADD_TRACK_TO_PLAYLIST_FAIL",
+};
+
+const PROFILE = {
+    PROFILE_GET_DATA: "PROFILE_GET_DATA",
+    CHANGE_PASSWORD_PENDING: "CHANGE_PASSWORD_PENDING",
+    CHANGE_PASSWORD_SUCCESS: "CHANGE_PASSWORD_SUCCESS",
+    CHANGE_PASSWORD_FAIL: "CHANGE_PASSWORD_FAIL",
+};
 
 export default {
     ...AUDIO,
     ...AUTH,
-}
+    ...UPLOAD,
+    ...TRACKS,
+    ...PLAYLISTS,
+    ...PROFILE,
+};

+ 30 - 2
src/redux/reducers/audioReducer.js

@@ -4,11 +4,12 @@ const initialState = {
     isPlaying: false,
     duration: 0,
     currentTime: 0,
+    currentQueueIndex: 0,
     volume: 1,
     isRepeated: false,
     isShuffled: false,
-    track: "",
-    playlist: {},
+    track: null,
+    queue: null, // {_id, name, tracks: []}
 };
 
 const playerReducer = (state = initialState, action) => {
@@ -34,6 +35,33 @@ const playerReducer = (state = initialState, action) => {
         case types.TOGGLE_REPEAT:
             return { ...state, isRepeated: action.payload };
 
+        case types.NEXT_TRACK:
+            return { ...state, currentQueueIndex: state.currentQueueIndex + 1 };
+
+        case types.PREV_TRACK:
+            return { ...state, currentQueueIndex: state.currentQueueIndex - 1 };
+
+        case types.PLAYER_START_POSITION:
+            return { ...state, isPlaying: false, currentTime: 0, };
+
+        case types.SET_TRACK:
+            return { ...state, track: action.payload, isPlaying: true };
+
+        case types.PLAYER_SET_QUEUE:
+            return { ...state, queue: action.payload, currentQueueIndex: 0 };
+
+        case types.PLAYER_SET_CURRENT_INDEX:
+            return { ...state, currentQueueIndex: action.payload };
+
+        case types.DELETE_TRACK_FROM_QUEUE:
+            const newTracks = state.queue.tracks.filter(
+                (track) => track._id !== action.payload
+            );
+            return { ...state, queue: { ...state.queue, tracks: newTracks } };
+
+        case types.CLEAR_QUEUE:
+            return { ...state, queue: {}, currentQueueIndex: 0 };
+
         default:
             return state;
     }

+ 11 - 3
src/redux/reducers/index.js

@@ -1,11 +1,19 @@
-import {combineReducers} from "redux";
+import { combineReducers } from "redux";
 
 import audioReducer from "./audioReducer";
 import authReducer from "./authReducer";
+import tracksReducer from "./tracksReducer";
+import uploadReducer from "./uploadReducer";
+import playlistsReducer from "./playlistsReducer";
+import profileReducer from "./profileReducer";
 
 const rootReducers = combineReducers({
     audio: audioReducer,
     auth: authReducer,
-})
+    upload: uploadReducer,
+    tracks: tracksReducer,
+    playlists: playlistsReducer,
+    profile: profileReducer,
+});
 
-export default rootReducers;
+export default rootReducers;

+ 120 - 0
src/redux/reducers/playlistsReducer.js

@@ -0,0 +1,120 @@
+import types from "../actions/types";
+
+const initialState = {
+    playlists: [], // _id, name, description, tracks
+    count: 1,
+    status: null,
+    currentPage: 1,
+    currentPlaylist: null,
+};
+
+const playlistsReducer = (state = initialState, action) => {
+    switch (action.type) {
+        case types.CREATE_PLAYLIST_PENDING:
+            return { ...state, status: action.status };
+
+        case types.CREATE_PLAYLIST_SUCCESS:
+            return { ...state, status: action.status };
+
+        case types.CREATE_PLAYLIST_FAIL:
+            return { ...state, status: action.status };
+
+        case types.GET_PLAYLISTS_COUNT_PENDING:
+            return { ...state, status: action.status };
+
+        case types.GET_PLAYLISTS_COUNT_SUCCESS:
+            return { ...state, count: action.payload, status: action.status };
+
+        case types.GET_PLAYLISTS_COUNT_FAIL:
+            return { ...state, status: action.status };
+
+        case types.PLAYLIST_PAGE_CHANGE:
+            return { ...state, currentPage: action.payload };
+
+        case types.GET_PLAYLISTS_PENDING:
+            return { ...state, status: action.status };
+
+        case types.GET_PLAYLISTS_SUCCESS:
+            return {
+                ...state,
+                playlists: action.payload,
+                status: action.status,
+            };
+
+        case types.GET_PLAYLISTS_FAIL:
+            return { ...state, status: action.status };
+
+        case types.GET_PLAYLIST_BY_ID_PENDING:
+            return { ...state, status: action.status };
+
+        case types.GET_PLAYLIST_BY_ID_SUCCESS:
+            return {
+                ...state,
+                currentPlaylist: action.payload,
+                status: action.status,
+            };
+
+        case types.GET_PLAYLIST_BY_ID_FAIL:
+            return { ...state, status: action.status };
+
+        case types.PLAYLIST_UPSERT_INFO_PENDING:
+            return { ...state, status: action.status };
+
+        case types.PLAYLIST_UPSERT_INFO_SUCCESS:
+            return {
+                ...state,
+                currentPlaylist: {
+                    ...state.currentPlaylist,
+                    ...action.payload,
+                },
+                status: action.status,
+            };
+
+        case types.PLAYLIST_UPSERT_INFO_FAIL:
+            return { ...state, status: action.status };
+
+        case types.REMOVE_TRACK_FROM_PLAYLIST_PENDING:
+            return { ...state, status: action.status };
+
+        case types.REMOVE_TRACK_FROM_PLAYLIST_SUCCESS:
+            const { tracks, playlistId } = action.payload;
+
+            const playlistIndex = state.playlists.findIndex(
+                (playlist) => playlist._id === playlistId
+            );
+            const playlists = state.playlists;
+            playlists[playlistIndex].tracks = tracks;
+
+            return {
+                ...state,
+                playlists,
+                currentPlaylist: {
+                    ...state.currentPlaylist,
+                    tracks,
+                },
+            };
+
+        case types.REMOVE_TRACK_FROM_PLAYLIST_FAIL:
+            return { ...state, status: action.status };
+
+        case types.ADD_TRACK_TO_PLAYLIST_PENDING:
+            return { ...state, status: action.status };
+
+        case types.ADD_TRACK_TO_PLAYLIST_SUCCESS:;
+            return {
+                ...state,
+                currentPlaylist: {
+                    ...state.currentPlaylist,
+                    tracks: action.payload,
+                },
+            };
+
+        case types.ADD_TRACK_TO_PLAYLIST_FAIL:
+            return { ...state, status: action.status };
+
+        default:
+            return state;
+    }
+};
+
+export default playlistsReducer;

+ 20 - 0
src/redux/reducers/profileReducer.js

@@ -0,0 +1,20 @@
+import types from "../actions/types";
+
+const initialState = {
+    status: null,
+    createdAt: null,
+    permission: null,
+    login: null,
+};
+
+const profileReducer = (state = initialState, action) => {
+    switch (action.type) {
+        case types.PROFILE_GET_DATA:
+            return {...state, ...action.payload};
+
+        default:
+            return state;
+    }
+};
+
+export default profileReducer;

+ 87 - 0
src/redux/reducers/tracksReducer.js

@@ -0,0 +1,87 @@
+import types from "../actions/types";
+
+const initialState = {
+    tracks: [],
+    count: 0,
+    page: 0,
+    isFetching: true,
+    status: null,
+    sortBy: 1,
+    toDelete: [],
+};
+
+const tracksReducer = (state = initialState, action) => {
+    switch (action.type) {
+        case types.GET_TRACKS_PENDING:
+            return { ...state, status: action.status };
+
+        case types.GET_TRACKS_SUCCESS:
+            return {
+                ...state,
+                tracks: [...state.tracks, ...action.payload],
+                status: action.status,
+            };
+
+        case types.GET_TRACKS_FAIL:
+            return { ...state, status: action.status };
+
+        case types.NEXT_TRACKS_PAGE:
+            return { ...state, page: state.page + 1 };
+
+        case types.TRACKS_FETCHING_ON:
+            return { ...state, isFetching: true };
+
+        case types.TRACKS_FETCHING_OFF:
+            return { ...state, isFetching: false };
+
+        case types.GET_TRACKS_COUNT_PENDING:
+            return { ...state, status: action.status };
+
+        case types.GET_TRACKS_COUNT_SUCCESS:
+            return { ...state, count: action.payload, status: action.status };
+
+        case types.GET_TRACKS_COUNT_FAIL:
+            return { ...state, status: action.payload };
+
+        case types.SET_SORT_BY_TRACKS:
+            return { ...state, sortBy: action.payload };
+
+        case types.REMOVE_TRACKS:
+            return {
+                ...state,
+                tracks: [],
+                page: 0,
+                isFetching: true,
+                status: null,
+            };
+
+        case types.TRACK_TO_DELETE:
+            const searchedIdx = state.toDelete.findIndex(
+                (id) => id === action.payload
+            );
+            const newArr = [...state.toDelete];
+            searchedIdx !== -1
+                ? newArr.splice(searchedIdx, 1)
+                : newArr.push(action.payload);
+
+            return { ...state, toDelete: newArr };
+
+        case types.DELETE_TRACKS_PENDING:
+            return { ...state, status: "PENDING" };
+
+        case types.DELETE_TRACKS_SUCCESS:
+            const toDeleteSet = new Set(state.toDelete);
+            const newTracks = state.tracks.filter(
+                (track) => !toDeleteSet.has(track._id)
+            );
+            return { ...state, tracks: newTracks, status: "SUCCESS", toDelete: [] };
+
+        case types.DELETE_TRACKS_FAIL:
+            return { ...state, status: "FAIL" };
+
+        default:
+            return state;
+    }
+};
+
+export default tracksReducer;

+ 26 - 0
src/redux/reducers/uploadReducer.js

@@ -0,0 +1,26 @@
+import types from "../actions/types";
+
+const initialState = {
+    status: null,
+};
+
+const uploadReducer = (state = initialState, action) => {
+    switch (action.type) {
+        case types.UPLOAD_OPEN:
+            return {...state, status: null}
+
+        case types.UPLOAD_TRACKS_PENDING:
+            return { ...state, status: action.status };
+
+        case types.UPLOAD_TRACKS_SUCCESS:
+            return { ...state, status: action.status };
+
+        case types.UPLOAD_TRACKS_FAIL:
+            return { ...state, status: action.status };
+
+        default:
+            return state;
+    }
+};
+
+export default uploadReducer;

+ 16 - 6
src/utils/constants.js

@@ -1,10 +1,20 @@
 export const backendURL = "http://player.node.ed.asmer.org.ua";
 
 export const ROUTES = {
-    "HOME": "/",
-    "PROFILE": "/profile",
-    "QUEUE": "/queue",
-    "TRACKS": "/tracks",
-    "PLAYLISTS": "/playlists",
-}
+    HOME: "/",
+    PROFILE: "/profile",
+    QUEUE: "/queue",
+    TRACKS: "/tracks",
+    PLAYLISTS: "/playlists",
+    UPLOAD: "/upload",
+};
 
+export const LIMIT = {
+    TRACKS_ON_PAGE: 20,
+    PLAYLISTS_ON_PAGE: 5,
+};
+
+export const DEFAULT_NAMING = {
+    PLAYLIST_NAME: "New playlist",
+    PLAYLIST_DESCRIPTION: "Description...",
+};

+ 10 - 0
src/utils/getGQL_Upload.js

@@ -0,0 +1,10 @@
+import { backendURL } from "./constants";
+
+export const getGQL_Upload = ({formData, fetchPart}) => fetch(`${backendURL}/${fetchPart}`, {
+    method: 'POST',
+    headers: {
+      ...(localStorage.authToken ? {'Authorization': `Bearer ${localStorage.authToken}`}
+        : {}),
+    },
+    body: formData,
+  }).then((response) => response.json());

+ 95 - 0
src/utils/graphQueries.js

@@ -1,3 +1,5 @@
+import { DEFAULT_NAMING } from "../utils/constants";
+
 export const loginQuery = `query log($login: String!, $password: String!) {
     login(login: $login, password: $password)
   }`;
@@ -7,3 +9,96 @@ export const registerQuery = `mutation register($login: String!, $password: Stri
         _id login 
     }
   }`;
+
+export const getTracksQuery = `query getTracks($query: String){
+    TrackFind(query: $query) {
+      _id url originalFileName
+      id3 {
+        title
+        artist
+        album
+        year
+        genre
+        trackNumber
+      }
+    }
+  }`;
+
+export const getTracksCountQuery = `query tracksCount($query: String){
+    TrackCount(query: $query)
+  }`;
+
+export const deleteTrackById = `mutation deleteTrack($id: ID!) {
+    TrackDelete(track: {_id: $id}) {
+      _id
+    }
+  }`;
+
+export const getPlaylistsQuery = `query getPlaylists($query: String) {
+    PlaylistFind(query: $query) {
+      _id
+      name
+      description
+      tracks {
+        _id
+        originalFileName
+        url
+        id3 {
+          title
+        }
+      }
+    }
+  }`;
+
+const { PLAYLIST_NAME, PLAYLIST_DESCRIPTION } = DEFAULT_NAMING;
+export const createPlaylistQuery = `mutation {
+   PlaylistUpsert(playlist: {name: "${PLAYLIST_NAME}", description: "${PLAYLIST_DESCRIPTION}"}) {
+      _id
+    }
+  }`;
+
+export const getPlaylistsCountQuery = `query playlistsCount($query: String){
+    PlaylistCount(query: $query)
+  }`;
+
+export const getPlaylistByIdQuery = `query playlistFindById($query: String) {
+    PlaylistFindOne(query: $query) {
+      name
+      description
+      tracks {
+        _id
+        originalFileName
+        id3 {
+          title
+        }
+      }
+    }
+}`;
+
+export const upsertPlaylistInfoQuery = `mutation upsertPlaylistInfo($query: PlaylistInput) {
+    PlaylistUpsert(playlist: $query) {
+      name
+      description
+      tracks {
+        _id
+        originalFileName
+        id3 {
+          title
+        }
+      }
+    }
+}`;
+
+export const getIdsInPlaylistQuery = `query getTracksId($query: String) {
+    PlaylistFindOne(query: $query) {
+      tracks {
+        _id
+      }
+    }
+}`;
+
+export const changePasswordQuery = `mutation changePassword($login: String!, $password: String!, $newPassword: String!) {
+  changePassword(login: $login, password: $password, newPassword: $newPassword) {
+    _id
+  }
+}`;

+ 5 - 0
src/utils/index.js

@@ -0,0 +1,5 @@
+export const secondsToHMS = (time) => {
+    return new Date(time * 1000)
+        .toISOString()
+        .substr(12, 7)
+};

+ 15 - 0
src/utils/jwtDecoder.js

@@ -0,0 +1,15 @@
+export const jwtDecode = (token) => {
+    try {
+      const [, decodedRaw] = token.split('.');
+      const data = JSON.parse(atob(decodedRaw));
+      return {
+        createdAt: data.iat,
+        id: data.sub.id,
+        login: data.sub.login,
+        permission: data.sub.acl[1]
+      }
+    } catch (e) {
+      throw new Error(e.message);
+    }
+  };
+  

+ 4 - 0
src/utils/regex.js

@@ -0,0 +1,4 @@
+const regexAudioExtensions = /\.(?:mp3|m4a|flac|mp4|wav|wma|aac)$/;
+
+export const removeAudioExtension = (name) =>
+    name.replace(regexAudioExtensions, "");