Browse Source

Profile page reworked, start of development of all tracks page

Антон Задорожный 2 years ago
commit
c87ad6a15d

+ 23 - 0
.gitignore

@@ -0,0 +1,23 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# production
+/build
+
+# misc
+.DS_Store
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*

+ 70 - 0
README.md

@@ -0,0 +1,70 @@
+# Getting Started with Create React App
+
+This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
+
+## Available Scripts
+
+In the project directory, you can run:
+
+### `npm start`
+
+Runs the app in the development mode.\
+Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
+
+The page will reload when you make changes.\
+You may also see any lint errors in the console.
+
+### `npm test`
+
+Launches the test runner in the interactive watch mode.\
+See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
+
+### `npm run build`
+
+Builds the app for production to the `build` folder.\
+It correctly bundles React in production mode and optimizes the build for the best performance.
+
+The build is minified and the filenames include the hashes.\
+Your app is ready to be deployed!
+
+See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
+
+### `npm run eject`
+
+**Note: this is a one-way operation. Once you `eject`, you can't go back!**
+
+If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
+
+Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
+
+You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
+
+## Learn More
+
+You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
+
+To learn React, check out the [React documentation](https://reactjs.org/).
+
+### Code Splitting
+
+This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
+
+### Analyzing the Bundle Size
+
+This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
+
+### Making a Progressive Web App
+
+This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
+
+### Advanced Configuration
+
+This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
+
+### Deployment
+
+This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
+
+### `npm run build` fails to minify
+
+This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)

File diff suppressed because it is too large
+ 12247 - 0
package-lock.json


+ 64 - 0
package.json

@@ -0,0 +1,64 @@
+{
+  "name": "player_project",
+  "version": "0.1.0",
+  "private": true,
+  "dependencies": {
+    "@fortawesome/fontawesome-svg-core": "^1.2.36",
+    "@fortawesome/free-brands-svg-icons": "^5.15.4",
+    "@fortawesome/free-solid-svg-icons": "^5.15.4",
+    "@fortawesome/react-fontawesome": "^0.1.16",
+    "@testing-library/jest-dom": "^5.16.1",
+    "@testing-library/react": "^12.1.2",
+    "@testing-library/user-event": "^13.5.0",
+    "bootstrap": "^5.1.3",
+    "cdbreact": "^1.2.1",
+    "react": "^17.0.2",
+    "react-bootstrap": "^2.1.0",
+    "react-dom": "^17.0.2",
+    "react-dropzone": "^11.5.1",
+    "react-redux": "^7.2.6",
+    "react-router": "^5.2.0",
+    "react-router-dom": "^5.2.0",
+    "react-scripts": "5.0.0",
+    "redux": "^4.1.2",
+    "redux-thunk": "^2.4.1",
+    "sass": "^1.45.2",
+    "web-vitals": "^2.1.2"
+  },
+  "scripts": {
+    "start": "react-scripts start",
+    "build": "react-scripts build",
+    "test": "react-scripts test",
+    "eject": "react-scripts eject"
+  },
+  "eslintConfig": {
+    "extends": [
+      "react-app",
+      "react-app/jest"
+    ]
+  },
+  "browserslist": {
+    "production": [
+      ">0.2%",
+      "not dead",
+      "not op_mini all"
+    ],
+    "development": [
+      "last 1 chrome version",
+      "last 1 firefox version",
+      "last 1 safari version"
+    ]
+  },
+  "description": "This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).",
+  "main": "index.js",
+  "devDependencies": {
+    "@babel/core": "^7.16.7",
+    "@babel/preset-env": "^7.16.7",
+    "babel-loader": "^8.2.3",
+    "html-webpack-plugin": "^5.5.0",
+    "webpack": "^5.65.0"
+  },
+  "keywords": [],
+  "author": "",
+  "license": "ISC"
+}

BIN
public/favicon.ico


+ 43 - 0
public/index.html

@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8" />
+    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
+    <meta name="viewport" content="width=device-width, initial-scale=1" />
+    <meta name="theme-color" content="#000000" />
+    <meta
+      name="description"
+      content="Web site created using create-react-app"
+    />
+    <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
+    <!--
+      manifest.json provides metadata used when your web app is installed on a
+      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
+    -->
+    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
+    <!--
+      Notice the use of %PUBLIC_URL% in the tags above.
+      It will be replaced with the URL of the `public` folder during the build.
+      Only files inside the `public` folder can be referenced from the HTML.
+
+      Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
+      work correctly both with client-side routing and a non-root public URL.
+      Learn how to configure a non-root public URL by running `npm run build`.
+    -->
+    <title>React App</title>
+  </head>
+  <body>
+    <noscript>You need to enable JavaScript to run this app.</noscript>
+    <div id="root"></div>
+    <!--
+      This HTML file is a template.
+      If you open it directly in the browser, you will see an empty page.
+
+      You can add webfonts, meta tags, or analytics to this file.
+      The build step will place the bundled scripts into the <body> tag.
+
+      To begin the development, run `npm start` or `yarn start`.
+      To create a production bundle, use `npm run build` or `yarn build`.
+    -->
+  </body>
+</html>

BIN
public/logo192.png


BIN
public/logo512.png


+ 25 - 0
public/manifest.json

@@ -0,0 +1,25 @@
+{
+  "short_name": "React App",
+  "name": "Create React App Sample",
+  "icons": [
+    {
+      "src": "favicon.ico",
+      "sizes": "64x64 32x32 24x24 16x16",
+      "type": "image/x-icon"
+    },
+    {
+      "src": "logo192.png",
+      "type": "image/png",
+      "sizes": "192x192"
+    },
+    {
+      "src": "logo512.png",
+      "type": "image/png",
+      "sizes": "512x512"
+    }
+  ],
+  "start_url": ".",
+  "display": "standalone",
+  "theme_color": "#000000",
+  "background_color": "#ffffff"
+}

+ 3 - 0
public/robots.txt

@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:

+ 42 - 0
src/App.js

