main.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688
  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 actionSearchGoods = (value) =>
  159. actionPromise('sort', gql(`query gf($query: String){
  160. GoodFind(query: $query){
  161. _id, name, description, price, images{
  162. _id, url
  163. }
  164. }
  165. }`, {
  166. query: JSON.stringify([
  167. {
  168. $or: [{ title: value }, { name: value }, { description: value }] //регулярки пишутся в строках
  169. },
  170. {
  171. sort: [{ title: 1 }]
  172. } //сортируем по title алфавитно
  173. ])
  174. }))
  175. const actionRootCats = () =>
  176. actionPromise('rootCats', gql(`query {
  177. CategoryFind(query: "[{\\"parent\\":null}]"){
  178. _id name
  179. }
  180. }`))
  181. const actionCatById = (_id) =>
  182. actionPromise('catById', gql(`query catById($q: String){
  183. CategoryFindOne(query: $q){
  184. subCategories{name, _id}
  185. _id name goods {
  186. _id name price images {
  187. url
  188. }
  189. }
  190. }
  191. }`, { q: JSON.stringify([{ _id }]) }))
  192. const actionGoodById = (_id) => //добавить подкатегории
  193. actionPromise('goodById', gql(`query goodByID($q: String) {
  194. GoodFind(query: $q){
  195. name _id description price images{url}
  196. }
  197. }`, {
  198. q: JSON.stringify([{ _id }])
  199. }))
  200. const actionLogin = (login, password) =>
  201. actionPromise('login', gql(`query NameForMe1($login:String, $password:String){
  202. login(login:$login, password:$password)
  203. }`, { login, password }))
  204. const actionMyOrders = () =>
  205. actionPromise('myOrders', gql(`query Order{
  206. OrderGoodFind(query:"[{}]"){
  207. good{ name _id} _id total price count
  208. }
  209. }`, {}))
  210. const actionFullLogin = (login, password) =>
  211. async dispatch => {
  212. let token = await dispatch(actionLogin(login, password))
  213. if (token) {
  214. dispatch(actionAuthLogin(token))
  215. }
  216. }
  217. const actionRegister = (login, password) =>
  218. actionPromise('register', gql(`
  219. mutation reg($login:String, $password:String){
  220. UserUpsert(user:{
  221. login:$login,
  222. password:$password,
  223. nick:$login}){
  224. _id login
  225. }
  226. }
  227. `, { login, password }))
  228. const actionFullRegister = (login, password) =>
  229. async dispatch => {
  230. await actionRegister(login, password)
  231. let token = await dispatch(actionLogin(login, password))
  232. debugger
  233. if (token) {
  234. dispatch(actionAuthLogin(token))
  235. }
  236. }
  237. const store = createStore(combineReducer)
  238. const getGQL = url =>
  239. async (query, variables = {}) => {
  240. let obj = await fetch(url, {
  241. method: 'POST',
  242. headers: {
  243. "Content-Type": "application/json",
  244. Authorization: localStorage.authToken ? 'Bearer ' + localStorage.authToken : {},
  245. },
  246. body: JSON.stringify({ query, variables })
  247. })
  248. let a = await obj.json()
  249. if (!a.data && a.errors)
  250. throw new Error(JSON.stringify(a.errors))
  251. return a.data[Object.keys(a.data)[0]]
  252. }
  253. const backURL = 'http://shop-roles.asmer.fs.a-level.com.ua'
  254. const gql = getGQL(backURL + '/graphql');
  255. store.dispatch(actionRootCats())
  256. store.dispatch(actionGoodById())
  257. // store.dispatch(actionAuthLogin(token))
  258. // store.dispatch(actionPromise('delay2000', delay(1000)))
  259. window.onhashchange = () => {
  260. const [, route, _id] = location.hash.split('/')
  261. const routes = {
  262. category() {
  263. store.dispatch(actionCatById(_id))
  264. },
  265. good() { //задиспатчить actionGoodById
  266. store.dispatch(actionGoodById(_id))
  267. },
  268. login() {
  269. userAuthorizationFields(route)
  270. },
  271. register() {
  272. userAuthorizationFields(route)
  273. },
  274. order() { //задиспатчить actionGoodById
  275. renderOrder()
  276. },
  277. dashboard() {
  278. store.dispatch(actionMyOrders())
  279. }
  280. }
  281. if (route in routes)
  282. routes[route]()
  283. else {
  284. startPage()
  285. }
  286. }
  287. window.onhashchange()
  288. function startPage() {
  289. main.innerHTML = ""
  290. }
  291. //поля авторизации
  292. function userAuthorizationFields(key) {
  293. const userBox = document.createElement('div')
  294. userBox.setAttribute('id', 'userBox')
  295. const h2 = document.createElement('h2')
  296. const inputNick = document.createElement('input')
  297. const inputPassword = document.createElement('input')
  298. inputNick.type = 'text'
  299. inputPassword.type = 'password'
  300. const btnEnter = document.createElement('a')
  301. const btnClose = document.createElement('a')
  302. btnClose.onclick = () => {
  303. userBox.remove()
  304. overlay.style.display = 'none'
  305. }
  306. btnClose.innerText = 'X'
  307. btnClose.classList.add('close')
  308. btnClose.href = '#'
  309. btnEnter.href = '#'
  310. overlay.style.display = 'block'
  311. if (key === 'login') {
  312. h2.innerText = 'Log In'
  313. btnEnter.innerText = 'Log In'
  314. btnEnter.setAttribute('id', 'logIn')
  315. btnEnter.onclick = () => store.dispatch(actionFullLogin(inputNick.value, inputPassword.value))
  316. } else {
  317. h2.innerText = 'Register'
  318. btnEnter.innerText = 'Register'
  319. btnEnter.setAttribute('id', 'register')
  320. btnEnter.onclick = () => store.dispatch(actionFullRegister(inputNick.value, inputPassword.value))
  321. }
  322. userBox.append(h2)
  323. userBox.append(inputNick)
  324. userBox.append(btnClose)
  325. userBox.append(inputPassword)
  326. userBox.append(btnEnter)
  327. user.append(userBox)
  328. }
  329. //ссылки когда не авторизирован
  330. function noAuthorization() {
  331. const loginLink = document.createElement('a')
  332. loginLink.classList.add('user__link')
  333. loginLink.innerText = 'Log In'
  334. loginLink.href = '#/login'
  335. const registerLink = document.createElement('a')
  336. registerLink.href = '#/register'
  337. registerLink.innerText = 'Register'
  338. registerLink.classList.add('user__link')
  339. user.append(loginLink)
  340. user.append(registerLink)
  341. }
  342. // страница заказа
  343. function renderOrder() {
  344. const { cart, auth } = store.getState()
  345. const [, route, _id] = location.hash.split('/')
  346. main.innerHTML = ''
  347. if (Object.keys(cart).length !== 0 && route === 'order') {
  348. const orderTop = document.createElement('div')
  349. orderTop.classList.add('order-top')
  350. const orderCleanBtn = document.createElement('button')
  351. orderCleanBtn.classList.add('clean-order')
  352. orderCleanBtn.innerText = 'Clean order'
  353. orderCleanBtn.onclick = () => {
  354. store.dispatch(actionCleanCart())
  355. main.innerHTML = ''
  356. }
  357. orderTop.append(orderCleanBtn)
  358. main.append(orderTop)
  359. for (const key in cart) {
  360. const { _id, name, price, images } = cart[key].good
  361. const divContainer = document.createElement('div')
  362. divContainer.classList.add('product-order__inner')
  363. const img = document.createElement('img')
  364. img.src = `${backURL}/${images[0].url}`
  365. const a = document.createElement('a')
  366. a.href = `#/good/${_id}`
  367. a.innerText = name
  368. const input = document.createElement('input')
  369. input.type = 'number'
  370. input.min = '1'
  371. input.value = cart[key].count
  372. input.oninput = () => {
  373. spanTotal.innerHTML = `Сумма: <strong>${price * +input.value}$</strong>`
  374. store.dispatch(actionChangeCart(cart[key].good, input.value))
  375. }
  376. const spanPrice = document.createElement('span')
  377. spanPrice.innerHTML = `Цена: ${price} $`
  378. const spanTotal = document.createElement('span')
  379. spanTotal.innerHTML = `Сумма: <strong>${price * +input.value} $</strong>`
  380. const buttonRemove = document.createElement('button')
  381. buttonRemove.innerText = 'x'
  382. buttonRemove.onclick = () => {
  383. store.dispatch(actionRemoveCart(cart[key].good))
  384. divContainer.remove()
  385. }
  386. divContainer.append(img)
  387. divContainer.append(a)
  388. divContainer.append(spanPrice)
  389. divContainer.append(input)
  390. divContainer.append(spanTotal)
  391. divContainer.append(buttonRemove)
  392. main.append(divContainer)
  393. }
  394. const orderSentBtn = document.createElement('button')
  395. orderSentBtn.innerText = 'Заказать'
  396. orderSentBtn.classList.add('order-sent__btn')
  397. if (auth?.token) {
  398. orderSentBtn.onclick = () => {
  399. store.dispatch(actionOrder())
  400. main.innerText = ' Спасибо, заказ оформлен'
  401. }
  402. } else orderSentBtn.onclick = () => {
  403. const err = document.createElement('div')
  404. err.innerText = 'Sorry, please Log In or Register Now'
  405. err.style.color = '#ff0000'
  406. main.prepend(err)
  407. }
  408. main.append(orderSentBtn)
  409. }
  410. }
  411. store.dispatch(actionRootCats())
  412. store.dispatch(actionGoodById())
  413. // рисуем категории
  414. store.subscribe(() => {
  415. let a = Object.entries(store.getState().promise).map(t => t[1].status === 'PENDING')
  416. let ss = el => el === true
  417. a.some(ss) ? preLoader.style.display = 'block' : preLoader.style.display = 'none'
  418. })
  419. store.subscribe(() => {
  420. const { rootCats } = store.getState().promise
  421. if (rootCats?.payload) {
  422. aside.innerHTML = ''
  423. for (const { _id, name } of rootCats?.payload) {
  424. const link = document.createElement('a')
  425. link.href = `#/category/${_id}`
  426. link.innerText = name
  427. aside.append(link)
  428. }
  429. }
  430. })
  431. // рисуем продукты категории
  432. store.subscribe(() => {
  433. const { catById } = store.getState().promise
  434. const [, route, _id] = location.hash.split('/')
  435. if (catById?.payload && route === 'category') {
  436. const { name, subCategories } = catById.payload
  437. main.innerHTML = `<h1>${name}</h1> `
  438. const subCatDiv = document.createElement('div')
  439. subCatDiv.classList.add('sub-catigories')
  440. subCategories ? subCategories.map(s => {
  441. const link = document.createElement('a')
  442. link.href = `#/category/${s._id}`
  443. link.innerText = s.name
  444. subCatDiv.append(link)
  445. }) : ''
  446. main.append(subCatDiv)
  447. for (const good of catById.payload.goods) {
  448. const { _id, name, price, images } = good
  449. const product = document.createElement('div')
  450. product.classList.add('product')
  451. const btn = document.createElement('button')
  452. btn.innerText = '+'
  453. let urlImage = images ? images[0].url : ''
  454. product.innerHTML = `<a class="product-title__link" href="#/good/${_id}">${name}</a>
  455. <div class="product__inner">
  456. <img src="${backURL}/${urlImage}" />
  457. <strong> ${price} $</strong>
  458. </div > `
  459. btn.onclick = () => store.dispatch(actionAddCart(good, 1))
  460. product.append(btn)
  461. main.append(product)
  462. }
  463. }
  464. })
  465. // отрисовка Истории заказов
  466. store.subscribe(() => {
  467. const { myOrders } = store.getState().promise
  468. const [, route, _id] = location.hash.split('/')
  469. if (myOrders?.payload && route === 'dashboard') {
  470. main.innerHTML = ''
  471. const table = document.createElement('table')
  472. table.setAttribute('border', '2')
  473. for (const { good, price, count, total } of myOrders.payload) {
  474. if (good !== null) {
  475. const tr = document.createElement('tr')
  476. const tdName = document.createElement('td')
  477. const tdPrice = document.createElement('td')
  478. const tdCount = document.createElement('td')
  479. const tdTotal = document.createElement('td')
  480. tdName.innerHTML = `<a href = "#/good/${good._id}" > ${good.name}</a > `
  481. tdPrice.innerText = price
  482. tdCount.innerText = count
  483. tdTotal.innerText = total
  484. tr.append(tdName)
  485. tr.append(tdPrice)
  486. tr.append(tdCount)
  487. tr.append(tdTotal)
  488. table.append(tr)
  489. }
  490. }
  491. main.append(table)
  492. }
  493. })
  494. // отрисовка Карточки продукты
  495. store.subscribe(() => {
  496. const { goodById } = store.getState().promise
  497. const [, route, _id] = location.hash.split('/')
  498. if (goodById?.payload && route === 'good') {
  499. main.innerHTML = ''
  500. const { name, description, price, images } = goodById.payload[0]
  501. const btn = document.createElement('button')
  502. btn.classList.add('productBtn')
  503. btn.onclick = () => store.dispatch(actionAddCart(goodById.payload[0], 1))
  504. btn.innerText = '+'
  505. main.innerHTML = `
  506. <div class="product-one">
  507. <div class="product-one__img">
  508. <img src="${backURL}/${images[0].url}" />
  509. </div>
  510. <div class="product-one__inner">
  511. <h2 class="product-one_title">${name}</h2>
  512. <p class="product-one__price"> <strong>${price} $</strong></p>
  513. <p class="product-one__description">
  514. <span>Обзор: ${description}</span>
  515. </p>
  516. </div>
  517. </div> `
  518. main.append(btn)
  519. }
  520. })
  521. // взависимости от страницы рисуем Log In / Registration
  522. store.subscribe(() => {
  523. const { auth } = store.getState()
  524. const [, route, _id] = location.hash.split('/')
  525. user.innerHTML = ''
  526. overlay.style.display = 'none'
  527. dashboardLink.style.display = 'none'
  528. if (auth?.payload) {
  529. const logOutBtn = document.createElement('button')
  530. logOutBtn.innerText = 'Выйти'
  531. logOutBtn.onclick = () => {
  532. store.dispatch(actionAuthLogout())
  533. store.dispatch(actionCleanCart())
  534. }
  535. user.innerHTML = `<h3> Hello, ${auth.payload.sub.login}</h3 >
  536. <div id="logOut"></div>`
  537. dashboardLink.style.display = 'block'
  538. logOut.append(logOutBtn)
  539. } else if (route === 'login' || route === 'register') {
  540. userAuthorizationFields(route)
  541. noAuthorization()
  542. } else if (user.children.length === 0) {
  543. noAuthorization()
  544. }
  545. })
  546. // счетчик корзины
  547. store.subscribe(() => {
  548. const { cart } = store.getState()
  549. if (Object.keys(cart).length !== 0) {
  550. countOrder.style.display = 'flex'
  551. let sum = Object.entries(cart).map(([, val]) => val.count)
  552. countOrder.innerHTML = sum.reduce((a, b) => a + b)
  553. } else {
  554. countOrder.style.display = 'none'
  555. }
  556. })
  557. search.oninput = () => {
  558. location.hash = '#/search'
  559. store.dispatch(actionSearchGoods('/' + search.value + '/'))
  560. }
  561. store.subscribe(() => {
  562. const { sort } = store.getState().promise
  563. const [, route, _id] = location.hash.split('/')
  564. if (sort?.payload && route === 'search') {
  565. main.innerHTML = ``
  566. if (sort.payload.length === 0) {
  567. main.innerHTML = `<strong>Совпадений нет!!!</strong>`
  568. }
  569. for (const { _id, name, price, images } of sort.payload) {
  570. const product = document.createElement('div')
  571. product.classList.add('product')
  572. const btn = document.createElement('button')
  573. btn.innerText = '+'
  574. let urlImage = images ? images[0].url : ''
  575. product.innerHTML = `<a class="product-title__link" href="#/good/${_id}">${name}</a>
  576. <div class="product__inner">
  577. <img src="${backURL}/${urlImage}" />
  578. <strong> ${price} $</strong>
  579. </div > `
  580. btn.onclick = () => store.dispatch(actionAddCart({ _id, name, price, images }, 1))
  581. product.append(btn)
  582. main.append(product)
  583. }
  584. }
  585. })
  586. console.log(store.getState());
  587. store.subscribe(() => console.log(store.getState()))
  588. // store.dispatch(actionPromise('', delay(1000)))
  589. // store.dispatch(actionPromise('delay2000', delay(2000)))
  590. // store.dispatch(actionPromise('luke', fetch('https://swapi.dev/api/people/1/').then(res => res.json())))