index.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720
  1. import React, { useState } from "react";
  2. import "./App.css";
  3. import thunk from "redux-thunk";
  4. import { createStore, combineReducers, applyMiddleware } from "redux";
  5. import { Provider, connect } from "react-redux";
  6. function createStore(reducer){
  7. let state = reducer(undefined, {}) //стартовая инициализация состояния, запуск редьюсера со state === undefined
  8. let cbs = [] //массив подписчиков
  9. const getState = () => state //функция, возвращающая переменную из замыкания
  10. const subscribe = cb => (cbs.push(cb), //запоминаем подписчиков в массиве
  11. () => cbs = cbs.filter(c => c !== cb)) //возвращаем функцию unsubscribe, которая удаляет подписчика из списка
  12. const dispatch = action => {
  13. if (typeof action === 'function'){ //если action - не объект, а функция
  14. return action(dispatch, getState) //запускаем эту функцию и даем ей dispatch и getState для работы
  15. }
  16. const newState = reducer(state, action) //пробуем запустить редьюсер
  17. if (newState !== state){ //проверяем, смог ли редьюсер обработать action
  18. state = newState //если смог, то обновляем state
  19. for (let cb of cbs) cb() //и запускаем подписчиков
  20. }
  21. }
  22. return {
  23. getState, //добавление функции getState в результирующий объект
  24. dispatch,
  25. subscribe //добавление subscribe в объект
  26. }
  27. }
  28. function combineReducers(reducers) {
  29. return (state={}, action) => {
  30. const newState = {}
  31. // перебрать все редьюсеры
  32. if (reducers) {
  33. for (const [reducerName, reducer] of Object.entries(reducers)) {
  34. const newSubState = reducer(state[reducerName], action)
  35. if (newSubState !== state[reducerName]) {
  36. newState[reducerName] = newSubState
  37. }
  38. }
  39. // если newState не пустой, то вернуть стейт в
  40. if (Object.keys(newState).length !== 0) {
  41. return {...state, ...newState}
  42. } else {
  43. return state
  44. }
  45. }
  46. }
  47. }
  48. const combinedReducer = combineReducers({promise: promiseReducer, auth: authReducer, cart: cartReducer})
  49. const store = createStore(combinedReducer)
  50. store.subscribe(() => console.log(store.getState()))
  51. function jwtDecode(token){
  52. try {
  53. return JSON.parse(atob(token.split('.')[1]))
  54. }
  55. catch(e){
  56. }
  57. }
  58. function authReducer(state, {type, token}) {
  59. if (!state) {
  60. if (localStorage.authToken) {
  61. token = localStorage.authToken
  62. type = 'AUTH_LOGIN'
  63. } else {
  64. return {}
  65. }
  66. }
  67. if (type === 'AUTH_LOGIN') {
  68. let payload = jwtDecode(token)
  69. if (typeof payload === 'object') {
  70. localStorage.authToken = token
  71. return {
  72. ...state,
  73. token,
  74. payload
  75. }
  76. } else {
  77. return state
  78. }
  79. }
  80. if (type === 'AUTH_LOGOUT') {
  81. delete localStorage.authToken
  82. location.reload()
  83. return {}
  84. }
  85. return state
  86. }
  87. const actionAuthLogin = (token) => ({type: 'AUTH_LOGIN', token})
  88. const actionAuthLogout = () => ({type: 'AUTH_LOGOUT'})
  89. function cartReducer (state={}, {type, good={}, count=1}) {
  90. if (Object.keys(state).length === 0 && localStorage.cart) {
  91. let currCart = JSON.parse(localStorage.cart)
  92. if (currCart && Object.keys(currCart).length !== 0) {
  93. state = currCart
  94. }
  95. }
  96. const {_id} = good
  97. const types = {
  98. CART_ADD() {
  99. count = +count
  100. if (!count) {
  101. return state
  102. }
  103. let newState = {
  104. ...state,
  105. [_id]: {good, count: (count + (state[_id]?.count || 0)) < 1 ? 1 : count + (state[_id]?.count || 0)}
  106. }
  107. localStorage.cart = JSON.stringify(newState)
  108. return newState
  109. },
  110. CART_CHANGE() {
  111. count = +count
  112. if (!count) {
  113. return state
  114. }
  115. let newState = {
  116. ...state,
  117. [_id]: {good, count: count < 0 ? 0 : count}
  118. }
  119. localStorage.cart = JSON.stringify(newState)
  120. return newState
  121. },
  122. CART_REMOVE() {
  123. let { [_id]: removed, ...newState } = state
  124. localStorage.cart = JSON.stringify(newState)
  125. return newState
  126. },
  127. CART_CLEAR() {
  128. localStorage.cart = JSON.stringify({})
  129. return {}
  130. },
  131. }
  132. if (type in types) {
  133. return types[type]()
  134. }
  135. return state
  136. }
  137. const actionCartAdd = (good, count) => ({type: 'CART_ADD', good, count})
  138. const actionCartChange = (good, count) => ({type: 'CART_CHANGE', good, count})
  139. const actionCartRemove = (good) => ({type: 'CART_REMOVE', good})
  140. const actionCartClear = () => ({type: 'CART_CLEAR'})
  141. function promiseReducer(state={}, {type, status, payload, error, name}) {
  142. if (!state) {
  143. return {}
  144. }
  145. if (type === 'PROMISE') {
  146. return {
  147. ...state,
  148. [name]: {
  149. status: status,
  150. payload : payload,
  151. error: error,
  152. }
  153. }
  154. }
  155. return state
  156. }
  157. const actionPending = (name) => ({type: 'PROMISE', status: 'PENDING', name})
  158. const actionResolved = (name, payload) => ({type: 'PROMISE', status: 'RESOLVED', name, payload})
  159. const actionRejected = (name, error) => ({type: 'PROMISE', status: 'REJECTED', name, error})
  160. const actionPromise = (name, promise) => (
  161. async (dispatch) => {
  162. dispatch(actionPending(name))
  163. try {
  164. let data = await promise
  165. dispatch(actionResolved(name, data))
  166. return data
  167. }
  168. catch(error){
  169. dispatch(actionRejected(name, error))
  170. }
  171. }
  172. )
  173. const getGQL = url => (
  174. async (query, variables={}) => {
  175. let obj = await fetch(url, {
  176. method: 'POST',
  177. headers: {
  178. "Content-Type": "application/json",
  179. ...(localStorage.authToken ? {Authorization: "Bearer " + localStorage.authToken} : {})
  180. },
  181. body: JSON.stringify({ query, variables })
  182. })
  183. let a = await obj.json()
  184. if (!a.data && a.errors) {
  185. throw new Error(JSON.stringify(a.errors))
  186. } else {
  187. return a.data[Object.keys(a.data)[0]]
  188. }
  189. }
  190. )
  191. const backURL = 'http://shop-roles.node.ed.asmer.org.ua/'
  192. const gql = getGQL(backURL + 'graphql');
  193. const actionOrder = () => (
  194. async (dispatch, getState) => {
  195. let {cart} = getState()
  196. const orderGoods = Object.entries(cart)
  197. .map(([_id, {good, count}]) => ({good: {_id}, count}))
  198. let result = await dispatch(actionPromise('order', gql(`
  199. mutation newOrder($order:OrderInput){
  200. OrderUpsert(order:$order)
  201. { _id total}
  202. }
  203. `, {order: {orderGoods}})))
  204. if (result?._id) {
  205. dispatch(actionCartClear())
  206. }
  207. })
  208. const actionLogin = (login, password) => (
  209. actionPromise('login', gql(`query log($login: String, $password: String) {
  210. login(login: $login, password: $password)
  211. }`, {login, password}))
  212. )
  213. const actionFullLogin = (login, password) => (
  214. async (dispatch) => {
  215. let token = await dispatch(actionLogin(login, password))
  216. if (token) {
  217. dispatch(actionAuthLogin(token))
  218. location.hash = '#/category'
  219. } else {
  220. showErrorMessage('please, enter correct login and password', main)
  221. }
  222. }
  223. )
  224. const actionRegister = (login, password) => (
  225. actionPromise('register', gql(`mutation reg($user:UserInput) {
  226. UserUpsert(user:$user) {
  227. _id
  228. }
  229. }
  230. `, {user: {login, password}})
  231. )
  232. )
  233. const actionFullRegister = (login, password) => (
  234. async (dispatch) => {
  235. let registerId = await dispatch(actionRegister(login, password))
  236. if (registerId) {
  237. dispatch(actionFullLogin(login, password))
  238. }
  239. }
  240. )
  241. const actionRootCats = () => (
  242. actionPromise('rootCats', gql(`query {
  243. CategoryFind(query: "[{\\"parent\\":null}]"){
  244. _id name
  245. }
  246. }`))
  247. )
  248. const actionCatById = (_id) => (
  249. actionPromise('catById', gql(`query catById($q: String){
  250. CategoryFindOne(query: $q){
  251. _id name goods {
  252. _id name price images {
  253. url
  254. }
  255. }
  256. subCategories {
  257. _id name
  258. }
  259. }
  260. }`, {q: JSON.stringify([{_id}])}))
  261. )
  262. const actionGoodById = (_id) => (
  263. actionPromise('goodById', gql(`query goodById($q: String) {
  264. GoodFindOne(query: $q) {
  265. _id name price description images {
  266. url
  267. }
  268. }
  269. }`, {q: JSON.stringify([{_id}])}))
  270. )
  271. const actionGoodsByUser = (_id) => (
  272. actionPromise('goodByUser', gql(`query oUser($query: String) {
  273. OrderFind(query:$query){
  274. _id orderGoods{
  275. price count total good{
  276. _id name categories{
  277. name
  278. }
  279. images {
  280. url
  281. }
  282. }
  283. }
  284. owner {
  285. _id login
  286. }
  287. }
  288. }`,
  289. {query: JSON.stringify([{___owner: _id}])}))
  290. )
  291. store.subscribe(() => {
  292. const {promise, auth} = store.getState()
  293. const {rootCats} = promise
  294. if (rootCats?.status === 'PENDING') {
  295. aside.innerHTML = `<img src="Loading_icon.gif">`
  296. } else {
  297. if (rootCats?.payload) {
  298. aside.innerHTML = ''
  299. authBox.innerHTML = ''
  300. const regBtn = document.createElement('a')
  301. regBtn.href = '#/register'
  302. regBtn.innerText = 'Register'
  303. const loginBtn = document.createElement('a')
  304. loginBtn.className = 'loginBtn'
  305. loginBtn.href = `#/login`
  306. loginBtn.innerText = 'Login'
  307. const logoutBtn = document.createElement('a')
  308. logoutBtn.innerText = 'Logout'
  309. auth.token ? authBox.append(logoutBtn) : authBox.append(regBtn, loginBtn)
  310. logoutBtn.onclick = () => {
  311. store.dispatch(actionAuthLogout())
  312. }
  313. for (const {_id, name} of rootCats?.payload) {
  314. const link = document.createElement('a')
  315. link.href = `#/category/${_id}`
  316. link.innerText = name
  317. aside.append(link)
  318. }
  319. }
  320. }
  321. })
  322. store.dispatch(actionRootCats())
  323. function createForm(parent, type, callback) {
  324. let {auth} = store.getState()
  325. let res = `<label for="login${type}">Nick</label>
  326. <input id="login${type}" type="text"/>
  327. <label for="pass${type}">Password</label>
  328. <input id="pass${type}" type="password"/>
  329. <button id="btn${type}">${type}</button>
  330. </div>`
  331. parent.innerHTML = res
  332. return () => window[`btn${type}`].onclick = () => {
  333. store.dispatch(callback(window[`login${type}`].value, window[`pass${type}`].value))
  334. }
  335. }
  336. let message = document.createElement('p')
  337. function showErrorMessage(text, parent) {
  338. message.innerHTML = text
  339. parent.append(message)
  340. }
  341. const createCartPage = (parent) => {
  342. parent.innerHTML = ''
  343. const {cart} = store.getState()
  344. const clearBtn = document.createElement('button')
  345. clearBtn.innerText = "clear all"
  346. if(Object.keys(cart).length !== 0) {
  347. parent.append(clearBtn)
  348. }
  349. clearBtn.onclick = () => {
  350. store.dispatch(actionCartClear())
  351. }
  352. const cartPage = document.createElement('div')
  353. if(Object.keys(cart).length === 0) {
  354. showErrorMessage('Hmm... Let`s add something into the cart!', cartPage)
  355. }
  356. main.append(cartPage)
  357. let cartCounter = 0
  358. for(const item in cart) {
  359. const {good} = cart[item]
  360. const {count, good: {_id: id, name: name, price: price, images: [{url}]}} = cart[item]
  361. cartCounter += count*price
  362. const card = document.createElement('div')
  363. card.innerHTML = `
  364. <h4>${name}</h4>
  365. </div>
  366. <img src="${backURL}/${url}" />
  367. <p>amount: </p>
  368. `
  369. const inputGr = document.createElement('div')
  370. card.lastElementChild.append(inputGr)
  371. const minusBtn = document.createElement('button')
  372. minusBtn.innerText = '-'
  373. inputGr.append(minusBtn)
  374. minusBtn.onclick = () => {
  375. store.dispatch(actionCartAdd(good, -1))
  376. }
  377. const changeCount = document.createElement('input')
  378. changeCount.type = 'number'
  379. changeCount.value = count
  380. changeCount.setAttribute('min', '1')
  381. inputGr.append(changeCount)
  382. changeCount.oninput = () => {
  383. store.dispatch(actionCartChange(good, changeCount.value))
  384. }
  385. const plusBtn = document.createElement('button')
  386. plusBtn.innerText = '+'
  387. inputGr.append(plusBtn)
  388. plusBtn.onclick = () => {
  389. store.dispatch(actionCartAdd(good))
  390. }
  391. const deleteGood = document.createElement('button')
  392. deleteGood.innerText = 'remove item'
  393. deleteGood.style.display = 'block'
  394. card.lastElementChild.append(deleteGood)
  395. deleteGood.onclick = () => {
  396. store.dispatch(actionCartRemove(good))
  397. }
  398. cartPage.append(card)
  399. }
  400. const total = document.createElement('h5')
  401. total.innerText = `Total: ${cartCounter} UAH`
  402. const sendOrder = document.createElement('button')
  403. sendOrder.innerText = 'Make an order'
  404. if(Object.keys(cart).length !== 0) {
  405. parent.append(total)
  406. parent.append(sendOrder)
  407. }
  408. const {auth} = store.getState()
  409. sendOrder.disabled = !auth.token;
  410. sendOrder.onclick = () => {
  411. store.dispatch(actionOrder())
  412. }
  413. }
  414. // location.hash
  415. window.onhashchange = () => {
  416. const [,route, _id] = location.hash.split('/')
  417. const routes = {
  418. category(){
  419. store.dispatch(actionCatById(_id))
  420. },
  421. good(){
  422. store.dispatch(actionGoodById(_id))
  423. },
  424. register(){
  425. const registerFunc = createForm(main, 'Register', actionFullRegister)
  426. registerFunc()
  427. },
  428. login(){
  429. const loginFunc = createForm(main, 'Login', actionFullLogin)
  430. loginFunc()
  431. },
  432. orders(){
  433. store.dispatch(actionGoodsByUser(_id))
  434. },
  435. cart(){
  436. createCartPage(main)
  437. }
  438. }
  439. if (route in routes) {
  440. routes[route]()
  441. }
  442. }
  443. store.subscribe(() => {
  444. const [,route] = location.hash.split('/')
  445. if (route === 'cart') {
  446. createCartPage(main)
  447. }
  448. })
  449. window.onhashchange()
  450. store.subscribe(() => {
  451. const {promise} = store.getState()
  452. const {catById} = promise
  453. const [,route, _id] = location.hash.split('/')
  454. if (catById?.status === 'PENDING') {
  455. main.innerHTML = `<img src="Loading_icon.gif">`
  456. } else {
  457. if (catById?.payload && route === 'category'){
  458. main.innerHTML = ''
  459. const catBody = document.createElement('div')
  460. main.append(catBody)
  461. const {name} = catById.payload;
  462. catBody.innerHTML = `<h1>${name}</h1>`
  463. if (catById.payload.subCategories) {
  464. const linkList = document.createElement('div')
  465. catBody.append(linkList)
  466. for(const {_id, name} of catById.payload.subCategories) {
  467. const link = document.createElement('a')
  468. link.href = `#/category/${_id}`
  469. link.innerText = name
  470. link.className = 'cat'
  471. catBody.append(link)
  472. }
  473. if(location.hash === '#/category/') {
  474. for(const {_id, name} of catById.payload) {
  475. const link = document.createElement('a')
  476. link.href = `#/category/${_id}`
  477. link.innerText = name
  478. link.className = 'cat'
  479. catBody.append(link)
  480. }
  481. }
  482. }
  483. if (catById.payload.goods) {
  484. const cardBody = document.createElement('div')
  485. main.append(cardBody)
  486. for (const good of catById.payload.goods){
  487. const {_id, name, price, images} = good
  488. const card = document.createElement('div')
  489. card.className = 'card'
  490. card.innerHTML = `
  491. <img src="${backURL}/${images[0].url}" />
  492. <div>
  493. <h4>${name}</h4>
  494. <h5>${price} UAH</h5>
  495. <a href="#/good/${_id}" class="showMore">
  496. Show more
  497. </a>
  498. </div>
  499. `
  500. const btnCart = document.createElement('button')
  501. btnCart.innerText = 'To cart'
  502. btnCart.onclick = () => {
  503. store.dispatch(actionCartAdd(good))
  504. }
  505. card.lastElementChild.append(btnCart)
  506. cardBody.append(card)
  507. }
  508. }
  509. }
  510. }
  511. })
  512. store.subscribe(() => {
  513. const {promise} = store.getState()
  514. const {goodById} = promise
  515. const [,route, _id] = location.hash.split('/');
  516. if (goodById?.status === 'PENDING') {
  517. main.innerHTML = `<img src="Loading_icon.gif">`
  518. } else {
  519. if (goodById?.payload && route === 'good') {
  520. main.innerHTML = ''
  521. const good = goodById.payload
  522. const {_id, name, images, price, description} = good
  523. const card = document.createElement('div')
  524. card.innerHTML = `<h2>${name}</h2>
  525. <img src="${backURL}/${images[0].url}" />
  526. <div>
  527. <h6>${description}</h6>
  528. <strong>Цена - ${price} грн</strong>
  529. </div>
  530. `
  531. const btnCart = document.createElement('button')
  532. btnCart.innerText = 'Add to cart'
  533. btnCart.onclick = () => {
  534. store.dispatch(actionCartAdd(good))
  535. }
  536. card.append(btnCart)
  537. main.append(card);
  538. }
  539. }
  540. }
  541. )
  542. store.subscribe(() => {
  543. const {auth} = store.getState()
  544. const name = document.createElement('div')
  545. name.innerText = `Hello, stranger`
  546. const {payload} = auth
  547. if (payload?.sub) {
  548. userBox.innerHTML = ''
  549. const {id, login} = payload.sub
  550. name.innerText = `Hello, ${login}`
  551. const myOrders = document.createElement('a')
  552. myOrders.innerText = 'My orders'
  553. myOrders.href = `#/orders/${id}`
  554. userBox.append(myOrders)
  555. } else {
  556. userBox.innerHTML = ''
  557. }
  558. userBox.append(name)
  559. })
  560. store.subscribe(() => {
  561. const {promise} = store.getState()
  562. const {goodByUser} = promise
  563. const [,route] = location.hash.split('/')
  564. if (goodByUser?.status === 'PENDING') {
  565. main.innerHTML = `<img src="Loading_icon.gif">`
  566. } else {
  567. if (goodByUser?.payload && route === 'orders'){
  568. main.innerHTML = ''
  569. const cardBody = document.createElement('div')
  570. main.append(cardBody)
  571. if (goodByUser.payload) {
  572. let totalMoney = 0
  573. for (const order of goodByUser.payload) {
  574. if (order.orderGoods) {
  575. for (const {price, count, total, good} of order.orderGoods) {
  576. if (price !== null && count !== null && total !== null && good !== null) {
  577. totalMoney += total
  578. const {_id, name, images} = good
  579. const card = document.createElement('div')
  580. card.innerHTML = `
  581. <img src="${backURL}/${images[0].url}" />
  582. <div>
  583. <h4>${name}</h4>
  584. // <h6>
  585. // bought: ${count}, ${price} UAH
  586. // </h6>
  587. <h6>
  588. Total: ${total} UAH
  589. </h6>
  590. <a href="#/good/${_id}">
  591. show more
  592. </a>
  593. </div>
  594. `
  595. cardBody.append(card)
  596. }
  597. }
  598. }
  599. }
  600. const totalBlock = document.createElement('h3')
  601. totalBlock.innerText = 'Total: ' + totalMoney + ' UAH'
  602. main.append(totalBlock)
  603. }
  604. }
  605. }
  606. })
  607. store.subscribe(() => {
  608. const {cart} = store.getState()
  609. let counter = 0;
  610. for (const key in cart) {
  611. counter += cart[key].count
  612. }
  613. cartCounter.innerText = counter
  614. })