@@ -0,0 +1,42 @@
+import { Main } from "./components/Main";
+import { Header } from "./components/Header";
+import { Router } from "react-router-dom";
+import { Provider } from "react-redux";
+import { createStore, combineReducers, applyMiddleware } from "redux";
+import thunk from "redux-thunk";
+import { createBrowserHistory } from "history";
+import { promiseReducer, authReducer } from "./reducers/index";
+import { Sidebar } from "./components/Sidebar";
+import "./App.scss";
+import { actionAboutMe } from "./actions";
+export const history = createBrowserHistory();
+
+export const store = createStore(
+  combineReducers({
+    promise: promiseReducer,
+    auth: authReducer,
+  }),
+  applyMiddleware(thunk)
+);
+
+if (localStorage.authToken) {
+  store.dispatch(actionAboutMe());
+}
+
+store.subscribe(() => console.log(store.getState()));
+
+function App() {
+  return (
+    <Router history={history}>
+      <Provider store={store}>
+        <div className="App">
+          <Header />
+          <Sidebar />
+          <Main />
+        </div>
+      </Provider>
+    </Router>
+  );
+}
+
+export default App;

+ 224 - 0
src/App.scss

@@ -0,0 +1,224 @@
+@import "~bootstrap/scss/bootstrap";
+
+body {
+  background-color: #34393d;
+  color: white;
+  font-family: "Catamaran", sans-serif;
+}
+
+.spoilerText {
+  float: left;
+}
+
+.Spoiler {
+  display: flex;
+  flex-wrap: wrap;
+
+  .header {
+    padding-left: 1.5em;
+    transition: opacity 0.15s linear;
+    -khtml-user-select: none;
+    user-select: none;
+    :hover {
+      text-decoration: underline;
+      cursor: pointer;
+    }
+  }
+  .content {
+    flex: 0 0 50%;
+  }
+}
+
+.spoilerArrow {
+  float: right;
+}
+
+.customBorder {
+  border-width: 1px;
+  border-style: dashed !important;
+}
+
+.customBrand {
+  color: white;
+  font-size: 16px;
+}
+
+.avatarProfile {
+  width: 30vh;
+  height: 30vh;
+}
+
+.avatarHeader {
+  vertical-align: middle;
+  margin-right: 2vh;
+  width: 40px;
+  height: 40px;
+}
+
+.MainContent {
+  margin-left: 7%;
+}
+
+.btn-circle {
+  width: 38px;
+  height: 38px;
+  border-radius: 19px;
+  text-align: center;
+  padding-left: 0;
+  padding-right: 0;
+  font-size: 16px;
+  margin: 0 1vh;
+}
+
+.AuthForm {
+  text-align: center;
+  width: 50%;
+}
+
+.error-container {
+  text-align: center;
+  font-size: 106px;
+  font-weight: 800;
+  margin: 70px 15px;
+}
+.error-container > span {
+  display: inline-block;
+  position: relative;
+}
+.error-container > span.four {
+  width: 136px;
+  height: 43px;
+  border-radius: 999px;
+  background: linear-gradient(
+      140deg,
+      rgba(0, 0, 0, 0.1) 0%,
+      rgba(0, 0, 0, 0.07) 43%,
+      transparent 44%,
+      transparent 100%
+    ),
+    linear-gradient(
+      105deg,
+      transparent 0%,
+      transparent 40%,
+      rgba(0, 0, 0, 0.06) 41%,
+      rgba(0, 0, 0, 0.07) 76%,
+      transparent 77%,
+      transparent 100%
+    ),
+    linear-gradient(to right, #a09cd8, #917be2);
+}
+.error-container > span.four:before,
+.error-container > span.four:after {
+  content: "";
+  display: block;
+  position: absolute;
+  border-radius: 999px;
+}
+.error-container > span.four:before {
+  width: 43px;
+  height: 156px;
+  left: 60px;
+  bottom: -43px;
+  background: linear-gradient(
+      128deg,
+      rgba(0, 0, 0, 0.1) 0%,
+      rgba(0, 0, 0, 0.07) 40%,
+      transparent 41%,
+      transparent 100%
+    ),
+    linear-gradient(
+      116deg,
+      rgba(0, 0, 0, 0.1) 0%,
+      rgba(0, 0, 0, 0.07) 50%,
+      transparent 51%,
+      transparent 100%
+    ),
+    linear-gradient(to top, #78749d, #77749d, #b895ab, #cc9aa6, #d7969e);
+}
+.error-container > span.four:after {
+  width: 137px;
+  height: 43px;
+  transform: rotate(-49.5deg);
+  left: -18px;
+  bottom: 36px;
+  background: linear-gradient(
+    to right,
+    #78749d,
+    #77749d,
+    #b895ab,
+    #cc9aa6,
+    #9697d7
+  );
+}
+
+.error-container > span.zero {
+  vertical-align: text-top;
+  width: 156px;
+  height: 156px;
+  border-radius: 999px;
+  background: linear-gradient(
+      -45deg,
+      transparent 0%,
+      rgba(0, 0, 0, 0.06) 50%,
+      transparent 51%,
+      transparent 100%
+    ),
+    linear-gradient(
+      to top right,
+      #78749d,
+      #77749d,
+      #b895ab,
+      #9b9acc,
+      #9a96d7,
+      #86bbed,
+      #9586ed
+    );
+  overflow: hidden;
+  animation: bgshadow 5s infinite;
+}
+.error-container > span.zero:before {
+  content: "";
+  display: block;
+  position: absolute;
+  transform: rotate(45deg);
+  width: 90px;
+  height: 90px;
+  background-color: transparent;
+  left: 0px;
+  bottom: 0px;
+  background: linear-gradient(
+      95deg,
+      transparent 0%,
+      transparent 8%,
+      rgba(0, 0, 0, 0.07) 9%,
+      transparent 50%,
+      transparent 100%
+    ),
+    linear-gradient(
+      85deg,
+      transparent 0%,
+      transparent 19%,
+      rgba(0, 0, 0, 0.05) 20%,
+      rgba(0, 0, 0, 0.07) 91%,
+      transparent 92%,
+      transparent 100%
+    );
+}
+.error-container > span.zero:after {
+  content: "";
+  display: block;
+  position: absolute;
+  border-radius: 999px;
+  width: 70px;
+  height: 70px;
+  left: 43px;
+  bottom: 43px;
+  background: #34393d;
+  box-shadow: -2px 2px 2px 0px rgba(0, 0, 0, 0.1);
+}
+
+.screen-reader-text {
+  position: absolute;
+  top: -9999em;
+  left: -9999em;
+}

+ 194 - 0
src/actions/index.js

@@ -0,0 +1,194 @@
+import { backURL, gql } from "../helpers";
+
+export const actionPending = (name) => ({
+  type: "PROMISE",
+  status: "PENDING",
+  name,
+});
+
+export const actionResolved = (name, payload) => ({
+  type: "PROMISE",
+  status: "RESOLVED",
+  name,
+  payload,
+});
+
+export const actionRejected = (name, error) => ({
+  type: "PROMISE",
+  status: "REJECTED",
+  name,
+  error,
+});
+
+export const actionAuthLogin = (token) => ({ type: "AUTH_LOGIN", token });
+
+export const actionAuthLogout = () => ({ type: "AUTH_LOGOUT" });
+
+export const actionPromise = (name, promise) => async (dispatch) => {
+  dispatch(actionPending(name));
+  try {
+    let data = await promise;
+    dispatch(actionResolved(name, data));
+    return data;
+  } catch (error) {
+    dispatch(actionRejected(name, error));
+  }
+};
+
+export const actionUserUpdate = ({ _id, login, password, nick, avatar }) =>
+  actionPromise(
+    "userUpdate",
+    gql(
+      `mutation userUpdate($user:UserInput){
+        UserUpsert(user: $user){
+          _id login nick avatar {
+            _id, url
+          }
+        }
+      }
+  `,
+      {
+        user: {
+          _id,
+          login,
+          password,
+          nick,
+          avatar,
+        },
+      }
+    )
+  );
+
+export const actionChangePassword = (login, password, newPassword) =>
+  actionPromise(
+    "changePassword",
+    gql(
+      `query changePass($login:String!, $password:String!, $newPassword:String!){
+        changePassword(login:$login, password: $password, newPassword: $newPassword)
+      }`,
+      { login, password, newPassword }
+    )
+  );
+
+const actionLogin = (login, password) =>
+  actionPromise(
+    "login",
+    gql(
+      `query log($login:String!, $password:String!){
+        login(login:$login, password: $password)
+      }`,
+      { login, password }
+    )
+  );
+
+const actionRegister = (login, password) =>
+  actionPromise(
+    "registration",
+    gql(
+      `mutation reg($login:String!, $password:String!) {
+        createUser(login:$login, password: $password) {
+        _id login
+    }
+  }
+  `,
+      { login, password }
+    )
+  );
+
+export const actionFindUser = (_id) =>
+  actionPromise(
+    "user",
+    gql(
+      `query findUser($q:String){
+        UserFindOne(query:$q){
+          _id login nick createdAt avatar {
+            _id url
+          }
+        }
+      }
+  `,
+      { q: JSON.stringify([{ _id }]) }
+    )
+  );
+
+export const actionAboutMe = () => async (dispatch, getState) => {
+  let { id } = getState().auth.payload.sub;
+  await dispatch(actionFindUser(id));
+};
+
+export const actionFullLogin = (l, p) => async (dispatch) => {
+  let token = await dispatch(actionLogin(l, p));
+  if (token) {
+    await dispatch(actionAuthLogin(token));
+    await dispatch(actionAboutMe());
+  }
+};
+
+export const actionFullRegister = (l, p) => async (dispatch) => {
+  let { _id } = await dispatch(actionRegister(l, p));
+  if (_id) {
+    let nick = l;
+    if (nick.includes("@")) {
+      nick = nick.substring(0, nick.indexOf("@"));
+      if (nick.length > 8) {
+        nick = nick.substring(0, 8);
+      }
+    }
+    await dispatch(actionUserUpdate({ _id, nick }));
+    await dispatch(actionFullLogin(l, p));
+  }
+};
+
+export const actionFindTracks = () =>
+  actionPromise(
+    "tracks",
+    gql(
+      `query findTracks($q:String){
+        TrackFind(query:$q){
+          _id url owner {
+            _id login nick
+          }
+        }
+      }
+  `,
+      { q: "[{}]" }
+    )
+  );
+
+export const actionFindUsers = () =>
+  actionPromise(
+    "users",
+    gql(
+      `query findUsers($q:String){
+        UserFind(query: $q){
+          _id login nick avatar {
+            _id url
+          }
+        }
+      }
+  `,
+      { q: "[{}]" }
+    )
+  );
+
+const actionUploadPhoto = (file) => {
+  let fd = new FormData();
+  fd.append("photo", file);
+  return actionPromise(
+    "uploadPhoto",
+    fetch(`${backURL}/upload`, {
+      method: "POST",
+      headers: localStorage.authToken
+        ? { Authorization: "Bearer " + localStorage.authToken }
+        : {},
+      body: fd,
+    }).then((res) => res.json())
+  );
+};
+
+export const actionSetAvatar = (file) => async (dispatch, getState) => {
+  let { _id } = await dispatch(actionUploadPhoto(file));
+  let { id } = getState().auth.payload.sub;
+  await dispatch(actionUserUpdate({ _id: id, avatar: { _id } }));
+  await dispatch(actionAboutMe());
+};

+ 1 - 0
src/components/Audio.js

@@ -0,0 +1 @@
+export const Audio = () => {};

+ 23 - 0
src/components/AuthCheck.js

@@ -0,0 +1,23 @@
+import { Alert } from "react-bootstrap";
+import { Link } from "react-router-dom";
+
+export const AuthCheck = ({ header }) => {
+  return (
+    <div>
+      <Alert>
+        <h2>{header}</h2>
+        <p>
+          Чтобы видеть треки других пользователей, ваши треки, ваш профиль и
+          остальное необходимо войти в аккаунт. Если у вас ещё нет аккаунта -
+          зарегистрируйтесь!
+        </p>
+        <Link to="/signup" className="btn btn-outline-primary">
+          Зарегистрироваться
+        </Link>{" "}
+        <Link to="/login" className="btn btn-outline-primary">
+          Войти
+        </Link>
+      </Alert>
+    </div>
+  );
+};

+ 32 - 0
src/components/Dropzone.js

@@ -0,0 +1,32 @@
+import React, { useCallback } from "react";
+import { useDropzone } from "react-dropzone";
+import { connect } from "react-redux";
+import { actionSetAvatar } from "../actions";
+
+const MyDropzone = ({ onload }) => {
+  const onDrop = useCallback(
+    (acceptedFiles) => {
+      onload(acceptedFiles[0]);
+    },
+    [onload]
+  );
+  const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop });
+
+  return (
+    <div className="mt-2 text-center customBorder" {...getRootProps()}>
+      <input {...getInputProps()} />
+      {isDragActive ? (
+        <p>Поместите файлы сюда...</p>
+      ) : (
+        <p>
+          Для загрузки перетащите файлы сюда или нажмите на поле и выберите
+          файлы
+        </p>
+      )}
+    </div>
+  );
+};
+
+export const CMyDropzone = connect(null, {
+  onload: actionSetAvatar,
+})(MyDropzone);

