Gql_promis.html 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486
  1. <Header>Gql</Header>
  2. <body>
  3. <header>
  4. <div id='cartIcon'></div>
  5. </header>
  6. <div id='mainContainer'>
  7. <aside id='aside'>
  8. </aside>
  9. <main id='main'>
  10. </main>
  11. </div>
  12. <script>
  13. function jwtDecode(token) { // расщифровки токена авторизации
  14. if (!token || typeof token != "string")
  15. return undefined;
  16. let tokenArr = token.split(".");
  17. if (tokenArr.length != 3)
  18. return undefined;
  19. try {
  20. let tokenJsonStr = atob(tokenArr[1]);
  21. let tokenJson = JSON.parse(tokenJsonStr);
  22. return tokenJson;
  23. }
  24. catch {
  25. return undefined;
  26. }
  27. }
  28. function combineReducers(reducers) {
  29. function totalReducer(totalState = {}, action) {
  30. const newTotalState = {} //объект, который будет хранить только новые состояния дочерних редьюсеров
  31. //цикл + квадратные скобочки позволяют написать код, который будет работать с любыми количеством дочерных редьюсеров
  32. for (const [reducerName, childReducer] of Object.entries(reducers)) {
  33. const newState = childReducer(totalState[reducerName], action) //запуск дочернего редьюсера
  34. if (newState !== totalState[reducerName]) { //если он отреагировал на action
  35. newTotalState[reducerName] = newState //добавляем его в newTotalState
  36. }
  37. }
  38. //Универсальная проверка на то, что хотя бы один дочерний редьюсер создал новый стейт:
  39. if (Object.values(newTotalState).length) {
  40. return { ...totalState, ...newTotalState } //создаем новый общий стейт, накладывая новый стейты дочерних редьюсеров на старые
  41. }
  42. return totalState //если экшен не был понят ни одним из дочерних редьюсеров, возвращаем общий стейт как был.
  43. }
  44. return totalReducer
  45. }
  46. function cartReducer(state = {}, action) { // диспетчер обработки
  47. switch (action.type) {
  48. case 'CART_ADD':
  49. if (action.count >= 0) {
  50. let newState = { ...state };
  51. let { count } = state[action.good._id] ?? { count: 0 };
  52. newState[action.good._id] = { count: action.count + count, good: { ...action.good } }
  53. return newState;
  54. }
  55. case 'CART_SUB':
  56. if (action.count >= 0) {
  57. let newState = { ...state };
  58. let { count } = state[action.good._id] ?? { count: 0 };
  59. if (count >= action.count) {
  60. newState[action.good._id] = { count: action.count - count, good: { ...action.good } }
  61. return newState;
  62. }
  63. }
  64. break;
  65. case 'CART_DEL':
  66. {
  67. let newState = { ...state };
  68. delete newState[action.good._id];
  69. return newState;
  70. }
  71. case 'CART_SET':
  72. {
  73. let newState = { ...state };
  74. newState[action.good._id] = { count: action.count, good: { ...action.good } };
  75. return newState;
  76. }
  77. case 'CART_CLEAR':
  78. return {};
  79. }
  80. return state;
  81. }
  82. function localStoredReducer(originalReducer, localStorageKey) {
  83. let firstRun = true;
  84. function wrapper(state, action) {
  85. if (firstRun) {
  86. firstRun = false;
  87. try {
  88. let state = JSON.parse(localStorage[localStorageKey]);
  89. if (state.authToken)
  90. return state;
  91. }
  92. catch { }
  93. }
  94. let res = originalReducer(state, action);
  95. localStorage[localStorageKey] = JSON.stringify(res);
  96. return res;
  97. }
  98. return wrapper
  99. }
  100. function promiseReducer(state = {}, action) { // диспетчер обработки
  101. if (action) {
  102. if (action.type === 'PROMISE') {
  103. let newState = { ...state };
  104. newState[action.name] = { status: action.status, payload: action.payload, error: action.error };
  105. return newState;
  106. }
  107. }
  108. return state;
  109. }
  110. function authReducer(state = {}, action) { // диспетчер обработки login
  111. if (action) {
  112. if (action.type === 'AUTH_LOGIN') {
  113. let newState = { ...state };
  114. newState.token = action.token;
  115. newState.payload = jwtDecode(action.token);
  116. if (!newState.payload) {
  117. newState.token = undefined;
  118. }
  119. if (newState.token)
  120. localStorage.authToken = newState.token;
  121. else
  122. delete localStorage.authToken;
  123. return newState;
  124. }
  125. else if (action.type === 'AUTH_LOGOUT') {
  126. let newState = { ...state };
  127. newState.token = undefined;
  128. newState.payload = undefined;
  129. delete localStorage.authToken;
  130. return newState;
  131. }
  132. }
  133. return state;
  134. }
  135. function createStore(reducer) {
  136. let state = reducer(undefined, {}) //стартовая инициализация состояния, запуск редьюсера со state === undefined
  137. let cbs = [] //массив подписчиков
  138. const getState = () => { return state; } //функция, возвращающая переменную из замыкания
  139. const subscribe = cb => (cbs.push(cb), //запоминаем подписчиков в массиве
  140. () => cbs = cbs.filter(c => c !== cb)) //возвращаем функцию unsubscribe, которая удаляет подписчика из списка
  141. function dispatch(action) {
  142. if (typeof action === 'function') { //если action - не объект, а функция
  143. return action(dispatch, getState) //запускаем эту функцию и даем ей dispatch и getState для работы
  144. }
  145. const newState = reducer(state, action) //пробуем запустить редьюсер
  146. if (newState !== state) { //проверяем, смог ли редьюсер обработать action
  147. state = newState //если смог, то обновляем state
  148. for (let cb of cbs) cb() //и запускаем подписчиков
  149. }
  150. }
  151. return {
  152. getState, //добавление функции getState в результирующий объект
  153. dispatch,
  154. subscribe //добавление subscribe в объект
  155. }
  156. }
  157. function getGql(url) {
  158. return function gql(query, vars = undefined) {
  159. let fetchSettings =
  160. {
  161. method: "POST",
  162. headers:
  163. {
  164. "Content-Type": "application/json",
  165. "Accept": "application/json"
  166. },
  167. body: JSON.stringify(
  168. {
  169. query: query,
  170. variables: vars
  171. })
  172. };
  173. let authToken = window.localStorage.authToken;
  174. if (authToken) {
  175. fetchSettings.headers["Authorization"] = `Bearer ${authToken}`;
  176. }
  177. return fetch(url, fetchSettings)
  178. .then(res => {
  179. if (!res.ok) {
  180. throw Error(res.statusText);
  181. }
  182. return res.json();
  183. });
  184. }
  185. }
  186. const gql = getGql("http://shop-roles.node.ed.asmer.org.ua/graphql");
  187. const actionPromise = (name, promise) => {
  188. return actionPromiseInt = async (dispatch) => {
  189. dispatch(actionPending(name)) //сигнализируем redux, что промис начался
  190. try {
  191. let payload = await promise //ожидаем промиса
  192. if (payload && payload.data)
  193. payload = Object.values(payload.data)[0];
  194. dispatch(actionFulfilled(name, payload)) //сигнализируем redux, что промис успешно выполнен
  195. return payload //в месте запуска store.dispatch с этим thunk можно так же получить результат промиса
  196. }
  197. catch (error) {
  198. dispatch(actionRejected(name, error)) //в случае ошибки - сигнализируем redux, что промис несложился
  199. }
  200. }
  201. }
  202. const actionPending = (name) => ({ type: 'PROMISE', name: name, status: 'PENDING' });
  203. const actionFulfilled = (name, payload) => ({ type: 'PROMISE', name: name, payload: payload, status: 'FULFILLED' });
  204. const actionRejected = (name, error) => ({ type: 'PROMISE', name: name, error: error, status: 'REJECTED' });
  205. const actionAuthLogin = token => ({ type: 'AUTH_LOGIN', token });
  206. const actionAuthLogout = () => ({ type: 'AUTH_LOGOUT' });
  207. const actionCartAdd = (good, count = 1) => ({ type: 'CART_ADD', count: count, good: good });
  208. const actionCartSub = (good, count = 1) => ({ type: 'CART_SUB', count, good }); //Уменьшение количества товара. Должен уменьшать количество товара в state, или удалять его если количество будет 0 или отрицательным
  209. const actionCartDel = (good) => ({ type: 'CART_DEL', good }); //Удаление товара. Должен удалять ключ из state
  210. const actionCartSet = (good, count = 1) => ({ type: 'CART_SET', count, good }); //Задание количества товара. В отличие от добавления и уменьшения, не учитывает того количества, которое уже было в корзине, а тупо назначает количество поверху (или создает новый ключ, если в корзине товара не было). Если count 0 или отрицательное число - удаляем ключ из корзины;
  211. const actionCartClear = () => ({ type: 'CART_CLEAR' }); //Очистка корзины. state должен стать пустым объектом {}
  212. ///////////////////////////////////////
  213. const gqlRootCats = () => {
  214. const catQuery = `query roots {
  215. CategoryFind(query: "[{\\"parent\\": null }]") {
  216. _id name
  217. }}`;
  218. return gql(catQuery);
  219. }
  220. const actionRootCats = () =>
  221. actionPromise('rootCats', gqlRootCats());
  222. const gqlCategoryFindOne = (id) => {
  223. const catQuery = `query CategoryFindOne($q: String) {
  224. CategoryFindOne(query: $q) {
  225. _id name
  226. parent { _id name }
  227. subCategories { _id name }
  228. goods { _id name price description
  229. images { url }
  230. }
  231. }
  232. }`;
  233. return gql(catQuery, { q: `[{\"_id\": \"${id}\"}]` });
  234. }
  235. const actionCategoryFindOne = (id) =>
  236. actionPromise('catFindOne', gqlCategoryFindOne(id));
  237. const gqlGoodFindOne = (id) => {
  238. const catQuery = `
  239. query GoodFindOne($q: String) {
  240. GoodFindOne(query: $q) {
  241. _id name price description
  242. images {
  243. url
  244. }
  245. }
  246. }
  247. `;
  248. return gql(catQuery, { q: `[{\"_id\": \"${id}\"}]` });
  249. }
  250. const actionGoodFindOne = (id) =>
  251. actionPromise('goodFindOne', gqlGoodFindOne(id));
  252. //////////////////////////////////
  253. const actionLogin = (login, password) => {
  254. const upsertQuery = `query login($login:String, $password:String){
  255. login(login:$login, password:$password)
  256. }`;
  257. return gql(upsertQuery, { login: login, password: password });
  258. }
  259. const actionFullLogin = (login, password) => {
  260. return gqlFullLogin = async (dispatch) => {
  261. //dispatch возвращает то, что вернул thunk, возвращаемый actionLogin, а там промис,
  262. //так как actionPromise возвращает асинхронную функцию
  263. let promiseResult = dispatch((dispatch) => actionLogin(login, password));
  264. let res = await promiseResult;
  265. if (res && res.data) {
  266. let token = Object.values(res.data)[0];
  267. if (token && typeof token == 'string')
  268. return dispatch(actionAuthLogin(token));
  269. }
  270. //проверьте что token - строка и отдайте его в actionAuthLogin
  271. }
  272. }
  273. ////////////////////////////////////////
  274. const actionAuthUpsert = (login, password) => {
  275. const loginQuery = `mutation UserRegistration($login: String, $password: String) {
  276. UserUpsert(user: {login: $login, password: $password}) {
  277. _id createdAt
  278. }
  279. }`;
  280. return gql(loginQuery, { login: login, password: password });
  281. }
  282. const actionFullAuthUpsert = (login, password) => {
  283. return gqlFullAuthUpsert = async (dispatch) => {
  284. //dispatch возвращает то, что вернул thunk, возвращаемый actionLogin, а там промис,
  285. //так как actionPromise возвращает асинхронную функцию
  286. let promiseResult = dispatch((dispatch) => actionAuthUpsert(login, password));
  287. let res = await promiseResult;
  288. dispatch(actionFullLogin(login, password));
  289. //проверьте что token - строка и отдайте его в actionAuthLogin
  290. }
  291. }
  292. ////////////////////////////////////////
  293. const orderUpsert = (order, id = null) => {
  294. const orderUpsertQuery = `mutation OrderUpsert($order: OrderInput) {
  295. OrderUpsert(order: $order) {
  296. _id
  297. }
  298. }`;
  299. return gql(orderUpsertQuery, { order: { "_id": id, "orderGoods": order } });
  300. }
  301. const orderFullUpsert = () => {
  302. return gqlFullOrderUpsert = async (dispatch, getState) => {
  303. let state = getState();
  304. let order = [];
  305. for (cartItem of Object.values(state.cartReducer)) {
  306. //{count: 3, good: {_id: "xxxx" }}
  307. order.push({ good: { _id: cartItem.good._id }, count: cartItem.count });
  308. }
  309. if (order.length == 0)
  310. return;
  311. //dispatch возвращает то, что вернул thunk, возвращаемый actionLogin, а там промис,
  312. //так как actionPromise возвращает асинхронную функцию
  313. let promiseResult = orderUpsert(order);
  314. let res = await promiseResult;
  315. if (res && res.errors && res.errors.length > 0) {
  316. throw res.errors[0];
  317. }
  318. dispatch(actionCartClear());
  319. //проверьте что token - строка и отдайте его в actionAuthLogin
  320. }
  321. }
  322. const gqlFindOrders = () => {
  323. const findOrdersQuery = `query OrderFind {
  324. OrderFind(query: "[{}]") {
  325. _id total
  326. orderGoods {
  327. _id price count total
  328. good {
  329. name
  330. }
  331. }
  332. }
  333. }`;
  334. return gql(findOrdersQuery);
  335. }
  336. const actionFindOrders = () =>
  337. actionPromise('orders', gqlFindOrders());
  338. ///////////////////////////////////////////////////
  339. const store = createStore(combineReducers({ promiseReducer, authReducer, cartReducer: localStoredReducer(cartReducer, 'cart') }));
  340. const delay = (ms, action) => new Promise(ok => setTimeout(() => {
  341. action();
  342. ok(ms);
  343. }, ms));
  344. /*store.subscribe(() => {
  345. console.log({ state: store.getState() });
  346. });*/
  347. //////////////////////////////////////////////
  348. const fillRootCategories = (categories, htmlEl) => {
  349. htmlEl.innerText = '';
  350. if (!categories)
  351. return;
  352. for (category of categories) {
  353. let el = document.createElement('a');
  354. el.innerText = `${category.name}`;
  355. el.href = `#/categories/${category._id}`;
  356. htmlEl.appendChild(el);
  357. }
  358. }
  359. store.subscribe(() =>
  360. subscribePromiseItem("rootCats", aside, [], fillRootCategories));
  361. const fillCurrentCategoryContent = (category, htmlEl) => {
  362. htmlEl.innerHTML = '';
  363. const { name, parent, subCategories, goods } = category;
  364. htmlEl.innerHTML = `<h1>Category: ${name}</h1>
  365. <section>Parent: <a href="#/subCategories/${parent?._id}">${parent?.name ?? 'Empty'}</a></section>
  366. `
  367. htmlEl.innerHTML += `<section>Sub Categories:</section><br>`
  368. for (const subCategory of subCategories) {
  369. htmlEl.innerHTML += `<a href="#/subCategories/${subCategory._id}">${subCategory.name}</a><br>`
  370. }
  371. htmlEl.innerHTML += `<section>Sub Categories:</section><br>`
  372. for (const good of goods) {
  373. htmlEl.innerHTML += `<a href="#/goods/${good._id}">${good.name}</a><br>`//вставить css display block
  374. }
  375. }
  376. store.subscribe(() =>
  377. subscribePromiseItem(
  378. "catFindOne", main, ["categories", "subCategories"], fillCurrentCategoryContent));
  379. const fillCurrentGoodContent = (good, htmlEl) => {
  380. htmlEl.innerHTML = '';
  381. const { name, _id, price, description, images } = good;
  382. htmlEl.innerHTML = `<h1>Good: ${name}</h1>
  383. <section>Description: ${description}</section>
  384. <section>Price: ${price}</section>
  385. `
  386. htmlEl.innerHTML += `<section>Images:</section><br>` //вставить css display block
  387. for (const image of images) {
  388. htmlEl.innerHTML += `<img src="http://shop-roles.node.ed.asmer.org.ua/${image.url}"</img><br>`//вставить css display block
  389. }
  390. }
  391. store.subscribe(() =>
  392. subscribePromiseItem(
  393. "goodFindOne", main, ["goods"], fillCurrentGoodContent));
  394. const subscribePromiseItem = (promiseName, htmlEl, subscrNames, execFunc) => {
  395. const [, route, _id] = location.hash.split('/');
  396. if ((subscrNames.length > 0 && (!route || !subscrNames.some(v => v == route)))/* || !_id*/)
  397. return;
  398. const { status, payload, error } = store.getState().promiseReducer[promiseName];
  399. if (status === 'PENDING') {
  400. htmlEl.innerHTML = `<img src='https://cdn.dribbble.com/users/63485/screenshots/1309731/infinite-gif-preloader.gif' />`
  401. }
  402. if (status == "FULFILLED") {
  403. execFunc(payload, htmlEl);
  404. }
  405. }
  406. window.onhashchange = () => {
  407. const [, route, _id] = location.hash.split('/')
  408. const routes = {
  409. categories() {
  410. console.log('Category', _id)
  411. store.dispatch(actionCategoryFindOne(_id))
  412. },
  413. subCategories() {
  414. console.log('subCategory', _id)
  415. store.dispatch(actionCategoryFindOne(_id))
  416. },
  417. goods() {
  418. console.log('good', _id)
  419. store.dispatch(actionGoodFindOne(_id))
  420. },
  421. //category() {
  422. //store.dispatch(actionCategoryById(_id))
  423. //},
  424. //good(){
  425. ////тут был store.dispatch goodById
  426. //console.log('good', _id)
  427. //},
  428. login() {
  429. console.log('А ТУТ ЩА ДОЛЖНА БЫТЬ ФОРМА ЛОГИНА')
  430. //нарисовать форму логина, которая по нажатию кнопки Login делает store.dispatch(actionFullLogin(login, password))
  431. },
  432. //register(){
  433. ////нарисовать форму регистрации, которая по нажатию кнопки Login делает store.dispatch(actionFullRegister(login, password))
  434. //},
  435. }
  436. if (route in routes) {
  437. routes[route]()
  438. }
  439. }
  440. window.onhashchange()
  441. store.dispatch(actionRootCats());
  442. /*store.dispatch(actionCategoryFindOne("6262ca7dbf8b206433f5b3d1"));
  443. store.dispatch(actionGoodFindOne("62d3099ab74e1f5f2ec1a125"));
  444. store.dispatch(actionFullLogin("Berg", "123456789"));
  445. //store.dispatch(actionFullAuthUpsert("Berg1", "12345678911"));
  446. store.dispatch(actionCartAdd({ _id: '62d30938b74e1f5f2ec1a124', price: 50 }));
  447. delay(3000, () => {
  448. store.dispatch(orderFullUpsert());
  449. store.dispatch(actionFindOrders());
  450. });*/
  451. //delay(500, () => store.dispatch(actionFindOrders()));
  452. </script>
  453. </body>