Browse Source

HW react-store done

Alyona Brytvina 2 years ago
parent
commit
d58438717a
2 changed files with 481 additions and 41 deletions
  1. 369 19
      src/App.js
  2. 112 22
      src/App.scss

+ 369 - 19
src/App.js

@@ -1,25 +1,375 @@
 import logo from './logo.svg';
-import './App.css';
+import './App.scss';
+import thunk from 'redux-thunk';
+import {createStore, combineReducers, applyMiddleware} from 'redux';
+import {Provider, connect} from 'react-redux';
 
+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)); // 1. {delay1000: {status: 'PENDING'}}
+        try {
+            let payload = await promise;
+            dispatch(actionResolved(name, payload));
+            return payload;
+        } catch (error) {
+            dispatch(actionRejected(name, error));
+        }
+    };
+
+const getGQL = url =>
+    (query, variables = {}) =>
+        fetch(url, {
+            //метод
+            method: 'POST',
+            headers: {
+                //заголовок content-type
+                'Content-Type': 'application/json',
+                ...(localStorage.authToken ? {'Authorization': 'Bearer ' + localStorage.authToken} :
+                    {})
+            },
+            //body с ключами query и variables
+            body: JSON.stringify({query, variables})
+        })
+            .then(res => res.json())
+            .then(data => {
+                if (data.errors && !data.data)
+                    throw new Error(JSON.stringify(data.errors));
+                return data.data[Object.keys(data.data)[0]];
+            });
+
+const backendURL = 'http://shop-roles.asmer.fs.a-level.com.ua';
+const gql = getGQL(backendURL + '/graphql');
+
+function jwtDecode(token) {
+    try {
+        let decoded = token.split('.');
+        decoded = decoded[1];
+        decoded = atob(decoded);
+        decoded = JSON.parse(decoded);
+        return decoded;
+
+    } catch (e) {
+        return;
+    }
+}
+
+
+//скопировать
+//3 редьюсера
+//экшоны к ним
+//
+const actionRootCats = () =>
+    actionPromise('rootCats', gql(`query {
+        CategoryFind(query: "[{\\"parent\\":null}]"){
+            _id name
+        }
+    }`));
+
+
+const actionCartAdd = (good, count = 1) => ({type: 'CART_ADD', good, count});
+const actionCartRemove = (good, count = 1) => ({type: 'CART_REMOVE', good, count});
+const actionCartChange = (good, count = 1) => ({type: 'CART_CHANGE', good, count});
+const actionCartClear = (good, count = 1) => ({type: 'CART_CLEAR', good, count});
+
+function cartReducer(state = {}, {type, good = {}, count = 1}) {
+    const {_id} = good;
+    const types = {
+        CART_ADD() {
+            return {
+                ...state,
+                [_id]: {good, count: count + (state[_id]?.count || 0)}
+            };
+        },
+        CART_REMOVE() {
+            let newState = {...state};
+            delete newState[_id];
+            return {
+                ...newState
+            };
+        },
+        CART_CHANGE() {
+            return {
+                ...state,
+                [_id]: {good, count}
+            };
+        },
+        CART_CLEAR() {
+            return {};
+        },
+    };
+
+    if (type in types)
+        return types[type]();
+
+
+    return state;
+}
+
+function authReducer(state, {type, token}) {
+    if (!state) {
+        if (localStorage.authToken) {
+            type = 'AUTH_LOGIN';
+            token = localStorage.authToken;
+        } else {
+            return {};
+        }
+    }
+    if (type === 'AUTH_LOGIN') {
+        let auth = jwtDecode(token);
+        if (auth) {
+            localStorage.authToken = token;
+            return {token, payload: auth};
+        }
+    }
+    if (type === 'AUTH_LOGOUT') {
+        localStorage.authToken = '';
+        return {};
+    }
+
+    return state;
+}
+
+function promiseReducer(state = {}, {type, name, status, payload, error}) {
+    if (type === 'PROMISE') {
+        return {
+            ...state,
+            [name]: {status, payload, error}
+        };
+    }
+    return state;
+}
+
+// Actions =============================
+// Логин и логаут
+const actionAuthLogin = (token) => ({type: 'AUTH_LOGIN', token});
+const actionAuthLogout = () => ({type: 'AUTH_LOGOUT'});
+
+const actionLogin = (login = 'tst', password = '123') =>
+    actionPromise('login', gql(`query ($login:String, $password:String){ login(login:$login, password:$password)}`, {
+        'login': login,
+        'password': password
+    }));
+
+const actionFullLogin = (login = 'tst', password = '123') =>
+    async dispatch => {
+        let token = await dispatch(actionLogin(login, password));
+        if (token) {
+            dispatch(actionAuthLogin(token));
+        }
+    };
+
+// Регистрация
+const actionRegister = (login = 'tst', password = '123') =>
+    actionPromise('login', gql(`mutation reg($login:String, $password:String) {
+        UserUpsert(user:{login:$login, password:$password, nick:$login}){
+          _id login
+        }
+      }`, {'login': login, 'password': password}));
+
+const actionFullRegister = (login = 'tst', password = '123') =>
+    async dispatch => {
+        await dispatch(actionRegister(login, password));
+        await dispatch(actionFullLogin(login, password));
+    };
+
+// Корневые категории
+
+// Товары категории
+const actionCatById = (_id) =>
+    actionPromise('catById', gql(`query ($q: String){
+        CategoryFindOne(query: $q){
+            _id name goods {
+                _id name price images {
+                    url
+                }
+            } 
+            subCategories {
+                _id name
+            }
+        }
+    }`, {q: JSON.stringify([{_id}])}));
+
+const actionGoodById = (_id) =>
+    actionPromise('goodById', gql(`query ($good:String) {
+        GoodFindOne(query:$good) {
+          _id name price images {
+              url
+          }
+        }
+      }`, {good: 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('5dc458985df9d670df48cc47'));
+
+
+const Logo = () =>
+    <img src={logo} className="Logo" alt="logo"/>;
+
+const Header = () =>
+    <header className="header">
+        <Logo/>
+        <CKoshik/>
+    </header>;
+
+const CategoryListItem = ({_id, name}) =>
+    <li><a href={`#/category/${_id}`}>{name}</a></li>;
+
+const CategoryList = ({cats}) =>{
+    return(
+        <ul>
+            {cats.map((item) => <CategoryListItem key={item._id} {...item}/>)}
+        </ul>
+    )
+}
+
+
+const CCategoryList = connect(state => ({cats: state.promise.rootCats?.payload || []}))(CategoryList);
+
+const Aside = () =>
+    <aside className="aside">
+        <CCategoryList/>
+    </aside>;
+
+const GoodCard = ({good: {_id, name, price, images}, onAdd}) =>
+    <li className="GoodCard">
+        <h2>{name}</h2>
+        {images && images[0] && images[0].url && <img src={backendURL + '/' + images[0].url}/>}
+        <strong>{price} грн</strong>
+        <button onClick={() => onAdd({_id, name, price, images})}>+</button>
+    </li>;
+
+const CGoodCard = connect(null, {onAdd: actionCartAdd})(GoodCard);
+
+const Koshik = ({cart}) => {
+    let goodsInCart = cart;
+    let allGoodsInCart = 0;
+    for (let key in goodsInCart) {
+        allGoodsInCart += goodsInCart[key].count;
+    }
+    return (
+        <h2 className="koshikCounter">Корзина:{allGoodsInCart}</h2>
+    );
+};
+
+const CKoshik = connect(({cart}) => ({cart}))(Koshik);
+
+
+const Category = ({cat: {name, goods = []} = {}}) => {
+    return(
+        <div className="Category">
+            <h1>{name}</h1>
+            <ul className="ul">
+                {goods.map(good => <CGoodCard key={good._id} good={good}/>)}
+            </ul>
+        </div>
+    )
+
+}
+
+
+const CCategory = connect(state => ({cat: state.promise.catById?.payload || {}}))(Category);
+
+const Cart = ({cart, onCartChange, onCartRemove}) => {
+    const error = typeof cart === 'undefined';
+    return (
+        <div className="Cart">
+            <div className="wrapperForName">
+            {error === false &&
+            (cart.map((good) => {
+                        return (
+                            <div className="name" key={good.good.name}>{good.good.name}</div>
+                        );
+                    })
+                )
+            }
+            </div>
+
+            <div className="wrapperForCount">
+                {error === false &&
+                (cart.map((good) => {
+                        return <div className="count" key={good.good._id}>{good.count}</div>;
+                    })
+                )}
+            </div>
+            <div className="wrapperForName">
+                {error === false &&
+                (cart.map((good) => {
+                        return (
+                            <button key={good.good._id} onClick={() => onCartRemove(good.good, good.count)
+                            }>
+                             Удалить {good.good.name}</button>);
+                    })
+                )
+                }
+
+            </div>
+            {/*    {` нарисовать страницу корзины , по изменению в input <input onChange={() => onCartChange(...)}*/
+            }
+            {/*по кнопке удалить */
+            }
+            {/*    ТУТ БУДЕТ КОРЗИНА*/
+            }
+        </div>
+    )
+};
+
+const CCart = connect(
+    state => ({cart: Object.values(state.cart) || []}),
+    {
+        onCartChange: actionCartChange,
+        onCartRemove: actionCartRemove
+    })(Cart);
+// забрать из редакса корзину положить в пропс cart,
+//дать компоненту onCartChange и onCartRemove с соответствующими actionCreator)(Cart)
+
+const Main = () =>
+    <main className="main">
+        <Aside/>
+        <Content>
+            <Category cat={{name: 'ЗАГЛУШКА'}}/>
+            <CCategory/>
+            <Cart/>
+            <CCart/>
+        </Content>
+    </main>;
+
+const Content = ({children}) =>
+    <div className="Content">
+        {children}
+    </div>;
+
+const Footer = () =>
+    <footer className="Footer">
+        <Logo/>
+    </footer>;
+
+//import {Provider, connect} from 'react-redux';
 function App() {
-  return (
-    <div className="App">
-      <header className="App-header">
-        <img src={logo} className="App-logo" alt="logo" />
-        <p>
-          Edit <code>src/App.js</code> and save to reload.
-        </p>
-        <a
-          className="App-link"
-          href="https://reactjs.org"
-          target="_blank"
-          rel="noopener noreferrer"
-        >
-          Learn React
-        </a>
-      </header>
-    </div>
-  );
+    return (
+        <Provider store={store}>
+            <div className="App">
+                <Header/>
+                <Main/>
+                <Footer/>
+            </div>
+        </Provider>
+    );
 }
 
 export default App;
+;

+ 112 - 22
src/App.scss

@@ -1,38 +1,128 @@
 .App {
-  text-align: center;
-}
+  font-family: Georgia sans-serif;
+  padding: 0;
+  margin: 0;
+  display: flex;
+  flex-direction: column;
 
-.App-logo {
-  height: 40vmin;
-  pointer-events: none;
+  .main{
+    display: flex;
+    text-align: center;
+  }
 }
 
-@media (prefers-reduced-motion: no-preference) {
-  .App-logo {
-    animation: App-logo-spin infinite 20s linear;
+.aside{
+  min-width: 20%;
+  border-right: 1px solid lightgrey;
+  display: flex;
+  justify-content: start;
+  flex-direction: column;
+  align-items: start;
+  margin: 5px;
+  text-align: start;
+
+  ul{
+    list-style: none;
+    margin: 0;
+    padding: 0;
+  }
+
+  li{
+    margin: 5px;
   }
+
 }
 
-.App-header {
-  background-color: #282c34;
-  min-height: 100vh;
+
+.header{
   display: flex;
-  flex-direction: column;
+  justify-content: space-between;
   align-items: center;
-  justify-content: center;
-  font-size: calc(10px + 2vmin);
-  color: white;
+  border-bottom: 1px solid lightgrey;
+
+  .Logo{
+    margin-left: 5%;
+  }
+  .koshikCounter{
+    margin-right: 5%;
+  }
+  img{
+    height: 40px;
+  }
 }
 
-.App-link {
-  color: #61dafb;
+.Cart{
+  margin: 10px;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+
+  .wrapperForName{
+    display: flex;
+    flex-direction: column;
+
+    .name{
+      margin: 10px;
+    }
+
+  }
+  .wrapperForCount{
+    display: flex;
+    flex-direction: column;
+
+    .count{
+      margin: 10px;
+    }
+  }
+
+  button{
+    margin: 10px;
+  }
 }
 
-@keyframes App-logo-spin {
-  from {
-    transform: rotate(0deg);
+.Content{
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  flex-direction: column;
+
+
+  ul, li{
+    list-style: none;
+    margin: 0;
+    padding: 0;
   }
-  to {
-    transform: rotate(360deg);
+
+
+  .GoodCard{
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+
+    h2{
+      margin: 5%;
+    }
+    strong{
+      margin: 2%;
+    }
+  }
+
+  img{
+    height: 80px;
   }
 }
+
+.Footer{
+  background-color: rebeccapurple;
+  height: 10%;
+  border-top: 1px solid lightgrey;
+  img{
+    height: 40px;
+    margin: 5px;
+  }
+}
+
+
+
+