Browse Source

Upload the project

surstrommed 2 years ago
parent
commit
5288f7388a

File diff suppressed because it is too large
+ 17745 - 38
package-lock.json


+ 26 - 1
package.json

@@ -3,12 +3,25 @@
   "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-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": {
@@ -34,5 +47,17 @@
       "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"
 }

+ 0 - 38
src/App.css

@@ -1,38 +0,0 @@
-.App {
-  text-align: center;
-}
-
-.App-logo {
-  height: 40vmin;
-  pointer-events: none;
-}
-
-@media (prefers-reduced-motion: no-preference) {
-  .App-logo {
-    animation: App-logo-spin infinite 20s linear;
-  }
-}
-
-.App-header {
-  background-color: #282c34;
-  min-height: 100vh;
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  justify-content: center;
-  font-size: calc(10px + 2vmin);
-  color: white;
-}
-
-.App-link {
-  color: #61dafb;
-}
-
-@keyframes App-logo-spin {
-  from {
-    transform: rotate(0deg);
-  }
-  to {
-    transform: rotate(360deg);
-  }
-}

+ 33 - 18
src/App.js

@@ -1,24 +1,39 @@
-import logo from './logo.svg';
-import './App.css';
+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";
+
+export const history = createBrowserHistory();
+
+export const store = createStore(
+  combineReducers({
+    promise: promiseReducer,
+    auth: authReducer,
+  }),
+  applyMiddleware(thunk)
+);
+
+export const getState = () => store.getState();
+
+store.subscribe(() => console.log(store.getState()));
 
 function App() {
   return (
-    <div className="App">
-      <header className="App-header">
-        <img src={logo} className="App-logo" alt="logo" />
-        <p>
-          Edit <code>src/App.js</code> and save to reload.
-        </p>
-        <a
-          className="App-link"
-          href="https://reactjs.org"
-          target="_blank"
-          rel="noopener noreferrer"
-        >
-          Learn React
-        </a>
-      </header>
-    </div>
+    <Router history={history}>
+      <Provider store={store}>
+        <div className="App">
+          <Header />
+          <Sidebar />
+          <Main />
+        </div>
+      </Provider>
+    </Router>
   );
 }
 

+ 175 - 0
src/App.scss

@@ -0,0 +1,175 @@
+@import "~bootstrap/scss/bootstrap";
+
+body {
+  background-color: #34393d;
+  color: white;
+  font-family: "Catamaran", sans-serif;
+}
+
+.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;
+}

+ 0 - 8
src/App.test.js

@@ -1,8 +0,0 @@
-import { render, screen } from '@testing-library/react';
-import App from './App';
-
-test('renders learn react link', () => {
-  render(<App />);
-  const linkElement = screen.getByText(/learn react/i);
-  expect(linkElement).toBeInTheDocument();
-});

+ 160 - 0
src/actions/index.js

