2 Commity 9e3a491b15 ... 77e686cebe

Autor SHA1 Wiadomość Data
  Mitrofanova Natali 77e686cebe Merge branch 'master' of gitlab.a-level.com.ua:Mitrofanova-Natali/homework 2 lat temu
  Mitrofanova Natali c88abe6aa9 graphQL-js 2 lat temu

+ 36 - 0
HW16 GraphQL/index.html

@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <title>GQL HW</title>
+    <meta charset='utf8' />
+
+    <link rel="stylesheet" href="style.css">
+</head>
+
+<body>
+    <header id="header">
+        <div id="loginBlock">
+            <button><a id="logIn" href='#/login'>Log In</a></button>
+            <button><a id="registration" href='#/register'>Registration</a></button>
+        </div>
+        <div><a id="orders" class='hide'>My orders</a></div>
+        <div id="userData"></div>
+        <div id='cartIcon'><img class="cart" src="./shopping-cart-solid.svg" alt="cart"></div>
+        <div id='cartList' class="hide">
+
+        </div>
+    </header>
+    <div id='mainContainer'>
+        <aside id='aside'>
+            Категории
+        </aside>
+        <main id='main'>
+            Контент
+        </main>
+    </div>
+    <script src='script.js'></script>
+</body>
+</body>
+
+</html>

+ 476 - 0
HW16 GraphQL/script.js

