js.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667
  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(state) //и запускаем подписчиков
  15. }
  16. }
  17. return {
  18. getState, //добавление функции getState в результирующий объект
  19. dispatch,
  20. subscribe //добавление subscribe в объект
  21. }
  22. }
  23. const jwtDecode = (token) => {
  24. try {
  25. let payload = JSON.parse(atob(token.split('.')[1]))
  26. console.log(payload)
  27. return payload
  28. } catch (e) {
  29. return undefined
  30. }
  31. }
  32. //---------------------------------------getGql---------------------------------------------
  33. const getGql = url =>
  34. (query, variables) => fetch(url, {
  35. method: 'POST',
  36. headers: {
  37. "Content-Type": "application/json",
  38. ...(localStorage.authToken ? { "Authorization": "Bearer " + localStorage.authToken } : {})
  39. },
  40. body: JSON.stringify({ query, variables })
  41. }).then(res => res.json())
  42. .then(data => {
  43. if (data.data) {
  44. return Object.values(data.data)[0]
  45. }
  46. else throw new Error(JSON.stringify(data.errors))
  47. })
  48. const url = 'http://shop-roles.node.ed.asmer.org.ua/'
  49. const gql = getGql(url + 'graphql')
  50. //------------------------------------------------PromiseReducer---------------------------------
  51. function promiseReducer(state={}, {type, status, payload, error, name}){
  52. if (type === 'PROMISE'){
  53. return {
  54. ...state,
  55. [name] : {status, payload, error}
  56. }
  57. }
  58. return state
  59. }
  60. const actionPending = (name) => ({type: 'PROMISE', status: 'PENDING', name})
  61. const actionFulfilled = (name, payload) => ({type: 'PROMISE', status: 'FULFILLED', name, payload})
  62. const actionRejected = (name, error) => ({type: 'PROMISE', status: 'REJECTED', name, error})
  63. //-----------------------------------------------------actionPromise-------------------------------------------
  64. const actionPromise = (name, promise) =>
  65. async dispatch => {
  66. dispatch(actionPending(name)) //сигнализируем redux, что промис начался
  67. try{
  68. const payload = await promise //ожидаем промиса
  69. dispatch(actionFulfilled(name, payload)) //сигнализируем redux, что промис успешно выполнен
  70. return payload //в месте запуска store.dispatch с этим thunk можно так же получить результат промиса
  71. }
  72. catch (error){
  73. dispatch(actionRejected(name, error)) //в случае ошибки - сигнализируем redux, что промис несложился
  74. }
  75. }
  76. //----------------------------------------------------authReducer---------------------------------------
  77. function authReducer(state={}, {type, token}) {
  78. if (type === 'AUTH_LOGOUT'){
  79. window.localStorage.removeItem('authToken');
  80. return {}
  81. }
  82. if(type === "AUTH_LOGIN"){
  83. try{
  84. window.localStorage.setItem('authToken',token);
  85. return {
  86. token: token,
  87. payload: jwtDecode(token)
  88. }
  89. }catch (e) {
  90. }
  91. }
  92. return state
  93. }
  94. const actionAuthLogin = token => ({type: 'AUTH_LOGIN', token})
  95. const actionAuthLogout = () => ({type: 'AUTH_LOGOUT'})
  96. //--------------------------------------------------cartReducer------------------------------------------
  97. function cartReducer (state = {}, {type, good, count=1}) {
  98. if (type === 'CART_ADD') {
  99. return {
  100. ...state,
  101. [good._id]: {
  102. good,
  103. count: +count}
  104. }
  105. }
  106. if (type === 'CART_SUB') {
  107. if (state([good._id].count - count) <= 0) {
  108. delete state[good._id]
  109. } else {
  110. return {
  111. ...state,
  112. [good._id]: {
  113. good,
  114. count: state[good._id].count - count}
  115. }
  116. }
  117. }
  118. if (type === 'CART_DEL') {
  119. delete state[good._id]
  120. return {...state}
  121. }
  122. if (type === 'CART_SET') {
  123. return {
  124. ...state,
  125. [good._id]: {
  126. good,
  127. count}
  128. }
  129. }
  130. if (type === 'CART_CLEAR') {
  131. state = {}
  132. }
  133. return state
  134. }
  135. const actionCartAdd = (good, count=1) => ({type: 'CART_ADD', count, good})
  136. const actionCartSub = (good, count=1) => ({type: 'CART_SUB', count, good})
  137. const actionCartDel = (good) => ({type: 'CART_DEL', good})
  138. const actionCartSet = (good, count=1) => ({type: 'CART_SET', count, good})
  139. const actionCartClear = () => ({type: 'CART_CLEAR'})
  140. //---------------------------------------------localStoredReducer---------------------------------
  141. function localStoredReducer(originalReducer, localStorageKey) {
  142. function wrapper(state, action) {
  143. if (!state) {
  144. try {
  145. return JSON.parse(localStorage[localStorageKey])
  146. }
  147. catch { }
  148. }
  149. let res = originalReducer(state, action)
  150. localStorage[localStorageKey] = JSON.stringify(res)
  151. return res;
  152. }
  153. return wrapper
  154. }
  155. // -------------------------------------------CombineReducers---------------------------------------
  156. function combineReducers(reducers){
  157. function totalReducer(state={}, action){
  158. const newTotalState = {}
  159. for (const [reducerName, reducer] of Object.entries(reducers)){
  160. const newSubState = reducer(state[reducerName], action)
  161. if (newSubState !== state[reducerName]){
  162. newTotalState[reducerName] = newSubState
  163. }
  164. }
  165. if (Object.keys(newTotalState).length){
  166. return {...state, ...newTotalState}
  167. }
  168. return state
  169. }
  170. return totalReducer
  171. }
  172. const totalReducer = combineReducers({
  173. promise: promiseReducer,
  174. auth: localStoredReducer(authReducer,'auth'),
  175. cart: localStoredReducer(cartReducer,'cart')
  176. })
  177. const store = createStore(totalReducer)
  178. store.subscribe(() => console.log(store.getState()))
  179. //Запрос на список корневых категорий
  180. const actionRootCats = () =>
  181. actionPromise('rootCats', gql(`query rootCats2{
  182. CategoryFind(query: "[{\\"parent\\": null}]"){
  183. _id
  184. name
  185. }
  186. }`))
  187. store.dispatch(actionRootCats())
  188. //Запрос для получения одной категории с товарами и картинками
  189. const oneCatWithGoods = (_id) =>
  190. actionPromise('oneCatWithGoods', gql(`query oneCatWithGoods ($q:String) {
  191. CategoryFindOne (query: $q){
  192. _id
  193. name
  194. parent{
  195. _id
  196. name}
  197. subCategories {
  198. _id
  199. name
  200. },
  201. goods {
  202. _id
  203. name
  204. price
  205. description
  206. images {
  207. url
  208. }
  209. }
  210. }}`,
  211. {q: JSON.stringify([{_id}])}
  212. ))
  213. //Запрос на получение товара с описанием и картинками
  214. const goodWithDescAndImg = (_id) =>
  215. actionPromise('goodWithDescAndImg', gql(`query goodWithDescAndImg ($q:String) {
  216. GoodFindOne (query: $q){
  217. _id
  218. name
  219. price
  220. description
  221. images {
  222. url
  223. }
  224. }}`,
  225. {q: JSON.stringify([{_id}])}
  226. ))
  227. // Запрос на регистрацию
  228. const registration = (login, password) =>
  229. actionPromise ('registration', gql(`mutation registration ($login:String, $password: String) {
  230. UserUpsert (user: {login: $login, password: $password}) {
  231. _id createdAt
  232. }
  233. }`,
  234. {"login" : login, "password": password}
  235. ))
  236. // Запрос на логин
  237. const loginUser = (login, password) =>
  238. actionPromise(
  239. 'login',
  240. gql(
  241. `query log($login: String, $password: String) {
  242. login(login: $login, password: $password)
  243. }`,
  244. {login, password}
  245. )
  246. )
  247. // Запрос истории заказов
  248. const historyOfOrders = () =>
  249. actionPromise('historyOfOrders', gql(`query historyOfOrders ($q: String) {
  250. OrderFind(query: $q) {
  251. _id
  252. total
  253. createdAt
  254. orderGoods {
  255. good {
  256. name
  257. }
  258. price
  259. count
  260. total
  261. }
  262. total
  263. }
  264. }`,
  265. {q: JSON.stringify([{}])}
  266. ))
  267. store.dispatch(actionRootCats())
  268. // Запрос оформления заказа
  269. const NewOrder = (orderGoods) =>
  270. actionPromise('NewOrder', gql(`mutation NewOrder($order: OrderInput) {
  271. OrderUpsert(order: $order) {
  272. _id
  273. orderGoods {
  274. _id
  275. price
  276. count
  277. total
  278. good {
  279. name
  280. _id
  281. price
  282. images {
  283. url
  284. }
  285. }
  286. }
  287. }
  288. }`,
  289. {order: {orderGoods}}
  290. ))
  291. //-----------------------------------Отрисовка категорий-------------------------------------
  292. store.subscribe(() => {
  293. const {status, payload, error} = store.getState().promise.rootCats
  294. if (status === 'FULFILLED'){
  295. aside.innerHTML = ''
  296. for (const {_id, name} of payload){
  297. aside.innerHTML += `<a href= "#/category/${_id}">${name}</a>`
  298. }
  299. }
  300. })
  301. //--------------------------------------отрисовка товаров в категории-----------------------
  302. store.subscribe(() => {
  303. const {status, payload, error} = store.getState().promise?.oneCatWithGoods || {}
  304. const [,route] = location.hash.split('/')
  305. if(route !== 'category') {
  306. return
  307. }
  308. if (status === 'FULFILLED'){
  309. main.innerHTML = ''
  310. const {name, goods, subCategories} = payload
  311. main.innerHTML = `<h1>${name}</h1>`
  312. if (subCategories !== null) {
  313. for (const {_id, name} of subCategories) {
  314. main.innerHTML += `<a href= "#/category/${_id}">${name}</a>`
  315. console.log(name)
  316. }
  317. }
  318. for (const {_id, name, price, images} of goods){
  319. for (const img of images) {
  320. main.innerHTML += `<img src= "${url+ img.url}"> </br>`
  321. }
  322. main.innerHTML += `<a href= "#/good/${_id}">${name} </br> ${price} грн</a>`
  323. }
  324. }}
  325. )
  326. //-------------------------------------Отрисовка товара------------------------------------------
  327. store.subscribe(() => {
  328. const {status, payload, error} = store.getState().promise?.goodWithDescAndImg || { }
  329. const [,route] = location.hash.split('/')
  330. if(route !== 'good') {
  331. return
  332. }
  333. if (status === 'FULFILLED'){
  334. main.innerHTML = ''
  335. const {name, description, images, price} = payload
  336. main.innerHTML = `<h1>${name}</h1>`
  337. for (const img of images) {
  338. main.innerHTML += `<img src= "${url+ img.url}">`
  339. }
  340. main.innerHTML += `<p>${description}</p>
  341. <p>${price} грн. </p>
  342. <button id="buy"> В корзину </button>`
  343. const buyButton = document.getElementById('buy')
  344. cartIcon.innerHTML = ''
  345. buyButton.onclick = function () {
  346. store.dispatch(actionCartAdd({_id: name, price: price, img: images}))
  347. }
  348. }
  349. }
  350. )
  351. //----------------------------------Отрисовка цифры в корзине-------------------------------
  352. store.subscribe(() => {
  353. const {cart} = store.getState()
  354. let summ = 0
  355. for(const {count} of Object.values(cart)) {
  356. summ += +count
  357. }
  358. cartIcon.innerHTML = `<b>${summ}</b>`
  359. })
  360. //-----------------------------------------Логин----------------------------------------
  361. const loginButton = document.getElementById('login')
  362. loginForm.append(loginButton)
  363. loginButton.onclick = () => location.href = `#/login`
  364. const actionFullLogin = (login, password) =>
  365. async (dispatch) => {
  366. const token = await dispatch(loginUser(login, password))
  367. if(typeof token === "string"){
  368. dispatch(actionAuthLogin(token))
  369. main.innerHTML = `<h1>Вы вошли на сайт</h1>`
  370. } else {
  371. main.innerHTML =
  372. `<p>Вы ввели неправильные логин или пароль. Повторите попытку </p>
  373. <button id="buttonRepeat">Повторить попытку</button>`
  374. const loginRepeat = document.getElementById('buttonRepeat')
  375. loginRepeat.onclick = () => {
  376. location.reload()
  377. location.href = `#/login`
  378. }
  379. }
  380. }
  381. //-----------------------------------------Авторизация-------------------------------------
  382. store.subscribe(() => {
  383. if(!store.getState().auth) return;
  384. const {payload} = store.getState().auth;
  385. if(payload){
  386. loginForm.innerHTML =
  387. `<button id="history"> История заказов </button>
  388. <button id="logOut"> Выйти с сайта </button>`
  389. loginButton.hidden = true
  390. registration.hidden = true
  391. const historyButton = document.getElementById('history')
  392. historyButton.onclick = function () {
  393. location.href = `#/history`
  394. }
  395. const logOutButton = document.getElementById('logOut')
  396. logOutButton.onclick = function () {
  397. store.dispatch(actionAuthLogout())
  398. main.innerHTML = ` `
  399. loginForm.innerHTML = ` `
  400. loginButton.hidden = false
  401. registration.hidden = false
  402. }
  403. }
  404. })
  405. //------------------------------------Регистрация--------------------------------------------
  406. const registrationButton = document.getElementById('registration')
  407. loginForm.append(registrationButton)
  408. registrationButton.onclick = () => location.href = `#/register`
  409. const actionFullRegister = (login, password) =>
  410. async (dispatch) => {
  411. let userReg = await dispatch(registration(login, password))
  412. if(userReg){
  413. dispatch(actionFullLogin(login,password))
  414. } else {
  415. main.innerHTML = `Регистрация не удалась. Повторите попытку ещё раз.
  416. <button id="buttonRepeatReg">Повторить попытку</button>`
  417. const buttonRepeatReg = document.getElementById('buttonRepeatReg')
  418. buttonRepeatReg.onclick = () => {
  419. location.reload()
  420. location.href = `#/register`
  421. }
  422. }
  423. }
  424. //-------------------------------------------Заказ-------------------------------------
  425. const newOrder = () => async (dispatch, getState) => {
  426. let { cart } = getState();
  427. const orderGoods = Object.entries(cart).map(([_id, { count }]) => ({ good: { _id }, count }));
  428. let result = await dispatch(NewOrder(orderGoods))
  429. if (result?._id) {
  430. dispatch(actionCartClear())
  431. }
  432. }
  433. //--------------------------------------Корзина------------------------------------------
  434. store.subscribe ( () => {
  435. let cartIcon = document.getElementById('cartIcon')
  436. cartIcon.onclick = function myCart() {
  437. location.href = `#/cartIcon`
  438. console.log(store.getState().cart)
  439. let storeCart = store.getState().cart
  440. main.innerHTML = `<h1>Корзина</h1>`
  441. for (let i=0; i<(Object.keys(storeCart).length); i++){
  442. let div = document.createElement('div')
  443. div.id = i
  444. main.append(div)
  445. let order = document.getElementById(i)
  446. let name = Object.keys(storeCart)[i]
  447. order.innerHTML += `<p>${store.getState().cart[name].good._id}</p>`
  448. for (const img of store.getState().cart[name].good.img) {
  449. order.innerHTML += `<p><img src= "${url+ img.url}"></p>`
  450. }
  451. order.innerHTML +=
  452. `<p>${store.getState().cart[name].count} шт</p>
  453. <p>Итого: ${store.getState().cart[name].count * store.getState().cart[name].good.price} </p>`
  454. let input = document.createElement('input')
  455. input.type = 'number'
  456. input.value = store.getState().cart[name].count
  457. order.append(input)
  458. let divForBtn = document.createElement('div')
  459. order.append(divForBtn)
  460. let button = document.createElement('button')
  461. button.id = 'delCartBtn'
  462. button.innerText = 'Удалить товар'
  463. divForBtn.append(button)
  464. input.oninput = function () {
  465. if (input.value <= 0){
  466. store.dispatch(actionCartDel({_id: name}))
  467. myCart()
  468. }
  469. console.log(input.value, name)
  470. store.dispatch(actionCartSet({_id: name, price: store.getState().cart[name].good.price, img: store.getState().cart[name].good.img}, input.value))
  471. myCart()
  472. }
  473. button.onclick = function () {
  474. store.dispatch(actionCartDel({_id: name}))
  475. myCart()
  476. }
  477. }
  478. let btnCreateOrder = document.createElement('button')
  479. btnCreateOrder.id = 'createOrder'
  480. btnCreateOrder.innerText = 'Оформить заказ'
  481. main.append(btnCreateOrder)
  482. const idCreateOrderBtn = document.getElementById('createOrder')
  483. if (Object.keys(store.getState().auth).length === 0) {
  484. idCreateOrderBtn.disabled = true
  485. }
  486. if (Object.keys(store.getState().auth).length !== 0) {
  487. idCreateOrderBtn.disabled = false
  488. idCreateOrderBtn.onclick = function () {
  489. store.dispatch(newOrder())
  490. store.dispatch(actionCartClear())
  491. myCart()
  492. }
  493. }
  494. }
  495. })
  496. //--------------------------------------------История заказов--------------------------------------
  497. store.subscribe ( () => {
  498. const {status, payload, error} = store.getState().promise?.historyOfOrders || { }
  499. const [,route] = location.hash.split('/')
  500. if(route !== 'history') {
  501. return
  502. }
  503. if (status === 'FULFILLED'){
  504. main.innerHTML = `<h1> История заказов </h1>`
  505. const {_id, total} = payload
  506. console.log(payload)
  507. for(const order of payload){
  508. const {_id, total} = order
  509. main.innerHTML +=
  510. `<div style="width: 300px; border: solid skyblue;">
  511. <p>Номер заказа: ${_id}</p>
  512. <p>Всего: ${total} денег </p>
  513. </div>
  514. `
  515. }
  516. }
  517. })
  518. window.onhashchange = () => {
  519. const [,route, _id] = location.hash.split('/')
  520. const routes = {
  521. category() {
  522. store.dispatch(oneCatWithGoods(_id))
  523. },
  524. good(){
  525. store.dispatch(goodWithDescAndImg(_id))
  526. },
  527. login(){
  528. main.innerHTML =
  529. `<h2 id="inputTitle">Вход на сайт:</h2>
  530. <input id="loginInput" type="text" name="login" placeholder="Введите логин">
  531. <input id="passwordInput" type="password" name="password" placeholder="Введите пароль">
  532. <button id="sign_in">Войти</button>`
  533. const sign_inBtn = document.getElementById('sign_in')
  534. sign_inBtn.onclick = function () {
  535. store.dispatch(actionFullLogin(loginInput.value, passwordInput.value))
  536. }
  537. },
  538. register(){
  539. main.innerHTML =
  540. `<h2>Регистрация:</h2>
  541. <input id="loginReg" type="text" name="login" placeholder="Введите логин">
  542. <input id="passwordReg" type="password" name="password" placeholder="Введите пароль">
  543. <button id="reg">Зарегистрироваться</button>`
  544. const regBtn = document.getElementById('reg')
  545. regBtn.onclick = function () {
  546. store.dispatch(actionFullRegister(loginReg.value, passwordReg.value))
  547. }
  548. },
  549. cart(){},
  550. history(){
  551. store.dispatch(historyOfOrders())
  552. }
  553. }
  554. if (route in routes){ //если route есть в routes
  555. routes[route]() //то запустить функцию, которая там лежит
  556. }
  557. }
  558. window.onhashchange()