DeJaVu 2 years ago
parent
commit
4c80052fa5

+ 23 - 0
.gitignore

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

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


+ 45 - 0
package.json

@@ -0,0 +1,45 @@
+{
+  "name": "olx",
+  "version": "0.1.0",
+  "private": true,
+  "dependencies": {
+    "@testing-library/jest-dom": "^5.14.1",
+    "@testing-library/react": "^11.2.7",
+    "@testing-library/user-event": "^12.8.3",
+    "bootstrap": "^5.1.0",
+    "node-sass": "^6.0.1",
+    "react": "^17.0.2",
+    "react-bootstrap": "^2.0.0-beta.6",
+    "react-dom": "^17.0.2",
+    "react-redux": "^7.2.5",
+    "react-router-dom": "^5.3.0",
+    "react-scripts": "4.0.3",
+    "redux": "^4.1.1",
+    "redux-thunk": "^2.3.0",
+    "web-vitals": "^1.1.2"
+  },
+  "scripts": {
+    "start": "react-scripts start",
+    "build": "react-scripts build",
+    "test": "react-scripts test",
+    "eject": "react-scripts eject"
+  },
+  "eslintConfig": {
+    "extends": [
+      "react-app",
+      "react-app/jest"
+    ]
+  },
+  "browserslist": {
+    "production": [
+      ">0.2%",
+      "not dead",
+      "not op_mini all"
+    ],
+    "development": [
+      "last 1 chrome version",
+      "last 1 firefox version",
+      "last 1 safari version"
+    ]
+  }
+}

BIN
public/favicon.ico


+ 43 - 0
public/index.html

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

BIN
public/logo192.png


BIN
public/logo512.png


+ 25 - 0
public/manifest.json

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

+ 3 - 0
public/robots.txt

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

+ 0 - 0
src/App.css


+ 197 - 0
src/App.js

