index.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761
  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 jwtDecode(token) {
  24. try {
  25. return JSON.parse(atob(token.split('.')[1]))
  26. }
  27. catch (e) {
  28. }
  29. }
  30. function authReducer(state = {}, { type, token }) {
  31. //{
  32. // token, payload
  33. //}
  34. if (type === 'AUTH_LOGIN') {
  35. //пытаемся токен раскодировать
  36. const payload = jwtDecode(token)
  37. if (payload) { //и если получилось
  38. return {
  39. token, payload //payload - раскодированный токен;
  40. }
  41. }
  42. }
  43. if (type === 'AUTH_LOGOUT') {
  44. return {}
  45. }
  46. return state;
  47. }
  48. function countReducer(state = { count: 0 }, { type }) {
  49. if (type === "COUNT_INC") {
  50. return {
  51. count: state.count + 1
  52. }
  53. }
  54. if (type === "COUNT_DEC") {
  55. return {
  56. count: state.count - 1
  57. }
  58. }
  59. return state
  60. }
  61. function localStoreReducer(reducer, localStorageKey) {
  62. function localStoredReducer(state, action) {
  63. // Если state === undefined, то достать старый state из local storage
  64. if (state === undefined) {
  65. try {
  66. return JSON.parse(localStorage[localStorageKey])
  67. } catch (e) { }
  68. }
  69. const newState = reducer(state, action)
  70. // Сохранить newState в local storage
  71. localStorage[localStorageKey] = JSON.stringify(newState)
  72. return newState
  73. }
  74. return localStoredReducer
  75. }
  76. function promiseReducer(state = {}, { type, name, status, payload, error }) {
  77. ////?????
  78. //ОДИН ПРОМИС:
  79. //состояние: PENDING/FULFILLED/REJECTED
  80. //результат
  81. //ошибка:
  82. //{status, payload, error}
  83. //{
  84. // name1:{status, payload, error}
  85. // name2:{status, payload, error}
  86. // name3:{status, payload, error}
  87. //}
  88. if (type === 'PROMISE') {
  89. return {
  90. ...state,
  91. [name]: { status, payload, error }
  92. }
  93. }
  94. return state
  95. }
  96. const actionPending = (name) => ({
  97. type: 'PROMISE',
  98. status: 'PENDING',
  99. name
  100. })
  101. const actionFulfilled = (name, payload) => ({
  102. type: 'PROMISE',
  103. status: 'FULFILLED',
  104. name,
  105. payload
  106. })
  107. const actionRejected = (name, error) => ({
  108. type: 'PROMISE',
  109. status: 'REJECTED',
  110. name,
  111. error
  112. })
  113. const actionPromise = (name, promise) =>
  114. async dispatch => {
  115. try {
  116. dispatch(actionPending(name))
  117. let payload = await promise
  118. dispatch(actionFulfilled(name, payload))
  119. return payload
  120. }
  121. catch (e) {
  122. dispatch(actionRejected(name, e))
  123. }
  124. }
  125. const delay = ms => new Promise(ok => setTimeout(() => ok(ms), ms))
  126. function combineReducers(reducers) { //пачку редьюсеров как объект {auth: authReducer, promise: promiseReducer}
  127. function combinedReducer(combinedState = {}, action) { //combinedState - типа {auth: {...}, promise: {....}}
  128. const newCombinedState = {}
  129. for (const [reducerName, reducer] of Object.entries(reducers)) {
  130. const newSubState = reducer(combinedState[reducerName], action)
  131. if (newSubState !== combinedState[reducerName]) {
  132. newCombinedState[reducerName] = newSubState
  133. }
  134. }
  135. if (Object.keys(newCombinedState).length === 0) {
  136. return combinedState
  137. }
  138. return { ...combinedState, ...newCombinedState }
  139. }
  140. return combinedReducer //нам возвращают один редьюсер, который имеет стейт вида {auth: {...стейт authReducer-а}, promise: {...стейт promiseReducer-а}}
  141. }
  142. function cartReducer(state = {}, { type, count = 1, good }) {
  143. // type CART_ADD CART_REMOVE CART_CLEAR CART_DEL
  144. // {
  145. // id1: {count: 1, good: {name, price, images, id}}
  146. // }
  147. if (type === "CART_ADD") {
  148. return {
  149. ...state,
  150. [good._id]: { count: count + (state[good._id]?.count || 0), good },
  151. };
  152. }
  153. if (type === "CART_DELETE") {
  154. if (state[good._id].count > 1) {
  155. return {
  156. ...state,
  157. [good._id]: {
  158. count: -count + (state[good._id]?.count || 0),
  159. good,
  160. },
  161. };
  162. }
  163. if (state[good._id].count === 1) {
  164. let { [good._id]: id1, ...newState } = state; //o4en strashnoe koldunstvo
  165. //delete newState[good._id]
  166. return newState;
  167. }
  168. }
  169. if (type === "CART_CLEAR") {
  170. return {};
  171. }
  172. if (type === "CART_REMOVE") {
  173. // let newState = {...state}
  174. let { [good._id]: id1, ...newState } = state; //o4en strashnoe koldunstvo
  175. //delete newState[good._id]
  176. return newState;
  177. }
  178. return state;
  179. }
  180. const backendURL = 'http://shop-roles.node.ed.asmer.org.ua/'
  181. //store.dispatch(actionPromise('delay1000', delay(1000)))
  182. //store.dispatch(actionPromise('delay3000', delay(3000)))
  183. //store.dispatch(actionPending('delay1000'))
  184. //delay(1000).then(result => store.dispatch(actionFulfilled('delay1000', result)),
  185. //error => store.dispatch(actionRejected('delay1000', error)))
  186. //store.dispatch(actionPending('delay3000'))
  187. //delay(3000).then(result => store.dispatch(actionFulfilled('delay3000', result)),
  188. //error => store.dispatch(actionRejected('delay3000', error)))
  189. const getGQL = (url) => (query, variables) =>
  190. fetch(url, {
  191. method: "POST",
  192. headers: {
  193. "Content-Type": "application/json",
  194. ...(localStorage.authToken
  195. ? { Authorization: "Bearer " + localStorage.authToken }
  196. : {}),
  197. },
  198. body: JSON.stringify({ query, variables }),
  199. })
  200. .then((res) => res.json())
  201. .then((data) => {
  202. if (data.data) {
  203. return Object.values(data.data)[0];
  204. } else throw new Error(JSON.stringify(data.errors));
  205. });
  206. const gql = getGQL(backendURL + "graphql");
  207. // const gql = (url, query, variables) => fetch(url, {
  208. // method: 'POST',
  209. // headers: {
  210. // "Content-Type": "application/json",
  211. // Accept: "application/json",
  212. // },
  213. // body: JSON.stringify({ query, variables })
  214. // }).then(res => res.json())
  215. // const backendURL = 'http://shop-roles.node.ed.asmer.org.ua/graphql'
  216. const actionRootCats = () =>
  217. actionPromise(
  218. 'rootCats',
  219. gql(
  220. `query {
  221. CategoryFind(query: "[{\\"parent\\":null}]"){
  222. _id name
  223. }
  224. }`
  225. )
  226. )
  227. const actionCatById = (_id) => //добавить подкатегории
  228. actionPromise(
  229. 'catById',
  230. gql(
  231. `query catById($q: String){
  232. CategoryFindOne(query: $q){
  233. _id name goods {
  234. _id name price images {
  235. url
  236. }
  237. }
  238. }
  239. }`,
  240. { q: JSON.stringify([{ _id }]) }
  241. )
  242. )
  243. const actionGoodById = (_id) =>
  244. actionPromise(
  245. 'goodByID',
  246. gql(
  247. `query goodByID($q:String){
  248. GoodFindOne(query: $q){
  249. _id
  250. name
  251. description
  252. price
  253. categories{
  254. _id
  255. name
  256. }
  257. images{
  258. url
  259. }
  260. }
  261. }`,
  262. { q: JSON.stringify([{ _id }]) }
  263. )
  264. )
  265. const actionRegistr = (login, password) =>
  266. actionPromise(
  267. 'registr',
  268. gql(
  269. `mutation register($login:String, $password:String){
  270. UserUpsert(user: {login:$login, password:$password}){
  271. _id login
  272. }
  273. }`,
  274. { login: login, password: password }
  275. )
  276. )
  277. const actionLogin = (login, password) =>
  278. actionPromise(
  279. 'login',
  280. gql(
  281. `query log($login:String, $password:String){
  282. login(login:$login, password:$password)
  283. }`,
  284. { login: login, password: password }
  285. )
  286. )
  287. const actionOrder = () => async (dispatch, getState) => {
  288. let { cart } = getState();
  289. const orderGoods = Object.entries(cart).map(([_id, { count }]) => ({
  290. good: { _id },
  291. count,
  292. }));
  293. let result = await dispatch(
  294. actionPromise(
  295. "order",
  296. gql(
  297. `
  298. mutation newOrder($order:OrderInput){
  299. OrderUpsert(order:$order)
  300. { _id total }
  301. }
  302. `,
  303. { order: { orderGoods } }
  304. )
  305. )
  306. );
  307. if (result?._id) {
  308. dispatch(actionCartClear());
  309. document.location.hash = "#/cart/";
  310. alert("Покупка успішна")
  311. }
  312. };
  313. const orderHistory = () =>
  314. actionPromise(
  315. "history",
  316. gql(` query OrderFind{
  317. OrderFind(query:"[{}]"){
  318. _id total createdAt orderGoods{
  319. count good{
  320. _id name price images{
  321. url
  322. }
  323. }
  324. owner{
  325. _id login
  326. }
  327. }
  328. }
  329. }
  330. `)
  331. );
  332. const actionAuthLogin = (token) =>
  333. (dispatch, getState) => {
  334. const oldState = getState()
  335. dispatch({ type: 'AUTH_LOGIN', token })
  336. const newState = getState()
  337. if (oldState !== newState)
  338. localStorage.authToken = token
  339. }
  340. const actionAuthLogout = () =>
  341. dispatch => {
  342. dispatch({ type: 'AUTH_LOGOUT' })
  343. localStorage.removeItem('authToken')
  344. }
  345. const actionCartAdd = (good, count = 1) => ({
  346. type: "CART_ADD",
  347. good,
  348. count
  349. });
  350. const actionCartChange = (good, count = 1) => ({
  351. type: "CART_CHANGE",
  352. good,
  353. count,
  354. }); ///oninput меняяем полностью
  355. const actionCartDelete = (good) => ({
  356. type: "CART_DELETE",
  357. good
  358. });
  359. const actionCartClear = () => ({
  360. type: "CART_CLEAR"
  361. });
  362. const actionCartRemove = (good) =>({
  363. type: "CART_REMOVE",
  364. good
  365. })
  366. const actionFullLogin = (login, password) => async (dispatch) => {
  367. let token = await dispatch(actionLogin(login, password))
  368. if (token) {
  369. dispatch(actionAuthLogin(token))
  370. }
  371. }
  372. const actionFullRegistr = (login, password) => async (dispatch) => {
  373. try {
  374. await dispatch(actionRegistr(login, password))
  375. }
  376. catch (e) {
  377. return console.log(e)
  378. }
  379. await dispatch(actionFullLogin(login, password))
  380. }
  381. const store = createStore(combineReducers({ auth: authReducer, promise: promiseReducer, cart: localStoreReducer(cartReducer, "cart") })) //не забудьте combineReducers если он у вас уже есть
  382. if (localStorage.authToken) {
  383. store.dispatch(actionAuthLogin(localStorage.authToken))
  384. }
  385. //const store = createStore(combineReducers({promise: promiseReducer, auth: authReducer, cart: cartReducer}))
  386. store.subscribe(() => console.log(store.getState()))
  387. store.dispatch(actionRootCats())
  388. // store.dispatch(actionLogin('test456', '123123'))
  389. store.subscribe(() => {
  390. const { rootCats } = store.getState().promise
  391. if (rootCats?.payload) {
  392. aside.innerHTML = ''
  393. for (let { _id, name } of rootCats?.payload) {
  394. const a = document.createElement('a')
  395. a.href = `#/category/${_id}`
  396. a.innerHTML = name
  397. aside.append(a)
  398. }
  399. }
  400. })
  401. store.subscribe(() => {
  402. const { catById } = store.getState().promise
  403. const [, route, _id] = location.hash.split('/')
  404. if (catById?.payload && route === 'category') {
  405. const { name, goods, _id } = catById?.payload
  406. main.innerHTML = `<h1>${name}</h1>`
  407. for (let { _id, name, price, images } of goods) {
  408. const cards = document.createElement("div")
  409. cards.innerHTML = `<h2>${name}</h2>
  410. <img src="${backendURL}/${images[0].url}"/>
  411. <strong>Ціна ${price} грн</strong>
  412. <a href="#/good/${_id}">Перейти</a>
  413. `;
  414. main.append(cards);
  415. const batonchikMars = document.createElement("button")
  416. batonchikMars.innerText = "Придбати"
  417. cards.append(batonchikMars)
  418. batonchikMars.onclick = () => {
  419. store.dispatch(actionCartAdd({_id, name, price, images }))
  420. }
  421. // const a = document.createElement('a')
  422. // const p = document.createElement('p')
  423. // p.href = `#/good/${_id}`
  424. // a.href = `#/good/${_id}`
  425. // a.innerHTML = name
  426. // p.innerHTML = price
  427. // main.append(a)
  428. // main.append(p)
  429. }
  430. }
  431. })
  432. store.subscribe(() => {
  433. const { goodByID } = store.getState().promise
  434. const [, route, _id] = location.hash.split('/')
  435. if (goodByID?.payload && route === 'good') {
  436. main.innerHTML = ""
  437. const { name, images, price, description } = goodByID?.payload
  438. // main.innerHTML = `<h1>${name}</h1> Продукт`
  439. const cards = document.createElement("div")
  440. cards.innerHTML = `<h2>${name}</h2>
  441. <img src="${backendURL}/${images[0].url}"/>
  442. <strong> Ціна ${price} грн</strong>
  443. <button id='buy'> Придбати</button>
  444. <p>${description}</p>`;
  445. main.append(cards)
  446. cards.style.marginTop = "10px"
  447. var btn = document.getElementById('buy')
  448. btn.onclick = () => {
  449. store.dispatch(actionCartAdd(goodByID.payload))
  450. }
  451. }
  452. })
  453. const bPopupContent = document.createElement("div");
  454. const obertkaDlyaTovara = document.createElement("div")
  455. const all = document.createElement('h2')
  456. const checkout = document.createElement("button")
  457. const clearToCart = document.createElement("button")
  458. store.subscribe(() => {
  459. obertkaDlyaTovara.innerHTML = ""
  460. const cartById = store.getState().cart
  461. let productCount = 0;
  462. let productPrice = 0
  463. for (let gPC of Object.values(cartById)) {
  464. const { good,count } = gPC
  465. productCount += count
  466. productPrice += good.price * count
  467. const tovar = document.createElement("div")
  468. tovar.id = "tovar"
  469. tovar.style.border = "3px solid blue"
  470. tovar.style.marginTop = "10px"
  471. const name = document.createElement('h1')
  472. const price = document.createElement('h3')
  473. const countById = document.createElement('p')
  474. const divDlyaKnopok = document.createElement("div")
  475. const plus = document.createElement("button")
  476. const minus = document.createElement("button")
  477. const deletGoods = document.createElement('button')
  478. plus.innerText = "+"
  479. minus.innerText = "-"
  480. deletGoods.innerText = "Видалити товар"
  481. tovar.append(name)
  482. tovar.append(price)
  483. tovar.append(countById)
  484. divDlyaKnopok.append(plus)
  485. divDlyaKnopok.append(minus)
  486. divDlyaKnopok.append(deletGoods)
  487. tovar.append(divDlyaKnopok)
  488. name.innerHTML = good.name
  489. price.innerHTML = good.price
  490. countById.innerHTML = count
  491. obertkaDlyaTovara.append(tovar)
  492. bPopupContent.append(obertkaDlyaTovara)
  493. plus.onclick = () => {
  494. store.dispatch(actionCartAdd(good))
  495. }
  496. minus.onclick = () => {
  497. store.dispatch(actionCartDelete(good))
  498. }
  499. deletGoods.onclick =() => {
  500. store.dispatch(actionCartRemove(good))
  501. }
  502. }
  503. clearToCart.id = "clearToCart"
  504. clearToCart.innerHTML = "Очистити кошик"
  505. clearToCart.style.margin = "0 auto"
  506. clearToCart.style.marginBottom = "20px"
  507. clearToCart.style.background = "blue"
  508. clearToCart.style.color = "yellow"
  509. bPopupContent.append(clearToCart)
  510. checkout.id = "checkout"
  511. checkout.innerHTML = "Оформити замовлення"
  512. checkout.style.margin = "0 auto"
  513. checkout.style.background = "blue"
  514. checkout.style.color = "yellow"
  515. bPopupContent.append(checkout)
  516. all.id = "all"
  517. all.innerHTML = "Всього: " + productPrice
  518. bPopupContent.append(all)
  519. all.style.marginLeft = "90%"
  520. clearToCart.onclick = () => {
  521. all.innerHTML = " "
  522. store.dispatch(actionCartClear())
  523. }
  524. checkout.onclick = () => {
  525. all.innerHTML = " "
  526. store.dispatch(actionOrder());
  527. store.dispatch(orderHistory());
  528. }
  529. korzina.innerHTML = "Кошик: " + productCount
  530. })
  531. store.subscribe(() => {
  532. batton.onclick = () => {
  533. store.dispatch(actionFullLogin(login.value, password.value))
  534. }
  535. battonchik.onclick = () => {
  536. store.dispatch(actionFullRegistr(login.value, password.value))
  537. }
  538. battonSMakom.onclick = () => {
  539. store.dispatch(actionAuthLogout())
  540. }
  541. const payload = store.getState().auth.token;
  542. if (payload) {
  543. korzina.style.display = "block"
  544. login.style.display = "none"
  545. password.style.display = "none"
  546. batton.style.display = "none"
  547. battonchik.style.display = "none"
  548. battonSMakom.style.display = "block"
  549. accaunt.style.display = "block"
  550. accaunt.innerText = jwtDecode(payload).sub.login;
  551. purchaseHistory.style.display = "block"
  552. } else {
  553. korzina.style.display = "none"
  554. battonSMakom.style.display = "none"
  555. login.style.display = "block"
  556. password.style.display = "block"
  557. batton.style.display = "block"
  558. battonchik.style.display = "block"
  559. accaunt.style.display = "none"
  560. purchaseHistory.style.display = "none"
  561. }
  562. })
  563. store.dispatch(orderHistory());
  564. const h2 = document.createElement("h2")
  565. store.subscribe(() => {
  566. const { history } = store.getState().promise;
  567. const [, route] = location.hash.split("/");
  568. purchaseHistory.onclick = () => {
  569. const bPopup = document.createElement("div");
  570. const bPopupContent = document.createElement("div");
  571. bPopup.id = "b-popup";
  572. bPopup.className = "b-popup";
  573. bPopupContent.className = "b-popup-content b-poput-container-flex";
  574. header.append(bPopup);
  575. bPopup.append(bPopupContent);
  576. const buttonCloseCart = document.createElement("button");
  577. buttonCloseCart.innerText = "×";
  578. buttonCloseCart.id = "buttonCloseCartId";
  579. bPopupContent.append(buttonCloseCart);
  580. buttonCloseCart.onclick = () => {
  581. var parent = document.getElementById("header");
  582. var child = document.getElementById("b-popup");
  583. parent.removeChild(child);
  584. };
  585. for (let [key, value] of Object.entries(history.payload)) {
  586. const { _id, good, createdAt, total, orderGoods } = value;
  587. const h2 = document.createElement("h2");
  588. h2.className = "h2History"
  589. const dateOfOrder = new Date(+createdAt);
  590. for( let {good,count} of orderGoods){
  591. h2.innerHTML = `Дата: ${dateOfOrder.toLocaleDateString()} </br>
  592. Час:${dateOfOrder.toLocaleTimeString()} </br>
  593. Товар: ${good.name} </br>
  594. Кількість: ${count}</br>
  595. Ціна одного товару: ${good.price} </br>
  596. Загальна ціна: ${total}`;
  597. bPopupContent.append(h2);
  598. h2.style.border = "blue solid 2px"
  599. }
  600. if (Object.keys(history.payload).length == 0) {
  601. const p = document.createElement("p");
  602. p.innerHTML = "<p>Ще немає покупок</p>";
  603. card.append(p);
  604. }
  605. }
  606. };
  607. });
  608. const buttonCloseCart = document.createElement("button");
  609. buttonCloseCart.innerText = `×`;
  610. buttonCloseCart.id = "buttonCloseCartId";
  611. bPopupContent.append(buttonCloseCart);
  612. buttonCloseCart.onclick = () => {
  613. var parent = document.getElementById("header");
  614. var child = document.getElementById("b-popup");
  615. parent.removeChild(child);
  616. };
  617. function bPopupCreate() {
  618. const bPopup = document.createElement("div");
  619. bPopup.id = "b-popup";
  620. bPopup.className = "b-popup";
  621. bPopupContent.className = "b-popup-content b-poput-container-flex";
  622. header.append(bPopup);
  623. bPopup.append(bPopupContent);
  624. }
  625. korzina.onclick = () => {
  626. bPopupCreate()
  627. }
  628. window.onhashchange = () => {
  629. const [, route, _id] = location.hash.split('/')
  630. console.log(route, _id)
  631. const routes = {
  632. category() {
  633. store.dispatch(actionCatById(_id))
  634. },
  635. good() {
  636. store.dispatch(actionGoodById(_id))
  637. },
  638. dashboard() {
  639. store.dispatch(orderHistory());
  640. },
  641. }
  642. if (route in routes) {
  643. routes[route]()
  644. }
  645. }
  646. window.onhashchange()