screamoviolence 2 vuotta sitten
commit
ab1a6a5c66
70 muutettua tiedostoa jossa 19475 lisäystä ja 0 poistoa
  1. 1 0
      client/.gitignore
  2. 70 0
      client/README.md
  3. 17026 0
      client/package-lock.json
  4. 64 0
      client/package.json
  5. 26 0
      client/public/index.html
  6. 3 0
      client/src/.vscode/settings.json
  7. 23 0
      client/src/App.css
  8. 45 0
      client/src/App.js
  9. BIN
      client/src/assets/images/1.webp
  10. BIN
      client/src/assets/images/default_userAvatar.png
  11. BIN
      client/src/assets/images/github_logo.png
  12. 18 0
      client/src/assets/images/preloader.svg
  13. 11 0
      client/src/components/Dialogs/Dialogs.jsx
  14. 32 0
      client/src/components/Dialogs/Dialogs.module.css
  15. 9 0
      client/src/components/Footer/Footer.jsx
  16. 12 0
      client/src/components/Footer/Footer.module.css
  17. 51 0
      client/src/components/Gallery/Gallery.jsx
  18. 56 0
      client/src/components/Gallery/Gallery.module.css
  19. 71 0
      client/src/components/Header/Header.jsx
  20. 24 0
      client/src/components/Header/Header.module.css
  21. 25 0
      client/src/components/Header/HeaderContainer.jsx
  22. 50 0
      client/src/components/Header/styles.js
  23. 167 0
      client/src/components/Login/Auth.jsx
  24. 14 0
      client/src/components/Login/Icon.js
  25. 45 0
      client/src/components/Login/Input.jsx
  26. 31 0
      client/src/components/Login/styles.js
  27. 7 0
      client/src/components/Music/Music.jsx
  28. 10 0
      client/src/components/Music/Music.module.css
  29. 85 0
      client/src/components/NavBar/Navbar.jsx
  30. 26 0
      client/src/components/NavBar/Navbar.module.css
  31. 37 0
      client/src/components/News/AllPosts/AllPosts.jsx
  32. 27 0
      client/src/components/News/News.jsx
  33. 10 0
      client/src/components/News/News.module.css
  34. 125 0
      client/src/components/Profile/MyPosts/Form/Form.jsx
  35. 25 0
      client/src/components/Profile/MyPosts/Form/styles.js
  36. 41 0
      client/src/components/Profile/MyPosts/MyPosts.jsx
  37. 11 0
      client/src/components/Profile/MyPosts/MyPosts.module.css
  38. 132 0
      client/src/components/Profile/MyPosts/Post/Post.jsx
  39. 10 0
      client/src/components/Profile/MyPosts/Post/Post.module.css
  40. 52 0
      client/src/components/Profile/MyPosts/Post/styles.js
  41. 18 0
      client/src/components/Profile/MyPosts/styles.js
  42. 113 0
      client/src/components/Profile/Profile.jsx
  43. 21 0
      client/src/components/Profile/ProfileContainer.jsx
  44. 42 0
      client/src/components/Profile/ProfileDataForm/ProfileDataForm.jsx
  45. 12 0
      client/src/components/Profile/ProfileDataForm/ProfileInfo.module.css
  46. 7 0
      client/src/components/Settings/Settings.jsx
  47. 10 0
      client/src/components/Settings/Settings.module.css
  48. 28 0
      client/src/components/Users/User.jsx
  49. 39 0
      client/src/components/Users/Users.jsx
  50. 35 0
      client/src/components/Users/Users.module.css
  51. 38 0
      client/src/components/api/Api.js
  52. 103 0
      client/src/components/api/OldApi.js
  53. 17 0
      client/src/components/common/FormControls/FormControls.module.css
  54. 70 0
      client/src/components/common/FormControls/FormsControls.js
  55. 68 0
      client/src/components/common/Pagination/Pagination.jsx
  56. 24 0
      client/src/components/common/Pagination/Pagination.module.css
  57. 12 0
      client/src/components/common/preLoader/preLoader.js
  58. 17 0
      client/src/index.css
  59. 21 0
      client/src/index.js
  60. 11 0
      client/src/redux/actions/UsersThunk.js
  61. 26 0
      client/src/redux/actions/authThunks.js
  62. 56 0
      client/src/redux/actions/postsThunks.js
  63. 23 0
      client/src/redux/actions/profileThunks.js
  64. 35 0
      client/src/redux/reducers/authReducer.js
  65. 36 0
      client/src/redux/reducers/dialogsReducer.js
  66. 55 0
      client/src/redux/reducers/profileReducer.js
  67. 26 0
      client/src/redux/reducers/usersReducer.js
  68. 26 0
      client/src/redux/reduxStore.js
  69. 13 0
      client/src/reportWebVitals.js
  70. 1 0
      server

+ 1 - 0
client/.gitignore

@@ -0,0 +1 @@
+node_modules

+ 70 - 0
client/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 the browser.
+
+The page will reload if you make edits.\
+You will 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)

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 17026 - 0
client/package-lock.json


+ 64 - 0
client/package.json

@@ -0,0 +1,64 @@
+{
+  "name": "my-first-app",
+  "version": "0.1.0",
+  "private": true,
+  "proxy": "http://localhost:5000",
+  "dependencies": {
+    "@material-ui/core": "^4.12.3",
+    "@material-ui/icons": "^4.11.2",
+    "@testing-library/jest-dom": "^5.14.1",
+    "@testing-library/react": "^11.2.7",
+    "@testing-library/user-event": "^12.8.3",
+    "axios": "^0.21.4",
+    "classnames": "^2.3.1",
+    "jwt-decode": "^3.1.2",
+    "moment": "^2.29.1",
+    "react": "^17.0.2",
+    "react-dom": "^17.0.2",
+    "react-file-base64": "^1.0.3",
+    "react-google-login": "^5.2.2",
+    "react-redux": "^7.2.4",
+    "react-router-dom": "^5.2.0",
+    "react-scripts": "4.0.3",
+    "redux": "^4.1.1",
+    "redux-form": "^8.3.7",
+    "redux-thunk": "^2.3.0",
+    "reselect": "^4.0.0",
+    "web-vitals": "^1.1.2"
+  },
+  "scripts": {
+    "start": "react-scripts start",
+    "build": "react-scripts build",
+    "test": "react-scripts test",
+    "eject": "react-scripts eject",
+    "predeploy": "npm run build",
+    "deploy": "gh-pages -d build"
+  },
+  "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"
+    ]
+  },
+  "devDependencies": {
+    "gh-pages": "^3.2.3",
+    "react-test-renderer": "^17.0.2"
+  },
+  "description": "This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).",
+  "main": "index.js",
+  "keywords": [],
+  "author": "",
+  "license": "ISC"
+}

+ 26 - 0
client/public/index.html

@@ -0,0 +1,26 @@
+<!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" />
+
+  <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
+
+  <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=Playfair+Display&display=swap" rel="stylesheet">
+  <title>InTouch</title>
+</head>
+
+<body>
+  <noscript>You need to enable JavaScript to run this app.</noscript>
+  <div id="root"></div>
+
+</body>
+
+</html>

+ 3 - 0
client/src/.vscode/settings.json

@@ -0,0 +1,3 @@
+{
+  "cSpell.words": ["call", "subcriber"]
+}

+ 23 - 0
client/src/App.css

@@ -0,0 +1,23 @@
+.app-wrapper {
+  font-family: 'Playfair Display', Arial, Helvetica, sans-serif;
+
+  width: 1170px;
+  min-height: 100vh;
+  margin: 0 auto;
+
+  display: grid;
+
+  grid-template-areas:
+  "h h"
+  "n c"
+  "f f";
+
+  grid-template-rows: 60px 1fr;
+  grid-template-columns: 2fr 10fr;
+  grid-gap: 10px;
+}
+
+.app-wrapper-content {
+    grid-area: c;
+    font-family: Cambria, Cochin, Georgia, Times, 'Times New Roman', serif;
+  }

+ 45 - 0
client/src/App.js

@@ -0,0 +1,45 @@
+import React from "react";
+import "./App.css";
+import Navbar from "./components/NavBar/Navbar";
+import News from "./components/News/News";
+import Music from "./components/Music/Music";
+import Settings from "./components/Settings/Settings";
+import {  Route } from "react-router-dom";
+import Gallery from "./components/Gallery/Gallery";
+import {  Switch } from "react-router";
+import Footer from "./components/Footer/Footer";
+import Auth from "./components/Login/Auth";
+import Users from "./components/Users/Users";
+import Header from "./components/Header/Header";
+import Profile from "./components/Profile/Profile"
+import Dialogs from "./components/Dialogs/Dialogs";
+import ProfileContainer from "./components/Profile/ProfileContainer";
+
+
+const App = () => {
+  return (
+    <div className="app-wrapper">
+      <Header />
+     
+      <Navbar />
+      <Footer />
+      <div className="app-wrapper-content">
+        <Switch>
+          <Route exact path="/" render={() => <News />} />
+          <Route path="/profile" render={() => <ProfileContainer />} />
+          <Route path="/dialogs" render={() => <Dialogs />} />
+          <Route path="/users" render={() => <Users />} />
+          <Route path="/auth" render={() => <Auth />} />
+          <Route path="/gallery" component={Gallery} />
+          <Route path="/news" component={News} />
+          <Route path="/music" component={Music} />
+          <Route path="/settings" component={Settings} />
+          <Route path="*" render={() => <div>404 Not found</div>} />
+          
+        </Switch>
+      </div>
+    </div>
+  );
+}
+
+export default App