@@ -0,0 +1,476 @@
+cartIcon.addEventListener("click", () => {
+  document.querySelector("#cartList").classList.remove("hide");
+});
+
+const backendURL = "http://shop-roles.asmer.fs.a-level.com.ua";
+
+//store
+function createStore(reducer) {
+  let state = reducer(undefined, {}); //стартовая инициализация состояния, запуск редьюсера со state === undefined
+  let cbs = []; //массив подписчиков
+
+  const getState = () => state; //функция, возвращающая переменную из замыкания
+  const subscribe = (cb) => (
+    cbs.push(cb), //запоминаем подписчиков в массиве
+    () => (cbs = cbs.filter((c) => c !== cb))
+  ); //возвращаем функцию unsubscribe, которая удаляет подписчика из списка
+
+  const dispatch = (action) => {
+    if (typeof action === "function") {
+      return action(dispatch, getState); //запускаем эту функцию и даем ей dispatch и getState для работы
+    }
+    const newState = reducer(state, action); //пробуем запустить редьюсер
+    if (newState !== state) {
+      //проверяем, смог ли редьюсер обработать action
+      state = newState; //если смог, то обновляем state
+      for (let cb of cbs) cb(); //и запускаем подписчиков
+    }
+  };
+  return {
+    getState, //добавление функции getState в результирующий объект
+    dispatch,
+    subscribe, //добавление subscribe в объект
+  };
+}
+
+const getGQL = (url) => (query, variables) =>
+  fetch(url, {
+    method: "POST",
+    headers: {
+      "Content-Type": "application/json",
+      ...(localStorage.authToken
+        ? { Authorization: "Bearer " + localStorage.authToken }
+        : {}),
+    },
+    body: JSON.stringify({ query, variables }),
+  })
+    .then((res) => res.json())
+    .then((data) => {
+      if (data.data) {
+        return Object.values(data.data)[0];
+      } else throw new Error(JSON.stringify(data.errors));
+    });
+
+const gql = getGQL(backendURL + "/graphql");
+
+function promiseReducer(state = {}, { type, name, status, payload, error }) {
+  if (type === "PROMISE") {
+    return {
+      ...state, //.......скопировать старый state
+      [name]: { status, payload, error }, //....... перекрыть в нем один name на новый объект со status, payload и error
+    };
+  }
+  return state;
+}
+
+const actionPromise = (name, promise) => async (dispatch) => {
+  dispatch(actionPending(name));
+  try {
+    let payload = await promise;
+    dispatch(actionFulfilled(name, payload));
+    return payload;
+  } catch (error) {
+    dispatch(actionRejected(name, error));
+  }
+};
+
+//actions
+const actionPending = (name) => ({ type: "PROMISE", name, status: "PENDING" });
+const actionFulfilled = (name, payload) => ({
+  type: "PROMISE",
+  name,
+  status: "FULFILLED",
+  payload,
+});
+const actionRejected = (name, error) => ({
+  type: "PROMISE",
+  name,
+  status: "REJECTED",
+  error,
+});
+const actionRootCats = () =>
+  actionPromise(
+    "rootCats",
+    gql(`query {
+        CategoryFind(query: "[{\\"parent\\":null}]"){
+            _id name
+        }
+    }`)
+  );
+const actionCatById = (_id) =>
+  actionPromise(
+    "catById",
+    gql(
+      `query catById($q: String){
+        CategoryFindOne(query: $q){
+            _id name goods {
+                _id name price images {
+                    url
+                }
+            }
+        }
+    }`,
+      { q: JSON.stringify([{ _id }]) }
+    )
+  );
+
+const actionGoodById = (_id) =>
+  actionPromise(
+    "goodById",
+    gql(
+      `query goodById($q: String){
+        GoodFindOne(query:$q){
+          _id
+          name
+          price
+          description
+          categories{_id name}
+          images{url}   
+          }
+        } `,
+      { q: JSON.stringify([{ _id }]) }
+    )
+  );
+
+const actionLogin = (login, password) =>
+  actionPromise(
+    "login",
+    gql(
+      `query login($login: String, $password: String){
+        login(login: $login, password: $password)
+        }`,
+      { login: login, password: password }
+    )
+  );
+
+const actionRegister = (login, password) =>
+  actionPromise(
+    "register",
+    gql(
+      `mutation register($login:String, $password:String) {
+    UserUpsert(user: {login:$login, password: $password}) {
+        _id login
+    } 
+}`,
+      { login: login, password: password }
+    )
+  );
+
+const actionAddToCard = (good, amount = 1) => ({
+  type: "ADD_TO_CARD",
+  good,
+  amount,
+});
+const actionChangeAmount = (good, amount = 1) => ({
+  type: "CHANGE_AMOUNT",
+  good,
+  amount,
+});
+const actionRemoveFromCard = (good, amount = 1) => ({
+  type: "REMOVE_FROM_CARD",
+  good,
+  amount,
+});
+const actionRemoveCard = () => ({ type: "REMOVE_CARD" });
+
+const actionFullLogin = (login, password) => async (dispatch) => {
+  let token = await dispatch(actionLogin(login, password));
+  if (token) {
+    dispatch(actionAuthLogin(token));
+  }
+};
+
+const actionFullRegister = (login, password) => async (dispatch) => {
+  try {
+    await dispatch(actionRegister(login, password));
+  } catch (error) {
+    return console.log(error);
+  }
+  await dispatch(actionFullLogin(login, password));
+};
+
+const actionAuthLogin = (token) => ({ type: "AUTH_LOGIN", token });
+const actionAuthLogout = () => ({ type: "AUTH_LOGOUT" });
+
+const jwtDecode = (token) => {
+  try {
+    const payload = JSON.parse(atob(token.split(".")[1]));
+    return payload;
+  } catch (e) {}
+};
+
+//reducers
+function authReducer(state, { type, token }) {
+  if (state === undefined && localStorage.authToken) {
+    token = localStorage.authToken;
+    type = "AUTH_LOGIN";
+  }
+  if (type === "AUTH_LOGIN") {
+    // раскодируем токен
+    let decode = jwtDecode(token); //  (пишем отдельно функцию jwtDecode, и да будет в ней try-catch)
+    if (decode) {
+      //  серединка, atob, JSON.parse
+      localStorage.authToken = token; //если получилось пишем его в localStorage
+      return { token, payload: decode }; // return{token, payload}
+    }
+  }
+  if (type === "AUTH_LOGOUT") {
+    localStorage.removeItem("authToken"); //чистим localStorage.authToken
+    return {}; //возвращаем {}
+  }
+  return state || {};
+}
+
+function cartReducer(state = {}, { type, good = {}, amount = 1 }) {
+  const id = good._id;
+  const types = {
+    ADD_TO_CARD() {
+      return {
+        ...state,
+        [id]: { good, count: +amount + +(state[id] ? +state[id].count : 0) },
+      };
+    },
+    CHANGE_AMOUNT() {
+      return {
+        ...state,
+        [id]: { good, count: +amount },
+      };
+    },
+    REMOVE_FROM_CARD() {
+      let newState = { ...state };
+      delete newState[id];
+      return {
+        ...newState,
+      };
+    },
+    REMOVE_CARD() {
+      return {};
+    },
+  };
+
+  if (type in types) return types[type]();
+  return state;
+}
+
+function combineReducers(reducers) {
+  return (state = {}, action) => {
+    const newState = {};
+    for (const [nameOfReducer, currentReducer] of Object.entries(reducers)) {
+      let newCurrentState = currentReducer(state[nameOfReducer], action);
+      if (newCurrentState !== state[nameOfReducer]) {
+        newState[nameOfReducer] = newCurrentState;
+      }
+    }
+    return Object.keys(newState).length !== 0
+      ? { ...state, ...newState }
+      : state;
+  };
+}
+const combinedReducer = combineReducers({
+  goods: promiseReducer,
+  auth: authReducer,
+  cart: cartReducer,
+});
+
+const store = createStore(combinedReducer);
+
+store.dispatch(actionAuthLogout());
+store.dispatch(actionRootCats());
+
+//subscribes
+store.subscribe(() => console.log(store.getState()));
+
+store.subscribe(() => {
+  const { rootCats } = store.getState().goods;
+  if (rootCats?.payload) {
+    aside.innerHTML = "";
+    for (const { _id, name } of rootCats.payload) {
+      const link = document.createElement("a");
+      link.href = `#/category/${_id}`;
+      link.innerText = name;
+      aside.append(link);
+    }
+  }
+});
+
+store.subscribe(() => {
+  const { catById } = store.getState().goods;
+  const [, route, _id] = location.hash.split("/");
+
+  if (catById?.payload && route === "category") {
+    const { name } = catById.payload;
+    main.innerHTML = `<h1>${name}</h1>`;
+    for (const { _id, name, price, images } of catById.payload.goods) {
+      const card = document.createElement("div");
+      card.innerHTML = `<h2>${name}</h2>
+                            <img src="${backendURL}/${images[0].url}" />    
+                            <strong>Цена ${price} грн</strong>                          
+                            <a href="#/good/${_id}">Перейти</a> `; //  ТУТ ДОЛЖНА БЫТЬ ССЫЛКА НА СТРАНИЦУ ТОВАРА ВИДА #/good/АЙДИ
+      main.append(card);
+    }
+  }
+});
+
+store.subscribe(() => {
+  //ТУТ ДОЛЖНА БЫТЬ ПРОВЕРКА НА НАЛИЧИЕ goodById в редакс
+  const { goodById } = store.getState().goods;
+  const [, route, _id] = location.hash.split("/");
+
+  if (goodById?.payload && route === "good") {
+    const { name, images, price, description } = goodById.payload;
+    main.innerHTML = `<h1>${name}</h1> Товар`;
+
+    const card = document.createElement("div");
+    card.innerHTML = `<h2>${name}</h2>
+                        <img src ="${backendURL}/${images[0].url}"/>
+                        <strong>Цена : ${price}  грн</strong>
+                        <button class='buy buy${_id}'>Купить</button>
+                        <p>${description}</p>                                             
+                        `;
+    main.append(card);
+    document.querySelector(`.buy${_id}`).addEventListener("click", () => {
+      store.dispatch(actionAddToCard(goodById.payload));
+    });
+  }
+});
+
+store.subscribe(() => {
+  const cartState = store.getState().cart;
+  cartList.innerHTML = `<div id='closeListBtn'>X</div>`;
+
+  closeListBtn.addEventListener("click", () => {
+    document.querySelector("#cartList").classList.add("hide");
+  });
+  let sum = 0;
+  for (const id in cartState) {
+    const { count, good } = cartState[id];
+    const cartListItem = document.createElement("div");
+    cartListItem.classList.add("cartListItem");
+    cartListItem.innerHTML += `<p>- ${good.name}: </p>`;
+    const inputCount = document.createElement("input");
+    inputCount.type = "number";
+    inputCount.value = count;
+
+    inputCount.addEventListener("change", (e) => {
+      store.dispatch(actionChangeAmount(good, e.target.value));
+    });
+
+    cartListItem.appendChild(inputCount);
+    const goodPrice = document.createElement("p");
+    goodPrice.innerText = `цена: ${good.price}`;
+    cartListItem.appendChild(goodPrice);
+    const goodCost = document.createElement("strong");
+    goodCost.innerText = `всего ${+good.price * +count} грн.`;
+    sum += +good.price * +count;
+    cartListItem.appendChild(goodCost);
+
+    const removeBtn = document.createElement("button");
+    removeBtn.innerText = "remove";
+    removeBtn.addEventListener("click", () => {
+      store.dispatch(actionRemoveFromCard(good));
+    });
+    cartListItem.appendChild(removeBtn);
+    cartList.appendChild(cartListItem);
+  }
+
+  if (cartList.querySelectorAll(".cartListItem").length) {
+    const btnToOrder = document.createElement("button");
+    btnToOrder.innerText = "Заказать";
+    btnToOrder.classList = "btnToOrder";
+    cartList.appendChild(btnToOrder);
+    btnToOrder.addEventListener("click", () => {
+      // store.dispatch(actionToOrder());
+      console.log("Ваш заказ в процессе обработки");
+    });
+
+    const priceOfOrder = document.createElement("div");
+    priceOfOrder.classList = "priceOfOrder";
+    priceOfOrder.innerText = `Итого : ${sum}`;
+    cartList.appendChild(priceOfOrder);
+
+    const btnDeleteAll = document.createElement("button");
+    btnDeleteAll.classList = "btnDeleteAll";
+    btnDeleteAll.innerText = "Очистить всё";
+    btnDeleteAll.addEventListener("click", () => {
+      store.dispatch(actionRemoveCard());
+    });
+
+    cartList.appendChild(btnDeleteAll);
+  }
+});
+
+window.onhashchange = () => {
+  const [, route, _id] = location.hash.split("/");
+
+  const routes = {
+    category() {
+      store.dispatch(actionCatById(_id));
+    },
+    good() {
+      //задиспатчить actionGoodById
+      store.dispatch(actionGoodById(_id));
+    },
+    login() {
+      loginBlock.classList.add("hide");
+      const innerContent = `
+            <div class="loginWrapper">
+                <input class="login">
+                <input type="password" class="password">
+                <button class="logInButton">LogIn</button>
+            </div>
+            `;
+      header.innerHTML = innerContent + header.innerHTML;
+      const inpLogIn = document.querySelector(".login");
+      const inpPassword = document.querySelector(".password");
+      const loginWrapper = document.querySelector(".loginWrapper");
+      document
+        .querySelector(".logInButton")
+        .addEventListener("click", async () => {
+          if (inpLogIn.value.length && inpPassword.value.length) {
+            await store.dispatch(
+              actionFullLogin(inpLogIn.value, inpPassword.value)
+            );
+            const login = store.getState().auth.payload.sub.login;
+            userData.innerHTML = `<p>Привет, ${login}</p>`;
+            header.removeChild(loginWrapper);
+
+            const btnLogout = document.createElement("button");
+            btnLogout.textContent = "Exit";
+            btnLogout.addEventListener("click", () => {
+              userData.innerHTML = "";
+              store.dispatch(actionAuthLogout());
+              loginBlock.style.display = "block";
+              oders.classList.add("hide");
+            });
+            orders.classList.remove("hide");
+            userData.append(btnLogout);
+          }
+        });
+    },
+    register() {
+      loginBlock.classList.add("hide");
+      const innerContent = `
+            <div class="loginWrapper">
+                <input class="loginRegister">
+                <input type="password" class="passwordRegister">
+                <button class="registrationButton">Registration</button>
+            </div>
+            `;
+      header.innerHTML = innerContent + header.innerHTML;
+      const loginRegister = document.querySelector(".loginRegister");
+      const passwordRegister = document.querySelector(".passwordRegister");
+      document
+        .querySelector(".registrationButton")
+        .addEventListener("click", () => {
+          if (loginRegister.value.length && passwordRegister.value.length) {
+            store.dispatch(
+              actionFullRegister(loginRegister.value, passwordRegister.value)
+            );
+            loginBlock.classList.remove("hide");
+          }
+        });
+    },
+  };
+  if (route in routes) routes[route]();
+};
+window.onhashchange();