+ 35 - 0
src/components/Header.js

@@ -0,0 +1,35 @@
+import React from "react";
+import { Navbar, Container, Nav, Button } from "react-bootstrap";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { faArrowLeft, faArrowRight } from "@fortawesome/free-solid-svg-icons";
+import { CAuth } from "../pages/Auth";
+import { history } from "./../App";
+
+export const Header = () => {
+  return (
+    <Navbar
+      className="Header"
+      collapseOnSelect
+      expand="lg"
+      bg="dark"
+      variant="dark"
+    >
+      <Container>
+        <Navbar.Toggle aria-controls="responsive-navbar-nav" />
+        <Navbar.Collapse id="responsive-navbar-nav">
+          <Nav className="me-auto d-flex mx-auto">
+            <Button className="btn-circle" onClick={() => history.goBack()}>
+              <FontAwesomeIcon icon={faArrowLeft} />
+            </Button>
+            <Button className="btn-circle" onClick={() => history.goForward()}>
+              <FontAwesomeIcon icon={faArrowRight} />
+            </Button>
+          </Nav>
+          <Nav>
+            <CAuth />
+          </Nav>
+        </Navbar.Collapse>
+      </Container>
+    </Navbar>
+  );
+};

+ 20 - 0
src/components/Loader.js

