main.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628
  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. const delay = ms => new Promise(ok => setTimeout(() => ok(ms), ms))
  24. const jwtDecode = token => {
  25. try {
  26. let arrToken = token.split('.')
  27. let base64Token = atob(arrToken[1])
  28. return JSON.parse(base64Token)
  29. }
  30. catch (e) {
  31. console.log('Лажа, Бро ' + e);
  32. }
  33. }
  34. function authReducer(state, { type, token }) {
  35. if (!state) {
  36. if (localStorage.authToken) {
  37. type = 'AUTH_LOGIN'
  38. token = localStorage.authToken
  39. } else state = {}
  40. }
  41. if (type === 'AUTH_LOGIN') {
  42. localStorage.setItem('authToken', token)
  43. let payload = jwtDecode(token)
  44. if (typeof payload === 'object') {
  45. return {
  46. ...state,
  47. token,
  48. payload
  49. }
  50. } else return state
  51. }
  52. if (type === 'AUTH_LOGOUT') {
  53. localStorage.removeItem('authToken')
  54. return {}
  55. }
  56. return state
  57. }
  58. function cartReducer(state = {}, { type, good = {}, count = 1 }) {
  59. const { _id } = good
  60. const types = {
  61. CART_ADD() {
  62. count = +count
  63. if (!count) return state
  64. return {
  65. ...state,
  66. [_id]: {
  67. good,
  68. count: count + (state[_id]?.count || 0)
  69. }
  70. }
  71. },
  72. CART_CHANGE() {
  73. count = +count
  74. if (!count) return state
  75. return {
  76. ...state,
  77. [_id]: {
  78. good,
  79. count: count
  80. }
  81. }
  82. },
  83. CART_REMOVE() {
  84. let { [_id]: remove, ...newState } = state
  85. return {
  86. ...newState
  87. }
  88. },
  89. CART_CLEAR() {
  90. return {}
  91. },
  92. }
  93. if (type in types) {
  94. return types[type]()
  95. }
  96. return state
  97. }
  98. function promiseReducer(state = {}, { type, status, payload, error, name }) {
  99. if (type === 'PROMISE') {
  100. return {
  101. ...state,
  102. [name]: { status, payload, error }
  103. }
  104. }
  105. return state;
  106. }
  107. const combineReducers = (reducers) => (state = {}, action) => {
  108. const newState = {}
  109. for (const [reducerName, reducer] of Object.entries(reducers)) {
  110. const newSubState = reducer(state[reducerName], action)
  111. if (newSubState !== state[reducerName]) {
  112. newState[reducerName] = newSubState
  113. }
  114. }
  115. if (Object.keys(newState).length !== 0) {
  116. return { ...state, ...newState }
  117. }
  118. else {
  119. return state
  120. }
  121. }
  122. const combineReducer = combineReducers({ promise: promiseReducer, auth: authReducer, cart: cartReducer })
  123. const actionAddCart = (good, count) => ({ type: 'CART_ADD', good, count })
  124. const actionChangeCart = (good, count) => ({ type: 'CART_CHANGE', good, count })
  125. const actionRemoveCart = good => ({ type: 'CART_REMOVE', good })
  126. const actionCleanCart = () => ({ type: 'CART_CLEAR' })
  127. const actionAuthLogin = token => ({ type: 'AUTH_LOGIN', token })
  128. const actionAuthLogout = () => ({ type: 'AUTH_LOGOUT' })
  129. const actionOrder = () =>
  130. async (dispatch, getState) => {
  131. let { cart } = getState()
  132. const orderGoods = Object.entries(cart).map(([_id, { count }]) => ({ good: { _id }, count }))
  133. let result = await dispatch(actionPromise('order', gql(`
  134. mutation newOrder($order:OrderInput){
  135. OrderUpsert(order:$order)
  136. { _id total }
  137. }
  138. `, { order: { orderGoods } })))
  139. if (result?._id) {
  140. dispatch(actionCleanCart())
  141. }
  142. }
  143. const actionPending = name => ({ type: 'PROMISE', status: 'PENDING', name })
  144. const actionResolved = (name, payload) => ({ type: 'PROMISE', status: 'RESOLVED', name, payload })
  145. const actionRejected = (name, error) => ({ type: 'PROMISE', status: 'REJECTED', name, error })
  146. const actionPromise = (name, promise) =>
  147. async dispatch => {
  148. dispatch(actionPending(name))
  149. try {
  150. let data = await promise
  151. dispatch(actionResolved(name, data))
  152. return data
  153. }
  154. catch (error) {
  155. dispatch(actionRejected(name, error))
  156. }
  157. }
  158. const actionRootCats = () =>
  159. actionPromise('rootCats', gql(`query {
  160. CategoryFind(query: "[{\\"parent\\":null}]"){
  161. _id name
  162. }
  163. }`))
  164. const actionCatById = (_id) =>
  165. actionPromise('catById', gql(`query catById($q: String){
  166. CategoryFindOne(query: $q){
  167. subCategories{name, _id}
  168. _id name goods {
  169. _id name price images {
  170. url
  171. }
  172. }
  173. }
  174. }`, { q: JSON.stringify([{ _id }]) }))
  175. const actionGoodById = (_id) => //добавить подкатегории
  176. actionPromise('goodById', gql(`query goodByID($q: String) {
  177. GoodFind(query: $q){
  178. name _id description price images{url}
  179. }
  180. }`, {
  181. q: JSON.stringify([{ _id }])
  182. }))
  183. const actionLogin = (login, password) =>
  184. actionPromise('login', gql(`query NameForMe1($login:String, $password:String){
  185. login(login:$login, password:$password)
  186. }`, { login, password }))
  187. const actionMyOrders = () =>
  188. actionPromise('myOrders', gql(`query Order{
  189. OrderGoodFind(query:"[{}]"){
  190. good{ name _id} _id total price count
  191. }
  192. }`, {}))
  193. const actionFullLogin = (login, password) =>
  194. async dispatch => {
  195. let token = await dispatch(actionLogin(login, password))
  196. if (token) {
  197. dispatch(actionAuthLogin(token))
  198. }
  199. }
  200. const actionRegister = (login, password) =>
  201. actionPromise('register', gql(`
  202. mutation reg($login:String, $password:String){
  203. UserUpsert(user:{
  204. login:$login,
  205. password:$password,
  206. nick:$login}){
  207. _id login
  208. }
  209. }
  210. `, { login, password }))
  211. const actionFullRegister = (login, password) =>
  212. async dispatch => {
  213. await actionRegister(login, password)
  214. let token = await dispatch(actionLogin(login, password))
  215. if (token) {
  216. dispatch(actionAuthLogin(token))
  217. }
  218. }
  219. const store = createStore(combineReducer)
  220. const getGQL = url =>
  221. async (query, variables = {}) => {
  222. let obj = await fetch(url, {
  223. method: 'POST',
  224. headers: {
  225. "Content-Type": "application/json",
  226. Authorization: localStorage.authToken ? 'Bearer ' + localStorage.authToken : {},
  227. },
  228. body: JSON.stringify({ query, variables })
  229. })
  230. let a = await obj.json()
  231. if (!a.data && a.errors)
  232. throw new Error(JSON.stringify(a.errors))
  233. return a.data[Object.keys(a.data)[0]]
  234. }
  235. const backURL = 'http://shop-roles.asmer.fs.a-level.com.ua'
  236. const gql = getGQL(backURL + '/graphql');
  237. store.dispatch(actionRootCats())
  238. store.dispatch(actionGoodById())
  239. // store.dispatch(actionAuthLogin(token))
  240. // store.dispatch(actionPromise('delay2000', delay(1000)))
  241. window.onhashchange = () => {
  242. const [, route, _id] = location.hash.split('/')
  243. const routes = {
  244. category() {
  245. store.dispatch(actionCatById(_id))
  246. },
  247. good() { //задиспатчить actionGoodById
  248. store.dispatch(actionGoodById(_id))
  249. },
  250. login() {
  251. userAuthorizationFields(route)
  252. },
  253. register() {
  254. userAuthorizationFields(route)
  255. },
  256. order() { //задиспатчить actionGoodById
  257. renderOrder()
  258. },
  259. dashboard() {
  260. store.dispatch(actionMyOrders())
  261. }
  262. }
  263. if (route in routes)
  264. routes[route]()
  265. else {
  266. startPage()
  267. }
  268. }
  269. window.onhashchange()
  270. function startPage() {
  271. main.innerHTML = ""
  272. }
  273. //поля авторизации
  274. function userAuthorizationFields(key) {
  275. const userBox = document.createElement('div')
  276. userBox.setAttribute('id', 'userBox')
  277. const h2 = document.createElement('h2')
  278. const inputNick = document.createElement('input')
  279. const inputPassword = document.createElement('input')
  280. inputNick.type = 'text'
  281. inputPassword.type = 'password'
  282. const btnEnter = document.createElement('a')
  283. const btnClose = document.createElement('a')
  284. btnClose.onclick = () => {
  285. userBox.remove()
  286. overlay.style.display = 'none'
  287. }
  288. btnClose.innerText = 'X'
  289. btnClose.classList.add('close')
  290. btnClose.href = '#'
  291. btnEnter.href = '#'
  292. overlay.style.display = 'block'
  293. if (key === 'login') {
  294. h2.innerText = 'Log In'
  295. btnEnter.innerText = 'Log In'
  296. btnEnter.setAttribute('id', 'logIn')
  297. btnEnter.onclick = () => store.dispatch(actionFullLogin(inputNick.value, inputPassword.value))
  298. } else {
  299. h2.innerText = 'Register'
  300. btnEnter.innerText = 'Register'
  301. btnEnter.setAttribute('id', 'register')
  302. btnEnter.onclick = () => store.dispatch(actionFullRegister(inputNick.value, inputPassword.value))
  303. }
  304. userBox.append(h2)
  305. userBox.append(inputNick)
  306. userBox.append(btnClose)
  307. userBox.append(inputPassword)
  308. userBox.append(btnEnter)
  309. user.append(userBox)
  310. }
  311. //ссылки когда не авторизирован
  312. function noAuthorization() {
  313. const loginLink = document.createElement('a')
  314. loginLink.classList.add('user__link')
  315. loginLink.innerText = 'Log In'
  316. loginLink.href = '#/login'
  317. const registerLink = document.createElement('a')
  318. registerLink.href = '#/register'
  319. registerLink.innerText = 'Register'
  320. registerLink.classList.add('user__link')
  321. user.append(loginLink)
  322. user.append(registerLink)
  323. }
  324. // страница заказа
  325. function renderOrder() {
  326. const { cart, auth } = store.getState()
  327. const [, route, _id] = location.hash.split('/')
  328. main.innerHTML = ''
  329. if (Object.keys(cart).length !== 0 && route === 'order') {
  330. const orderTop = document.createElement('div')
  331. orderTop.classList.add('order-top')
  332. const orderCleanBtn = document.createElement('button')
  333. orderCleanBtn.classList.add('clean-order')
  334. orderCleanBtn.innerText = 'Clean order'
  335. orderCleanBtn.onclick = () => {
  336. store.dispatch(actionCleanCart())
  337. main.innerHTML = ''
  338. }
  339. orderTop.append(orderCleanBtn)
  340. main.append(orderTop)
  341. for (const key in cart) {
  342. const { _id, name, price, images } = cart[key].good
  343. const divContainer = document.createElement('div')
  344. divContainer.classList.add('product-order__inner')
  345. const img = document.createElement('img')
  346. img.src = `${backURL}/${images[0].url}`
  347. const a = document.createElement('a')
  348. a.href = `#/good/${_id}`
  349. a.innerText = name
  350. const input = document.createElement('input')
  351. input.type = 'number'
  352. input.min = '1'
  353. input.value = cart[key].count
  354. input.oninput = () => {
  355. spanTotal.innerHTML = `Сумма: <strong>${price * +input.value}$</strong>`
  356. store.dispatch(actionChangeCart(cart[key].good, input.value))
  357. }
  358. const spanPrice = document.createElement('span')
  359. spanPrice.innerHTML = `Цена: ${price} $`
  360. const spanTotal = document.createElement('span')
  361. spanTotal.innerHTML = `Сумма: <strong>${price * +input.value} $</strong>`
  362. const buttonRemove = document.createElement('button')
  363. buttonRemove.innerText = 'x'
  364. buttonRemove.onclick = () => {
  365. store.dispatch(actionRemoveCart(cart[key].good))
  366. divContainer.remove()
  367. }
  368. divContainer.append(img)
  369. divContainer.append(a)
  370. divContainer.append(spanPrice)
  371. divContainer.append(input)
  372. divContainer.append(spanTotal)
  373. divContainer.append(buttonRemove)
  374. main.append(divContainer)
  375. }
  376. const orderSentBtn = document.createElement('button')
  377. orderSentBtn.innerText = 'Заказать'
  378. orderSentBtn.classList.add('order-sent__btn')
  379. if (auth?.token) {
  380. orderSentBtn.onclick = () => {
  381. store.dispatch(actionOrder())
  382. main.innerText = ' Спасибо, заказ оформлен'
  383. }
  384. } else orderSentBtn.onclick = () => {
  385. const err = document.createElement('div')
  386. err.innerText = 'Sorry, please Log In or Register Now'
  387. err.style.color = '#ff0000'
  388. main.prepend(err)
  389. }
  390. main.append(orderSentBtn)
  391. }
  392. }
  393. store.dispatch(actionRootCats())
  394. store.dispatch(actionGoodById())
  395. // рисуем категории
  396. store.subscribe(() => {
  397. const { rootCats } = store.getState().promise
  398. if (rootCats?.payload) {
  399. aside.innerHTML = ''
  400. for (const { _id, name } of rootCats?.payload) {
  401. const link = document.createElement('a')
  402. link.href = `#/category/${_id}`
  403. link.innerText = name
  404. aside.append(link)
  405. }
  406. }
  407. })
  408. // рисуем продукты категории
  409. store.subscribe(() => {
  410. const { catById } = store.getState().promise
  411. const [, route, _id] = location.hash.split('/')
  412. if (catById?.payload && route === 'category') {
  413. const { name, subCategories } = catById.payload
  414. main.innerHTML = `<h1>${name}</h1> `
  415. const subCatDiv = document.createElement('div')
  416. subCatDiv.classList.add('sub-catigories')
  417. subCategories ? subCategories.map(s => {
  418. const link = document.createElement('a')
  419. link.href = `#/category/${s._id}`
  420. link.innerText = s.name
  421. subCatDiv.append(link)
  422. }) : ''
  423. main.append(subCatDiv)
  424. for (const good of catById.payload.goods) {
  425. const { _id, name, price, images } = good
  426. const product = document.createElement('div')
  427. product.classList.add('product')
  428. const btn = document.createElement('button')
  429. btn.innerText = '+'
  430. let urlImage = images ? images[0].url : ''
  431. product.innerHTML = `<a class="product-title__link" href="#/good/${_id}">${name}</a>
  432. <div class="product__inner">
  433. <img src="${backURL}/${urlImage}" />
  434. <strong> ${price} $</strong>
  435. </div > `
  436. btn.onclick = () => store.dispatch(actionAddCart(good, 1))
  437. product.append(btn)
  438. main.append(product)
  439. }
  440. }
  441. })
  442. // отрисовка Истории заказов
  443. store.subscribe(() => {
  444. const { myOrders } = store.getState().promise
  445. const [, route, _id] = location.hash.split('/')
  446. if (myOrders?.payload && route === 'dashboard') {
  447. main.innerHTML = ''
  448. const table = document.createElement('table')
  449. table.setAttribute('border', '2')
  450. for (const { good, price, count, total } of myOrders.payload) {
  451. if (good !== null) {
  452. const tr = document.createElement('tr')
  453. const tdName = document.createElement('td')
  454. const tdPrice = document.createElement('td')
  455. const tdCount = document.createElement('td')
  456. const tdTotal = document.createElement('td')
  457. tdName.innerHTML = `<a href = "#/good/${good._id}" > ${good.name}</a > `
  458. tdPrice.innerText = price
  459. tdCount.innerText = count
  460. tdTotal.innerText = total
  461. tr.append(tdName)
  462. tr.append(tdPrice)
  463. tr.append(tdCount)
  464. tr.append(tdTotal)
  465. table.append(tr)
  466. }
  467. }
  468. main.append(table)
  469. }
  470. })
  471. // отрисовка Карточки продукты
  472. store.subscribe(() => {
  473. const { goodById } = store.getState().promise
  474. const [, route, _id] = location.hash.split('/')
  475. if (goodById?.payload && route === 'good') {
  476. main.innerHTML = ''
  477. const { name, description, price, images } = goodById.payload[0]
  478. const btn = document.createElement('button')
  479. btn.classList.add('productBtn')
  480. btn.onclick = () => store.dispatch(actionAddCart(goodById.payload[0], 1))
  481. btn.innerText = '+'
  482. main.innerHTML = `
  483. <div class="product-one">
  484. <div class="product-one__img">
  485. <img src="${backURL}/${images[0].url}" />
  486. </div>
  487. <div class="product-one__inner">
  488. <h2 class="product-one_title">${name}</h2>
  489. <p class="product-one__price"> <strong>${price} $</strong></p>
  490. <p class="product-one__description">
  491. <span>Обзор: ${description}</span>
  492. </p>
  493. </div>
  494. </div> `
  495. main.append(btn)
  496. }
  497. })
  498. // взависимости от страницы рисуем Log In / Registration
  499. store.subscribe(() => {
  500. const { auth } = store.getState()
  501. const [, route, _id] = location.hash.split('/')
  502. user.innerHTML = ''
  503. overlay.style.display = 'none'
  504. dashboardLink.style.display = 'none'
  505. if (auth?.payload) {
  506. const logOutBtn = document.createElement('button')
  507. logOutBtn.innerText = 'Выйти'
  508. logOutBtn.onclick = () => {
  509. store.dispatch(actionAuthLogout())
  510. store.dispatch(actionCleanCart())
  511. }
  512. user.innerHTML = `<h3> Hello, ${auth.payload.sub.login}</h3 >
  513. <div id="logOut"></div>`
  514. dashboardLink.style.display = 'block'
  515. logOut.append(logOutBtn)
  516. } else if (route === 'login' || route === 'register') {
  517. userAuthorizationFields(route)
  518. noAuthorization()
  519. } else if (user.children.length === 0) {
  520. noAuthorization()
  521. }
  522. })
  523. // счетчик корзины
  524. store.subscribe(() => {
  525. const { cart } = store.getState()
  526. if (Object.keys(cart).length !== 0) {
  527. countOrder.style.display = 'flex'
  528. let sum = Object.entries(cart).map(([, val]) => val.count)
  529. countOrder.innerHTML = sum.reduce((a, b) => a + b)
  530. } else {
  531. countOrder.style.display = 'none'
  532. }
  533. })
  534. console.log(store.getState());
  535. store.subscribe(() => console.log(store.getState()))
  536. // store.dispatch(actionPromise('', delay(1000)))
  537. // store.dispatch(actionPromise('delay2000', delay(2000)))
  538. // store.dispatch(actionPromise('luke', fetch('https://swapi.dev/api/people/1/').then(res => res.json())))