@@ -0,0 +1,197 @@
+import React from 'react';
+import './App.scss';
+import createHistory from "history/createBrowserHistory";
+import {Provider, connect}   from 'react-redux';
+import thunk from 'redux-thunk';
+import {createStore, combineReducers, applyMiddleware} from 'redux';
+import Navibar from './Components/NaviBar';
+import Footer from './Components/Footer';
+
+import store from './reducers';
+import ConnectLog from './pages/Login';
+
+import {
+  BrowserRouter as Router,
+  Switch,
+  Route, 
+  Link
+} from "react-router-dom";
+
+import TypeAd, {Home} from "./pages/Home";
+import ConnectSign from './pages/Login';
+import {Login} from "./pages/Login";
+import {Instruction} from "./pages/Instriction";
+import {Advertisment} from "./pages/Advertisment";
+
+// const store = createStore((state, action) => { //единственный редьюсер данного хранилища
+//   if (state === undefined){ //redux запускает редьюсер хотя бы раз, что бы инициализировать хранилище
+//       return {counter: 0};  //обязательно вернуть новый объект, а не изменить текущий state
+//   }
+//   if (action.type === 'COUNTER_INC'){ //в каждом action должен быть type
+//       return {counter: state.counter +1} //создаем новый объект базируясь на данных из предыдущего состояния
+//   }
+//   if (action.type === 'COUNTER_DEC'){
+//       return {counter: state.counter -1}
+//   }
+//   return state; //редьюсеров может быть несколько, в таком случае вызываются все редьюсеры, но далеко не всегда action.type будет относится к этому редьюсеру. Тогда редьюсер должен вернуть state как есть. 
+// })
+
+// store.subscribe(()=> console.log(store.getState())) // подписка на обновления store
+
+// store.dispatch({
+//   type: 'COUNTER_INC'
+// })
+
+// store.dispatch({
+//   type: 'COUNTER_DEC'
+// })
+
+// const actiononInc = () => ({type: 'COUNTER_INC'})
+// const actiononDec = () => ({type: 'COUNTER_DEC'})
+// store.dispatch(actiononInc())
+// store.dispatch(actiononDec())
+
+// const Counter = ({i=0,onInc,onDec}) => {
+//   return (
+//     <>
+//       <h4>{i}</h4>
+//       <button onClick={onInc}>+</button>
+//       <button disabled={i <= 0} onClick={onDec}>-</button>
+//     </>
+//   )
+// }
+
+// const ConnectedCounter = connect(state => ({i: state.counter}), {onInc: actiononInc ,onDec: actiononDec})(Counter)
+
+// const getGQL = url => 
+//     (query, variables={}) => fetch(url, {
+//         method: 'POST',
+//         headers: {
+//             //Accept: "application/json",
+//             "Content-Type": "application/json",
+//             ...(localStorage.authToken ? {Authorization: "Bearer " + localStorage.authToken } : {})
+//         },
+//         body: JSON.stringify({query, variables})
+//     }).then(res => res.json())
+
+// const actionPending = name => ({type: 'PROMISE', status: 'PENDING', name})
+// const actionResolved = (name, payload) => ({type: 'PROMISE', status: 'RESOLVED', name, payload})
+// const actionRejected = (name, error) => ({type: 'PROMISE', status: 'REJECTED', name, error})
+
+// let shopGQL = getGQL('http://shop-roles.asmer.fs.a-level.com.ua/graphql')
+// const actionPromise = (name, promise) => 
+//     async dispatch => {
+//         dispatch(actionPending(name))
+//         try{
+//             let payload = await promise
+//             dispatch(actionResolved(name, payload)) 
+//             return payload
+//         }
+//         catch(error){
+//              dispatch(actionRejected(name, error))
+//         }
+//     }
+
+
+// const combineReducer = combineReducers({auth: authReducer, promise: promiseReducer})
+//  function authReducer(state, action){
+//     if(state === undefined){
+//         if(localStorage.authToken || localStorage.authToken === 'null'){
+//             action.type = 'LOGIN'
+//             action.token = localStorage.authToken
+//         }
+//         else {
+//             return {}
+//         }
+//     }
+//     if(action.type === 'LOGIN'){
+//         console.log('LOGIN')
+//         localStorage.authToken = action.token
+//         let tok = localStorage.authToken.split('.')[1]
+//         let decoded = JSON.parse(atob((tok)))
+//         return {token:action.token, payload: decoded}
+//     }
+//     if(action.type === 'LOGOUT'){
+//         console.log("LOGOUT")
+//         localStorage.authToken = ''
+//         return {}
+//     }
+//     return state
+//   }
+//   function promiseReducer(state={}, {type, status, payload, error, name}){
+//       if (type === 'PROMISE'){
+//           return {
+//               ...state,
+//               [name]:{status, payload, error}
+//           }
+//       }
+//       return state
+//   }
+
+// const store = createStore(combineReducer, applyMiddleware(thunk)) 
+
+// const actionAuthLogin = token => ({type:'LOGIN', token})
+// const actionAuthLogout = () => ({type:"LOGOUT"})
+
+// let log = async (login, password) => {
+//   let query = `query log($l: String, $p: String) {
+//         login(login: $l, password: $p)
+//       }`
+
+//   let variables = {
+//       "l": login,
+//       "p": password
+//   }
+
+//   let token = await shopGQL(query, variables)
+//   console.log(token)
+//   return token.data.login
+// }
+
+// const actionLogin = (login, password) => actionPromise("login", log(login, password))
+
+// const actionFullLogin = (login, password) => {
+//   return async (dispatch) => {
+//       let result = await dispatch(actionLogin(login, password))
+//       if(result)
+//           dispatch(actionAuthLogin(result))
+//   }
+// }
+
+// const LoginForm = ({onLogin}) => {
+//   const [login,setLogin] = useState('')
+//   const [password,setPassword] = useState('')
+//   return (
+//     <div>
+//       <input value={login} onChange={e => setLogin(e.target.value)} ></input>
+//       <input value={password} onChange={e => setPassword(e.target.value)} type="password"></input>
+//       <button onClick={() => onLogin(login,password)}>Send</button>
+//     </div>
+//   )
+// }
+
+
+// const ConnectLog = connect(null, {onLogin: actionFullLogin})(LoginForm)
+// const ConnectLogout = connect(state => ({children: 'logout'}),{onClick: actionAuthLogout})('button')
+
+function App() {
+  return (
+    <>
+      <Provider store={store}>
+        <Router history = {createHistory()}>
+          <Navibar />
+          <Footer />
+          <Switch>
+            <Route exact path='/' component={Home}/>
+            <Route path='/sign' component={ConnectSign}/>
+            <Route path='/login' component={ConnectLog}/>
+            <Route path='/instruction' component={Instruction} />
+            <Route path='/advertisment' component={Advertisment} />
+          </Switch>
+        </Router>
+      </Provider>
+    </>
+  );
+}
+
+export default App;

