Prechádzať zdrojové kódy

added pages LogIn, registration, Cart, DashBoard

makstravm 2 rokov pred
rodič
commit
78ec4d7a32

+ 2 - 1
package.json

@@ -9,6 +9,7 @@
         "react": "^17.0.2",
         "react-dom": "^17.0.2",
         "react-redux": "^7.2.6",
+        "react-router-dom": "5.3.0",
         "react-scripts": "5.0.0",
         "redux": "^4.1.2",
         "redux-thunk": "^2.4.1",
@@ -39,4 +40,4 @@
             "last 1 safari version"
         ]
     }
-}
+}

+ 23 - 367
src/App.js

@@ -1,141 +1,15 @@
-import logoDefault from './logo.svg';
 import './App.scss';
-import { Provider, connect } from 'react-redux';
-import { createStore, combineReducers, applyMiddleware } from 'redux';
-import thunk from 'redux-thunk';
-
-const jwtDecode = token => {
-    try {
-        let arrToken = token.split('.')
-        let base64Token = atob(arrToken[1])
-        return JSON.parse(base64Token)
-    }
-    catch (e) {
-        console.log('Лажа, Бро ' + e);
-    }
-}
-
-function authReducer(state, { type, token }) {
-    if (!state) {
-        if (localStorage.authToken) {
-            type = 'AUTH_LOGIN'
-            token = localStorage.authToken
-        } else state = {}
-    }
-    if (type === 'AUTH_LOGIN') {
-        localStorage.setItem('authToken', token)
-        let payload = jwtDecode(token)
-        if (typeof payload === 'object') {
-            return {
-                ...state,
-                token,
-                payload
-            }
-        } else return state
-    }
-    if (type === 'AUTH_LOGOUT') {
-        localStorage.removeItem('authToken')
-        return {}
-    }
-    return state
-}
-
-const actionAuthLogin = token => ({ type: 'AUTH_LOGIN', token })
-const actionAuthLogout = () => ({ type: 'AUTH_LOGOUT' })
-
-
-function cartReducer(state = {}, { type, good = {}, count = 1 }) {
-    const { _id } = good
-    const types = {
-        CART_ADD() {
-            count = +count
-            if (!count) return state
-            return {
-                ...state,
-                [_id]: {
-                    good,
-                    count: count + (state[_id]?.count || 0)
-                }
-            }
-        },
-        CART_CHANGE() {
-            count = +count
-            if (!count) return state
-            return {
-                ...state,
-                [_id]: {
-                    good,
-                    count: count
-                }
-            }
-        },
-        CART_REMOVE() {
-            let { [_id]: remove, ...newState } = state
-            return {
-                ...newState
-            }
-        },
-        CART_CLEAR() {
-            return {}
-        },
-    }
-    if (type in types) {
-        return types[type]()
-    }
-    return state
-}
-
-const actionAddCart = (good, count) => ({ type: 'CART_ADD', good, count })
-const actionChangeCart = (good, count) => ({ type: 'CART_CHANGE', good, count })
-const actionRemoveCart = good => ({ type: 'CART_REMOVE', good })
-const actionCleanCart = () => ({ type: 'CART_CLEAR' })
-
-function promiseReducer(state = {}, { type, status, payload, error, name }) {
-    if (type === 'PROMISE') {
-        return {
-            ...state,
-            [name]: { status, payload, error }
-        }
-    }
-    return state;
-}
-
-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 })
-
-const actionPromise = (name, promise) =>
-    async dispatch => {
-        dispatch(actionPending(name))
-        try {
-            let data = await promise
-            dispatch(actionResolved(name, data))
-            return data
-        }
-        catch (error) {
-            dispatch(actionRejected(name, error))
-        }
-    }
-
-const getGQL = url =>
-    async (query, variables = {}) => {
-        let obj = await fetch(url, {
-            method: 'POST',
-            headers: {
-                "Content-Type": "application/json",
-                Authorization: localStorage.authToken ? 'Bearer ' + localStorage.authToken : {},
-            },
-            body: JSON.stringify({ query, variables })
-        })
-        let a = await obj.json()
-        if (!a.data && a.errors)
-            throw new Error(JSON.stringify(a.errors))
-        return a.data[Object.keys(a.data)[0]]
-    }
-
-const backURL = 'http://shop-roles.asmer.fs.a-level.com.ua'
-
-const gql = getGQL(backURL + '/graphql');
+import { Router, Route } from 'react-router-dom';
+import createHistory from "history/createBrowserHistory";
+import { Provider } from 'react-redux';
+import { Header } from './components/header/Header';
+import { Main } from './components/main/Main';
+import { Footer } from './components/Footer';
+import { store } from './redux-Store/redux-Store'
+import { actionPromise } from './redux-Store/promiseReducer'
+import { gql } from './redux-Store/redux-Store'
+import { CLoginForm } from './components/authorization/LoginForm';
+import { CRegisterForm } from './components/authorization/RegisterForm';
 
 const actionRootCats = () =>
     actionPromise('rootCats', gql(`query {
@@ -144,243 +18,25 @@ const actionRootCats = () =>
             }
         }`))
 
-const actionCatById = (_id) =>
-    actionPromise('catById', gql(`query catById($q: String){
-            CategoryFindOne(query: $q){
-                subCategories{name, _id}
-                _id name goods {
-                    _id name price images {
-                        url
-                    }
-                }
-            }
-        }`, { q: JSON.stringify([{ _id }]) }))
-
-const store = createStore(combineReducers({
-    promise: promiseReducer,
-    auth: authReducer,
-    cart: cartReducer
-}),
-    applyMiddleware(thunk))
-
 
 store.subscribe(() => console.log(store.getState()))
 store.dispatch(actionRootCats())