Plik diff jest za duży
+ 1 - 0
HW16 GraphQL/shopping-cart-solid.svg


+ 114 - 0
HW16 GraphQL/style.css

@@ -0,0 +1,114 @@
+.hide {
+    display: none;
+}
+
+#mainContainer {
+    display: flex;
+}
+
+#aside {
+    width: 30%;
+}
+
+#aside>a {
+    display: block;
+}
+
+#cartIcon {
+    width: 70px;
+    height: 70px;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    background-color: rgb(245, 221, 163);
+    border-radius: 3px;
+}
+
+.cart {
+    width: 40px;
+    height: 40px;
+}
+
+header {
+    display: flex;
+    justify-content: space-between;
+}
+
+#cartList {
+    width: 500px;
+    min-height: 500px;
+    background-color: honeydew;
+    position: absolute;
+    right: 8px;
+    top: 80px;
+    border-radius: 2%;
+    padding-top: 30px;
+}
+
+#closeListBtn {
+    width: 25px;
+    height: 25px;
+    position: absolute;
+    top: 10px;
+    right: 10px;
+    background-color: red;
+    font-size: 25px;
+    align-items: center;
+    text-align: center;
+    border-radius: 25%;
+
+}
+
+.cartListItem {
+    display: flex;
+    padding-top: 10px;
+    width: 100%;
+    justify-content: space-between;
+    align-items: center;
+
+}
+
+.cartListItem p {
+    margin: 5px 3px;
+}
+
+.cartListItem input {
+    height: 20px;
+    width: 35px;
+
+}
+
+.btnDeleteAll {
+    position: absolute;
+    bottom: 20px;
+    left: 20px;
+}
+
+.btnToOrder {
+    position: absolute;
+    bottom: 20px;
+    right: 20px;
+}
+
+.priceOfOrder {
+    position: absolute;
+    bottom: 20px;
+    right: 200px;
+}
+
+img {
+    max-width: 400px;
+    max-height: 400px;
+}
+
+a {
+
+    padding: 10px 15px;
+}
+
+button {
+    padding: 5px;
+    background-color: rgb(158, 188, 243);
+    border: 1px solid grey;
+    border-radius: 5px;
+}