Parcourir la source

Make homework with graphql and redux (login/registration) / there are some bugs

Bonyant il y a 2 ans
Parent
commit
d0427db62b
5 fichiers modifiés avec 572 ajouts et 22 suppressions
  1. 0 1
      14/index.html
  2. 26 21
      14/index.js
  3. 37 0
      15/index.html
  4. 483 0
      15/index.js
  5. 26 0
      15/styles.css

+ 0 - 1
14/index.html

@@ -16,7 +16,6 @@
     </style>
   </head>
   <body>
-    <header>КУДА Я ПОПАЛ?</header>
     <div id="mainContainer">
       <aside id="aside">Категории</aside>
       <main id="main">Контент</main>

+ 26 - 21
14/index.js

@@ -99,26 +99,29 @@ const actionRootCats = () =>
   }`)
   );
 
-const actionCatById = (
-  _id //добавить подкатегории
-) =>
+const actionCatById = (_id) =>
   actionPromise(
     "catById",
     gql(
       `query catById($q: String){
       CategoryFindOne(query: $q){
-          _id name goods {
-              _id name price images {
-                  url
-              }
+          _id name,
+          goods{
+            _id name price images {
+               url
+            }
+          },
+          subCategories{
+            name, subCategories{
+              name
+            }
           }
-      }
+        }
   }`,
       { q: JSON.stringify([{ _id }]) }
     )
   );
 
-//actionGoodById по аналогии
 const actionGoodById = (_id) =>
   actionPromise(
     "goodById",
@@ -158,7 +161,6 @@ window.onhashchange = () => {
       store.dispatch(actionCatById(_id));
     },
     good() {
-      //задиспатчить actionGoodById
       store.dispatch(actionGoodById(_id));
     },
   };
@@ -172,26 +174,29 @@ store.subscribe(() => {
   const [, route, _id] = location.hash.split("/");
   if (catById?.payload && route === "category") {
     const { name } = catById.payload;
+    main.innerHTML = `<h1>${name}</h1>`;
     if (catById.payload?.subCategories) {
-      for (const { _id, name } of catById.payload?.subCategories) {
+      for (const { _id, name } of catById.payload.subCategories) {
         const link = document.createElement("a");
-        link.innerText = name;
         link.href = `#/category/${_id}`;
+        link.innerText = name;
         main.append(link);
+        main.innerHTML += "<br/>";
       }
     }
-    main.innerHTML = `<h1>${name}</h1>`;
-    for (const { _id, name, price, images } of catById.payload.goods) {
-      const card = document.createElement("div");
-      const link = document.createElement("a");
-      card.innerHTML = `<h2>${name}</h2>
+    if (catById.payload?.goods) {
+      for (const { _id, name, price, images } of catById.payload.goods) {
+        const card = document.createElement("div");
+        const link = document.createElement("a");
+        card.innerHTML = `<h2>${name}</h2>
                             <img src="${backURL}/${images[0].url}" /><br/>
                             <strong>${price}$</strong><br/>
                               `;
-      link.innerText = name;
-      link.href = `#/good/${_id}`;
-      card.appendChild(link);
-      main.append(card);
+        link.innerText = name;
+        link.href = `#/good/${_id}`;
+        card.appendChild(link);
+        main.append(card);
+      }
     }
   }
 });

+ 37 - 0
15/index.html

@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <link
+      href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css"
+      rel="stylesheet"
+      integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3"
+      crossorigin="anonymous"
+    />
+    <link rel="stylesheet" href="styles.css" />
+    <title>GQL Practise</title>
+  </head>
+  <body>
+    <nav class="navbar navbar-dark bg-dark">
+      <div class="container-fluid">
+        <a class="navbar-brand" href="#">Piece of shit</a>
+        <form class="d-flex" id="authnav"></form>
+      </div>
+    </nav>
+    <div id="mainContainer">
+      <nav
+        id="aside"
+        class="navbar navbar-light bg-light flex-column align-items-stretch p-3"
+      >
+        <a class="navbar-brand">Категории</a>
+        <nav class="nav nav-pills flex-column" id="asideList"></nav>
+      </nav>
+      <main id="main">
+        <h1>Главная</h1>
+      </main>
+    </div>
+    <script src="index.js"></script>
+  </body>
+</html>

+ 483 - 0
15/index.js