BIN
client/src/assets/images/1.webp


BIN
client/src/assets/images/default_userAvatar.png


BIN
client/src/assets/images/github_logo.png


+ 18 - 0
client/src/assets/images/preloader.svg

@@ -0,0 +1,18 @@
+<svg class="lds-coolors" width="200px"  height="200px"  xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid" style="background: none;"><defs>
+ <mask id="coolors-e7ea09d86ed2b">
+   <circle cx="50" cy="50" r="10" stroke="#fff" stroke-linecap="round" stroke-dasharray="47.12388980384689 15.707963267948966" stroke-width="3" transform="rotate(132 50 50)">
+     <animateTransform attributeName="transform" type="rotate" values="0 50 50;360 50 50" times="0;1" dur="2s" repeatCount="indefinite"></animateTransform>
+   </circle>
+ </mask>
+</defs>
+<g mask="url(#coolors-e7ea09d86ed2b)"><rect x="36.5" y="0" width="6.2" height="100">
+  <animate attributeName="fill" values="#3be8b0;#1aafd0;#6a67ce;#ffb900;#fc636b" times="0;0.25;0.5;0.75;1" dur="2s" repeatCount="indefinite" begin="-0.8s"></animate>
+</rect><rect x="41.7" y="0" width="6.2" height="100">
+  <animate attributeName="fill" values="#3be8b0;#1aafd0;#6a67ce;#ffb900;#fc636b" times="0;0.25;0.5;0.75;1" dur="2s" repeatCount="indefinite" begin="-0.6s"></animate>
+</rect><rect x="46.9" y="0" width="6.2" height="100">
+  <animate attributeName="fill" values="#3be8b0;#1aafd0;#6a67ce;#ffb900;#fc636b" times="0;0.25;0.5;0.75;1" dur="2s" repeatCount="indefinite" begin="-0.4s"></animate>
+</rect><rect x="52.1" y="0" width="6.2" height="100">
+  <animate attributeName="fill" values="#3be8b0;#1aafd0;#6a67ce;#ffb900;#fc636b" times="0;0.25;0.5;0.75;1" dur="2s" repeatCount="indefinite" begin="-0.2s"></animate>
+</rect><rect x="57.3" y="0" width="6.2" height="100">
+  <animate attributeName="fill" values="#3be8b0;#1aafd0;#6a67ce;#ffb900;#fc636b" times="0;0.25;0.5;0.75;1" dur="2s" repeatCount="indefinite" begin="0s"></animate>
+</rect></g></svg>

+ 11 - 0
client/src/components/Dialogs/Dialogs.jsx

@@ -0,0 +1,11 @@
+import React from 'react'
+
+const Dialogs = () => {
+  return (
+    <div>
+      Dialogs
+    </div>
+  )
+}
+
+export default Dialogs

+ 32 - 0
client/src/components/Dialogs/Dialogs.module.css

@@ -0,0 +1,32 @@
+.item {
+  font-family: Cambria, Cochin, Georgia, Times, 'Times New Roman', serif;
+}
+
+.item img {
+  width: 50px;
+  height: 50px;
+  
+  border-radius: 50px;
+}
+
+.dialogs {
+display: grid;
+grid-template-columns: 2fr 10fr;
+}
+
+.dialogsItems {
+
+}
+
+.dialogsItems .active {
+  font-weight: 700;
+}
+
+
+.dialog {
+
+}
+
+.messages {
+
+}

+ 9 - 0
client/src/components/Footer/Footer.jsx

@@ -0,0 +1,9 @@
+import React from 'react';
+import classes from './Footer.module.css';
+
+
+const Footer = () => {
+    return (
+        <footer className={classes.footer}>Footer</footer>)
+}
+export default Footer;

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 12 - 0
client/src/components/Footer/Footer.module.css


+ 51 - 0
client/src/components/Gallery/Gallery.jsx

@@ -0,0 +1,51 @@
+import React from "react";
+
+const Gallery = () => {
+  return <div>Gallery</div>;
+
+  // return (
+  //   <div className={classes.divWrapper}>
+  //   <div className={classes.container}>
+  //     <div
+  //       className={classes.slide}
+  //       style={{backgroundImage: `url('https://images.unsplash.com/photo-1540939615471-650169276c13?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=1031&q=80')`}}
+  //     >
+  //       <h3>flower</h3>
+  //     </div>
+  //     <div
+  //       className={classes.slide}
+  //        style=
+  //          {{backgroundImage: `url('https://images.unsplash.com/photo-1503262028195-93c528f03218?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=1051&q=80')`}}
+
+  //     >
+  //       <h3>flower</h3>
+  //     </div>
+  //     <div
+  //       className={classes.slide}
+  //        style=
+  //          {{backgroundImage: `url('https://images.unsplash.com/photo-1605015238459-ebb14c9f865d?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=1050&q=80')`}}
+
+  //     >
+  //       <h3>flower</h3>
+  //     </div>
+  //     <div
+  //       className={classes.slide}
+  //        style=
+  //          {{backgroundImage: `url('https://images.unsplash.com/photo-1610768252994-c88f146aaeae?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=1050&q=80')`}}
+
+  //     >
+  //       <h3>flower</h3>
+  //     </div>
+  //     <div
+  //      className={classes.slide}
+  //       style={{backgroundImage: `url('https://images.unsplash.com/photo-1516834474-48c0abc2a902?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=1053&q=80')`}}
+
+  //     >
+  //       <h3>flower</h3>
+  //     </div>
+  //   </div>
+  //   </div>
+  // );
+};
+
+export default Gallery;

+ 56 - 0
client/src/components/Gallery/Gallery.module.css

