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 cartReducer(state = {}, { type, good = {}, count }) {
const { _id } = good;
const types = {
CART_ADD() {
count = +count;
if (!count) {
return state;
}
return {
...state,
[_id]: { good, count: count + (state[_id]?.count || 0) },
};
},
CART_CHANGE() {
count = +count;
if (!count) {
return state;
}
return {
...state,
[_id]: { good, count },
};
},
CART_REMOVE() {
let { [_id]: remove, ...goods } = state;
return goods;
},
CART_CLEAR() {
return {};
},
CART_SHOW() {
state = JSON.parse(localStorage.cart);
return state;
},
};
if (type in types) {
return types[type]();
}
return state;
}
function promiseReducer(state = {}, { type, status, payload, errors, name }) {
if (!state) {
return {};
}
if (type === "PROMISE") {
return {
...state,
[name]: { status, payload, errors },
};
}
return state;
}
const actionPending = (name) => ({ type: "PROMISE", status: "PENDING", name });
const actionResolved = (name, payload) => ({
type: "PROMISE",
status: "RESOLVED",
name,
payload,
});
const actionRejected = (name, errors) => ({
type: "PROMISE",
status: "REJECTED",
name,
errors,
});
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,
cart: cartReducer,
});
const store = createStore(combinedReducer);
const actionCartAdd = (good, count) => ({
type: "CART_ADD",
good,
count,
});
const actionCartChange = (good, count) => ({
type: "CART_CHANGE",
good,
count,
});
const actionCartRemove = (good) => ({ type: "CART_REMOVE", good });
const actionCartClear = () => ({ type: "CART_CLEAR" });
const actionCartShow = () => ({ type: "CART_SHOW" });
const actionOrder = () => async (dispatch, getState) => {
let { cart } = getState();
const orderGoods = Object.entries(cart).map(([_id, { count }]) => ({
good: { _id },
count,
}));
let result = await dispatch(
actionPromise(
"order",
gql(
`
mutation newOrder($order:OrderInput){
OrderUpsert(order:$order)
{ _id total }
}
`,
{ order: { orderGoods } }
)
)
);
if (result?._id) {
dispatch(actionCleanCart());
}
};
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 }]) }
)
);
const actionLogin = (login, password) =>
actionPromise(
"login",
gql(
`query log($login: String, $password: String) {
login(login: $login, password: $password)
}`,
{ login: login, password: password }
)
);
const actionFullLogin = (login, password) => async (dispatch) => {
console.log(login, password);
let token = await dispatch(actionLogin(login, password));
console.log(token);
if (token) {
dispatch(actionAuthLogin(token));
}
};
const actionRegister = (login, password) =>
actionPromise(
"registration",
gql(
`mutation reg2($user:UserInput) {
UserUpsert(user:$user) {
_id login
}
}
`,
{ user: { login: login, password: password } }
)
);
const actionFullRegister = (login, password) => async (dispatch) => {
console.log(login, password);
let check = await dispatch(actionRegister(login, password));
console.log(check);
if (check) {
dispatch(actionFullLogin(login, password));
}
};
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);
}
}
});
const openCart = () => {
const [, route] = location.hash.split("/");
if (route === "cart") {
main.innerHTML = "";
const { cart } = store.getState();
for (let good in cart) {
let {
good: {
_id: id,
name: name,
price: price,
images: [{ url }],
},
count,
} = cart[good];
let cardMain = document.createElement("div");
cardMain.id = "cardMain";
let goodImgBlock = document.createElement("div");
goodImgBlock.innerHTML = `
`;
cardMain.appendChild(goodImgBlock);
let goodInfoBlock = document.createElement("div");
let goodName = document.createElement("h2");
goodName.innerText = `${name}`;
let goodPrice = document.createElement("p");
goodPrice.innerText = `${price * count}$`;
let goodCount = document.createElement("p");
goodCount.innerText = `${count} шт.`;
goodInfoBlock.appendChild(goodName);
goodInfoBlock.appendChild(goodPrice);
goodInfoBlock.appendChild(goodCount);
cardMain.appendChild(goodInfoBlock);
let br = document.createElement("br");
let goodEditBlock = document.createElement("div");
goodEditBlock.className = "form-outline";
let deleteBtn = document.createElement("button");
deleteBtn.innerText = "Delete";
deleteBtn.id = "deleteBtn";
deleteBtn.type = "button";
deleteBtn.className = "btn btn-danger";
goodEditBlock.appendChild(deleteBtn);
goodEditBlock.appendChild(br);
deleteBtn.onclick = () => {
store.dispatch(actionCartRemove(cart[good].good));
cardMain.remove();
console.log(store.getState());
};
let countLabel = document.createElement("label");
countLabel.className = "form-label";
countLabel.htmlFor = "countField";
countLabel.innerText = "Change count";
let countField = document.createElement("input");
countField.type = "number";
countField.value = cart[good].count;
countField.min = "1";
countField.id = "countField";
countField.className = "form-control";
goodEditBlock.appendChild(countLabel);
goodEditBlock.appendChild(countField);
countField.oninput = () => {
goodPrice.innerText = `${price * +countField.value}$`;
goodCount.innerText = `${countField.value} шт.`;
store.dispatch(actionCartChange(cart[good].good, countField.value));
};
cardMain.appendChild(goodEditBlock);
main.appendChild(cardMain);
}
let clearBtn = document.createElement("button");
clearBtn.innerText = "Clear Cart";
clearBtn.id = "clearBtn";
clearBtn.type = "button";
clearBtn.className = "btn btn-dark";
let makeOrder = document.createElement("button");
makeOrder.innerText = "Make Order";
makeOrder.id = "makeOrder";
makeOrder.type = "button";
makeOrder.className = "btn btn-success";
if (Object.keys(cart).length !== 0) {
main.appendChild(makeOrder);
main.appendChild(clearBtn);
} else {
main.innerHTML = "
${price}$
${description}