main.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471
  1. function createStore(reducer){
  2. let state = reducer(undefined, {})
  3. let cbs = []
  4. function dispatch(action){
  5. if (typeof action === 'function'){
  6. return action(dispatch)
  7. }
  8. const newState = reducer(state, action)
  9. if (state !== newState){
  10. state = newState
  11. cbs.forEach(cb => cb())
  12. }
  13. }
  14. return {
  15. dispatch,
  16. subscribe(cb){
  17. cbs.push(cb)
  18. return () => cbs = cbs.filter(c => c !== cb)
  19. },
  20. getState(){
  21. return state
  22. }
  23. }
  24. }
  25. function combineReducers(reducers={cart: cartReducer, promise: promiseReducer, auth: authReducer}){
  26. return (state={}, action) => {
  27. let newState = {}
  28. for (let key in reducers) {
  29. let newSubState = reducers[key](state[key], action)
  30. if(newSubState !== state[key]) {
  31. newState[key] = newSubState
  32. }
  33. }
  34. if (Object.keys(newState).length) {
  35. return {...state, ...newState}
  36. } else {
  37. return state
  38. }
  39. }
  40. }
  41. //promise
  42. function promiseReducer(state={}, {type, status, payload, error, name}){
  43. if (type === 'PROMISE'){
  44. return {
  45. ...state,
  46. [name]:{status, payload, error}
  47. }
  48. }
  49. return state
  50. }
  51. const actionPending = name => ({type: 'PROMISE', status: 'PENDING', name})
  52. const actionResolved = (name, payload) => ({type: 'PROMISE', status: 'RESOLVED', name, payload})
  53. const actionRejected = (name, error) => ({type: 'PROMISE', status: 'REJECTED', name, error})
  54. const delay = ms => new Promise(ok => setTimeout(() => ok(ms), ms))
  55. const actionPromise = (name, promise) =>
  56. async dispatch => {
  57. dispatch(actionPending(name))
  58. try{
  59. let payload = await promise
  60. dispatch(actionResolved(name, payload))
  61. return payload
  62. }
  63. catch(error){
  64. dispatch(actionRejected(name, error))
  65. }
  66. }
  67. const getGQL = url => {
  68. return function(query, variables={}) {
  69. return fetch(url,
  70. {
  71. method: "POST",
  72. headers:
  73. {"Content-Type": "application/json",
  74. ...(localStorage.authToken ? {Authorization: 'Bearer ' + localStorage.authToken} : {})
  75. },
  76. body: JSON.stringify({query, variables})
  77. }).then(resp => resp.json())
  78. .then(data => {
  79. if ("errors" in data) {
  80. let error = new Error('ашипка, угадывай што не так')
  81. throw error
  82. }
  83. else {
  84. return data.data[Object.keys(variables)[0]]
  85. }
  86. })
  87. }
  88. }
  89. let shopGQL = getGQL('http://shop-roles.asmer.fs.a-level.com.ua/graphql')
  90. const goodById = goodId => {
  91. let id = `[{"_id":"${goodId}"}]`
  92. return shopGQL(`
  93. query good($id:String){
  94. GoodFindOne(query: $id) {
  95. _id name description price images {
  96. _id text url
  97. }
  98. categories {
  99. _id name
  100. }
  101. }
  102. }`, {GoodFindOne: '', id })
  103. }
  104. const actionGoodById = id =>
  105. actionPromise('goodById', goodById(id))
  106. const actionRootCategories = () =>
  107. actionPromise('rootCategories', shopGQL(`
  108. query cats($query:String){
  109. CategoryFind(query:$query){
  110. _id name
  111. }
  112. }
  113. `, {CategoryFind:'', query: JSON.stringify([{parent:null}])}))
  114. const actionCategoryById = (_id) =>
  115. actionPromise('catById', shopGQL(`
  116. query catById($query:String){
  117. CategoryFindOne(query:$query){
  118. _id name goods{
  119. _id name price description images{
  120. url
  121. }
  122. }
  123. }
  124. }`, { CategoryFindOne: '', query: JSON.stringify([{ _id }]) }))
  125. const actionAddOrder = (cart) => {
  126. let string = '['
  127. for (let goodId in cart) {
  128. string += `{good: {_id: "${goodId}"}, count: ${cart[goodId].count}},`
  129. }
  130. string = string.slice(0, string.length - 1)
  131. string += ']'
  132. store.dispatch(actionPromise('orderAdd', shopGQL(`
  133. mutation {
  134. OrderUpsert(order: {
  135. orderGoods: ${string}
  136. }) {
  137. _id total
  138. }
  139. }`, {OrderUpsert: ''})))
  140. }
  141. //cart
  142. function cartReducer(state={}, {type, good, price, count=1}) {
  143. if (Object.keys(state).length === 0 && localStorage.cart?.length > 10) {
  144. let newState = JSON.parse(localStorage.cart)
  145. return newState
  146. }
  147. const types = {
  148. CART_ADD() {
  149. let newState = {
  150. ...state,
  151. [good._id]: {count: (state[good._id]?.count || 0) + count, good: {id: good._id, name: good.name}, price}
  152. }
  153. localStorage.cart = JSON.stringify(newState)
  154. return newState
  155. },
  156. CART_REMOVE() {
  157. let {[good._id]:poh, ...newState} = state
  158. localStorage.cart = JSON.stringify(newState)
  159. return newState
  160. // let newState = {...state}
  161. // delete newState[good._id]
  162. // return newState
  163. },
  164. CART_CLEAR() {
  165. let newState = {}
  166. localStorage.cart = JSON.stringify(newState)
  167. return newState
  168. },
  169. CART_SET() {
  170. let newState = {
  171. ...state,
  172. [good._id]: {count, good: {id: good._id, name: good.name}, price}
  173. }
  174. localStorage.cart = JSON.stringify(newState)
  175. return newState
  176. }
  177. }
  178. if (type in types) {
  179. return types[type]()
  180. }
  181. return state
  182. }
  183. const actionCartAdd = (_id, name, price, count) => ({type: 'CART_ADD', good: {_id, name}, price, count})
  184. const actionCartRemove = (_id, name) => ({type: 'CART_REMOVE', good: {_id, name}})
  185. const actionCartSet = (_id, name, price, count) => ({type: 'CART_SET', good: {_id, name}, price, count})
  186. const actionCartClear = () => ({type: 'CART_CLEAR'})
  187. //auth
  188. const jwt_decode = (jwt) => {
  189. let payload = jwt.split('.')
  190. return JSON.parse(atob(payload[1]))
  191. }
  192. function authReducer(state, action={}){ //....
  193. if (state === undefined){
  194. //добавить в action token из localStorage, и проимитировать LOGIN (action.type = 'LOGIN')
  195. if (localStorage.authToken) {
  196. action.jwt = localStorage.authToken
  197. return {token: action.jwt, payload: jwt_decode(action.jwt)}
  198. }
  199. }
  200. if (action.type === 'LOGIN'){
  201. console.log('ЛОГИН')
  202. //+localStorage
  203. //jwt_decode
  204. return {token: action.jwt, payload: jwt_decode(action.jwt)}
  205. }
  206. if (action.type === 'LOGOUT'){
  207. console.log('ЛОГАУТ')
  208. //-localStorage
  209. //вернуть пустой объект
  210. return {}
  211. }
  212. if (action.type === 'LOGGING_IN'){
  213. return {loginPageHello: true}
  214. }
  215. return state
  216. }
  217. const actionLogin = (jwt) => ({type: 'LOGIN', jwt})
  218. const thunkLogin = (login, password) => {
  219. return (dispatch) => {
  220. shopGQL(`query login($login:String, $password: String) {login(login:$login, password:$password)}`, { login, password })
  221. .then(jwt => {
  222. if (jwt) {
  223. localStorage.authToken = jwt
  224. dispatch(actionLogin(jwt))
  225. } else {
  226. throw new Error('wrong')
  227. }
  228. })
  229. }
  230. }
  231. const actionLogout = () => ({type: 'LOGOUT'})
  232. const thunkLogout = () => {
  233. return (dispatch) => {
  234. localStorage.authToken = ''
  235. localStorage.cart = ''
  236. store.dispatch(actionCartClear())
  237. dispatch(actionLogout())
  238. }
  239. }
  240. const actionLoggingIn = () => ({type: 'LOGGING_IN'})
  241. //store
  242. const store = createStore(combineReducers({cart: cartReducer, promise: promiseReducer, auth: authReducer}))
  243. const unsubscribe1 = store.subscribe(() => console.log(store.getState()))
  244. store.dispatch(actionRootCategories())
  245. window.onhashchange = () => {
  246. let {1: route, 2:id} = location.hash.split('/')
  247. if (route === 'categories'){
  248. store.dispatch(actionCategoryById(id))
  249. }
  250. if (route === 'good'){
  251. store.dispatch(actionGoodById(id))
  252. }
  253. if (route === 'login'){
  254. store.dispatch(actionLoggingIn())
  255. }
  256. if (route === 'cart'){
  257. drawCart()
  258. }
  259. }
  260. window.onhashchange()
  261. function drawMainMenu(){
  262. let cats = store.getState().promise.rootCategories.payload
  263. if (cats){ //каждый раз дорисовываются в body
  264. aside.innerText = ''
  265. for (let {_id, name} of cats){
  266. let catA = document.createElement('a')
  267. catA.href = `#/categories/${_id}`
  268. catA.innerText = name
  269. aside.append(catA)
  270. }
  271. }
  272. }
  273. function drawHeader() {
  274. login.innerHTML = (store.getState().auth?.payload ? `${store.getState().auth.payload.sub.login} | <a id='logout' href="#/login">Log out</a>` : '<a href="#/login">Log in</a>')
  275. if (document.querySelector('#logout')) {
  276. logout.onclick = () => {
  277. store.dispatch(thunkLogout())
  278. }
  279. }
  280. }
  281. function drawCart() {
  282. let cart = store.getState().cart
  283. if (!localStorage.authToken) {
  284. main.innerText = 'Залогинтесь плез'
  285. } else if (!Object.keys(cart).length) {
  286. main.innerText = 'Корзина пуста'
  287. } else {
  288. main.innerText = 'Ваша корзина: '
  289. for (let goodId in cart) {
  290. let {good: {id, name}, price, count} = cart[goodId]
  291. let goodContainer = document.createElement('div')
  292. goodContainer.classList.add('good-container')
  293. let goodName = document.createElement('div')
  294. goodName.innerText = name
  295. let goodPrice = document.createElement('div')
  296. goodPrice.innerText = 'Стоимость: ' + price
  297. let goodCount = document.createElement('input')
  298. goodCount.type = 'number'
  299. goodCount.value = count
  300. goodCount.onchange = () => {
  301. store.dispatch(actionCartSet(id, name, price, (goodCount.value > 0 ? +goodCount.value : 1)))
  302. }
  303. let removeBtn = document.createElement('button')
  304. removeBtn.innerText = 'Удалить товар'
  305. removeBtn.onclick = () => {
  306. store.dispatch(actionCartRemove (id, name))
  307. }
  308. goodContainer.append(goodName, goodPrice, goodCount, removeBtn)
  309. main.append(goodContainer)
  310. }
  311. let price = 0
  312. for (let goodId in cart) {
  313. price += cart[goodId].price * cart[goodId].count
  314. }
  315. let totalPriceContainer = document.createElement('div')
  316. totalPriceContainer.innerText = 'Общая стоимость: ' + price
  317. main.append(totalPriceContainer)
  318. let setOrderBtn = document.createElement('button')
  319. setOrderBtn.innerText = 'Оформить заказ'
  320. setOrderBtn.onclick = () => {
  321. actionAddOrder(store.getState().cart)
  322. }
  323. main.append(setOrderBtn)
  324. let clearBtn = document.createElement('button')
  325. clearBtn.innerText = 'Очистить корзину'
  326. clearBtn.onclick = () => {
  327. store.dispatch(actionCartClear())
  328. }
  329. main.append(clearBtn)
  330. }
  331. }
  332. function drawOrderSuccessful() {
  333. if (store.getState().promise.orderAdd?.status === 'RESOLVED') {
  334. let order = store.getState().promise.orderAdd.payload
  335. main.innerText = 'Заказ оформился, всё круто'
  336. let orderInfo = document.createElement('div')
  337. orderInfo.innerText = `Номер заказа: ${order._id}. Стоимость: ${order.total}`
  338. main.append(orderInfo)
  339. }
  340. }
  341. store.subscribe(drawMainMenu)
  342. store.subscribe(drawHeader)
  343. store.subscribe(() => {
  344. const {1: route, 2:id} = location.hash.split('/')
  345. if (route === 'categories'){
  346. const catById = store.getState().promise.catById?.payload
  347. if (catById){
  348. main.innerText = ''
  349. let categoryName = document.createElement('div')
  350. categoryName.innerText = catById.name
  351. categoryName.style.fontSize = '25px'
  352. categoryName.style.fontWeight = 'bold'
  353. main.append(categoryName)
  354. for (let {_id, name, price} of catById.goods){
  355. let good = document.createElement('a')
  356. good.href = `#/good/${_id}`
  357. good.innerText = name
  358. let btn = document.createElement('button')
  359. btn.onclick = () => {
  360. if (!localStorage.authToken) {
  361. main.innerText = 'Залогинтесь плез'
  362. } else {
  363. store.dispatch(actionCartAdd(_id, name, price))
  364. }
  365. }
  366. btn.style.cursor = 'pointer'
  367. btn.innerText = 'купыть'
  368. main.append(good, btn)
  369. }
  370. }
  371. }
  372. if (route === 'good'){
  373. const goodById = store.getState().promise.goodById?.payload
  374. if (goodById){
  375. main.innerText = ''
  376. let {name, description, price, _id} = goodById
  377. let goodName = document.createElement('div')
  378. goodName.innerText = name
  379. goodName.style.fontSize = '35px'
  380. goodName.style.fontWeight = 'bold'
  381. goodName.style.marginBottom = '25px'
  382. let goodDescription = document.createElement('div')
  383. goodDescription.innerText = description
  384. goodDescription.style.marginBottom = '25px'
  385. let goodPrice = document.createElement('div')
  386. goodPrice.innerText = 'Цена: ' + price
  387. goodPrice.style.marginBottom = '5px'
  388. let btn = document.createElement('button')
  389. btn.onclick = () => {
  390. if (!localStorage.authToken) {
  391. main.innerText = 'Залогинтесь плез'
  392. } else {
  393. store.dispatch(actionCartAdd(_id, name, price))
  394. }
  395. }
  396. btn.style.cursor = 'pointer'
  397. btn.innerText = 'купыть'
  398. main.append(goodName, goodDescription, goodPrice, btn)
  399. }
  400. }
  401. if (route === 'login') {
  402. main.innerText = ''
  403. let inputsContainer = document.createElement('div')
  404. inputsContainer.id = 'inputs'
  405. let loginInput = document.createElement('input')
  406. loginInput.type = 'text'
  407. let loginLabel = document.createElement('span')
  408. loginLabel.innerText = 'Login:'
  409. let passwordInput = document.createElement('input')
  410. passwordInput.type = 'password'
  411. let passwordLabel = document.createElement('span')
  412. passwordLabel.innerText = 'Password:'
  413. let button = document.createElement('button')
  414. button.innerText = 'log in cyka'
  415. button.onclick = () => {
  416. if (loginInput.value && passwordInput.value){
  417. store.dispatch(thunkLogin(loginInput.value, passwordInput.value))
  418. }
  419. }
  420. inputsContainer.append(loginLabel, loginInput, passwordLabel, passwordInput, button)
  421. main.append(inputsContainer)
  422. if (store.getState().auth?.payload) {
  423. button.disabled = true
  424. }
  425. }
  426. if (route === 'cart') {
  427. drawCart()
  428. drawOrderSuccessful()
  429. }
  430. })