+ 60 - 0
src/App.scss

@@ -0,0 +1,60 @@
+@import "~bootstrap/scss/bootstrap";
+
+.Navbar {
+    background: #002f34;
+    display: flex;
+
+    .header {
+        img {
+            width: 20%;
+        }
+
+        #basic-navbar-nav {
+            display: flex;
+            justify-content: end;
+
+            a {
+                color: white;
+                text-decoration: none;
+
+            }
+
+            a:hover {
+                margin-top: 1px ;
+            }
+        }
+    }
+    
+}
+
+.divLogin {
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+
+    .pwd-container {
+        display: flex;
+        flex-direction: column;
+        position: relative;
+        img {
+            position: absolute;
+            cursor: pointer;
+            width: 12%;
+            top: 50%;
+            left: 85%;
+        }
+    }
+}
+
+
+footer {
+    background: #002f34;
+    color: white;
+    padding-top: 25px;
+    a {
+        color: white;
+        text-decoration: none;
+    }
+}
+

+ 8 - 0
src/App.test.js

@@ -0,0 +1,8 @@
+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();
+});

+ 27 - 0
src/Components/Footer.js

@@ -0,0 +1,27 @@
+import React from "react";
+import {Link} from "react-router-dom";
+
+const Footer = () => <footer className="page-footer  fixed-bottom">
+    <div className="container text-center ">
+        <div className="row">
+            <div className="col-md-6 mt-md-0 mt-3">
+                <h5 className="text-uppercase">Что-то</h5>
+                <p>будет</p>
+            </div>
+
+            {/* <hr className="clearfix w-100 d-md-none pb-0"/> */}
+
+            <div className="col-md-6 mb-md-0 mb-3">
+                <ul className="list-unstyled">
+                    <li><Link to="/instruction">Инструкция</Link></li>
+                    <li><Link to="/advertisment">Реклама</Link></li>
+                </ul>
+            </div>
+        </div>
+    </div>
+
+    <div className="footer-copyright text-center py-3">© 2021 Copyright</div>
+
+</footer>
+
+export default Footer

+ 30 - 0
src/Components/NaviBar.js

@@ -0,0 +1,30 @@
+import logo from '../logoOlx.png';
+import React from "react";
+import { Navbar, Nav, NavDropdown, Container } from 'react-bootstrap';
+import {Link} from "react-router-dom";
+
+export default function Navibar(){
+    return(
+    <>
+        <Navbar className='Navbar'>
+            <Container className='header'>
+              <Navbar.Brand><Link to="/"><img src={logo} alt={"logo"}/></Link></Navbar.Brand>
+              <Navbar.Toggle aria-controls="basic-navbar-nav" />
+              <Navbar.Collapse id="basic-navbar-nav">
+                <Nav>
+                  <Nav.Link><Link to="/login">Войти</Link></Nav.Link>
+                  <Nav.Link><Link to="/sign">Зарегистрироваться</Link></Nav.Link>
+                  {/* <NavDropdown title="Dropdown" id="basic-nav-dropdown">
+                    <NavDropdown.Item href="#action/3.1">Action</NavDropdown.Item>
+                    <NavDropdown.Item href="#action/3.2">Another action</NavDropdown.Item>
+                    <NavDropdown.Item href="#action/3.3">Something</NavDropdown.Item>
+                    <NavDropdown.Divider />
+                    <NavDropdown.Item href="#action/3.4">Separated link</NavDropdown.Item>
+                  </NavDropdown> */}
+                </Nav>
+              </Navbar.Collapse>
+            </Container>
+        </Navbar>
+    </>
+    )
+  }

+ 105 - 0
src/actions/index.js

