瀏覽代碼

HW react jsx && shop done

Ivar 2 年之前
父節點
當前提交
5f7b03dcbd

+ 68 - 0
js/22_react_cw/shop-1/src/App.js

@@ -359,6 +359,74 @@ const List = ({data=["пиво", "чипсы", "сиги",]}) => (
 
 
 
+
+
+class LifeCycle extends Component {
+  //first time methods:
+
+  constructor(props){ //once
+      super(props)
+      this.state = {counter: 0};
+      console.log('constructor', props, this.state)
+  }
+
+  componentWillMount(){ //once
+      console.log('componentWillMount')
+
+      this.interval = setInterval(()=>{
+          this.setState((prevState, props) => ({counter: prevState.counter +1}))
+      },5000)
+  }
+
+  render(){ //as many as component should update + once at first time
+      console.log('render')
+      return (
+          <div> life cycle: {this.state.counter}</div>
+      );
+  }
+
+  componentDidMount(){ //first time render success
+      this.id = this.props.match.params._id
+      this.props.getData(this.id)
+      console.log('componentDidMount')
+  }
+
+  componentWillUnmount(){ //end of life
+      clearInterval(this.interval)
+      console.log('componentWillUnmount')
+  }
+
+
+  //main loop methods:
+
+  shouldComponentUpdate(nextProps, nextState){ //every time when props or state changed
+      console.log('shouldComponentUpdate', nextProps, nextState)
+      return Math.random() > 0.7; //here we decide, is this update so major to re-render, or no
+  }
+
+  componentWillUpdate(nextProps, nextState){ //before every render (except first)
+      console.log('componentWillUpdate', nextProps, nextState)
+  }
+
+  componentDidUpdate(prevProps, prevState){ //after render
+      if (this.props.match.params._id !== this.id)
+          this.componentDidMount()
+      console.log('componentDidUpdate', prevProps, prevState)
+  }
+
+
+
+  componentWillReceiveProps(nextProps){ //when we receive new props
+      console.log('componentWillReceiveProps', nextProps)
+  }
+}
+
+
+
+
+
+
+
 function App() {
   return (
     <Provider store={store}> 

+ 113 - 14
js/23_react_hw/react_hw/src/App.js

@@ -1,10 +1,15 @@
 import React, {useState, useEffect, useRef} from 'react'
-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';
 
+import logoDefault from './img/logo.svg';
+import clockFace from './img/ClockFace.png';
+import clockH from './img/ClockFace_H.png';
+import clockM from './img/ClockFace_M.png';
+import clockS from './img/ClockFace_S.png';
+
 
 const Spoiler = ({header="+", open=true, children}) => {
   const [isOpen, setOpen] = useState(open)
@@ -96,13 +101,14 @@ const Timer = ({seconds=100}) => {
       setCounter(seconds)
     }, [seconds])
   
-     useEffect(() => {
-      let interval = null
-      if (counter > 0 && !paused) {
-          interval = setInterval(() => {
-              setCounter(counter => counter - 1)
-          }, 1000);
-      } 
+     useEffect(() => {     
+        const interval = setInterval(() => {
+          if (counter > 0 && !paused) {
+            setCounter(counter => counter - 1)
+          } else {
+            clearInterval(interval)
+          }
+        }, 1000); 
       return () => {
           clearInterval(interval)
       }
@@ -137,27 +143,111 @@ const TimerControl = ({setSeconds}) => {
 }
 
 
+
 const SecondsTimer = ({seconds}) => (
-  <h2>{seconds}</h2>
+  <h2>{parseInt(seconds)}</h2>
 )
 
 const TimerContainer = ({seconds=100, refresh=100, render}) => {
+  const [paused, setPause] = useState(false)
+  const [time, setTime] = useState(seconds)
+  const t0 = useRef(performance.now())
+  const pausedAt = useRef(0)
 
 
-  
+  useEffect(() => {   
+    t0.current = performance.now()
+    setTime(seconds)
+    console.log('mount1')
+  }, [seconds])
+
+
+  useEffect(() => {   
+    console.log('mount2')
+
+    let interval
+    if (paused) {
+      pausedAt.current = performance.now()
+      clearInterval(interval)
+
+    } else {
+      // console.log('pause-', pausedAt.current, performance.now())
+      if (pausedAt.current !== 0) {
+        t0.current += (performance.now() - pausedAt.current)
+      }
+
+      interval = setInterval(() => {
+
+        let t1 = performance.now()
+        let delta = (t1-t0.current)/1000
+        
+        if (seconds >= delta) {
+            // console.log('timeset', time, seconds, delta)
+          setTime(seconds - delta)
+        } else {
+            console.log('0')
+          setTime(0)
+          clearInterval(interval)
+        }
+
+      }, refresh)
+
+    }
+     
+    return () => {
+        console.log('unMount')
+        pausedAt.current = 0
+        setTime(0)
+        clearInterval(interval)
+    }
+  }, [paused, seconds, refresh])
+
   const DisplayEl = render
   return (
     <>
-      <DisplayEl seconds={200} />
+      <DisplayEl seconds={time} setPause={() => setPause(!paused)} paused={paused}/>
     </>
   )
 }
 
 
+const LCD = ({seconds, setPause, paused}) => {
+  let h = (Math.floor(seconds / 3600))
+  let m = (Math.floor(seconds % 3600 / 60))
+  let s = parseInt(seconds % 60)
+  return (
+    <div>
+      <h2>{h >= 10 ? h : '0'+h}:{m >= 10 ? m : '0'+m}:{s >= 10 ? s : '0'+s}</h2>
+      <button onClick={setPause}>{paused ? 'Го' : 'Пауза'}</button>
+    </div>
+  )
+}
+
+
+const Watch = ({seconds, setPause, paused}) => {
+
+  let hDeg = Math.floor(seconds / 720) * 6
+  let mDeg = Math.floor(seconds % 3600 / 60) * 6
+  let sDeg = parseInt(seconds % 60) * 6 
+  return (
+    <div className="staticCont">
+      <div className="watchContainer">
+        <img className="watch"  src={clockFace} />
+        <img className="hours" style={{transform: `rotate(${hDeg}deg)`}} src={clockH} />
+        <img className="minutes" style={{transform: `rotate(${mDeg}deg)`}} src={clockM} />
+        <img className="seconds" style={{transform: `rotate(${sDeg}deg)`}} src={clockS} />
+      </div>
+        <button className="watchBtn" onClick={setPause}>{paused ? 'Го' : 'Пауза'}</button>
+    </div>
+  )
+}
+
+
 
 function App() {
   const [seconds, setSeconds] = useState(200)
-
+  const [secondsCont, setSecondsCont] = useState(200)
+  
   return (
     <>
       <Spoiler header={<h1>Заголовок</h1>} open={false}>
@@ -171,12 +261,21 @@ function App() {
 
       <PasswordConfirm min={3} char={true} bigChar={true} number={true}/>
 
-
       <Timer seconds={seconds} />
       <TimerControl setSeconds={setSeconds}/>
 
 
-      <TimerContainer seconds={1800} refresh={100} render={SecondsTimer}/>
+      <TimerContainer seconds={10} refresh={2000} render={SecondsTimer}/>
+
+      <TimerContainer seconds={65} refresh={100} render={LCD}/> 
+
+      <TimerContainer seconds={3660} refresh={1} render={Watch}/> 
+
+
+      <div className="controlContainerComb">
+        <TimerContainer seconds={secondsCont} refresh={500} render={LCD}/>
+        <TimerControl setSeconds={setSecondsCont}/>
+      </div>
 
     </>
   );

+ 39 - 0
js/23_react_hw/react_hw/src/App.scss

@@ -0,0 +1,39 @@
+
+.staticCont {
+   height: 500px;
+   margin: 50px 0;
+}
+
+.watchContainer {
+   position: relative;
+}
+
+.watch {
+   position: absolute;
+   z-index: 10;
+}
+
+.hours {
+   position: absolute;
+   z-index: 11;
+}
+
+.minutes {
+   position: absolute;
+   z-index: 12;
+}
+
+.seconds {
+   position: absolute;
+   z-index: 13;
+}
+
+.watchBtn {
+   display: block;
+   position: relative;
+   top: 440px;
+}
+
+.controlContainerComb {
+   margin: 50px 0;
+}

二進制
js/23_react_hw/react_hw/src/img/ClockFace.png


二進制
js/23_react_hw/react_hw/src/img/ClockFace_H.png


二進制
js/23_react_hw/react_hw/src/img/ClockFace_M.png


二進制
js/23_react_hw/react_hw/src/img/ClockFace_S.png


js/23_react_hw/react_hw/src/logo.svg → js/23_react_hw/react_hw/src/img/logo.svg


+ 364 - 1
js/24_react_shop/react_shop/package-lock.json

@@ -1,5 +1,5 @@
 {
-  "name": "hw",
+  "name": "shop",
   "version": "0.1.0",
   "lockfileVersion": 2,
   "requires": true,
@@ -11,7 +11,9 @@
         "@testing-library/jest-dom": "^5.16.1",
         "@testing-library/react": "^12.1.2",
         "@testing-library/user-event": "^13.5.0",
+        "bootstrap": "^5.1.3",
         "react": "^17.0.2",
+        "react-bootstrap": "^2.1.0",
         "react-dom": "^17.0.2",
         "react-redux": "^7.2.6",
         "react-router-dom": "^5.3.0",
@@ -2687,6 +2689,58 @@
         "node": ">= 8"
       }
     },
+    "node_modules/@popperjs/core": {
+      "version": "2.11.2",
+      "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.2.tgz",
+      "integrity": "sha512-92FRmppjjqz29VMJ2dn+xdyXZBrMlE42AV6Kq6BwjWV7CNUW1hs2FtxSNLQE+gJhaZ6AAmYuO9y8dshhcBl7vA==",
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/popperjs"
+      }
+    },
+    "node_modules/@react-aria/ssr": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.1.0.tgz",
+      "integrity": "sha512-RxqQKmE8sO7TGdrcSlHTcVzMP450hqowtBSd2bBS9oPlcokVkaGq28c3Rwa8ty5ctw4EBCjXqjP7xdcKMGDzug==",
+      "dependencies": {
+        "@babel/runtime": "^7.6.2"
+      },
+      "peerDependencies": {
+        "react": "^16.8.0 || ^17.0.0-rc.1"
+      }
+    },
+    "node_modules/@restart/hooks": {
+      "version": "0.4.5",
+      "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.5.tgz",
+      "integrity": "sha512-tLGtY0aHeIfT7aPwUkvQuhIy3+q3w4iqmUzFLPlOAf/vNUacLaBt1j/S//jv/dQhenRh8jvswyMojCwmLvJw8A==",
+      "dependencies": {
+        "dequal": "^2.0.2"
+      },
+      "peerDependencies": {
+        "react": ">=16.8.0"
+      }
+    },
+    "node_modules/@restart/ui": {
+      "version": "0.2.5",
+      "resolved": "https://registry.npmjs.org/@restart/ui/-/ui-0.2.5.tgz",
+      "integrity": "sha512-3dP8pMFickPpvAG5MVQW53HnJl0c17h7MwvI4nNy9QF66sHSYVchudlqlI8eOSaqnmc5YVjGura63vMb9LTNbQ==",
+      "dependencies": {
+        "@babel/runtime": "^7.13.16",
+        "@popperjs/core": "^2.10.1",
+        "@react-aria/ssr": "^3.0.1",
+        "@restart/hooks": "^0.4.0",
+        "@types/warning": "^3.0.0",
+        "dequal": "^2.0.2",
+        "dom-helpers": "^5.2.0",
+        "prop-types": "^15.7.2",
+        "uncontrollable": "^7.2.1",
+        "warning": "^4.0.3"
+      },
+      "peerDependencies": {
+        "react": ">=16.14.0",
+        "react-dom": ">=16.14.0"
+      }
+    },
     "node_modules/@rollup/plugin-babel": {
       "version": "5.3.0",
       "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.0.tgz",
@@ -3377,6 +3431,11 @@
         "@types/node": "*"
       }
     },
+    "node_modules/@types/invariant": {
+      "version": "2.2.35",
+      "resolved": "https://registry.npmjs.org/@types/invariant/-/invariant-2.2.35.tgz",
+      "integrity": "sha512-DxX1V9P8zdJPYQat1gHyY0xj3efl8gnMVjiM9iCY6y27lj+PoQWkgjt8jDqmovPqULkKVpKRg8J36iQiA+EtEg=="
+    },
     "node_modules/@types/istanbul-lib-coverage": {
       "version": "2.0.3",
       "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz",
@@ -3478,6 +3537,14 @@
         "redux": "^4.0.0"
       }
     },
+    "node_modules/@types/react-transition-group": {
+      "version": "4.4.4",
+      "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.4.tgz",
+      "integrity": "sha512-7gAPz7anVK5xzbeQW9wFBDg7G++aPLAFY0QaSMOou9rJZpbuI58WAuJrgu+qR92l61grlnCUe7AFX8KGahAgug==",
+      "dependencies": {
+        "@types/react": "*"
+      }
+    },
     "node_modules/@types/resolve": {
       "version": "1.17.1",
       "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz",
@@ -3539,6 +3606,11 @@
       "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.2.tgz",
       "integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg=="
     },
+    "node_modules/@types/warning": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.0.tgz",
+      "integrity": "sha1-DSUBJorY+ZYrdA04fEZU9fjiPlI="
+    },
     "node_modules/@types/ws": {
       "version": "8.2.2",
       "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.2.2.tgz",
@@ -4739,6 +4811,18 @@
       "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
       "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24="
     },
+    "node_modules/bootstrap": {
+      "version": "5.1.3",
+      "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.1.3.tgz",
+      "integrity": "sha512-fcQztozJ8jToQWXxVuEyXWW+dSo8AiXWKwiSSrKWsRB/Qt+Ewwza+JWoLKiTuQLaEPhdNAJ7+Dosc9DOIqNy7Q==",
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/bootstrap"
+      },
+      "peerDependencies": {
+        "@popperjs/core": "^2.10.2"
+      }
+    },
     "node_modules/brace-expansion": {
       "version": "1.1.11",
       "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@@ -4974,6 +5058,11 @@
       "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz",
       "integrity": "sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA=="
     },
+    "node_modules/classnames": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz",
+      "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA=="
+    },
     "node_modules/clean-css": {
       "version": "5.2.2",
       "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.2.2.tgz",
@@ -5854,6 +5943,14 @@
         "node": ">= 0.6"
       }
     },
