index.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525
  1. function createStore(reducer) {
  2. let state = reducer(undefined, {}) //стартовая инициализация состояния, запуск редьюсера со state === undefined
  3. let cbs = [] //массив подписчиков
  4. const getState = () => state //функция, возвращающая переменную из замыкания
  5. const subscribe = (cb) => (
  6. cbs.push(cb),
  7. () => (cbs = cbs.filter((c) => c !== cb))
  8. )
  9. const dispatch = (action) => {
  10. if (typeof action === 'function') { //если action - не объект, а функция
  11. return action(dispatch, getState) //запускаем эту функцию и даем ей dispatch и getState для работы
  12. }
  13. const newState = reducer(state, action) //пробуем запустить редьюсер
  14. if (newState !== state) { //проверяем, смог ли редьюсер обработать action
  15. state = newState //если смог, то обновляем state
  16. for (let cb of cbs) cb() //и запускаем подписчиков
  17. }
  18. }
  19. return {
  20. getState, //добавление функции getState в результирующий объект
  21. dispatch,
  22. subscribe //добавление subscribe в объект
  23. }
  24. }
  25. function jwtDecode(token) {
  26. try {
  27. return JSON.parse(atob(token.split('.')[1]))
  28. }
  29. catch (e) {
  30. }
  31. }
  32. function authReducer(state = {}, { type, token }) {
  33. if (type === "AUTH_LOGIN") {
  34. const payload = jwtDecode(token)
  35. if (payload) {
  36. return {
  37. token, payload
  38. }
  39. }
  40. }
  41. if (type === "AUTH_LOGOUT") {
  42. return {}
  43. }
  44. return state
  45. }
  46. const actionAuthLogin = (token) =>
  47. (dispatch, getState) => {
  48. const oldState = getState()
  49. dispatch({ type: "AUTH_LOGIN", token })
  50. const newState = getState()
  51. if (oldState !== newState) {
  52. localStorage.setItem('authToken', token);
  53. }
  54. }
  55. const actionAuthLogout = () =>
  56. dispatch => {
  57. dispatch({ type: "AUTH_LOGOUT" })
  58. localStorage.removeItem('authToken')
  59. }
  60. function promiseReducer(state = {}, { type, name, status, payload, error }) {
  61. ////?????
  62. //ОДИН ПРОМИС:
  63. //состояние: PENDING/FULFILLED/REJECTED
  64. //результат
  65. //ошибка:
  66. //{status, payload, error}
  67. //{
  68. // name1:{status, payload, error}
  69. // name2:{status, payload, error}
  70. // name3:{status, payload, error}
  71. //}
  72. if (type === 'PROMISE') {
  73. //if (name == 'login' && status == 'FULFILLED') {
  74. // store.dispatch(actionAuthLogin(payload.data.login))
  75. // state = store.getState();
  76. //}
  77. return {
  78. ...state,
  79. [name]: { status, payload, error }
  80. }
  81. }
  82. return state;
  83. }
  84. const actionPending = (name) => ({ type: 'PROMISE', status: 'PENDING', name })
  85. const actionFulfilled = (name, payload) => ({ type: 'PROMISE', status: 'FULFILLED', name, payload })
  86. const actionRejected = (name, error) => ({ type: 'PROMISE', status: 'REJECTED', name, error })
  87. const actionPromise = (name, promise) =>
  88. async (dispatch) => {
  89. try {
  90. dispatch(actionPending(name))
  91. let payload = await promise
  92. dispatch(actionFulfilled(name, payload))
  93. if (name === 'login') {
  94. store.dispatch(actionAuthLogin(payload.data.login))
  95. }
  96. return payload
  97. }
  98. catch (e) {
  99. console.log(e);
  100. dispatch(actionRejected(name, e))
  101. }
  102. }
  103. function cartReducer(state = {}, { type, count = 1, good }) {
  104. // type CART_ADD CART_REMOVE CAT_CLEAR CART_DEC
  105. // {
  106. // id1: {count: 1, good{name, price, images, id}, total: price}
  107. // }
  108. if (type === "CART_ADD") {
  109. return {
  110. ...state,
  111. [good._id]: { count: count + (state[good._id]?.count || 0), good }
  112. }
  113. }
  114. if (type === "CART_CLEAR") {
  115. return {}
  116. }
  117. if (type === "CART_REMOVE") {
  118. let newState = { ...state }
  119. delete newState[good._id]
  120. return newState
  121. }
  122. if (type === "CART_DELETE") {
  123. if (state[good._id].count > 1) {
  124. return {
  125. ...state, [good._id]: { count: -count + (state[good._id]?.count || 0), good },
  126. };
  127. }
  128. }
  129. return state
  130. }
  131. const cartAdd = (good, count = 1) => ({ type: 'CART_ADD', good, count })
  132. const cartClear = () => ({ type: 'CART_CLEAR' })
  133. const cartRemove = (good) => ({ type: 'CART_REMOVE', good })
  134. const cartDelete = (good) => ({ type: 'CART_DELETE', good })
  135. /* store.dispatch({type: "CART_ADD", good: {_id: "чипсы"}}) ПРОБНЫЙ СТОР!!! */
  136. const delay = (ms) => new Promise((ok) => setTimeout(() => ok(ms), ms))
  137. function combineReducers(reducers) {
  138. function combinedReducer(combinedState = {}, action) {
  139. const newCombinedState = {}
  140. for (const [reducerName, reducer] of Object.entries(reducers)) {
  141. const newSubState = reducer(combinedState[reducerName], action)
  142. if (newSubState !== combinedState[reducerName]) {
  143. newCombinedState[reducerName] = newSubState
  144. }
  145. }
  146. if (Object.keys(newCombinedState).length === 0) {
  147. return combinedState
  148. }
  149. return { ...combinedState, ...newCombinedState }
  150. }
  151. return combinedReducer
  152. }
  153. const store = createStore(
  154. combineReducers({ auth: authReducer, promise: promiseReducer, cart: cartReducer })
  155. ) //не забудьте combineReducers если он у вас уже есть
  156. const enterLogin = document.getElementById('enterLogin')
  157. const outLogin = document.getElementById('outLogin')
  158. const enterLogout = document.getElementById('enterLogout')
  159. const loginContainer = document.getElementById('mainLoginContainer')
  160. const enterCart = document.getElementById('enterCart')
  161. const cartContainer = document.getElementById('cartContainer')
  162. const leaveCart = document.getElementById('closeCart')
  163. const cartProductsContainer = document.getElementById('cartProductsContainer')
  164. const enterRegister = document.getElementById('enterRegister')
  165. const registerContainer = document.getElementById('registerContainer')
  166. const hideLogin = document.getElementById('hideLogin')
  167. const outRegister = document.getElementById('outRegister')
  168. if (localStorage.authToken) {
  169. store.dispatch(actionAuthLogin(localStorage.authToken))
  170. enterLogin.style.display = 'none';
  171. enterCart.style.display = 'block'
  172. enterLogout.style.display = 'block';
  173. }
  174. //const store = createStore(combineReducers({promise: promiseReducer, auth: authReducer, cart: cartReducer}))
  175. store.subscribe(() => console.log(store.getState()))
  176. const gql = (url, query, variables) => fetch(url, {
  177. method: 'POST',
  178. headers: {
  179. "Content-Type": "application/json",
  180. Accept: "application/json",
  181. },
  182. body: JSON.stringify({ query, variables })
  183. }).then(res => res.json())
  184. const backendURL = 'http://shop-roles.node.ed.asmer.org.ua'
  185. const graphQLBackendUrl = `${backendURL}/graphql`
  186. const actionRootCats = () =>
  187. actionPromise('rootCats', gql(graphQLBackendUrl, `query {
  188. CategoryFind(query: "[{\\"parent\\":null}]"){
  189. _id name
  190. }
  191. }`))
  192. const actionCatById = (_id) => //добавить подкатегории
  193. actionPromise('catById', gql(graphQLBackendUrl, `query categoryById($q: String){
  194. CategoryFindOne(query: $q){
  195. _id name goods {
  196. _id name price images {
  197. url
  198. }
  199. }
  200. }
  201. }`, { q: JSON.stringify([{ _id }]) }))
  202. const actionGoodById = (_id) =>
  203. actionPromise('goodById', gql(graphQLBackendUrl, `query goodById($q: String){
  204. GoodFindOne(query: $q){
  205. _id name description price
  206. }
  207. }`, { q: JSON.stringify([{ _id }]) }))
  208. const actionLogin = (login, password) =>
  209. actionPromise('login', gql(graphQLBackendUrl,
  210. `query log($login: String, $password: String){
  211. login(login:$login, password: $password)
  212. }`
  213. , { login, password }))
  214. const actionRegister = (login, password) =>
  215. actionPromise(
  216. "register",
  217. gql(graphQLBackendUrl,
  218. `mutation register($login: String, $password: String) {
  219. UserUpsert(user: {login: $login, password: $password}) {
  220. _id
  221. login
  222. }
  223. }`,
  224. { login: login, password: password }
  225. )
  226. )
  227. store.dispatch(actionRootCats())
  228. /* const actionEnterLogin = () */
  229. enterLogin.onclick = () => {
  230. return loginContainer.style.display = 'block'
  231. }
  232. outLogin.onclick = () => {
  233. return loginContainer.style.display = 'none'
  234. }
  235. enterLogout.onclick = () => {
  236. store.dispatch(actionAuthLogout())
  237. }
  238. enterRegister.onclick = () => {
  239. return hideLogin.style.display = 'none', registerContainer.style.display = 'block'
  240. }
  241. outRegister.onclick = () => {
  242. return hideLogin.style.display = 'block', registerContainer.style.display = 'none'
  243. }
  244. const updateCartInfo = () => {
  245. cartProductsContainer.innerHTML = '';
  246. cartContainer.style.display = 'block';
  247. const cart = store.getState().cart;
  248. const ul = document.createElement('ul')
  249. cartProductsContainer.append(ul)
  250. const clearFromCartButton = document.createElement('button')
  251. clearFromCartButton.innerHTML = "Очистить"
  252. clearFromCartButton.onclick = () => { store.dispatch(cartClear()) }
  253. cartProductsContainer.append(clearFromCartButton)
  254. const buyGoods = document.createElement('button')
  255. buyGoods.innerHTML = "Купить"
  256. buyGoods.onclick = () => { store.dispatch(cartClear()), alert('Спасибо за покупку!') }
  257. cartProductsContainer.append(buyGoods)
  258. for (const [key, value] of Object.entries(cart)) {
  259. const li = document.createElement('li')
  260. const a = document.createElement('a')
  261. const removeFromCartButton = document.createElement('button')
  262. removeFromCartButton.innerText = "-"
  263. removeFromCartButton.onclick = () => { store.dispatch(cartRemove(value.good)) }
  264. a.innerHTML = value.good.name;
  265. ul.append(li)
  266. li.append(a)
  267. if (value.good.images) {
  268. for (let i = 0; i < value.good.images.length; i++) {
  269. const imgElement = document.createElement('img')
  270. imgElement.src = `${backendURL}/${value.good.images[i].url}`;
  271. imgElement.style.height = '100px';
  272. imgElement.style.width = '200px';
  273. li.append(imgElement)
  274. }
  275. }
  276. ul.append(removeFromCartButton)
  277. if (value.count > 1) {
  278. const deleteFromCartButton = document.createElement('button')
  279. deleteFromCartButton.innerHTML = "-1"
  280. deleteFromCartButton.onclick = () => { store.dispatch(cartDelete(value.good)) }
  281. ul.append(deleteFromCartButton)
  282. }
  283. const allCountElement = document.createElement('p');
  284. allCountElement.innerHTML = `Total: ${value.count}`;
  285. ul.append(allCountElement)
  286. }
  287. };
  288. enterCart.onclick = () => {
  289. updateCartInfo();
  290. }
  291. leaveCart.onclick = () => {
  292. cartProductsContainer.innerHTML = '';
  293. cartContainer.style.display = 'none';
  294. }
  295. const loginBtn = document.getElementById('loginBtn')
  296. loginBtn.onclick = handleLogin
  297. function handleLogin() {
  298. let userName = document.getElementById('inputUserName')
  299. let userPassword = document.getElementById('inputPassword')
  300. store.dispatch(actionLogin(userName.value, userPassword.value));
  301. outLogin.click();
  302. }
  303. const registerBtn = document.getElementById('registerBtn');
  304. registerBtn.onclick = handleRegister
  305. function handleRegister() {
  306. let userName = document.getElementById('inputUserNameRegister')
  307. let userPassword = document.getElementById('inputPasswordRegister')
  308. store.dispatch(actionRegister(userName.value, userPassword.value));
  309. return hideLogin.style.display = 'block', registerContainer.style.display = 'none'
  310. outLogin.click();
  311. }
  312. /* store.dispatch(actionLogin('levshin95', '123123')) */
  313. store.subscribe(() => {
  314. if (cartContainer.style.display === 'block' && store.getState().cart) {
  315. updateCartInfo();
  316. }
  317. })
  318. store.subscribe(() => {
  319. const rootCats = store.getState().promise.rootCats?.payload?.data.CategoryFind
  320. if (!rootCats) {
  321. aside.innerHTML = '<img src="Loading_icon.gif">'
  322. } else {
  323. aside.innerHTML = ''
  324. const ul = document.createElement('ul')
  325. aside.append(ul)
  326. for (let { _id, name } of rootCats) {
  327. const li = document.createElement('li')
  328. const a = document.createElement('a')
  329. a.href = "#/category/" + _id
  330. a.innerHTML = name
  331. ul.append(li)
  332. li.append(a)
  333. }
  334. }
  335. })
  336. const displayCategory = function () {
  337. const catById = store.getState().promise.catById?.payload?.data.CategoryFindOne
  338. const [, route] = location.hash.split('/')
  339. if (catById && route === 'category') {
  340. const { name, goods, _id } = catById
  341. categories.innerHTML = `<h1>${name}</h1>`
  342. const ul = document.createElement('ul')
  343. categories.append(ul)
  344. for (let good of goods) {
  345. const li = document.createElement('li')
  346. const a = document.createElement('a')
  347. a.href = "#/good/" + good._id
  348. a.innerHTML = good.name
  349. ul.append(li)
  350. li.append(a)
  351. if (good.images) {
  352. for (let i = 0; i < good.images.length; i++) {
  353. const imgElement = document.createElement('img')
  354. imgElement.src = `${backendURL}/${good.images[i].url}`;
  355. imgElement.style.height = '100px';
  356. imgElement.style.width = '200px';
  357. li.append(imgElement)
  358. }
  359. }
  360. const authToken = store.getState().auth?.token;
  361. if (authToken) {
  362. const addToCartButton = document.createElement('button')
  363. addToCartButton.innerText = "+"
  364. addToCartButton.onclick = () => { store.dispatch(cartAdd(good, 1)) }
  365. ul.append(addToCartButton)
  366. }
  367. }
  368. }
  369. }
  370. store.subscribe(() => {
  371. const authToken = store.getState().auth?.token;
  372. if (!authToken) {
  373. enterLogout.style.display = 'none';
  374. enterCart.style.display = 'none';
  375. enterLogin.style.display = 'block';
  376. displayCategory();
  377. } else {
  378. enterLogin.style.display = 'none';
  379. enterCart.style.display = 'block';
  380. enterLogout.style.display = 'block';
  381. displayCategory();
  382. /* alert(`Hello, ${store.getState().auth?.payload?.sub?.login}`); */
  383. }
  384. })
  385. store.subscribe(() => {
  386. displayCategory();
  387. })
  388. store.subscribe(() => {
  389. const goodById = store.getState().promise.goodById?.payload?.data.GoodFindOne
  390. const [, route] = location.hash.split('/')
  391. if (goodById && route === 'good') {
  392. const { name, description, _id, price, images } = goodById
  393. categories.innerHTML = `<h1>${name}</h1>`
  394. const strongPrice = document.createElement('b')
  395. const div = document.createElement('div')
  396. categories.appendChild(div)
  397. categories.appendChild(strongPrice)
  398. categories.appendChild(strongPrice)
  399. div.innerText = description
  400. strongPrice.innerHTML = price
  401. }
  402. })
  403. store.subscribe(() => { })
  404. /* store.subscribe(() => {
  405. const goodById = store.getState().promise.goodById?.payload?.data.CategoryFindOne
  406. const [,route] = location.hash.split('/')
  407. if (catById && route === 'good') {
  408. const {name, goods, _id} = catById
  409. categories.innerHTML = `<h1>${name}</h1>`
  410. // нарисовать картинки, описание, цену и т.д.
  411. }
  412. }) */
  413. window.onhashchange = () => {
  414. const [, route, _id] = location.hash.split('/')
  415. console.log(route, _id)
  416. const routes = {
  417. category() {
  418. store.dispatch(actionCatById(_id))
  419. },
  420. good() {
  421. store.dispatch(actionGoodById(_id))
  422. },
  423. login() {
  424. console.log('Тут надо нарисовать форму логина. по нажатию кнопки в ней - задиспатчить actionFullLogin')
  425. },
  426. register() {
  427. console.log('Тут надо нарисовать форму логина/регистрации. по нажатию кнопки в ней - задиспатчить actionFullRegister')
  428. }
  429. }
  430. if (route in routes) {
  431. routes[route]()
  432. }
  433. }
  434. window.onhashchange()