index.js 20 KB

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