kurkabein vor 2 Jahren
Ursprung
Commit
3111e65531
5 geänderte Dateien mit 3324 neuen und 121 gelöschten Zeilen
  1. 2818 61
      package-lock.json
  2. 4 0
      package.json
  3. 0 38
      src/App.css
  4. 472 22
      src/App.js
  5. 30 0
      src/App.scss

Datei-Diff unterdrückt, da er zu groß ist
+ 2818 - 61
package-lock.json


+ 4 - 0
package.json

@@ -6,9 +6,13 @@
     "@testing-library/jest-dom": "^5.16.1",
     "@testing-library/react": "^12.1.2",
     "@testing-library/user-event": "^13.5.0",
+    "node-sass": "^7.0.1",
     "react": "^17.0.2",
     "react-dom": "^17.0.2",
+    "react-redux": "^7.2.6",
     "react-scripts": "5.0.0",
+    "redux": "^4.1.2",
+    "redux-thunk": "^2.4.1",
     "web-vitals": "^2.1.4"
   },
   "scripts": {

+ 0 - 38
src/App.css

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

+ 472 - 22
src/App.js

@@ -1,25 +1,475 @@
-import logo from './logo.svg';
-import './App.css';
-
-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>
+import React, { useState } from 'react';
+import defaultLogo from './logo.svg';
+import './App.scss';
+import thunk from 'redux-thunk';
+import { Provider, connect } from 'react-redux';
+import { createStore, combineReducers, applyMiddleware} from 'redux';
+
+const getGQL = url =>
+(query, variables) => fetch(url, {
+    method: 'POST',
+    headers: {
+        "Content-Type": "application/json",
+        ...(localStorage.authToken ? {"Authorization": "Bearer " + localStorage.authToken} : {})
+    },
+    body: JSON.stringify({query, variables})
+}).then(res => res.json())
+    .then(data => {
+        if (data.data){
+            return Object.values(data.data)[0] 
+        } 
+        else throw new Error(JSON.stringify(data.errors))
+    })
+
+const backendURL = 'http://shop-roles.asmer.fs.a-level.com.ua'
+
+const gql = getGQL(backendURL + '/graphql');
+
+
+const store = createStore(combineReducers({promise: promiseReducer, auth: authReducer, cart: cartReducer}), applyMiddleware(thunk))
+store.subscribe(() => console.log(store.getState()));
+
+function promiseReducer(state={}, {type,name,status,payload,error}){
+  /* delay1000:{status, payload,error}
+      delay2000: {status, payload,error}*/
+      /* if(!state){
+          delay1000:{status,payload,error}
+          delay2000:{status,payload,error}
+      } */
+      if(type === 'PROMISE'){
+          return{
+              ...state,
+              [name]:{status,payload,error}
+          }
+      }
+      return state
+}
+const actionPending = name => ({name,type:"PROMISE",status:'PENDING'})
+const actionFulfiled = (name,payload) =>({name,type:"PROMISE",status:'FULFILLED',payload})
+const actionrRejected = (name,error) => ({name,type:"PROMISE",status:'REJECTED',error})
+
+const actionPromise = (name,promise) =>
+        async dispatch => {
+            dispatch(actionPending(name))
+            try{
+                let payload = await promise
+                dispatch(actionFulfiled(name,payload))
+                return payload
+            }
+            catch(e){
+                dispatch(actionrRejected(name,e))
+            }
+        }
+
+
+
+/* const jwtDecode = (token) =>{
+  try{
+      let payload = JSON.parse(atob(token.split('.')[1]));
+      return payload;
+  } catch(e){
+      
+  }
+} */
+
+function jwtDecode(token){
+  try{
+    let payload = JSON.parse(atob(token.split('.')[1]));
+    return payload;
+} catch(e){
+    
+}
+}
+
+function authReducer(state,{type, token}){
+  if(state === undefined){
+      if(localStorage.authToken){
+          type = 'AUTH_LOGIN';
+          token = localStorage.authToken
+      }
+  }
+  if(type === 'AUTH_LOGIN'){
+      let payload = jwtDecode(token);
+      if(payload){
+          localStorage.authToken = token;
+          return {token,payload}
+      }
+  }
+  if(type === 'AUTH_LOGOUT'){
+      localStorage.authToken = '';
+      return {};
+  }
+  return state || {};
+}
+
+function cartReducer(state={},{type, good, count=1}){
+  
+/*  _id1: {count:1, good:    {_id1,name,price,images}}
+_id2: {count:1, good:    {_id2,name,price,images}}
+*/
+
+if (type === 'CART_ADD'){
+ /*  if(good._id in state){
+      return{
+          ...state,
+          [good._id]: {count:state[good._id].count+count,good}
+      }
+  }else{ */
+      return{
+          ...state,
+          [good._id] : {count: count + (good._id in state ? state[good._id].count : 0),good}
+      }
+  }
+  
+
+if (type === 'CART_CHANGE'){
+ /*  let _id1 = good._id;
+  console.log(_id1); */
+  return{
+      ...state,
+      [good._id]: {count:count,good:good}
+  }
+}
+if (type === 'CART_DELETE'){
+ /*  const clone = {...state}
+  delete clone[good._id]
+  state = {...clone} */
+  let {[good._id]: remove, ...newState} = state;
+  return newState;
+}
+if (type === 'CART_CLEAN'){
+  return {}
+}
+return state
+}   
+
+const actionCartAdd = (good, count=1) => ({type:'CART_ADD', good, count})
+const actionCartChange = (good, count=1) => ({type:'CART_CHANGE',good,count:+count}) 
+const actionCartDelete = (good) => ({type: 'CART_DELETE',good})
+const actionCartClean = () => ({type:'CART_CLEAN'})
+
+
+
+const actionAuthLogin = (tokennn) => ({type:'AUTH_LOGIN',token:tokennn})
+const actionAuthLogout= () => ({type: 'AUTH_LOGOUT'})
+
+const actionRootCats = () => 
+        actionPromise('rootCats', gql(`query {
+            CategoryFind(query: "[{\\"parent\\":null}]"){
+                _id name
+            }
+        }`))
+const actionCatById = (_id) =>  //добавить подкатегории
+        actionPromise('catById', gql(`query catById($q: String){
+            CategoryFindOne(query: $q){
+                _id name goods {
+                    _id name price images {
+                        url
+                    }
+                }
+            }
+        }`, {q: JSON.stringify([{_id}])}))
+const actionGoodById = (_id) => 
+        actionPromise('goodById', gql(`query goodById ($goodId: String){
+            GoodFindOne(query:$goodId){
+              _id name price images{
+                url
+              }
+              description
+            }
+          }`,{goodId: JSON.stringify([{_id}])}))
+
+const actionFullRegister = (Login,password) =>
+          actionPromise('fullRegister', gql(`mutation register($Login:String,$password:String){
+            UserUpsert(user:{login:$Login,password:$password}){
+              _id login
+            }
+          }`,{Login: Login, password: password}))
+
+const actionFullLogin = (Login,password) => async (dispatch) => {
+   const tokennn = await dispatch(
+    actionPromise('fullLogin', gql(`query login($Login:String,$password:String){
+        login(login:$Login,password:$password)
+    }`,{Login:Login,password:password}))
+   );
+   await dispatch(actionAuthLogin(tokennn)); 
+}
+
+const actionAllOrders = () => 
+          actionPromise('allOrders',gql(`query orders{
+              OrderFind(query:"[{}]"){
+                _id total
+                orderGoods{
+                  count total good{name,price,
+                  images{
+                    url
+                  }}
+                }
+              }
+            }`))
+const actionCreateOrder = () => async (dispatch) =>{
+              let orderGoods = [];
+              Object.entries(store.getState().cart).map(([_id,{count}])=>orderGoods.push({"count":count,"good":{_id:_id}}))
+              actionPromise('createOrder',gql(`mutation newOrder($order:OrderInput){
+                  OrderUpsert(order:$order){
+                    _id total
+                  }
+                }`,{order:{orderGoods}}));
+               await store.dispatch(actionCartClean());
+          }
+const actionSearchGood = (good) => 
+          actionPromise('searchGood', gql(`query gf($query: String){
+              GoodFind(query: $query){
+                  _id, name, description, price, images{
+                      _id, url
+                  }
+              }
+          }`, {query: JSON.stringify([
+                      {
+                          $or: [{name: `/${good}/`}, {description: `/${good}/`}] //регулярки пишутся в строках
+                      },
+                      {
+                          sort: [{name: 1}]} //сортируем по title алфавитно
+                      ])
+          }))
+store.dispatch(actionRootCats());
+store.dispatch(actionCatById("5dc458985df9d670df48cc47"))
+store.dispatch(actionAllOrders());
+
+
+let defaultCategories = [{
+  "name": "     Smartphones",
+  "_id": "5dc458985df9d670df48cc47"
+},
+{
+  "name": "Холодильники",
+  "_id": "5dc4b2553f23b553bf3540fc"
+},
+{
+  "name": "Стиральные машины",
+  "_id": "5dc4b2553f23b553bf3540fd"
+},
+{
+  "name": "Стирально-сушильные машины",
+  "_id": "5dc4b2553f23b553bf3540fe"
+},
+{
+  "name": "Стиральные машины",
+  "_id": "5dc4b2553f23b553bf3540ff"
+},
+{
+  "name": "Духовые шкафы",
+  "_id": "5dc4b2553f23b553bf354100"
+},
+{
+  "name": "Крупная бытовая техника",
+  "_id": "5dc4b2553f23b553bf354101"
+},
+{
+  "name": " Макароны",
+  "_id": "5dcac1b56d09c45440d14cf8"
+},
+{
+  "name": "Drinks",
+  "_id": "5dcac6cf6d09c45440d14cfd"
+}]
+
+let defaultGoods = [{
+  "name": "tv",
+  "_id": "61afa8e0c750c12ba6ba444d",
+  "price": 443,
+  "images": [
+    {
+      "url": "images/b0e4301f809296e1cb52c81936e9b41c"
+    }
+  ]
+},
+{
+  "name": "От Прохора",
+  "_id": "61782338ef4e1b3e3b677079",
+  "price": 10000,
+  "images": [
+    {
+      "url": "images/d650672a6df67bfb4fb69bacd098981b"
+    },
+    {
+      "url": "images/1a16d34cd470d7b6d7bd4d5acad30d81"
+    }
+  ]
+}]
+
+let defaultOrders = [{
+  "_id": "61c1ed7ac750c12ba6ba4bf5",
+  "total": 13900,
+  "orderGoods": [
+    {
+      "count": 2,
+      "total": 10000,
+      "good": {
+        "name": "Apple iPhone X 64GB Space Gray",
+        "price": 5000,
+        "images": [
+          {
+            "url": "images/00505f5f08ac113874318dee67975aa9"
+          }
+        ]
+      }
+    },
+    {
+      "count": 3,
+      "total": 3900,
+      "good": {
+        "name": "Apple iPhone XS Max 256GB Gold",
+        "price": 1300,
+        "images": [
+          {
+            "url": "images/63c4a052377862494e33746b375903f6"
+          }
+        ]
+      }
+    }
+  ]
+}]
+const CategoryItem = ({category:{_id,name}= {}}) =>
+<li className='CategoryItem'>
+  <a target="_blank" href={_id}>{name}</a>
+</li>
+const ProductItem = ({good:{_id,name,price,description,images} = {}}) =>
+{ let source = backendURL+"/"+images[0].url;
+  let good = {_id,name,price,images}
+ return (<div className='ProductItem'>
+  <h2 className='ProductCartName'>{name}</h2>
+  <img src={source}/>
+  <strong>{price}</strong>
+  <p>{description}</p>
+  <button onClick={() => {store.dispatch(actionCartAdd(good))}}>Купить</button>
+  
+</div>) 
+}
+const OrderGoodItem = ({orderedGoods:{count,total,good} = {}}) =>
+<div>
+<h3>{good.name}</h3>
+<strong>price :{good.price}</strong>
+<img src={`${backendURL}/${good.images[0].url}`}/>
+<span>total count: {count }</span>
+<strong> total price : {total}</strong>
+</div>
+const OrderItem = ({order:{_id,total,orderGoods} = {}}) =>
+{   /* console.log(orderGoods[0].good.name); */
+  /* let images = orderGoods.good.images;
+  let source = backendURL+"/"+images[0].url */
+  return(<div className='OrderItem'>
+    <h2>{_id}</h2>
+    <strong>{total}</strong>
+    {orderGoods.map(orderedGoods => <OrderGoodItem orderedGoods={orderedGoods}/>)}
+  </div>)
+}
+const CartGood = ({good:{count,good} = {},onChange,onDelete}) =>
+{ console.log(count);
+  return(<div className='cartGood'>
+    <h2>{good.name}</h2>
+    <img src={`${backendURL}/${good.images[0].url}`}/>
+    <input type="number" value={count}   onChange={evt => onChange(good,evt.target.value)}/>
+    <button onClick={() => onDelete(good)}>Delete</button>
+  </div>)
+}
+
+const CCartGood = connect(null,{onChange: actionCartChange, onDelete: actionCartDelete})(CartGood)
+
+const CartGoods = ({goodds}) => {
+  console.log(goodds);
+  return(
+    <div>
+      <h1>КОРЗИНА</h1>
+{goodds.map(good => <CCartGood good={good}/>)}
+  </div>
+  )
+}
+
+const LoginForm = ({Login}) =>{
+  const [login,setLogin] = useState('');
+  const [pswd,setPswd] = useState('');
+
+  return(
+    <div className='LoginForm'>
+      <label>Login <input placeholder='login' type="text" onChange={event => setLogin(event.target.value)}/></label>
+      <label>Password <input placeholder='password'type="password" onChange={event => setPswd(event.target.value)}/></label>
+      <button onClick={() => Login(login,pswd)}>login</button>
     </div>
-  );
+  )
 }