@@ -0,0 +1,20 @@
+import { Button, Spinner } from "react-bootstrap";
+
+export const Loader = () => {
+  return (
+    <Button
+      className="d-block mx-auto mt-5 text-center"
+      variant="dark"
+      disabled
+    >
+      <Spinner
+        as="span"
+        animation="grow"
+        size="sm"
+        role="status"
+        aria-hidden="true"
+      />
+      Loading...
+    </Button>
+  );
+};

+ 11 - 0
src/components/Logo.js

@@ -0,0 +1,11 @@
+import { faMusic } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { Link } from "react-router-dom";
+
+const LogoIcon = () => <FontAwesomeIcon icon={faMusic} color="#0B5ED7" />;
+
+export const Logo = () => (
+  <Link to="/" className="navbar-brand customBrand">
+    <LogoIcon /> Navy Web Player
+  </Link>
+);

+ 34 - 0
src/components/Main.js

@@ -0,0 +1,34 @@
+import React from "react";
+import { Route, Switch, withRouter } from "react-router-dom";
+import { CLoginForm } from "../pages/Login";
+import { CSignUpForm } from "../pages/Register";
+import { Page404 } from "../pages/Page404";
+import { CSearch } from "./../pages/Search";
+import { CLibrary } from "./../pages/Library";
+import { CProfile } from "./../pages/Profile";
+
+const Content = ({ children }) => <div className="Content">{children}</div>;
+
+const PageMain = () => {
+  return (
+    <div className="MainContent">
+      <h1>Главная страница</h1>
+    </div>
+  );
+};
+
+export const Main = () => (
+  <main className="Main">
+    <Content>
+      <Switch>
+        <Route path="/" component={withRouter(PageMain)} exact />
+        <Route path="/login" component={withRouter(CLoginForm)} />
+        <Route path="/signup" component={withRouter(CSignUpForm)} />
+        <Route path="/search" component={withRouter(CSearch)} />
+        <Route path="/library" component={withRouter(CLibrary)} />
+        <Route path="/profile" component={withRouter(CProfile)} />
+        <Route path="" component={withRouter(Page404)} />
+      </Switch>
+    </Content>
+  </main>
+);

+ 20 - 0
src/components/SearchField.js

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

+ 51 - 0
src/components/Sidebar.js

@@ -0,0 +1,51 @@
+import {
+  CDBSidebar,
+  CDBSidebarContent,
+  CDBSidebarFooter,
+  CDBSidebarHeader,
+  CDBSidebarMenu,
+  CDBSidebarMenuItem,
+} from "cdbreact";
+import { NavLink } from "react-router-dom";
+import { Logo } from "./Logo";
+
+export const Sidebar = () => {
+  return (
+    <div
+      style={{
+        display: "flex",
+        height: "100vh",
+        overflow: "scroll initial",
+        top: 0,
+        position: "fixed",
+      }}
+    >
+      <CDBSidebar backgroundColor="#212529">
+        <CDBSidebarHeader prefix={<i className="fa fa-bars fa-large"></i>}>
+          <Logo />
+        </CDBSidebarHeader>
+        <CDBSidebarContent className="sidebar-content">
+          <CDBSidebarMenu>
+            <NavLink exact to="/" activeClassName="activeClicked">
+              <CDBSidebarMenuItem icon="columns">Главная</CDBSidebarMenuItem>
+            </NavLink>
+            <NavLink exact to="/search" activeClassName="activeClicked">
+              <CDBSidebarMenuItem icon="search">Поиск</CDBSidebarMenuItem>
+            </NavLink>
+            <NavLink exact to="/library" activeClassName="activeClicked">
+              <CDBSidebarMenuItem icon="user">Моя музыка</CDBSidebarMenuItem>
+            </NavLink>
+          </CDBSidebarMenu>
+        </CDBSidebarContent>
+        <CDBSidebarFooter
+          className="m-1"
+          style={{
+            textAlign: "center",
+          }}
+        >
+          Navy Web Player
+        </CDBSidebarFooter>
+      </CDBSidebar>
+    </div>
+  );
+};

+ 21 - 0
src/components/Spoiler.js