@@ -0,0 +1,56 @@
+@import url('https://fonts.googleapis.com/css?family=Muli&display=swap'); 
+
+* {
+  box-sizing: border-box;
+}
+
+.divWrapper {
+  font-family: 'Muli', sans-serif;
+  overflow: hidden;
+  margin: 0;
+  background: white;
+  height: 100vh;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+
+.container {
+  width: 100%;
+  display: flex;
+  padding: 0 20px;
+}
+
+.slide {
+  height: 80vh;
+  border-radius: 20px;
+  margin: 10px;
+  cursor: pointer;
+  color: #fff;
+  flex: 1;
+  background-size: cover;
+  background-position: center;
+  background-repeat: no-repeat;
+  position: relative;
+  
+  transition: all 500ms ease-in-out 0.1s;
+
+}
+
+.slide h3 {
+  position: absolute;
+  font-size: 22px;
+  bottom: 30px;
+  left: 10px;
+  margin: 0;
+  opacity: 0;
+}
+
+.slide.active {
+  flex: 10;
+}
+
+.slide.active h3 {
+  opacity: 1;
+  transition:  opacity 0.3s ease-in ;
+}

+ 71 - 0
client/src/components/Header/Header.jsx

@@ -0,0 +1,71 @@
+import React, { useEffect, useState } from "react";
+import classes from "./Header.module.css";
+import { Link, useHistory, useLocation } from "react-router-dom";
+import logo from "../../assets/images/github_logo.png";
+import {  Avatar, Button, Typography } from "@material-ui/core";
+import useStyles from "./styles.js";
+import decode from 'jwt-decode'
+import { useDispatch } from "react-redux";
+import { LOGOUT } from "../../redux/reducers/authReducer";
+
+
+const Header = () => {
+  const uiClasses = useStyles();
+  const [user, setUser] = useState(JSON.parse(localStorage.getItem("profile")));
+  const history = useHistory();
+  const location = useLocation();
+
+  const dispatch = useDispatch();
+
+  const logout = () => {
+    dispatch({ type: LOGOUT });
+    history.push("/");
+    setUser(null);
+  };
+
+  useEffect(() => {
+    const token = user?.token;
+    if(token) {
+      const decodedToken = decode(token)
+      if (decodedToken.exp * 1000 < new Date().getTime()) logout()
+    }
+    setUser(JSON.parse(localStorage.getItem("profile")));
+  }, [location]);
+
+
+  return (
+
+    <header className={classes.header}>
+      <img src={logo} alt="icon" height="60" />
+      <div className={classes.loginBlock}>
+        {user ? (
+          <div className={uiClasses.profile}>
+            <Avatar
+              className={uiClasses.purple}
+              alt={user?.result.name}
+              src={user?.result.imageUrl}
+            >
+              {user?.result.name.charAt(0)}
+            </Avatar>
+            <Typography className={uiClasses.userName} variant="h6">
+              {user?.result.name}
+            </Typography>
+            <Button
+              variant="contained"
+              className={uiClasses.logout}
+              color="secondary"
+              onClick={logout}
+            >
+              Logout
+            </Button>
+          </div>
+        ) : (
+          <Button component={Link} to="/auth">
+            sign in
+          </Button>
+        )}
+      </div>
+    </header>
+  );
+};
+export default Header;

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 24 - 0
client/src/components/Header/Header.module.css


+ 25 - 0
client/src/components/Header/HeaderContainer.jsx

@@ -0,0 +1,25 @@
+import {
+    addPostActionCreator,
+  } from "../../../redux/profileReducer";
+  import MyPosts from "./MyPosts";
+  import { connect } from "react-redux";
+  
+  let mapStateToProps = (state) => {
+    return {
+      postsData: state.profilePage.postsData,
+      newPostText: state.profilePage.newPostText,
+    };
+  };
+  
+  let mapDispatchToProps = (dispatch) => {
+    return {
+      addPost: (formData) => {
+        dispatch(addPostActionCreator(formData));
+      },
+    };
+  };
+  
+  const MyPostsContainer = connect(mapStateToProps, mapDispatchToProps)(MyPosts); // конект создает контейнерную колмпоненту
+  
+  export default MyPostsContainer;
+  

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 50 - 0
client/src/components/Header/styles.js


+ 167 - 0
client/src/components/Login/Auth.jsx

@@ -0,0 +1,167 @@
+import React, { useState } from "react";
+import {
+  Avatar,
+  Button,
+  Container,
+  Grid,
+  Paper,
+  Typography,
+} from "@material-ui/core";
+import { GoogleLogin } from "react-google-login";
+import LockOutlinedIcon from "@material-ui/icons/LockOutlined";
+import useStyles from "./styles";
+import Input from "./Input";
+import Icon from "./Icon";
+import { useDispatch } from "react-redux";
+import { AUTH } from "../../redux/reducers/authReducer";
+import { useHistory } from "react-router";
+import { signIn, signUp } from "../../redux/actions/authThunks";
+
+const initialState = {
+  firstName: "",
+  lastName: "",
+  email: "",
+  password: "",
+  confirmPassword: "",
+};
+
+const Auth = () => {
+  const classes = useStyles();
+  const [showPassword, setShowPassword] = useState(false);
+  const [isSignup, setIsSignup] = useState(false);
+  const [formData, setFormData] = useState(initialState);
+  const dispatch = useDispatch();
+  const history = useHistory();
+
+  const handleSubmit = (e) => {
+    e.preventDefault();
+    if (isSignup) {
+        dispatch(signUp(formData, history))
+    }else {
+        dispatch(signIn(formData, history))
+
+    }
+    console.log(formData);
+  };
+
+  const handleChange = (e) => {
+
+    setFormData({ ...formData, [e.target.name]: e.target.value });
+  };
+
+  const handleShowPassword = () => {
+    setShowPassword((prevShowPassword) => !prevShowPassword);
+  };
+
+  const switchMode = () => {
+    setIsSignup((prevIsSignUp) => !prevIsSignUp);
+    setShowPassword(false);
+  };
+
+  const googleSuccess = async (res) => {
+    const result = res?.profileObj;
+    const token = res?.tokenId;
+
+    try {
+      dispatch({ type: AUTH, data: { result, token } });
+      history.push("/");
+    } catch (error) {
+      console.log(error);
+    }
+  };
+  const googleFailure = () => {
+    console.log("failed");
+  };
+
+  return (
+    <Container component="main" maxWidth="xs">
+      <Paper className={classes.paper} elevation={3}>
+        <Avatar className={classes.avatar}>
+          <LockOutlinedIcon />
+        </Avatar>
+        <Typography variant="h5">{isSignup ? "Sign Up" : "Sign In"}</Typography>
+        <form className={classes.form} onSubmit={handleSubmit}>
+          <Grid container spacing={2}>
+            {isSignup && (
+              <>
+                <Input
+                  name="firstName"
+                  label="First Name"
+                  handleChange={handleChange}
+                  autoFocus
+                  half
+                />
+                <Input
+                  name="lastName"
+                  label="Last Name"
+                  handleChange={handleChange}
+                  half
+                />
+              </>
+            )}
+            <Input
+              name="email"
+              label="Email Address"
+              handleChange={handleChange}
+              type="email"
+            />
+            <Input
+              name="password"
+              label="Password"
+              handleChange={handleChange}
+              type={showPassword ? "text" : "password"}
+              handleShowPassword={handleShowPassword}
+            />
+            {isSignup && (
+              <Input
+                name="confirmPassword"
+                label="Repeat Password"
+                handleChange={handleChange}
+                type="password"
+              />
+            )}
+          </Grid>
+          <Button
+            type="submit"
+            fullWidth
+            variant="contained"
+            color="primary"
+            className={classes.submit}
+          >
+            {isSignup ? "Sign Up" : "Sign In"}
+          </Button>
+          <GoogleLogin
+            clientId="1095914572545-h16ak2hlqg1v8tadmjc82diueg4t6oir.apps.googleusercontent.com"
+            render={(renderProps) => (
+              <Button
+                className={classes.googleButton}
+                color="primary"
+                fullWidth
+                onClick={renderProps.onClick}
+                disabled={renderProps.disabled}
+                startIcon={<Icon />}
+                variant="contained"
+              >
+                Google Sign In
+              </Button>
+            )}
+            onSuccess={googleSuccess}
+            onFailure={googleFailure}
+            cookiePolicy="single_host_origin"
+          />
+          <Grid container justify="flex-end">
+            <Grid item>
+              <Button onClick={switchMode}>
+                {isSignup
+                  ? "Already have an account? Sign In"
+                  : "Dont have an account? Sign Up"}
+              </Button>
+            </Grid>
+          </Grid>
+        </form>
+      </Paper>
+    </Container>
+  );
+};
+
+export default Auth;

+ 14 - 0
client/src/components/Login/Icon.js

@@ -0,0 +1,14 @@
+import React from "react"
+
+function Icon() {
+    return (
+        <svg style={{ width: '20px', height: '20px' }} viewBox="0 0 24 24">
+    <path
+      fill="currentColor"
+      d="M21.35,11.1H12.18V13.83H18.69C18.36,17.64 15.19,19.27 12.19,19.27C8.36,19.27 5,16.25 5,12C5,7.9 8.2,4.73 12.2,4.73C15.29,4.73 17.1,6.7 17.1,6.7L19,4.72C19,4.72 16.56,2 12.1,2C6.42,2 2.03,6.8 2.03,12C2.03,17.05 6.16,22 12.25,22C17.6,22 21.5,18.33 21.5,12.91C21.5,11.76 21.35,11.1 21.35,11.1V11.1Z"
+    />
+  </svg>          
+    )
+}
+
+export default Icon

+ 45 - 0
client/src/components/Login/Input.jsx

@@ -0,0 +1,45 @@
+import { Grid, IconButton, InputAdornment, TextField } from "@material-ui/core";
+import Visibility from "@material-ui/icons/Visibility";
+import VisibilityOff from "@material-ui/icons/VisibilityOff";
+
+import React from "react";
+
+const Input = ({
+  name,
+  handleChange,
+  label,
+  autoFocus,
+  type,
+  handleShowPassword,
+  half,
+}) => {
+  return (
+    <Grid item xs={12} sm={half ? 6 : 12}>
+      <TextField
+        name={name}
+        onChange={handleChange}
+        variant="outlined"
+        required
+        fullWidth
+        label={label}
+        autoFocus={autoFocus}
+        type={type}
+        InputProps={
+          name === "password"
+            ? {
+                endAdornment: (
+                  <InputAdornment position="end">
+                    <IconButton onClick={handleShowPassword}>
+                      {type === "password" ? <Visibility /> : <VisibilityOff />}
+                    </IconButton>
+                  </InputAdornment>
+                ),
+              }
+            : null
+        }
+      />
+    </Grid>
+  );
+};
+
+export default Input;

+ 31 - 0
client/src/components/Login/styles.js

@@ -0,0 +1,31 @@
+import { makeStyles } from '@material-ui/core/styles';
+
+export default makeStyles((theme) => ({
+  paper: {
+    marginTop: theme.spacing(8),
+    display: 'flex',
+    flexDirection: 'column',
+    alignItems: 'center',
+    padding: theme.spacing(2),
+  },
+  root: {
+    '& .MuiTextField-root': {
+      margin: theme.spacing(1),
+    },
+  },
+  avatar: {
+    margin: theme.spacing(1),
+    backgroundColor: theme.palette.secondary.main,
+  },
+  form: {
+    width: '100%', // Fix IE 11 issue.
+    marginTop: theme.spacing(3),
+  },
+  submit: {
+    margin: theme.spacing(3, 0, 2),
+  },
+  googleButton: {
+    marginBottom: theme.spacing(2),
+  },
+}));
+

+ 7 - 0
client/src/components/Music/Music.jsx

@@ -0,0 +1,7 @@
+import React from "react";
+
+const Music = (props) => {
+  return <div>Music</div>;
+};
+
+export default Music;

+ 10 - 0
client/src/components/Music/Music.module.css

@@ -0,0 +1,10 @@
+.item {
+  font-family: Cambria, Cochin, Georgia, Times, 'Times New Roman', serif;
+}
+
+.item img {
+  width: 50px;
+  height: 50px;
+  
+  border-radius: 50px;
+}

+ 85 - 0
client/src/components/NavBar/Navbar.jsx

@@ -0,0 +1,85 @@
+import React from "react";
+import { NavLink } from "react-router-dom";
+import classes from "./Navbar.module.css";
+
+const Navbar = () => {
+  return (
+    <nav className={classes.nav}>
+      <ul>
+        <li className={classes.item}>
+          <NavLink to="/profile" activeClassName={classes.active}>
+            Profile
+          </NavLink>
+        </li>
+        <li className={classes.item}>
+          <NavLink to="/dialogs" activeClassName={classes.active}>
+            Messages
+          </NavLink>
+        </li>
+        <li className={classes.item}>
+          <NavLink to="/users" activeClassName={classes.active}>
+            Users
+          </NavLink>
+        </li>
+        <li className={classes.item}>
+          <NavLink to="/gallery" activeClassName={classes.active}>
+            Gallery
+          </NavLink>
+        </li>
+        <li className={classes.item}>
+          <NavLink to="/news" activeClassName={classes.active}>
+            News
+          </NavLink>
+        </li>
+        <li className={classes.item}>
+          <NavLink to="/music" activeClassName={classes.active}>
+            Music
+          </NavLink>
+        </li>
+        <li className={classes.item}>
+          <NavLink to="/settings" activeClassName={classes.active}>
+            Settings
+          </NavLink>
+        </li>
+        <li className={classes.item}>
+          <Clock />
+        </li>
+      </ul>
+    </nav>
+  );
+};
+
+//let classesNew = `${classes.item} ${classes.active}`
+class Clock extends React.Component {
+  constructor(props) {
+    super(props);
+    this.state = { date: new Date() };
+  }
+  tick() {
+    this.setState({
+      date: new Date(),
+    });
+  }
+
+  componentDidMount() {
+    this.timerID = setInterval(() => this.tick(), 1000);
+  }
+
+  componentWillUnmount() {
+    clearInterval(this.timerID);
+  }
+  render() {
+    return (
+      <div>
+        {this.state.date
+          .toLocaleTimeString(navigator.language, {
+            hour: "2-digit",
+            minute: "2-digit",
+          })
+          .replace(/(:\d{2}| [AP]M)$/, "")}
+      </div>
+    );
+  }
+}
+
+export default Navbar;

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 26 - 0
client/src/components/NavBar/Navbar.module.css


+ 37 - 0
client/src/components/News/AllPosts/AllPosts.jsx

@@ -0,0 +1,37 @@
+import React from "react";
+import { Grid } from "@material-ui/core";
+import useStyles from "../../Profile/MyPosts/styles.js";
+import { useSelector } from "react-redux";
+import Post from "../../Profile/MyPosts/Post/Post";
+
+const AllPosts = ({currentId, setCurrentId}) => {
+  const posts = useSelector((state) => state.profilePage.postsData);
+  const uiClasses = useStyles();
+
+
+ // console.log(posts);
+
+  let postsElements = posts.map((post) => (
+    <Grid key={post._id} item xs={12} sm={6}>
+      <Post post={post} setCurrentId={setCurrentId} />
+    </Grid>
+  ));
+
+  return !posts.length ? (
+    'Empty.'
+  ) : (
+    <div>
+      <h1>Posts</h1>
+      <Grid
+        className={uiClasses.container}
+        container
+        alignItems="stretch"
+        spacing={3}
+      >
+        {postsElements}
+      </Grid>
+    </div>
+  );
+};
+
+export default AllPosts;

+ 27 - 0
client/src/components/News/News.jsx

@@ -0,0 +1,27 @@
+import React, { useEffect } from "react";
+import { Container, Grow, Grid } from "@material-ui/core";
+import { useDispatch } from "react-redux";
+import { getPostsThunk } from "../../redux/actions/postsThunks";
+import { useState } from "react";
+import AllPosts from "./AllPosts/AllPosts";
+
+const News = () => {
+  const [currentId, setCurrentId] = useState(null);
+  const dispatch = useDispatch();
+
+  useEffect(() => {
+    dispatch(getPostsThunk());
+  }, [currentId, dispatch]);
+
+  return (
+    <Grow in>
+      <Container>
+        <Grid item xs={12} sm={12}>
+          <AllPosts setCurrentId={setCurrentId} />
+        </Grid>
+      </Container>
+    </Grow>
+  );
+};
+
+export default News;

+ 10 - 0
client/src/components/News/News.module.css

@@ -0,0 +1,10 @@
+.item {
+  font-family: Cambria, Cochin, Georgia, Times, 'Times New Roman', serif;
+}
+
+.item img {
+  width: 50px;
+  height: 50px;
+  
+  border-radius: 50px;
+}

+ 125 - 0
client/src/components/Profile/MyPosts/Form/Form.jsx

@@ -0,0 +1,125 @@
+import React from "react";
+import useStyles from "./styles.js";
+import { Button, Grid, Paper, TextField, Typography } from "@material-ui/core";
+import FileBase from "react-file-base64";
+import { useState, useEffect } from "react";
+import { useDispatch } from "react-redux";
+import {
+  createPostThunk,
+  updatePostThunk,
+} from "../../../../redux/actions/postsThunks.js";
+import { useSelector } from "react-redux";
+
+const PostForm = ({ currentId, setCurrentId }) => {
+  
+  const [postData, setPostData] = useState({
+    title: "",
+    message: "",
+    selectedFile: "",
+  });
+
+
+  const post = useSelector((state) => currentId ? state.profilePage.postsData.find((p) => p._id === currentId) : null)
+  const dispatch = useDispatch();
+  const uiClasses = useStyles();
+  const user = JSON.parse(localStorage.getItem('profile'))
+
+  useEffect(() => {
+    if(post) {
+    
+     setPostData(post)
+    }
+ } ,[post])
+
+
+  const handleSubmit = async (e) => {
+    e.preventDefault();
+    if (currentId === null) {
+      dispatch(createPostThunk({...postData, name: user?.result?.name}));
+      clear()
+    } else {
+      dispatch(updatePostThunk(currentId, {...postData, name: user?.result?.name}));
+      clear()
+    }
+  };
+
+  const clear = () => {
+    
+    setCurrentId(null);
+    setPostData({title: '', message: '', selectedFile: ''})
+  };
+
+  if (!user?.result?.name) {
+    return (
+      <Paper className={uiClasses.paper}>
+        <Typography variant="h6" align="center">
+          Please Sign In to create your own posts and like other's posts.
+        </Typography>
+      </Paper>
+    );
+  }
+
+  return (
+    <Grid className={uiClasses.paper}>
+      <form
+        autoComplete="off"
+        noValidate
+        className={`${uiClasses.root} ${uiClasses.form} `}
+        onSubmit={handleSubmit}
+      >
+        <Typography variant="h6">{currentId ? 'Editing' : 'Create'} post</Typography>
+        <TextField
+          name="title"
+          variant="outlined"
+          label="Title"
+          fullWidth
+          value={postData.title}
+          onChange={(e) => setPostData({ ...postData, title: e.target.value })}
+        />
+        <TextField
+          name="message"
+          variant="outlined"
+          multiline
+          rows={4}
+          label="Message"
+          fullWidth
+          value={postData.message}
+          onChange={(e) =>
+            setPostData({ ...postData, message: e.target.value })
+          }
+        />
+       
+        <div className={uiClasses.fileInput}>
+          <FileBase
+            type="file"
+            multiple={false}
+            onDone={({ base64 }) =>
+              setPostData({ ...postData, selectedFile: base64 })
+            }
+          ></FileBase>
+        </div>
+        <Button
+          className={uiClasses.buttonSubmit}
+          variant="container"
+          color="primary"
+          size="large"
+          type="submit"
+          fullWidth
+        >
+          Add post
+        </Button>
+        <Button
+          variant="contained"
+          color="secondary"
+          size="small"
+          onClick={clear}
+          fullWidth
+        >
+          Clear
+        </Button>
+      </form>
+    </Grid>
+  );
+};
+
+export default PostForm;

+ 25 - 0
client/src/components/Profile/MyPosts/Form/styles.js

@@ -0,0 +1,25 @@
+import { makeStyles } from "@material-ui/core";
+
+export default makeStyles((theme) => ({
+  root: {
+    "& .MuiTextField-root": {
+      margin: theme.spacing(1),
+    },
+  },
+  paper: {
+    
+    padding: theme.spacing(1),
+  },
+  form: {
+    display: "flex",
+    flexWrap: "wrap",
+    justifyContent: "center",
+  },
+  fileInput: {
+    width: "97%",
+    margin: "10px 0",
+  },
+  buttonSubmit: {
+    marginBottom: 10,
+  },
+}));

+ 41 - 0
client/src/components/Profile/MyPosts/MyPosts.jsx

@@ -0,0 +1,41 @@
+import React, { useEffect } from "react";
+import Post from "./Post/Post";
+import { Grid } from "@material-ui/core";
+import useStyles from "./styles.js";
+import { useSelector } from "react-redux";
+
+
+const MyPosts = ({ currentId, setCurrentId }) => {
+  const posts = useSelector((state) => state.profilePage.postsData);
+  const uiClasses = useStyles();
+  const user = JSON.parse(localStorage.getItem("profile"));
+  
+
+  let postsElements = posts.map(
+    (post) =>
+      (user?.result?.googleId === post?.creator ||
+        user?.result?._id === post?.creator) && (
+        <Grid key={post._id} item xs={12} sm={6}>
+          <Post post={post} setCurrentId={setCurrentId} />
+        </Grid>
+      )
+  );
+
+  return !posts.length ? (
+    "Empty."
+  ) : (
+    <div>
+      <h1>{user?.result?.name && "My posts"}</h1>
+      <Grid
+        className={uiClasses.container}
+        container
+        alignItems="stretch"
+        spacing={3}
+      >
+        {postsElements}
+      </Grid>
+    </div>
+  );
+};
+
+export default MyPosts;

+ 11 - 0
client/src/components/Profile/MyPosts/MyPosts.module.css

@@ -0,0 +1,11 @@
+.item {
+  font-family: Cambria, Cochin, Georgia, Times, 'Times New Roman', serif;
+}
+
+.item img {
+  width: 50px;
+}
+
+.posts {
+  margin-top: 10px;
+}

+ 132 - 0
client/src/components/Profile/MyPosts/Post/Post.jsx

@@ -0,0 +1,132 @@
+import React, { useEffect } from "react";
+import useStyles from "./styles.js";
+import {
+  Card,
+  CardActions,
+  CardContent,
+  CardMedia,
+  Button,
+  Typography,
+} from "@material-ui/core";
+import ThumbUpAltIcon from "@material-ui/icons/ThumbUpAlt";
+import DeleteIcon from "@material-ui/icons/Delete";
+import MoreHorizIcon from "@material-ui/icons/MoreHoriz";
+import ThumbUpAltOutlined from "@material-ui/icons/ThumbUpAltOutlined";
+import moment from "moment";
+import { useDispatch } from "react-redux";
+import {
+  deletePostThunk,
+  likePostThunk,
+} from "../../../../redux/actions/postsThunks";
+
+const Post = ({ post, setCurrentId }) => {
+  const uiClasses = useStyles();
+  const dispatch = useDispatch();
+  const user = JSON.parse(localStorage.getItem("profile"));
+
+ 
+
+  const Likes = () => {
+    if (post.likes.length > 0) {
+      return post.likes.find(
+        (like) => like === (user?.result?.googleId || user?.result?._id)
+      ) ? (
+        <>
+          <ThumbUpAltIcon fontSize="small" />
+          &nbsp;
+          {post.likes.length > 2
+            ? `You and ${post.likes.length - 1} others`
+            : `${post.likes.length} like${post.likes.length > 1 ? "s" : ""}`}
+        </>
+      ) : (
+        <>
+          <ThumbUpAltOutlined fontSize="small" />
+          &nbsp;{post.likes.length} {post.likes.length === 1 ? "Like" : "Likes"}
+        </>
+      );
+    }
+
+    return (
+      <>
+        <ThumbUpAltOutlined fontSize="small" />
+        &nbsp;Like
+      </>
+    );
+  };
+
+  return (
+    <Card className={uiClasses.card}>
+      <CardMedia
+        className={uiClasses.media}
+        image={post.selectedFile}
+        image={
+          post.selectedFile ||
+          "https://user-images.githubusercontent.com/194400/49531010-48dad180-f8b1-11e8-8d89-1e61320e1d82.png"
+        }
+        title={post.title}
+      />
+      <div className={uiClasses.overlay}>
+        <Typography variant="h6">{post.name}</Typography>
+        <Typography variant="body2">
+          {moment(post.createdAt).fromNow()}
+        </Typography>
+      </div>
+      {(user?.result?.googleId === post?.creator ||
+        user?.result?._id === post?.creator) && (
+        <div className={uiClasses.overlay2}>
+          <Button
+            style={{ color: "white" }}
+            size="small"
+            onClick={() => {
+              setCurrentId(post._id);
+            }}
+          >
+            <MoreHorizIcon fontSize="default"></MoreHorizIcon>
+          </Button>
+        </div>
+      )}
+
+      <Typography
+        className={uiClasses.title}
+        gutterBottom
+        variant="h5"
+        component="h2"
+      >
+        {post.title}
+      </Typography>
+      <CardContent>
+        <Typography variant="body2" color="textSecondary" component="p">
+          {post.message}
+        </Typography>
+      </CardContent>
+      <CardActions className={uiClasses.cardActions}>
+        <Button
+          size="small"
+          color="primary"
+          disabled={!user?.result}
+          onClick={() => {
+            dispatch(likePostThunk(post._id));
+          }}
+        >
+          <Likes />
+        </Button>
+        {(user?.result?.googleId === post?.creator ||
+          user?.result?._id === post?.creator) && (
+          <Button
+            size="small"
+            color="primary"
+            onClick={() => {
+              dispatch(deletePostThunk(post._id));
+            }}
+          >
+            <DeleteIcon fontSize="small" /> Delete
+          </Button>
+        )}
+      </CardActions>
+    </Card>
+  );
+
+  
+};
+
+export default Post;

+ 10 - 0
client/src/components/Profile/MyPosts/Post/Post.module.css

@@ -0,0 +1,10 @@
+.item {
+  font-family: Cambria, Cochin, Georgia, Times, 'Times New Roman', serif;
+}
+
+.item img {
+  width: 50px;
+  height: 50px;
+  
+  border-radius: 50px;
+}

+ 52 - 0
client/src/components/Profile/MyPosts/Post/styles.js

@@ -0,0 +1,52 @@
+import { makeStyles } from '@material-ui/core/styles';
+
+export default makeStyles({
+  media: {
+    height: 0,
+    paddingTop: '56.25%',
+    backgroundColor: 'rgba(0, 0, 0, 0.5)',
+    backgroundBlendMode: 'darken',
+  },
+  border: {
+    border: 'solid',
+  },
+  fullHeightCard: {
+    height: '100%',
+  },
+  card: {
+    display: 'flex',
+    flexDirection: 'column',
+    justifyContent: 'space-between',
+    borderRadius: '15px',
+    height: '100%',
+    position: 'relative',
+  },
+  overlay: {
+    position: 'absolute',
+    top: '20px',
+    left: '20px',
+    color: 'white',
+  },
+  overlay2: {
+    position: 'absolute',
+    top: '20px',
+    right: '20px',
+    color: 'white',
+  },
+  grid: {
+    display: 'flex',
+  },
+  details: {
+    display: 'flex',
+    justifyContent: 'space-between',
+    margin: '20px',
+  },
+  title: {
+    padding: '0 16px',
+  },
+  cardActions: {
+    padding: '0 16px 8px 16px',
+    display: 'flex',
+    justifyContent: 'space-between',
+  },
+});

+ 18 - 0
client/src/components/Profile/MyPosts/styles.js

@@ -0,0 +1,18 @@
+import { makeStyles } from '@material-ui/core/styles';
+
+export default makeStyles((theme) => ({
+  mainContainer: {
+    display: 'flex',
+    alignItems: 'center',
+  },
+  smMargin: {
+    margin: theme.spacing(1),
+  },
+  actionDiv: {
+    textAlign: 'center',
+  },
+userProfileImg: {
+  width: "250px",
+},
+ 
+}));

+ 113 - 0
client/src/components/Profile/Profile.jsx

@@ -0,0 +1,113 @@
+import React, { useEffect } from "react";
+import { Container, Grow, Grid } from "@material-ui/core";
+import { useDispatch } from "react-redux";
+import PostForm from "./MyPosts/Form/Form";
+import { getPostsThunk } from "../../redux/actions/postsThunks";
+import { useState } from "react";
+import MyPosts from "./MyPosts/MyPosts";
+import userPhoto from "../../assets/images/default_userAvatar.png";
+import useStyles from "./MyPosts/styles.js";
+import { getProfileThunk, updateProfileThunk } from "../../redux/actions/profileThunks";
+import { updateProfile } from "../api/Api";
+import ProfileDataForm from "./ProfileDataForm/ProfileDataForm";
+
+
+const Profile = ({ profile, getProfile }) => {
+  const [currentId, setCurrentId] = useState(null);
+  let [editMode, setEditMode] = useState(false);
+
+  const dispatch = useDispatch();
+  const user = JSON.parse(localStorage.getItem("profile"));
+  const uiClasses = useStyles();
+
+  useEffect(() => {
+    dispatch(getProfileThunk());
+    dispatch(getPostsThunk());
+  }, [currentId]);
+
+  const onSubmit = (formData) => {
+    debugger
+    dispatch(updateProfileThunk(formData))
+      .then(() => {
+        setEditMode(false);
+      })
+      .catch();
+  };
+
+  return (
+    <div>
+      <Grow in>
+        <Container>
+  {profile && (<div>
+        {editMode ? (
+          <ProfileDataForm
+            initialValues={profile}
+            onSubmit={onSubmit}
+            profile={profile}
+          />
+        ) : (
+          <div>
+            <img
+              className={uiClasses.userProfileImg}
+              src={!user?.result.avatar && userPhoto}
+            />
+            <div>
+              <button onClick={() => setEditMode(true)}>edit</button>
+            </div>
+            <div> name: {profile.user.name}</div>
+            <div>bio: {profile.bio} </div>
+            <div>
+              contacts:{" "}
+              {Object.keys(profile.contacts).map((key) => {
+                return (
+                  <div>
+                    <b>{key}:</b>
+                    {profile.contacts[key]}
+                  </div>
+                );
+              })}
+            </div>
+          </div>
+        )}
+
+        </div>)}
+
+
+          {/* {profile && (
+            <div>
+              <img
+                className={uiClasses.userProfileImg}
+                src={!user?.result.avatar && userPhoto}
+              />
+              <div>
+                <button onClick={() => setEditMode(true)}>edit</button>
+              </div>
+              <div> name: {profile.user.name}</div>
+              <div>bio: {profile.bio} </div>
+              <div>
+                contacts:{" "}
+                {Object.keys(profile.contacts).map((key) => {
+                  return (
+                    <div>
+                      <b>{key}:</b>
+                      {profile.contacts[key]}
+                    </div>
+                  );
+                })}
+              </div>
+            </div>
+          )}  */}
+
+          <Grid item xs={12} sm={12}>
+            <PostForm currentId={currentId} setCurrentId={setCurrentId} />
+          </Grid>
+          <Grid item xs={12} sm={12}>
+            <MyPosts setCurrentId={setCurrentId} />
+          </Grid>
+        </Container>
+      </Grow>
+    </div>
+  );
+};
+
+export default Profile;

+ 21 - 0
client/src/components/Profile/ProfileContainer.jsx

@@ -0,0 +1,21 @@
+import { getProfileThunk } from "../../redux/actions/profileThunks";
+import Profile from "./Profile";
+import { connect } from "react-redux";
+
+let mapStateToProps = (state) => {
+    return {
+        profile: state.profilePage.profile
+    }
+}
+
+let mapDispatchToProps = (dispatch) => {
+    return {
+        getProfile: () => {
+            dispatch(getProfileThunk())
+        }
+    }
+}
+
+const ProfileContainer = connect(mapStateToProps, mapDispatchToProps)(Profile)
+
+export default ProfileContainer

+ 42 - 0
client/src/components/Profile/ProfileDataForm/ProfileDataForm.jsx

@@ -0,0 +1,42 @@
+import React from "react";
+import { reduxForm } from "redux-form";
+import classes from "./ProfileInfo.module.css";
+
+import {
+  createField,
+  Input,
+} from "../../common/FormControls/FormsControls";
+
+const ProfileDataForm = ({ handleSubmit,profile,error }) => {
+  return (
+    <form onSubmit={handleSubmit}>
+      <div>
+        <button>save</button>
+        {error && <div className={classes.formSummaryError}>{error}</div>}
+      </div>
+      <div>
+        <b>bio:</b> {createField("bio", "bio", [], Input)}
+      </div>
+      <div>
+        <b>age: </b>
+        {createField("age", "age", [], Input)}
+      </div>
+      <div>
+        <b>Contacts:</b>{" "}
+        {Object.keys(profile.contacts).map((key) => {
+          return (
+            <div key={key} className={classes.contact}>
+                <b>{key}: </b>{createField(key, "contacts."+ key, [], Input)}
+            </div>
+          );
+        })}
+      </div>
+    </form>
+  );
+};
+
+const ProfileDataReduxForm = reduxForm({ form: "edit-profile" })(
+  ProfileDataForm
+);
+
+export default ProfileDataReduxForm;

+ 12 - 0
client/src/components/Profile/ProfileDataForm/ProfileInfo.module.css

@@ -0,0 +1,12 @@
+.userProfileImg {
+    width: 250px;
+}
+
+.contact {
+    padding-left: 10px;
+}
+
+.formSummaryError {
+    display: inline;
+    color: red;
+}

+ 7 - 0
client/src/components/Settings/Settings.jsx

@@ -0,0 +1,7 @@
+import React from "react";
+
+const Settings = (props) => {
+  return <div>Settings</div>;
+};
+
+export default Settings;

+ 10 - 0
client/src/components/Settings/Settings.module.css

@@ -0,0 +1,10 @@
+.item {
+  font-family: Cambria, Cochin, Georgia, Times, 'Times New Roman', serif;
+}
+
+.item img {
+  width: 50px;
+  height: 50px;
+  
+  border-radius: 50px;
+}

+ 28 - 0
client/src/components/Users/User.jsx

@@ -0,0 +1,28 @@
+import React from "react";
+import classes from "./Users.module.css";
+import userPhoto from "../../assets/images/default_userAvatar.png";
+
+let User = ({ user }) => {
+  console.log(user.user.name)
+  return (
+    <div className={classes.usersWrapper}>
+      <div>
+        <img
+          src={user.avatar ? user.avatar : userPhoto}
+          alt=""
+          className={classes.usersPhoto}
+        />
+      </div>
+      <div className={classes.infoHolder}>
+        <div>
+          <div>Name: {user.user.name}</div>
+          <div>Bio: {user.user.bio}</div>
+        </div>
+        <div>
+          <div>contacts: {"Object.keys(profile.contacts).map"}</div>
+        </div>
+      </div>
+    </div>
+  );
+};
+export default User;

+ 39 - 0
client/src/components/Users/Users.jsx

@@ -0,0 +1,39 @@
+import React, { useEffect } from "react";
+import User from "./User";
+import { useSelector } from "react-redux";
+import { Grid } from "@material-ui/core";
+import {useDispatch} from 'react-redux'
+import { getUsersThunk } from "../../redux/actions/UsersThunk";
+
+let Users = () => {
+  const dispatch = useDispatch()
+
+  useEffect(() => {
+    dispatch(getUsersThunk())
+  } , [dispatch])
+  const users = useSelector((state) => state.usersPage.usersData);
+
+
+  console.log(users);
+
+  let usersElements = users.map((user) => (
+    <Grid key={user._id} item xs={12} sm={12}>
+      <User user={user} />
+    </Grid>
+  ));
+
+  return (
+    <div>
+      {/* <Pagination
+        currentPage={currentPage}
+        totalItemsCount={totalUsersCount}
+        pageSize={pageSize}
+        onPageChanged={onPageChanged}
+      /> */}
+      <div>
+        {usersElements}
+      </div>
+    </div>
+  );
+};
+export default Users;

+ 35 - 0
client/src/components/Users/Users.module.css

@@ -0,0 +1,35 @@
+.usersPhoto {
+  
+  border-radius: 50%;
+  width: 80px;
+  height: 80px;
+
+}
+
+.usersWrapper {
+  
+  padding: 20px;
+  display: flex;
+  border: 2px solid orange;
+  border-radius: 8px;
+  margin-bottom: 8px;
+}
+
+.usersWrapper:hover {
+  border: 4px solid orange;
+}
+
+.usersButtonsHolder {
+  text-align: center;
+}
+
+.infoHolder {
+  margin-left: 10px;
+}
+
+.pagesHolder {
+  width: 200px;
+  overflow: hidden;
+}
+
+

+ 38 - 0
client/src/components/api/Api.js

@@ -0,0 +1,38 @@
+import axios from "axios";
+
+const instance = axios.create({
+  baseURL: "http://localhost:5000",
+});
+
+instance.interceptors.request.use((req) => {
+  if (localStorage.getItem("profile")) {
+    req.headers.Authorization = `Bearer ${
+      JSON.parse(localStorage.getItem("profile")).token
+    }`;
+  }
+
+  return req;
+});
+export const getProfile = () => instance.get("/profile");
+export const updateProfile = () => instance.patch("/profile");
+
+export const getPosts = () => instance.get("/posts");
+export const createPost = (newPost) => instance.post("/posts", newPost);
+export const updatePost = (id, updatedPost) =>{ 
+  
+  instance.patch(`/posts/${id}`, updatedPost)
+}
+export const deletePost = (id) => instance.delete(`/posts/${id}`);
+export const likePost = (id) => instance.patch(`/posts/${id}/likePost`);
+
+export const signin = (formData) => {
+  return instance.post("/auth/signin", formData);
+};
+export const signup = (formData) => {
+  return instance.post("/auth/signup", formData);
+};
+
+export const getUsers = () => instance.get("/users");
+
+
+

+ 103 - 0
client/src/components/api/OldApi.js

@@ -0,0 +1,103 @@
+import * as axios from "axios";
+
+const instance = axios.create({
+  withCredentials: true,
+  baseURL: "https://social-network.samuraijs.com/api/1.0/",
+  headers: { "API-KEY": "f1a176e4-de89-4bde-b937-0379a56cdd5c" },
+});
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+export const usersAPI = {
+  getUsers(currentPage = 1, pageSize = 5) {
+    return instance
+      .get(`users?page=${currentPage}&count=${pageSize}`)
+      .then((response) => {
+        return response.data;
+      });
+  },
+  follow(userId) {
+    return instance.post(`follow/${userId}`).then((response) => {
+      return response.data;
+    });
+  },
+
+  unfollow(userId) {
+    return instance.delete(`follow/${userId}`).then((response) => {
+      return response.data;
+    });
+  },
+};
+
+export const authAPI = {
+  login(email, password, rememberMe = false, captcha = null) {
+    return instance.post(`auth/login`, {
+      email,
+      password,
+      rememberMe,
+      captcha,
+    });
+  },
+  logout() {
+    return instance.delete(`auth/login`);
+  },
+  me() {
+    return instance.get(`auth/me`);
+  },
+};
+
+export const securityAPI = {
+  getCaptchaUrl() {
+    return instance.get(`security/get-captcha-url`);
+  },
+};
+
+export const profileAPI = {
+  getUserProfile(userId) {
+    return instance.get(`profile/${userId}`).then((response) => {
+      return response.data;
+    });
+  },
+  getUserStatus(userId) {
+    return instance.get(`profile/status/` + userId);
+  },
+  updateUserStatus(status) {
+    return instance.put(`profile/status`, { status });
+  },
+  savePhoto(photoFile) {
+    const formData = new FormData();
+    formData.append("image", photoFile);
+    return instance.put(`profile/photo`, formData, {
+      headers: {
+        "Content-Type": "multipart/form-data",
+      },
+    });
+  },
+  saveProfile(profile) {
+    return instance.put(`profile`, profile);
+  },
+};

+ 17 - 0
client/src/components/common/FormControls/FormControls.module.css

@@ -0,0 +1,17 @@
+.form-control {
+
+}
+
+.formControl.error input,
+.formControl.error textarea{
+    border: 1px red solid
+}
+
+.formControl.error span{
+    color:red
+}
+
+.formSummaryError {
+    display: inline;
+    color: red;
+}

+ 70 - 0
client/src/components/common/FormControls/FormsControls.js

@@ -0,0 +1,70 @@
+import { TextField } from "@material-ui/core";
+import React from "react";
+import { Field } from "redux-form";
+import classes from "./FormControls.module.css";
+
+export const FormControl = ({ input, meta: { touched, error }, children }) => {
+  const hasError = touched && error;
+
+  return (
+    <div
+      className={classes.formControl + " " + (hasError ? classes.error : "")}
+    >
+      <div>{children}</div>
+      {hasError && <span>{error}</span>}
+    </div>
+  );
+};
+
+export const Textarea = (props) => {
+  const { input, meta, child, ...restProps } = props;
+  return (
+    <div>
+      <FormControl {...props}>
+        <textarea {...input} {...restProps} />
+      </FormControl>
+    </div>
+  );
+};
+
+export const TextArea = (props) => {
+  const { input, meta, child, ...restProps } = props;
+  return (
+    <div>
+      <FormControl {...props}>
+        <TextField variant="outlined"  {...input} {...restProps} />
+      </FormControl>
+    </div>
+  );
+};
+
+export const Input = (props) => {
+  const { input, meta, child, ...restProps } = props;
+  return (
+    <div>
+      <FormControl {...props}>
+        <input {...input} {...restProps} />
+      </FormControl>
+    </div>
+  );
+};
+
+export const createField = (
+  placeholder,
+  name,
+  validators,
+  component,
+  props = {},
+  text = ""
+) => (
+  <div>
+    <Field
+      placeholder={placeholder}
+      name={name}
+      validate={validators}
+      component={component}
+      {...props}
+    />
+    {text}
+  </div>
+);

+ 68 - 0
client/src/components/common/Pagination/Pagination.jsx

@@ -0,0 +1,68 @@
+import React, { useState } from "react";
+import classes from "./Pagination.module.css";
+import cn from "classnames";
+
+const Pagination = ({
+  currentPage,
+  totalItemsCount,
+  pageSize,
+  onPageChanged,
+  portionSize = 10,
+}) => {
+  let pagesCount = Math.ceil(totalItemsCount / pageSize);
+
+  let pages = [];
+  for (let i = 1; i <= pagesCount; i++) {
+    pages.push(i);
+  }
+
+  let portionCount = Math.ceil(pagesCount / portionSize);
+  let [portionNumber, setPortionNumber] = useState(1);
+  let leftPortionPageNumber = (portionNumber - 1) * portionSize + 1;
+  let rightPortionPageNumber = portionNumber * portionSize;
+
+  return (
+    <div className={cn(classes.pagination)}>
+      {portionNumber > 1 && (
+        <button
+         className={cn(classes.setButton)}
+          onClick={() => {
+            setPortionNumber(portionNumber - 1);
+          }}
+        >
+          ◀
+        </button>
+      )}
+      {pages
+        .filter(
+          (p) => p >= leftPortionPageNumber && p <= rightPortionPageNumber
+        )
+        .map((p) => {
+          return (
+            <span
+              className={cn(
+                { [classes.selectedPage]: currentPage === p }, //класс добавляется только в случае совпадения со страницей
+                classes.pageNumber
+              )}
+              key={p}
+              onClick={() => {
+                onPageChanged(p);
+              }}
+            >
+              {p}
+            </span>
+          );
+        })}
+      {portionCount > portionNumber && (
+        <button className={cn(classes.setButton)}
+          onClick={() => {
+            setPortionNumber(portionNumber + 1);
+          }}
+        >
+          ▶
+        </button>
+      )}
+    </div>
+  );
+};
+export default Pagination;

+ 24 - 0
client/src/components/common/Pagination/Pagination.module.css

@@ -0,0 +1,24 @@
+.selectedPage {
+  font-weight: bold;
+  
+ }
+
+ .pagination {
+   margin: 10px;
+   text-align:center
+  }
+
+  .pageNumber {
+    padding:3px;
+    border: 1px solid grey;
+  }
+
+  .pageNumber.selectedPage {
+    font-weight: bold;
+    border-color: black;
+  }
+
+  .setButton {
+    border-radius: 30px;
+    
+  }

+ 12 - 0
client/src/components/common/preLoader/preLoader.js

@@ -0,0 +1,12 @@
+import React from "react";
+import preloader from "../../../assets/images/preloader.svg";
+
+let PreLoader = () => {
+  return (
+    <div>
+      <img alt="" src={preloader}  />
+    </div>
+  );
+};
+
+export default PreLoader;

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 17 - 0
client/src/index.css


+ 21 - 0
client/src/index.js

@@ -0,0 +1,21 @@
+import reportWebVitals from "./reportWebVitals";
+import React from "react";
+import ReactDOM from "react-dom";
+import "./index.css";
+import App from "./App";
+import { Provider } from "react-redux";
+import store from "./redux/reduxStore";
+import { BrowserRouter } from "react-router-dom";
+
+ReactDOM.render(
+  <React.StrictMode>
+    <BrowserRouter>
+      <Provider store={store}>
+        <App />
+      </Provider>
+    </BrowserRouter>
+  </React.StrictMode>,
+  document.getElementById("root")
+);
+
+reportWebVitals();

+ 11 - 0
client/src/redux/actions/UsersThunk.js

@@ -0,0 +1,11 @@
+import * as api from "../../components/api/Api.js";
+import { GET_USERS } from "../reducers/usersReducer";
+
+export const getUsersThunk = () => async (dispatch) => {
+  try {
+    const { data } = await api.getUsers();
+    dispatch({ type: GET_USERS, payload: data });
+  } catch (error) {
+      console.log("some error" + console.error)
+  }
+};

+ 26 - 0
client/src/redux/actions/authThunks.js

@@ -0,0 +1,26 @@
+import * as api from "../../components/api/Api.js";
+import { AUTH } from "../reducers/authReducer";
+
+export const signIn = (formData, history) => async (dispatch) => {
+  try {
+    const { data } = await api.signin(formData);
+
+    dispatch({ type: AUTH, data });
+
+    history.push("/");
+  } catch (error) {
+    console.log(error);
+  }
+};
+
+export const signUp = (formData, history) => async (dispatch) => {
+  try {
+    const { data } = await api.signup(formData);
+    
+    dispatch({ type: AUTH, data });
+
+    history.push("/");
+  } catch (error) {
+    console.log(error);
+  }
+};

+ 56 - 0
client/src/redux/actions/postsThunks.js

@@ -0,0 +1,56 @@
+import * as api from "../../components/api/Api.js";
+
+import {
+  CREATE_POST,
+  GET_POSTS,
+  UPDATE_POST,
+  DELETE_POST,
+  LIKE_POST,
+} from "../reducers/profileReducer";
+
+export const getPostsThunk = () => async (dispatch) => {
+  try {
+    
+    const { data } = await api.getPosts();
+    dispatch({ type: GET_POSTS, payload: data });
+  } catch (error) {
+    console.log("some error" + error);
+  }
+};
+
+export const createPostThunk = (post) => async (dispatch) => {
+  try {
+    debugger
+    const { data } = await api.createPost(post);
+    dispatch({ type: CREATE_POST, payload: data });
+  } catch (error) {
+    console.log(error);
+  }
+};
+
+export const updatePostThunk = (id, post) => async (dispatch) => {
+  try {
+    const { data } = await api.updatePost(id, post);
+    dispatch({ type: UPDATE_POST, payload: data });
+  } catch (error) {
+    console.log(error);
+  }
+};
+
+export const deletePostThunk = (id) => async (dispatch) => {
+  try {
+    await api.deletePost(id);
+    dispatch({ type: DELETE_POST, payload: id });
+  } catch (error) {
+    console.log(error);
+  }
+};
+
+export const likePostThunk = (id) => async (dispatch) => {
+  try {
+    const { data } = await api.likePost(id);
+    dispatch({ type: LIKE_POST, payload: data });
+  } catch (error) {
+    console.log(error);
+  }
+};

+ 23 - 0
client/src/redux/actions/profileThunks.js

@@ -0,0 +1,23 @@
+import * as api from "../../components/api/Api.js";
+import { GET_PROFILE } from "../reducers/profileReducer";
+import { UPDATE_POST } from "../reducers/profileReducer";
+
+
+export const getProfileThunk = () => async (dispatch) => {
+  try {
+    const { data } = await api.getProfile();
+    dispatch({ type: GET_PROFILE, payload: data });
+  } catch (error) {
+      console.log("some error" + console.error)
+  }
+};
+
+export const updateProfileThunk = (profile) => async (dispatch) => {
+  try {
+    debugger
+    const { data } = await api.updateProfile(profile);
+    dispatch({ type: UPDATE_POST, payload: data });
+  } catch (error) {
+    console.log(error);
+  }
+};

+ 35 - 0
client/src/redux/reducers/authReducer.js

@@ -0,0 +1,35 @@
+
+export const AUTH = "AUTH";
+export const LOGOUT = "LOGOUT";
+
+
+let initialState = {
+  authData: null,
+  userId: null,
+  email: null,
+  login: null,
+  isLogin: false,
+  captchaUrl: null, // if null then captcha isnt required
+};
+
+const authReducer = (state = initialState, action) => {
+  switch (action.type) {
+    case AUTH:
+      localStorage.setItem("profile", JSON.stringify({ ...action?.data }));
+      return {
+        ...state,
+        authData: action?.data,
+      };
+    case LOGOUT:
+      localStorage.clear();
+      return {
+        ...state,
+        authData: null,
+      };
+    default:
+      return state;
+  }
+};
+
+
+export default authReducer;

+ 36 - 0
client/src/redux/reducers/dialogsReducer.js

@@ -0,0 +1,36 @@
+const SEND_MESSAGE = "SEND-MESSAGE";
+
+let initialState = {
+  dialogsData: [
+    { id: 1, name: "John" },
+    { id: 2, name: "Alan" },
+    { id: 3, name: "Alex" },
+    { id: 4, name: "Sofia" },
+  ],
+  messagesData: [
+    { id: 1, message: "hi" },
+    { id: 2, message: "hello" },
+    { id: 3, message: "how are you" },
+  ],
+ 
+};
+
+
+const dialogsReducer = (state = initialState, action) => {
+  switch (action.type) 
+  {
+    case SEND_MESSAGE: {
+      let body = action.newMessageBody;
+      return {
+        ...state,
+        
+        messagesData: [...state.messagesData, { id: 6, message: body }],
+      };
+    }
+    default:
+      return state;
+  }
+};
+
+
+export default dialogsReducer;

+ 55 - 0
client/src/redux/reducers/profileReducer.js

@@ -0,0 +1,55 @@
+export const GET_POSTS = "GET_POSTS";
+export const CREATE_POST = "CREATE_POST";
+export const UPDATE_POST = "UPDATE_POST";
+export const DELETE_POST = "DELETE_POST";
+export const LIKE_POST = "LIKE_POST";
+
+export const GET_PROFILE = "GET_PROFILE";
+export const UPDATE_PROFILE = "UPDATE_PROFILE";
+
+
+let initialState = {
+  postsData: [],
+  profiles: [],
+  profile: null,
+  status: "",
+}; 
+
+const profileReducer = (state = initialState, action) => {
+  switch (action.type) {
+
+    
+    case GET_PROFILE:
+      case UPDATE_PROFILE:
+      return {
+        ...state,
+        profile: action.payload,
+      };
+    case CREATE_POST:
+      return {
+        ...state,
+        postsData: [...state.postsData, action.payload],
+      };
+    case GET_POSTS:
+      return {
+        ...state,
+        postsData: action.payload
+      }
+      case UPDATE_POST:
+      case LIKE_POST  :
+        return {
+        ...state,
+        postsData: state.postsData.map((post) => post._id === action.payload._id ? action.payload : post)
+      }
+      case DELETE_POST  :
+        return {
+        ...state,
+        postsData: state.postsData.filter((post) => post._id !== action.payload)
+      }
+    default:
+      return state;
+  }
+};
+
+
+export default profileReducer;

+ 26 - 0
client/src/redux/reducers/usersReducer.js

@@ -0,0 +1,26 @@
+export const  GET_USERS = "GET_USERS";
+
+let initialState = {
+  usersData: [],
+  pageSize: 10,
+  totalUsersCount: 0,
+  currentPage: 1,
+  isFetching: true,
+  followingInProgress: [],
+};
+
+const usersReducer = (state = initialState, action) => {
+  switch (action.type) {
+    case GET_USERS:
+      return {
+        ...state,
+        usersData: action.payload,
+      };
+    default:
+      return state;
+  }
+};
+
+
+
+export default usersReducer;

+ 26 - 0
client/src/redux/reduxStore.js

@@ -0,0 +1,26 @@
+import { applyMiddleware, combineReducers, compose, createStore } from "redux";
+import profileReducer from "./reducers/profileReducer";
+import dialogsReducer from "./reducers/dialogsReducer";
+import usersReducer from "./reducers/usersReducer";
+import authReducer from "./reducers/authReducer";
+import thunkMiddleware from "redux-thunk";
+import {reducer as formReducer} from 'redux-form'
+
+let reducers = combineReducers({
+  profilePage: profileReducer,
+  dialogsPage: dialogsReducer,
+  usersPage: usersReducer,
+  auth: authReducer,
+  form: formReducer,
+});
+
+ const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
+ const store = createStore(reducers, /* preloadedState, */ composeEnhancers(
+    applyMiddleware(thunkMiddleware)
+  ));
+
+//let store = createStore(reducers, applyMiddleware(thunkMiddleware));
+
+window.store = store;
+
+export default store;

+ 13 - 0
client/src/reportWebVitals.js

@@ -0,0 +1,13 @@
+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;

+ 1 - 0
server

@@ -0,0 +1 @@
+Subproject commit 7035d7b7c5d61d451a5de70ec3a2c0177121d16b