index.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633
  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 combineReducers(reducers) {
  24. return (state={}, action) => {
  25. const newState = {}
  26. // перебрать все редьюсеры
  27. if (reducers) {
  28. for (const [reducerName, reducer] of Object.entries(reducers)) {
  29. const newSubState = reducer(state[reducerName], action)
  30. if (newSubState !== state[reducerName]) {
  31. newState[reducerName] = newSubState
  32. }
  33. }
  34. // если newState не пустой, то вернуть стейт в
  35. if (Object.keys(newState).length !== 0) {
  36. return {...state, ...newState}
  37. } else {
  38. return state
  39. }
  40. }
  41. }
  42. }
  43. const combinedReducer = combineReducers({promise: promiseReducer, auth: authReducer, cart: cartReducer})
  44. const store = createStore(combinedReducer)
  45. store.subscribe(() => console.log(store.getState()))
  46. function jwtDecode(token) {
  47. try {
  48. let decoded = JSON.parse(atob(token.split('.')[1]))
  49. return decoded
  50. } catch (err) {
  51. console.log(err)
  52. }
  53. }
  54. function authReducer(state, {type, token}) {
  55. if (!state) {
  56. if (localStorage.authToken) {
  57. token = localStorage.authToken
  58. type = 'AUTH_LOGIN'
  59. } else {
  60. return {}
  61. }
  62. }
  63. if (type === 'AUTH_LOGIN') {
  64. let payload = jwtDecode(token)
  65. if (typeof payload === 'object') {
  66. localStorage.authToken = token
  67. return {
  68. ...state,
  69. token,
  70. payload
  71. }
  72. } else {
  73. return state
  74. }
  75. }
  76. if (type === 'AUTH_LOGOUT') {
  77. delete localStorage.authToken
  78. return {}
  79. }
  80. return state
  81. }
  82. const actionAuthLogin = (token) => ({type: 'AUTH_LOGIN', token})
  83. const actionAuthLogout = () => ({type: 'AUTH_LOGOUT'})
  84. function cartReducer (state={}, {type, good={}, count=1}) {
  85. if (!state) {
  86. return {}
  87. }
  88. // только если в функции задан count по умолчанию вызывать тут
  89. // а так лучше вызвать в типах add и change
  90. // count = +count
  91. // if (!count) {
  92. // return state
  93. // }
  94. const {_id} = good
  95. const types = {
  96. CART_ADD() {
  97. count = +count
  98. if (!count) {
  99. return state
  100. }
  101. return {
  102. ...state,
  103. [_id]: {good, count: (count + (state[_id]?.count || 0)) < 0 ? 0 : count + (state[_id]?.count || 0)}
  104. }
  105. },
  106. CART_CHANGE() {
  107. count = +count
  108. if (!count) {
  109. return state
  110. }
  111. return {
  112. ...state,
  113. [_id]: {good, count: count < 0 ? 0 : count}
  114. }
  115. },
  116. CART_REMOVE() {
  117. let { [_id]: removed, ...newState } = state
  118. return newState
  119. },
  120. CART_CLEAR() {
  121. return {}
  122. },
  123. }
  124. if (type in types) {
  125. return types[type]()
  126. }
  127. return state
  128. }
  129. const actionCartAdd = (good) => ({type: 'CART_ADD', good})
  130. const actionCartChange = (good, count) => ({type: 'CART_CHANGE', good, count})
  131. const actionCartRemove = (good) => ({type: 'CART_REMOVE', good})
  132. const actionCartClear = () => ({type: 'CART_CLEAR'})
  133. store.dispatch(actionCartAdd({_id: '111111'}))
  134. store.dispatch(actionCartChange({_id: '111111'}, 10))
  135. store.dispatch(actionCartAdd({_id: '22222'}))
  136. store.dispatch(actionCartRemove({_id: '22222'}))
  137. store.dispatch(actionCartClear())
  138. function promiseReducer(state={}, {type, status, payload, error, name}) {
  139. if (!state) {
  140. return {}
  141. }
  142. if (type === 'PROMISE') {
  143. return {
  144. ...state,
  145. [name]: {
  146. status: status,
  147. payload : payload,
  148. error: error,
  149. }
  150. }
  151. }
  152. return state
  153. }
  154. const actionPending = (name) => ({type: 'PROMISE', status: 'PENDING', name})
  155. const actionResolved = (name, payload) => ({type: 'PROMISE', status: 'RESOLVED', name, payload})
  156. const actionRejected = (name, error) => ({type: 'PROMISE', status: 'REJECTED', name, error})
  157. const actionPromise = (name, promise) =>
  158. async (dispatch) => {
  159. dispatch(actionPending(name))
  160. try {
  161. let data = await promise
  162. dispatch(actionResolved(name, data))
  163. return data
  164. }
  165. catch(error){
  166. dispatch(actionRejected(name, error))
  167. }
  168. }
  169. const getGQL = url =>
  170. async (query, variables={}) => {
  171. let obj = await fetch(url, {
  172. method: 'POST',
  173. headers: {
  174. "Content-Type": "application/json",
  175. ...(localStorage.authToken ? {Authorization: "Bearer " + localStorage.authToken} : {})
  176. },
  177. body: JSON.stringify({ query, variables })
  178. })
  179. let a = await obj.json()
  180. if (!a.data && a.errors) {
  181. throw new Error(JSON.stringify(a.errors))
  182. } else {
  183. return a.data[Object.keys(a.data)[0]]
  184. }
  185. }
  186. const backURL = 'http://shop-roles.asmer.fs.a-level.com.ua/'
  187. const gql = getGQL(backURL + 'graphql');
  188. const actionOrder = () => (
  189. async (dispatch, getState) => {
  190. let {cart} = getState()
  191. //магия по созданию структуры вида
  192. //let orderGoods = [{good: {_id}, count}, {good: {_id}, count} .......]
  193. //из структуры вида
  194. //{_id1: {good, count},
  195. //_id2: {good, count}}
  196. const orderGoods = Object.entries(cart)
  197. .map(([_id, {good, count}]) => ({good: {_id}, count}))
  198. let result = await dispatch(actionPromise('order', gql(`
  199. mutation newOrder($order:OrderInput){
  200. OrderUpsert(order:$order)
  201. { _id total}
  202. }
  203. `, {order: {orderGoods}})))
  204. if (result?._id) {
  205. dispatch(actionCartClear())
  206. }
  207. })
  208. const actionLogin = (login, password) => (
  209. actionPromise('login', gql(`query log($login: String, $password: String) {
  210. login(login: $login, password: $password)
  211. }`, {login, password}))
  212. )
  213. const actionFullLogin = (login, password) => (
  214. async (dispatch) => {
  215. let token = await dispatch(actionLogin(login, password))
  216. if (token) {
  217. // console.log(token)
  218. dispatch(actionAuthLogin(token))
  219. }
  220. }
  221. )
  222. const actionRegister = (login, password) => (
  223. actionPromise('register', gql(`mutation reg($user:UserInput) {
  224. UserUpsert(user:$user) {
  225. _id
  226. }
  227. }
  228. `, {user: {login, password}})
  229. )
  230. )
  231. const actionFullRegister = (login, password) => (
  232. async (dispatch) => {
  233. let regId = await dispatch(actionRegister(login, password))
  234. if (regId) {
  235. // console.log(regId)
  236. dispatch(actionFullLogin(login, password))
  237. }
  238. }
  239. )
  240. const actionRootCats = () => (
  241. actionPromise('rootCats', gql(`query {
  242. CategoryFind(query: "[{\\"parent\\":null}]"){
  243. _id name
  244. }
  245. }`))
  246. )
  247. const actionCatById = (_id) => (
  248. actionPromise('catById', gql(`query catById($q: String){
  249. CategoryFindOne(query: $q){
  250. _id name goods {
  251. _id name price images {
  252. url
  253. }
  254. }
  255. subCategories {
  256. _id name
  257. }
  258. }
  259. }`, {q: JSON.stringify([{_id}])}))
  260. )
  261. const actionGoodById = (_id) => (
  262. actionPromise('goodById', gql(`query goodById($q: String) {
  263. GoodFindOne(query: $q) {
  264. _id name price description images {
  265. url
  266. }
  267. }
  268. }`, {q: JSON.stringify([{_id}])}))
  269. )
  270. const actionGoodsByUser = () => (
  271. actionPromise('goodByUser', gql(`query oUser($query: String) {
  272. OrderFind(query:$query){
  273. _id orderGoods{
  274. price count total good{
  275. _id name categories{
  276. name
  277. }
  278. images {
  279. url
  280. }
  281. }
  282. }
  283. owner {
  284. _id login
  285. }
  286. }
  287. }`,
  288. {query: JSON.stringify([{}])}))
  289. )
  290. store.dispatch(actionRootCats())
  291. store.subscribe(() => {
  292. const {promise} = store.getState()
  293. const {rootCats} = promise
  294. if (rootCats?.payload) {
  295. aside.innerHTML = ''
  296. const regBtn = document.createElement('a')
  297. regBtn.href = `#/register`
  298. regBtn.innerText = 'Register'
  299. const loginBtn = document.createElement('a')
  300. loginBtn.href = `#/login`
  301. loginBtn.innerText = 'Login'
  302. const logoutBtn = document.createElement('button')
  303. logoutBtn.innerText = 'Logout'
  304. aside.append(regBtn, loginBtn, logoutBtn)
  305. logoutBtn.onclick = () => {
  306. store.dispatch(actionAuthLogout())
  307. }
  308. for (const {_id, name} of rootCats?.payload) {
  309. const link = document.createElement('a')
  310. link.href = `#/category/${_id}`
  311. link.innerText = name
  312. aside.append(link)
  313. }
  314. }
  315. })
  316. function createForm(parent, type, callback) {
  317. parent.innerHTML = `
  318. <input id="login${type}" type="text"/>
  319. <input id="pass${type}" type="password"/>
  320. <button id="btn${type}">${type}</button>
  321. `
  322. return () => window[`btn${type}`].onclick = () => {
  323. store.dispatch(callback(window[`login${type}`].value, window[`pass${type}`].value))
  324. window[`pass${type}`].value = ''
  325. }
  326. }
  327. const createCartPage = (parent) => {
  328. parent.innerHTML = '';
  329. const {cart} = store.getState();
  330. const clearBtn = document.createElement('button');
  331. clearBtn.innerText = "ОЧИСТИТЬ КОРЗИНУ";
  332. if(Object.keys(cart).length !== 0) {
  333. main.append(clearBtn);
  334. }
  335. clearBtn.onclick = () => {
  336. store.dispatch(actionCartClear());
  337. }
  338. for(const item in cart) {
  339. const {good} = cart[item];
  340. const {count, good: {_id: id, name: name, price: price, images: [{url}]}} = cart[item];
  341. const card = document.createElement('div');
  342. card.innerHTML = `
  343. <div>
  344. <p>${name}</p>
  345. <img src="${backURL}/${url}">
  346. <p>${count}</p>
  347. <p>${price} грн</p>
  348. </div>
  349. `;
  350. const changeCount = document.createElement('input');
  351. changeCount.type = 'number';
  352. changeCount.value = count;
  353. card.append(changeCount);
  354. changeCount.oninput = () => {
  355. store.dispatch(actionCartChange(good, changeCount.value))
  356. };
  357. const deleteGood = document.createElement('button');
  358. deleteGood.innerText = 'X';
  359. deleteGood.style.display = 'block';
  360. card.append(deleteGood);
  361. deleteGood.onclick = () => {
  362. store.dispatch(actionCartRemove(good))
  363. }
  364. parent.append(card);
  365. }
  366. const sendOrder = document.createElement('button');
  367. sendOrder.innerText = "ОФОРМИТЬ ЗАКАЗ";
  368. if(Object.keys(cart).length !== 0) {
  369. main.append(sendOrder);
  370. }
  371. sendOrder.onclick = () => {
  372. store.dispatch(actionOrder())
  373. }
  374. }
  375. // location.hash - адресная строка после решетки
  376. window.onhashchange = () => {
  377. const [,route, _id] = location.hash.split('/')
  378. const routes = {
  379. category(){
  380. store.dispatch(actionCatById(_id))
  381. console.log('страница категорий')
  382. },
  383. good(){
  384. store.dispatch(actionGoodById(_id))
  385. console.log('страница товара')
  386. },
  387. register(){
  388. let goRegister = createForm(main, 'Register', actionFullRegister)
  389. goRegister()
  390. },
  391. login(){
  392. let goLogin = createForm(main, 'Login', actionFullLogin)
  393. goLogin()
  394. },
  395. orders(){
  396. store.dispatch(actionGoodsByUser())
  397. },
  398. cart(){
  399. createCartPage(main)
  400. },
  401. }
  402. if (route in routes) {
  403. routes[route]()
  404. }
  405. }
  406. store.subscribe(() => {
  407. const [,route] = location.hash.split('/')
  408. if (route === 'cart') {
  409. createCartPage(main);
  410. }
  411. })
  412. window.onhashchange()
  413. store.subscribe(() => {
  414. const {promise} = store.getState()
  415. const {catById} = promise
  416. const [,route, _id] = location.hash.split('/')
  417. if (catById?.payload && route === 'category'){
  418. const {name} = catById.payload;
  419. main.innerHTML = `<h1>${name}</h1>`
  420. if (catById.payload.subCategories) {
  421. for(const {_id, name} of catById.payload.subCategories) {
  422. const link = document.createElement('a');
  423. link.href = `#/category/${_id}`;
  424. link.innerText = name;
  425. main.append(link);
  426. }
  427. }
  428. if (catById.payload.goods) {
  429. for (const good of catById.payload.goods){
  430. const {_id, name, price, images} = good
  431. const card = document.createElement('div')
  432. card.innerHTML = `<h2>${name}</h2>
  433. <img src="${backURL}/${images[0].url}" />
  434. <strong>${price} грн</strong>
  435. <br>
  436. <a href="#/good/${_id}">Перейти на страницу товара</a>
  437. `
  438. const btnCart = document.createElement('button')
  439. btnCart.innerText = 'Добавить в корзину'
  440. btnCart.onclick = () => {
  441. store.dispatch(actionCartAdd(good))
  442. }
  443. card.append(btnCart)
  444. main.append(card)
  445. }
  446. }
  447. }
  448. })
  449. store.subscribe(() => {
  450. const {promise} = store.getState()
  451. const {goodById} = promise
  452. const [,route, _id] = location.hash.split('/');
  453. if (goodById?.payload && route === 'good') {
  454. main.innerHTML = '';
  455. const good = goodById.payload;
  456. const {_id, name, images, price, description} = good
  457. const card = document.createElement('div');
  458. card.innerHTML = `<h2>${name}</h2>
  459. <img src="${backURL}/${images[0].url}" />
  460. <strong>${price} грн</strong>
  461. <h2>${description}</h2>
  462. `;
  463. const btnCart = document.createElement('button')
  464. btnCart.innerText = 'Добавить в корзину'
  465. btnCart.onclick = () => {
  466. store.dispatch(actionCartAdd(good))
  467. }
  468. card.append(btnCart)
  469. main.append(card);
  470. }
  471. }
  472. )
  473. store.subscribe(() => {
  474. const {auth} = store.getState()
  475. const {payload} = auth
  476. if (payload?.sub ) {
  477. topContaner.innerHTML = ''
  478. const {id, login} = payload.sub
  479. const name = document.createElement('div')
  480. name.innerText = `ПРИВЕТ ${login}`
  481. topContaner.append(name)
  482. const myOrders = document.createElement('a')
  483. myOrders.innerText = 'Мои заказы'
  484. myOrders.href = `#/orders/${id}`
  485. topContaner.append(myOrders)
  486. } else {
  487. topContaner.innerHTML = ''
  488. }
  489. })
  490. store.subscribe(() => {
  491. const {promise} = store.getState()
  492. const {goodByUser} = promise
  493. const [,route] = location.hash.split('/')
  494. if (goodByUser?.payload && route === 'orders'){
  495. main.innerHTML = ''
  496. if (goodByUser.payload) {
  497. let totalMoney = 0
  498. for (const order of goodByUser.payload) {
  499. if (order.orderGoods) {
  500. for (const {price, count, total, good} of order.orderGoods) {
  501. if (price !== null && count !== null && total !== null && good !== null) {
  502. totalMoney += total
  503. const {_id, name, images} = good
  504. const card = document.createElement('div')
  505. card.innerHTML = `<h2>${name}</h2>
  506. <img src="${backURL}/${images[0].url}" />
  507. <strong>Куплено ${count} по ${price} грн. Итого: ${total} грн</strong>
  508. <br>
  509. <a href="#/good/${_id}">Перейти на страницу товара</a>
  510. `
  511. main.append(card)
  512. }
  513. }
  514. }
  515. }
  516. const totalBlock = document.createElement('b')
  517. totalBlock.innerText = 'Итого потрачено: ' + totalMoney + ' грн'
  518. main.append(totalBlock)
  519. }
  520. }
  521. })
  522. store.subscribe(() => {
  523. const {cart} = store.getState()
  524. let counter = 0;
  525. for (const key in cart) {
  526. counter += cart[key].count
  527. }
  528. cartIcon.innerText = counter
  529. })