@@ -0,0 +1,105 @@
+const getGQL = url => 
+    (query, variables={}) => fetch(url, {
+        method: 'POST',
+        headers: {
+            //Accept: "application/json",
+            "Content-Type": "application/json",
+            ...(localStorage.authToken ? {Authorization: "Bearer " + localStorage.authToken } : {})
+        },
+        body: JSON.stringify({query, variables})
+    }).then(res => res.json())
+
+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})
+
+let shopGQL = getGQL('http://marketplace.asmer.fs.a-level.com.ua/graphql')
+
+export const actionPromise = (name, promise) => 
+    async dispatch => {
+        dispatch(actionPending(name))
+        try{
+            let payload = await promise
+            dispatch(actionResolved(name, payload)) 
+            return payload
+        }
+        catch(error){
+             dispatch(actionRejected(name, error))
+        }
+    }
+
+const actionAuthLogin = token => ({type:'LOGIN', token})
+export const actionAuthLogout = () => ({type:"LOGOUT"})
+
+let log = async (login, password) => {
+  let query = `query log($l: String!, $p: String!) {
+        login(login: $l, password: $p)
+      }`
+
+  let variables = {
+      "l": login,
+      "p": password
+  }
+
+  let token = await shopGQL(query, variables)
+  console.log(token)
+  return token.data.login
+}
+
+const actionLogin = (login, password) => actionPromise("login", log(login, password))
+
+export const actionFullLogin = (login, password) => {
+  return async (dispatch) => {
+      let result = await dispatch(actionLogin(login, password))
+      if(result)
+          dispatch(actionAuthLogin(result))
+  }
+}
+
+// const actionRegister = (login,password) =>
+//     actionPromise('reg',shopGQL(`mutation reg($login: String!, $password: String!){
+//         createUser(login:$login, password: $password){
+//         _id login
+//     }
+// }`,{query: {login,password}}))
+
+// export const actionFullRegister = (login,password) => 
+//   async dispatch => {
+//     let payload = await dispatch(actionRegister(login,password))
+//     if(payload.data.UserUpsert != null){
+//       await dispatch(actionFullLogin(login,password))
+//     }
+//   }
+
+export const actionReg = (login, password) =>
+    async dispatch => {
+        let loginData = await dispatch(actionPromise('reg', shopGQL(
+            `mutation reg($login: String!, $password: String!){
+                createUser(login:$login, password: $password){
+                  _id
+                }
+              }`, { login, password })))
+        if (loginData && loginData.data && (loginData.data.createUser != null)) {
+            dispatch(actionLogin(login, password))
+            console.log(loginData.data)
+        }
+    }
+
+export const actionTypeAd = () =>
+    actionPromise('type Ad', shopGQL(`
+            query Ad($query:String){
+              type AdFind(query:$query){
+                _id 
+                owner
+                images
+                comments
+                createdAt
+                title
+                description
+                tags
+                address
+                price
+              }
+            }
+        `, {query: JSON.stringify([{parent:null}])}))
+

+ 13 - 0
src/index.css

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

+ 17 - 0
src/index.js