+    "node_modules/dequal": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.2.tgz",
+      "integrity": "sha512-q9K8BlJVxK7hQYqa6XISGmBZbtQQWVXSrRrWreHC94rMt1QL/Impruc+7p2CYSYuVIUr+YCt6hjrs1kkdJRTug==",
+      "engines": {
+        "node": ">=6"
+      }
+    },
     "node_modules/destroy": {
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
@@ -5992,6 +6089,15 @@
         "utila": "~0.4"
       }
     },
+    "node_modules/dom-helpers": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
+      "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
+      "dependencies": {
+        "@babel/runtime": "^7.8.7",
+        "csstype": "^3.0.2"
+      }
+    },
     "node_modules/dom-serializer": {
       "version": "1.3.2",
       "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz",
@@ -8377,6 +8483,14 @@
         "node": ">= 0.4"
       }
     },
+    "node_modules/invariant": {
+      "version": "2.2.4",
+      "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
+      "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
+      "dependencies": {
+        "loose-envify": "^1.0.0"
+      }
+    },
     "node_modules/ip": {
       "version": "1.1.5",
       "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz",
@@ -12962,6 +13076,23 @@
         "react-is": "^16.8.1"
       }
     },
+    "node_modules/prop-types-extra": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/prop-types-extra/-/prop-types-extra-1.1.1.tgz",
+      "integrity": "sha512-59+AHNnHYCdiC+vMwY52WmvP5dM3QLeoumYuEyceQDi9aEhtwN9zIQ2ZNo25sMyXnbh32h+P1ezDsUpUH3JAew==",
+      "dependencies": {
+        "react-is": "^16.3.2",
+        "warning": "^4.0.0"
+      },
+      "peerDependencies": {
+        "react": ">=0.14.0"
+      }
+    },
+    "node_modules/prop-types-extra/node_modules/react-is": {
+      "version": "16.13.1",
+      "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+      "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
+    },
     "node_modules/prop-types/node_modules/react-is": {
       "version": "16.13.1",
       "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
@@ -13144,6 +13275,33 @@
         "node": ">=14"
       }
     },