-store.dispatch(actionCatById('5dc49f4d5df9d670df48cc64'))
-
-
-
-
-// {
-// const defaultRootCats = [
-//     {
-//         "_id": "5dc49f4d5df9d670df48cc64",
-//         "name": "Airconditions"
-//     },
-//     {
-//         "_id": "5dc458985df9d670df48cc47",
-//         "name": "     Smartphones"
-//     },
-//     {
-//         "_id": "5dc4b2553f23b553bf354101",
-//         "name": "Крупная бытовая техника"
-//     },
-//     {
-//         "_id": "5dcac1b56d09c45440d14cf8",
-//         "name": "Макароны"
-//     }]
-
-
-// const defaultCat = {
-//     "subCategories": null,
-//     "_id": "5dc458985df9d670df48cc47",
-//     "name": "     Smartphones",
-//     "goods": [
-//         {
-//             "_id": "61b105f9c750c12ba6ba4524",
-//             "name": "iPhone ",
-//             "price": 1200,
-//             "images": [
-//                 {
-//                     "url": "images/50842a3af34bfa28be037aa644910d07"
-//                 }
-//             ]
-//         },
-//         {
-//             "_id": "61b1069ac750c12ba6ba4526",
-//             "name": "iPhone ",
-//             "price": 1000,
-//             "images": [
-//                 {
-//                     "url": "images/d12b07d983dac81ccad404582a54d8be"
-//                 }
-//             ]
-//         },
-//         {
-//             "_id": "61b23f94c750c12ba6ba472a",
-//             "name": "name1",
-//             "price": 1214,
-//             "images": [
-//                 {
-//                     "url": null
-//                 }
-//             ]
-//         },
-//         {
-//             "_id": "61b23fbac750c12ba6ba472c",
-//             "name": "smart",
-//             "price": 1222,
-//             "images": [
-//                 {
-//                     "url": "images/871f4e6edbf86c35f70b72dcdebcd8b2"
-//                 }
-//             ]
-//         }
-//     ]
-// }
-
-// const JSONTest = ({ data }) =>
-//     <pre>
-//         {JSON.stringify(data, null, 4)}
-//         {Math.random() > 0.5 && <h1>asdfasf</h1>}
-//     </pre>
-
-// const ReduxJSON = connect(state => ({ data: state }))(JSONTest)
-
-// const ListItem = ({ item }) =>
-//     <li>{item}</li>
-
-// const List = ({ data = ["пиво", "чипсы", "сиги"] }) =>
-//     <ul>
-//         {data.map(item => <ListItem item={item} />)}
-//     </ul>
-// }
-
-//============== RootCategory =================
-
-const RootCategory = ({ cat: { _id, name } = {} }) =>
-    <li>
-        <a href={`#/${_id}`}>{name}</a>
-    </li>
-
-const RootCategories = ({ cats = [] }) =>
-    <ul className='RootCategories'>
-        {cats.map(cat => <RootCategory cat={cat} />)}
-    </ul>
-
-const CRootCategories = connect(state => ({ cats: state.promise.rootCats?.payload || [] }))
-    (RootCategories)
-
-//============== Category =================
-
-const SubCategories = ({ cats }) =>
-    <></>
-
-const GoodCard = ({ good: { _id, name, price, images } = {}, onCartAdd }) =>
-    <div className='GoodCard'>
-        <h2>{name}</h2>
-        {images && images[0] && images[0].url && <img src={backURL + '/' + images[0].url} alt='product img' />}
-        <strong>{price}</strong>
-        <button onClick={() => onCartAdd({ _id, name, price, images })}>+</button>
-    </div>
-
-const CGoodCard = connect(null, { onCartAdd: actionAddCart })(GoodCard)
-
-const Category = ({ cat: { _id, name, goods, subCategories } = {} }) =>
-    <div className='Category'>
-        <h1>{name}</h1>
-        {subCategories && <SubCategories cats={subCategories} />}
-        {(goods || []).map(good => <CGoodCard good={good} />)}
-    </div>
-
-const CCategory = connect(state => ({ cat: state.promise.catById?.payload }))(Category)
-
-
-//============== Cart =================
-
-const CartItem = ({ cartItem: { good, count }, removeCart, onChangeCart }) =>
-    <div className='CartItem'>
-        {good.images && good.images[0] && good.images[0].url && <img src={backURL + '/' + good.images[0].url} alt='productimg'/>}
-        <h4>{good.name}</h4>
-        <span>К-во:<input type='number' value={count} onChange={(e) => onChangeCart(good, e.currentTarget.value)} /></span>
-        <span>Цена: <strong>{good.price * count}</strong></span>
-        <button onClick={() => removeCart(good)}>X</button>
-    </div>
-
-const CCartItem = connect(null, { removeCart: actionRemoveCart, onChangeCart: actionChangeCart })(CartItem)
-
-const Cart = ({ cart, clearCart }) =>
-    <div className='Cart'>
-        <h3>Корзин</h3>
-        {Object.keys(cart).length !== 0 ?
-            <button onClick={() => clearCart()}
-                className='clearBtn'> Очистить корзину</button > : ''}
-        {Object.entries(cart).map(([, cartItem]) => <CCartItem cartItem={cartItem} />)}
-    </div >
-
-const CCart = connect(state => ({ cart: state.cart }), { clearCart: actionCleanCart })(Cart)
-
-//============== Header =================
-const Header = ({ logo = logoDefault }) =>
-    <header>
-        <Logo logo={logo} />
-        <CKoshik />
-    </header>
-const Logo = ({ logo = logoDefault }) =>
-    <a href='#' className="Logo">
-        <img src={logo} alt='logo' alt='logo' />
-    </a>
-
-const Koshik = ({ cart }) => {
-    let count = 0;
-    let sum = Object.entries(cart).map(([, val]) => val.count)
-    count = sum.reduce((a, b) => a + b, 0)
-    return (
-        <div className='Koshik'>{count}</div>
-    )
-}
-
-const CKoshik = connect(({ cart }) => ({ cart }))(Koshik)
-
-//============== Main =================
-
-const Aside = () =>
-    <aside>
-        <CRootCategories />
-    </aside>
-
-const Content = ({ children }) =>
-    <div className='Content'>
-        {children}
-    </div>
-
-const Main = () =>
-    <main>
-        <Aside />
-        <Content>
-            <CCategory />
-        </Content>
-    </main>
-
-//============== Footer =================
-
-const Footer = ({ logo = logoDefault }) =>
-    <footer>
-        <Logo logo={logo} />
-    </footer>
 