+  
+const CLoginForm = connect(state=>({auth: state.auth?.payload || []}),{Login: actionFullLogin})(LoginForm)
+
+
+const RootCategories = ({categories = defaultCategories}) =>
+<ul className='RootCategories'>
+      {categories.map(category => <CategoryItem category={category}/>)}
+</ul>
+const ProductCart = ({goods = defaultGoods}) =>
+<div>
+{goods.map(good => <ProductItem good={good}/>)}
+</div> 
+const AllOrders = ({orders = defaultOrders}) =>
+<div>
+  {orders.map(order => <OrderItem order={order}/>)}
+</div>
+const CCartGoods = connect(state => ({goodds: Object.values(state.cart)}))(CartGoods)
+const CRootCategories = connect(state => ({categories: state.promise.rootCats.payload || []}),)(RootCategories)
+const CLogout = connect(state => ({children: `logout(${state.auth.payload && state.auth.payload.sub.login || 'anon'})`}),
+          {onClick: actionAuthLogout})("button")
+const CategoriesProducts = connect(state =>({goods:state.promise.catById?.payload?.goods}),)(ProductCart)
+const Orders = connect(state =>({orders: state.promise.allOrders?.payload || []}))(AllOrders)          
+
+const Logo = ({logo=defaultLogo}) =>
+<img  className="Logo" src={logo} alt='logo'/>
+
+const Header = ({children}) =>
+<header>
+    <Logo />
+    <CLoginForm/>
+    {children}
+    <CLogout/>
+</header>
 