@@ -0,0 +1,21 @@
+import { useState } from "react";
+import { Loader } from "./Loader";
+
+export const Spoiler = ({
+  open = false,
+  children = <Loader />,
+  header = "Spoiler",
+}) => {
+  const [visible, setVisible] = useState(open);
+  return (
+    <div className="Spoiler">
+      <div className="header" onClick={(e) => setVisible(!visible)}>
+        <div>
+          <span className="spoilerText">{header}</span>
+          <span className="spoilerArrow">{visible ? "▲" : "▼"}</span>
+        </div>
+      </div>
+      <div className="content">{visible && children}</div>
+    </div>
+  );
+};

+ 45 - 0
src/helpers/index.js

@@ -0,0 +1,45 @@
+export const jwtDecode = (token) => {
+  try {
+    let arrToken = token.split(".");
+    let base64Token = atob(arrToken[1]);
+    return JSON.parse(base64Token);
+  } catch (e) {
+    console.log("Error JWT: " + e);
+  }
+};
+
+export const getGQL =
+  (url) =>
+  async (query, variables = {}) => {
+    let obj = await fetch(url, {
+      method: "POST",
+      headers: {
+        "Content-Type": "application/json",
+        Authorization: localStorage.authToken
+          ? "Bearer " + localStorage.authToken
+          : {},
+      },
+      body: JSON.stringify({ query, variables }),
+    });
+    let a = await obj.json();
+    if (!a.data && a.errors) throw new Error(JSON.stringify(a.errors));
+    return a.data[Object.keys(a.data)[0]];
+  };
+
+export const backURL = "http://player.asmer.fs.a-level.com.ua";
+
+export const gql = getGQL(backURL + "/graphql");
+
+export function validateEmail(email) {
+  return /[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/.test(
+    email.toLowerCase()
+  );
+}
+
+export function validatePassword(password) {
+  return /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9]).{6,}/.test(password);
+}
+
+export function validateNickname(nick) {
+  return /^[a-z0-9_-]{3,8}$/.test(nick);
+}

BIN
src/images/default-avatar.jpg


+ 4 - 0
src/images/no-view.svg

@@ -0,0 +1,4 @@
+<!-- License: Apache. Made by grommet: https://github.com/grommet/grommet-icons -->
+<svg width="24px" height="24px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
+  <path fill="none" stroke="#000" stroke-width="2" d="M3,12 L6,12 C6.5,14.5 9.27272727,17 12,17 C14.7272727,17 17.5,14.5 18,12 L21,12 M12,17 L12,20 M7.5,15.5 L5.5,17.5 M16.5,15.5 L18.5,17.5"/>
+</svg>

+ 4 - 0
src/images/view.svg

@@ -0,0 +1,4 @@
+<!-- License: Apache. Made by grommet: https://github.com/grommet/grommet-icons -->
+<svg width="24px" height="24px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
+  <path fill="none" stroke="#000" stroke-width="2" d="M12,17 C9.27272727,17 6,14.2222222 6,12 C6,9.77777778 9.27272727,7 12,7 C14.7272727,7 18,9.77777778 18,12 C18,14.2222222 14.7272727,17 12,17 Z M11,12 C11,12.55225 11.44775,13 12,13 C12.55225,13 13,12.55225 13,12 C13,11.44775 12.55225,11 12,11 C11.44775,11 11,11.44775 11,12 Z"/>
+</svg>

+ 13 - 0
src/index.css