-//============== APP =================
+export const history = createHistory()
 
 function App() {
     return (
-        <Provider store={store}>
-            <div className="App">
-                <Header />
-                <Main />
-                <CCart />
-                <Footer />
-            </div>
-        </Provider>
+        <Router history={history}>
+            <Provider store={store}>
+                <div className="App">
+                    <Header />
+                    <Main />
+                    <Footer />
+                    <Route path='/login' component={CLoginForm} />
+                    <Route path='/registration' component={CRegisterForm} />
+                </div>
+            </Provider>
+        </Router>
     );
 }
 

+ 127 - 3
src/App.scss

@@ -1,17 +1,87 @@
 .Logo {
     img {
-        max-height: 100px;
+        width: 100px;
     }
 }
+.HeaderTop {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+}
 
-.GoodCard {
+.GoodCart {
     border: 1px solid cyan;
     border-radius: 15px;
     img {
         max-width: 50%;
     }
 }
+.UserPanel {
+    h2 {
+        display: inline-block;
+        padding-right: 10px;
+        vertical-align: middle;
+    }
+    a {
+        text-decoration: none;
+        padding: 5px 10px;
+        margin: 0 10px;
+        display: inline-block;
+        border: 1px solid #ec0099;
+    }
+}
+.LoginForm {
+    position: absolute;
+    top: 0;
+    bottom: 0;
+    right: 0;
+    left: 0;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    align-content: center;
+    background-color: rgba(#fff, 0.8);
 
+    &__inner {
+        position: relative;
+        padding: 30px 25px;
+        text-align: center;
+        background-color: #fff;
+        border-radius: 25px;
+        box-shadow: 0 0 6px rgb(0, 0, 0);
+        label {
+            display: block;
+            padding-bottom: 10px;
+        }
+        input {
+            width: 85%;
+            padding: 5px 10px;
+            margin-top: 5px;
+        }
+        button {
+            text-align: center;
+            cursor: pointer;
+            margin-bottom: 5px;
+        }
+        a {
+            display: block;
+            text-decoration: none;
+            padding: 5px;
+            font-size: 0.8em;
+        }
+        .close {
+            position: absolute;
+            top: 0;
+            right: 0;
+            border: none;
+            background-color: transparent;
+            cursor: pointer;
+            padding: 10px 15px;
+            line-height: 1.2em;
+            font-weight: 700;
+        }
+    }
+}
 .App {
     header {
         .Logo {
@@ -49,7 +119,7 @@
     img {
         max-width: 10vw;
     }
-    
+
     input {
         max-width: 7vw;
         text-align: center;
@@ -59,3 +129,57 @@
         cursor: pointer;
     }
 }
+.Cart {
+    button {
+        display: block;
+    }
+}
+.Good {
+    img {
+        display: block;
+        width: 35vw;
+    }
+}
+.Koshik {
+    display: flex;
+    justify-content: flex-end;
+    align-items: center;
+    padding: 0 25px;
+    &__link {
+        display: inline-block;
+        position: relative;
+        margin-right: 15px;
+        img {
+            display: block;
+            width: 5vw;
+        }
+        span {
+            position: absolute;
+            top: 0;
+            right: 0;
+            background-color: #fff;
+            color: #000;
+            border-radius: 50%;
+            width: 20px;
+            text-decoration: none;
+            text-align: center;
+            box-shadow: 0 0 3px #000;
+        }
+    }
+}
+.MyordersList {
+    width: 100%;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    a {
+        font-weight: 700;
+        color: #000;
+        padding: 5px 10px;
+        width: 250px;
+    }
+    p {
+        padding: 0 15px;
+        width: 9vw;
+    }
+}

+ 6 - 0
src/components/Footer.js

@@ -0,0 +1,6 @@
+import { Logo } from './LogoR';
+
+export const Footer = () =>
+    <footer>
+        <Logo />
+    </footer>

+ 12 - 0
src/components/LogoR.js

@@ -0,0 +1,12 @@
+import { Link } from 'react-router-dom'
+import logoImg from '../logo.svg'
+
+export const Logo = () => {
+    return (
+        <div>
+            <Link to='/' className="Logo">
+                <img src={logoImg} alt='logo' />
+            </Link>
+        </div>
+    )
+}

+ 7 - 0
src/components/aside/Aside.js

@@ -0,0 +1,7 @@
+import { CRootCategories } from './rootCategory/RootCategory'
+
+
+export const Aside = () =>
+    <aside>
+        <CRootCategories />
+    </aside>

+ 16 - 0
src/components/aside/rootCategory/RootCategory.js

@@ -0,0 +1,16 @@
+import { Link } from "react-router-dom"
+import {connect } from 'react-redux';
+
+const RootCategory = ({ cat: { _id, name } = {} }) =>
+    <li>
+        <Link to={`/category/${_id}`}>{name}</Link>
+    </li>
+
+export const RootCategories = ({ cats = [] }) =>{
+   return <ul className='RootCategories'>
+        {cats.map(cat => <RootCategory key={cat._id} cat={cat} />)}
+    </ul>
+}
+
+export const CRootCategories = connect(state => ({ cats: state.promise.rootCats?.payload || [] }))(RootCategories)
+

+ 59 - 0
src/components/authorization/LoginForm.js

@@ -0,0 +1,59 @@
+import { Link } from 'react-router-dom';
+import { useState } from 'react';
+import { connect } from 'react-redux';
+import { actionFullLogin } from '../../redux-Store/authReducer'
+import { history } from '../../App';
+
+const LoginForm = ({ onLogin }) => {
+    const [log, setLog] = useState('')
+    const [pas, setPas] = useState('')
+    const [errorMode, setErrorMode] = useState(false)
+    const onChangeLog = (e) => {
+        setLog(e.currentTarget.value)
+        setErrorMode(false)
+    }
+    const onChangePas = (e) => {
+        setPas(e.currentTarget.value)
+        setErrorMode(false)
+    }
+    const onCheckLogin = () => {
+        const validateSpace = /^\S*$/
+        const validetedSpaceLog = validateSpace.test(log)
+        const validetedSpacePas = validateSpace.test(pas)
+        if (validetedSpaceLog && validetedSpacePas) {
+            onLogin(log, pas)
+            history.push('/')
+        } else {
+            setErrorMode(true)
+        }
+    }
+    return (
+        <div className='LoginForm' onClick={() => history.push('/')}>
+            <div className="LoginForm__inner" onClick={(e => e.stopPropagation())
+            }>
+                <h4>Login</h4>
+                <label >
+                    Login:
+                    <input style={{ borderColor: errorMode ? 'red' : 'black' }}
+                        value={log}
+                        onChange={onChangeLog}
+                        placeholder='login' />
+                </label>
+                <label>
+                    Password:
+                    <input style={{ borderColor: errorMode ? 'red' : 'black' }}
+                        value={pas}
+                        onChange={onChangePas}
+                        placeholder='password' />
+                </label>
+                {errorMode && <div style={{ color: 'red' }}>Error, please check for spaces </div>}
+                <button onClick={onCheckLogin}
+                    disabled={!log || !pas || errorMode ? true : false}>Login
+                </button>
+                <Link to={'/registration'}>Registration</Link>
+                <button className="close" onClick={() => history.push('/')}>X</button>
+            </div >
+        </div >
+    )
+}
+export const CLoginForm = connect(null, { onLogin: actionFullLogin, })(LoginForm)

+ 58 - 0
src/components/authorization/RegisterForm.js

@@ -0,0 +1,58 @@
+import { Link } from 'react-router-dom';
+import { useState } from 'react';
+import { connect } from 'react-redux';
+import { actionFullRegister } from '../../redux-Store/authReducer'
+import { history } from '../../App';
+
+const RegisterForm = ({ onRegister }) => {
+    const [log, setLog] = useState('')
+    const [pas, setPas] = useState('')
+    const [errorMode, setErrorMode] = useState(false)
+    const onChangeLog = (e) => {
+        setLog(e.currentTarget.value)
+        setErrorMode(false)
+    }
+    const onChangePas = (e) => {
+        setPas(e.currentTarget.value)
+        setErrorMode(false)
+    }
+    const onCheckLogin = () => {
+        const validateSpace = /^\S*$/
+        const validetedSpaceLog = validateSpace.test(log)
+        const validetedSpacePas = validateSpace.test(pas)
+        if (validetedSpaceLog && validetedSpacePas) {
+            onRegister(log, pas)
+            history.push('/')
+        } else {
+            setErrorMode(true)
+        }
+    }
+    return (
+        <div className='LoginForm' onClick={() => history.push('/')}>
+            <div className="LoginForm__inner" onClick={(e => e.stopPropagation())}>
+                <h4>Registration</h4>
+                <label >
+                    Login:
+                    <input style={{ borderColor: errorMode ? 'red' : 'black' }}
+                        value={log}
+                        onChange={onChangeLog}
+                        placeholder='login' />
+                </label>
+                <label>
+                    Password:
+                    <input style={{ borderColor: errorMode ? 'red' : 'black' }}
+                        value={pas}
+                        onChange={onChangePas}
+                        placeholder='password' />
+                </label>
+                {errorMode && <div style={{ color: 'red' }}>Error, please check for spaces </div>}
+                <button onClick={onCheckLogin}
+                    disabled={!log || !pas || errorMode ? true : false}>Regitration
+                </button>
+                <Link to={'/login'}>Login</Link>
+                <button className="close" onClick={() => history.push('/')}>X</button>
+            </div>
+        </div >
+    )
+}
+export const CRegisterForm = connect(null, { onRegister: actionFullRegister })(RegisterForm)

+ 61 - 0
src/components/header/Header.js

@@ -0,0 +1,61 @@
+import { Logo } from '../LogoR';
+import { connect } from 'react-redux';
+import { Link } from "react-router-dom"
+import order from '../../order.png'
+import dashboard from '../../dashboard.png'
+import { actionAuthLogout } from '../../redux-Store/authReducer'
+
+
+const Koshik = ({ cart, auth }) => {
+    let count = 0;
+    let sum = Object.entries(cart).map(([, val]) => val.count)
+    count = sum.reduce((a, b) => a + b, 0)
+    return (
+        <div className='Koshik'>
+            <Link className='Koshik__link' to={'/order'}>
+                <img src={order} alt='order' />
+                {count !== 0 ? <span>{sum}</span> : ''}
+            </Link>
+            {auth?.token &&
+                <Link to={'/dashboard'}>
+                    <img src={dashboard} alt="dashboard" />
+                </Link>}
+        </div>
+
+    )
+}
+
+const CKoshik = connect(state => ({ cart: state.cart, auth: state.auth }))(Koshik)
+
+const HeaderTop = ({ children }) =>
+    <div className='HeaderTop'>
+        {children}
+    </div>
+
+
+const UserPanel = ({ auth, onLogout }) => {
+    return (
+        <div className='UserPanel'>
+            {!auth?.token ?
+                <>
+                    <Link to={`/login`} >Sign in</Link>
+                    <Link to={'/registration'}>Sign up</Link>
+                </> :
+                <>
+                    <h2>Hello, {auth?.payload?.sub.login}</h2>
+                    <button onClick={() => onLogout()}>Выйти</button>
+                </>}
+        </div>
+    )
+}
+
+const CUserPanel = connect(state => ({ auth: state.auth }), { onLogout: actionAuthLogout })(UserPanel)
+
+export const Header = () =>
+    <header>
+        <HeaderTop>
+            <Logo />
+            <CUserPanel />
+        </HeaderTop>
+        <CKoshik />
+    </header>

+ 7 - 0
src/components/header/LogIn.js

@@ -0,0 +1,7 @@
+
+
+
+
+const LogIn = () => {
+
+}

+ 28 - 0
src/components/main/Main.js

@@ -0,0 +1,28 @@
+import { CPageCategory } from './category/Category'
+import { Route } from 'react-router-dom';
+import { Aside } from '../aside/Aside';
+import { CCart } from './cart/Cart';
+import { CPageGood } from './good/PageGood';
+import { CDashboard } from './dashboard/Dashboard';
+
+const Content = ({ children }) =>
+    <div className='Content'>
+        {children}
+    </div>
+
+const PageMain = () => <h2>Главная</h2>
+
+
+
+
+export const Main = () =>
+    <main>
+        <Aside />
+        <Content>
+            <Route path='/' component={PageMain} exact />
+            <Route path='/category/:_id' component={CPageCategory} />
+            <Route path='/good/:_id' component={CPageGood} />
+            <Route path='/order' component={CCart} />
+            <Route path='/dashboard' component={CDashboard} />
+        </Content>
+    </main>

+ 25 - 0
src/components/main/cart/Cart.js

@@ -0,0 +1,25 @@
+import { connect } from 'react-redux';
+import { Link } from "react-router-dom"
+import { CartItem } from './CartItem';
+import { actionCleanCart, actionChangeCart, actionRemoveCart } from '../../../redux-Store/cartReducer'
+import { actionOrder } from '../../../redux-Store/cartReducer';
+export const Cart = ({ cart, auth, clearCart, sentOrder }) => {
+
+    return <div className='Cart'>
+        <h3>Корзина</h3>
+        {!auth?.token && <strong>Для офрмления заказа нужно <Link to={'/login'}>войти в кабинет</Link></strong>}
+        {Object.keys(cart).length !== 0 ?
+            <button onClick={() => clearCart()}
+                className='clearBtn'> Очистить корзину</button > : <div>Пока Пусто</div>}
+        {Object.entries(cart).map(([, cartItem]) => <CCartItem key={cartItem.good._id} cartItem={cartItem} />)}
+        {<button disabled={auth?.token ? false : true}
+            onClick={() => sentOrder()}>
+            Оформиь Заказ
+        </button>}
+
+    </div >
+}
+
+export const CCart = connect(state => ({ cart: state.cart, auth: state.auth }), { clearCart: actionCleanCart, sentOrder: actionOrder })(Cart)
+
+const CCartItem = connect(null, { removeCart: actionRemoveCart, onChangeCart: actionChangeCart })(CartItem)

+ 11 - 0
src/components/main/cart/CartItem.js

@@ -0,0 +1,11 @@
+
+import { backURL } from "../../../redux-Store/redux-Store"
+
+export const CartItem = ({ cartItem: { good, count }, removeCart, onChangeCart }) =>
+    <div className='CartItem'>
+        {good.images && good.images[0] && good.images[0].url && <img src={backURL + '/' + good.images[0].url} alt='productimg' />}
+        <h4>{good.name}</h4>
+        <span>К-во:<input type='number' value={count} onChange={(e) => onChangeCart(good, e.currentTarget.value)} /></span>
+        <span>Цена: <strong>{good.price * count}</strong></span>
+        <button onClick={() => removeCart(good)}>X</button>
+    </div>

+ 27 - 0
src/components/main/category/Category.js

@@ -0,0 +1,27 @@
+import { connect } from 'react-redux';
+import { useEffect } from 'react'
+import { CGoodCart } from './GoodCart'
+import { actionCatById } from '../../../redux-Store/redux-Store'
+
+export const SubCategories = ({ cats }) =>
+    <></>
+
+const Category = ({ cat: { _id, name, goods, subCategories } = {} }) =>
+    <div className='Category'>
+        <h1>{name}</h1>
+        {subCategories && <SubCategories cats={subCategories} />}
+        {(goods || []).map(good => <CGoodCart key={good._id} good={good} />)}
+    </div>
+
+const CCategory = connect(state => ({ cat: state.promise.catById?.payload }))(Category)
+
+export const PageCategory = ({ match: { params: { _id } }, getData }) => {
+    useEffect(() => {
+        getData(_id)
+    }, [_id])
+    return (
+        <CCategory />
+    )
+}
+
+export const CPageCategory = connect(null, { getData: actionCatById })(PageCategory)

+ 15 - 0
src/components/main/category/GoodCart.js

@@ -0,0 +1,15 @@
+
+import { Link } from "react-router-dom"
+import { backURL } from "../../../redux-Store/redux-Store"
+import { connect } from 'react-redux';
+import { actionAddCart } from '../../../redux-Store/cartReducer'
+
+export const GoodCart = ({ good: { _id, name, price, images } = {}, onCartAdd }) =>
+    <div className='GoodCart'>
+        <Link to={`/good/${_id}`}>{name}</Link>
+        {images && images[0] && images[0].url && <img src={backURL + '/' + images[0].url} alt='product img' />}
+        <strong>{price}</strong>
+        <button onClick={() => onCartAdd({ _id, name, price, images })}>+</button>
+    </div>
+
+export const CGoodCart = connect(null, { onCartAdd: actionAddCart })(GoodCart)

+ 24 - 0
src/components/main/dashboard/Dashboard.js

@@ -0,0 +1,24 @@
+import { actionMyOrders } from '../../../redux-Store/redux-Store';
+import { connect } from 'react-redux';
+import { Link } from "react-router-dom"
+
+const MyordersList = ({ myorder }) => {
+    return (
+        <>{myorder.map(t => <div key={t._id} className='MyordersList'>
+            {console.log(t)}
+            <Link to={`/good/${t.good?._id || 'СсылкаГовно'}`}>{t.good?.name || 'Спасибо Беку за Дичь'}</Link>
+            <p>Цена: <strong>{t.price}</strong></p>
+            <p>К-во: <strong>{t.count}</strong></p>
+            <p>Сумма: <strong>{t.total}</strong></p>
+        </div>)} </>
+    )
+}
+const CMyordersList = connect(state => ({ myorder: state?.promise?.myOrders?.payload || [] }))(MyordersList)
+
+const Dashboard = ({ getMyorders }) => {
+    getMyorders()
+    return (
+        <>  <CMyordersList /></>
+    )
+}
+export const CDashboard = connect(null, { getMyorders: actionMyOrders })(Dashboard)

+ 18 - 0
src/components/main/good/Good.js

@@ -0,0 +1,18 @@
+import { connect } from 'react-redux';
+import { actionAddCart } from '../../../redux-Store/cartReducer'
+import { backURL } from '../../../redux-Store/redux-Store';
+
+const Good = ({ good: { name, description, images, price, _id }, onCartAdd }) => {
+    return (
+        <div className='Good'>
+            <h3>{name}</h3>
+            {images && images[0] && images[0].url && <img src={backURL + '/' + images[0].url} alt='product img' />}
+            <strong>{price}</strong>
+            <p>{description}</p>
+            <button onClick={() => onCartAdd({ name, description, images, price, _id })}>+</button>
+        </div>
+    )
+}
+
+
+export const CGood = connect(state => ({ good: state.promise?.goodById?.payload || {} }), { onCartAdd: actionAddCart })(Good)

+ 17 - 0
src/components/main/good/PageGood.js

@@ -0,0 +1,17 @@
+import { connect } from 'react-redux';
+import { useEffect } from "react"
+import { actionGoodById } from '../../../redux-Store/redux-Store'
+import { CGood } from './Good';
+
+export const PageGood = ({ match: { params: { _id } }, getGood }) => {
+    useEffect(() => {
+        getGood(_id)
+    }, [_id])
+    return (
+        <><CGood /></>
+    )
+}
+
+export const CPageGood = connect(null, { getGood: actionGoodById })(PageGood)
+
+

BIN
src/dashboard.png


BIN
src/order.png


+ 78 - 0
src/redux-Store/authReducer.js

@@ -0,0 +1,78 @@
+import { gql } from "./redux-Store";
+import { actionPromise } from './promiseReducer';
+
+function jwtDecode  (token)  {
+    try {
+        let arrToken = token.split('.')
+        let base64Token = atob(arrToken[1])
+        return JSON.parse(base64Token)
+    }
+    catch (e) {
+        console.log('Лажа, Бро ' + e);
+    }
+}
+
+export function authReducer(state, { type, token }) {
+    if (!state) {
+        if (localStorage.authToken) {
+            type = 'AUTH_LOGIN'
+            token = localStorage.authToken
+        } else state = {}
+    }
+
+    if (type === 'AUTH_LOGIN') {
+        localStorage.setItem('authToken', token)
+
+        let payload = jwtDecode(token)
+        if (typeof payload === 'object') {
+            return {
+                ...state,
+                token,
+                payload
+            }
+        } else return state
+    }
+    if (type === 'AUTH_LOGOUT') {
+        localStorage.removeItem('authToken')
+        return {}
+    }
+    return state
+}
+
+const actionAuthLogin = token => ({ type: 'AUTH_LOGIN', token })
+
+export const actionAuthLogout = () => ({ type: 'AUTH_LOGOUT' })
+
+export const actionFullLogin = (login, password) =>
+    async dispatch => {
+        let token = await dispatch(actionLogin(login, password))
+        if (token) {
+            dispatch(actionAuthLogin(token))
+        }
+    }
+
+const actionLogin = (login, password) =>
+    actionPromise('login', gql(`query NameForMe1($login:String, $password:String){
+            login(login:$login, password:$password)
+        }`, { login, password }))
+
+const actionRegister = (login, password) =>
+    actionPromise('register', gql(`
+                mutation reg($login:String, $password:String){
+                UserUpsert(user:{
+                    login:$login,
+                        password:$password,
+                        nick:$login}){
+                _id login
+                }
+            }
+            `, { login, password }))
+
+export const actionFullRegister = (login, password) =>
+    async dispatch => {
+        await actionRegister(login, password)
+        let token = await dispatch(actionLogin(login, password))
+        if (token) {
+            dispatch(actionAuthLogin(token))
+        }
+    }

+ 64 - 0
src/redux-Store/cartReducer.js

@@ -0,0 +1,64 @@
+import { gql } from "./redux-Store";
+import { actionPromise } from './promiseReducer';
+
+export function cartReducer(state = {}, { type, good = {}, count = 1 }) {
+    const { _id } = good
+    const types = {
+        CART_ADD() {
+            count = +count
+            if (!count) return state
+            return {
+                ...state,
+                [_id]: {
+                    good,
+                    count: count + (state[_id]?.count || 0)
+                }
+            }
+        },
+        CART_CHANGE() {
+            count = +count
+            if (!count) return state
+            return {
+                ...state,
+                [_id]: {
+                    good,
+                    count: count
+                }
+            }
+        },
+        CART_REMOVE() {
+            let { [_id]: remove, ...newState } = state
+            return {
+                ...newState
+            }
+        },
+        CART_CLEAR() {
+            return {}
+        },
+    }
+    if (type in types) {
+        return types[type]()
+    }
+    return state
+}
+
+export const actionAddCart = (good, count) => ({ type: 'CART_ADD', good, count })
+export const actionCleanCart = () => ({ type: 'CART_CLEAR' })
+export const actionRemoveCart = good => ({ type: 'CART_REMOVE', good })
+export const actionChangeCart = (good, count) => ({ type: 'CART_CHANGE', good, count })
+
+export const actionOrder = () =>
+    async (dispatch, getState) => {
+        let { cart } = getState()
+        const orderGoods = Object.entries(cart).map(([_id, { count }]) => ({ good: { _id }, count }))
+        let result = await dispatch(actionPromise('order', gql(`
+                    mutation newOrder($order:OrderInput){
+                      OrderUpsert(order:$order)
+                        { _id total }
+                    }
+            `, { order: { orderGoods } })))
+
+        if (result?._id) {
+            dispatch(actionCleanCart())
+        }
+    }

+ 27 - 0
src/redux-Store/promiseReducer.js

@@ -0,0 +1,27 @@
+
+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 })
+
+export const actionPromise = (name, promise) =>
+    async dispatch => {
+        dispatch(actionPending(name))
+        try {
+            let data = await promise
+            dispatch(actionResolved(name, data))
+            return data
+        }
+        catch (error) {
+            dispatch(actionRejected(name, error))
+        }
+    }
+
+export function promiseReducer(state = {}, { type, status, payload, error, name }) {
+    if (type === 'PROMISE') {
+        return {
+            ...state,
+            [name]: { status, payload, error }
+        }
+    }
+    return state;
+}

+ 64 - 0
src/redux-Store/redux-Store.js

@@ -0,0 +1,64 @@
+import { createStore, combineReducers, applyMiddleware } from 'redux';
+import thunk from 'redux-thunk';
+import { authReducer } from './authReducer'
+import { promiseReducer } from './promiseReducer'
+import { cartReducer } from './cartReducer'
+import { actionPromise } from './promiseReducer';
+
+export const backURL = 'http://shop-roles.asmer.fs.a-level.com.ua'
+
+export const actionCatById = (_id) =>
+    actionPromise('catById', gql(`query catById($q: String){
+            CategoryFindOne(query: $q){
+                subCategories{name, _id}
+                _id name goods {
+                    _id name price images {
+                        url
+                    }
+                }
+            }
+        }`, { q: JSON.stringify([{ _id }]) }))
+
+
+export const actionGoodById = (_id) =>
+    actionPromise('goodById', gql(`query goodByID($q: String) {
+                                        GoodFindOne(query: $q){
+                                            name _id  description price images{url}
+                                        }
+        }`, {
+        q: JSON.stringify([{ _id }])
+    }))
+
+
+export const actionMyOrders = () =>
+    actionPromise('myOrders', gql(`query Order{
+                                            OrderGoodFind(query:"[{}]"){
+                                            good{ name _id} _id total price count
+                                        }
+    }`, {}))
+
+const getGQL = url =>
+    async (query, variables = {}) => {
+        let obj = await fetch(url, {
+            method: 'POST',
+            headers: {
+                "Content-Type": "application/json",
+                Authorization: localStorage.authToken ? 'Bearer ' + localStorage.authToken : {},
+            },
+            body: JSON.stringify({ query, variables })
+        })
+
+        let a = await obj.json()
+        if (!a.data && a.errors)
+            throw new Error(JSON.stringify(a.errors))
+        return a.data[Object.keys(a.data)[0]]
+    }
+
+export const gql = getGQL(backURL + '/graphql');
+
+export const store = createStore(combineReducers({
+    promise: promiseReducer,
+    auth: authReducer,
+    cart: cartReducer
+}),
+    applyMiddleware(thunk))

+ 94 - 4
yarn.lock

@@ -1014,7 +1014,7 @@
     core-js-pure "^3.19.0"
     regenerator-runtime "^0.13.4"
 
-"@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.16.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
+"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.16.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
   version "7.16.5"
   resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.5.tgz#7f3e34bf8bdbbadf03fbb7b1ea0d929569c9487a"
   integrity sha512-TXWihFIS3Pyv5hzR7j6ihmeLkZfrXGxAr5UfSl8CHf+6q/wpiYDkUau0czckpYG8QmnCIuPpdLtuA9VmuGGyMA==
@@ -4333,7 +4333,19 @@ he@^1.2.0:
   resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
   integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
 
-hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2:
+history@^4.9.0:
+  version "4.10.1"
+  resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3"
+  integrity sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==
+  dependencies:
+    "@babel/runtime" "^7.1.2"
+    loose-envify "^1.2.0"
+    resolve-pathname "^3.0.0"
+    tiny-invariant "^1.0.2"
+    tiny-warning "^1.0.0"
+    value-equal "^1.0.1"
+
+hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2:
   version "3.3.2"
   resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
   integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
@@ -4822,6 +4834,11 @@ is-wsl@^2.2.0:
   dependencies:
     is-docker "^2.0.0"
 
+isarray@0.0.1:
+  version "0.0.1"
+  resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
+  integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=
+
 isarray@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
@@ -5602,7 +5619,7 @@ lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.7.
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
   integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
 
-loose-envify@^1.1.0, loose-envify@^1.4.0:
+loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
   integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
@@ -5726,6 +5743,14 @@ min-indent@^1.0.0:
   resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869"
   integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==
 
+mini-create-react-context@^0.4.0:
+  version "0.4.1"
+  resolved "https://registry.yarnpkg.com/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz#072171561bfdc922da08a60c2197a497cc2d1d5e"
+  integrity sha512-YWCYEmd5CQeHGSAKrYvXgmzzkrvssZcuuQDDeqkT+PziKGMgE+0MCCtcKbROzocGBG1meBLl2FotlRwf4gAzbQ==
+  dependencies:
+    "@babel/runtime" "^7.12.1"
+    tiny-warning "^1.0.3"
+
 mini-css-extract-plugin@^2.4.5:
   version "2.4.5"
   resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.4.5.tgz#191d6c170226037212c483af1180b4010b7b9eef"
@@ -6167,6 +6192,13 @@ path-to-regexp@0.1.7:
   resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
   integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=
 
+path-to-regexp@^1.7.0:
+  version "1.8.0"
+  resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a"
+  integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==
+  dependencies:
+    isarray "0.0.1"
+
 path-type@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
@@ -6801,6 +6833,15 @@ prompts@^2.0.1, prompts@^2.4.2:
     kleur "^3.0.3"
     sisteransi "^1.0.5"
 
+prop-types@^15.6.2:
+  version "15.8.0"
+  resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.0.tgz#d237e624c45a9846e469f5f31117f970017ff588"
+  integrity sha512-fDGekdaHh65eI3lMi5OnErU6a8Ighg2KjcjQxO7m8VHyWjcPyj5kiOgV1LQDOOOgVy3+5FgjXvdSSX7B8/5/4g==
+  dependencies:
+    loose-envify "^1.4.0"
+    object-assign "^4.1.1"
+    react-is "^16.13.1"
+
 prop-types@^15.7.2:
   version "15.7.2"
   resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
@@ -6943,7 +6984,7 @@ react-error-overlay@^6.0.10:
   resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.10.tgz#0fe26db4fa85d9dbb8624729580e90e7159a59a6"
   integrity sha512-mKR90fX7Pm5seCOfz8q9F+66VCc1PGsWSBxKbITjfKVQHMNF2zudxHnMdJiB1fRCb+XsbQV9sO9DCkgsMQgBIA==
 
-react-is@^16.7.0, react-is@^16.8.1:
+react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1:
   version "16.13.1"
   resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
   integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
@@ -6970,6 +7011,35 @@ react-refresh@^0.11.0:
   resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.11.0.tgz#77198b944733f0f1f1a90e791de4541f9f074046"
   integrity sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==
 
+react-router-dom@5.3.0:
+  version "5.3.0"
+  resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.3.0.tgz#da1bfb535a0e89a712a93b97dd76f47ad1f32363"
+  integrity sha512-ObVBLjUZsphUUMVycibxgMdh5jJ1e3o+KpAZBVeHcNQZ4W+uUGGWsokurzlF4YOldQYRQL4y6yFRWM4m3svmuQ==
+  dependencies:
+    "@babel/runtime" "^7.12.13"
+    history "^4.9.0"
+    loose-envify "^1.3.1"
+    prop-types "^15.6.2"
+    react-router "5.2.1"
+    tiny-invariant "^1.0.2"
+    tiny-warning "^1.0.0"
+
+react-router@5.2.1:
+  version "5.2.1"
+  resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.2.1.tgz#4d2e4e9d5ae9425091845b8dbc6d9d276239774d"
+  integrity sha512-lIboRiOtDLFdg1VTemMwud9vRVuOCZmUIT/7lUoZiSpPODiiH1UQlfXy+vPLC/7IWdFYnhRwAyNqA/+I7wnvKQ==
+  dependencies:
+    "@babel/runtime" "^7.12.13"
+    history "^4.9.0"
+    hoist-non-react-statics "^3.1.0"
+    loose-envify "^1.3.1"
+    mini-create-react-context "^0.4.0"
+    path-to-regexp "^1.7.0"
+    prop-types "^15.6.2"
+    react-is "^16.6.0"
+    tiny-invariant "^1.0.2"
+    tiny-warning "^1.0.0"
+
 react-scripts@5.0.0:
   version "5.0.0"
   resolved "https://registry.yarnpkg.com/react-scripts/-/react-scripts-5.0.0.tgz#6547a6d7f8b64364ef95273767466cc577cb4b60"
@@ -7203,6 +7273,11 @@ resolve-from@^5.0.0:
   resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69"
   integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==
 
+resolve-pathname@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-3.0.0.tgz#99d02224d3cf263689becbb393bc560313025dcd"
+  integrity sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==
+
 resolve-url-loader@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/resolve-url-loader/-/resolve-url-loader-4.0.0.tgz#d50d4ddc746bb10468443167acf800dcd6c3ad57"
@@ -7964,6 +8039,16 @@ timsort@^0.3.0:
   resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
   integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=
 
+tiny-invariant@^1.0.2:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.2.0.tgz#a1141f86b672a9148c72e978a19a73b9b94a15a9"
+  integrity sha512-1Uhn/aqw5C6RI4KejVeTg6mIS7IqxnLJ8Mv2tV5rTc0qWobay7pDUz6Wi392Cnc8ak1H0F2cjoRzb2/AW4+Fvg==
+
+tiny-warning@^1.0.0, tiny-warning@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
+  integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
+
 tmp@^0.2.1:
   version "0.2.1"
   resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14"
@@ -8221,6 +8306,11 @@ v8-to-istanbul@^8.1.0:
     convert-source-map "^1.6.0"
     source-map "^0.7.3"
 
+value-equal@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c"
+  integrity sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==
+
 vary@~1.1.2:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"