App.js 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017
  1. import React, {useState, useEffect, useRef, Component, createRef} from 'react';
  2. import logoDefault from './logo.svg';
  3. import cartImg from './cart.svg';
  4. import 'bootstrap/dist/css/bootstrap.min.css';
  5. import './App.scss';
  6. import {Provider, connect} from 'react-redux';
  7. import {createStore, combineReducers, applyMiddleware} from 'redux';
  8. import thunk from 'redux-thunk';
  9. import createSagaMiddleware from 'redux-saga';
  10. import {all, takeEvery, put, call, take, select} from 'redux-saga/effects';
  11. import {Router, Route, Link, Redirect, Switch} from 'react-router-dom';
  12. import createHistory from "history/createBrowserHistory";
  13. const defaultRootCats = [
  14. {
  15. "_id": "5dc49f4d5df9d670df48cc64",
  16. "name": "Airconditions"
  17. },
  18. {
  19. "_id": "5dc458985df9d670df48cc47",
  20. "name": " Smartphones"
  21. },
  22. {
  23. "_id": "5dc4b2553f23b553bf354101",
  24. "name": "Крупная бытовая техника"
  25. },
  26. {
  27. "_id": "5dcac1b56d09c45440d14cf8",
  28. "name": "Макароны"
  29. }
  30. ]
  31. const defaultCat ={
  32. "subCategories": null,
  33. "_id": "5dc458985df9d670df48cc47",
  34. "name": " Smartphones",
  35. "goods": [
  36. {
  37. "_id": "61b105f9c750c12ba6ba4524",
  38. "name": "iPhone ",
  39. "price": 1200,
  40. "images": [
  41. {
  42. "url": "images/50842a3af34bfa28be037aa644910d07"
  43. }
  44. ]
  45. },
  46. {
  47. "_id": "61b1069ac750c12ba6ba4526",
  48. "name": "iPhone ",
  49. "price": 1000,
  50. "images": [
  51. {
  52. "url": "images/d12b07d983dac81ccad404582a54d8be"
  53. }
  54. ]
  55. },
  56. {
  57. "_id": "61b23f94c750c12ba6ba472a",
  58. "name": "name1",
  59. "price": 1214,
  60. "images": [
  61. {
  62. "url": null
  63. }
  64. ]
  65. },
  66. {
  67. "_id": "61b23fbac750c12ba6ba472c",
  68. "name": "smart",
  69. "price": 1222,
  70. "images": [
  71. {
  72. "url": "images/871f4e6edbf86c35f70b72dcdebcd8b2"
  73. }
  74. ]
  75. }
  76. ]
  77. }
  78. function jwtDecode(token) {
  79. try {
  80. let decoded = JSON.parse(atob(token.split('.')[1]))
  81. return decoded
  82. } catch (err) {
  83. console.log(err)
  84. }
  85. }
  86. function authReducer(state, {type, token}) {
  87. if (!state) {
  88. if (localStorage.authToken) {
  89. token = localStorage.authToken
  90. type = 'AUTH_LOGIN'
  91. } else {
  92. return {}
  93. }
  94. }
  95. if (type === 'AUTH_LOGIN') {
  96. let payload = jwtDecode(token)
  97. if (typeof payload === 'object') {
  98. localStorage.authToken = token
  99. return {
  100. ...state,
  101. token,
  102. payload
  103. }
  104. } else {
  105. return state
  106. }
  107. }
  108. if (type === 'AUTH_LOGOUT') {
  109. delete localStorage.authToken
  110. return {}
  111. }
  112. return state
  113. }
  114. const actionAuthLogin = (token) => ({type: 'AUTH_LOGIN', token})
  115. const actionAuthLogout = () => ({type: 'AUTH_LOGOUT'})
  116. function cartReducer (state={}, {type, good={}, count=1}) {
  117. if (Object.keys(state).length === 0 && localStorage.cart) {
  118. let currCart = JSON.parse(localStorage.cart)
  119. if (currCart && Object.keys(currCart).length !== 0) {
  120. state = currCart
  121. }
  122. }
  123. const {_id} = good
  124. const types = {
  125. CART_ADD() {
  126. count = +count
  127. if (!count) {
  128. return state
  129. }
  130. let newState = {
  131. ...state,
  132. [_id]: {good, count: (count + (state[_id]?.count || 0)) < 1 ? 1 : count + (state[_id]?.count || 0)}
  133. }
  134. localStorage.cart = JSON.stringify(newState)
  135. return newState
  136. },
  137. CART_CHANGE() {
  138. count = +count
  139. let newState = null
  140. // if (!count) {
  141. // return state
  142. // }
  143. newState = {
  144. ...state,
  145. [_id]: {good, count: count < 1 ? 1 : count}
  146. }
  147. localStorage.cart = JSON.stringify(newState)
  148. return newState
  149. },
  150. CART_REMOVE() {
  151. let { [_id]: removed, ...newState } = state
  152. localStorage.cart = JSON.stringify(newState)
  153. return newState
  154. },
  155. CART_CLEAR() {
  156. localStorage.cart = JSON.stringify({})
  157. return {}
  158. },
  159. }
  160. if (type in types) {
  161. return types[type]()
  162. }
  163. return state
  164. }
  165. const actionCartAdd = (good, count) => ({type: 'CART_ADD', good, count})
  166. const actionCartChange = (good, count) => ({type: 'CART_CHANGE', good, count})
  167. const actionCartRemove = (good) => ({type: 'CART_REMOVE', good})
  168. const actionCartClear = () => ({type: 'CART_CLEAR'})
  169. function routeReducer(state={}, {type, match}) {
  170. if (type === 'ROUTE') {
  171. return match
  172. }
  173. return state
  174. }
  175. function promiseReducer(state={}, {type, status, payload, error, name}) {
  176. if (!state) {
  177. return {}
  178. }
  179. if (type === 'PROMISE') {
  180. return {
  181. ...state,
  182. [name]: {
  183. status: status,
  184. payload : payload,
  185. error: error,
  186. }
  187. }
  188. }
  189. return state
  190. }
  191. const actionPending = (name) => ({type: 'PROMISE', status: 'PENDING', name})
  192. const actionResolved = (name, payload) => ({type: 'PROMISE', status: 'RESOLVED', name, payload})
  193. const actionRejected = (name, error) => ({type: 'PROMISE', status: 'REJECTED', name, error})
  194. const actionPromise = (name, promise) => (
  195. {type: 'PROMISE_START', name, promise}
  196. // async (dispatch) => {
  197. // dispatch(actionPending(name))
  198. // try {
  199. // let data = await promise
  200. // dispatch(actionResolved(name, data))
  201. // return data
  202. // }
  203. // catch(error){
  204. // dispatch(actionRejected(name, error))
  205. // }
  206. // }
  207. )
  208. const getGQL = url => (
  209. async (query, variables={}) => {
  210. let obj = await fetch(url, {
  211. method: 'POST',
  212. headers: {
  213. "Content-Type": "application/json",
  214. ...(localStorage.authToken ? {Authorization: "Bearer " + localStorage.authToken} : {})
  215. },
  216. body: JSON.stringify({ query, variables })
  217. })
  218. let a = await obj.json()
  219. if (!a.data && a.errors) {
  220. throw new Error(JSON.stringify(a.errors))
  221. } else {
  222. return a.data[Object.keys(a.data)[0]]
  223. }
  224. }
  225. )
  226. const backURL = 'http://shop-roles.asmer.fs.a-level.com.ua/'
  227. const gql = getGQL(backURL + 'graphql')
  228. const actionOrder = () => (
  229. async (dispatch, getState) => {
  230. let {cart} = getState()
  231. const orderGoods = Object.entries(cart)
  232. .map(([_id, {good, count}]) => ({good: {_id}, count}))
  233. let result = await dispatch(actionPromise('order', gql(`
  234. mutation newOrder($order:OrderInput){
  235. OrderUpsert(order:$order)
  236. { _id total}
  237. }
  238. `, {order: {orderGoods}})))
  239. if (result?._id) {
  240. dispatch(actionCartClear())
  241. }
  242. }
  243. )
  244. const actionLogin = (login, password) => (
  245. actionPromise('login', gql(`query log($login: String, $password: String) {
  246. login(login: $login, password: $password)
  247. }`, {login, password}))
  248. )
  249. const actionFullLogin = (login, password) => (
  250. {type: 'FULL_LOGIN', login, password}
  251. // async (dispatch) => {
  252. // let token = await dispatch(actionLogin(login, password))
  253. // if (token) {
  254. // dispatch(actionAuthLogin(token))
  255. // }
  256. // }
  257. )
  258. const actionRegister = (login, password) => (
  259. actionPromise('register', gql(`mutation reg($user:UserInput) {
  260. UserUpsert(user:$user) {
  261. _id
  262. }
  263. }
  264. `, {user: {login, password}})
  265. )
  266. )
  267. const actionFullRegister = (login, password) => (
  268. {type: 'FULL_REGISTER', login, password}
  269. // async (dispatch) => {
  270. // let regId = await dispatch(actionRegister(login, password))
  271. // if (regId) {
  272. // dispatch(actionFullLogin(login, password))
  273. // }
  274. // }
  275. )
  276. const actionRootCats = () => (
  277. actionPromise('rootCats', gql(`query {
  278. CategoryFind(query: "[{\\"parent\\":null}]"){
  279. _id name
  280. }
  281. }`))
  282. )
  283. const actionCatById = (_id) => (
  284. actionPromise('catById', gql(`query catById($q: String){
  285. CategoryFindOne(query: $q){
  286. _id name goods {
  287. _id name price images {
  288. url
  289. }
  290. }
  291. subCategories {
  292. _id name
  293. }
  294. }
  295. }`, {q: JSON.stringify([{_id}])}))
  296. )
  297. const actionGoodById = (_id) => (
  298. actionPromise('goodById', gql(`query goodById($q: String) {
  299. GoodFindOne(query: $q) {
  300. _id name price description images {
  301. url
  302. }
  303. }
  304. }`, {q: JSON.stringify([{_id}])}))
  305. )
  306. const actionGoodsByUser = (_id) => (
  307. actionPromise('goodByUser', gql(`query oUser($query: String) {
  308. OrderFind(query:$query){
  309. _id orderGoods{
  310. price count total good{
  311. _id name categories{
  312. name
  313. }
  314. images {
  315. url
  316. }
  317. }
  318. }
  319. owner {
  320. _id login
  321. }
  322. }
  323. }`,
  324. {query: JSON.stringify([{___owner: _id}])}))
  325. )
  326. const actionGoodFind = (word) => (
  327. actionPromise('goodFind', gql(`query goodById($q: String) {
  328. GoodFind(query: $q) {
  329. _id name price description images {
  330. url
  331. }
  332. }
  333. }`, {q: JSON.stringify([
  334. {
  335. $or: [{title: `/${word}/`}, {description: `/${word}/`}, {name: `/${word}/`}]
  336. },
  337. {
  338. sort: [{title: 1}]
  339. }
  340. ])
  341. }
  342. ))
  343. )
  344. // const store = createStore( combineReducers({ promise: promiseReducer,
  345. // auth: authReducer,
  346. // cart: cartReducer}),
  347. // applyMiddleware(thunk))
  348. const sagaMiddleware = createSagaMiddleware()
  349. const store = createStore( combineReducers({ promise: promiseReducer,
  350. auth: authReducer,
  351. cart: cartReducer,
  352. route: routeReducer}),
  353. applyMiddleware(sagaMiddleware))
  354. // аналог actionPromise
  355. function* promiseWorker(action) {
  356. const {name, promise} = action
  357. yield put(actionPending(name))
  358. try {
  359. let data = yield promise
  360. yield put(actionResolved(name, data))
  361. return data
  362. }
  363. catch(error){
  364. yield put(actionRejected(name, error))
  365. }
  366. }
  367. function* promiseWatcher() {
  368. // запуск worker для каждого экшна типа промис старт,
  369. // в обработчмк приходит результат функции с промис старт
  370. yield takeEvery('PROMISE_START', promiseWorker)
  371. }
  372. function* loginWorker(action) {
  373. const {login, password} = action
  374. // аналог вложенного диспатча вложенного санка
  375. let token = yield call(promiseWorker, actionLogin(login, password))
  376. if (token) {
  377. yield put(actionAuthLogin(token))
  378. }
  379. }
  380. function* loginWatcher() {
  381. yield takeEvery('FULL_LOGIN', loginWorker)
  382. }
  383. function* registerWorker(action) {
  384. const {login, password} = action
  385. let regId = yield call(promiseWorker, actionRegister(login, password))
  386. if (regId) {
  387. yield put(actionFullLogin(login, password))
  388. }
  389. }
  390. function* registerWatcher() {
  391. yield takeEvery('FULL_REGISTER', registerWorker)
  392. }
  393. const queries = {
  394. "/good/:_id": match => ({ name: 'goodById',
  395. query: `query goodById($q: String) {
  396. GoodFindOne(query: $q) {
  397. _id name price description images {
  398. url
  399. }
  400. }
  401. }`,
  402. variables: {q: JSON.stringify([{_id: match.params._id}])}
  403. })
  404. }
  405. function* routeWorker({match}) {
  406. console.log(match)
  407. if (match.path in queries) {
  408. const {name, query, variables} = queries[match.path](match)
  409. yield call(promiseWorker, actionPromise(name, gql(query, variables)))
  410. }
  411. }
  412. function* routeWatcher() {
  413. yield takeEvery('ROUTE', routeWorker)
  414. }
  415. function* rootSaga() {
  416. yield all([
  417. routeWatcher(),
  418. promiseWatcher(),
  419. loginWatcher(),
  420. registerWatcher()
  421. ])
  422. }
  423. sagaMiddleware.run(rootSaga)
  424. store.subscribe(() => console.log(store.getState()))
  425. store.dispatch(actionRootCats())
  426. const RRoute = ({ action, component:Component, ...routeProps}) => {
  427. const WrapperComponent = (componentProps) => {
  428. action(componentProps.match)
  429. return <Component {...componentProps} />
  430. }
  431. return <Route {...routeProps} component={WrapperComponent} />
  432. }
  433. const CRRoute = connect(null, {action: match => ({type: 'ROUTE', match})})(RRoute)
  434. const ProtectedRoute = ({ fallback='/',
  435. roles=["admin"],
  436. auth,
  437. component:Component,
  438. ...routeProps}) => {
  439. const WrapperComponent = (componentProps) => {
  440. let aclArr = auth?.payload?.sub?.acl
  441. if (!aclArr) {
  442. aclArr = ['anon']
  443. }
  444. let crossArr = [];
  445. for (const role of roles) {
  446. crossArr = [...crossArr, ...aclArr.filter(aclItem => aclItem === role)]
  447. }
  448. if (crossArr.length === 0) {
  449. return <Redirect to={fallback} />
  450. } else {
  451. return <Component {...componentProps} />
  452. }
  453. }
  454. return <CRRoute {...routeProps} component={WrapperComponent} />
  455. }
  456. const CPRoute = connect(state => ({auth: state.auth}))(ProtectedRoute)
  457. const useLocalStoredState = (defaultState, localStorageName) => {
  458. let inputJson
  459. try {
  460. inputJson = JSON.parse(localStorage.getItem(localStorageName))
  461. } catch(err) {
  462. console.error(err)
  463. localStorage.removeItem(localStorageName)
  464. }
  465. const [state, setState] = useState(inputJson || defaultState)
  466. return [state, newState => {
  467. setState(newState)
  468. localStorage.setItem(localStorageName, JSON.stringify(newState))
  469. }]
  470. }
  471. const useProxyState = (defaultState) => {
  472. const [state, setState] = useState(defaultState)
  473. return new Proxy(state, {
  474. get(obj, key) {
  475. return obj[key]
  476. },
  477. set(obj, key, value) {
  478. setState({...obj, [key]: value})
  479. return true
  480. }
  481. })
  482. }
  483. const Logo = ({logo=logoDefault}) => (
  484. <Link to='/' className="Logo">
  485. <img src={logo} />
  486. </Link>
  487. )
  488. const Koshik = ({cart}) => {
  489. let count = 0;
  490. let sum = Object.entries(cart).map(([, val]) => val.count)
  491. count = sum.reduce((a, b) => a + b, 0)
  492. //перебрать cart, посчитать суммарный count
  493. return (
  494. <Link to='/cart' className='Koshik'>
  495. <img src={cartImg} />
  496. {count}
  497. </Link>
  498. )
  499. }
  500. const CKoshik = connect(({cart}) => ({cart}))(Koshik)
  501. const GreetContainer = ({user: {id, login}}) => {
  502. if (id && login) {
  503. return (
  504. <div>
  505. <div>
  506. ПРИВЕТ, {login}
  507. </div>
  508. <Link to={`/orders/${id}`} >
  509. Мои заказы
  510. </Link>
  511. </div>
  512. )
  513. } else {
  514. return (<></>)
  515. }
  516. }
  517. const CGreetContainer = connect(state => ({user: state.auth.payload?.sub || {}}))(GreetContainer)
  518. const Header = ({logo=logoDefault}) => (
  519. <header className="bg-light">
  520. <div className="leftBlock">
  521. <Logo logo={logo} />
  522. </div>
  523. <div className="rightBlock">
  524. <CKoshik />
  525. <CGreetContainer />
  526. </div>
  527. </header>
  528. )
  529. const RootCategory = ({cat:{_id, name}={}}) => (
  530. <Link to={`/category/${_id}`}
  531. className="catBtn list-group-item list-group-item-action list-group-item-light">
  532. {name}
  533. </Link>
  534. )
  535. const RootCategories = ({cats=defaultRootCats}) => (
  536. <div className='RootCategories list-group linkList'>
  537. {cats.map(cat => <RootCategory cat={cat} />)}
  538. </div>
  539. )
  540. const CRootCategories = connect(state => ({cats: state.promise.rootCats?.payload || []}))(RootCategories)
  541. const LogoutBTn = ({onLogout}) => (
  542. <button className='logoutBtn btn btn-primary'
  543. onClick={() => onLogout()}>
  544. Logout
  545. </button>
  546. )
  547. const CLogoutBTn = connect(null, {onLogout: actionAuthLogout})(LogoutBTn)
  548. const Aside = () => (
  549. <aside className="bg-light">
  550. <div className='LoginBlock'>
  551. <Link to='/login' className='loginBtn btn btn-primary'>Login</Link>
  552. <Link to='/register' className='registerBtn btn btn-primary'>Register</Link>
  553. <CLogoutBTn />
  554. </div>
  555. <CRootCategories />
  556. </aside>
  557. )
  558. const Content = ({children}) => (
  559. <div className='Content'>
  560. {children}
  561. </div>
  562. )
  563. const SubCategory = ({cat:{_id, name}={}}) => (
  564. <Link to={`/category/${_id}`}
  565. className='list-group-item list-group-item-action list-group-item-primary linkItem'>
  566. {name}
  567. </Link>
  568. )
  569. const SubCategories = ({cats}) => (
  570. <div className="CatsList list-group linkList">
  571. {cats.map(cat => <SubCategory cat={cat} />)}
  572. </div>
  573. )
  574. const GoodCard = ({good:{_id, name, price, images}={}, onCartAdd}) => (
  575. <div className='GoodCard card'>
  576. {images && images[0] && images[0].url &&
  577. <img className='card-img-top' src={backURL + '/' + images[0].url} />}
  578. <div className="card-body">
  579. <h4 className="card-title">{name}</h4>
  580. <h5>{price} грн</h5>
  581. <button className='btn btn-primary'
  582. onClick={() => onCartAdd({_id, name, price, images})}>В корзину</button>
  583. <Link className='btn btn-success'
  584. to={`/good/${_id}`}>Подробнее</Link>
  585. </div>
  586. </div>
  587. )
  588. const CGoodCard = connect(null, {onCartAdd: actionCartAdd})(GoodCard)
  589. const Category = ({cat:{_id, name, goods, subCategories}=defaultCat}) => (
  590. <div className='Category'>
  591. <h1>{name}</h1>
  592. {subCategories && <SubCategories cats={subCategories} />}
  593. <div className='GoodCards'>
  594. {(goods || []).map(good => <CGoodCard good={good}/>)}
  595. </div>
  596. </div>
  597. )
  598. const CCategory = connect(state => ({cat: state.promise.catById?.payload}))(Category)
  599. const GoodInCart = ({item: {count, good: {_id, name, price, images}}, onCartChange, onCartRemove, onCartAdd}) => {
  600. const [goodCount, setGoodCount] = useState(count)
  601. return (
  602. <div className='CartCard card'>
  603. <div className="card-header">
  604. <h4 className="card-title">{name}</h4>
  605. </div>
  606. {images && images[0] && images[0].url &&
  607. <img className="card-img-top" src={backURL + '/' + images[0].url} />}
  608. <div className="card-body">
  609. <p>{count} шт</p>
  610. <p>{price} грн</p>
  611. <h6>Итого: {count*price} грн</h6>
  612. <button className="btn btn-success"
  613. onClick={() => { setGoodCount((count === 1 ) ? 1 : count - 1);
  614. onCartAdd({_id, name, price, images}, -1) }}>-</button>
  615. <input onBlur={() => onCartChange({_id, name, price, images}, goodCount)}
  616. onInput={(e) => setGoodCount(e.currentTarget.value)}
  617. // onInput={(e) => onCartChange({_id, name, price, images}, e.currentTarget.value)}
  618. value={goodCount} type="number"/>
  619. <button className="btn btn-success"
  620. onClick={() => { setGoodCount(count + 1);
  621. onCartAdd({_id, name, price, images}) }}>+</button>
  622. <button className="btn btn-success delBtn"
  623. onClick={() => onCartRemove({_id, name, price, images})}>Удалить</button>
  624. </div>
  625. </div>
  626. )
  627. }
  628. const CGoodInCart = connect(null, { onCartChange: actionCartChange,
  629. onCartRemove: actionCartRemove,
  630. onCartAdd: actionCartAdd})(GoodInCart)
  631. const Cart = ({cart, user, onCartClear, onOrderSend}) => {
  632. let total = 0
  633. if (cart.length !== 0) {
  634. let countes = Object.entries(cart).map(([, val]) => val.count)
  635. let prices = Object.entries(cart).map(([, val]) => val.good.price)
  636. let sum = countes.map((count, index) => prices[index] * count)
  637. total = sum.reduce((a, b) => a + b)
  638. }
  639. return (
  640. <div className='Cart'>
  641. {cart.length === 0 ? <h5>Корзина пуста</h5> :
  642. <button className='btn btn-primary'
  643. onClick={() => onCartClear()}>
  644. Очистить корзину
  645. </button>}
  646. <div className='CartBlock'>
  647. {cart.map(item => <CGoodInCart item={item}/>)}
  648. </div>
  649. {cart.length === 0 ? <></> :
  650. <>
  651. <h5>Всего к оплате: {total} грн</h5>
  652. <button className='btn btn-primary'
  653. onClick={() => onOrderSend()}
  654. disabled={user ? false : true}>
  655. Оформить заказ
  656. </button>
  657. </>}
  658. </div>
  659. )
  660. }
  661. const CCart = connect(state => ({ cart: Object.values(state.cart) || [],
  662. user: state.auth.payload || null}),
  663. { onCartClear: actionCartClear,
  664. onOrderSend: actionOrder})(Cart)
  665. const LoginForm = ({onLogin}) => {
  666. const [login, setLogin] = useState("")
  667. const [pass, setPass] = useState("")
  668. return (
  669. <div className='LoginForm form'>
  670. <div className="mb-3">
  671. <label className="form-label">Логин</label>
  672. <input className="form-input form-control"
  673. type="text"style={ {backgroundColor: (login.length === 0) ? '#f00b' : '#fff'} }
  674. value={login} onChange={(e) => setLogin(e.target.value)}/>
  675. </div>
  676. <div className="mb-3">
  677. <label className="form-label">Пароль</label>
  678. <input className="form-input form-control"
  679. type="password" style={ {backgroundColor: (pass.length === 0) ? '#f00b' : '#fff'} }
  680. value={pass} onChange={(e) => setPass(e.target.value)}/>
  681. </div>
  682. <button className="btn btn-primary"
  683. onClick={() => { onLogin(login,pass);
  684. setPass('')
  685. }}
  686. disabled={(login.length !== 0 && pass.length !== 0) ? false : true}>Войти</button>
  687. </div>
  688. )
  689. }
  690. const CLoginForm = connect(null, {onLogin: actionFullLogin})(LoginForm)
  691. const RegisterForm = ({onRegister}) => {
  692. const [login, setLogin] = useState("")
  693. const [pass, setPass] = useState("")
  694. return (
  695. <div className='RegisterForm form'>
  696. <div className="mb-3">
  697. <label className="form-label">Логин</label>
  698. <input className="form-input form-control"
  699. type="text" style={ {backgroundColor: (login.length === 0) ? '#f00b' : '#fff'} }
  700. value={login} onChange={(e) => setLogin(e.target.value)}/>
  701. </div>
  702. <div className="mb-3">
  703. <label className="form-label">Пароль</label>
  704. <input className="form-input form-control"
  705. type="password" style={ {backgroundColor: (pass.length === 0) ? '#f00b' : '#fff'} }
  706. value={pass} onChange={(e) => setPass(e.target.value)}/>
  707. </div>
  708. <button className="btn btn-primary"
  709. onClick={() => { onRegister(login,pass);
  710. setPass('')
  711. }}
  712. disabled={(login.length !== 0 && pass.length !== 0) ? false : true}>Зарегистрироваться</button>
  713. </div>
  714. )
  715. }
  716. const CRegisterForm = connect(null, {onRegister: actionFullRegister})(RegisterForm)
  717. const PageMain = () => (
  718. <h1>Главная страница</h1>
  719. )
  720. const PageCategory = ({match:{params:{_id}}, getData, history}) => {
  721. useEffect(() => {
  722. getData(_id)
  723. },[_id])
  724. return (
  725. <CCategory />
  726. )
  727. }
  728. const CPageCategory = connect(null, {getData: actionCatById})(PageCategory)
  729. const ThisGood = ({good:{_id, name, price, images, description}={}, onCartAdd}) => (
  730. <div className='GoodPage'>
  731. <h2>{name}</h2>
  732. {images && images[0] && images[0].url && <img src={backURL + '/' + images[0].url} />}
  733. <div>
  734. <h6>{description}</h6>
  735. <strong>Цена - {price} грн</strong>
  736. </div>
  737. <button className="btn btn-success"
  738. onClick={() => onCartAdd({_id, name, price, images})}>В корзину</button>
  739. </div>
  740. )
  741. const CThisGood = connect( state => ({good: state.promise.goodById?.payload}),
  742. {onCartAdd: actionCartAdd})(ThisGood)
  743. const PageGood = ({match:{params:{_id}}}) => (
  744. <>
  745. <CThisGood />
  746. </>
  747. )
  748. // const PageGood = ({match:{params:{_id}}, getData}) => {
  749. // useEffect(() => {
  750. // getData(_id)
  751. // },[_id])
  752. // return (
  753. // <>
  754. // <CThisGood />
  755. // </>
  756. // )
  757. // }
  758. // const CPageGood = connect(null, {getData: actionGoodById})(PageGood)
  759. // на текущий момент функция отображает только 100 последних заказов
  760. // подсчет потраченных средств нужно реализовать для всех заказов
  761. // но при этом сразу отображать только 100 последних
  762. const OrderedGood = ({goodOrder:{price, count, total, good}={}}) => {
  763. if (price !== null && count !== null && total !== null && good !== null) {
  764. const {_id, name, images} = good
  765. return (
  766. <div className='OrderCard card'>
  767. {images && images[0] && images[0].url &&
  768. <img className='card-img-top' src={backURL + '/' + images[0].url} />}
  769. <div className="card-body">
  770. <h4 className="card-title">{name}</h4>
  771. <h6>
  772. Куплено: {count} по {price} грн.
  773. </h6>
  774. <h6>
  775. Итого: {total} грн
  776. </h6>
  777. <Link className="btn btn-primary"
  778. to={`/good/${_id}`}>Подробнее</Link>
  779. </div>
  780. </div>
  781. )
  782. } else {
  783. return <></>
  784. }
  785. }
  786. const Orders = ({orders}) => (
  787. <div className='Orders'>
  788. {/* <h3>Всего потрачено: {totalMoney} грн</h3> */}
  789. <div className='OrderCards'>
  790. { orders.map(order =>
  791. (order.orderGoods ? order.orderGoods: []).map(goodOrder =>
  792. <OrderedGood goodOrder={goodOrder}/>))}
  793. </div>
  794. </div>
  795. )
  796. const COrders = connect(state => ({orders: state.promise.goodByUser?.payload || []}))(Orders)
  797. const PageOrders = ({match:{params:{_id}}, getData}) => {
  798. useEffect(() => {
  799. getData(_id)
  800. },[_id])
  801. return (
  802. <>
  803. <COrders />
  804. </>
  805. )
  806. }
  807. const CPageOrders = connect(null, {getData: actionGoodsByUser})(PageOrders)
  808. const Page404 = () => (
  809. <h1>PAGE НЭМА </h1>
  810. )
  811. const Main = () => (
  812. <main>
  813. <Aside />
  814. <Content>
  815. <Redirect from="/main" to="/" />
  816. <Switch>
  817. <Route path="/" component={PageMain} exact />
  818. <Route path="/category/:_id" component={CPageCategory} />
  819. {/* <Route path="/good/:_id" component={CPageGood} /> */}
  820. <CPRoute roles={['anon', 'user', 'admin']} path="/good/:_id" component={PageGood} />
  821. <Route path="/cart" component={CCart} />
  822. <Route path="/login" component={CLoginForm} />
  823. <Route path="/register" component={CRegisterForm} />
  824. <Route path="/orders/:_id" component={CPageOrders} />
  825. <Route path="*" component={Page404} />
  826. </Switch>
  827. </Content>
  828. </main>
  829. )
  830. const Footer = ({logo=logoDefault}) => (
  831. <footer>
  832. <Logo logo={logo} />
  833. </footer>
  834. )
  835. const history = createHistory()
  836. function App() {
  837. return (
  838. <Router history={history}>
  839. <Provider store={store}>
  840. <div className="App">
  841. <Header />
  842. <Main />
  843. <Footer />
  844. </div>
  845. </Provider>
  846. </Router>
  847. )
  848. }
  849. export default App;