main.js 22 KB

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