@@ -0,0 +1,483 @@
+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") {
+      //если action - не объект, а функция
+      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 в объект
+  };
+}
+
+function authReducer(state, { type, token }) {
+  if (!state) {
+    if (localStorage.authToken) {
+      type = "AUTH_LOGIN";
+      token = localStorage.authToken;
+    } else {
+      return {};
+    }
+  }
+  if (type === "AUTH_LOGIN") {
+    localStorage.authToken = token;
+    let payload = jwtDecode(token);
+    if (typeof payload !== "object") {
+      return {};
+    }
+    return { token, payload };
+  }
+  if (type === "AUTH_LOGOUT") {
+    localStorage.authToken = "";
+    return {};
+  }
+  return state;
+}
+
+const actionAuthLogin = (token) => ({
+  type: "AUTH_LOGIN",
+  token,
+});
+const actionAuthLogout = () => ({ type: "AUTH_LOGOUT" });
+
+function promiseReducer(state = {}, { type, status, payload, error, name }) {
+  if (type === "PROMISE") {
+    return {
+      ...state,
+      [name]: { status, payload, error },
+    };
+  }
+  return state;
+}
+const actionPending = (name) => ({ type: "PROMISE", status: "PENDING", name });
+const actionResolved = (name, payload) => ({
+  type: "PROMISE",
+  status: "RESOLVED",
+  name,
+  payload,
+});
+const actionRejected = (name, error) => ({
+  type: "PROMISE",
+  status: "REJECTED",
+  name,
+  error,
+});
+const actionPromise = (name, promise) => async (dispatch) => {
+  dispatch(actionPending(name));
+  try {
+    let data = await promise;
+    dispatch(actionResolved(name, data));
+    return data;
+  } catch (error) {
+    dispatch(actionRejected(name, error));
+  }
+};
+
+function combineReducers(reducers) {
+  return (state = {}, action) => {
+    const newState = {};
+    let newSubState;
+    for (const [reducerName, reducer] of Object.entries(reducers)) {
+      newSubState = reducer(state[reducerName], action);
+      if (state[reducerName] !== newSubState) {
+        newState[reducerName] = newSubState;
+      }
+    }
+    if (Object.keys(newState).length !== 0) {
+      return { ...state, ...newState };
+    } else {
+      return state;
+    }
+  };
+}
+
+const combinedReducer = combineReducers({
+  promise: promiseReducer,
+  auth: authReducer,
+});
+const store = createStore(combinedReducer);
+
+const getGQL =
+  (url) =>
+  async (query, variables = {}) => {
+    let obj = await fetch(url, {
+      method: "POST",
+      headers: {
+        "Content-Type": "application/json",
+      },
+      body: JSON.stringify({ query, variables }),
+    });
+    let a = await obj.json();
+    if (!a.data && a.errors) throw new Error(JSON.stringify(a.errors));
+    return a.data[Object.keys(a.data)[0]];
+  };
+
+const backURL = "http://shop-roles.asmer.fs.a-level.com.ua";
+
+const gql = getGQL(backURL + "/graphql");
+
+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
+            }
+          },
+          subCategories{
+            name, subCategories{
+              name
+            }
+          }
+        }
+  }`,
+      { q: JSON.stringify([{ _id }]) }
+    )
+  );
+
+const actionGoodById = (_id) =>
+  actionPromise(
+    "goodById",
+    gql(
+      `query goodById($q: String){
+  GoodFindOne(query: $q){
+          _id name description price images{
+            url
+          }
+      }
+  }`,
+      { q: JSON.stringify([{ _id }]) }
+    )
+  );
+
+store.dispatch(actionRootCats());
+store.dispatch(actionGoodById());
+
+store.subscribe(() => {
+  const { promise } = store.getState();
+  if (promise?.rootCats?.payload) {
+    asideList.innerHTML = "";
+    let count = -1;
+    for (const { _id, name } of promise.rootCats.payload) {
+      count++;
+      const link = document.createElement("a");
+      link.href = `#/category/${_id}`;
+      link.innerText = name;
+      link.className = "nav-link";
+      link.id = `rootCat${count}`;
+      asideList.appendChild(link);
+    }
+  }
+});
+
+window.onhashchange = () => {
+  const [, route, _id] = location.hash.split("/");
+  const routes = {
+    category() {
+      store.dispatch(actionCatById(_id));
+    },
+    good() {
+      store.dispatch(actionGoodById(_id));
+    },
+  };
+  if (route in routes) routes[route]();
+};
+
+window.onhashchange();
+
+store.subscribe(() => {
+  const { promise } = store.getState();
+  const [, route, _id] = location.hash.split("/");
+  if (promise?.catById?.payload && route === "category") {
+    const { name } = promise.catById.payload;
+    main.innerHTML = `<h1>${name}</h1>`;
+    if (promise.catById.payload?.subCategories) {
+      for (const { _id, name } of promise.catById.payload.subCategories) {
+        const rootCat = document.querySelector(
+          `a[href="#/category/${promise.catById.payload._id}"]`
+        );
+        const subNav = document.createElement("nav");
+        subNav.className = "nav nav-pills flex-column";
+        const link = document.createElement("a");
+        link.href = `#/category/${_id}`;
+        link.innerText = name;
+        link.className = "nav-link ms-3 my-1";
+        subNav.appendChild(link);
+        rootCat.appendChild(subNav);
+      }
+    }
+    if (promise.catById.payload?.goods) {
+      for (const { _id, name, price, images } of promise.catById.payload
+        .goods) {
+        const card = document.createElement("div");
+        const link = document.createElement("a");
+        card.innerHTML = `<h2>${name}</h2>
+                            <img src="${backURL}/${images[0].url}" width="400" height="200"/><br/>
+                            <strong>${price}$</strong><br/>
+                              `;
+        link.innerText = name;
+        link.href = `#/good/${_id}`;
+        card.appendChild(link);
+        main.append(card);
+      }
+    }
+  }
+});
+
+store.subscribe(() => {
+  const { promise } = store.getState();
+  const [, route, _id] = location.hash.split("/");
+  if (promise?.goodById?.payload && route === "good") {
+    main.innerHTML = "";
+    const { name, description, price, images } = promise.goodById.payload;
+    main.innerHTML = `
+    <div>
+        <div>
+            <img src="${backURL}/${images[0].url}" width="400" height="200"/>
+        </div>
+        <div>
+            <h2>${name}</h2>
+            <p><strong>${price}$</strong></p>
+            <p><span>${description}</span></p>
+        </div>
+    </div>`;
+  }
+});
+
+store.subscribe(() => console.log(store.getState()));
+
+function jwtDecode(token) {
+  try {
+    return JSON.parse(atob(token.split(".")[1]));
+  } catch (error) {
+    console.log(error);
+  }
+}
+
+const actionLogin = (login, password) =>
+  actionPromise(
+    "login",
+    gql(
+      `query login($login: String, $password: String){
+      login(login: $login, password: $password)
+    }`,
+      { login: login, password: password }
+    )
+  );
+
+const actionFullLogin = (login, password) => {
+  async (dispatch) => {
+    let token = await dispatch(actionLogin(login, password));
+    if (token) {
+      dispatch(actionAuthLogin(token));
+    }
+    return token;
+  };
+};
+
+const actionRegister = (login, password) =>
+  actionPromise(
+    "register",
+    gql(
+      `mutation register($login:String, $password: String){
+      UserUpsert(user:{
+                 login: $login, 
+                 password: $password, 
+                 nick: $login}){
+        _id login
+      }
+    }`,
+      { login: login, password: password }
+    )
+  );
+
+const actionFullRegister = (login, password) => async (dispatch) => {
+  let check = await dispatch(actionRegister(login, password));
+  if (check) {
+    let token = await dispatch(actionLogin(login, password));
+    if (token) {
+      dispatch(actionAuthLogin(token));
+    }
+  }
+};
+
+let count = 0;
+
+store.subscribe(() => {
+  const { auth, promise } = store.getState();
+  count++;
+  if (!auth?.payload && count === 1) {
+    let signIn = document.createElement("button");
+    signIn.innerText = "Sign In";
+    signIn.id = "signinBtn";
+    signIn.className = "btn btn-light";
+    let signUp = document.createElement("button");
+    signUp.innerText = "Sign Up";
+    signUp.id = "signupBtn";
+    signUp.className = "btn btn-light";
+    let authBlock = document.createElement("div");
+    authBlock.id = "authBlock";
+    authBlock.appendChild(signIn);
+    authBlock.appendChild(signUp);
+    authnav.appendChild(authBlock);
+    let signOut = document.createElement("button");
+    signOut.innerText = "Sign Out";
+    signOut.id = "signoutBtn";
+    signOut.className = "btn btn-secondary";
+
+    signIn.onclick = () => {
+      try {
+        let registerFields = document.getElementById("registerFields");
+        if (registerFields !== null) {
+          registerFields.style.display = "none";
+        }
+        signIn.style.display = "none";
+        signUp.style.display = "inline";
+
+        let fieldsBlock = document.createElement("div");
+        fieldsBlock.className = "fields";
+        fieldsBlock.id = "loginFields";
+        let email = document.createElement("input");
+        email.type = "email";
+        email.placeholder = "Enter your email";
+        email.className = "form-control";
+        email.id = "loginInput";
+        let password = document.createElement("input");
+        password.type = "password";
+        password.placeholder = "Enter your password";
+        password.className = "form-control";
+        password.id = "pass1Input";
+        let login = document.createElement("button");
+        login.innerText = "Login";
+        login.id = "loginBtn";
+        login.className = "btn btn-primary";
+        fieldsBlock.appendChild(email);
+        fieldsBlock.appendChild(password);
+        fieldsBlock.appendChild(login);
+        authBlock.prepend(fieldsBlock);
+
+        let loginBtn = document.getElementById("loginBtn");
+        loginBtn.onclick = async () => {
+          let loginInput = document.getElementById("loginInput");
+          let passwordInput = document.getElementById("pass1Input");
+          if (loginInput.value && passwordInput.value) {
+            store.dispatch(
+              actionFullLogin(loginInput.value, passwordInput.value)
+            );
+            if (actionFullLogin(loginInput.value, passwordInput.value)) {
+              let loginBlock = document.getElementById("loginFields");
+              loginBlock.style.display = "none";
+              signupBtn.style.display = "none";
+              authBlock.appendChild(signOut);
+            }
+          }
+          console.log(store.getState());
+        };
+      } catch (error) {
+        console.log(error);
+      }
+    };
+
+    signUp.onclick = () => {
+      try {
+        let loginFields = document.getElementById("loginFields");
+        if (loginFields !== null) {
+          loginFields.style.display = "none";
+        }
+        signIn.style.display = "inline";
+        signUp.style.display = "none";
+        let fieldsBlock = document.createElement("div");
+        fieldsBlock.className = "fields";
+        fieldsBlock.id = "registerFields";
+        let email = document.createElement("input");
+        email.type = "email";
+        email.placeholder = "Enter your email";
+        email.className = "form-control";
+        email.id = "regInput";
+        let password = document.createElement("input");
+        password.type = "password";
+        password.placeholder = "Enter your password";
+        password.className = "form-control";
+        password.id = "pass2Input";
+        let register = document.createElement("button");
+        register.innerText = "Register";
+        register.id = "regBtn";
+        register.className = "btn btn-primary";
+        fieldsBlock.appendChild(email);
+        fieldsBlock.appendChild(password);
+        fieldsBlock.appendChild(register);
+        authBlock.prepend(fieldsBlock);
+
+        let regBtn = document.getElementById("regBtn");
+        regBtn.onclick = async () => {
+          let regInput = document.getElementById("regInput");
+          let passwordInput = document.getElementById("pass2Input");
+          if (regInput.value && passwordInput.value) {
+            store.dispatch(
+              actionFullRegister(regInput.value, passwordInput.value)
+            );
+            if (actionFullRegister(regInput.value, passwordInput.value)) {
+              let registerBlock = document.getElementById("registerFields");
+              registerBlock.style.display = "none";
+              signinBtn.style.display = "none";
+              authBlock.appendChild(signOut);
+              let nickname = regInput.value.slice(
+                0,
+                regInput.value.indexOf("@")
+              );
+              let account = document.createElement("button");
+              account.textContent = nickname;
+              account.type = "button";
+              account.id = "accountLink";
+              account.className = "btn btn-success";
+              authBlock.prepend(account);
+            }
+          }
+          console.log(store.getState());
+        };
+      } catch (error) {
+        console.log(error);
+      }
+    };
+
+    signOut.onclick = () => {
+      signOut.style.display = "none";
+      store.dispatch(actionAuthLogout());
+      signIn.style.display = "inline";
+      signUp.style.display = "inline";
+      accountLink.remove();
+    };
+  }
+});

+ 26 - 0
15/styles.css

@@ -0,0 +1,26 @@
+#mainContainer {
+  display: flex;
+}
+#aside {
+  width: 20%;
+  display: block;
+}
+button {
+  margin: 1vh;
+}
+.fields {
+  display: flex;
+}
+
+.fields input {
+  margin: 0 1vh;
+  height: 5vh;
+}
+
+.fields button {
+  height: auto;
+}
+
+#authBlock {
+  display: flex;
+}