@@ -0,0 +1,17 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import './index.css';
+import App from './App';
+import reportWebVitals from './reportWebVitals';
+
+ReactDOM.render(
+  <React.StrictMode>
+    <App />
+  </React.StrictMode>,
+  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();

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


BIN
src/logoOlx.png


File diff suppressed because it is too large
+ 1 - 0
src/pages/3844476-eye-see-show-view-watch_110339.svg


File diff suppressed because it is too large
+ 1 - 0
src/pages/3844477-disable-eye-inactive-see-show-view-watch_110343.svg


+ 5 - 0
src/pages/Advertisment.js

@@ -0,0 +1,5 @@
+import React from "react";
+
+export const Advertisment = () => (
+    <h1>Advertisment</h1>
+)

+ 14 - 0
src/pages/Home.js

@@ -0,0 +1,14 @@
+import React from "react";
+import {connect}   from 'react-redux';
+import {useState} from "react";
+import {actionTypeAd} from '../actions'
+
+
+export const Home = (_id,owner) => {
+    return (
+        <h1>home</h1>
+    )
+}
+
+const TypeAd = connect(null,{_id: actionTypeAd})(Home)
+export default TypeAd

+ 5 - 0
src/pages/Instriction.js

@@ -0,0 +1,5 @@
+import React from "react";
+
+export const Instruction = () => (
+    <h1>Instruction</h1>
+)

+ 31 - 0
src/pages/Login.js

@@ -0,0 +1,31 @@
+import React from "react";
+import {connect}   from 'react-redux';
+import {useState} from "react";
+import {actionFullLogin} from '../actions'
+
+import showPwdImg from './3844476-eye-see-show-view-watch_110339.svg';
+import hidePwdImg from './3844477-disable-eye-inactive-see-show-view-watch_110343.svg';
+
+const LoginForm = ({onLogin}) => {
+  const [login,setLogin] = useState('')
+  const [password,setPassword] = useState('')
+  const [open,setOpen] = useState(false)
+  return (
+    <div className="divLogin">
+        <h4>Войти</h4>
+        <div>
+            <label>Nickname</label>
+            <input value={login} onChange={e => setLogin(e.target.value)} placeholder="Nickname"></input>
+        </div>
+        <div className='pwd-container'>
+            <label>Ваш текущий пароль от olx</label>
+            <input value={password} type={open ? "text" : "password"} onChange={e => setPassword(e.target.value)} placeholder="Пароль"  />
+            <img src={open ? hidePwdImg : showPwdImg} onClick={() => setOpen(!open)}/>
+        </div>
+            <button onClick={() => onLogin(login,password)}>Send</button>  
+    </div>
+  )
+}
+
+const ConnectLog = connect(null, {onLogin: actionFullLogin})(LoginForm)
+export default ConnectLog

+ 31 - 0
src/pages/Sign.js

@@ -0,0 +1,31 @@
+import React from "react";
+import {connect}   from 'react-redux';
+import {useState} from "react";
+import {actionReg} from '../actions'
+import {Redirect} from 'react-router-dom';
+
+const Sign = ({onSign,loggedIn}) => {
+    const [login,setLogin] = useState('')
+    const [password,setPassword] = useState('')
+    const [open,setOpen] = useState(false)
+    return (
+      <div className="divSign">
+          <h4>Войти</h4>
+          <div>
+              <label>Nickname</label>
+              <input value={login} onChange={e => setLogin(e.target.value)} placeholder="Nickname"></input>
+          </div>
+          <div className='pwd-container2'>
+              <label>Ваш текущий пароль от olx</label>
+              <input value={password} type={open ? "text" : "password"} onChange={e => setPassword(e.target.value)} placeholder="Пароль"  />
+              <img src={open ? hidePwdImg : showPwdImg} onClick={() => setOpen(!open)}/>
+          </div>
+              <button onClick={() => onSign(login,password)}>Send</button>  
+              {loggedIn && <Redirect from="/sign" to="/" />}
+      </div>
+
+    )
+}
+
+const ConnectSign = connect(store => ({loggedIn: (store.authReducer.login || store.login)}) , {onSign: actionFullRegister})(Sign)
+export default ConnectSign

+ 44 - 0
src/reducers/index.js

@@ -0,0 +1,44 @@
+import thunk from 'redux-thunk';
+import {createStore, combineReducers, applyMiddleware} from 'redux';
+
+ function authReducer(state, action){
+    if(state === undefined){
+        if(localStorage.authToken || localStorage.authToken === 'null'){
+            action.type = 'LOGIN'
+            action.token = localStorage.authToken
+        }
+        else {
+            return {}
+        }
+    }
+    if(action.type === 'LOGIN'){
+        console.log('LOGIN')
+        localStorage.authToken = action.token
+        let tok = localStorage.authToken.split('.')[1]
+        let decoded = JSON.parse(atob((tok)))
+        return {token:action.token, payload: decoded}
+    }
+    if(action.type === 'LOGOUT'){
+        console.log("LOGOUT")
+        localStorage.authToken = ''
+        return {}
+    }
+    return state
+  }
+  
+  function promiseReducer(state={}, {type, status, payload, error, name}){
+      if (type === 'PROMISE'){
+          return {
+              ...state,
+              [name]:{status, payload, error}
+          }
+      }
+      return state
+  }
+
+
+
+const rootReducer = combineReducers({authReducer, promiseReducer});
+const store = createStore(rootReducer, applyMiddleware(thunk)) 
+
+export default store

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

+ 5 - 0
src/setupTests.js

@@ -0,0 +1,5 @@
+// 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';