|
@@ -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();
|
|
|
+ };
|
|
|
+ }
|
|
|
+});
|