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