+    "node_modules/react-bootstrap": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-2.1.0.tgz",
+      "integrity": "sha512-ZbEq8in6XzoDh4dyrANVwqeqrj1oRqL56unlBYzjsvdWPIaBP/B7qLMHCvnwuzpANwMrh/hSNpIocKn6rvOFIQ==",
+      "dependencies": {
+        "@babel/runtime": "^7.14.0",
+        "@restart/hooks": "^0.4.5",
+        "@restart/ui": "^0.2.5",
+        "@types/invariant": "^2.2.33",
+        "@types/prop-types": "^15.7.3",
+        "@types/react": ">=16.14.8",
+        "@types/react-transition-group": "^4.4.1",
+        "@types/warning": "^3.0.0",
+        "classnames": "^2.3.1",
+        "dom-helpers": "^5.2.1",
+        "invariant": "^2.2.4",
+        "prop-types": "^15.7.2",
+        "prop-types-extra": "^1.1.0",
+        "react-transition-group": "^4.4.1",
+        "uncontrollable": "^7.2.1",
+        "warning": "^4.0.3"
+      },
+      "peerDependencies": {
+        "react": ">=16.14.0",
+        "react-dom": ">=16.14.0"
+      }
+    },
     "node_modules/react-dev-utils": {
       "version": "12.0.0",
       "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.0.tgz",
@@ -13284,6 +13442,11 @@
       "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
       "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
     },
+    "node_modules/react-lifecycles-compat": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
+      "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
+    },
     "node_modules/react-redux": {
       "version": "7.2.6",
       "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.6.tgz",
@@ -13443,6 +13606,21 @@
         }
       }
     },
+    "node_modules/react-transition-group": {
+      "version": "4.4.2",
+      "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.2.tgz",
+      "integrity": "sha512-/RNYfRAMlZwDSr6z4zNKV6xu53/e2BuaBbGhbyYIXTrmgu/bGHzmqOs7mJSJBHy9Ud+ApHx3QjrkKSp1pxvlFg==",
+      "dependencies": {
+        "@babel/runtime": "^7.5.5",
+        "dom-helpers": "^5.0.1",
+        "loose-envify": "^1.4.0",
+        "prop-types": "^15.6.2"
+      },
+      "peerDependencies": {
+        "react": ">=16.6.0",
+        "react-dom": ">=16.6.0"
+      }
+    },
     "node_modules/readable-stream": {
       "version": "3.6.0",
       "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
@@ -15173,6 +15351,20 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/uncontrollable": {
+      "version": "7.2.1",
+      "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz",
+      "integrity": "sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==",
+      "dependencies": {
+        "@babel/runtime": "^7.6.3",
+        "@types/react": ">=16.9.11",
+        "invariant": "^2.2.4",
+        "react-lifecycles-compat": "^3.0.4"
+      },
+      "peerDependencies": {
+        "react": ">=15.0.0"
+      }
+    },
     "node_modules/unicode-canonical-property-names-ecmascript": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz",
@@ -15378,6 +15570,14 @@
         "makeerror": "1.0.12"
       }
     },
+    "node_modules/warning": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
+      "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
+      "dependencies": {
+        "loose-envify": "^1.0.0"
+      }
+    },
     "node_modules/watchpack": {
       "version": "2.3.1",
       "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.3.1.tgz",
@@ -18119,6 +18319,44 @@
         }
       }
     },
