index.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711
  1. let loginValue = "";
  2. let passwordValue = "";
  3. const authDiv = document.createElement("div");
  4. const noAuthDiv = document.createElement("div");
  5. const cartIcon = document.createElement("div");
  6. const loginButton = document.createElement("button");
  7. const registerButton = document.createElement("button");
  8. const nameDiv = document.createElement("div");
  9. const logoutButton = document.createElement("button");
  10. cartIcon.innerHTML = "<a href = '#/cart'>Cart</a>";
  11. loginButton.innerText = "Login";
  12. loginButton.onclick = () => {
  13. window.location = "#/login";
  14. };
  15. registerButton.innerText = "Register";
  16. registerButton.onclick = () => {
  17. window.location = "#/register";
  18. };
  19. logoutButton.innerText = "Logout";
  20. noAuthDiv.append(loginButton);
  21. noAuthDiv.append(registerButton);
  22. authDiv.append(nameDiv);
  23. authDiv.append(logoutButton);
  24. authDiv.append(cartIcon);
  25. cartIcon.style.marginLeft = "auto";
  26. authDiv.style.display = "flex";
  27. header.append(authDiv);
  28. header.append(noAuthDiv);
  29. const jwtDecode = (token) => {
  30. try {
  31. let payload = JSON.parse(atob(token.split(".")[1]));
  32. return payload;
  33. } catch (e) {
  34. console.log(e);
  35. }
  36. };
  37. function createStore(reducer) {
  38. let state = reducer(undefined, {});
  39. let cbs = [];
  40. const getState = () => state;
  41. const subscribe = (cb) => (cbs.push(cb), () => (cbs = cbs.filter((c) => c !== cb)));
  42. const dispatch = (action) => {
  43. if (typeof action === "function") {
  44. return action(dispatch, getState);
  45. }
  46. const newState = reducer(state, action);
  47. if (newState !== state) {
  48. state = newState;
  49. for (let cb of cbs) cb();
  50. }
  51. };
  52. return {
  53. getState,
  54. dispatch,
  55. subscribe,
  56. };
  57. }
  58. function cartReducer(state = {}, { type, good, count = 1 }) {
  59. if (!Object.keys(state).length) {
  60. let savedData =
  61. (localStorage[jwtDecode(localStorage.authToken)?.sub.login] &&
  62. JSON.parse(localStorage[jwtDecode(localStorage.authToken)?.sub.login])) ||
  63. null;
  64. if (savedData) {
  65. state = savedData?.cart || {};
  66. } else {
  67. state = {};
  68. }
  69. }
  70. if (count <= 0) {
  71. type = "CART_DELETE";
  72. }
  73. if (type === "CART_ADD") {
  74. return {
  75. ...state,
  76. [good["_id"]]: {
  77. good,
  78. count: good["_id"] in state ? state[good._id].count + count : count,
  79. },
  80. };
  81. }
  82. if (type === "CART_CHANGE") {
  83. return {
  84. ...state,
  85. [good["_id"]]: {
  86. good,
  87. count: count,
  88. },
  89. };
  90. }
  91. if (type === "CART_DELETE") {
  92. let { [good._id]: toRemove, ...newState } = state;
  93. return newState;
  94. }
  95. if (type === "CART_CLEAR") {
  96. return {};
  97. }
  98. return state;
  99. }
  100. function promiseReducer(state = {}, { type, name, status, payload, error }) {
  101. if (type === "PROMISE") {
  102. return {
  103. ...state,
  104. [name]: { status, payload, error },
  105. };
  106. }
  107. return state;
  108. }
  109. function authReducer(state, { type, token }) {
  110. if (state === undefined) {
  111. if (localStorage.authToken) {
  112. token = localStorage.authToken;
  113. type = "AUTH_LOGIN";
  114. state = {};
  115. }
  116. }
  117. if (type === "AUTH_LOGIN") {
  118. if (!token || !jwtDecode(token)) return {};
  119. localStorage.authToken = token;
  120. return {
  121. ...state,
  122. token: token,
  123. payload: jwtDecode(token),
  124. };
  125. }
  126. if (type === "AUTH_LOGOUT") {
  127. localStorage.removeItem("authToken");
  128. return {};
  129. }
  130. return state || {};
  131. }
  132. const combineReducers =
  133. (reducers) =>
  134. (state = {}, action) => {
  135. let newState = {};
  136. for (let [key, reducer] of Object.entries(reducers)) {
  137. let reducerResult = reducer(state[key], action);
  138. if (state[key] !== reducerResult) {
  139. newState = { ...newState, [key]: reducerResult };
  140. }
  141. }
  142. if (Object.keys(newState).length) {
  143. state = { ...state, ...newState };
  144. }
  145. return state;
  146. };
  147. const store = createStore(combineReducers({ auth: authReducer, promise: promiseReducer, cart: cartReducer }));
  148. store.subscribe(() => console.log(store.getState()));
  149. store.subscribe(() => {
  150. let {
  151. cart,
  152. auth: { payload },
  153. } = store.getState();
  154. let localStorageLoginSave = {};
  155. let login = payload?.sub.login || null;
  156. if (!login) {
  157. return;
  158. }
  159. if (localStorage[login]) {
  160. localStorageLoginSave = JSON.parse(localStorage[login]);
  161. if (Object.keys(cart).length === 0) {
  162. let { cart, ...newState } = localStorageLoginSave;
  163. localStorage[login] = JSON.stringify(newState);
  164. return;
  165. }
  166. }
  167. if (Object.keys(cart).length) {
  168. localStorage[login] = JSON.stringify({ ...localStorageLoginSave, cart });
  169. }
  170. });
  171. const actionPending = (name) => ({ type: "PROMISE", name, status: "PENDING" });
  172. const actionFulfilled = (name, payload) => ({ type: "PROMISE", name, status: "FULFILLED", payload });
  173. const actionRejected = (name, error) => ({ type: "PROMISE", name, status: "REJECTED", error });
  174. const actionPromise = (name, promise) => async (dispatch) => {
  175. dispatch(actionPending(name));
  176. try {
  177. let payload = await promise;
  178. dispatch(actionFulfilled(name, payload));
  179. return payload;
  180. } catch (error) {
  181. dispatch(actionRejected(name, error));
  182. }
  183. };
  184. const getGQL = (url) => (query, variables) =>
  185. fetch(url, {
  186. method: "POST",
  187. headers: {
  188. "Content-Type": "application/json",
  189. ...(localStorage.authToken ? { Authorization: "Bearer " + localStorage.authToken } : {}),
  190. },
  191. body: JSON.stringify({ query, variables }),
  192. })
  193. .then((res) => res.json())
  194. .then((data) => {
  195. if (data.errors) {
  196. throw new Error(JSON.stringify(data.errors));
  197. } else return Object.values(data.data)[0];
  198. });
  199. const backendURL = "http://shop-roles.asmer.fs.a-level.com.ua";
  200. const gql = getGQL(backendURL + "/graphql");
  201. const actionRootCats = () =>
  202. actionPromise(
  203. "rootCats",
  204. gql(`query {
  205. CategoryFind(query: "[{\\"parent\\":null}]"){
  206. _id name
  207. }
  208. }`)
  209. );
  210. const actionGoodById = (_id) =>
  211. actionPromise(
  212. "good",
  213. gql(
  214. `query GoodById($q: String){
  215. GoodFindOne(query: $q){
  216. _id name price images{
  217. url
  218. }
  219. }
  220. }`,
  221. { q: JSON.stringify([{ _id }]) }
  222. )
  223. );
  224. const actionCatById = (
  225. _id //добавить подкатегории
  226. ) =>
  227. actionPromise(
  228. "catById",
  229. gql(
  230. `query catById($q: String){
  231. CategoryFindOne(query: $q){
  232. _id name goods {
  233. _id name price images {
  234. url
  235. }
  236. }
  237. }
  238. }`,
  239. { q: JSON.stringify([{ _id }]) }
  240. )
  241. );
  242. const actionOrders = () => (dispatch) =>
  243. dispatch(
  244. actionPromise(
  245. "orders",
  246. gql(`
  247. query orders{
  248. OrderFind(query:"[{}]"){
  249. _id total createdAt orderGoods{
  250. _id count price good{
  251. name _id price images{
  252. url
  253. }
  254. }
  255. }
  256. }
  257. }
  258. `)
  259. )
  260. );
  261. const actionNewOrder =
  262. (orderGoods = []) =>
  263. async (dispatch, getState) => {
  264. await dispatch(
  265. actionPromise(
  266. "newOrder",
  267. gql(
  268. `mutation newOrder($order:OrderInput){
  269. OrderUpsert(order:$order){
  270. _id total
  271. }
  272. }
  273. `,
  274. {
  275. order: {
  276. orderGoods: orderGoods,
  277. },
  278. }
  279. )
  280. )
  281. );
  282. let {
  283. promise: { newOrder },
  284. } = getState();
  285. if (newOrder.status === "FULFILLED") {
  286. dispatch(actionCartClear());
  287. }
  288. };
  289. const actionAuthLogin = (token) => ({
  290. type: "AUTH_LOGIN",
  291. token: token,
  292. });
  293. const actionLogin = (login, password) => async (dispatch) => {
  294. const token = await dispatch(
  295. actionPromise(
  296. "login",
  297. gql(
  298. `query log($login:String,$password:String){
  299. login(login:$login,password:$password)
  300. }
  301. `,
  302. {
  303. login: login,
  304. password: password,
  305. }
  306. )
  307. )
  308. );
  309. await dispatch(actionAuthLogin(token));
  310. window.location = "#/home";
  311. };
  312. const actionAuthLogout = () => ({ type: "AUTH_LOGOUT" });
  313. const actionLogout = () => async (dispatch) => {
  314. await dispatch(actionAuthLogout());
  315. await dispatch(actionCartClear());
  316. window.location = "#/home";
  317. };
  318. const actionRegister = (login, password) => async (dispatch) => {
  319. await dispatch(
  320. actionPromise(
  321. "register",
  322. gql(
  323. `mutation register($login:String,$password:String){
  324. UserUpsert(user:{login:$login,password:$password}){
  325. _id login
  326. }
  327. }`,
  328. {
  329. login: login,
  330. password: password,
  331. }
  332. )
  333. )
  334. );
  335. await dispatch(actionLogin(loginValue, passwordValue));
  336. };
  337. const actionCartAdd = (good, count = 1) => ({ type: "CART_ADD", good, count });
  338. const actionCartChange = (good, count = 1) => ({ type: "CART_CHANGE", good, count });
  339. const actionCartDelete = (good) => ({ type: "CART_DELETE", good });
  340. const actionCartClear = () => ({ type: "CART_CLEAR" });
  341. store.dispatch(actionRootCats());
  342. // store.dispatch(actionCartAdd({ _id: "пиво", name: "одеколонь", price: 30 }));
  343. // //{пиво: {count: 1, good: {_id: 'пиво', name: 'одеколонь', price: 30}}}
  344. // store.dispatch(actionCartAdd({ _id: "чипсы", name: "одеколонь", price: 30 }));
  345. // //{пиво: {count: 1, good: {_id: 'пиво', name: 'одеколонь', price: 30}},
  346. // // чипсы: {count: 1, good: {_id: 'чипсы', name: 'огурец с молоком', price: 50}},
  347. // //}
  348. // store.dispatch(actionCartAdd({ _id: "пиво", name: "одеколонь", price: 30 })); //count: 2
  349. // //{пиво: {count: 2, good: {_id: 'пиво', name: 'одеколонь', price: 30}},
  350. // // чипсы: {count: 1, good: {_id: 'чипсы', name: 'огурец с молоком', price: 50}},
  351. // //}
  352. // store.dispatch(actionCartChange({ _id: "чипсы", name: "одеколонь", price: 30 }, 2));
  353. // //{пиво: {count: 2, good: {_id: 'пиво', name: 'одеколонь', price: 30}},
  354. // // чипсы: {count: 2, good: {_id: 'чипсы', name: 'огурец с молоком', price: 50}},
  355. // //}
  356. // store.dispatch(actionCartDelete({ _id: "пиво", name: "одеколонь", price: 30 }));
  357. // //{чипсы: {count: 2, good: {_id: 'чипсы', name: 'огурец с молоком', price: 50}},
  358. // //}
  359. // store.dispatch(actionCartClear()); // {}
  360. logoutButton.onclick = () => store.dispatch(actionLogout());
  361. store.subscribe(() => {
  362. const {
  363. auth: { token, payload },
  364. } = store.getState();
  365. if (token) {
  366. nameDiv.innerHTML = `<a href = "#/dashboard">${payload.sub.login}</a>`;
  367. authDiv.style.display = "flex";
  368. noAuthDiv.style.display = "none";
  369. } else {
  370. authDiv.style.display = "none";
  371. noAuthDiv.style.display = "block";
  372. }
  373. });
  374. store.subscribe(() => {
  375. const {
  376. promise: { rootCats },
  377. } = store.getState();
  378. if (rootCats?.payload) {
  379. aside.innerHTML = "";
  380. for (const { _id, name } of rootCats?.payload) {
  381. const link = document.createElement("a");
  382. link.href = `#/category/${_id}`;
  383. link.innerText = name;
  384. aside.append(link);
  385. }
  386. }
  387. });
  388. const updateCart = () => {
  389. let {
  390. cart,
  391. auth: { token },
  392. } = store.getState();
  393. if (!token) {
  394. return;
  395. }
  396. let orderGoods = [];
  397. Object.entries(cart).map(([index, order]) => {
  398. orderGoods[orderGoods.length] = { count: order.count, good: { _id: index } };
  399. });
  400. console.log(orderGoods);
  401. main.innerHTML = "";
  402. let orderList = document.createElement("div");
  403. let clearButton = document.createElement("button");
  404. let sum = 0;
  405. let sumField = document.createElement("div");
  406. let submitButton = document.createElement("button");
  407. Object.entries(cart).length ? (submitButton.disabled = false) : (submitButton.disabled = true);
  408. submitButton.innerText = "Buy";
  409. submitButton.style.display = "block";
  410. submitButton.style.marginLeft = "auto";
  411. submitButton.style.marginTop = "10px";
  412. sumField.style.marginLeft = "100%";
  413. sumField.style.fontWeight = "bold";
  414. clearButton.innerHTML = "Clear cart";
  415. clearButton.style.display = "block";
  416. clearButton.style.marginLeft = "auto";
  417. Object.keys(cart).length > 0 ? (clearButton.disabled = false) : (clearButton.disabled = true);
  418. Object.keys(cart).length > 0 ? (sumField.style.display = "block") : (sumField.style.display = "none");
  419. for (let [id, order] of Object.entries(cart)) {
  420. let { name, images, price } = order.good;
  421. let orderWrapper = document.createElement("div");
  422. orderWrapper.style.display = "flex";
  423. orderWrapper.style.width = "100%";
  424. let goodBox = document.createElement("div");
  425. goodBox.style = "display:flex; flex:1;";
  426. let countBox = document.createElement("div");
  427. let addButton = document.createElement("button");
  428. addButton.innerHTML = "+";
  429. let removeButton = document.createElement("button");
  430. removeButton.innerHTML = "-";
  431. let deleteButton = document.createElement("button");
  432. deleteButton.innerHTML = "X";
  433. let countField = document.createElement("span");
  434. countField.innerHTML = order.count;
  435. countField.innerHTML = order.count;
  436. goodBox.innerHTML = `
  437. <div >
  438. <div><img style="height:100px;" src = "${backendURL}/${images ? images[0].url : ""}"></div>
  439. <div><h3>${name}</h3></div>
  440. <div>${order.count} * ${price} = ${+order.count * +price}</div>
  441. </div>
  442. `;
  443. sum += +order.count * +price;
  444. countBox.append(addButton);
  445. countBox.append(countField);
  446. countBox.append(removeButton);
  447. countBox.append(deleteButton);
  448. orderWrapper.append(goodBox, countBox);
  449. addButton.onclick = () => {
  450. store.dispatch(actionCartChange(order.good, ++order.count));
  451. };
  452. removeButton.onclick = () => {
  453. store.dispatch(actionCartChange(order.good, --order.count));
  454. };
  455. deleteButton.onclick = () => {
  456. store.dispatch(actionCartDelete(order.good));
  457. };
  458. orderList.append(orderWrapper);
  459. }
  460. clearButton.onclick = () => store.dispatch(actionCartClear());
  461. submitButton.onclick = () => store.dispatch(actionNewOrder(orderGoods));
  462. sumField.innerText = sum;
  463. main.append(orderList);
  464. main.append(sumField);
  465. main.append(clearButton);
  466. main.append(submitButton);
  467. };
  468. window.onhashchange = () => {
  469. const [, route, _id] = location.hash.split("/");
  470. const routes = {
  471. home() {
  472. main.innerHTML = "Контент";
  473. },
  474. category() {
  475. store.dispatch(actionCatById(_id));
  476. },
  477. good() {
  478. //задиспатчить actionGoodById
  479. store.dispatch(actionGoodById(_id));
  480. },
  481. login() {
  482. const loginInput = document.createElement("input");
  483. const passwordInput = document.createElement("input");
  484. const loginSubmitButton = document.createElement("button");
  485. loginSubmitButton.innerHTML = "Login";
  486. main.innerHTML = "";
  487. main.append(loginInput);
  488. main.append(passwordInput);
  489. main.append(loginSubmitButton);
  490. loginSubmitButton.onclick = () => {
  491. loginValue = loginInput.value;
  492. passwordValue = passwordInput.value;
  493. store.dispatch(actionLogin(loginValue, passwordValue));
  494. };
  495. },
  496. register() {
  497. const loginInput = document.createElement("input");
  498. const passwordInput = document.createElement("input");
  499. const registerSubmitButton = document.createElement("button");
  500. registerSubmitButton.innerHTML = "Register";
  501. main.innerHTML = "";
  502. main.append(loginInput);
  503. main.append(passwordInput);
  504. main.append(registerSubmitButton);
  505. registerSubmitButton.onclick = () => {
  506. loginValue = loginInput.value;
  507. passwordValue = passwordInput.value;
  508. store.dispatch(actionRegister(loginValue, passwordValue));
  509. };
  510. },
  511. async dashboard() {
  512. await store.dispatch(actionOrders());
  513. let {
  514. promise: { orders },
  515. auth: { token },
  516. } = store.getState();
  517. if (!token || orders.status === "REJECTED") {
  518. return;
  519. }
  520. main.innerHTML = "";
  521. let orderList = document.createElement("div");
  522. for (let order of orders.payload) {
  523. let orderBlock = document.createElement("div");
  524. let orderGoodsBlock = document.createElement("div");
  525. let orderHeaderBlock = document.createElement("div");
  526. let orderSumBlock = document.createElement("div");
  527. let createdAtBlock = document.createElement("div");
  528. createdAtBlock.innerHTML = ("" + new Date(+order.createdAt)).slice(0, 25);
  529. for (let orderGood of order.orderGoods) {
  530. let {
  531. count,
  532. good: { name, price, images },
  533. } = orderGood;
  534. let goodBlock = document.createElement("div");
  535. goodBlock.innerHTML = `
  536. <div style = "width:30%;">${name}</div><div style = "width:40%;"><img src ="${backendURL}/${
  537. images[0].url
  538. }" style="height:100px"></div><div style = "width:30%;">${count}x${price} = ${
  539. +price * +count
  540. }</div>
  541. `;
  542. goodBlock.style.display = "flex";
  543. goodBlock.style.width = "100%";
  544. goodBlock.style.marginTop = "7px";
  545. goodBlock.style.background = "#EAEAEA";
  546. orderGoodsBlock.append(goodBlock);
  547. }
  548. orderSumBlock.innerHTML = `<b>TOTAL</b>:${order.total}`;
  549. orderHeaderBlock.style.padding = "10px";
  550. orderHeaderBlock.style.display = "flex";
  551. orderHeaderBlock.style.justifyContent = "space-between";
  552. orderHeaderBlock.append(createdAtBlock);
  553. orderHeaderBlock.append(orderSumBlock);
  554. orderBlock.append(orderHeaderBlock);
  555. orderBlock.append(orderGoodsBlock);
  556. orderGoodsBlock.style.marginTop = "10px";
  557. orderBlock.style.marginTop = "20px";
  558. orderBlock.style.borderBottom = "1px solid black";
  559. orderBlock.style.padding = "10px";
  560. orderList.append(orderBlock);
  561. }
  562. main.append(orderList);
  563. },
  564. cart() {
  565. updateCart();
  566. },
  567. };
  568. if (route in routes) routes[route]();
  569. };
  570. store.subscribe(() => {
  571. let { cart } = store.getState();
  572. const [, route, _id] = location.hash.split("/");
  573. if (cart && route === "cart") {
  574. updateCart();
  575. }
  576. });
  577. window.onhashchange();
  578. store.subscribe(() => {
  579. const {
  580. promise: { catById },
  581. } = store.getState();
  582. const [, route, _id] = location.hash.split("/");
  583. if (catById?.payload && route === "category") {
  584. const { name } = catById.payload;
  585. main.innerHTML = `<h1>${name}</h1> ТУТ ДОЛЖНЫ БЫТЬ ПОДКАТЕГОРИИ`;
  586. for (const { _id, name, price, images } of catById.payload.goods) {
  587. const card = document.createElement("div");
  588. card.innerHTML = `<h2>${name}</h2>
  589. <img src="${backendURL}/${images[0].url}" />
  590. <strong>${price}</strong>
  591. <a href="#/good/${_id}">${name}</a>
  592. `;
  593. main.append(card);
  594. }
  595. }
  596. });
  597. store.subscribe(() => {
  598. const {
  599. promise: { good },
  600. auth: { token },
  601. } = store.getState();
  602. const [, route, _id] = location.hash.split("/");
  603. if (good?.payload && route === "good") {
  604. const { name, price, images } = good.payload;
  605. main.innerHTML = `<h1>${name}</h1>`;
  606. const card = document.createElement("div");
  607. card.innerHTML = `<h2>${name}</h2>
  608. <img src="${backendURL}/${images[0].url}" />
  609. <strong>${price}</strong>
  610. `;
  611. if (token) {
  612. let buyButton = document.createElement("button");
  613. buyButton.innerHTML = "Купить";
  614. buyButton.onclick = () => store.dispatch(actionCartAdd(good.payload));
  615. card.append(buyButton);
  616. }
  617. main.append(card);
  618. }
  619. });