index.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727
  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 actionFullLogin = (login, password) => async (dispatch) => {
  363. let token = await dispatch(actionLogin(login, password))
  364. if (token) {
  365. dispatch(actionAuthLogin(token))
  366. }
  367. }
  368. const actionFullRegistr = (login, password) => async (dispatch) => {
  369. try {
  370. await dispatch(actionRegistr(login, password))
  371. }
  372. catch (e) {
  373. return console.log(e)
  374. }
  375. await dispatch(actionFullLogin(login, password))
  376. }
  377. const store = createStore(combineReducers({ auth: authReducer, promise: promiseReducer, cart: localStoreReducer(cartReducer, "cart") })) //не забудьте combineReducers если он у вас уже есть
  378. if (localStorage.authToken) {
  379. store.dispatch(actionAuthLogin(localStorage.authToken))
  380. }
  381. //const store = createStore(combineReducers({promise: promiseReducer, auth: authReducer, cart: cartReducer}))
  382. store.subscribe(() => console.log(store.getState()))
  383. store.dispatch(actionRootCats())
  384. // store.dispatch(actionLogin('test456', '123123'))
  385. store.subscribe(() => {
  386. const { rootCats } = store.getState().promise
  387. if (rootCats?.payload) {
  388. aside.innerHTML = ''
  389. for (let { _id, name } of rootCats?.payload) {
  390. const a = document.createElement('a')
  391. a.href = `#/category/${_id}`
  392. a.innerHTML = name
  393. aside.append(a)
  394. }
  395. }
  396. })
  397. store.subscribe(() => {
  398. const { catById } = store.getState().promise
  399. const [, route, _id] = location.hash.split('/')
  400. if (catById?.payload && route === 'category') {
  401. const { name, goods, _id } = catById?.payload
  402. main.innerHTML = `<h1>${name}</h1>`
  403. for (let { _id, name, price, images } of goods) {
  404. const cards = document.createElement("div")
  405. cards.innerHTML = `<h2>${name}</h2>
  406. <img src="${backendURL}/${images[0].url}"/>
  407. <strong>Ціна ${price} грн</strong>
  408. <a href="#/good/${_id}">Перейти</a> `;
  409. main.append(cards);
  410. // const a = document.createElement('a')
  411. // const p = document.createElement('p')
  412. // p.href = `#/good/${_id}`
  413. // a.href = `#/good/${_id}`
  414. // a.innerHTML = name
  415. // p.innerHTML = price
  416. // main.append(a)
  417. // main.append(p)
  418. }
  419. }
  420. })
  421. store.subscribe(() => {
  422. const { goodByID } = store.getState().promise
  423. const [, route, _id] = location.hash.split('/')
  424. if (goodByID?.payload && route === 'good') {
  425. main.innerHTML = ""
  426. const { name, images, price, description } = goodByID?.payload
  427. // main.innerHTML = `<h1>${name}</h1> Продукт`
  428. const cards = document.createElement("div")
  429. cards.innerHTML = `<h2>${name}</h2>
  430. <img src="${backendURL}/${images[0].url}"/>
  431. <strong> Ціна ${price} грн</strong>
  432. <button id='buy'> Придбати</button>
  433. <p>${description}</p>`;
  434. main.append(cards)
  435. cards.style.marginTop = "10px"
  436. var btn = document.getElementById('buy')
  437. btn.onclick = () => {
  438. store.dispatch(actionCartAdd(goodByID.payload))
  439. }
  440. }
  441. })
  442. const bPopupContent = document.createElement("div");
  443. const obertkaDlyaTovara = document.createElement("div")
  444. const all = document.createElement('h2')
  445. const checkout = document.createElement("button")
  446. const clearToCart = document.createElement("button")
  447. store.subscribe(() => {
  448. obertkaDlyaTovara.innerHTML = ""
  449. const cartById = store.getState().cart
  450. let productCount = 0;
  451. let productPrice = 0
  452. for (let gPC of Object.values(cartById)) {
  453. const { good,count } = gPC
  454. productCount += count
  455. productPrice += good.price * count
  456. const tovar = document.createElement("div")
  457. tovar.id = "tovar"
  458. tovar.style.border = "3px solid blue"
  459. tovar.style.marginTop = "10px"
  460. const name = document.createElement('h1')
  461. const price = document.createElement('h3')
  462. const countById = document.createElement('p')
  463. const divDlyaKnopok = document.createElement("div")
  464. const plus = document.createElement("button")
  465. const minus = document.createElement("button")
  466. plus.innerText = "+"
  467. minus.innerText = "-"
  468. tovar.append(name)
  469. tovar.append(price)
  470. tovar.append(countById)
  471. divDlyaKnopok.append(plus)
  472. divDlyaKnopok.append(minus)
  473. tovar.append(divDlyaKnopok)
  474. name.innerHTML = good.name
  475. price.innerHTML = good.price
  476. countById.innerHTML = count
  477. obertkaDlyaTovara.append(tovar)
  478. bPopupContent.append(obertkaDlyaTovara)
  479. plus.onclick = () => {
  480. store.dispatch(actionCartAdd(good))
  481. }
  482. minus.onclick = () => {
  483. store.dispatch(actionCartDelete(good))
  484. }
  485. }
  486. clearToCart.id = "clearToCart"
  487. clearToCart.innerHTML = "Очистити кошик"
  488. clearToCart.style.margin = "0 auto"
  489. clearToCart.style.marginBottom = "20px"
  490. clearToCart.style.background = "blue"
  491. clearToCart.style.color = "yellow"
  492. bPopupContent.append(clearToCart)
  493. checkout.id = "checkout"
  494. checkout.innerHTML = "Оформити замовлення"
  495. checkout.style.margin = "0 auto"
  496. checkout.style.background = "blue"
  497. checkout.style.color = "yellow"
  498. bPopupContent.append(checkout)
  499. all.id = "all"
  500. all.innerHTML = "Всього: " + productPrice
  501. bPopupContent.append(all)
  502. all.style.marginLeft = "90%"
  503. clearToCart.onclick = () => {
  504. all.innerHTML = " "
  505. store.dispatch(actionCartClear())
  506. }
  507. checkout.onclick = () => {
  508. all.innerHTML = " "
  509. store.dispatch(actionOrder());
  510. store.dispatch(orderHistory());
  511. }
  512. })
  513. store.subscribe(() => {
  514. batton.onclick = () => {
  515. store.dispatch(actionFullLogin(login.value, password.value))
  516. }
  517. battonchik.onclick = () => {
  518. store.dispatch(actionFullRegistr(login.value, password.value))
  519. }
  520. battonSMakom.onclick = () => {
  521. store.dispatch(actionAuthLogout())
  522. }
  523. const payload = store.getState().auth.token;
  524. if (payload) {
  525. korzina.style.display = "block"
  526. login.style.display = "none"
  527. password.style.display = "none"
  528. batton.style.display = "none"
  529. battonchik.style.display = "none"
  530. battonSMakom.style.display = "block"
  531. accaunt.style.display = "block"
  532. accaunt.innerText = jwtDecode(payload).sub.login;
  533. purchaseHistory.style.display = "block"
  534. } else {
  535. korzina.style.display = "none"
  536. battonSMakom.style.display = "none"
  537. login.style.display = "block"
  538. password.style.display = "block"
  539. batton.style.display = "block"
  540. battonchik.style.display = "block"
  541. accaunt.style.display = "none"
  542. purchaseHistory.style.display = "none"
  543. }
  544. })
  545. store.dispatch(orderHistory());
  546. const h2 = document.createElement("h2")
  547. store.subscribe(() => {
  548. const { history } = store.getState().promise;
  549. const [, route] = location.hash.split("/");
  550. purchaseHistory.onclick = () => {
  551. const bPopup = document.createElement("div");
  552. const bPopupContent = document.createElement("div");
  553. bPopup.id = "b-popup";
  554. bPopup.className = "b-popup";
  555. bPopupContent.className = "b-popup-content b-poput-container-flex";
  556. header.append(bPopup);
  557. bPopup.append(bPopupContent);
  558. const buttonCloseCart = document.createElement("button");
  559. buttonCloseCart.innerText = "×";
  560. buttonCloseCart.id = "buttonCloseCartId";
  561. bPopupContent.append(buttonCloseCart);
  562. buttonCloseCart.onclick = () => {
  563. var parent = document.getElementById("header");
  564. var child = document.getElementById("b-popup");
  565. parent.removeChild(child);
  566. };
  567. for (let [key, value] of Object.entries(history.payload)) {
  568. const { _id, createdAt, total, orderGoods } = value;
  569. const h2 = document.createElement("h2");
  570. h2.className = "h2History"
  571. const dateOfOrder = new Date(+createdAt);
  572. h2.innerHTML = `${dateOfOrder.toLocaleDateString()} ${dateOfOrder.toLocaleTimeString()}
  573. Order ID: ${_id} от , c ${orderGoods.length} goods worth: ${total}`;
  574. bPopupContent.append(h2);
  575. }
  576. if (Object.keys(history.payload).length == 0) {
  577. const p = document.createElement("p");
  578. p.innerHTML = "<p>Ще немає покупок</p>";
  579. card.append(p);
  580. }
  581. };
  582. });
  583. const buttonCloseCart = document.createElement("button");
  584. buttonCloseCart.innerText = `×`;
  585. buttonCloseCart.id = "buttonCloseCartId";
  586. bPopupContent.append(buttonCloseCart);
  587. buttonCloseCart.onclick = () => {
  588. var parent = document.getElementById("header");
  589. var child = document.getElementById("b-popup");
  590. parent.removeChild(child);
  591. };
  592. function bPopupCreate() {
  593. const bPopup = document.createElement("div");
  594. bPopup.id = "b-popup";
  595. bPopup.className = "b-popup";
  596. bPopupContent.className = "b-popup-content b-poput-container-flex";
  597. header.append(bPopup);
  598. bPopup.append(bPopupContent);
  599. }
  600. korzina.onclick = () => {
  601. bPopupCreate()
  602. }
  603. window.onhashchange = () => {
  604. const [, route, _id] = location.hash.split('/')
  605. console.log(route, _id)
  606. const routes = {
  607. category() {
  608. store.dispatch(actionCatById(_id))
  609. },
  610. good() {
  611. store.dispatch(actionGoodById(_id))
  612. },
  613. dashboard() {
  614. store.dispatch(orderHistory());
  615. },
  616. }
  617. if (route in routes) {
  618. routes[route]()
  619. }
  620. }
  621. window.onhashchange()