@@ -0,0 +1,13 @@
+body {
+  margin: 0;
+  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
+    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
+    sans-serif;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+code {
+  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
+    monospace;
+}

+ 10 - 0
src/index.js

@@ -0,0 +1,10 @@
+import React from "react";
+import ReactDOM from "react-dom";
+import App from "./App";
+
+ReactDOM.render(
+  <React.StrictMode>
+    <App />
+  </React.StrictMode>,
+  document.getElementById("root")
+);

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


+ 78 - 0
src/pages/Auth.js

@@ -0,0 +1,78 @@
+import { actionAuthLogout } from "../actions/index";
+import { NavDropdown } from "react-bootstrap";
+import { Link } from "react-router-dom";
+import { connect } from "react-redux";
+import { history } from "../App";
+import { backURL } from "../helpers";
+
+const Auth = ({ auth, promise, actionLogOut }) => {
+  if (
+    auth.token &&
+    (history.location.pathname === "/login" ||
+      history.location.pathname === "/signup")
+  ) {
+    history.push("/");
+  }
+  if (auth.token) {
+    localStorage.authToken = auth.token;
+  }
+  return (
+    <>
+      {auth.payload ? (
+        <NavDropdown
+          id="dropdownProfileMenu"
+          title={
+            <div className="pull-left d-inline-block">
+              <img
+                className="thumbnail-image avatarHeader"
+                src={
+                  promise?.user?.payload?.avatar
+                    ? `${backURL}/${promise.user.payload.avatar.url}`
+                    : "https://i.ibb.co/bBxzmTm/default-avatar.jpg"
+                }
+                alt="Avatar"
+              />
+              {promise?.user?.payload?.nick}
+            </div>
+          }
+          menuVariant="dark"
+        >
+          <NavDropdown.Item
+            componentclass={Link}
+            href="/profile"
+            to={`/profile`}
+          >
+            Профиль
+          </NavDropdown.Item>
+          <NavDropdown.Item
+            componentclass={Link}
+            href="/settings"
+            to={`/settings`}
+          >
+            Настройки
+          </NavDropdown.Item>
+          <NavDropdown.Divider />
+          <NavDropdown.Item as="button" onClick={() => actionLogOut()}>
+            Выйти
+          </NavDropdown.Item>
+        </NavDropdown>
+      ) : (
+        <>
+          <Link className="nav-link" to={"/signup"}>
+            Зарегистрироваться
+          </Link>
+          <Link className="btn btn-light" to={"/login"}>
+            Войти
+          </Link>
+        </>
+      )}
+    </>
+  );
+};
+
+export const CAuth = connect(
+  (state) => ({ auth: state.auth, promise: state.promise }),
+  {
+    actionLogOut: actionAuthLogout,
+  }
+)(Auth);

+ 30 - 0
src/pages/Library.js

@@ -0,0 +1,30 @@
+import { connect } from "react-redux";
+import { AuthCheck } from "./../components/AuthCheck";
+import { history } from "./../App";
+import { actionFindTracks, actionFindUser } from "./../actions/index";
+import { CMyDropzone } from "../components/Dropzone";
+
+const Library = ({ auth, promise, actionTracks, actionUser }) => {
+  return (
+    <div className="SearchPage">
+      {auth.token && history.location.pathname === "/library" ? (
+        <div className="d-block mx-auto mt-2 container w-50">
+          <h1>Ваша библиотека с музыкой, {promise?.user?.payload?.nick}</h1>
+          <CMyDropzone />
+        </div>
+      ) : (
+        <div className="d-block mx-auto mt-2 container w-50">
+          <AuthCheck header="Ваша музыка" />
+        </div>
+      )}
+    </div>
+  );
+};
+
+export const CLibrary = connect(
+  (state) => ({ auth: state.auth, promise: state.promise }),
+  {
+    actionTracks: actionFindTracks,
+    actionUser: actionFindUser,
+  }
+)(Library);

+ 73 - 0
src/pages/Login.js

@@ -0,0 +1,73 @@
+import { useState } from "react";
+import { actionFullLogin } from "./../actions/index";
+import { Form, Row, Col, Button, Alert } from "react-bootstrap";
+import { connect } from "react-redux";
+import { Link } from "react-router-dom";
+
+const LoginForm = ({ promise, onLogin }) => {
+  const [login, setLogin] = useState("");
+  const [password, setPassword] = useState("");
+  return (
+    <div className="AuthForm mx-auto mt-5">
+      <Form>
+        {promise.login &&
+        promise.login.status === "RESOLVED" &&
+        !promise.login.payload ? (
+          <Alert>
+            Извините, но такого пользователя не существует, попробуйте
+            зарегистрироваться или повторите ещё раз.
+          </Alert>
+        ) : null}
+        <Form.Group as={Row} className="mb-3" controlId="formHorizontalEmail">
+          <Form.Label column sm={2}>
+            Почта:
+          </Form.Label>
+          <Col sm={10}>
+            <Form.Control
+              type="text"
+              placeholder="Введите вашу почту"
+              onChange={(e) => setLogin(e.target.value)}
+            />
+          </Col>
+        </Form.Group>
+        <Form.Group
+          as={Row}
+          className="mb-3"
+          controlId="formHorizontalPassword"
+        >
+          <Form.Label column sm={2}>
+            Пароль:
+          </Form.Label>
+          <Col sm={10}>
+            <Form.Control
+              type="password"
+              placeholder="Введите ваш пароль"
+              onChange={(e) => setPassword(e.target.value)}
+            />
+          </Col>
+        </Form.Group>
+        <Form.Group as={Row} className="mb-3">
+          <Col sm={{ span: 10, offset: 2 }} className="my-3">
+            <Button
+              variant="success"
+              disabled={login.length < 1 || password.length < 1 ? true : false}
+              onClick={() => onLogin(login, password)}
+            >
+              Войти
+            </Button>
+          </Col>
+          <Col sm={{ span: 10, offset: 2 }}>
+            Нет аккаунта?{" "}
+            <Link className="btn btn-warning" to={"/signup"}>
+              Зарегистрироваться
+            </Link>
+          </Col>
+        </Form.Group>
+      </Form>
+    </div>
+  );
+};
+
+export const CLoginForm = connect((state) => ({ promise: state.promise }), {
+  onLogin: actionFullLogin,
+})(LoginForm);

+ 24 - 0
src/pages/Page404.js

@@ -0,0 +1,24 @@
+import { Link } from "react-router-dom";
+
+export const Page404 = () => (
+  <div className="text-center">
+    <h1>404 Error Page</h1>
+    <p className="zoom-area">Page was not found</p>
+    <section className="error-container">
+      <span className="four">
+        <span className="screen-reader-text">4</span>
+      </span>
+      <span className="zero">
+        <span className="screen-reader-text">0</span>
+      </span>
+      <span className="four">
+        <span className="screen-reader-text">4</span>
+      </span>
+    </section>
+    <div className="link-container">
+      <Link to="/" className="btn btn-light">
+        Go to home
+      </Link>
+    </div>
+  </div>
+);

+ 318 - 0
src/pages/Profile.js

@@ -0,0 +1,318 @@
+import { connect } from "react-redux";
+import { history } from "./../App";
+import { Button, Form, Row, Col, Alert } from "react-bootstrap";
+import { AuthCheck } from "./../components/AuthCheck";
+import { useState } from "react";
+
+import {
+  backURL,
+  validateEmail,
+  validatePassword,
+  validateNickname,
+} from "./../helpers/index";
+import {
+  actionUserUpdate,
+  actionAboutMe,
+  actionChangePassword,
+} from "./../actions/index";
+import { Loader } from "../components/Loader";
+import { CMyDropzone } from "../components/Dropzone";
+import { Spoiler } from "../components/Spoiler";
+
+const Profile = ({ auth, promise, actionUpdate, changePassword, aboutMe }) => {
+  const [login, setLogin] = useState("");
+  const [nick, setNickname] = useState("");
+  const [password, setPassword] = useState("");
+  const [newPassword, setNewPassword] = useState("");
+  const [passwordShown, setPasswordShown] = useState(false);
+
+  const togglePassword = () => {
+    setPasswordShown(!passwordShown);
+  };
+
+  return promise?.user?.status === "PENDING" ? (
+    <Loader />
+  ) : (
+    <div className="ProfilePage">
+      {auth.token && history.location.pathname === "/profile" ? (
+        <div className="d-block mx-auto mt-2 container w-50">
+          <h1>
+            Ваш профиль,{" "}
+            {promise?.user?.payload?.nick
+              ? promise?.user?.payload?.nick
+              : "user"}
+          </h1>
+          <Spoiler
+            children={
+              <>
+                <br />
+                <form
+                  action="/upload"
+                  method="post"
+                  enctype="multipart/form-data"
+                  id="form"
+                >
+                  <img
+                    className="avatarProfile"
+                    src={
+                      promise?.user?.payload?.avatar
+                        ? `${backURL}/${promise.user.payload.avatar.url}`
+                        : "https://i.ibb.co/bBxzmTm/default-avatar.jpg"
+                    }
+                    alt="Avatar"
+                  />
+                  <CMyDropzone />
+                </form>
+              </>
+            }
+            header={<h3>Изменить аватар</h3>}
+          />
+          <Spoiler
+            children={
+              <>
+                <br />
+                <Form>
+                  {promise?.user?.payload?.nick === nick ? (
+                    <Alert>Никнейм не должен повторяться с предыдущим.</Alert>
+                  ) : null}
+                  {validateNickname(nick) ? null : (
+                    <Alert>
+                      Никнейм может состоять только из строчных букв и цифр,
+                      символы - и _, а так же иметь длину от 3 до 8 символов.
+                    </Alert>
+                  )}
+                  <Form.Group
+                    as={Row}
+                    className="m-2"
+                    controlId="formHorizontalEmail"
+                  >
+                    <Form.Label column sm={2}>
+                      Ваш никнейм:
+                    </Form.Label>
+                    <Col sm={10}>
+                      <Form.Control
+                        type="text"
+                        placeholder="Ваш текущий никнейм"
+                        value={
+                          promise?.user?.payload?.nick
+                            ? promise?.user?.payload?.nick
+                            : "Никнейм не установлен"
+                        }
+                        disabled
+                      />
+                    </Col>
+                  </Form.Group>
+                  <Form.Group
+                    as={Row}
+                    className="m-2"
+                    controlId="formHorizontalEmail"
+                  >
+                    <Form.Label column sm={2}>
+                      Новый никнейм:
+                    </Form.Label>
+                    <Col sm={10}>
+                      <Form.Control
+                        type="text"
+                        placeholder="Введите ваш новый никнейм"
+                        value={nick}
+                        max="8"
+                        onChange={(e) => setNickname(e.target.value)}
+                      />
+                    </Col>
+                  </Form.Group>
+                  <Form.Group as={Row} className="mb-3">
+                    <Col sm={{ span: 10, offset: 2 }} className="my-3">
+                      <Button
+                        variant="success"
+                        disabled={
+                          promise?.user?.payload?.nick !== nick &&
+                          validateNickname(nick)
+                            ? false
+                            : true
+                        }
+                        onClick={() => {
+                          actionUpdate({
+                            _id: promise?.user?.payload?._id,
+                            nick,
+                          });
+                          aboutMe();
+                        }}
+                      >
+                        Сохранить
+                      </Button>
+                    </Col>
+                  </Form.Group>
+                </Form>
+              </>
+            }
+            header={<h3>Изменить никнейм</h3>}
+          />
+          <Spoiler
+            children={
+              <>
+                <br />
+                <Form>
+                  {promise?.user?.payload?.login === login ? (
+                    <Alert>Email не должен повторяться с предыдущим.</Alert>
+                  ) : null}
+                  {validateEmail(login) ? null : (
+                    <Alert>Email должен быть в формате: email@gmail.com.</Alert>
+                  )}
+                  <Form.Group
+                    as={Row}
+                    className="m-2"
+                    controlId="formHorizontalEmail"
+                  >
+                    <Form.Label column sm={2}>
+                      Ваша почта:
+                    </Form.Label>
+                    <Col sm={10}>
+                      <Form.Control
+                        type="text"
+                        placeholder="Ваша текущая почта"
+                        value={promise?.user?.payload?.login}
+                        disabled
+                      />
+                    </Col>
+                  </Form.Group>
+                  <Form.Group
+                    as={Row}
+                    className="m-2"
+                    controlId="formHorizontalEmail"
+                  >
+                    <Form.Label column sm={2}>
+                      Новая почта:
+                    </Form.Label>
+                    <Col sm={10}>
+                      <Form.Control
+                        type="text"
+                        placeholder="Введите вашу новую почту"
+                        value={login}
+                        onChange={(e) => setLogin(e.target.value)}
+                      />
+                    </Col>
+                  </Form.Group>
+                  <Form.Group as={Row} className="mb-3">
+                    <Col sm={{ span: 10, offset: 2 }} className="my-3">
+                      <Button
+                        variant="success"
+                        disabled={
+                          validateEmail(login) &&
+                          promise?.user?.payload?.login !== login
+                            ? false
+                            : true
+                        }
+                        onClick={() => {
+                          actionUpdate({
+                            _id: promise?.user?.payload?._id,
+                            login,
+                          });
+                          aboutMe();
+                        }}
+                      >
+                        Сохранить
+                      </Button>
+                    </Col>
+                  </Form.Group>
+                </Form>
+              </>
+            }
+            header={<h3>Изменить почту</h3>}
+          />
+          <Spoiler
+            children={
+              <>
+                <br />
+                <Form>
+                  {password.length !== 0 ? null : (
+                    <Alert>
+                      Пожалуйста, введите свой текущий пароль в первое поле для
+                      изменения пароля.
+                    </Alert>
+                  )}
+                  {validatePassword(newPassword) ? null : (
+                    <Alert>
+                      Новый пароль должен быть от 6 символов, иметь хотя бы одну
+                      цифру и заглавную букву.
+                    </Alert>
+                  )}
+                  <Form.Group
+                    as={Row}
+                    className="m-2"
+                    controlId="formHorizontalPassword"
+                  >
+                    <Form.Label column sm={2}>
+                      Пароль:
+                    </Form.Label>
+                    <Col sm={10}>
+                      <Form.Control
+                        type={passwordShown ? "text" : "password"}
+                        placeholder="Введите ваш текущий пароль"
+                        onChange={(e) => setPassword(e.target.value)}
+                      />
+                      <Button
+                        className="mt-2"
+                        variant="secondary"
+                        onClick={togglePassword}
+                      >
+                        {`${passwordShown ? "Hide" : "Show"} passwords`}
+                      </Button>
+                    </Col>
+                  </Form.Group>
+                  <Form.Group
+                    as={Row}
+                    className="m-2"
+                    controlId="formHorizontalPassword"
+                  >
+                    <Form.Label column sm={2}>
+                      Новый пароль:
+                    </Form.Label>
+                    <Col sm={10}>
+                      <Form.Control
+                        type={passwordShown ? "text" : "password"}
+                        placeholder="Введите ваш новый пароль"
+                        onChange={(e) => setNewPassword(e.target.value)}
+                      />
+                    </Col>
+                  </Form.Group>
+                  <Form.Group as={Row} className="mb-3">
+                    <Col sm={{ span: 10, offset: 2 }} className="my-3">
+                      <Button
+                        variant="success"
+                        disabled={validatePassword(newPassword) ? false : true}
+                        onClick={() => {
+                          changePassword(
+                            promise?.user?.payload?.login,
+                            password,
+                            newPassword
+                          );
+                          aboutMe();
+                        }}
+                      >
+                        Сохранить
+                      </Button>
+                    </Col>
+                  </Form.Group>
+                </Form>
+              </>
+            }
+            header={<h3>Изменить пароль</h3>}
+          />
+        </div>
+      ) : (
+        <div className="d-block mx-auto mt-2 container w-50">
+          <AuthCheck header="Ваш профиль" />
+        </div>
+      )}
+    </div>
+  );
+};
+
+export const CProfile = connect(
+  (state) => ({ auth: state.auth, promise: state.promise }),
+  {
+    actionUpdate: actionUserUpdate,
+    changePassword: actionChangePassword,
+    aboutMe: actionAboutMe,
+  }
+)(Profile);

+ 94 - 0
src/pages/Register.js

@@ -0,0 +1,94 @@
+import { useState } from "react";
+import { actionFullRegister } from "./../actions/index";
+import { Form, Row, Col, Button, Alert } from "react-bootstrap";
+import { connect } from "react-redux";
+import { Link } from "react-router-dom";
+import { validateEmail, validatePassword } from "./../helpers/index";
+
+const RegisterForm = ({ promise, auth, onRegister }) => {
+  const [login, setLogin] = useState("");
+  const [password, setPassword] = useState("");
+
+  return (
+    <div className="AuthForm mx-auto mt-5">
+      <Form>
+        {login.length === 0 ? null : validateEmail(login) ? (
+          password.length === 0 ? null : validatePassword(password) ? null : (
+            <Alert>
+              Пароль должен быть от 6 символов, иметь хотя бы одну цифру и
+              заглавную букву.
+            </Alert>
+          )
+        ) : (
+          <Alert>Email должен быть в формате: email@gmail.com.</Alert>
+        )}
+        {Object.keys(auth).length === 0 &&
+        promise?.registration?.status === "RESOLVED" ? (
+          <Alert>
+            Произошла ошибка при регистрации, пожалуйста, повторите ещё раз.
+            Возможно, такой пользователь уже существует.
+          </Alert>
+        ) : null}
+        <Form.Group as={Row} className="mb-3" controlId="formHorizontalEmail">
+          <Form.Label column sm={2}>
+            Почта:
+          </Form.Label>
+          <Col sm={10}>
+            <Form.Control
+              type="email"
+              required
+              placeholder="Введите вашу почту"
+              onChange={(e) => setLogin(e.target.value)}
+            />
+          </Col>
+        </Form.Group>
+        <Form.Group
+          as={Row}
+          className="mb-3"
+          controlId="formHorizontalPassword"
+        >
+          <Form.Label column sm={2}>
+            Пароль:
+          </Form.Label>
+          <Col sm={10}>
+            <Form.Control
+              type="password"
+              required
+              placeholder="Введите ваш пароль"
+              onChange={(e) => setPassword(e.target.value)}
+            />
+          </Col>
+        </Form.Group>
+        <Form.Group as={Row} className="mb-3">
+          <Col sm={{ span: 10, offset: 2 }} className="my-3">
+            <Button
+              id="signupBtn"
+              variant="success"
+              disabled={
+                validateEmail(login) && validatePassword(password)
+                  ? false
+                  : true
+              }
+              onClick={() => onRegister(login, password)}
+            >
+              Зарегистрироваться
+            </Button>
+          </Col>
+          <Col sm={{ span: 10, offset: 2 }}>
+            Есть аккаунт?{" "}
+            <Link className="btn btn-warning" to={"/login"}>
+              Авторизоваться
+            </Link>
+          </Col>
+        </Form.Group>
+      </Form>
+    </div>
+  );
+};
+
+export const CSignUpForm = connect(
+  (state) => ({ auth: state.auth, promise: state.promise }),
+  {
+    onRegister: actionFullRegister,
+  }
+)(RegisterForm);

+ 33 - 0
src/pages/Search.js

@@ -0,0 +1,33 @@
+import { connect } from "react-redux";
+import { SearchField } from "./../components/SearchField";
+import { AuthCheck } from "./../components/AuthCheck";
+import { history } from "./../App";
+import { Button } from "react-bootstrap";
+import { actionFindTracks, actionFindUser } from "./../actions/index";
+import { backURL } from "./../helpers/index";
+
+const Search = ({ auth, actionTracks, actionUser }) => {
+  return (
+    <div className="SearchPage">
+      {auth.token && history.location.pathname === "/search" ? (
+        <div className="d-block mx-auto mt-2 container w-50">
+          <h1>Поиск по сайту</h1>
+          <SearchField />
+          <Button onClick={() => actionTracks()}>Tracks</Button>
+        </div>
+      ) : (
+        <div className="d-block mx-auto mt-2 container w-50">
+          <AuthCheck header="Поиск по сайту" />
+        </div>
+      )}
+    </div>
+  );
+};
+
+export const CSearch = connect(
+  (state) => ({ auth: state.auth, promise: state.promise }),
+  {
+    actionTracks: actionFindTracks,
+    actionUser: actionFindUser,
+  }
+)(Search);

+ 39 - 0
src/reducers/index.js

@@ -0,0 +1,39 @@
+import { jwtDecode } from "./../helpers/index";
+
+export function promiseReducer(
+  state = {},
+  { type, status, payload, error, name }
+) {
+  if (type === "PROMISE") {
+    return {
+      ...state,
+      [name]: { status, payload, error },
+    };
+  }
+  return state;
+}
+
+export function authReducer(state, { type, token }) {
+  if (!state) {
+    if (localStorage.authToken) {
+      type = "AUTH_LOGIN";
+      token = localStorage.authToken;
+    } else state = {};
+  }
+  if (type === "AUTH_LOGIN") {
+    let payload = jwtDecode(token);
+    if (!!token && typeof payload === "object") {
+      localStorage.authToken = token;
+      return {
+        ...state,
+        token,
+        payload,
+      };
+    } else return state;
+  }
+  if (type === "AUTH_LOGOUT") {
+    localStorage.removeItem("authToken");
+    return {};
+  }
+  return state;
+}