+    "@popperjs/core": {
+      "version": "2.11.2",
+      "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.2.tgz",
+      "integrity": "sha512-92FRmppjjqz29VMJ2dn+xdyXZBrMlE42AV6Kq6BwjWV7CNUW1hs2FtxSNLQE+gJhaZ6AAmYuO9y8dshhcBl7vA=="
+    },
+    "@react-aria/ssr": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.1.0.tgz",
+      "integrity": "sha512-RxqQKmE8sO7TGdrcSlHTcVzMP450hqowtBSd2bBS9oPlcokVkaGq28c3Rwa8ty5ctw4EBCjXqjP7xdcKMGDzug==",
+      "requires": {
+        "@babel/runtime": "^7.6.2"
+      }
+    },
+    "@restart/hooks": {
+      "version": "0.4.5",
+      "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.5.tgz",
+      "integrity": "sha512-tLGtY0aHeIfT7aPwUkvQuhIy3+q3w4iqmUzFLPlOAf/vNUacLaBt1j/S//jv/dQhenRh8jvswyMojCwmLvJw8A==",
+      "requires": {
+        "dequal": "^2.0.2"
+      }
+    },
+    "@restart/ui": {
+      "version": "0.2.5",
+      "resolved": "https://registry.npmjs.org/@restart/ui/-/ui-0.2.5.tgz",
+      "integrity": "sha512-3dP8pMFickPpvAG5MVQW53HnJl0c17h7MwvI4nNy9QF66sHSYVchudlqlI8eOSaqnmc5YVjGura63vMb9LTNbQ==",
+      "requires": {
+        "@babel/runtime": "^7.13.16",
+        "@popperjs/core": "^2.10.1",
+        "@react-aria/ssr": "^3.0.1",
+        "@restart/hooks": "^0.4.0",
+        "@types/warning": "^3.0.0",
+        "dequal": "^2.0.2",
+        "dom-helpers": "^5.2.0",
+        "prop-types": "^15.7.2",
+        "uncontrollable": "^7.2.1",
+        "warning": "^4.0.3"
+      }
+    },
     "@rollup/plugin-babel": {
       "version": "5.3.0",
       "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.0.tgz",
@@ -18616,6 +18854,11 @@
         "@types/node": "*"
       }
     },
+    "@types/invariant": {
+      "version": "2.2.35",
+      "resolved": "https://registry.npmjs.org/@types/invariant/-/invariant-2.2.35.tgz",
+      "integrity": "sha512-DxX1V9P8zdJPYQat1gHyY0xj3efl8gnMVjiM9iCY6y27lj+PoQWkgjt8jDqmovPqULkKVpKRg8J36iQiA+EtEg=="
+    },
     "@types/istanbul-lib-coverage": {
       "version": "2.0.3",
       "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz",
@@ -18717,6 +18960,14 @@
         "redux": "^4.0.0"
       }
     },
+    "@types/react-transition-group": {
+      "version": "4.4.4",
+      "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.4.tgz",
+      "integrity": "sha512-7gAPz7anVK5xzbeQW9wFBDg7G++aPLAFY0QaSMOou9rJZpbuI58WAuJrgu+qR92l61grlnCUe7AFX8KGahAgug==",
+      "requires": {
+        "@types/react": "*"
+      }
+    },
     "@types/resolve": {
       "version": "1.17.1",
       "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz",
@@ -18778,6 +19029,11 @@
       "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.2.tgz",
       "integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg=="
     },
+    "@types/warning": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.0.tgz",
+      "integrity": "sha1-DSUBJorY+ZYrdA04fEZU9fjiPlI="
+    },
     "@types/ws": {
       "version": "8.2.2",
       "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.2.2.tgz",
@@ -19684,6 +19940,12 @@
       "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
       "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24="
     },
+    "bootstrap": {
+      "version": "5.1.3",
+      "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.1.3.tgz",
+      "integrity": "sha512-fcQztozJ8jToQWXxVuEyXWW+dSo8AiXWKwiSSrKWsRB/Qt+Ewwza+JWoLKiTuQLaEPhdNAJ7+Dosc9DOIqNy7Q==",
+      "requires": {}
+    },
     "brace-expansion": {
       "version": "1.1.11",
       "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@@ -19860,6 +20122,11 @@
       "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz",
       "integrity": "sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA=="
     },
+    "classnames": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz",
+      "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA=="
+    },
     "clean-css": {
       "version": "5.2.2",
       "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.2.2.tgz",
@@ -20497,6 +20764,11 @@
       "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
       "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
     },
+    "dequal": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.2.tgz",
+      "integrity": "sha512-q9K8BlJVxK7hQYqa6XISGmBZbtQQWVXSrRrWreHC94rMt1QL/Impruc+7p2CYSYuVIUr+YCt6hjrs1kkdJRTug=="
+    },
     "destroy": {
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
@@ -20612,6 +20884,15 @@
         "utila": "~0.4"
       }
     },
+    "dom-helpers": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
+      "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
+      "requires": {
+        "@babel/runtime": "^7.8.7",
+        "csstype": "^3.0.2"
+      }
+    },
     "dom-serializer": {
       "version": "1.3.2",
       "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz",
@@ -22350,6 +22631,14 @@
         "side-channel": "^1.0.4"
       }
     },
+    "invariant": {
+      "version": "2.2.4",
+      "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
+      "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
+      "requires": {
+        "loose-envify": "^1.0.0"
+      }
+    },
     "ip": {
       "version": "1.1.5",
       "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz",
@@ -25509,6 +25798,22 @@
         }
       }
     },
+    "prop-types-extra": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/prop-types-extra/-/prop-types-extra-1.1.1.tgz",
+      "integrity": "sha512-59+AHNnHYCdiC+vMwY52WmvP5dM3QLeoumYuEyceQDi9aEhtwN9zIQ2ZNo25sMyXnbh32h+P1ezDsUpUH3JAew==",
+      "requires": {
+        "react-is": "^16.3.2",
+        "warning": "^4.0.0"
+      },
+      "dependencies": {
+        "react-is": {
+          "version": "16.13.1",
+          "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+          "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
+        }
+      }
+    },
     "proxy-addr": {
       "version": "2.0.7",
       "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@@ -25629,6 +25934,29 @@
         "whatwg-fetch": "^3.6.2"
       }
     },
+    "react-bootstrap": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-2.1.0.tgz",
+      "integrity": "sha512-ZbEq8in6XzoDh4dyrANVwqeqrj1oRqL56unlBYzjsvdWPIaBP/B7qLMHCvnwuzpANwMrh/hSNpIocKn6rvOFIQ==",
+      "requires": {
+        "@babel/runtime": "^7.14.0",
+        "@restart/hooks": "^0.4.5",
+        "@restart/ui": "^0.2.5",
+        "@types/invariant": "^2.2.33",
+        "@types/prop-types": "^15.7.3",
+        "@types/react": ">=16.14.8",
+        "@types/react-transition-group": "^4.4.1",
+        "@types/warning": "^3.0.0",
+        "classnames": "^2.3.1",
+        "dom-helpers": "^5.2.1",
+        "invariant": "^2.2.4",
+        "prop-types": "^15.7.2",
+        "prop-types-extra": "^1.1.0",
+        "react-transition-group": "^4.4.1",
+        "uncontrollable": "^7.2.1",
+        "warning": "^4.0.3"
+      }
+    },
     "react-dev-utils": {
       "version": "12.0.0",
       "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.0.tgz",
@@ -25735,6 +26063,11 @@
       "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
       "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
     },
+    "react-lifecycles-compat": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
+      "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
+    },
     "react-redux": {
       "version": "7.2.6",
       "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.6.tgz",
@@ -25859,6 +26192,17 @@
         "workbox-webpack-plugin": "^6.4.1"
       }
     },