-export default App;
+
+const Aside = ({}) =>
+<aside>
+  <CRootCategories/>
+</aside>
+const Content = ({children}) =>
+<section className='Content'>
+    
+    {children}
+</section>
+
+const Main = ({children}) =>
+<main>
+  <Aside/>
+  <Content>
+    {/* <Logo/> */}
+    <CategoriesProducts/>  
+    <CCartGoods/>
+    <Orders/>
+    {children}
+  </Content>
+  {children}
+</main>
+
+const Footer = ({children}) =>
+<footer>
+  <Logo/>
+  {children}
+</footer>
+function App () {
+  return(
+    <Provider store={store}>
+    <div className='App'>
+      <Header />
+      <Main />
+      <Footer />
+    </div>
+    </Provider>
+  );
+}
+export default App;

+ 30 - 0
src/App.scss

@@ -0,0 +1,30 @@
+.App {
+  header{
+    .Logo{
+      max-width: 100px;
+    }
+  }
+  .RootCategories{
+      list-style: none;
+    
+  }
+  main {
+    display: flex;
+    aside{
+      width: 30%;
+    }
+  }
+  .ProductItem {
+    img {
+      max-width: 300px;
+      max-height: 300px;
+    }
+  }
+  .cartGood {
+    img {
+      max-width: 300px;
+      max-height: 300px;
+    }
+  }
+}
+