@@ -0,0 +1,160 @@
+import { gql } from "../helpers";
+import { getState } from "./../App";
+
+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 actionAboutMe = (id, login, nick, avatar) => ({
+  type: "ABOUT_ME",
+  id,
+  login,
+  nick,
+  avatar,
+});
+
+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));
+  }
+};
+
+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 nick
+    }
+  }
+  `,
+      { 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 actionFullLogin =
+  (login, password) => async (dispatch, getState) => {
+    let token = await dispatch(actionLogin(login, password));
+    if (token) {
+      dispatch(actionAuthLogin(token));
+      const currentState = getState();
+      let id = currentState.auth.payload.sub.id;
+      let user = await dispatch(actionFindUser(id));
+      if (user._id) {
+        dispatch(actionAboutMe(user._id, user.login, user.nick, user.avatar));
+      }
+    }
+  };
+
+export const actionFullRegister = (login, password) => async (dispatch) => {
+  let check = await dispatch(actionRegister(login, password));
+  if (check) {
+    dispatch(actionFullLogin(login, password));
+  }
+};
+
+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: "[{}]" }
+    )
+  );
+
+export const actionUserUpdate = (_id, nick) =>
+  actionPromise(
+    "userUpdate",
+    gql(
+      `mutation userUpdate($user:UserInput){
+        UserUpsert(user: $user){
+          _id login nick
+        }
+      }
+  `,
+      {
+        user: {
+          _id: "61d45a16e9472933a6785f04",
+          login: "",
+          password: "",
+          nick: "",
+        },
+      }
+    )
+  );

+ 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>
+  );
+};

+ 46 - 0
src/components/Header.js

@@ -0,0 +1,46 @@
+import { faMusic } from "@fortawesome/free-solid-svg-icons";
+import React from "react";
+import { Navbar, Container, Nav, Button } from "react-bootstrap";
+import { Link } from "react-router-dom";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { faArrowLeft, faArrowRight } from "@fortawesome/free-solid-svg-icons";
+import { CAuth } from "../pages/Auth";
+import { history } from "./../App";
+
+const Logo = ({ logo }) => (
+  <Link to="/" className="navbar-brand">
+    <FontAwesomeIcon icon={faMusic} color="#0B5ED7" /> Navy Web Player
+  </Link>
+);
+
+export const Header = () => {
+  return (
+    <Navbar
+      className="Header"
+      collapseOnSelect
+      expand="lg"
+      bg="dark"
+      variant="dark"
+    >
+      <Container>
+        <Navbar.Brand>
+          <Logo logo={faMusic} />
+        </Navbar.Brand>
+        <Navbar.Toggle aria-controls="responsive-navbar-nav" />
+        <Navbar.Collapse id="responsive-navbar-nav">
+          <Nav className="me-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>
+  );
+};

+ 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>
+  );
+};

+ 56 - 0
src/components/Sidebar.js

@@ -0,0 +1,56 @@
+import {
+  CDBSidebar,
+  CDBSidebarContent,
+  CDBSidebarFooter,
+  CDBSidebarHeader,
+  CDBSidebarMenu,
+  CDBSidebarMenuItem,
+} from "cdbreact";
+import { Link } from "react-router-dom";
+import { NavLink } from "react-router-dom";
+
+export const Sidebar = () => {
+  return (
+    <div
+      style={{
+        display: "flex",
+        height: "100vh",
+        overflow: "scroll initial",
+        position: "fixed",
+      }}
+    >
+      <CDBSidebar backgroundColor="#212529">
+        <CDBSidebarHeader prefix={<i className="fa fa-bars fa-large"></i>}>
+          <Link
+            to="/"
+            className="text-decoration-none"
+            style={{ color: "inherit" }}
+          >
+            Player Menu
+          </Link>
+        </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>
+  );
+};

+ 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-zA-Z0-9]+/g.test(nick);
+}

+ 4 - 11
src/index.js

@@ -1,17 +1,10 @@
-import React from 'react';
-import ReactDOM from 'react-dom';
-import './index.css';
-import App from './App';
-import reportWebVitals from './reportWebVitals';
+import React from "react";
+import ReactDOM from "react-dom";
+import App from "./App";
 
 ReactDOM.render(
   <React.StrictMode>
     <App />
   </React.StrictMode>,
-  document.getElementById('root')
+  document.getElementById("root")
 );
-
-// If you want to start measuring performance in your app, pass a function
-// to log results (for example: reportWebVitals(console.log))
-// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
-reportWebVitals();

+ 65 - 0
src/pages/Auth.js

@@ -0,0 +1,65 @@
+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";
+
+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={promise.user.payload.nick}
+          title="dick"
+          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);

+ 33 - 0
src/pages/Library.js

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

+ 124 - 0
src/pages/Profile.js

@@ -0,0 +1,124 @@
+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 {
+  validateEmail,
+  validatePassword,
+  validateNickname,
+} from "./../helpers/index";
+import { actionUserUpdate } from "./../actions/index";
+
+const Profile = ({ auth, promise, actionUpdate }) => {
+  const [login, setLogin] = useState(promise.user.payload.login);
+  const [password, setPassword] = useState("");
+  const [nick, setNickname] = useState(promise.user.payload.nick);
+
+  return (
+    <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}</h1>
+          <Form>
+            {validateEmail(login) ? null : (
+              <Alert>Email должен быть в формате: email@gmail.com.</Alert>
+            )}
+            {validatePassword(password) ? null : (
+              <Alert>
+                Пароль должен быть от 6 символов, иметь хотя бы одну цифру и
+                заглавную букву.
+              </Alert>
+            )}
+            {validateNickname(nick) ? null : (
+              <Alert>
+                Никнейм может состоять только из букв и цифр, а так же иметь
+                максимальную длину в 8 символов.
+              </Alert>
+            )}
+            <Form.Group
+              as={Row}
+              className="mb-3"
+              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"
+              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"
+              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={
+                    validateEmail(login) &&
+                    password.length !== 0 &&
+                    validatePassword(password) &&
+                    validateNickname(nick)
+                      ? false
+                      : true
+                  }
+                  onClick={() => actionUpdate(promise.user.payload._id)}
+                >
+                  Сохранить
+                </Button>
+              </Col>
+            </Form.Group>
+          </Form>
+        </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,
+  }
+)(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);

+ 23 - 0
src/pages/Search.js

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

+ 109 - 0
src/reducers/index.js

@@ -0,0 +1,109 @@
+// 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, id, login, nick, avatar }) {
+//   if (!state) {
+//     if (localStorage.authToken) {
+//       type = "AUTH_LOGIN";
+//       token = localStorage.authToken;
+//     } else state = {};
+//   }
+//   if (type === "AUTH_LOGIN") {
+//     localStorage.setItem("authToken", token);
+//     let payload = jwtDecode(token);
+//     if (typeof payload === "object") {
+//       return {
+//         ...state,
+//         token,
+//         payload,
+//       };
+//     } else return state;
+//   }
+//   if (type === "ABOUT_ME") {
+//     let user = {
+//       id,
+//       login,
+//       nick,
+//       avatar,
+//     };
+//     if (id) {
+//       localStorage.setItem("user", JSON.stringify(user));
+//       return {
+//         ...state,
+//         id,
+//         login,
+//         nick,
+//         avatar,
+//       };
+//     } else return state;
+//   }
+//   if (type === "AUTH_LOGOUT") {
+//     localStorage.removeItem("authToken");
+//     localStorage.removeItem("user");
+//     return {};
+//   }
+// }
+
+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, id, login, nick, avatar }) {
+  if (!state) {
+    if (localStorage.authToken) {
+      type = "AUTH_LOGIN";
+      token = localStorage.authToken;
+    } else state = {};
+  }
+  if (type === "AUTH_LOGIN") {
+    localStorage.setItem("authToken", token);
+    let payload = jwtDecode(token);
+    if (typeof payload === "object") {
+      return {
+        ...state,
+        token,
+        payload,
+      };
+    } else return state;
+  }
+  if (type === "ABOUT_ME") {
+    localStorage.setItem("user", JSON.stringify("object"));
+    localStorage.setItem("authToken", token);
+    let payload = jwtDecode(token);
+    if (typeof payload === "object") {
+      return {
+        ...state,
+        token,
+        payload,
+      };
+    } else return state;
+  }
+  if (type === "AUTH_LOGOUT") {
+    localStorage.removeItem("authToken");
+    return {};
+  }
+  return state;
+}

+ 0 - 13
src/reportWebVitals.js

@@ -1,13 +0,0 @@
-const reportWebVitals = onPerfEntry => {
-  if (onPerfEntry && onPerfEntry instanceof Function) {
-    import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
-      getCLS(onPerfEntry);
-      getFID(onPerfEntry);
-      getFCP(onPerfEntry);
-      getLCP(onPerfEntry);
-      getTTFB(onPerfEntry);
-    });
-  }
-};
-
-export default reportWebVitals;

+ 0 - 5
src/setupTests.js

@@ -1,5 +0,0 @@
-// jest-dom adds custom jest matchers for asserting on DOM nodes.
-// allows you to do things like:
-// expect(element).toHaveTextContent(/react/i)
-// learn more: https://github.com/testing-library/jest-dom
-import '@testing-library/jest-dom';