+    "react-transition-group": {
+      "version": "4.4.2",
+      "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.2.tgz",
+      "integrity": "sha512-/RNYfRAMlZwDSr6z4zNKV6xu53/e2BuaBbGhbyYIXTrmgu/bGHzmqOs7mJSJBHy9Ud+ApHx3QjrkKSp1pxvlFg==",
+      "requires": {
+        "@babel/runtime": "^7.5.5",
+        "dom-helpers": "^5.0.1",
+        "loose-envify": "^1.4.0",
+        "prop-types": "^15.6.2"
+      }
+    },
     "readable-stream": {
       "version": "3.6.0",
       "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
@@ -27147,6 +27491,17 @@
         "which-boxed-primitive": "^1.0.2"
       }
     },
+    "uncontrollable": {
+      "version": "7.2.1",
+      "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz",
+      "integrity": "sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==",
+      "requires": {
+        "@babel/runtime": "^7.6.3",
+        "@types/react": ">=16.9.11",
+        "invariant": "^2.2.4",
+        "react-lifecycles-compat": "^3.0.4"
+      }
+    },
     "unicode-canonical-property-names-ecmascript": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz",
@@ -27310,6 +27665,14 @@
         "makeerror": "1.0.12"
       }
     },
+    "warning": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
+      "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
+      "requires": {
+        "loose-envify": "^1.0.0"
+      }
+    },
     "watchpack": {
       "version": "2.3.1",
       "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.3.1.tgz",

+ 3 - 1
js/24_react_shop/react_shop/package.json

@@ -1,12 +1,14 @@
 {
-  "name": "hw",
+  "name": "shop",
   "version": "0.1.0",
   "private": true,
   "dependencies": {
     "@testing-library/jest-dom": "^5.16.1",
     "@testing-library/react": "^12.1.2",
     "@testing-library/user-event": "^13.5.0",
+    "bootstrap": "^5.1.3",
     "react": "^17.0.2",
+    "react-bootstrap": "^2.1.0",
     "react-dom": "^17.0.2",
     "react-redux": "^7.2.6",
     "react-router-dom": "^5.3.0",

+ 812 - 3
js/24_react_shop/react_shop/src/App.js

@@ -1,21 +1,830 @@
-import React, {useState, useEffect, useRef} from 'react'
+import React, {useState, useEffect, useRef, Component} from 'react';
 import logoDefault from './logo.svg';
+import cartImg from './cart.svg';
+import 'bootstrap/dist/css/bootstrap.min.css';
 import './App.scss';
 import {Provider, connect}   from 'react-redux';
 import {createStore, combineReducers, applyMiddleware} from 'redux';
 import thunk from 'redux-thunk';
+import {Router, Route, Link, Redirect, Switch} from 'react-router-dom';
+import createHistory from "history/createBrowserHistory";
 
 
+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"
+              }
+          ]
+      }
+  ]
+} 
 
 
-function App() {
+
+function jwtDecode(token) {
+  try {
+      let decoded = JSON.parse(atob(token.split('.')[1])) 
+      return decoded
+  } catch (err) {
+      console.log(err)
+  }
+}
+
+function authReducer(state, {type, token}) {
+  if (!state) {
+      if (localStorage.authToken) {
+          token = localStorage.authToken
+          type = 'AUTH_LOGIN'
+      } else {
+          return {}
+      }
+  }
+  if (type === 'AUTH_LOGIN') {
+      let payload = jwtDecode(token)
+      if (typeof payload === 'object') {
+          localStorage.authToken = token
+          return {
+              ...state,
+              token, 
+              payload
+          }
+      } else {
+          return state
+      }
+  }
+  if (type === 'AUTH_LOGOUT') {
+      delete localStorage.authToken
+      return {}
+  }
+  return state
+}
+
+const actionAuthLogin = (token) => ({type: 'AUTH_LOGIN', token})
+const actionAuthLogout = () => ({type: 'AUTH_LOGOUT'})
+
+
+
+
+function cartReducer (state={}, {type, good={}, count=1}) {
+
+  if (Object.keys(state).length === 0 && localStorage.cart) {
+      let currCart = JSON.parse(localStorage.cart)
+      if (currCart && Object.keys(currCart).length !== 0) {
+          state = currCart
+      } 
+  }
+
+  const {_id} = good
+
+  const types = {
+      CART_ADD() {
+          count = +count
+          if (!count) {
+              return state
+          }
+          let newState = {
+              ...state,
+              [_id]: {good, count: (count + (state[_id]?.count || 0)) < 1 ? 1 : count + (state[_id]?.count || 0)}
+          }
+          localStorage.cart = JSON.stringify(newState) 
+          return newState
+      },
+      CART_CHANGE() {
+          count = +count
+          let newState = null
+          // if (!count) {
+          //   return state
+          // } 
+          newState = {
+            ...state,
+            [_id]: {good, count: count < 1 ? 1 : count}
+          }
+          localStorage.cart = JSON.stringify(newState) 
+          return newState
+      },
+      CART_REMOVE() {           
+          let { [_id]: removed, ...newState }  = state
+          localStorage.cart = JSON.stringify(newState) 
+          return newState
+      },
+      CART_CLEAR() {
+          localStorage.cart = JSON.stringify({}) 
+          return {}
+      },
+  }
+  if (type in types) {        
+      return types[type]()
+  }
+  return state
+}
+
+const actionCartAdd = (good, count) => ({type: 'CART_ADD', good, count})
+const actionCartChange = (good, count) => ({type: 'CART_CHANGE', good, count})
+const actionCartRemove = (good) => ({type: 'CART_REMOVE', good})
+const actionCartClear = () => ({type: 'CART_CLEAR'})
+
+
+
+
+function promiseReducer(state={}, {type, status, payload, error, name}) {
+  if (!state) {
+      return {}
+  }
+  if (type === 'PROMISE') {         
+      return {
+          ...state,
+          [name]: {
+              status: status,
+              payload : payload,
+              error: 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",
+          ...(localStorage.authToken ? {Authorization: "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))
+      } else {
+          return a.data[Object.keys(a.data)[0]]
+      }      
+  }
+)
+
+  const backURL = 'http://shop-roles.asmer.fs.a-level.com.ua/'
+  const gql = getGQL(backURL + 'graphql')
+
+
+
+
+
+  const actionOrder = () => (
+    async (dispatch, getState) => {
+        let {cart} = getState()
+        const orderGoods = Object.entries(cart)
+                            .map(([_id, {good, 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(actionCartClear())
+            }
+    }
+  )
+    
+    
+  
+  
+  const actionLogin = (login, password) => (
+      actionPromise('login', gql(`query log($login: String, $password: String) {
+          login(login: $login, password: $password)
+      }`, {login, password}))
+  )
+  
+  const actionFullLogin = (login, password) => (
+      async (dispatch) => {
+          let token = await dispatch(actionLogin(login, password))
+          if (token) {
+              dispatch(actionAuthLogin(token))
+          }
+      }
+  )
+    
+  const actionRegister = (login, password) => (
+      actionPromise('register', gql(`mutation reg($user:UserInput) {
+          UserUpsert(user:$user) {
+          _id 
+          }
+      }
+      `, {user: {login, password}})      
+      )
+  )
+  
+  const actionFullRegister = (login, password) => (
+      async (dispatch) => {
+          let regId = await dispatch(actionRegister(login, password))
+          if (regId) {
+              dispatch(actionFullLogin(login, password))
+          }
+      }
+  )
+  
+  
+  
+  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
+                  }
+              }
+              subCategories {
+                  _id name 
+              }
+          }
+      }`, {q: JSON.stringify([{_id}])}))
+  )
+  
+  const actionGoodById = (_id) => (
+      actionPromise('goodById', gql(`query goodById($q: String) {
+          GoodFindOne(query: $q) {
+              _id name price description images {
+              url
+              }
+          }
+      }`, {q: JSON.stringify([{_id}])}))
+  )
+  
+  const actionGoodsByUser = (_id) => (
+      actionPromise('goodByUser', gql(`query oUser($query: String) {
+          OrderFind(query:$query){
+          _id orderGoods{
+                  price count total good{
+                      _id name categories{
+                      name
+                      }
+                      images {
+                          url
+                      }
+                  }
+              } 
+              owner {
+              _id login
+              }
+          }
+      }`, 
+      {query: JSON.stringify([{___owner: _id}])}))
+  )
+  
+  
+  const actionGoodFind = (word) => (
+      actionPromise('goodFind', gql(`query goodById($q: String) {
+          GoodFind(query: $q) {
+              _id name price description images {
+              url
+              }
+          }
+      }`, {q: JSON.stringify([
+              {
+                  $or: [{title: `/${word}/`}, {description: `/${word}/`}, {name: `/${word}/`}] 
+              },
+              {
+                  sort: [{title: 1}]  
+              } 
+              ])
+          }
+      ))
+  )
+
+
+
+
+const store = createStore(  combineReducers({ promise: promiseReducer, 
+                                              auth: authReducer, 
+                                              cart: cartReducer}),
+                            applyMiddleware(thunk))
+store.subscribe(() => console.log(store.getState()))
+
+
+store.dispatch(actionRootCats())
+
+
+
+
+
+
+
+
+const Logo = ({logo=logoDefault}) => (
+  <Link to='/' className="Logo">
+      <img src={logo} />
+  </Link>
+)
+
+
+const Koshik = ({cart}) => {
+    let count = 0;
+    let sum = Object.entries(cart).map(([, val]) => val.count)
+    count = sum.reduce((a, b) => a + b, 0)
+    //перебрать cart, посчитать суммарный count
+    return (
+        <Link to='/cart' className='Koshik'>
+          <img src={cartImg} />          
+          {count}
+        </Link>
+    )
+}
+const CKoshik = connect(({cart}) => ({cart}))(Koshik)
+
+
+
+const GreetContainer = ({user: {id, login}}) => {
+  if (id && login) {
+    return (
+      <div>
+        <div>
+          ПРИВЕТ, {login}
+        </div>
+        <Link to={`/orders/${id}`} >
+          Мои заказы
+        </Link>
+      </div>
+    )
+  } else {
+    return (<></>)
+  }
+}
+const CGreetContainer = connect(state => ({user: state.auth.payload?.sub || {}}))(GreetContainer)
+
+
+const Header = ({logo=logoDefault}) => (
+  <header className="bg-light">
+    <div className="leftBlock">
+      <Logo logo={logo} />
+    </div>
+    <div className="rightBlock">
+      <CKoshik />
+      <CGreetContainer />
+    </div>
+  </header>
+)
+
+
+
+const RootCategory = ({cat:{_id, name}={}}) => (
+    <Link  to={`/category/${_id}`} 
+          className="catBtn list-group-item list-group-item-action list-group-item-light">
+      {name}
+    </Link>
+)
+
+const RootCategories = ({cats=defaultRootCats}) => (
+  <div className='RootCategories list-group linkList'>
+      {cats.map(cat => <RootCategory cat={cat} />)}
+  </div>
+)
+const CRootCategories = connect(state => ({cats: state.promise.rootCats?.payload || []}))(RootCategories)
+
+
+const LogoutBTn = ({onLogout}) => (
+  <button className='logoutBtn btn btn-primary'
+          onClick={() => onLogout()}>
+    Logout
+  </button>
+)
+const CLogoutBTn = connect(null, {onLogout: actionAuthLogout})(LogoutBTn)
+
+
+const Aside = () => (
+  <aside className="bg-light">
+      <div className='LoginBlock'>
+        <Link to='/login' className='loginBtn btn btn-primary'>Login</Link>
+        <Link to='/register' className='registerBtn btn btn-primary'>Register</Link>
+        <CLogoutBTn />
+      </div>
+      <CRootCategories />
+  </aside>
+)
+
+const Content = ({children}) => (
+  <div className='Content'>
+      {children}
+  </div>
+)
+
+
+
+const SubCategory = ({cat:{_id, name}={}}) => ( 
+  <Link to={`/category/${_id}`}
+        className='list-group-item list-group-item-action list-group-item-primary linkItem'>
+          {name}
+  </Link>
+)
+
+const SubCategories = ({cats}) => (
+  <div className="CatsList list-group linkList">
+    {cats.map(cat => <SubCategory cat={cat} />)}
+  </div>
+)
+
+
+const GoodCard = ({good:{_id, name, price, images}={}, onCartAdd}) => (
+  <div className='GoodCard card'>      
+      {images && images[0] && images[0].url && 
+      <img className='card-img-top' src={backURL + '/' + images[0].url} />}
+      <div className="card-body">
+        <h4 className="card-title">{name}</h4>
+        <h5>{price} грн</h5>
+        <button className='btn btn-primary'
+              onClick={() => onCartAdd({_id, name, price, images})}>В корзину</button>
+        <Link className='btn btn-success'
+              to={`/good/${_id}`}>Подробнее</Link>
+      </div>
+  </div>
+)
+const CGoodCard = connect(null, {onCartAdd: actionCartAdd})(GoodCard)
+
+const Category = ({cat:{_id, name, goods, subCategories}=defaultCat}) => (
+  <div className='Category'>
+      <h1>{name}</h1>
+      {subCategories && <SubCategories cats={subCategories} />}
+      <div className='GoodCards'>
+        {(goods || []).map(good => <CGoodCard good={good}/>)}
+      </div>      
+  </div>
+)
+const CCategory = connect(state => ({cat: state.promise.catById?.payload}))(Category)
+
+
+
+
+const GoodInCart = ({item: {count, good: {_id, name, price, images}}, onCartChange, onCartRemove, onCartAdd}) => {
+  const [goodCount, setGoodCount] = useState(count)
+
+  return (
+    <div className='CartCard card'>
+        <div className="card-header">
+          <h4 className="card-title">{name}</h4>
+        </div>
+        {images && images[0] && images[0].url && 
+        <img className="card-img-top" src={backURL + '/' + images[0].url} />}
+        <div className="card-body">
+          <p>{count} шт</p>
+          <p>{price} грн</p>   
+          <h6>Итого: {count*price} грн</h6>  
+          <button className="btn btn-success"
+                  onClick={() => {  setGoodCount((count === 1 ) ? 1 : count - 1); 
+                                    onCartAdd({_id, name, price, images}, -1) }}>-</button>
+          <input  onBlur={() => onCartChange({_id, name, price, images}, goodCount)}  
+                  onInput={(e) => setGoodCount(e.currentTarget.value)}
+                  // onInput={(e) => onCartChange({_id, name, price, images}, e.currentTarget.value)}
+                  value={goodCount} type="number"/>
+          <button className="btn btn-success"
+                  onClick={() => {  setGoodCount(count + 1); 
+                                    onCartAdd({_id, name, price, images}) }}>+</button>
+          <button className="btn btn-success delBtn"
+                  onClick={() => onCartRemove({_id, name, price, images})}>Удалить</button>
+        </div>
+    </div>
+  )
+} 
+const CGoodInCart = connect(null, { onCartChange: actionCartChange, 
+                                    onCartRemove: actionCartRemove, 
+                                    onCartAdd: actionCartAdd})(GoodInCart)
+
+
+const Cart = ({cart, user, onCartClear, onOrderSend}) => {
+  let total = 0
+  if (cart.length !== 0) {
+    let countes = Object.entries(cart).map(([, val]) => val.count)
+    let prices = Object.entries(cart).map(([, val]) => val.good.price)
+    let sum = countes.map((count, index) => prices[index] * count)
+      total = sum.reduce((a, b) => a + b)
+  }
+  return (  
+    <div className='Cart'>
+      {cart.length === 0 ? <h5>Корзина пуста</h5> :  
+      <button className='btn btn-primary'
+              onClick={() => onCartClear()}>
+        Очистить корзину
+      </button>} 
+      <div className='CartBlock'>
+        {cart.map(item => <CGoodInCart item={item}/>)}  
+      </div>
+      {cart.length === 0 ? <></> :  
+      <>
+        <h5>Всего к оплате: {total} грн</h5>
+        <button className='btn btn-primary'
+                onClick={() => onOrderSend()}
+                disabled={user ? false : true}>
+          Оформить заказ
+        </button>
+      </>} 
+    </div>
+  )
+}
+const CCart = connect(state => ({ cart: Object.values(state.cart) || [], 
+                                  user: state.auth.payload || null}), 
+                                { onCartClear: actionCartClear, 
+                                  onOrderSend: actionOrder})(Cart)
+
+
+const LoginForm = ({onLogin}) => {
+  const [login, setLogin] = useState("")
+  const [pass, setPass] = useState("")
+
+  return (
+    <div className='LoginForm form'>
+      <div className="mb-3">
+        <label className="form-label">Логин</label>
+        <input  className="form-input form-control"
+                type="text"style={ {backgroundColor: (login.length === 0) ? '#f00b' : '#fff'} }
+                value={login} onChange={(e) => setLogin(e.target.value)}/>
+      </div>
+      <div className="mb-3">
+        <label className="form-label">Пароль</label>
+        <input  className="form-input form-control" 
+                type="password" style={ {backgroundColor: (pass.length === 0) ? '#f00b' : '#fff'} }
+                value={pass} onChange={(e) => setPass(e.target.value)}/>
+      </div>
+      <button className="btn btn-primary"
+              onClick={() => {  onLogin(login,pass);
+                                setPass('')
+                              }} 
+              disabled={(login.length !== 0 && pass.length !== 0) ? false : true}>Войти</button>
+    </div>
+  )
+}
+const CLoginForm = connect(null, {onLogin: actionFullLogin})(LoginForm)
+
+
+
+const RegisterForm = ({onRegister}) => {
+  const [login, setLogin] = useState("")
+  const [pass, setPass] = useState("")
+
+  return (
+    <div className='RegisterForm form'>
+      <div className="mb-3">
+        <label className="form-label">Логин</label>
+        <input  className="form-input form-control" 
+                type="text" style={ {backgroundColor: (login.length === 0) ? '#f00b' : '#fff'} }
+                value={login} onChange={(e) => setLogin(e.target.value)}/>
+      </div>
+      <div className="mb-3">
+        <label className="form-label">Пароль</label>
+        <input  className="form-input form-control" 
+                type="password" style={ {backgroundColor: (pass.length === 0) ? '#f00b' : '#fff'} }
+                value={pass} onChange={(e) => setPass(e.target.value)}/>
+      </div>
+      <button className="btn btn-primary"
+              onClick={() => {  onRegister(login,pass);
+                                setPass('')
+                              }} 
+              disabled={(login.length !== 0 && pass.length !== 0) ? false : true}>Зарегистрироваться</button>
+    </div>
+  )
+}
+const CRegisterForm = connect(null, {onRegister: actionFullRegister})(RegisterForm)
+
+
+
+const PageMain = () => (
+  <h1>Главная страница</h1>
+)
+
+const PageCategory = ({match:{params:{_id}}, getData, history}) => {
+  useEffect(() => {
+      getData(_id)
+  },[_id])
+
+  return (
+      <CCategory />
+  )
+}
+const CPageCategory = connect(null, {getData: actionCatById})(PageCategory)
+
+
+
+
+
+const ThisGood = ({good:{_id, name, price, images, description}={}, onCartAdd}) => (
+  <div className='GoodPage'>
+      <h2>{name}</h2>
+      {images && images[0] && images[0].url && <img src={backURL + '/' + images[0].url} />}
+      <div>
+        <h6>{description}</h6>
+        <strong>Цена - {price} грн</strong>
+      </div>
+      <button className="btn btn-success"
+              onClick={() => onCartAdd({_id, name, price, images})}>В корзину</button>
+  </div>
+)
+const CThisGood = connect(  state => ({good: state.promise.goodById?.payload}), 
+                            {onCartAdd: actionCartAdd})(ThisGood)
+
+
+const PageGood = ({match:{params:{_id}}, getData}) => {
+  useEffect(() => {
+      getData(_id)
+  },[_id])
+
   return (
     <>
+      <CThisGood />
+    </>
+  )
+}
+const CPageGood = connect(null, {getData: actionGoodById})(PageGood)
+
+
+// на текущий момент функция отображает только 100 последних заказов
+// подсчет потраченных средств нужно реализовать для всех заказов
+// но при этом сразу отображать только 100 последних
+const OrderedGood = ({goodOrder:{price, count, total, good}={}}) => {
+  if (price !== null && count !== null && total !== null && good !== null) {
+    const {_id, name, images} = good  
+    return (
+      <div className='OrderCard card'>
+          {images && images[0] && images[0].url && 
+            <img className='card-img-top' src={backURL + '/' + images[0].url} />}
+          <div className="card-body">
+            <h4 className="card-title">{name}</h4>
+            <h6>
+                Куплено: {count} по {price} грн. 
+            </h6>  
+            <h6>
+                Итого: {total} грн
+            </h6>  
+            <Link className="btn btn-primary"
+                  to={`/good/${_id}`}>Подробнее</Link>
+          </div>
+      </div>
+    )
+  } else {
+    return <></>
+  }
+}
+
+const Orders = ({orders}) => (
+  <div className='Orders'>
+    {/* <h3>Всего потрачено: {totalMoney} грн</h3> */}
+    <div className='OrderCards'>
+      { orders.map(order => 
+        (order.orderGoods ? order.orderGoods: []).map(goodOrder => 
+            <OrderedGood goodOrder={goodOrder}/>))}
+    </div>
+  </div>
+)
+const COrders = connect(state => ({orders: state.promise.goodByUser?.payload || []}))(Orders)
 
 
+const PageOrders = ({match:{params:{_id}}, getData}) => {
+  useEffect(() => {
+      getData(_id)
+  },[_id])
+
+  return (
+    <>
+      <COrders />
     </>
-  );
+  )
+}
+const CPageOrders = connect(null, {getData: actionGoodsByUser})(PageOrders)
+
+
+
+const Page404 = () => (
+  <h1>PAGE НЭМА </h1>
+)
+
+const Main = () => (
+  <main>
+      <Aside />
+      <Content>
+        <Redirect from="/main" to="/" />
+        <Switch>
+          <Route path="/" component={PageMain} exact />
+          <Route path="/category/:_id" component={CPageCategory} />
+          <Route path="/good/:_id" component={CPageGood} />
+          <Route path="/cart" component={CCart} />
+          <Route path="/login" component={CLoginForm} />
+          <Route path="/register" component={CRegisterForm} />
+          <Route path="/orders/:_id" component={CPageOrders} />
+          <Route path="*" component={Page404} />
+        </Switch>
+      </Content>
+  </main>
+)
+
+
+const Footer = ({logo=logoDefault}) => (
+  <footer>
+      <Logo logo={logo} />
+  </footer>
+)
+
+
+
+const history = createHistory()
+
+function App() {
+  return (
+      <Router history={history}>
+        <Provider store={store}>
+          <div className="App">
+            <Header /> 
+
+            <Main />          
+
+            <Footer />
+          </div>
+        </Provider>
+      </Router>
+  )
 }
 
 export default App;

+ 188 - 0
js/24_react_shop/react_shop/src/App.scss

@@ -0,0 +1,188 @@
+
+ 
+ .App {
+   display: flex;
+   flex-direction: column;
+   justify-content: space-between;
+   min-height: 100vh;
+
+   header {
+      display: flex;
+      align-items: center;
+      height: 80px;
+
+      .leftBlock {
+         flex: 0 1 10%;
+         padding: 0 20px;
+
+         .Logo {
+            img {
+              max-height: 100px;
+            }
+          }
+      }
+      .rightBlock {
+         display: flex;
+         justify-content: space-between;
+         align-items: center;
+         height: 100%;
+         flex: 0 1 90%;
+         padding: 0 20px;
+
+         .Koshik {
+            img {
+               padding: 0 5px;
+            }
+         }
+      }
+   }
+
+   main {
+     display: flex;
+     flex-direction: row;
+     min-height: calc(100vh - 200px);
+
+     aside {
+       flex: 0 1 30%;
+       padding: 0 10px;
+
+       .LoginBlock {
+         padding: 5px 0;
+         border-top: 1px solid #ddd;
+
+         .btn {
+            margin: 10px;
+         }
+       }
+
+       .RootCategories {
+         padding: 10px 0;
+       }
+     }
+
+     .Content {
+         flex: 0 1 70%;
+         padding: 0 5px;
+         display: flex;
+         flex-direction: column;
+         align-items: center;
+
+         .form {
+            width: max-content;
+            margin: 50px auto;
+         }
+
+         .Cart {
+            display: flex;
+            flex-direction: column;
+            align-items: center;
+            padding: 20px 0;
+
+            .CartBlock {
+               width: 100%;
+               display: flex;
+               flex-wrap: wrap;
+               justify-content: center;
+               align-items: stretch;
+   
+               .CartCard {
+                  flex: 0 1 50%;
+                  margin: 10px;
+
+                  .btn {
+                     margin: 5px;
+                  }
+
+                  .delBtn {
+                     display: block;
+                     margin: 10px 0 10px auto;
+                  }
+
+                  input {
+                     max-width: 50%;
+                  }
+               }
+            }
+         }
+
+
+         .Category {
+            display: flex;
+            flex-direction: column;
+            align-items: center;
+            padding: 20px 0;
+
+            .CatsList {
+               width: 50%;
+            }
+
+            .GoodCards {
+               width: 100%;
+               display: flex;
+               flex-wrap: wrap;
+               justify-content: center;
+               align-items: start;
+
+               .GoodCard {
+                  flex: 0 1 25%;
+                  padding: 10px;
+                  margin: 20px;
+
+                  .btn {
+                     margin: 5px;
+                  }
+
+                  img {
+                     max-width: 300px;
+                  }
+               }
+            }   
+         }
+
+         .GoodPage {
+            padding: 10px;
+            margin: 20px;
+         }
+
+         .Orders {
+
+            .OrderCards {
+               width: 100%;
+               display: flex;
+               flex-wrap: wrap;
+               justify-content: center;
+               align-items: start;
+   
+               .OrderCard {
+                  flex: 0 1 25%;
+                  padding: 10px;
+                  margin: 20px;
+   
+                  .btn {
+                     margin: 5px;
+                  }
+   
+                  img {
+                     max-width: 300px;
+                  }
+               }
+            }
+         }
+
+      }
+   }
+
+ 
+   footer {
+     background-color: #303030;
+     height: 120px;
+     text-align: center;
+
+     .Logo {
+       img {
+         max-height: 120px;
+       }
+     }
+   }
+ }
+ 

+ 3 - 0
js/24_react_shop/react_shop/src/cart.svg

@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M6 16C4.9 16 4.01 16.9 4.01 18C4.01 19.1 4.9 20 6 20C7.1 20 8 19.1 8 18C8 16.9 7.1 16 6 16ZM0 0V2H2L5.6 9.59L4.25 12.04C4.09 12.32 4 12.65 4 13C4 14.1 4.9 15 6 15H18V13H6.42C6.28 13 6.17 12.89 6.17 12.75L6.2 12.63L7.1 11H14.55C15.3 11 15.96 10.59 16.3 9.97L19.88 3.48C19.96 3.34 20 3.17 20 3C20 2.45 19.55 2 19 2H4.21L3.27 0H0ZM16 16C14.9 16 14.01 16.9 14.01 18C14.01 19.1 14.9 20 16 20C17.1 20 18 19.1 18 18C18 16.9 17.1 16 16 16Z" fill="#474030"/>
+</svg>