index.js 22 KB

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